背景
针对库存操作,宗旨:绝不超卖(存在资损、造成客诉、用户体验差)、尽量避免少卖(相对资损)。
在明星直播、大促、秒杀等高并发场景下,数据库的性能会变得非常差,传统的分库分表变得很鸡肋,因为大流量冲击的都是少量商品,最后还是会针对一条Sku库存数据做update操作,行锁会使得这些命令排序执行,导致慢SQL。(数据库用的MySQL架构,数据隔离级别为RC,提升并发度,降低死锁概率,另外表增加Version乐观锁,做并发情况的补偿操作)
为了解决这种场景,域内解决方案通常有三种:
- 库存数据水平拆分,打散热点。
- 限流排队扣减,增加扣减异步回调能力。
- 读写分离,同步缓存读,异步数据写,域内维护数据一致性。(采用)
新旧方案
旧方案
以前的方案,简单粗暴:
- 加分布式锁,防止重复扣减库存。
- 拆单后排序,防止高并发下数据库死锁。
- 同步扣减库存
以前方案的可行性在于自研的数据库引擎通过Inventory Hint技术实现热点数据更新的并发,改造后的MySQL在相同的配置下,相比传统的MySQL,读写性能提升了 2~4 倍。
UPDATE SKU_INVENTORY_TABLE
SET inventory = inventory - 1
WHERE sku_id = 1 and inventory > 0;
UPDATE /*+ COMMIT_ON_SUCCESS ROLLBACK_ON_FAIL TARGET_AFFECT_ROW(1)*/ SKU_INVENTORY_TABLE
SET inventory = inventory - 1
WHERE sku_id = 1 and inventory > 0;
Tip:
- COMMIT_ON_SUCCESS:语句成功即提交事务。
- ROLLBACK_ON_FAIL:语句失败则回滚事务。
- TARGET_AFFECT_ROW(NUMBER):语句影响指定行数才成功,否则失败。
当使用COMMIT_ON_SUCCESS等hint标记了一条SQL之后,就相当于告诉MySQL内核,这行可能是热点更新。于是,MySQL的内核层就会自动识别带此类标记的更新操作,在一定的时间间隔内,将收集到的更新操作按照主键或者唯一键进行分组,这样更新相同行的操作就会被分到同一组中。
为了进一步提升性能,又做了主备执行引擎,在主执行引擎收集完毕准备提交时,备执行引擎立即开始收集更新操作,主备无缝切换,提升处理效率。
热点数据在存储引擎内核分组之后,针对三个维度做了性能优化:
- 申请行锁等待的时间
- 减少B+数据索引遍历
- 减少事务提交次数
踩坑:在组合商品的场景,会涉及到拆单,此时可能会出现死锁问题,为了避免产生死锁,在进行DB扣减之前,可在SkuID维度做排序操作,打破锁的循环依赖。
新方案
虽然针对数据库引擎做了性能优化,但是在高并发的场景下,性能还是有所欠缺,所以在自研数据库引擎的基础上,做了一版新的方案。
库存扣减总共有三点:
- 防重(分布式锁|防重表)
- 防超卖(inventory > 0)
- 扣减库存(inventory = inventory -1)
设计思想:
- 用redis去做防重和防超卖的操作
- 库存操作异步落库
- 通过库存一致性引擎保证Redis和MySQL数据一致性
超卖校验
数据库:insert 主键;缓存:setnx key。
超卖校验包括两个操作:防重 + 扣减,防重将分布式锁方案调整为防重码方案,通过Pipeline命令将防重和Redis库存扣减操作合并为一个操作执行,这样可以大大降低Redis的RTT,提升性能。
超卖校验过程中,要注意防重操作和扣减操作的时序,如果防重在扣减操作之前,在极端情况下(机器宕机)可能会出现超卖现象。除此之外,还要注意Redis集群的可用性,针对热点防刷做本地限流即可。
扣减库存的流水信息也可以在一个管道进行记录,方便后续支持库存数据一致的“对账”功能,流水信息数据结构:skuId+订单ID、库存操作类型(+/-)、时间。
商品下架时可以清除商品的流水记录,也可以设置指定的缓存有效期。
库存变更
在发品时,需要将产品的库存同步双写至Redis和MySQL中,但是如果运营变更正在售卖的产品/商品的库存时,会存在数据混乱的问题(并发),针对这个问题,可通过MySQL行锁和Redis事务来保证数据一致性。
任务调度引擎
在超高并发下,优先消息队列存在瓶颈,可将扣减主任务通过MQ异步写入MySQL中,热卖品和库存高优品对任务增加优先级,降低热卖品对库存一致性引擎的影响。
库存一致性引擎
库存每天变动千万次,为了性能考虑,做了以下方案:
- 只针对低于一定水位线的商品,启动库存一致性引擎进行库存一致性对比,考虑到消息队列/任务调度的影响,在一定时间内两个数据源的流水信息经过多次对比,只要有一次判断为缓存-DB相等,就认定当前库存数据是一致的。
- 如果库存充足,在一定时间内进行一次对比,可以根据实际情况在配置中心,做动态调整。