前言..........

我们在使用Akka时,会经常遇到一些存储Actor内部状态的场景,在系统正常运行的情况下,我们不需要担心什么,但是当系统出错,比如Actor错误需要重启,或者内存溢出,亦或者整个系统崩溃,如果我们不采取一定的方案的话,在系统重启时Actor的状态就会丢失,这会导致我们丢失一些关键的数据,造成系统数据不一致的问题。Akka作为一款成熟的生产环境应用,为我们提供了相应的解决方案就是Akka persistence。

为什么需要持久化的Actor?

万变不离其宗,数据的一致性是永恒的主题,一个性能再好的系统,不能保证数据的正确,也称不上是一个好的系统,一个系统在运行的时候难免会出错,如何保证系统在出错后能正确的恢复数据,不让数据出现混乱是一个难题。使用Actor模型的时候,我们会有这么一个想法,就是能不对数据库操作就尽量不对数据库操作(这里我们假定我们的数据库是安全,可靠的,能保证数据的正确性和一致性,比如使用国内某云的云数据库),一方面如果大量的数据操作会使数据库面临的巨大的压力,导致崩溃,另一方面即使数据库能处理的过来,比如一些count,update的大表操作也会消耗很多的时间,远没有内存中直接操作来的快,大大影响性能。但是又有人说内存操作这么快,为什么不把数据都放内存中呢?答案显而易见,当出现机器死机,或者内存溢出等问题时,数据很有可能就丢失了导致无法恢复。在这种背景下,我们是不是有一种比较好的解决方案,既能满足需求又能用最小的性能消耗,答案就是上面我们的说的Akka persistence。

Akka persistence的核心架构

在具体深入Akka persistence之前,我们可以先了解一下它的核心设计理念,其实简单来说,我们可以利用一些thing来恢复Actor的状态,这里的thing可以是日志、数据库中的数据,亦或者是文件,所以说它的本质非常容易理解,在Actor处理的时候我们会保存一些数据,Actor在恢复的时候能根据这些数据恢复其自身的状态。

所以Akka persistence 有以下几个关键部分组成:

  • PersistentActor:任何一个需要持久化的Actor都必须继承它,并必须定义或者实现其中的三个关键属性:
 def persistenceId = "example" //作为持久化Actor的唯一表示,用于持久化或者查询时使用

 def receiveCommand: Receive = ??? //Actor正常运行时处理处理消息逻辑,可在这部分内容里持久化自己想要的消息

 def receiveRecover: Receive = ??? //Actor重启恢复是执行的逻辑

相比普通的Actor,除receiveCommand相似以外,还必须实现另外两个属性。 另外在持久化Actor中还有另外两个关键的的概念就是JournalSnapshot,前者用于持久化事件,后者用于保存Actor的快照,两者在Actor恢复状态的时候都起到了至关重要的作用。

Akka persistence的demo实战

这里我首先会用一个demo让大家能对Akka persistence的使用有一定了解的,并能大致明白它的工作原理,后面再继续讲解一些实战可能会遇到的问题。

假定现在有这么一个场景,现在假设有一个1w元的大红包,瞬间可能会很多人同时来抢,每个人抢的金额也可能不一样,场景很简单,实现方式也有很多种,但前提是保证数据的正确性,比如最普通的使用数据库保证,但对这方面有所了解的同学都知道这并不是一个很好的方案,因为需要锁,并需要大量的数据库操作,导致性能不高,那么我们是否可以用Actor来实现这个需求么?答案是当然可以。

我们首先来定义一个抽奖命令, scala case class LotteryCmd( userId: Long, // 参与用户Id username: String, //参与用户名 email: String // 参与用户邮箱 ) 然后我们实现一个抽奖Actor,并继承PersistentActor作出相应的实现:

case class LuckyEvent(  //抽奖成功事件
userId: Long,
luckyMoney: Int
)
case class FailureEvent( //抽奖失败事件
userId: Long,
reason: String
)
case class Lottery(
totalAmount: Int, //红包总金额
remainAmount: Int //剩余红包金额
) {
def update(luckyMoney: Int) = {
copy(
remainAmount = remainAmount - luckyMoney
)
}
}
class LotteryActor(initState: Lottery) extends PersistentActor with ActorLogging{
override def persistenceId: String = "lottery-actor-1" var state = initState //初始化Actor的状态 override def receiveRecover: Receive = {
case event: LuckyEvent =>
updateState(event) //恢复Actor时根据持久化的事件恢复Actor状态
case SnapshotOffer(_, snapshot: Lottery) =>
log.info(s"Recover actor state from snapshot and the snapshot is ${snapshot}")
state = snapshot //利用快照恢复Actor的状态
case RecoveryCompleted => log.info("the actor recover completed")
} def updateState(le: LuckyEvent) =
state = state.update(le.luckyMoney) //更新自身状态 override def receiveCommand: Receive = {
case lc: LotteryCmd =>
doLottery(lc) match { //进行抽奖,并得到抽奖结果,根据结果做出不同的处理
case le: LuckyEvent => //抽到随机红包
persist(le) { event =>
updateState(event)
increaseEvtCountAndSnapshot()
sender() ! event
}
case fe: FailureEvent => //红包已经抽完
sender() ! fe
}
case "saveSnapshot" => // 接收存储快照命令执行存储快照操作
saveSnapshot(state)
case SaveSnapshotSuccess(metadata) => ??? //你可以在快照存储成功后做一些操作,比如删除之前的快照等
} private def increaseEvtCountAndSnapshot() = {
val snapShotInterval = 5
if (lastSequenceNr % snapShotInterval == 0 && lastSequenceNr != 0) { //当有持久化5个事件后我们便存储一次当前Actor状态的快照
self ! "saveSnapshot"
}
} def doLottery(lc: LotteryCmd) = { //抽奖逻辑具体实现
if (state.remainAmount > 0) {
val luckyMoney = scala.util.Random.nextInt(state.remainAmount) + 1
LuckyEvent(lc.userId, luckyMoney)
}
else {
FailureEvent(lc.userId, "下次早点来,红包已被抽完咯!")
}
}
}

程序很简单,关键位置我也给了注释,相信大家对Actor有所了解的话很容易理解,当然要是有些疑惑,可以看看我之前写的文章,下面我们就对刚才写的抽红包Actor进行测试:

object PersistenceTest extends App {
val lottery = Lottery(10000,10000)
val system = ActorSystem("example-05")
val lotteryActor = system.actorOf(Props(new LotteryActor(lottery)), "LotteryActor-1") //创建抽奖Actor
val pool: ExecutorService = Executors.newFixedThreadPool(10)
val r = (1 to 100).map(i =>
new LotteryRun(lotteryActor, LotteryCmd(i.toLong,"godpan","xx@gmail.com")) //创建100个抽奖请求
)
r.map(pool.execute(_)) //使用线程池来发起抽奖请求,模拟同时多人参加
Thread.sleep(5000)
pool.shutdown()
system.terminate()
} class LotteryRun(lotteryActor: ActorRef, lotteryCmd: LotteryCmd) extends Runnable { //抽奖请求
implicit val timeout = Timeout(3.seconds)
def run: Unit = {
for {
fut <- lotteryActor ? lotteryCmd
} yield fut match { //根据不同事件显示不同的抽奖结果
case le: LuckyEvent => println(s"恭喜用户${le.userId}抽到了${le.luckyMoney}元红包")
case fe: FailureEvent => println(fe.reason)
case _ => println("系统错误,请重新抽取")
}
}
}

运行程序,我们可能看到以下的结果:

下面我会把persistence actor在整个运行过程的步骤给出,帮助大家理解它的原理:

  • 1.初始化Persistence Actor

    • 1.1若是第一次初始化,则与正常的Actor的初始化一致。
    • 1.2若是重启恢复Actor,这根据Actor之前持久的数据恢复。
    • 1.2.1从快照恢复,可快速恢复Actor,但并非每次持久化事件都会保存快照,在快照完整的情况下,Actor优先从快照恢复自身状态。
    • 1.2.2从事件(日志,数据库记录等)恢复,通过重放持久化事件恢复Actor状态,比较关键。
  • 2.接收命令进行处理,转化为需要持久化的事件(持久化的事件尽量只包含关键性的数据)使用Persistence Actor的持久化方法进行持久化(上述例子中的persist,后面我会讲一下批量持久化),并处理持久化成功后的逻辑处理,比如修改Actor状态,向外部Actor发送消息等。

  • 3.若是我们需要存储快照,那么可以主动指定存储快照的频率,比如持久化事件100次我们就存储一次快照,这个频率应该要考虑实际的业务场景,在存储快照成功后我们也可以执行一些操作。

总的来说Persistence Actor运行时的大致操作就是以上这些,当然它是r如何持久化事件,恢复时的机制是怎么样的等有兴趣的可以看一下Akka源码。

使用Akka persistence的相关配置

首先我们必须加载相应的依赖包,在bulid.sbt中加入以下依赖:

libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor" % "2.4.16", //Akka actor 核心依赖
"com.typesafe.akka" %% "akka-persistence" % "2.4.16", //Akka persistence 依赖
"org.iq80.leveldb" % "leveldb" % "0.7", //leveldb java版本依赖
"org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8", //leveldb java版本依赖
"com.twitter" %% "chill-akka" % "0.8.0" //事件序列化依赖
)

另外我们还需在application.conf加入以下配置:

akka.persistence.journal.plugin = "akka.persistence.journal.leveldb"
akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local" akka.persistence.journal.leveldb.dir = "log/journal"
akka.persistence.snapshot-store.local.dir = "log/snapshots" # DO NOT USE THIS IN PRODUCTION !!!
# See also https://github.com/typesafehub/activator/issues/287
akka.persistence.journal.leveldb.native = false //因为我们本地并没有安装leveldb,所以这个属性置为false,但是生产环境并不推荐使用 akka.actor.serializers {
kryo = "com.twitter.chill.akka.AkkaSerializer"
} akka.actor.serialization-bindings {
"scala.Product" = kryo
"akka.persistence.PersistentRepr" = kryo
}

至此为止我们整个Akka persistence demo已经搭建好了,可以正常运行了,有兴趣的同学可以下载源码。源码链接

Akka persistence进阶

1.持久化插件

有同学可能会问,我对leveldb不是很熟悉亦或者觉得单机存储并不是安全,有没有支持分布式数据存储的插件呢,比如某爸的云数据库?答案当然是有咯,良心的我当然是帮你们都找好咯。

  • 1.akka-persistence-sql-async: 支持MySQL和PostgreSQL,另外使用了全异步的数据库驱动,提供异步非阻塞的API,我司用的就是它的变种版,6的飞起。项目地址

  • 2.akka-persistence-cassandra: 官方推荐的插件,使用写性能very very very fast的cassandra数据库,是几个插件中比较流行的一个,另外它还支持persistence query。项目地址

  • 3.akka-persistence-redis: redis应该也很符合Akka persistence的场景,熟悉redis的同学可以使用看看。项目地址

  • 4.akka-persistence-jdbc: 怎么能少了jdbc呢?不然怎么对的起java爸爸呢,支持scala和java哦。项目地址

相应的插件的具体使用可以看该项目的具体介绍使用,我看了下相对来说都是比较容易的。

2.批量持久化

上面说到我司用的是akka-persistence-sql-async插件,所以我们是将事件和快照持久化到数据库的,一开始我也是像上面demo一样,每次事件都会持久化到数据库,但是后来在性能测试的时候,因为本身业务场景对数据库的压力也比较大,在当数据库到达每秒1000+的读写量后,另外说明一下使用的是某云数据库,性能中配以上,发现每次持久化的时间将近要15ms,这样换算一下的话Actor每秒只能处理60~70个需要持久化的事件,而实际业务场景要求Actor必须在3秒内返回处理结果,这种情况下导致大量消息处理超时得不到反馈,另外还有大量的消息得不到处理,导致系统错误暴增,用户体验下降,既然我们发现了问题,那么我们能不能进行优化呢?事实上当然是可以,既然单个插入慢,那么我们能不能批量插入呢,Akka persistence为我们提供了persistAll方法,下面我就对上面的demo进行一下改造,让其变成批量持久化:

class LotteryActorN(initState: Lottery) extends PersistentActor with ActorLogging{
override def persistenceId: String = "lottery-actor-2" var state = initState //初始化Actor的状态 override def receiveRecover: Receive = {
case event: LuckyEvent =>
updateState(event) //恢复Actor时根据持久化的事件恢复Actor状态
case SnapshotOffer(_, snapshot: Lottery) =>
log.info(s"Recover actor state from snapshot and the snapshot is ${snapshot}")
state = snapshot //利用快照恢复Actor的状态
case RecoveryCompleted => log.info("the actor recover completed")
} def updateState(le: LuckyEvent) =
state = state.update(le.luckyMoney) //更新自身状态 var lotteryQueue : ArrayBuffer[(LotteryCmd, ActorRef)] = ArrayBuffer() context.system.scheduler //定时器,定时触发抽奖逻辑
.schedule(
0.milliseconds,
100.milliseconds,
new Runnable {
def run = {
self ! "doLottery"
}
}
) override def receiveCommand: Receive = {
case lc: LotteryCmd =>
lotteryQueue = lotteryQueue :+ (lc, sender()) //参与信息加入抽奖队列
println(s"the lotteryQueue size is ${lotteryQueue.size}")
if (lotteryQueue.size > 5) //当参与人数有5个时触发抽奖
joinN(lotteryQueue)
case "doLottery" =>
if (lotteryQueue.size > 0)
joinN(lotteryQueue)
case "saveSnapshot" => // 接收存储快照命令执行存储快照操作
saveSnapshot(state)
case SaveSnapshotSuccess(metadata) => ??? //你可以在快照存储成功后做一些操作,比如删除之前的快照等
} private def joinN(lotteryQueue: ArrayBuffer[(LotteryCmd, ActorRef)]) = { //批量处理抽奖结果
val rs = doLotteryN(lotteryQueue)
val success = rs.collect { //得到其中中奖的相应信息
case (event: LuckyEvent, ref: ActorRef) =>
event -> ref
}.toMap
val failure = rs.collect { //得到其中未中奖的相应信息
case (event: FailureEvent, ref: ActorRef) => event -> ref
}
persistAll(success.keys.toIndexedSeq) { //批量持久化中奖用户事件
case event => println(event)
updateState(event)
increaseEvtCountAndSnapshot()
success(event) ! event
}
failure.foreach {
case (event, ref) => ref ! event
}
this.lotteryQueue.clear() //清空参与队列
} private def increaseEvtCountAndSnapshot() = {
val snapShotInterval = 5
if (lastSequenceNr % snapShotInterval == 0 && lastSequenceNr != 0) { //当有持久化5个事件后我们便存储一次当前Actor状态的快照
self ! "saveSnapshot"
}
} private def doLotteryN(lotteryQueue: ArrayBuffer[(LotteryCmd, ActorRef)]) = { //抽奖逻辑具体实现
var remainAmount = state.remainAmount
lotteryQueue.map(lq =>
if (remainAmount > 0) {
val luckyMoney = scala.util.Random.nextInt(remainAmount) + 1
remainAmount = remainAmount - luckyMoney
(LuckyEvent(lq._1.userId, luckyMoney),lq._2)
}
else {
(FailureEvent(lq._1.userId, "下次早点来,红包已被抽完咯!"),lq._2)
}
)
}
}

这是改造后的参与Actor,实现了批量持久的功能,当然这里为了给发送者返回消息,处理逻辑稍微复杂了一点,不过真实场景可能会更复杂,相关源码也在刚才的项目上。

3.Persistence Query

另外Akka Persistence还提供了Query接口,用于需要查询持久化事件的需求,这部分内容可能要根据实际业务场景考虑是否需要应用。

来源于:https://godpan.me/2017/07/25/learning-akka-7.html

Akka系列(七):Actor持久化之Akka persistence的更多相关文章

  1. Akka系列(二):Akka中的Actor系统

    前言......... Actor模型作为Akka中最核心的概念,所以Actor在Akka中的组织结构是至关重要,本文主要介绍Akka中Actor系统. 1.Actor系统 Actor作为一种封装状态 ...

  2. Akka系列(十):Akka集群之Akka Cluster

    前言........... 上一篇文章我们讲了Akka Remote,理解了Akka中的远程通信,其实Akka Cluster可以看成Akka Remote的扩展,由原来的两点变成由多点组成的通信网络 ...

  3. Akka系列(八):Akka persistence设计理念之CQRS

    前言........ 这一篇文章主要是讲解Akka persistence的核心设计理念,也是CQRS(Command Query Responsibility Segregation)架构设计的典型 ...

  4. Akka系列(九):Akka分布式之Akka Remote

    前言.... Akka作为一个天生用于构建分布式应用的工具,当然提供了用于分布式组件即Akka Remote,那么我们就来看看如何用Akka Remote以及Akka Serialization来构建 ...

  5. Akka系列(四):Akka中的共享内存模型

    前言...... 通过前几篇的学习,相信大家对Akka应该有所了解了,都说解决并发哪家强,JVM上面找Akka,那么Akka到底在解决并发问题上帮我们做了什么呢? 共享内存 众所周知,在处理并发问题上 ...

  6. Akka系列(三):监管与容错

    前言...... Akka作为一种成熟的生产环境并发解决方案,必须拥有一套完善的错误异常处理机制,本文主要讲讲Akka中的监管和容错. 监管 看过我上篇文章的同学应该对Actor系统的工作流程有了一定 ...

  7. akka设计模式系列(Actor模型)

    谈到Akka就必须介绍Actor并发模型,而谈到Actor就必须看一篇叫做<A Universal Modular Actor Formalism for Artificial Intellig ...

  8. Akka系列---什么是Actor

    本文已.Net语法为主,同时写有Scala及Java实现代码 严肃的说,演员是一个广泛的概念,作为外行人我对Actor 模型的定义: Actor是一个系统中参与者的虚拟人物,Actor与Actor之间 ...

  9. Akka系列(六):Actor解决了什么问题?

    前言..... 文档来源于  : What problems does the actor model solve? Actor解决了什么问题? Akka使用Actor模型来克服传统面向对象编程模型的 ...

随机推荐

  1. electron 设置-webkit-app-region: drag 后, 双击放大窗口变形

    双击放大后窗口只是在最左上角,并没有放大, 或者放大了页面变形,如图 原因: 是设置了窗口 transparent: true,和背景色导致的, 不要设置就可以,默认为false mainWindow ...

  2. mvc api 关于 post 跟get 请求的一些想法[FromUri] 跟[FromBody] 同一个控制器如何实现共存

    wep api  在设置接收请求参数的时候,会自动根据模型进行解析. [FromUrl] 跟[FromBody] 不可以同时使用. 要拆分开: [HttpGet] public object GetP ...

  3. cordova打包遇到Connection timedout:

    在cordova项目打包时,有时候处在公司内网环境,导致有些文件无法下载报下面的错误: A problem occurred configuring root project 'android'. & ...

  4. Centos 下硬盘分区的最佳方案

    Centos7从零开始]Centos 下硬盘分区的最佳方案 2016年12月25日 10:09:02 浮華的滄桑 阅读数 41971   在对硬盘进行分区前,应该先弄清楚计算机担负的工作及硬盘的容量有 ...

  5. 堤堤云海外IDC

    http://www.ddyidc.com 堤堤云网络全球互联 堤堤云网络致力为客户提供优质的海外服务器租用服务,各种专线解决方案. 产品分类:服务器租用.IP租用.托管.专线传输.防御.优质回国CN ...

  6. 二、Smarty中的三种主要变量

    1.从PHP中分配的变量 $smarty -> assign(); 从PHP分配给模板使用的变量:动态变量 2.从配置文件中读取的变量 $smarty配置文件中的内容不是PHP读取,而是就在sm ...

  7. 有关C#写一个WindowsService的两篇文章

    1.http://blog.csdn.net/yysyangyangyangshan/article/details/10515035 上面的这篇文章一共两段,第二段讲的是使用代码来安装发布这个Win ...

  8. Python学习笔记:第一次接触

    用的是windows的IDLE(python 3) 对象的认识:先创建一个list对象(用方括号) a = ['xieziyang','chenmanru'] a 对list中对象的引用 a[0] # ...

  9. laravel 浏览器图标的设置方式

    <head> <meta charset="UTF-8"> <title>叮叮书店</title> <link href=&q ...

  10. 微信、QQ第三方登录授权时的问题总结

    一.微信第一个问题:redirect_uri域名与后台配置不一致,错误码:10003 解决方案: 1,首先确定访问的第三方接口地址参数前后顺序是否正确,redirect_uri回调地址是否加了http ...