什么是事件驱动架构

Event-driven architecture (EDA) is a software architecture paradigm promoting the production, detection, consumption of, and reaction to events. - Wikipedia 什么是事件驱动架构? 按 维基百科的定义,事件驱动架构(EDA)是一种软件架构范式,促进事件的产生、观测、消费和反应。 在认识事件驱动架构之前,先来明确一下,什么是事件。 事件是已经发生的事实,并且不可变。例如,“创建订单”是个指令,而“订单已创建”就是个事件,订单的信息、订单创建的时间,在这个事件发生后就无法被修改。 在传统的架构体系下,编程思维可以说是过程驱动的,一个指令触发了一段逻辑,这其中可能包含了几个不同的方法行为,它们之间按照过程顺序来编排、嵌套。看起来似乎很清晰,但是很容易写出长长的“面条”代码。而事件驱动架构则强调了事件的产生与消费,将逻辑的过程转换为事件之间的环环相扣。 就如上图所展示的,如果采用事件驱动架构,订单服务就只负责订单的创建,然后发出“订单已创建”的事件,交由 EventBus 路由,转发给关心这个事件的各个监听者。 在我的开发经历中,也有采用事件驱动架构的项目,接下来,就结合自身经验,总结一下 EDA 的优缺点和其适用场景。 EDA 的优点 解耦 首先,解耦是事件驱动架构所带来的最明显的优点。从上述的例子中就可以看出,采用了 EDA 后,订单服务只需要负责处理对应的业务逻辑,而其他的切面逻辑,例如操作记录、发短信等,都可以通过事件的通知机制来解耦,从而保证业务逻辑的简洁、专注。并且,这些切面功能点可以考虑使用异步处理,也能很好地提升系统的响应能力。 DDD 与事件风暴 在领域驱动设计(DDD)中,如果采用 以“领域事件”为核心的建模思路,与业务人员通过事件风暴的形式来梳理业务流程,就会得到一系列的事件列表,也就是事件驱动架构中的所有事件。从业务人员到开发人员,从真实业务逻辑到系统设计实现,都以这些事件为核心,就能够形成统一语言,方便沟通和理解。 事件溯源 事件上存储了必要的信息和时间,如果将所有的事件持久化,结合最初的实体信息,你就拥有了一台来去自如的时光机。例如在一个资产管理系统中,存储了“资产已入池”、“资产归属已变更”、“资产已出池”等所有的事件,并且存储了资产初始化时的快照数据,就可以根据快照数据和给定时间范围内的事件列表,重新计算出某个时间的资产数据,这就是事件溯源。如果整个生命周期中事件量很大,可以考虑定期给实体做快照,溯源时获取最近的一份快照数据。事件溯源能给业务提供更灵活的数据支撑,同时,因为存储了所有的事件数据,也保障了系统的故障恢复能力和数据一致性。 EDA 的缺点 系统复杂度 “在软件工程中,没有一个中间层解决不了的问题”,事件驱动架构,就是通过引入中间层 EventBus 来实现事件机制,看似能带来不少诱人的优点,也必然会增加系统的复杂度。Spring 虽然有提供事件机制,但是比较简单,如果有复杂的使用场景,得考虑自己实现。 事件驱动架构改变了编程思维,一切都围绕事件的发布和订阅,将完整的功能过程,拆解为了不同的事件,也丧失了过程驱动带来的叙事能力。如果事件数量众多,就容易在“事件丛林”中迷了路。 使用场景 软件工程没有银弹,事件驱动架构也有其适用的场景,结合上述优缺点,个人认为,以下几个方向可以考虑采用 EDA: 组件解耦(消息推送、操作记录等) 数据沉淀系统(审计、资产等) 常规的业务逻辑里,如果有一些切面组件,例如消息、操作记录,可以接入轻量的 EDA 来解耦,保证业务逻辑清晰和职责的单一;而在数据沉淀系统中,例如审计、资产等领域,通常没有特别复杂、长链路的业务逻辑,由业务系统来触发更新数据,更关注的是数据的准确性、一致性,就可以考虑使用 EDA 和事件溯源机制。...

July 11, 2021

一招让 Spring 本地测试弹射起步

开发过程中,在本地启动系统来进行一些测试、联调是很日常的操作。然而随着项目不断壮大,系统内会依赖越来越多的中间件或服务,例如 Rocket MQ、Dubbo、定时任务等。在系统启动的时候,需要对这些组件进行初始化,导致启动过程缓慢无比。 系统启动 5 分钟,测试运行 3 秒钟,这谁顶得住啊。而且在本地测试时,通常也用不到这些组件。 如果你也有同样的困扰,接下来就教你一招,利用 Spring 的扩展点 BeanFactoryPostProcessor,无侵入地“阉割”这些拖慢启动时间的组件,让你的系统一键弹射起步。 我们就以 Rocket MQ 为例,在启动的时候加载 5 个消费者。 public class RocketMQConfiguration { /** * SpringBoot 启动时加载所有消费者 */ @PostConstruct public void initConsumer() { Map<String, AbstractRocketConsumer> consumers = applicationContext.getBeansOfType(AbstractRocketConsumer.class); if (CollectionUtils.isEmpty(consumers)) { log.info("no consumers"); } for (String beanName : consumers.keySet()) { AbstractRocketConsumer consumer = consumers.get(beanName); consumer.init(); createConsumer(consumer); log.info("init consumer: title {} , topics {} , tags {}", consumer.consumerTitle, consumer.topics, consumer.tags); } } } 运行一下测试类,系统启动耗时 30 秒,其中 Bean 的初始化过程就占了 28 秒。...

June 22, 2021

Spring 事务事件监听,为什么会出现事务失效?

Spring 在 4.2 版本之后提供了@TransactionlEventListener 注解,可以很方便地在事务提交后做一些处理,但是如果使用不当,或者没有正确理解其背后的运行逻辑,很容易踩坑甚至导致线上故障。 之前工作中就遇到了一个问题,在事务监听时,做了一些事务操作,但是这个事务并没有生效。 今天我们就来深入了解一下,这个问题是怎么产生的,又该如何解决。 问题复现 我们来模拟一个很简单的场景:创建订单的时候会发布“订单已注册”的事件,在事件监听里保存操作记录,再发布“操作记录已保存”的事件,最后在这个事件监听里做一些逻辑。 以下代码中省略了一些不重要的实现。 首先是 OrderService,createOrder() 方法里保存订单记录,发布“订单已注册”的事件: public class OrderService { @Transactional public void createOrder() { String orderNo = "test_no"; Order order = new Order(orderNo); orderRepository.save(order); log.info("publish OrderCreatedEvent"); applicationContext.publishEvent(new OrderCreatedEvent(orderNo)); } } “订单已注册”的事件监听里,调用 operationService.saveOperation(): public class OrderCreatedEventListener { @TransactionalEventListener public void handle(OrderCreatedEvent event) { log.info("handle OrderCreatedEvent : " + event.getOrderNo()); operationService.saveOperation(event.getOrderNo(), "创建订单"); } } OperationService.saveOperation(),保存操作记录,并发布“操作记录已保存”的事件: public class OperationService { @Transactional public void saveOperation(String orderNo, String info) { Operation operation = new Operation(orderNo, info); operationRepository....

March 20, 2021

Mockito饮用指南

介绍 Mockito是一个用于Java的开源测试mock框架,提供了非常清爽、简洁的API,这个名字来源于经典鸡尾酒 Mojito。 什么是mock?mock就是模拟,有了一个类或接口的定义,我们可以创建一个模拟对象来模拟它的行为,从而就不需要提供这个类或接口的真实实现。 这样在写单元测试的时候,我们只需要mock其他依赖,假设它们的预期返回,就可以专注于测试自己的实现逻辑。 听起来还不错吧,赶紧来尝一口试试。 先尝一口 引入 Mockito Mockito支持使用Gradle、Maven、Jar包引入,如果使用Spring Boot的话,spring-boot-starter-test默认已经集成了Mockito。 下文使用的Mockito,版本为3.6.0,项目代码基于Spring Boot。 入门操作 先来看一下官方文档上最简单的两个栗子 验证交互: import static org.mockito.Mockito.*; // mock创建一个 List List mockedList = mock(List.class); // 调用mock对象的方法 mockedList.add("one"); mockedList.clear(); // 可以直接验证方法被调用了 verify(mockedList).add("one"); verify(mockedList).clear(); mock调用返回: // mock创建一个 LinkedList LinkedList mockedList = mock(LinkedList.class); // 使用stub,假设mockedList.get(0)被调用时,会返回"first" when(mockedList.get(0)).thenReturn("first"); // 控制台会打印"first" System.out.println(mockedList.get(0)); // 控制台会打印"null",因为我们没有假设get(999)的返回值 System.out.println(mockedList.get(999)); 怎么样,看起来是不是很简单,语法也很贴近自然语言。 Mockito本质上是代理模式的应用,mock就是创建proxy对象,在proxy被调用前,使用stub的方式设置返回值,proxy还能记录并跟踪行为。 饮用搭配 doSomething() void方法或者spy对象,在mock行为时需要使用doThrow()、doAnswer()、doNothing()、doReturn()、doCallRealMethod() 注意区分mock和spy,mock就是完全代理,spy则是部分mock,可以调用真实方法,同时也能被跟踪验证 List list = new LinkedList(); List spy = spy(list); // 这里会抛出IndexOutOfBoundsException,因为调用了真实方法,而list实际上是个空列表 when(spy.get(0)).thenReturn("foo"); // 使用doReturn()来设置spy....

January 6, 2021

译 | 各种服务总线: 命令总线,服务总线和查询总线

本文为个人翻译,原文链接 上一篇文章讲了一个特殊的服务总线,即命令总线。现在我们回过头来看看其他服务总线,他们有什么相似或是不同之处。 什么是服务总线? 好像很难用简单的话来解释什么是服务总线。如果你在维基百科上查询服务总线,会看到一些关于企业级软件开发的术语定义,还列举了一些微软、IBM和Oracle的例子。这和我平时接触的PHP驱动的网络开发相比,完全是另一个世界。 我先用自己的话来总结一下什么是服务总线吧: 服务总线是组件之间传递信息的方式 消息是以DTO的形式,包含了需要执行的信息 “消息发送组件”会创建消息并传递给总线 “消息接收组件”会告知总线它想要消费的消息类型 当总线接收到消息时,会分发消息给接收者 总线是能够解耦组件的边界,发送者和接收者都不会感知到其他组件 由于解耦,总线能够让组件很高效地合作 而且因为总线是所有消息的“中间商”,可以给所有的消息增加功能,而不需要改动消息、发送者或接收者。例如给所有的消息加上日志打印或者给消息排序 希望这些描述能让你更清楚什么是服务总线。如果还不明白的话,可以看看我之前的文章,解释了什么是命令总线。那篇文章不会很抽象,而且有一些代码样例。 不同的总线 到目前为止,我们讨论了“通用的”服务总线。这个总线只会分发信息,无论如何也不会限定消息,或者是做一些处理。 你可以想象一下,不同类型的消息应该用不同的方式处理,这也是为什么我们有各种各样的服务总线,接下来我会讨论3种: 命令总线 查询总线 事件总线 我们来看一下它们的关键点 命令总线 消息(命令)标识了用户的意图。比如说“创建文章”或者“注册帐号”。 一个命令只能有一个确定的处理者 命令不会返回任何值 查询总线 消息(查询)标识了一次查询操作,注意不是数据库查询。比如“最新的文章”或者“文章的所有评论” 一个查询只能有一个确定的矗立着 查询会返回数据 查询不会改变应用的状态 事件总线 消息(事件)标识了一个已经发生的事件。比如“文章已创建”或者“用户已注册”。 一个事件可以有任意数量的处理者([0, inf])。 只会持有基础变量(字符串,整数值,布尔值),而不是整个类。 事件不会返回值 可以看到这些总线很相似,在我看来这也是他们很有用的原因。总线的概念很容易理解,使用也简单,可以给你的应用增强结构性和可预测性。 最后一点 验证 消息都应该被验证,这就意味着消息对象应该验证自己的入参,这样一来,只有有效的消息会被分发,也会带来一点限制。 “注册用户”命令应该需要(不考虑其他情况)一个用户名。这个命令应该要验证一下用户名是个字符串,并且字符的长度在6和100之间。而用户名是否唯一,这个需要由处理者来验证,而不是通过命令本身。 更大的模式 实现命令和查询是命令查询职责分离(CQRS)的一部分,当然你也可以不应用CQRS,直接使用服务总线。 命令和事件经常一起使用,比如“注册用户”命令完成后,会发送事件“用户已注册”。可以阅读 Matthias Noback 写的 From Commands To Events 。 拓展阅读 如果想更深入了解服务总线和消息: Mathias Verraes 写的 Messaging Flavours Robert Basic 写的 All Aboard The Service Bus Matthias Noback 写的 Some questions about the command bus 或者阅读我的其他文章:...

January 5, 2021