分庫(kù)分表帶來(lái)的完整性和一致性問(wèn)題
在最近做的一個(gè)項(xiàng)目中,由于每天核算的數(shù)據(jù)量過(guò)于龐大,需要把數(shù)據(jù)庫(kù)進(jìn)行分庫(kù)保存。當(dāng)數(shù)據(jù)分散到各個(gè)庫(kù)之后,帶來(lái)的數(shù)據(jù)更新操作就會(huì)存在一個(gè)一致性和完整性的問(wèn)題。下面是一個(gè)典型的場(chǎng)景
假設(shè)目前存在三個(gè)物理庫(kù),現(xiàn)在有一個(gè)文件,里面有1W條數(shù)據(jù),根據(jù)分庫(kù)的規(guī)則,可以把文件里面的數(shù)據(jù)分到三個(gè)庫(kù)中,現(xiàn)在需要保證這1W條數(shù)據(jù)要要完整的保存到這三個(gè)庫(kù)里面,并且數(shù)據(jù)是一致性的,也就是說(shuō) 三個(gè)庫(kù)里面已導(dǎo)入的數(shù)據(jù)完全和文件里面的數(shù)據(jù)一致。
正常情況下,我們先把文件里面的數(shù)據(jù)按照所屬的數(shù)據(jù)庫(kù)分成三份,然后針對(duì)每一份數(shù)據(jù)庫(kù)進(jìn)行保存,在單庫(kù)的情況下,可以保證單庫(kù)的數(shù)據(jù)完整性。但是三個(gè)庫(kù)要保證一致性,就是非常復(fù)雜的一項(xiàng)工作,很有可能第一個(gè)庫(kù)的數(shù)據(jù)保存成功了,但是后面三個(gè)庫(kù)的數(shù)據(jù)保存失敗了,導(dǎo)致整個(gè)文件的里面的數(shù)據(jù)在數(shù)據(jù)庫(kù)里面不完整。
如何解決這種問(wèn)題,目前想到的有幾個(gè)辦法:
方案1
使用類似JTA提供的分布式事物機(jī)制,也就是說(shuō)需要相關(guān)的數(shù)據(jù)庫(kù)提供支持XA的驅(qū)動(dòng)。( XA 是指由 X/Open 組織提出的分布式交易處理的規(guī)范)。這個(gè)需要依賴特定的數(shù)據(jù)庫(kù)廠商,也是比較簡(jiǎn)單的方案。畢竟復(fù)雜的事務(wù)管理都可以通過(guò)提供JTA服務(wù)的廠商和提供XA驅(qū)動(dòng)的數(shù)據(jù)庫(kù)廠商來(lái)完成。目前大多數(shù)實(shí)現(xiàn)了JTA的服務(wù)器廠商比較多,比如JBOSS,或者開源的JOTM(Java Open Transaction Manager)——ObjectWeb的一個(gè)開源JTA實(shí)現(xiàn)。但是引入支持XA的數(shù)據(jù)庫(kù)驅(qū)動(dòng)會(huì)帶來(lái)很多潛在的問(wèn)題,在 《java事務(wù)設(shè)計(jì)策略》里面:在Java事務(wù)管理中,常常令人困惑的一個(gè)問(wèn)題是什么時(shí)候應(yīng)該使用XA,什么時(shí)候不應(yīng)使用XA。由于大多數(shù)商業(yè)應(yīng)用服務(wù)器執(zhí)行單階段提交(one-phase commit)操作,性能下降并非一個(gè)值得考慮的問(wèn)題。然而,非必要性的在您的應(yīng)用中引入XA數(shù)據(jù)庫(kù)驅(qū)動(dòng),會(huì)導(dǎo)致不可預(yù)料的后果與錯(cuò)誤,特別是在使用本地事務(wù)模型(Local Transaction Model)時(shí)。因此,一般來(lái)說(shuō)在您不需要XA的時(shí)候,應(yīng)該盡量避免使用它?!? 所以這個(gè)是一個(gè)可選的方案,也是最簡(jiǎn)單的一個(gè)方案
方案2
建立一張文件批次表(放在一個(gè)獨(dú)立的數(shù)據(jù)庫(kù)里面),保存待處理的文件批次信息(不是明細(xì)數(shù)據(jù),簡(jiǎn)單說(shuō)的就是要處理的文件名和所在路徑),在每次處理文件數(shù)據(jù)的時(shí)候,先往表里面插入一條文件批次信息,并且設(shè)置文件的狀態(tài)為初始狀態(tài),在文件中的數(shù)據(jù)全部成功的保存到三個(gè)分庫(kù)里面之后,在更新文件的批次狀態(tài)為成功。如果保存到分庫(kù)的過(guò)程中出現(xiàn)異常,文件批次的狀態(tài)還是初始狀態(tài)。而后臺(tái)啟動(dòng)一個(gè)定時(shí)機(jī)制,定時(shí)去掃描文件批次狀態(tài),如果發(fā)現(xiàn)是初始狀態(tài),就重新執(zhí)行文件的導(dǎo)入操作,直到文件完全導(dǎo)入成功。這個(gè)方案看起來(lái)沒(méi)有問(wèn)題,但是可能存在重復(fù)導(dǎo)入的情況,比如批次導(dǎo)入到第一個(gè)分庫(kù)成功了,后面兩個(gè)庫(kù)失敗了,重新導(dǎo)入的話,可能會(huì)重復(fù)把數(shù)據(jù)重復(fù)導(dǎo)入第一個(gè)分庫(kù)。我們可以在導(dǎo)入之間進(jìn)行判斷,如果導(dǎo)入過(guò),就不進(jìn)行導(dǎo)入,但是極端的情況,我們無(wú)法判斷數(shù)據(jù)是否導(dǎo)入過(guò),也是一個(gè)有缺陷的方案,并且如果每次導(dǎo)入之前,都進(jìn)行數(shù)據(jù)是否導(dǎo)入的操作,性能會(huì)有一些影響。我們也可以通過(guò)異?;謴?fù)機(jī)制來(lái)進(jìn)行,如果發(fā)現(xiàn)文件導(dǎo)入失敗了,我們刪除已經(jīng)導(dǎo)入入庫(kù)的流水,但是這也引入了錯(cuò)誤處理帶來(lái)的一致性問(wèn)題,比如我們已經(jīng)導(dǎo)入成功2個(gè)分庫(kù)的數(shù)據(jù),在導(dǎo)入第三個(gè)分庫(kù)失敗的情況下,要?jiǎng)h除掉前面兩個(gè)分庫(kù)的數(shù)據(jù),這也沒(méi)有辦法保證是一致的。
在這個(gè)方案里面,我們可以在進(jìn)行一定的優(yōu)化,讓它看起來(lái)運(yùn)作起來(lái)是沒(méi)有問(wèn)題的。首先再建立一張子批次表(和文件批次表放在同一個(gè)庫(kù)),在進(jìn)行處理的時(shí)候,我們把大的文件的數(shù)據(jù)按照分庫(kù)規(guī)則拆成三個(gè)子文件,每一個(gè)子文件里面的數(shù)據(jù)對(duì)應(yīng)一個(gè)分庫(kù)。這樣就產(chǎn)生三條子批次信息,由于文件批次信息和子批次信息 在同一個(gè)庫(kù)里面,可以保證一致性。這樣每個(gè)待處理的文件就分成了四條記錄,一條主文件批次信息,三條子批次信息,在導(dǎo)入數(shù)據(jù)之前,這些批次的信息的狀態(tài)都是初始狀態(tài)。這樣一個(gè)文件的導(dǎo)入就分解為三個(gè)子文件,分別導(dǎo)入到對(duì)應(yīng)庫(kù)里面去。對(duì)于每個(gè)子文件批次,我們可以保證子文件數(shù)據(jù)的都是在同一個(gè)庫(kù)里面,保證每個(gè)子文件里面數(shù)據(jù)的一致性和完整性,然后導(dǎo)入成功之后,在更新子批次的狀態(tài)為成功,如果所有的子文件的批次狀態(tài)都為成功,那么對(duì)應(yīng)的文件批次狀態(tài)就更新為成功。這樣看起來(lái)非常完美,解決了問(wèn)題。但是仔細(xì)考慮一下,有一個(gè)小的細(xì)節(jié)問(wèn)題:子批次信息和一個(gè)獨(dú)立的庫(kù),要導(dǎo)入的數(shù)據(jù)是和子批次信息可能不再一個(gè)庫(kù),沒(méi)有辦法保證這兩個(gè)操作是一致性的,也就是說(shuō) 子文件里面的數(shù)據(jù)成功的導(dǎo)入到分庫(kù),但是可能子批次信息狀態(tài)沒(méi)有更新。那子批次信息能不能放在每個(gè)分庫(kù)里面了,這樣的話,又回到剛開始提出的問(wèn)題了(這里面就不解釋,可以去自己去想想)。
下面一副圖簡(jiǎn)單的演示的設(shè)計(jì)思想:


方案3
第2個(gè)方案的基礎(chǔ)上,可以繼續(xù)加以優(yōu)化。首先我們保留第二個(gè)方案的文件批次信息表和子文件批次信息表,而且我們必須把這兩個(gè)表放在同一個(gè)庫(kù)里面(這里假設(shè)分配到主庫(kù)),保證我們拆分任務(wù)時(shí)的一致性。然后在各個(gè)分庫(kù)里面,我們建立一張各個(gè)分庫(kù)的子文件批次表。這個(gè)表模型基本上是和主庫(kù)的子文件批次信息表一樣。當(dāng)拆分任務(wù)的時(shí)候,先保證主庫(kù)數(shù)據(jù)的完整性,也就是產(chǎn)生了一條文件批次信息記錄和三條子文件批次記錄,然后把這三條子文件批次信息分別復(fù)制到對(duì)應(yīng)的分庫(kù)中的子文件批次信息表里面,然后更新主庫(kù)的子文件批次信息狀態(tài)為“已同步”。當(dāng)然,這個(gè)過(guò)程是無(wú)法保證一致性的。解決方案啟動(dòng)一個(gè)定時(shí)任務(wù),定期的把主庫(kù)重點(diǎn)的子文件批次表信息中初始狀態(tài)的記錄 同步到各個(gè)分庫(kù)的子文件批次表里面,這里面可能導(dǎo)致兩種情況
1 分庫(kù)子批次信息表已經(jīng)存在相同的信息(這個(gè)可以通過(guò)唯一性主鍵保證),說(shuō)明已經(jīng)同步,直接更新主庫(kù)的子文件批次信息狀態(tài)為 “已經(jīng)同步”
2 分庫(kù)子批次信息不存在,則往對(duì)應(yīng)的分庫(kù)insert一條數(shù)據(jù),然后更新主庫(kù)的子文件批次信息狀態(tài)為 “已經(jīng)同步”
然后各個(gè)分庫(kù) 就是先導(dǎo)入子文件中的數(shù)據(jù),在更新分庫(kù)的子文件批次表的狀態(tài)為處理成功 ,這兩個(gè)操作由于都是分庫(kù)的上的操作,可以保證一致性。最后,在更新主庫(kù)的子批次信息表的狀態(tài)為 “處理成功”。同樣,更新主庫(kù)的子批次信息狀態(tài)如果失敗,可以采取類似的定時(shí)機(jī)制,同步分庫(kù)子文件批次信息表和主庫(kù)的子文件批次信息表的狀態(tài)。通過(guò)這種努力重試型機(jī)制,保證了主庫(kù)中的子文件批次表和分庫(kù)的子文件批次表是一致的。等所有的主庫(kù)子文件批次信息表狀態(tài)全部更新為“處理成功”,則文件批次狀態(tài)就更新為“處理成功”。
相比第二個(gè)方案,我們?cè)趦蓚€(gè)庫(kù)里面增加了數(shù)據(jù)的同步,用這種機(jī)制,保證了主庫(kù)分庫(kù)數(shù)據(jù)的一致性。
這里簡(jiǎn)單的介紹一下第二個(gè)方案的簡(jiǎn)單實(shí)現(xiàn)細(xì)節(jié):
首先是數(shù)據(jù)庫(kù)之間表結(jié)構(gòu)關(guān)聯(lián)關(guān)系

下面用腳本的方式簡(jiǎn)單的演示一下這個(gè)過(guò)程
我們假設(shè)有四個(gè)庫(kù),一個(gè)主庫(kù)MAIN,三個(gè)字庫(kù)SUB1,SUB2,SUB3
MAIN庫(kù)兩張表:
FILE_BATCH_NO,主要關(guān)注status狀態(tài) I(初始)->S(成功)
SUB_BATCH_NO,主要關(guān)注status狀態(tài) I(初始)->R(同步成功)->S(處理成功)
SUB庫(kù)兩張表
DATA_DEAIL:保存明細(xì)數(shù)據(jù),也就是業(yè)務(wù)邏輯主要處理的表
SUB_BATCH_NO:主要關(guān)注status狀態(tài),I(初始)->S(處理成功)
1 拆分文件批次的過(guò)程
begin declare file_name,batch_no,sub_batch_no; insert into MAIN.FILE_BATCH_INFO(id,file_name,batch_no,status) values(seq.FILE_BATCH_INFO,#file_name#,#batch_no#,'I'); insert into MAIN.SUB_BATCH_INFO(id,file_name,main_batch_no,status) values(seq.SUB_BATCH_INFO,#file_name#,#batch_no#,#sub_batch_no#,'I'); insert into MAIN.SUB_BATCH_INFO(id,file_name,main_batch_no,status) values(seq.SUB_BATCH_INFO,#file_name#,#batch_no#,#sub_batch_no#,'I'); insert into MAIN.SUB_BATCH_INFO(id,file_name,main_batch_no,status) values(seq.SUB_BATCH_INFO,#file_name#,#batch_no#,#sub_batch_no#,'I'); commit; end;
2 同步MAIN庫(kù)的子批次信息到分庫(kù)的各個(gè)SUB庫(kù)中對(duì)應(yīng)的子批次信息表,同步成功,更新MAIN庫(kù)對(duì)應(yīng)的子批次信息狀態(tài)為同步成功。
##分庫(kù)的操作,從MAIN庫(kù)SUB_BATCH_INFO表中獲取對(duì)應(yīng)的數(shù)據(jù)插入到SUB1庫(kù)里面 begin transaction in SUB1 declare file_name,batch_no,sub_batch_no; select SUB_BATCH_INFO.ID into SUB_ID from MAIN.SUB_BATCH_INFO where SUB_BATCH_INFO.DATA_BASE = SUB1 //判斷分庫(kù)數(shù)據(jù)是否存在,存在就返回true if(select * from SUB1.SUB_BATCH_INFO where SUB_ID = SUB_BATCH_INFO.ID) return SUCCESS insert into SUB1.SUB_BATCH_INFO(id,file_name,main_batch_no,status) values(SUB_ID,#file_name#,#batch_no#,#sub_batch_no#,'I'); commit; end; ##SUB1庫(kù)的操作完成之后,開始進(jìn)行MAIN庫(kù)SUB_BATCH_INFO表對(duì)應(yīng)的update操作 begin transaction in MAIN declare SUB_ID; ## R代表已經(jīng)同步的狀態(tài),這里面可以判斷status的狀態(tài),不過(guò)意義不大 update MAIN.SUB_BATCH_INFO set status ='R' where ID = SUB_ID commit; end;
上面只是一個(gè)SUB庫(kù)的操作,如果有多個(gè)庫(kù),循環(huán)進(jìn)行操作。如果某一個(gè)庫(kù)沒(méi)有同步成功,有定時(shí)恢復(fù)機(jī)制。定時(shí)恢復(fù)機(jī)制的對(duì)應(yīng)的SQL就是從MAIN中提取出是狀態(tài)的SUB_BATCH_INFO記錄,重復(fù)進(jìn)行上述處理的過(guò)程
3 SUB庫(kù)處理子批次信息,對(duì)流水進(jìn)行保存,然后更新SUB庫(kù)對(duì)應(yīng)的SUB_BATCH_INFO記錄狀態(tài)為處理成功。然后在更新MAIN庫(kù)的對(duì)應(yīng)的SUB_BATCH_INFO記錄狀態(tài)為成功。
##分庫(kù)的流水操作 begin transaction in SUB1 declare file_name,batch_no,sub_batch_no; select SUB_BATCH_INFO.status into SUB_ID from MAIN.SUB_BATCH_INFO where SUB_BATCH_INFO.DATA_BASE = SUB1 //判斷狀態(tài)是否是初始 if status == 'I' insert into SUB1.DATA_DETAIL update SUB1.SUB_BATCH_INFO.status ='S' end if commit; end; ##SUB1庫(kù)的操作完成之后,開始進(jìn)行MAIN庫(kù)SUB_BATCH_INFO表對(duì)應(yīng)的update操作 begin transaction in MAIN declare SUB_ID; ## R代表已經(jīng)同步的狀態(tài),這里面可以判斷status的狀態(tài),不過(guò)意義不大 update MAIN.SUB_BATCH_INFO set status ='S' where ID = SUB_ID commit; end;
這里的情況一樣,就是SUB庫(kù)和MAIN庫(kù)也存在狀態(tài)同步的問(wèn)題,這里也需要一個(gè)定時(shí)對(duì)MAIN庫(kù)的 SUB_BATCH_INFO表狀態(tài)進(jìn)行同步更新
4 判斷MAIN庫(kù)對(duì)應(yīng)的SUB_BATCH_INFO所有狀態(tài)是否已經(jīng)為成功,如果成功,更新MAIN庫(kù)的FILE_BATCH_NO 的狀態(tài)為成功。
在這四個(gè)過(guò)程中,需要三個(gè)定時(shí)器。有兩個(gè)定時(shí)器保證MAIN庫(kù)和SUB庫(kù)之間的數(shù)據(jù)一致性問(wèn)題,另外一個(gè)定時(shí)器負(fù)責(zé)異步更新MAIN庫(kù) 批次和子批次的一致性問(wèn)題。
對(duì)于第三個(gè)方案,可以抽取出通用的邏輯,來(lái)解決后續(xù)類似的場(chǎng)景。比如根據(jù)條件,刪除各個(gè)分庫(kù)中滿足條件的流水,或者批量更新各個(gè)分庫(kù)中滿足條件的流水。我們可以把這些作為一個(gè)任務(wù)來(lái)抽象出來(lái),一個(gè)具體的任務(wù)由N個(gè)子任務(wù)組成(N為分庫(kù)的個(gè)數(shù)),系統(tǒng)要保證N個(gè)子任務(wù)要么全部成功,要么全部失敗,不允許部分成功。我們可以在方案三的思想上,建立總?cè)蝿?wù)表和子任務(wù)表,文件導(dǎo)入的處理只是其中的一個(gè)任務(wù)類型而已,批量刪除,批量更新以及其他類似的操作,都可以當(dāng)做具體的任務(wù)類型。
4 第四種方案就是經(jīng)典的分布式事務(wù)設(shè)計(jì)中的 兩階段提交思想。兩階段提交的有三個(gè)重要的子操作:準(zhǔn)備提交,提交,回滾。
繼續(xù)拿文件導(dǎo)入來(lái)舉例子,各個(gè)分庫(kù)作為一個(gè)事務(wù)參與者 , 我們需要設(shè)計(jì)各個(gè)分庫(kù)的準(zhǔn)備提交操作,提交,回滾操作。
準(zhǔn)備提交階段:各個(gè)分庫(kù)可以把要處理的文件明細(xì)保存到一張臨時(shí)表里面,并且記住這一次事務(wù)中上下文信息。
提交階段:把這一次事務(wù)上下文中對(duì)應(yīng)的臨時(shí)表數(shù)據(jù)同步到對(duì)應(yīng)的明細(xì)表中
回滾階段:刪除本次事務(wù)相關(guān)的臨時(shí)表流水信息。
通過(guò)設(shè)計(jì)一個(gè)兩階段的提交的事務(wù)管理器,我們可以在導(dǎo)入文件的時(shí)候啟動(dòng)一個(gè)分布式事務(wù),生成一個(gè)事務(wù)上下文(這個(gè)上下文信息要保存到數(shù)據(jù)庫(kù)里面),然后在調(diào)用各個(gè)子參與者的時(shí)候,需要把這個(gè)上下文信息傳遞下去,分庫(kù)先進(jìn)行準(zhǔn)備工作(就是保存明細(xì)到臨時(shí)表),如果成功,就返回準(zhǔn)備成功。等所有的參與者成功了,事務(wù)管理器就提交這個(gè)事務(wù),這個(gè)分庫(kù)完成提交動(dòng)作,把數(shù)據(jù)從臨時(shí)表插入到正式表。如果某一個(gè)準(zhǔn)備操作失敗,所有的分庫(kù)執(zhí)行回滾操作,刪除導(dǎo)入的流水。
這里面最重要的就是,如果某分庫(kù)準(zhǔn)備階段返回成功,那么提交一定要成功,否則只能做數(shù)據(jù)訂正或者人工處理了。這個(gè)是在兩階段中事務(wù)中沒(méi)有辦法解決。
對(duì)于不同的操作,要設(shè)計(jì)對(duì)應(yīng)的準(zhǔn)備提交,提交,回滾操作,開發(fā)量比較大,而且分布式事務(wù)管理器的實(shí)現(xiàn)也需要一定的功底。

上面四種方案,能夠保證完整性和一致性的只有第三種和第四種方案。其實(shí)這兩種方案的設(shè)計(jì)思想是一致的。就是通過(guò)努力重試以及異步確認(rèn)進(jìn)行的。嚴(yán)格的說(shuō),第三種方案會(huì)有一定的問(wèn)題,因?yàn)樵谡麄€(gè)處理過(guò)程中,只能保證最終一致性,而沒(méi)有辦法保證ACID里面的孤立性。因?yàn)榇嬖诓糠痔峤坏那闆r,而這一些數(shù)據(jù)有可能后續(xù)會(huì)進(jìn)行回滾。不過(guò)可以就第三種方案在進(jìn)行優(yōu)化,加上一個(gè)鎖機(jī)制,不過(guò)擴(kuò)展下來(lái)就比較復(fù)雜了。
14年互聯(lián)網(wǎng)技術(shù)、產(chǎn)品、運(yùn)營(yíng)經(jīng)驗(yàn),前支付寶技術(shù)專家,互金創(chuàng)業(yè)公司CTO,大令保事業(yè)部總經(jīng)理。
在互金領(lǐng)域有比較強(qiáng)的產(chǎn)品以及運(yùn)營(yíng)經(jīng)驗(yàn),尤其擅長(zhǎng)用戶增長(zhǎng)、轉(zhuǎn)化、運(yùn)營(yíng)上的經(jīng)驗(yàn),兼具技術(shù)、產(chǎn)品、運(yùn)營(yíng)思維。
目前是云貓?jiān)鲩L(zhǎng)實(shí)驗(yàn)室 創(chuàng)始人
團(tuán)隊(duì)成員來(lái)自阿里等國(guó)內(nèi)知名互聯(lián)網(wǎng)公司,曾在互聯(lián)網(wǎng)金融、互聯(lián)網(wǎng)保險(xiǎn)、企業(yè)級(jí)SaaS等項(xiàng)目中負(fù)責(zé)用戶增長(zhǎng),團(tuán)隊(duì)管理的工作,擁有豐富的一線流量增長(zhǎng)經(jīng)驗(yàn)與實(shí)操手段。
歡迎關(guān)注我們,用技術(shù)驅(qū)動(dòng)增長(zhǎng)
浙公網(wǎng)安備 33010602011771號(hào)