redisson可重入锁

今天学习redisson锁,在学习之前,先做好准备工作。先掌握如何使用,再来学习原理。redisson的红锁已经过时了,废弃的原因我就不多说了。redisson的锁分单锁和多锁,多锁是将多个锁捆绑在一起统一管理,红锁就是多锁的一种实现。而今天我要学习的可重入锁是个单锁。

实验步骤

启动redis

我是在微软的WSL上安装了一个redis服务器,所以我启动就比较容易了。

console 复制代码
hope@hope:~$ sudo service redis-server status
[sudo] password for hope:
 * redis-server is not running
hope@hope:~$ sudo service redis-server start
Starting redis-server: redis-server.
hope@hope:~$ sudo service redis-server status
 * redis-server is running

接下来就是写java程序了。我使用maven来搭建这个实验程序。

配置依赖

xml 复制代码
    <dependencies>

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.24.2</version>
            <exclusions>
                <exclusion>
                    <groupId>org.slf4j</groupId>
                    <artifactId>slf4j-api</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>3.2.6</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>6.1.8</version> <!-- 例如 5.3.21 -->
        </dependency>
    </dependencies>

控制器代码

java 复制代码
@RestController
public class LockController {

    @Autowired
    private RedissonClient redissonClient;

    @GetMapping("/lock")
    public String lock() {

        RLock lock= redissonClient.getLock("register-lock");

        try {

            boolean isLocked = lock.tryLock(0, 30, TimeUnit.SECONDS);

            if (!isLocked) {
                // 获取锁失败,快速失败,不要阻塞
                return "获取锁失败,请稍后重试";
            }
            // 业务代码
            System.out.println(System.currentTimeMillis()+","+Thread.currentThread()+"正在操作资源");
            Thread.currentThread().join(10000); // 模拟业务耗时
            System.out.println(System.currentTimeMillis()+","+ Thread.currentThread() +"操作资源结束");
            return "got lock";
        } catch (InterruptedException e) {
            return "执行中断";
        } finally {
            if (lock != null && lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    
}

配置文件我没加,所以使用的是默认的配置localhost:6379,并且是没有密码的单机模式。
需要注意的是,测试加锁是要用两个不同的浏览器,在同一个浏览器开多个窗口,浏览器会顺序请求。


加锁原理

redisson先执行一个lua脚本。该脚本内容为:

lua 复制代码
if ((redis.call('exists', KEYS[1]) == 0) or (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]);

该代码位于RedissonLock#tryLockInnerAsync方法中。其中参数如下:

  • KEYS[1]是锁的名称
  • ARGV[1]是锁释放时间
  • ARGV[2]是当前线程名称
    这说明redisson的锁是用redis的hash来实现的。使用hincrby来自增以统计锁重入次数,使用pexpire来设置过期时间。

等待原理

tryAcquire方法里waitTime参数直接被忽略。上层调用是tryLock,在tryLock里有一段无限循环代码:

java 复制代码
while (true) {
    long currentTime = System.currentTimeMillis();
    ttl = tryAcquire(waitTime, leaseTime, unit, threadId);
    // lock acquired
    if (ttl == null) {
        return true;
    }

    time -= System.currentTimeMillis() - currentTime;
    if (time <= 0) {
        acquireFailed(waitTime, unit, threadId);
        return false;
    }

    // waiting for message
    currentTime = System.currentTimeMillis();
    if (ttl >= 0 && ttl < time) {
        commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
    } else {
        commandExecutor.getNow(subscribeFuture).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
    }

    time -= System.currentTimeMillis() - currentTime;
    if (time <= 0) {
        acquireFailed(waitTime, unit, threadId);
        return false;
    }
}

学过编程的都知道,如果在无限循环里不时线程进入waiting或block状态的话,CPU使用率会非常高。这段无限循环代码不至于CPU飙升的核心就在于getLatch()返回了一个信号量Semaphore对象,并且使用信号量的tryAcquire方法来避免CPU使用率飙升。使用无限循环是因为抢占锁的极可能不止一个线程,如果还失败了就继续等待。从信号量的阻塞时间来看,直接是阻塞了剩余时间,那么肯定要有一个唤醒机制,否则一直沉睡,等待就没有意义。如果缺少一个唤醒机制,那么等待就永远只是等待,拿不到锁。

所以redisson的锁订阅了一个redis的频道,在收到释放锁的订阅消息时,把信号量释放掉,这样阻塞的线程就得到了释放,执行无限循环的下一个动作。在LockPubSub的订阅回调里有这么一段代码:

java 复制代码
@Override
protected void onMessage(RedissonLockEntry value, Long message) {
    if (message.equals(UNLOCK_MESSAGE)) {
        Runnable runnableToExecute = value.getListeners().poll();
        if (runnableToExecute != null) {
            runnableToExecute.run();
        }

        value.getLatch().release();
    } else if (message.equals(READ_UNLOCK_MESSAGE)) {
        while (true) {
            Runnable runnableToExecute = value.getListeners().poll();
            if (runnableToExecute == null) {
                break;
            }
            runnableToExecute.run();
        }

        value.getLatch().release(value.getLatch().getQueueLength());
    }
}

释放原理

redisson锁释放有两种方式:

  • 异步释放
  • 同步释放

redisson为了复用代码,同步释放只是把异步释放过程同步执行,也就是直接调用get方法去阻塞等待异步方法完成。redisson释放锁要做至少两件事情:

  • 修改redis服务器上锁对应的hash值
  • 发布锁释放消息

现在就是仔细分析这两个功能在源码哪里实现的。首先在RedissonLockunlockInnerAsync里有一段超级长的lua代码:

lua 复制代码
local val = redis.call('get', KEYS[3]); 
if val ~= false then 
	return tonumber(val);
end; 
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then
	return nil;
end;
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); 
if (counter > 0) then
	redis.call('pexpire', KEYS[1], ARGV[2]); 
	redis.call('set', KEYS[3], 0, 'px', ARGV[5]); 
	return 0; 
else 
	redis.call('del', KEYS[1]); 
	redis.call(ARGV[4], KEYS[2], ARGV[1]); 
	redis.call('set', KEYS[3], 1, 'px', ARGV[5]); 
	return 1; 
end; 

这里我详细解释下参数的含义:

  • KEYS[1],是锁的名称;
  • KEYS[2],是订阅频道的名称;
  • KEYS[3],是解锁的Latch名称;
  • ARGV[1],恒为0,表示释放;
  • ARGV[2],为锁释放时间;
  • ARGV[3],当前现场ID;
  • ARGV[4],发布消息命令;
  • ARGV[5],超时时间。
    从这段代码中可以看到如果因为递归,让锁的重入次数增加,那么需要逐步解锁,直到计数器为0时才会完全释放锁。同时删除锁和发布消息时在一起的,因为处于同一个脚本中,所以是原子性的。
相关推荐
__万波__9 小时前
二十三种设计模式(二十)--解释器模式
java·设计模式·解释器模式
网安_秋刀鱼9 小时前
【java安全】反序列化 - CC1链
java·c语言·安全
零度@9 小时前
Java消息中间件-Kafka全解(2026精简版)
java·kafka·c#·linq
钱多多_qdd9 小时前
springboot注解(二)
java·spring boot·后端
2501_941877139 小时前
在法兰克福企业级场景中落地零信任安全架构的系统设计与工程实践分享
开发语言·php
Cosmoshhhyyy9 小时前
《Effective Java》解读第32条:谨慎并用泛型和可变参数
java·python
帅气的你10 小时前
面向Java程序员的思维链(CoT)提示词写法学习指南
java
leiming610 小时前
c++ QT 开发第二天,用ui按钮点亮实体led
开发语言·qt·ui
2501_9418824810 小时前
在开普敦跨区域部署环境中构建高可靠分布式配置中心的设计思路与实现实践
开发语言·c#
一只小小Java10 小时前
Java面试场景高频题
java·开发语言·面试