事件溯源概述






4.27/5 (4投票s)
面向关系数据库专家开发人员的事件溯源简介
引言
事件溯源是一种在计算机系统中以只向前、一次写入的方式存储数据的方法,这样,任何影响事物状态的事件都会被存储下来。然后,可以通过重放状态变更的历史来重建该事物的当前状态。
说明其工作原理的一个好方法是以下棋为例。如果你从一个已知状态开始——例如,棋局设置好,棋子处于开局位置——并依次记录每一步棋,就可以存储并重放这些棋步,以重建当前棋局的状态。
从初始状态开始,我们记录游戏中发生的事件。在上面的插图中,事件是白兵移动
、黑兵移动
、白兵移动
、黑兵被吃
。
我们按照这些事件发生的顺序将其记录为序列号,而聚合是这些事件发生的游戏唯一标识符。
为事件提供的任何附加信息都作为附加到每个事件的有效载荷进行保存。
在任何给定时间点,您都可以通过按顺序重放事件来重建游戏的当前状态。因此,棋盘的这个状态是事件流的投影。但是,您也可以使用相同的事件流来生成其他投影——例如,如果您想知道任何给定棋子在游戏任何阶段移动了多远,您可以创建另一个跟踪该棋子移动事件的投影。
如果您慷慨大方,您可能会允许一个经验不足的玩家“悔棋”。在事件流中,任何事件都不能被删除,因此这通过添加一个“撤销上一步”类型的事件来完成。
正如您从这个例子中看到的,事件流包含了游戏中发生的所有事件。
术语表
- 聚合 (Aggregate) - 任何可以发生事件的唯一标识事物。
- 投影 (Projection) - 通过事件回放以确定实体的状态。这可以是当前状态,也可以是特定时间点的状态。
事件溯源理智守护者
遵循以下几条指导原则可以大大简化事件溯源,并维护开发人员的理智。
- 每个聚合都有自己的事件流。这些事件流在物理上可能相同也可能不同,但在逻辑上,任何事件流都只能包含与一个聚合相关的事件。
- 事件需要记录事件类型(或记录类型)以及写入该事件的系统版本。这允许软件识别任何给定事件是否可以预期存在某个属性。
- 表示事件的类应是最终的(密封的/不可继承的),以最大程度地降低系统更改超出其预期目标的风险。
- 每个事件的有效载荷(或信息内容)是可变的。在实践中,这适合使用 NoSQL 系统或链接的 JSON 或其他对象存储方法。
- 事件流是只向前、一次写入的数据存储。
- 不允许“调整”式事件。如果写入了不正确的事件,则必须写入一对“取消-重订”事件以实现更改。
- 事件应幂等。
- 与任何给定事件相关的所有数据都应存储——无论当前是否存在对该数据的业务需求。理想情况下,不应丢弃任何数据。
- 所有读取事件都应针对投影操作。
- 投影可以缓存到给定事件,这可以通过从该缓存状态开始,并仅应用自缓存以来发生的事件来加速当前状态的生成。
- 可以从同一个事件流生成多个投影。
投影
投影可以想象成一个过滤器,它决定事件是否与投影相关,以及一个将事件应用到当前状态的过程。通过这个投影运行事件流将为您提供对象当前状态的视图。
以下指导原则在编写投影时很有用:
- 投影需要知道哪些事件类型(记录类型)会影响其状态,并且需要忽略所有其他事件类型。
- 投影需要一个处理缺失属性的规则——要么分配一个默认值,要么忽略它们。
- 投影应该知道它所针对的聚合以及它已处理的最新事件的序列号。这可用于创建状态的缓存快照。
投影也可以运行到事件流中的给定点,这允许您在给定时间点获取聚合的状态。这种历史查询功能在某些应用领域(例如,金融系统)中非常强大。
查询
对使用事件溯源提出的反对意见之一是,它在即席查询时速度慢,因为每个事件都必须被播放到每个投影中才能生成供查询的状态。
这在某种程度上是真实的,但也可以很容易地通过简单地将缓存的投影存储在关系数据库系统中来缓解。然后可以像查询关系数据库一样查询这些缓存状态。
您还可以通过将投影代码发送到保存事件流数据的地方,而不是将整个事件流带过来运行投影,从而减少运行投影的开销。这在概念上类似于一个MapReduce系统。
优点
事件溯源比关系数据库派生系统有许多优势,其中最主要的是您拥有一个内置的、有保证的审计追踪。所有记录系统都需要审计追踪,将这样的系统附加到关系数据库之上是次优的——它往往会使数据库复杂化并降低该系统上的操作速度。
此外,大量不同类型的业务已经以事件源的等效方式运作——例如,任何经营分类账的业务都将进行映射。
在技术方面,关系数据库系统中涉及一致性检查的额外成本大大降低了其速度。当系统需要扩展以应对业务快速增长时,这可能是一个大问题。事件溯源系统没有这些相同的一致性检查问题,因为记录不能被删除或修改。
缺点
用于处理事件源的工具滞后于关系数据库系统可用的工具。此外,掌握事件溯源知识的熟练技术人员的稀缺性也阻碍了其在企业中的采用。
代码示例 (VB.NET)
以下(节略的)代码示例展示了如何实现此功能。
聚合
为了使聚合类型安全,我添加了一个接口来创建特定的聚合标识符。
Public Interface IAggregateIdentity
''' <summary>
''' Get the unique name that identifies this aggregate item
''' </summary>
Function GetAggregateIdentity() As String
End Interface
以及一个实现此身份接口的具体类。
''' <summary>
''' Class to identify events pertaining to a known user
''' </summary>
''' <remarks>
''' The user is uniquely identified by a user name (or handle) which must
''' be unique system-wide.
''' </remarks>
Public Class UserAggregateIdentity
Implements IAggregateIdentity
ReadOnly m_userName As String
Public Function GetAggregateIdentity() As String _
Implements IAggregateIdentity.GetAggregateIdentity
Return m_userName
End Function
Public Sub New(ByVal userName As String)
m_userName = userName
End Sub
End Class
这用于限制任何给定事件,使其只能应用于一种聚合根类型。
事件
出于类似的原因,所有事件都派生自一个绑定到事件所属特定聚合类型的接口。
''' <summary>
''' Bare bones interface to identify an event
''' </summary>
''' <remarks>
''' The payload (data) are provided by the concrete implementation of the event
''' </remarks>
Public Interface IEvent(Of In TAggregate As IAggregateIdentity)
End Interface
反过来,每个不同的事件类型都实现此接口。
''' <summary>
''' A new user was added to the system
''' </summary>
Public NotInheritable Class CreatedEvent
Inherits EventBase
Implements IEvent(Of AggregateIdentifiers.UserAggregateIdentity)
''' <summary>
''' The unique identifier of the user that was created
''' </summary>
Public Property UserIdentifier As String
''' <summary>
''' The email address the user was initially created with
''' </summary>
Public Property EmailAddress As String
''' <summary>
''' How was this user created - self service, administrator or bulk import
''' </summary>
Public Property Source As String
Public Overrides Function ToString() As String
Return "User was created - " & UserIdentifier
End Function
End Class
当然,并非所有事件类型都需要额外的有效载荷。表示用户被禁用的事件可以只是:
''' <summary>
''' A user account was disabled
''' </summary>
Public NotInheritable Class AccountDisabledEvent
Inherits EventBase
Implements IEvent(Of AggregateIdentifiers.UserAggregateIdentity)
Public Overrides Function ToString() As String
Return "User account was disabled - " & Reason
End Function
End Class
这要求事件类是NotInheritable
(不可继承的),这样就不会对发生了什么事件产生歧义。
投影
一个投影可以运行在许多不同类型的事件上,但这些事件都必须与同一个聚合相关,所以我们将这个限制构建到一个基类中。
''' <summary>
''' Base class for all projections
''' </summary>
''' <remarks>
''' A projection can only operate on a single aggregate by design
''' </remarks>
Public MustInherit Class ProjectionBase(Of TAggregate As Event.IAggregateIdentity)
Implements IEventConsumer(Of TAggregate, IEvent(Of TAggregate))
''' <summary>
''' The aggregate identity that this projection is operating against
''' </summary>
MustOverride ReadOnly Property Identity As TAggregate
''' <summary>
''' Process an event of the given aggregate to get the current point-in-time state
''' </summary>
''' <param name="eventToConsume">
''' The event with the payload that might impact the state of the projection
''' </param>
MustOverride Sub ConsumeEvent(eventToConsume As IEvent(Of TAggregate)) _
Implements IEventConsumer(Of TAggregate, IEvent(Of TAggregate)).ConsumeEvent
End Class
一个具体的投影可能看起来像这样:
''' &ly;summary>
''' Projection over the User event stream to
''' </summary>
''' <remarks>&ly;/remarks>
Public Class UserSummaryProjection
Inherits ProjectionBase(Of AggregateIdentifiers.UserAggregateIdentity)
ReadOnly m_identity As AggregateIdentifiers.UserAggregateIdentity
''' <summary>
''' The aggregate identifier of the client to which this project applies
''' </summary>
Public Overrides ReadOnly Property Identity _
As AggregateIdentifiers.UserAggregateIdentity
Get
Return m_identity
End Get
End Property
Public Overrides Sub ConsumeEvent(eventToConsume _
As IEvent(Of AggregateIdentifiers.UserAggregateIdentity))
If (TypeOf (eventToConsume) Is Events.User.CreatedEvent) Then
Dim userCreated As Events.User.CreatedEvent = eventToConsume
m_userIdentifier = userCreated.UserIdentifier
m_emailAddress = userCreated.EmailAddress
End If
If (TypeOf (eventToConsume) Is Events.User.AccountEnabledEvent) Then
m_enabled = True
End If
If (TypeOf (eventToConsume) Is Events.User.AccountDisabledEvent) Then
m_enabled = False
End If
End Sub
''' <summary>
''' The public unique identifier of the user
''' (could be a user name or company employee code etc.)
''' </summary>
Private m_userIdentifier As String
Public ReadOnly Property UserIdentifier As String
Get
Return m_userIdentifier
End Get
End Property
''' <summary>
''' The email address of the user
''' </summary>
Private m_emailAddress As String
Public ReadOnly Property EmailAddress As String
Get
Return m_emailAddress
End Get
End Property
''' <summary>
''' Is the user enabled or not
''' </summary>
''' <remarks>
''' This allows users to be removed from the system without any data integrity issues
''' </remarks>
Private m_enabled As Boolean
Public ReadOnly Property Enabled As Boolean
Get
Return m_enabled
End Get
End Property
End Class
值得注意的是,投影不依赖于事件流实际存储的位置或方式。这使得编写单元测试非常容易,可以证明在一组事件运行在硬编码事件流上之后,会出现预期的状态。
历史
- 2015年3月5日:初始版本