Redis分布式锁从入门到精通从SETNX到Redisson看门狗机制引言1. 分布式锁的核心要求2. 基于Redis的简易分布式锁实现2.1 第一阶段SETNX EXPIRE有问题2.2 第二阶段SET原子操作正确基础版2.3 第三阶段安全解锁Lua脚本2.4 完整基础代码示例Java版2.5 基础版流程图3. 基础版存在的问题4. 工业级方案Redisson分布式锁4.1 Redisson快速入门4.2 核心特性5. Redisson源码深度剖析5.1 加锁核心Lua脚本5.2 加锁流程图5.3 看门狗机制自动续期5.4 可重入实现原理6. 分布式锁的扩展形态6.1 读写锁ReadWriteLock6.2 公平锁FairLock6.3 红锁RedLock7. 最佳实践与避坑指南7.1 合理设置过期时间7.2 务必在finally中释放锁7.3 监控与告警8. 总结对比核心观点The Begin点点关注收藏不迷路引言在分布式系统中多个服务节点同时操作共享资源时传统的单机锁如synchronized、ReentrantLock就失效了。如何保证同一时刻只有一个节点能执行关键代码分布式锁就是解决方案。Redis凭借其高性能和原子操作成为实现分布式锁的首选工具。本文将深入剖析Redis分布式锁的实现原理、常见坑点以及工业级解决方案Redisson的源码级解析。1. 分布式锁的核心要求在设计分布式锁时必须满足以下四个条件要求说明互斥性同一时刻只有一个客户端能持有锁死锁预防持有锁的客户端崩溃或网络异常锁能自动释放容错性大多数Redis节点正常运行时客户端仍可加解锁解铃还须系铃人加锁和解锁必须是同一个客户端不能释放别人的锁2. 基于Redis的简易分布式锁实现2.1 第一阶段SETNX EXPIRE有问题最原始的想法用SETNX加锁再用EXPIRE设过期时间。# 1. 加锁SETNX lock_key1# 2. 设置过期时间防止死锁EXPIRE lock_key30问题所在SETNX和EXPIRE是两个命令非原子性。如果SETNX成功后客户端崩溃EXPIRE没执行就导致死锁2.2 第二阶段SET原子操作正确基础版Redis 2.6.12提供了SET命令的扩展参数解决了原子性问题SET lock_key unique_value NX PX30000参数说明NX只有key不存在时才设置实现互斥PX 30000设置过期时间30秒防止死锁unique_value客户端唯一ID如UUID用于安全释放锁2.3 第三阶段安全解锁Lua脚本为什么释放锁要用Lua脚本因为要确保当前客户端只能释放自己加的锁不能误删别人的锁。-- 解锁Lua脚本ifredis.call(get,KEYS[1])ARGV[1]thenreturnredis.call(del,KEYS[1])elsereturn0end2.4 完整基础代码示例Java版publicclassRedisDistributedLock{privateJedisjedis;privateStringlockKey;privateStringuniqueValue;privateintexpireTime;// 毫秒publicbooleantryLock(){// SET key value NX PX expireTimeStringresultjedis.set(lockKey,uniqueValue,NX,PX,expireTime);returnOK.equals(result);}publicbooleanunlock(){StringluaScriptif redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end;Objectresultjedis.eval(luaScript,Collections.singletonList(lockKey),Collections.singletonList(uniqueValue));returnLong.valueOf(1).equals(result);}}2.5 基础版流程图RedisClientBClientARedisClientBClientA业务执行完成SET lock UUID_A NX PX 30000OK (加锁成功)SET lock UUID_B NX PX 30000(nil) 加锁失败执行业务逻辑...EVAL 解锁脚本 (UUID_A)检查valueUUID_Adel成功SET lock UUID_B NX PX 30000OK (加锁成功)3. 基础版存在的问题上述实现虽然正确但在生产环境中仍有几个痛点锁超时释放业务执行时间超过过期时间锁自动释放导致并发问题不可重入同一线程无法多次获取同一把锁不支持等待获取不到锁直接返回失败没有阻塞等待机制无续期机制无法在业务执行过程中延长锁的有效期4. 工业级方案Redisson分布式锁Redisson是Redis官方推荐的Java分布式锁实现完美解决了上述所有问题。4.1 Redisson快速入门dependencygroupIdorg.redisson/groupIdartifactIdredisson/artifactIdversion3.20.0/version/dependencyConfigurationpublicclassRedissonConfig{BeanpublicRedissonClientredissonClient(){ConfigconfignewConfig();config.useSingleServer().setAddress(redis://127.0.0.1:6379).setPassword(123456).setConnectionPoolSize(10);returnRedisson.create(config);}}ServicepublicclassOrderService{AutowiredprivateRedissonClientredissonClient;publicvoidcreateOrder(LongorderId){// 1. 获取分布式锁RLocklockredissonClient.getLock(order:orderId);try{// 2. 加锁支持自动续期lock.lock(30,TimeUnit.SECONDS);// 3. 执行业务逻辑doBusiness();}finally{// 4. 释放锁lock.unlock();}}}4.2 核心特性特性说明可重入同一线程可多次获取同一把锁内部计数器维护自动续期看门狗机制业务未完成自动延长过期时间阻塞等待支持tryLock带超时时间的等待公平锁支持FIFO顺序获取锁读写锁读读共享、读写互斥、写写互斥5. Redisson源码深度剖析5.1 加锁核心Lua脚本Redisson的加锁逻辑通过Lua脚本实现原子性源码如下TRFutureTtryLockInnerAsync(longleaseTime,TimeUnitunit,longthreadId,RedisStrictCommandTcommand){returncommandExecutor.evalWriteAsync(getName(),LongCodec.INSTANCE,command,// 1. 判断锁是否存在if (redis.call(exists, KEYS[1]) 0) then redis.call(hincrby, KEYS[1], ARGV[2], 1); redis.call(pexpire, KEYS[1], ARGV[1]); return nil; end; // 2. 判断是否是当前线程持有if (redis.call(hexists, KEYS[1], ARGV[2]) 1) then redis.call(hincrby, KEYS[1], ARGV[2], 1); redis.call(pexpire, KEYS[1], ARGV[1]); return nil; end; // 3. 其他线程持有返回剩余过期时间return redis.call(pttl, KEYS[1]);,Collections.singletonList(getName()),unit.toMillis(leaseTime),getLockName(threadId));}数据结构说明Redisson使用Hash结构存储锁信息key: 锁名称如order:123field: 线程标识如UUID:threadIdvalue: 重入计数5.2 加锁流程图否是是否收到信号调用lock方法执行Lua加锁脚本锁是否存在?创建Hash,重入计数1,设置过期时间是否是当前线程?重入计数1,重置过期时间返回锁剩余过期时间加锁成功订阅锁释放Channel等待锁释放信号启动看门狗定期续期5.3 看门狗机制自动续期看门狗是Redisson最核心的特性解决了业务执行超时导致锁自动释放的问题。privatevoidscheduleExpirationRenewal(longthreadId){ExpirationEntryentrynewExpirationEntry();EXPIRATION_RENEWAL_MAP.put(getEntryName(),entry);// 定时任务每10秒执行一次TimeouttaskcommandExecutor.getConnectionManager().newTimeout(newTimerTask(){Overridepublicvoidrun(Timeouttimeout)throwsException{// 续期Lua脚本重置锁过期时间为30秒RFutureBooleanfuturerenewExpirationAsync(threadId);future.onComplete((res,e)-{if(res){// 续期成功递归调用继续续期scheduleExpirationRenewal(threadId);}});}},internalLockLeaseTime/3,TimeUnit.MILLISECONDS);// 默认30秒/310秒执行一次entry.setTimeout(task);}看门狗工作流程客户端加锁成功默认过期时间30秒启动后台线程每10秒检查一次如果锁仍被当前线程持有执行续期Lua脚本重置过期时间为30秒业务执行完毕主动释放锁取消看门狗如果客户端崩溃看门狗停止续期锁30秒后自动释放5.4 可重入实现原理通过Hash结构的计数器实现可重入# 第一次加锁HSETorder:123clientA:thread11PEXPIREorder:12330000# 同一线程再次加锁HINCRBYorder:123clientA:thread11# 计数变为2PEXPIREorder:12330000# 重置过期时间# 释放锁HINCRBYorder:123clientA:thread1-1# 计数减1# 计数0保留锁# 计数0删除key6. 分布式锁的扩展形态Redisson还提供了多种高级锁类型6.1 读写锁ReadWriteLock适用于读多写少的场景提升并发性能。RReadWriteLockrwLockredissonClient.getReadWriteLock(data-lock);RLockreadLockrwLock.readLock();// 读锁RLockwriteLockrwLock.writeLock();// 写锁// 读锁可被多个线程同时持有readLock.lock();// 写锁独占writeLock.lock();6.2 公平锁FairLock保证先等待的线程先获得锁。RLockfairLockredissonClient.getFairLock(fair-lock);fairLock.lock();// 按请求顺序分配6.3 红锁RedLock多Redis节点部署时的高可用方案需要大多数节点同意才能加锁。7. 最佳实践与避坑指南7.1 合理设置过期时间// 错误过期时间太短业务可能未完成lock.lock(5,TimeUnit.SECONDS);// 正确使用默认30秒配合看门狗自动续期lock.lock();// 或者根据业务评估设置足够长时间lock.lock(60,TimeUnit.SECONDS);7.2 务必在finally中释放锁RLocklockredissonClient.getLock(key);lock.lock();try{// 业务逻辑}finally{// 确保一定释放if(lock.isLocked()lock.isHeldByCurrentThread()){lock.unlock();}}7.3 监控与告警ComponentpublicclassLockMonitor{privatestaticfinalMeterRegistryregistrynewSimpleMeterRegistry();// 监控锁等待时间privateTimerlockWaitTimerTimer.builder(lock.wait.time).register(registry);// 监控锁持有时间privateTimerlockHoldTimerTimer.builder(lock.hold.time).register(registry);publicTTexecuteWithMonitor(StringlockKey,SupplierTsupplier){RLocklockredissonClient.getLock(lockKey);longstartWaitSystem.currentTimeMillis();lock.lock();longwaitTimeSystem.currentTimeMillis()-startWait;lockWaitTimer.record(waitTime,TimeUnit.MILLISECONDS);longstartHoldSystem.currentTimeMillis();try{returnsupplier.get();}finally{lock.unlock();longholdTimeSystem.currentTimeMillis()-startHold;lockHoldTimer.record(holdTime,TimeUnit.MILLISECONDS);// 告警检查if(holdTime10000){// 持有超过10秒alertService.sendAlert(锁持有时间过长: lockKey);}}}}8. 总结对比方案优点缺点适用场景SETNX基础版实现简单无续期、不可重入学习测试手动Lua脚本原子操作功能单一简单场景Redisson功能完善、自动续期、可重入引入依赖生产环境首选核心观点原子性是基础加解锁必须用Lua或原子命令唯一标识是关键用UUID标识锁持有者防止误删看门狗是保障自动续期解决业务超时问题选型要慎重生产环境直接用Redisson不要重复造轮子The End点点关注收藏不迷路