在上一篇播客 Spring boot 2.X 简单集成redis lettuce 已经简单集成的redis,客户端使用的是lettuce,接下来将尝试基于lettuce实现分布式锁。
分布式锁实现方式有多种,基于redis的分布式锁,数据库的乐观锁,基于Zookeeper的分布式锁。
我们要确保分布式锁可用,我们需要确保 :1、当一个线程获得锁之后,其他线程无法在获得。2、不会发生死锁问题。3、加锁和解锁必须是同一个线程。4、当redis只有部分节点可用时,也能确保加锁和解锁可以正常进行。
查询资料我们可以发现在Jedis组件中可以使用
jedis.set(String key, String requestId, String nxxx, String expx, int time)
第一个为key,我们使用key来当锁,因为key是唯一的。
第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,
分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。
requestId可以使用UUID.randomUUID().toString()方法生成。
第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;
第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
第五个为time,与第四个参数相呼应,代表key的过期时间。
总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,
同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。
基于lettuce 客户端实现示例:
public static boolean lockSet (String key,String requestId,long expiredTime) {
return template.execute (new RedisCallback <Boolean> () {
@Override
public Boolean doInRedis (
RedisConnection redisConnection ) throws DataAccessException {
Object nativeConnection = redisConnection.getNativeConnection();
RedisSerializer<String> serializer = template.getStringSerializer();
String status = null;
// lettuce连接包下 redis 单机模式setnx
if (nativeConnection instanceof RedisAsyncCommands) {
logger.debug("lettuce single:---setKey:"+key+"---value"+requestId+"---maxTimes:"+expiredTime);
status = ((RedisAsyncCommands ) nativeConnection).getStatefulConnection().sync()
.set(serializer.serialize (key), serializer.serialize(requestId), SetArgs.Builder.nx().ex(expiredTime));
logger.debug("lettuce single:---status:"+status);
}
//lettuce连接包,集群模式,ex为秒,px为毫秒
if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
logger.debug("lettuce Cluster:---setKey:"+key+"---value"+requestId+"---maxTimes:"+expiredTime);
status = ((RedisAdvancedClusterAsyncCommands ) nativeConnection).getStatefulConnection().sync()
.set(serializer.serialize (key), serializer.serialize(requestId),SetArgs.Builder.nx().ex(expiredTime));
logger.debug("lettuce Cluster:---status:"+status);
}
if ( "OK".equalsIgnoreCase (status)) {
return true;
}
return false;
}
});
}
(说明:代码中的template是基于上一篇博客中的实现[Spring boot 2.X 简单集成redis lettuce)
上面的实现,我们在测试的时候会发现,它并不能完全满足我们真正的需求,当锁的自动失效之后,可能存在多个线程都同时获得锁的情况,我们在查询资料后得知:
setIfAbsent相当于jedis中的setnx,如果能赋值就返回true,如果已经有值了,就返回false
又尝试了下面的一种实现,
//加锁
public static boolean lockKey(String key, String value) {
//即:在判断这个key是不是第一次进入这个方法
if (template.opsForValue().setIfAbsent(key, value)) {
//第一次,即:这个key还没有被赋值的时候
return true;
}
String oldTime = template.opsForValue().get(key);
if (StringUtils.isNotEmpty (oldTime) && Long.parseLong(oldTime) < System.currentTimeMillis()) {
String newTime = template.opsForValue().getAndSet(key, value);
if (StringUtils.isNotEmpty(oldTime) && oldTime.equals(newTime)) {
return true;
}
}
return false;
}
//解锁
public static boolean unlock(String key, String value) {
try {
if (StringUtils.equals (template.opsForValue().get(key),value) ){
return template.opsForValue().getOperations().delete(key);
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
这种方式可以解决在锁失效之后,多个线程可能会同时获得锁的情况,但是还是存在缺陷。
在一个线程A获得锁之后,由于一些原因A执行的时间超过了锁的失效时间,那么此时A还在执行,但是其他线程因为锁已经失效了,也可以获得锁。这是一个种极端情况,但是也可能发生。我们在设计锁的失效时间时需要多方面的考虑,计算出一个合理的失效时间。可以考虑做一个守护线程来更新这个失效时间,但这种操作也不能100%避免出错,一直更新失效时间可能造成死锁的情况发生。
本文仅是个人学习时的笔记,仅供参考,如有不对之处,欢迎指出。
参考
https://www.cnblogs.com/ClareZjy/p/10448791.html
https://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/