秒杀库存扣减可以用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,是业界标准的抗超卖方案。

相关推荐
wertyuytrewm3 分钟前
自动化与脚本
jvm·数据库·python
Lyyaoo.4 分钟前
Spring Boot日志
spring boot·缓存·单元测试
Hello.Reader5 分钟前
PySpark DataFrame 快速入门创建、查询、分组、读写、SQL 实战一篇讲透
数据库·sql·spark
无籽西瓜a7 分钟前
Docker 环境下 Redis Lua 脚本部署与执行
redis·docker·lua
qq_417695058 分钟前
Python深度学习入门:TensorFlow 2.0/Keras实战
jvm·数据库·python
疯狂成瘾者9 分钟前
Redis 实用学习清单
redis·学习
只能是遇见11 分钟前
ERROR 1524 (HY000) Plugin ‘mysql_native_password‘ is not loaded
android·数据库·mysql
七夜zippoe11 分钟前
消息队列选型:Kafka vs RabbitMQ vs Redis 深度对比
redis·python·kafka·消息队列·rabbitmq
番茄去哪了13 分钟前
从0到1独立开发一个论坛项目(一)
java·数据库·oracle·maven
API开发18 分钟前
一个MCP操作所有的数据库
数据库·api·api接口·apisql·mcp·mcpserver·openclaw