使用事件溯源机制构建主备高可用服务

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的使用,请期待下次更新。

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值