抢红包系统设计

Published: 06 Jan 2019 Category: arch

零、总结

红包系统业务特点:海量的并发要求和更严格的安全级别。

设计要点:

  • 对于高并发,采用系统SET化设计,分而治之;逻辑Server层将请求分组、排队串行化、并发控制,解决DB并发问题;采用冷热数据分离,保障系统性能稳定。
  • 对于高可用:采用多IDC分地域的部署设计;异步化设计将拆红包和金额到账等业务进行异步分离;通过版本控制来屏蔽故障号段的订单生成,来实现DB故障自愈能力建设。
  • 对于可扩展性:平行缩扩容设计,尽量避免迁移SET。

一、红包系统业务特点

微信红包(尤其是发在微信群里的红包,即群红包)业务形态上很类似网上的普通商品“秒杀”活动。

红包还具备特点:

首先,微信红包业务比普通商品“秒杀”有更海量的并发要求。

微信红包用户在微信群里发一个红包,等同于在网上发布一次商品“秒杀”活动。假设同一时间有10万个群里的用户同时在发红包,那就相当于同一时间有10万个“秒杀”活动发布出去。10万个微信群里的用户同时抢红包,将产生海量的并发请求。

其次,微信红包业务要求更严格的安全级别。

微信红包业务本质上是资金交易。微信红包是微信支付的一个商户,提供资金流转服务。

普通的商品“秒杀”商品由商户提供,库存是商户预设的,“秒杀”时可以允许存在“超卖”(即实际被抢的商品数量比计划的库存多)、“少卖”(即实际被抢的商户数量比计划的库存少)的情况。但是对于微信红包,不能多也不能少。

二、典型秒杀系统的架构设计

2.1 秒杀系统的设计

miaosha

该系统由接入层、逻辑服务层、存储层与缓存构成。Proxy处理请求接入,Server承载主要的业务逻辑,Cache用于缓存库存数量、DB则用于数据持久化。

一个“秒杀”活动,对应DB中的一条库存记录。当用户进行商品“秒杀”时,系统的主要逻辑在于DB中库存的操作上。

一般来说,对DB的操作流程有以下三步:(同时要求这三步操作需要在一个事务中完成)

  • 1.锁库存
  • 2.插入“秒杀”记录
  • 3.更新库存

“秒杀”系统的设计难点就在这个事务操作上。商品库存在DB中记为一行,大量用户同时“秒杀”同一商品时,第一个到达DB的请求锁住了这行库存记录。在第一个事务完成提交之前这个锁一直被第一个请求占用,后面的所有请求需要排队等待。同时参与“秒杀”的用户越多,并发进DB的请求越多,请求排队越严重。因此,并发请求抢锁,是典型的商品“秒杀”系统的设计难点。

红包业务还有两个突出的难点:

首先,事务级操作量级大。普遍情况下同时会有数以万计的微信群在发红包,这个业务特点映射到微信红包系统设计上,就是有数以万计的“并发请求抢锁”同时在进行。这使得DB的压力比普通单个商品“库存”被锁要大很多倍。

其次,事务性要求严格。微信红包系统本质上是一个资金交易系统,相比普通商品“秒杀”系统有更高的事务级别要求。

2.2 秒杀系统的高并发解决方案

方案一,使用内存操作替代实时的DB事务操作。(红包系统不可取)

bonus2

将“实时扣库存”的行为上移到内存Cache中操作,内存Cache操作成功直接给Server返回成功,然后异步落DB持久化。

缺点也很明显,在内存操作成功但DB持久化失败,或者内存Cache故障的情况下,DB持久化会丢数据,不适合微信红包这种资金交易系统。

方案二,使用乐观锁替代悲观锁。(红包系统也不可取)

所谓悲观锁,是关系数据库管理系统里的一种并发控制的方法。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事务执行的操作对某行数据应用了锁,那只有当这个事务把锁释放,其他事务才能够执行与该锁冲突的操作。

所谓乐观锁,它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。

商品“秒杀”系统中,乐观锁的具体应用方法,是在DB的“库存”记录中维护一个版本号。在更新“库存”的操作进行前,先去DB获取当前版本号。在更新库存的事务提交时,检查该版本号是否已被其他事务修改。如果版本没被修改,则提交事务,且版本号加1;如果版本号已经被其他事务修改,则回滚事务,并给上层报错。

这个方案解决了“并发请求抢锁”的问题,可以提高DB的并发处理能力。

但是如果应用于微信红包系统,则会存在下面三个问题:

  • 如果拆红包采用乐观锁,那么在并发抢到相同版本号的拆红包请求中,只有一个能拆红包成功,其他的请求将事务回滚并返回失败,给用户报错,用户体验完全不可接受。
  • 如果采用乐观锁,将会导致第一时间同时拆红包的用户有一部分直接返回失败,反而那些“手慢”的用户,有可能因为并发减小后拆红包成功,这会带来用户体验上的负面影响。
  • 如果采用乐观锁的方式,会带来大数量的无效更新请求、事务回滚,给DB造成不必要的额外压力。

基于以上原因,微信红包系统不能采用乐观锁的方式解决并发抢锁问题。

三、红包系统的高并发解决方案

3.1 系统垂直SET化,分而治之。

微信红包用户发一个红包时,微信红包系统生成一个ID作为这个红包的唯一标识。接下来这个红包的所有发红包、抢红包、拆红包、查询红包详情等操作,都根据这个ID关联。

红包系统根据这个红包ID,按一定的规则(如按ID尾号取模等),垂直上下切分。切分后,一个垂直链条上的逻辑Server服务器、DB统称为一个SET。

各个SET之间相互独立,互相解耦。并且同一个红包ID的所有请求,包括发红包、抢红包、拆红包、查详情详情等,垂直stick到同一个SET内处理,高度内聚。通过这样的方式,系统将所有红包请求这个巨大的洪流分散为多股小流,互不影响,分而治之,如下图所示。

bonus3

这个方案解决了同时存在海量事务级操作的问题,将海量化为小量。

3.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值的分流,如下图所示。

bonus4

其次,设计单机请求排队方案。

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

bonus5

最后,增加memcached控制并发。

为了防止Server中的请求队列过载导致队列被降级,从而所有请求拥进DB,系统增加了与Server服务器同机部署的memcached,用于控制拆同一个红包的请求并发数。

具体来说,利用memcached的CAS原子累增操作,控制同时进入DB执行拆红包事务的请求数,超过预先设定数值则直接拒绝服务。用于DB负载升高时的降级体验。

通过以上三个措施,系统有效地控制了DB的“并发抢锁”情况。

3.3 双维度库表设计,保障系统性能稳定

红包系统的分库表规则,初期是根据红包ID的hash值分为多库多表。随着红包数据量逐渐增大,单表数据量也逐渐增加。而DB的性能与单表数据量有一定相关性。当单表数据量达到一定程度时,DB性能会有大幅度下降,影响系统性能稳定性。采用冷热分离,将历史冷数据与当前热数据分开存储,可以解决这个问题。

处理微信红包数据的冷热分离时,系统在以红包ID维度分库表的基础上,增加了以循环天分表的维度,形成了双维度分库表的特色。

具体来说,就是分库表规则像db_xx.t_y_dd(.前面是库,后面是表)设计,其中,xx/y是红包ID的hash值后三位,dd的取值范围在01~31,代表一个月天数最多31天。

通过这种双维度分库表方式,解决了DB单表数据量膨胀导致性能下降的问题,保障了系统性能的稳定性。同时,在热冷分离的问题上,又使得数据搬迁变得简单而优雅。

综上所述,微信红包系统在解决高并发问题上的设计,主要采用了SET化分治、请求排队、双维度分库表等方案,使得单组DB的并发性能提升了8倍左右,取得了很好的效果。

四、架构设计

4.1 微信红包的系统架构

bonus6

架构包括微信统一接入层,下面是微信红包系统API,包括发、抢、拆、查红包详情、查红包用户列表。再下面是封装微信红包关键业务的逻辑服务;最下面一层是数据存储层,微信红包最主要的数据是订单数据,包括发红包订单和拆红包订单两部分。业务逻辑和存储服务器之间是数据接入层,它最重要的作用是封装数据库操作的领域逻辑,使得业务逻辑服务不需要感知对MySQL的连接管理、性能、容灾等问题。

微信红包数据的访问热度,随着时间流逝会急剧降低,也就是数据的访问时间段非常集中,一般红包发出三天后,99%的用户不会再去点开这个红包了。因此微信红包系统采取按时间做冷热数据分离,降低数据的存储成本,同时提升了热数据的访问性能。

数据平台用于对红包数据的分析计算,比如朋友圈的文章,统计从2016年1月1日到2017年1月一个用户总共抢红包的金额,在全国的排名情况,发红包数最多的城市等。
另外一个作用就是对账,红包的订单和微信支付的订单需要对账,以保证最终资金的一致性;订单的数据和订单的cache需要做对账,以保证数据的完整性;订单数据和用户的收发记录需要对账,以保证用户列表完整性。

4.2 可用性保证

首先是降低意外故障的影响,重点讲解订单存储层在订单DB故障的情况下如何降低对红包系统可用性的影响。

业务逻辑层——部署方案设计

首先是业务逻辑层的部署方案。业务逻辑层是无状态的,微信红包系统的业务逻辑层,部署在两个城市,即两地部署,每一个城市部署至少三个园区,即三个IDC。并且每个服务需要保证三个IDC的部署均衡。另外,三个IDC总服务能力需要冗余三分之一,当一个IDC出现故障时,服务能力仍然足够。从而达到IDC故障不会对可用性产生影响。

业务逻辑层——异步化设计

bonus7

第二是异步化设计。如上图所示,微信红包的某些步骤不实时完成也不会影响用户对红包业务可用性的体验。比如拆红包,正常的业务流程很长,但关键步骤只有订单相关的几步。至于转零钱、写红包记录等操作不需要实时。用户抢到红包时,一般不会实时去钱包查看微信零钱,而是在微信群中点开消息查看本次抢到的金额和他人抢红包金额。所以拆红包时只需要从cache查询用户是否拆过红包,然后写入拆红包的订单记录,更新发红包订单,其他的操作都可以优化。当然,不是每个业务都可以进行异步优化,需要进行业务分析,判断是否存在非关键步骤之外的事情可以将其异步化,并通过异步对账保证最终一致。

接下来是微信红包订单存储设计。主要办法是解决DB间的相互影响,将DB间相互隔离,可见上文的SET化。

如果一个SET发生了故障,会导致业务层写发红包订单失败,这时生成另一个SET订单号重试。同时统计DB失败次数,达到某个阈值时报警。

平行扩缩容设计:如果需要将数据从旧SET迁到新SET,免不了要停服扩容。如果新产生数据与旧数据没关系,那么就可以省掉这部分的迁移动作,不需停服。

分析发现,需要把数据迁移出来的原因是订单号段00-99已全部使用,每个物理数据库包含了10个逻辑库。如果将订单号重新设计,预留三位空间,三位数字每一个代表独立的物理DB,原来10组DB分别为000-009号段。

这种设计,缩容时,比如要缩掉000这组,只需在业务逻辑服务上不生成订单号为000的红包订单。扩容时,比如扩为11组,只需多生成010的订单号,这个数据便自动写入新DB。当然缩容需要一个前提条件,也就是冷热分离,缩容后数据变为冷数据,可下线热数据机器。以上就是红包的平行扩缩容方案。

Q&A

1.业务逻辑层为什么不一起SET化?

业务逻辑层承载了用户维度相关的业务操作,不可以按照订单的维度分业务逻辑,例如业务逻辑层会请求用户头像、昵称等,如果继续按照订单分业务逻辑层,会导致跨地域调用。

所以只对订单存储层SET化,将订单DB和订单接入SERVER垂直stick一起。

REF

微信高并发资金交易系统设计方案——百亿红包背后的技术支撑
微信红包后台系统可用性设计实践