事件驱动可能是客观世界的运作方式,当你点击鼠标、敲击键盘或者插上U盘时,计算机便以中断的形式处理各种外部事件。可见,“事件”这个概念一直在计算机科学领域中扮演着重要的角色。
考虑实际应用,我们经常发现一个用例需要修改多个聚合根的情况,并且不同的聚合根还处于不同的限界上下文中。比如,当你在电商网站上买了东西之后,你的积分会相应增加。这里的购买行为可能被建模为一个订单(Order)对象,而积分可以建模成账户(Account)对象的某个属性,订单和账户均为聚合根,并且分别属于订单系统和账户系统。显然,我们需要在订单和积分之间维护数据一致性,通常的做法是在同一个事务中同时更新两者,意味着要调用不同包里的应用,这会存在不同系统中的耦合性问题。
为遵循原则,一个业务用例对应一个事务,一个事务对应一个聚合根,也即在一次事务中,只能对一个聚合根进行操作。通过引入领域事件,我们可以很好地解决上述问题。
这时候的解决方法,称为基于事件的最终一致性。
注意:不能同时更新多个聚合根,并不是不能同时更新多个数据表。
一、如何定义领域事件?
(一)找出事件
可以通过事件风暴活动找出领域事件,领域事件是用来捕获领域中发生的具有业务价值的一些事情,是领域专家所关心的(需要跟踪的、希望被通知的、会引起其他模型对象改变状态的)发生在领域中的一些事情,要忽略无业务价值的领域事件。
(二)事件命名
一般可以根据聚合中的方法来命名,比如
命令方法:BacklogItem#commitTo
事件输出:BacklogItemCommitted
(三)事件成员
- 事件名称
- 事件发生时间
- 事件ID
- 事件发起者
- 事件参与者
- 上下文必需的信息
领域事件的接口设计示例如下:
//示例代码1
public interface DomainEvent{
//事件ID,身份标识
public int eventVersion();
//事件发生时间
public Date occurredOn();
}
//示例代码2
public abstract class DomainEvent {
//事件ID,身份标识
private final String _id;
//事件发生类型
private final DomainEventType _type;
//事件发生时间
private final Instant _createdAt;
}
//示例代码3
public interface DomainEvent {
//事件ID,身份标识
String id();
//事件发生时间
Date occurredOn();
default Date getCreateTime(){ return occurredOn();}
//事件发生类型
default String type(){return getClass().getSimpleName();}
default String getType(){return type();}
}
实现事件如下:
//事件名称
public final class BacklogItemCommitted implements DomainEvent{
private final Date occurredOn; //事件发生时间
private final BacklogItemId backlogItemId; //事件发起者
private final SprintId sprintId; //事件参与者
private final TenantId tenantId; //上下文必需的信息
//省略getter和setter
}
//有时希望监听某个聚合根的所有事件类型,所以需要抽象出单独一类
public abstract class OrderEvent extends DomainEvent {
private final String orderId; //补充聚合根信息
}
//继承某聚合根,补充上下文信息
public class OrderCreatedEvent extends OrderEvent {
private final BigDecimal price;
private final Address address;
private final List<OrderItem> items;
private final Instant createdAt;
}
//继承某聚合根,补充上下文信息
public class ProductNameUpdatedEvent extends ProductEvent {
private String oldName; //更新前的名称
private String newName; // 更新后的名称
}
(四)事件结构
领域事件是过去发生过的事情,应该建模成不可变的。即通过构造函数初始化后,不可通过setter方法改变。需要提供getter访问各个属性。各个属性和类都声明final
关键字,保证事件不可变。
事件驱动架构分为三种
1.单纯的事件通知,事件结构简单仅包含聚合根ID,消费方需要再请求发送方的数据才能完成事件处理。
2.带状态的事件通知,事件结构复杂,包含上下文信息,消费方不需请求API可直接处理事件。
3.事件溯源中的事件,事件结构复杂,聚合状态的改变必将记录事件,而不是之前只发布感兴趣的事件。
目录组织:事件类文件(class)推荐与聚合根放在相同目录
二、如何进行事件发布?
领域事件一般随着聚合根状态的更新而产生,进行事件发布的模式为发布订阅模式。
(一)认识发布订阅模式
也叫观察者模式。当然严格来说,两者也是有区别的:
观察者模式是主题(Subject)的状态发生变化,遍历观察者(Observer)执行特定方法。两者松耦合。
发布订阅模式是发布者(publisher)发送消息给经纪人(broker),订阅者(subscriber)通过经纪人(broker)订阅某主题(topic)的消息。两者解耦合。
但在此处为了方便,命名为发布订阅模式。
发布订阅模式(观察者模式)代码示例:
//主题类
public class Subject {
//观察者列表
private List<Observer> observers = new ArrayList<Observer>();
//需要监督状态变化的属性
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
//状态变化通知
notifyAllObservers();
}
//注册观察者
public void attach(Observer observer){
observers.add(observer);
}
public void notifyAllObservers(){
//遍历观察者执行相同更新
for (Observer observer : observers) {
observer.update();
}
}
}
//观察者抽象类
public abstract class Observer {
protected Subject subject;
//需要实现update方法才能处理事件
public abstract void update();
}
//实现观察者的具体实现类
public class HexaObserver extends Observer{
public HexaObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
@Override
public void update() {
System.out.println( "Hex String: "
+ Integer.toHexString( subject.getState() ).toUpperCase() );
}
}
//客户端调用
public class Client{
public static void main(String[] args) {
Subject subject = new Subject();
new HexaObserver(subject);
new OctalObserver(subject);
new BinaryObserver(subject);
System.out.println("First state change: 15");
subject.setState(15);
System.out.println("Second state change: 10");
subject.setState(10);
}
}
上述示例代码,实现了观察者模式,对于主题的变化,每个观察者都得到了通知,并做出相应的处理。
唯一的问题是,这属于同步处理,无论发布者,还是订阅者都处于同一进程,同一线程之中,没有网络参与。
状态变更后,要依次执行各个观察者的方法,才能结束调用,被同一事务所管理。
因此,只针对本地限界上下文的轻量级订阅者使用。
对于重量级订阅者,我们希望状态变更后,线程可以去做别的事,不需要等待执行。
这需要实现异步操作。参考下方异步操作。
(二)事件的订阅方
一般在应用服务或领域服务中向领域事件注册订阅方。
注意:事件订阅方被声明为接口,使用的方式不是由需要订阅的对象实现这个接口。
而是临时匿名构造接口的实现,类似于线程的构造,可以用lambda表达式简化代码。
订阅方处理事件,是去修改另一聚合,或对事件进行转发等等。
public interface DomainEventSubscriber<T> {
public void handleEvent(final T aDomainEvent);
public Class<T> subscribedToEventType();
}
实际调用过程示例:
public class OrderApplicationService{
public void commitOrder(String id){
//生成匿名订阅方对象
DomainEventSubscriber subscriber = new DomainEventSubscriber<OrderCommitted>(){
@Override
public void handleEvent(OrderCommitted event){
//订阅方处理事件
//不要在这里修改聚合,否则违背同一事务不能修改多个聚合根的原则
//或者异步进行处理
}
}
//向事件发布者注册订阅方
EventPublisher.subscribe(subscriber);
//获取聚合
Order order = orderRepository.ofId(orderId(id));
//执行业务逻辑,在聚合中发布事件
order.commitOrder();
//保存数据库
orderRepository.save(order);
}
}
(三)领域事件的发布方式
当然,所有的事件发布都是采用的发布订阅模式。
根据复用的对象,发布方式有两种选择:
一种是复用线程,根据线程共享变量来管理发布。
一种是复用实体,根据实体缓存来发布。(推荐)
1.线程方式
用户的每个请求都会由单独的线程予以处理,为了防止冲突,每个线程需要有属于自己的线程变量,由ThreadLocal来实现。
由于很多服务器会维持一个线程池,不同的请求可能同时重用一个线程,因此每次使用前,应该清理先前的订阅方。
//核心类,通过共享线程变量DomainEventBus的方式管理领域事件
public class DomainEventBusHolder {
private static final ThreadLocal<DomainEventBus> THREAD_LOCAL = new ThreadLocal<DomainEventBus>(){
@Override
protected DomainEventBus initialValue() {
return new DefaultDomainEventBus();
}
};
// 获取领域发布者
public static DomainEventPublisher getPublisher(){
return THREAD_LOCAL.get();
}
//获取领域事件注册机构
public static DomainEventHandlerRegistry getHandlerRegistry(){
return THREAD_LOCAL.get();
}
// 清理线程内变量
public static void clean(){
THREAD_LOCAL.remove();
}
}
实际调用流程
//1.应用服务中注册订阅者
public class applicationService {
public void deleteDepartment(DeleteDepartmentCommand aCommand){
//线程清理之前的绑定
DomainEventBusHolder.clean();
//获得事件处理器
DeleteDepartmentHandler handler =new DeleteDepartmentHandler();
//线程注册事件并绑定事件处理器
DomainEventBusHolder.getHandlerRegistry().register(DepartmentDeleted.class,handler);
//删除某个机构
Department department = repository.departmentNamed(aCommand.getName());
department.deactivate();
repository.save(department);
}
}
//2.聚合根里发布事件
public class Department extends AbstractAggregate {
private String name;
private Boolean isDeleted;
private DepartmentId departmentId;
//软删除某个部门
public void deactivate() {
if (!this.isDeleted()) {
//更改状态
this.setDeleted(true);
//通过线程订阅,事件发布总线发布某个事件
DomainEventBusHolder.getPublisher().publish(new DepartmentDeleted(this,this.getName()));
}
}
}
//机构被删除的事件定义
public class DepartmentDeleted extends AbstractAggregateEvent<Department> {
String name;
public DepartmentDeleted(Department source,String name) {
super(source);
this.name = name;
}
}
//3.实际的事件处理
public class DepartmentDeletedHandler implements DomainEventHandler<DeleteDepartment> {
@Override
public void handle(DepartmentDeleted event) {
//短信异步发送
SMSSender.send("机构"+event.getName()+"被删除");
}
}
2.实体缓存发布
先将事件缓存在实体中,在实体状态成功持久化到存储后,再进行事件发布。
发布后,记住清理实体中的事件。
//核心类
public abstract class AbstractAggregate {
//事件列表
private final transient List<DomainEventItem> events = Lists.newArrayList();
//注册事件
protected void registerEvent(DomainEvent event) {
events.add(new DomainEventItem(event));
}
//获得事件
public List<DomainEvent> getEvents() {
return Collections.unmodifiableList(events.stream()
.map(DomainEventItem::getEvent)
.collect(Collectors.toList()));
}
//清理事件
public void cleanEvents() {
events.clear();
}
private static class DomainEventItem {
private DomainEvent domainEvent;
DomainEventItem(DomainEvent event) {
Preconditions.checkArgument(event != null);
this.domainEvent = event;
}
public DomainEvent getEvent() {
if (domainEvent != null) {
return domainEvent;
}
}
}
}
实际调用流程
//1.应用服务中注册订阅者
public class applicationService {
@Autowired
private DomainEventBus domainEventBus;
//初始化,通过实体缓存进行订阅
@PostConstruct
public void init() {
// 使用 Spring 生命周期注册事件处理器
this.domainEventBus.register(DepartmentDeleted.class, new DepartmentDeletedHandler());
}
public void deleteDepartment(DeleteDepartmentCommand aCommand){
//删除某个机构
Department department = repository.departmentNamed(aCommand.getName());
department.deactivate();
repository.save(department);
//获取聚合实体的所有事件
List<DomainEvent> events = department.getEvents();
if (!CollectionUtils.isEmpty(events)) {
// 成功持久化后,对事件进行发布
this.domainEventBus.publishAll(events);
}
events.cleanEvents();
}
}
//2.聚合中注册事件
public class Department extends AbstractAggregate {
//软删除某个机构
public void deactivate() {
if (!this.isDeleted()) {
//更改状态
this.setDeleted(true);
//实体中注册事件
registerEvent(new DepartmentDeleted(this,this.getName());
}
}
}
//3.实际的事件处理
public class DepartmentDeletedHandler implements DomainEventHandler<DeleteDepartment> {
@Override
public void handle(DepartmentDeleted event) {
//短信异步发送
SMSSender.send("部门"+event.getName()+"被删除");
}
}
(三)事件的发布位置有哪些?
1.应用服务(ApplicationService)
2.聚合根(Aggregate)
3.资源库(Repository)
4.事件表(Event Store)(重点推荐)
比较受推崇的方式是引入事件表
1.应用服务
可以在应用服务,保存完聚合根之后再发布事件。发布事件之前需要在聚合中注册事件。
public class OrderApplicationService{
private DomainEventBus domainEventBus;
@Transactional
public void commitOrder(String id){
//...
//获取聚合
Order order = orderRepository.ofId(orderId(id));
//执行业务逻辑,在聚合中注册事件
order.commitOrder();
//保存聚合根
orderRepository.save(order);
//发布事件
List<Event> events = order.getEvents();
events.forEach(event -> eventPublisher.publish(event));
order.clearEvents();
}
}
2.聚合发起
在领域对象中通过调用EventPublisher上的静态方法发布领域事件:
public class Order {
public Order() {
//create order
//...
EventPublisher.publish(new OrderPlacedEvent());
}
}
可以看到,这里聚合根的构造方法中使用静态方法发布事件,并且使用的线程变量的形式发布,虽然避免了API污染(不用注入EventPublisher)。
但是这里的publish()静态方法将产生副作用,对Department对象的测试带来了难处。
此时,我们可以采用“在聚合根中临时保存领域事件”的方式予以改进,即使用实体缓存发布的方式。
3.资源库
也可以在资源库发布事件,同时保存聚合。
public class OrderRepository {
private EventPublisher eventPublisher;
public void save(Order order) {
//获取在Order对象中注册的事件
List<Event> events = order.getEvents();
events.forEach(event -> eventPublisher.publish(event));
order.clearEvents();
//save the order
//...
}
}
4.事件表
背景:
在事件发布过程中,保存业务数据和发布事件要一起进行,若业务保存成功,而事件发送失败,或者业务保存失败,而事件发送成功,都会导致不一致。
解决方式:
在更新业务表的同时,将领域事件一并保存到关系型数据库的事件表中,此时业务表和事件表在同一个本地事务中,事件表被当做临时消息队列使用,即保证了原子性,又保证了效率。
在《微服务架构设计模式》中,这被称为“TRANSACTION OUTBOX TABLE”(事务性消息)。
能将事件保存在NOSQL中,提高效率吗?
若将事件保存NOSQL中,虽然效率高,但只能保证事件写入的原子性,不保证业务更新和事件写入的原子性。
这里事件表用作临时消息队列,发布消息后事件就被删除。
同时,事件存储也有其他作用:
- 作为消息队列。
- 将事件存储用于基于 Rest 的事件通知。
- 作为审计日志,检查模型命名方法产生结果的历史记录。 使用事件存储来进行业务预测和分析。
- 使用事件溯源模式时,使用事件来重建聚合实例,执行聚合的撤销操作。
所以,我们用时间表的主要目的是事务性双写,保证业务更新和发送事件原子性操作,也就是当做临时消息队列。
其中审计日志,我们选择另外通过记录日志表插入感兴趣的事件的方式实现,并不是记录所有事件。
而事件溯源,是另外一种设计模式,查询比较繁琐,需要配合CQRS才能较好的运作。这增加了复杂性和开发难度,除非有必要,否则不建议采用。
在后台开启一个任务,通过AOP、定时任务轮询的方式,也可以采用binlog的方式,将事件表中的事件发布到消息队列中,发送成功之后删除掉事件。
当业务操作的事务完成之后,需要通知消息发送设施即时发布事件到消息队列。
(四)如何选择事件的发布方式?
我们需要达成以下共识:
1.内部事件和外部事件
- 内部事件,是一个领域模型内部(微服务)的事件,在单个限界上下文内,不在多个限界上下文间进行共享。被限制在单个有界上下文边界内部,所以可以直接引用领域对象。
- 外部事件,是对外发布的事件(微服务外),在多个有界上下文中进行共享。只作为数据载体存在。常常采用平面结构,并公开所有属性。版本管理非常重要,以避免重大更改对其服务造成影响。
2.一般情况下,在典型的业务用例中,可能会有很多的内部事件,而只有一两个外部事件。
3.领域事件可以同步发送,也可以异步发送。同步发送可以在一个事务内,异步发送可以由异步线程或消息中间件内实现。
4.根据是否使用消息队列,分为内存总线(事件总线)发布和消息队列发布。内存总线简单高效,同时支持同步、异步两个处理方案,比较适合处理繁杂的内部事件;消息队列虽然复杂,但擅长解决服务间通信问题,适合处理外部事件。
5.是否使用事件存储。
解决方案
因此首先判断某个领域事件是内部还是外部事件
- 若是微服务内的订阅者(内部事件),选择内存总线(事件总线),选择同步或异步发送
- 若是微服务外的订阅者(外部事件),进行事件存储,必须消息中间件异步发送
- 若是同时存在微服务内和外订阅者,则先分发到内部订阅者,将事件消息保存到事件库(表),再异步发送到消息中间件。
例如,在微服务内部使用领域事件时,是内部事件,不一定非得引入消息中间件(比如ActiveMQ等)。以“注册后发送欢迎邮件”为例,注册行为和发送邮件行为虽然通过领域事件集成,但是他们依然发生在同一个线程中,并且是同步的。
注意,发送欢迎邮件并没有更新聚合,而若“注册后更新其他聚合”,则必须是异步的。
其中,应用服务、聚合、资源库中发布可使用内存总线,也可使用消息队列。
而事件表发布必须使用消息队列。
注意:异步修改聚合根需要操作数据库,则所有相关组件必须通过@Autowired注入,不能用new的方式。
比如之前订阅方与处理器用new DepartmentDeletedHandler()的形式绑定,而若修改数据库,则该类需要加@Component的同时通过@Autowired注入。
解决方案
根据上述分析,首先判断事件属于内部事件还是外部事件?
事件总线是实现微服务内聚合之间领域事件的重要组件,它提供事件分发和接收等服务。事件总线是进程内模型,它会在微服务内聚合之间遍历订阅者列表,采取同步或异步的模式传递数据。事件分发流程大致如下:
示例代码:
//内部事件
public class applicationService {
@Autowired
private DomainEventBus domainEventBus;
//初始化,通过实体缓存进行订阅
@PostConstruct
public void init() {
// 使用 Spring 生命周期注册事件处理器
this.domainEventBus.register(DepartmentDeleted.class, new DepartmentDeletedHandler());
}
public void deleteDepartment(DeleteDepartmentCommand aCommand){
//删除某个机构
Department department = repository.departmentNamed(aCommand.getName());
department.deactivate();
repository.save(department);
//获取聚合实体的所有事件
List<DomainEvent> events = department.getEvents();
if (!CollectionUtils.isEmpty(events)) {
// 成功持久化后,对事件进行发布
this.domainEventBus.publishAll(events);
}
department.cleanEvents();
}
}
//2.聚合中注册事件
public class Department extends AbstractAggregate {
//软删除某个机构
public void deactivate() {
if (!this.isDeleted()) {
//更改状态
this.setDeleted(true);
//实体中注册事件
registerEvent(new DepartmentDeleted(this,this.getName());
}
}
}
//3.实际的事件处理
public class DepartmentDeletedHandler implements DomainEventHandler<DeleteDepartment> {
@Override
public void handle(DepartmentDeleted event) {
//短信异步发送
SMSSender.send("部门"+event.getName()+"被删除");
}
}
使用领域事件时需要对事件进行区分,以避免技术实现的问题。
参考资料:在微服务中使用领域事件
三、如何进行事件处理?
发布过程最好做成异步的后台操作,这样不会影响业务处理的正常返回,也不会影响业务处理的效率。
异步操作实现
在Spring Boot项目中,可以考虑采用AOP的方式,在HTTP的POST/PUT/PATCH/DELETE方法完成之后统一发布事件:
//Springboot异步方式
@Slf4j
@Aspect
public class RabbitDomainEventPublishAspect {
private TaskExecutor taskExecutor;
private DomainEventPublisher publisher;
public RabbitDomainEventPublishAspect(DomainEventPublisher publisher) {
this.taskExecutor = taskExecutor();
this.publisher = publisher;
}
private TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1);
executor.setMaxPoolSize(1);
executor.setQueueCapacity(500);
executor.setRejectedExecutionHandler((r, e)
-> log.debug("Domain event publish job rejected silently."));
executor.setThreadNamePrefix("domain-event-publish-executor-");
executor.initialize();
return executor;
}
@After("@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
"@annotation(org.springframework.web.bind.annotation.PutMapping) || " +
"@annotation(org.springframework.web.bind.annotation.PatchMapping) || " +
"@annotation(org.springframework.web.bind.annotation.DeleteMapping) ||" +
"@annotation(org.springframework.amqp.rabbit.annotation.RabbitHandler) ||" +
"@annotation(org.springframework.amqp.rabbit.annotation.RabbitListener) ||" +
"@annotation(com.ecommerce.spring.common.event.messaging.rabbit.EcommerceRabbitListener)")
public void publishEvents(JoinPoint joinPoint) {
log.info("Trigger domain event publish process using Spring AOP.");
taskExecutor.execute(() -> publisher.publishNextBatch());
}
}
//或者聚合持久化之后自己进行异步发布
public class ExecutorBasedDomainEventExecutor implements DomainEventExecutor {
private static final Logger LOGGER = LoggerFactory.getLogger(ExecutorBasedDomainEventExecutor.class);
private final ExecutorService executorService;
public ExecutorBasedDomainEventExecutor(String name, int nThreads, int buffer) {
BasicThreadFactory threadFactory = new BasicThreadFactory.Builder()
.namingPattern(name + "-%d")
.daemon(true)
.uncaughtExceptionHandler((t, e) -> {
LOGGER.error("failed to run task on {}.", t, e);
})
.build();
this.executorService = new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(buffer),
threadFactory,
new ThreadPoolExecutor.CallerRunsPolicy());
}
@Override
public <E extends DomainEvent> void submit(Task<E> task) {
executorService.submit(task);
}
}
DomainEventExecutor eventExecutor = new ExecutorBasedDomainEventExecutor("EventHandler", 1, 100);
this.domainEventBus.register(AccountEnabledEvent.class, eventExecutor,
new AccountEnableEventHandler());
所有聚合根之间的最终一致性必须通过异步的方式得到处理。此时,可以通过消息中间件异步处理,也可以在本地开启多线程进行异步处理,然而会破坏原子性操作。
是否异步遵循的原则:
如果由执行该用例的用户来保证数据的一致性,则使用事务一致性。
如果需要其他系统或用户来保证数据的一致性,则使用最终一致性。
消息设施转发事件应该放在整个企业范围内,或者更大的范围
四、事件存储
(一)为什么进行事件存储?
背景:
之前更改业务数据后,我们通过异步发送领域事件的方式,快速得到了结果。然而,更改业务数据和发布事件应该同时成功,或同时失败,也就是保持原子化。若修改数据成功,而事件发送由于属于异步操作,并不清楚是否会失败,这样会导致bug。
解决办法:
其中一种解决方式是在修改业务表的同时,将领域事件记录到事件表来实现原子操作,两种操作由同一事务控制,也就是同步操作。再通过后台任务将事件表中的事件取出,发送到消息队列。
(二)如何实现事件存储?
1.更新业务表的同时插入事件表
事件由聚合产生,由聚合对事件临时注册和保存
/**
* 聚合根基类,通过实体缓存事件
*/
public abstract class DomainEventAwareAggregate {
@JsonIgnore
private final List<DomainEvent> events = newArrayList();
protected void raiseEvent(DomainEvent event) {
this.events.add(event);
}
void clearEvents() {
this.events.clear();
}
List<DomainEvent> getEvents() {
return Collections.unmodifiableList(events);
}
}
在仓库保存业务数据的同时,保存业务事件
/**
* 领域事件仓库基类,保存实体的同时保存事件
*/
public abstract class DomainEventAwareRepository<AR extends DomainEventAwareAggregate> {
@Autowired
private DomainEventDao eventDao;
public void save(AR aggregate) {
//保存事件到事件表
eventDao.insert(aggregate.getEvents());
aggregate.clearEvents();
doSave(aggregate);
}
protected abstract void doSave(AR aggregate);
}
//业务
public class Order extends DomainEventAwareAggregate {
public void changeAddressDetail(String detail) {
if (this.status == PAID) {
throw new OrderCannotBeModifiedException(this.id);
}
//业务逻辑
this.address = this.address.changeDetailTo(detail);
//记录领域事件
raiseEvent(new OrderAddressChangedEvent(getId().toString(), detail, address.getDetail()));
}
//省略其他代码
}
2.建立事件表和事件记录表
事件存储在数据库表中,数据表分为事件表和事件记录表
//事件存储类DAO
public interface DomainEventDao {
void save(List<DomainEvent> events);
void delete(String eventId);
DomainEvent get(String eventId);
List<DomainEvent> nextPublishBatch(int size);
void markAsPublished(String eventId);
void markAsPublishFailed(String eventId);
void deleteAll();
}
//事件存储类
@Getter
public abstract class DomainEvent {
private String _id = UuidGenerator.newUuid();
private Instant _createdAt = now();
@Override
public String toString() {
return this.getClass().getSimpleName() + "[" + _id + "]";
}
}
sql建表
# 用作分布式定时锁
CREATE TABLE shedlock
(
name VARCHAR(64) NOT NULL,
lock_until DATETIME NULL,
locked_at DATETIME NULL,
locked_by VARCHAR(255) NOT NULL,
PRIMARY KEY (name)
);
# 事件表,事务性发件箱模式
CREATE TABLE DOMAIN_EVENT
(
ID VARCHAR(32) NOT NULL,
JSON_CONTENT JSON NOT NULL,
TYPE VARCHAR(100) GENERATED ALWAYS AS (JSON_CONTENT ->> '$._type') VIRTUAL,
CREATED_AT BIGINT GENERATED ALWAYS AS (JSON_CONTENT ->> '$.createdAt') VIRTUAL,
STATUS VARCHAR(10) NOT NULL DEFAULT 'CREATED',
PRIMARY KEY (ID)
) CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
# 事件表,用作重复消息比对
CREATE TABLE DOMAIN_EVENT_RECEIVE_RECORD
(
EVENT_ID CHAR(32) NOT NULL,
RECORDED_AT TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (EVENT_ID)
) CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
参考示例代码:基于RabbitMQ的示例项目(e-commerce-sample/ecommerce-spring-common)——ThoughtWorks洞见
3.从事件表发布领域事件
在将事件存储在事件表之后,发布者就可以不需要再管了。接下来是消费方需要消费事件表中的事件,所以需要将事件表从数据库表中发送出去。
分为两种方式:
- REST资源方式——拉
- 消息中间件方式——推
REST方式需要自己完善细节,用消息中间件方式更方便。因此,重点介绍。
(三)如何通过消息中间件转发事件?
背景
在更新业务表的同时,将领域事件一并保存到数据库的事件表中,此时业务表和事件表在同一个本地事务中,即保证了原子性,又保证了效率。
然后当需要发送事件时,我们在后台开启一个任务,通过事件表与事件记录表对比的方式识别出未发布的事件。将事件表中的未发布的事件取出,发布到消息队列中,发送成功之后删除掉事件。
在最后一步中,代码中先发布事件,成功后再从事件表中删除事件;
会遇到以下情况:
1.发布消息成功,事件删除也成功,皆大欢喜;
2.如果消息发布不成功,那么代码中不会执行事件删除逻辑,就像事情没有发生一样,一致性得到保证;
3.如果消息发布成功,但是事件删除失败,那么在第二次任务执行时,会重新发布消息,导致消息的重复发送。然而,由于我们要求了消费方的幂等性,也即消费方多次消费同一条消息是ok的,整个过程的一致性也得到了保证。
当消息中间件发送消息时,订阅方可能处理停机状态。消息系统不需要等待订阅方的消息确认信号,只需要保证自身至少投递过一次。
问题:如何保证消息被成功消费?
解决方案
1.接受用户请求;
2.处理用户请求;
3.写入业务表;
4.写入事件表,事件表和业务表的更新在同一个本地数据库事务中;
5.事务完成后,即时触发事件的发送(比如可以通过Spring AOP的方式完成,也可以定时扫描事件表,还可以借助诸如MySQL的binlog之类的机制);
6.后台任务读取事件表,从事件存储中找到所有还没被发布的事件,并按升序排列。
7.后台任务依次遍历领域事件对象,发送事件到某个扇出交换器
8.发送成功后删除事件
接下来看实现代码:
//消息发布服务,只有一个实现,不写接口
public class NotificationApplicationService {
@Autowired
private EventStore eventStore;
@Autowired
private NotificationPublisher notificationPublisher;
//REST访问
@Transactional(readOnly=true)
public NotificationLog currentNotificationLog() {
NotificationLogFactory factory = new NotificationLogFactory(this.eventStore());
return factory.createCurrentNotificationLog();
}
//REST访问
@Transactional(readOnly=true)
public NotificationLog notificationLog(String aNotificationLogId) {
NotificationLogFactory factory = new NotificationLogFactory(this.eventStore());
return factory.createNotificationLog(new NotificationLogId(aNotificationLogId));
}
//中间件发布消息
@Transactional
public void publishNotifications() {
this.notificationPublisher().publishNotifications();
}
}
//实际交给发布者实现
public class RabbitMQNotificationPublisher implements NotificationPublisher {
private EventStore eventStore;
private String exchangeName;
private PublishedNotificationTrackerStore publishedNotificationTrackerStore;
@Override
public void publishNotifications() {
PublishedNotificationTracker publishedNotificationTracker =
this.publishedNotificationTrackerStore().publishedNotificationTracker();
//多通道消息
List<Notification> notifications =
this.listUnpublishedNotifications(
publishedNotificationTracker.mostRecentPublishedNotificationId());
MessageProducer messageProducer = this.messageProducer();
try {
for (Notification notification : notifications) {
this.publish(notification, messageProducer);
}
this.publishedNotificationTrackerStore()
.trackMostRecentPublishedNotification(
publishedNotificationTracker,
notifications);
} finally {
messageProducer.close();
}
}
//多通道发布
private List<Notification> listUnpublishedNotifications(
long aMostRecentPublishedMessageId) {
List<StoredEvent> storedEvents =
this.eventStore().allStoredEventsSince(aMostRecentPublishedMessageId);
List<Notification> notifications =
this.notificationsFrom(storedEvents);
return notifications;
}
private MessageProducer messageProducer() {
// creates my exchange if non-existing
Exchange exchange =
Exchange.fanOutInstance(
ConnectionSettings.instance(),
this.exchangeName(),
true);
// create a message producer used to forward events
MessageProducer messageProducer = MessageProducer.instance(exchange);
return messageProducer;
}
private List<Notification> notificationsFrom(List<StoredEvent> aStoredEvents) {
List<Notification> notifications =
new ArrayList<Notification>(aStoredEvents.size());
for (StoredEvent storedEvent : aStoredEvents) {
DomainEvent domainEvent = storedEvent.toDomainEvent();
Notification notification =
new Notification(storedEvent.eventId(), domainEvent);
notifications.add(notification);
}
return notifications;
}
private void publish(
Notification aNotification,
MessageProducer aMessageProducer) {
MessageParameters messageParameters =
MessageParameters.durableTextParameters(
aNotification.typeName(),
Long.toString(aNotification.notificationId()),
aNotification.occurredOn());
String notification =
NotificationSerializer
.instance()
.serialize(aNotification);
aMessageProducer.send(notification, messageParameters);
}
private PublishedNotificationTrackerStore publishedNotificationTrackerStore() {
return publishedNotificationTrackerStore;
}
private void setPublishedNotificationTrackerStore(PublishedNotificationTrackerStore publishedNotificationTrackerStore) {
this.publishedNotificationTrackerStore = publishedNotificationTrackerStore;
}
}
五、使用工具实现领域事件
以上是通过自己手动编码的方式实现领域事件,还可以借用现成的工具实现发布领域事件。例如,若深度绑定springboot可以使用Spring框架的ApplicationEvent实现发布领域事件。若不是使用spring框架,可以考虑guava的EventBus工具实现。另外,若完全实现领域驱动设计(DDD),可以使用AXON框架。这里主要以使用ApplicationEvent为例。
1.定义事件
首先,定义一个继承自ApplicationEvent的事件类,通常用来封装事件携带的数据。
import org.springframework.context.ApplicationEvent;
public class CustomEvent extends ApplicationEvent {
private String message;
public CustomEvent(Object source, String message) {
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
}
2.创建事件监听器
为了响应这些事件,你需要创建一个监听器,它通常实现 ApplicationListener 接口或使用 @EventListener 注解。
使用 ApplicationListener 接口:
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
public class CustomEventListener implements ApplicationListener<CustomEvent> {
@Override
public void onApplicationEvent(CustomEvent event) {
System.out.println("Received custom event - " + event.getMessage());
}
}
或者使用@EventListener注解(Spring 4.2+):
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class CustomEventListener {
@EventListener
public void handleCustomEvent(CustomEvent event) {
System.out.println("Received custom event - " + event.getMessage());
}
}
3. 发布事件
在需要的地方,通过注入ApplicationEventPublisher来发布事件。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
@Service
public class SomeService {
private final ApplicationEventPublisher publisher;
@Autowired
public SomeService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void triggerCustomEvent(String message) {
CustomEvent event = new CustomEvent(this, message);
publisher.publishEvent(event);
}
}
现在,当调用SomeService的triggerCustomEvent方法时,就会触发CustomEvent,并由CustomEventListener接收到这个事件并执行相应的处理逻辑。
确保你的 Spring 应用程序上下文扫描到了你的监听器和发布者。通常,使用 @Component、@Service 等注解可以自动完成这一任务。
注意:使用 ApplicationEvent 时,请确保你的监听器在发布事件之前已经被 Spring 容器创建和初始化。否则,事件可能不会被捕获。此外,你还可以使用 AsyncEventListener 来异步处理事件,这在处理耗时操作时非常有用。
4.异步处理
在Spring Framework中,异步处理事件可以通过多种方式实现。以下是一个使用@Async注解进行异步事件处理的例子:
@Component
@EnableAsync
public class CustomNotifier {
@Async
@EventListener
public void listenerAsync(CustomEvent event) {
System.out.println("Async: " + event.getName());
}
}
使用 @Async 注解需要 @EnableAsync 来启用 Spring 的异步功能。这个可以放在配置类中开启,此处只是方便显示。