实现领域驱动设计(DDD)系列详解:如何发布领域事件

事件驱动可能是客观世界的运作方式,当你点击鼠标、敲击键盘或者插上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()+"被删除");
    }
}

使用领域事件时需要对事件进行区分,以避免技术实现的问题。

在这里插入图片描述

参考资料:在微服务中使用领域事件

领域驱动设计战术模式:领域事件

后端开发实践系列——事件驱动架构(EDA)编码实践

后端开发实践

三、如何进行事件处理?

发布过程最好做成异步的后台操作,这样不会影响业务处理的正常返回,也不会影响业务处理的效率。

异步操作实现

在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 的异步功能。这个可以放在配置类中开启,此处只是方便显示。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值