最近发现项目使用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的参数配置,主要是如下两个参数控制
key | desc | remark | change record |
---|---|---|---|
server.undo.logSaveDays | undo 保留天数 | 默认 7 天,log_status=1(附录 3)和未正常清理的 undo | |
server.undo.logDeletePeriod | undo 清理线程间隔时间 | 默认 86400000,单位毫秒 |
RM接收TC指令执行删除的代码
TC是根据注册在其上面的RM信息,往RM发送指令的。
如上图所示,在应用启动的时候,会往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);
}
});
}
}