转账引发数据一致性思考
鑒于一個boy 發(fā)生支付寶提現(xiàn)事件,引發(fā)思考。支付寶提現(xiàn)成功,銀行卡沒查到到賬。數(shù)據(jù)庫不一致性問題··········
事務(wù)分為本地事務(wù)和分布式事務(wù)
本地事務(wù)
數(shù)據(jù)庫事務(wù)四大特征:原子性(A),一致性(C),隔離性(I)和持久性(D),而在這四大特性中,一致性是最基本的特性,其它的三個特性都為了保證一致性而存在的
A賬戶給B賬戶轉(zhuǎn)賬100元(A、B處于同一個庫中),如果A的賬戶發(fā)生扣款,B的賬戶卻沒有到賬,這就出現(xiàn)了數(shù)據(jù)的不一致!為了保證數(shù)據(jù)的一致性,數(shù)據(jù)庫的事務(wù)機(jī)制會讓A賬戶扣款和B在賬戶到賬的兩個操作要么同時成功,如果有一個操作失敗,則多個操作同時回滾,這就是事務(wù)的原子性,為了保證事務(wù)操作的原子性,就必須實(shí)現(xiàn)基于日志的REDO/UNDO機(jī)制。
 但是,僅有原子性還不夠,因為我們的系統(tǒng)是運(yùn)行在多線程環(huán)境下,如果多個事務(wù)并行,即使保證了每一個事務(wù)的原子性,仍然會出現(xiàn)數(shù)據(jù)不一致的情況。
 例如A賬戶原來有200元的余額, A賬戶給B賬戶轉(zhuǎn)賬100元,先讀取A賬戶的余額,然后在這個值上減去100元,但是在這兩個操作之間,A賬戶又給C賬戶轉(zhuǎn)賬100元,那么最后的結(jié)果應(yīng)該是A減去了200元。但事實(shí)上,A賬戶給B賬戶最終完成轉(zhuǎn)賬后,A賬戶只減掉了100元,因為A賬戶向C賬戶轉(zhuǎn)賬減掉的100元被覆蓋了!所以為了保證并發(fā)情況下的一致性,又引入的隔離性,即多個事務(wù)并發(fā)執(zhí)行后的狀態(tài),和它們串行執(zhí)行后的狀態(tài)是等價的!隔離性又有多種隔離級別,為了實(shí)現(xiàn)隔離性(最終都是為了保證一致性)數(shù)據(jù)庫又引入了悲觀鎖、樂觀鎖等等……本文的主題是分布式事務(wù),所以本地事務(wù)就只是簡單回顧一下,需要記住的一點(diǎn)是,事務(wù)是為了保證數(shù)據(jù)的一致性
事務(wù)隔離性理解:H1:當(dāng)A轉(zhuǎn)給B 100 之后,H2:A又轉(zhuǎn)給C 100,但是在H2未提交之前,A查詢到的余額還是H1,所以A此時余額只減少了100元,與實(shí)際應(yīng)該減少200元,并發(fā)情況下數(shù)據(jù)不一致。
 如下圖是我自己的理解
 
分布式理論
學(xué)習(xí)一下別人的經(jīng)歷,警惕自己。
 該經(jīng)歷來源 https://www.cnblogs.com/sujing/p/11006424.html
 領(lǐng)導(dǎo)給我的第一個任務(wù)就是在列表上增加一個修改數(shù)據(jù)的功能。這能難倒我?我分分鐘給你搞出來!不就是在列表上增加了一個“修改”按鈕,點(diǎn)擊按鈕彈出框修改后保存就好了么。然而一切不像我想象的那么順利,點(diǎn)擊保存并刷新列表后,頁面上的數(shù)據(jù)還是顯示的修改之前的內(nèi)容,像沒有修改成功一樣!過一會兒再刷新列表,數(shù)據(jù)就能正常顯示了!測試多次之后都是這樣!沒見過什么大場面的我開始有點(diǎn)慌了,是我哪里寫得不對么?最終,我不得不求助組內(nèi)經(jīng)驗比較豐富的前輩!他深吸了一口氣告訴我說:“畢竟是剛畢業(yè)的小伙子啊!我來跟你講講原因吧!我們的數(shù)據(jù)庫是做了讀寫分離的,部分讀庫與寫庫在不同的網(wǎng)絡(luò)分區(qū)。你的數(shù)據(jù)更新到了寫庫,而讀數(shù)據(jù)的時候是從讀庫讀取的。更新到寫庫的數(shù)據(jù)同步到讀庫是有一定的延遲的,也就是說讀庫與寫庫會有短暫的數(shù)據(jù)不一致”! “這樣不會體驗不好么?為什么不能做到寫入的數(shù)據(jù)立馬能讀出來?那我這個功能該怎么實(shí)現(xiàn)呢?” 面對我的一堆問題,同事有些不耐煩的說:“聽說過CAP理論嗎?你先自己去了解一下吧”!
看了別人的經(jīng)歷,接下來屬于我自己的探索了
1.CAP理論
CAP理論指的是一個分布式系統(tǒng)最多只能同時滿足一致性(Consistency)、可用性(Availability)和分區(qū)容錯性(Partition tolerance)
 
好了,那如何理解CAP理論?
 聲明:鏈接:https://www.zhihu.com/question/54105974/answer/139037688
 一個分布式系統(tǒng)里面,節(jié)點(diǎn)組成的網(wǎng)絡(luò)本來應(yīng)該是連通的。然而可能因為一些故障,使得有些節(jié)點(diǎn)之間不連通了,整個網(wǎng)絡(luò)就分成了幾塊區(qū)域。數(shù)據(jù)就散布在了這些不連通的區(qū)域中。這就叫分區(qū)。當(dāng)你一個數(shù)據(jù)項只在一個節(jié)點(diǎn)中保存,那么分區(qū)出現(xiàn)后,和這個節(jié)點(diǎn)不連通的部分就訪問不到這個數(shù)據(jù)了。這時分區(qū)就是無法容忍的。提高分區(qū)容忍性的辦法就是一個數(shù)據(jù)項復(fù)制到多個節(jié)點(diǎn)上,那么出現(xiàn)分區(qū)之后,這一數(shù)據(jù)項就可能分布到各個區(qū)里。容忍性就提高了。然而,要把數(shù)據(jù)復(fù)制到多個節(jié)點(diǎn),就會帶來一致性的問題,就是多個節(jié)點(diǎn)上面的數(shù)據(jù)可能是不一致的。要保證一致,每次寫操作就都要等待全部節(jié)點(diǎn)寫成功,而這等待又會帶來可用性的問題。總的來說就是,數(shù)據(jù)存在的節(jié)點(diǎn)越多,分區(qū)容忍性越高,但要復(fù)制更新的數(shù)據(jù)就越多,一致性就越難保證。為了保證一致性,更新所有節(jié)點(diǎn)數(shù)據(jù)所需要的時間就越長,可用性就會降低。
如何理解P?
 假設(shè)有三個節(jié)點(diǎn)ABC,還有一個你的客戶端節(jié)點(diǎn)D,原先D和ABC都在一個網(wǎng)絡(luò)中,你的D可以訪問ABC,某時刻C節(jié)點(diǎn)因為網(wǎng)線斷了還是啥原因反正和ABD的網(wǎng)絡(luò)斷開了,ABD是一個網(wǎng)絡(luò)分區(qū),C自己獨(dú)立為一個網(wǎng)絡(luò)分區(qū),此時你的D客戶端就訪問不到C節(jié)點(diǎn)的數(shù)據(jù)了,這就是分區(qū)容忍性差,如果C節(jié)點(diǎn)有一個復(fù)制節(jié)點(diǎn)C1,他沒有和大部隊ABD斷開網(wǎng)絡(luò)連接,那么此時分區(qū)容忍性就相較之前較好。
分布式事務(wù)
與本地事務(wù)不同的是,分布式事務(wù)需要保證的是分布式環(huán)境下,不同數(shù)據(jù)庫表中的數(shù)據(jù)的一致性問題。分布式事務(wù)的解決方案有多種,如XA協(xié)議、TCC三階段提交、基于消息隊列等等,本文只會涉及基于消息隊列的解決方案。
本地事務(wù)講到了一致性,分布式事務(wù)不可避免的面臨著一致性的問題!回到最開始跨行轉(zhuǎn)賬的例子,如果A銀行用戶向B銀行用戶轉(zhuǎn)賬,正常流程應(yīng)該是
1、A銀行對轉(zhuǎn)出賬戶執(zhí)行檢查校驗,進(jìn)行金額扣減。
 2、A銀行同步調(diào)用B銀行轉(zhuǎn)賬接口。
 3、B銀行對轉(zhuǎn)入賬戶進(jìn)行檢查校驗,進(jìn)行金額增加。
 4、B銀行返回處理結(jié)果給A銀行。
在正常情況對一致性要求不高的場景,這樣的設(shè)計是可以滿足需求的。但是像銀行這樣的系統(tǒng),如果這樣實(shí)現(xiàn)大概早就破產(chǎn)了吧。我們先看看這樣的設(shè)計最主要的問題:
1、同步調(diào)用遠(yuǎn)程接口,如果接口比較耗時,會導(dǎo)致主線程阻塞時間較長。
 2、流量不能很好控制,A銀行系統(tǒng)的流量高峰可能壓垮B銀行系統(tǒng)(當(dāng)然B銀行肯定會有自己的限流機(jī)制)。
 3、如果“第1步”剛執(zhí)行完,系統(tǒng)由于某種原因宕機(jī)了,那會導(dǎo)致A銀行賬戶扣款了,但是B銀行沒有收到接口的調(diào)用,這就出現(xiàn)了兩個系統(tǒng)數(shù)據(jù)的不一致。
 4、如果在執(zhí)行“第3步”后,B銀行由于某種原因宕機(jī)了而無法正確回應(yīng)請求(實(shí)際上轉(zhuǎn)賬操作在B銀行系統(tǒng)已經(jīng)執(zhí)行且入庫),這時候A銀行等待接口響應(yīng)會異常,誤以為轉(zhuǎn)賬失敗而回滾“第1步”操作,這也會出現(xiàn)了兩個系統(tǒng)數(shù)據(jù)的不一致。
 ?? 對于問題的1、2都很好解決,如果對消息隊列熟悉的朋友應(yīng)該很快能想到可以引入消息中間件進(jìn)行異步和削峰處理,于是又重新設(shè)計了一個方案,流程如下:
 ?? >1、A銀行對賬戶進(jìn)行檢查校驗,進(jìn)行金額扣減。
 2、將對B銀行的請求異步寫入隊列,主線程返回。
 3、啟動后臺程序從隊列獲取待處理數(shù)據(jù)。
 4、后臺程序?qū)銀行接口進(jìn)行遠(yuǎn)程調(diào)用。
 5、B銀行對轉(zhuǎn)入賬戶進(jìn)行檢查校驗,進(jìn)行金額增加。
 6、B銀行處理完成回調(diào)A銀行接口通知處理結(jié)果。
 ? 通過上面的圖我們能看到,引入消息隊列后,系統(tǒng)的復(fù)雜性瞬間提升了,雖然彌補(bǔ)了我們第一種方案的幾個不足點(diǎn),但也帶來了更多的問題,比如消息隊列系統(tǒng)本身的可用性、消息隊列的延遲等等!并且,這樣的設(shè)計依然沒有解決我們面臨的核心問題-數(shù)據(jù)的一致性!
1、如果“第1步”剛執(zhí)行完,系統(tǒng)由于某種原因宕機(jī)了,那會導(dǎo)致A銀行賬戶扣款了,但是寫入消息隊列失敗,無法進(jìn)行B銀行接口調(diào)用,從而導(dǎo)致數(shù)據(jù)不一致。
 2、如果B銀行在執(zhí)行“第5步”時由于校驗失敗而未能成功轉(zhuǎn)賬,在回調(diào)A銀行接口通知回滾時網(wǎng)絡(luò)異常或者宕機(jī),會導(dǎo)致A銀行轉(zhuǎn)賬無法完成回滾,從而導(dǎo)致數(shù)據(jù)不一致。
面對上述問題,我們不得不對系統(tǒng)再次進(jìn)行升級改造。為了解決“A銀行賬戶扣款了,但是寫入消息隊列失敗”的問題,我們需要借助一個轉(zhuǎn)賬日志表,或者叫轉(zhuǎn)賬流水表,該表簡單的設(shè)計如下:
 ? 這個流水表需要怎么用呢?我們在“第1步”進(jìn)行扣款時,同時往流水表寫入一條操作流水,狀態(tài)為“待處理”,并且這兩個操作必須是原子的,也就是說必須通過本地事務(wù)保證這兩個操作要么同時成功,要么同時失敗!這就保證了只要轉(zhuǎn)賬扣款成功,必定會記錄一條狀態(tài)為“待處理”的轉(zhuǎn)賬流水。如果在這一步失敗了,那自然就是轉(zhuǎn)賬失敗,沒有后續(xù)操作了。如果這步操作后系統(tǒng)宕機(jī)了導(dǎo)致沒有將消息成功寫入消息隊列(也就是“第2步”)也沒關(guān)系,因為我們的流水?dāng)?shù)據(jù)已經(jīng)持久化了!這時候我們只需要加入一個后臺線程進(jìn)行補(bǔ)償,定期的從轉(zhuǎn)賬流水表中讀取狀態(tài)為“待處理”且最后更新的時間距當(dāng)前時間大于某個閾值的數(shù)據(jù),重新放入消息隊列進(jìn)行補(bǔ)償。這樣,就保證了消息即使丟失,也會有補(bǔ)償機(jī)制!B銀行在處理完轉(zhuǎn)賬請求后會回調(diào)A銀行的接口通知轉(zhuǎn)賬的狀態(tài),從而更新A銀行流水表中的狀態(tài)字段!這樣就完美解決了上一個方案中的兩個不足點(diǎn)。系統(tǒng)設(shè)計圖如下:
 
 ? 到目前為止,我們很好的解決了消息丟失的問題,保證了只要A銀行轉(zhuǎn)賬操作成功,轉(zhuǎn)賬的請求就一定能發(fā)送到B銀行!但是該方案又引入了一個問題,通過后臺線程輪詢將消息放入消息隊列處理,同一次轉(zhuǎn)賬請求可能會出現(xiàn)多次放入消息隊列而多次消費(fèi)的情況,這樣B銀行會對同一轉(zhuǎn)賬多次處理導(dǎo)致數(shù)據(jù)出現(xiàn)不一致!那怎么保證B銀行轉(zhuǎn)賬接口的冪等性呢?
同樣的,我們可以在B銀行系統(tǒng)中需要增加一個轉(zhuǎn)賬日志表,或者叫轉(zhuǎn)賬流水表,B銀行每次接收到轉(zhuǎn)賬請求,在對賬戶進(jìn)行操作的時候同時往轉(zhuǎn)賬日志表中插入一條轉(zhuǎn)賬日志記錄,同樣這兩個操作也必須是原子的!在接收到轉(zhuǎn)賬請求后,首先根據(jù)唯一轉(zhuǎn)賬流水Id在日志表中查找判斷該轉(zhuǎn)賬是否已經(jīng)處理過,如果未處理過則進(jìn)行處理,否則直接回調(diào)返回! 最終的架構(gòu)圖如下:
 ?? 
 所以,我們這里最核心的就是A銀行通過本地事務(wù)保證日志記錄+后臺線程輪詢保證消息不丟失。B銀行通過本地事務(wù)保證日志記錄從而保證消息不重復(fù)消費(fèi)!B銀行在回調(diào)A銀行的接口時會通知處理結(jié)果,如果轉(zhuǎn)賬失敗,A銀行會根據(jù)處理結(jié)果進(jìn)行回滾。
本文綜合csdn文章、知乎、百度學(xué)習(xí)與理解,進(jìn)行總結(jié)。歡迎小伙伴一起學(xué)習(xí)!
總結(jié)
以上是生活随笔為你收集整理的转账引发数据一致性思考的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: (四)RTL级低功耗设计
 - 下一篇: (五)门级电路低功耗设计优化