12.事件溯源

258 阅读19分钟

事件溯源

1.历史

MartinFowler在2005年的博客中提及了“Event Sourcing”这个词语,他将事件描述为一个应用的一系列状态改变,这一系列事件能够捕获用来重建当前状态的一切事实真相。他认为事件是不可变的,事件日志是一种只会不断追(加appendonly)的存储。事件从来不会被删除,这意味着事件可以被重放

2.介绍

事件溯源(Event Sourcing)是一种软件设计模式(数据存储模式),它将系统的状态变化表示为一系列事件(Event),并将这些事件持久化存储(按照发生的顺序进行存储),通过记录和存储事件,通过记录和存储事件,系统可以根据事件重放的方式来重建和恢复系统的状态

对于DDD来说事件溯源是构建业务逻辑和持久化聚合的另一种选择,它将聚合以一系列事件的方式持久化保存。每个事件代表聚合的一次状态变化,应用程序通过重放事件来还原聚合的状态

3.案例理解事件溯源(一)

传统存储方式

使用数据库来存储当前应用程序的当前状态,假设我们要记录库存中的物品数量,所以当我们再收到5件时,我们将总数设置为15

1745473549934.png

1745473561211.png

事件溯源方式

事件溯源不会更新当前状态,而是将每个变化作为新事件插入数据库,这样我们就可以利用这些事件来计算当前的状态,或者计算任意给定时间的状态

1745473652172.png

一旦我们插入了事件,就永远不会删除它们。当我们想要删除任何内容时,我们会为这件事创建一个新的事件,这意味着我们永远不会丢失任何信息

1745473715456.png

4.案例理解事件溯源(二)

传统存储方式

在传统的实现中,每个命令作用于聚合根上,都会更新聚合根的状态,数据库保存聚合根的最新状态,如图所示:

45343543457asdawio.png

这种方式在数据库中将每个聚合根实例存储为一行,并且当前保存的是聚合根最新的状态。传统的保存聚合根最新状态的数据库表设计如图所示:

asdasdasdajdasjdoawio.png

事件溯源方式

事件溯源的执行方法与之相反,事件溯源不保存聚合根的最新状态,保存的是每个命令作用域聚合根之后产生的领域事件。事件溯源只保存领域事件,如图所示:

asdasdsa12341241232awio.png

事件溯源的表结构如图所示:

asdadasdasdsaxzcasd21awio.png

在事件溯源的实现方案中,数据库保存的是每个领域事件,当有新的命令(Command)需要作用在聚合上时,需要先从数据库中查询出历史事件,并通过回放历史事件进行重建聚合根,再将命令作用在聚合根上进行验证和计算本次命令对应的领域事件,最后将新的领域事件保存起来

5.事件溯源中核心组件

与传统DDD核心组件的区别

asdsadasdzxczxcasdasdwio.png

聚合根

CommandProcessor接口

考虑到聚合根的process方法经常需要与外部服务进行交互,例如,分布式ID服务中为聚合根申请唯一标识,由于不建议在聚合根中依赖注入基础设施层的对象,因此可以将process方法从聚合根中抽取出来,形成一个CommandProcessor接口,在该接口的实现类中完成聚合根process方法相同的处理逻辑,也可以为其依赖注入某些基础设施层的对象,使聚合根与基础设施层解耦。相应地,仍然要求CommandProcessor接口不能修改聚合根的状态

伪代码如下:

/**
 * 规范:只能读取聚合根的值,不能修改聚合根
 * 作用:校验聚合根的数据合法性,通过Command与聚合原本的属性进行运算生成领域事件
 */
public interface CommandProcessor {
    List<DomainEvent> process(AggregateRoot root,Command command);
}
@Component
public class ProductCreateCommandProcessor implements CommandProcessor {
    @Resource
    private IdService idService;
    
    @Override
    public List<DomainEvent> process(AggregateRoot root,Command command) {
        // 强转获得真正的类型
        ProductCreateCommand createCommand = (ProductCreateCommand)command;
        Product product = (Product)root;
        // 创建领域事件和基本信息赋值
        ProductCreated productCreated = new ProductCreated();
        productCreated.setEventId("xxx");
        productCreated.setEntityId(idService.nextId());
        productCreated.setEventTime(new Date());
        productCreated.setEventType(createCommand.getClass().getSimpleName());
        // 领域事件业务信息赋值
        productCreated.setProductName(createCommand.getProductName());
        productCreated.setCount(createCommand.getCount());
        productCreated.setPicture(createCommand.getPicture());
        // 返回
        return Collections.singletonList(productCreated);
    }
}
apply方法

在过去,聚合根通过提供业务方法来修改自身的状态。在事件溯源机制下,聚合根将状态的修改看作两个过程:

  1. process校验参数的合法性,并尝试通过聚合根的状态和Command中的信息计算出某个命令应该得到的结果,则将其封装为领域事件
  2. apply方法读取领域事件中的信息,并将领域事件的信息更新到聚合根,最后将该领域事件保存起来
public void apply(ProductCreated domainEvent) {
    // 新创建的聚合根需要赋予唯一标识
    this.productId = new ProductId(domainEvent.getEntityId());
    this.productName = domainEvent.getProductName();
    this.count = domainEvent.getCount();
    this.picture = domainEvent.getPicture();
    // 注册领域事件,暂存起来,后续进行持久化
    super.registerDomainEvents(domainEvent);
}
rebuild方法与抽象基类

这是rebuild方法用来重建聚合根的最基本的伪代码:

public void rebuild(List<DomainEvent> events) {
    try{
        // 1.逐个事件回放
        for(DomainEvent domainEvent : events) {
            this.apply(domainEvent);
        }
        // 2.清空领域事件,因为重建领域事件不需要记录下来保存在库中
        this.clearDomainEvents();
    }catch(Exception e) {
    	throw new RuntimeException("找不到对应的apply方法");    
    }
}

rebuild方法在实现时要注意两点:

  1. 需要找到具体的领域事件对应的apply方法。由于此时events中的领域事件类型被转成了DomainEvent,因此在重放时无法匹配到对应的方法,需要简单优雅地找到领域事件对应的apply方法
  2. 需要在完成重建之后,清空apply方法保存的领域事件。因为这些领域事件已经生效,不需要重新持久化一次

rebuild操作是所有聚合根不可或缺的操作,因此可以将聚合根共同的属性和操作抽取出来,形成抽象的超类型层,命名为AbstractAggregateRoot,伪代码如下:

public abstract AbstractAggregateRoot extends AbstractDomainMask {
    /**
     * 事件和方法的映射关系
     */
     private static final Map<Class<?>,Method> applyMethodMap = new ConcurrentHashMap();
     /**
      * 事件列表
      */
      private List<DomainEvent> domainEvents = new ArrayList();
      
      // 初始化applyMethodMap
      {
          if(applyMethodMap.isEmpty()){
          		Method[] methods = getClass().getMethods();
          		for(Method method : methods) {
                    // 跳过非apply方法
                    if(!method.getName().equals("apply")) {
                        continue;
                    }
                    Class<?>[] parameterTypes = method.getParameterTypes();
                    applyMethodMap.put(parameterTypes[0],method);
          	}
          }
      }
      
      /**
       * 获取事件列表
       */
       public List<DomainEvent> getDomainEvents(){
           return domainEvents;
       }
       /**
        * 注册领域事件
        */
       protected final void registerDomainEvents(DomainEvent event){
           this.domainEvents.add(event);
       }
       /**
        * 注册领域事件
        */
       protected final void registerDomainEvents(List<DomainEvent> events){
           this.domainEvents.addAll(events);
       }
       /**
        * 重建聚合根方法
        */
        public void rebuild(List<DomainEvent> events) {
            try{
                for(DomainEvent domainEvent : events){
                    Method method = applyMethodMap.get(domainEvent.getClass());
                    method.invoke(this,domainEvent);
                }
            }catch(Exception e) {
                throw new RuntimeException("找不到对应的apply方法");
            }
        }
}

工厂

在事件溯源机制下,Factory只需要创建空白的聚合根就可以了。原先给刚创建的聚合根赋值唯一标识的操作已交给CommandProcessor

仓储

Repository的调整主要是增加对领域事件的维护逻辑

load方法:查询event表的历史事件,将历史事件转化为对应的领域事件类型,最后通过回放领域事件重建聚合根,并查询出实体的版本号(version)

save方法:不再保存聚合根状态,而是保存领域事件

@Component
public class DomainRepositoryImpl implements DomainRepository {
    DomainFactory domainFactory = new DomainFactory();
    
    @Resource
    private EventJdbcRepository eventJdbcRepository;
    @Resource
    private EntityJdbcRepository entityJdbcRepository;
    
    @Override
    public Product load(ProductId productId) {
        // 1.创建空的聚合根
        AggregateRoot root = DomainFactory.newInstace();
        // 2.查询数据库entity表获得该聚合根的乐观锁
        DataEntity dataEntity = entityJdbcRepository.queryOneByEntityId(productId.getValue());
        // 3.赋值属性,这里省略部分属性
        root.setVersion(dataEntity.getVersion());
        root.setDeleted(dataEntity.getDeleted());
        // 4.查询Event Store表取出历史事件列表
        List<DataEvent> dataEvents = eventJdbcRepository.loadHistoryEvents(productId.getValue());
        // 4.回放重建聚合
        root.rebuild(domainEvents);
        // 返回
        return root;
    }
}

5.实现事件溯源

基础版

使用前提

首先,事件溯源是基于领域事件的,所以需要实现事件存储(Event Store),在领域事件章节已经讲过

介绍

在事件溯源的实现方案中,数据库不再存储聚合根的状态,而是存储导致聚合状态比发生变更的事件,因此事件溯源的执行过程于经典的DDD设计执行过程有一些区别

并发修改问题

事件溯源中聚合根是通过回放历史领域事件进行重建的,假设同时有两个命令请求修改聚合根,则有可能执行某个命令的线程还没有完成领域事件的持久化,此时另外的线程又将历史事件读取出来重建聚合根执行业务操作,从而产生并发修改数据的问题,导致业务结果不正确

一般通过引入乐观锁的机制来避免数据并发更新不正确的问题

创建聚合根的过程

asdasda213123asdasdcawio.png

  1. 创建空聚合根:当收到创建聚合根的CreateCommand命令时,由于此时聚合根并不存在,所以不涉及聚合根重建的过程,只需要通过DomainFactory创建一个空的聚合根即可
  2. 执行聚合根的process方法:将CreateCommand命令应用于聚合根的process方法来校验Command数据的合法性,校验通过之后将命令携带的信息与聚合根原本的属性进行运算,并生成命令对应的领域事件
  3. 注意:process方法不管是接收创建还是修改命令,都只是对聚合根进行状态的读取,不会修改聚合根内部的值。process方法获取聚合根的状态,并于命令中的值进行计算,将计算结果封装成领域事件,在这个过程中不会修改聚合根的状态
  4. 执行聚合根的apply方法:得到命令对应的领域事件后,将领域事件应用于聚合根的apply方法会获取领域事件内的信息并更新聚合根内部的状态
  5. 仓储进行持久化聚合根:以上过程执行完之后,就得到了最新状态的聚合根,此时使用仓储的save方法将聚合根进行持久化。事件溯源下的save和传统的DDD的save不太一样,做两件事,第一件是持久化apply方法保存起来的领域事件,第二件是持久化聚合根的唯一标识并为其分配一个version用来实现乐观锁

应用层伪代码如下:

@Service
public class ApplicationService {
    private DomainFactory domainFactory = new DomainFactory();
    
	@Resource
	private DomainRepository domainRepository;
	
	public void create(Command command) {
        // 1.创建一个空聚合根
        AggregateRoot root = domainFactory.newInstace();
        // 2.找到对应Command对应的CommandProcessor,这个类很简单里面就是个Map做个映射,就不多说了
        CommandProcessor commandProcessor = CommandProcessorRegister.mappingCommandProcessor(command);
        // 3.使用Command处理器生成领域事件
        List<DomainEvent> domainEvents = commandProcessor.process(root,command);
        // 4.应用事件到聚合根中
        root.apply(domainEvents);
        // 5.保存聚和根
        domainRepository.save(root);
	}
}
修改聚合根的过程

asdsadasdsdasdafawio.png

@Service
public class ProductApplicationService {
    @Resource
    private ProductRepository productRepository;
    
    public void modify (Command command) {
        // 1.加载聚合根
        Product product = productRepository.load(new ProductId(command.getEntityId()));
        // 2.找到Command对应的处理器
        CmmandProcessor commandProcessor = CmmandProcessorRegister.mappingCommandProcessor(command);
        // 3.使用Command处理器生成领域事件
        List<DomainEvent> domainEvents = commandProcessor.process(product,command);
        // 4.应用领域事件
        product.apply(domainEvents);
        // 5.保存聚合根
        productRepository.save(product);
    }
}

快照版

基础版的问题

执行每个修改的命令都需要查询所有的领域事件来重建聚合根。当历史事件数量庞大时,这种方式是相当低效的,当历史领域事件非常多时,很有可能占用大量的内存资源

介绍

在基础版的方案上引入了快照的概念。当领域事件到达一定数量时,将这些领域事件构建的聚合根状态保存为快照。下次收到修改命令时,先读取该快照得到一个聚合根,再读取该快照之后的领域事件,将聚合根更新到最新状态。通过快照,提升了聚合根重建的效率,并且解决了潜在的内存资源占用问题

方案

可以在聚合根的超类型中增加一个是否创建快照的标识takeSnapshot,数据类型为布尔类型

当聚合根通过rebuild方法回放历史事件进行重建时,对当前需要回放的事件数量进行判断,如果超过一定的阈值,则将该takeSnapshot设置为true,然后Repository的save方法在保存领域事件时,判断takeSnapshot的值,如果takeSnapshot被设置为true,则创建或者更新快照

创建聚合根过程

由于创建聚合根的过程不涉及聚合根的重建,因此不需要考虑读取和创建快照

修改聚合根的过程

在修改聚合根时,涉及加载聚合根和保存聚合根两个过程,下面分别对其进行探讨:

加载聚合根:通过Repository的load方法加载聚合根。具体流程如下图所示:

11111111111111rawio.png

保存聚合根:通过Repository的load方法加载聚合根,具体流程如下图所示:

asdasdzxczxasdasfwio.png

快照表
// 省略了通用字段,比如创建时间,创建人啥的
create table `t_snapshot` (
	'id' bigint NOT NULL AUTO_INCREMENT COMMENT '自增主键',
	'entity_id' varchar(64) NOT NULL COMMENT '事件ID',
	'event_data' varchar(4096) NOT NULL COMMENT '聚合根序列化后JSON串',
	'event_id' varchar(64) NOT NULL COMMENT '事件id',
	'event_time' datetime NOT NULL COMMENT '事件发生时间',
	'version' bigint DEFAULT 1 COMMENT '乐观锁',
	PRIMARY KEY('id'),
	UNIQUE INDEX unique_event_id(entity_id)
) ENGINE = INNODB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE utf8mb4_bin COMMENT '快照表';
关键点
  • 快照的作用范围:快照仅用于保存聚合根的状态,因此最好将维护快照的处理逻辑封闭再Repository内,使业务对快照的存在无感知,避免将维护快照的代码泄露到应用层
  • 快照的应该无感知:业务人员不应该关系快照的创建逻辑,即它们既不需要了解何时创建快照,也不需要了解如何创建快照
调整后的rebuild方法
public abstract class AbstractEventSourcingAggregateRoot extends AbstractAggregateRoot {
    /**
     * 是否需要创建快照
     */
     private Boolean takeSnapshot = Boolean.FALSE;
     // 省略其他属性
     
     /**
      * 加入判断需要创建快照的逻辑
      */
      public void rebuild(List<DomainEvent> events) {
          try {
              for (DomainEvent domainEvent : events) {
                  Method method = applyMethodMap.get(domainEvent.getClass());
                  method.invoke(this,domainEvent);
              }
              // 如果有20个以上的领域事件需要回放,则生成快照
              if (events.size() > 20) {
                  this.takeSnapshot = Boolean.TRUE;
              }
              // 清空领域事件,因为重建领域事件不需要被记录并保存在库中
              super.clearDomainEvents();
          } catch(Exception e) {
              throw new RuntimeException("找不到对应的apply方法");
          }
      }
}
Repository

Repository的调整主要是load方法和save方法要引入维护快照相关的逻辑。调整后的Repository参考代码如下:

@Component
public class ProductRepositoryImpl implements ProductRepository {
    ProductFactory productFactory = new ProductFactory();
    @Resource
    private EventJdbcRepository eventJdbcRepository;
    @Resource
    private EntityJdbcRepository entityJdbcRepository;
    @Resource
    private SnapshotJdbcRepository snapshotJdbcRepository;
   	
    @Override
    @Trsansactional
    public void save (Product product) {
        // 1.存事件
        List<DomainEvent> domainEvents = product.getDomainEvents();
        List<Event> entityList = domainEvents.stream().map(de -> {
            Event event = new Event();
            event.setEventTime(de.getEventTime());
            event.setEventType(de.getEventType());
            event.setEventData(JSON.toJsonString(de));
            event.setEntityId(de.getEntityId());
            event.setDeleted(0);
            event.setEventId(de.getEventId());
            return event;
        }).collect(Collectors.toList());
        eventJdbcRepository.saveAll(eventList);
        // 2.存实体
        Entity entity = new Entity();
        entity.setId(product.getId());
        entity.setDeleted(product.getDeleted());
        entity.setEntityId(product.getProductId().getValue());
        entity.setVersion(product.getVersion());
        entityJdbcRepository.save(entity);
        // 3.生成快照
        if (product.getTakeSnapshot()) {
            Snapshot snapshot = snapshotJdbcRepository.queryOneByEntityId(product.getProductId().getValue());
            // 如果数据库中没有对应实体的快照
            if(Objects.isNull(snapshot)){
                snapshot = new Snapshot();
                snapshot.setDeleted(0);
                snapshot.setEntityId(product.getProductId().getValue());
                // 最后一个事件的信息
                DomainEvent lastDomainEvent = domainEvents.get(domainEvents.size()-1);
                snapshot.setEventId(lastDomainEvent.getEventId());
                snapshot.setEventTime(lastDomainEvent.getEventTime());
                // 快照中不保存本次的领域事件
                product.getDomainEvents().clear();
                // 生成聚合根快照
                snapshot.setEntityData(JSON.toJsonString(product));
                snapshotJdbcRepository.save(snapshot);
            }
        }
    }
   	
    @Override
    public Product load (ProductId productId) {
        // 1.加载Entity获得乐观锁
        Entity entity = eventJdbcRepository.queryOneByEntityId(productId.getValue());
        // 2.获得聚合根和待回放事件
        Product product;
        List<Event> events;
        // 查询快照,如果能拿到快照,则直接用快照进行反序列化,如果拿不到快照,则创建空的聚合根
        Snapshot snapshot = snapshotJdbcRepository.queryOneByEntityId(productId.getValue());
        if (Objects.isNull(snapshot)) {
            product = productFactory.newInstance();
            events = eventJdbcRepository.loadHistoryEvents(productId.getValue());
        } else {
            product = JSON.toObject(snapshot.getEntityData(),Product.class);
            events = eventJdbcRepository.loadEventAfter(productId.getValue(),snapshot.getEventTime());
        }
        List<DomainEvent> domainEvents = this.toDomainEvent(events);
        // 3.回放事件重建聚合根
        product.rebuild(domainEvents);
        product.setVersion(entity.getVersion());
        product.setDeleted(entity.getDeleted());
        product.setId(entity.getId());
        // 忽略其他属性返回
        return product;
    }
   	
    private List<DOmainEvent> toDomainEvent (List<Event> events) {
        List<DomainEvent> domainEvents = events.stream().map(e -> {
        String eventType = e.getEventType();
        Class<? extends DomainEvent> typeClass = EventTypeMapping.getEventTypeClass(eventType);
        DomainEvent domainEvent = JSON.toObject(e.getEventData(),typeClass);
            return domainEvent;
        }).collect(Collectors.toList());
        return domainEvents;
    }
}

拉链版

快照版的问题
  • 虽然引入了快照,但大多数命令仍需要通过事件回放来重建聚合根。快照只对快照生成时间点后的领域事件有效,无法优化快照时间点之前的事件
  • 由于聚合根仅保存一系列事件,因此很难直接对领域事件的运营数据进行分析
SCD与拉链表

在数据仓库中,缓慢变化维度(Slowly Changing Dimension,SCD)用于描述数据仓库的维度表中数据的变化方式。简单地说,SCD指的是维度属性中包含相对静态的数据,但其中某些列的数据可能会以不可预测,无固定周期的方式缓慢变化。处理这些数据历史变化的问题被称为缓慢变化维度问题(SCD问题)

拉链表是一种用于处理缓慢变化维度问题的数据结构,它可以有效地处理维度数据的历史变化。在拉链表中,每条记录都有一个开始时间和结束时间,用于描述该记录的存活时间,即该记录的有效期

当某条数据首次被保存时,会在拉链表中插入一行新纪录,该行的开始时间为操作时间,结束时间为一个非常大的值,表示该行记录在操作时间之后一直有效

当该数据发生变化时,不会直接更新已有的行,而是将该行的结束时间改为操作时间,表示该行记录在操作时间之后失效;同时,新增一条更新后的行记录,并将开始时间设置为操作时间,结束时间设置为一个非常大的值。表示在该操作之后,数据长期有效

实现拉链表的方法比较简单,只需要在表中添加两列:开始时间,结束时间。开始时间表示记录的生效时间,结束时间表示记录的失效时间,有时也会添加一个标记当前生效行的列,如row_current,用于表示当前生效的行,拉链表的运行机制如下图所示:

asdasdadasfeqweasdio.png

上图展示了一条数据在拉链表中的变化过程,在ABC三个不同的时间段内,分别有一个版本的数据生效

拉链表在数据库中的存储格式如下图所示:

12345343577678wio.png

介绍

通过对快照表引入拉链表的机制,我们凭借拉链表存储了数据变化过程的特点,使得重建聚合根时不再需要进行事件回放

为了将快照表实现为拉链表,该表新增三列:row_start_time,row_end_time,row_current

创建聚合根的过程

整体创建过程其实并没有太大区别,只是在Repository保存聚合根哪里有一些区别,Repistory的save流程如下图所示:

1561651rawio.png

由于聚合根是刚刚创建的,并没有上一个快照的信息,所以直接创建新快照并保存即可,save方法伪代码如下:

@Transactional
public void save (AggregateRoot root) {
    // 1.存事件
    List<DomainEvent> domainEvents = root.getDomainEvents();
    List<Event> eventList = domainEvents.stream().map(de -> {
        Event event = new Event();
        event.setEventTime(de.getEventTime());
        event.setEventType(de.getEventType());
        event.setEventData(JSON.toJsonString(de));
        event.setEntityId(de.getEntityId());
        event.setDeleted(0);
        event.setEventId(de.getEventId());
        return event;
    }).collect(Collectors.toList());
    eventJdbcRepository.saveAll(eventList);
    // 2.t_entity表创建实体并保存版本号
    Entity entity = new Entity();
    entity.setId(root.getId());
    etntiy.setDeleted(root.getDeleted());
    entity.setEntityId(root.getProductId().getValue());
    entity.setVersion(root.getVersion());
    entityJdbcRepository.save(entity);
    // 3.获取最后一个事件信息
    DomainEvent lastDomainEvent = domainEvents.get(domainEvents.size()-1);
    Date lastDomainEventEventTime = lastDomainEvent.getEventTime();
    Long lastSnapshotId = root.getLastSnapshotId();
    // 4.上一个快照ID不为空,则将其标记为失效
    if (!Objects.isNULL(lastSnapshotId)) {
		snapshotJdbcRepository.markAsExpired(root.getProductId().getValue,lastSnapshotId,lastDomainEventEventTime);
    } else {
        // 没有快照的情况是新创建的,需要给productId赋值自增主键
        root.setId(entity.getId());
        root.setDeleted(0);
    }
    // 5.创建新的快照
    root.clearLastSnapshot();
    Snapshot snapshot = new Snapshot();
    snapshot.setDeleted(0);
    snapshot.setEntityId(root.getEntityId().getValue());
    snapshot.setEventId(lastDomainEvent.getEventId);
    snapshot.setEventTime(lastDomainEventEventTime);
    root.getDomainEvents().clear();
    snapshot.setEntityData(JSON.toJsonString(root));
    snapshot.setRowCurrent(1);
    snapshot.setRowStartTime(lastDomainEventEventTime);
    snapshot.setRowEndTime(getMaxDate());
    snapshotJdbcRepository.save(snapshot);
}
修改聚合根的过程

修改聚合根之前,需要先加载用Repository的load方法,待聚合根对领域事件执行apply方法之后,才能使用Repository的save方法保存,load方法流程如下图所示:

asdasdzxcasdasawio.png

load方法伪伪代码如下:

public AggregateRoot load(EntityId entityId) {
    // 1.加载实体获得乐观锁
    DataEntity dataEntity = entityJdbcRepository.queryOneByEntityId(entityId.getValue());
    // 2.查询快照,如果能拿到快照,则直接用快照进行反序列化
    Snapshot snapshot = snapshotJdbcRepository.queyrOneByEntityId(entityId.getValue());
    if(Objects.isNUll(snapshot)){
        throw new RuntimeException("数据不存在");
    }
    AggregateRoot root = JSON.toObject(snapshot.getEntityData(),AggregateRoot.class);
    root.setVersion(dataEntity.getVersion());
    root.setDeleted(dataEntity.getDeleted());
    root.setId(dataEntity.getId());
    // 3.记录当前快照ID
    root.markLastSnapshot(snapshot.getId());
    return root;
}

6.优缺点

优点

  • 系统重建和状态恢复:通过事件进行重建和状态恢复,无论是由于系统故障数据损坏还是其他原因导致的状态丢失,都可以通过回放事件来重新构建系统状态
  • 方便数据分析:可以通过分析事件序列的方式,我们就能了解应用程序的使用方式,比如哪些功能最受欢迎,用户在那个环节退出了流程,以及它们浏览各个屏幕需要多长时间等;或者将Event直接发送到某个分析系统。我们就不需要为了做数据分析,再在系统里定义各种事件,发送事件等
  • 高性能写:事件数据的保存是一个一直新增的写表操作,没有更新。这在很多情况下都能够提供一个非常好的写的性能
  • 版本控制和数据审计:事件溯源提供了完整的状态变化历史记录,可以用于版本控制和数据审计,每个事件都包含了状态变化的详细信息,可以用于追踪和审计系统中数据的变化和操作历史
  • 分布式系统一致性:在分布式系统中,事件溯源可以用于实现一致性和数据复制,通过将事件日志进行复制和分发,不同的节点可以按照相同的顺序重放事件,从而达到数据的一致性和同步
  • 方便bug的修复:由于我们可以重现历史,所以当发现一个bug以后,我们可以在修复完以后,直接重新聚合我们的业务数据,修复我们的数据。如果使用传统的设计方法,我们就需要通过SQL或者写一段程序,去手动的修改数据库,以使它达到正常的状态。如果这个bug存在的时间较长,牵扯的数据较多,这将会是一个非常麻烦的事情
  • 模型不受到存储的实现影响:由于只存储事件日志,这不涉及到具体存储是什么,模型不会被影响,建模更自由
  • 大数据集成简单:通过事件来集成大数据更加简单

缺点

  • 依赖CQRS:必须实现CQRS
  • 编码复杂度:重建等部分能力需要依靠编码实现
  • 必须设计可回溯的模型:聚合必须能够这些事件回溯回来