摘要:记录下整体的设计思路以及运营过程中的各种问题。如果钱是负数了,还得从已生成的小红包中抽取回来将红包放入队列之中创建红包失败,请检查参数生产和之间的随机数,但是概率不是平均的,从到方向概率逐渐加大。
公司前段时间根据业务方需求需要做一个抢红包的活动,网上也搜索了很多资料。记录下整体的设计思路以及运营过程中的各种问题。
产品需求:1.红包支持配置开始时间、结束时间、类型(随机金额或固定金额)、单个最小红包金额、单个最大红包金额
2.可领取红包的业务条件(根据业务信息指定某些满足条件的人可以抢)
难点1:红包算法(根据红包配置最大、最小金额、数量生成符合条件的红包集合)
因为红包有配置单个红包的最大和最小金额,所以不能完全使用随机分配的方式。
所以要求:
* 单个红包金额既要大于最小金额,又要小于最大金额
* 根据红包总金额和个数要正好将钱分完
* 单个红包精确到分,也就是小数点后两位
实现代码:
/* * @todo 设置随机红包金额 * return array */ public function setRandMoney() { $result = []; //取小数点后两位将金额乘100 $this->total = $this->total * 100;//红包总金额 $this->min = $this->min * 100;//单个红包最小金额 $this->max = $this->max * 100;//单个红包最大金额 //获取红包平均金额 $average = $this->total / $this->num; for ($i = 0; $i < $this->num; $i++) { //因为小红包的数量通常是要比大红包的数量要多的,因为这里的概率要调换过来。 //当随机数>平均值,则产生小红包 //当随机数<平均值,则产生大红包 if (rand($this->min, $this->max) > $average) { // 在平均线上减钱 $temp = $this->min + $this->xRandom($this->min, $average); $result[$i] = $temp; $this->total -= $temp; } else { // 在平均线上加钱 $temp = $this->max - $this->xRandom($average, $this->max); $result[$i] = $temp; $this->total -= $temp; } } // 如果还有余钱,则尝试加到小红包里,如果加不进去,则尝试下一个。 while ($this->total > 0) { for ($i = 0; $i < $this->num; $i++) { if ($this->total > 0 && $result[$i] < $this->max) { $result[$i]++; $this->total--; } } } // 如果钱是负数了,还得从已生成的小红包中抽取回来 while ($this->total < 0) { for ($i = 0; $i < $this->num; $i++) { if ($this->total < 0 && $result[$i] > $this->min) { $result[$i]--; $this->total++; } } } if (!empty($result)) { //将红包放入队列之中 foreach ($result as $val) { $this->redis->lPush($this->redpack_money_queue . $this->act_id, $val / 100); } return ["code" => "0", "msg" => "success"]; } return ["code" => "1", "msg" => "创建红包失败,请检查参数"]; } /** * 生产min和max之间的随机数,但是概率不是平均的,从min到max方向概率逐渐加大。 * 先平方,然后产生一个平方值范围内的随机数,再开方,这样就产生了一种“膨胀”再“收缩”的效果。 */ private function xRandom($bonus_min, $bonus_max) { $sqr = intval($this->sqr($bonus_max - $bonus_min)); $rand_num = rand(0, ($sqr - 1)); return intval(sqrt($rand_num)); } private function sqr($n) { return $n * $n; }
因为取最小和最大金额之间随机数的时候使用了intval()函数导致该算法只能处理整数,故在处理的时候将金额乘100 ,在最后入队列的时候再将其 除100,这样就将其精确到小数点后两位。
难点2:高并发时对服务器的访问压力
类似抢红包、1元抢购,秒杀等业务场景都是在同一时间大量请求堆积到服务器,从而导致服务器资源紧张,程序处理不过来。那么我们要做的就是将流量控制住,不让大量的请求透过web服务器直接打到数据库层。那么从用户访问url到收到返回结果整体流程是什么样子呢?
客户端层,用户在微信中打开URL,DNS解析域名至服务器
web服务器层, Apache、Nginx或Tomcat等
服务器层,分配php-fpm进程,代码接收参数进行逻辑处理
数据持续化层次,将结果保存至mysql或Redis层次
客户端层优化方案:(限流)
前端URL使用html静态页面显示内容,并将页面显示图片尽量压缩,减少服务器带宽压力。推荐使用base64解码图片
使用连接池控制流量,用户点击抢红包时,发起ajax请求,调用后台使用java写的redis incr 接口,每次调用则键值 +1,并将自增id返回,当后台代码处理完后再将其键值减掉,因为incr自增为原子级别,所以前端可以根据当前有多少用户在等待中。 根据自身服务器配置以及业务场景预估N多请求会导致服务器出现问题,如果当前等待处理的请求数大于N则前端提示用户 "当前请求过多,请稍后再试",反之则可以正常发起请求。
Web层优化方案(lua+nginx实现频率控制)
Nginx来处理访问控制的方法有多种,实现的效果也有多种,访问IP段,访问内容限制,访问频率限制等。
用Nginx+Lua+Redis来做访问限制主要是考虑到高并发环境下快速访问控制的需求。
Nginx处理请求的过程一共划分为11个阶段,分别是:
`post-read、server-rewrite、find-config、rewrite、post-rewrite、 preaccess、access、post-access、try- files、content、log`. 在openresty中,可以找到: `set_by_lua`,`access_by_lua`,`content_by_lua`,`rewrite_by_lua`等方法。 那么访问控制应该是,`access`阶段。
2.根据请求的ip段来控制访问流量,每次接收到抢红包的url后将redis连接池中id自增,当超过某个峰值时跳转到等待页。
具体配置方案参考:http://homeway.me/2015/08/11/...
php代码层(防止出现发多、重复领取、权限等情况)
使用redis queue 队列功能来控制超发的情况,将每个算出来的小红包lpush至队列中,每次收到请求后消费最后一个小红包,因为redis的的队列为阻塞模式,所以当队列中为空时是不返回数据的,也就可以保证出现并发时不会一个红包分配给多人。
使用 redis list集合来控制重复领取的情况,每次接收到请求后将用户id放置已领取的集合中(这点很重要,一定要在消费队列前放置集合中,要不会出现因为并发导致重复领取),消费成功则跳出,反之则将其移出已领取集合。
因为业务需求处理起来很繁琐,所以在活动创建的时候就根据活动规则将可领取的人员放置集合中,权限判断可以使用待领取集合来控制。
以下为我的代码实现(小菜一枚,大神勿喷)
/* * @todo 获取红包金额 * @return array */ public function doRush() { $act_info = $this->getPackInfo($this->act_id); if(empty($act_info)){ return ["code"=>"1","msg"=>"活动信息错误,请联系管理员"]; } if($act_info["start_time"] > now()){ return ["code"=>"2","msg"=>"红包尚未开抢,请稍后再试"]; } if($act_info["end_time"] <= now()){ return ["code"=>"1","msg"=>"活动已结束"]; } //将请求用户先放置已领取的集合中 if(!$this->redis->sAdd($this->rushed_list_key,$this->user_id)){ return ["code"=>"1","msg"=>"每个红包只能领取一次哦"]; } $money = $this->redis->lPop($this->redpack_money_queue); if(empty($money)){ $this->redis->sRem($this->rushed_list_key,$this->user_id); return ["code"=>"1","msg"=>"您来完了呦,红包已抢光"]; } //将已抢的用户和金额记录至队列中 $add_res = $this->amountAdd($money); if($add_res["code"] != 0){ return ["code"=>"1","msg"=>"系统繁忙,请稍后再试"]; } return ["code"=>"0","msg"=>"success","data"=>$money."元"]; }
数据层(使用异步持续化)
用户领取成功后,将用户id及领取的金额存至已领取的redis queue中,异步进程根据其中的user_id和money值将其数据更新至mysql表中
--------------------------------------------------我是万恶的分割线------------------------------------------------------------------
补充说明:
本人第一次将实际开发过程以及想法落实到书面上,对于我这种小菜来说已经很不错了,恳求各位大神勿喷。其中红包算法和一些处理方案也是第一次接触,参考了网上很多资料,学到了很多。如果你有更好的方案的话多多交流~~
----PHP小菜一枚------
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/39923.html
摘要:记录下整体的设计思路以及运营过程中的各种问题。如果钱是负数了,还得从已生成的小红包中抽取回来将红包放入队列之中创建红包失败,请检查参数生产和之间的随机数,但是概率不是平均的,从到方向概率逐渐加大。 公司前段时间根据业务方需求需要做一个抢红包的活动,网上也搜索了很多资料。记录下整体的设计思路以及运营过程中的各种问题。 产品需求: 1.红包支持配置开始时间、结束时间、类型(随机金额或固定金...
摘要:记录下整体的设计思路以及运营过程中的各种问题。如果钱是负数了,还得从已生成的小红包中抽取回来将红包放入队列之中创建红包失败,请检查参数生产和之间的随机数,但是概率不是平均的,从到方向概率逐渐加大。 公司前段时间根据业务方需求需要做一个抢红包的活动,网上也搜索了很多资料。记录下整体的设计思路以及运营过程中的各种问题。 产品需求: 1.红包支持配置开始时间、结束时间、类型(随机金额或固定金...
摘要:,大家好,很荣幸有这个机会可以通过写博文的方式,把这些年在后端开发过程中总结沉淀下来的经验和设计思路分享出来模块化设计根据业务场景,将业务抽离成独立模块,对外通过接口提供服务,减少系统复杂度和耦合度,实现可复用,易维护,易拓展项目中实践例子 Hi,大家好,很荣幸有这个机会可以通过写博文的方式,把这些年在后端开发过程中总结沉淀下来的经验和设计思路分享出来 模块化设计 根据业务场景,将业务...
阅读 843·2023-04-25 21:21
阅读 3225·2021-11-24 09:39
阅读 3066·2021-09-02 15:41
阅读 1993·2021-08-26 14:13
阅读 1826·2019-08-30 11:18
阅读 2767·2019-08-29 16:25
阅读 505·2019-08-28 18:27
阅读 1579·2019-08-28 18:17