注:该文是本博主记录学习之用,没有太多详细的讲解,敬请谅解!
一、背景
之前写过《JedisLock基于Redis实现分布式锁》的文章,从它的实现逻辑中你会发现存在一些问题(具体问题可以跳转过去看,故这里不作详细说明),所以本文基于Redis的官方文档的实现逻辑实现分布式锁。
二、实现分布式锁的原理
1、实现思想:
(1)获取锁的时候,使用Set命令加锁,这个命令仅在不存在key的时候才能被执行成功(NX选项),并且这个key有一个自动失效时间expire(PX属性)。这个key的值是“my_random_value”(一个随机值),这个值在所有的客户端必须是唯一的,所有同一key的获取者(竞争者)这个值都不能一样。
官方命令:SET resource_name my_random_value NX PX expire
(2)第二步是为了防止操作超时,锁过期,别人获得了锁,导致业务不一致。实现逻辑是通过key获取到value值跟当前的value值对比是否一致。一致则没有超时,不一致则表示操作超时导致了锁过期,别人获得锁,可以解决业务一致性问题。
(3)释放锁的时候,通过lua脚本告诉Redis只有key存在并且存储的值和我指定的值一样才能告诉我删除成功。
Lua脚本:
if redis.call('GET',KEYS[1]) == ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end
三、Maven依赖
注意这里使用的是3.1.0版本,每个版本实现不同。
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
四、代码实现
JedisLock锁工具类
package com.test.demo;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
import java.util.UUID;
/**
* @date: 2019/8/2 18:49
* @description: 实现分布式锁工具类
*/
public class JedisLock {
/**
* 客户端jedis
*/
private Jedis jedis;
/**
* 锁key
*/
private String key;
/**
* 锁超时时间,毫秒为单位
*/
private int expire = 5000;
/**
* 获取锁等待时间,毫秒为单位
*/
private int timeout = 3000;
/**
* 是否占有锁
*/
private volatile boolean locked = false;
/**
* 唯一标识
*/
private UUID uuid;
/**
* 线程等待时间
*/
private static final int DEFAULT_ACQUIRY_RESOLUTION_MILLIS = 100;
/**
* 删除key的lua脚本
*/
private static final String LUA_DEL_SCRIPT = "if redis.call('GET',KEYS[1]) == ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end";
public JedisLock(Jedis jedis,String key,int timeout,int expire){
this.jedis=jedis;
this.key=key;
this.timeout=timeout;
this.expire=expire;
this.uuid=UUID.randomUUID();
}
/**
* set值
* 说明:这个命令仅在不存在key的时候才能被执行成功(NX选项),并且这个key有一个自动失效时间(PX属性)
* 这个key的值是一个唯一值,这个值在所有的客户端必须是唯一的,所有同一key的获取者(竞争者)这个值都不能一样。
* @param value
* @return
*/
public String setNX(final String value){
SetParams setParams=new SetParams();
setParams.nx();
setParams.px(this.expire);
return this.jedis.set(this.key,value,setParams) ;
}
/**
* 获取锁
* @return
*/
public synchronized boolean lock() throws InterruptedException {
long timeout=this.timeout;
while (timeout>0){
//获取锁,返回OK则代表获取锁成功
if("OK".equals(this.setNX(this.getLockValue(Thread.currentThread().getId())))){
System.out.println("#######获得锁#######");
this.locked=true;
return true;
}
timeout -= DEFAULT_ACQUIRY_RESOLUTION_MILLIS;
Thread.sleep(DEFAULT_ACQUIRY_RESOLUTION_MILLIS);
}
return false;
}
/**
* 释放锁
* 说明:通过key和唯一value值删除
* @return
*/
public synchronized void release(){
if(this.locked){
System.out.println("#######释放锁#######");
long result= (long) this.jedis.eval(LUA_DEL_SCRIPT,1,this.key,this.getLockValue(Thread.currentThread().getId()));
if(result>0){
this.locked=false;
}
}
}
/**
* 判断当前线程是否还继续拥有锁
* 说明:该方法主要用来判断操作时间已经超过key的过期时间,可以用来做业务过滚
* @return
*/
public boolean checkTimeOut(){
if(this.locked){
String value=this.jedis.get(this.key);
if(this.getLockValue(Thread.currentThread().getId()).equals(value)){
return true;
}
}
return false;
}
/**
* 锁的value值(唯一值)
* 说明:value的值必须是唯一主要是为了更安全的释放锁,释放锁的时候使用脚本告诉Redis:只有key存在并且存储的值和我指定的值一样才能告诉我删除成功
* @param threadId
* @return
*/
public String getLockValue(Long threadId){
return this.uuid.toString()+"_"+threadId;
}
}
调用实现方法
package com.test.demo;
import redis.clients.jedis.Jedis;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Test {
public static volatile int num=0;
public static void main(String[] args) {
ThreadPoolExecutor executor=new ThreadPoolExecutor(10, 20, 20L,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(2), new ThreadPoolExecutor.AbortPolicy());
for (int i=0;i<10;i++){
executor.execute(()->{testLock();});
}
}
public static void testLock(){
Jedis jedis=new Jedis("127.0.0.1",6379);
JedisLock jedisLock=new JedisLock(jedis,"test",8000,3000);
try {
//第一步:获取锁
if(jedisLock.lock()){
num+=1;
//该步骤是为了验证操作超时
if(num==3){
Thread.sleep(5000);
}
System.out.println(num);
//第二步:验证是否操作超时
if(!jedisLock.checkTimeOut()){
System.out.println("######操作超时#######");
}
}
} catch (Exception e) {
e.printStackTrace();
}finally {
//第三步:释放锁
if(null!=jedisLock){
jedisLock.release();
}
if(jedis!=null){
jedis.close();
}
}
}
}
五、注意点
1、该分布式实现是基于单例Redis,依然无法解决Redis集群下出现数据同步问题。若想集群下实现分布式锁,可以利用Redis推荐的Redlock算法实现。
1、Redission实现分布式锁(官网推荐)
https://blog.csdn.net/weixin_43947588/article/details/90064064
2、Curator基于zookeeper实现分布式锁
https://blog.csdn.net/weixin_43947588/article/details/84893777
3、JedisLock实现分布式锁
https://blog.csdn.net/weixin_43947588/article/details/84977357