多线程测试redisson实现分布式锁出现org.redisson.RedissonShutdownException: Redisson is shutdown。
起源:在使用Redisson实现分布式定时调度的过程中,遇到一个非常奇怪的问题:经过一些操作后,调度器会报一个莫名其妙的错误,status is shutdown,经过多次观察发现是在rancher平台重新发布Spring Boot项目后就会报这个错误,怀疑和Spring Boot优雅关闭有关系,所以进行一个追溯确认的过程。
Reids数据:
然后翻阅 RScheduledExecutorService 的方法,发现shutdown方法代码执行的 lua 脚本和目前看到的结果很相似,但是,身为程序员的我们是看证据的,没有证据都是耍流氓。
经过
方案1
尝试使用AOP拦截目标对象的方法,然后触发优雅关闭,看看是否能被拦截到,如果拦截到的话,它的调用栈信息也就随之出来了。
import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;@Slf4j@Aspectpublic class ExecutorServiceAspect {//定义切点@Pointcut("execution(public * org.redisson.*.*(..))")public void pointCut(){}// 抛异常@Around("pointCut()")public Object aroundMethod(ProceedingJoinPoint pjd) throws Throwable {log.info("invoke method. {}", pjd.getTarget());throw new RuntimeException("切点被捕捉到!");}}
然后调用使用postman调用http://127.0.0.1:8080/actuator/shutdown,实现优雅关闭,但是多次尝试,改变切点信息,发现都不能切到对象。后来仔细想了想,这个对象被代理了,甚至多层代理,肯定切不到这个对象的实际操作,所以这个方案失败。
方案2
在Redisson包中的shutdown等方法入口,尝试搜索一下调用关系。
通过schedule方法作为入口,看看能否找到一些线索。
发现大致经过以下几个流程:
并没有太大的参考价值,所以这个方案也有点行不通。
方案3
因为对象是在Spring 容器平缓关闭的时候触发的shutdown操作,而在Spring中每一个对象都是一个bean,bean 的生命周期中有一个destory的过程,所以可以基本断定是destory方法中执行了shutdown方法,并且RScheduledExecutorService是实现了java中的ExecutorService接口,而这个接口中定义了shutdown方法,而Spring可能使用ExecutorService统一管理这些bean。
DisposableBean这个接口定义的destory方法,是Spring bean生命周期中的一个方法,通过查看他的实现发现有一个可疑的对象 TaskExecutorFactoryBean
通过查看源码发现,其中确实调用了shutdown方法。
而这个executor对象,实际就是ExecutorService这个接口
就在这一刻,突然感觉真相大白了,终于找到证据了,所以可以确信是Spring Boot的平缓关闭导致的这个问题
小总结
通过本次追溯源码确实看到了许多大佬们设计这个框架的思想,特别是Spring设计的巧妙之处,使用这个统一的方式管理了Spring bean对象。
原因:多线程还没跑完,主线程就跑完了。主线程走完,关闭了资源。redisson关闭,
多线程操作redisson报错:Redisson is shutdown。
解决办法:主线程等待多线程跑完。Thread.sleep(30000);。
package com.user.test.spring_redis;import java.util.HashSet;import java.util.Iterator;import java.util.Set;import org.junit.Test;import org.junit.runner.RunWith;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.test.context.ContextConfiguration;import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests;import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;import com.user.service.redis.SecondKillService;import com.user.service.redis.SecondKillServiceImp;import com.user.service.redis.SecondKillThread;@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration({"classpath:applicationContext.xml"})public class RedisDistributedLockTest extends AbstractJUnit4SpringContextTests{@Autowiredprivate SecondKillService secondKillService;@Autowiredprivate SecondKillThread secondKillThread;/*** 模拟秒杀*/@Testpublic void secKill(){System.out.println("秒杀活动开始---");try {for(int i=0;i<2000;i++){new Thread(secondKillThread,"Thread" + i).start();}} catch (Exception e) {e.printStackTrace();}try {// 主线程需要等待线程执行完,否则,其他线程还没执行完,主线程就走完了,redisson会报错:Redisson is shutdownThread.sleep(30000);} catch (InterruptedException e1) {e1.printStackTrace();}System.out.println(SecondKillServiceImp.list);Set set = new HashSet();for(int i : SecondKillServiceImp.list){int count = 0;for(int j : SecondKillServiceImp.list){if(i == j){count = count + 1;}}if(count > 1){set.add(i);}}if(set != null && set.size() > 0){// Iterator it = set.iterator();// while(it.hasNext()){// System.out.println(it.next());// }System.out.println(set);}else{System.out.println("没有重复的记录!");}}}
package com.user.service.redis;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;@Componentpublic class SecondKillThread implements Runnable{@Autowiredprivate SecondKillService secondKillService;@Overridepublic void run() {secondKillService.seckill();}}
package com.user.service.redis;public interface SecondKillService {public void seckill();}
package com.user.service.redis;import java.util.ArrayList;import java.util.List;import java.util.concurrent.TimeUnit;import org.apache.commons.lang3.StringUtils;import org.redisson.api.RLock;import org.redisson.api.RedissonClient;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import com.user.base.utils.redis.DistributedLockUtils;import com.user.base.utils.redis.DistributedLockUtils2;import com.user.base.utils.redis.redisson.RedissonConfig;@Servicepublic class SecondKillServiceImp implements SecondKillService{@Autowiredprivate RedissonClient redissonClient;private static int count = 2000;public static List<Integer> list = new ArrayList<>();@Overridepublic void seckill() {// count = count - 1;// list.add(count);// System.out.println(Thread.currentThread().getName() + "秒杀操作,singleRedis," + "剩余数量:" + count);// 可以防止重复提交的数据。String uuid = DistributedLockUtils2.lockWithTimeout("test", 10);// 上锁,如果锁一直保持,其他线程无法操作,只有过期或者主动释放锁。if(StringUtils.isNotEmpty(uuid)){try {count = count - 1;list.add(count);System.out.println(Thread.currentThread().getName() + "秒杀操作,singleRedis," + "剩余数量:" + count);} catch (Exception e) {//e.printStackTrace();} finally {// 如果业务代码出现异常了,不在finally中执行释放锁的操作,也会导致锁无法释放。DistributedLockUtils2.releaseLock("test",uuid);}}else{System.out.println("获取锁超时!");}}// @Override// public void seckill() {// RLock redissonLock = redissonClient.getLock("test");// // 相当于distributedLockUtil.stringRedisTemplate.opsForValue().setIfAbsent(lockKey, identifier, timeout, TimeUnit.SECONDS)// redissonLock.lock();// try {// count = count - 1;// list.add(count);// System.out.println(Thread.currentThread().getName() + "秒杀操作,clusterRedis," + "剩余数量:" + count);// } catch (Exception e) {// e.printStackTrace();// } finally {// // 相当于distributedLockUtil.stringRedisTemplate.delete(lockKey);// /*// * 由于开启了watchdog看门狗线程监听,所以线程执行完之前不会出现:A线程锁过期时间过期,此时B线程设置锁,然后又切换到A线程删锁,误删B线程的锁。// * 因为A线程执行完之前,A线程的锁会一直续命,不会过期。所以A线程在delete锁之前,会一直持有锁。// * 如果服务器非宕机情况,那么锁会一直续命,A线程一直持有锁。最终都会执行到finally释放锁。// * 如果中间出现宕机,那么锁不会续命,到了过期时间就会过期。锁自动释放。// * 因此不会出现锁无法释放,死锁的情况。// *// * 自己写续命比较麻烦,而且容易出错。redisson是个很好的框架和解决方案。// */// redissonLock.unlock();// }// }}
