多线程测试redisson实现分布式锁出现org.redisson.RedissonShutdownException: Redisson is shutdown。

起源:在使用Redisson实现分布式定时调度的过程中,遇到一个非常奇怪的问题:经过一些操作后,调度器会报一个莫名其妙的错误,status is shutdown,经过多次观察发现是在rancher平台重新发布Spring Boot项目后就会报这个错误,怀疑和Spring Boot优雅关闭有关系,所以进行一个追溯确认的过程。
image.png
Reids数据:
image.png
然后翻阅 RScheduledExecutorService 的方法,发现shutdown方法代码执行的 lua 脚本和目前看到的结果很相似,但是,身为程序员的我们是看证据的,没有证据都是耍流氓。
image.png
经过

方案1

尝试使用AOP拦截目标对象的方法,然后触发优雅关闭,看看是否能被拦截到,如果拦截到的话,它的调用栈信息也就随之出来了。

  1. import lombok.extern.slf4j.Slf4j;
  2. import org.aspectj.lang.ProceedingJoinPoint;
  3. import org.aspectj.lang.annotation.Around;
  4. import org.aspectj.lang.annotation.Aspect;
  5. import org.aspectj.lang.annotation.Pointcut;
  6. @Slf4j
  7. @Aspect
  8. public class ExecutorServiceAspect {
  9. //定义切点
  10. @Pointcut("execution(public * org.redisson.*.*(..))")
  11. public void pointCut(){}
  12. // 抛异常
  13. @Around("pointCut()")
  14. public Object aroundMethod(ProceedingJoinPoint pjd) throws Throwable {
  15. log.info("invoke method. {}", pjd.getTarget());
  16. throw new RuntimeException("切点被捕捉到!");
  17. }
  18. }

然后调用使用postman调用http://127.0.0.1:8080/actuator/shutdown,实现优雅关闭,但是多次尝试,改变切点信息,发现都不能切到对象。后来仔细想了想,这个对象被代理了,甚至多层代理,肯定切不到这个对象的实际操作,所以这个方案失败。

方案2

在Redisson包中的shutdown等方法入口,尝试搜索一下调用关系。
通过schedule方法作为入口,看看能否找到一些线索。
image.png
发现大致经过以下几个流程:
image.png
并没有太大的参考价值,所以这个方案也有点行不通。

方案3

因为对象是在Spring 容器平缓关闭的时候触发的shutdown操作,而在Spring中每一个对象都是一个bean,bean 的生命周期中有一个destory的过程,所以可以基本断定是destory方法中执行了shutdown方法,并且RScheduledExecutorService是实现了java中的ExecutorService接口,而这个接口中定义了shutdown方法,而Spring可能使用ExecutorService统一管理这些bean。
DisposableBean这个接口定义的destory方法,是Spring bean生命周期中的一个方法,通过查看他的实现发现有一个可疑的对象 TaskExecutorFactoryBean
image.png
通过查看源码发现,其中确实调用了shutdown方法。
image.png
而这个executor对象,实际就是ExecutorService这个接口
image.png
就在这一刻,突然感觉真相大白了,终于找到证据了,所以可以确信是Spring Boot的平缓关闭导致的这个问题
小总结
通过本次追溯源码确实看到了许多大佬们设计这个框架的思想,特别是Spring设计的巧妙之处,使用这个统一的方式管理了Spring bean对象。

原因:多线程还没跑完,主线程就跑完了。主线程走完,关闭了资源。redisson关闭,
多线程操作redisson报错:Redisson is shutdown。
解决办法:主线程等待多线程跑完。Thread.sleep(30000);。

  1. package com.user.test.spring_redis;
  2. import java.util.HashSet;
  3. import java.util.Iterator;
  4. import java.util.Set;
  5. import org.junit.Test;
  6. import org.junit.runner.RunWith;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.test.context.ContextConfiguration;
  9. import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;
  10. import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
  11. import com.user.service.redis.SecondKillService;
  12. import com.user.service.redis.SecondKillServiceImp;
  13. import com.user.service.redis.SecondKillThread;
  14. @RunWith(SpringJUnit4ClassRunner.class)
  15. @ContextConfiguration({"classpath:applicationContext.xml"})
  16. public class RedisDistributedLockTest extends AbstractJUnit4SpringContextTests{
  17. @Autowired
  18. private SecondKillService secondKillService;
  19. @Autowired
  20. private SecondKillThread secondKillThread;
  21. /**
  22. * 模拟秒杀
  23. */
  24. @Test
  25. public void secKill(){
  26. System.out.println("秒杀活动开始---");
  27. try {
  28. for(int i=0;i<2000;i++){
  29. new Thread(secondKillThread,"Thread" + i).start();
  30. }
  31. } catch (Exception e) {
  32. e.printStackTrace();
  33. }
  34. try {
  35. // 主线程需要等待线程执行完,否则,其他线程还没执行完,主线程就走完了,redisson会报错:Redisson is shutdown
  36. Thread.sleep(30000);
  37. } catch (InterruptedException e1) {
  38. e1.printStackTrace();
  39. }
  40. System.out.println(SecondKillServiceImp.list);
  41. Set set = new HashSet();
  42. for(int i : SecondKillServiceImp.list){
  43. int count = 0;
  44. for(int j : SecondKillServiceImp.list){
  45. if(i == j){
  46. count = count + 1;
  47. }
  48. }
  49. if(count > 1){
  50. set.add(i);
  51. }
  52. }
  53. if(set != null && set.size() > 0){
  54. // Iterator it = set.iterator();
  55. // while(it.hasNext()){
  56. // System.out.println(it.next());
  57. // }
  58. System.out.println(set);
  59. }else{
  60. System.out.println("没有重复的记录!");
  61. }
  62. }
  63. }
  1. package com.user.service.redis;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.stereotype.Component;
  4. @Component
  5. public class SecondKillThread implements Runnable{
  6. @Autowired
  7. private SecondKillService secondKillService;
  8. @Override
  9. public void run() {
  10. secondKillService.seckill();
  11. }
  12. }
  1. package com.user.service.redis;
  2. public interface SecondKillService {
  3. public void seckill();
  4. }
  1. package com.user.service.redis;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. import java.util.concurrent.TimeUnit;
  5. import org.apache.commons.lang3.StringUtils;
  6. import org.redisson.api.RLock;
  7. import org.redisson.api.RedissonClient;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.stereotype.Service;
  10. import com.user.base.utils.redis.DistributedLockUtils;
  11. import com.user.base.utils.redis.DistributedLockUtils2;
  12. import com.user.base.utils.redis.redisson.RedissonConfig;
  13. @Service
  14. public class SecondKillServiceImp implements SecondKillService{
  15. @Autowired
  16. private RedissonClient redissonClient;
  17. private static int count = 2000;
  18. public static List<Integer> list = new ArrayList<>();
  19. @Override
  20. public void seckill() {
  21. // count = count - 1;
  22. // list.add(count);
  23. // System.out.println(Thread.currentThread().getName() + "秒杀操作,singleRedis," + "剩余数量:" + count);
  24. // 可以防止重复提交的数据。
  25. String uuid = DistributedLockUtils2.lockWithTimeout("test", 10);
  26. // 上锁,如果锁一直保持,其他线程无法操作,只有过期或者主动释放锁。
  27. if(StringUtils.isNotEmpty(uuid)){
  28. try {
  29. count = count - 1;
  30. list.add(count);
  31. System.out.println(Thread.currentThread().getName() + "秒杀操作,singleRedis," + "剩余数量:" + count);
  32. } catch (Exception e) {
  33. //e.printStackTrace();
  34. } finally {
  35. // 如果业务代码出现异常了,不在finally中执行释放锁的操作,也会导致锁无法释放。
  36. DistributedLockUtils2.releaseLock("test",uuid);
  37. }
  38. }else{
  39. System.out.println("获取锁超时!");
  40. }
  41. }
  42. // @Override
  43. // public void seckill() {
  44. // RLock redissonLock = redissonClient.getLock("test");
  45. // // 相当于distributedLockUtil.stringRedisTemplate.opsForValue().setIfAbsent(lockKey, identifier, timeout, TimeUnit.SECONDS)
  46. // redissonLock.lock();
  47. // try {
  48. // count = count - 1;
  49. // list.add(count);
  50. // System.out.println(Thread.currentThread().getName() + "秒杀操作,clusterRedis," + "剩余数量:" + count);
  51. // } catch (Exception e) {
  52. // e.printStackTrace();
  53. // } finally {
  54. // // 相当于distributedLockUtil.stringRedisTemplate.delete(lockKey);
  55. // /*
  56. // * 由于开启了watchdog看门狗线程监听,所以线程执行完之前不会出现:A线程锁过期时间过期,此时B线程设置锁,然后又切换到A线程删锁,误删B线程的锁。
  57. // * 因为A线程执行完之前,A线程的锁会一直续命,不会过期。所以A线程在delete锁之前,会一直持有锁。
  58. // * 如果服务器非宕机情况,那么锁会一直续命,A线程一直持有锁。最终都会执行到finally释放锁。
  59. // * 如果中间出现宕机,那么锁不会续命,到了过期时间就会过期。锁自动释放。
  60. // * 因此不会出现锁无法释放,死锁的情况。
  61. // *
  62. // * 自己写续命比较麻烦,而且容易出错。redisson是个很好的框架和解决方案。
  63. // */
  64. // redissonLock.unlock();
  65. // }
  66. // }
  67. }

参考:https://github.com/redisson/redisson/issues/1872