关于ShardingSphere结合Seata时,报 Failed to delete expired undo_log问题解决

20 阅读2分钟

最近发现项目使用ShardingSphere读写分离结合Seata时,发现日志每天定时打印如下异常


java.sql.SQLException: The MySQL server is running with the --read-only option so it cannot execute this statement
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129)
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
	at com.mysql.cj.jdbc.ServerPreparedStatement.serverExecute(ServerPreparedStatement.java:633)
	at com.mysql.cj.jdbc.ServerPreparedStatement.executeInternal(ServerPreparedStatement.java:417)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1098)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdateInternal(ClientPreparedStatement.java:1046)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeLargeUpdate(ClientPreparedStatement.java:1371)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeUpdate(ClientPreparedStatement.java:1031)
	at com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)
	at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeUpdate(HikariProxyPreparedStatement.java)
	at io.seata.rm.datasource.undo.mysql.MySQLUndoLogManager.deleteUndoLogByLogCreated(MySQLUndoLogManager.java:58)
	at io.seata.rm.RMHandlerAT.deleteUndoLog(RMHandlerAT.java:111)
	at io.seata.rm.RMHandlerAT.handle(RMHandlerAT.java:75)
	at io.seata.rm.DefaultRMHandler.handle(DefaultRMHandler.java:73)
	at io.seata.core.protocol.transaction.UndoLogDeleteRequest.handle(UndoLogDeleteRequest.java:70)
	at io.seata.rm.AbstractRMHandler.onRequest(AbstractRMHandler.java:150)
	at io.seata.core.rpc.processor.client.RmUndoLogProcessor.handleUndoLogDelete(RmUndoLogProcessor.java:56)
	at io.seata.core.rpc.processor.client.RmUndoLogProcessor.process(RmUndoLogProcessor.java:51)
	at io.seata.core.rpc.netty.AbstractNettyRemoting.lambda$processMessage$2(AbstractNettyRemoting.java:281)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.base/java.lang.Thread.run(Thread.java:833)

跟踪代码结合官方文档发现问题产生原因:SEATA TC事物协调者会定时向RM发送清理过期undo_log的指令,详细参数可以查看Seata的参数配置,主要是如下两个参数控制

keydescremarkchange record
server.undo.logSaveDaysundo 保留天数默认 7 天,log_status=1(附录 3)和未正常清理的 undo
server.undo.logDeletePeriodundo 清理线程间隔时间默认 86400000,单位毫秒

RM接收TC指令执行删除的代码

image.png

TC是根据注册在其上面的RM信息,往RM发送指令的。

image.png

如上图所示,在应用启动的时候,会往seata注册RM信息。我们在使用sharding的时候,通过sharding-jdbc.yaml配置了1个主库,两个从库,两个从库是只读库,都被注册到seata上了,才导致在从库上执行了delete undo_log操作,导致报错。所以只要在注册的时候控制从库不注册到seata上即可。注册的逻辑主要在 shardingsphere-transaction-base-seata-at模块 SeataATShardingSphereTransactionManager类的init方法

@Override
public void init(final DatabaseType databaseType, final Collection<ResourceDataSource> resourceDataSources, final String providerType) {
    if (enableSeataAT) {
        initSeataRPCClient();
        resourceDataSources.forEach(each -> dataSourceMap.put(each.getOriginalName(), new DataSourceProxy(each.getDataSource())));
    }
}

之前在解决sharding空指针异常的时候,已经把SeataATShardingSphereTransactionManager.java拷贝到自己的工程目录下了,所以可以直接修改,只读库不注册到seata上,不需要被DataSourceProxy托管。

@Override
public void init(final DatabaseType databaseType, final Collection<ResourceDataSource> resourceDataSources, final String providerType) {
    if (enableSeataAT) {
        initSeataRPCClient();
        resourceDataSources.forEach(each -> {
            try {
                if (!each.getDataSource().getConnection().isReadOnly()) {
                    //非只读库才注册到seata上
                    dataSourceMap.put(each.getOriginalName(), new DataSourceProxy(each.getDataSource()));
                }
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        });
    }
}