怎么设计一个抢红包系统?

大家应该经常领红包吧,那怎么设计一个抢红包系统呢?本文将做详细的系统设计说明。

活动规模

公司在年底,为员工准备了 25 万元的抢红包活动,在年三十晚 8 点准时开始。

微信截图_20220703115217.png

 
技术方案

首先要理解到抢红包特殊的业务场景,红包抢到了并不等于把钱拿到手了。抢红包其实主要有 3 个核心流程:红包金额拆分->抢红包->打款。 
  • 红包金额拆分是指将指定金额拆分为指定数目红包的过程,用来确定每个红包的金额数;
  • 抢红包是用户抢红包的这个操作,典型的高并发场景,需要系统扛流量且避免红包超发的情况;
  • 打款就是将抢到的红包通过微信/银行打款到用户钱包的过程(真正把红包的钱拿到手了),因为要对接三方支付系统是整个系统比较耗时的操作,一般通过异步任务来实现;

 红包金额拆分
可选的方案

拆分方式

1、实时拆分

实时拆分,指的是在抢红包时实时计算每个红包的金额,以实现红包的拆分过程,对系统性能和拆分算法要求较高,例如拆分过程要一直保证后续待拆分红包的金额不能为空,不容易做到拆分的红包金额服从正态分布规律。

2、预先生成

预先生成,指的是在红包开抢之前已经完成了红包的金额拆分,抢红包时只是依次取出拆分好的红包金额,对拆分算法要求较低,可以拆分出随机性很好的红包金额,通常需要结合队列使用。

拆分算法

红包拆分算法拆分的金额要看起来随机,最好能够服从正态分布,可以参考 微信 和 @lcode 提供的红包拆分算法。

微信拆分算法的优点是算法较简单,拆分效率高,同时由于该算法天然的特性,可以保证后续红包金额一定不为空,特别适合实时拆分场景,但缺点是会导致大额红包较大概率地在拆分的最后出现。 @lcode 拆分算法的优点是拆分金额基本符合正态分布,适合随机性要求较高的拆分场景。

我们的方案

我们这次的场景对红包金额的随机性要求不高,但是对系统可靠性要求较高,所以我们选用了预先生成方式,使用 二倍均值法 的算法拆分红包金额。

拆分算法可以描述为:假设剩余拆分金额为 M,剩余待拆分红包个数为 N,红包最小金额为 1 元,那么定义当前红包的金额为:


m=rand(1,floor(M/N∗2))



其中,floor 表示向下取整,rand(min, max) 表示从 [min, max] 区间随机一个值。M/N*2 表示剩余待拆分金额平均金额的 2 倍,因为 N >= 2,所以 M/N*2 <= M,表示一定能保证后续红包能拆分到金额。

代码实现为:
for ($i = 0; $i < $N - 1; $i++) {
$max = (int)floor($M / ($N - $i)) * 2;
$m[$i] = $max ? mt_rand(1, $max) : 0;
$M -= $m[$i];
}

$m = $M;
值得一提的是,为了保证红包金额差异尽量小,先将总金额平均拆分成 N+1 份,将第 N+1 份红包按照上述的红包拆分算法拆分成 N 份,这 N 份红包加上之前的平均金额才作为最终的红包金额。

抢红包

可选的方案

限流

1、前端限流

前端限制用户在 n 秒之内只能提交一次请求,虽然这种方式只能挡住小白(99% 的用户),所以也必须得做。

2、后端限流

常用的后端限流方法有 漏桶算法 和 令牌桶算法。漏桶算法 主要目的是控制请求数据注入的速率,如果此时漏桶溢出,后续的请求数据会被丢弃。而 令牌桶算法 是以一个恒定的速度往桶里放入令牌,而如果请求数据需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌时,这些请求才被丢弃,令牌桶算法的一个好处是可以方便地改变应用接受请求的速率。

防并发超发红包

1、库存加锁


可以通过加锁的方式解决,但是加锁会增加系统开销,大流量下更容易拖垮系统,也可以尝试一下基于版本号的乐观锁。

2、队列串行化请求

之所会出现超发问题,是因为并发时会出现多个进程同时操作同一资源的现象,如果使用高速队列将并行请求串行化,那么问题就不存在了。高速队列可以使用 Redis 来实现,但是还要必须保证整个流程调用链要短、要快,否则队列会积压严重,甚至会拖垮整个系统。

我们的方案

我们选用队列串行化的方案,抢红包整个过程只会操作 Redis,且都是简单高效的 Pop 和 Push 命令操作。

抢红包流程:先从红包队列中 Pop 占有红包,然后 Push 红包到任务队列(待异步打款处理),并同步告知用户抢到红包的结果,抢红包流程就结束了。

微信截图_20220703115237.png

 
当然,在实际应用中,占有红包过程中还会有一些前置规则校验,比如用户是否已经领取过,领取次数是否已经达到上限等?红包占有流程图如下:

微信截图_20220703115247.png

 
其中,red::list为 List 结构,存放预先拆分好金额的红包;red::task 也为 List 结构,异步打款处理任务队列;red::draw为 Hash 结构,存放红包领取记录,field为用户的 openid,value为序列化的红包信息;red::draw_count:u:openid为 k-v 结构,用户领取红包计数器。

1、怎么保证不超发

从红包占有流程图可看出,这个过程是会操作很多 Key,那怎么保证原子性?我们选用了 Lua 方案,一方面是因为首先要保证性能,没有多次请求的网络开销,另一方面 Lua 脚本执行时本身就是原子性的,满足需求。

红包占有的 Lua 脚本实现如下:
-- 领取人的openid为xxxxxxxxxxx
local openid = 'xxxxxxxxxxx'
local isDraw = redis.call('HEXISTS', 'red::draw', openid)
-- 已经领取
if isDraw ~= 0 then
return true
end
-- 领取太多次了
local times = redis.call('INCR', 'red::draw_count:u:'..openid)
if times and tonumber(times) > 9 then
return 0
end

local number = redis.call('RPOP', 'red::list')
-- 没有红包
if not number then
return {}
end
-- 领取人昵称为Fhb,头像为https://xxxxxxx
local red = {money=number,name='Fhb',pic='https://xxxxxxx'}
-- 领取记录
redis.call('HSET', 'red::draw', openid, cjson.encode(red))
-- 处理队列
red['openid'] = openid
redis.call('RPUSH', 'red::task', cjson.encode(red))

return true


需要注意 Lua 脚本执行过程并不是事务的,脚本中的操作命令在执行时是有先后顺序的,当某个操作执行失败时不会回滚已经执行成功的操作。


 
2、怎么提高系统响应

由于抢红包和打款流程分开,抢红包过程只需操作 Redis,整个操作短且快,故不存在性能问题。

打款

我们的方案

采用 Worker 任务去消费任务队列,调用红包支付 API,以及数据持久化操作(后续对账)。尽管红包发放调用链又长又慢,但是这些 Worker 是 无状态 的,所以可以通过增加 Worker 数量,提高系统的消费处理能力。
微信截图_20220703115258.png

 
1、怎么保证数据一致性

若红包打款失败了,前面已经告知用户抢到红包,但是却木有发,那用户肯定会很愤怒。但是根据 CAP 原理,我们通常只需做到数据最终一致性。

我们在打款流程里面做了重试机制,生成一个全局唯一的外部订单号,当某红包打款失败,就会放回任务队列重试,当然重试时要处理好幂等。

2、怎么保证Worker不异常结束

Worker 的实现如下:
$maxTask = 1000;
$sleepTime = 1000;

while (true) {
while ($red = RedLogic::getTask()) {
RedLogic::doTask($red);
//处理多少个任务主动退出
$maxTask--;
if ($maxTask < 0) {
return EXIT_CODE_NORMAL;
}
}
//等待任务
usleep($sleepTime);
}


这里使用 LPOP 命令获取任务,所以使用了 while 结构,并且无任务时需要等待,可以用阻塞命令 BLPOP 来改进。


由于 Worker 需要常驻内存运行,难免会出现异常结束的情况(也有主动 Exit), 所以需要保持 Worker 一直处于运行状态。我们使用进程管理工具 Supervisor 来监控和管理 Worker 任务,当任务队列出现堆积时,增加 Worker 数量即可。Supervisor 的监控后台如下:

微信截图_20220703115309.png

 
保障方案

资源CDN缓存


由于本次活动力度较大,静态页面占流量的很大一部分,所以静态页面在发布时都会放置一份在 CDN 上,这样回源的流量就很小了。

降级措施

尽管做了很多准备,还是无法确保万无一失,我们在每个关键节点都增加了开关,一但出现异常,可以通过配置中心人工介入做降级处理。 

原文来自微信公众号:后端搬运工
原文地址:https://mp.weixin.qq.com/s/VG_Wcxte8avnXzn4bPXiGA
欢迎大家多关注!

0 个评论

要回复文章请先登录注册