https://zhuanlan.zhihu.com/p/135864820
实现分布式锁
-
在集群模式下,synchronized只能保证单个JVM内部的线程互斥,不能保证跨JVM的互斥
1、redisTemplate是基于某个具体实现的再封装,比如说springBoot1.x时,具体实现是jedis;而到了springBoot2.x时,具体实现变成了lettuce。封装的好处就是隐藏了具体的实现,使调用更简单,但是有人测试过jedis效率要10-30倍的高于redisTemplate的执行效率,所以单从执行效率上来讲,jedis完爆redisTemplate。redisTemplate的好处就是基于springBoot自动装配的原理,使得整合redis时比较简单。
2、jedis作为老牌的redis客户端,采用同步阻塞式IO,采用线程池时是线程安全的。优点是简单、灵活、api全面,缺点是某些redis高级功能需要自己封装。
3、lettuce作为新式的redis客户端,基于netty采用异步非阻塞式IO,是线程安全的,优点是提供了很多redis高级功能,例如集群、哨兵、管道等,缺点是api抽象,学习成本高。lettuce好是好,但是jedis比他生得早。
4、redission作为redis的分布式客户端,同样基于netty采用异步非阻塞式IO,是线程安全的,优点是提供了很多redis的分布式操作和高级功能,缺点
是api抽象,学习成本高。
Jedissetnx
jedis是redis的java客户端,通过它可以对redis进行操作。与之功能相似的还包括:Lettuce等
spring-boot-data-redis 内部实现了对Lettuce和jedis两个客户端的封装,默认使用的是Lettuce
/*场景一: 假如释放锁失败,则后面永远无法执行*/
lock_name 使用线程id或者一个唯一id
Jedis redis = getJedis();
Long lockResult = redis.setnx(LOCK_NAME, LOCK_VALUE);
if (1 == lockResult) {
// 2. 执行业务
executeBusiness();
// 3. 释放锁
redis.del(LOCK_NAME);
} else {
// 获取锁失败
System.out.println("Can not get lock");
}
/*场景二: 释放锁失败,通过自动过期来保证*/
Jedis redis = getJedis();
String lockResult = redis.set(LOCK_NAME, LOCK_VALUE, "NX", "EX", EXPIRE_SECS);
if ("OK".equalsIgnoreCase(lockResult)) {
executeBusiness();
redis.del(LOCK_NAME);
} else {
System.out.println("Can not get lock");
}
Redisson(Lua脚本)
KEYS[1] key
ARGV[1] key 的默认生存时间
ARGV[2] 加锁的客户端的 ID
if (redis.call('exists', KEYS[1]) == 0) then " + //值过来先判断是否存在
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " + //命令设置一个 hash 结构
"redis.call('pexpire', KEYS[1], ARGV[1]); " + //设置存活时间
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " + //myLockkey的hash 数据结构中是否包含客户端2的ID
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);" //返回myLock 这个锁key 的剩余生存时间
续期机制
Redisson 提供了一个续期机制, 只要客户端 1 一旦加锁成功,就会启动一个 Watch Dog。
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1) {
return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining == null) {
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
Watch Dog 机制必须使用默认的加锁时间为 30s。(`leaseTime` 为 -1 开启 Watch Dog 机制,)
自定义时间,超过这个时间,锁就会自定释放,并不会延长。Watch Dog 机制是一个后台定时任务线程
获取锁成功之后,会将持有锁的线程放入到一个 `RedissonLock.EXPIRATION_RENEWAL_MAP`里面
每隔 10 秒 检查一下,如果客户端 1 还持有锁 key(判断客户端是否还持有 key,其实就是遍历 `EXPIRATION_RENEWAL_MAP` 里面线程 id 然后根据线程 id 去 Redis 中查,如果存在就会延长 key 的时间),那么就会不断的延长锁 key 的生存时间。
Lettuce
public class RedisLockUtil {
/**
* redis分布式锁-加锁
* @param redisTemplate redis 加锁解锁要保证同一个
* @param key 分布式锁key
* @param value 分布式锁value 一般为随机数
* @param timeout 分布式锁过期时间 秒
* @param number 重试次数
* @param interval 重试间隔 毫秒
* @return
*/
public static boolean lock(RedisTemplate redisTemplate, String key, String value, int timeout, int number, int interval) {
//加锁
for (int i = 0; i < number; i++) {
//尝试获取锁,成功则返回不成功则重试
if (redisTemplate.opsForValue().setIfAbsent(key, value, Duration.ofSeconds(timeout))) {
return true;
}
//暂停
try {
TimeUnit.MILLISECONDS.sleep(interval);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//最终获取不到锁返回失败
return false;
}
public static boolean lock(RedisTemplate redisTemplate, String key, String value) {
return lock(redisTemplate, key, value, 30, 3, 1000);
}
/**
* 解锁脚本,防止线程将其他线程的锁释放
*/
private static String UN_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
/**
* redis分布式锁-解锁
* @param redisTemplate redis 加锁解锁要保证同一个
* @param key 分布式锁key
* @param value 分布式锁value 一般为随机数
* @return
*/
public static void unLock(RedisTemplate redisTemplate, String key, String value) {
//解锁
redisTemplate.execute(new DefaultRedisScript<>(UN_LOCK_SCRIPT, Long.class), Collections.singletonList(key), value);
}
/**
* 获取调用者的类名和方法名
* @return
*/
public static String getMethodPath() {
StackTraceElement stackTraceElement = Thread.currentThread().getStackTrace()[2];
//获取当前类名
String className = stackTraceElement.getClassName();
//获取当前方法名
String methodName = stackTraceElement.getMethodName();
return className + "#" + methodName;
}
/**
* 获取随机数
* @return
*/
public static String getRandom() {
return Objects.toString(ThreadLocalRandom.current().nextInt(0, 100000));
}
}