23讲搞定后台架构实战学习-扣减秒杀场景

643 阅读7分钟

来源:23讲搞定后台架构实战

思考:

面对百万并发的极端场景,比如大量用户在同一时间内抢购同一商品

16 | 秒杀场景:热点扣减如何保证命中的存储分片不挂?

热点扣减的典型业务场景

杀的特点主要有以下两点。

  • 首先,秒杀带来的热点量非常大,其他热点场景很难比拟。比如,在刚过去的 2020 年,大家在电商平台里准点抢购口罩,上百万人同时在线抢购同一商品,此时就带来了超大并发量。

  • 其次,秒杀对于扣减的准确性要求极高。 秒杀在绝大部分场景里是一种营销手段,如一元抢 iPhone。 商家对有限的商品设置一个亏本价,吸引用户下载或注册 App,达到拉新、提升知名度等目的。 因为是亏本营销,如果出现了大面积的超卖,业务上是绝不允许的。

技术挑战

通过副本和本地缓存方式不合适

图 1:基于数据库+缓存的热点扣减现状 图 1:基于数据库+缓存的热点扣减现状

可以看到,秒杀与热点扣减所带来技术问题是一样的——所有的热点请求均命中同一个存储分片。 那为什么不能直接复用“第 06 讲”介绍的“通过增加缓存副本以及使用本地缓存”的方式来应对呢?

首先,扣减是写请求,即每一次请求都会修改当前商品对应的总数量,且当商品数量扣减为零或当前剩余商品数量小于当次要扣减的数量时,均会返还失败。

  • 而“第 06 讲”热点查询里的缓存副本或者本地缓存里的商品数量均是原始分片的数据镜像,不能被拿来进行扣减的,否则就会出现数据错乱,甚至超卖的现象

  • 其次,本地缓存里的数据是非持久化数据,易丢失。即使将本地缓存持久化至宿主机的磁盘,也会因磁盘故障、不满足 ACID 等原因而导致数据丢失。

图 2:副本的镜像架构图 图 2:副本的镜像架构图

如何应对秒杀流量?

既然不能采用热点查询里的方案,只能使用缓存单分片来应对秒杀的流量, 但单分片能够支持的流量是有上限。当流量超过上限后如何处理呢

  • 在实际的应用中,推荐采用单机维度的限流器,因为它会更加精准和实时。

image.png

image.png

水平扩展架构升级

提示:同一个商品的库存 还能拆分?

在设置秒杀库存时,将秒杀库存按缓存分片的数量进行平均等分,每一个缓存里均存储一等份即可。 比如某一个商品(记为 SKU1)的秒杀库存为 10,当前部署的缓存分片共计 10 个,那么每一个分片里存储该 SKU 的库存数可以为 1,存储在各个缓存里的 key 可以为:SKU1_1、SKU1_2、...、SKU1_10

image.png

如何利用缓存+数据库构建高可靠的扣减方案?

问题:

纯缓存方案虽不会导致超卖,但因缓存不具备事务特性,极端情况下会存在缓存里的数据无法回滚,导致出现少卖的情况。 且因“第 13 讲”是异步写库,也可能发生异步写库失败, 导致多扣的数据再也无法找回的情况。

本讲的方案是借助了“顺序写要比随机更新性能好”这个特性进行设计的。

在向磁盘进行数据操作时,向文件末尾不断追加写入的性能要远大于随机修改的性能。因为对于传统的机械硬盘来说,每一次的随机更新都需要机械键盘的磁头在硬盘的盘面上进行寻址,再去更新目标数据,这种方式十分消耗性能。而向文件末尾追加写入,每一次的写入只需要磁头一次寻址,将磁头定位到文件末尾即可,后续的顺序写入不断追加即可。

对于固态硬盘来说,虽然避免了磁头移动,但依然存在一定的寻址过程

此外,对文件内容的随机更新和数据库的表更新比较类似,都存在加锁带来的性能消耗。

数据库同样是插入要比更新的性能好。

对于数据库的更新,为了保证对同一条数据并发更新的一致性,会在更新时增加锁,但加锁是十分消耗性能的。

此外,对于没有索引的更新条件,要想找到需要更新的那条数据,需要遍历整张表,时间复杂度为 O(N)。

而插入只在末尾进行追加,性能非常好

image.png

上述的架构和纯缓存的架构区别在于,写入数据库不是异步写入,而是在扣减的时候同步写入。

如果你仔细看架构图,会发现并非如此。同步写入数据库使用是 insert 操作,也就是顺序写,而不是 update 做数据库数量的扣减。因此,它的性能较好。

insert 的数据库称为任务库,它只存储每次扣减的原始数据,而不做真实扣减(即不进行 update)。它的表结构大致如下:

疑惑 顺序写入和扣减服务有什么关系

原理分析

本讲介绍的数据库+缓存的架构主要利用了数据库顺序写入要比更新性能快的这一特性。

此外,在写入的基础之上,又利用了数据库的事务特性来保证数据的最终一致性。

当异常出现后,通过事务进行回滚,来保证数据库里的数据不会丢失

在整体的流程上,还是复用了上一讲纯缓存的架构流程。

当新加入一个商品,或者对已有商品进行补货时,对应的新增商品数量都会通过 Binlog 同步至缓存里。在扣减时,依然以缓存中的数量为准。

补货或新增商品的数据同步架构如下图 3 所示

image.png

这里你可能会产生疑问:通过任务库同步至正式业务库里那份数据岂不是没用了?当然不是。正式业务库异构的那份扣减明细和 SKU 当前实时剩余数量的数据,是最为准确的一份数据,我们以它作为数据对比的基准。如果发现缓存中的数据不一致,就可以及时进行修复。对于数据校准,你可以参考“第 05 讲”里介绍的方案。

“顺序追加写要比随机修改的性能好”这个技巧,其实在很多场景里都有应用,是一个值得你深入学习和理解的技能。

比如数据库的 Redo log、Undo log; Elasticsearch 里的 Translog 都是先将数据按非结构化的方式顺序写入日志文件里,再进行正常的变更。 当出现宕机后,采用日志进行数据恢复。

总结

  1. 通过副本和本地缓存方式不合适
  2. 单机情况下如何扩展 在处理秒杀请求时,不只是固定地命中某一个缓存分片,而是在每次请求时轮询命中缓存集群中的每一个缓存分片。

将秒杀商品的库存前置散列到各个缓存分片,可以将原先热点扣减只能使用一个缓存分片升级至多个,提升吞吐量