1. 程式人生 > >Mysql高手系列 - 第27篇:mysql如何確保資料不丟失的?我們借鑑這種設計思想實現熱點賬戶高併發設計及跨庫轉賬問題

Mysql高手系列 - 第27篇:mysql如何確保資料不丟失的?我們借鑑這種設計思想實現熱點賬戶高併發設計及跨庫轉賬問題

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。

歡迎大家加我微信itsoku一起交流java、演算法、資料庫相關技術。

這是Mysql系列第27篇。

本篇文章我們先來看一下mysql是如何確保資料不丟失的,通過本文我們可以瞭解mysql內部確保資料不丟失的原理,學習裡面優秀的設計要點,然後我們再借鑑這些優秀的設計要點進行實踐應用,加深理解。

預備知識

  1. mysql內部是使用b+樹的結構將資料儲存在磁碟中,b+樹中節點對應mysql中的頁,mysql和磁碟互動的最小單位為頁,頁預設情況下為16kb,表中的資料記錄儲存在b+樹的葉子節點中,當我們需要修改、刪除、插入資料時,都需要按照頁來對磁碟進行操作。
  2. 磁碟順序寫比隨機寫效率要高很多,通常我們使用的是機械硬碟,機械硬碟寫資料的時候涉及磁碟尋道、磁碟旋轉定址、資料寫入的時間,耗時比較長,如果是順序寫,省去了尋道和磁碟旋轉的時間,效率會高几個數量級。
  3. 記憶體中資料讀寫操作比磁碟中資料讀寫操作速度高好多個數量級。

mysql確保資料不丟失原理分析

我們來思考一下,下面這條語句的執行過程是什麼樣的:

start transaction;
update t_user set name = '路人甲Java' where user_id = 666;
commit;

按照正常的思路,通常過程如下:

  1. 找到user_id=666這條記錄所在的頁p1,將p1從磁碟載入到記憶體中
  2. 在記憶體中對p1中user_id=666這條記錄資訊進行修改
  3. mysql收到commit指令
  4. 將p1頁寫入磁碟
  5. 給客戶端返回更新成功

上面過程可以確保資料被持久化到了磁碟中。

我們將需求改一下,如下:

start transaction;
update t_user set name = '路人甲Java' where user_id = 666;
update t_user set name = 'javacode2018' where user_id = 888;
commit;

來看一下處理過程:

  1. 找到user_id=666這條記錄所在的頁p1,將p1從磁碟載入到記憶體中
  2. 在記憶體中對p1中user_id=666這條記錄資訊進行修改
  3. 找到user_id=888這條記錄所在的頁p2,將p2從磁碟載入到記憶體中
  4. 在記憶體中對p2中user_id=888這條記錄資訊進行修改
  5. mysql收到commit指令
  6. 將p1頁寫入磁碟
  7. 將p2頁寫入磁碟
  8. 給客戶端返回更新成功

上面過程我們看有什麼問題

  1. 假如6成功之後,mysql宕機了,此時p1修改已寫入磁碟,但是p2的修改還未寫入磁碟,最終導致user_id=666的記錄被修改成功了,user_id=888的資料被修改失敗了,資料是有問題的
  2. 上面p1和p2可能位於磁碟的不同位置,涉及到磁碟隨機寫的問題,導致整個過程耗時也比較長

上面問題可以歸納為2點:無法確保資料可靠性、隨機寫導致耗時比較長。

關於上面問題,我們看一下mysql是如何優化的,mysql內部引入了一個redo log,這是一個檔案,對於上面2條更新操作,mysql實現如下:

mysql內部有個redo log buffer,是記憶體中一塊區域,我們將其理解為陣列結構,向redo log檔案中寫資料時,會先將內容寫入redo log buffer中,後續會將這個buffer中的內容寫入磁碟中的redo log檔案,這個個redo log buffer是整個mysql中所有連線共享的記憶體區域,可以被重複使用。

  1. mysql收到start transaction後,生成一個全域性的事務編號trx_id,比如trx_id=10

  2. user_id=666這個記錄我們就叫r1,user_id=888這個記錄叫r2

  3. 找到r1記錄所在的資料頁p1,將其從磁碟中載入到記憶體中

  4. 在記憶體中找到r1在p1中的位置,然後對p1進行修改(這個過程可以描述為:將p1中的pos_start1到pos_start2位置的值改為v1),這個過程我們記為rb1(內部包含事務編號trx_id),將rb1放入redo log buffer陣列中,此時p1的資訊在記憶體中被修改了,和磁碟中p1的資料不一樣了

  5. 找到r2記錄所在的資料頁p2,將其從磁碟中載入到記憶體中

  6. 在記憶體中找到r2在p2中的位置,然後對p2進行修改(這個過程可以描述為:將p2中的pos_start1到pos_start2位置的值改為v2),這個過程我們記為rb2(內部包含事務編號trx_id),將rb2放入redo log buffer陣列中,此時p2的資訊在記憶體中被修改了,和磁碟中p2的資料不一樣了

  7. 此時redo log buffer陣列中有2條記錄[rb1,rb2]

  8. mysql收到commit指令

  9. 將redo log buffer陣列中內容寫入到redo log檔案中,寫入的內容:

    1.start trx=10;
    2.寫入rb1
    3.寫入rb2
    4.end trx=10;
  10. 返回給客戶端更新成功。

上面過程執行完畢之後,資料是這樣的:

  1. 記憶體中p1、p2頁被修改了,還未同步到磁碟中,此時記憶體中資料頁和磁碟中資料頁是不一致的,此時記憶體中資料頁我們稱為髒頁
  2. 對p1、p2頁修改被持久到磁碟中的redolog檔案中了,不會丟失

認真看一下上面過程中第9步驟,一個成功的事務記錄在redo log中是有start和end的,redo log檔案中如果一個trx_id對應start和end成對出現,說明這個事務執行成功了,如果只有start沒有end說明是有問題的。

那麼對p1、p2頁的修改什麼時候會同步到磁碟中呢?

redo log是mysql中所有連線共享的檔案,對mysql執行insert、delete和上面update的過程類似,都是先在記憶體中修改頁資料,然後將修改過程持久化到redo log所在的磁碟檔案中,然後返回成功。redo log檔案是有大小的,需要重複利用的(redo log有多個,多個之間採用環形結構結合幾個變數來做到重複利用,這塊知識不做說明,有興趣的可以去網上找一下),當redo log滿了,或者系統比較閒的時候,會對redo log檔案中的內容進行處理,處理過程如下:

  1. 讀取redo log資訊,讀取一個完整的trx_id對應的資訊,然後進行處理

  2. 比如讀取到了trx_id=10的完整內容,包含了start end,表示這個事務操作是成功的,然後繼續向下

  3. 判斷p1在記憶體中是否存在,如果存在,則直接將p1資訊寫到p1所在的磁碟中;如果p1在記憶體中不存在,則將p1從磁碟載入到記憶體,通過redo log中的資訊在記憶體中對p1進行修改,然後將其寫到磁碟中

    上面的update之後,p1在記憶體中是存在的,並且p1是已經被修改過的,可以直接重新整理到磁碟中。

    如果上面的update之後,mysql宕機,然後重啟了,p1在記憶體中是不存在的,此時系統會讀取redo log檔案中的內容進行恢復處理。

  4. 將redo log檔案中trx_id=10的佔有的空間標記為已處理,這塊空間會被釋放出來可以重複利用了

  5. 如果第2步讀取到的trx_id對應的內容沒有end,表示這個事務執行到一半失敗了(可能是第9步驟寫到一半宕機了),此時這個記錄是無效的,可以直接跳過不用處理

上面的過程做到了:資料最後一定會被持久化到磁碟中的頁中,不會丟失,做到了可靠性。

並且內部採用了先把頁的修改操作先在記憶體中進行操作,然後再寫入了redo log檔案,此處redo log是按順序寫的,使用到了io的順序寫,效率會非常高,相對於使用者來說響應會更快。

對於將資料頁的變更持久化到磁碟中,此處又採用了非同步的方式去讀取redo log的內容,然後將頁的變更刷到磁碟中,這塊的設計也非常好,非同步刷盤操作!

但是有一種情況,當一個事務commit的時候,剛好發現redo log不夠了,此時會先停下來處理redo log中的內容,然後在進行後續的操作,遇到這種情況時,整個事物響應會稍微慢一些。

mysql中還有一個binlog,在事務操作過程中也會寫binlog,先說一下binlog的作用,binlog中詳細記錄了對資料庫做了什麼操作,算是對資料庫操作的一個流水,這個流水也是相當重要的,主從同步就是使用binlog來實現的,從庫讀取主庫中binlog的資訊,然後在從庫中執行,最後,從庫就和主庫資訊保持同步一致了。還有一些其他系統也可以使用binlog的功能,比如可以通過binlog來實現bi系統中etl的功能,將業務資料抽取到資料倉庫,阿里提供了一個java版本的專案:canal,這個專案可以模擬從庫從主庫讀取binlog的功能,也就是說可以通過java程式來監控資料庫詳細變化的流水,這個大家可以腦洞大開一下,可以做很多事情的,有興趣的朋友可以去研究一下;所以binlog對mysql來說也是相當重要的,我們來看一下系統如何確保redo log 和binlog在一致性的,都寫入成功的。

還是以update為例:

start transaction;
update t_user set name = '路人甲Java' where user_id = 666;
update t_user set name = 'javacode2018' where user_id = 888;
commit;

一個事務中可能有很多操作,這些操作會寫很多binlog日誌,為了加快寫的速度,mysql先把整個過程中產生的binlog日誌先寫到記憶體中的binlog cache快取中,後面再將binlog cache中內容一次性持久化到binlog檔案中。一個事務的 binlog 是不能被拆開的,因此不論這個事務多大,也要確保一次性寫入。這就涉及到了 binlog cache 的儲存問題。系統給 binlog cache 分配了一片記憶體,每個執行緒一個,引數 binlog_cache_size 用於控制單個執行緒內 binlog cache 所佔記憶體的大小。如果超過了這個引數規定的大小,就要暫存到磁碟。

過程如下:

  1. mysql收到start transaction後,生成一個全域性的事務編號trx_id,比如trx_id=10

  2. user_id=666這個記錄我們就叫r1,user_id=888這個記錄叫r2

  3. 找到r1記錄所在的資料頁p1,將其從磁碟中載入到記憶體中

  4. 在記憶體中對p1進行修改

  5. 將p1修改操作記錄到redo log buffer中

  6. 將p1修改記錄流水記錄到binlog cache中

  7. 找到r2記錄所在的資料頁p2,將其從磁碟中載入到記憶體中

  8. 在記憶體中對p2進行修改

  9. 將p2修改操作記錄到redo log buffer中

  10. 將p2修改記錄流水記錄到binlog cache中

  11. mysql收到commit指令

  12. 將redo log buffer攜帶trx_id=10寫入到redo log檔案,持久化到磁碟,這步操作叫做redo log prepare,內容如下

    1.start trx=10;
    2.寫入rb1
    3.寫入rb2
    4.prepare trx=10;

    注意上面是prepare了,不是之前說的end了。

  13. 將binlog cache攜帶trx_id=10寫入到binlog檔案,持久化到磁碟

  14. 向redo log中寫入一條資料:end trx=10;表示redo log中這個事務完成了,這步操作叫做redo log commit

  15. 返回給客戶端更新成功

我們來分析一下上面過程可能出現的一些情況:

步驟10操作完成後,mysql宕機了

宕機之前,所有修改都位於記憶體中,mysql重啟之後,記憶體修改還未同步到磁碟,對磁碟資料沒有影響,所以無影響。

步驟12執行完畢之後,mysql宕機了

此時redo log prepare過程是寫入redo log檔案了,但是binlog寫入失敗了,此時mysql重啟之後會讀取redo log進行恢復處理,查詢到trx_id=10的記錄是prepare狀態,會去binlog中查詢trx_id=10的操作在binlog中是否存在,如果不存在,說明binlog寫入失敗了,此時可以將此操作回滾

步驟13執行完畢之後,mysql宕機

此時redo log prepare過程是寫入redo log檔案了,但是binlog寫入失敗了,此時mysql重啟之後會讀取redo log進行恢復處理,查詢到trx_id=10的記錄是prepare狀態,會去binlog中查詢trx_id=10的操作在binlog是存在的,然後接著執行上面的步驟14和15.

做一個總結

上面的過程設計比較好的地方,有2點

日誌先行,io順序寫,非同步操作,做到了高效操作

對資料頁,先在記憶體中修改,然後使用io順序寫的方式持久化到redo log檔案;然後非同步去處理redo log,將資料頁的修改持久化到磁碟中,效率非常高,整個過程,其實就是 MySQL 裡經常說到的 WAL 技術,WAL 的全稱是 Write-Ahead Logging,它的關鍵點就是先寫日誌,再寫磁碟。

兩階段提交確保redo log和binlog一致性

為了確保redo log和binlog一致性,此處使用了二階段提交技術,redo log 和binlog的寫分了3步走:

  1. 攜帶trx_id,redo log prepare到磁碟

  2. 攜帶trx_id,binlog寫入磁碟

  3. 攜帶trx_id,redo log commit到磁碟

上面3步驟,可以確保同一個trx_id關聯的redo log 和binlog的可靠性。

關於上面2點優秀的設計,我們平時開發的過程中也可以借鑑,下面舉2個常見的案例來學習一下。

案例:電商中資金賬戶高頻變動解決方案

電商中有賬戶表和賬戶流水錶,2個表結構如下:

drop table IF EXISTS t_acct;
create table t_acct(
  acct_id int primary key NOT NULL COMMENT '賬戶id',
  balance decimal(12,2) NOT NULL COMMENT '賬戶餘額',
  version INT NOT NULL DEFAULT 0 COMMENT '版本號,每次更新+1'
)COMMENT '賬戶表';

drop table IF EXISTS t_acct_data;
create table t_acct_data(
  id int AUTO_INCREMENT PRIMARY KEY COMMENT '編號',
  acct_id int primary key NOT NULL COMMENT '賬戶id',
  price DECIMAL(12,2) NOT NULL COMMENT '交易額',
  open_balance decimal(12,2) NOT NULL COMMENT '期初餘額',
  end_balance decimal(12,2) NOT NULL COMMENT '期末餘額'
) COMMENT '賬戶流水錶';

INSERT INTO t_acct(acct_id, balance, version) VALUES (1,10000,0);

上面向賬戶表t_acct插入了一條資料,餘額為10000,當我們下單成功或者充值的時候,會對上面2個表進行操作,會修改t_acct的資料,順便向t_acct_data表寫一條流水,這個t_acct_data表有個期初和期末的流水,關係如下:

end_balance = open_balance + price;
open_balance為操作業務時,t_acct表的balance的值。

如給賬戶1充值100,過程如下:

t1:開啟事務:start transaction;
t2:R1 = (select * from t_acct where acct_id = 1);
t3:建立幾個變數
    v_balance = R1.balance;
t4:update t_acct set balnce = v_balance+100,version = version + 1 where acct_id = 1;
t5:insert into t_acct_data(acct_id,price,open_balnace,end_balance) 
    values (1,100,#v_balance#,#v_balance+100#)
t6:提交事務:commit;

分析一下上面過程存在的問題:

我們開啟2個執行緒【thread1、thread2】模擬分別充值100,正常情況下資料應該是這樣的:

t_acct表記錄:
(1,10200,1);
t_acct_data表產生2條資料:
(1,100,10000,10100);
(2,100,10100,10200);

但是當2個執行緒同時執行到t2的時候獲取R1記錄資訊是一樣的,變數v_balance的值也一樣的,最後執行完成之後,資料變成了下面這樣:

t_acct表:1,10200
t_acct_data表產生2條資料:
1,100,10000,10100;
2,100,10100,10100;

導致t_acct_data產生的2條資料是一樣的,這種情況是有問題的,這就是併發導致的問題。

上篇文章中有說道樂觀鎖可以解決這種併發問題,有興趣的可以去看一下,過程如下:

t1:開啟事務start transaction
t2:R1 = (select * from t_acct where acct_id = 1);
t3:建立幾個變數
    v_version = R1.version;
    v_balance = R1.balance;
    v_open_balance = v_balance;
    v_balance = R1.balance + 100;
    v_open_balance = v_balance;
t3:對R1進行編輯
t4:執行更新操作
    int count = (update t_acct set balance = #v_balance#,version = version + 1 where acct_id = 1 and version = #v_version#);
t5:if(count==1){
        //向t_acct_data表寫入資料
        insert into t_acct_data(acct_id,price,open_balnace,end_balance) values (1,100,#v_open_balance#,#v_open_balance#)
        //提交事務
        commit;
    }else{
        //回滾事務
        rollback;
    }

上面的過程中,如果2個執行緒同時執行到t2看到的R1資料是一樣的,但是最後走到t4的時候會被資料庫加鎖,2個執行緒的update在mysql中會排隊執行,最後只有一個update的結果返回的影響行數是1,然後根據t5,會有一個會被回滾,另外一個被提交,避免了併發導致的問題。

我們分析一下上面過程會有什麼問題?

剛才上面也提到了,併發量大的時候,只有部分會成功,比如10個執行緒同時執行到t2的時候,其中只有1個會成功,其他9個都會失敗,併發量大的情況下失敗的概率比較高,這個大家可以併發測試一下,失敗率很高,下面我們繼續優化。

分析一下問題主要出現在寫t_acct_data上面,如果沒有這個表的操作,我們直接用一個update就完成了操作,速度是非常快的,上面我們學到的了mysql中先寫日誌,然後非同步刷盤的方式,此處我們也可以採用這種思路,先記錄一條交易日誌,然後非同步根據交易日誌將交易流水寫到t_acct_data表中。

那我們繼續優化,新增一個賬戶操作日誌表:

drop table IF EXISTS t_acct_log;
create table t_acct_log(
  id INT AUTO_INCREMENT PRIMARY KEY COMMENT '編號',
  acct_id int primary key NOT NULL COMMENT '賬戶id',
  price DECIMAL(12,2) NOT NULL COMMENT '交易額',
  status SMALLINT NOT NULL DEFAULT 0 COMMENT '狀態,0:待處理,1:處理成功'
) COMMENT '賬戶操作日誌表';

順便對t_acct標做一下改造,新增一個欄位old_balance,新結構如下:

drop table IF EXISTS t_acct;
create table t_acct(
  acct_id int primary key NOT NULL COMMENT '賬戶id',
  balance decimal(12,2) NOT NULL COMMENT '賬戶餘額',
  old_balance decimal(12,2) NOT NULL COMMENT '賬戶餘額(老的值)',
  version INT NOT NULL DEFAULT 0 COMMENT '版本號,每次更新+1'
)COMMENT '賬戶表';

INSERT INTO t_acct(acct_id, balance,old_balance,version) VALUES (1,10000,10000,0);

新增了一個old_balance欄位,這個欄位的值剛開始的時候和balance的值是一致的,後面會在job中進行改變,可以先向下看,後面有解釋

假設賬戶v_acct_id交易金額為v_price,過程如下:

t1.開啟事務:start transaction;
t2.insert into t_acct_log(acct_id,price,status) values (#v_acct_id#,#v_price#,0)
t3.int count = (update t_acct set balnce = v_balance+#v_price#,version = version+1 where acct_id = #v_acct_id# and v_balance+#v_price#>=0);
t6.if(count==1){
        //提交事務
        commit;
    }else{
        //回滾事務
        rollback;
    }

可以看到上面沒有記錄流水了,變成插入了一條日誌t_acct_log,後面我們非同步根據t_acct_log的資料來生成t_acct_data記錄。

上面這個操作支撐併發操作還是比較高的,測試了一下每秒500筆,並且都成功了,效率非常高。

新增一個job,查詢t_acct_log中狀態為0的記錄,然後遍歷進行一個個處理,處理過程如下:

假設t_acct_log中當前需要處理的記錄為L1
t1:開啟事務start transaction
t2:建立變數
    v_price = L1.price;
    v_acct_id = L1.acct_id;
t3:R1 = (select * from t_acct where acct_id = #v_acct_id#);
t4:建立幾個變數
    v_old_balance = R1.old_balance;
    v_open_balance = v_old_balance;
    v_old_balance = R1.old_balance + v_price;
    v_open_balance = v_old_balance;
t5:int count = (update t_acct set old_balance = #v_old_balance#,version = version + 1 where acct_id = #v_acct_id# and version = #v_version#);
t6:if(count==1){
        //更新t_acct_log的status置為1
        count = (update t_acct_log set status=1 where status=0 and id = #L1.id#);
    }

    if(count==1){
        //提交事務
        commit;
    }else{
        //回滾事務
        rollback;
    }

上面t5中update條件中加了version,t6中的update條件中加了status=0的操作,主要是為了防止併發操作修改可能會出錯的問題。

上面t_acct_log中所有status=0的記錄被處理完畢之後,t_acct表中的balance和old_balance會變為一致。

上面這種方式採用了先寫賬戶操作日誌,然後非同步對日誌進行操作,在生成流水,借鑑了mysql中的設計,大家也可以學習學習。

案例2:跨庫轉賬問題

此處我們使用mysql上面介紹的二階段提交來解決。

如從A庫的T1錶轉100到B庫的T1表。

我們建立一個C庫,在C庫新增一個轉賬訂單表,如:

drop table IF EXISTS t_transfer_order;
create table t_transfer_order(
  id int NOT NULL AUTO_INCREMENT primary key COMMENT '賬戶id',
  from_acct_id int NOT NULL COMMENT '轉出方賬戶',
  to_acct_id int NOT NULL COMMENT '轉入方賬戶',
  price decimal(12,2) NOT NULL COMMENT '轉賬金額',
  addtime int COMMENT '入庫時間(秒)',
  status SMALLINT NOT NULL DEFAULT 0 COMMENT '狀態,0:待處理,1:轉賬成功,2:轉賬失敗',
  version INT NOT NULL DEFAULT 0 COMMENT '版本號,每次更新+1'
) COMMENT '轉賬訂單表';

A、B庫加3張表,如:

drop table IF EXISTS t_acct;
create table t_acct(
  acct_id int primary key NOT NULL COMMENT '賬戶id',
  balance decimal(12,2) NOT NULL COMMENT '賬戶餘額',
  version INT NOT NULL DEFAULT 0 COMMENT '版本號,每次更新+1'
)COMMENT '賬戶表';

drop table IF EXISTS t_order;
create table t_order(
  transfer_order_id int primary key NOT NULL COMMENT '轉賬訂單id',
  price decimal(12,2) NOT NULL COMMENT '轉賬金額',
  status SMALLINT NOT NULL DEFAULT 0 COMMENT '狀態,1:轉賬成功,2:轉賬失敗',
  version INT NOT NULL DEFAULT 0 COMMENT '版本號,每次更新+1'
) COMMENT '轉賬訂單表';

drop table IF EXISTS t_transfer_step_log;
create table t_transfer_step_log(
  id int primary key NOT NULL COMMENT '賬戶id',
  transfer_order_id int NOT NULL COMMENT '轉賬訂單id',
  step SMALLINT NOT NULL COMMENT '轉賬步驟,0:正向操作,1:回滾操作',
  UNIQUE KEY (transfer_order_id,step)
) COMMENT '轉賬步驟日誌表';

t_transfer_step_log表用於記錄轉賬日誌操作步驟的,transfer_order_id,step上加了唯一約束,表示每個步驟只能執行一次,可以確保步驟的冪等性。

定義幾個變數:

v_from_acct_id:轉出方賬戶

v_to_acct_id:轉入方賬戶

v_price:交易金額

整個轉賬流程如下:

每個步驟都有返回值,返回值是陣列型別的,含義是:0:處理中(結果未知),1:成功,2:失敗

step1:建立轉賬訂單,訂單狀態為0,表示處理中
C1:start transaction;
C2:insert into t_transfer_order(from_acct_id,to_acct_id,price,addtime,status,version) 
    values(#v_from_acct_id#,#v_to_acct_id#,#v_price#,0,unix_timestamp(now()));
C3:獲取剛才insert成功的訂單id,放在變數v_transfer_order_id中
C4:commit;

step2:A庫操作如下
A1:AR1 = (select * from t_order where transfer_order_id = #v_transfer_order_id#);
A2:if(AR1!=null){
        return AR1.status==1?1:2;
    }
A3:start transaction;
A4:AR2 = (select 1 from t_acct where acct_id = #v_from_acct_id#);
A5:if(AR2.balance<v_price){
        //表示餘額不足,那轉賬肯定是失敗了,插入一個轉賬失敗訂單
        insert into t_order (transfer_order_id,price,status) values (#transfer_order_id#,#v_price#,2);
        commit;
        //返回失敗的狀態2
        return 2;
    }else{
        //通過樂觀鎖 & balance - #v_price# >= 0更新賬戶資金,防止併發操作
        int count = (update t_acct set balance = balance - #v_price#, version = version + 1 where acct_id = #v_from_acct_id# and balance - #v_price# >= 0 and version = #AR2.version#);
        //count為1表示上面的更新成功
        if(count==1){
            //插入轉賬成功訂單,狀態為1
            insert into t_order (transfer_order_id,price,status) values (#transfer_order_id#,#v_price#,1);
            //插入步驟日誌
            insert into t_transfer_step_log (transfer_order_id,step) values (#v_transfer_order_id#,1);
            commit;
            return 1;
        }else{
            //插入轉賬失敗訂單,狀態為2
            insert into t_order (transfer_order_id,price,status) values (#transfer_order_id#,#v_price#,2);
            commit;
            return 2;
        }
    }

step3:
    if(step2的結果==1){
        //表示A庫中扣款成功了
        執行step4;
    }else if(step2的結果==2){
        //表示A庫中扣款失敗了
        執行step6;
    }

step4:對B庫進行操作,如下:
B1:BR1 = (select * from t_order where transfer_order_id = #v_transfer_order_id#);
B2:if(BR1!=null){
    return BR1.status==1?1:2;
}else{
    執行B3;
}
B3:start transaction;
B4:BR2 = (select 1 from t_acct where acct_id = #v_to_acct_id#);
B5:int count = (update t_acct set balance = balance + #v_price#, version = version + 1 where acct_id = #v_to_acct_id# and version = #BR2.version#);
if(count==1){
    //插入訂單,狀態為1
    insert into t_order (transfer_order_id,price,status) values (#transfer_order_id#,#v_price#,1);
    //插入日誌
    insert into t_transfer_step_log (transfer_order_id,step) values (#v_transfer_order_id#,1);
    commit;
    return 1;
}else{
    //進入到此處說明有併發,返回0
    rollback;
    return 0;
}

step5:
    if(step4的結果==1){
        //表示B庫中加錢成功了
        執行step7;
    }

step6:對C庫操作(轉賬失敗,將訂單置為失敗)
C1:AR1 = (select 1 from t_transfer_order where id = #v_transfer_order_id#);
C2:if(AR1.status==1 || AR1.status=2){
        return AR1.status=1?"轉賬成功":"轉賬失敗";
    }
C3:start transaction;
C4:int count = (udpate t_transfer_order set status = 2,version = version+1 where id = #v_transfer_order_id# and version = version + #AR1.version#)
C5:if(count==1){
        commit;
        return "轉賬失敗";
    }else{
        rollback;
        return "處理中";
    }

step7:對C庫操作(轉賬成功,將訂單置為成功)
C1:AR1 = (select 1 from t_transfer_order where id = #v_transfer_order_id#);
C2:if(AR1.status==1 || AR1.status=2){
        return AR1.status=1?"轉賬成功":"轉賬失敗";
    }
C3:start transaction;
C4:int count = (udpate t_transfer_order set status = 1,version = version+1 where id = #v_transfer_order_id# and version = version + #AR1.version#)
C5:if(count==1){
        commit;
        return "轉賬成功";
    }else{
        rollback;
        return "處理中";
    }

還需要新增一個補償的job,處理C庫中狀態為0的超過10分鐘的轉賬訂單訂單,過程如下:

while(true){
    List list = select * from t_transfer_order where status = 0 and addtime+10*60<unix_timestamp(now());
    if(list為空){
        //插敘無記錄,退出迴圈
        break;
    }
    //迴圈遍歷list進行處理
    for(Object r:list){
        //呼叫上面的steap2進行處理,最終訂單狀態會變為1或者2
    }
}

說一下:這個job的處理有不好的地方,可能會死迴圈,這個留給大家去思考一下,如何解決?歡迎留言

Mysql系列目錄

  1. 第1篇:mysql基礎知識
  2. 第2篇:詳解mysql資料型別(重點)
  3. 第3篇:管理員必備技能(必須掌握)
  4. 第4篇:DDL常見操作
  5. 第5篇:DML操作彙總(insert,update,delete)
  6. 第6篇:select查詢基礎篇
  7. 第7篇:玩轉select條件查詢,避免採坑
  8. 第8篇:詳解排序和分頁(order by & limit)
  9. 第9篇:分組查詢詳解(group by & having)
  10. 第10篇:常用的幾十個函式詳解
  11. 第11篇:深入瞭解連線查詢及原理
  12. 第12篇:子查詢
  13. 第13篇:細說NULL導致的神坑,讓人防不勝防
  14. 第14篇:詳解事務
  15. 第15篇:詳解檢視
  16. 第16篇:變數詳解
  17. 第17篇:儲存過程&自定義函式詳解
  18. 第18篇:流程控制語句
  19. 第19篇:遊標詳解
  20. 第20篇:異常捕獲及處理詳解
  21. 第21篇:什麼是索引?
  22. 第22篇:mysql索引原理詳解
  23. 第23篇:mysql索引管理詳解
  24. 第24篇:如何正確的使用索引?
  25. 第25篇:sql中where條件在資料庫中提取與應用淺析
  26. 第26篇:聊聊mysql如何實現分散式鎖?

mysql系列大概有20多篇,喜歡的請關注一下,歡迎大家加我微信itsoku或者留言交流mysql相關技術!

相關推薦

Mysql高手系列 - 27mysql如何確保資料丟失的?我們借鑑這種設計思想實現熱點賬戶併發設計轉賬問題

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 歡迎大家加我微信itsoku一起交流java、演算法、資料庫相關技術。 這是Mysql系列第27篇。 本篇文章我們先來看一下mysql是如何確保資料不丟失的,通過本文我們可以瞭解mysql內部確保資料不丟失的原理,學習裡面優秀

Mysql高手系列 - 18mysql流程控制語句詳解(高手進階)

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 這是Mysql系列第18篇。 環境:mysql5.7.25,cmd命令中進行演示。 程式碼中被[]包含的表示可選,|符號分開的表示可選其一。 上一篇儲存過程&自定義函式,對儲存過程和自定義函式做了一個簡單的介紹,但是如

Mysql高手系列 - 19mysql遊標詳解,此技能可用於救火

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 這是Mysql系列第19篇。 環境:mysql5.7.25,cmd命令中進行演示。 程式碼中被[]包含的表示可選,|符號分開的表示可選其一。 需求背景 當我們需要對一個select的查詢結果進行遍歷處理的時候,如何實現呢? 此

Mysql高手系列 - 7玩轉select條件查詢,避免踩坑

這是Mysql系列第7篇。 環境:mysql5.7.25,cmd命令中進行演示。 電商中:我們想檢視某個使用者所有的訂單,或者想檢視某個使用者在某個時間段內所有的訂單,此時我們需要對訂單表資料進行篩選,按照使用者、時間進行過濾,得到我們期望的結果。 此時我們需要使用條件查詢來對指定表進行操作,我們需要了解sq

Mysql高手系列 - 8詳解排序和分頁(order by & limit),存在的坑

這是Mysql系列第8篇。 環境:mysql5.7.25,cmd命令中進行演示。 程式碼中被[]包含的表示可選,|符號分開的表示可選其一。 本章內容 詳解排序查詢 詳解limit limit存在的坑 分頁查詢中的坑 排序查詢(order by) 電商中:我們想檢視今天所有成交的訂單,按照交易額從高到低排序

Mysql高手系列 - 9詳解分組查詢,mysql分組有大坑!

這是Mysql系列第9篇。 環境:mysql5.7.25,cmd命令中進行演示。 本篇內容 分組查詢語法 聚合函式 單欄位分組 多欄位分組 分組前篩選資料 分組後篩選資料 where和having的區別 分組後排序 where & group by & having & order

Mysql高手系列 - 11深入瞭解連線查詢原理

這是Mysql系列第11篇。 環境:mysql5.7.25,cmd命令中進行演示。 當我們查詢的資料來源於多張表的時候,我們需要用到連線查詢,連線查詢使用率非常高,希望大家都務必掌握。 本文內容 笛卡爾積 內連線 外連線 左連線 右連線 表連線的原理 使用java實現連線查詢,加深理解 準備資料 2張表

Mysql高手系列 - 10常用的幾十個函式詳解,收藏慢慢看

這是Mysql系列第10篇。 環境:mysql5.7.25,cmd命令中進行演示。 MySQL 數值型函式 函式名稱 作 用 abs 求絕對值 sqrt 求二次方根 mod 求餘數 ceil 和 ceiling 兩個函式功能相同,都是返回不小於引數的最小整數,即向上取整 floo

Mysql高手系列 - 12子查詢詳解

這是Mysql系列第12篇。 環境:mysql5.7.25,cmd命令中進行演示。 本章節非常重要。 子查詢 出現在select語句中的select語句,稱為子查詢或內查詢。 外部的select查詢語句,稱為主查詢或外查詢。 子查詢分類 按結果集的行列數不同分為4種 標量子查詢(結果集只有一行一列) 列子查

Mysql高手系列 - 13細說NULL導致的神坑,讓人防不勝防

這是Mysql系列第13篇。 環境:mysql5.7.25,cmd命令中進行演示。 當資料的值為NULL的時候,可能出現各種意想不到的效果,讓人防不勝防,我們來看看NULL導致的各種神坑,如何避免? 比較運算子中使用NULL 認真看下面的效果 mysql> select 1>NULL; +--

Mysql高手系列 - 14詳解事務

這是Mysql系列第14篇。 環境:mysql5.7.25,cmd命令中進行演示。 開發過程中,會經常用到資料庫事務,所以本章非常重要。 本篇內容 什麼是事務,它有什麼用? 事務的幾個特性 事務常見操作指令詳解 事務的隔離級別詳解 髒讀、不可重複讀、可重複讀、幻讀詳解 演示各種隔離級別產生的現象 關於隔離級

Mysql高手系列 - 21什麼是索引?

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 這是Mysql系列第21篇。 本文開始連續3篇詳解mysql索引: 第1篇來說說什麼是索引? 第2篇詳解Mysql中索引的原理 第3篇結合索引詳解關鍵字explain 本文為索引第一篇:我們來了解一下什麼是索引? 路人在搞

Mysql高手系列 - 20異常捕獲處理詳解(實戰經驗)

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 這是Mysql系列第20篇。 環境:mysql5.7.25,cmd命令中進行演示。 程式碼中被[]包含的表示可選,|符號分開的表示可選其一。 需求背景 我們在寫儲存過程的時候,可能會出現下列一些情況: 插入的資料違反唯一約束

Mysql高手系列 - 22深入理解mysql索引原理,連載中

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 歡迎大家加我微信itsoku一起交流java、演算法、資料庫相關技術。 這是Mysql系列第22篇。 背景 使用mysql最多的就是查詢,我們迫切的希望mysql能查詢的更快一些,我們經常用到的查詢有: 按照id查詢唯一一條

Mysql高手系列 - 24如何正確的使用索引?【高手進階】

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 歡迎大家加我微信itsoku一起交流java、演算法、資料庫相關技術。 這是Mysql系列第24篇。 學習索引,主要是寫出更快的sql,當我們寫sql的時候,需要明確的知道sql為什麼會走索引?為什麼有些sql不走索引?sql

Mysql高手系列 - 26聊聊如何使用mysql實現分散式鎖

Mysql系列的目標是:通過這個系列從入門到全面掌握一個高階開發所需要的全部技能。 歡迎大家加我微信itsoku一起交流java、演算法、資料庫相關技術。 這是Mysql系列第26篇。 本篇我們使用mysql實現一個分散式鎖。 分散式鎖的功能 分散式鎖使用者位於不同的機器中,鎖獲取成功之後,才可以對共享資源

Mysql高手系列 - 4天DDL常見操作彙總

這是Mysql系列第4篇。 環境:mysql5.7.25,cmd命令中進行演示。 DDL:Data Define Language資料定義語言,主要用來對資料庫、表進行一些管理操作。 如:建庫、刪庫、建表、修改表、刪除表、對列的增刪改等等。 文中涉及到的語法用[]包含的內容屬於可選項,下面做詳細說明。 庫的管

Mysql高手系列 - 5天DML操作彙總,確定你都會?

這是Mysql系列第5篇。 環境:mysql5.7.25,cmd命令中進行演示。 DML(Data Manipulation Language)資料操作語言,以INSERT、UPDATE、DELETE三種指令為核心,分別代表插入、更新與刪除,是必須要掌握的指令,DML和SQL中的select熟稱CRUD(增刪

【搞定MySQL資料庫】7MySQL中的鎖全域性鎖、表鎖、行鎖

本文為本人學習極客時間《MySQL實戰45講》的學習筆記。 原文連結:https://time.geekbang.org/column/article/69862                  &n

【搞定MySQL資料庫】6索引的概述

本文為本人學習極客時間《MySQL實戰45講》的學習筆記。 原文連結:https://time.geekbang.org/column/article/69236                  &n