5

Seata 全局锁等待超时 问题排查 - 废物大师兄

 1 year ago
source link: https://www.cnblogs.com/cjsblog/p/17266281.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Seata 全局锁等待超时 问题排查

生产环境,一个简单的事务方法,提交失败,报 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()

874963-20230328154453747-712636067.png

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

874963-20230328175649591-1472640450.png

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

874963-20230328180233555-2049864027.png

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

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

874963-20230328155753927-940694522.png
874963-20230328160207639-722957370.png
874963-20230328160216321-200504732.png

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

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

874963-20230328160753879-1154232335.png

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

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

874963-20230328161300821-1214963860.png
874963-20230328161653688-360675913.png

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

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

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

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

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

874963-20230328162335786-784090682.png

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

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

874963-20230328163123968-132178793.png
874963-20230328163140117-2126704502.png

874963-20230328163146759-281267468.png

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

874963-20230328164042923-628670594.png
874963-20230328163641066-1611679361.png
874963-20230328164422419-223089933.png

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

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

874963-20230328165417549-1779700953.png

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

874963-20230328170651100-246049700.png

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

874963-20230328170038078-1369706264.png
874963-20230328170043788-1651824505.png

最终拼接好的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,表示加锁成功,对应的分支事务获取锁成功,分支事务注册成功,皆大欢喜

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

874963-20230328171049380-378797905.png

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

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

874963-20230328171646431-638988798.png

874963-20230328172724225-1787820538.png
874963-20230328172745454-1493378946.png

因此,假设客户端发过来的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服务端日志发现,是由于字段长度问题,导致插入失败,于是加锁失败

874963-20230328174743515-1759798983.png

原来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)本地事务都提交成功的话,整个全局事务算是提交成功了

874963-20230328183604625-1037573724.png

Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeUpdate();
感谢您的阅读,如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!
欢迎各位转载,但必须在文章页面中给出作者和原文链接!

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK