快取與資料庫一致性保證

2021-07-15 11:01:41 字數 4027 閱讀 6221

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

(1)啥時候資料庫和快取中的資料會不一致

(2)不一致優化思路

(3)如何保證資料庫與快取的一致性

當資料發生變化時,「先淘汰快取,再修改資料庫」這個點是大家討論的最多的。

上篇文章得出這個結論的依據是,由於操作快取與運算元據庫不是原子的,非常有可能出現執行失敗。

假設先寫資料庫,再淘汰快取:第一步寫資料庫操作成功,第二步淘汰快取失敗,則會出現

db中是新資料,

cache

中是舊資料,資料不一致【如上圖:

db中是新資料,

cache

中是舊資料】。

假設先淘汰快取,再寫資料庫:第一步淘汰快取成功,第二步寫資料庫失敗,則只會引發一次

cache miss

【如上圖:

cache

中無資料,

db中是舊資料】。

結論:先淘汰快取,再寫資料庫。

引發大家熱烈討論的點是「先操作快取,在寫資料庫成功之前,如果有讀請求發生,可能導致舊資料入快取,引發資料不一致」,這就是本文要討論的主題。

寫流程:

(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先完成了

,讀出了髒資料,髒資料又入了快取,快取與資料庫中的資料不一致出現了

能否做到先發出的請求一定先執行完成呢?

常見的思路是「序列化」,今天將和大家一起**「序列化」這個點。

先一起細看一下,在乙個服務中,併發的多個讀寫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()

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

答:不行,因為

(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層面的執行是序列化的,實際上服務是分布式部署的,在全域性範圍內的訪問仍是並行的,怎麼解決呢?能不能做到同乙個資料的訪問一定落到同乙個服務呢?

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

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

(1)業務應用的上游不確定是啥,可能是直接是http請求,可能也是乙個服務的上游呼叫

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

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

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

(3.2)中間是工作執行緒【或許web-server的工作執行緒或者cgi工作執行緒幫你幹了執行緒分派這個事情了】,每個工作執行緒完成實際的業務任務,典型的工作任務是通過服務連線池進行

rpc呼叫

(3.3)最下層是服務連線池,所有的rpc呼叫都是通過服務連線池往下游服務去發包執行的

工作執行緒的典型工作流是這樣的:

voidwork_thread_routine()

似曾相識吧?沒錯,只要對服務連線池進行少量改動:

獲取service連線的cpool.getserviceconnection()【返回任何乙個可用service連線】改為

cpool.getserviceconnection(longid)【返回id取模相關聯的service連線】

這樣的話,就能夠保證同乙個資料例如uid的請求落到同乙個服務service上。

由於資料庫層面的讀寫併發,引發的資料庫與快取資料不一致的問題(本質是後發生的讀請求先返回了),可能通過兩個小的改動解決:

(1)修改服務service連線池,id取模選取服務連線,能夠保證同乙個資料的讀寫都落在同乙個後端服務上

(2)修改資料庫db連線池,id取模選取db連線,能夠保證同乙個資料的讀寫在資料庫層面是序列的

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

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

提問:取模訪問服務

與取模訪問db,是否會影響各連線上請求的負載均衡?

答:不會,只要資料訪問id是均衡的,從全域性來看,由id取模獲取各連線的概率也是均等的,即負載是均衡的。

提問:要是資料庫的架構做了主從同步,讀寫分離:寫請求寫主庫,讀請求讀從庫也有可能導致快取中進入髒資料呀,這種情況怎麼解決呢(讀寫請求根本不落在同乙個db上,並且讀寫db有同步時延)?

資料庫快取如何保證一致性

先新資料庫再更新快取。問題 更新資料庫後,更新快取時,如果資料庫資料又變更了,那快取裡就更新成髒資料了。先刪除快取,然後再更新資料庫。刪除快取後,進行更新資料庫時,如果乙個請求來了,它在快取中沒命中,就會去資料庫中查詢,並把查到的資料更新到快取裡,隨後資料庫才更新完畢,這就導致快取裡的資料又成為髒資...

快取與資料庫一致性

此時系統的讀寫流量很小,這個時候所有的讀寫操作都在主庫 此時,從庫的角色只是作為災備。風險分析 從資料一致性的角度來看沒有任何問題,所有讀寫操作都在主庫 隨著業務的前進和流量的激增,會出現大表和資料庫寫入效能下降的問題。我們可以通過分庫的方式,提公升資料庫單機的qps壓下來 通過分表的方式,降低單錶...

如何保證快取與資料庫資料一致性

重點文章 你只要用快取,就可能會涉及到快取與資料庫雙儲存雙寫,你只要是雙寫,就一定會有資料一致性的問題,那麼你如何解決一致性問題?一般來說,就是如果你的系統不是嚴格要求快取 資料庫必須一致性的話,快取可以稍微的跟資料庫偶爾有不一致的情況,最好不要做這個方案,讀請求和寫請求序列化,串到乙個記憶體佇列裡...