seata at模式死锁

背景

mysql版本为5.7,在mysql的系统表中,有一个lock_deadlocks的指标

SELECT * FROM INFORMATION_SCHEMA.INNODB_METRICS WHERE name LIKE '%deadlock%'

该指标的count值记录了数据库实例从启动到现在的死锁次数,我们通过普罗米修斯的mysql探针,采集了这个指标,制定了告警策略

https://github.com/prometheus/mysqld_exporter

线上收到死锁告警后, 发现是seata at模式所用的seata库发生的死锁,同时相应的业务系统出现阻塞卡顿情况

排查过程

先通过命令拿到死锁的信息, 拿到后恢复系统,然后慢慢查问题

show engine innodb status

拿到关键信息,日志比较长,贴一下关键点


*** (1) TRANSACTION:
delete from lock_table where xid = '172.16..:8091:440483815345885184' and branch_id in (440483816079888385,440483816235077633,440483816327356417,440483816373497857,440483816398655489,440483816746790913,440483817065558017)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 301 page no 6 n bits 144 index PRIMARY of table `seata`.`lock_table` trx id 338204135 lock_mode X waiting


*** (2) TRANSACTION:
insert into lock_table(xid, transaction_id, branch_id, resource_id, table_name, pk, row_key, gmt_create, gmt_modified) values ('172.16.22.13:8091:440483789588664320', 440483789588664320, 440483816818094081, 'jdbc:mysql://172.16..:3306/op', 't_order_agent_profit_explanation', '53641', 'jdbc:mysql://172.16..:3306/op^^^t_order_agent_profit_explanation^^^53641', now(), now())
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 301 page no 6 n bits 144 index PRIMARY of table `seata`.`lock_table` trx id 338204130 lock_mode X locks rec but not gap
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 301 page no 6 n bits 144 index PRIMARY of table `seata`.`lock_table` trx id 338204130 lock_mode X locks gap before rec insert intention waiting

我们来大概阅读一下这个死锁日志:

  1. 第一个事务执行一个delete语句,但是他在等待一个主键索引的写锁
  2. 第二个事务在执行insert语句,他现在持有一个写锁,但是在等待一个间隙锁的意向锁
    到这里大致猜测一下,可能是上面的delete语句会持有间隙锁,导致和insert语句的冲突,我们先看下这个语句持有锁的情况
begin;
delete from lock_table where xid = '172.16..:8091:440483815345885184' and branch_id in (440483816079888385,440483816235077633,440483816327356417,440483816373497857,440483816398655489,440483816746790913,440483817065558017);
SELECT * from `performance_schema`.data_locks;

在这里插入图片描述

这里有个关键点,supremum pseudo-record,这一行的意思就是对主键索引的最大索引值加锁,所以后续的insert都要等待这个锁,这里就产生竞争条件了,在并发的情况下就会出现死锁

解决

  1. 修改隔离级别为RC,不再添加间隙锁
  2. 升级seata版本,seata社区已经复现了这个bug,把数据库的链接修改为RC隔离级别了, 参照
https://github.com/seata/seata/pull/5638
``
### Seata AT 模式下的全局事务锁表机制 在 SeataAT 模式中,为了确保分布式事务的一致性和隔离性,引入了全局锁的概念。当多个分支事务并发执行时,可能会发生数据冲突的情况。为了避免这种情况的发生并保持一致性,Seata 使用了一种基于全局写排他锁的设计。 #### 全局锁的作用 全局锁主要用于防止不同全局事务之间的脏写覆盖问题。具体来说: - 当某个分支事务在一阶段修改了某些记录之后,这些被修改的数据会被加上全局写排他锁。 - 如果其他全局事务尝试在同一时刻对该条目进行更新,则会因为无法获取相应的全局锁而阻塞等待或立即失败返回给客户端应用层处理逻辑[^3]。 #### 如何实现全局锁? 对于每一个涉及数据变更的操作(INSERT/UPDATE/DELETE),Seata 都会在一阶段向 TC (Transaction Coordinator) 注册该操作所影响到的所有行级主键作为锁定资源;而在二阶段提交成功后则释放掉之前持有的所有锁资源。如果遇到回滚情况,则直接撤销已经申请成功的锁而不做任何实际更改动作[^1]。 此外,在一些特殊场景下还可以通过 `@GlobalLock` 注解来显式指定需要加锁的对象范围,即使这不是严格意义上的分布式事务也可以利用这套机制来进行简单的悲观锁控制[^2]。 ```java @Service public class AccountService { @Transactional public void transfer(@Param("from") String from, @Param("to") String to, @Param("amount") BigDecimal amount){ accountMapper.decreaseBalance(from, amount); // 显式添加全局锁以保护转账过程中的余额变化 @GlobalLock(key = "account:" + from) accountMapper.increaseBalance(to, amount); } } ``` #### 处理死锁与超时 考虑到长时间持有锁可能导致性能瓶颈甚至死锁风险,Seata 提供了一些参数配置用于调整最大等待时间和重试次数等行为特性。例如可以通过设置 `max-global-lock-timeout` 来限定每次请求最长能等待多久才能获得所需资源的使用权。一旦超过这个时间限制仍未拿到合适的锁对象就会触发相应错误提示告知调用方采取进一步措施解决当前困境。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值