资讯动态

基于Redis的分布式锁

发布时间:2026/10/15 3:54:25 来源:尧图企业网站定制
分布式锁介绍Synchronized 只能保证单 JVM 内的互斥多集群下就会失效因此需要一种多进程可见的锁具体要求有JVM 外部、互斥、高可用、高并发且安全。目前主流实现分布式锁的方案是 Redis作为独立部署的中间件天然在 JVM 外部其可用性和性能都优于 MySQL 和 ZooKeeper安全性在多数场景下也可接受。常规使用流程是 SetNX 获取锁并设置超时自动释放配合 Lua 脚本保证解锁等组合操作的原子性。分布式锁主要存在以下问题不可重入同一线程在未释放锁的情况下再次获取锁会被自己的锁挡在门外无重试原生SetNX一次性尝试阻塞等待与重试需开发者手动设置误删锁长期阻塞的锁会因为TTL到期被手动释放进程执行完毕后可能释放其他进程的锁主从不一致异步执行的主从复制可能因宕机未完成导致锁被破坏。Redission原生方案需要手动处理续期、重入计数、安全解锁等细节代码量不小且容易写错。针对这些问题Java 生态中已有成熟的封装方案——Redisson。它把上述原生短板作为内置能力提供是目前 Java 使用 Redis 实现分布式锁的主流选择。Redission快速入门引入依赖dependencygroupIdorg.redisson/groupId!-- SpringBoot 项目使用--!-- artifactIdredisson-spring-boot-starter/artifactId--artifactIdredisson/artifactIdversion4.7.0/version/dependency简单使用实例如下// 1.构建配置ConfigconfignewConfig();// 单节点模式config.useSingleServer().setAddress(redis://127.0.0.1:6379).setPassword(123456).setDatabase(0).setConnectionMinimumIdleSize(5).setConnectionPoolSize(10);// 2.创建实例RedissonClientredissonClientRedisson.create(config);// 3. 获取可重入锁示例RLockrlockredissonClient.getLock(demo:lock:test);// 4. 尝试获取锁try{booleanoklock.tryLock(5,TimeUnit.SECONDS);}catch(InterruptedExceptione){thrownewRuntimeException(e);}解决原生方法的不足可重入锁存在时比较加锁的线程id相等则计数1重置过期时间底层tryAcquire → tryAcquireAsync → tryLockInnerAsync源码如下/** * 尝试获取锁的异步底层方法执行Lua脚本原子实现【首次加锁 可重入加锁】逻辑 */TRFutureTtryLockInnerAsync(longwaitTime,longleaseTime,TimeUnitunit,longthreadId,RedisStrictCommandTcommand){// evalWriteSyncedNoRetryAsync执行Lua脚本写类型操作不自动重试returnthis.evalWriteSyncedNoRetryAsync(this.getRawName(),// Redis锁key的原始名称对应Lua里 KEYS[1]LongCodec.INSTANCE,// 编解码器用来序列化/反序列化返回值command,// Lua脚本开始 if ((redis.call(exists, KEYS[1]) 0) or (redis.call(hexists, KEYS[1], ARGV[2]) 1)) then --条件锁不存在OR锁存在并且是当前线程持有的锁可重入场景redis.call(hincrby, KEYS[1], ARGV[2], 1); --hincrbyhash中【当前线程标识】对应的计数1--首次加锁field不存在自动创建值1--同一线程重入计数1实现可重入计数redis.call(pexpire, KEYS[1], ARGV[1]); --设置/刷新锁的过期时间毫秒每次加锁/重入都会重置过期时间return nil; --返回nil代表加锁成功首次成功 或 重入成功end; return redis.call(pttl, KEYS[1]);,--走到这里锁被其他线程占用返回锁剩余过期时间ttl毫秒// Lua脚本结束 // KEYS数组传给Lua脚本的KEYS参数Collections.singletonList(this.getRawName()),// ARGV数组传给Lua脚本的ARGV参数newObject[]{unit.toMillis(leaseTime),// ARGV[1]leaseTime转成毫秒锁过期时长this.getLockName(threadId)// ARGV[2]锁持有者标识 Redisson客户端唯一ID : threadId// 用来区分是哪个客户端的哪个线程持有锁实现可重入判断});}自动重试信号量和PubSub实现等待唤醒获取到锁释放信号后才尝试抢锁否则阻塞。currentSystem.currentTimeMillis();// 根据线程id订阅频道CompletableFutureRedissonLockEntrysubscribeFuturethis.subscribe(threadId);// 等待订阅操作异步完成超时时间time成功进入后续抢锁逻辑try{subscribeFuture.get(time,TimeUnit.MILLISECONDS);}do{// 循环重试抢锁为null说明成功返回ttlthis.tryAcquire(waitTime,leaseTime,unit,threadId);// 抢锁失败扣除本次tryAcquire调用消耗的时间time-System.currentTimeMillis()-currentTime;// 计算时间阻塞获取锁((RedissonLockEntry)this.commandExecutor.getNow(subscribeFuture)).getLatch().tryAcquire(ttl,TimeUnit.MILLISECONDS);}while(time0L);超时续约避免超时导致误删其他进程的锁采用看门狗watch dog判断锁归属属于当前线程就重置超时时间protectedRFutureBooleanrenewExpirationAsync(longthreadId){returnevalWriteAsync(getName(),LongCodec.INSTANCE,RedisCommands.EVAL_BOOLEAN,// Lua脚本if (redis.call(hexists, KEYS[1], ARGV[2]) 1) then // 锁存在且属于当前线程 → 重置过期时间为30sredis.call(pexpire, KEYS[1], ARGV[1]); return 1; end; return 0;,Collections.singletonList(getName()),internalLockLeaseTime,// ARGV[1] 30000msgetLockName(threadId));// ARGV[2] UUID:threadId}主从一致性主机刚加锁成功宕机从机未获取到锁状态通过联锁MultiLock解决多个key必须同时锁住使用示例如下// 获取多个独立锁不同keyRLocklockAccountAredisson.getLock(account:A);RLocklockAccountBredisson.getLock(account:B);// 构造联锁MultiLockRLockmultiLockredisson.getMultiLock(lockAccountA,lockAccountB);总结Redission 自动解决分布式锁场景下不可重入、无重试机制、业务超时误删锁以及主从不一致的问题。解决思路分别为不可重入判断线程id计数器记录重入次数重试发布订阅模式监听锁状态锁释放重试抢锁超时误删看门狗比较锁持有者定时任务自动续约主从一致联锁保证锁不可部分失败。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑