本文转载自微信公众号「捉虫大师」,作者捉虫大师。转载本文请联系捉虫大师公众号。

1

时间追溯到2018年12月的某一天夜晚,那天我正准备上线一个需求完就回家,刚点下发布按钮,告警就响起,我擦,难道回不了家了?看着报错量只有一两个,断定只是e Y m u S 9 ( w偶发,稳住不要慌。

把剩下的机器H c ] @ D [发完,又出现了几个同样的错误,作为一名优(咸)秀(鱼)程序员,这种问题必须追查到底B g 4 x I m

2

娴熟地查询到报错日志

  1. org.apache.ibatis.exceptions.PersistenceException:###Errorqueryingdatabase.Cause:org.sprinK [ P * ) hgframeworr m n ` 0k.jdbc.CannotGetJdbcCo! e Q + .nnectionException:CouldnotgetJDBCConnection;nestedexceptioB U Q , = o ] Gniscom.alibaba.druid.pool.DataSourceClosedException:dataSourcealreadyclosed

看着异常信息,陷入了沉思

表面上看报错是因为使用了已经关闭的数据源

数据源什么时候会关闭呢?只有进程被杀死的时% # h – & # 8 !

莫非是应用关闭时不够平滑?发布时会先摘除流量的呀,应该不至于呀A _ h % % w

天色已经很晚,漫无目的地拖动日志,疲惫地寻找新线索,突然报错日志中一个单词引入眼帘:「rocketmq」

精神抖擞,大概知道原因了,这应用中还有个兢兢8 H p S业业的rocketmq consumer一直在消费消息,在应用关闭时,外部流量被摘除了,但没人通知rocketmq consumer,于是它抛异常了。

3

出于我对rocketmq不深刻甚至有点肤浅的理解,它的消费采用ac. Q ] { Hk的方式,s J ; 6 ( { 9如果报错,消息稍后还会重试,不会丢消息,而且如果消费代码是幂等的,也不会有业务上的异常,总之这不重要,因为它也不是我写的代码。

瞅了一眼consumer的代码(这里就不贴代码了,反正贴了你也不会看: g ) Q),consumer注册了一个ShutdownHook,ShutdownHook里consumer执行了shutdown7 p W – } :来优雅地退出,并且给这个shutdownThread设置了最高优先级,然而从实践看来,这个线程最高优先x % B 1 W级并没有什么卵用。

而且从《ShutdownHook原理》这篇文章中也知道ShutdownHook是并发执行的,spring容器关闭也是一个ShutdownHoos A % d u \ mk,他们之前没有先后顺序。

了解原因后,第一时间想到了类似dubbo摘流的方案,吭哧吭哧写了个优雅关闭rocketmq cosnumer的接口,在应用关闭脚本的kill之前调用该接口,完美解决问题,赶紧下班回家,不然要猝死了。

4

夜里入睡,梦到老板让我把所有的系统都改造掉,吓得我一机灵。

于是第二天又重新思考这) ; | E g M {个问题,总觉得在应用里实现一个接口Y t ; ) E = O Ot 5 ) / L – * f在stop脚本中d t B去调用S _ 7 q d o ,是一件非常不优雅的事,更重要的是这也没法复制到其他项目,我又陷入了沉思。

既然是spring容器关闭时beab 9 R =n的销毁顺序导致的问题,那么能不能利用spring的depend-8 H \ R U _ Bon把顺序理顺了?说干就干。

起初我遇到是这样的依赖关系:

手把手在xml的每个bean中把depend-on关系都配上,似乎也起到了作用。

但当我打开第二个项目时,它的bean之间的K 3 / 9依赖关系大致如下:

好家伙,26个字母差点不够用,当时我的心情是这样的

所以我觉得以当前的速度,改造完所有项目可能T 0 4 B都到9102年了。

5

又过了一段时间,在github交友网~ E } + { u 9 m k站上突然看到了rocketmq官方实现的spring-boot-starter,于是点进去看了它的实现。好家伙,看完直呼666。

官方startert ! ` i l d实现了spring的Smart^ x ) #Lifecycle接口,它的start方法能在所有bean初始化完成后被调用,s$ ; $ 2top方法会在bean被销毁前调用,对rocketmq consumer来说简直完美。

顺便还复. y * Z z习了一下spring容器的关闭,代码在AbstractApplicationContext的doClose方法,这里我o ^ – ` s G总结成一幅图:

通过上图能看到,销毁bean之前,有关闭lifec– p – _ 1 `ycle br O ] Y C b k j 9ean和发送ContextClosedEvent两个动作,官方starter选择了实现LifeCycle% T A接口的方式。

6

到这里我该给老板汇报去了,之所以rocketmq co0 D F * g x Zn( h [ C \ a / ?sumer发布时不平滑是我们的使用姿势问题,虽然对业务没影响,但不优雅,解决方案有两个,老板你选吧:

  • 全都换成g K Q * N官方starter,依赖spring-boot,官方维护,改造成本很高,
  • 监听ContextClosedEvent来实现优雅关闭,这s _ ? h块可以封装一下,让业务方引入依赖即可