一、 引言在电商、抢购、票务等高并发高流量的业务场景中“防超卖”永远是核心代码中最核心的一环。商品超卖即卖出的商品数量大于实际库存不仅会导致严重的资损更会引发公关危机而由于锁设计不当导致的单线程卡顿少卖又会极大影响用户体验。很多同学在面对分布式集群时第一反应是用 Redis 的setnx或者 Spring Boot 中的redisTemplate.opsForValue().setIfAbsent()来做分布式锁。但你有没有想过如果业务还没执行完锁的过期时间到了怎么办如果线程 A 误删了线程 B 的锁怎么办如果 Redis 集群发生了主从切换锁丢了怎么办今天我们就来硬核拆解这些生产环境的致命痛点并给出基于Redisson的完美高可用解决方案。二、 传统 setnx 分布式锁的“三大夺命连环坑”我们先来看一下为什么基于简单命令的分布式锁在真正的生产环境下是不安全的。1. 坑一锁过期退出的“看门狗”痛点假设线程 A 获取了锁设置过期时间为 5 秒。但由于网络突发抖动、或者垃圾回收触发了 Full GC导致线程 A 的业务整整执行了 8 秒。在第 5 秒时锁自动过期失效。线程 B 趁虚而入成功获取了锁开始执行业务。后果此时线程 A 和线程 B 同时在执行临界区代码分布式锁直接失效。2. 坑二误删邻居的锁承接上面的场景当线程 A 在第 8 秒终于执行完业务后它会习惯性地调用delete(key)去释放锁。后果此时它删掉的实际上是线程 B 刚刚获取的锁接着线程 C 又进来了系统彻底陷入混乱。三、 生产级最优解Redisson 分布式锁方案为了完美解决上述痛点成熟的架构团队普遍采用开源框架Redisson。Redisson 在底层通过精心设计的Lua 脚本和Watchdog看门狗机制实现了真正高可用的分布式锁。核心运作原理Redisson 的看门狗机制会自动给锁续期。只要线程还持有锁后台线程每隔 10 秒默认就会去检查一次如果业务没完就自动将过期时间重置为 30 秒彻底解决了“业务没完锁先过期”的尴尬。四、 代码落地Spring Boot Redisson 秒杀扣减库存实战接下来我们直接上可用于生产环境的标准代码。1. 引入 Maven 依赖dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.23.2/version /dependency2. 核心秒杀业务实现分布式锁 数据库乐观锁双保险Service Slf4j public class SeckillService { Autowired private RedissonClient redissonClient; Autowired private GoodsStockMapper goodsStockMapper; /** * 高并发秒杀下单接口 * param userId 用户ID * param goodsId 商品ID */ public void seckillOrder(String userId, Long goodsId) { String lockKey lock:seckill: goodsId; // 1. 获取一个可重入锁实例 RLock lock redissonClient.getLock(lockKey); try { // 2. 尝试获取锁最多等待 5 秒防憋死获取成功后锁持有 30 秒 // 注意如果不显式指定 leaseTime才会触发看门狗的自动续期机制 boolean isLocked lock.tryLock(5, TimeUnit.SECONDS); if (isLocked) { log.info(【秒杀成功】线程: {} 成功抢到分布式锁开始处理库存..., Thread.currentThread().getId()); // 3. 核心业务查询当前商品剩余库存 GoodsStock stock goodsStockMapper.selectByGoodsId(goodsId); if (stock.getStock() 0) { throw new BizException(很抱歉该商品已被抢购一空); } // 4. 扣减库存利用数据库乐观锁进行终极防线兜底 // SQL 落地语句示例: UPDATE t_goods_stock SET stock stock - 1 WHERE goods_id #{goodsId} AND stock 0 int rows goodsStockMapper.decreaseStock(goodsId); if (rows 0) { log.info(【库存扣减成功】用户: {} 成功秒杀商品: {}, userId, goodsId); // 此处可继续异步投递 MQ 消息生成真实订单 } else { log.warn(【并发冲突】数据库乐观锁拦截用户: {} 秒杀失败, userId); throw new BizException(手慢了请重新尝试); } } else { log.warn(【请求被限】线程: {} 未在规定时间内获取到锁直接降级, Thread.currentThread().getId()); throw new BizException(服务器拥挤请稍后再试); } } catch (InterruptedException e) { log.error(秒杀获取锁期间发生异常中断, e); Thread.currentThread().interrupt(); } finally { // 5. 关键务必在 finally 块中释放锁且只能释放当前线程持有的锁 if (lock.isHeldByCurrentThread()) { lock.unlock(); log.info(【锁释放成功】线程: {} 已安全归还分布式锁, Thread.currentThread().getId()); } } } }五、 更高维度的思考红锁Redlock该不该用很多同学看八股文时都知道针对 Redis 主从架构可能存在的“锁丢失”问题即主节点刚写成功锁还没同步给从节点就挂了从节点升级为主后锁丢了大厂提出了Redlock红锁算法。但在这里要给大家泼一瓢冷水在目前的工程实践中非常不建议盲目使用 Redlock。极高的复杂性与性能开销红锁要求同时向过半的独立 Redis 节点至少3-5个发起加锁请求这使得 Redis 原本极高的吞吐量断崖式下跌。业界大佬的博弈分布式专家 Martin Kleppmann 曾专门写文章反驳过 Redlock 的安全性认为它过度依赖系统时钟依然无法彻底解决极端情况下的安全问题。大厂真实选择如果业务场景真的要求强一致性、绝不能忍受哪怕万分之一的锁丢失如金融财务系统大厂往往直接改用ZooKeeper基于 CP 模型的 ZAB 协议来实现分布式锁而在绝大多数高并发电商场景下普通的 Redisson 锁AP 模型配合数据库最终的stock 0乐观锁约束已经完全足够且性能拉满。六、 总结架构设计的本质就是在性能与一致性之间做权衡。Redis 分布式锁帮我们在网关和应用层挡住了 99% 的无效并发请求减轻了数据库的压力而数据库的行级锁/乐观锁则是确保数据绝不超卖的最后底线。两者结合才是最完美的分布式高可用架构。你的项目中是用 Redis 还是 ZooKeeper 实现分布式锁在大大促期间你的库存扣减逻辑是怎么玩的欢迎在评论区贴出你的神仙方案我们一起探讨