zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    如何去除ecshop标题和网站底部的Powered by ECShop
    ecshop标签
    安装Wamp后 Apache无法启动的解决方法
    wamp5中的apache不能启动,80端口被占用
    iOS UI-AlertView(警示框)和ActionSheet(选择框、操作表单)
    iOS UI-三种简单的动画设置
    iOS UI-IOS开发中Xcode的一些使用技巧
    iOS UI-九宫格
    iOS开发-开发文档安装
    iOS UI-创建空项目
  • 原文地址:https://www.cnblogs.com/crazymakercircle/p/15314246.html
Copyright © 2011-2022 走看看