程序員修神之路–有狀態的服務其實可以做更多的事情

菜菜哥,你換形象啦?


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


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


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


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


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


簡介

對於初學者,心裏對“有狀態服務”的理解可能比較模糊,但是從面向對象編程思想的角度去理解也許會明朗很多。面向對象編程思想提倡的是用編程語言去描述世間萬物,所以面向對象編程的語言都會提供描述對象的容器以及對象行為的表達方式。舉一個很簡單的栗子,在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:

1. 首先我們定義玩家的狀態信息

//玩家的信息,其實也就是玩家的狀態信息
    public class Player {
        /// <summary>
        /// 玩家id,同時也是玩家這個服務的主鍵
        /// </summary>
        public long Id { getset; }
        /// <summary>
        /// 玩家姓名
        /// </summary>
        public string Name { getset; }
        /// <summary>
        /// 玩家等級
        /// </summary>
        public int Level { getset; }
    }

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

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

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

public class PlayerService : GrainIPlayerService
    {
        //這裏可以用玩家的信息來代表玩家的狀態信息,而且這個狀態信息又充當了進程內緩存的作用
        Player playerInfo;
        public async Task<intGetLevel()
        
{
            return (await LoadPlayer()).Level;
        }

        public async Task<stringGetName()
        
{
            return (await LoadPlayer()).Name;
        }

        public async Task<intSetLevel(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;
        }
    }

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

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包”嚨底家”

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

小三通海運與一般國際貿易有何不同?

小三通快遞通關作業有哪些?