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

posted @ 2021-09-20 14:25  疯狂创客圈  阅读(366)  评论(0编辑  收藏  举报