基本思路
先決條件
- 支持本地ACID事務的關系數據庫。
- 通過JDBC訪問數據庫的Java應用程序。
整體機制
從兩個階段提交協議的演變:
- 階段1:在同一本地事務中提交業務數據和回滾日志,然后釋放本地鎖和連接資源。
- 階段2:
- 對于提交情況,異步快速地完成工作。
- 對于回滾情況,請根據階段1中創建的回滾日志進行補償。
寫隔離
- 在全局鎖,必須犯的階段1的本地事務之前獲取。
- 如果未獲取全局鎖,則不應提交本地事務。
- 如果失敗,一個事務將嘗試多次獲取全局鎖,但如果超時,則會發生超時,并回滾本地事務并釋放本地鎖。
例如:
兩個事務tx1和tx2試圖更新表a的字段m。m的原始值為1000。
tx1首先開始,開始本地事務,獲取本地鎖,然后執行更新操作:m = 1000-100 =900。tx1必須在提交本地事務之前獲取全局鎖,然后再提交本地事務并釋放本地鎖。
接下來,tx2開始本地事務,獲取本地鎖,執行更新操作:m = 900-100 =800。在tx2可以提交本地事務之前,它必須獲取全局鎖,但是全局鎖可能由tx1持有,因此tx2會重試。在tx1執行全局提交并釋放全局鎖之后,tx2可以獲取全局鎖,然后可以提交本地事務并釋放本地鎖。
正在上傳…重新上傳取消
參見上圖,tx1在階段2中執行全局提交并釋放全局鎖,tx2獲取全局鎖并提交本地事務。
正在上傳…重新上傳取消
參見上圖,如果tx1要執行全局回滾,則它必須獲取本地鎖以還原階段1的更新操作。
但是,現在本地鎖由希望獲取全局鎖的tx2持有,因此tx1無法回滾,但是它將嘗試多次,直到tx2獲取全局鎖超時,然后tx2回滾本地事務并釋放本地鎖定后,tx1可以獲取本地鎖定,并成功執行分支回滾。
因為全局鎖在整個過程中都由tx1保持,所以沒有寫臟問題。
讀取隔離
本地數據庫的隔離級別被讀為commit commit或更高,因此全局事務的默認隔離級別被讀為uncommitted。
如果當前需要讀取全局事務的隔離級別,則Fescar可以通過SELECT FOR UPDATE語句來實現它。
轉存失敗重新上傳取消
在全局鎖是SELECT FOR UPDATE語句的執行過程中被應用,如果全局鎖被其他事務持有,該交易將釋放本地鎖重試執行SELECT FOR UPDATE語句。在整個過程中,查詢將被阻塞,直到獲取了全局鎖為止,如果獲取了鎖,則意味著另一個全局事務已提交,因此全局事務的隔離級別被讀取為commit。
出于性能方面的考慮,Fescar只對SELECT FOR UPDATE做代理工作。對于常規的SELECT語句,什么也不做。
工作過程
以一個例子來說明它。
業務表:product
領域 | 類型 | 鍵 |
---|---|---|
ID | bigint(20) | PRI |
名稱 | varchar(100) | ? |
以來 | varchar(100) | ? |
AT模式下分支事務的sql:
update product set name = 'GTS' where name = 'TXC';
階段1
處理:
- 解析sql:知道sql類型為更新操作,表名稱為product,條件為name ='TXC',依此類推。
- 在更新之前查詢數據(在圖像之前命名):為了找到將要更新的數據,請通過上述where條件生成查詢語句。
select id, name, since from product where name = 'TXC';
得到了“之前的圖像”:
ID | 名稱 | 以來 |
---|---|---|
1個 | TXC | 2014年 |
- 執行更新sql:更新名稱等于“ GTS”的記錄。
- 更新后查詢數據(以圖像命名):通過更新前圖像數據的主鍵找到記錄。
select id, name, since from product where id = 1;
得到了殘像:
ID | 名稱 | 以來 |
---|---|---|
1個 | GTS | 2014年 |
- 插入回滾日志:使用前后圖像以及SQL語句相關信息構建回滾日志,然后插入table中
UNDO_LOG
。
{"branchId": 641789253,"undoItems": [{"afterImage": {"rows": [{"fields": [{"name": "id","type": 4,"value": 1}, {"name": "name","type": 12,"value": "GTS"}, {"name": "since","type": 12,"value": "2014"}]}],"tableName": "product"},"beforeImage": {"rows": [{"fields": [{"name": "id","type": 4,"value": 1}, {"name": "name","type": 12,"value": "TXC"}, {"name": "since","type": 12,"value": "2014"}]}],"tableName": "product"},"sqlType": "UPDATE"}],"xid": "xid:xxx"
}
- 在本地提交之前,該事務將應用提交給TC,以獲取表產品中主鍵等于1的記錄的全局鎖。
- 提交本地事務:在同一本地事務中提交PRODUCT表的更新和UNDO_LOG表的插入。
- 向TC報告步驟7的結果。
階段2-回退案例
- 從TC收到回滾請求后,開始本地事務,執行以下操作。
- 通過XID和分支ID檢索UNDO LOG。
- 驗證數據:將UNDO LOG中更新后的圖像數據與當前數據進行比較,如果存在差異,則表示該數據已被當前事務以外的操作所更改,應采用不同的策略進行處理,其他將對此進行詳細描述文獻。
- 基于UNDO LOG中的前映像和業務SQL的相關信息,生成回滾SQL語句。
update product set name = 'TXC' where id = 1;
- 提交本地事務,將本地事務的執行結果(分支事務的回滾結果)報告給TC。
階段2-提交案例
- 收到TC的提交請求后,將請求放入工作隊列,立即將成功返回TC。
- 在隊列中執行異步工作的階段,將以批處理方式刪除UNDO LOG。
附錄
撤消日志表
UNDO_LOG表:不同數據庫的數據類型略有不同。
對于MySQL示例:
領域 | 類型 |
---|---|
branch_id | bigint PK |
西德 | varchar(100) |
rollback_info | 長毛 |
log_status | tinyint |
log_created | 約會時間 |
log_modified | 約會時間 |
分機 | varchar(100) |
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'increment id',`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',`xid` varchar(100) NOT NULL COMMENT 'global transaction id',`context` varchar(128) NOT NULL COMMENT 'undo_log context,such as serialization',`rollback_info` longblob NOT NULL COMMENT 'rollback info',`log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',`log_created` datetime NOT NULL COMMENT 'create datetime',`log_modified` datetime NOT NULL COMMENT 'modify datetime',`ext` varchar(100) DEFAULT NULL COMMENT 'reserved field',PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='AT transaction mode undo table';