(1)社区电商系统库存模块的设计要求
由于该库存模块可以支持高性能的并发读写,因此需要支持对商品库存进行多分片写入和读取处理(分片一般等于节点),需要提供单个分片库存不足以扣减时的合并库存功能,以及需要提供操作商品入库时的库存渐进性写入缓存的实现。
也就是对于热点库存能够实现缓存分片。
进行库存分片后,如果遇到单个分片库存不足可以进行合并扣减库存。库存落库之后,库存数据以渐进式的方式写入到缓存里。
(2)社区电商系统库存模块功能分析
主要会有两个系统会操作库存的数据,即商品系统 + 订单系统。首先是商品系统会对商品库存进行入库和出库,然后是订单系统会对商品库存进行购买时的扣减和退款时的返还,所以商品系统和订单系统会影响库存数据变更。
一般而言,库存的数据都是要放到Redis里去的。因为这可以方便后面进行高并发活动如大促和秒杀,而大促和秒杀活动往往会对库存进行高并发读和写,所以库存数据是典型的读多写多数据。
(3)商品系统处理库存出⼊库时影响库存数据的设计
商品中⼼调⽤库存中⼼,添加商品库存信息时,一般会涉及到3个表的数据。第⼀个表是库存表,需要更新相关库存信息(第⼀次要新增)。第二个表是库存变更记录表,需要记录当次的库存变更记录。第三个表是库存变更明细表,需要记录当次的库存变更明细。
库存初始化到库存分⽚中的时候,采⽤渐进性同步的⽅式来进行同步。否则如果采用⼀次性同步的方式,假如过程中失败了就会造成库存不均匀。
例如每个库存分片(节点)需要写100个库存:
说明一:如果一次性同步,那么就是遍历一次节点,每个节点写100个库存。当遍历到某个节点却写入失败时,写入失败的库存数要重新遍历节点写入,这时候就会造成节点库存分配不均匀了。
说明二:如果渐进性同步,那么就是分多次遍历节点,已做好某次遍历节点写入库存就存在节点写入失败情况的准备了。比如每个节点写100个库存,那么就遍历节点10次,每次写10个库存,这样就可以尽量避免节点库存不均匀了。
说明三:当同步过程中出现异常导致同步中断,此时就发送⼀条消息给MQ做补偿。MQ补偿时,会扣减掉已同步缓存的数量,只同步剩余数量。补偿消息要避免重复消费,默认收到就只处理⼀次,异常则再次发送新的消息补偿缓存。
(4)订单系统扣减和返还库存时影响库存数据的设计
说明一:进⾏下单、缺货、取消、⻛控等业务场景时,会涉及对库存的操作变更。
说明二:每个商品SKU都会维护⼀个key,每次操作一个SKU库存时,这个key都会自增+1。通过这个key值对分⽚机器数取模,就可以选择其中⼀台机器进⾏库存扣减。
说明三:当被访问的分⽚库存不能完成此次扣减,则前往下⼀个分⽚继续尝试,直到所有分⽚都不⾜以扣减此次库存以后,则开始尝试合并库存扣减。
说明四:合并扣减⾸先会从每个分⽚尝试扣减,默认扣减分⽚的最⼤剩余库存。当分⽚内的库存可购买数量⼩于用户需要购买数量时,就从lua脚本中返还本次分⽚的实际扣除数量,并记录起来。当全部扣除后还是失败或中途扣除过程发⽣异常时,可以进⾏回滚。
注意:Redis能执行lua脚本,一段lua脚本可以作为一个整体,这样将多条Redis命令写入lua,就可以实现事务的原子性。
(5)查看商品SKU库存的设计
每次查看商品SKU库存时,会去各个分⽚获取分⽚库存,然后合并才返回。
2.库存缓存分片和渐进式同步方案
(1)库存缓存分片方案避免瞬时流量倾斜
(2)渐进性同步方案避免节点库存不均
(1)库存缓存分片方案避免瞬时流量倾斜
库存数据写入单节点缓存后:如果遇到大促活动如秒杀,需要瞬时高并发去操作一个商品SKU的库存时,就会导致对缓存集群里某个Redis节点造成过大压力,造成瞬时流量倾斜。
所以为了解决瞬时流量倾斜问题,往往采用缓存分片。比如商品SKU库存有100个,这时可以把这100个库存拆分为10个分片。假如Redis集群有5个节点,此时分10个分片,那么每个节点就有2个分片。不过库存分片的数量一般设置成与Redis节点数量一样(分片一般等于节点)。这样出现库存的瞬时高并发操作时,就可以将库存扣减请求分到多个节点上。这样高并发流量就能均匀负载到各个节点上去,避免对单个节点写压力过高。
(2)渐进性同步方案避免节点库存不均
在分配库存到分片缓存时,采用渐进性分配库存的方式。例如每个库存分片(分片一般等于节点)需要写100个库存。
如果一次性同步,那么就是遍历一次节点,每个节点写100个库存。当遍历到某个节点却写入失败时,写入失败的库存数要重新遍历节点写入,这时候就会造成节点库存分配不均匀了。
如果渐进性同步,那么就是分多次遍历节点,已做好某次遍历节点写入库存就存在节点写入失败情况的准备了。比如每个节点写100个库存,那么就遍历节点10次,每次写10个库存,这样就可以尽量避免节点库存不均匀了。
但是无论是一次性同步(刚性同步)或者是渐进性同步(柔性同步),都需要考虑将数据从数据库同步到缓存的过程中是有可能出现失败的。失败时就需要基于MQ来做补偿,把没同步成功的库存补偿回去。
3.基于缓存分片的下单库存扣减方案
(1)缓存分片下如何选择节点
(2)如何通过轮询选择Redis节点
(3)如何处理库存分片的库存不足问题
假设一个商品SKU有10000个库存,拆分为10个库存分片,每个分片1000,这10个库存分片会分散在多个Redis节点里。那么用户下单需要扣减商品库存时,到底去哪个Redis节点进行库存扣减。
(1)缓存分片下如何选择节点
此时有两种选择Redis节点的方案:可以通过随机的方式选出一个Redis节点来进行库存扣减,也可以通过轮询的方式选出一个Redis节点来进行库存扣减,这里会通过轮询的方式来选择Redis节点去进行库存扣减。
(2)如何通过轮询选择Redis节点
首先商品SKU需要维护一个访问key,然后每次扣减库存时都对这个访问key进行自增。接着根据这个自增值对库存分片数量进行取模,通过取模确定一个库存分片。然后再根据这个库存分片,确定该分片是在哪个Redis节点里的。这样就可以将库存扣减请求发送到那个Redis节点里进行处理了。
(3)如何处理库存分片的库存不足问题
如果轮询出的某个库存分片没库存或者库存不够了,比如当前库存分片还有1个库存,但这次用户请求需要扣减3个库存。明显当前库存分片不足以扣减,此时就可以尝试下一个库存分片来进行扣减。如果下一个库存分片也不足以扣减,那么继续下一个库存分片来进行扣减。如果最后发现每个库存分片都无法单独进行扣减,那就合并库存再进行扣减。合并库存进行扣减时,会对多个库存分片里的库存逐一扣减。
4.商品库存设置流程与异步落库的实现
商品中心操作库存会分为3步:
第一步:对库存设置进行异步落库
第二步:落库的库存数据会被同步到缓存分片里,并且是渐进式写入的
第三步:如果同步到缓存分片过程出现问题,需要基于MQ进行补偿
比如采购系统发起商品采购,然后供应商把商品发到仓库里。接着仓库操作员对商品入库,商品进行入库时会发送商品入库事件消息。库存系统可以监听并消费该事件,然后异步触发商品库存的设置和初始化。
如果商品系统创建商品时就设置了商品库存,这时就可以同步调用库存系统的接口,去执行商品库存初始化设置操作。商品库存初始化时会更新库存,这时对DB的操作也是通过MQ异步进行。
也就是商品库存初始化、商品库存入库、购物车库存扣减,都是异步写库,但是写缓存是同步的。
@Service
public class InventoryServiceImpl implements InventoryService {...//商品库存入库@Overridepublic void putStorage(InventoryRequest request) {//1.异步更新数据到DBsendAsyncStockUpdateMessage(request);//2.同步执行库存均匀分发到缓存executeStockLua(request);}//发送库存变更的消息private void sendAsyncStockUpdateMessage(InventoryRequest request) {Long startTime = System.currentTimeMillis();//发送消息到MQdefaultProducer.sendMessage(RocketMqConstant.INVENTORY_PRODUCT_STOCK_TOPIC, JsonUtil.object2Json(request), "COOKBOOK库存变更异步落库消息");log.info("商品编号:" + request.getSkuId() + "发送mq,总计耗时" + (System.currentTimeMillis() - startTime) + "毫秒");}
}@Configuration
public class ConsumerBeanConfig {//配置内容对象@Autowiredprivate RocketMQProperties rocketMQProperties;//商品库存扣减变更的topic@Bean("inventoryStockUpdateTopic")public DefaultMQPushConsumer inventoryStockUpdateConsumer(InventoryStockUpdateListener inventoryStockUpdateListener) throws MQClientException {DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(RocketMqConstant.PUSH_DEFAULT_PRODUCER_GROUP);