摘要:源碼鏈接進階持久化插件有同學可能會問,我對不是很熟悉亦或者覺得單機存儲并不是安全,有沒有支持分布式數據存儲的插件呢,比如某爸的云數據庫答案當然是有咯,良心的我當然是幫你們都找好咯。
這次把這部分內容提到現在寫,是因為這段時間開發的項目剛好在這一塊遇到了一些難點,所以準備把經驗分享給大家,我們在使用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中還有另外兩個關鍵的的概念就是Journal和Snapshot,前者用于持久化事件,后者用于保存Actor的快照,兩者在Actor恢復狀態的時候都起到了至關重要的作用。
這里我首先會用一個demo讓大家能對Akka persistence的使用有一定了解的,并能大致明白它的工作原理,后面再繼續講解一些實戰可能會遇到的問題。
假定現在有這么一個場景,現在假設有一個1w元的大紅包,瞬間可能會很多人同時來搶,每個人搶的金額也可能不一樣,場景很簡單,實現方式也有很多種,但前提是保證數據的正確性,比如最普通的使用數據庫保證,但對這方面有所了解的同學都知道這并不是一個很好的方案,因為需要鎖,并需要大量的數據庫操作,導致性能不高,那么我們是否可以用Actor來實現這個需求么?答案是當然可以。
我們首先來定義一個抽獎命令,
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接口,用于需要查詢持久化事件的需求,這部分內容可能要根據實際業務場景考慮是否需要應用,我就不展開講了,另外我也寫了一個小demo在項目中,想要嘗試的同學也可以試試。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/67470.html
摘要:創建訂單時同步操作有查詢庫存,扣款,刷新庫存可異步的操作有通知風控系統,給買家發送扣款郵件和短信,通知賣家,創建一些定時任務。 同步轉異步是一種常見的優化手段,最近一次在做調優時便大量使用了這種方式。通常在一個業務場景中會包含多個操作,有些操作的結果需要讓用戶立馬知道,但有些操作則不需要。這些用戶不需要等待結果的操作,我們在編程的時候便可以異步處理。這么做最直接的效果就是縮短接口響應速...
摘要:是一個構建在上,基于模型的的并發框架,為構建伸縮性強,有彈性的響應式并發應用提高更好的平臺。上述例子中的信件就相當于中的消息,與之間只能通過消息通信。當然模型比這要復雜的多,這里主要是簡潔的闡述一下模型的概念。模型的出現解決了這個問題。 Akka是一個構建在JVM上,基于Actor模型的的并發框架,為構建伸縮性強,有彈性的響應式并發應用提高更好的平臺。本文主要是個人對Akka的學習和應...
摘要:模型作為中最核心的概念,所以在中的組織結構也至關重要,本文主要介紹中系統。這里主要是演示可以根據配置文件的內容去加載相應的環境,并應用到整個中,這對于我們配置環境來說是非常方便的。路徑與地址熟悉類系統的同學應該對路徑這個概念很熟悉了。 Actor模型作為Akka中最核心的概念,所以Actor在Akka中的組織結構也至關重要,本文主要介紹Akka中Actor系統。 Actor系統 Act...
摘要:關于三者的一些概括總結離線分析框架,適合離線的復雜的大數據處理內存計算框架,適合在線離線快速的大數據處理流式計算框架,適合在線的實時的大數據處理我是一個以架構師為年之內目標的小小白。 整理自《架構解密從分布式到微服務》第七章——聊聊分布式計算.做了相應補充和修改。 [TOC] 前言 不管是網絡、內存、還是存儲的分布式,它們最終目的都是為了實現計算的分布式:數據在各個計算機節點上流動,同...
摘要:是所有由系統創建的頂級的監管者,如日志監聽器,或由配置指定在系統啟動時自動部署的。所有其他被上升到根監管者,然后整個系統將會關閉。監管容錯示例本示例主要演示在發生錯誤時,它的監管者會根據相應的監管策略進行不同的處理。 Akka作為一種成熟的生產環境并發解決方案,必須擁有一套完善的錯誤異常處理機制,本文主要講講Akka中的監管和容錯。 監管 看過我上篇文章的同學應該對Actor系統的工作...
閱讀 532·2024-11-06 13:38
閱讀 832·2024-09-10 13:19
閱讀 971·2024-08-22 19:45
閱讀 1392·2021-11-19 09:40
閱讀 2636·2021-11-18 13:14
閱讀 4300·2021-10-09 10:02
閱讀 2326·2021-08-21 14:12
閱讀 1291·2019-08-30 15:54