在最近做的一個項目中,由於每天核算的數據量過於龐大,需要把數據庫進行分庫保存。當數據分散到各個庫之后,帶來的數據更新操作就會存在一個一致性和完整性的問題。下面是一個典型的場景
假設目前存在三個物理庫,現在有一個文件,里面有1W條數據,根據分庫的規則,可以把文件里面的數據分到三個庫中,現在需要保證這1W條數據要要完整的保存到這三個庫里面,並且數據是一致性的,也就是說 三個庫里面已導入的數據完全和文件里面的數據一致。
正常情況下,我們先把文件里面的數據按照所屬的數據庫分成三份,然后針對每一份數據庫進行保存,在單庫的情況下,可以保證單庫的數據完整性。但是三個庫要保證一致性,就是非常復雜的一項工作,很有可能第一個庫的數據保存成功了,但是后面三個庫的數據保存失敗了,導致整個文件的里面的數據在數據庫里面不完整。
如何解決這種問題,目前想到的有幾個辦法:
方案1
使用類似JTA提供的分布式事物機制,也就是說需要相關的數據庫提供支持XA的驅動。( XA 是指由 X/Open 組織提出的分布式交易處理的規范)。這個需要依賴特定的數據庫廠商,也是比較簡單的方案。畢竟復雜的事務管理都可以通過提供JTA服務的廠商和提供XA驅動的數據庫廠商來完成。目前大多數實現了JTA的服務器廠商比較多,比如JBOSS,或者開源的JOTM(Java Open Transaction Manager)——ObjectWeb的一個開源JTA實現。但是引入支持XA的數據庫驅動會帶來很多潛在的問題,在 《java事務設計策略》里面:在Java事務管理中,常常令人困惑的一個問題是什么時候應該使用XA,什么時候不應使用XA。由於大多數商業應用服務器執行單階段提交(one-phase commit)操作,性能下降並非一個值得考慮的問題。然而,非必要性的在您的應用中引入XA數據庫驅動,會導致不可預料的后果與錯誤,特別是在使用本地事務模型(Local Transaction Model)時。因此,一般來說在您不需要XA的時候,應該盡量避免使用它。” 所以這個是一個可選的方案,也是最簡單的一個方案
方案2
建立一張文件批次表(放在一個獨立的數據庫里面),保存待處理的文件批次信息(不是明細數據,簡單說的就是要處理的文件名和所在路徑),在每次處理文件數據的時候,先往表里面插入一條文件批次信息,並且設置文件的狀態為初始狀態,在文件中的數據全部成功的保存到三個分庫里面之后,在更新文件的批次狀態為成功。如果保存到分庫的過程中出現異常,文件批次的狀態還是初始狀態。而后台啟動一個定時機制,定時去掃描文件批次狀態,如果發現是初始狀態,就重新執行文件的導入操作,直到文件完全導入成功。這個方案看起來沒有問題,但是可能存在重復導入的情況,比如批次導入到第一個分庫成功了,后面兩個庫失敗了,重新導入的話,可能會重復把數據重復導入第一個分庫。我們可以在導入之間進行判斷,如果導入過,就不進行導入,但是極端的情況,我們無法判斷數據是否導入過,也是一個有缺陷的方案,並且如果每次導入之前,都進行數據是否導入的操作,性能會有一些影響。我們也可以通過異常恢復機制來進行,如果發現文件導入失敗了,我們刪除已經導入入庫的流水,但是這也引入了錯誤處理帶來的一致性問題,比如我們已經導入成功2個分庫的數據,在導入第三個分庫失敗的情況下,要刪除掉前面兩個分庫的數據,這也沒有辦法保證是一致的。
在這個方案里面,我們可以在進行一定的優化,讓它看起來運作起來是沒有問題的。首先再建立一張子批次表(和文件批次表放在同一個庫),在進行處理的時候,我們把大的文件的數據按照分庫規則拆成三個子文件,每一個子文件里面的數據對應一個分庫。這樣就產生三條子批次信息,由於文件批次信息和子批次信息 在同一個庫里面,可以保證一致性。這樣每個待處理的文件就分成了四條記錄,一條主文件批次信息,三條子批次信息,在導入數據之前,這些批次的信息的狀態都是初始狀態。這樣一個文件的導入就分解為三個子文件,分別導入到對應庫里面去。對於每個子文件批次,我們可以保證子文件數據的都是在同一個庫里面,保證每個子文件里面數據的一致性和完整性,然后導入成功之后,在更新子批次的狀態為成功,如果所有的子文件的批次狀態都為成功,那么對應的文件批次狀態就更新為成功。這樣看起來非常完美,解決了問題。但是仔細考慮一下,有一個小的細節問題:子批次信息和一個獨立的庫,要導入的數據是和子批次信息可能不再一個庫,沒有辦法保證這兩個操作是一致性的,也就是說 子文件里面的數據成功的導入到分庫,但是可能子批次信息狀態沒有更新。那子批次信息能不能放在每個分庫里面了,這樣的話,又回到剛開始提出的問題了(這里面就不解釋,可以去自己去想想)。
下面一副圖簡單的演示的設計思想:
方案3
第2個方案的基礎上,可以繼續加以優化。首先我們保留第二個方案的文件批次信息表和子文件批次信息表,而且我們必須把這兩個表放在同一個庫里面(這里假設分配到主庫),保證我們拆分任務時的一致性。然后在各個分庫里面,我們建立一張各個分庫的子文件批次表。這個表模型基本上是和主庫的子文件批次信息表一樣。當拆分任務的時候,先保證主庫數據的完整性,也就是產生了一條文件批次信息記錄和三條子文件批次記錄,然后把這三條子文件批次信息分別復制到對應的分庫中的子文件批次信息表里面,然后更新主庫的子文件批次信息狀態為“已同步”。當然,這個過程是無法保證一致性的。解決方案啟動一個定時任務,定期的把主庫重點的子文件批次表信息中初始狀態的記錄 同步到各個分庫的子文件批次表里面,這里面可能導致兩種情況
1 分庫子批次信息表已經存在相同的信息(這個可以通過唯一性主鍵保證),說明已經同步,直接更新主庫的子文件批次信息狀態為 “已經同步”
2 分庫子批次信息不存在,則往對應的分庫insert一條數據,然后更新主庫的子文件批次信息狀態為 “已經同步”
然后各個分庫 就是先導入子文件中的數據,在更新分庫的子文件批次表的狀態為處理成功 ,這兩個操作由於都是分庫的上的操作,可以保證一致性。最后,在更新主庫的子批次信息表的狀態為 “處理成功”。同樣,更新主庫的子批次信息狀態如果失敗,可以采取類似的定時機制,同步分庫子文件批次信息表和主庫的子文件批次信息表的狀態。通過這種努力重試型機制,保證了主庫中的子文件批次表和分庫的子文件批次表是一致的。等所有的主庫子文件批次信息表狀態全部更新為“處理成功”,則文件批次狀態就更新為“處理成功”。
相比第二個方案,我們在兩個庫里面增加了數據的同步,用這種機制,保證了主庫分庫數據的一致性。
這里簡單的介紹一下第二個方案的簡單實現細節:
首先是數據庫之間表結構關聯關系
下面用腳本的方式簡單的演示一下這個過程
我們假設有四個庫,一個主庫MAIN,三個字庫SUB1,SUB2,SUB3
MAIN庫兩張表:
FILE_BATCH_NO,主要關注status狀態 I(初始)->S(成功)
SUB_BATCH_NO,主要關注status狀態 I(初始)->R(同步成功)->S(處理成功)
SUB庫兩張表
DATA_DEAIL:保存明細數據,也就是業務邏輯主要處理的表
SUB_BATCH_NO:主要關注status狀態,I(初始)->S(處理成功)
1 拆分文件批次的過程
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庫的子批次信息到分庫的各個SUB庫中對應的子批次信息表,同步成功,更新MAIN庫對應的子批次信息狀態為同步成功。
##分庫的操作,從MAIN庫SUB_BATCH_INFO表中獲取對應的數據插入到SUB1庫里面 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 //判斷分庫數據是否存在,存在就返回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庫的操作完成之后,開始進行MAIN庫SUB_BATCH_INFO表對應的update操作 begin transaction in MAIN declare SUB_ID; ## R代表已經同步的狀態,這里面可以判斷status的狀態,不過意義不大 update MAIN.SUB_BATCH_INFO set status ='R' where ID = SUB_ID commit; end;
上面只是一個SUB庫的操作,如果有多個庫,循環進行操作。如果某一個庫沒有同步成功,有定時恢復機制。定時恢復機制的對應的SQL就是從MAIN中提取出是狀態的SUB_BATCH_INFO記錄,重復進行上述處理的過程
3 SUB庫處理子批次信息,對流水進行保存,然后更新SUB庫對應的SUB_BATCH_INFO記錄狀態為處理成功。然后在更新MAIN庫的對應的SUB_BATCH_INFO記錄狀態為成功。
##分庫的流水操作 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 //判斷狀態是否是初始 if status == 'I' insert into SUB1.DATA_DETAIL update SUB1.SUB_BATCH_INFO.status ='S' end if commit; end; ##SUB1庫的操作完成之后,開始進行MAIN庫SUB_BATCH_INFO表對應的update操作 begin transaction in MAIN declare SUB_ID; ## R代表已經同步的狀態,這里面可以判斷status的狀態,不過意義不大 update MAIN.SUB_BATCH_INFO set status ='S' where ID = SUB_ID commit; end;
這里的情況一樣,就是SUB庫和MAIN庫也存在狀態同步的問題,這里也需要一個定時對MAIN庫的 SUB_BATCH_INFO表狀態進行同步更新
4 判斷MAIN庫對應的SUB_BATCH_INFO所有狀態是否已經為成功,如果成功,更新MAIN庫的FILE_BATCH_NO 的狀態為成功。
在這四個過程中,需要三個定時器。有兩個定時器保證MAIN庫和SUB庫之間的數據一致性問題,另外一個定時器負責異步更新MAIN庫 批次和子批次的一致性問題。
對於第三個方案,可以抽取出通用的邏輯,來解決后續類似的場景。比如根據條件,刪除各個分庫中滿足條件的流水,或者批量更新各個分庫中滿足條件的流水。我們可以把這些作為一個任務來抽象出來,一個具體的任務由N個子任務組成(N為分庫的個數),系統要保證N個子任務要么全部成功,要么全部失敗,不允許部分成功。我們可以在方案三的思想上,建立總任務表和子任務表,文件導入的處理只是其中的一個任務類型而已,批量刪除,批量更新以及其他類似的操作,都可以當做具體的任務類型。
4 第四種方案就是經典的分布式事務設計中的 兩階段提交思想。兩階段提交的有三個重要的子操作:准備提交,提交,回滾。
繼續拿文件導入來舉例子,各個分庫作為一個事務參與者 , 我們需要設計各個分庫的准備提交操作,提交,回滾操作。
准備提交階段:各個分庫可以把要處理的文件明細保存到一張臨時表里面,並且記住這一次事務中上下文信息。
提交階段:把這一次事務上下文中對應的臨時表數據同步到對應的明細表中
回滾階段:刪除本次事務相關的臨時表流水信息。
通過設計一個兩階段的提交的事務管理器,我們可以在導入文件的時候啟動一個分布式事務,生成一個事務上下文(這個上下文信息要保存到數據庫里面),然后在調用各個子參與者的時候,需要把這個上下文信息傳遞下去,分庫先進行准備工作(就是保存明細到臨時表),如果成功,就返回准備成功。等所有的參與者成功了,事務管理器就提交這個事務,這個分庫完成提交動作,把數據從臨時表插入到正式表。如果某一個准備操作失敗,所有的分庫執行回滾操作,刪除導入的流水。
這里面最重要的就是,如果某分庫准備階段返回成功,那么提交一定要成功,否則只能做數據訂正或者人工處理了。這個是在兩階段中事務中沒有辦法解決。
對於不同的操作,要設計對應的准備提交,提交,回滾操作,開發量比較大,而且分布式事務管理器的實現也需要一定的功底。
上面四種方案,能夠保證完整性和一致性的只有第三種和第四種方案。其實這兩種方案的設計思想是一致的。就是通過努力重試以及異步確認進行的。嚴格的說,第三種方案會有一定的問題,因為在整個處理過程中,只能保證最終一致性,而沒有辦法保證ACID里面的孤立性。因為存在部分提交的情況,而這一些數據有可能后續會進行回滾。不過可以就第三種方案在進行優化,加上一個鎖機制,不過擴展下來就比較復雜了。