别再只用synchronized了!用Redis+Lua脚本给你的SpringBoot秒杀项目上个分布式锁(附完整代码)
从单体锁到分布式锁RedisLua构建高并发秒杀系统的终极方案当你在本地开发环境测试秒杀功能时synchronized关键字或许能完美解决超卖问题。但一旦部署到生产环境的集群中你会发现精心设计的锁机制突然失效了——这不是代码bug而是分布式系统给我们上的第一课。本文将带你深入理解分布式锁的本质并手把手教你用RedisLua实现一个工业级的解决方案。1. 为什么synchronized在分布式场景会失效在单机Java应用中synchronized确实能有效解决线程安全问题。它的实现原理是基于JVM内置的监视器锁Monitor当多个线程访问同步代码块时public synchronized void doSomething() { // 临界区代码 }问题本质在于synchronized的锁范围仅限于单个JVM进程。在集群环境下你的应用可能部署在多个服务器节点上每个节点都有自己的JVM实例。这时候就会出现节点A的线程1和线程2竞争同一把锁有效节点B的线程3和线程4竞争同一把锁有效但节点A和节点B之间的线程完全无法感知对方的存在锁失效关键点分布式锁的核心诉求是要找到一个所有节点都能访问的公证人用它来协调跨进程的锁竞争。2. 主流分布式锁方案对比方案实现原理优点缺点适用场景MySQL锁基于唯一索引或排他锁实现简单性能差死锁风险高低并发传统系统ZooKeeper锁临时顺序节点Watch机制可靠性高部署复杂性能一般金融等强一致场景Redis锁SETNX过期时间Lua脚本性能极高实现简单需处理锁续期问题互联网高并发场景Redis分布式锁之所以成为主流选择主要因为性能可达10万 QPS丰富的原子操作指令内置Lua脚本支持保证复杂操作的原子性3. Redis分布式锁的进阶实现3.1 基础版SETNX实现最基础的Redis锁实现只需要两个命令# 加锁NX表示不存在才设置EX设置过期时间 SET lock_key unique_value NX EX 30 # 解锁需要先判断value再删除 if redis.call(GET,KEYS[1]) ARGV[1] then return redis.call(DEL,KEYS[1]) else return 0 end但这个方案存在几个致命缺陷锁过期但业务未完成设置30秒过期但业务执行需要60秒非原子性解锁判断value和删除是两个独立操作不可重入同一个线程无法重复获取锁3.2 增强版Redisson实现Redisson是Redis官方推荐的Java客户端它提供了开箱即用的分布式锁实现// 获取锁对象 RLock lock redisson.getLock(orderLock); try { // 尝试加锁最多等待100秒上锁后30秒自动解锁 boolean res lock.tryLock(100, 30, TimeUnit.SECONDS); if (res) { // 业务逻辑 } } finally { lock.unlock(); }Redisson通过看门狗机制解决了锁续期问题默认每10秒检查一次如果业务还在执行则延长锁时间客户端宕机时锁会自动过期释放避免死锁4. Lua脚本保证原子性操作Redis执行Lua脚本时具有原子性这是实现复杂锁逻辑的关键。下面是一个完整的秒杀示例-- KEYS[1]: 库存key -- KEYS[2]: 订单key -- ARGV[1]: 用户ID -- ARGV[2]: 商品ID -- 返回1-成功 0-失败 -- 检查库存 local stock tonumber(redis.call(GET, KEYS[1])) if stock 0 then return 0 end -- 检查是否已购买 local ordered redis.call(SISMEMBER, KEYS[2], ARGV[1]) if ordered 1 then return 0 end -- 扣减库存并记录订单 redis.call(DECR, KEYS[1]) redis.call(SADD, KEYS[2], ARGV[1]) return 1Java调用代码DefaultRedisScriptLong script new DefaultRedisScript(); script.setScriptText(luaScript); script.setResultType(Long.class); ListString keys Arrays.asList(stock:productId, orders:productId); Long result redisTemplate.execute(script, keys, userId, productId); if (result 1) { // 秒杀成功 } else { // 秒杀失败 }5. 生产环境最佳实践5.1 锁命名规范好的锁名称应该包含业务前缀避免不同业务冲突资源标识锁定具体哪条数据场景标识区分读锁/写锁等示例order:pay:lock:{orderId}5.2 锁超时时间设置建议采用动态超时策略设置基础超时时间如30秒启动后台线程定期检查并延长锁时间业务完成时主动释放private void scheduleLockRenewal(String lockKey, String lockValue, long timeout) { ScheduledExecutorService executor Executors.newSingleThreadScheduledExecutor(); executor.scheduleAtFixedRate(() - { if (redisTemplate.opsForValue().get(lockKey).equals(lockValue)) { redisTemplate.expire(lockKey, timeout, TimeUnit.SECONDS); } }, timeout/3, timeout/3, TimeUnit.SECONDS); }5.3 集群部署注意事项在Redis Cluster环境下使用Redisson的RedLock算法需要多数节点获取成功或者考虑使用主从架构持久化配置// RedLock实现 RLock lock1 redissonInstance1.getLock(lock); RLock lock2 redissonInstance2.getLock(lock); RLock lock3 redissonInstance3.getLock(lock); RedissonRedLock lock new RedissonRedLock(lock1, lock2, lock3); lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); }6. 性能优化技巧锁粒度控制尽可能减小锁的范围错误做法锁定整个秒杀系统正确做法只锁定特定商品ID分段锁将库存拆分为多个段// 将100个库存拆分为10个段 int segment productId.hashCode() % 10; String lockKey stock: productId : segment;本地缓存分布式锁先检查本地缓存中的库存只有可能成交时才申请分布式锁异步日志处理// 使用Disruptor等高性能队列 EventQueueOrderEvent queue new DisruptorQueue(); queue.publish(new OrderEvent(userId, productId));在实际电商秒杀场景中我曾用这套方案将系统承载能力从原来的500QPS提升到2万QPS。关键点在于Redis锁只用于保证核心库存计算的原子性其他非核心逻辑全部异步化处理。