作者:冰河
星球:http://m6z.cn/6aeFbs
博客:https://binghe.gitcode.host
源码获取地址:https://t.zsxq.com/0dhvFs5oR
沉淀,成长,突破,帮助他人,成就自我。
大家好,我是冰河~~
相信很多小伙伴都在大厂的秒杀大促中抢购过商品,那大家有没有想过这样一个问题:在秒杀这种高并发大流量的场景下,商品的库存是如何设计呢?怎么才能抗住瞬时高并发的流量呢?
也有不少小伙伴出去面试时,简历上写了秒杀系统,此时面试官通常也会问这样一个问题:你们的秒杀系统库存是怎么设计的呢?要知道,秒杀系统的库存如果只是简单的按照普通商品的库存进行设计,是根本撑不住瞬时的高并发流量的。
今天,冰河就结合多年参与大厂秒杀大促基础架构设计,以及多次保障大促期间下单扣减库存的核心链路稳定的经验,为大家揭秘大厂秒杀系统是如何设计库存的。当然,大部分架构设计和编码实现以及部署上线等内容也都沉淀在冰河技术知识星球的《高并发Seckill秒杀系统》专栏。
大家可以猛戳链接:https://t.zsxq.com/iG6Fq 学习《高并发Seckill秒杀系统》
一、前言
对秒杀系统数据库的读写操作进行优化,并不是简单的进行主从复制和分库分表。而是需要从秒杀特有的瞬时高并发、大流量的业务场景出发,针对场景进行数据库优化。
对于秒杀这种场景来说,关键是要支撑瞬时的高并发、大流量,大量用户抢购商品下单时,会频繁调用查询和更新商品库存的接口,所以,对于商品库存来说,我们需要增强数据库的读写性能。
在具体设计上,就是要对商品的库存进行分库分表和分桶设计,使得商品的库存不再由单数据库进行存储,扩展成多台数据库,并且在每个数据库中,又对商品的库存进行分桶设计。同时,在缓存层面,也需要对商品的库存进行分桶设计
二、库存优化目标
在正式对商品库存进行分库分表和分桶设计之前,我们先来确定下库存优化的目标,也就是分库分表和分桶设计的目标,这样在后续的实现中更有针对性。这里,我主要把库存优化的目标分成了六点:分库设计、分表设计、分桶设计、缓存设计、一致性设计和兼容性设计,如下图所示。
- 根据秒杀商品对库存进行分库设计:使得相同秒杀商品的库存能够路由到同一数据库进行处理。
- 根据秒杀商品对库存进行分表设计:使得相同秒杀商品的库存能够路由到同一数据库中,然后再进一步根据商品id进行分表。
- 根据秒杀商品对库存进行分桶设计:对于秒杀系统来说,分库分表主要提升的是多场秒杀活动的并发处理能力,而分桶设计主要解决的是单场秒杀活动的并发处理能力。
- 根据库存的分库分表和分桶方案,设计对应的库存缓存方案:根据库存的分库分表和分桶方案,为商品的分桶库存设计分桶缓存方案:真正扣减商品分桶库存之前会预扣缓存中的分桶库存数据,以提高系统的并发处理能力。
- 数据一致性设计:在缓存与数据库的数据一致性层面,基于分库分表和分桶设计,在缓存层面实现弱一致性,数据库层面实现强一致性。
- 兼容性设计:对于新增的商品库存分库分表和分桶设计,要兼容之前的商品库存设计,能够根据简单的配置进行自由切换。
三、分库分表设计
在分库分表的设计上,这里我们使用了三个库实现(实际场景可以根据具体需要灵活配置分库和分表的数量),默认一个商品库和两个库存库,将商品的库存信息从商品表中独立出来,单独进行分库分表和分桶设计。
- 商品库:在秒杀下单的过程中,主要以读操作为主,比如获取秒杀商品详情信息等。
- 库存库:在秒杀下单的过程中,主要以写操作为主,主要是在下单过程中扣减商品的库存,分摊数据库的写压力。
这里需要注意的是:在我们实现的秒杀系统中,使用了一个商品库和两个库存库来实现商品库存的分库分表和分桶设计,在实际场景下,大家可以根据实际的业务需要,灵活配置分库、分表和分桶的数量。
对商品库存进行分库分表设计时,一个很重要的设计就是对分片键的设计。所谓的分片键就是指定一个字段,通过这个字段将数据路由到对应的数据库和数据表中。在秒杀系统分片键的设计上,尽量将同一个用户的同一次事务中的相关操作路由到同一个数据库中,降低跨库操作的事务成本。
对于商品库存进行分库分表之后的示意图如下图所示。
可以看到,对于商品商品库存来说,分库分表后,会分成一个商品库和两个库存库,其中商品库中存放的是秒杀商品信息,主要在用户抢购下单的业务场景中,以读操作为主。库存库中则存放的是库存分桶数据,每个库存库中存放了三个分桶后的库存信息。这些分库分表的数据,大家可以根据实际需要灵活调整。
对于商品库存的分库分表来说,在实际场景下,可以根据商品id进行分片。也就是说,这里我们选择的分片键是商品的id,同一个商品的库存会被路由到同一个数据库中,不会出现跨数据库的操作。
四、库存分桶设计
在分库分表的基础上,为了进一步提升数据库的并发写性能,可以对商品的库存进行分桶存储。当运营人员在配置库存信息时,可以设置库存的总量和分桶数量,比如,要将1500个商品分配到5个分桶中,则每个分桶中会分得300个商品库存,如下图所示。
这样,每个分桶就能够承担一部分写压力,从而将商品的库存写压力分担出去,使得秒杀系统的库存数据库能够具备更高的并发写能力。
当用户抢购下单时,会根据分桶的数量对用户的id进行取模来定位对应的库存分桶,比如用户的id为10001,目前库存的分桶数量设置为5,则用户抢购下单时,会将当前用户抢购下单时,扣减商品库存的请求路由到分桶1,如下图所示。
用户id为10002的用户抢购下单时,扣减商品库存的请求会被路由到分桶2,如下图所示。
可以看到,用户抢购下单时,扣减商品库存的请求会被路由到不同的分桶中,这样就可以大大降低扣减商品库存的并发写冲突问题,提升扣减商品库存的并发写性能。
这里,还有一个问题就是对商品的库存进行分桶设计后,每个分桶中保存的是当前商品的一部分库存信息,那如何确定商品的总库存呢?其实有两种方案和解决这个问题。
-
第一个方案就是在商品数据表中存储商品的总库存和分桶数量,每个分桶中存储当前分桶的库存信息即可。
-
第二个方案就是在多个分桶中选择一个主分桶用来存储商品的总库存。
-
第三个方案就是在商品数据表中存储商品的总库存和分桶数量,每个分桶中存储当前分桶的商品总库存和当前可用库存。
考虑到对商品库存的并发写操作,以及后续运营人员可能要调整商品的库存信息,这里我们采用的是方案三,也就是在商品数据表中存储商品总库存和分桶数量,每个分桶中存储当前分桶的商品总库存和当前可用库存。如下图所示。
运营人员在设置商品库存时,将商品的总库存和分桶数量存储到商品库,每个分桶中存储当前分桶的总库存和可用库存。
五、分桶库存扣减策略
我们对库存进行分库分表和分桶设计后,在实际场景中,大部分情况下都是路由到不同库存分桶的流量是存在差异的,这就会导致不同库存分桶中的库存剩余量有所不同,比如,id为10001的用户抢购下单时,会被路由到分桶1,id为10002的用户抢购下单时,会被路由到分桶2。
有可能存在的一种情况是:此时分桶1中没有库存了,分桶2中有库存,那对于id为10001的用户来说,该怎么处理呢?此时,我们可以考虑三种方案:
方案1: 设计库存分桶的“争抢”机制,类似Java中的Fork/Join框架,如果当前分桶中的库存不足,则按照一定的规则“争抢”其他分桶中的库存。
方案2: 每个分桶中预留一些冗余的库存,某个分桶库存不足,向其他分桶借用。
方案3: 路由到不同库存分桶的用户看到的剩余库存量不同,如果某个分桶的库存不足,直接向路由到该分桶的用户提示库存不足。
这三种方案各有利弊,经过对秒杀这种场景的权衡,我们最终采用的是方案3。要知道,在秒杀绝大部分场景下,都是大量的用户去抢购有限数量的商品,大部分情况下,所有分桶的库存会被瞬间抢购一空。
那有没有一些极端情况,某些分桶中的库存无法售罄呢?这种情况不能说没有,有可能会出现,但是概率极低。如果确实存在某些分桶中的库存无法售罄的情况,则可以通过人工干预的方式收缩库存分桶,将没有售罄的分桶库存收缩到一个分桶中,这样将相当于库存没有分桶了,后续所有的请求都会被路由到同一个库存分桶中,最终库存都会被售罄。
方案1和方案2在实现上比较复杂,要充分考虑在高并发、大流量场景下如何实现库存的争抢机制,并要考虑不能出现库存超卖和少卖的问题,无疑是在系统的架构设计和实现层面增加了复杂度。
在这种秒杀场景下,大可不必非要实现方案1和方案3,换个角度思考,对于平台和商户来说,保证所有商品都能售罄,并保证数据一致。对于用户来说,完全必要保证库存数据的强一致性,只要保证用户能看到对应分桶中的库存就可以了,完全没必要保证用户看到库存数据的强一致性。
六、缓存与一致性设计
对于商品的库存在数据库层面进行分桶设计是远远不够的,要知道MySQL单行并发写的TPS大概在300~500之间,即使我们对商品进行了分库分表和分桶设计,如果将秒杀系统扣减库存的流量直接打入数据库,哪怕部署了MySQL集群,估计也很难抗下所有的并发流量。所以,我们同样要对商品库存在缓存中进行分桶设计。
商品库存在缓存层面的分桶设计与在数据库层面的分桶设计规则保持一致,例如,运营人员要将1500个商品分配到5个分桶中,则每个缓存分桶和数据库分桶中都会分得300个商品库存,如下图所示。
当用户抢购下单时,同样会根据分桶的数量对用户的id进行取模来定位对应的库存分桶,先预扣缓存分桶中的库存,然后进行下单操作,最后扣减数据库分桶中的库存,比如用户的id为10001,目前库存的分桶数量设置为5,则用户抢购下单时,会将当前用户抢购下单时,扣减商品库存的请求路由到分桶1,如下图所示。
此时,就会将id为10001的用户扣减商品库存的请求路由到缓存分桶1来预扣商品库存,预扣成功就会构建订单数据并保存,最后扣减数据库分桶1中的库存数据。
如果用户的id为10002,目前库存的分桶数量设置为5,则用户抢购下单时,会将当前用户抢购下单时,扣减商品库存的请求路由到分桶2,如下图所示。
此时,就会将id为10001的用户扣减商品库存的请求路由到缓存分桶2来预扣商品库存,预扣成功就会构建订单数据并保存,最后扣减数据库分桶2中的库存数据。
这里,还有一个问题就是如何同步缓存中分桶中的库存数据与数据库分桶中的库存数据呢?其实,设计起来也比较简单,就是运营人员设置或者调整商品库存和分桶数量时,会将计算出来的商品分桶库存写入数据库,写入成功后,更新缓存中的分桶库存数据即可。缓存中的商品分桶库存保持弱一致性,数据库中的商品分桶库存保持强一致性。如下图所示。
当运营人员设置商品库存和分桶数量时,会将商品的总库存和分桶数量存储到商品数据表,每个分桶中存储当前分桶的总库存和可用库存,当数据库分桶中的商品库存数据设置成功后,将其同步到缓存中,缓存中的商品分桶库存规则与数据库中的商品分桶库存规则相同,同时,缓存中的分桶库存数据保持弱一致性,数据库中的分桶库存数据保持强一致性。
七、重置和调整分桶设计
运营人员难免会调整秒杀商品的库存信息,比如原来的商品库存为1500,后来想调整成1000或者2000,所以,我们的秒杀系统要支持运营人员动态的调整秒杀商品的库存,以此对秒杀商品的库存进行实时调整,运营人员调整库存时,会涉及到三种情况,分别如下所示。
(1)第一种情况是调整商品库存,但是分桶数量不变,如下图所示。
(2)第二种情况是商品库存不变,调大或者调下分桶数量,如下图所示。
(3)第三种情况是既调整了商品库存,又调整了分桶数量,如下图所示。
其实,这三种情况在秒杀系统的实现中,本质上就是对商品库存和分桶数量的调整,秒杀系统要支持运营实时调整这些策略。
八、总结
本章,主要对商品的库存进行了分库分表和分桶设计。首先,简单描述了本章的需求。随后,对库存优化的目标进行了阐述。紧接着对库存的分库分表和分桶进行了设计和说明。接下来,对商品库存分库分表和分桶涉及到的缓存数据进行了设计,并对缓存数据与数据库数据的一致性进行了设计。最后,对重置和调整商品库存的分桶数据进行了设计。
好了,今天就到这儿吧,我是冰河,我们下期见~~