緩存與數據庫一致性保證

原創 2016-03-16 58沈劍 架構師之路web

本文主要討論這麼幾個問題:sql

(1)啥時候數據庫和緩存中的數據會不一致數據庫

(2)不一致優化思路編程

(3)如何保證數據庫與緩存的一致性後端

 

1、需求緣起

上一篇《緩存架構設計細節二三事》(點擊查看)引發了普遍的討論,其中有一個結論:當數據發生變化時,「先淘汰緩存,再修改數據庫」這個點是你們討論的最多的。緩存

 

上篇文章得出這個結論的依據是,因爲操做緩存與操做數據庫不是原子的,很是有可能出現執行失敗。tomcat


假設先寫數據庫,再淘汰緩存:第一步寫數據庫操做成功,第二步淘汰緩存失敗,則會出現DB中是新數據,Cache中是舊數據,數據不一致【如上圖:db中是新數據,cache中是舊數據】。服務器

 


假設先淘汰緩存,再寫數據庫:第一步淘汰緩存成功,第二步寫數據庫失敗,則只會引起一次Cache miss【如上圖:cache中無數據,db中是舊數據】。微信

 

結論:先淘汰緩存,再寫數據庫。網絡

 

引起你們熱烈討論的點是「先操做緩存,在寫數據庫成功以前,若是有讀請求發生,可能致使舊數據入緩存,引起數據不一致」,這就是本文要討論的主題。

 

2、爲何數據會不一致

回顧一下上一篇文章中對緩存、數據庫進行讀寫操做的流程。

寫流程:

(1)先淘汰cache

(2)再寫db

讀流程:

(1)先讀cache,若是數據命中hit則返回

(2)若是數據未命中miss則讀db

(3)將db中讀取出來的數據入緩存

 

什麼狀況下可能出現緩存和數據庫中數據不一致呢?


在分佈式環境下,數據的讀寫都是併發的,上游有多個應用,經過一個服務的多個部署(爲了保證可用性,必定是部署多份的),對同一個數據進行讀寫,在數據庫層面併發的讀寫並不能保證完成順序,也就是說後發出的讀請求極可能先完成(讀出髒數據):

(a)發生了寫請求A,A的第一步淘汰了cache(如上圖中的1)

(b)A的第二步寫數據庫,發出修改請求(如上圖中的2)

(c)發生了讀請求B,B的第一步讀取cache,發現cache中是空的(如上圖中的步驟3)

(d)B的第二步讀取數據庫,發出讀取請求,此時A的第二步寫數據還沒完成,讀出了一個髒數據放入cache(如上圖中的步驟4)

即在數據庫層面,後發出的請求4比先發出的請求2先完成了,讀出了髒數據,髒數據又入了緩存,緩存與數據庫中的數據不一致出現了

 

3、不一致優化思路

可否作到先發出的請求必定先執行完成呢?常見的思路是「串行化」,今天將和你們一塊兒探討「串行化」這個點。

先一塊兒細看一下,在一個服務中,併發的多個讀寫SQL通常是怎麼執行的


上圖是一個service服務的上下游及服務內部詳細展開,細節以下:

(1)service的上游是多個業務應用,上游發起請求對同一個數據併發的進行讀寫操做,上例中併發進行了一個uid=1的餘額修改(寫)操做與uid=1的餘額查詢(讀)操做

(2)service的下游是數據庫DB,假設只讀寫一個DB

(3)中間是服務層service,它又分爲了這麼幾個部分

(3.1)最上層是任務隊列

(3.2)中間是工做線程,每一個工做線程完成實際的工做任務,典型的工做任務是經過數據庫鏈接池讀寫數據庫

(3.3)最下層是數據庫鏈接池,全部的SQL語句都是經過數據庫鏈接池發往數據庫去執行的

 

工做線程的典型工做流是這樣的:

void work_thread_routine(){

Task t = TaskQueue.pop(); // 獲取任務

// 任務邏輯處理,生成sql語句

DBConnection c = CPool.GetDBConnection(); // 從DB鏈接池獲取一個DB鏈接

c.execSQL(sql); // 經過DB鏈接執行sql語句

CPool.PutDBConnection(c); // 將DB鏈接放回DB鏈接池

}

 

提問:任務隊列其實已經作了任務串行化的工做,可否保證任務不併發執行?

答:不行,由於

(1)1個服務有多個工做線程,串行彈出的任務會被並行執行

(2)1個服務有多個數據庫鏈接,每一個工做線程獲取不一樣的數據庫鏈接會在DB層面併發執行

 

提問:假設服務只部署一份,可否保證任務不併發執行?

答:不行,緣由同上

 

提問:假設1個服務只有1條數據庫鏈接,可否保證任務不併發執行?

答:不行,由於

(1)1個服務只有1條數據庫鏈接,只能保證在一個服務器上的請求在數據庫層面是串行執行的

(2)由於服務是分佈式部署的,多個服務上的請求在數據庫層面仍多是併發執行的

 

提問:假設服務只部署一份,且1個服務只有1條鏈接,可否保證任務不併發執行?

答:能夠,全局來看請求是串行執行的,吞吐量很低,而且服務沒法保證可用性

 

完了,看似無望了,

1)任務隊列不能保證串行化

2)單服務多數據庫鏈接不能保證串行化

3)多服務單數據庫鏈接不能保證串行化

4)單服務單數據庫鏈接可能保證串行化,但吞吐量級低,且不能保證服務的可用性,幾乎不可行,那是否還有解?

 

退一步想,其實不須要讓全局的請求串行化,而只須要「讓同一個數據的訪問能串行化」就行。

在一個服務內,如何作到「讓同一個數據的訪問串行化」,只須要「讓同一個數據的訪問經過同一條DB鏈接執行」就行。

如何作到「讓同一個數據的訪問經過同一條DB鏈接執行」,只須要「在DB鏈接池層面稍微修改,按數據取鏈接便可」

獲取DB鏈接的CPool.GetDBConnection()【返回任何一個可用DB鏈接】改成

CPool.GetDBConnection(longid)【返回id取模相關聯的DB鏈接】

 

這個修改的好處是:

(1)簡單,只須要修改DB鏈接池實現,以及DB鏈接獲取處

(2)鏈接池的修改不須要關注業務,傳入的id是什麼含義鏈接池不關注,直接按照id取模返回DB鏈接便可

(3)能夠適用多種業務場景,取用戶數據業務傳入user-id取鏈接,取訂單數據業務傳入order-id取鏈接便可

這樣的話,就可以保證同一個數據例如uid在數據庫層面的執行必定是串行的

 

稍等稍等,服務但是部署了不少份的,上述方案只能保證同一個數據在一個服務上的訪問,在DB層面的執行是串行化的,實際上服務是分佈式部署的,在全局範圍內的訪問還是並行的,怎麼解決呢?能不能作到同一個數據的訪問必定落到同一個服務呢?

 

4、可否作到同一個數據的訪問落在同一個服務上?

上面分析了服務層service的上下游及內部結構,再一塊兒看一下應用層上下游及內部結構


上圖是一個業務應用的上下游及服務內部詳細展開,細節以下:

(1)業務應用的上游不肯定是啥,多是直接是http請求,可能也是一個服務的上游調用

(2)業務應用的下游是多個服務service

(3)中間是業務應用,它又分爲了這麼幾個部分

(3.1)最上層是任務隊列【或許web-server例如tomcat幫你幹了這個事情了】

(3.2)中間是工做線程【或許web-server的工做線程或者cgi工做線程幫你幹了線程分派這個事情了】,每一個工做線程完成實際的業務任務,典型的工做任務是經過服務鏈接池進行RPC調用

(3.3)最下層是服務鏈接池,全部的RPC調用都是經過服務鏈接池往下游服務去發包執行的

 

工做線程的典型工做流是這樣的:

voidwork_thread_routine(){

Task t = TaskQueue.pop(); // 獲取任務

// 任務邏輯處理,組成一個網絡包packet,調用下游RPC接口

ServiceConnection c = CPool.GetServiceConnection(); // 從Service鏈接池獲取一個Service鏈接

c.Send(packet); // 經過Service鏈接發送報文執行RPC請求

CPool.PutServiceConnection(c); // 將Service鏈接放回Service鏈接池

}

 

似曾相識吧?沒錯,只要對服務鏈接池進行少許改動:

獲取Service鏈接的CPool.GetServiceConnection()【返回任何一個可用Service鏈接】改成

CPool.GetServiceConnection(longid)【返回id取模相關聯的Service鏈接】

這樣的話,就可以保證同一個數據例如uid的請求落到同一個服務Service上。

                                                                                  

5、總結

因爲數據庫層面的讀寫併發,引起的數據庫與緩存數據不一致的問題(本質是後發生的讀請求先返回了),可能經過兩個小的改動解決:

(1)修改服務Service鏈接池,id取模選取服務鏈接,可以保證同一個數據的讀寫都落在同一個後端服務上

(2)修改數據庫DB鏈接池,id取模選取DB鏈接,可以保證同一個數據的讀寫在數據庫層面是串行的

 

6、遺留問題

提問:取模訪問服務是否會影響服務的可用性?

答:不會,當有下游服務掛掉的時候,服務鏈接池可以檢測到鏈接的可用性,取模時要把不可用的服務鏈接排除掉。

 

提問:取模訪問服務取模訪問DB,是否會影響各鏈接上請求的負載均衡?

答:不會,只要數據訪問id是均衡的,從全局來看,由id取模獲取各鏈接的機率也是均等的,即負載是均衡的。

 

提問:要是數據庫的架構作了主從同步,讀寫分離:寫請求寫主庫,讀請求讀從庫也有可能致使緩存中進入髒數據呀,這種狀況怎麼解決呢(讀寫請求根本不落在同一個DB上,而且讀寫DB有同步時延)?

答:下一篇文章和你們分享。

 

若是你有烘培、攝影、繪畫、音樂、舞蹈、健身、瑜伽、早教、編程培訓、交友的需求或者技能,長按二維碼關注「美好到家」,點擊「達人報名」,成爲達人,賺取外快。(很差意思,答應了小馬哥幫他宣傳的,大夥幫忙關注下,幫忙完成KPI哈)

==【完】==

回【58】58怎麼玩數據庫架構

回【微信】微信爲啥這麼省流量

回【秒殺】秒殺系統架構優化思路

回【百度】百度咋作長文本去重(一分鐘系列)

回【id】細聊分佈式ID生成方法

回【冗餘】細聊冗餘表數據一致性

回【招聘】入職58到家

回【緩存】緩存架構設計細節二三事

 

【小遊戲:回大於10的整數,隨機返回好文,試試看喲】

 

歡迎討論,有問必回。

轉發,只需3秒。

相關文章
相關標籤/搜索