Mysql系列的目標是:通過這個系列從入門到全面掌握一個高級開發所需要的全部技能。
歡迎大家加我微信itsoku一起交流java、算法、數據庫相關技術。
這是Mysql系列第27篇。
本篇文章我們先來看一下mysql是如何確保數據不丟失的,通過本文我們可以了解mysql內部確保數據不丟失的原理,學習里面優秀的設計要點,然后我們再借鑒這些優秀的設計要點進行實踐應用,加深理解。
預備知識
- mysql內部是使用b+樹的結構將數據存儲在磁盤中,b+樹中節點對應mysql中的頁,mysql和磁盤交互的最小單位為頁,頁默認情況下為16kb,表中的數據記錄存儲在b+樹的葉子節點中,當我們需要修改、刪除、插入數據時,都需要按照頁來對磁盤進行操作。
- 磁盤順序寫比隨機寫效率要高很多,通常我們使用的是機械硬盤,機械硬盤寫數據的時候涉及磁盤尋道、磁盤旋轉尋址、數據寫入的時間,耗時比較長,如果是順序寫,省去了尋道和磁盤旋轉的時間,效率會高幾個數量級。
- 內存中數據讀寫操作比磁盤中數據讀寫操作速度高好多個數量級。
mysql確保數據不丟失原理分析
我們來思考一下,下面這條語句的執行過程是什么樣的:
start transaction;
update t_user set name = '路人甲Java' where user_id = 666;
commit;
按照正常的思路,通常過程如下:
- 找到user_id=666這條記錄所在的頁p1,將p1從磁盤加載到內存中
- 在內存中對p1中user_id=666這條記錄信息進行修改
- mysql收到commit指令
- 將p1頁寫入磁盤
- 給客戶端返回更新成功
上面過程可以確保數據被持久化到了磁盤中。
我們將需求改一下,如下:
start transaction;
update t_user set name = '路人甲Java' where user_id = 666;
update t_user set name = 'javacode2018' where user_id = 888;
commit;
來看一下處理過程:
- 找到user_id=666這條記錄所在的頁p1,將p1從磁盤加載到內存中
- 在內存中對p1中user_id=666這條記錄信息進行修改
- 找到user_id=888這條記錄所在的頁p2,將p2從磁盤加載到內存中
- 在內存中對p2中user_id=888這條記錄信息進行修改
- mysql收到commit指令
- 將p1頁寫入磁盤
- 將p2頁寫入磁盤
- 給客戶端返回更新成功
上面過程我們看有什么問題
- 假如6成功之后,mysql宕機了,此時p1修改已寫入磁盤,但是p2的修改還未寫入磁盤,最終導致user_id=666的記錄被修改成功了,user_id=888的數據被修改失敗了,數據是有問題的
- 上面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中所有連接共享的內存區域,可以被重復使用。
-
mysql收到start transaction后,生成一個全局的事務編號trx_id,比如trx_id=10
-
user_id=666這個記錄我們就叫r1,user_id=888這個記錄叫r2
-
找到r1記錄所在的數據頁p1,將其從磁盤中加載到內存中
-
在內存中找到r1在p1中的位置,然后對p1進行修改(這個過程可以描述為:將p1中的pos_start1到pos_start2位置的值改為v1),這個過程我們記為rb1(內部包含事務編號trx_id),將rb1放入redo log buffer數組中,此時p1的信息在內存中被修改了,和磁盤中p1的數據不一樣了
-
找到r2記錄所在的數據頁p2,將其從磁盤中加載到內存中
-
在內存中找到r2在p2中的位置,然后對p2進行修改(這個過程可以描述為:將p2中的pos_start1到pos_start2位置的值改為v2),這個過程我們記為rb2(內部包含事務編號trx_id),將rb2放入redo log buffer數組中,此時p2的信息在內存中被修改了,和磁盤中p2的數據不一樣了
-
此時redo log buffer數組中有2條記錄[rb1,rb2]
-
mysql收到commit指令
-
將redo log buffer數組中內容寫入到redo log文件中,寫入的內容:
1.start trx=10; 2.寫入rb1 3.寫入rb2 4.end trx=10;
-
返回給客戶端更新成功。
上面過程執行完畢之后,數據是這樣的:
- 內存中p1、p2頁被修改了,還未同步到磁盤中,此時內存中數據頁和磁盤中數據頁是不一致的,此時內存中數據頁我們稱為臟頁
- 對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文件中的內容進行處理,處理過程如下:
-
讀取redo log信息,讀取一個完整的trx_id對應的信息,然后進行處理
-
比如讀取到了trx_id=10的完整內容,包含了start end,表示這個事務操作是成功的,然后繼續向下
-
判斷p1在內存中是否存在,如果存在,則直接將p1信息寫到p1所在的磁盤中;如果p1在內存中不存在,則將p1從磁盤加載到內存,通過redo log中的信息在內存中對p1進行修改,然后將其寫到磁盤中
上面的update之后,p1在內存中是存在的,並且p1是已經被修改過的,可以直接刷新到磁盤中。
如果上面的update之后,mysql宕機,然后重啟了,p1在內存中是不存在的,此時系統會讀取redo log文件中的內容進行恢復處理。
-
將redo log文件中trx_id=10的占有的空間標記為已處理,這塊空間會被釋放出來可以重復利用了
-
如果第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 所占內存的大小。如果超過了這個參數規定的大小,就要暫存到磁盤。
過程如下:
-
mysql收到start transaction后,生成一個全局的事務編號trx_id,比如trx_id=10
-
user_id=666這個記錄我們就叫r1,user_id=888這個記錄叫r2
-
找到r1記錄所在的數據頁p1,將其從磁盤中加載到內存中
-
在內存中對p1進行修改
-
將p1修改操作記錄到redo log buffer中
-
將p1修改記錄流水記錄到binlog cache中
-
找到r2記錄所在的數據頁p2,將其從磁盤中加載到內存中
-
在內存中對p2進行修改
-
將p2修改操作記錄到redo log buffer中
-
將p2修改記錄流水記錄到binlog cache中
-
mysql收到commit指令
-
將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了。
-
將binlog cache攜帶trx_id=10寫入到binlog文件,持久化到磁盤
-
向redo log中寫入一條數據:
end trx=10;
表示redo log中這個事務完成了,這步操作叫做redo log commit -
返回給客戶端更新成功
我們來分析一下上面過程可能出現的一些情況:
步驟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步走:
攜帶trx_id,redo log prepare到磁盤
攜帶trx_id,binlog寫入磁盤
攜帶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篇:mysql基礎知識
- 第2篇:詳解mysql數據類型(重點)
- 第3篇:管理員必備技能(必須掌握)
- 第4篇:DDL常見操作
- 第5篇:DML操作匯總(insert,update,delete)
- 第6篇:select查詢基礎篇
- 第7篇:玩轉select條件查詢,避免采坑
- 第8篇:詳解排序和分頁(order by & limit)
- 第9篇:分組查詢詳解(group by & having)
- 第10篇:常用的幾十個函數詳解
- 第11篇:深入了解連接查詢及原理
- 第12篇:子查詢
- 第13篇:細說NULL導致的神坑,讓人防不勝防
- 第14篇:詳解事務
- 第15篇:詳解視圖
- 第16篇:變量詳解
- 第17篇:存儲過程&自定義函數詳解
- 第18篇:流程控制語句
- 第19篇:游標詳解
- 第20篇:異常捕獲及處理詳解
- 第21篇:什么是索引?
- 第22篇:mysql索引原理詳解
- 第23篇:mysql索引管理詳解
- 第24篇:如何正確的使用索引?
- 第25篇:sql中where條件在數據庫中提取與應用淺析
- 第26篇:聊聊mysql如何實現分布式鎖?
mysql系列大概有20多篇,喜歡的請關注一下,歡迎大家加我微信itsoku或者留言交流mysql相關技術!