一般来说,从数据库层面讲,库存业务会分为两步,第一步是插入一条记录到扣减明细表inventory_detail,第二步是对库存扣减表inventory的一条记录进行扣减,这两步往往是在一个事务中实现的。
数据库业务架构图如下,所有的请求均发往同一个Database。
从上文的架构图不难看出,所有的商品的库存信息都存在单一的表和库里,当商品种类繁多或者业务并发请求暴涨时,单实例的数据库显然会成为容量或者性能瓶颈。该数据库架构一般只是功能性的实现,主要用于微型库存系统或者测试使用。
为了解决单实例存在的容量和性能上限问题,阿里巴巴所有的库存系统在十年前就实现了分库分表设计,主要通过数据的水平拆分实现不同商品的库存扣减请求路由到不同的数据库。基本数据库架构图如下
从上图不难看出,库存扣减表和扣减明细表一般都使用商品id作为片键,这样可以保证满足整个系统在高并发扣减请求的同时,同一商品的库存扣减操作和添加明细操作在同一个事务中实现。如果数据分布和业务请求足够均匀,理论上经过分库分表设计后,整个系统的吞吐量将会是线性的增长,主要取决于分实例的数量。
在电商业务中,商家活动比如秒杀不可避免。秒杀活动会给电商库存系统带来巨大的挑战,尤其体现在数据库层面。因为往往一个商品id对应于数据库的一行记录,所以在DB架构上按照商品维度做了分库分表也是无效的。而更新这行记录时必然需要给这行记录加X锁。热点商品的库存扣减本质上就是热点行更新的能力,高并发的同行更新会造成严重的行锁等待现象,从而导致数据库的threads_running和rt飙升,造成雪崩。在当前的官方mysql中,一般单行更新的QPS在500以内,对于热点商品的秒杀需求,这个量往往是不达标的。
阿里巴巴PolarDB-X数据库团队基于以上场景的需求,针对内核优化,引入了先进的水车模型,在识别出热点SQL后,实现了在内核层面优化处理,相比官方MySQL提高了10倍以上的热点行扣减能力,广泛应用于集团电商库存集群,资金平台,权益发放平台等核心数据库集群。
其主要的核心思想是:针对应用层SQL做轻量化改造,带上"热点行SQL"的标签,当这种SQL进入内核后,在内存中维护一个hash表,将主键或唯一键相同的请求(一般也就是同一商品id)hash到同一个地方做请求的合并,经过一段时间后(默认100us)统一提交,从而实现了将串行处理变成了批处理,让每个热点行更新请求并不需要都去扫描和更新btree。
类似原理,阿里云RDS数据库团队同样在内核层面针对热点行更新做了大量的优化,核心思路为引入SQL语句的排队机制,将可能存在行锁冲突的语句放在一个组内排序,从而减少行锁冲突带来的额外系统开销。Statement Queque和Inventory Hint可以结合使用,不过在事务中,热点行更新必须是该事务的最后一条记录,因为commit on success的机制存在,一旦该SQL执行成功就会自动提交或自动回滚。简单的使用范例如下
begin;
insert into inventory_detail(inventory_id,user_id) values(1,1);
update /*+ ccl_queue_value(1) commit_on_success rollback_on_fail target_affect_row(1) */ inventory set inventory_count=inventory_count+1 where inventory_id=1
更多文档参考inventory hint statement queue
在库存数据库系统中,一般都会在更新库存记录后,写入一条库存扣减明细的流水记录,用于后续可能存在的查询需求。举个例子,在集团的权益发放平台中,库存流水记录主要用于实现库存扣减的幂等性,即同一个用户只能领取一次权益。在系统的实际运行过程中,可能因为一些网络故障等其他原因,当底层数据库的扣减成功以后并没有成功返回给用户时,用户可能会有重试操作,这时就必须避免库存记录的重复扣减情况。所以针对这些情况,应用在设计时会考虑先查询一遍库存流水记录,如果该用户已经领取过该权益,则不再重复扣减,直接返回。为了实现这种强幂等性的需求,库存扣减和插入流水就必须在同一个事务中,满足同时成功或同时失败。
在更大规模,针对单一商品的超高并发扣减的库存集群中,可能基于数据库内核的改造优化还无法满足业务需求。单一商品的超高并发扣减可能会影响到同一数据库实例上的其他商品扣减,同一个数据库实例上也可能存在多个热点商品造成互相影响,这时就需要考虑在业务和数据库架构上再做一次升级,我们引入基于缓存的分桶扣减方案。
下图为该方案数据库架构图,基于缓存的分桶扣减方案的主要思路为
在分桶方案详细落地实现上,需要考虑的细节问题会多很多,比较重要的有以下几点
为了更通俗和直观的描述,缓存集群的一个key就对应于于一个"分桶"。要实现一个基于缓存分桶方案的高扩展性的库存系统,分桶的设计至关重要,比如一个热点商品应该对应多少个分桶,分桶的数量能否根据当前的业务变化做到弹性的伸缩
本文系作者在时代Java发表,未经许可,不得转载。
如有侵权,请联系nowjava@qq.com删除。