事件溯源
1.历史
MartinFowler在2005年的博客中提及了“Event Sourcing”这个词语,他将事件描述为一个应用的一系列状态改变,这一系列事件能够捕获用来重建当前状态的一切事实真相。他认为事件是不可变的,事件日志是一种只会不断追(加appendonly)的存储。事件从来不会被删除,这意味着事件可以被重放
2.介绍
事件溯源(Event Sourcing)是一种软件设计模式(数据存储模式),它将系统的状态变化表示为一系列事件(Event),并将这些事件持久化存储(按照发生的顺序进行存储),通过记录和存储事件,通过记录和存储事件,系统可以根据事件重放的方式来重建和恢复系统的状态
对于DDD来说事件溯源是构建业务逻辑和持久化聚合的另一种选择,它将聚合以一系列事件的方式持久化保存。每个事件代表聚合的一次状态变化,应用程序通过重放事件来还原聚合的状态
3.案例理解事件溯源(一)
传统存储方式
使用数据库来存储当前应用程序的当前状态,假设我们要记录库存中的物品数量,所以当我们再收到5件时,我们将总数设置为15
事件溯源方式
事件溯源不会更新当前状态,而是将每个变化作为新事件插入数据库,这样我们就可以利用这些事件来计算当前的状态,或者计算任意给定时间的状态
一旦我们插入了事件,就永远不会删除它们。当我们想要删除任何内容时,我们会为这件事创建一个新的事件,这意味着我们永远不会丢失任何信息
4.案例理解事件溯源(二)
传统存储方式
在传统的实现中,每个命令作用于聚合根上,都会更新聚合根的状态,数据库保存聚合根的最新状态,如图所示:
这种方式在数据库中将每个聚合根实例存储为一行,并且当前保存的是聚合根最新的状态。传统的保存聚合根最新状态的数据库表设计如图所示:
事件溯源方式
事件溯源的执行方法与之相反,事件溯源不保存聚合根的最新状态,保存的是每个命令作用域聚合根之后产生的领域事件。事件溯源只保存领域事件,如图所示:
事件溯源的表结构如图所示:
在事件溯源的实现方案中,数据库保存的是每个领域事件,当有新的命令(Command)需要作用在聚合上时,需要先从数据库中查询出历史事件,并通过回放历史事件进行重建聚合根,再将命令作用在聚合根上进行验证和计算本次命令对应的领域事件,最后将新的领域事件保存起来
5.事件溯源中核心组件
与传统DDD核心组件的区别
聚合根
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方法
在过去,聚合根通过提供业务方法来修改自身的状态。在事件溯源机制下,聚合根将状态的修改看作两个过程:
- process校验参数的合法性,并尝试通过聚合根的状态和Command中的信息计算出某个命令应该得到的结果,则将其封装为领域事件
- 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方法在实现时要注意两点:
- 需要找到具体的领域事件对应的apply方法。由于此时events中的领域事件类型被转成了DomainEvent,因此在重放时无法匹配到对应的方法,需要简单优雅地找到领域事件对应的apply方法
- 需要在完成重建之后,清空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设计执行过程有一些区别
并发修改问题
事件溯源中聚合根是通过回放历史领域事件进行重建的,假设同时有两个命令请求修改聚合根,则有可能执行某个命令的线程还没有完成领域事件的持久化,此时另外的线程又将历史事件读取出来重建聚合根执行业务操作,从而产生并发修改数据的问题,导致业务结果不正确
一般通过引入乐观锁的机制来避免数据并发更新不正确的问题
创建聚合根的过程
- 创建空聚合根:当收到创建聚合根的CreateCommand命令时,由于此时聚合根并不存在,所以不涉及聚合根重建的过程,只需要通过DomainFactory创建一个空的聚合根即可
- 执行聚合根的process方法:将CreateCommand命令应用于聚合根的process方法来校验Command数据的合法性,校验通过之后将命令携带的信息与聚合根原本的属性进行运算,并生成命令对应的领域事件
- 注意:process方法不管是接收创建还是修改命令,都只是对聚合根进行状态的读取,不会修改聚合根内部的值。process方法获取聚合根的状态,并于命令中的值进行计算,将计算结果封装成领域事件,在这个过程中不会修改聚合根的状态
- 执行聚合根的apply方法:得到命令对应的领域事件后,将领域事件应用于聚合根的apply方法会获取领域事件内的信息并更新聚合根内部的状态
- 仓储进行持久化聚合根:以上过程执行完之后,就得到了最新状态的聚合根,此时使用仓储的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);
}
}
修改聚合根的过程
@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方法加载聚合根。具体流程如下图所示:
保存聚合根:通过Repository的load方法加载聚合根,具体流程如下图所示:
快照表
// 省略了通用字段,比如创建时间,创建人啥的
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,用于表示当前生效的行,拉链表的运行机制如下图所示:
上图展示了一条数据在拉链表中的变化过程,在ABC三个不同的时间段内,分别有一个版本的数据生效
拉链表在数据库中的存储格式如下图所示:
介绍
通过对快照表引入拉链表的机制,我们凭借拉链表存储了数据变化过程的特点,使得重建聚合根时不再需要进行事件回放
为了将快照表实现为拉链表,该表新增三列:row_start_time,row_end_time,row_current
创建聚合根的过程
整体创建过程其实并没有太大区别,只是在Repository保存聚合根哪里有一些区别,Repistory的save流程如下图所示:
由于聚合根是刚刚创建的,并没有上一个快照的信息,所以直接创建新快照并保存即可,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方法流程如下图所示:
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
- 编码复杂度:重建等部分能力需要依靠编码实现
- 必须设计可回溯的模型:聚合必须能够这些事件回溯回来