seat TCC 实战(图解_秒懂_史上最全)
文章很長,而且持續更新,建議收藏起來,慢慢讀! Java 高并發 發燒友社群:瘋狂創客圈(總入口) 奉上以下珍貴的學習資源:
- 免費贈送 經典圖書 : 極致經典 + 社群大片好評 《 Java 高并發 三部曲 》 面試必備 + 大廠必備 + 漲薪必備
- 免費贈送 經典圖書 : 《Netty Zookeeper Redis 高并發實戰》 面試必備 + 大廠必備 +漲薪必備 (加尼恩領取)
- 免費贈送 經典圖書 : 《SpringCloud、Nginx高并發核心編程》 面試必備 + 大廠必備 + 漲薪必備 (加尼恩領取)
- 免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 (加尼恩領取)
推薦:入大廠 、做架構、大力提升Java 內功 的 精彩博文
| 入大廠 、做架構、大力提升Java 內功 必備的精彩博文 | 2021 秋招漲薪1W + 必備的精彩博文 |
|---|---|
| 1:Redis 分布式鎖 (圖解-秒懂-史上最全) | 2:Zookeeper 分布式鎖 (圖解-秒懂-史上最全) |
| 3: Redis與MySQL雙寫一致性如何保證? (面試必備) | 4: 面試必備:秒殺超賣 解決方案 (史上最全) |
| 5:面試必備之:Reactor模式 | 6: 10分鐘看懂, Java NIO 底層原理 |
| 7:TCP/IP(圖解+秒懂+史上最全) | 8:Feign原理 (圖解) |
| 9:DNS圖解(秒懂 + 史上最全 + 高薪必備) | 10:CDN圖解(秒懂 + 史上最全 + 高薪必備) |
| 11: 分布式事務( 圖解 + 史上最全 + 吐血推薦 ) | 12:seata AT模式實戰(圖解+秒懂+史上最全) |
| 13:seata 源碼解讀(圖解+秒懂+史上最全) | 14:seata TCC模式實戰(圖解+秒懂+史上最全) |
| Java 面試題 30個專題 , 史上最全 , 面試必刷 | 阿里、京東、美團… 隨意挑、橫著走!!! |
|---|---|
| 1: JVM面試題(史上最強、持續更新、吐血推薦) | 2:Java基礎面試題(史上最全、持續更新、吐血推薦 |
| 3:架構設計面試題 (史上最全、持續更新、吐血推薦) | 4:設計模式面試題 (史上最全、持續更新、吐血推薦) |
| 17、分布式事務面試題 (史上最全、持續更新、吐血推薦) | 一致性協議 (史上最全) |
| 29、多線程面試題(史上最全) | 30、HR面經,過五關斬六將后,小心陰溝翻船! |
| 9.網絡協議面試題(史上最全、持續更新、吐血推薦) | 更多專題, 請參見【 瘋狂創客圈 高并發 總目錄 】 |
| SpringCloud 精彩博文 | |
|---|---|
| nacos 實戰(史上最全) | sentinel (史上最全+入門教程) |
| SpringCloud gateway (史上最全) | 更多專題, 請參見【 瘋狂創客圈 高并發 總目錄 】 |
seata AT模式源碼解讀( 圖解+秒懂+史上最全)
閱讀此文之前,請先閱讀 :
分布式事務( 圖解 + 史上最全 + 吐血推薦 )
seata AT模式實戰(圖解+秒懂+史上最全)
參考鏈接
系統架構知識圖譜(一張價值10w的系統架構知識圖譜)
https://www.processon.com/view/link/60fb9421637689719d246739
秒殺系統的架構
https://www.processon.com/view/link/61148c2b1e08536191d8f92f
Seata TCC基本原理
AT模式的依賴的還是依賴單個服務或單個數據源自己的事務控制(分支事務),采用的是wal的思想,提交事務的時候同時記錄undolog,如果全局事務成功,則刪除undolog,如果失敗,則使用undolog的數據回滾分支事務,最后刪除undolog。
Seata TCC模式的流程圖
TCC模式的特點是不再依賴于undolog,但是還是采用2階段提交的方式:
第一階段使用prepare嘗試事務提交,第二階段使用commit或者rollback讓事務提交或者回滾。
引用網上一張TCC原理的參考圖片
Seata TCC 事務的3個操作
TCC 將事務提交分為 Try - Confirm - Cancel 3個操作。
其和兩階段提交有點類似,Try為第一階段,Confirm - Cancel為第二階段,是一種應用層面侵入業務的兩階段提交。
| 操作方法 | 含義 |
|---|---|
| Try | 預留業務資源/數據效驗 |
| Confirm | 確認執行業務操作,實際提交數據,不做任何業務檢查,try成功,confirm必定成功,需保證冪等 |
| Cancel | 取消執行業務操作,實際回滾數據,需保證冪等 |
其核心在于將業務分為兩個操作步驟完成。不依賴 RM 對分布式事務的支持,而是通過對業務邏輯的分解來實現分布式事務。
下面還以銀行轉賬例子來說明
假設用戶user表中有兩個字段:可用余額(available_money)、凍結余額(frozen_money)
-
A扣錢對應服務A(ServiceA)
-
B加錢對應服務B(ServiceB)
-
轉賬訂單服務(OrderService)
-
業務轉賬方法服務(BusinessService)
ServiceA,ServiceB,OrderService都需分別實現try(),confirm(),cancle()方法,方法對應業務邏輯如下
| ServiceA | ServiceB | OrderService | |
|---|---|---|---|
| try() | 校驗余額(并發控制) 凍結余額+1000 余額-1000 | 凍結余額+1000 | 創建轉賬訂單,狀態待轉賬 |
| confirm() | 凍結余額-1000 | 余額+1000 凍結余額-1000 | 狀態變為轉賬成功 |
| cancle() | 凍結余額-1000 余額+1000 | 凍結余額-1000 | 狀態變為轉賬失敗 |
其中業務調用方BusinessService中就需要調用
-
ServiceA.try()
-
ServiceB.try()
-
OrderService.try()
1、當所有try()方法均執行成功時,對全局事物進行提交,即由事物管理器調用每個微服務的confirm()方法
2、 當任意一個方法try()失敗(預留資源不足,抑或網絡異常,代碼異常等任何異常),由事物管理器調用每個微服務的cancle()方法對全局事務進行回滾
10WQPS秒殺的TCC分布式事務架構
庫存服務
controller
package com.crazymaker.cloud.seata.seckill.controller;@Slf4j
@RestController
@RequestMapping("/api/tcc/sku/")
@Api(tags = "商品庫存")
public class SeataTCCStockController {@ResourceSeataStockServiceImpl seckillSkuStockService;/*** minusStock 秒殺庫存** @return 商品 skuDTO*/@PostMapping("/minusStock/v1")@ApiOperation(value = "減少秒殺庫存")boolean minusStock(@RequestBody BusinessActionContext actionContext,@RequestParam("sku_id") Long skuId, @RequestParam("uid") Long uId) {boolean result = seckillSkuStockService.minusStock(actionContext, skuId,uId);return result;}@ApiOperation(value = "提交")@PostMapping("/commit/v1")boolean commit(@RequestBody BusinessActionContext actionContext) {boolean result = seckillSkuStockService.commit(actionContext);return result;}@ApiOperation(value = "回滾")@PostMapping("/rollback/v1")boolean rollback(@RequestBody BusinessActionContext actionContext) {boolean result = seckillSkuStockService.rollback(actionContext);return result;}}
service
package com.crazymaker.cloud.seata.seckill.impl;@Configuration
@Slf4j
@Service
public class SeataStockServiceImpl {private Map<String, Statement> statementMap = new ConcurrentHashMap<>(100);private Map<String, Connection> connectionMap = new ConcurrentHashMap<>(100);@Resourceprivate DataSource dataSource;/*** 執行秒殺下單** @param inDto* @param skuId* @return*/
// @Transactionalpublic boolean minusStock(BusinessActionContext inDto, Long skuId, Long userId) {Map<String, Object> params = inDto.getActionContext();try {log.info("減庫存, prepare, xid:{}", inDto.getXid());Connection connection = dataSource.getConnection();connection.setAutoCommit(false);int stock = 0;PreparedStatement pstmt = null;try {pstmt = connection.prepareStatement("SELECT `sku_id` , `stock_count` FROM `seckill_sku` WHERE `sku_id`=?");pstmt.setLong(1, skuId);ResultSet resultSet = pstmt.executeQuery();if (resultSet.next()) {stock = resultSet.getInt("stock_count");}resultSet.close();} finally {if (pstmt != null) {pstmt.close();}}if (stock<=0) {log.info("減庫存, prepare 失敗, xid:{}", inDto.getXid());if (null != connection) {connection.close();connection.commit();}throw BusinessException.builder().errMsg("庫存不夠").build();}String sql = "UPDATE `seckill_sku` SET `stock_count` = `stock_count` -1 WHERE `sku_id` = ?;";PreparedStatement stmt = connection.prepareStatement(sql);stmt.setLong(1, skuId);stmt.executeUpdate();statementMap.put(inDto.getXid(), stmt);connectionMap.put(inDto.getXid(), connection);} catch (SQLException e) {log.error("庫存失敗:", e);return false;}return true;}public boolean commit(BusinessActionContext dto) {String xid = dto.getXid();log.info("減庫存, commit, xid:{}", xid);PreparedStatement statement = (PreparedStatement) statementMap.get(xid);Connection connection = connectionMap.get(xid);try {//判斷一下,防止空懸掛,具備冪等性if (null != connection) {connection.rollback();}} catch (SQLException e) {log.error("提交失敗:", e);return false;} finally {try {statementMap.remove(xid);connectionMap.remove(xid);if (null != statement) {statement.close();}if (null != connection) {connection.close();}} catch (SQLException e) {log.error("減庫存回滾事務后歸還連接失敗:", e);}}return true;}public boolean rollback(BusinessActionContext dto) {String xid = dto.getXid();log.info("減庫存, rollback, xid:{}", xid);PreparedStatement statement = (PreparedStatement) statementMap.get(xid);Connection connection = connectionMap.get(xid);try {if (null != connection) {connection.commit();}} catch (SQLException e) {log.error("回滾失敗:", e);return false;} finally {try {statementMap.remove(xid);connectionMap.remove(xid);if (null != statement) {statement.close();}if (null != connection) {connection.close();}} catch (SQLException e) {log.error("減庫存提交事務后歸還連接池失敗:", e);}}return true;}}
訂單服務
controller
@RestController
@RequestMapping("/api/tcc/order/")
@Api(tags = "秒殺練習 訂單管理")
public class SeataTCCOrderController {@ResourceTCCOrderServiceImpl seckillOrderService;/*** 執行秒殺的操作* <p>* <p>* {* "exposedKey": "4b70903f6e1aa87788d3ea962f8b2f0e",* "newStockNum": 10000,* "seckillSkuId": 1157197244718385152,* "seckillToken": "0f8459cbae1748c7b14e4cea3d991000",* "userId": 37* }** @return*/@ApiOperation(value = "下訂單")@PostMapping("/addOrder/v1")boolean addOrder(@RequestBody BusinessActionContext actionContext, @RequestParam("sku_id") Long skuId, @RequestParam("uid") Long uId) {boolean orderDTO = seckillOrderService.addOrder(actionContext, skuId,uId);return orderDTO;}@ApiOperation(value = "下訂單提交")@PostMapping("/commit/v1")boolean commit(@RequestBody BusinessActionContext actionContext) {boolean orderDTO = seckillOrderService.commitAddOrder(actionContext);return orderDTO;}@ApiOperation(value = "下訂單回滾")@PostMapping("/rollback/v1")boolean rollback(@RequestBody BusinessActionContext actionContext) {boolean orderDTO = seckillOrderService.rollbackAddOrder(actionContext);return orderDTO;}}
service
@Slf4j
@Service
public class TCCOrderServiceImpl {private Map<String, Statement> statementMap = new ConcurrentHashMap<>(100);private Map<String, Connection> connectionMap = new ConcurrentHashMap<>(100);@Resourceprivate DataSource dataSource;private IdGenerator idGenerator;public IdGenerator getIdGenerator() {if (null == idGenerator) {idGenerator = CommonSnowflakeIdGenerator.getFromMap("tcc_order");}return idGenerator;}/*** 執行秒殺下單** @param inDto* @return*/
// @Transactional //開啟本地事務// @GlobalTransactional//不,開啟全局事務(重點) 使用 seata 的全局事務public boolean addOrder(BusinessActionContext inDto, Long skuId, Long userId) {Map<String, Object> params = inDto.getActionContext();// long skuId = (Long) params.get("sku");
// Long userId = (Long) params.get("user");Long id = getIdGenerator().nextId();try {Connection connection = dataSource.getConnection();connection.setAutoCommit(false);boolean isExist = false;log.info("檢查是否已經下單過");PreparedStatement pstmt = null;try {pstmt = connection.prepareStatement("SELECT * FROM `seckill_order` WHERE `user_id` =?");pstmt.setLong(1, userId);ResultSet resultSet = pstmt.executeQuery();if (resultSet.next()) {isExist = true;}resultSet.close();} finally {if (pstmt != null) {pstmt.close();}}if (isExist) {log.info("已經下單過");if (null != connection) {try {connection.close();connection.commit();}catch (Throwable t){}}throw BusinessException.builder().errMsg("已經秒殺過了").build();}log.info("pass: 檢查是否已經下單過");String sql = "INSERT INTO `seckill_order`(`order_id`, `sku_id`, `status`, `user_id`) VALUES( ?, ?, 1, ?)";PreparedStatement stmt = connection.prepareStatement(sql);stmt.setLong(1, id);stmt.setLong(2, skuId);stmt.setLong(3, userId);stmt.executeUpdate();statementMap.put(inDto.getXid(), stmt);connectionMap.put(inDto.getXid(), connection);log.info("prepare 下單 完成");return true;} catch (SQLException e) {log.error("保存訂單失敗:", e);return false;}}public boolean commitAddOrder(BusinessActionContext dto) {String xid = dto.getXid();log.info("提交 下訂單, commit, xid:{}", xid);PreparedStatement statement = (PreparedStatement) statementMap.get(xid);Connection connection = connectionMap.get(xid);try {if (null != connection) {connection.commit();}} catch (SQLException e) {log.error("提交失敗:", e);return false;} finally {try {statementMap.remove(xid);connectionMap.remove(xid);if (null != statement) {statement.close();}if (null != connection) {connection.close();}} catch (SQLException e) {log.error("下訂單提交事務后歸還連接池失敗:", e);}}return true;}public boolean rollbackAddOrder(BusinessActionContext dto) {String xid = dto.getXid();log.info("回滾 下訂單, rollback, xid:{}", xid);PreparedStatement statement = (PreparedStatement) statementMap.get(xid);Connection connection = connectionMap.get(xid);try {//判斷一下,防止空懸掛,具備冪等性if (null != connection) {connection.rollback();}} catch (SQLException e) {log.error("回滾失敗:", e);return false;} finally {try {statementMap.remove(xid);connectionMap.remove(xid);if (null != statement) {statement.close();}if (null != connection) {connection.close();}} catch (SQLException e) {log.error("下訂單回滾事務后歸還連接失敗:", e);}}return true;}
}
秒殺服務
controller
@RestController
@RequestMapping("/api/seckill/seglock/")
@Api(tags = "秒殺練習分布式事務 版本")
public class SeckillTCCController {@ResourceTCCSeckillServiceImpl seataSeckillServiceImpl;/*** 執行秒殺的操作* 減庫存,下訂單* <p>* {* "exposedKey": "4b70903f6e1aa87788d3ea962f8b2f0e",* "newStockNum": 10000,* "seckillSkuId": 1247695238068177920,* "seckillToken": "0f8459cbae1748c7b14e4cea3d991000",* "userId": 37* }** @return*/@ApiOperation(value = "秒殺")@PostMapping("/doSeckill/v1")RestOut<SeckillDTO> doSeckill(@RequestBody SeckillDTO dto) {seataSeckillServiceImpl.doSeckill(dto);return RestOut.success(dto).setRespMsg("秒殺成功");}}
service
@Slf4j
@Service
public class TCCSeckillServiceImpl {@Autowiredprivate OrderApi orderApi;@Autowiredprivate StockApi stockApi ;/*** 減庫存,下訂單*///開啟全局事務(重點) 使用 seata 的全局事務@GlobalTransactionalpublic boolean doSeckill(@RequestBody SeckillDTO dto) {String xid = RootContext.getXID();log.info("------->分布式操作開始");BusinessActionContext actionContext = new BusinessActionContext();actionContext.setXid(xid);Long skuId=dto.getSeckillSkuId();Long uId=dto.getUserId();//遠程方法 扣減庫存log.info("------->扣減庫存開始storage中");boolean result = stockApi.prepare(actionContext,skuId,uId);if (!result) {throw new RuntimeException("扣減庫存失敗");}result = orderApi.prepare(actionContext,skuId,uId);if (!result) {throw new RuntimeException("保存訂單失敗");}log.info("------->分布式下訂單操作完成");
// throw new RuntimeException("調用2階段提交的rollback方法");return true;}
}
以下兩個實驗,請參見配套視頻
基于TCC的分布式事務的提交實驗
基于TCC的分布式事務的回滾實驗
Seata TCC 事務的常見問題
冪等控制
使用TCC時要注意Try - Confirm - Cancel 3個操作的冪等控制,網絡原因,或者重試操作都有可能導致這幾個操作的重復執行
業務實現過程中需重點關注冪等實現,講到冪等,以上述TCC轉賬例子中confirm()方法來說明
在confirm()方法中
余額-1000,凍結余額-1000,這一步是實現冪等性的關鍵,你會怎么做?
大家在自己系統里操作資金賬戶時,為了防止并發情況下數據不一致的出現,肯定會避免出現這種代碼.
因為這本質上是一個 讀-改-寫的過程,不是原子的,在并發情況下會出現數據不一致問題
所以最簡單的做法是
這利用了數據庫行鎖特性解決了并發情況下的數據不一致問題,但是TCC中,單純使用這個方法適用么?
答案是不行的,該方法能解決并發單次操作下的扣減余額問題,但是不能解決多次操作帶來的多次扣減問題,假設我執行了兩次,按這種方案,用戶賬戶就少了2000塊
那么具體怎么做?
上訴轉賬例子中,可以引入轉賬訂單狀態來做判斷,若訂單狀態為已支付,則直接return
當然,新建一張去重表,用訂單id做唯一建,若插入報錯返回也是可以的,不管怎么樣,核心就是保證,操作冪等性
空回滾
如下圖所示,事務協調器在調用TCC服務的一階段Try操作時,可能會出現因為丟包而導致的網絡超時,此時事務協調器會觸發二階段回滾,調用TCC服務的Cancel操作;
TCC服務在未收到Try請求的情況下收到Cancel請求,這種場景被稱為空回滾;TCC服務在實現時應當允許空回滾的執行;
那么具體代碼里怎么做呢?
分析下,如果try()方法沒執行,那么訂單一定沒創建,所以cancle方法里可以加一個判斷,如果上下文中訂單編號orderNo不存在或者訂單不存在,直接return
核心思想就是 回滾請求處理時,如果對應的具體業務數據為空,則返回成功
當然這種問題也可以通過中間件層面來實現,如,在第一階段try()執行完后,向一張事務表中插入一條數據(包含事務id,分支id),cancle()執行時,判斷如果沒有事務記錄則直接返回,但是現在還不支持
防懸掛
如下圖所示,事務協調器在調用TCC服務的一階段Try操作時,可能會出現因網絡擁堵而導致的超時,此時事務協調器會觸發二階段回滾,調用TCC服務的Cancel操作;
在此之后,擁堵在網絡上的一階段Try數據包被TCC服務收到,出現了二階段Cancel請求比一階段Try請求先執行的情況;
用戶在實現TCC服務時,應當允許空回滾,但是要拒絕執行空回滾之后到來的一階段Try請求;
這里又怎么做呢?
可以在二階段執行時插入一條事務控制記錄,狀態為已回滾,這樣當一階段執行時,先讀取該記錄,如果記錄存在,就認為二階段回滾操作已經執行,不再執行try方法;
解決實驗過程中MYSQL出現死鎖問題
MYSQL出現死鎖的現象
現象1:Lock wait timeout exceeded; try restarting transaction
com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transactionat com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:123)at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953)at com.mysql.cj.jdbc.ClientPreparedStatement.executeQuery(ClientPreparedStatement.java:1003)at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_executeQuery(FilterChainImpl.java:3240)at com.alibaba.druid.filter.FilterEventAdapter.preparedStatement_executeQuery(FilterEventAdapter.java:465)at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_executeQuery(FilterChainImpl.java:3237)at com.alibaba.druid.wall.WallFilter.preparedStatement_executeQuery(WallFilter.java:647)
直接執行減少庫存的語句:
UPDATE `seckill_sku` SET `stock_count` = `stock_count` -1 WHERE `sku_id` =1247822053000613888
超時,并且 報錯:
如何排查?
MYSQL出現死鎖,首先查詢information_schema.innodb_trx表,查看哪些mysql查詢線程ID導致的,
SELECT * FROM information_schema.innodb_trx
SELECT * FROM information_schema.innodb_trx 命令是用來查看當前運行的所以事務:
說明:
FORMATION_SCHEMA提供對數據庫元數據的訪問、關于MySQL服務器的信息,如數據庫或表的名稱、列的數據類型或訪問權限。其中有一個關于InnoDB數據庫引擎表的集合,里面有記錄數據庫事務和鎖的相關表,InnoDB INFORMATION_SCHEMA表可以用來監視正在進行的InnoDB活動,在它們變成問題之前檢測低效,或者對性能和容量問題進行故障排除。在實際開發和應用中,會碰到和數據庫事務相關的問題,比如事務一直未結束,出現行鎖,表鎖以及死鎖等情況,這時我們就需要有一個快速定位問題行之有效的方法,所以我們來系統了解下INFORMATION_SCHEMA和定位事務問題。
記錄如下:
INNODB_TRX表提供了關于當前在InnoDB中執行的每個事務(不包括只讀事務)的信息,包括事務是否等待鎖、事務何時啟動以及事務正在執行的SQL語句(如果有的話)。INNODB_TRX表有以下字段:
| Field | Comment |
|---|---|
| TRX_ID | 自增id |
| TRX_WEIGHT | 事務權重,反映(但不一定是準確的計數)事務更改的行數和鎖定的行數。為了解決死鎖,InnoDB選擇權重最小的事務作為要回滾的“受害者” |
| TRX_STATE | 事務執行狀態。允許的值包括運行(RUNNING)、鎖等待(LOCK WAIT)、回滾(ROLLING BACK)和提交(COMMITTING)。 |
| TRX_STARTED | 事務開始時間 |
| TRX_REQUESTED_LOCK_ID | 事務當前等待的鎖的ID,如果TRX_STATE為LOCK WAIT;否則無效。要獲取關于鎖的詳細信息,請將此列與INNODB_LOCKS表的LOCK_ID列關聯 |
| TRX_WAIT_STARTED | 事務開始等待鎖的時間,如果TRX_STATE為鎖等待(LOCK WAIT);否則無效。 |
| TRX_MYSQL_THREAD_ID | MySql事務線程id,要獲取關于線程的詳細信息,與INFORMATION_SCHEMA PROCESSLIST表的ID列關聯 |
| TRX_QUERY | 事務正在執行的SQL語句 |
| TRX_OPERATION_STATE | 事務當前操作 |
| TRX_TABLES_IN_USE | 處理此事務的當前SQL語句使用的InnoDB表的數量 |
| TRX_TABLES_LOCKED | 當前SQL語句具有行鎖(row locks)的InnoDB表的數量(因為這些是行鎖(row locks),而不是表鎖(table locks),所以表通常仍然可以由多個事務讀寫,盡管有些行被鎖定了) |
| TRX_LOCK_STRUCTS | 事務保留的鎖的數量 |
| TRX_LOCK_MEMORY_BYTES | 此事務在內存中的鎖結構占用的總大小 |
| TRX_ROWS_LOCKED | 此事務鎖定的近似數目或行。該值可能包括物理上存在但對事務不可見的刪除標記行 |
| TRX_ROWS_MODIFIED | 此事務中修改和插入的行數量 |
| TRX_CONCURRENCY_TICKETS | 指示當前事務在換出之前可以做多少工作的值,由innodb_concurrency_tickets系統變量指定 |
| TRX_ISOLATION_LEVEL | 事務隔離級別 |
| TRX_UNIQUE_CHECKS | 是否為當前事務打開或關閉唯一性檢查 |
| TRX_FOREIGN_KEY_CHECKS | 是否為當前事務打開或關閉外鍵檢查 |
| TRX_ADAPTIVE_HASH_LATCHED | 自適應哈希索引是否被當前事務鎖定 |
| TRX_ADAPTIVE_HASH_TIMEOUT | 是否立即放棄自適應哈希索引的搜索鎖存器,還是在來自MySQL的調用之間保留它 |
| TRX_IS_READ_ONLY | 值為1表示只讀事務 |
| TRX_AUTOCOMMIT_NON_LOCKING | 值1表示事務是一個SELECT語句,它不使用FOR UPDATE或LOCK IN SHARED MODE子句,并且在執行時啟用了autocommit,因此事務將只包含這一條語句。當這個列和TRX_IS_READ_ONLY都為1時,InnoDB優化事務,以減少與更改表數據的事務相關的開銷。 |
操作步驟
使用如下語句查看事務,找到狀態為RUNNING的記錄
SELECT * FROM information_schema.INNODB_TRX;
通過trx_mysql_thread_id: xxx的去查詢information_schema.processlist找到執行事務的客戶端請求的SQL線程
select * from information_schema.PROCESSLIST WHERE ID in( '219','218');
根據我們拿到的線程id去查,可以獲取到具體的執行sql
select * from performance_schema.events_statements_current
where THREAD_ID in (select THREAD_ID from performance_schema.threads where PROCESSLIST_ID in( '219','218'))
結果如下:
問題就已經出來了,這兩個in字句,導致死鎖。
說明:
如果以上根據SQL分析不出來問題,我們需要從我們系統來進行定位,此時需要保存“案發現場”,數據庫中處于RUNNING的事務先不要結束掉,然后根據上面定位的進程對應的項目來跟蹤線程的執行情況,可以利用jconsole或者jmc來跟蹤線程的執行活動,或者用jstack來跟蹤。
結束線程
在執行結果中可以看到是否有表鎖等待或者死鎖,如果有死鎖發生,可以通過下面的命令來殺掉當前運行的事務:
KILL thread id;
KILL 后面的數字指的是 trx_mysql_thread_id 值。
KILL '219','218'
線程ID是23464106,通過information_schema.processlist查看對應的記錄,可以從中看到連接的IP地址和用戶等信息,特別是里面的INFO字段。
最簡單的死鎖避免方案:
沒有其它的辦法,只能再次檢查代碼。
所有事務中出現了問題, 需要return的地方一定需要加上回滾,最后對執行結果的判斷時,如果有一個結果未成功就需要回滾。
總結
分布式事務的TCC模式和AT模式的本質區別是一個是2階段提交,一個是交易補償。
seata框架對AT模式的支持是非常方便的,但是對TCC模式的支持,最大的就是自動觸發commit和prepare方法,真正的實現還是需要開發人員自己做。
大家有更好的實現2階段事務提交的方法,歡迎指點。
參考文檔:
seata 官方文檔地址:
http://seata.io/zh-cn/docs/overview/what-is-seata.html
https://www.cnblogs.com/babycomeon/p/11504210.html
https://www.cnblogs.com/javashare/p/12535702.html
https://blog.csdn.net/qq853632587/article/details/111356009
https://blog.csdn.net/qq_35721287/article/details/103573862
https://www.cnblogs.com/anhaogoon/p/13033986.html
https://blog.51cto.com/u_15072921/2606182
https://blog.csdn.net/weixin_45661382/article/details/105539999
https://blog.csdn.net/f4761/article/details/89077400
https://blog.csdn.net/qq_27834905/article/details/107353159
https://zhuanlan.zhihu.com/p/266584169
總結
以上是生活随笔為你收集整理的seat TCC 实战(图解_秒懂_史上最全)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 疾病预测和天气分析练习赛
- 下一篇: 百度开源深度学习平台Paddle