文章很长,而且持续更新,建议收藏起来,慢慢读! 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
AT模式的依赖的还是依赖单个服务或单个数据源自己的事务控制(分支事务),采用的是wal的思想,提交事务的时候同时记录undolog,如果全局事务成功,则删除undolog,如果失败,则使用undolog的数据回滚分支事务,最后删除undolog。
TCC模式的特点是不再依赖于undolog,但是还是采用2阶段提交的方式:
第一阶段使用prepare尝试事务提交,第二阶段使用commit或者rollback让事务提交或者回滚。
引用网上一张TCC原理的参考图片
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()方法对全局事务进行回滚
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;
}
}
package com.crazymaker.cloud.seata.seckill.impl;
@Configuration
@Slf4j
@Service
public class SeataStockServiceImpl {
private Map statementMap = new ConcurrentHashMap<>(100);
private Map connectionMap = new ConcurrentHashMap<>(100);
@Resource
private DataSource dataSource;
/**
* 执行秒杀下单
*
* @param inDto
* @param skuId
* @return
*/
// @Transactional
public boolean minusStock(BusinessActionContext inDto, Long skuId, Long userId) {
Map 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;
}
}
@RestController
@RequestMapping("/api/tcc/order/")
@Api(tags = "秒杀练习 订单管理")
public class SeataTCCOrderController {
@Resource
TCCOrderServiceImpl seckillOrderService;
/**
* 执行秒杀的操作
*
*
* {
* "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;
}
}
@Slf4j
@Service
public class TCCOrderServiceImpl {
private Map statementMap = new ConcurrentHashMap<>(100);
private Map 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 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;
}
}
@RestController
@RequestMapping("/api/seckill/seglock/")
@Api(tags = "秒杀练习分布式事务 版本")
public class SeckillTCCController {
@Resource
TCCSeckillServiceImpl seataSeckillServiceImpl;
/**
* 执行秒杀的操作
* 减库存,下订单
*
* {
* "exposedKey": "4b70903f6e1aa87788d3ea962f8b2f0e",
* "newStockNum": 10000,
* "seckillSkuId": 1247695238068177920,
* "seckillToken": "0f8459cbae1748c7b14e4cea3d991000",
* "userId": 37
* }
*
* @return
*/
@ApiOperation(value = "秒杀")
@PostMapping("/doSeckill/v1")
RestOut doSeckill(@RequestBody SeckillDTO dto) {
seataSeckillServiceImpl.doSeckill(dto);
return RestOut.success(dto).setRespMsg("秒杀成功");
}
}
@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的分布式事务的回滚实验
使用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方法;
现象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