红包系统的设计

秒杀系统中,很常见的一种就是抢红包,还有其他的例如:双11抢购、直播间秒杀下单、一元夺宝等等。其中抢红包的特点在于无法超售, 对于下单,如果比库存超售少许,其实是可以接受的,但是对于红包,一旦用户抢到的钱比发出去的钱更多,那就是大问题了。当然, 抢购也有特定的业务复杂度,例如下单之后,用户可能没有付款,超时之后需要重新把库存加回去等。我们这里主要讨论红包系统。

对于红包系统,我们需要保证两点:

  1. 扛得住并发,用户抢红包的时候带来的流量峰值是非常高的;
  2. 不能超售,数据一定要准确

那么我们怎么处理这两个问题呢?

处理峰值(削峰)

对于单个红包来说,红包的可抢人数是有限制的,例如200个,500个,再往上就比较少了,而参与的用户,大部分只要看到红包就会 参与点击,并且大都是多次点击,直到抢完。因此,这种行为带来的一个特点,就是红包发出以后,会迎来一波比红包个数大很多的 流量,但是超过红包个数的流量基本是无用的流量。因此,对于红包系统,我们需要在请求到达真正处理的后端服务之前,加上如下 防火墙,对流量进行过滤:

  • 用户组织层限流:限制群组大小,这样就限制了一个红包最大的参与人数,例如一个群组最大2万人。但是有的场景下无法限制,例如 直播间。这一层面的限制取决于产品形态

  • 用户端限流:浏览器、App、小程序等用户参与游戏的客户端,我们可以进行限速操作,例如1s点击一次,点击后1s内按钮置灰不可 再次点击

  • 网关层限流:CDN,Nginx,限速,从这里开始,就涉及到服务端的处理了,这里就涉及到多种策略,例如针对IP限速,针对用户限速等

  • 服务层限流:这里需要代码进行处理,例如设置当前参与人数超过红包总数3倍,直接拒绝当前请求,一般会用Redis来存储这个信息

  • 应用层限流:当红包抢完之后,可以在Redis中记录一个标记,应用检测到这个信息,就不需要进行后面的操作了,可以直接拒绝

数据准确(加锁)

如何保证红包的数据准确性呢?如果是使用数据库来存储的话,就需要使用事务,加上行锁,也就是 SELECT ... FOR UPDATE 语句, 如果是使用Redis来存储的话,就需要使用CAS操作,由于Redis中的事务并不保证ACID,但是Lua脚本是原子执行的,因此需要使用Lua 脚本来进行操作。

分配方案

对于红包的分配,有两种方案:

  • 预分配。发送红包的时候,就提前分配好每一个人能抢到多少,抢红包的时候,取一个未抢的金额出来,分配给对应用户,并且更新状态, 此处需要保证一个用户在一个红包下,只能抢一次。

  • 实时分配。发送红包的时候不分配,在抢红包的时候进行分配,根据红包的剩余金额和剩余个数,计算一个金额给当前用户,此处也许要 在数据库层面保证一个用户在一个红包下,只能抢一次。

分配算法

红包分配算法可以参考微信红包的分配算法:随机,额度在0.01和剩余平均值*2之间。

数据落地方案

  • 数据库事务 + SELECT ... FOR UPDATE,通过事务保证ACID,通过行锁保证不会产生并发更新错误。
  • 数据库集群(分库sharding),当一个数据库吃不住的时候,可以使用集群来扛并发。
  • Redis Lua 脚本,当集群扛不住的时候,就可以考虑使用Redis来进行操作了。但是最终数据还是需要落库到数据库。

因此我实现时,采用的是第一种方案,也就是直接数据库+行锁来实现。使用数据库实现时,需要注意的是避免死锁场景,我就踩到 死锁的坑导致数据库连接池爆了,排查了好一会儿。这种方案对于数据库本身有一定要求,尤其是磁盘IOPS,需要好好的调校数据库以 得到一个比较好的并发性能。

缓存

对于红包本身,我认为缓存的必要性不大,因为每一次成功抢红包,都会导致缓存失效,还是需要数据库参与。与之相对的,反而是红包 的基本信息更加管用,例如:

  • 当前红包是否已经抢完?可以用于提前拒绝请求
  • 当前用户是否抢过?可以用于提前拒绝请求
  • 当前用户是否最近1s抢过?可以用于限速

把这些信息写到缓存里,更有助于提高整个系统的并发能力。

总结

这篇文章中,总结了一个红包系统需要用到的知识,以及如何取处理流量和如何设计系统。



更多文章
  • 写一个简单的ORM
  • 从源码看Python的descriptor
  • Python字符串格式化
  • Gunicorn 简明教程
  • Raft 论文阅读笔记
  • 什么是 Golang Comparable Types
  • GFS 论文阅读
  • MapReduce 论文阅读
  • 一起来做贼:Goroutine原理和Work stealing
  • Go语言的defer, panic和recover
  • 再读 Python Language Reference
  • 再读vim help:vim小技巧
  • 设计模式(2)- 深入浅出设计模式 阅读笔记
  • 设计模式(1)- 深入浅出设计模式 阅读笔记
  • Cython! Python和C两个世界的交叉点