learn.lianglianglee.com/专栏/领域驱动设计实践(完)/082 事件溯源模式.md.html
2022-05-11 19:04:14 +08:00

1061 lines
55 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>082 事件溯源模式.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/领域驱动设计实践(完)/001 「战略篇」访谈 DDD 和微服务是什么关系?.md.html">001 「战略篇」访谈 DDD 和微服务是什么关系?.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/002 「战略篇」开篇词:领域驱动设计,重焕青春的设计经典.md.html">002 「战略篇」开篇词:领域驱动设计,重焕青春的设计经典.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/003 领域驱动设计概览.md.html">003 领域驱动设计概览.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/004 深入分析软件的复杂度.md.html">004 深入分析软件的复杂度.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/005 控制软件复杂度的原则.md.html">005 控制软件复杂度的原则.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/006 领域驱动设计对软件复杂度的应对(上).md.html">006 领域驱动设计对软件复杂度的应对(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/007 领域驱动设计对软件复杂度的应对(下).md.html">007 领域驱动设计对软件复杂度的应对(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/008 软件开发团队的沟通与协作.md.html">008 软件开发团队的沟通与协作.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/009 运用领域场景分析提炼领域知识(上).md.html">009 运用领域场景分析提炼领域知识(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/010 运用领域场景分析提炼领域知识(下).md.html">010 运用领域场景分析提炼领域知识(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/011 建立统一语言.md.html">011 建立统一语言.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/012 理解限界上下文.md.html">012 理解限界上下文.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/013 限界上下文的控制力(上).md.html">013 限界上下文的控制力(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/014 限界上下文的控制力(下).md.html">014 限界上下文的控制力(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/015 识别限界上下文(上).md.html">015 识别限界上下文(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/016 识别限界上下文(下).md.html">016 识别限界上下文(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/017 理解上下文映射.md.html">017 理解上下文映射.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/018 上下文映射的团队协作模式.md.html">018 上下文映射的团队协作模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/019 上下文映射的通信集成模式.md.html">019 上下文映射的通信集成模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/020 辨别限界上下文的协作关系(上).md.html">020 辨别限界上下文的协作关系(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/021 辨别限界上下文的协作关系(下).md.html">021 辨别限界上下文的协作关系(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/022 认识分层架构.md.html">022 认识分层架构.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/023 分层架构的演化.md.html">023 分层架构的演化.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/024 领域驱动架构的演进.md.html">024 领域驱动架构的演进.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/025 案例 层次的职责与协作关系(图文篇).md.html">025 案例 层次的职责与协作关系(图文篇).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/026 限界上下文与架构.md.html">026 限界上下文与架构.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/027 限界上下文对架构的影响.md.html">027 限界上下文对架构的影响.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/028 领域驱动设计的代码模型.md.html">028 领域驱动设计的代码模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/029 代码模型的架构决策.md.html">029 代码模型的架构决策.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/030 实践 先启阶段的需求分析.md.html">030 实践 先启阶段的需求分析.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/031 实践 先启阶段的领域场景分析(上).md.html">031 实践 先启阶段的领域场景分析(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/032 实践 先启阶段的领域场景分析(下).md.html">032 实践 先启阶段的领域场景分析(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/033 实践 识别限界上下文.md.html">033 实践 识别限界上下文.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/034 实践 确定限界上下文的协作关系.md.html">034 实践 确定限界上下文的协作关系.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/035 实践 EAS 的整体架构.md.html">035 实践 EAS 的整体架构.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/036 「战术篇」访谈DDD 能帮开发团队提高设计水平吗?.md.html">036 「战术篇」访谈DDD 能帮开发团队提高设计水平吗?.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/037 「战术篇」开篇词:领域驱动设计的不确定性.md.html">037 「战术篇」开篇词:领域驱动设计的不确定性.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/038 什么是模型.md.html">038 什么是模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/039 数据分析模型.md.html">039 数据分析模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/040 数据设计模型.md.html">040 数据设计模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/041 数据模型与对象模型.md.html">041 数据模型与对象模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/042 数据实现模型.md.html">042 数据实现模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/043 案例 培训管理系统.md.html">043 案例 培训管理系统.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/044 服务资源模型.md.html">044 服务资源模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/045 服务行为模型.md.html">045 服务行为模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/046 服务设计模型.md.html">046 服务设计模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/047 领域模型驱动设计.md.html">047 领域模型驱动设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/048 领域实现模型.md.html">048 领域实现模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/049 理解领域模型.md.html">049 理解领域模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/050 领域模型与结构范式.md.html">050 领域模型与结构范式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/051 领域模型与对象范式(上).md.html">051 领域模型与对象范式(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/052 领域模型与对象范式(中).md.html">052 领域模型与对象范式(中).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/053 领域模型与对象范式(下).md.html">053 领域模型与对象范式(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/054 领域模型与函数范式.md.html">054 领域模型与函数范式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/055 领域驱动分层架构与对象模型.md.html">055 领域驱动分层架构与对象模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/056 统一语言与领域分析模型.md.html">056 统一语言与领域分析模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/057 精炼领域分析模型.md.html">057 精炼领域分析模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/058 彩色 UML 与彩色建模.md.html">058 彩色 UML 与彩色建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/059 四色建模法.md.html">059 四色建模法.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/060 案例 订单核心流程的四色建模.md.html">060 案例 订单核心流程的四色建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/061 事件风暴与业务全景探索.md.html">061 事件风暴与业务全景探索.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/062 事件风暴与领域分析建模.md.html">062 事件风暴与领域分析建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/063 案例 订单核心流程的事件风暴.md.html">063 案例 订单核心流程的事件风暴.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/064 表达领域设计模型.md.html">064 表达领域设计模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/065 实体.md.html">065 实体.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/066 值对象.md.html">066 值对象.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/067 对象图与聚合.md.html">067 对象图与聚合.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/068 聚合设计原则.md.html">068 聚合设计原则.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/069 聚合之间的关系.md.html">069 聚合之间的关系.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/070 聚合的设计过程.md.html">070 聚合的设计过程.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/071 案例 培训领域模型的聚合设计.md.html">071 案例 培训领域模型的聚合设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/072 领域模型对象的生命周期-工厂.md.html">072 领域模型对象的生命周期-工厂.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/073 领域模型对象的生命周期-资源库.md.html">073 领域模型对象的生命周期-资源库.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/074 领域服务.md.html">074 领域服务.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/075 案例 领域设计模型的价值.md.html">075 案例 领域设计模型的价值.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/076 应用服务.md.html">076 应用服务.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/077 场景的设计驱动力.md.html">077 场景的设计驱动力.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/078 案例 薪资管理系统的场景驱动设计.md.html">078 案例 薪资管理系统的场景驱动设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/079 场景驱动设计与 DCI 模式.md.html">079 场景驱动设计与 DCI 模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/080 领域事件.md.html">080 领域事件.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/081 发布者—订阅者模式.md.html">081 发布者—订阅者模式.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/领域驱动设计实践(完)/082 事件溯源模式.md.html">082 事件溯源模式.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/083 测试优先的领域实现建模.md.html">083 测试优先的领域实现建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/084 深入理解简单设计.md.html">084 深入理解简单设计.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/085 案例 薪资管理系统的测试驱动开发(上).md.html">085 案例 薪资管理系统的测试驱动开发(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/086 案例 薪资管理系统的测试驱动开发(下).md.html">086 案例 薪资管理系统的测试驱动开发(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/087 对象关系映射(上).md.html">087 对象关系映射(上).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/088 对象关系映射(下).md.html">088 对象关系映射(下).md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/089 领域模型与数据模型.md.html">089 领域模型与数据模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/090 领域驱动设计对持久化的影响.md.html">090 领域驱动设计对持久化的影响.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/091 领域驱动设计体系.md.html">091 领域驱动设计体系.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/092 子领域与限界上下文.md.html">092 子领域与限界上下文.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/093 限界上下文的边界与协作.md.html">093 限界上下文的边界与协作.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/094 限界上下文之间的分布式通信.md.html">094 限界上下文之间的分布式通信.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/095 命令查询职责分离.md.html">095 命令查询职责分离.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/096 分布式柔性事务.md.html">096 分布式柔性事务.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/097 设计概念的统一语言.md.html">097 设计概念的统一语言.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/098 模型对象.md.html">098 模型对象.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/099 领域驱动设计参考过程模型.md.html">099 领域驱动设计参考过程模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/100 领域驱动设计的精髓.md.html">100 领域驱动设计的精髓.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/101 实践 员工上下文的领域建模.md.html">101 实践 员工上下文的领域建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/102 实践 考勤上下文的领域建模.md.html">102 实践 考勤上下文的领域建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/103 实践 项目上下文的领域建模.md.html">103 实践 项目上下文的领域建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/104 实践 培训上下文的业务需求.md.html">104 实践 培训上下文的业务需求.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/105 实践 培训上下文的领域分析建模.md.html">105 实践 培训上下文的领域分析建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/106 实践 培训上下文的领域设计建模.md.html">106 实践 培训上下文的领域设计建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/107 实践 培训上下文的领域实现建模.md.html">107 实践 培训上下文的领域实现建模.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/108 实践 EAS 系统的代码模型.md.html">108 实践 EAS 系统的代码模型.md.html</a>
</li>
<li>
<a href="/专栏/领域驱动设计实践(完)/109 后记:如何学习领域驱动设计.md.html">109 后记:如何学习领域驱动设计.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>082 事件溯源模式</h1>
<h3>事件溯源模式</h3>
<p>事件溯源Event Sourcing模式是针对事件范式提供的设计模式通过事件风暴识别到的领域事件与聚合将成为领域设计模型的核心要素。事件溯源模式与传统领域驱动设计模式的最大区别在于<strong>对聚合生命周期的管理</strong>。资源库在管理聚合生命周期时会直接针对聚合内的实体与值对象执行持久化操作而事件溯源则将聚合以一系列事件的方式进行持久化。因为领域事件记录的就是聚合状态的变化如果能够将每次状态变化产生的领域事件记录下来就相当于记录了聚合生命周期每一步“成长的脚印”。此时持久化的事件就成为了一个自由的“时空穿梭机”随时可以根据需求通过重放Replaying回到任意时刻的聚合对象。</p>
<p>《微服务架构设计模式》总结了事件溯源的优点和缺点:</p>
<blockquote>
<p>事件溯源有几个重要的好处。例如,它保留了聚合的历史记录,这对于实现审计和监管的功能非常有帮助。它可靠地发布领域事件,这在微服务架构中特别有用。事件溯源也有弊端。它有一定的学习曲线,因为这是一种完全不同的业务逻辑开发方式。此外,查询事件存储库通常很困难,这需要你使用 CQRS 模式。</p>
</blockquote>
<p>事件溯源模式的首要原则是“事件永远是不变的”,因此对事件的持久化就变得非常简单,无论发生了什么样的事件,在持久化时都是追加操作。这就好似在 GitHub 上提交代码,每次提交都会在提交日志上增加一条记录。因此,我们在理解事件溯源模式时,可以把握两个关键原则:</p>
<ul>
<li>聚合的每次状态变化,都是一个事件的发生</li>
<li>事件是不变的,以追加方式记录事件,形成事件日志</li>
</ul>
<p>由于事件溯源模式运用在限界上下文的边界之内,它所操作的事件属于领域设计模型的一部分。若要准确说明,应称呼其为“领域事件”,以区分于发布者—订阅者模式操作的“应用事件”。</p>
<h4>领域事件的定义</h4>
<p>既然事件溯源以追加形式持久化领域事件,就可以不受聚合持久化实现机制的限制,例如对象与关系之间的阻抗不匹配,复杂的数据一致性问题,聚合历史记录存储等。**事件溯源持久化的不是聚合而是由聚合状态变化产生的领域事件这种持久化方式称之为事件存储Event Store。**事件存储会建立一张事件表,记录下事件的 ID、类型、关联聚合和事件的内容以及产生事件时的时间戳。其中事件内容将作为重建聚合的数据来源。由于事件表需要支持各种类型的领域事件意味着事件内容需要存储不同结构的数据值因此通常选择 JSON 格式的字符串。例如 IssueCreated 事件:</p>
<pre><code class="language-json">{
&quot;eventId&quot;: &quot;111&quot;,
&quot;eventType&quot;: &quot;IssueCreated&quot;,
&quot;aggregateType&quot;: &quot;Issue&quot;,
&quot;aggregateId&quot;: &quot;100&quot;,
&quot;eventPayload&quot;: {
&quot;issueId&quot;: &quot;100&quot;,
&quot;title&quot;: &quot;Global Consent Management&quot;,
&quot;description&quot;: &quot;Manage global consent for customer&quot;,
&quot;label&quot;: &quot;STORY&quot;,
&quot;iterationId&quot;: &quot;111&quot;,
&quot;points&quot;: 5
},
&quot;createdTimestamp&quot;: &quot;2019-08-30 12:10:11 756&quot;
}
</code></pre>
<p>只要保证 eventPayload 的内容为可解析的标准格式IssueCreated 事件也可存储在关系数据库中,通过 eventType、aggregateType 和 aggregateId 可以确定事件以及该事件对应的聚合,重建聚合的数据则来自 eventPayload 的值。显然,我们需要结合具体的领域场景来定义领域事件。领域事件包含的值必须是订阅方需要了解的信息,例如 IssueCreated 事件会创建一张任务卡,如果事件没有提供该任务的 title、description 等值,就无法通过这些值重建 Issue 聚合对象。显然,事件溯源操作的领域事件主要是为了追溯状态变更,并可以根据存储的事件来重建聚合。这与发布者—订阅者模式引入事件的目的大相径庭。</p>
<h4>聚合的创建与更新</h4>
<p>要实现事件溯源,需要执行的操作(或职责)包括:</p>
<ul>
<li>处理命令</li>
<li>发布事件</li>
<li>存储事件</li>
<li>查询事件</li>
<li>创建以及重建聚合</li>
</ul>
<p>虽然事件溯源采用了和传统领域驱动设计不同的建模范式和设计模式,但仍然需要遵守领域驱动设计的根本原则:<strong>保证领域模型的纯粹性</strong>。如果结合事件风暴来理解事件溯源,相与协作的对象包括:领域事件、决策命令和聚合,同时,决策命令包含的信息则为读模型。由于事件溯源采用了事件存储模式,因此它与发布者—订阅者模式不同,实际上并不会真正发布事件到消息队列或者事件总线。事件溯源的所谓“发布事件”实则为创建并存储事件。</p>
<p>如果我们将决策命令、读模型、领域事件和聚合皆视为领域设计模型的一部分,为了保证领域模型的纯粹性,就必须将存储事件和查询事件的职责交给事件存储。与场景驱动设计相似,领域服务承担了协作这些领域模型对象实现领域场景的职责,并由它与抽象的 EventStore 协作。为了让领域服务知道该如何存储事件,聚合在处理了决策命令之后,需要将生成的领域事件返回给领域服务。聚合仅负责创建领域事件,领域服务通过调用 EventStore 存储领域事件。</p>
<p>初次创建聚合实例时,聚合还未产生任何一次状态的变更,不需要重建聚合。因此,聚合的创建操作与更新操作的流程并不相同,实现事件溯源时需区分对待。创建聚合需要执行如下活动:</p>
<ul>
<li>创建一个新的聚合实例</li>
<li>聚合实例接收命令生成领域事件</li>
<li>运用生成的领域事件改变聚合状态</li>
<li>存储生成的领域事件</li>
</ul>
<p>例如,要创建一张新的问题卡片。在领域层,首先由领域服务接收决策命令,由其统筹安排:</p>
<pre><code class="language-java">public class CreatingIssueService {
private EventStore eventStore;
public void execute(CreateIssue command) {
Issue issue = Issue.newInstance();
List&lt;DomainEvent&gt; events = issue.process(command);
eventStore.save(events);
}
}
</code></pre>
<p>领域服务首先会调用聚合的工厂方法创建一个新的聚合,然后调用该聚合实例的 process(command) 方法处理创建 Issue 的决策命令。聚合的 process(command) 方法首先会验证命令有效性,然后根据命令执行领域逻辑,再生成新的领域事件。在返回领域事件之前,会调用 apply(event) 方法更改聚合的状态:</p>
<pre><code class="language-java">public class Issue extends AggregateRoot&lt;Issue&gt; {
public List&lt;DomainEvent&gt; process(CreateIssue command) {
try {
command.validate();
IssueCreated event = new IssueCreated(command.issueDetail());
apply(event);
return Collections.singletonList(event);
} catch (InvalidCommandException ex) {
logger.warn(ex.getMessage());
return Collections.emptyList();
}
}
public void apply(IssueCreated event) {
this.state = IssueState.CREATED;
}
}
</code></pre>
<p>process(command) 方法并不负责修改聚合的状态,它将这一职责交给了单独定义的 apply(event) 方法,然后它会调用该方法。之所以要单独定义 apply(event) 方法,是为了聚合的重建。在重建聚合时,通过遍历该聚合发生的所有领域事件,再调用这一单独定义的 apply(event) 方法,完成对聚合实例的状态变更。如此的设计,就能够重用运用事件逻辑的逻辑,同时保证聚合状态变更的一致性,真实地体现了状态变更的历史。</p>
<p>IssueCreated 事件是不可变的,故而大体可以视 process(command) 方法是一个没有副作用的<strong>纯函数pure function</strong>。此为状态变迁的本质特征,即聚合从一个状态(事件)变迁到一个新的状态(事件),而非真正修改聚合本身的状态值。这也正是我认为事件范式与函数范式更为契合的原因所在。</p>
<p>聚合处理了命令并返回领域事件后,领域服务会通过它依赖的 EventStore 存储这些领域事件。事件的存储既可以认为是对外部资源的依赖,也可以认为是一种副作用。显然,将存储事件的职责转移给领域服务,既符合面向对象尽量将依赖向外推的设计原则,也符合函数编程将副作用往外推的设计原则。遵循这一原则设计的聚合,能很好地支持单元测试的编写。</p>
<p>更新聚合需要执行如下活动:</p>
<ul>
<li>从事件存储加载聚合对应的事件</li>
<li>创建一个新的聚合实例</li>
<li>遍历加载的事件,完成对聚合的重建</li>
<li>聚合实例接收命令生成领域事件</li>
<li>运用生成的领域事件改变聚合状态</li>
<li>存储生成的领域事件</li>
</ul>
<p>例如,要将刚才创建好的 Issue 分配给团队成员,就可以发送命令 AssignIssue 给领域服务:</p>
<pre><code class="language-java">public class AssigningIssueService {
private EventStore eventStore;
public void execute(AssignIssue command) {
Issue issue = Issue.newInstance();
List&lt;DomainEvent&gt; events = eventStore.findBy(command.aggregateId());
issue.applyEvents(events);
List&lt;DomainEvent&gt; events = issue.process(command); // 注意process方法内部会apply新的领域事件
eventStore.save(events);
}
}
</code></pre>
<p>领域服务通过 EventStore 与命令传递过来的聚合 ID 获得该聚合的历史事件,然后针对新生的聚合进行生命状态的重建,这就相当于重新执行了一遍历史上曾经执行过的领域行为,使得当前聚合恢复到接受本次命令之前的正确状态,然后处理当前决策命令,生成事件并存储。</p>
<h4>快照</h4>
<p>聚合的生命周期各有长短。例如 Issue 的生命周期就相对简短,一旦该问题被标记为完成,几乎就可以认为具有该身份标识的 Issue 已经寿终正寝。除了极少数的 Issue 需要被 ReOpen 之外,该聚合不会再发布新的领域事件了。有的聚合则不同,或许聚合变化的频率不高,但它的生命周期相当漫长,例如账户 Account就可能随着时间的推移积累大量的领域事件。当一个聚合的历史领域事件变得越来越多时如前所述的加载事件以及重建聚合的执行效率就会越来越低。</p>
<p>在事件溯源中,通常通过“快照”形式来解决此问题。</p>
<p>使用快照时,通常会定期将聚合以 JSON 格式持久化到聚合快照表Snapshots中。注意快照表持久化的是当前时间戳的聚合数据而非事件数据。故而快照表记录了聚合类型、聚合 ID 和聚合的内容,当然也包括持久化快照时的时间戳。创建聚合时,可直接根据聚合 ID 从快照表中获取聚合的内容,然后利用反序列化直接创建聚合实例,如此即可让聚合实例直接从某个时间戳“带着记忆重生”,省去了从初生到快照时间戳的重建过程。由于快照内容并不一定是最新的聚合值,因而还需要运用快照时间戳之后的领域事件,才能快速而正确地恢复到当前状态:</p>
<pre><code class="language-java">public class AssigningIssueService {
private EventStore eventStore;
private SnapshotRepository snapshotRepo;
public void execute(AssignIssue command) {
// 利用快照重建聚合
Snapshot snapshot = snapshotRepo.snapshotOf(command.aggregateId());
Issue issue = snapshot.rebuildTo(Issue.getClass());
// 获得快照时间戳之后的领域事件
List&lt;DomainEvent&gt; events = eventStore.findBy(command.aggregateId(), snapshot.createdTimestamp());
issue.applyEvents(events);
List&lt;DomainEvent&gt; events = issue.process(command);
eventStore.save(events);
}
}
</code></pre>
<h4>面向聚合的事件溯源</h4>
<p>事件溯源其实有两个不同的视角。一个视角面向事件,另一个视角可以面向聚合。前述代码中,无论是获取事件、存储事件或者运用事件,其目的还是为了操作聚合。例如,获取事件是为了实例化或者重建一个聚合实例;存储事件虽然是针对事件的持久化,但最终目的却是为了将来对聚合的重建,因此也可同等视为聚合的持久化;至于运用事件,就是为了正确地变更聚合的状态,相当于更新聚合。因此,在领域层,我们可以通过聚合资源库来封装事件溯源与事件存储的底层机制。如此,既可以简化领域服务的逻辑,又可以帮助代码的阅读者更加直观地理解领域逻辑。仍以 Issue 为例,可定义 IssueRepository 类:</p>
<pre><code class="language-java">public class IssueRepository {
private EventStore eventStore;
private SnapshotRepository snapshotRepo;
// 查询聚合
public Issue issueOf(IssueId issueId) {
Snapshot snapshot = snapshotRepo.snapshotOf(issueId);
Issue issue = snapshot.rebuildTo(Issue.getClass());
List&lt;DomainEvent&gt; events = eventStore.findBy(command.aggregateId(), snapshot.createdTimestamp());
issue.applyEvents(events);
return issue;
}
// 新建聚合
public void add(CreateIssue command) {
Issue issue = Issue.newInstance();
processCommandThenSave(issue, command);
}
// 更新聚合
public void update(AssignIssue command) {
Issue issue = issueOf(command.issueId());
processCommandThenSave(issue, command);
}
private void processCommandThenSave(Issue issue, DecisionCommand command) {
List&lt;DomainEvent&gt; events = issue.process(command);
eventStore.save(events);
}
}
</code></pre>
<p>定义了这样一个面向聚合的资源库后,事件溯源的细节就被隔离在资源库内,领域服务操作聚合就像对象范式的实现一样,不同之处在于领域服务接收的仍然是决策命令。这时的领域服务就从承担领域行为的职责蜕变为对决策命令的分发,由于它封装的领域逻辑非常简单,因此可以为一个聚合定义一个领域服务:</p>
<pre><code class="language-java">public class IssueService {
private IssueRepository issueRepo;
public void execute(CreateIssue command) {
issueRepo.add(command);
}
public void execute(AssignIssue command) {
issueRepo.update(command);
}
}
</code></pre>
<p>领域服务的职责变成了对命令的分发。在事件范式中,我们甚至可以将领域服务命名为 IssueCommandDispatcher使其命名更加名副其实。</p>
<h4>聚合查询的改进</h4>
<p>通过 IssueRepository 的实现,可以看出事件溯源在聚合查询功能上存在的限制:它仅仅支持基于主键的查询,这是由事件存储机制决定的。要提高对聚合的查询能力,唯一有效的解决方案就是在存储事件的同时存储聚合。</p>
<p>单独存储的聚合与领域事件无关,它是根据领域模型对象的数据结构进行设计和管理的,可以满足复杂的聚合查询请求。显然,存储的事件由于真实地反应了聚合状态的变迁,故而用于满足客户端的命令请求;存储的聚合则依照对象范式的聚合对象进行设计,通过 ORM 框架就能满足对聚合的高级查询请求。事件与聚合的分离,意味着命令与查询的分离,实际上就是 CQRSCommand Query Responsibility Segregation命令查询职责分离模式的设计初衷。</p>
<p>CQRS 在架构设计上有许多变化,我会在课程的第五部分《融合:战略设计与战术设计》进行深入讲解。这里仅提供事件溯源模式下如何解决查询聚合局限性的方案:在事件溯源的基础上,分别实现事件的存储与聚合的存储,前者用于体现聚合的历史状态,后者用于体现聚合的当前状态。</p>
<p>整个系统架构的领域模型被分为命令与查询两部分。命令请求下的领域模型采用事件溯源模式,聚合负责命令的处理与事件的创建;查询请求下的领域模型采用查询视图模式,直接查询业务数据库,获得它所存储的聚合信息。问题在于:命令端如何做到<strong>及时可靠一致</strong>地将事件的最新状态反映给查询端的业务数据库?</p>
<p>根据设计者对事件存储和聚合存储的态度,存在两种迥然不同的解决方案:本地式和分布式。</p>
<p>在领域驱动战略设计的指导下,一个聚合产生的所有领域事件和聚合应处于同一个限界上下文。因此,可以选择将事件存储与聚合存储放在同一个数据库,如此即可保证事件存储与聚合存储的事务强一致性。存储事件时,同时将更新后的聚合持久化。既然数据库已经存储了聚合的最新状态,就无需通过事件存储来重建聚合,但领域逻辑的处理模式仍然体现为命令—事件的状态迁移形式。至于查询,就与事件无关了,可以直接查询聚合所在的数据库。如此,可修改资源库的实现,如 IssueRepository</p>
<pre><code class="language-java">public class IssueRepository {
private EventStore eventStore;
private AggregateRepository&lt;Issue&gt; repo;
public Issue issueOf(IssueId issueId) {
return repo.findBy(issueId);
}
public List&lt;Issue&gt; allIssues() {
return repo.findAll();
}
public void add(CreateIssue command) {
Issue issue = Issue.newInstance();
processCommandThenSave(issue, command);
}
public void update(AssignIssue command) {
Issue issue = issueOf(command.issueId());
processCommandThenSave(issue, command);
}
private void processCommandThenSave(Issue issue, DecisionCommand command) {
List&lt;DomainEvent&gt; events = issue.process(command);
eventStore.save(events);
repo.save(issue);
}
}
</code></pre>
<p>这一方案的优势在于事件存储和聚合存储都在本地数据库,通过本地事务即可保证数据存储的一致性,且在支持事件追溯与审计的同时,还能避免重建聚合带来的性能影响。与不变的事件不同,聚合会被更新,因此它的持久化要比事件存储更加复杂,既然在本地已经存储了聚合对象,引入事件溯源的价值就没有这么明显了。由于事件与聚合存储在一个强一致的事务范围内,事件的异步非阻塞特性也未曾凸显出来。</p>
<p>如果事件存储和聚合存储不在同一个数据库中,就需要将事件的最新状态反映给存储在业务数据库中的聚合,方法是通过<strong>发布或轮询事件</strong>来搭建事件存储与聚合存储之间沟通的桥梁。此时的事件起到了通知状态变更的作用。</p>
<p>若采用事件发布的机制,由于事件模型与聚合模型之间属于跨进程的分布式通信,因此需要引入消息中间件作为事件的传输通道。这就相当于在事件溯源模式中引入了发布者—订阅者模式。要通知状态变更,可以直接将领域事件视为应用事件进行发布,也可以将领域事件转换为耦合度更低的应用事件。</p>
<p>事件存储端作为发布者,当聚合接收到决策命令请求后生成领域事件,然后将领域事件或转换后的应用事件发布到诸如 Kafka、RabbitMQ 之类的消息中间件。发布事件的同时还需存储领域事件,以支持事件的溯源。聚合存储端作为订阅者,会订阅它关心的事件,借由事件携带的数据创建或更新业务数据库中的聚合。由于事件消息的发布是异步的,处理命令请求和存储聚合数据的功能又分布在不同的进程,就能更快地响应客户端发送来的命令请求,提高整个系统的响应能力。</p>
<p>如果不需要实时发布事件,则可以定时轮询存储到事件表中事件,获取未曾发布的新事件发布到消息中间件。为了避免事件的重复发布,可以在事件表中增加一个 published 列,用于判断该事件消息是否已经发布。一旦成功发布了该事件消息,就需要更新事件表中的 published 标记为 true。</p>
<p>无论是发布还是轮询事件,都需要考虑分布式事务的一致性问题,事务范围要协调的操作包括:</p>
<ul>
<li>存储领域事件(针对发布事件)</li>
<li>发送事件消息</li>
<li>更新聚合状态</li>
<li>更新事件表标记(针对轮询事件)</li>
</ul>
<p>虽然在一个事务范围内要协调的操作较多,但要保证数据的一致性也没有想象的那么棘手。首先,事件的存储与聚合的更新并不要求强一致性,尤其对于命令端而言,选择了这样一种模式,意味着你已经接受了执行命令请求时的异步非实时能力。如果选择实时发布事件,为了避免存储领域事件与发送事件消息之间的不一致性,我们可以考虑在事件存储成功之后,再发送事件消息。由于领域事件是不变的,存储事件皆以追加方式进行,故而无需对数据行加锁来控制并发,这使得领域事件的存储操作相对高效。</p>
<p>许多消息中间件都可以保证消息投递做到“至少一次at least once那么在事件的订阅方只要保证更新聚合状态操作的幂等性就能避免重复消费事件消息变相地做到了“恰好一次exactly once”。更新聚合状态的操作包括创建、更新和删除除了创建操作其余操作本身就是幂等的。</p>
<p>由于创建聚合的事件消息中包含了聚合的 ID因此在创建聚合时只需要判断业务数据库是否已经存在该聚合 ID若已存在则证明该事件消息已被消费过此时应忽略该事件消息避免重复创建。当然我们也可以在事件订阅方引入事件发送历史表。由于该历史表可以和聚合所在的业务数据表放在同一个数据库可保证二者的事务强一致性也能避免事件消息的重复消费。</p>
<p>针对轮询事件,由于消息中间件保证了事件消息的成功投递,就无需等待事件消息发送的结果,立即更新事件表标记。即使更新标记的操作有可能出现错误,只要能保证事件的订阅者遵循了幂等性,避免了事件消息的重复消费,就可以降低一致性要求。即使事件表标记的更新未曾满足一致性,也不会产生负面影响。</p>
<p>要保证数据的最终一致性,剩下的工作就是如何保证聚合状态的成功更新。在确保了事件消息已成功投递之后,对聚合状态更新的操作已经由分布式事务的协调“降低”为对本地数据库的访问操作。许多消息中间件都可以缓存甚至持久化队列中的事件,在设置了合理的保存时间后,倘若事件的订阅者处理失败,还可通过重试机制来提高更新操作的成功率。</p>
<p>即使如此,要保证 100% 的操作都满足了事务的最终一致性,仍然很难。倘若发布的事件不止一个消费者订阅,事务的一致性问题会变得更加复杂。若业务场景对一致性的要求极高,要么就只能采用本地式的方案,要么就考虑引入诸如 TCC 模式、可靠消息传递以及 Saga 模式等分布式事务模式来实现最终一致性。在当今的软件系统架构中,分布式事务是一个永恒且艰难的话题,若需要深入了解分布式事务,建议阅读与此主题相关的技术文档或技术书籍。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/领域驱动设计实践(完)/081 发布者—订阅者模式.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/领域驱动设计实践(完)/083 测试优先的领域实现建模.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997ebb3b823cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>