Actor模型是解決高併發的終極解決方案

寫在開始

通常來講有兩種策略用來在併發線程中進行通訊:共享數據和消息傳遞。使用共享數據方式的併發編程面臨的最大的一個問題就是數據條件競爭。處理各類鎖的問題是讓人十分頭痛的一件事。java

   傳統多數流行的語言併發是基於多線程之間的共享內存,使用同步方法防止寫爭奪,Actors使用消息模型,每一個Actor在同一時間處理最多一個消息,能夠發送消息給其餘Actor,保證了單獨寫原則。從而巧妙避免了多線程寫爭奪。和共享數據方式相比,消息傳遞機制最大的優勢就是不會產生數據競爭狀態。實現消息傳遞有兩種常見的類型:基於channel(golang爲典型表明)的消息傳遞和基於Actor(erlang爲表明)的消息傳遞。golang

Actor簡介

Actor模型(Actor model)首先是由Carl Hewitt在1973定義, 由Erlang OTP 推廣,其 消息傳遞更加符合面向對象的原始意圖。Actor屬於併發組件模型,經過組件方式定義併發編程範式的高級階段,避免使用者直接接觸多線程併發或線程池等基礎概念。

Actor模型=數據+行爲+消息。redis

Actor模型是一個通用的併發編程模型,而非某個語言或框架全部,幾乎能夠用在任何一門編程語言中,其中最典型的是erlang,在語言層面就提供了Actor模型的支持,殺手鐗應用RabbitMQ 就是基於erlang開發的。編程

更加面向對象

Actor相似面向對象編程(OO)中的對象,每一個Actor實例封裝了本身相關的狀態,而且和其餘Actor處於物理隔離狀態。舉個遊戲玩家的例子,每一個玩家在Actor系統中是Player 這個Actor的一個實例,每一個player都有本身的屬性,好比Id,暱稱,攻擊力等,體現到代碼級別其實和咱們OO的代碼並沒有多大區別,在系統內存級別也是出現了多個OO的實例緩存

class PlayerActor
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

無鎖

在使用Java/C# 等語言進行併發編程時須要特別的關注鎖和內存原子性等一系列線程問題,而Actor模型內部的狀態由它本身維護即它內部數據只能由它本身修改(經過消息傳遞來進行狀態修改),因此使用Actors模型進行併發編程能夠很好地避免這些問題。Actor內部是以單線程的模式來執行的,相似於redis,因此Actor徹底能夠實現分佈式鎖相似的應用。服務器

異步

每一個Actor都有一個專用的MailBox來接收消息,這也是Actor實現異步的基礎。當一個Actor實例向另一個Actor發消息的時候,並不是直接調用Actor的方法,而是把消息傳遞到對應的MailBox裏,就好像郵遞員,並非把郵件直接送到收信人手裏,而是放進每家的郵箱,這樣郵遞員就能夠快速的進行下一項工做。因此在Actor系統裏,Actor發送一條消息是很是快的。網絡

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-vWwNZ5t0-1570964270753)(https://timgsa.baidu.com/timg...]多線程

這樣的設計主要優點就是解耦了Actor,數萬個Actor併發的運行,每一個actor都以本身的步調運行,且發送消息,接收消息都不會被阻塞。架構

隔離

每一個Actor的實例都維護這本身的狀態,與其餘Actor實例處於物理隔離狀態,並不是像 多線程+鎖 模式那樣基於共享數據。Actor經過消息的模式與其餘Actor進行通訊,與OO式的消息傳遞方式不一樣,Actor之間消息的傳遞是真正物理上的消息傳遞。併發

天生分佈式

每一個Actor實例的位置透明,不管Actor地址是在本地仍是在遠程機器上對於代碼來講都是同樣的。每一個Actor的實例很是小,最多幾百字節,因此單機幾十萬的Actor的實例很輕鬆。若是你寫過golang代碼,就會發現其實Actor在重量級上很像Goroutine。因爲位置透明性,因此Actor系統能夠隨意的橫向擴展來應對併發,對於調用者來講,調用的Actor的位置就在本地,固然這也得益於Actor系統強大的路由系統。
image

生命週期

每一個Actor實例都有本身的生命週期,就像C# java 中的GC機制同樣,對於須要淘汰的Actor,系統會銷燬而後釋放內存等資源來保證系統的持續性。其實在Actor系統中,Actor的銷燬徹底能夠手動干預,或者作到系統自動化銷燬。

容錯

說到Actor的容錯,不得不說仍是挺使人意外的。傳統的編程方式都是在未來可能出現異常的地方去捕獲異常來保證系統的穩定性,這就是所謂的防護式編程。可是防護式編程也有本身的缺點,相似於現實,防護的一方永遠不能100%的防護住全部未來可能出現代碼缺陷的地方。好比在java代碼中不少地方充斥着判斷變量是否爲nil,這些就屬於防護式編碼最典型的案例。可是Actor模型的程序並不進行防護式編程,而是遵循「任其崩潰」的哲學,讓Actor的管理者們來處理這些崩潰問題。好比一個Actor崩潰以後,管理者能夠選擇建立新的實例或者記錄日誌。每一個Actor的崩潰或者異常信息均可以反饋到管理者那裏,這就保證了Actor系統在管理每一個Actor實例的靈活性。

劣勢

天下無完美的語言,框架/模型亦是如此。Actor做爲分佈式下併發模型的一種,也有其劣勢。

  1. 因爲同一類型的Actor對象是分散在多個宿主之中,因此取多個Actor的集合是個軟肋。好比在電商系統中,商品做爲一類Actor,查詢一個商品的列表在多數狀況下通過如下過程:首先根據查詢條件篩選出一系列商品id,根據商品id分別取商品Actor列表(極可能會產生一個商品搜索的服務,不管是用es或者其餘搜索引擎)。若是量很是大的話,有產生網絡風暴的危險(雖然概率很是小)。在實時性要求不是過高的狀況下,其實也能夠獨立出來商品Actor的列表,利用MQ接收商品信息修改的信號來處理數據一致性的問題。
  2. 在不少狀況下基於Actor模型的分佈式系統,緩存頗有多是進程內緩存,也就是說每一個Actor其實都在進程內保存了本身的狀態信息,業內一般把這種服務成爲有狀態服務。可是每一個Actor又有本身的生命週期,會產生問題嗎?呵呵,也許吧。想一想一下,仍是拿商品做爲例子, 若是環境是非Actor併發模型,商品的緩存能夠利用LRU策略來淘汰非活躍的商品緩存,來保證內存不會使用過量,若是是基於Actor模型的進程內緩存呢,每一個actor其實就是緩存自己,就不那麼容易利用LRU策略來保證內存使用量了,由於Actor的活躍狀態對於你來講是未知的。
  3. 分佈式事物問題,其實這是全部分佈式模型都面臨的問題,非因爲Actor而存在。仍是以商品Actor爲例,添加一個商品的時候,商品Actor和統計商品的Actor(不少狀況下確實被設計爲兩類Actor服務)須要保證事物的完整性,數據的一致性。在不少的狀況下能夠犧牲實時一致性用最終一致性來保證。
  4. 每一個Actor的mailBox有可能會出現堆積或者滿的狀況,當這種狀況發生,新消息的處理方式是被拋棄仍是等待呢,因此當設計一個Actor系統的時候mailBox的設計須要注意。

昇華一下

  1. 經過以上介紹,既然Actor對於位置是透明的,任何Actor對於其餘Actor就好像在本地同樣。基於這個特性咱們能夠作不少事情了,之前傳統的分佈式系統,A服務器若是想和B服務器通訊,要麼RPC的調用(http調用不太經常使用),要麼經過MQ系統。可是在Actor系統中,服務器之間的通訊都變的很優雅了,雖然本質上也屬於RPC調用,可是對於編碼者來講就好像在調用本地函數同樣。其實如今比較時興的是Streaming方式。
  2. 因爲Actor系統的執行模型是單線程,而且異步,因此凡有資源競爭的相似功能都很是適合Actor模型,好比秒殺活動。
  3. 基於以上的介紹,Actor模型在設計層面天生就支持了負載均衡,並且對於水平擴容支持的很是好。固然Actor的分佈式系統也是須要服務註冊中心的。
  4. 雖然Actor是單線程執行模型,並不意味着每一個Actor都須要佔用一個線程,其實Actor上執行的任務就像Golang的goroutine同樣,徹底能夠是一個輕量級的東西,並且一個宿主上全部的Actor能夠共享一個線程池,這就保證了在使用最少線程資源的狀況下,最大量化業務代碼。

搜索公衆號:架構師修行之路,領取福利,獲取更多精彩內容
相關文章
相關標籤/搜索