Spring Boot - 事件驱动
# 事件驱动
事件驱动:即跟随当前时间点上出现的时间,调动可用资源,执行相关任务,使不断出现的问题得以解决,防止事务堆积。
如:注册账号时会收到验证码,火车发车前收到异性,预订酒店后收到短信通知。如:浏览器中点击按钮请求后台,鼠标点击变化内容,键盘输入显示数据,服务接受请求后分发请求等。
在解决上述问题时,应用程序是由「时间」驱动运行的,这类程序在编写时往往可以采用相同的模型实现,我们可以将这种编程模型称为事件驱动模型。
# 事件驱动模型
事件驱动模型有很多种体现形式,如简单的事件触发机制、单线程异步任务、多线程异步任务等,但是各种技术中实现事件驱动模型的思路基本相同。事件驱动模型包括四个(三个)基本要素:事件、事件消费方、事件生产方。
- 事件:描述发生的事情。比如说浏览器页面点击事件,鼠标、键盘输入事件,spring 请求处理完成、spring 容器刷新完毕等
- 事件生产方(事件源):事件的生产方。任何一个事件都必须有一个事件源。比如 input、button、spring 中请求处理完毕的事件源就是 DispacherServlet、spring 容器刷新完毕的事件源 就是 ApplicationContext
- 事件管理器(时间广播器):派发事件。事件和事件监听的桥梁、负责把事件通知给事件监听器(可在事件源中实现)
- 事件消费方(事件监听器):处理事件。监听事件的发生、可以再监听器中做一些处理
# 解决的问题
基于事件驱动的的应用程序可以实时响应所关心的时间,实现实时检测、响应外部动作,这是事件驱动模型的基本功能和作用。在一些复杂的系统中,事件驱动还可以很好地发挥一下作用。
实现组件之间的松耦合、解耦
在复杂系统中,往往存在多个组件相互耦合的情况,如果将组件之间的耦合关系抽象成「时间(Event)」,让事件担任组件之间的通信任务,就能降低、解除组件之间的耦合关系。
事件驱动模型,实际上是将组件之间的耦合关系转移到了「事件(Event)」上,但是对于某个领域而言事件(Event)一般具有通用性并且不会频繁变更实现逻辑,所以事件驱动模型可以很好的实现组件之间的解耦。
实现异步任务
一些业务场景中,顺序、阻塞式地执行任务会遇到一些比较耗时的中间步骤,但是不希望整个流程都停下来等待这些中间步骤完成,而是触发一个异步操作然后继续执行当前任务,在收到异步操作处理完成的消息之后再执行相关的处理。
使用事件驱动模型实现异步任务的一般思路是:当遇到耗时较大、没有同步执行要求的操作时,针对这个操作触发一个事件,将这个事件加入到任务队列中,直到有一个进程(线程)能够获取并执行这个任务,才开始执行这个任务。
跟踪状态变化
在存储实体模型的业务中通常需要修改实体模型的数据,对于部分业务场景需要存储、使用实体模型的历史变更记录,例如什么时间对实体数据做了什么修改。
对于这类需求,事件驱动模型也可以提供很好的解决方案,我们可以认为每次修改实体数据都是一次事件,那么在修改实体数据后将这个事件存储到事件队列中即可实现跟踪状态变化的需求。
限流、消峰等
# 观察者模型
观察者模式是一种对象行为模式。它定义对象间的一种一对多的依赖关系(被观察者维护观察者列表),当一个对象的状态发生改变时,列表中所有观察者都会接收到状态改变的通知,观察者把自己注册到被观察者持有的列表中,当被观察者发布通知,也就是有事件触发时,由被观察者轮询调用观察者的处理代码。
# 发布、订阅模型
发布订阅模式其实是对象间的一对多的依赖关系(利用消息管理器),当一个对象的状态发生改变时,所有依赖于它的对象都得到状态改变的通知,订阅者通过调度中心订阅自己关注的事件,当发布者发布事件到调度中心,也就是该事件触发时,由调度中心统一调度订阅者的处理代码。
# Spring 事件驱动
经过上面的介绍,实际我们可以看事件驱动看作 MQ,而在 Spring Boot 的事件驱动,它类似于单机版 MQ。
如果想了解 Spring Boot 更多的内置事件,请看:Spring Boot - 生命周期与事件。
# 前言
简介: 在项目实际开发过程中,我们有很多这样的业务场景:一个事务中处理完一个业务逻辑后需要跟着处理另外一个业务逻辑,伪码大致如下:
@Service
public class ProductServiceImpl {
public void saveProduct(Product product) {
// 1. 保存订单
productMapper.saveOrder(product);
// 2. 发送通知
notifyService.notify(product);
}
}
2
3
4
5
6
7
8
9
很简单并且很常见的一段业务逻辑:首先将产品先保存数据库,然后发送通知。
某一天你们可能需要把新增的产品存到 ES 数据库中,这时候也需要代码可能变成这样:
@Service
public class ProductServiceImpl {
public void saveProduct(Product product) {
// 1. 保存订单
productMapper.saveProduct(product);
// 保存到 ES 数据库
esService.saveProduct(product)
// 2. 发送通知
notifyService.notify(product);
}
}
2
3
4
5
6
7
8
9
10
11
随着业务需求的变化,代码也需要跟着一遍遍的修改。而且还会存在另外一个问题,如果通知系统挂了,那就不能再新增产品了,这也不符合设计模式的开闭原则。
对于上面这种情况非常适合引入消息中间件(消息队列)来对业务进行解耦,但并非所有的业务系统都会引入消息中间件(引入会第三方架构组件会带来很大的运维成本)。
Spring 提供了事件驱动机制可以帮助我们实现这一需求。
# Spring 事件驱动
Spring 事件驱动由 3 个部分组成:
- ApplicationEvent:表示事件本身,自定义事件需要继承该类,用来定义事件
- ApplicationEventPublisher:事件发送器,主要用来发布事件
- ApplicationListener:事件监听器接口,监听类实现 ApplicationListener 里 onApplicationEvent 方法即可,也可以在方法上增加@EventListener以实现事件监听
实现 Spring 事件驱动一般只需要三步:
- 自定义需要发布的事件类,需要继承 ApplicationEvent 类
- 使用 ApplicationEventPublisher 来发布自定义事件
- 使用 @EventListener 来监听事件
这里需要特别注意一点,默认情况下事件是同步的。即事件被 Publish 后会等待 Listener 的处理。如果发布事件处的业务存在事务,监听器处理也会在相同的事务中。如果需要异步处理事件,可以 onApplicationEvent 方法上加 @Aync
支持异步或在有 @EventListener
的注解方法上加上 @Aync
。
# 使用事件驱动
这里以用户登录为例子。
User 类
@NoArgsConstructor
@AllArgsConstructor
@Setter
@Getter
public class User {
private String username;
private String passdown;
}
2
3
4
5
6
7
8
首先创建户登录成功的一个事件类,类似于 MQ 的消息。
public class LoinSuccessEvent extends ApplicationEvent {
// 将用户的信息发送到事件里存储
public LoinSuccessEvent(User user) {
super(user);
}
}
2
3
4
5
6
7
然后在用户登录的时候使用 Spring 自带的 ApplicationEventPublisher 类发布该事件,该类就类似于 MQ。
@RestController
@Slf4j
public class UserController {
@Autowired
private ApplicationEventPublisher applicationEventPublisher;
@GetMapping("/login")
public String login(User user) {
// 1. 创建事件
LoinSuccessEvent loinSuccessEvent = new LoinSuccessEvent(user);
// 2. 发布事件
applicationEventPublisher.publishEvent(loinSuccessEvent);
// 模拟登录成功,实际应该去数据库校验
return user.getUsername() + "登录成功!";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
第一种方式:创建事件的监听类
自定义类需要继承 ApplicationListener,泛型就是要监听的事件类。然后重写 onApplicationEvent
方法,当事件产生后,则调用该方法,将事件类接收的对象传过来。
@Service
public class AccountService implements ApplicationListener<LoinSuccessEvent> {
@Override
public void onApplicationEvent(LoinSuccessEvent event) {
User source = (User) event.getSource();
login(source);
}
public void login(User user) {
System.out.println(user.getUsername() + " 登录,密码为:" + user.getPassdown());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
第二种方式:创建事件的监听类
使用 @EventListener
注解,然后对应方法的参数就是要监听的事件类。
@Service
public class CouponService {
@EventListener
public void onEvent(LoinSuccessEvent event) {
User source = (User) event.getSource();
sendCoupon(source.getUsername());
}
public void sendCoupon(String username){
System.out.println(username + " 随机得到了一张优惠券");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
如果创建了很多的监听类,那么如果这些监听类之间也有关联,比如监听类 A 必须等监听类 B 执行后才执行,那么可以使用 @Order
注解来控制执行顺序。
@Service
public class CouponService {
@Order(1)
@EventListener
public void onEvent(LoinSuccessEvent event) {
User source = (User) event.getSource();
sendCoupon(source.getUsername());
}
public void sendCoupon(String username){
System.out.println(username + " 随机得到了一张优惠券");
}
}
@Service
public class AccountService implements ApplicationListener<LoinSuccessEvent> {
@Order(1)
@Override
public void onApplicationEvent(LoinSuccessEvent event) {
User source = (User) event.getSource();
login(source);
}
public void login(User user) {
System.out.println(user.getUsername() + " 登录,密码为:" + user.getPassdown());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
番外:上面使用了 ApplicationEventPublisher 来发布事件,除了通过依赖注入的形式获取 ApplicationEventPublisher,也可以使用如下方式获取 ApplicationEventPublisher:
@Component
public class MyEventPublisher implements ApplicationEventPublisherAware {
private ApplicationEventPublisher applicationEventPublisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
/**
* 发送事件到 Spring Boot 底层的事件发布类
*/
public void publishEvent(ApplicationEvent applicationEvent) {
applicationEventPublisher.publishEvent(applicationEvent);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这样可以使用 MyEventPublisher 类来发布事件:
@RestController
@Slf4j
public class UserController {
@Autowired
private MyEventPublisher myEventPublisher;
@GetMapping("/login")
public String login(User user) {
// 1. 创建事件
LoinSuccessEvent loinSuccessEvent = new LoinSuccessEvent(user);
// 2. 发布事件
myEventPublisher.publishEvent(loinSuccessEvent);
// 模拟登录成功,实际应该去数据库校验
return user.getUsername() + "登录成功!";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17