Redisson实现Redis分布式锁的底层原理

2023-05-24T07:08:29.png

在Spring中,使用Redis实现分布式锁的方式是通过Redisson框架。它提供了一种基于Java的简单易用的分布式锁解决方案,并且支持自动续期功能。

Redisson具有自动续期锁的功能,称为“看门狗”。该功能使用Redis的发布订阅机制来实现。当获取到锁之后,Redisson会启动一个看门狗线程,定时检查当前节点是否仍然持有该锁。如果持有,则会对锁进行延期操作;否则,将释放锁。这种方式的优点是可以保证在锁的过期时间内,锁始终处于被占用状态。

在Spring Boot中,我们可以很方便地集成Redisson框架,使用它提供的分布式锁和自动续期功能。

在pom.xml文件中添加Redisson依赖:

本文章中使用的是以下方式引入,该方式对低版本的springboot项目兼容性更好
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>${redisson.version}</version>
</dependency>
或
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>${redisson.version}</version>
</dependency>

然后在application.yml配置文件中增加Redisson的相关配置:

spring:
  redis:
    host: localhost
    port: 6379
    password: 123456
    database: 0

配置文件

@Configuration
public class RedissionConfig {
    @Value("${spring.redis.host}")
    private String redisHost;

    @Value("${spring.redis.port}")
    private String port;

    @Value("${spring.redis.password}")
    private String password;

    @Value("${spring.redis.database}")
    private int database;

    @Bean
    public RedissonClient getRedisson() {
        Config config = new Config();
        config.useSingleServer().
            setAddress(redisHost + ":" + port).
            setPassword(password).
            setConnectionPoolSize(16).
            setDatabase(database);
        config.setCodec(new JsonJacksonCodec());
        return Redisson.create(config);
    }
}

(可选)如需设置锁的公共前缀可再次封装Redisson对象。例如:

// 创建一个MyRedisson对象
public class MyRedisson extends Redisson {

    private String KeyPrefix;

    protected MyRedisson(Config config) {
        super(config);
        this.KeyPrefix = "";
    }

    protected MyRedisson(Config config, String KeyPrefix) {
        super(config);
        this.KeyPrefix = KeyPrefix;
    }

    public static RedissonClient create(Config config) {
        return new MyRedisson(config);
    }

    public static RedissonClient create(Config config, String KeyPrefix) {
        return new MyRedisson(config, KeyPrefix);
    }

    public RLock getLock(String name) {
        return super.getLock(KeyPrefix + name);
    }
}
然后将配置文件中的Redisson.create(config)替换成MyRedisson.create(config, "你期望的前缀(如redisson-lock:)")

最后,在Java代码中使用Redisson的RLock对象来使用分布式锁。例如:

@Service
public class UserService {
    @Autowired
    private RedissonClient redissonClient;

    public void doSomething() {
        RLock lock = redissonClient.getLock("some_key");
        try {
            if (lock.tryLock(10, TimeUnit.SECONDS)) {
                try {
                    // 执行业务逻辑
                } finally {
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            // 处理获取锁超时异常
        }
    }
}

在上述代码中,我们获取了一个名为“some_key”的RLock对象,并使用tryLock方法尝试获取锁。如果获取成功,则执行业务逻辑,并在释放锁之前保持锁的状态。在RLock对象内部,Redisson会创建一个看门狗线程,并定期检查锁的状态,自动续期锁的过期时间。

释放锁核心逻辑

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    /**
     * Redisson解锁:通过LUA脚本释放锁,保证多个命令之间的原子性
     */
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            "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]); " +
                    "return 0; " +
                    "else " +
                    "redis.call('del', KEYS[1]); " +
                    "redis.call('publish', KEYS[2], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return nil;",
            Arrays.asList(getRawName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

Redisson可重入锁的释放还是通过LUA脚本实现,保证多个命令之间的原子性。

基本流程:

1、通过hexists指令判断锁是不是自己的,如果不是自己的锁,说明非法释放锁,返回nil,

2、如果是自己的锁,通过hincrby将锁的重入次数减1;

3、判断减1后的数是否大于0,如果减1后的数大于0,说明还没有完全释放锁,则重置锁的过期时间,并返回0;

4、如果减1后的数已经等于0,说明已经完全释放锁,则通过del指令释放锁,并通过publish发布一条消息,告诉其它订阅了这把锁的线程,我已经释放锁了,你们可以过来获取了;释放锁成功,返回1

5、其它情况,返回nil;

主动释放锁这块考虑的不仅仅是对 key 进行处理,因为可能存在重入锁,所以会先对 redis key 对应的 hash value 进行递减,相当于减去重入次数。

redisson-spring-boot-starter方式引入需注意的问题(通过查询资料获知,未经过验证)

将 Redisson 与 Spring Boot 库集成。取决于Spring Data Redis模块,支持 Spring Boot 1.3.x - 2.4.x

这句话是官方说的,不过现在的2.5.x也是支持的,只需要注意springboot最低版本不要低于1.3.x即可。

redisson-spring-data与Spring Boot version的版本对应关系
2023-05-26T02:29:04.png
maven依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.1</version>
</dependency>

点击redisson-spring-boot-starter进去

org.redisson
redisson-spring-data-26
${project.version}
依赖的springboot版本为2.6

若与项目中版本不匹配
可如此处理

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.1</version>
    <exclusions>
        <exclusion>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-data-26</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-data-21</artifactId>
    <version>3.17.1</version>
</dependency>
参考文档:https://blog.csdn.net/liuerchong/article/details/124942903
最后修改:2023 年 07 月 26 日
如果觉得我的文章对你有用,请随意赞赏