问题:
公司报警群,在项目上线时,会多出一些redis连接异常,在项目系统日志查到,多数错误信息,如下:
redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
根据对应的报错类行中找出,其报错是mq消费者Consumer,在消费mq代码逻辑用到redis,大多报出上述redis线程池无连接错误。
分析:
出现问题,肯定是服务关闭时,redis线程池已经销毁了。查阅代码:
redis客户端的销毁方式
@PreDestroy
public void destroy() {
if (this.scheduler != null) {
this.scheduler.shutdown();
}
}
consumer客户端的销毁方式
@Bean(
destroyMethod = "shutdown"
)
@ConditionalOnMissingBean
public MQConsumerArray messageQueueConsumer(MqConfigArray mqConfigArray) {
。。。
return mqConsumerArray;
}
从上面代码看出,redis和consumer都是依赖spring管理bean的生命周期,进行管理的。首先想到,可不可以控制下,spring的bean销毁顺序来进行,或者先销毁consumer客户端,再处理其他bean的声明周期呢?
@DependsOn注解
之前了解到@DependsOn注解,可以控制bean之间的依赖,进而控制bean的创建顺序以及销毁顺序。
上手测试一下,很久没用这个注解了,上代码:
Person类
public class Person {
private int age;
private String name;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@PreDestroy
public void destroy(){
System.out.println("Person.destroy");
}
public void shutdown(){
System.out.println("Person.shutdown");
}
}
Student类
public class Student {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@PreDestroy
public void destroy(){
System.out.println("Student.destroy");
}
public void shutdown(){
System.out.println("Student.shutdown");
}
}
ConfigBean类
@Configuration
public class ConfigBean {
@DependsOn("student")
@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean
public Person buildPerson(){
System.out.println("Person.build");
return new Person();
}
@Bean(name = "student",destroyMethod = "shutdown")
@ConditionalOnMissingBean
public Student buildStudent(){
System.out.println("Student.build");
return new Student();
}
}
?启动项目:
关闭项目:
??
?注释掉ConfigBean的@DependsOn的使用后:
启动项目:
关闭项目:
?
?从上述实践可以看出,@DependsOn可以通过依赖关系控制促使bean之间生命周期的顺序。但是,redis和consumer客户端都是基础架构提供的,在spring实例化对应的bean时,没有对应的实例名称,所以@DependsOn用不上。
提前销毁Consumer客户端
如果控制不了redis和consumer之间的生命周期顺序,单一先销毁consumer也可以。 先看下,spring关闭方法的源码org.springframework.context.support.AbstractApplicationContext#registerShutdownHook ?
public void registerShutdownHook() {
if (this.shutdownHook == null) {
this.shutdownHook = new Thread("SpringContextShutdownHook") {
public void run() {
synchronized(AbstractApplicationContext.this.startupShutdownMonitor) {
AbstractApplicationContext.this.doClose();
}
}
};
Runtime.getRuntime().addShutdownHook(this.shutdownHook);
}
}
protected void doClose() {
if (this.active.get() && this.closed.compareAndSet(false, true)) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Closing " + this);
}
LiveBeansView.unregisterApplicationContext(this);
//发布上下文关闭事件
try {
this.publishEvent((ApplicationEvent)(new ContextClosedEvent(this)));
} catch (Throwable var3) {
this.logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", var3);
}
if (this.lifecycleProcessor != null) {
try {
this.lifecycleProcessor.onClose();
} catch (Throwable var2) {
this.logger.warn("Exception thrown from LifecycleProcessor on context close", var2);
}
}
//销毁bean
this.destroyBeans();
this.closeBeanFactory();
this.onClose();
if (this.earlyApplicationListeners != null) {
this.applicationListeners.clear();
this.applicationListeners.addAll(this.earlyApplicationListeners);
}
this.active.set(false);
}
}
从spring关闭的过程中可以看到,spring利用了JVM虚拟机关闭的钩子。
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
解决办法
上述分析可以知道,spring关闭事件发布,早于bean的销毁。可以利用关闭事件监听器提前处理需要的逻辑。鉴于这次只是解决consumer客户端没提前结束的问题。有两种选择:
1.在监听到spring关闭事件的同时,利用consumer提供的shutdown接口关闭consumer。
2.在监听到spring关闭事件之后,对mq的消息接收做拦截。
从解决方案上分析,都可以,考虑到处理问题的可控影响,选择第二种方案。代码如下: ?
/**
* 拦截mq容器类
*/
@Component
public class MqInterceptContainer implements ApplicationListener<ContextClosedEvent> {
/**
* 系统活跃标识 0关闭;1活跃
*/
public volatile static boolean activeCode = true;
/**
* 判断系统活跃 活跃返回true,关闭返回false
* @return
*/
public static boolean isActive(){
return activeCode;
}
@Override
public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
activeCode = false;
}
}
再需要拦截的地方用MqInterceptContainer.isActive()即可。
|