SpringBoot 分布式锁实战:从单机锁到Redis分布式锁全覆盖,解决超卖、重复下单、幂等并发问题(生产直接可用源码)
作者:后端实战老码农
标签:SpringBoot、Redis、分布式锁、高并发、秒杀、接口幂等、后端架构
适合人群:SpringBoot后端开发、电商秒杀业务开发、并发问题排查、面试突击、生产架构优化人员
阅读收获:彻底搞懂为什么单机锁分布式失效、手写生产级Redis分布式锁、规避超时误删/死锁/锁失效三大线上致命坑、直接复用工具类、面试满分回答分布式锁核心题
一、前言:90%项目都踩过的并发噩梦------多实例下锁直接失效
做电商、支付、库存扣减、优惠券核销、订单创建、接口幂等场景,并发安全是底线。
很多新手本地测试加个synchronized 或者 ReentrantLock,本地压测没问题,一上生产集群部署,立马出现:
✅ 库存莫名超卖、✅ 同一用户重复下单、✅ 重复扣款对账不平、✅ 定时任务集群重复执行
核心原因就一句话:单机锁只锁当前JVM实例,集群多台服务互不感知,锁直接失效。
今天不讲废话,不堆砌理论,直接手把手带你:为什么锁失效 → 分布式锁硬性要求 → Redis原生分布式锁手写实现 → 解决超时误删、死锁、原子性问题 → SpringBoot一键集成落地 → 线上事故复盘 → 面试必背,全篇可直接上线复用。
二、先搞懂:为什么 synchronized / ReentrantLock 扛不住分布式集群?
2.1 单机锁的底层局限性
synchronized、ReentrantLock 本质都是 JVM 内部锁,依赖当前进程内线程队列实现互斥。
单体架构时代,所有请求都落在同一台服务、同一个JVM,单机锁可以完美控制并发争抢。
2.2 集群部署后,直接全线崩盘
现在项目全部微服务集群部署,同一接口多台实例负载均衡分发请求。
不同服务实例的锁互相隔离,A服务器加锁成功,B服务器完全看不见,照样同时扣库存、同时创建订单。
结论 :只要项目部署多实例,必须上跨JVM、跨服务全局互斥的分布式锁。
三、生产级分布式锁,必须满足这5条硬性规范(不符合一律不准上线)
不是随便 set 一个 key 就叫分布式锁,线上低并发看不出问题,高并发直接出资金事故。
合格分布式锁必须同时满足:
-
互斥性:同一时刻只有一个客户端能拿到锁,全局互斥
-
防死锁:客户端宕机、程序异常崩溃,锁必须自动释放,否则永久卡死业务
-
防误删:A加的锁,B绝对不能误删,杜绝超时乱释放导致并发击穿
-
原子操作:加锁、过期、判断必须原子执行,防止并发临界区击穿
-
可重入/高可用:同一线程可重入、Redis集群宕机有兜底,不影响核心业务
四、避坑先行:网传三种错误分布式锁写法,线上必死无疑
4.1 错误写法一:单纯 set key 不加过期时间
业务中途报错、服务宕机,锁 key 永远不删除,后续所有人拿不到锁,直接全线堵死,生产重大事故。
4.2 错误写法二:先 set key 再 expire 分两步执行
非原子操作,中间网络抖动、程序挂掉,key 没有过期时间,瞬间变死锁,和上面一样致命。
4.3 错误写法三:谁来都直接 del 删除锁
锁超时自动释放后,新线程拿到锁,旧线程执行完乱删 key,直接把别人有效锁删掉,并发直接击穿超卖。
五、生产最优方案:Redis + Lua 脚本实现安全分布式锁(原子+防误删+自动过期)
5.1 核心设计思路
-
加锁:使用 Lua 脚本,原子完成【不存在则写入 + 设置过期时间】
-
锁 value 存入当前线程唯一ID,标记锁归属人
-
解锁:先判断 value 是否是自己的ID,是自己才删除,防止误删
-
自动过期兜底,防止宕机死锁
5.2 为什么一定要用Lua脚本?
Redis 单线程模型,Lua 脚本整体执行不被插队,天然保证多命令复合操作原子性,并发不会击穿临界区。
六、SpringBoot 完整集成:可直接上线分布式锁工具类(无依赖侵入、开箱即用)
无需额外中间件、不依赖Redisson复杂框架,轻量稳定,中小厂、中台、业务系统全部适配,直接复制即可用。
import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.Collections; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * 生产级Redis分布式锁工具类 * 特性:原子加锁、防死锁、防误删、高并发安全、适配SpringBoot全版本 * 作者:后端实战老码农 */ @Slf4j @Component public class RedisDistributedLockUtil { @Resource private StringRedisTemplate stringRedisTemplate; // 锁前缀统一规范 private static final String LOCK_PREFIX = "business:distributed:lock:"; // 锁默认过期时间,防止宕机死锁 private static final long LOCK_EXPIRE_SECOND = 30; /** * 尝试获取分布式锁 * @param lockKey 业务唯一锁key,例如:stock:reduce:1001 * @return 成功返回唯一标识,失败返回null */ public boolean tryLock(String lockKey) { String realKey = LOCK_PREFIX + lockKey; // 唯一线程标识,防止误删锁 String lockUuid = UUID.randomUUID().toString().replace("-", ""); // Lua原子加锁脚本:不存在则set + 设置过期 String luaLockScript = "if redis.call('exists',KEYS[1])==0 then redis.call('set',KEYS[1],ARGV[1],'EX',ARGV[2]) return 1 else return 0 end"; DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaLockScript, Long.class); Long result = stringRedisTemplate.execute( script, Collections.singletonList(realKey), lockUuid, String.valueOf(LOCK_EXPIRE_SECOND) ); if (result != null && result == 1) { // 本地线程缓存当前锁标识,用于后续解锁校验 ThreadLocalHolder.setLockId(lockUuid); log.info("分布式锁加锁成功,key:{}", realKey); return true; } log.warn("分布式锁加锁失败,业务并发争抢,key:{}", realKey); return false; } /** * 安全释放分布式锁:只能解自己加的锁,杜绝误删 * @param lockKey 业务锁key */ public void releaseLock(String lockKey) { String realKey = LOCK_PREFIX + lockKey; String currentLockId = ThreadLocalHolder.getLockId(); if (currentLockId == null) { return; } // Lua原子解锁脚本:校验归属再删除 String luaUnLockScript = "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaUnLockScript, Long.class); stringRedisTemplate.execute(script, Collections.singletonList(realKey), currentLockId); // 清除本地缓存 ThreadLocalHolder.clear(); log.info("分布式锁释放完成,key:{}", realKey); } /** * 线程本地缓存,保存当前线程持有的锁ID */ private static class ThreadLocalHolder { private static final ThreadLocal<String> LOCK_ID = new ThreadLocal<>(); public static void setLockId(String id) { LOCK_ID.set(id); } public static String getLockId() { return LOCK_ID.get(); } public static void clear() { LOCK_ID.remove(); } } }
七、业务层实战演示:秒杀扣库存、防止重复下单真实场景
直接在业务接口中优雅使用,极简写法,无侵入,安全防并发。
@RestController @RequestMapping("/order") @Slf4j public class OrderController { @Autowired private RedisDistributedLockUtil distributedLockUtil; @Autowired private StockService stockService; /** * 秒杀扣库存下单接口,分布式锁保障不超卖、不重复下单 */ @PostMapping("/seckill/create") public R createSeckillOrder(@RequestParam Long goodsId) { // 拼接全局唯一锁Key,按商品维度争抢锁 String lockKey = "seckill:goods:" + goodsId; // 1.尝试加锁 boolean lockSuccess = distributedLockUtil.tryLock(lockKey); if (!lockSuccess) { return R.fail("当前抢购人数过多,请稍后重试"); } try { // 2.执行业务临界区:查库存、判重、扣减、生成订单 boolean result = stockService.reduceStockAndCreateOrder(goodsId); if (result) { return R.success("下单成功"); } else { return R.fail("库存不足"); } } catch (Exception e) { log.error("秒杀下单业务异常", e); throw new RuntimeException("系统繁忙,请重试"); } finally { // 3.无论成功失败,最终一定释放锁 distributedLockUtil.releaseLock(lockKey); } } }
八、线上三大高危坑点复盘:90%公司都踩过
坑点一:锁超时时间 < 业务执行时间
业务还没跑完,锁自动过期释放,下一个线程进来并发执行,直接超卖。
解决方案 :核心长任务配合锁续期机制,定时检查业务是否跑完,没跑完自动延长过期时间。
坑点二:finally 不释放锁、异常直接抛出去
代码报错直接跳出,锁不释放,后续请求全部阻塞排队,接口全线超时雪崩。
解决方案:强制写在 finally 中释放锁,兜底必执行。
坑点三:锁 key 设计不规范,业务互相串锁
所有业务共用一个固定锁key,并行业务全部串行排队,接口性能直接腰斩。
解决方案:按商品ID、用户ID、订单号维度拆分粒度,精细化锁粒度提升并发吞吐量。
九、面试高频硬核问答:面试官必问分布式锁考点
Q1:为什么分布式锁一定要用Lua脚本? A:保证加锁、判断、过期、解锁全程原子性,防止高并发临界区击穿,杜绝中间态异常。
Q2:Redis分布式锁宕机怎么办?会不会丢锁? A:主从异步复制有极小概率丢数据,金融级强一致性推荐Redisson+红锁,普通业务单机Redis足够稳定。
Q3:为什么一定要存唯一UUID,不能直接del key? A:防止锁超时自动释放后,旧线程删除新线程的合法锁,击穿并发安全屏障。
Q4:和Zookeeper分布式锁对比优缺点? A:Redis性能高、适合高并发秒杀;Zookeeper强一致性、适合资金对账低并发场景。
十、全文总结
集群环境下,单机锁彻底失效,Redis+Lua分布式锁是中小厂高并发业务性价比最高、落地最快的方案。
记住三句话:加锁必须原子、解锁必须验权、过期必须兜底。直接复制本文工具类,秒杀、库存、幂等、定时任务全部稳得住,线上零事故。
原创干货不灌水,实测可直接上线!点赞+收藏+关注,下期更新《Redisson看门狗自动续期分布式锁,解决长任务超时锁失效问题