文章很長,建議收藏起來,慢慢讀! Java 高並發 發燒友社群:瘋狂創客圈 奉上以下珍貴的學習資源:
-
免費贈送 經典圖書:《Java高並發核心編程(卷1)》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
-
免費贈送 經典圖書:《Java高並發核心編程(卷2)》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
-
免費贈送 經典圖書:《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模式源碼解讀( 圖解+秒懂+史上最全)
閱讀此文之前,請先閱讀 :
參考鏈接
系統架構知識圖譜(一張價值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