Orleans之EventSourcing
Orleans之EventSourcing
这是Orleans系列文章中的一篇.首篇文章在此
引入:
如果没有意外,我再这篇文章中用ES代替EventSourcing,如果碰到"事件回溯","事件溯源","事溯"等词语,都一般代表Eventsourcing.
如果引入Orleans而不用es的话,那就只用了Orleans一半的优点,多线程编程的逻辑\排错的简化以及可分布式.下面我聊聊重头戏ES,这些都是我个人理解,如果有错误欢迎指正.
有时候产生一个理论是为了解决目前的困难,但是随着事物的发展,原先的困难不再是主要困难,新的夜王必然会出现,听闻三眼乌鸦要成为新的夜王我很是不解.马丁这么写会收到愤怒的读者寄来的刀片,我估计马丁把收到全世界的刀片都卖了,都比他的小说值钱.扯远了。总而言之,我们关注的矛盾点会随着时间变化而变化。以前为了节省存储空间(当然并不仅仅是为了空间考虑),对数据库的设计提出了三个范式,来指导数据库的设计.可是现在时代变了狗蛋,存储空间不值钱了.现在数据库读写成为了新的瓶颈,所以我们设计数据库的理念就应该变了.
ES遵循这一个简单的思想,就是存储的时候只存储变化量,而不存储最终结果.需要最终结果的地方,就必须提取所有的变化量以及初始状态,让它们相加得到最终结果.这样的看起来是个麻烦的迂回(我不就是想得到最终结果嘛),但是却隐藏着一个事实:由于只存储变化量,意味着数据只增不减,意味着数据存储后就不会被更改,意味着高并发和高吞吐量.但是有个缺点就是数据量增加很多,现在硬盘是最不值钱的,如果随着时间的推移,变化量变的很多,要得到最终结果需要大量的运算,但是.这些缺点不算什么大缺点,有方法可以避免.虽然一直说空间不值钱,并不是说数据大小无关紧要,在合理的设计中,数据应当能小则小.
简单的一句总结就是满足只增不减,存后不改的数据,都能够设计成ES.以便于提高吞吐量.
为啥只增不减会提供吞吐量?因为数据库的数据永远不变,所以就放心大胆的读取吧。哪怕是多线程都不需要架锁。可是这里隐藏着另一个风险,你读取的数据不一定是最新的。这个"非最新"的确是个难题,不过好消息是,如果你一直读,最终能够读到最新的。这就是"最终一致性"。
关于es网络上有很多的文章,可以拿来读读,这里就不做过多的叙述了。
Orleans内置了三种ES的实现方式,但并不是我提到的ES.它内置的三种ES实现方式,分别是:StateStorage.LogConsistencyProvider,LogStorage.LogConsistencyProvider和CustomStorage.LogConsistencyProvider。这里我只使用介绍官方例子,它使用的是CustomStorage这个类。
使用Orleans达成ES,要明白几个基本的概念,所谓的Event是个啥东西?在Orleans中Event一般是指的自定义的grain事件,这些事件的发生更改了grain的状态。笼统的讲就是更改了grain类相关的各种变量的值。那么你要把这些值存起来,就要问两个问题,存到哪里去,用哪个类来控制存。又要要求溯源,那就必须读,那就要解决用什么序列化的问题。这本质上是一个问题:用什么类控制存储。
我打算拿官方的例子来介绍一下ES在orleans中基本实现方式。看完介绍,如果读者要实现自己的ES,可以仿照更改。Orleans源码里有一个例子是ReplicatedEventSample(以下简称RES),这个是我下面要说的重点。
RES例子是一个网页例子,它的网页如何体现的,以及如何运行RES,我将不做介绍,这里我只是重点说明网页后边的支撑程序:就是以下三个项目
其中主要设计的类总体图像如下:
EventGrain是这个项目的主角,它接受到外界发送来的一些消息,把这些消息使用ES的方式存储下来。在使用RES的时候,要注意EventGrain的基类以及需要实现的接口。
EventState是Grain的状态值。
ReplicatedEventTable控制着实际的读取与存储。
GeneratorGrain一个消息制造器,它发送OutCome消息给EventGrain
TickerGrain,可有可无的东西。
它们工作流程如下
刚开始启动的时候,silo会调用GeneratorGrain,这个GeneratorGrain的n个实例会激活EventGrain并每2.5秒发送一次OutCome消息。如下图
EventGrain接受到激活指令后初始化一个ReplicatedEventTable实例用来控制自己的存储,并调用RefreshNow()获取最新的状态,这时候orleans会调用ReadStateFromStorage,EventGrain在这里使用ReplicatedEventTable完成真正的读取动作。
GeneratorGrain每隔2.5秒制造一个OutCome消息发送给EventGrain,EventGrain接受到消息并立即确认此消息,这时候可以在OnStateChanged函数中针对Grain状态值变化做一些必要的处理。这些必要的处理不应该包括ES存储动作,ES存储动作会在ApplyUpdatesToStorage中进行处理,它里面使用了ReplicatedEventTable来进行真正的存储。
这样一个简单的ES就实现了。这个ES里面的每一条数据都是OutCome,我们只需要改写ReplicatedEventTable实现自己的存储控制类。在本例子中,使用的是AzureTable作为存储媒介的。
这里有几个问题,如果我改写后的新存储控制类,假设名字是EventSql是一个普通类,在EventSql中我精确的控制了sql连接,sql读取等等。因为EventGrain是一个key一个实例的。在一个soli中有可能存在非常多个Grain实例,这时候我再使用EventSql就有可能会产生很多的链接,导致出错。要改进这里,只需要让EventSql本身也是扩展自Grain(而不仅仅是一个普通的类)。同时使用一些机制来控制链接数。可以池化这些EventSqlGrain,使用的时候从池子中取出一个。
这样官方的例子就算介绍完毕了,剩下的来聊聊一些其他细节的问题。
Orleans运行时会时不时的自动更新Grain的状态值。但是有时候也是需要针对某些事件,我们自定义的去更新状态值,要实现这样的动作,可以重写TransitionState 函数。
ReadStateFromStorage返回的值中需要有version 和对应的State。刚开始的时候,压根什么都没有存,那么就应该返回0和一个State的默认值。ApplyUpdatesToStorage有时候会存储失败,这时候运行时会进行重试。有时候虽然状态值已经实际存储了,但是却返回一个错误,这样会造成同一状态值的重复存储,这里就需要程序有一个过滤机制或者确保一个重复的状态值不会对程序逻辑造成损害。
使用Orleans.EventSourcing.CustomStorage.LogConsistencyProvider(本例就是),它并不支持JournaledGrain类中的RetrieveConfirmedEvents方法,如果需要使用它,要另行实现。
这里只是我个人写一些基础的东西,具体改写还是需要自己去实现,刚开始可以仿照这个RES进行改写,把OutCome存储到自己想要的地方。等改写完毕就会一个大致的概念。
至此我写完了orleans所有的主要方面,剩余的orleans底层的实现细节,还是需要自己对照文档啃源码了。
ES是实现业务需求的方法,一个项目本身是一个客观物体,我们选择了使用ES的办法去实现它,会将这个项目的复杂度进行转移,降低了业务在程序实现上的复杂度而同时增加了数据设计的复杂度。ES这个新的方法会让某些复杂的业务逻辑变的简单,特别适合于那些需要高并发与高吞吐量的场景。
开篇曾经说过的,"如果随着时间的推移,变化量变的很多,要得到最终结果需要大量的运算"是个缺点,要克服这个缺点,可以这样搞:每隔一段时间(比如一周)就计算所有的最终结果,并把这个最终结果当作新的初始值,同时把变化量转移备份。简单的如下图所示: