前言:分布式锁的实现方式一般有三种,1:基于数据库的乐观锁。2:基于redis的分布式锁。3:基于zk的分布式锁,本文主要介绍第二种实现,由于以前一直是单机写笔记,所以第一次写有写的不好的地方欢迎大家指正。
网上对于redis分布式锁的实现各有不同,今天分享的这种,不确定是不是最好的,但是个人觉得最易懂,好了废话不多说,贴公司错误例子跟正确写法。
//错误例子
public class RedisLock {
private static final Logger logger = LoggerFactory.getLogger(RedisLockNew.class);
private static final String REDIS_KEY_PREFIX = "redis_key_";
private static final String SET_IF_NOT_EXIST = "NX";//key不存在时,我们进行set操作;若key已经存在,则不做任何操作
private static final String SET_WITH_EXPIRE_TIME = "PX";//给key加一个过期的设置
@Autowired
private JedisCluster jedisCluster;
/**
* @description redis锁
* @param key
* @param seconds 锁过期时间(单位:秒)
* @return
*/
public Boolean getLock(String key,Integer seconds) {
if(StringUtils.isBlank(key)){
logger.warn("key({}) cann't be blank(null or empty)!", key);
return false;
}
String redis_key = REDIS_KEY_PREFIX+key;
String value = "1";//value可以任意,但是如果解锁方式是基于value的,可以设置成唯一标识
Boolean res;
try{
Integer failedStaus = jedisCluster.ttl(redis_key).intValue();//判断是否过期失效
if(-1 == failedStaus){
jedisCluster.expire(redis_key, seconds);//重新设置过期时间
}
String result = jedisCluster.set(redis_key, value,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,(long) seconds);
res = JedisUtils.isStatusOk(result);
}catch(Exception e){
logger.error("setnx key:{},value:{},seconds:{}", new Object[]{redis_key, value, seconds, e});
return false;
}
return res;
}
/**
* @description 释放锁
* @param key
* @param seconds 锁过期时间(单位:秒)
* @return
*/
public void releaseLock(String key){
String redis_key = REDIS_KEY_PREFIX+key;
jedisCluster.del(redis_key);
logger.info("delete redis_key={}", redis_key);
}
}
上面这段代码可以说写的很不严谨,而且有点冗余,相信细心的朋友已经发现一个问题了,如果存在两个客户端,客户端a在释放锁之前刚好锁失效了,客户端b获取了锁,然后a再执行del操作会导致a删了b的锁,所以这里删除锁的操作是不对的,还有一段超时的判断的代码并无实际意义,至少我看不出到底是什么意思。。。而且缺少重新获取锁的操作。。。,下面是我改造后的代码。
public class RedisLock {
private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);
private static final String LOCK_SUCCESS = "OK";
private static final String REDIS_KEY_PREFIX = "redis_key_";
private static final String SET_IF_NOT_EXIST = "NX";//key不存在时,我们进行set操作;若key已经存在,则不做任何操作
private static final String SET_WITH_EXPIRE_TIME = "PX";//给key加一个过期的设置
@Autowired
private JedisCluster jedisCluster;
/**
* @description redis锁
* @param key
* @param seconds 锁过期时间(单位:秒)
* @return
*/
public Boolean getNonBlockLock(String key,Integer seconds,String requestId) {
if(StringUtils.isBlank(key)){
logger.warn("key({}) cann't be blank(null or empty)!", key);
return false;
}
String redis_key = REDIS_KEY_PREFIX+key;
String result = null;
try{
result = jedisCluster.set(redis_key, requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,(long) seconds);
if(LOCK_SUCCESS != result){//内部尝试获取锁
logger.info(">>> 没有获取到锁,正在尝试 。。。 <<<");
result = innerTryLock(redis_key, requestId,seconds);
}
}catch(Exception e){
logger.error("setnx key:{},value:{},seconds:{}", new Object[]{redis_key, requestId, seconds, e});
return false;
}
return JedisUtils.isStatusOk(result);
}
/**
* @description 内部尝试获取锁
* @param redis_key
* @param value
* @param seconds
* @return
*/
public String innerTryLock(String redis_key,String requestId,Integer seconds){
String oldVal = jedisCluster.get(redis_key);
String result = "";
if(!StringUtils.isNotBlank(oldVal)){
result = jedisCluster.set(redis_key, requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,(long) seconds);
logger.info(">>> 尝试之后获取到了锁! <<<");
}else{
logger.info(">>> 尝试之后没有获取到锁! <<<");
}
return result;
}
/**
* @description 释放锁
* @param key
* @param seconds 锁过期时间(单位:秒)
* @return
*/
public void releaseLock(String key,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(key), Collections.singletonList(requestId));//luna脚本方式*/
String redis_key = REDIS_KEY_PREFIX+key;
if(requestId.equals(jedisCluster.get(redis_key))){
jedisCluster.del(redis_key);
logger.info("delete redis_key={}", redis_key);
}else{
logger.info("can not del lock cause it is being used,redis_key={}", redis_key);
}
}
}
如果有朋友想在本地做测试的话,我写了段粗略的测试代码,大家可以配合redis客户端进行测试。
public class testRedisLock {
public static void main(String[] args) {
Jedis jedisCluster=new Jedis("127.0.0.1",6379);
//30 60
Long setnx = jedisCluster.setnx("LOCK_NAME", String.valueOf(System.currentTimeMillis() + 60*1000));
//获取锁成功
if(setnx !=null && setnx.intValue()==1){
System.out.println(">>>Task02成功获取到redis分布式锁<<<");
System.out.println("执行业务>>>");
}else {
System.out.println("Task02>>>没有获取到锁,正在尝试<<<");
String lockVal = jedisCluster.get("LOCK_NAME");//获取当前LOCK_NAME的时间
//如果老的lockVal不为空,并且当前时间已经大于过期锁过期时间,则证明可以获取锁
//100
if(lockVal !=null && System.currentTimeMillis()>Long.parseLong(lockVal)){
//设置LOCK_NAME新的值,返回的是LOCK_NAME老的值
String getOldSetVal = jedisCluster.getSet("LOCK_NAME", String.valueOf(System.currentTimeMillis() +60*1000));
if(getOldSetVal ==null ||(getOldSetVal!=null && StringUtils.equals(lockVal,getOldSetVal))){
System.out.println("Task02>>>尝试之后获取到了锁<<<");
//如果返回的getOldSetVal 为空则证明以前的锁已经被释放,则可以重新获取锁
//如果返回的getOldSetVal 不为空且lockVal == getOldSetVal ,返回的值和查询的时间过期值确实相等,那么证明我确实拿到这把锁
System.out.println("执行业务>>>2<<<");
}else{
System.out.println("Task02>>>尝试之后没有获取到了锁>>>");
}
}else{
System.out.println("Task02>>>尝试之后没有获取到了锁>>>");
}
}
jedisCluster.close();
}
}
第一次写博客,还有很多不足,希望大家多多指正,一起进步。