问题描述

生产环境,一个简单的事务方法,提交失败,报 Global lock wait timeout

伪代码如下:

@GlobalTransactional(rollbackFor = Exception.class,timeoutMills = 30000,lockRetryInternal=3000,lockRetryTimes=10)
@Override
public Boolean cancel(Long id, Long userId, Long companyId) {
// 保存业务数据
...
// 启动工作流
wkflAppServiceProvider.startProcess(....);
...
}

异常如下:

org.springframework.dao.QueryTimeoutException: JDBC commit; Global lock wait timeout; nested exception is io.seata.rm.datasource.exec.LockWaitTimeoutException: Global lock wait timeout

Caused by: io.seata.rm.datasource.exec.LockWaitTimeoutException: Global lock wait timeout
at io.seata.rm.datasource.exec.LockRetryController.sleep(LockRetryController.java:63)
at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.doRetryOnLockConflict(ConnectionProxy.java:346)
at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.execute(ConnectionProxy.java:335)
at io.seata.rm.datasource.ConnectionProxy.commit(ConnectionProxy.java:187)
at org.springframework.jdbc.datasource.DataSourceTransactionManager.doCommit(DataSourceTransactionManager.java:333)
... 57 more
Caused by: io.seata.rm.datasource.exec.LockConflictException: get global lock fail, xid:10.222.248.60:8091:2900686326154883760, lockKeys:wkfl_app_auth:12326192,12326193;act_ge_bytearray:6515890,6515891;act_re_procdef:rediscountClickSubmitCancel_UserTask_0yze6zf_5:1:6515892;act_re_deployment:6515889
at io.seata.rm.datasource.ConnectionProxy.recognizeLockKeyConflictException(ConnectionProxy.java:159)
at io.seata.rm.datasource.ConnectionProxy.processGlobalTransactionCommit(ConnectionProxy.java:252)
at io.seata.rm.datasource.ConnectionProxy.doCommit(ConnectionProxy.java:230)
at io.seata.rm.datasource.ConnectionProxy.lambda$commit$0(ConnectionProxy.java:188)
at io.seata.rm.datasource.ConnectionProxy$LockRetryPolicy.doRetryOnLockConflict(ConnectionProxy.java:343)
... 60 more

看到“LockWaitTimeoutException: Global lock wait timeout” 我以为是有资源竞争,导致加锁等待超时。但这个疑虑很快被打消了,因为这是必现的一个问题,每次执行到这个方法都报错,甚至在下班后系统没有人使用的情况下,我一点,还是报这个错,这个时候可以确定就我一个人在用,而且查了数据库没有被锁定的数据和事务,所以应该不是资源竞争导致的获取锁等待超时。

于是,我开始翻源码

数据源被代理,本地事务提交走的是io.seata.rm.datasource.ConnectionProxy#commit()

doCommit()方法是放在io.seata.rm.datasource.ConnectionProxy.LockRetryPolicy#execute()中执行的

由于我们这里client.rm.lock.retryPolicyBranchRollbackOnConflict配置的是false,所以这里失败后会重试,如果是true,则不重试

看到这里,我们找到了“Global lock wait timeout”的出处了,原来是因为doCommit()执行过程中抛异常了,再重试次数用完后就会抛出LockWaitTimeoutException。因此,LockWaitTimeoutException只是表象,并不是最根本的原因,根本原因是doCommit()报错了。

接着doCommit()看,我们知道,分支事务提交要先注册,注册成功后才能提交。而注册就是要获取全局锁。

通过观察DEBUG日志,发现保存业务数据部分的分支注册都是成功的

日志太多,截取关键部分,如图所示

结合代码,发现真正的报错发生在调用远程服务启动工作流那里

查看工作流相关服务的日志,发现一开始分支注册就失败了,部分关键日志如下

工作流那个服务里面,分支注册返回的信息是:Global lock acquire failed xid = ....

幸好之前读过Seata的源码,不然此时肯定手足无措

于是,翻开Seata Server的源码,看看为什么返回的消息是这样的

直接快进到io.seata.server.transaction.at.ATCore#branchSessionLock()

具体参见我的另一篇博文 https://www.cnblogs.com/cjsblog/p/16878067.html

在这里,我们找到了“Global lock acquire failed”这个报错信息的出处

证明,在执行branchSession.lock(autoCommit, skipCheckLock)的时候要么失败返回false,要么抛异常了

根据配置,这里是db,所以是DataBaseLockManager

接下来进入到LockStoreDataBaseDAO#acquireLock()开始真正加锁了(往表里插数据)

io.seata.server.storage.db.lock.LockStoreDataBaseDAO#acquireLock(java.util.List<io.seata.core.store.LockDO>, boolean, boolean)

方法太长,不细看了,重点看加锁的SQL语句

由于用的MySQL,所以是io.seata.core.store.db.sql.lock.MysqlLockStoreSql

最终拼接好的SQL是这样的:

insert into lock_table (xid, transaction_id, branch_id, resource_id, table_name, pk, row_key, gmt_create, gmt_modified) values (?, ?, ?, ?, ?, ?, ?, now(), now(), ?)

如果插入成功,则返回true,表示加锁成功,对应的分支事务获取锁成功,分支事务注册成功,皆大欢喜

补充一下,这里面有很多地方配置项

至此,整个分支事务获取锁的逻辑我们都清楚了

接下来,再回头看看lock_table表的各个列,首先看看怎么从客户端传过来的一个lockKey变成List<LockDO>的

因此,假设客户端发过来的lockKey是这样:

offer message: xid=10.222.248.60:8091:2900686326154883760,branchType=AT,resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow,lockKey=wkfl_app_auth:12326192,12326193;act_ge_bytearray:6515890,6515891;act_re_procdef:rediscountClickSubmitCancel_UserTask_0yze6zf_5:1:6515892;act_re_deployment:6515889

那么这里得到的List<LockDO>就是这样的:

LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=wkfl_app_auth, pk=12326192, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^wkfl_app_auth^^^12326192)
LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=wkfl_app_auth, pk=12326193, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^wkfl_app_auth^^^12326193)
LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=act_ge_bytearray, pk=6515890, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^act_ge_bytearray^^^6515890)
LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=act_ge_bytearray, pk=6515891, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^act_ge_bytearray^^^6515891)
LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=act_re_procdef, pk=rediscountClickSubmitCancel_UserTask_0yze6zf_5:1:6515892, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^act_re_procdef^^^rediscountClickSubmitCancel_UserTask_0yze6zf_5:1:6515892)
LockDO(xid=10.222.248.60:8091:2900686326154883760, transactionId=153490553438167612, branchId=153490553438162971, resourceId=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow, tableName=act_re_deployment, pk=6515889, status=0, rowKey=jdbc:mysql://xxx.xxx.com:23306/newdraft_workflow^^^act_re_deployment^^^6515889)

往lock_table表里就会插入这6条数据,最后查看Seata服务端日志发现,是由于字段长度问题,导致插入失败,于是加锁失败

原来pk字段长度只有32,row_key字段长度只有128,修改后的只读长度如上图所示

最后的最后,补充一个知识点

1、在整个全局事务中,每条SQL语句执行的时候都是一样的流程,先注册获取全局锁,然后才能提交,注意是每条SQL

2、所有的RM在执行本地操作的时候都是一样的流程,因为数据源被Seata代理,所以在执行各自本地的逻辑时,设计到数据库操作的,都是首先更改连接为非自动提交,然后进行分支注册,注册成功后连接可以提交了,最后报告分支状态。

3、分支注册会传lockKey,注册的过程就是获取全局锁的过程,也就是对这些lockKey包含的数据加锁的过程。如果store.lock.mode=db的话,就是向lock_table表插数据。

4、在整个全局事务执行过程中,有多少次数据库操作就有多少次分支注册、提交、报告。因为每次跟数据库的交互都要先获取Connection,最终获取到的都是ConnectionProxy

5、 所有RM(Resource Manager)本地事务都提交成功的话,整个全局事务算是提交成功了

Connection conn = getConnection.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeUpdate();

Seata锁等待超时问题排查的更多相关文章

  1. GC Ergonomics间接引发的锁等待超时问题排查分析

    1. 问题背景 上周线上某模块出现锁等待超时,如下图所示: 我虽然不是该模块负责人,但出于好奇,也一起帮忙排查定位问题. 这里的业务背景就是在执行到某个地方时,需要去表中插入一批数据,这批数据需要根据 ...

  2. RDS MySQL InnoDB 锁等待和锁等待超时的处理

    https://help.aliyun.com/knowledge_detail/41705.html 1. Innodb 引擎表行锁等待和等待超时发生的场景 2.Innodb 引擎行锁等待情况的处理 ...

  3. MySQL事务锁等待超时 Lock wait timeout exceeded; try restarting transaction

    工作中处理定时任务分发消息时出现的问题,在查找并解决问题的时候,将相关的问题博客收集整理,在此记录下,以便之后再遇到相同的问题,方便查阅. 问题场景 问题出现的场景: 在消息队列处理消息时,同一事务内 ...

  4. Mysql事物锁等待超时(Lock wait timeout exceeded; try restarting transaction)

    一.问题描述 在做查询语句时,MySQL 抛出了这样的异常:锁等待超时 Lock wait timeout exceeded; try restarting transaction,是当前事务在等待其 ...

  5. mysql 开发进阶篇系列 13 锁问题(关于表锁,死锁示例,锁等待设置)

    一. 什么时候使用表锁 对于INNODB表,在绝大部分情况下都应该使用行锁.在个别特殊事务中,可以考虑使用表锁(建议). 1. 事务需要更新大部份或全部数据,表又比较大,默认的行锁不仅使这个事务执行效 ...

  6. MySQL Backup--Xtrabackup备份设置锁等待问题

    问题描述 innobackupex备份过程需要保证备份数据一致性,通过刷新表缓存和加全局读锁(FLUSH TABLES WITH READ LOCK)获取备份位点,而为防止锁等待超时,会先设置: SE ...

  7. MySQL - 锁等待超时与information_schema的三个表

    引用地址:https://blog.csdn.net/J080624/article/details/80596958 回顾一下生产中的一次MySQL异常,Cause: java.sql.SQLExc ...

  8. MySQL锁等待与死锁问题分析

    前言: 在 MySQL 运维过程中,锁等待和死锁问题是令各位 DBA 及开发同学非常头痛的事.出现此类问题会造成业务回滚.卡顿等故障,特别是业务繁忙的系统,出现死锁问题后影响会更严重.本篇文章我们一起 ...

  9. 排查MySQL事务没有提交导致 锁等待 Lock wait timeout exceeded

    解决思路: select * from information_schema.innodb_trx 之后找到了一个一直没有提交的只读事务, kill 到了对应的线程后ok 了. 转载自:http:// ...

  10. MySQL锁等待分析【1】

    场景: 昨天业务系统上遇到了数据库慢的问题(对dcsdba.og_file_audit表的insert 慢&超时).分析后定位到是由于锁等待造成的.分析过程如下: 1.执行show proce ...

随机推荐

  1. bzoj 4176

    题意:求$\sum_{i=1}^{n}\sum_{j=1}^{n}d(ij)$ 首先推一发式子: $\sum_{i=1}^{n}\sum_{j=1}^{n}d(ij)$ 有一个结论:$d(nm)=\s ...

  2. mysql之数据库操作-第一篇

    1.数据库的创建 mysql> create database if not exists myTestDB; Query OK, 1 row affected (0.00 sec) 2.查看可 ...

  3. Gstreamer 随笔

    1. Gstreamer在Ubuntu上需要安装得全部库: gstreamer1.0-alsa - GStreamer plugin for ALSAgstreamer1.0-clutter-3.0  ...

  4. echarts区域选择(brush)默认开启选择

    api.dispatchAction({ // 刷选模式的开关.使用此 action 可将当前鼠标变为可刷选状态. 事实上,点击 toolbox 中的 brush 按钮时,就是通过这个 action, ...

  5. python使用openpyxl读取合并单元格的值(转)

    目录问题:解决思路:问题:假设在test.xlsx的"Sheet1"工作表中,A1:D3区域的值如下:要求给定指定的行.列以及对应的工作表作为参数,能够正确解析合并单元格,获取指定 ...

  6. 攻防(一)tomcat CVE-2020-1938 ftp 21端口

    TOMCAT kali自带POE msf6 > use auxiliary/admin/http/tomcat_ghostcat set RHOST 10.98.xx.xx msf6 auxil ...

  7. 如何在Windows 10上使用VS2019 编译C

    一.安装VS 2019 首先VS官网上下载免费的VS2019 community.我们的目的是在Windows上使用该软件编译C,如下图所示,把使用C++的桌面开发打上勾 二.创建.C 项目 1.打开 ...

  8. AES加密 php7版本 openssl_encrypt 遇到的坑

    与前端对接api ,解密不了前端加密的数据. 问题描述: 1.前端用 cryptojs  加密的 密钥是24位 , 2.后端用的php7的 openssl_encrypt  同密钥来进行解密,发现解密 ...

  9. Spring Boot基础依赖

    <properties> <java.version>1.8</java.version></properties><parent> < ...

  10. C语言II—作业03

    1.作业头 这个作业属于哪个课程 https://edu.cnblogs.com/campus/zswxy/SE2020-3 这个作业要求在哪里 https://edu.cnblogs.com/cam ...