菜菜哥,你換形象啦?

這麼巧,你也換啦!聽說是不會畫畫的菜嫂經過九牛二虎之力的功勞哦!鼓掌......

前幾天我出去面試了,面試官問我微服務的知識,我回答的可好了

看來微服務你真的下功夫研究了呀

是呀是呀,但是碰到一個問題,有狀態的服務是什麼意思呢?

看來你又掛在這個問題上了,且聽這次分解

簡介

對於初學者,心裡對“有狀態服務”的理解可能比較模糊,但是從面向物件程式設計思想的角度去理解也許會明朗很多。面向物件程式設計思想提倡的是用程式語言去描述世間萬物,所以面向物件程式設計的語言都會提供描述物件的容器以及物件行為的表達方式。舉一個很簡單的栗子,在c#或者java中,表達物件的容器就是class,物件的行為通過一系列的介面或者函式來表達。更進一步,物件抽象出來之後,大多數物件都有自己的內部狀態,體現到程式碼上也就是常見的類的屬性。

面向物件程式設計的基本思想本質上是對現實世界的一種抽象,萬物皆可抽象。

根據業務把物件抽象出來之後,每一個例項化的物件其實都可以有自己的狀態,比如:在最常見的遊戲場景中,每一個玩家都是“玩家"這類物件的一個例項,每一個玩家都有自己的名字,性別,等級,HP等屬性,這些屬性本質上就是玩家的狀態,隨著時間的推移,每個玩家的HP,等級等屬性會隨之變化,這些變化其實就是這個玩家狀態的變化。對應到有狀態的服務也是如此,之所以稱之為有狀態,是因為服務內部的物件狀態會隨著業務有著對應的變動,而這些變動只發生在這個服務內部,在外界看來,這個服務好像是有狀態的。

有狀態的服務本質上是一些有狀態物件的集合,這些物件狀態的變化只發生在當前服務程序中。

優勢和劣勢

有狀態服務之所以被稱為有狀態,一個很大的原因是它可以追溯狀態的變化過程,也就是說一個有狀態的服務儲存著狀態變化的記錄,並可以根據這些歷史記錄恢復到指定的狀態,這在很多場景下非常有用。舉一個很簡單的栗子:我們平時玩的鬥地主遊戲,三個玩家,當有一個玩家因為網路原因掉線,經過一段時間,這個玩家又重新上線,需要根據某些記錄來恢復玩家掉線期間系統自動出牌的記錄,這些出牌記錄在這個業務中其實就是這個玩家的狀態變化記錄。在有狀態的服務中,很容易做到這一點。

其實實際開發中很多場景不需要記錄每個狀態的變化,只保留最新狀態即可,不單單是因為儲存每個狀態的變化需要大量的儲存和架構設計,更因為是很多業務根本不需要這些狀態變化記錄,業務需要的只是最新的狀態,所以大部分有狀態的服務只儲存著最新的狀態。

有狀態的服務在設計難度上比無狀態的服務要大很多,不僅僅是因為開發設計人員需要更好的抽象能力,更多的是一致性的設計問題。現代的分散式系統,都是由多個伺服器組成一個叢集來對外提供服務,當一個物件在伺服器A產生之後,如果請求被分配到了伺服器B上,這種情況下有狀態的服務毫無意義,為什麼呢?當一個相同的業務物件存在於不同的伺服器上的時候,本質上就違背了現實世界的規則,你能說一個人,即出生在中國,又出生在美國嗎? 所以有狀態的服務對於一致性問題有著天然的要求,這種思想和微服務設計理想不謀而合,舉個栗子:一個使用者資訊的服務,對外提供查詢修改能力,凡是使用者資訊的業務必須通過這個服務來實現。同理,一個物件狀態的查詢修改以及這個物件的行為,必須由這個物件的服務來完成。

有狀態的服務要求相同業務物件的請求必須被路由到同一個服務程序。

因此,有狀態的服務對於同一個物件的橫向擴容是做不到的,就算是做的到,多個相同物件之間的狀態同步工作也必然會花費更多的資源。在很多場景下,有狀態的服務要注意熱點問題,例如最常見的秒殺,這裡並非是說有狀態服務不適合大併發的場景,反而在高併發的場景下,有狀態的服務往往表現的比無狀態服務更加出色。

Actor模型



在眾多的併發模型中,最適合有狀態服務設計的莫過於Actor模型了,如果你對actor模型還不熟悉,可以擼一遍菜菜之前的文章:https://mp.weixin.qq.com/s/eEiypRysw5jsC7iYUp_yAg  actor模型天生就具備了一致性這種特點,讓我們在對業務進行抽象的時候,不必考慮一致性的問題,而且每一個請求都是非同步模式,在物件內部修改物件的狀態不必加鎖,這在傳統的架構中是做不到的。

基於actor模型,系統設計的難點在於抽象業務模型,一旦業務模型穩定,我們完全可以用記憶體方式來儲存物件狀態(也可以定時去持久化),記憶體方式比用其他網路儲存(例如redis)要快上幾個量級,菜菜也有一篇文章大家可以去擼一下:https://mp.weixin.qq.com/s/6YL3SnSriKEnpCyB5qkk0g  ,既滿足了一致性,又可以利用程序內物件狀態來應對高併發業務場景,何樂而不為呢?

有不少同學問過我,actor模型要避免出現熱點問題,就算有記憶體狀態為其加速,那併發數還是超過actor的處理能力怎麼辦呢? 其實和傳統做法類似,所有的高併發系統設計無非就是“分”一個字,無論是簡單的負載均衡,還是複雜的分庫分表策略,都是分治的一種體現。一臺伺服器不夠,我就上十臺,百臺.....

所有的高併發系統設計都是基於分治思想,把每一臺伺服器的能力發揮到極致,難度最大的還是其中的排程演算法。

用actor模型來應對高併發,我們可以採用讀寫分離的思想,主actor負責寫請求,並利用某種通訊機制把狀態的變化通知到多個從actor,從actor負責對外的讀請求,這個DB的讀寫分離思想一致,其中最難的當屬actor的狀態同步問題了,解決問題的方式千百種,總有一種適合你,歡迎你留言寫下你認為最好的解決方案。

案例(玩家資訊服務)

由於菜菜是c#出身,對c#的Actor服務框架Orleans比較熟悉,這裡就以Orleans為例,其他語言的coder不要見怪,Orleans是一個非常優秀的Actor模型框架,而且支援最新的netcore 3.0版本,地址為:https://github.com/dotnet/orleans  有興趣的同學可以去看一下,而且分散式事物已經出正式版,非常給力。其他語言的也非常出色java:https://github.com/akka/akka

golang:https://github.com/AsynkronIT/protoactor-go


1. 首先我們定義玩家的狀態資訊

//玩家的資訊,其實也就是玩家的狀態資訊
    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; }
    }

2. 接下來定義玩家的服務介面

 /// <summary>
    /// 玩家的服務介面
    /// </summary>
    interface IPlayerService: Orleans.IGrainWithIntegerKey
    {
        //獲取玩家名稱
        Task<string> GetName();
        //獲取玩家等級
        Task<int> GetLevel();
        //設定玩家等級,這個操作會改變玩家的狀態
        Task<int> SetLevel(int newLevel);
    }

3. 接下來實現玩家服務的介面

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;
        }
    }

以上只是一個簡單案例,有狀態的服務還有更多的設計方案,以上只供參考完

相關文章