前言:最近做的一个接口由于没有实现幂等性,老是会出现重复提交导致数据出错的情况。之前只能依靠接口调用方去做逻辑控制避免这种情况,这次决定使用分布式锁来解决之歌问题,之前学习的时候用的是jedis写分布式锁,但是发现确还有些许缺陷,机缘巧合下得知redission框架封装了分布式锁,不但类型全面,而且使用方便,周六特略微学习了一波,今天则记录下来。附上阿里社区的redission中文版官方文档地址:https://yq.aliyun.com/articles/551423
分布式锁类型
- 普通锁 lock:如果多个线程竞争锁,获取不到锁的线程就会一直死循环不断去获取。
- 可重入锁 tryLock : 多个线程竞争锁,获取不到锁的线程会间隔一定时间重试几次,获取不到就返回false,获取到了返回true
- 联锁 MultiLock:多把锁作为一组锁使用,同时锁住,同时释放
- 红锁 RedissonRedLock :用于redis分布式,通过算法,获取大多数节点的锁住之后才标识为锁住。
- 读写锁 RReadWriteLock:可以控制redis数据是否可读或可写
- 闭锁
redis客户端类型
- 集群模式
- 单机模式
- 云托管模式
- 单机模式
- 哨兵模式
- 主从模式
实操源码
说明: 这里举例的是我写的一个demo关于解决重复提交的例子,用比较简单的可重入锁加单机模式的形式实现的。
redissionClient配置
public RedissonClient redissionClient(RedisProperties redisProperties) {
Config config = new Config();
config
//看门狗超时时间(续期时间为三分之一)
.setLockWatchdogTimeout(5*3*1000)
//单机模式
.useSingleServer()
//redis地址
.setAddress("redis://"+redisProperties.getHost() + ":" + redisProperties.getPort())
//redis密码
.setPassword(redisProperties.getPassword())
//最小空闲数
.setConnectionMinimumIdleSize(redisProperties.getLettuce().getPool().getMinIdle())
//最大连接数量
.setConnectionPoolSize(redisProperties.getLettuce().getPool().getMaxActive());
//重试次数,重试间隔 采用默认值
return Redisson.create(config);
}
标识注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface NoRepeatSubmit {
//过期时间 秒为单位
long ttl() default 5L;
}
拦截切面
@Aspect
@Component
public class RepeatSubmitAspect {
@Autowired
private RedissonClient redissonClient;
@Pointcut("@annotation(com.tensquare.redislock.anno.NoRepeatSubmit)")
public void pointCut(){
}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Logger logger = LoggerFactory.getLogger(this.getClass());
//生产的话需要从theadLocal里面获取用户信息,与request请求,request用来获取请求路径,这里用方法名与写死的userId代替
String userId="111";
String methodName = joinPoint.getSignature().getName();
String key = userId+"_"+methodName;
MethodSignature signature = (MethodSignature)joinPoint.getSignature();
NoRepeatSubmit noRepeatSubmit = signature.getMethod().getAnnotation(NoRepeatSubmit.class);
long ttl = noRepeatSubmit.ttl();
//获取锁对象
RLock lock = redissonClient.getLock(key);
if(lock.tryLock(1,ttl, TimeUnit.SECONDS)){
Object o=null;
try {
o = joinPoint.proceed();
return o;
}catch (Exception e){
throw e;
}finally {
try {
lock.unlock();
}catch (Exception e){
logger.error("释放锁出错",e);
throw new RuntimeException("释放锁出错");
}
}
}else {
logger.info("未获取到锁"+key);
throw new RuntimeException("重复提交,拒绝请求") ;
}
}
}
请求controller
@RestController
public class LockTestController {
@RequestMapping(value = "/lockTest")
@NoRepeatSubmit(ttl = 21)
public String lockTest(){
long startTime = System.currentTimeMillis();
while (System.currentTimeMillis()-startTime<40*1000){
}
return "请求成功";
}
}
案例说明
该案例通过注解 @NoRepeatSubmit(ttl = 21)去标识锁定该方法(可以自己去扩展实现锁一条数据或一张业务表),过期时间设为21s,该方法运行最少需要40s,所以unlock过程中会报锁不存在的错。
通过解读源码发现,如果lock.tryLock方法用户提供了过期时间,则redission不会开启另一个线程来作为看门狗(每隔三分之一的过期时间,会自动续期),所以需要将 if(lock.tryLock(1,ttl, TimeUnit.SECONDS)) 改为 if(lock.tryLock(1,TimeUnit.SECONDS)),则该方法跑完之前就算看门狗的超时lockWatchdogTimeout只有十五秒,在释放锁的时候也不会出现锁不存在的情况;
源码分析
//获取锁对象 RLock lock = redissonClient.getLock(key);
public RLock getLock(String name) {
//new一个锁
return new RedissonLock(this.connectionManager.getCommandExecutor(), name);
}
public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
super(commandExecutor, name);
//异步命令执行器
this.commandExecutor = commandExecutor;
//线程id
this.id = commandExecutor.getConnectionManager().getId();
//默认过期时间
this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
this.entryName = this.id + ":" + name;
//消息订阅 貌似是多个线程竞争锁的时候用到的 没细看
this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
}
- lock.tryLock(1,ttl, TimeUnit.SECONDS)
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
*********
Long ttl = this.tryAcquire(leaseTime, unit, threadId);
//如果获取到了就返回
if (ttl == null) {
return true;
} else {
//如果没有获取到就经过一定的时间 在一定的重试次数内反复获取
*********
}
}
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
return (Long)this.get(this.tryAcquireAsync(leaseTime, unit, threadId));
}
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
//如果给了过期时间
if (leaseTime != -1L) {
return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
//如果没给过期时间就使用LockWatchdogTimeout作为过期时间,并启动另一个线程去监听,当方法运行完之前不断的续期
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e == null) {
if (ttlRemaining == null) {
this.scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
}
//执行lua脚本去获取锁(lua脚本可保证原子性)
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); 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); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end;
return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}
以下是看门狗定时器续期的代码
private void scheduleExpirationRenewal(long threadId) {
RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry();
RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
//如果已存在对当前锁的监听续期任务就把当前线程id加入进去
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
//如果不存在对当前锁的续期任务就新建一个
entry.addThreadId(threadId);
this.renewExpiration();
}
}
private void renewExpiration() {
RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (ee != null) {
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
if (ent != null) {
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
} else {
if (res) {
RedissonLock.this.renewExpiration();
}
}
});
}
}
}
//每隔三分之一超时时间续期一次
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}