实现分布式锁的方式有很多,可以通过redis、mysql、zk等方式去实现一个分布式锁。什么情况下需要用到分布式锁呢?一种是多个微服务实例,另外一种是在多线程的情况下可能也需要用到。
今天主要介绍用redis怎么去实现分布式锁。
实现分布式锁,需要考虑3个要素:
1.锁过期时间
2.锁的释放
3.锁的续期
1.锁过期时间:之所以要设置锁的过期时间是因为在服务宕机的场景下,没办法去主动释放锁,因此只能基于redis的缓存过期时间来辅助释放。
2.锁的释放:锁的释放一般是在try{}catch(){}finally{}中的finally里面去释放,这样可以保证就算业务发生报错,也能即使把锁释放掉
3.锁的续期:有一种场景是当前的业务执行时间比锁的过期时间还要长,就会出现,当前业务还没执行完,锁就被释放掉了,为了避免这种情况,需要单独为这个锁重启一个线程,专门用来观察业务是否执行完毕,如果在锁快到期之前,业务还没执行完,那么该线程会更新这个锁的过期时间。当然如果业务时间很短,能保证每次执行的业务时间在过期时间内,那么就没必要去增加一个锁的续期
以下为代码实现:
1.加锁,加锁成功后再触发watchDog,一般redis加锁可以直接通过setnx或者hsetnx的方式,也可以用lua表达式,一般推荐用lua表达式,因为这样可以写出更丰富的逻辑,并且能保证一条lua脚本是在一个原子操作里面执行的。
public boolean lock(String id, int keyExpireTime, int watchTime) throws InterruptedException {
for (; ; ) {
//HSET命令返回OK ,则证明获取锁成功
String script = "if (redis.call('exists', KEYS[1]) ~= 1) then "
+ "return redis.call('hsetnx', KEYS[1], KEYS[2],ARGV[1]) "
+ "else "
+ "return 0 "
+ "end";
List<String> keys = new ArrayList<>();
keys.add(LOCK_KEY);
keys.add(id);
List<String> arg = new ArrayList<>();
arg.add(""+keyExpireTime);
String result = null;
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
result = jedis.eval(script, keys, arg).toString();
} catch (Exception e) {
e.getStackTrace();
}finally {
jedis.close();
}
if ("1".equals(result)) {
Runnable r = ()->{
try {
watchDog(id, keyExpireTime,watchTime);
} catch (Exception e) {
e.getStackTrace();
throw new RuntimeException(e);
}
};
pool.execute(r);
System.out.println(id+"获取到锁");
return true;
}
}
}
2.解锁:
public boolean unlock(String id) {
Jedis jedis = jedisPool.getResource();
List<String> keys = new ArrayList<>();
keys.add(LOCK_KEY);
keys.add(id);
//LUA表达式
String script =
"if (redis.call('hexists', KEYS[1], KEYS[2]) == 1) then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
String result = jedis.eval(script, keys, Collections.singletonList(id)).toString();
jedis.close();
boolean flag = "1".equals(result) ? true : false;
if (flag){
System.out.println(id+"解锁");
}
return flag;
}
3.续期(watchDog)
public boolean watchDog(String uuid, int keyExpireTime,int time) {
//每time秒去检查一下线程任务是否执行完毕,如果没有执行完毕,那么需要对锁进行续签
String script = "if (redis.call('hexists', KEYS[1],KEYS[2]) == 1) then "
+ "return redis.call('expire', KEYS[1], ARGV[1]) "
+ "else "
+ "return 0 "
+ "end ";
List<String> arg = new ArrayList<>();
List<String> keys = new ArrayList<>();
keys.add(LOCK_KEY);
keys.add(uuid);
arg.add(keyExpireTime+"");
String result = null;
Jedis jedis = jedisPool.getResource();
try {
result = jedis.eval(script, keys, arg).toString();
} catch (Exception e) {
e.getStackTrace();
throw new RuntimeException(e);
}finally {
jedis.close();
}
boolean flag = "1".equals(result) ? true : false;
if (flag){
try {
System.out.println(uuid+"续期");
Thread.sleep(time*1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return watchDog(uuid,keyExpireTime,time);
}else {
System.out.println("续期结束");
return false;
}
}