秒杀库存扣减可以用redis原子自增么

答案是:可以,但仅靠简单的 INCR/DECR 指令是不够的,必须配合逻辑判断或 Lua 脚本,并解决"数据库一致性"问题。

使用 Redis 处理库存扣减是高并发场景(如秒杀)中最通用的方案。以下是详细的分析、实现方式以及必须注意的坑。


一、 为什么可以用 Redis?

Redis 的 INCR(自增)、DECR(自减)、INCRBY 等操作是**原子性(Atomic)**的。

这意味着在同一时刻,无论有多少个并发请求,Redis 都会一个个排队执行,不会出现"读取到库存是1,两个线程同时减1,结果还是0"这种竞态条件(Race Condition)。

二、 实现方案

方案 1:直接使用 DECR(最简单,但有小缺陷)

通常库存是减少,所以用 DECR(或者 INCRBY key -1)。

逻辑:

  1. 初始化库存:SET stock:1001 10
  2. 请求进来执行:DECR stock:1001
  3. 判断返回值:
    • 如果返回值 ≥\ge≥ 0:扣减成功,允许下单。
    • 如果返回值 < 0:库存不足,扣减失败。

缺陷:

如果库存是 0,瞬间涌入 1000 个请求,库存会变成 -1000。虽然业务逻辑上你可以判断返回值 < 0 就拒绝,但 Redis 里的数据很难看,且如果后续有"回滚库存"的操作,逻辑会变得复杂(比如你是要加回 1 变成 -999 还是怎样?)。

方案 2:使用 Lua 脚本(推荐方案)

为了避免库存变成负数,并保证"读取库存"和"扣减库存"这两个动作的原子性,推荐使用 Lua 脚本。

Lua 脚本逻辑:

lua 复制代码
-- keys[1] 是商品库存 key
local key = KEYS[1]
-- 获取当前库存
local stock = tonumber(redis.call('get', key))

-- 判断库存是否充足
if (stock and stock > 0) then
    -- 扣减库存
    redis.call('decr', key)
    return 1 -- 成功
else
    return 0 -- 失败
end

优势:

  • 原子性: Redis 执行 Lua 脚本时是原子的,中间不会插入其他命令。
  • 数据干净: 库存减到 0 后就不会再减了,不会出现负数。

三、 必须解决的核心问题

仅在 Redis 里扣减库存是不够的,你还必须考虑整个交易流程的闭环:

1. 数据库与 Redis 的一致性(最重要)

Redis 只是抗并发的"挡箭牌",最终数据必须落到 MySQL/Oracle 等数据库中。

  • 问题: Redis 扣减成功了,但后续代码写数据库(创建订单)失败了,或者服务崩了,怎么办?
  • 解决方案:
    • 同步写入(简单场景): Redis 扣减成功 -> 开启数据库事务 -> 写入订单 -> 扣减数据库库存 -> 提交事务。如果数据库操作失败,需要Catch异常并回滚 Redis 库存INCR 回去)。
    • 异步写入(高并发场景): Redis 扣减成功 -> 发送消息到 MQ(RabbitMQ/Kafka/RocketMQ) -> 消费者慢慢消费消息并写入数据库。这能最大程度抗压,但逻辑最复杂(需要处理消息丢失、重复消费等问题)。
2. 库存回滚(超时/取消)

用户抢到了库存(Redis 减了),但是:

  • 用户一直不付款(超时)。
  • 用户主动取消订单。
  • 支付失败。

解决方案:

必须有一个机制监听这些事件(如订单超时延迟队列),一旦发生,必须调用 Redis 的 INCR 把库存加回去,否则会导致"少卖"。

3. 缓存预热

秒杀开始前,必须先把数据库里的库存数量同步加载到 Redis 中(预热)。如果 Redis 里没有 key,直接 DECR 会报错或变成 -1。

4. Redis 宕机风险

如果 Redis 挂了,整个秒杀功能就不可用了,且 Redis 里的库存数据可能丢失(取决于持久化策略 AOF/RDB)。
解决方案: 使用 Redis Sentinel 或 Cluster 高可用架构;在代码层面做降级处理(比如 Redis 挂了直接查数据库,但要做好数据库限流)。


四、 总结建议

可以直接用 INCR/DECR 操作吗?

可以,但建议用 Lua 脚本 代替简单的指令。

完整的架构建议:

  1. 预热: 活动开始前将库存写入 Redis。
  2. 扣减: 请求到达 -> 运行 Redis Lua 脚本扣减库存。
    • 失败 -> 返回"已售罄"。
    • 成功 -> 进入下一步。
  3. 下单:
    • 方案 A(强一致): 写入数据库订单,如果失败则回滚 Redis。
    • 方案 B(高性能): 发送 MQ 消息,异步创建订单。
  4. 回滚: 监听未支付订单,超时后触发 Redis INCR 回补库存。

这种方案能抗住上万甚至十万级的 QPS,是业界标准的抗超卖方案。

相关推荐
找不到、了2 小时前
MySQL 索引下推(ICP)的实战,彻底提升查询性能
数据库·mysql
b***67642 小时前
Springboot3 Mybatis-plus 3.5.9
数据库·oracle·mybatis
kitty_hi2 小时前
mysql主从配置升级,从mysql5.7升级到mysql8.4
linux·数据库·mysql·adb
王宪笙5 小时前
Qt之数据库使用示例
数据库·qt
q***42825 小时前
Redis 设置密码(配置文件、docker容器、命令行3种场景)
数据库·redis·docker
Y***K4345 小时前
后端缓存策略设计,多级缓存架构实践
缓存·架构
运维行者_6 小时前
网站出现 525 错误(SSL 握手失败)修复指南
服务器·网络·数据库·redis·网络协议·bootstrap·ssl
fruge6 小时前
openGauss数据库实操过程:从环境搭建到连接配置,第三方软件进行数据库管理
数据库·oracle
5***79006 小时前
后端服务监控面板,关键业务指标
数据库