当前位置: 代码网 > it编程>数据库>Redis > Redisson分布式锁原理深入分析

Redisson分布式锁原理深入分析

2026年04月26日 Redis 我要评论
一. redisson是什么?先澄清一个误区:redisson不是新的中间件,它就是一个操作redis的java工具(客户端),基于netty做的,速度很快。它的核心作用,就是把redis里复杂的分布

一. redisson是什么?

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

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

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

// 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()的核心源码:

// 核心加锁方法:尝试获取锁,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秒,直到线程释放锁。

核心源码:

// 启动看门狗,续期用的
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()方法:

// 核心解锁方法
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脚本原子性,封装了加锁、续期、解锁的逻辑,还提供了可重入、看门狗、红锁、公平锁、读写锁等实用功能,让我们能“开箱即用”。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。

(0)

相关文章:

  • Redis限流算法解析与实战教程

    Redis限流算法解析与实战教程

    一、经典限流算法的深度对比与选型建议算法核心思想适用场景推荐使用场景固定窗口时间分段统计,每段独立计数对性能要求极高、允许短时突增的简单限流日志上报、非核心接口... [阅读全文]
  • Redis Cluster部署实践

    Redis Cluster部署实践

    一、我们要做什么?在一台机器上(192.168.166.9)跑6 个 redis 实例:3 个 master(主节点):6379、6381、63833 个 sl... [阅读全文]
  • Redis 旁路缓存深度解析

    Redis 旁路缓存深度解析

    旁路缓存(cache-aside pattern)是 redis 最常用的缓存策略,通过"先查缓存,后查数据库"的读写模式,显著提升系统读取... [阅读全文]
  • Redis实现高效插入大量数据的三种方法

    Redis实现高效插入大量数据的三种方法

    一、引言在实际开发中,我们经常需要向redis批量写入大量数据,例如初始化缓存、导入历史数据、批量更新用户状态等。如果采用普通的set命令逐条插入,每次命令都需... [阅读全文]
  • Redis之十大数据类型解读

    一、常用key命令keys *查看当前库的所有keyexists key判断某个key是否存在type key查看key的类型del key删除指定keyunlink key非阻塞…

    2026年04月12日 数据库
  • Redis配置只读账号实现方式

    一、需求说明作为一名运维工程师,经常安装和配置redis,前阵子有测试同事申请开一个redis的只读账户,以往都是配置一个读写用户,并没有对redis的权限进行细化管理,实际上这种…

    2026年04月08日 数据库

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com