公司前段时间根据业务方需求需要做一个抢红包的活动,网上也搜索了很多资料。记录下整体的设计思路以及运营过程中的各种问题。
产品需求:
1.红包支持配置开始时间、结束时间、类型(随机金额或固定金额)、单个最小红包金额、单个最大红包金额
2.可领取红包的业务条件(根据业务信息指定某些满足条件的人可以抢)
QQ截图20180507142703.png
设计思路:
难点1:红包算法(根据红包配置最大、最小金额、数量生成符合条件的红包集合)
因为红包有配置单个红包的最大和最小金额,所以不能完全使用随机分配的方式。
所以要求:
1.单个红包金额既要大于最小金额,又要小于最大金额
2.根据红包总金额和个数要正好将钱分完
3.单个红包精确到分,也就是小数点后两位
实现代码:
/*
* @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
阶段。 -
根据请求的ip段来控制访问流量,每次接收到抢红包的url后将redis连接池中id自增,当超过某个峰值时跳转到等待页。
具体配置方案参考:http://homeway.me/2015/08/11/nginx-lua-redis-access-control/
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小菜一枚------
网友评论