1.关于有状态和无状态服务
在微服务日趋流行的今天,无状态、基于SpringBoot的微服务越来越流行,这种程序结构的服务针对每次请求产生的状态都立即写入数据库,下次请求到来再从数据库中拿出来做初始状态,这种模式会有如下一些典型的问题:
- 无疑会增加一些数据库的性能消耗,然后衍生出redis/member cache等中间缓存层,但缓存层跟持久层的一致性问题又需要小心解决
- 另外一个问题是很容易面向数据库编程,把业务逻辑都耦合在了sql里,导致业务扩展困难
- 可测试性较差,特别是单元测试,需要连接数据库才能开始单元测试,虽然可以用h2等内存数据库解决,但如果代码里用了mysql/pg等特殊的语法,h2数据库是不能完全兼容的
一些复杂的业务系统,比如调度系统、数据库系统,由于业务复杂或者性能原因,不可能做成无状态,有状态服务的状态都在内存中,所有的代码操作都是针对本地内存,则有如下优势:
- 性能较高,由于本地内存的状态就是最新状态,因此无需从持久化系统中加载状态,省去了IO消耗
- 无需代码数据结构到数据库表结构的操作转换,因此也更容易执行领域驱动设计
- 由于数据模型跟数据库不直接耦合,因此也更容易单元测试
有状态服务还是有不少好处的,但关键问题是程序肯定会重启,如何保证重启后状态能还原到重启之前呢?特别是如何防止当机、如何做高可用呢?
目前基于事件溯源的主要思路如下:
1.对于每个客户端的请求,可以抽象为命令,对于命令首先转换为事件并序列化到外部存储
2.根据事件执行本地内存状态变更
3.执行命令施加的业务逻辑并返回结果数据
目前akka框架的event source提供了这样的封装,使用示例如下:
command match {
case Add(data) =>
Effect.persist(Added(data)).thenRun(newState => subscriber ! newState)
case Clear =>
Effect.persist(Cleared).thenRun((newState: State) => subscriber ! newState).thenStop()
}
val eventHandler: (State, Event) => State = { (state, event) =>
event match {
case Added(data) => state.copy((data :: state.history).take(5))
case Cleared => State(Nil)
}
}
通过以上三部分拆分,Add命令会首先转换为Added事件,Effect.persist(Added(data))语义为把事件持久化到外部持久化存储,然后执行.thenRun的命令逻辑,最后返回给客户端,而eventHandler专门处理事件,在此处理函数里不执行施加外部影响的操作。
这样的机制非常方便我们做高可用,比如我们可以在节点1把事件同步给节点2,然后节点2接收到节点1的事件后直接应用修改状态,这样当节点1当机后节点2的状态是跟节点1的状态是一致的,是不是有点类似mysql复制或redis主从复制?是的,我们应用程序也可以做成这样的。
但服务重启了数据怎么恢复呢?akka框架会自动从事件存储中按序应用eventHandler函数,这样等事件数据都应用完,状态就跟重启前是一致的了。问题来了,当事件日积月累过多怎么办呢,这样恢复时间太长了,数据库或者redis都有snapshot功能,akka也有类似机制,使用snapshot可以大大加快数据状态的恢复时长,关于akka的使用,请期待下次更新。