淺聊讀寫分離


 一、前言

    最近工作很繁忙,同事的離職給我帶來了很多的事情,投身於博客的時間比較少,另外在宿舍住可能部分的時間要隨大流,鶴立雞群有一些不好,當然這也是給自己找借口和理由,趁着周末整理下最近的感悟;另外公司用的ElasticSearch,最近我也在探索,微服務方面暫時擱淺,待到搬出宿舍的時候在開始一波666的操作;另外隨着數據量增加自己還需要去接觸波大數據東西,不得說真是有些挑戰和機遇,看自己如何把握了;再送給自己一句話:少找一些無用借口和理由,擼起袖子就是干!

   開始今天主題讀寫分離;

二、為啥要用讀寫分離

   首先明白什么是讀寫分離,讀寫分離基本原理就是將數據庫的讀寫操作分配到不同的節點上,如下圖:

   

   讀寫分離就是主從集群,一主多從或者一主一叢都是可以的,就是數據庫主機復制寫入操作,從機負責讀的操作,主機寫入以后再同步給從機;這里我簡單說下當時我接觸的一個項目的場景,當時我們是做了一個類似於釘釘發送內部消息用的一個東西,大概用戶在30W左右,但是實際肯定不可能一起上線的,但是每次發總歸會有一波大批量讀取的操作和批量寫入的操作,這個時候我們發現一台單機已經不夠用了,那時候我們數據庫和業務已經做過分離,我們就考慮用到了主從復制這一手段,在這個的基礎上我們已經按照地區分表的操作,有機會我們也可以談談當時是怎么實現的;那段時間進步還是蠻大的,這個項目我也是花了很多心思,但是由於公司內部和很多很多原因,我看不到希望,這個項目發展以及大家迎合上層內部的原因我還是離開了,到了現在的公司,最近我把當時我的另外的想法簡單進行實現,還有當時我們怎么實現的思路談談;

三、代碼層去聊聊實現的思路

   明確一點,那就是我們讀的時候用從庫,寫的時候用主庫,這一點必須要聲明的,這個是我們的需求,根據這個需求我們去演化我們的代碼,無非就是用兩個或者多個連接字符串的,這個是C#的名詞,Java的小伙伴可能不是很清楚,也就是配置2個數據DataSource,這篇文章代碼層的實現主要是針對C#的,Java的話我感覺可能這種文章遍地都是,C#相對還是比較少吧,我也稍微做點貢獻嘛,一下偏題了,這里在說下字符串的內容,一個是主連接字符串,另外是一個或者多個從的字符配置,我們要實現讀寫分離就是去實現讀取的時候用從庫的連接字符串,寫入的時候用主庫的連接字符串,說到這里我想大家應該很明確自己的思路,接下來看看我的2種思路和你的想法一樣不一樣,或許能給你帶來一些思想上的突破;

  1.寫兩份ORM的操作

   這個怎么講的,根據我們上面的操作來明確下兩份ORM的操作指的是什么,一份是讀的ORM操作,另外一份是寫的ORM操作,這個時候我想你應該明白怎么去做,那就是Get操作的方法在DB層去調用讀的ORM,POST的操作去調用寫操作的ORM,這個我們當時就是用這種方式去實現的,我用EF還是相對比較少的,我喜歡用傑哥的Sqlsugar或者Dapper,這里我推薦下我傑哥的博客:http://www.codeisbug.com/Doc/8;這里在推薦下EF的實現,感覺這個作者也是比較用心的,https://blog.csdn.net/slowlifes/article/details/72874582,上面我們只是說進行一主一從,一組多從我們如何去實現,這個地方我們采用的類似於EF作者隨機操作方式,具體大家可以去實現下,應該不是很難,我主要想要介紹我另外的想法;

  2.采用AOP的方式去實現

  上面的方式我感覺還是過於手動化操作,我感覺還是不是很滿意,我在想有什么自動化的方式沒有,這里我想到用AOP去實現,為什么能實現?AOP在MVC或者WebApi種的體現主要在於過濾器,這里面可以拿到請求的上下問,根據這個我們能拿到這個方法是GET還是POST請求,這個根據這個我們就能去區分這個請求是要定位到主庫還是從庫,但是這樣還會存在一個問題,那就定位到以后我們必須在每個方法傳入這個字符串的參數,這個代碼耦合度太過於高了,不是我想要的效果,這個時候我想到ThreadLoacl這個類,這個類能干什,官方的話是提供數據的線程本地存儲,這個可能你不是很明白,我用比較通俗的話給大家解釋下,但是能不是很嚴謹,就是說每個線程id只能對應自己的變量的操作,不會影響其他變量,這個時候就解決多個用戶訪問的時候,正確的將這個字符串變量傳遞給DB層,保證了變量不會在多線程的情況下發生錯誤,另外也對各層之間做了解耦,這個時候我們解決了一組一叢的問題,那對於一主多從的問題我們怎么平均去分配到從庫上,我采用的取余的操作,這個時候還是要解決一個問題,多線程時候的變量共享問題和原子性的問題,這個時候我采用了Interlocked對全局的變量進行新增,然后取余,就這樣就能平均分配到每個從庫上面了,簡單的把代碼貼一下,后續可以在加一些東西上github上;這個上面存在一個問題POST請求不一定是寫入請求,這個地方大家可以根據自己情況自己來判斷,因為在過濾器中還可以獲取到Action和Controller名字,具體怎么組合看大家;

    public class MasterSlaveThreadLocal
    {
        //private static volatile int slaveCount = 0;
        private static  int slaveCount = 0;

        private static ThreadLocal<string> threadLocal = new ThreadLocal<string>();

        private static string masterConnection = ConfigurationManager.ConnectionStrings["MasterConnection"].ToString();

        private static string slaveConnection0 = ConfigurationManager.ConnectionStrings["SlaveConnection0"].ToString();

        private static string slaveConnection1 = ConfigurationManager.ConnectionStrings["SlaveConnection1"].ToString();

        private static string slaveConnection2 = ConfigurationManager.ConnectionStrings["SlaveConnection2"].ToString();
        public static void setDataSourceKey(string httpType)
        {
            if (httpType == "GET")
            {
                Interlocked.Add(ref slaveCount, 1);
                if (slaveCount % 3 == 0)
                {
                    threadLocal.Value = slaveConnection0;
                }
                else if (slaveCount % 3 == 1)
                {
                    threadLocal.Value = slaveConnection1;
                }
                else
                {
                    threadLocal.Value = slaveConnection2;
                }
            }
            if(httpType=="POST")
                threadLocal.Value = masterConnection;
        }

        public static string getDataSourceKey()
        {
            return threadLocal.Value;
        }
    }

    /// <summary>
    /// 主從過濾器
    /// 設置為全局過濾器
    /// </summary>
    public class MasterSlaveFilterAttribute: ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            base.OnActionExecuting(filterContext);
            MasterSlaveThreadLocal.setDataSourceKey(filterContext.HttpContext.Request.HttpMethod);
        }
    }

    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new MasterSlaveFilterAttribute());
        }
    }
View Code

四、讀寫分離帶來的問題以及我自己的一些思考

  讀寫分離以后,我們數據庫在同步數據的時候,會存在延遲這個問題,這個延遲是數據量的增加延遲也會越來越久,但是有些業務是要求實時,那我們怎么去處理這個問題,還是舉個比較例子,就按照登錄來說,假設在某網站注冊一個賬號,該網站祖冊這個業務采用的是讀寫分離的設計,這個時候我們在注冊完成以后,需要立即登錄,比如說這個延遲在2-3之間,這個時候我們做登錄的時候會訪問不到該注冊信息,這個時候會提示該用戶不存在,那么我們怎么處理這種問題,接下來我談談我的處理方式,當然我還是感覺這種問題還是根據業務來,如果業務沒有要求必須實時,那我是提倡忽略這種問題,要是業務必須處理我認為可以從兩方面處理:

  1.數據庫層面

    數據庫層面可以采用暴力讀取也就是在讀取一次主機的辦法,什么是再讀一次主機,比如說我們登錄操作,如果從庫讀取不到,我們再次讀取下主機看該條數據是否存在,這樣主機壓力很大,可能導致主機奔潰;

    另外還有一個比較推薦的一個方式,這個方式我沒有嘗試過,也是我在別的地方學習到的,但是本質還是讀不到讀主機,我們可以這樣操作,我們記錄一個K-V緩存,key是由數據庫:表:主鍵,設置過期時間大於等於數據庫同步延遲的時間,當我們訪問判斷下有無這個key,如果存在這個則讀取主庫,否者讀取從庫;

  2.從界面層面

   還是以登錄為案例,我們在很多網站注冊完成以后,會出現這樣情況,比等待幾秒跳轉或者閱讀下什么規則,這個時候我們考慮下為什么他要這么做,當讓一部分可能真的是規定,但是我們腦袋大開一下,其實這也是一種數據同步,在等待的那段時間,主從已經完成了同步,這也是我腦洞大開想的,這個大家可以當做看笑話,但是我認為還是可以嘗試下;

五、結束

  有什么不懂的我們可以一起探討!!GO!

  歡迎大家加我群:438836709;

  歡迎大家關注我公眾號:

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM