前言
谈到Redisson就不得不说Redis了,一想到Redis就不得不想到并发编程锁机制,一想到锁机制那么就不能不考虑一个很头疼的问题,如何保证原子性的问题,高QPS请求量的系统对每次执行数据的原子性由为的关键,保证不了原子性就会导致一系列重复提交的操作,重复的数据导致在某些逻辑运算的时候发生误差;
ACID的特性首先是原子性,原子性永远是放在首位的,所以我们首先要解决的就是接口请求的原子性;
Redis分布式锁的原子性
Redis分布式锁到底能不能保证原子性,这是面试会被经常问到的一个问题。大部分的人回答都是不能保证原子性,但是其中的所以然大家都很模糊,我也一样,但为什么还在用Redis的分布式锁,如何让它可以具有原子性呢?
Redis分布式锁实现
SET NX相信大家都知道是redis实现加锁的一个命令,这里用Jedis封装的Api接口去对redis进行使用,Jedis是Redis官方推荐的面向Java操作Redis的客户段,RedisTemplate是SpringDataRedis中对Redis的封装客户端,方便的就是可以搭配Spring框架使用,如Spring cache;
先上代码吧,这是我曾经写的一段关于redis分布式锁的代码,主要用于重复提交的判断,这里我用了jedis的setnx方法,加锁后的返回不为null && 值等于1的时候表示加锁成功,并且调用expire方法对key进行赋过期时间,业务处理完成后进行锁的释放,防止死锁,这里是常规的redis的加锁方式;
java复制代码
/**
* 分布式事务锁-判断是否请求过
*
* @param key 键
* @param time 过期时间
* @return true:存在
*/
@HystrixCommand(fallbackMethod = "isExistFail")
public boolean isExist(String key, int time) {
Jedis jedis = null;
try {
jedis = jedisPoolManager.getJedis();
String uniqKey = JEDIS_KEYNAME + key;
String val = jedis.get(uniqKey);
if (val != null) {
return true;
}
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
Long flag = jedis.setnx(uniqKey, LOCAL_VAL);
if (flag != null && flag.intValue() == 1) {
jedis.expire(uniqKey, time);
} else {
throw new ExceptionTyche("加锁失败! ");
}
} finally {
lock.unlock();
}
} else {
log.info("执行Redis超时!直接返回!");
}
} catch (Exception e) {
log.warn("Redis缓存异常! key={} errMsg:" + e.getMessage(), key, e);
} finally {
jedisPoolManager.close(jedis); // !!!关闭
}
return false;
}
细心的小伙伴可以看到, 我在setnx加锁之前,用了lock.tryLock这个方法,用jdk的锁先尝试进行获取锁,如果没有获取到直接返回,这样就能在一定程度上避免通过redis加锁后,业务逻辑还未执行完锁超时进行释放,导致下次同样的key获取到锁就会出现重复提交的操作
并且使用了HystrixCommand熔断注解,防止在高并发的情况下加锁方法出现异常,对其进行降级,保证业务提交的原子性;
Redisson分布式锁的原子性
关于Redisson是目前使用比较多的一个关于分布式锁的客户端,其主要原理相信大家知道,那就是watchDog机制,俗称“看门狗机制”,由于这种机制能对锁的过期时间进行续期在很大程度上能保证加锁的原子性;
关于Redisson的分布式锁之前的文章写过,通过注解的方式去实现juejin.cn/post/721514…
Redisson看门狗机制
前段时间突然遇到了这么一个问题,Redisson锁是统一进行续期的还是分开续期的,之前确实没有考虑过,下面来看下源码具体分析下;
这里我使用的是,redisson3.8.2的版本
xml复制代码<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.8.2</version>
</dependency>
这个方法是Redisson加锁的核心代码,本质也是通过redis的lua脚本;
加锁原理
代码解读:*KEYS[1]*加锁key,*ARGV[2]*加锁的值,*ARGV[1]*看门狗机制的过期时间,internalLockLeaseTime续期时间;
- 第一个if:如果key不存在,则通过hest去加锁,KEYS[1]加锁key,ARGV[2]加锁的值,pexpire命令设置对key值设置过期时间;
- 第二个if:key存在的情况,hincrby命令判断是不是自己的线程,如果是自己的线程的情况下,就对已经加锁的线程+1操作,并且设置过期时间,这也是可重入锁的一种实现;
- 直接return,加锁失败,如果key值不是自己所在的线程则返回过期时间;
解锁原理
既然有加锁,相对肯定有解锁实现;
- 第一个if:判断加锁key存不存在,*KEYS[2]*这里代表发布订阅消息的管道名称;
- 第二个if:判断KEY[1]的ARGV[3]是否存在,*APGV[3]*代表线程id标识,如果不存在则返回null;
- 第三方if:如果存在将APGV[3]的值减一,如果counter的值相当于线程id标识经过运算后的值大于0,由于是可重入锁,该线程依旧可以获取到锁,重新设置*ARGV[2]*锁过期时间,返回0;
- del操作是真正的解锁操作,并且通过publish命令发布消息,*APGV[1]*代表消息内容,操作成功后返回1;
- 解锁失败返回nil,nil相当于Java中的null;
Watchdog锁续期
通过对redisson的加锁、解锁源码分析,相信大家对这块已经有个很清楚的认识, 还有就是只有当前线程才是获取自己的锁,不是当前线程无法获取到锁,就意味着无法进行锁续期的操作 ,由此可证明Redisson锁的续期是分开进行的,commandExecutor.evalWriteAsync的加锁方法比较长,这里就不截出来了,有兴趣的小伙伴可以去追踪看看;
主要续期逻辑就是,例如一个线程加锁成功,就是自动触发Watchdog锁续期机制,后台是一个工作线程,每隔10秒钟的时间会check当前线程是否还持有锁,如果持有锁就将锁的过期时间延长至30秒;
总结
这里也是对之前redisson锁的知识遗漏点的一个学习,结合实际的业务开发使用的案例,来分析锁的底层原理,从而来避免我在使用过程中遇到问题,能有更加清晰的解决思路,理解不对的地方也欢迎大家在评论区提出。
原文链接:https://juejin.cn/post/7269385060612309007