對於初學者,內心對「有狀態服務」的理解可能比較模糊,可是從面向對象編程思想的角度去理解也許會明朗不少。面向對象編程思想提倡的是用編程語言去描述世間萬物,因此面向對象編程的語言都會提供描述對象的容器以及對象行爲的表達方式。舉一個很簡單的栗子,在c#或者java中,表達對象的容器就是class,對象的行爲經過一系列的接口或者函數來表達。更進一步,對象抽象出來以後,大多數對象都有本身的內部狀態,體現到代碼上也就是常見的類的屬性。java
面向對象編程的基本思想本質上是對現實世界的一種抽象,萬物皆可抽象。
根據業務把對象抽象出來以後,每個實例化的對象其實均可以有本身的狀態,好比:在最多見的遊戲場景中,每個玩家都是「玩家"這類對象的一個實例,每個玩家都有本身的名字,性別,等級,HP等屬性,這些屬性本質上就是玩家的狀態,隨着時間的推移,每一個玩家的HP,等級等屬性會隨之變化,這些變化其實就是這個玩家狀態的變化。對應到有狀態的服務也是如此,之因此稱之爲有狀態,是由於服務內部的對象狀態會隨着業務有着對應的變更,而這些變更只發生在這個服務內部,在外界看來,這個服務好像是有狀態的。git
有狀態的服務本質上是一些有狀態對象的集合,這些對象狀態的變化只發生在當前服務進程中。
有狀態服務之因此被稱爲有狀態,一個很大的緣由是它能夠追溯狀態的變化過程,也就是說一個有狀態的服務保存着狀態變化的記錄,並能夠根據這些歷史記錄恢復到指定的狀態,這在不少場景下很是有用。舉一個很簡單的栗子:咱們平時玩的鬥地主遊戲,三個玩家,當有一個玩家由於網絡緣由掉線,通過一段時間,這個玩家又從新上線,須要根據某些記錄來恢復玩家掉線期間系統自動出牌的記錄,這些出牌記錄在這個業務中其實就是這個玩家的狀態變化記錄。在有狀態的服務中,很容易作到這一點。github
其實實際開發中不少場景不須要記錄每一個狀態的變化,只保留最新狀態便可,不僅僅是由於保存每一個狀態的變化須要大量的存儲和架構設計,更由於是不少業務根本不須要這些狀態變化記錄,業務須要的只是最新的狀態,因此大部分有狀態的服務只保存着最新的狀態。golang
有狀態的服務在設計難度上比無狀態的服務要大不少,不只僅是由於開發設計人員須要更好的抽象能力,更多的是一致性的設計問題。現代的分佈式系統,都是由多個服務器組成一個集羣來對外提供服務,當一個對象在服務器A產生以後,若是請求被分配到了服務器B上,這種狀況下有狀態的服務毫無心義,爲何呢?當一個相同的業務對象存在於不一樣的服務器上的時候,本質上就違背了現實世界的規則,你能說一我的,即出生在中國,又出生在美國嗎? 因此有狀態的服務對於一致性問題有着自然的要求,這種思想和微服務設計理想不謀而合,舉個栗子:一個用戶信息的服務,對外提供查詢修改能力,凡是用戶信息的業務必須經過這個服務來實現。同理,一個對象狀態的查詢修改以及這個對象的行爲,必須由這個對象的服務來完成。redis
有狀態的服務要求相同業務對象的請求必須被路由到同一個服務進程。
所以,有狀態的服務對於同一個對象的橫向擴容是作不到的,就算是作的到,多個相同對象之間的狀態同步工做也必然會花費更多的資源。在不少場景下,有狀態的服務要注意熱點問題,例如最多見的秒殺,這裏並不是是說有狀態服務不適合大併發的場景,反而在高併發的場景下,有狀態的服務每每表現的比無狀態服務更加出色。算法
在衆多的併發模型中,最適合有狀態服務設計的莫過於Actor模型了,若是你對actor模型還不熟悉,能夠擼一遍菜菜以前的文章:數據庫
actor模型天生就具有了一致性這種特色,讓咱們在對業務進行抽象的時候,沒必要考慮一致性的問題,並且每個請求都是異步模式,在對象內部修改對象的狀態沒必要加鎖,這在傳統的架構中是作不到的。c#
基於actor模型,系統設計的難點在於抽象業務模型,一旦業務模型穩定,咱們徹底能夠用內存方式來保存對象狀態(也能夠定時去持久化),內存方式比用其餘網絡存儲(例如redis)要快上幾個量級,菜菜也有一篇文章你們能夠去擼一下:設計模式
,既知足了一致性,又能夠利用進程內對象狀態來應對高併發業務場景,何樂而不爲呢?
有很多同窗問過我,Actor模型要避免出現熱點問題,就算有內存狀態爲其加速,那併發數仍是超過actor的處理能力怎麼辦呢? 其實和傳統作法相似,全部的高併發系統設計無非就是「分」一個字,不管是簡單的負載均衡,仍是複雜的分庫分表策略,都是分治的一種體現。一臺服務器不夠,我就上十臺,百臺.....
全部的高併發系統設計都是基於分治思想,把每一臺服務器的能力發揮到極致,難度最大的仍是其中的調度算法。
用actor模型來應對高併發,咱們能夠採用讀寫分離的思想,主actor負責寫請求,並利用某種通訊機制把狀態的變化通知到多個從actor,從actor負責對外的讀請求,這個DB的讀寫分離思想一致,其中最難的當屬actor的狀態同步問題了,解決問題的方式千百種,總有一種適合你,歡迎你留言寫下你認爲最好的解決方案。
因爲菜菜是c#出身,對c#的Actor服務框架Orleans比較熟悉,這裏就以Orleans爲例,其餘語言的coder不要見怪,Orleans是一個很是優秀的Actor模型框架,並且支持最新的netcore 3.0版本,地址爲:https://github.com/dotnet/orl... 有興趣的同窗能夠去看一下,並且分佈式事物已經出正式版,很是給力。其餘語言的也很是出色
java:https://github.com/akka/akka
golang:https://github.com/AsynkronIT...
//玩家的信息,其實也就是玩家的狀態信息 public class Player { /// <summary> /// 玩家id,同時也是玩家這個服務的主鍵 /// </summary> public long Id { get; set; } /// <summary> /// 玩家姓名 /// </summary> public string Name { get; set; } /// <summary> /// 玩家等級 /// </summary> public int Level { get; set; } }
/// <summary> /// 玩家的服務接口 /// </summary> interface IPlayerService: Orleans.IGrainWithIntegerKey { //獲取玩家名稱 Task<string> GetName(); //獲取玩家等級 Task<int> GetLevel(); //設置玩家等級,這個操做會改變玩家的狀態 Task<int> SetLevel(int newLevel); }
public class PlayerService : Grain, IPlayerService { //這裏能夠用玩家的信息來表明玩家的狀態信息,並且這個狀態信息又充當了進程內緩存的做用 Player playerInfo; public async Task<int> GetLevel() { return (await LoadPlayer()).Level; } public async Task<string> GetName() { return (await LoadPlayer()).Name; } public async Task<int> SetLevel(int newLevel) { var playerInfo =await LoadPlayer(); if (playerInfo != null) { //先進行數據庫的更新,而後在更新緩存的狀態, 進程內緩存更新失敗的概率幾乎爲0 playerInfo.Level = newLevel; } return 1; } private async Task< Player> LoadPlayer() { if (playerInfo == null) { var id = this.GetPrimaryKeyLong(); //這裏模擬的信息,真實環境徹底能夠從持久化設備進行讀取 playerInfo= new Player() { Id = id, Name = "玩家姓名", Level = 1 }; } return playerInfo; } }
以上只是一個簡單案例,有狀態的服務還有更多的設計方案,以上只供參考
更多精彩文章