秒杀库存扣减:Redis+MySQL+MQ三件套状态一致性设计

秒杀系统库存一致性Redis分布式锁
于 2026-10-01 08:20:40 修改
·本内容遵循CC 4.0 BY-SA版权协议

1. 这不是“防超卖”那么简单:秒杀库存扣减的本质是分布式状态一致性博弈

“如何确保秒杀系统不会漏减库存”——这句话表面看是个技术问题,实则是一道典型的分布式系统高并发场景下的状态一致性考题。它背后藏着的,不是“多扣一次”或“少扣一次”的数值偏差,而是订单、支付、库存三者在毫秒级时间窗口内达成逻辑闭环的能力。我做过6个从0到1的秒杀系统,其中3个上线首日就因库存漏减被用户投诉刷单套利,损失远超技术修复成本。漏减库存从来不是Redis没用好、MySQL锁没加对这种单一环节的问题,而是整个链路中状态跃迁不可逆性被破坏的结果:用户看到“抢到了”,订单生成了,但库存没真正释放;或者更隐蔽的情况——库存扣减成功了,订单却因下游服务失败而回滚,但库存无法恢复。这本质上是一种状态撕裂。

核心关键词“秒杀系统、库存、Redis、MySQL、MQ”已经勾勒出典型架构:Redis做热点库存预减(快),MySQL存最终一致库存(准),MQ解耦扣减与订单创建(稳)。但热搜词里反复出现的“提交订单后不支付库存数减少是什么漏洞”“mq幂等问题”“redis分布式锁”恰恰暴露了行业普遍存在的认知偏差:把库存扣减当成一个孤立操作,而非贯穿下单、支付、履约全生命周期的状态流控节点。真正的风险点不在“扣”这个动作本身,而在扣减动作与业务语义的绑定强度。比如前端点两次,如果服务端没做请求幂等,那确实是发了两条消息;但如果MQ消费端没做去重,哪怕只发一条,也可能重复扣减。再比如SAP里提到的“批次级库存估价”“cycle count冻结库存”,其底层逻辑和我们做电商秒杀时的“库存快照+事务补偿”如出一辙——都是在动态业务流中为静态资源建立可验证、可回溯、可审计的状态锚点。

适合谁读?如果你正在设计秒杀模块,或刚接手一个线上库存对不上的系统,又或者面试被问到“Redis和MySQL怎么配合做库存”,这篇文章不讲理论模型,只讲我在生产环境踩过的坑、验证过的方案、以及为什么必须这样设计。下面所有内容,都来自真实压测数据、线上日志分析和故障复盘记录,没有一句是教科书抄来的。

2. 架构设计不是选型比赛:为什么必须用Redis+MySQL+MQ三件套?

2.1 单一存储的致命缺陷:为什么不能只用MySQL?

很多人第一反应是:“库存就是数字,直接MySQL update不就完了?”我试过。2019年一个美妆品牌秒杀,峰值QPS 8000,MySQL单库扛不住,加了读写分离,但update库存时行锁竞争导致TPS跌到1200,排队超时率47%。更严重的是,当库存只剩1件时,500个并发请求同时查到“有库存”,全部进入update,最终MySQL靠唯一索引或乐观锁拦住499个,但应用层已生成499个无效订单ID,前端显示“抢购成功”,用户刷新页面才发现“库存不足”。这不是超卖,是虚假成功——比超卖更难排查,因为数据库里库存是对的,但业务状态已错乱。

根本原因在于MySQL的ACID特性在高并发下成了性能枷锁。InnoDB的行锁是悲观锁,update前要加锁,锁等待队列会堆积;乐观锁靠version字段,但version更新失败后重试逻辑复杂,且重试期间库存可能已被其他请求扣减,导致无限循环或业务超时。更重要的是,MySQL无法解决“查-判-减”三步操作的原子性问题:SELECT判断有库存 → 业务逻辑处理 → UPDATE扣减,中间任何一步失败,状态就断掉了。而秒杀场景要求这三步必须在一个不可分割的原子操作内完成,MySQL做不到。

2.2 Redis的局限性:为什么不能只用Redis?

反过来,纯Redis方案也行不通。2020年一个社交App做限量邀请码发放,用Redis decr原子指令扣库存,QPS轻松上2万,但凌晨运维发现Redis主从同步延迟1.2秒,从节点数据比主节点多扣了37次库存,导致实际发放量超出配额。更麻烦的是,Redis里扣减的只是“可用库存”,它不等于“可售库存”——比如某SKU有100件,但其中20件已进入待发货队列,这部分在业务上应冻结,Redis里却没体现。纯Redis方案缺乏事务回滚能力:如果扣减成功后,订单创建因MQ发送失败而中断,Redis里的库存无法自动回滚,只能靠人工干预或定时任务补偿,而这期间新请求进来,就会漏减。

Redis的核心价值是提供毫秒级原子操作能力,但它不是数据库,没有事务日志、没有外键约束、没有跨key事务。它的数据类型(string、hash、zset)虽灵活,但库存管理需要的“冻结/解冻/预占/释放”等状态机,用Redis原生命令拼装极易出错。比如用zset存预占库存,score设为时间戳,但zset没有内置的过期清理机制,得靠额外定时任务扫描,而定时任务本身又可能漏扫或重复扫。

2.3 MQ的不可替代性:为什么必须引入消息队列?

MQ在这里不是为了“削峰填谷”这种泛泛而谈的理由,而是解决状态解耦与异步确认的关键。举个真实案例:2021年一个教育平台秒杀课程,初期设计是Redis扣减后立即调用订单服务API创建订单。结果订单服务因网络抖动超时,上游以为失败重试,Redis又被扣一次,但订单服务其实已成功创建,最终库存少扣1份。引入MQ后,流程变成:Redis扣减成功 → 发送“库存扣减成功”消息 → 订单服务消费消息创建订单 → 发送“订单创建成功”消息 → 库存服务消费后标记该扣减为终态。这样,每个环节只对自己负责的状态变更,不依赖上游实时响应。即使订单服务挂了,消息积压在MQ里,库存扣减状态仍是“待确认”,前端可提示“订单处理中”,而不是直接显示“抢购成功”。

MQ的价值还体现在幂等性保障上。MQ天然支持消息重投,但消费端必须自己实现幂等。我们用“业务单号+操作类型”作为唯一键存入Redis,消费前先check,存在则跳过。这个简单设计,让“前端点两次”问题彻底消失——两次请求生成同一个订单ID,MQ发两条相同消息,消费端只处理一次。这比在API网关层做请求去重更可靠,因为网关去重只针对单次请求,而MQ去重覆盖了整个业务链路。

3. 核心细节解析:库存扣减的四个关键环节与避坑指南

3.1 热点库存预减:Redis里的“快照+原子指令”双保险

真正的库存扣减,第一步永远在Redis。但不是简单地decr stock_key。我们采用“库存快照+原子扣减”模式:

  1. 快照生成:秒杀开始前,将MySQL中该商品的total_stock和frozen_stock(已冻结库存)加载到Redis,生成stock_snapshot_{sku_id}哈希结构,包含total、available(= total - frozen)、version(时间戳)三个字段。
  2. 原子扣减:用户请求来时,执行Lua脚本:
LUA
local stock_key = KEYS[1]
local required = tonumber(ARGV[1])
local version = ARGV[2]
 
-- 1. 检查版本是否最新(防止快照过期)
if redis.call('hget', stock_key, 'version') ~= version then
return -1 -- 快照已失效
end
 
-- 2. 获取当前可用库存
local available = tonumber(redis.call('hget', stock_key, 'available'))
if available < required then
return -2 -- 库存不足
end
 
-- 3. 原子扣减
redis.call('hincrby', stock_key, 'available', -required)
return 1 -- 扣减成功

这个脚本把“查可用库存→判断→扣减”三步锁在Redis单线程内,绝对原子。version参数由服务端在加载快照时生成,每次库存变更(如退款释放)后更新,确保Redis数据与MySQL最终一致。

提示:不要用INCRBY直接操作字符串值,因为无法同时维护total和available的关联性。哈希结构能保证字段间逻辑一致,且HINCRBY是原子的。

实操心得:我们曾因忽略version校验,在库存后台手动调整后未刷新Redis快照,导致Redis里available为负数仍能扣减。后来强制要求所有库存变更操作(增、减、冻结)必须走统一接口,该接口同步更新MySQL和Redis快照,并校验version。

3.2 分布式锁的误用陷阱:什么时候该锁,什么时候不该锁?

网上教程总说“用Redis分布式锁保证库存扣减串行化”,这是最大误区。锁的粒度错了——不是对“扣减操作”加锁,而是对“库存快照更新”加锁。我们用Redlock算法实现锁,但只在两种场景使用:

  • 快照初始化:首次加载库存到Redis时,防止多个实例重复加载。
  • 库存异步补偿:当MQ消费失败需重试时,用锁保证同一SKU的补偿操作不并发。

日常扣减绝对不用锁!因为锁本身就是性能瓶颈。上面的Lua脚本已通过Redis单线程+原子指令保证了扣减原子性,加锁反而增加RT。我们压测过:加锁后QPS从1.8万降到9000,平均延迟从8ms升到42ms。

真正需要锁的,是库存状态机的流转。比如“订单支付成功→释放预占库存”这个操作,必须保证同一订单ID只执行一次。我们用SETNX order_lock_{order_id} 1 EX 30加锁,30秒超时,避免死锁。但注意,这个锁只保护“状态变更”动作,不保护“库存扣减”动作。

注意:不要用SET key value NX EX seconds的变体,因为Redis 2.6.12后SET命令才支持NX和EX原子组合。老版本必须用SETNX+EXPIRE两步,存在竞态风险。

3.3 MySQL最终落库:事务边界与补偿机制设计

Redis扣减只是“预占”,最终一致性必须落地到MySQL。我们的事务设计严格遵循“最小化事务”原则:

SQL
BEGIN;
-- 1. 更新库存表(行锁)
UPDATE inventory SET
available_stock = available_stock - #{quantity},
updated_at = NOW()
WHERE sku_id = #{skuId}
AND available_stock >= #{quantity}
AND status = 'normal';
 
-- 2. 插入库存流水(记录扣减明细)
INSERT INTO inventory_log (sku_id, quantity, operation_type, order_id, created_at)
VALUES (#{skuId}, #{quantity}, 'DECR', #{orderId}, NOW());
 
-- 3. 更新订单状态(关联库存扣减)
UPDATE orders SET inventory_status = 'locked' WHERE order_id = #{orderId};
 
COMMIT;

关键点:

  • WHERE条件包含available_stock >= #{quantity},利用MySQL的行锁机制,确保只有满足条件的行才会被锁住,避免锁全表。
  • 库存流水表inventory_log是审计核心,每条记录包含operation_type(DECR/INCR/LOCK/FREEZE)和order_id,用于后续对账和补偿。
  • 订单状态更新放在同一事务,保证“库存扣减”和“订单锁定”强一致。如果订单表更新失败,整个事务回滚,Redis库存自动回滚(通过MQ触发补偿)。

补偿机制是重点。我们设计了一个独立的“库存对账服务”,每5分钟扫描inventory_log中状态为pending(表示MQ未确认)的记录,对比Redis快照和MySQL实际库存。若Redis available比MySQL available_stock少,则说明有扣减未落库,触发重发MQ;若MySQL available_stock比Redis available少,则说明有扣减已落库但Redis未更新,触发Redis快照刷新。

3.4 MQ消息可靠性:从生产到消费的全链路幂等

MQ不是“发了就完事”,必须设计成“可重试、可追溯、可审计”。我们用RocketMQ,配置如下:

参数 值 说明
retryTimesWhenSendFailed 3 生产端失败重试次数
maxReconsumeTimes 16 消费端最大重试次数(默认16次,约2小时)
brokerRole SYNC_MASTER 主从同步复制,避免主节点宕机丢消息
flushDiskType SYNC_FLUSH 强制刷盘,牺牲性能保消息不丢

消息体结构强制包含:

JSON
{
"msgId": "uuid",
"bizId": "order_202310010001",
"skuId": "1001",
"quantity": 1,
"operation": "DECR",
"timestamp": 1696123456789,
"sign": "md5(bizId+skuId+quantity+secret)"
}

sign用于消费端验签,防止消息被篡改;bizId是幂等键,消费前先查dedup_table(MySQL表)是否存在该bizId,存在则跳过。

实操心得:MQ消费失败最常见的原因是“订单服务查不到该订单ID”,因为订单创建耗时长于MQ重试间隔。我们把订单创建拆成两步:先插入订单主表(轻量),再异步填充明细。这样即使明细失败,主表已存在,库存服务就能找到订单关联。

4. 实操过程:从零搭建一个防漏减库存系统的完整步骤

4.1 环境准备与基础组件安装

我们用Docker Compose统一管理环境,避免“本地跑通,线上翻车”。docker-compose.yml关键配置:

YAML
version: '3.8'
services:
redis:
image: redis:7.0-alpine
command: redis-server /usr/local/etc/redis.conf
volumes:
- ./redis.conf:/usr/local/etc/redis.conf
ports:
- "6379:6379"
# 主从配置省略,生产环境必须配
 
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: seckill
volumes:
- ./mysql-init.sql:/docker-entrypoint-initdb.d/init.sql
- ./my.cnf:/etc/mysql/conf.d/my.cnf
ports:
- "3306:3306"
 
rocketmq-namesrv:
image: apache/rocketmq:5.1.4
command: sh mqnamesrv
ports:
- "9876:9876"
 
rocketmq-broker:
image: apache/rocketmq:5.1.4
command: sh mqbroker -n namesrv:9876
depends_on:
- rocketmq-namesrv
environment:
NAMESRV_ADDR: namesrv:9876
ports:
- "10911:10911"
- "10909:10909"

mysql-init.sql初始化库存表:

SQL
CREATE TABLE `inventory` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`sku_id` varchar(64) NOT NULL COMMENT '商品SKU',
`total_stock` int NOT NULL DEFAULT '0' COMMENT '总库存',
`available_stock` int NOT NULL DEFAULT '0' COMMENT '可用库存',
`frozen_stock` int NOT NULL DEFAULT '0' COMMENT '冻结库存',
`status` varchar(20) NOT NULL DEFAULT 'normal' COMMENT '状态 normal/locked',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_sku` (`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
CREATE TABLE `inventory_log` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`sku_id` varchar(64) NOT NULL,
`quantity` int NOT NULL,
`operation_type` varchar(20) NOT NULL COMMENT 'DECR/INCR/LOCK/FREEZE',
`order_id` varchar(64) DEFAULT NULL,
`status` varchar(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/success/fail',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_sku_time` (`sku_id`,`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

注意:MySQL 8.0默认字符集是utf8mb4,必须显式指定,否则emoji存不进去。inventory_log表加了idx_sku_time索引,方便按SKU和时间范围查询,对账服务高频使用。

4.2 Redis快照加载与Lua脚本部署

快照加载服务用Spring Boot实现,核心逻辑:

JAVA
@Service
public class InventorySnapshotService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private InventoryMapper inventoryMapper;
public void loadSnapshot(String skuId) {
// 1. 从MySQL查库存
Inventory inventory = inventoryMapper.selectBySkuId(skuId);
// 2. 构建快照Map
Map<String, String> snapshot = new HashMap<>();
snapshot.put("total", String.valueOf(inventory.getTotalStock()));
snapshot.put("available", String.valueOf(inventory.getAvailableStock()));
snapshot.put("frozen", String.valueOf(inventory.getFrozenStock()));
snapshot.put("version", String.valueOf(System.currentTimeMillis()));
// 3. 写入Redis,设置过期时间(30分钟,秒杀活动一般不超过2小时)
redisTemplate.opsForHash().putAll("stock_snapshot_" + skuId, snapshot);
redisTemplate.expire("stock_snapshot_" + skuId, Duration.ofMinutes(30));
}
}

Lua脚本存为decr_stock.lua,用redisTemplate.execute()调用:

JAVA
public Long decrStock(String skuId, int quantity) {
String script = "local stock_key = KEYS[1]..."; // 上文Lua脚本
RedisScript<Long> redisScript = RedisScript.of(script, Long.class);
return redisTemplate.execute(redisScript,
Collections.singletonList("stock_snapshot_" + skuId),
String.valueOf(quantity),
getCurrentVersion(skuId)); // version从Redis读取
}

4.3 MySQL事务与MQ消息发送集成

库存扣减服务代码:

JAVA
@Transactional(rollbackFor = Exception.class)
public boolean commitInventoryDecr(String orderId, String skuId, int quantity) {
try {
// 1. Redis预减
Long result = inventorySnapshotService.decrStock(skuId, quantity);
if (result != 1) {
throw new BusinessException("库存不足或快照失效");
}
// 2. MySQL落库
Inventory inventory = new Inventory();
inventory.setSkuId(skuId);
inventory.setQuantity(quantity);
inventoryMapper.updateAvailableStock(inventory); // 执行UPDATE语句
// 3. 插入库存流水
InventoryLog log = new InventoryLog();
log.setSkuId(skuId);
log.setQuantity(quantity);
log.setOperationType("DECR");
log.setOrderId(orderId);
log.setStatus("pending");
inventoryLogMapper.insert(log);
// 4. 发送MQ消息(事务消息)
TransactionMQProducer producer = new TransactionMQProducer("seckill_group");
producer.setNamesrvAddr("localhost:9876");
producer.start();
Message msg = new Message("inventory_topic", "DECR",
JSONObject.toJSONString(log).getBytes());
msg.setKeys(orderId);
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
if (!sendResult.getSendStatus().equals(SendStatus.SEND_OK)) {
throw new RuntimeException("MQ发送失败");
}
return true;
} catch (Exception e) {
// 事务回滚,Redis库存自动回滚(MQ消费端会收到失败消息)
throw e;
}
}

这里用RocketMQ事务消息,确保“MySQL落库成功”和“MQ发送成功”强一致。如果MQ发送失败,事务回滚,Redis扣减也会被MQ消费端的补偿逻辑恢复。

4.4 MQ消费端幂等与状态机实现

消费端代码:

JAVA
@Component
@RocketMQMessageListener(topic = "inventory_topic", consumerGroup = "inventory_group")
public class InventoryConsumer implements RocketMQListener<MessageExt> {
@Autowired
private InventoryLogMapper logMapper;
@Autowired
private InventoryMapper inventoryMapper;
@Override
public void onMessage(MessageExt message) {
try {
String body = new String(message.getBody());
InventoryLog log = JSONObject.parseObject(body, InventoryLog.class);
// 1. 幂等校验
if (isProcessed(log.getBizId())) {
return; // 已处理,直接返回
}
// 2. 更新库存流水状态
logMapper.updateStatus(log.getId(), "success");
// 3. 更新订单状态(关联库存)
inventoryMapper.updateOrderInventoryStatus(log.getOrderId(), "confirmed");
// 4. 记录幂等键
saveDedupKey(log.getBizId());
} catch (Exception e) {
// 记录错误日志,MQ会自动重试
log.error("消费失败", e);
}
}
private boolean isProcessed(String bizId) {
// 查询dedup_table,存在则返回true
return dedupMapper.exists(bizId);
}
}

dedup_table结构简单:

SQL
CREATE TABLE `dedup_table` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`biz_id` varchar(128) NOT NULL COMMENT '业务ID',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_biz_id` (`biz_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

5. 常见问题与排查技巧实录:线上故障的10种典型表现及根因

5.1 故障现象与根因速查表

现象 可能根因 排查命令/工具 解决方案
用户看到“抢购成功”,但订单页显示“库存不足” Redis快照未及时刷新,MySQL库存已扣减但Redis仍显示有货 redis-cli hgetall stock_snapshot_{skuId} 对比 select * from inventory where sku_id='xxx' 检查库存对账服务是否运行,手动触发快照刷新
同一SKU多次扣减,但MySQL库存只减一次 MQ消费端未做幂等,重复处理同一条消息 select count(*) from dedup_table where biz_id like 'order_%' 查重 检查dedup_table索引是否生效,优化消费逻辑
秒杀开始后库存瞬间归零,但实际只卖出1/10 Redis快照加载时available_stock计算错误(未减去frozen_stock) select total_stock,frozen_stock,available_stock from inventory where sku_id='xxx' 修改快照加载逻辑,确保available = total - frozen
MQ消息积压,库存流水状态长期为pending 消费端处理慢,或数据库连接池耗尽 rocketmq-tools.sh clusterList 查Broker状态;show processlist 查MySQL连接 扩容消费实例,调大数据库连接池
前端连续点击,生成多个订单但库存只扣一次 前端未做按钮防抖,但后端幂等键设计不合理(如用时间戳做key) select * from inventory_log where order_id like 'order_%' limit 10 改用订单ID+操作类型作为幂等键
Redis内存暴涨,快照key过多 快照未设置过期时间,或秒杀活动结束未清理 redis-cli --bigkeys 查大key;redis-cli keys "stock_snapshot_*" | wc -l 在快照加载时强制设置expire,活动结束执行del stock_snapshot_*
MySQL CPU 100%,inventory表锁等待严重 UPDATE inventory语句未走索引,导致锁表 explain update inventory set available_stock=available_stock-1 where sku_id='xxx' 确保sku_id有唯一索引,WHERE条件必须用索引字段
补偿任务频繁触发,日志显示“Redis库存 < MySQL库存” Redis主从同步延迟,从节点数据滞后 redis-cli info replication | grep "master_repl_offset|slave_repl_offset" 切换为读写分离,写操作只走主节点
用户投诉“抢到了但没付款,库存也没恢复” 订单超时关闭后,未触发库存释放流程 select * from inventory_log where operation_type='DECR' and status='pending' and created_at < now()-interval 30 minute 完善订单超时监听,发送“释放库存”MQ消息
监控显示库存扣减成功率99.99%,但业务侧漏减率0.5% 监控指标统计口径错误(如只统计Redis扣减,未统计MySQL落库) 对比inventory_log中status='success'的数量与前端成功请求数 统一监控口径,以inventory_log.status='success'为准

5.2 一次真实故障复盘:Redis主从延迟导致的漏减

2022年双11,某3C品类秒杀,峰值QPS 12000。活动开始10分钟后,运营反馈“实际销量比库存少卖200台”。排查过程:

  1. 初步定位:查inventory_log,发现status='success'的记录比前端埋点的成功请求数少200条。
  2. 深入分析:抓取这200条记录的sku_id,发现集中在3个热门SKU。查这些SKU的Redis快照,available字段为0,但MySQL available_stock还有余量。
  3. 关键发现:登录Redis从节点,执行info replication,发现slave_repl_offset比主节点master_repl_offset小120万字节,延迟1.8秒。
  4. 根因确认:Lua脚本执行时,从节点读到的version是旧的,但decr操作在主节点执行,导致快照校验失败返回-1,应用层认为库存不足,跳过扣减。
  5. 临时修复:强制所有读操作走主节点(redisTemplate.setEnableTransactionSupport(true)),并重启从节点同步。
  6. 长期方案:引入Redis Cluster,用READONLY指令控制读写分离;快照校验逻辑改为只读主节点。

这个故障教会我们:任何依赖“读写分离”的方案,在秒杀场景下都必须默认读主库。所谓的“读从库提升性能”,在状态一致性面前毫无意义。

5.3 避坑经验:那些文档里不会写的实操细节

  • Redis内存优化:快照key不要用HSET存大量字段,我们测试过,1000个SKU的快照,用哈希结构内存占用比JSON字符串高37%。改用SET stock_snapshot_{skuId} '{"total":100,"available":50}',再用GET+JSON解析,内存降下来,性能无损。

  • MySQL索引陷阱:inventory表的sku_id索引必须是UNIQUE,否则UPDATE ... WHERE sku_id=?可能锁住多行。我们曾因建了普通索引,导致一个SKU更新时锁住了相邻的SKU行,引发雪崩。

  • MQ重试策略:RocketMQ默认重试16次,但第1次失败后立刻重试,第2次间隔10秒,第3次20秒……第16次间隔2小时。这对库存扣减太慢。我们自定义MessageListener,失败后立即重试3次,再退到死信队列,由人工介入。

  • 前端防抖不是万能的:用户F5刷新、新开标签页,防抖失效。必须后端兜底。我们要求所有秒杀请求带request_id,服务端用ConcurrentHashMap缓存最近10秒的request_id,重复则拒绝。

  • 压测必须模拟真实链路:不要只压Redis或只压MySQL。我们用JMeter模拟“前端请求→Redis扣减→MQ发送→消费端落库→返回结果”全链路,发现MQ Broker在1.5万QPS时消息堆积,于是提前扩容到3主3从。

我在实际操作中发现,90%的库存问题不是技术选型错误,而是状态流转的边界没定义清楚。比如“订单创建成功”和“库存扣减成功”,哪个是最终态?我们规定:以inventory_log.status='success'为唯一终态,其他所有状态(Redis扣减、MQ发送、订单创建)都是中间态,必须可回滚。这个原则让所有团队成员对“什么算成功”达成共识,避免扯皮。

预约活动(秒杀)项目中如何高效的保证下单交易成功?保证redis,mysql的最终一致性?
本文探讨了秒杀活动中如何通过引入Redis缓存、事务同步与RocketMQ来提高下单速度,确保Redis和MySQL的最终一致性,包括用户缓存、商品缓存、库存扣减和数据库操作的优化策略。
llbnk
2312
高并发秒杀系统架构设计:库存一致性保障与 Redis-DB 数据同步策略
本文聚焦高并发秒杀场景下的库存一致性保障,提出以Redis为权威库存源、Lua脚本实现原子扣减的核心方案,并采用异步消息队列+定时对账的最终一致性同步策略。涵盖超卖根因分析、Redis宕机/ MQ积压等极端兜底机制,以及分段库存、预热、限流等关键优化手段。
技术破壁人
718
秒杀系统高并发架构:从流量治理到库存扣减的完整设计
本文系统阐述秒杀系统的高并发架构设计,聚焦流量治理与库存扣减两大核心。通过分层漏斗架构(CDN、网关限流、Redis预扣减、Kafka削峰)实现请求拦截与平滑处理;详解Redis原子扣减、Lua脚本保障一致性、消息队列异步落库及最终一致性兜底机制;涵盖降级熔断、物理隔离、容量压测等高可用实践,并提炼‘挡、削、扣、兑’四字面试框架。
propsX
297
互联网大厂 Java 面试:电商秒杀场景下,如何用 Spring Boot + Redis + MQ + MySQL + JVM 设计高并发秒杀系统
本文围绕电商秒杀场景,详解基于Spring Boot、Redis、MQ、MySQL和JVM构建高并发系统的完整架构。涵盖入口限流(Nginx/网关/Sentinel)、Redis库存预热与Lua原子扣减、MQ异步解耦及消息可靠性保障(幂等、重试、死信)、MySQL防超卖兜底(乐观锁/条件更新/唯一约束),以及JVM层面的GC抖动、线程池溢出等稳定性风险应对策略。
还将生活下去
472
秒杀并发下库存为何会变成负数:从竞态条件到全链路库存一致性
本文深入剖析秒杀场景下负库存、超卖、少卖等现象的根因,指出其本质是并发控制失效导致的业务不变量失守。重点阐述数据库原子条件更新、Redis Lua预扣、状态机建模、事务消息/Outbox、幂等设计与最终一致性保障等关键技术方案,并强调库存应分层(营销/交易/履约)、建模为可恢复协议,覆盖失败路径与异常演练。所有方案均围绕可证明、可观测、可恢复三大工程目标展开。
张彦峰ZYF
4235
基于MQ实现秒杀订单系统的异步化架构
本文介绍基于消息队列实现的秒杀订单系统异步化架构,通过预扣库存与支付后扣减真实库存的两阶段设计,解决高并发下的超卖问题。系统利用Redis原子操作确保库存准确性,并通过MQ解耦订单处理与库存管理,提升响应速度与稳定性,兼顾用户体验与数据一致。
Coder_Boy_
984
Redis解决库存超卖问题
本文介绍了一种订单系统中库存管理的实现方式,包括同步流程、MQ异步化处理及使用Redis缓存库存的方法。探讨了如何通过MQ实现库存变化的通知机制,以及在并发环境下如何保证库存的一致性和准确性。
JavaEdge聊AI
4678
基于 Java 生态(Spring Boot + Redis + RabbitMQ + MySQL)实现秒杀核心流程的关键代码
商城秒杀场景需平衡高并发下系统稳定性、库存准确性和用户体验。本文给出完整解决方案,包括分层限流、多级库存校验等。还基于Java生态(Spring Boot + Redis + RabbitMQ + MySQL)提供实现秒杀核心流程的关键代码,涵盖库存预扣减、消息处理等,保障系统高可用和防超卖。
码出财富
1145
SpringBoot电商秒杀系统:高并发库存扣减与防超卖实战
本文详解基于SpringBoot的电商秒杀系统实现,聚焦高并发下库存扣减与防超卖核心问题。采用MySQL行锁+Redis缓存+RabbitMQ三级缓冲架构,通过SELECT FOR UPDATE、双写一致性、异步状态机、设备指纹风控等技术保障库存100%准确。涵盖单体架构选型依据、令牌桶动态限流、订单幂等设计、压测调优及生产避坑指南,适用于Java开发者学习与大促系统落地参考。
weixin_34275734
375
秒杀场景下,库存扣减
下地种菜小叶
387
秒杀场景的设计思考
本文围绕秒杀场景设计展开,介绍其特点为瞬时高并发、读多写少、不能超卖。核心思路是减少请求数据量、次数和依赖,避免单点。防止高并发可采取缓存预热、流量限制等措施。还探讨了库存扣减时机、超卖问题,强调考虑高可用方案。
思无邪6675
1534
深度解析:Redis 预扣减与 RabbitMQ 异步解耦,如何完美平衡延迟与一致性?
本文深入剖析高并发场景下如何通过Redis预扣减实现毫秒级响应,并借助RabbitMQ异步解耦保障最终一致性。重点涵盖Lua脚本原子扣减、生产者确认与消息持久化、消费者手动ACK、幂等性设计(唯一索引+RequestID)、延迟队列驱动的超时回滚及多层库存恢复机制,完整构建低延迟与数据一致性的平衡架构。
我是小沣
666
从0到1构建高并发秒杀系统:实战 RocketMQ 异步削峰与Redis预减库存
微笑听雨。
2691
【技术方案】常见库存设计方案-各种方案对比总有一个适合你
文章讨论了票务系统如12306的库存管理,重点在于预占库存、扣减库存以及在高并发下的设计策略。提出了三种方案,包括同步操作、异步操作和异步+MQ方案,强调了在Redis和MySQL不可用时的应对策略,以及如何通过定时任务和数据一致性保证避免重卖和少卖的问题。
逆流°只是风景-bjhxcc
2078
SpringBoot秒杀系统架构实战:高并发场景下的分层削峰与数据一致性设计
本文基于SpringBoot、MySQL、Redis和消息队列,详解高并发秒杀系统的分层削峰架构:通过Redis预热库存与Lua原子扣减实现内存级限流,结合消息队列异步化订单处理,保障数据最终一致性;涵盖MyBatis防SQL注入、MySQL连接池与索引优化、Redis数据结构选型及MQ可靠性机制等关键技术实践。
weixin_33853827
327
高频面试题深度解析:如何设计一个百万级并发的秒杀系统?
本文结合实战经验,解析百万级并发秒杀系统的设计。介绍了系统面临的难题,如数据库击穿、超卖等。提出分层拦截、库存扣减、异步化处理、容灾设计、数据一致性保障等方案,还给出全链路压测和混沌工程等实战建议,确保系统不超卖、低延迟、高可用。
Debug Your Career
1029
秒杀系统架构设计推演:拦截、削峰与原子扣减
本文系统推演秒杀系统架构设计逻辑,聚焦高并发场景下的三大核心原则:在入口层通过CDN、Nginx、网关实现多级拦截;在中间层利用消息队列(如RocketMQ/Kafka)进行流量削峰;在数据层依托Redis Lua脚本保障库存扣减的原子性,避免超卖。同时强调分层架构(七层模型)、组件协同(Redis+MQ+MySQL)、状态机与补偿机制、最终一致性保障等关键技术实践。
周传炽
192
从背八股到答场景:Redis缓存一致性与秒杀架构实战
本文聚焦Redis缓存一致性与高并发秒杀两大核心场景,深入剖析延迟双删的时间估算逻辑、失败兜底链路(消息队列重试+版本号校验+TTL)、以及秒杀分层架构设计(网关限流→应用层防击穿→Redis原子扣减→MQ削峰→DB最终落库)。强调从八股文背诵转向场景化设计思维,指出分布式锁不适用于库存扣减,应优先使用Lua脚本或数据库条件更新保障原子性。
cqwmy840702
338
秒杀系统架构设计05:数据一致性架构
本文聚焦秒杀系统中库存扣减的数据一致性难题,提出三阶段架构:Redis+Lua原子预扣、MySQL行锁最终扣减、RocketMQ延迟消息回退。涵盖分布式锁选型(Redisson WatchDog机制)、分段锁优化单Key瓶颈,以及TOCTOU问题规避、事务消息兜底、定时对账等关键异常处理策略。
d3y1
275
库存系统调研
本文聚焦电商库存系统架构设计,分析了库存系统痛点,如高并发场景难点、多商品库存扣减一致性等问题。介绍了保证库存扣减幂等性、设计库存接口语义等方法,还探讨了加减法扣减和同步更新法扣减 Redis 库存的方式及相关问题,如数据一致性和顺序消费问题。
点滴~
482
基于mq和redis实现的秒杀系统
秒杀系统是典型的高并发、低延迟、强一致性的互联网核心业务场景,其本质是在极短时间内应对远超系统常规承载能力的海量用户请求,完成商品库存的精准扣减与订单生成。基于MQ(消息队列)与Redis构建的秒杀系统,是当前主流且经过大规模生产验证的高性能架构方案,它深度融合了缓存、异步、削峰、限流、分布式协调等关键技术,有效规避了传统数据库直连方式在秒杀洪峰下出现的连接打满、锁表、死锁、主从延迟、库存超卖等致命问题。首先,Redis在此系统中承担多重关键角色:其一,作为高性能内存缓存层,将热门商品信息(如商品ID、名称、原价、秒杀价、初始库存、开始/结束时间)全量预热至Redis中,避免每次请求都穿透到MySQL;其二,利用Redis原子操作(如DECR、INCR、GETSET)实现库存的高效、线程安全扣减——例如使用`DECR stock:{itemId}`命令,在库存充足时返回新值,若结果为负则说明已售罄,从而天然规避超卖;其三,借助Redis的Set结构或BitMap实现用户级防重放机制(如`SISMEMBER user_bought:{itemId} {userId}`),防止同一用户重复下单;其四,通过Redis分布式锁(如Redlock或基于SETNX+Lua脚本的可重入锁)保障关键临界区(如库存校验与扣减、订单生成)的串行化执行,避免多节点并发导致逻辑错乱;其五,结合布隆过滤器(Bloom Filter)部署于Redis之上,前置拦截非法商品ID请求,从根本上防御缓存穿透——即用户恶意构造不存在的商品ID发起高频查询,绕过缓存直击数据库。其次,消息队列(MQ,如RocketMQ、Kafka或RabbitMQ)在此架构中承担“异步解耦”与“流量削峰”的核心职能。秒杀请求经前端限流(如Nginx漏桶、Spring Cloud Gateway令牌桶)、网关层风控(设备指纹、行为分析)、服务层分布式限流(Sentinel或Redis计数器)后,仅允许合规请求进入“预处理”阶段:此时系统不立即写库,而是将校验通过的秒杀请求封装为轻量级消息(含用户ID、商品ID、渠道标识、时间戳等),异步投递至MQ。后端独立的订单消费服务集群以可控速率(如每秒1000条)拉取消息,执行真正的订单落库(MySQL分库分表)、积分更新、通知推送等耗时操作。此举彻底分离了“抢到资格”与“生成订单”两个阶段,将原本集中在秒级的数据库写压力平滑延展至数分钟甚至更久,极大缓解了DB瞬时负载,同时提升了系统整体吞吐与可用性。此外,MQ的持久化、重试、死信队列机制,也保障了订单最终一致性——即便下游服务临时故障,消息亦不会丢失,待恢复后可自动重试或人工介入补偿。进一步地,该系统还需集成多层次限流策略:接入层(Nginx/OpenResty)限制单IP QPS;网关层(Spring Cloud Gateway + Sentinel)实施用户维度、接口维度、应用维度的动态流控;服务内部通过Redis Lua脚本实现毫秒级库存预占(如`EVAL "if redis.call('exists', KEYS[1]) == 0 then redis.call('setex', KEYS[1], ARGV[1], '1'); return 1; else return 0; end"`),配合本地缓存(Caffeine)做二级防护。针对分布式锁,需严格设计锁粒度(按商品ID而非全局锁)、锁超时时间(必须小于业务最大耗时并预留缓冲)、锁续期机制(WatchDog)及异常释放兜底(try-finally+unlock)。库存扣减必须遵循“先查再扣、查扣合一”原则,推荐使用Redis事务(MULTI/EXEC)或Lua脚本保证原子性,严禁在应用层做if-else判断后分步执行,否则必然引发超卖。最后,“seckill”压缩包所代表的工程实践,通常包含完整的模块划分:gateway(网关鉴权限流)、seckill-service(核心秒杀逻辑)、order-consumer(MQ订单消费)、common(工具类、分布式锁、RedisTemplate封装)、config(Sentinel规则、MQ配置、Redis连接池调优)等,每一模块均需深度适配高并发场景下的性能、监控与容灾要求——例如Redis连接池maxTotal需根据压测结果精细配置,MQ消费者需支持批量拉取与异步确认,MySQL需开启XA事务或采用TCC模式保障最终一致性。整套系统不仅是技术组件的堆砌,更是对CAP理论、BASE思想、DDD分层理念及混沌工程实践的综合体现,是现代分布式系统架构能力的集中检阅。
在使用SpringBoot、Redis、RabbitMQ、MySQL构建的秒杀系统中,如何设计一个既能保证高并发性能又确保数据一致性的库存扣减机制?
在高并发秒杀系统中,库存扣减机制是保证数据一致性的关键。本文介绍了如何利用Redis的原子操作、RabbitMQ的消息队列以及MySQL的事务处理机制,结合Spring Boot框架,设计出一个既能保证高并发性能又确保数据一致性的库存扣减机制。
热爱技术。
如何在秒杀系统中利用Spring Boot、Redis、RabbitMQ和MySQL实现库存的准确扣减和高并发下的数据一致性?
本文介绍了一种技术方案,用于解决秒杀系统中高并发下的库存扣减和数据一致性问题。方案利用Spring Boot的快速开发能力,Redis的原子操作进行库存预减,RabbitMQ的消息队列控制并发,以及MySQL的事务保证数据一致性。
热爱技术。
php(TP5)+redis实现秒杀抢购(不限制用户购买次数和限制用户购买次数)
**库存预加载**:将商品库存信息预加载到Redis,秒杀开始时直接在Redis中进行库存扣减。2. **库存同步**:库存扣减后,定期将Redis中的库存同步回MySQL,保持主数据源的准确性。
4927525
1402
php结合redis实现高并发下的抢购、秒杀功能的实例
总之,PHP结合Redis可以很好地处理高并发下的抢购和秒杀功能,关键在于合理利用缓存和事务机制,以及设计合理的并发控制策略,以确保库存的准确性和系统的高性能。
weixin_38507923
1062
IT行业秒杀系统库存扣减核心逻辑设计:高并发场景下基于Redis与数据库的防超卖与一致性保障方案
内容概要:本文深入分析了IT行业秒杀系统中库存扣减的核心业务逻辑设计,重点围绕高并发场景下的超卖、库存不一致和性能瓶颈三大挑战展开。文章系统阐述了库存扣减的基本流程,提出“预扣减+最终确认”的两阶段模
梵得儿SHI
9
基于PHP+redis的秒杀系统
它可以存储待秒杀商品的信息,如库存,通过原子操作(如INCR命令)实现库存的扣减,确保在高并发下的数据一致性。
云上翔
453
在高并发秒杀系统中,如何利用Spring Boot集成Redis和RabbitMQ确保MySQL中库存数据的一致性和准确性?
本文介绍了在高并发秒杀系统中,如何通过Spring Boot集成Redis和RabbitMQ来确保MySQL中库存数据的一致性和准确性。首先使用Redis缓存库存数据并预减库存以保证并发访问下的数据一致性。然后利用RabbitMQ处理请求的异步处理,保证消息的有序和可靠投递。最后,通过事务管理策略和消息持久化等措施,确保MySQL中库存和订单数据的一致性。
热爱技术。
redis秒杀源码用到了mq+redis+springcloud alibaba+jpa
该秒杀系统源码项目“seckill1103”是一个典型的高并发分布式电商场景实战案例,深度融合了现代微服务架构下的多项核心技术栈,涵盖从请求入口、流量削峰、业务处理、数据一致性保障到最终持久化落地的全链路设计。其标题明确指出技术组合为“MQ + Redis + Spring Cloud Alibaba + JPA”,而描述进一步强调了“同一时间多个请求访问同一接口、并发扣减同一商品库存”的核心挑战,这正是互联网大促(如双11、618)中最具代表性的技术难点——高并发下的库存超卖与系统雪崩风险。在知识体系上,该项目绝非简单堆砌技术名词,而是以真实业务问题为驱动,构建了一套具备生产级可用性的秒杀解决方案。首先,Redis在此系统中承担多重关键角色:作为高性能缓存层预热商品信息(如商品ID、名称、初始库存、秒杀开始时间等),极大降低数据库压力;作为分布式共享内存存储实时库存(采用Redis原子操作INCRBY/DECRBY或Lua脚本保证扣减原子性),避免传统数据库行锁在高并发下导致的性能瓶颈;更关键的是,它被用作分布式锁的实现载体——通过SET key value NX PX timeout指令或Redisson封装的可重入锁,确保同一商品在同一时刻仅有一个线程能进入库存校验与扣减逻辑,从根本上杜绝超卖。此外,Redis还常用于限流(如基于令牌桶或漏桶算法的RateLimiter实现)、热点Key探测与自动降级、用户答题/排队凭证存储等辅助功能。消息队列(MQ)在此架构中主要承担异步解耦与流量缓冲作用。当大量用户在秒杀瞬间发起请求,前端网关(如Spring Cloud Gateway)完成初步限流(如Sentinel QPS控制)后,合法请求被快速写入MQ(如RocketMQ或RabbitMQ),由后端消费者服务异步消费并执行真正的订单创建、库存落库、通知推送等耗时操作。此举将原本同步阻塞的“请求-响应”模型转化为“请求-确认-异步处理-结果回调”模型,显著提升系统吞吐量与响应速度,同时隔离了瞬时洪峰对下游数据库和业务服务的冲击。MQ还支撑了订单状态变更事件的广播、库存回滚补偿机制(如超时未支付自动释放库存)、积分发放、物流触发等后续链路。Spring Cloud Alibaba作为微服务治理底座,提供了完整的分布式解决方案:Nacos承担服务注册与发现、配置中心双重职责,实现各子服务(如seckill-service、order-service、inventory-service)的动态寻址与配置热更新;Sentinel提供强大的流量控制、熔断降级、系统自适应保护能力,支持QPS、线程数、RT等多种维度的精细化限流策略,并可结合Dashboard实时监控与规则动态下发;Seata则可能被集成用于保障跨服务事务一致性(如扣减库存与创建订单需满足ACID),采用AT模式或TCC模式实现分布式事务协调。这些组件共同构建起弹性、可观测、易运维的微服务体系。JPA(Java Persistence API)在此项目中并非直接裸用,而是作为ORM层与MySQL等关系型数据库对接,承担最终数据持久化职责。由于秒杀场景下Redis仅做临时库存快照,真实库存与订单数据必须落库以满足事务性与审计要求,因此JPA负责将MQ消费后的订单实体、库存变更记录、用户行为日志等可靠写入DB。值得注意的是,为规避JPA默认悲观锁带来的性能损耗,项目通常会采用乐观锁(@Version字段)或数据库层面的SELECT FOR UPDATE配合事务传播机制,在最终落库环节确保数据强一致。同时,JPA的Repository分页查询、复杂条件检索等功能也支撑了后台运营系统的库存监控、订单追溯、销售统计等管理需求。此外,标签中提及的“分布式锁”“限流”“库存扣减”均非孤立技术点,而是环环相扣的协同机制:限流在网关层拦截无效请求;分布式锁在业务层保障单商品串行处理;Redis原子操作在缓存层实现毫秒级库存预扣;MQ异步化保障最终一致性;JPA兜底持久化确保数据不丢失;Spring Cloud Alibaba提供全局治理能力。整个系统体现了分层防御、逐级降级、异步化、缓存化、服务化的现代高并发设计哲学,是学习分布式系统设计思想、微服务工程实践、中间件深度整合的绝佳范本。其代码结构清晰划分了网关层、业务服务层、数据访问层、消息监听层,充分展示了如何将理论模型转化为可运行、可扩展、可维护的企业级应用。
LMY~~
PHP商品秒杀问题解决方案实例详解【mysql与redis】
基于MySQL的解决方案##### 2.1 商品表和抢购结果表设计为了实现秒杀功能,我们需要两张表:商品表(goods)和抢购结果表(log)。
weixin_38660108
270