我们在使用MyBatis或衍生产品时,通常会打开其默认日志输出功能,通过SQL日志来排查问题。但你是否注意过为什么MyBatis日志总是与其它Log格格不入,没有日期、级别等等,全部靠左侧输出?
在指定默认日志输出时,经常使用一个名为StdOutImpl的类:
java
public class StdOutImpl implements Log {
...
public void debug(String s) {
System.out.println(s);
}
public void trace(String s) {
System.out.println(s);
}
...
}
默认使用System.out.println()
输出日志,众所周知,其输出时会阻塞当前线程,导致SQL查询不得不等待日志输出完后才能返回结果。
这也是大多数文章推荐使用Log而不是System.out.println()
的原因,因为Log是异步的。
但并发量不大的时候,可能感知不到System.out.println()
带来的负面影响。
更糟糕的情况
接口响应缓慢、CPU占用率超高,系统几乎处于不可用状态,我曾怀疑SQL写的有问题,但在数据库中查询却又没有多少活跃线程。
直到使用jstack命令检查,发现多个web线程都在等待获取java.io.PrintStream
的锁。
这就不得不提到System.out.println更糟糕的问题,如果进一步检查println()
函数就会发现:
java
public void println(String x) {
synchronized (this) {
print(x);
newLine();
}
}
System.out
是一个全局静态PrintStream
对象,而println()
函数又会对其加锁调用,这就导致所有线程的SQL日志都是在排队输出。这在上一节问题的基础上,又导致了更严重的并发性能问题。
只需有几个像批量插入或数据导出这样有大量参数或返回结果的SQL语句,就可能影响所有接口的响应速度。
如何优化
内置Log实现
MyBatis中其实还提供了其它日志输出实现,例如使用Slf4jImpl
代替StdOutImpl
即可。
这不仅可以解决日志输出的性能问题,也可以通过Log库配置统一SQL日志的格式和输出位置。
MyBatis原生:
xml
<configuration>
<settings>
<setting name="logImpl" value="SLF4J"/>
</settings>
</configuration>
MyBatis Plus:
yaml
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
MyBatis Flex:
java
@Configuration
public class MyBatisFlexConfig implements ConfigurationCustomizer {
@Override
public void customize(FlexConfiguration config) {
// 使用Slf4j输出日志
config.setLogImpl(Slf4jImpl.class);
}
}
但注意的是,MyBatis Log接口中是没有info级别的:
java
public interface Log {
boolean isDebugEnabled();
boolean isTraceEnabled();
void error(String s, Throwable e);
void error(String s);
void debug(String s);
void trace(String s);
void warn(String s);
}
SQL语句(Preparing)、SQL参数(Parameters)、结果数量(Total)是调用debug()
方法打印,而结果列(Columns、Row)是调用trace()
打印的。
因为Log通常只会打印info级别,还需要在Log配置中指定日志输出级别。例如logback的配置:
xml
<logger name="org.example" level="TRACE" additivity="true" />
MyBatis会为每个Mapper中的每个方法创建一个Log对象,所以前缀一般只需取到项目的根目录即可,但如果还引入根包名不同的子模块,还得一并添加。
自定义Log实现
或者我们可以实现自己的Log,将输出改为info级别:
java
public class MyBatisLogImpl implements Log {
private final Logger log;
public MyBatisLogImpl(String clazz) {
log = LoggerFactory.getLogger(clazz);
System.out.println(clazz);
}
@Override
public boolean isDebugEnabled() {
return true;
}
// 如果要打印具体查询结果(Columns & Row),修改返回值为true
// 很多时候查询结果的日志是没必要的,关掉后可显著减少日志文件的体积
@Override
public boolean isTraceEnabled() {
return true;
}
@Override
public void error(String s, Throwable e) {
log.error(s, e);
}
@Override
public void error(String s) {
log.error(s);
}
@Override
public void debug(String s) {
// 输出:SQL语句(Preparing)、SQL参数(Parameters)、结果数量(Total)
log.info(s);
}
@Override
public void trace(String s) {
// 输出:结果列(Columns、Row)
log.info(s);
}
@Override
public void warn(String s) {
log.warn(s);
}
}
此外,MyBatis日志还有些没什么用的debug日志:
csharp
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@729bcc15] was not registered for synchronization because synchronization is not active
也可以在日志配置中屏蔽:
xml
<!-- 排除SqlSessionUtils中的所有日志 -->
<logger name="org.mybatis.spring.SqlSessionUtils" level="OFF" />
结论
System.out.println()
是一种比预想中还要糟糕的日志输出方法,不止在MyBatis中不要使用StdOutImpl
,在任何系统中都应该避免使用System.out.println()
打印日志或依赖库基于其的实现,尤其是要警惕没有被Log库格式化过日志内容。
其问题远不止是在单线程中输出时会阻塞当前线程,在多线程环境中还会因为要对同一个System.out
加锁使用,进而导致严重的并发性能问题。