一、jedis/luttuce/redisson关系
在redis官网推荐的三大框架就是:jedis、lettuce,redission。
1、jedis
-
jedis使用阻塞的I/O,是同步的,即当jedis与redis数据库建立连接后,只有当连接释放后才允许下一次的连接
-
jedis客户端实例API非线程安全,需要通过连接池来使用jedis
-
jedis是redis的java实现的客户端,,其API提供了比较全面的redis命令的支持,jedis的每个方法底层都是对redis的单个命令的封装
2、
lettuce
-
lettuce底层基于netty,使用异步非阻塞I/O
-
lettuce客户端实例API是线程安全的,可通过操作单个lettuce连接来完成各种操作
-
spring-boot-starter-data-redis默认使用的lettuce作为redis客户端
3、redisson
-
redisson使用异步非阻塞I/O,且基于netty框架的事件驱动通信层
-
redisson的API是线程安全的,可以操作单个redisson连接来完成各种操作
-
redisson对的方法基本进行了较高的抽象,每个方法可能封装了一个或多个redis命令方法
二、使用redisson实现分布式锁原因
常见的分布式锁,基本是通过Redis或Zookeeper实现,其中通过redis实现最常见。
1、redis实现分布式锁的简单思路
redis分布式锁的实现思路可以概括为:在redis中设置一个值表示加了锁,然后获取到该锁后即可进行一系列业务逻辑操作,最后删除这个key(或设置过期时间)即表示释放了该锁。
2、redis原生实现方法
2.1、加锁
通过命令
SET key value [EX seconds] [PX milliseconds] [NX|XX] 的组合, Redis 中实现锁的简单方法一般如下
//如果不用以下命令,先设置了值,再设置过期时间,这个不是原子性操作,有可能在设置过期时间之前宕机,会造成死锁(key永久存在)
SET key value NX EX max-lock-time
其中参数意义如下:
-
EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
-
PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
-
NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
-
XX :只在键已经存在时,才对键进行设置操作。
客户端执行以上的命令:
-
如果服务器返回 OK ,那么这个客户端获得锁。
-
如果服务器返回 NIL ,那么客户端获取锁失败,可以在稍后再重试。
同时通过以下修改,可以让锁的实现更健壮:
-
不使用固定的字符串作为键的值,而是设置一个不可猜测(non-guessable)的长随机字符串,作为口令串(token)。
-
不使用 DEL 命令来释放锁,而是发送一个 Lua 脚本,这个脚本只在客户端传入的值和键的口令串相匹配时,才对键进行删除。
这两个改动可以
防止持有过期锁的客户端误删现有锁的情况出现,如:
假设A获取了锁,过期时间30s,此时35s之后,锁已经自动释放了,A去释放锁,但是此时可能B获取了锁。A客户端就不能删除B的锁了
2.2、释放锁
释放锁涉及到两条指令即get和del,但这两条指令不是原子性的;所以,为保证原子性操作,需要用到redis的lua脚本执行,lua脚本是原子性的。
释放锁时,可通过以下脚本
if redis.call("get",KEYS[1] == ARGV[1])
then
return redis.call("del",KEYS[1])
else
return 0
end
2.3、组合加锁解锁
组合在一起即
// 获取锁
// NX是指如果key不存在就成功,key存在返回false,EX可以指定过期时间
SET lock_key unique_value NX EX 30
// 释放锁涉及到两条指令,这两条指令不是原子性的
// 需要用到redis的lua脚本支持特性,redis执行lua脚本是原子性的
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
上述脚本可以通过 EVAL script 1 key value 命令来调用。
通过redis原生编码实现分布式锁,除了需要考虑客户端怎么实现外,还需要考虑redis的部署问题等等,需要注意的细节很多,一般没有大牛的中小公司,很难顾全。
3、redisson分布式锁的优势
Redisson作为一款优秀的企业级开源redis client,也提供了分布式锁的实现,并且屏蔽了很多细节的处理,减少了一般公司程序员的工作,提供了较稳定的技术方案。
-
redisson所有指令都通过lua脚本执行,保证了操作的原子性
-
redisson设置了watchdog看门狗,“看门狗”的逻辑保证了没有死锁发生
举个例子,如:
redisson设置一个key的默认过期时间为30s,如果某个客户端持有一个锁超过了30s怎么办?
因为有看门狗的处理,它会在获取锁之后,每隔10秒把key的超时时间设为30s;
这样的话,就算一直持有锁也不会因为出现key过期,导致其他线程获取到锁的问题;
同时,如果机器宕机了,看门狗也就没了。此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程可以获取到锁。
三、Redisson分布式锁的实现
1、新建一个 spring-boot项目,pom文件引入以下依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--使用lettuce连接池必须-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.0</version>
</dependency>
2、yaml中添加redis相关配置
spring:
redis:
host: 127.0.0.1
port: 6379
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 100
# 连接池中的最小空闲连接
max-idle: 10
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1
# 连接超时时间(毫秒)
timeout: 5000
#默认是索引为0的数据库
database: 2
password: 123456
3、配置redisson
package com.he.comm.redis;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* <b>@Desc</b>: redisson 配置
* <b>@Author</b>: hesh
* <b>@Date</b>: 2020/6/27
* <b>@Modify</b>:
*/
@Configuration
public class RedissonConfig {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Value("${spring.redis.password:}")
private String password;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
//单节点
config.useSingleServer().setAddress("redis://" + host + ":" + port).setPassword(password);
// //主从模式配置
// config.useMasterSlaveServers().setMasterAddress("").setPassword("").addSlaveAddress(new String[]{"",""});
// //集群模式
// config.useClusterServers().setScanInterval(2000).addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001");
return Redisson.create(config);
}
}
4、编写分布式锁
4.1、通用接口
package com.he.comm.redis;
import org.redisson.api.RCountDownLatch;
import org.redisson.api.RSemaphore;
import java.util.concurrent.TimeUnit;
/**
* <b>@Desc</b>: 分布式锁,T-锁类型
* <b>@Author</b>: hesh
* <b>@Date</b>: 2020/6/27
* <b>@Modify</b>:
*/
public interface DistributedLocker<T> {
/**
* 加锁
* @param lockKey
* @return T
*/
T lock(String lockKey);
/**
* 带超时的锁
* @param lockKey
* @param expireTime 超时释放时间,单位:秒
* @return T
*/
T lock(String lockKey, int expireTime);
/**
* 带超时的锁
* @param lockKey
* @param unit 时间单位
* @param expireTime 超时释放时间
* @return T
*/
T lock(String lockKey, TimeUnit unit, int expireTime);
/**
* 尝试获取锁
* @param lockKey
* @param unit 时间单位
* @param waitTime 最多等待时间
* @param expireTime 超时释放时间
* @return boolean
*/
boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int expireTime);
/**
* 释放锁
* @param lockKey
*/
void unlock(String lockKey);
/**
* 释放锁
* @param lock
*/
void unlock(T lock);
/**
* 获取计数器
* @param name
* @return
*/
RCountDownLatch getRCountDownLatch(String name);
/**
* 获取信号量
* @param name
* @return
*/
RSemaphore getRSemaphore(String name);
}
4.2、redisson锁实现类
package com.he.comm.redis;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* <b>@Desc</b>: redisson实现的分布式锁
* <b>@Author</b>: hesh
* <b>@Date</b>: 2020/6/27
* <b>@Modify</b>:
*/
@Component
public class RedisDistributedLocker implements DistributedLocker<RLock> {
@Autowired
private RedissonClient redissonClient;
@Override
public RLock lock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock();
return lock;
}
@Override
public RLock lock(String lockKey, int expireTime) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(expireTime, TimeUnit.SECONDS);
return lock;
}
@Override
public RLock lock(String lockKey, TimeUnit unit, int expireTime) {
RLock lock = redissonClient.getLock(lockKey);
lock.lock(expireTime, unit);
return lock;
}
@Override
public boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int expireTime) {
RLock lock = redissonClient.getLock(lockKey);
try {
return lock.tryLock(waitTime, expireTime, unit);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
@Override
public void unlock(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
//防止线程不持有锁的情况下,在finally调用unlock释放锁,报IllegalMonitorStateException错,增加判断
// java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: eb987c01-9f60-4612-9a87-2ec8186128f1 thread-id: 124
// 是否还是锁定状态
if (lock.isLocked()) {
// 是否为当前执行线程持有的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
@Override
public void unlock(RLock lock) {
//防止线程不持有锁的情况下,在finally调用unlock释放锁,报IllegalMonitorStateException错,增加判断
// java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: eb987c01-9f60-4612-9a87-2ec8186128f1 thread-id: 124
if (lock.isLocked()) {
// 是否为当前执行线程持有的锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
// @Override
// public RCountDownLatch getRCountDownLatch(String name) {
// return redissonClient.getCountDownLatch(name);
// }
// @Override
// public RSemaphore getRSemaphore(String name) {
// return redissonClient.getSemaphore(name);
// }
}
4.3、工具类
package com.he.comm.redis;
import org.redisson.api.RCountDownLatch;
import org.redisson.api.RLock;
import org.redisson.api.RSemaphore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* <b>@Desc</b>: redis分布式锁工具类
* <b>@Author</b>: hesh
* <b>@Date</b>: 2020/6/27
* <b>@Modify</b>:
*/
@Component
public class RedisLockUtil{
@Autowired
private DistributedLocker<RLock> distributedLocker;
/**
* 加锁
* @param lockKey
* @return RLock
*/
public RLock lock(String lockKey) {
return distributedLocker.lock(lockKey);
}
/**
* 带超时的锁
* @param lockKey
* @param expireTime 超时释放时间,单位:秒
* @return RLock
*/
public RLock lock(String lockKey, int expireTime) {
return distributedLocker.lock(lockKey,expireTime);
}
/**
* 带超时的锁
* @param lockKey
* @param unit 时间单位
* @param expireTime 超时释放时间
* @return RLock
*/
public RLock lock(String lockKey, TimeUnit unit, int expireTime) {
return distributedLocker.lock(lockKey, unit, expireTime);
}
/**
* 尝试获取锁
* @param lockKey
* @param unit 时间单位
* @param waitTime 最多等待时间
* @param expireTime 超时释放时间
* @return boolean
*/
public boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int expireTime) {
return distributedLocker.tryLock(lockKey, unit, waitTime, expireTime);
}
/**
* 释放锁
* @param lockKey
*/
public void unlock(String lockKey) {
distributedLocker.unlock(lockKey);
}
/**
* 释放锁
* @param lock
*/
public void unlock(RLock lock) {
distributedLocker.unlock(lock);
}
/**
* 获取计数器
* @param name
* @return
*/
// public RCountDownLatch getRCountDownLatch(String name) {
// return distributedLocker.getRCountDownLatch(name);
// }
/**
* 获取信号量
* @param name
* @return
*/
// public RSemaphore getRSemaphore(String name) {
// return distributedLocker.getRSemaphore(name);
// }
}
4.4、编写测试controller
通过多线程模拟分布式多实例部署,高并发业务场景
package com.he.comm.controller;
import com.he.comm.redis.RedisLockUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* <b>@Desc</b>: 测试redis分布式锁
* <b>@Author</b>: hesh
* <b>@Date</b>: 2020/6/28
* <b>@Modify</b>:
*/
@Slf4j
@RequestMapping("/redis_lock")
@RestController
public class TestRedisLockController {
@Autowired
private RedisLockUtil redisLockUtil;
private int lockCount = 10;//有锁共享变量
private int unlockCount = 10;//无锁共享变量
@GetMapping("/test")
public void test(@RequestParam Integer threadnum) {
//模拟并发测试加锁和不加锁,观察日志打印
if (threadnum == null || threadnum == 0) threadnum = 10;
for (int i = 0; i < threadnum; i++) {
new Thread(() -> {
//无锁
testUnlockCount();
//有锁
testLockCount();
}).start();
}
}
private void testUnlockCount() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
unlockCount--;
log.info("--unlockCount 值:{}", unlockCount);
}
private void testLockCount() {
String lockKey = "lock-test";
try {
// 加锁,设置超时时间2s
redisLockUtil.lock(lockKey, 2);
Thread.sleep(100);
lockCount--;
log.info("--lockCount值: {}", lockCount);
} catch (Exception e) {
e.printStackTrace();
} finally {
redisLockUtil.unlock(lockKey);
}
}
}
4.5、测试结果
调用API
http://localhost:8060/redis_lock/test?threadnum=10
后,日志打印如下,可见加了分布式锁的,变量有序减少;未加锁的,则出现不规律错乱。

到此,简单的redisson实现分布式锁编码完毕。
参考文章:
1、分布式锁用 Redis 还是 Zookeeper?
2、Redisson是如何实现分布式锁的?
3、SET — Redis 命令参考