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

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注