资讯动态

Redis库存系统设计

发布时间:2026/8/12 18:04:33 来源:尧图企业网站定制
Task1.1 创建库存服务以后所有库存操作统一入口boolean decreaseStock(Long productId,Integer count);不要让订单Service直接操作库存。原因未来加入RedisLuaMQ分布式锁全部在库存模块扩展。适用场景秒杀、大促、定时抢购逻辑不依赖用户点击。在活动开始前比如 10 点开抢9 点 59 分运维或系统后台主动把 MySQL 里的 10 台库存写进 Redis。物理过程系统启动时执行一段代码强行SET stock:product:1001 10。此时 Redis 里已经有数据了用户甚至还没打开 App。为什么秒杀必须用“预热”而不能懒加载查询后再换成假设 10 点整开抢第 1 个用户点击下单。如果用懒加载那一刻会发生请求 1第一个用户查 Redis - 没数据 - 查 MySQLstock10- 准备写回 Redis。就在请求 1 查完 MySQL、还没来得及写回 Redis 的0.1 毫秒内请求 2第二个用户也进来了查 Redis - 没数据 - 也去查 MySQLstock10。结果两个请求都读到了10都认为自己能买导致超卖。这就是“缓存击穿”的变种——首次请求的缓存穿透。为了绝杀这个问题我们必须在开抢前把数据先塞进去让所有请求一开始就能在 Redis 里找到数据避免大批请求同时扎进 MySQL。高危场景一重复支付幂等性场景描述用户点击“立即支付”网络卡顿他连续点了 3 次。你的系统生成了 3 笔支付流水扣了 3 次钱但只生成了一笔订单。你现在的代码状态无防护。只要前端调 3 次pay后端就会无脑改状态钱会多扣。必须引入的概念幂等性Idempotency。解决手段是引入“支付令牌Token”后端生成一个一次性 token 给前端支付时带着后端校验并删除该 token确保一笔订单只能被支付一次。⏰ 高危场景二订单超时自动取消延迟任务场景描述用户下单占用了库存我们刚聊的 Redis 扣减但 30 分钟没付钱。如果没人管这件商品永远被锁死其他买家买不到。你现在的代码状态没有兜底。除非用户手动取消否则库存永远被占着。必须引入的概念延迟队列Delayed Queue或定时任务Scheduled Task。下单 30 分钟后系统自动检查状态如果还是“待支付”自动改“已取消”并加回库存INCR stock:xxx。这是商城防呆的底线。 高危场景三缓存雪崩 击穿高并发自毁场景描述你做了缓存预热但把 1000 个商品的缓存过期时间都设成了 30 分钟。30 分钟后这些缓存同时失效。瞬间 10 万请求直接穿透到 MySQL数据库连接池耗尽系统直接宕机。你现在的代码状态存在隐患。你现在的getProduct缓存 TTL 是固定的。必须引入的概念TTL 随机抖动TTL Jitter。给每个商品的过期时间加上一个随机值比如 30 分钟 随机 0~5 分钟让它们错峰失效避免集体自杀。 高危场景四操作日志与审计谁动了我的商品场景描述运营后台把 iPhone 价格从 6999 改成了 1999手滑。一分钟后发现赶紧改回来。但财务追责时没人知道“谁改的、什么时候改的、改之前是多少”。你现在的代码状态裸奔。你的updateProduct只改了数据库没有任何记录。必须引入的概念操作日志Operation Log / Audit Log。在修改敏感字段价格、库存、状态时通过 AOP切面拦截记录操作人、操作时间、变更前值、变更后值。这是企业法务和财务的生命线。 高危场景五分布式锁集群环境下的互斥场景描述你的商城为了扛流量部署了 3 台服务器微服务集群。用户下单扣库存时3 台服务器上的代码同时执行你的synchronized单机锁就失效了因为它是只锁当前服务器的内存。你现在的代码状态仅单机安全。你现在写的是单体项目synchronized没问题。一旦以后变成多实例集群就会超卖。必须引入的概念分布式锁Distributed Lock。用 RedisSETNX或 Zookeeper 做一把所有服务器共享的锁保证同一时刻只有一台服务器能扣库存。这也是为何秒杀扣库存必须用 Redis Lua 脚本的原因之一——它天然是原子性的不需要分布式锁就能保证安全。这里有一个巨大问题你应该马上发现这个代码Integer stock get(); 判断; set();安全吗答案❌ 不安全。为什么因为线程Aget 100线程Bget 100A:set 99B:set 99虽然这里没有超卖但是如果购买数量不同A买10100-1090B买20100-2080最终80但是应该70库存丢失。这叫并发更新丢失问题。所以这只是库存业务结构。还不是高并发版本。真正版本下一步Task1.3使用 Redis Lua 实现原子扣减库存改成判断库存 ↓ 扣减库存 ↓ 返回结果全部在 Redis 内一次完成。不过现在先测试基础版本。我们需要先给 Redis 加库存数据。Task1.3商品库存同步 Redis方案1项目启动时同步流程SpringBoot启动 ↓ 查询所有商品库存 ↓ 写入Redis优点简单。适合现在学习项目。缺点如果商品很多启动压力大。方案2商品新增/修改库存时同步例如管理员修改商品库存修改MySQL ↓ 同步Redis这个更接近真实项目。方案3定时同步例如每5分钟MySQL ↓ Redis用于校准。FlashMall目前采用第一阶段启动同步后续商品管理同步这样既简单又符合项目演进。进行 商品同步初始化Spring Boot 应用启动预热在这里用了CommandLineRunner它的特点Spring Boot启动完成后执行。启动流程SpringBoot启动 ↓ Bean加载完成 ↓ 执行run() ↓ 同步库存 ↓ 项目正常运行CommandLineRunner接口Spring Boot 在应用启动完成后会执行run()方法适合做初始化工作。与“流行方案”的衔接这段代码仅仅完成了缓存预热读场景真正的库存扣减写场景仍需要结合我之前提到的Lua 脚本原子扣减。预热只是为后续扣减提供 Redis 初始值扣减逻辑应当独立实现并且确保扣减时使用DECRBY或 Lua 脚本保证原子性扣减成功后通过异步消息队列如 RocketMQ或Binlog最终同步到数据库避免双写不一致。Spring 还提供了另一个接口叫ApplicationRunner它和CommandLineRunner作用完全一样唯一的区别是入参封装CommandLineRunner.run(String... args)接收的是原始字符串数组。ApplicationRunner.run(ApplicationArguments args)接收的是封装好的对象可以方便地获取--keyvalue这种形式的参数。日常使用中绝大多数项目都用CommandLineRunner因为够用了。总结一句话它就是一个“开机自启动”方法负责在应用上线前把准备工作做好。在库存同步里用它思路是对的但要注意别让它干活太久否则会影响应用上线速度。---------------------------------------------------------------------------------------------------------------------------------Lua读作“卢啊”它是一种轻量级脚本编程语言。特点就是短小精悍、执行极快。在Redis里它就像内嵌的一个“小机器人”你可以写几行Lua代码发给Redis让它帮你在内部执行一系列操作。因为你不需要它是复杂的Java或Python越轻量越好。原子性 / 原子操作这是核心在计算机里“原子”意味着不可分割。一个原子操作要么完全执行成功要么完全不执行不存在“执行到一半停下来”的状态。比如银行转账A转给B 100元必须是“A扣100”和“B加100”同时成功不能出现A扣了但B没收到。单线程Redis的核心机制Redis处理客户端请求的网络模块是单线程的。这意味着它就像一个只有一个窗口的银行柜台所有客户请求必须排成一队一个一个处理。这是Lua脚本能实现原子性的物理基础——因为在处理你这笔业务时后面的客户必须老老实实等着没法插队搞乱你的数据网络往返时间RTT Round-Trip Time你的Java代码发一个命令给RedisRedis返回结果这算一次网络来回。如果你的扣减逻辑需要“查一下-判断-扣减”那就要发3次命令就是3次网络来回非常慢。超卖库存只剩1件但因为有2个人同时下单系统查库存时都查到“还剩1件”于是都扣减成功最后卖出2件这就是超卖。Lua脚本就是为了零超卖。幂等令牌Idempotent Token“幂等”指的是同一操作执行多次结果和执行一次完全一样。令牌就是一个唯一ID比如订单号。当你扣库存时先把“订单号”记在Redis里。如果网络重试第二次请求看到这个令牌已存在就直接返回“已处理”避免把库存扣两次。Lua脚本实现原子操作的逻辑就是把“查、判、改”三个动作捆绑成一条不可拆分的命令利用Redis单线程排队机制在内存中瞬间完成以此彻底消灭超卖再通过异步MQ将最终结果同步给数据库。高并发扣减Lua脚本上场用户下单传入productId1001, qty1, orderTokenuuid。Java调用EVAL脚本内部逻辑如下检查令牌EXISTS idem:uuid—— 这单扣过没扣过就直接返回“重复请求成功”。查询库存GET stock:1001—— 得到当前剩余量。比较判断if (current qty)—— 不够直接返回0失败。执行扣减DECRBY stock:1001 1—— 够内存里直接减1。记录令牌SETEX idem:uuid 604800 1—— 记录这单已处理7天后自动过期。返回成功return 1。因为整个过程在Redis内部耗时不到1毫秒且不经过网络多次交互所以吞吐量极高2.8万 QPS。RedisScriptConfig.java是专用于加载Lua脚本的配置类。它的作用很简单但很关键把lua/decrease_stock.lua这个脚本文件加载成一个Spring管理的Bean可注入对象方便你的StockService随时拿去使用。Configuration标记这个类是一个配置类。Spring在启动时会把里面所有带Bean的方法执行一遍把返回的对象放进IOC容器大仓库里管理。--------------------------------------------------------------------------------------------------------------------------------为什么不用Transactional解决因为Spring事务只能控制MySQL例如orders表 order_item表 product表但是Redis不是它管理的事务。所以Transactional 不能回滚Redis这就是分布式事务问题的开始。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价