一、引入Redisson依赖,并配置相关的Bean
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客户端
通过上述配置,就可以在代码中通过自动装配,直接获取并使用RedissonClient
或RedisTemplate
/ReactiveRedisTemplate
等Bean了。
二、使用Redisson分布式锁
用锁的一般步骤:
- 获取锁实例(只是获得一把锁的引用,并不是占有锁)
- 通过锁实例加锁(占有了这把锁)
- 通过锁实例释放锁
Redisson提供很多种类型的锁,其中最常用的就是可重入锁(Reentrant Lock)了。
Redisson中的可重入锁
1. 获取锁实例
RLock lock = redissonClient.getLock(String lockName);
获取的锁实例实现了RLock
接口,而该接口扩展了JUC包中的Lock
接口,以及异步锁接口RLockAsync
。
2. 通过锁实例加锁
从同步与异步特性来区分,加锁方法可分为同步加锁和异步加锁两类。异步加锁方法的名称一般是在相应的同步加锁方法后加上“Async”后缀。
从阻塞与非阻塞特性来区分,加锁方法可分为阻塞加锁和非阻塞加锁两类。非阻塞加锁方法的名称一般是“try”开头。
下面以比较常用的同步加锁方法来说明加锁的一些细节。
阻塞加锁的方法:
void lock()
: (JUC中Lock接口定义的方法)如果当前锁可用,则加锁成功,并立即返回;如果当前锁不可用,则阻塞等待直至锁可用,然后返回。void lock(long leaseTime, TimeUnit unit)
: 加锁机制与void lock()
相同,只是增加了锁的有效(租赁)时长leaseTime。加锁成功后,可以在程序中显式调用unlock()
方法进行释放;如果未显式释放,则经过leaseTime时间,该锁会自动释放。如果leaseTime传入-1,则会一直持有,直至调用unlock()
。
非阻塞加锁的方法:
boolean tryLock()
: (JUC中Lock接口定义的方法)调用该方法会立刻返回。返回值为true则表示锁可用,加锁成功;返回值为false则表示锁不可用,加锁失败。boolean tryLock(long time, TimeUnit unit)
: (JUC中Lock接口定义的方法)如果锁可用则立刻返回true,否则最多等待time长的时间(如果time<=0,则不会等待)。在time时间内锁可用则立刻返回true,time时间之后返回false。如果在等待期间线程被其他线程中断,则会抛出nterruptedException 异常。boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)
: 与boolean tryLock(long time, TimeUnit unit)
类似,只是增加了锁的使用(租赁)时长leaseTime。
3. 通过锁实例释放锁
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(单机)(参考这里)
- 自己动手实现注解式加锁(参考这里)
参考文档
- Redisson的GitHub仓库
- redisson-spring-boot-starter 使用说明
- Redisson: Distributed locks and synchronizers
- JDK & Redisson 中的源码注释
- 慢谈 Redis 实现分布式锁 以及 Redisson 源码解析
- 使用Redisson实现分布式锁
- Redisson 实现分布式锁原理分析(对Redisson的源码进行分析)
- Redis官方文档:[用Redis构建分布式锁](http://ifeve.com/redis-lock/)
- Redisson分布式锁实现
- redisson实现分布式锁原理