主要为了解决并发访问排队执行及重复下发保证幂等的问题
实现技术方案
如何实现并发访问时,请求排队执行方法。@Redssion
如何实现MQ重复下发时,保证幂等。@redis
先插入一个分布式锁的话题,如下,使用时先想下,有没有共享资源,保护什么?再想一下mq消费,是否存在这样的问题,需要用什么去解决这些问题。不是为了用而用一个技术,应该从问题本身出发,再找技术方案。
当并发去读写一个【共享资源】的时候,我们为了保证数据的正确,需要控制同一时刻只有一个线程访问。
分布式锁就是用来控制同一时刻,只有一个 JVM 进程中的一个线程可以访问被保护的资源
RLock lock = redissonClient.getLock(lockKey); if (lock.tryLock(RedisEnum.ORDER_TRACE_MQ_LOCK_KEY.expired, TimeUnit.SECONDS)) { 业务逻辑 } } finally { lock.unlock(); }
提出以下疑问
1.不同lockKey获取的锁是否相同
2.相同lockKey,多次获取锁,是否相同
3.加锁粒度是什么
相同的key与不同的key,是否都可以实现并发访问,如何保证对线程队排执行
4.什么时候会加锁失败,如何解决问题的
5.定义不同key意义是什么,为什么不是相同key
6.多线程排队执行,变成串行执行,是否会影响执行效率,怎么解决效率问题
7.如果多线程排查,线程池在哪里,可以有多少线程排查,多少可以在队列中
8.与分布式锁概念比较
猜测
1.不同
2.相同
3.相同的key可以实现多线程排队执行,不同key不知道,参考
Redisson分布式锁入门使用(可重入锁(lock))_redissonclient.getlock_Upstream LV@菜哥的博客-CSDN博客
4.加锁失败,只是暂时未加锁成功,在排队,等前面的锁释放,就加锁成功了
5.相同的key,多线程并发访问,会排队执行。不同的key难道会并发访问?
那加锁的意义是什么?它不就是一个分布式锁吗,控制的粒度,如果是MQ key的话,没有出现MQ重复下发的情况下,其实相当于没有控制,就是允许并发访问
是否可以理解为:不同key就是不同锁,既然是不同锁,就可以各自加锁成功,也就不用排队,只有key相同,同一把锁时,才需要排队。那mq下发基本就是并发访问。
6.参考5,如果穿行执行,肯定影响效率,待确认。
7.不知道
Redisson 实现分布式锁原理分析 - 知乎
这篇文章说了加锁的源码,跟之前排队,存在线程池理解不对。
先看下这个业务方法
if (lock.tryLock(RedisEnum.ORDER_TRACE_MQ_LOCK_KEY.expired, TimeUnit.SECONDS)) {
这个方法,是接口Lock的方法
实现
现在回想一下,tryLock中时间参数是什么含义,对应下面waitTime即该线程的等待时间,
leaseTime这个参数,上面默认-1,这个时间是看门狗默认的过期时间。
注意区分一个等待时间一个key过期时间。
注意:从以上源码我们看到 leaseTime
必须是 -1 才会开启 Watch Dog 机制,也就是如果你想开启 Watch Dog 机制必须使用默认的加锁时间为 30s。如果你自己自定义时间,超过这个时间,锁就会自定释放,并不会延长。
怎么手动设置过期时间?
通过这个方法
lock.lock(1,TimeUnit.SECONDS); 也就是说下吗两个方法没有设置过期时间,的默认leaseTime = -1,实现了看门口机制。 lock.lock(); if (lock.tryLock(RedisEnum.ORDER_TRACE_MQ_LOCK_KEY.expired, TimeUnit.SECONDS)) {
如何等待获取锁的呢?
第一个持有锁的线程后,其他线程进来会订阅释放锁的消息,当持有锁线程释放锁,会发布消息,其他持有锁的会订阅消息,尝试获得锁。总体通过while死循环来获得锁,可以试想一下,不会一直无条件的循环尝试获取锁,这样效率太低。详细参考上文与下文
Redis进阶- Redisson分布式锁实现原理及源码解析-腾讯云开发者社区-腾讯云
7.1多线程不还是存在线程池吗?
8.是否可以理解为就是分布式锁,分布式锁如果并发访问(key相同),第二个请求是不会执行方法的,这里是会排队的,还是会执行,有区别的。
相当于这里解决的还是并发时重复的问题,排队访问,所以还需要通过redis校验是否重复访问。
假如不是重复下发不是并发场景,有时间间隔,还是需要redis校验是否重复下发。
那redission就是控制重复消息排队的问题了,且真正控制重复下发的是redis,那是否可以直接把
redisson直接去掉,只用redis控制重复消费。
因为key不同,redission也就无法控制并发消费。
且当key相同时,是排队的,也无法实现分布式锁的目的。
分布式锁概念是什么:
在分布式高并发的条件下,我们最需要保证,同一时刻只能有一个线程获得锁,这是最基本的一点。
再想一下,同一时刻只能有一个线程获得锁,这是否有前提,key要相同,还是与key无关?
@这里有一个前提,key相同
参考文档
【精选】Redis:Redisson分布式锁的使用(推荐使用)_redisson分布式锁使用_穿城大饼的博客-CSDN博客
该文章说明了加锁的详细过程,可得出结论,key不同可以加锁成功,mq下发如果key是不同的,那么每次都可以加锁成功,即是并发处理的,粒度较小,符合高效率,高并发的要求。
那么他的意义是什么,@上面第8点的分析,好像可以去掉。
@那么控制重复消费的任务完全落在了redis了,过期时间很重要,设置一个合理的过期时间,控制这段时间的重复消费问题。如果过了这段时间还会重复,要么不会,要么给出原因。
继续提问
9.或者说mq体现不了,分布式锁的意思,场景不适合,哪些场景符合,请举例
10.mq高并发访问,会不会存在问题,比如更新失败,共享资源异常
Redisson分布式锁入门使用(可重入锁(lock))_redissonclient.getlock_Upstream LV@菜哥的博客-CSDN博客
Spring AOP 中的 @Around 通知执行原理_aop around 控制方法执行-CSDN博客
结合以上知识,在分析代码
package com.yonghui.yh.rme.srm.ordercenter.service.annotation;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.yonghui.redis.utils.RedisUtils;
import com.yonghui.yh.rme.srm.ordercenter.common.enums.RedisEnum;
import lombok.extern.slf4j.Slf4j;
/**
* 解决什么问题
* 1,控制并发访问,实现多线程并发访问时,排队
* 2,幂等,防止MQ重复消费,不通过反射调用(joinPoint.proceed()),不执行原方法
*/
@Aspect
@Component
@Slf4j
public class MqConsumerAspect {
@Autowired
private RedissonClient redissonClient;
@Autowired
private RedisUtils redisUtils;
@Pointcut("@annotation(com.yonghui.yh.rme.srm.ordercenter.service.annotation.MqConsumer)")
public void point(){}
/**
* @Around的作用
* 既可以在目标方法之前织入增强动作,也可以在执行目标方法之后织入增强动作;
*
* 可以决定目标方法在什么时候执行,如何执行,甚至可以完全阻止目标目标方法的执行;
*
* 可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值; 当需要改变目标方法的返回值时,只能使用Around方法;
*
* 虽然Around功能强大,但通常需要在线程安全的环境下使用。因此,如果使用普通的Before、AfterReturing增强方法就可以解决的事情,就没有必要使用Around增强处理了。
*
* 注解方式:如果需要对某一方法进行增强,只需要在相应的方法上添加上自定义注解即可
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("point()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object o = joinPoint.getArgs()[0];
JSONObject jsonObject;
if (o instanceof String) {
jsonObject = JSON.parseObject(o.toString());
} else {
jsonObject = JSON.parseObject(JSON.toJSONString(o));
}
String messageKey = jsonObject.getString("messageKey");
if (StringUtils.isBlank(messageKey)) {
// 方法是否会继续执行?
// @ 答: 会执行 //利用反射调用目标方法,就是method.invoke() proceed = joinPoint.proceed(args);
log.info("MqConsumerAspect mq消息消费 messageKey 为空");
return joinPoint.proceed();
}
// 虽然Around功能强大,但通常需要在线程安全的环境下使用。这是使用Lock的原因,保证现在安全,多线程排队访问。
String lockKey = RedisEnum.ORDER_TRACE_MQ_LOCK_KEY.toKey(messageKey);
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(RedisEnum.ORDER_TRACE_MQ_LOCK_KEY.expired, TimeUnit.SECONDS)) {
// 并发请求,排队等待,在这里写能控制吗? @应该能,决定方法是否执行,调用joinPoint.proceed()方法
// @ 所以会排队
// @ 所以,重复消费,返回true的结果,相当于,原方法直接返回true
String key = RedisEnum.ORDER_TRACE_MQ_REPEAT_CONSUMER_KEY.toKey(messageKey);
// 幂等,这里什么用,能控制方法幂等,不再执行吗?还是只是记录重复记录。
if (redisUtils.exists(key)) {
log.info("MqConsumerAspect messageKey={} 重复消费",key);
return true;
}
// 这里只是记录消费完成日志?
Object proceed = joinPoint.proceed();
if (proceed.equals(Boolean.TRUE)) {
log.info("MqConsumerAspect messageKey={} 消费完成",key);
redisUtils.set(key, "1", RedisEnum.ORDER_TRACE_MQ_REPEAT_CONSUMER_KEY.expired);
}
return proceed;
}
// 这里排队,直接返回false为什么?todo,原方法没有执行,这里直接返回false,代表原方法返回false?
return false;
// 上面返回true / proceed / false,有什么影响?
} catch (Exception e) {
log.error("MqConsumerAspect error param={}", JSON.toJSONString(o), e);
throw e;
} finally {
lock.unlock();
}
}
}
参考文档
Redis 分布式锁的正确实现原理演化历程与 Redission 实战总结 - 知乎
这篇文章说明了红锁redLock,不同方法区别,是否有看门狗机制,并通过举例说明分布式锁应用场景。
Redisson 实现分布式锁原理分析 - 知乎
这个文章,通过源码讲解了如何发布订阅的