背景:
由于公司新项目是做成微服务的(SpringBoot + Dubbo),而关于更新用户积分(代名词),就是关于赏金的、钱的必须是要高一致性的。简单点来说,就是每次只允许一个更新用户积分的操作,但是由于我们项目是做成微服务的,每个服务分成很多块,那么我们可以怎么控制每次只能一个服务去执行更新用户积分的操作呢,我们想到分布式锁。我知道的现在主要做分布式锁有两种方式,一种是基于Redis的分布式锁,另外一种是基于Zookeeper的分布式锁。
分析:
简单分析一下上面两种锁的优缺点:从可靠性上来说,Zookeeper分布式锁有好于Redis分布式锁;而从性能上来说,Redis分布式锁要好于Zookeeper分布式锁,毕竟Redis是纯内存操作的,性能是想当的好,号称每秒可以处理10万次读写操作呢。那么因为我们的app必须是要性能好的(yy有上十万用户,啊哈哈),而且我们一直是使用Redis来做缓存的,所以我们最终选择了基于Redis的分布式锁。
实现过程:
1、首先加入依赖Redis的客户端依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
2、一开始的想法:是先获取锁,然后再加上过期时间,这个过期时间是为了防止程序崩溃而导致锁没有得到释放,最后造成死锁了。放上代码。
public static void tryLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
// 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
jedis.expire(lockKey, expireTime);
}
}
setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。但是这里有一个问题:我们获取锁和设置过期时间是两条命令,这将不具备原子性!怎么说,就是如果当你成功获取锁后,正准备给锁加一个过期时间,但是在添加过期时间前,Redis实例崩溃了,这将导致锁没有了过期时间,最后会造成死锁。。。。
3、正确的使用方法是使用set指令,因为set 指令有非常复杂的参数,这个可以同时把 setnx 和 expire 合成一条指令来用的。上代码:
public static boolean trydLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
if ("ok".equals(result)) {
return true;
}
return false;
}
下面讲解一下set命令的五个参数。
- 第一个lockKey,这个不用说了,key就是拿来当锁用,是唯一的。
- 第二个requestId,这个是关乎到可靠性的,在解锁里面我们需要用到,解锁的方式我是看上网搜的,为了达到原子性,推荐使用的是lua脚本,里头需要用到这个,古话也有说:解铃还须系铃人,谁加的锁谁自己释放,别人释放莫得用。
- 第三个NX,其实就是像上面的setnx命令一样,获取锁前会先判断锁是否存在
- 第四个PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。
- 第五个expireTime,就是上面说到的过期时间了,单位是毫秒
4、加锁的代码已经没问题了,下面我们就是解锁代码。
其实解锁也要做到原子性和可靠性,所以如果直接使用del(lockKey)那肯定是傻逼的做法了,如果是先获取锁再删除也是不行的,如果这时这把锁刚好被其他客户端拿到,那就变成误解锁了。那么有什么方案呢,我是上网看到别的大神做的,是使用lua脚本,让redis使用eval()方法执行解锁脚本就完事了,那为什么这就是能做到原子性呢,我也不知道,那是Redis的特性了,大家可以自己上网搜,下面先放大神的代码:
private static final Long RELEASE_SUCCESS = 1L;
/**
* 释放分布式锁
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
究极进化:
事实上这类琐最大的缺点就是它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换,那么就会出现锁丢失的情况:
-
在Redis的master节点上拿到了锁;
-
但是这个加锁的key还没有同步到slave节点;
-
master故障,发生故障转移,slave节点升级为master节点;
-
导致锁丢失。
正因为如此,Redis作者antirez基于分布式环境下提出了一种更高级的分布式锁的实现方式:Redlock
下面是我看到的写得很好的文章:Redisson实现Redis分布式锁的N种姿势 Redlock:Redis分布式锁的实现
之后,我就使用Redisson来搞基于Redis分布式锁了,非常的简单好使。 而到此,感谢你的阅读,也希望能帮到你,哪怕一点点~