W3Cschool
恭喜您成為首批注冊(cè)用戶
獲得88經(jīng)驗(yàn)值獎(jiǎng)勵(lì)
本文主要討論這么幾個(gè)問(wèn)題:
(1)啥時(shí)候數(shù)據(jù)庫(kù)和緩存中的數(shù)據(jù)會(huì)不一致
(2)不一致優(yōu)化思路
(3)如何保證數(shù)據(jù)庫(kù)與緩存的一致性
上一篇《緩存架構(gòu)設(shè)計(jì)細(xì)節(jié)二三事》(點(diǎn)擊查看)引起了廣泛的討論,其中有一個(gè)結(jié)論:當(dāng)數(shù)據(jù)發(fā)生變化時(shí),“先淘汰緩存,再修改數(shù)據(jù)庫(kù)”這個(gè)點(diǎn)是大家討論的最多的。
回顧一下上一篇文章中對(duì)緩存、數(shù)據(jù)庫(kù)進(jìn)行讀寫操作的流程。
寫流程:
(1)先淘汰cache
(2)再寫db
讀流程:
(1)先讀cache,如果數(shù)據(jù)命中hit則返回
(2)如果數(shù)據(jù)未命中miss則讀db
(3)將db中讀取出來(lái)的數(shù)據(jù)入緩存
在分布式環(huán)境下,數(shù)據(jù)的讀寫都是并發(fā)的,上游有多個(gè)應(yīng)用,通過(guò)一個(gè)服務(wù)的多個(gè)部署(為了保證可用性,一定是部署多份的),對(duì)同一個(gè)數(shù)據(jù)進(jìn)行讀寫,在數(shù)據(jù)庫(kù)層面并發(fā)的讀寫并不能保證完成順序,也就是說(shuō)后發(fā)出的讀請(qǐng)求很可能先完成(讀出臟數(shù)據(jù)):
(a)發(fā)生了寫請(qǐng)求A,A的第一步淘汰了cache(如上圖中的1)
(b)A的第二步寫數(shù)據(jù)庫(kù),發(fā)出修改請(qǐng)求(如上圖中的2)
(c)發(fā)生了讀請(qǐng)求B,B的第一步讀取cache,發(fā)現(xiàn)cache中是空的(如上圖中的步驟3)
(d)B的第二步讀取數(shù)據(jù)庫(kù),發(fā)出讀取請(qǐng)求,此時(shí)A的第二步寫數(shù)據(jù)還沒(méi)完成,讀出了一個(gè)臟數(shù)據(jù)放入cache(如上圖中的步驟4)
即在數(shù)據(jù)庫(kù)層面,后發(fā)出的請(qǐng)求4比先發(fā)出的請(qǐng)求2先完成了,讀出了臟數(shù)據(jù),臟數(shù)據(jù)又入了緩存,緩存與數(shù)據(jù)庫(kù)中的數(shù)據(jù)不一致出現(xiàn)了
能否做到先發(fā)出的請(qǐng)求一定先執(zhí)行完成呢?常見(jiàn)的思路是“串行化”,今天將和大家一起探討“串行化”這個(gè)點(diǎn)。
先一起細(xì)看一下,在一個(gè)服務(wù)中,并發(fā)的多個(gè)讀寫SQL一般是怎么執(zhí)行的
上圖是一個(gè)service服務(wù)的上下游及服務(wù)內(nèi)部詳細(xì)展開,細(xì)節(jié)如下:
(1)service的上游是多個(gè)業(yè)務(wù)應(yīng)用,上游發(fā)起請(qǐng)求對(duì)同一個(gè)數(shù)據(jù)并發(fā)的進(jìn)行讀寫操作,上例中并發(fā)進(jìn)行了一個(gè)uid=1的余額修改(寫)操作與uid=1的余額查詢(讀)操作
(2)service的下游是數(shù)據(jù)庫(kù)DB,假設(shè)只讀寫一個(gè)DB
(3)中間是服務(wù)層service,它又分為了這么幾個(gè)部分
(3.1)最上層是任務(wù)隊(duì)列
(3.2)中間是工作線程,每個(gè)工作線程完成實(shí)際的工作任務(wù),典型的工作任務(wù)是通過(guò)數(shù)據(jù)庫(kù)連接池讀寫數(shù)據(jù)庫(kù)
(3.3)最下層是數(shù)據(jù)庫(kù)連接池,所有的SQL語(yǔ)句都是通過(guò)數(shù)據(jù)庫(kù)連接池發(fā)往數(shù)據(jù)庫(kù)去執(zhí)行的
工作線程的典型工作流是這樣的:
void work_thread_routine(){
Task t = TaskQueue.pop(); // 獲取任務(wù)
// 任務(wù)邏輯處理,生成sql語(yǔ)句
DBConnection c = CPool.GetDBConnection(); // 從DB連接池獲取一個(gè)DB連接
c.execSQL(sql); // 通過(guò)DB連接執(zhí)行sql語(yǔ)句
CPool.PutDBConnection(c); // 將DB連接放回DB連接池
}
提問(wèn):任務(wù)隊(duì)列其實(shí)已經(jīng)做了任務(wù)串行化的工作,能否保證任務(wù)不并發(fā)執(zhí)行?
答:不行,因?yàn)?/p>
(1)1個(gè)服務(wù)有多個(gè)工作線程,串行彈出的任務(wù)會(huì)被并行執(zhí)行
(2)1個(gè)服務(wù)有多個(gè)數(shù)據(jù)庫(kù)連接,每個(gè)工作線程獲取不同的數(shù)據(jù)庫(kù)連接會(huì)在DB層面并發(fā)執(zhí)行
提問(wèn):假設(shè)服務(wù)只部署一份,能否保證任務(wù)不并發(fā)執(zhí)行?
答:不行,原因同上
提問(wèn):假設(shè)1個(gè)服務(wù)只有1條數(shù)據(jù)庫(kù)連接,能否保證任務(wù)不并發(fā)執(zhí)行?
答:不行,因?yàn)?/p>
(1)1個(gè)服務(wù)只有1條數(shù)據(jù)庫(kù)連接,只能保證在一個(gè)服務(wù)器上的請(qǐng)求在數(shù)據(jù)庫(kù)層面是串行執(zhí)行的
(2)因?yàn)榉?wù)是分布式部署的,多個(gè)服務(wù)上的請(qǐng)求在數(shù)據(jù)庫(kù)層面仍可能是并發(fā)執(zhí)行的
提問(wèn):假設(shè)服務(wù)只部署一份,且1個(gè)服務(wù)只有1條連接,能否保證任務(wù)不并發(fā)執(zhí)行?
答:可以,全局來(lái)看請(qǐng)求是串行執(zhí)行的,吞吐量很低,并且服務(wù)無(wú)法保證可用性
完了,看似無(wú)望了,
1)任務(wù)隊(duì)列不能保證串行化
2)單服務(wù)多數(shù)據(jù)庫(kù)連接不能保證串行化
3)多服務(wù)單數(shù)據(jù)庫(kù)連接不能保證串行化
4)單服務(wù)單數(shù)據(jù)庫(kù)連接可能保證串行化,但吞吐量級(jí)低,且不能保證服務(wù)的可用性,幾乎不可行,那是否還有解?
退一步想,其實(shí)不需要讓全局的請(qǐng)求串行化,而只需要“讓同一個(gè)數(shù)據(jù)的訪問(wèn)能串行化”就行。
在一個(gè)服務(wù)內(nèi),如何做到“讓同一個(gè)數(shù)據(jù)的訪問(wèn)串行化”,只需要“讓同一個(gè)數(shù)據(jù)的訪問(wèn)通過(guò)同一條DB連接執(zhí)行”就行。
如何做到“讓同一個(gè)數(shù)據(jù)的訪問(wèn)通過(guò)同一條DB連接執(zhí)行”,只需要“在DB連接池層面稍微修改,按數(shù)據(jù)取連接即可”
獲取DB連接的CPool.GetDBConnection()【返回任何一個(gè)可用DB連接】改為
CPool.GetDBConnection(longid)【返回id取模相關(guān)聯(lián)的DB連接】
這個(gè)修改的好處是:
(1)簡(jiǎn)單,只需要修改DB連接池實(shí)現(xiàn),以及DB連接獲取處
(2)連接池的修改不需要關(guān)注業(yè)務(wù),傳入的id是什么含義連接池不關(guān)注,直接按照id取模返回DB連接即可
(3)可以適用多種業(yè)務(wù)場(chǎng)景,取用戶數(shù)據(jù)業(yè)務(wù)傳入user-id取連接,取訂單數(shù)據(jù)業(yè)務(wù)傳入order-id取連接即可
這樣的話,就能夠保證同一個(gè)數(shù)據(jù)例如uid在數(shù)據(jù)庫(kù)層面的執(zhí)行一定是串行的
上面分析了服務(wù)層service的上下游及內(nèi)部結(jié)構(gòu),再一起看一下應(yīng)用層上下游及內(nèi)部結(jié)構(gòu)
上圖是一個(gè)業(yè)務(wù)應(yīng)用的上下游及服務(wù)內(nèi)部詳細(xì)展開,細(xì)節(jié)如下:
(1)業(yè)務(wù)應(yīng)用的上游不確定是啥,可能是直接是http請(qǐng)求,可能也是一個(gè)服務(wù)的上游調(diào)用
(2)業(yè)務(wù)應(yīng)用的下游是多個(gè)服務(wù)service
(3)中間是業(yè)務(wù)應(yīng)用,它又分為了這么幾個(gè)部分
(3.1)最上層是任務(wù)隊(duì)列【或許web-server例如tomcat幫你干了這個(gè)事情了】
(3.2)中間是工作線程【或許web-server的工作線程或者cgi工作線程幫你干了線程分派這個(gè)事情了】,每個(gè)工作線程完成實(shí)際的業(yè)務(wù)任務(wù),典型的工作任務(wù)是通過(guò)服務(wù)連接池進(jìn)行RPC調(diào)用
(3.3)最下層是服務(wù)連接池,所有的RPC調(diào)用都是通過(guò)服務(wù)連接池往下游服務(wù)去發(fā)包執(zhí)行的
工作線程的典型工作流是這樣的:
voidwork_thread_routine(){
Task t = TaskQueue.pop(); // 獲取任務(wù)
// 任務(wù)邏輯處理,組成一個(gè)網(wǎng)絡(luò)包packet,調(diào)用下游RPC接口
ServiceConnection c = CPool.GetServiceConnection(); // 從Service連接池獲取一個(gè)Service連接
c.Send(packet); // 通過(guò)Service連接發(fā)送報(bào)文執(zhí)行RPC請(qǐng)求
CPool.PutServiceConnection(c); // 將Service連接放回Service連接池
}
似曾相識(shí)吧?沒(méi)錯(cuò),只要對(duì)服務(wù)連接池進(jìn)行少量改動(dòng):
獲取Service連接的CPool.GetServiceConnection()【返回任何一個(gè)可用Service連接】改為
CPool.GetServiceConnection(longid)【返回id取模相關(guān)聯(lián)的Service連接】
這樣的話,就能夠保證同一個(gè)數(shù)據(jù)例如uid的請(qǐng)求落到同一個(gè)服務(wù)Service上。
由于數(shù)據(jù)庫(kù)層面的讀寫并發(fā),引發(fā)的數(shù)據(jù)庫(kù)與緩存數(shù)據(jù)不一致的問(wèn)題(本質(zhì)是后發(fā)生的讀請(qǐng)求先返回了),可能通過(guò)兩個(gè)小的改動(dòng)解決:
(1)修改服務(wù)Service連接池,id取模選取服務(wù)連接,能夠保證同一個(gè)數(shù)據(jù)的讀寫都落在同一個(gè)后端服務(wù)上
(2)修改數(shù)據(jù)庫(kù)DB連接池,id取模選取DB連接,能夠保證同一個(gè)數(shù)據(jù)的讀寫在數(shù)據(jù)庫(kù)層面是串行的
提問(wèn):取模訪問(wèn)服務(wù)是否會(huì)影響服務(wù)的可用性?
答:不會(huì),當(dāng)有下游服務(wù)掛掉的時(shí)候,服務(wù)連接池能夠檢測(cè)到連接的可用性,取模時(shí)要把不可用的服務(wù)連接排除掉。
提問(wèn):取模訪問(wèn)服務(wù)與 取模訪問(wèn)DB,是否會(huì)影響各連接上請(qǐng)求的負(fù)載均衡?
答:不會(huì),只要數(shù)據(jù)訪問(wèn)id是均衡的,從全局來(lái)看,由id取模獲取各連接的概率也是均等的,即負(fù)載是均衡的。
提問(wèn):要是數(shù)據(jù)庫(kù)的架構(gòu)做了主從同步,讀寫分離:寫請(qǐng)求寫主庫(kù),讀請(qǐng)求讀從庫(kù)也有可能導(dǎo)致緩存中進(jìn)入臟數(shù)據(jù)呀,這種情況怎么解決呢(讀寫請(qǐng)求根本不落在同一個(gè)DB上,并且讀寫DB有同步時(shí)延)?
答:下一篇文章和大家分享。
Copyright©2021 w3cschool編程獅|閩ICP備15016281號(hào)-3|閩公網(wǎng)安備35020302033924號(hào)
違法和不良信息舉報(bào)電話:173-0602-2364|舉報(bào)郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號(hào)
聯(lián)系方式:
更多建議: