【Redis】Redisson分布式锁原理

目录

[一. Redisson是什么?](#一. Redisson是什么?)

[二. Redisson锁能干活,全靠Redis这3个本事](#二. Redisson锁能干活,全靠Redis这3个本事)

[三. 核心流程:加锁、续期、解锁](#三. 核心流程:加锁、续期、解锁)

[1. 加锁:怎么保证只有一个线程能抢到锁?](#1. 加锁:怎么保证只有一个线程能抢到锁?)

[2. 续期:看门狗机制](#2. 续期:看门狗机制)

[3. 解锁 :怎么正确释放锁?](#3. 解锁 :怎么正确释放锁?)

[四. Redisson分布式锁的避坑点](#四. Redisson分布式锁的避坑点)

[五. 总结](#五. 总结)


一. Redisson是什么?

先澄清一个误区:Redisson不是新的中间件,它就是一个操作Redis的Java工具(客户端),基于Netty做的,速度很快。它的核心作用,就是把Redis里复杂的分布式锁操作,封装成了简单的Java代码------咱们不用记Redis的命令,不用写复杂的逻辑,调用它的API就能实现分布式锁,就像用本地的Lock一样简单。

Redisson里最核心的就是RLock这个接口,它和咱们本地用的Lock用法差不多,学起来特别简单。但它背后的逻辑,比咱们想的要严谨,这也是它能避免很多坑的原因。

这里贴一段咱们实际开发中最常用的Redisson锁代码,先有个直观感受:

java 复制代码
// 1. 获取Redisson客户端(项目里一般是配置好的,直接注入)
RedissonClient redissonClient = Redisson.create();
// 2. 获取一把分布式锁(锁的名字自己定义,比如"order:lock:123",唯一就行)
RLock lock = redissonClient.getLock("order:lock:123");
try {
    // 3. 加锁:默认30秒过期,也能自己设置,比如lock.lock(60, TimeUnit.SECONDS)
    lock.lock();
    // 4. 执行业务逻辑(比如修改订单状态、扣减库存,这部分是咱们自己的代码)
    doBusiness();
} finally {
    // 5. 解锁:必须放在finally里,防止业务报错,锁没释放
    lock.unlock();
}

就是这么简单!一行lock()加锁,一行unlock()解锁,剩下的底层逻辑,Redisson全帮咱们搞定了。接下来,咱们就扒一扒这两行代码背后,Redisson到底做了什么。

二. Redisson锁能干活,全靠Redis这3个本事

Redisson分布式锁,本质上是靠Redis实现的,没有Redis,它也玩不转。主要依赖Redis的3个核心能力:

  1. Redis是单线程干活:Redis同一时间只执行一个命令,不会出现两个命令同时执行的情况。这就天然保证了,同一时刻只有一个线程能抢到锁,不会出现"两个人同时抢到锁"的尴尬。
  2. 锁能自动过期:给锁设置一个过期时间(比如30秒),就算持有锁的线程崩溃了、网络断了,过了这个时间,锁会自动消失,不会一直占着资源,避免了"死锁"(锁一直没人放,其他线程都抢不到)。
  3. 一堆命令能一次性执行完:Redisson的加锁、解锁这些操作,都是用Lua脚本写的。Lua脚本能把多个Redis命令打包,要么全部执行成功,要么全部失败,不会出现"执行了一半卡住"的情况。比如"检查锁是否存在→创建锁",这两步能一次性完成,避免了"两个线程同时检查到锁不存在,同时创建锁"的问题。

除此之外,Redisson还做了个优化:把Lua脚本缓存起来,不用每次都传输完整脚本,能节省时间、提升速度,尤其是在多台Redis组成的集群里,效果更明显。

三. 核心流程:加锁、续期、解锁

Redisson分布式锁的核心,就是"加锁→续期→解锁"这三步。

1. 加锁:怎么保证只有一个线程能抢到锁?

咱们平时调用的lock()方法,底层最终会调用RedissonLock类的lockInterruptibly()方法(核心加锁方法),再往下走,会调用tryAcquire()方法,尝试获取锁。这里贴tryAcquire()的核心源码:

java 复制代码
// 核心加锁方法:尝试获取锁,leaseTime是过期时间,unit是时间单位
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
    // 1. 如果设置了过期时间,直接调用tryLockInnerAsync(真正执行加锁的方法)
    if (leaseTime != -1) {
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    // 2. 如果没设置过期时间(用默认30秒),先尝试加锁
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    // 3. 加锁成功后,启动"看门狗"(续期用的),后面会讲
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        if (e == null) {
            if (ttlRemaining == null) {
                scheduleExpirationRenewal(threadId);
            }
        }
    });
    return ttlRemainingFuture;
}

// 真正执行加锁的方法:调用Lua脚本,和咱们之前讲的Lua逻辑一致
private <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisCommand<T> command) {
    // 转换过期时间为毫秒
    long ttl = unit.toMillis(leaseTime);
    // 返回Lua脚本的执行结果,这里就是调用Redis执行Lua脚本
    return evalWriteAsync(getName(), LongCodec.INSTANCE, command,
            "if (redis.call('exists', KEYS[1]) == 0) then " +
                "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                "return nil; " +
            "end; " +
            "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; " +
            "return redis.call('pttl', KEYS[1]);",
            Collections.singletonList(getName()), ttl, getLockName(threadId));
}

逐行解读源码,不用懂复杂语法:

  • tryAcquireAsync方法:就是"尝试获取锁"的核心,接收三个参数------过期时间(leaseTime)、时间单位(unit)、当前线程ID(threadId)。

  • 第1步:如果咱们自己设置了过期时间(比如lock(60, TimeUnit.SECONDS)),就直接调用tryLockInnerAsync方法,真正去执行加锁。

  • 第2步:如果没设置过期时间,就用Redisson默认的30秒过期时间(getLockWatchdogTimeout()就是默认30秒),先尝试加锁。

  • 第3步:加锁成功后,启动"看门狗"(scheduleExpirationRenewal方法),作用是"续期"------防止业务没执行完,锁就过期了,后面会详细讲。

  • tryLockInnerAsync方法:真正执行加锁的逻辑,本质就是调用Redis执行咱们之前讲的Lua脚本,参数对应关系很简单: - KEYS[1]:锁的名字(getName()获取,就是咱们自己定义的"order:lock:123"); - ARGV[1]:过期时间(ttl,转换为毫秒); - ARGV[2]:线程身份证(getLockName(threadId),就是UUID+线程ID,比如"abc123:456")。

这段源码其实就是把咱们之前讲的"加锁逻辑",用Java代码实现了一遍,核心还是Lua脚本的原子性,保证只有一个线程能抢到锁。这里再强调3个关键设计,彻底解决咱们自己写锁的坑:

  • 不会抢乱:因为Lua脚本是一次性执行完的,不会出现"你检查锁不存在,正要创建,别人先创建了"的情况,保证了只有一个人能抢到锁。

  • 自己能多次抢锁(可重入):比如你的线程抢到锁后,又需要调用另一个需要同一把锁的方法,这时候不用等自己释放锁,直接再抢一次就行,计数会加1;解锁的时候,计数减1,直到计数为0,才真正把锁删掉------和本地锁的逻辑一样,很灵活。

  • 不会删别人的锁:每个线程都有自己的"身份证"(UUID+线程ID),只有持有锁的线程,才能操作这把锁,别人就算想删,也删不了,避免了误删别人锁的问题。

2. 续期:看门狗机制

很多人会有疑问:如果我的业务逻辑比较复杂,执行时间超过了锁的过期时间(比如默认30秒),怎么办?这时候锁会自动过期,当锁一过期的时候,就给了其他重试获取锁的线程可乘之机,它们会抢到锁执行自己的操作,导致数据错乱。

Redisson早就想到了这个问题,自带了"看门狗"机制(Watch Dog),核心作用就是:只要线程还持有锁,就会每隔一段时间(默认10秒),把锁的过期时间刷新回30秒,直到线程释放锁。

核心源码:

java 复制代码
// 启动看门狗,续期用的
private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    // 把当前线程的续期信息,存到本地缓存里
    EXPIRATION_RENEWAL_MAP.put(getEntryName(), entry);
    // 启动一个定时任务,每隔10秒执行一次,刷新锁的过期时间
    entry.task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            // 调用续期的Lua脚本,把锁的过期时间刷新回30秒
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    // 续期失败,移除本地缓存,停止续期
                    EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                    return;
                }
                if (res) {
                    // 续期成功,继续启动下一个定时任务,循环续期
                    scheduleExpirationRenewal(threadId);
                } else {
                    // 续期失败,移除本地缓存
                    cancelExpirationRenewal(threadId);
                }
            });
        }
    }, getLockWatchdogTimeout() / 3, TimeUnit.MILLISECONDS);
}

// 续期的核心方法:调用Lua脚本,刷新过期时间
private RFuture<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                "return 1; " +
            "end; " +
            "return 0;",
            Collections.singletonList(getName()), getLockWatchdogTimeout(), getLockName(threadId));
}

大白话解读看门狗机制:

  • 当咱们调用lock()方法(不设置过期时间)时,Redisson会自动启动看门狗,也就是scheduleExpirationRenewal方法。

  • 看门狗会启动一个定时任务,每隔10秒(默认30秒/3)执行一次,调用renewExpirationAsync方法,执行续期的Lua脚本。

  • 续期的Lua脚本逻辑很简单:检查当前锁是不是当前线程持有,如果是,就把锁的过期时间刷新回30秒,返回1(续期成功);如果不是,返回0(续期失败)。

  • 只要续期成功,就会继续启动下一个定时任务,循环续期;如果续期失败(比如锁已经被释放了),就停止续期,移除本地缓存。

  • 注意:如果咱们自己设置了过期时间(比如lock(60, TimeUnit.SECONDS)),Redisson不会启动看门狗,锁到期后会自动释放------因为你已经明确指定了锁的存活时间,Redisson默认你能把控业务执行时间。

3. 解锁 :怎么正确释放锁?

解锁的逻辑和加锁对应,核心是"只有持有锁的线程,才能释放锁",而且要处理"可重入"的情况(计数减1,直到为0才删除锁)。咱们平时调用的unlock()方法,底层调用的是RedissonLock类的unlockAsync()方法:

java 复制代码
// 核心解锁方法
public RFuture<Void> unlockAsync(long threadId) {
    // 调用解锁的Lua脚本,返回解锁结果
    RFuture<Boolean> future = unlockInnerAsync(threadId);
    future.onComplete((opStatus, e) -> {
        // 解锁成功后,停止看门狗续期
        cancelExpirationRenewal(threadId);
        if (e != null) {
            throw new CompletionException(e);
        }
        // 如果返回null,说明解锁失败(不是锁的持有者)
        if (opStatus == null) {
            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + getNodeId() + " thread-id: " + threadId);
        }
    });
    return future;
}

// 真正执行解锁的方法:调用Lua脚本
private RFuture<Boolean> unlockInnerAsync(long threadId) {
    return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then " +
                "return nil; " +  // 不是锁的持有者,返回null,解锁失败
            "end; " +
            "local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); " +  // 重入计数减1
            "if (counter > 0) then " +
                "redis.call('pexpire', KEYS[1], ARGV[1]); " +  // 计数还大于0,刷新过期时间
                "return 1; " +  // 解锁成功(只是计数减1,没删除锁)
            "else " +
                "redis.call('del', KEYS[1]); " +  // 计数为0,删除锁
                "return 1; " +  // 解锁成功(删除锁)
            "end;",
            Collections.singletonList(getName()), getLockWatchdogTimeout(), getLockName(threadId));
}

解锁源码:

  • unlockAsync方法:核心是调用unlockInnerAsync方法,执行解锁的Lua脚本,然后停止看门狗续期(cancelExpirationRenewal方法)。

  • 解锁的Lua脚本逻辑,分3步: 1. 先检查当前线程是不是锁的持有者(hexists判断),如果不是,返回null,解锁失败,抛出异常(比如你试图解锁别人的锁); 2. 如果是持有者,就把重入计数减1(hincrby -1); 3. 如果计数减1后还大于0(说明线程还在重入,没执行完所有业务),就刷新锁的过期时间,返回1(解锁成功,但没删除锁);如果计数为0(说明线程所有业务都执行完了),就删除锁(del命令),返回1(解锁成功,删除锁)。

  • 这里有个坑:一定要在finally里调用unlock()!如果业务逻辑报错,没执行到unlock(),锁就会一直被持有(虽然有看门狗续期,但线程崩溃后,看门狗也会停止,锁会过期释放,但会有延迟),可能导致其他线程一直抢不到锁。

四. Redisson分布式锁的避坑点

  • 必须在finally里解锁:不管业务逻辑有没有报错,都要释放锁,避免锁泄露(锁一直被持有,其他线程抢不到)。

  • 不要混用普通锁和红锁:如果用了红锁,就全程用红锁的API,不要和普通锁混用,否则会导致锁失效。

  • 设置合理的过期时间:如果自己设置过期时间,一定要比业务执行时间长,避免业务没执行完,锁就过期了;如果业务执行时间不确定,就用默认的30秒,依赖看门狗续期。

  • 避免锁的粒度太大:比如不要给整个"订单模块"加一把锁,应该给每个订单加一把锁(比如"order:lock:123",123是订单ID),这样不同订单的线程可以同时执行,提升并发性能。

  • Redis集群要保证高可用:Redisson锁依赖Redis,所以Redis集群一定要做好高可用(比如主从复制、哨兵模式),避免Redis挂了,整个分布式锁失效。

五. 总结

Redisson分布式锁的核心逻辑:基于Redis的单线程、过期机制和Lua脚本原子性,封装了加锁、续期、解锁的逻辑,还提供了可重入、看门狗、红锁、公平锁、读写锁等实用功能,让我们能"开箱即用"。

相关推荐
A.A呐5 小时前
【QT第五章】系统相关
开发语言·qt
QCzblack5 小时前
BugKu BUUCTF ——Reverse
java·前端·数据库
Orange_sparkle5 小时前
learn claude code学习记录-S02
java·python·学习
李白你好5 小时前
Java GUI-未授权漏洞检测工具
java·开发语言
leo__5206 小时前
拉丁超立方抽样(Latin Hypercube Sampling, LHS)MATLAB实现
开发语言·matlab
sycmancia6 小时前
Qt——Qt中的标准对话框
开发语言·qt
aq55356006 小时前
四大编程语言对比:PHP、Python、Java、易语言
java·python·php
橙露6 小时前
Python 对接 API:自动化拉取、清洗、入库一站式教程
开发语言·python·自动化
Omigeq6 小时前
1.4 - 曲线生成轨迹优化算法(以BSpline和ReedsShepp为例) - Python运动规划库教程(Python Motion Planning)
开发语言·人工智能·python·算法·机器人