秒杀结束后库存回滚的资源释放,核心不是简单把Redis里的库存数字加回去,而是先按正确顺序释放分布式锁、数据库连接、缓存key和线程资源,否则回滚越频繁,系统越容易被拖垮。
秒杀结束后库存怎么回滚?资源释放先于数据修改
很多人以为回滚就是执行一条 update stock set stock=stock+1,实际线上秒杀结束后,回滚要处理四个层面:Redis预扣减、数据库行锁、分布式锁、HTTP线程,如果顺序错,会出现超卖、重复回滚、连接池耗尽。
第一步:读取活动结束标记,停止后续扣减
秒杀结束时间一到,运营后台或定时任务写一个轻量标记:
SET seckill:activity:1001:end 1 EX 3600
下单入口先查这个key,存在就直接拒绝,这个key本身占不了多少内存,回滚完成后不要立刻删,保留一段时间,防止延迟请求穿透到数据库。
第二步:回滚Redis预扣减库存,同时删除用户购买记录
Redis回滚不是单独一条 INCRBY,要和用户购买记录一起处理,用Lua脚本保证原子性:
-- KEYS[1] 库存key
-- KEYS[2] 用户购买记录key
-- KEYS[3] 活动结束标记key
-- ARGV[1] 回滚数量
if redis.call('exists', KEYS[3]) == 1 then
redis.call('incrby', KEYS[1], ARGV[1])
redis.call('del', KEYS[2])
return 1
end
return 0
这段逻辑先判断结束标记存在,再回滚库存,最后删除购买记录key,顺序不能反,购买记录key删除后,Redis内存才真正释放,除了购买记录,用户限购key、资格key也要在同一次脚本里或紧随其后删除。
第三步:数据库库存回滚必须把连接归还放在finally
数据库连接是秒杀回滚里最容易泄漏的资源,Java代码不写 finally 就会出问题:
Connection conn = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);
// 执行 update stock set stock = stock + ? where goods_id = ? and activity_id = ?
conn.commit();
} catch (Exception e) {
if (conn != null) conn.rollback();
} finally {
if (conn != null) conn.close(); // 归还连接池
}
conn.close() 在连接池中是归还,不是物理关闭,漏掉 finally 会导致连接不归还,数据库行锁在 commit 或 rollback 时释放,所以先提交再关连接,如果回滚逻辑里有提前 return,连接就会泄漏,多数线上连接池耗尽都出现在这种分支里。
秒杀库存回滚和预扣减对比:资源释放侧重点完全不同
预扣减和回滚虽然都操作库存,但资源释放重点不一样,下面这张表把关键差异列出来。
| 维度 | 秒杀预扣减 | 秒杀结束回滚 |
| Redis key 操作 | 先 DECRBY,再写用户购买记录 | 先判断结束标记,再 INCRBY,后删记录 |
| 分布式锁 | 锁定库存行或key,扣减完立即释放 | 回滚前加锁,回滚后主动释放,避免锁过期误删 |
| 数据库连接 | 短事务,快速提交 | 分支多,容易漏关连接 |
| 线程资源 | HTTP线程等待库存结果 | 需考虑异步化,释放同步线程 |
预扣减阶段容易忽略的连接占用问题
预扣减如果先查数据库再写Redis,连接持有时间长,高并发时连接池容易被打满,建议把Redis作为第一道扣减,数据库连接只在最终确认阶段使用,这样预扣减阶段连接占用时间能缩短到毫秒级。
回滚阶段必须处理的分布式锁过期与主动释放
回滚给Redis库存key加分布式锁,如果业务逻辑卡住,锁自动过期,另一个回滚任务又加锁成功,可能重复回滚,正确做法是锁value用唯一token,释放时判断token:
String token = UUID.randomUUID().toString();
boolean locked = redis.set(lockKey, token, "NX", "EX", 10);
if (locked) {
try {
// 回滚库存
} finally {
String current = redis.get(lockKey);
if (token.equals(current)) {
redis.del(lockKey);
}
}
}
锁过期时间设短,回滚操作本身控制在几十毫秒内,主动释放锁比等过期更可靠,也减少Redis里无用的锁key。
电商大促秒杀库存回滚方案中,Redis key 释放顺序不能反
大促结束后,Redis里会留下库存key、订单资格key、用户限购key、活动结束标记,释放顺序错了会出问题,最常见错误是先删除用户限购key,再回滚库存,用户限购key删除后,用户重复请求进来,此时库存还没回滚,可能造成超卖,正确顺序:
- 第一步:写
seckill:activity:1001:end,阻断新请求 - 第二步:对库存key执行
INCRBY - 第三步:删除
seckill:user:limit:1001:和seckill:order:1001: - 第四步:保留结束标记至少到活动结束后24小时
北京地区秒杀库存回滚方案:地域库存key要先标记再回滚
北京仓单独放库存,key为 seckill:stock:1001:beijing,活动结束后,不能直接 DEL 这个key,因为可能还有北京用户在途请求,要先通过结束标记拦截,再回滚,如果直接 DEL,Redis重建key时从数据库加载库存,可能把已回滚的数量覆盖掉,具体命令:
SET seckill:stock:1001:beijing 0 NX EX 60
INCRBY seckill:stock:1001:beijing 10
先确保key存在并置0,再 INCRBY,避免 DEL 后的窗口期,北京地区用户退款场景,需要从数据库反查活动是否结束,再决定回滚到北京仓还是全国仓。
秒杀超卖后库存回滚慢怎么办?先释放数据库连接池
回滚慢通常不是SQL慢,而是连接池被占满,回滚请求在等连接,表现是接口超时、库存一直不恢复,排查步骤:
- 查HikariCP活跃连接:
/actuator/metrics/hikaricp.connections.active - 查慢SQL:
show processlist或数据库慢日志 - 查Redis慢日志:
slowlog get 10 - 看线程池队列:
ThreadPoolExecutor.getQueue().size()
找到问题后,先把回滚从同步改为异步,HTTP线程不直接执行回滚,而是发MQ消息,立即返回,消费者处理回滚,处理完手动ack,这样HTTP线程先释放,连接池压力下降。
用MQ延迟消息做最终回滚,释放同步线程
具体步骤:
- 秒杀结束回调发送延迟消息,延迟5秒
- 消费者收到消息,先查活动结束标记
- 执行Redis回滚和数据库回滚
- 回滚成功后手动ack
- 失败不ack,消息重新投递
命令示例:
mq.send("seckill.rollback.queue", {activityId:1001}, 5000);
消费者端在 finally 释放数据库连接,确保线程池不被占满,行业内共识认为,回滚资源释放的顺序决定了数据一致性,顺序错了,再快的机器也会被锁等待拖垮。
资源释放的验证与监控
回滚资源是否真正释放,不能靠猜,需要看几个指标:
- Redis内存使用:
INFO memory中used_memory是否下降 - 连接池 active 连接数是否回到基准
- 分布式锁 key 是否过期删除
- JVM线程数是否回落
- 数据库行锁等待数:
show engine innodb status中的 lock wait
一个简单验证清单
- 回滚后执行
EXISTS seckill:order:1001:,应为0 - 连接池 active count 低于最大连接数的30%
- Redis slowlog 没有超过10ms的命令
- 分布式锁key的TTL为-2(已删除)
近年电商大促规模扩大后,回滚资源释放问题更加突出,业内专家指出,连接池泄漏是秒杀回滚阶段最常见的故障,而不是SQL本身。
秒杀结束后库存回滚的资源释放,本质是一次顺序敏感的清理动作,先把结束标记落下去,再回滚Redis,最后释放连接和锁,多数超卖和连接池耗尽问题都能避免。
秒杀结束后库存回滚的资源释放常见问题
秒杀结束后库存回滚必须释放哪些资源?
必须释放四类:Redis预扣减key、分布式锁、数据库连接、MQ消费者线程,Redis key包括库存key、用户限购key、购买记录key;分布式锁要按token判断后删除;数据库连接放在 finally 中 close;MQ消费者处理完手动ack,其中Redis内存和数据库连接池最容易出问题。
秒杀库存回滚和预扣减哪个更容易造成连接池泄漏?
回滚阶段更容易,预扣减路径相对单一,往往是Redis操作后立即结束;回滚阶段涉及活动结束判断、Redis回滚、数据库回滚、锁释放多个分支,任一个分支漏写 finally 或提前 return,连接就不会归还,因此回滚代码必须把连接 close 放在最外层 finally,多数情况下,线上连接池耗尽是在回滚密集时出现。
秒杀超卖后库存回滚慢怎么办?
先查数据库连接池active数和Redis slowlog,多数情况下是连接等待或者大key删除阻塞,确认后把同步回滚改成MQ延迟消息,HTTP线程先返回释放,消费者端再串行回滚,并将连接归还逻辑写入 finally,同时把Redis大key拆成地域key,避免单个key过大导致 DEL 阻塞,事实是回滚慢的根因很少是SQL本身,而是资源争用顺序错误。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/635622.html


