在设计住范儿电商秒杀架构时,笔者对开团瞬间的高并发需求做过全链路的分析,经过各种推导、建模,发现高并发下单过程的单点最终只集中到了一个点上——库存。于是为了提升库存的处理能力,笔者把设计出的几种看起来很复杂的架构合并成了一个“略微复杂”的队列扣库存架构,如图 11-4 所示。接下来,我们对每一步进行详细解读。
下单令牌可以看做一个用户在向后端发送了下单请求之后拿到的唯一查询凭证,此处笔者使用 MySQL 来承载,理论上用 Redis 也可以,推荐在性能压力更大的情况下使用 Redis。
第二步,我们使用下单令牌作为标记,将这个订单的下单任务插入到队列中,等待队列处理器进行处理。
第三步,队列处理器取出一个下单任务,开始执行。需要注意的是,此处的下单队列处理器并不只有一个,只要保持一个比较低的并发量就可以实现对 MySQL 的保护。
随后,在队列处理器进程中,我们会利用 MySQL 事务来进行 sku 库存的真正扣减。
update set store_num = store_num - 1 where store_num >= 1
,然后判断影响的行数,如果大于 0,说明这个 sku 的库存扣减成功了。需要说明的是,4、5 步的两个 MySQL 事务是被同一个 Try Catch 包裹住的,所以如果第一个事务成功而第二个事务失败的话,需要再新增一个队列任务,把库存再加回去。
如果所有 sku 扣库存操作都成功了,那我们就需要执行第二个新建订单的事务了。读者可能会疑惑,生成订单不是一个简单的 Insert 吗,为什么需要事务呢?因为真实世界中,下单的时候需要做的检查和数据的写入还是很多的,例如用户状态检查、收货地址有效性检查、活动和优惠有效性检查、商品有效性检查、店铺有效性检查、购物车对应的商品删除等操作;更极端一点,数据库服务器磁盘满了怎么办。所以下单这么重要的操作加上事务是非常有必要的,我们需要确保订单信息的自洽和完整。
在生成订单后,还有各种繁杂的生成快照和日志的需求,笔者在图中没有画,但是依然需要使用队列任务来异步进行。
下单完成后,就可以将下单令牌标记为下单成功了,客户端的轮询终于可以拿到结果了。之后,我们就可以唤起支付了。
理论上前面的两个 MySQL 事务完全可以用一个事务来实现,但笔者设计成了两个,这是为了尽量减少多个队列处理线程之间的冲突:在最有可能发生冲突的 sku 扣减库存操作中,一个 MySQL 事务需要执行尽量少的任务,这对我们宏观上的系统总容量来说意义重大。
📙 高并发的哲学原理 《Philosophical Principles of High Concurrency》
Copyright © 2023 吕文翰