记录一下最近做的一个Web项目中的日志问题
项目使用的日志体系如下:使用sl4j作为门面,log的实际实现是log4j
问题如下:
在请求高并发的情况下,推测日志会出现串的情况。
举个例子:
public void echo() {
log.info("Function begin");
//...省略若干行代码...
log.info("Function running");
//...省略若干行代码...
log.info("Function end");
}
如果多个请求调用方法echo,最终的输出情况可能如下
Thread-1: Function begin
Thread-2: Function begin
Thread-3: Function begin
Thread-3: Function running
Thread-1: Function running
Thread-1: Function end
Thread-2: Function running
Thread-3: Function end
Thread-2: Function end
不同线程输出的日志会串到一起,很乱
网上查找资料,很少有提这一块的。基本上都是log4j2的异步输出
可是log4j2的异步输出只是先放到buf中,buf满了再一次输出。提高的是写的性能,向buf中加入的顺序仍不能保证,仍然会串。并不是用来解决目前的这个情况的
此外还有使用日志分析工具的,在日志输出时加上某些标志 如IP、用户名、线程名等业务信息,之后使用日志分析工具筛选。这样做还有一个问题 就是每次使用需要手动拼接业务信息输出。log4j倒是有ThreadContext做这个事情,每次设置ThreadContext输出时自动加上。
But!!!我们用的是sl4j,sl4j是一个日志门面,实现了统一的接口 后面可以去找各个实现,logback、jul、log4j等等。sl4j没有ThreadContext这个东西,如果使用ThreadContext意味着什么?我们可以洗洗睡了,sl4j的使用就没有意义了
我的解决思路:
解题基础是动态代理、反射、多线程,新建一个自定义handler,在这个handler中存放一个sl4j的Logger对象
通过反射获取Logger的Method列表(最好是static的 不要重复获取)
定义一个static Map用于存储线程间的信息
在invoke中,对比method,如果当前调用的method在Logger的method列表中,那么不要执行这个method,而是将method和参数args记录下来(这里可以创建一个类 专门存放这些信息)。
然后问题来了 都存放下来不执行 那么我的日志怎么输出呢?
所以要做一个判断,根据线程ID从前面提到的map中查看一个标志是否存在(标志的作用后面就明白了)
如果不存在 获取当前线程 caller,新建一个线程commitTask,并设置这个标志(为了不重复创建commitTask),在commitTask中执行caller.join(),在caller.join()后获取前面拦截的所有method和arg信息,然后使用logger实例依次执行。
所以提交的关键在于这个commitTask,因为执行了caller.join(),所以commitTask会等待线程caller执行完后再执行caller.join()之后的代码。
自己做一个LoggerFactory,在这个LoggerFactory中实例化上面创建的handler,调用sl4j的LoggerFactory创建一个sl4j的Logger实例,将Logger实例赋值给handler对象,根据handler对象创建Logger接口(可以自己定义接口继承sl4j的Logger接口,这样可以添加commit等自定义方法)的动态代理,返回这个动态代理
上述思路的效果:
在一个线程的运行期间,所有logger调用的info、warn等日志方法都不输出而是依次记录下来,在线程执行完毕后,自动按照记录的顺序一起输出(记得加锁)。这样一次请求期间产生的log日志会放在一起输出 不会出现串的现象
PS:
1、这种思路有利有弊,需要根据实际情况自己取舍
2、上述思路还有漏洞,即在一次业务请求中,如果业务代码中有多线程的部分,会因为根据线程为单位收集日志而使一个业务流程中的几个线程间的日志无法一起输出。
解决:使用自己定义的ID替换线程ID,这里和事务有点像,不开启事务就使用线程ID作为单位收集。开启后根据业务ID为单位收集日志(但是这样没办法自动输出,需要添加方法手动调用输出。如 commit)