事件溯源是一种记录对象在其生命周期中发生的事件,而不是存储对象某一时刻的状态的技术。这种技术在金融应用中特别有用,因为任何账户余额通常是导致该余额的事件的结果,希望既能看到数字的底层推导过程,也能看到在给定时间点的估值。
事件溯源允许通过过滤事件或在替代流中播放来进行“如果”分析。
银行账户可能是最简单的金融流,仅由单一货币的存款和取款组成。在这个模型中,事件就是调整余额的金额——存款是正数,取款是负数。
[ - - +50 - - +50 - - -100 - ->]
在这个例子中,只有在实际发生存款或取款时,估值才会变化——所以通过播放事件流到结尾,可以看到当前余额现在是0。
对于股份持有账户,有两个流。有一个位置流记录股份的购买或销售,还有一个定价流记录底层股份的价格。
[ - - +50 - - +50 - - -100 - ->]
[178.1 - - - - 178.0 - - - - 178.9 - - >]
这些可以合并成一个估值流,即当前持有量乘以最新价格的组合。持有事件和/或定价事件都会触发估值。
[ 0 - - 8905.00 - - 17810.00 - - 17800.00 - - 0.00 - - 0.00 ]
如果股份持有是以非基础货币持有的,那么持有货币和账户基础货币之间的汇率变化也会影响估值。
[ - - +50- - +50- - -100- -> ]
[178.1 - - - - 178.0 - - - - 178.9 -> ]
[1.1- - - 1.2 - - - 1.3 - - - 1.4 -> ]
对于任何事件,需要知道——它何时发生,它在哪个流中,它的价值是多少。从上述例子中,也可以看到,价值可以是绝对金额(例如,价格或汇率),也可以是变化(或增量)金额。
对于流是增量金额的事件流,写入当前值的定期快照可能会很有帮助。这允许通过转到该点之前的快照,然后播放该快照时间之后的任何增量事件,来播放流到任何给定点。
流可以是源数据,也可以是通过一个或多个源流的函数派生的。对于派生事件,使用函数从其组成流中计算值,并且对于这些流中的任何事件都会触发该函数。
在复式记账中,持有事件记录在两个(或更多)流中——一个代表事件影响的每个账本账户(最常见的是,一个用于资产,一个用于负债)。传统上,这是通过在输入流上使用发布规则来发布到相关的两个(或更多)账户流来完成的。
为了将读取访问时间保持在最低限度,这种架构应该支持读取和创建快照。这些快照可以被视为数据传输对象,用于将它们集成到基于它们的任何系统(如MVC应用程序)中。
定义所需操作的接口可能如下所示:
C#
///
<summary>
///
Repository to write to objects that are held in snapshot form
///
</summary>
///
<typeparam name="TEntity">
///
The type of entity in the repository
///
</typeparam>
///
<typeparam name="TKey">
///
The type of the key to uniquely identify the entity
///
</typeparam>
///
<remarks>
///
This does not inherit from IRepositoryWrite
///
because it is possible that some background task will be
///
creating the snapshots independent of front-end write requests
///
</remarks>
public interface IRepositoryWriteSnapshot<TKey, TEntity>
where TEntity : IKeyedEntity<TKey>
{
///
<summary>
///
Request that a snapshot as-of-now be taken for the given entity aggregate
///
</summary>
///
<param name="key">
///
The unique identifier of the entity that we want snapshotted
///
</param>
///
<remarks>
///
This does not return any value as it is designed to
///
operate asynchronously by setting a "needs snapshot" flag to prevent
///
the snapshot generator system being flooded
///
</remarks>
void RequestSnapshot(TKey key);
///
<summary>
///
Delete and regenerate any snapshots post the as-of synchronization value
///
</summary>
///
<param name="key">
///
The unique identifier of the entity that we want snapshotted
///
</param>
///
<param name="synchronization">
///
The synchronization point for which we want the data regenerated
///
</param>
void RegenerateFromAsOf(TKey key,
long synchronisation);
}
And the read side can be implemented as an extension to the usual repository pattern:
C#
///
<summary>
///
Repository to read objects that are held in snapshot form
///
</summary>
///
<typeparam name="TEntity">
///
The key-identified type of entity we are reading
///
</typeparam>
///
<typeparam name="TKey">
///
The type of the key
///
</typeparam>
///
<remarks>
///
Where an object is based on one or more event streams,
///
this allows for retrieval of the state of that object as at a given
///
snapshot time. This extends repository-read
///
</remarks>
public interface IRepositoryReadSnaphot<TKey, TEntity>
: IRepositoryRead<TKey, TEntity>
where TEntity : IKeyedEntity<TKey>
{
///
<summary>
///
Did any record matching the key exist as at the given point in time
///
</summary>
///
<param name="key">
///
The unique aggregate identifier of the record we are seeking
///
</param>
///
<returns>
///
True if a matching record is found
///
</returns>
bool ExistedAsOf(TKey key,
long synchronisation);
///
<summary>
///
Get the entity state as it was at the given point in time
///
</summary>
///
<param name="key">
///
The aggregate identifier of the record for which we want a point-in-time view
///
</param>
///
<param name="synchronisation">
///
The synchronisation point for which we want the data
///
</param>
///
<returns>
</returns>
Nullable<TEntity> GetByKeyAsOf(TKey key,
long synchronisation);
}
在实践中,审计或法律要求可能需要保留旧的快照(如果它们被外部发送)。需要一个版本控制机制,以确保任何显示(UI或打印)使用最新版本的快照。
这种类型的架构需要考虑的最重要的事情之一是幂等性——使系统对错误免疫,如果一个事件运行了多次。在实践中,这通常意味着用执行绝对操作的消息替换执行增量操作的消息(例如,“将账户减少50美元”)。
然而,与其人为地制造绝对事件,不如深入挖掘,直到得到固有的绝对事件。在上面的例子中,有绝对的存款和取款事件比有一个“将余额设置为x”的派生事件更可取。