Spring 广播消息时,Spring会在 ApplicationContext 中查找所有的监听者,即需要 getBean 获取 bean 实例。然而 Spring 有个限制————ApplicationContext关闭期间,不得GetBean 否则会报错。
这个知识点得来不易。它是我们公司在线上环境发生故障后,最终定位的原因,大家一定要重视!
前几天,线上系统出现两条异常日志Get Bean时找不到对应的bean,调用堆栈让我非常迷惑,为什么Get Bean找不到对应的Bean呢? 如下图所示图片堆栈中的信息 解释了原因。Do not request a bean from a BeanFactory in a destroy method implementation在应用上下文关闭时,不得从上下文中Get Bean。恰好,这个问题出现在服务关闭期间…..由于系统流量较高,日订单几百万,即便在低峰期单机的并发度也是比较高的,所以服务在关闭期间有少量流量进来或未处理完。这个场景下,使用 Spring Event 发布事件,Spring 无法正常广播事件,一定会出现异常,导致处理失败!大家一定要切记!使用 SpringEvent 之前,一定要先治理服务,确保服务关闭时,先切断入口流量(Http、MQ、RPC),然后再关闭服务,关闭 Spring 上下文!详细的分析请参考:
强一致性的业务例如提单场景。提单阶段,库存扣减成功和订单提单成功务必完全一致。库存扣减失败但提单成功;提单失败,库存未回滚等场景都是要避免发生的异常场景!提单场景,使用 Spring Event会有很多问题。假设提单前,发布提单前置事件,事件订阅者的业务逻辑可能有扣减库存,锁定优惠券资源等操作。库存扣减失败或者锁定资源失败需要回滚整个提单流程,然而 Spring 事件订阅模式无法提供这种 订阅异常——>回滚 的能力。事件发布者无法获知哪些订阅消费失败,哪些订阅者成功?无法准确的触发回滚流程。(如果基于 Spring Event 强行搞回滚,也可以做到,但方案会很复杂!)
4. 最终一致性的业务特性适合——发布订阅模式
最终一致性场景非常适合使用 Spring Event。例如提单成功后,发布 MQ ,释放锁等资源,可使用 SpringEvent 解耦。为什么呢?因为业务上确保提单成功后,提单实际上已经成功,后续的收尾工作不应该触发订单提单失败。在提单成功事件的订阅者中,只有一种执行结果——————成功。即使出现失败,也应该重试直至成功。例如 发布 提单成功MQ 消息,释放提单锁等资源都是务必成功的业务逻辑。再来举一个例子,我们公司在处理订单消息时使用了Spring Event框架。在这个场景中,我们需要处理履约完成、退款完成、订单过期等事件,并且每个事件都有一些独立的业务逻辑,每一个业务场景都属于最终一致性的场景。举个例子,履约完成后需要将履约数据和订单金额等数据通知结算系统。这个业务场景是最终一致性场景,而不是强一致性,这是因为通知结算即便失败,重试即可,无需回滚履约过程。如果我们不使用Spring Event,那么我就需要手动编写观察者模式,并将订单消息根据状态通知到相应的观察者中。又或者每当新增一个业务逻辑时,我需要新增一个Kafka消费组,并且在代码中解析订单消息,然后根据状态将事件发送给相应的订阅者。总之我需要把事件按照状态分发给对应的监听者。在这个场景中,使用Spring Event非常适合。可以将每个事件封装为Spring Event,并且每个业务逻辑都可以通过@EventListener注解来注册对应状态的事件监听器(不过需要注意的是,如果订阅者过多,那么Kafka消息的消费时间可能会增加。那么该如何解决呢?)。使用 Spring Event 框架比自己手写监听者模式强多了。
5. 使用SpringEvent 要有额外的可靠性保证!
Spring Event适用于需要保证最终一致性的业务场景,但为了确保可靠性,必须提供重试能力。通过使用 applicationContext.publishEvent(event) 方法发布事件,Spring会按顺序执行相关的订阅者。如果出现异常,publishEvent 方法会抛出异常,发布者能够感知订阅逻辑处理失败了。在发布事件时,需要考虑事件订阅逻辑出现异常的情况,我提出三种解决办法
订阅者自行重试
订阅逻辑可自行重试保证成功。例如使用 Spring retry注解可以保证出现异常时,重新执行该方法。以下代码示例 performSuccess 方法抛出异常时,Spring 会重新执行该方法直至成功,最多重试 3 次,可设置间隔时间,重试间隔递增时间。