最近要排查问题,在代码堆中“考古”时看到一段用SpringEvent实现了业务解耦,刚开始觉得“那叫一个优雅”,回头一想,发现有隐患。
Spring Event是Spring框架提供的一个基于事件的发布订阅机制,它允许不同组件之间通过发布-订阅机制进行解耦的通信,从而增加代码的解耦和灵活性。在Spring中,事件是表示应用程序中特定事件的对象,例如用户注册、订单创建、数据更新等。当这些事件发生时,可以通知其他组件来执行相应的操作。
SpringEvent 适合哪些业务场景呢?这由订阅发布模式的特性决定
在讲SpringEvent隐患前,先来欣赏下它的优雅。
一个发布/订阅至少要包含这几个元素:事件、事件监听器、发布事件
step1:定义事件类:
创建一个事件类,它通常继承自ApplicationEvent类。在这个类中,你可以添加任何需要的属性来传递事件相关的数据。
import org.springframework.context.ApplicationEvent;
public class CustomEvent extends ApplicationEvent {
private final String message;
public CustomEvent(Object source, String message) {
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
}
step2:创建事件监听器: 实现ApplicationListener接口或使用@EventListener注解来创建事件监听器。在监听器中,你可以定义当事件被触发时需要执行的逻辑。
本次我们定义两个事件监听器:
监听器1:
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class CustomEventListenerNo1 implements ApplicationListener<CustomEvent> {
@Override
public void onApplicationEvent(CustomEvent event) {
log.info(" Received custom event - {} ", event.getMessage());
// throw new RuntimeException("报错了No1");
}
}
监听器2:
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class CustomEventListenerNo2 implements ApplicationListener<CustomEvent> {
@Override
public void onApplicationEvent(CustomEvent event) {
log.info(" Received custom event - {} ", event.getMessage());
// throw new RuntimeException("报错了No2");
}
}
或者使用@EventListener注解
@Component
public class CustomEventListener {
@EventListener
public void handleCustomEvent(CustomEvent event) {
log.info(" Received custom event - {} ", event.getMessage());
}
}
step3:发布事件:
注入ApplicationEventPublisher并使用它来发布事件。这可以在任何Spring管理的Bean中完成。
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class CustomEventRunner implements CommandLineRunner {
private final CustomEventPublisher customEventPublisher;
@Autowired
public CustomEventRunner(CustomEventPublisher customEventPublisher) {
this.customEventPublisher = customEventPublisher;
}
@Override
public void run(String... args) throws Exception {
String msg = "Hello, this is a custom event!";
log.info(" begin to publish {} ", msg);
customEventPublisher.publish(msg);
log.info(" end to publish {} ", msg);
}
}
最后,启动应用并测试:
启动Spring Boot应用,在启动过程中CustomEventRunner的run方法会调用发布事件的方法进行事件发布。
执行结果:
2024-10-24 21:42:29.979 INFO 46572 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 80 (http) with context path ''
2024-10-24 21:42:29.992 INFO 46572 --- [ main] com.tree.thrive.TopTreeApplication : Started TopTreeApplication in 2.946 seconds (JVM running for 8.911)
2024-10-24 21:42:29.994 INFO 46572 --- [ main] c.t.thrive.adapter.log.LogAppenderInit : --- LogAppenderInit start ---
2024-10-24 21:42:29.995 INFO 46572 --- [ main] c.t.thrive.adapter.log.LogAppenderInit : --- LogAppenderInit end ---
2024-10-24 21:42:29.995 INFO 46572 --- [ main] c.t.t.business.event.CustomEventRunner : begin to publish Hello, this is a custom event!
2024-10-24 21:42:29.995 INFO 46572 --- [ main] c.t.t.b.event.CustomEventListenerNo1 : Received custom event - Hello, this is a custom event!
2024-10-24 21:42:29.995 INFO 46572 --- [ main] c.t.t.b.event.CustomEventListenerNo2 : Received custom event - Hello, this is a custom event!
2024-10-24 21:42:29.995 INFO 46572 --- [ main] c.t.t.business.event.CustomEventRunner : end to publish Hello, this is a custom event!
好了。一个简版SpringEvent事件驱动代码就写好了。
可以看到,当一个事件发生时,可以通知其他组件来执行相应的操作。 在本例中CustomEvent事件发生后,Spring通知了CustomEventListenerNo1组件和CustomEventListenerNo2组件。是不是完成了事件触发与事件消费解耦,是不是扩展性很好。 事件消费者可以根据需求变化而改动,不影响事件发布者和其它的事件消费者,是不是践行了开闭原则?是不是很优雅?
从上面的日志上看,事件发布和事件消费者是不是都是main线程在执行,即事件发布和事件消费在线程层面是耦合的!
是不是其中一个事件出现异常,后面的事件就不执行了?是的,不执行。
让上面两个事件监听器抛异常:
rerun下:
事件消费者CustomEventListenerNo1抛异常后,事件通知就结束了。其它事件未通知的消费者是不会收到这个事件的。
是的,没错。 SpringEvent真的只是遍历符合条件的事件监听器, 然后在当前线程中逐个通知的。
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
org.springframework.context.event.SimpleApplicationEventMulticaster#multicastEvent(org.springframework.context.ApplicationEvent, org.springframework.core.ResolvableType)
有同学会讲,事件执行时,我可以加异步呀,使用@Async或自己手搓一个线程池都能异步化。 异步事件处理:如果你需要异步处理事件,可以使用@Async注解在事件监听器的方法上,并且确保你的配置中启用了异步支持。
@Component
public class AsyncCustomEventListener {
@Async
@EventListener
public void handleCustomEvent(CustomEvent event) {
// 异步处理事件
}
}
但有一点没变: 所有的事件监听器都在一个JVM中,并且没有机制能保证事件一定会被正常的消费掉。 譬如发版时,把一个正在消费事件的应用kill掉。事件是不是就丢了?
我们来小结下SpringEvent发布/订阅机制的缺点: 1、业务系统一定要先实现优雅关闭服务,才能使用 Spring Event
使用 SpringEvent 之前,一定要先治理服务,确保服务关闭时,先切断入口流量(Http、MQ、RPC),然后再关闭服务,关闭 Spring 上下文! 不然能怎么样?事件数据丢了呗,然后苦哈哈自己手动fix数据。
2、服务启动阶段,Spring Event 事件丢失
在 init-method
阶段开始消费,然而 Spring EventListener 被注册进 Spring 的时间点滞后于 init-method
时间点,所以 Kafka Consumer 中使用 Spring Event 发布事件时,没有找到监听者,出现消息处理丢失的情况。
3、使用SpringEvent 要有额外的可靠性保证!
(1)订阅者自行重试
(2)Spring 订阅者务必保证幂等
为了提高可靠性,要有额外的重试机制保证 Spring 订阅发布的可靠性。
有重试就要有幂等!要保证 订阅者逻辑具备幂等性。Spring 不知道哪些订阅者成功,哪些订阅者失败,下一次重试时,会全部执行所有的订阅者。所以订阅逻辑要做好幂等,防止数据不一致情况发生。
SpringEvent和MQ都具备发布/订阅的能力,使用的成本和适用场景不同。
Spring Event和 MQ 都属于订阅发布模式的应用,然而 MQ 比 SpringEvent 强大且复杂。MQ 更适合应用之间的解耦、隔离、事件通知。例如订单支付、订单完成、订单履约完成等等事件需要广播出去,通知下游其他微服务, 这种场景更适合使用 MQ 。
然而对于应用内需要订阅发布的场景更适合使用 SpringEvent。两者并不矛盾,MQ 能力更强大,技术方案也更”重“一些。Spring Event 更加小巧适合应用内订阅发布,实现业务逻辑解耦。
最后,提一个建议: 重要的数据和事件不能使用SpringEvent。
因为做到系统优雅关闭很难。
一是技术落地难,涉及到DevOps和应用自己。
二是上线的过程会变慢。要等老服务需要任务跑完才能关闭。不然事件数据就丢了呀,然后苦哈哈fix数据,然后到处赔笑脸解释数据不一致的原因。
Spring Event 业务解耦神器,大大提高可扩展性,刷爆了!
实战来了~Spring Event 业务解耦神器,大大提高可扩展性!
SpringBoot Event,事件驱动轻松实现业务解耦 Spring boot 优雅的 EventListener 事件驱动发送邮件 轻量级内部组件解耦神器 Spring Event(Spring 事件)
今天是1024程序员节,在这特别的日子里,愿所有程序员同志代码如往日一般行云流水,bug如过眼云烟视而不见;愿大家的键盘谱写美妙的乐章,创造出更加神奇的数字世界!祝大家节日快乐
另外,今天看到一个梗,信息量很大,挺有意思的,分享一下:
今天是1024程序员节,祝各位Windows开机蓝屏,Linux开机Kernel Panic,macos开机五国,
祝各位运维:服务器iDRAC/ iLO/IPMI/KVM全部失联,机柜全断电,raid全爆炸,nas数据全丢,电表全倒转,空开全烧穿。CN2全绕路,线路全阻断,海外网站全被墙,服务器炸库,网关无响应,代理500,网站502,RAID组几个一起炸几个,UPS爆炸,一年到头DDOS CC不断,流量几千个T,并发上亿,ping全超时,资源404,SSL全重定向,CDN全不回源
祝网络工程师:路由器全爆炸,路由表内存全溢出,交换机全环路,防火墙全阻断,无线信道全冲突,压接网线全短路,bgp全漏表,光模块全炸,光纤全不通,光猫全烫手,
祝dba:数据库Delete,数据库超时。锁库锁表,binlog丢失,硬盘打满。主从数据库脑裂。
祝运营:备案全重审,爬虫三毫秒来你家一次但是收录零蛋
祝PHP程序员:PHP全Fatal Error,fileinfo全装不上,Laravel Mix全报未知错误,
祝前端程序:npm/composer install全报错,服务器全部宕机,Linux rm -rf /*,
祝java程序员:全oom
祝go程序员:全panic
祝c/c++ 程序员:全空指针
祝ai训练师:显存oom,参数不收敛
祝iOS程序员:都4.3
祝android程序员:各种设备不能适配。
入秋了,早上8点出门要穿外套了。不然走上2-3公里,浑身凉飕飕的。 白天,单衣不暖短裤薄; 晚上,薄被不耐五更寒。
今天干了件不小的事,祝自己和看到的人:“顺遂无虞,所得皆所愿”。