公司前段时间根据业务方需求须要作一个抢红包的活动,网上也搜索了不少资料。记录下总体的设计思路以及运营过程当中的各类问题。php
1.红包支持配置开始时间、结束时间、类型(随机金额或固定金额)、单个最小红包金额、单个最大红包金额html
2.可领取红包的业务条件(根据业务信息指定某些知足条件的人能够抢)前端
难点1:红包算法(根据红包配置最大、最小金额、数量生成符合条件的红包集合)java
由于红包有配置单个红包的最大和最小金额,因此不能彻底使用随机分配的方式。mysql
因此要求:nginx
* 单个红包金额既要大于最小金额,又要小于最大金额
* 根据红包总金额和个数要正好将钱分完web
* 单个红包精确到分,也就是小数点后两位
实现代码:ajax
/* * @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,这样就将其精确到小数点后两位。redis
难点2:高并发时对服务器的访问压力
相似抢红包、1元抢购,秒杀等业务场景都是在同一时间大量请求堆积到服务器,从而致使服务器资源紧张,程序处理不过来。那么咱们要作的就是将流量控制住,不让大量的请求透过web服务器直接打到数据库层。那么从用户访问url到收到返回结果总体流程是什么样子呢?算法
客户端层
,用户在微信中打开URL,DNS解析域名至服务器web服务器层
, Apache、Nginx或Tomcat等服务器层
,分配php-fpm进程,代码接收参数进行逻辑处理数据持续化层次
,将结果保存至mysql或Redis层次
客户端层优化方案:(限流)
ajax
请求,调用后台使用java写的redis incr
接口,每次调用则键值 +1,并将自增id返回,当后台代码处理完后再将其键值减掉,由于incr
自增为原子级别,因此前端能够根据当前有多少用户在等待中。 根据自身服务器配置以及业务场景预估N多请求会致使服务器出现问题,若是当前等待处理的请求数大于N则前端提示用户 "当前请求过多,请稍后再试",反之则能够正常发起请求。Web层优化方案(lua+nginx实现频率控制)
用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.'元']; }
数据层(使用异步持续化)
redis queue
中,异步进程根据其中的user_id和money值将其数据更新至mysql表中--------------------------------------------------我是万恶的分割线------------------------------------------------------------------
补充说明:
本人第一次将实际开发过程以及想法落实到书面上,对于我这种小菜来讲已经很不错了,恳求各位大神勿喷。其中红包算法和一些处理方案也是第一次接触,参考了网上不少资料,学到了不少。若是你有更好的方案的话多多交流~~
----PHP小菜一枚------