news 2026/10/23 3:00:19

高并发秒杀库存超卖

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
高并发秒杀库存超卖

高并发秒杀库存超卖

一、业务背景

电商平台举办秒杀活动,初始库存为 1000 件。

技术架构:

  • 后端:Spring Boot
  • 缓存:Redis
  • 数据库:MySQL
  • 消息队列:MQ
  • 业务流程:用户请求 → 库存扣减 → 创建订单 → 异步处理后续业务

活动过程中出现以下问题:

  1. 活动结束后,有效订单数量超过库存。
  2. 用户请求超时后重试,产生重复订单。
  3. Redis 与 MySQL 库存不一致。
  4. 订单创建失败,但已经扣减的库存没有及时恢复。
  5. 服务创建订单后、更新库存预占状态前宕机,导致业务状态不明确。
  6. 库存确认任务与超时释放任务并发执行,可能造成错误释放。

核心目标:

保证库存不超卖、订单不重复、库存操作可恢复,并让异常情况下的数据最终达到一致。


二、问题一:为什么 Redis 扣库存仍然可能超卖?

1. Redis 是共享的,不代表业务逻辑天然原子

假设库存剩余 1 件,两个请求同时执行:

请求 A:读取库存 = 1 请求 B:读取库存 = 1 请求 A:判断库存充足 请求 B:判断库存充足 请求 A:扣减库存 请求 B:扣减库存

如果读取、判断、扣减是独立操作,两个请求就可能都认为自己能够购买。

2. 使用 Redis Lua 脚本实现原子扣减

将库存检查和扣减放进同一个 Lua 脚本中:

localstock=tonumber(redis.call('GET',KEYS[1])or'-1')localquantity=tonumber(ARGV[1])ifstock<0thenreturn-1endifstock<quantitythenreturn0endredis.call('DECRBY',KEYS[1],quantity)return1

返回值含义:

  • 1:扣减成功。
  • 0:库存不足。
  • -1:库存键不存在,需要按业务策略处理。

Redis 会原子执行脚本中的操作,因此多个请求不会在脚本执行过程中交错修改同一个库存键。

注意:这只保证 Redis 内部的扣减操作原子执行,不代表 Redis、MySQL、订单和 MQ 之间自动保持一致。

此外,真实生产环境还需要防范缓存初始化错误、缓存重建时覆盖并发扣减、Redis 故障恢复以及库存键过期等问题。


三、问题二:如何避免重复创建订单?

1. 为什么会出现重复订单?

例如:

用户发起购买请求 ↓ 服务器成功创建订单 ↓ 服务器返回响应前宕机 ↓ 用户没有收到成功响应 ↓ 用户重试 ↓ 系统再次创建订单

客户端没有收到成功响应,不代表服务端没有成功执行操作。

因此,不能简单地把超时等同于失败。

2. 使用业务唯一标识实现幂等

每次购买请求生成一个稳定的业务请求标识,例如:

request_id = REQ_20261021_000001

订单表增加唯一约束:

CREATETABLEorders(idBIGINTPRIMARYKEYAUTO_INCREMENT,request_idVARCHAR(64)NOTNULL,user_idBIGINTNOTNULL,product_idBIGINTNOTNULL,statusVARCHAR(32)NOTNULL,UNIQUEKEYuk_request_id(request_id));

这里的request_id仅用于演示,生产环境应使用可靠的唯一标识生成方案。

当相同请求被重复执行时:

  1. 首次请求创建订单。
  2. 重试请求尝试创建同一笔订单。
  3. 数据库唯一约束阻止重复记录。
  4. 系统查询已存在的订单,确认真实状态并返回对应结果。

不能仅依靠“先查询订单是否存在,再决定是否创建”,因为两个并发请求可能同时查询到订单不存在。

3. 必须区分两类请求

  • 同一业务请求重试:应返回原请求的处理结果,不应重复创建订单或重复扣减库存。
  • 用户主动再次购买:是否允许创建新订单,应根据业务规则判断。

因此,幂等键必须与业务语义对应,不能简单地使用用户 ID 作为唯一键,否则可能错误限制用户的后续购买。

关键结论:应用层幂等判断与数据库唯一约束配合,才能可靠地防止重复订单。


四、问题三:如何设计完整的库存预占流程?

不能把“扣减库存”和“创建订单”看成一个天然的原子操作,因为 Redis、MySQL 和 MQ 之间通常不存在统一事务。

可以引入库存预占记录,跟踪每次购买的处理状态。

1. 推荐的业务流程

用户提交购买请求 ↓ 生成唯一 request_id ↓ 检查该请求是否已经处理 ↓ Redis 原子检查并预占库存 ↓ 持久化库存预占记录 ↓ 创建订单 ↓ 确认库存预占 ↓ 返回购买成功

其中,实际执行顺序需要结合库存预占记录的持久化方式设计。

尤其需要注意:如果先扣减 Redis 库存,再持久化预占记录,那么服务可能恰好在两步之间宕机,导致出现无法追踪的库存扣减。

因此,不能只在 Redis 中保存预占状态。应设计持久化的业务记录、可靠的执行顺序和恢复机制,保证每笔库存预占都能被追踪。

2. 库存预占状态机

可以定义以下状态:

状态含义
PROCESSING库存已预占,业务尚未完成
CONFIRMED订单已成功,预占已确认
RELEASED预占已释放,库存可以重新使用

状态流转:

PROCESSING ──────→ CONFIRMED │ └──────────→ RELEASED

正常情况下:

预占库存 ↓ 创建订单成功 ↓ PROCESSING → CONFIRMED

创建订单失败且确认没有成功订单时:

PROCESSING ↓ 确认订单未创建成功 ↓ 释放库存 ↓ PROCESSING → RELEASED

关键原则:同一笔预占只能确认一次,或者释放一次,不能既确认又释放。


五、问题四:服务宕机后如何恢复?

这是整个场景最重要的故障恢复问题。

假设:

  1. A 成功预占库存。
  2. A 的订单成功写入 MySQL。
  3. 服务还没来得及将预占状态更新为CONFIRMED,就宕机了。
  4. 服务重启后,发现预占状态仍然是PROCESSING。

此时不能直接创建新订单,也不能直接释放库存。

1. 恢复任务的正确思路

扫描长期处于 PROCESSING 的预占记录 ↓ 查询对应 request_id 的订单状态 ↓ ┌─────┴─────┐ ↓ ↓ 订单已存在 订单不存在 ↓ ↓ 核对订单状态 判断是否仍可继续处理 ↓ ↓ 确认预占 重试创建订单 ↓ ↓ CONFIRMED 根据结果决定确认或释放

实际生产环境中,订单不存在不一定意味着创建过程没有正在执行的事务。

因此,恢复任务还需要考虑:

  • 订单事务是否已经提交。
  • 原请求是否仍在执行。
  • 是否存在重复执行的恢复任务。
  • 当前预占是否已经被其他任务确认或释放。

对于结果暂时无法确定的请求,应继续查询、重试或进入人工处理流程,而不是贸然释放库存。

2. 为什么不能只靠服务重启后的对账?

对账适合发现和修复不一致,但不能替代正常业务流程中的状态管理。

如果所有恢复都依赖重启后的对账,可能导致:

  • 库存长时间被占用。
  • 用户等待时间过长。
  • 异常无法及时发现。
  • 重复订单或错误释放在对账前持续存在。

更可靠的做法是组合使用:

  1. 持久化预占记录。
  2. 定时扫描超时记录。
  3. 根据订单真实状态恢复业务。
  4. 对失败操作进行有限重试。
  5. 对无法自动处理的异常告警或人工介入。
  6. 定期对账,修复残留的不一致。

六、问题五:如何防止确认任务和释放任务发生并发冲突?

假设两个任务同时处理同一笔预占:

  • 确认任务发现订单已经成功,准备确认库存。
  • 超时补偿任务认为预占已经超时,准备释放库存。

如果没有并发控制,就可能发生一边确认订单,一边释放库存的错误。

1. 加锁是一种思路,但不是完整答案

可以使用分布式锁协调多个任务,但必须考虑:

  • 锁的粒度是否合理。
  • 服务宕机后锁如何释放。
  • 锁过期后旧任务是否还会继续执行。
  • 多个服务实例是否都遵守同一套锁规则。

如果对所有库存补偿使用一把全局锁,虽然实现简单,却可能造成大量无关业务互相阻塞。

2. 使用条件更新控制状态流转

可以让确认任务和释放任务都尝试从PROCESSING更新状态。

确认操作:

UPDATEinventory_reservationSETstatus='CONFIRMED'WHERErequest_id=?ANDstatus='PROCESSING';

释放操作:

UPDATEinventory_reservationSETstatus='RELEASED'WHERErequest_id=?ANDstatus='PROCESSING';

数据库会协调并发更新同一行的竞争。两个任务中,只有一个能成功完成这次状态转换;另一个任务应检查受影响行数,并重新查询当前状态。

但要注意:

条件更新只能保护这次状态转换,不能自动保证 Redis 库存操作与数据库状态一致。

如果库存状态保存在 Redis,而预占状态保存在 MySQL,就需要额外的可靠执行机制,确保状态更新和实际库存变更能够恢复。

3. 推荐的组合方式

技术主要作用
Redis Lua原子检查并扣减 Redis 库存
业务唯一标识识别同一笔购买请求
数据库唯一约束防止重复订单
条件更新、状态机防止非法状态流转
分布式锁协调特定场景下的并发任务
幂等释放防止重复补偿导致库存增加过多
重试与补偿恢复执行失败的业务
定期对账发现并修复残留的不一致

这些手段解决的是不同问题,不能互相替代。


七、问题六:如何保证库存释放不会执行多次?

补偿任务可能重复执行,例如:

第一次释放库存成功 ↓ 服务返回结果前宕机 ↓ 补偿任务重试 ↓ 再次释放库存

如果每次执行都直接增加库存,就可能导致库存被重复返还。

因此,释放操作必须具备幂等性。

可以为每次库存预占建立唯一记录,并为释放操作设置明确状态。

释放时:

  1. 检查该预占是否已经释放。
  2. 确保当前状态允许释放。
  3. 以可靠的方式执行库存返还。
  4. 记录释放成功状态。
  5. 重试时识别已成功的操作,不重复返还库存。

如果库存返还与状态更新分布在 Redis 和 MySQL 中,不能简单地依靠一个数据库事务解决所有问题。需要使用持久化操作记录、幂等标识和可重试的执行流程,处理跨系统操作的中断。

补偿本身也必须是幂等的,否则修复机制可能制造新的数据错误。


八、Redis 与 MySQL 库存如何保持一致?

Redis 常用于高并发库存检查与扣减,MySQL 用于持久化订单和业务数据。

二者的更新无法天然形成一个跨系统原子事务。

例如:

Redis 库存扣减成功 ↓ MySQL 订单创建失败 ↓ 需要恢复库存

也可能发生:

MySQL 订单创建成功 ↓ Redis 状态更新失败 ↓ 订单与缓存状态不一致

建议:

  1. 明确 Redis 与 MySQL 各自承担的职责。
  2. 持久化库存预占及业务状态。
  3. 让库存操作、订单操作具备幂等性。
  4. 使用可靠的异步消息或任务驱动后续处理。
  5. 对执行失败的操作进行重试和补偿。
  6. 定期核对订单、预占记录和库存数据。

需要特别注意:不能简单地将 Redis 库存定期覆盖为 MySQL 库存,因为 MySQL 中的库存数据也可能处于异步更新过程中。库存重建必须以明确的业务事实为依据,并避免覆盖正在进行的有效操作。


九、完整方案总结

对于秒杀系统,可以从以下几个层面设计。

第一层:入口防护

  • 限流与请求校验。
  • 使用唯一业务请求标识。
  • 防止同一请求被重复处理。

第二层:库存扣减

  • 使用 Redis Lua 实现原子检查和扣减。
  • 正确处理库存不足、缓存异常和重复请求。
  • 通过可靠的业务记录追踪每次预占。

第三层:订单创建

  • 使用数据库事务保证订单自身的数据完整性。
  • 使用业务唯一标识与唯一约束避免重复订单。
  • 订单创建结果不明确时,先查询真实状态,不直接认定失败。

第四层:异步处理

  • 使用 MQ 或可靠任务处理后续业务。
  • 消费端保证幂等。
  • 对消息发送失败、消费失败、重复消费和消息丢失风险制定恢复策略。

第五层:异常恢复

  • 持久化库存预占状态。
  • 定时扫描长期未完成的预占。
  • 根据订单实际状态决定确认、重试或释放。
  • 确保库存释放幂等。
  • 对无法自动恢复的异常告警或人工介入。

第六层:一致性校验

  • 定期对账。
  • 检查订单、预占记录和库存之间的不一致。
  • 通过可追踪的修复任务恢复业务。
  • 对持续异常建立监控与告警。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 2:59:59

Java 中操作 R:从环境搭建到实战调用

1. 引言R 语言在统计分析、数据可视化和机器学习领域拥有丰富的生态&#xff0c;而 Java 则是企业级应用开发的主流语言。在实际项目中&#xff0c;我们经常需要在 Java 应用中调用 R 的统计能力&#xff0c;例如批量生成统计报表、执行回归分析或调用 R 中的算法包。本文将介绍…

作者头像 李华
网站建设 2026/10/23 2:59:58

A2A实战,Spring AI Alibaba双Agent协作

目录 前言 1. 两个 Agent 都能工作&#xff0c;为什么协作还需要设计 2. A2A 与 HTTP、MCP、多 Agent 分别是什么关系 2.1 HTTP 接口能实现&#xff0c;为什么还要协议 2.2 已经有 MCP&#xff0c;还需要 A2A 吗 2.3 多 Agent 系统一定要用 A2A 吗 3. 看懂一次 A2A 交互…

作者头像 李华
网站建设 2026/10/23 2:59:42

OpenClaw 实操排坑手册,Gateway 离线、路径报错问题一次性解决

&#x1f4cc; 说明 本文基于 OpenClaw 版本展开讲解&#xff0c;全程采用图形化、可视化的交互模式。整合包已内置全部运行依赖&#xff0c;即使是普通用户&#xff0c;也能轻松复现完整的部署流程。 ✨核心亮点&#xff1a; 全程可视化图形交互界面&#xff0c;自动补齐全部运…

作者头像 李华
网站建设 2026/10/23 2:58:44

数据契约(Data Contract)—— 上下游数据接口规范

数据契约&#xff08;Data Contract&#xff09;是数据生产者与消费者之间一份可机器识别、可自动校验、版本化、可治理的数据接口约定&#xff0c;替代传统文档式口头约定。契约明确定义接口字段、数据类型、格式、值域、业务口径、语义含义、变更策略&#xff0c;配套自动化校…

作者头像 李华
网站建设 2026/10/23 2:58:12

隔离风险:用备用机测试不明APP,电脑远程操控安卓保主力机安全

在这个数字化生存的时代&#xff0c;我们的手机早已不再是单纯的通讯工具&#xff0c;而是集钱包、社交中心于一体的“数字生命体”。然而&#xff0c;职场的复杂性往往伴随着各种意想不到的“陷阱”。比如&#xff0c;老板突然发来一个号称能提升效率的“内部测试版”APP&…

作者头像 李华