秒杀系统设计
用内存 或者 redis只能降低到关系型数据库的流量,不能强一致性。比如库存扣减,redis扣减到0时,mysql还没更新到0,减少流量。redis扣减时也同步记录了流水,流水会负责数据库的库存更新。
内存可以处理排队,通过流量hash到一个server上,本地内存排队处理即可。
流程
根据商品id,先从缓存中查询商品,如果商品存在,则参与秒杀。如果不存在,则需要从数据库中查询商品,如果存在,则将商品信息放入缓存,然后参与秒杀。如果商品不存在,则直接提示失败。
扣库存
对于秒杀系统,一般要预先扣库存,在支付前就扣库存
扣减库存中除了上面说到的预扣库存和回退库存之外,还需要特别注意的是库存不足和库存超卖问题。
扣库存,需要是否还有,还有就减一。可以用redis lua脚本。
支付 - 分布式事务
先扣库存,如何保证支付失败后再返库存。
可通过RocketMQ的事务消息补偿的分布式事务。
库存成功后mq消息才提交,这样consumer track订单状态。失败就返还,要不mq就是dlq了。
如果不是RocketMQ,还需要定时任务来配合,避免事务失败了,但却没有发mq成功,这时候就需要定时任务来检查那些事务失败却没标记为已发送mq成功的数据来处理。
也可以接入分布式事务协调者,比如Amazon的watchDog. 它是本地的一个守护进程,负责监控事务是否完成,以及中间的状态检查。
限流
目前有两种常用的限流方式:
- 基于nginx限流
- 基于redis限流
用户限流、ip限流、验证码。
Server里不用所有请求都穿透到Redis, 可以hash server 然后每个server有内存队列,队列满了就直接抛弃。
比如秒杀10个库存的商品,100个server负责这个库存。 每个server队列长度为10。
极度秒杀场景
比如说春节红包,可能几百万的qps, 客户端所需图片资源需要提前下发到端上,否则服务器带宽承受不住,以及延迟影响用户体验。
倒计时可以打散,有益于平衡服务器压力。
金额预分配,之后对没点击的金额再回收。
http链接保活,定时Ping-pong。
redis的kv存金额可能也扛不住,那就预先从redis里取些放到本地,本地通过并发安全逻辑减少金额,退出时要接收进程退出信号 并归还金额。
或者一个账户拆分多个子账户,分散压力。
如果可以最终一致性,就记录好,后续batch操作,类似redo log。
异步处理,比如有提现等。
降级、限流。

抢红包
和秒杀系统类似,但延迟要求更高。
- 高并发,考虑redis
- 大容量,考虑分库分表
- redis/db2,考虑同步
- 有状态,考虑hash
- 消息顺序,考虑单线程
- 实时,考虑同步,延时,考虑异步
抢红包其实主要有 3 个核心流程:红包金额拆分->抢红包->打款。
- 红包金额拆分是指将指定金额拆分为指定数目红包的过程,用来确定每个红包的金额数;
- 抢红包是用户抢红包的这个操作,典型的高并发场景,需要系统扛流量且避免红包超发的情况;
- 打款就是将抢到的红包通过微信/银行打款到用户钱包的过程(真正把红包的钱拿到手了),因为要对接三方支付系统是整个系统比较耗时的操作,一般通过异步任务来实现;
拆分方式
1、实时拆分
实时拆分,指的是在抢红包时实时计算每个红包的金额,以实现红包的拆分过程,对系统性能和拆分算法要求较高,例如拆分过程要一直保证后续待拆分红包的金额不能为空,不容易做到拆分的红包金额服从正态分布规律。
2、预先生成 推荐
预先生成,指的是在红包开抢之前已经完成了红包的金额拆分,抢红包时只是依次取出拆分好的红包金额,对拆分算法要求较低,可以拆分出随机性很好的红包金额,通常需要结合队列使用。
二倍均值法 的算法拆分红包金额。

抢红包
我们选用队列串行化的方案,抢红包整个过程只会操作 Redis,且都是简单高效的 Pop 和 Push 命令操作。
抢红包流程:先从红包队列中 Pop 占有红包,然后 Push 红包到任务队列(待异步打款处理),并同步告知用户抢到红包的结果,抢红包流程就结束了。
其中,red::list为 List 结构,存放预先拆分好金额的红包;red::task 也为 List 结构,异步打款处理任务队列;red::draw为 Hash 结构,存放红包领取记录,field为用户的 openid,value为序列化的红包信息;red::draw_count:u:openid为 k-v 结构,用户领取红包计数器。
怎么保证不超发
- Lua脚本保证原子性和隔离性
Redis挂了怎么办
- 根据WAL log重新恢复,人工介入。
- 或者Lua脚本和事务消息一起做。先写消息到rocketMq, redis更新成功就提交mq。 mq消费接着处理,保证抢到的红包依然还在。
打款
采用 Worker 任务去消费任务队列,调用红包支付 API,以及数据持久化操作(后续对账)。尽管红包发放调用链又长又慢,但是这些 Worker 是 无状态 的,所以可以通过增加 Worker 数量,提高系统的消费处理能力。
在打款流程里面做了重试机制,生成一个全局唯一的外部订单号,当某红包打款失败,就会放回任务队列重试,当然重试时要处理好幂等。
总结
- 红包预分配,放redis list
- redis队列串行化,pop时lua脚本放到另一个task list里。这样worker异步的消费task list来发钱,失败了就重试。
腾讯的红包方案
微信公布了用户在除夕当天收发微信红包的数量——142亿个,而其收发峰值也已达到76万每秒。百亿级别的红包,如何保障并发性能与资金安全?这给微信带来了超级挑战。面对挑战,微信红包在分析了业界“秒杀”系统解决方案的基础上,
采用了SET化、请求排队串行化、双维度分库表等设计,形成了独特的高并发、资金安全系统解决方案。实践证明,该方案表现稳定,且实现了除夕夜系统零故障运行。
解决高并发通常的解决方案
普通商品“秒杀”活动系统,解决高并发问题的方案,大体有以下几种:
方案一,使用内存操作替代实时的DB事务操作。
- 将“实时扣库存”的行为上移到内存Cache中操作,内存Cache操作成功直接给Server返回成功,然后异步落DB持久化。这个方案的优点是用内存操作替代磁盘操作,提高了并发性能. 但是缺点也很明显,在内存操作成功但DB持久化失败,或者内存Cache故障的情况下,DB持化会丢数据,不适合微信红包这种资金交易系统。
方案二,使用乐观锁替代悲观锁。
商品“秒杀”系统中,乐观锁的具体应用方法,是在DB的“库存”记录中维护一个版本号。在更新“库存”的操作进行前,先去DB获取当前版本号。
在更新库存的事务提交时,检查该版本号是否已被其他事务修改。如果版本没被修改,则提交事务,且版本号加1;
如果版本号已经被其他事务修改,则回滚事务,并给上层报错。这个方案解决了“并发请求抢锁”的问题,可以提高DB的并发处理能力。
但是如果应用于微信红包系统,则会存在下面三个问题:
- 如果拆红包采用乐观锁,那么在并发抢到相同版本号的拆红包请求中,只有一个能拆红包成功,其他的请求将事务回滚并返回失败,给用户报错,用户体验完全不可接受。
- 如果采用乐观锁,将会导致第一时间同时拆红包的用户有一部分直接返回失败,反而那些“手慢”的用户,有可能因为并发减小后拆红包成功,这会带来用户体验上的负面影响。
- 如果采用乐观锁的方式,会带来大数量的无效更新请求、事务回滚,给DB造成不必要的额外压力。
微信红包系统的高并发解决方案
综合上面的分析,微信红包系统针对相应的技术难点,采用了下面几个方案,解决高并发问题。
1.系统垂直SET化,分而治之。
微信红包用户发一个红包时,微信红包系统生成一个ID作为这个红包的唯一标识。接下来这个红包的所有发红包、抢红包、拆红包、查询红包详情等操作,都根据这个ID关联。
红包系统根据这个红包ID,按一定的规则(如按ID尾号取模等),垂直上下切分。切分后,一个垂直链条上的逻辑Server服务器、DB统称为一个SET。
各个SET之间相互独立,互相解耦。并且同一个红包ID的所有请求,包括发红包、抢红包、拆红包、查详情详情等,垂直stick到同一个SET内处理,高度内聚。通过这样的方式,系统将所有红包请求这个巨大的洪流分散为多股小流,互不影响,分而治之,如下图所示。

这个方案解决了同时存在海量事务级操作的问题,将海量化为小量。
2.逻辑Server层将请求排队,解决DB并发问题。
红包系统是资金交易系统,DB操作的事务性无法避免,所以会存在“并发抢锁”问题。但是如果到达DB的事务操作(也即拆红包行为)不是并发的,而是串行的,就不会存在“并发抢锁”的问题了。
按这个思路,为了使拆红包的事务操作串行地进入DB,只需要将请求在Server层以FIFO(先进先出)的方式排队,就可以达到这个效果。从而问题就集中到Server的FIFO队列设计上。
微信红包系统设计了分布式的、轻巧的、灵活的FIFO队列方案。其具体实现如下:
首先,将同一个红包ID的所有请求stick到同一台Server。
上面SET化方案已经介绍,同个红包ID的所有请求,按红包ID stick到同个SET中。不过在同个SET中,会存在多台Server服务器同时连接同一台DB(基于容灾、性能考虑,需要多台Server互备、均衡压力)。
为了使同一个红包ID的所有请求,stick到同一台Server服务器上,在SET化的设计之外,微信红包系统添加了一层基于红包ID hash值的分流,如下图所示。

其次,设计单机请求排队方案。
将stick到同一台Server上的所有请求在被接收进程接收后,按红包ID进行排队。然后串行地进入worker进程(执行业务逻辑)进行处理,从而达到排队的效果,如下图所示。

最后,增加memcached控制并发。
为了防止Server中的请求队列过载导致队列被降级,从而所有请求拥进DB,系统增加了与Server服务器同机部署的memcached,用于控制拆同一个红包的请求并发数。
具体来说,利用memcached的CAS原子累增操作,控制同时进入DB执行拆红包事务的请求数,超过预先设定数值则直接拒绝服务。用于DB负载升高时的降级体验。
通过以上三个措施,系统有效地控制了DB的“并发抢锁”情况。
3.双维度库表设计,保障系统性能稳定
红包系统的分库表规则,初期是根据红包ID的hash值分为多库多表。随着红包数据量逐渐增大,单表数据量也逐渐增加。而DB的性能与单表数据量有一定相关性。当单表数据量达到一定程度时,DB性能会有大幅度下降,影响系统性能稳定性。采用冷热分离,将历史冷数据与当前热数据分开存储,可以解决这个问题。
处理微信红包数据的冷热分离时,系统在以红包ID维度分库表的基础上,增加了以循环天分表的维度,形成了双维度分库表的特色。
具体来说,就是分库表规则像db_xx.t_y_dd设计,其中,xx/y是红包ID的hash值后三位,dd的取值范围在01~31,代表一个月天数最多31天。
通过这种双维度分库表方式,解决了DB单表数据量膨胀导致性能下降的问题,保障了系统性能的稳定性。同时,在热冷分离的问题上,又使得数据搬迁变得简单而优雅。
综上所述,微信红包系统在解决高并发问题上的设计,主要采用了SET化分治、请求排队、双维度分库表等方案,使得单组DB的并发性能提升了8倍左右,取得了很好的效果。
总结
- 利用每个红包路由到一个server上。
- DB是也是分库的,因此先hash DB再hash server。
- 类似本地内存mq形式,在server里的内存里排序。
- DB除了hash分配还可以加上冷热分离,避免数据膨胀影响性能。