电商库存系统设计案例详解(下)
正文
电商库存系统设计案例详解(下)
6 纯Redis扣减方案
Redis单线程模型,具原子性。当有多个客户端给Redis发命令,Redis会按接收顺序串行执行。对于还未被调度的命令,则放在队列里排队。
库存扣减为保证数据并发安全,要求原子性,而 Redis 正好满足扣减类需求。
6.1 基于的Redis库存扣减
6.2 Redis数据模型
剩余库存(KV结构)
K = sku_leaved_amount_{sku_id} V = 剩余的库存数值
流水(hash结构)
K = inventory_flow_{sku_id} hash—K = 订单明细id(不同业务场景的全局性id,用来做幂等控制) hash—V = 本次购买的数量
购物车下单,多个sku批量扣减,需按单个sku循环发起Redis调用。但多个Redis命令无法保证原子性。可采用lua,将这些命令打包到一个脚本,作为一个命令发给Redis执行,保证原子性。
lua 是一个类似 JavaScript、Shell 等的解释性语言,它可以完成 Redis 已有命令不支持的功能。用户在编写完 lua 脚本之后,将此脚本上传至 Redis 服务端,服务端会返回一个标识码代表此脚本。在实际执行具体请求时,将数据和此标识码发送至 Redis 即可。Redis 会和执行普通命令一样,采用单线程执行此 lua 脚本和对应数据。
6.3 Lua执行流程
批量扣减,是对单个扣减的循环调用,所以这里只讨论单次扣减的处理步骤:
- 1. 先据【订单明细id】查询【扣减流水】,是否已操作过,做幂等性校验
- 2. 再查询sku的剩余库存,并根据 【下单购买数】 做校验,只要有一个sku 数量不足,则返回失败
- 3. 修改所有sku的缓存中的剩余库存数
- 4. 缓存中插入扣减流水记录
当Redis扣减成功后,应用程序再将此次扣减 异步化 保存到MySQL。
6.4 Redis方案利弊分析:
- Redis高性能 ACID 少卖
- 为避免 少卖, 纯缓存方案 需做大量对账、异常处理的设计,系统复杂度增加很多
- 纯缓存方案 适合一些高并发、大流量场景,但对数据准确度要求不是特别苛刻的业务场景
6.5 风险
上述 Lua脚本 把多条命令打包在一起,虽保证原子性,但不具备 事务回滚。如库存扣减成功,此时 Redis宕机 ,扣减流水并没有插入成功,应用程序认为本次 Redis 调用失败,前台给用户反馈错误提示,但已扣减的数量不会回滚。当Redis故障修复后,再次启动,此时恢复的数据已不一致。需要结合 Redis 和 数据库 做数据核对check,并结合扣减服务的日志,做数据的增量修正。
9 分库分表的扣减方案
上面提到的数据库方式基于 单库单表,虽借助 ACID 保证数据一致性,但是单台MySQL并发有限,如何提升性能?
除了 纯缓存 化方案外,还可考虑将 库存表 进行 水平拆分 ,分摊洪峰压力。
假如库存表QPS 要求 1.6w,经过拆分成16张表后,若数据分布均匀,每个物理表预计处理 1000 QPS,完全处于MySQL单实例的承载范围之内。
拆分后,单表数据量减少很多,假如分表前有一个亿数据,分表后每张表不到1千万,索引查询性能也会提高。
同一次扣减业务,库存扣减和插入流水要放在同一个分库,通过事务保证一致性。若数据分布和业务请求够均匀,经过分库分表后,整个系统的吞吐量是线性增长,主要取决于分表的实例数量。
10 其他扣减方案
1、如果某个sku_id的库存扣减过热,单台实例支撑不了( mysql官方测评:一般单行更新的QPS在500以内 ),可以考虑将一个sku的大库存拆分成N份,放在不同的库中(也就是说所有子库的库存数总和才是一件sku的真实库存),由于前台的访问流量非常大,按照 均分原则 ,每个子库分到的流量应该差不多。上层路由时只需要在 sku_id 后面拼接 一个范围内的随机数 ,即可找到对应的子库,有效减轻系统压力。
2、单条sku库存记录更新过热,也可以采用批量提交方式,将多次扣减累计计数,集中成一次扣减, 从而实现了将串行处理变成了批处理 ,也可以大大减轻数据库压力。
3、引入 RocketMQ 消息队列,经过前置校验后,如果有剩余库存,则把创建订单的操作封装成消息发送给MQ,订单系统从RocketMQ中以特定的频率消费,创建订单,该方案有一定的延迟性。
文章转载自公众号: JavaEdge