slf4j是The Simple Logging Facade for Java的简称,笼统的讲就是slf4j是一系列的日志接口,这里是一个典型的门面模式的设计。slf4j,log4j和logback的作者都是Ceki Gülcü。最早开发的是log4j,后来基于log4j抽出了统一的日志接口slf4j,并基于slf4j和log4j,优化开发了logback,所以logback无论在使用还是性能方面都要优于log4j。
Log4j(Log for Java)是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。
Loggers组件在此系统中被分为五个级别:DEBUG、INFO、WARN、ERROR和FATAL。这五个级别是有顺序的, FATAL>ERROR >WARN >INFO >DEBUG
分别用来指定这条日志信息的重要程度,明白这一点很重要。
Log4j级别控制规则:只输出级别不低于设定级别的日志信息,假设Loggers级别设定为INFO,则INFO、WARN、ERROR和FATAL级别的日志信息都会输出,
而级别比INFO低的DEBUG则不会输出。
常使用的类如下:
org.apache.log4j.ConsoleAppender(控制台)
org.apache.log4j.FileAppender(文件)
org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
有时用户希望根据自己的喜好格式化自己的日志输出,Log4j可以在Appenders的后面附加Layouts来完成这个功能。Layouts提供四种日志输出样式,
如根据HTML样式、自由指定样式、包含日志级别与信息的样式和包含日志时间、线程、类别等信息的样式。
3.1、常使用的类如下:
org.apache.log4j.HTMLLayout(以HTML表格形式布局)
org.apache.log4j.PatternLayout(可以灵活地指定布局模式)
org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)
org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息)
基本模式
log4j
log4j
${log4j.version}
下面这种模式会引入log4j,log4j-api和log4j-core
org.apache.logging.log4j
log4j-slf4j-impl
${log4j.version}
Log4j支持两种配置文件格式,一种是XML(标准通用标记语言下的一个应用)格式的文件,一种是Java特性文件log4j.properties(键=值)。
这里先用log4j.properties的方式,后面log4j2使用xml的方式
### set log levels ###
log4j.rootLogger = DEBUG,Console,File
### 输出到控制台 ###
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.Target=System.out
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern= %d{ABSOLUTE} %5p %c{1}:%L - %m%n
### 输出到日志文件 ###
log4j.appender.File=org.apache.log4j.RollingFileAppender
log4j.appender.File.File=${project}/WEB-INF/logs/app.log
log4j.appender.File.DatePattern=_yyyyMMdd'.log'
log4j.appender.File.MaxFileSize=10MB
log4j.appender.File.Threshold=ALL
log4j.appender.File.layout=org.apache.log4j.PatternLayout
log4j.appender.File.layout.ConversionPattern=[%p][%d{yyyy-MM-dd HH\:mm\:ss,SSS}][%c]%m%n
logback是java的日志开源组件,是log4j创始人基于log4j改造和优化的一个版本,性能比log4j要好,目前主要分为3个模块:
logback-core:核心代码模块
logback-classic:log4j的一个改良版本,同时实现了slf4j的接口,这样你如果之后要切换其他日志组件也是一件很容易的事
logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能
1.2.3
ch.qos.logback
logback-classic
${logback.version}
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
${LOG_PATH}/info.log
${LOG_PATH}/info.%d{yyyy-MM-dd}.%i.tar.gz
10
15GB
1024MB
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
INFO
ACCEPT
NEUTRAL
DEBUG
ACCEPT
DENY
${LOG_PATH}/warn.log
${LOG_PATH}/warn.%d{yyyy-MM-dd}.%i.tar.gz
10
10GB
1024MB
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
WARN
ACCEPT
DENY
${LOG_PATH}/error.log
${LOG_PATH}/error.%d{yyyy-MM-dd}.%i.log
10
10GB
512MB
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
ERROR
ACCEPT
DENY
0
512
%d{HH:mm:ss.SSS} %-5level [%thread][%logger{0}] %m%n
当然,也可以带上日期
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
UTF-8
log.debug("Found {} records matching filter: '{}'", records, filter);
错误示例
log.debug("Found " + records + " recordsmatching filter: '" + filter + "'");
logbakc是站在log4j的基础上开发设计的,而Log4j2则是站在logback这个巨人肩膀上升级优化的,虽然在各个方面都与logback非常相似,但是却提供了更强的性能和并发性,尤其在异步logget这块。
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
ERROR
ACCEPT
DENY
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
${LOG_HOME}/sys.log
${LOG_HOME}/sys-%d{yyyy-MM-dd}.%i.log
20MB
60
2GB
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
UTF-8
${LOG_HOME}/app.log
${LOG_HOME}/app-%d{yyyy-MM-dd}.%i.log
20MB
60
2GB
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
UTF-8
${LOG_HOME}/monitor.log
${LOG_HOME}/monitor-%d{yyyy-MM-dd}.%i.log
20MB
60
2GB
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
UTF-8
${LOG_HOME}/dyeing.log
${LOG_HOME}/dyeing-%d{yyyy-MM-dd}.%i.log
20MB
60
2GB
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level - %msg%n
UTF-8
ArrayBlockingQueue -- 默认的队列,通过 java 原生的 ArrayBlockingQueue 实现。
DisruptorBlockingQueue -- disruptor 包实现的高性能队列。
JCToolsBlockingQueue -- JCTools 实现的无锁队列。
LinkedTransferQueue -- 通过 java7 以上原生支持的 LinkedTransferQueue 实现。
讲解mysql的文章中,有说到mysql的redolog 是通过一个环形的存储区域实现其循环写入的,以保障性能和一致性。
在 linux 内核中,进程间通信所使用的 fifo 也是通过环形存储区域来实现的。
RingBuffer的好处:
disruptor 正是借鉴这一思想,使用环形队列实现缓冲,由于 RingBuffer 实现了空间的循环利用,一次开辟,即可一直驻留在内存中,降低了 GC 的压力,从而提升缓冲的性能。同时,disruptor 基于RingBuffer,提供了单生产者、多生产者、单消费者、多消费者组等多种模型供不同的场景中可以灵活使用,在这些模式下,disruptor 尽量通过 Unsafe 包中的 CAS 操作结合自旋的方式避免了锁的使用,从而让整个实现十分简洁而高效。
相对于多生产者模型而言,单生产者模型显然更为简单,我们来看看他是怎么实现的:
// Disruptor
public void publishEvent(final EventTranslatorOneArg eventTranslator, final A arg) {
ringBuffer.publishEvent(eventTranslator, arg);
}
// RingBuffer
public void publishEvent(EventTranslatorOneArg translator, A arg0) {
long sequence = this.sequencer.next();
this.translateAndPublish(translator, sequence, arg0);
}
public long next() {
return this.next(1);
}
public long next(int n) {
if (n < 1) {
throw new IllegalArgumentException("n must be > 0");
} else {
// 获取上次数据写入位置
long nextValue = this.pad.nextValue;
// 获取本次数据写入位置
long nextSequence = nextValue + (long)n;
// 计算成环点
long wrapPoint = nextSequence - (long)this.bufferSize;
// 消费者下次消费位置
long cachedGatingSequence = this.pad.cachedValue;
// 缓存位置不足,自旋等待
if (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue) {
long minSequence;
while(wrapPoint > (minSequence = Util.getMinimumSequence(this.gatingSequences, nextValue))) {
Thread.yield();
}
// 被唤醒说明有消费者消费,更新消费位置
this.pad.cachedValue = minSequence;
}
// 获取数据插入位置并返回
this.pad.nextValue = nextSequence;
return nextSequence;
}
}
可以看到,整个获取写入位置的代码并不复杂,即在 RingBuffer 中获取写入位置,如果 RingBuffer 空间不足,则调用 yield 等待 consumer 唤醒,一旦位置充足,则返回写入位置,之后,调用 translateAndPublish 方法发布数据。
多生产者模型下,disruptor 通过对不同生产者进行隔离实现了生产过程的无冲突,也就是说,每个生产者只能对 RingBuffer 上分配给自己的独立空间进行写入,但这样一来,就引入了一个新的问题,由于 RingBuffer 不再是连续的,consumer 怎么知道到哪里去获取数据呢?解决方法也很简单,disruptor 引入了一个额外的缓冲区 availableBuffer,他的长度与 RingBuffer 长度相同,因此,他的槽位与 RingBuffer 的槽位一一对应,一旦有数据写入就在 availableBuffer 的对应位置置1,消费后则置 0,从而让读取的时候明确获知下一位置。
availableBuffer 在使用中,虽然被多个生产者划分为多个区域,实际上,每个生产者在操作自己所持有的 availableBuffer 片段时,也是将这个片段作为一个 RingBuffer 来使用,这样巧妙地转化,便让多生产模型完全可以复用单生产者模型中的实现,因此在多生产者模型下,写入流程与单生产者模型并无太大区别,仅在 next 方法与 publish 方法的实现上有所区别:
public long next(int n) {
if (n < 1) {
throw new IllegalArgumentException("n must be > 0");
} else {
long current;
long next;
do {
while(true) {
// 获取下一写入位置
current = this.cursor.get();
next = current + (long)n;
// 计算持有的 availableBuffer 片段的城环段
long wrapPoint = next - (long)this.bufferSize;
// 计算下一消费位置
long cachedGatingSequence = this.gatingSequenceCache.get();
if (wrapPoint <= cachedGatingSequence && cachedGatingSequence <= current) {
break;
}
// 自旋等待
long gatingSequence = Util.getMinimumSequence(this.gatingSequences, current);
if (wrapPoint > gatingSequence) {
LockSupport.parkNanos(1L);
} else {
this.gatingSequenceCache.set(gatingSequence);
}
}
} while(!this.cursor.compareAndSet(current, next));
return next;
}
}
多生产者模型下,通过自旋与缓存切片的方式,成功避免了锁的使用,实现了高效的生产操作。
EventProcessor 是整个消费者事件处理框架,EventProcessor 接口继承了 Runnable 接口,主要有两种实现:
单线程批量处理 BatchEventProcessor
多线程处理 WorkProcessor
针对单消费者和多消费者,实现模式区别:
广播模式 – 使用 handleEventsWith 方法传入多个 EventHandler,内部使用多个 BatchEventProcessor 关联多个线程执行,是典型的发布订阅模式,同一事件会被多个消费者并行消费,适用于同一事件触发多种操作。每个 BatchEventProcessor 是单线程的任务链,任务执行有序且非常快。
集群消费模式 – 使用 handleEventsWithWorkerPool 方法传入多个WorkHandler时,内部使用多个 WorkProcessor 关联多个线程执行,类似于 JMS 的点对点模式,同一事件会被一组消费者其中之一消费,适用于提升消费者并行处理能力,每个 WorkProcessor 内部实现是多线程的,无法保证任务执行的顺序
BlockingWaitStrategy:这是默认的策略。使用锁和条件进行数据的监控和线程的唤醒。因为涉及到线程的切换,是最节省CPU,但在高并发下性能表现最糟糕的一种等待策略。
SleepingWaitStrategy:会自旋等待数据,如果不成功,才让出cpu,最终进行线程休眠,以确保不占用太多的CPU数据,因此可能产生比较高的平均延时。比较适合对延时要求不高的场合,好处是对生产者线程的影响最小。典型的应用场景是异步日志。
YieldingWaitStrategy:用于低延时的场合。消费者线程不断循环监控缓冲区变化,在循环内部,会使用Thread.yield()让出cpu给别的线程执行时间。
BusySpinWaitStrategy:开启的是一个死循环监控,消费者线程会尽最大努力监控缓冲区变化,因此,CPU负担比较大