多线程测试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
@Aspect
public 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{
@Autowired
private SecondKillService secondKillService;
@Autowired
private SecondKillThread secondKillThread;
/**
* 模拟秒杀
*/
@Test
public 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 shutdown
Thread.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;
@Component
public class SecondKillThread implements Runnable{
@Autowired
private SecondKillService secondKillService;
@Override
public 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;
@Service
public class SecondKillServiceImp implements SecondKillService{
@Autowired
private RedissonClient redissonClient;
private static int count = 2000;
public static List<Integer> list = new ArrayList<>();
@Override
public 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();
// }
// }
}