seat TCC 實戰(圖解_秒懂_史上最全)


文章很長,建議收藏起來,慢慢讀! Java 高並發 發燒友社群:瘋狂創客圈 奉上以下珍貴的學習資源:


推薦:入大廠 、做架構、大力提升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 {
    @Resource
    SeataStockServiceImpl 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);
    @Resource
    private DataSource dataSource;



    /**
     * 執行秒殺下單
     *
     * @param inDto
     * @param skuId
     * @return
     */
//    @Transactional
    public 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 {
    @Resource
    TCCOrderServiceImpl 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);
    @Resource
    private 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 {


    @Resource
    TCCSeckillServiceImpl 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 {

    @Autowired
    private OrderApi orderApi;
    @Autowired
    private StockApi stockApi ;

    /**
     * 減庫存,下訂單
     */
    //開啟全局事務(重點) 使用 seata 的全局事務
    @GlobalTransactional
    public 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 transaction
	at 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM