Redisson分布式锁在Java应用中的使用

a. Spring 应用

通过Maven引入依赖

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.14.0</version>
</dependency>

配置相关的Bean

创建配置类的Bean:

Config config = new Config();
config.useClusterServers()
       // use "rediss://" for SSL connection
      .addNodeAddress("redis://127.0.0.1:7181");

创建 Redisson 实例:

// 同步与异步API
RedissonClient redisson = Redisson.create(config);

此外还有Reactive API和RxJava2 API相关的客户端,具体查看Redisson在GitHub上的说明

b. Spring Boot 应用

通过Maven引入依赖

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

注意,如果通过其他方式引入了redisson-spring-data模块,则需要根据Spring Boot的版本,调整redisson-spring-data的版本,具体的版本适配见这里

在application.properties中添加配置

基本的Redis配置(其中host、port、password必须配置):

spring:
  redis:
    database: 
    host:
    port:
    password:
    ssl: 
    timeout:
    cluster:
      nodes:
    sentinel:
      master:
      nodes:

根据需要添加Redisson相关的配置:

  # path to config - redisson.yaml
  redisson: 
    file: classpath:redisson.yaml
    config:
      clusterServersConfig:
        idleConnectionTimeout: 10000
        connectTimeout: 10000
        timeout: 3000
        retryAttempts: 3
        retryInterval: 1500
        failedSlaveReconnectionInterval: 3000
        failedSlaveCheckInterval: 60000
        password: null
        subscriptionsPerConnection: 5
        clientName: null
        loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
        subscriptionConnectionMinimumIdleSize: 1
        subscriptionConnectionPoolSize: 50
        slaveConnectionMinimumIdleSize: 24
        slaveConnectionPoolSize: 64
        masterConnectionMinimumIdleSize: 24
        masterConnectionPoolSize: 64
        readMode: "SLAVE"
        subscriptionMode: "SLAVE"
        nodeAddresses:
        - "redis://127.0.0.1:7004"
        - "redis://127.0.0.1:7001"
        - "redis://127.0.0.1:7000"
        scanInterval: 1000
        pingConnectionInterval: 0
        keepAlive: false
        tcpNoDelay: false
      threads: 16
      nettyThreads: 32
      codec: !<org.redisson.codec.FstCodec> {}
      transportMode: "NIO"

获取并使用Redisson客户端

通过上述配置,就可以在代码中通过自动装配,直接获取并使用RedissonClientRedisTemplate/ReactiveRedisTemplate 等Bean了。

二、使用Redisson分布式锁

用锁的一般步骤:

  1. 获取锁实例(只是获得一把锁的引用,并不是占有锁)
  2. 通过锁实例加锁(占有了这把锁)
  3. 通过锁实例释放锁

Redisson提供很多种类型的锁,其中最常用的就是可重入锁(Reentrant Lock)了。

Redisson中的可重入锁

1. 获取锁实例

RLock lock = redissonClient.getLock(String lockName);

获取的锁实例实现了RLock接口,而该接口扩展了JUC包中的Lock接口,以及异步锁接口RLockAsync

2. 通过锁实例加锁

同步异步特性来区分,加锁方法可分为同步加锁和异步加锁两类。异步加锁方法的名称一般是在相应的同步加锁方法后加上“Async”后缀。

阻塞非阻塞特性来区分,加锁方法可分为阻塞加锁和非阻塞加锁两类。非阻塞加锁方法的名称一般是“try”开头。

下面以比较常用的同步加锁方法来说明加锁的一些细节。

阻塞加锁的方法:

  1. void lock(): (JUC中Lock接口定义的方法)如果当前锁可用,则加锁成功,并立即返回;如果当前锁不可用,则阻塞等待直至锁可用,然后返回。
  2. void lock(long leaseTime, TimeUnit unit): 加锁机制与void lock()相同,只是增加了锁的有效(租赁)时长leaseTime。加锁成功后,可以在程序中显式调用unlock()方法进行释放;如果未显式释放,则经过leaseTime时间,该锁会自动释放。如果leaseTime传入-1,则会一直持有,直至调用unlock()

非阻塞加锁的方法:

  1. boolean tryLock(): (JUC中Lock接口定义的方法)调用该方法会立刻返回。返回值为true则表示锁可用,加锁成功;返回值为false则表示锁不可用,加锁失败。
  2. boolean tryLock(long time, TimeUnit unit): (JUC中Lock接口定义的方法)如果锁可用则立刻返回true,否则最多等待time长的时间(如果time<=0,则不会等待)。在time时间内锁可用则立刻返回true,time时间之后返回false。如果在等待期间线程被其他线程中断,则会抛出nterruptedException 异常。
  3. boolean tryLock(long waitTime, long leaseTime, TimeUnit unit): 与boolean tryLock(long time, TimeUnit unit)类似,只是增加了锁的使用(租赁)时长leaseTime。

3. 通过锁实例释放锁

  1. void unlock(): 释放锁。如果当前线程是锁的持有者(即在该锁实例上加锁成功的线程),则会释放成功,否则会抛出异常。

一般编程范式

同步阻塞加锁

String lockName = ...
RLock lock = redissonClient.getLock(lockName);
// 阻塞式加锁
lock.lock();
try {
    // 操作受锁保护的资源

} finally {
    // 释放锁
    lock.unlock();
}

同步非阻塞加锁

String lockName = ...
RLock lock = redissonClient.getLock(lockName);
if (lock.tryLock()) {
    try {
        // 操作受锁保护的资源
    } finally {
        lock.unlock();
    }
} else {
    // 执行其他业务操作
}

三、分布式锁分析

优秀的分布式锁需要具备以下特性:

  • 互斥性:在任意时刻,只有一个客户端(线程)能持有锁,这是锁的基本要求。
  • 锁的可重入:同一个客户端能多次持有同一把锁。实现上只要检查锁的持有者是否为当前客户端,若是则重入锁成功,并将锁的持有数加1。一般通过给每个客户端分配一个唯一的ID,并在加锁成功时向锁中写入该ID即可。
  • 不会因客户端异常而长久锁住:当客户端在持有锁期间崩溃而未主动解锁时,锁也会在一定时间后自动释放,即锁有超时自动释放的特性。
  • 解锁的安全性:加锁和解锁必须是同一个客户端,客户端不能把别人加的锁给释放了,即不能误解锁。实现上与锁的可重入类似,在释放锁时检查客户端ID与锁中保存的ID是否一致即可。

Redisson的分布式锁除了实现上述几个特性外,还具有锁的自动续期功能。即当我们加锁而未指定锁的有效时长时,Redisson会按一定的周期,定时检查当前线程是否活跃,若是则自动为锁续期,这一特性称为watchdog(看门狗)机制。

有了这个特性,我们就可以不必为设定锁的有效时间而纠结了(设得太长,则会在客户端崩溃后仍长时间占有锁;设得太短,则可能在业务逻辑执行完成前,锁自动释放),Redisson分布式锁可以在客户端崩坏时自动释放,业务逻辑未执行完时自动续期。

用 Redis 实现分布式锁的最佳实践

假设没有Redisson,需要我们自己用Redis实现分布式锁,以下是一些不错的编程实践:

  • set 命令要用set key value px milliseconds nx,替代 setnx + expire 需要分两次执行命令的方式,保证原子性。
  • 客户端的ID一般保存在线程本地变量(ThreadLocal)中。客户端ID要求全局唯一,可以考虑【IP+线程ID】组合,或者用UUID。
  • 阻塞式子加锁可用Object对象的wait+notify机制实现。
  • 释放锁时,需要检查当前客户端是否为锁的持有者,因此有compare and set的逻辑。为了保证两个操作的原子性,用单个Lua脚本来执行多个Redis操作(利用了eval命令执行Lua脚本的原子性,参考这里这里)。另外,如果锁有自动续期的定时任务,在解锁的时候需要停掉该任务。

其他主题(TODO)

  • 锁的高可用(容错性):只要大多数Redis节点正常运行,客户端就能够获取和释放锁(参考这里)。
  • 死锁问题:当因客户端编程逻辑问题导致两个线程死锁时,如何检测并解决死锁问题?
  • Redisson中其他类型的锁(参考这里
  • 锁的续期机制分析(参考这里
  • Redisson 支持4种链接redis的方式:Cluster(集群)、Sentinel servers(哨兵)、Master/Slave servers(主从)、Single server(单机)(参考这里
  • 自己动手实现注解式加锁(参考这里

参考文档

  1. Redisson的GitHub仓库
  2. redisson-spring-boot-starter 使用说明
  3. Redisson: Distributed locks and synchronizers
  4. JDK & Redisson 中的源码注释
  5. 慢谈 Redis 实现分布式锁 以及 Redisson 源码解析
  6. 使用Redisson实现分布式锁
  7. Redisson 实现分布式锁原理分析(对Redisson的源码进行分析)
  8. Redis官方文档:[用Redis构建分布式锁](http://ifeve.com/redis-lock/)
  9. Redisson分布式锁实现
  10. redisson实现分布式锁原理