常见日志框架介绍和对比(log4j,logback,log4j2)

1. 什么是slf4j?

slf4j是The Simple Logging Facade for Java的简称,笼统的讲就是slf4j是一系列的日志接口,这里是一个典型的门面模式的设计。slf4j,log4j和logback的作者都是Ceki Gülcü。最早开发的是log4j,后来基于log4j抽出了统一的日志接口slf4j,并基于slf4j和log4j,优化开发了logback,所以logback无论在使用还是性能方面都要优于log4j。

2. log4j简介和配置

2.1 log4j介绍

Log4j(Log for Java)是Apache的一个开源项目,通过使用Log4j,我们可以控制日志信息输送的目的地是控制台、文件、GUI组件,甚至是套接口服务器、NT的事件记录器、UNIX Syslog守护进程等;我们也可以控制每一条日志的输出格式;通过定义每一条日志信息的级别,我们能够更加细致地控制日志的生成过程。最令人感兴趣的就是,这些可以通过一个配置文件来灵活地进行配置,而不需要修改应用的代码。

2.2 log4j三大组件

  • Loggers(记录器)  主要作用控制日志输出类别;
Loggers组件在此系统中被分为五个级别:DEBUG、INFO、WARN、ERROR和FATAL。这五个级别是有顺序的, FATAL>ERROR >WARN >INFO >DEBUG 

分别用来指定这条日志信息的重要程度,明白这一点很重要。

Log4j级别控制规则:只输出级别不低于设定级别的日志信息,假设Loggers级别设定为INFO,则INFO、WARN、ERROR和FATAL级别的日志信息都会输出,

而级别比INFO低的DEBUG则不会输出。
  • Appenders (输出源)  主要作用控制日志输出的地方,如控制台(Console)、文件(Files)等;
常使用的类如下:
      org.apache.log4j.ConsoleAppender(控制台)
      org.apache.log4j.FileAppender(文件)
      org.apache.log4j.DailyRollingFileAppender(每天产生一个日志文件)
      org.apache.log4j.RollingFileAppender(文件大小到达指定尺寸的时候产生一个新的文件)
      org.apache.log4j.WriterAppender(将日志信息以流格式发送到任意指定的地方)
  • Layouts(布局)  主要作用控制日志以何种形式输出;
有时用户希望根据自己的喜好格式化自己的日志输出,Log4j可以在Appenders的后面附加Layouts来完成这个功能。Layouts提供四种日志输出样式,

如根据HTML样式、自由指定样式、包含日志级别与信息的样式和包含日志时间、线程、类别等信息的样式。
3.1、常使用的类如下:
org.apache.log4j.HTMLLayout(以HTML表格形式布局)
org.apache.log4j.PatternLayout(可以灵活地指定布局模式)
org.apache.log4j.SimpleLayout(包含日志信息的级别和信息字符串)
org.apache.log4j.TTCCLayout(包含日志产生的时间、线程、类别等信息)

2.3 log4j pom依赖引入

基本模式
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>${log4j.version}</version>
</dependency>
下面这种模式会引入log4j,log4j-api和log4j-core
 <!--slf4j对应log4j2的中间件,即桥接,告诉slf4j使用log4j2-->
  <dependency>
          <groupId>org.apache.logging.log4j</groupId>
          <artifactId>log4j-slf4j-impl</artifactId>
          <version>${log4j.version}</version>
 </dependency>

2.4 配置log4j

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

3. logback简介和配置

3.1 logback简介

logback是java的日志开源组件,是log4j创始人基于log4j改造和优化的一个版本,性能比log4j要好,目前主要分为3个模块:

logback-core:核心代码模块
logback-classic:log4j的一个改良版本,同时实现了slf4j的接口,这样你如果之后要切换其他日志组件也是一件很容易的事
logback-access:访问模块与Servlet容器集成提供通过Http来访问日志的功能

3.2 logback pom依赖引入

<logback.version>1.2.3</logback.version>
<!--这个依赖直接包含了 logback-core 以及 slf4j-api的依赖-->
<dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>${logback.version}</version>
</dependency>

3.3 logback 配置模版

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
            <!--设置app名称,后面日志路径使用-->
    <property name="APP_NAME" value="metadata-manage" />
    <springProfile name="dev">
    	<!--开发环境路径-->
        <property name="LOG_HOME" value="/Users/aaa/IdeaProjects/log"/>
    </springProfile>
    <springProfile name="test,production">
        	<!--测试和生产环境路径-->
        <property name="LOG_PATH" value="/data/logs/${APP_NAME}" />
    </springProfile>

    <!--配置控制台appender-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
        <!--配置info文件appender-->
    <appender name="INFO"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/info.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>${LOG_PATH}/info.%d{yyyy-MM-dd}.%i.tar.gz</FileNamePattern>
            <!--日志文件保留天数-->
            <maxHistory>10</maxHistory>
            <totalSizeCap>15GB</totalSizeCap>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!--文件达到 最大128MB时会被压缩和切割 -->
                <maxFileSize>1024MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>NEUTRAL</onMismatch>
        </filter>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
     <!--配置WARN文件appender-->
    <appender name="WARN"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/warn.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>${LOG_PATH}/warn.%d{yyyy-MM-dd}.%i.tar.gz</FileNamePattern>
            <!--日志文件保留天数-->
            <maxHistory>10</maxHistory>
            <totalSizeCap>10GB</totalSizeCap>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!--文件达到 最大128MB时会被压缩和切割 -->
                <maxFileSize>1024MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>WARN</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
     <!--配置ERROR文件appender-->
    <appender name="ERROR"  class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_PATH}/error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>${LOG_PATH}/error.%d{yyyy-MM-dd}.%i.log</FileNamePattern>
            <!--日志文件保留天数-->
            <maxHistory>10</maxHistory>
            <totalSizeCap>10GB</totalSizeCap>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!--文件达到 最大128MB时会被压缩和切割 -->
                <maxFileSize>512MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
     <!--配置异步打印日志文件的appender-->
    <appender name="asyncFileAppender" class="ch.qos.logback.classic.AsyncAppender">
        <discardingThreshold>0</discardingThreshold>
        <queueSize>512</queueSize>
        <appender-ref ref="INFO" />
    </appender>

    <!--<logger name="com.package....." level="DEBUG"/>-->
    <!-- 开发环境 -->
    <springProfile name="dev,test,prod">
        <root level="DEBUG">
            <appender-ref ref="STDOUT" />
        </root>
    </springProfile>

    <!-- 生产环境 -->
    <springProfile name="release,test">
        <root level="INFO">
            <appender-ref ref="ERROR" />
            <appender-ref ref="WARN" />
            <appender-ref ref="INFO" />
        </root>
    </springProfile>


</configuration>

3.4 logback总结

  1. 从我的经验来看,理想的日志格式应当包括(当然除了日志信息本身了):当前时间(无日期,毫秒级精度,因为外围的文件已经按照日期来进行区分了),日志级别,线程名,简单的日志名称(不用全称)还有消息。在logback里会是这样的:
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">   
   <encoder>        
    <pattern>%d{HH:mm:ss.SSS} %-5level [%thread][%logger{0}] %m%n</pattern>   
   </encoder> 
</appender>

当然,也可以带上日期
 <encoder>
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        <charset>UTF-8</charset>
 </encoder>
  1. 打印日志时使用模式注入的方式,不要通过字符串拼接;
log.debug("Found {} records matching filter: '{}'", records, filter);
错误示例
log.debug("Found " + records + " recordsmatching filter: '" + filter + "'");
  1. 尽量使用异步的方式打印日志

4. logback和log4j对比

  • 更快的执行速度: logback基于log4j的基础上开发的,重写了内部的实现,在某些特定的场景上面,甚至可以比之前的速度快上10倍。在保证logback的组件更加快速的同时,同时所需的内存更加少。
  • logback-classic 非常自然的实现了SLF4J:ogback-classic中的longging类自然的实现了SLF4J。当你使用 logback-classic作为底层实现时,涉及到LF4J日记系统的问题你完全不需要考虑。更进一步来说,由于 logback-classic强烈建议使用SLF4J作为客户端日记系统实现,如果需要切换到log4j或者其他,你只需要替换一个jar包即可,不需要去改变那些通过SLF4J API 实现的代码。这可以大大减少更换日记系统的工作量。
  • 自动重新载入配置文件:Logback-classic可以在配置文件被修改后,自动重新载入。这个扫描过程很快,无资源争用,并且可以动态扩展支持在上百个线程之间每秒上百万个调用。它和应用服务器结合良好,并且在JEE环境通用,因为它不会调用创建一个单独的线程来做扫描。
    优雅地从I/O错误中恢复:FileAppender和它的子类,包括RollingFileAppender,可以优雅的从I/O错误中恢复。所以,如果一个文件服务器临时宕机,你再也不需要重启你的应用,而日志功能就能正常工作。当文件服务器恢复工作,logback相关的appender就会透明地和快速的从上一个错误中恢复。
  • 自动清除旧的日志归档文件:通过设置TimeBasedRollingPolicy 或者 SizeAndTimeBasedFNATP的 maxHistory 属性,你就可以控制日志归档文件的最大数量。如果你的回滚策略是每月回滚的,并且你希望保存一年的日志,那么只需简单的设置maxHistory属性为12。对于12个月之前的归档日志文件将被自动清除。
  • 自动压缩归档日志文件:RollingFileAppender可以在回滚操作中,自动压缩归档日志文件。压缩通常是异步执行的,所以即使是很大的日志文件,你的应用都不会因此而被阻塞。
    配置文件中的条件处理:开发者通常需要在不同的目标环境中变换logback的配置文件,例如开发环境,测试环境和生产环境。这些配置文件大体是一样的,除了某部分会有不同。为了避免重复,logback支持配置文件中的条件处理,只需使用,和,那么同一个配置文件就可以在不同的环境中使用了。
  • 过滤: Logback拥有远比log4j更丰富的过滤能力。例如,让我们假设,有一个相当重要的商业应用部署在生产环境。考虑到大量的交易数据需要处理,记录级别被设置为WARN,那么只有警告和错误信息才会被记录。现在,想象一下,你在开发环境遇到了一个臭虫,但是在测试平台中却很难发现,因为一些环境之间(生产环境/测试环境)的未知差异。使用log4j,你只能选择在生产系统中降低记录的级别到DEBUG,来尝试发现问题。但是很不幸,这会生成大量的日志记录,让分析变得困难。更重要的是,多余的日志记录会影响到生产环境的性能。使用logback,你可以选择保留只所有用户的WARN级别的日志,而除了某个用户,例如Alice,而她就是问题的相关用户。当Alice登录系统,她就会以DEBUG级别被记录,而其他用户仍然是以WARN级别来记录日志。这个功能,可以通过在配置文件的XML中添加4行。请在相关章节中查找MDCFilter
  • logback原生支持同时按日期和文件大小分割日志,而log4j需要自己写代码实现

5. log4j2简介和配置

5.1 log4j2简介

logbakc是站在log4j的基础上开发设计的,而Log4j2则是站在logback这个巨人肩膀上升级优化的,虽然在各个方面都与logback非常相似,但是却提供了更强的性能和并发性,尤其在异步logget这块。

5.2 log4j2常见组件

  • LoggerContext:日志系统上下文;
  • Configuration:每一个 LoggerContext 都有一个有效的 Configuration, Configuration 包含所有的Appender 、Filter、LoggerConfig ,StrSubstitutor引用和对Layout的格式设置;
  • Logger : Logger继承自 AbstractLogger,当配置被修改后,它将与不同的 LoggerConfig 相关联,这导致其行为也被改变;
  • LoggerConfig:LoggerConfig 对象在 Logger 被声明时创建,它包含了一组用于处理事件的Appender引用,以及一组用于过滤传递给Appender事件的Filter,相当于是Appender的集合;
  • Appender:Log4j2 还允许将记录请求输出到多个目标中,而这种输出目标被称为Appender。目前Appender的类型有控制台、文件、socket、Apache Flume、JMS、远程UNIX 系统日志守护进程以及各种数据库API,用户可以根据需要选择将日志输出到不同的目标上,同时在一个Logger的配置中,允许开启多个Appender;
  • Filter :Log4j2 提供了Filter 来过滤消息事件,它可被应用于事件传递给LoggerConfig之前,及传递给LoggerConfig之后,即LoggerConfig的前后置拦截器。Filter包含了三种行为: Accept, Deny 或 Neutral,其中Accept,Deny分别代表着接受和拒绝,即过滤器接受或拒绝某种日志过滤表达式等,经过这两种行为处理后将不再经过其他过滤器。Neutral代表着中立,意味着事件应由其他Filter来处理。如果未配置任何Filter,那么事件将直接被处理;
  • Layout:Log4j2除了可以输出到不同的目标Appender之外,还支持在目标中定义自定义的日志格式,Layout 负责对日志事件进行格式化,通过配置PatternLayout来实现;
  • Policy是用来控制日志文件何时(When)进行滚动的;Strategy是用来控制日志文件如何(How)进行滚动的。如果配置的是RollingFile或RollingRandomAccessFile,则必须配置一个Policy

5.3 log4j2配置文件

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
    <property name="LOG_HOME" value="${app.log.root}"/>
    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出,%d:日期;%thread:线程名;%-5level:级别,从左显示5个字符宽度;%msg:日志消息;%n:换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="ERROR" class="ch.qos.logback.core.ConsoleAppender">
        <!-- filter过滤需要输出的类型或者级别 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <!-- 格式化方式 -->
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出,%d:日期;%thread:线程名;%-5level:级别,从左显示5个字符宽度;%msg:日志消息;%n:换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
    <!-- 按照每天生成日志文件 -->

    <appender name="SYS" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/sys.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- rollover daily -->
            <fileNamePattern>${LOG_HOME}/sys-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
            <maxFileSize>20MB</maxFileSize>
            <maxHistory>60</maxHistory>
            <totalSizeCap>2GB</totalSizeCap>
        </rollingPolicy>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <appender name="APP" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- rollover daily -->
            <fileNamePattern>${LOG_HOME}/app-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
            <maxFileSize>20MB</maxFileSize>
            <maxHistory>60</maxHistory>
            <totalSizeCap>2GB</totalSizeCap>
        </rollingPolicy>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <appender name="MONITOR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/monitor.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- rollover daily -->
            <fileNamePattern>${LOG_HOME}/monitor-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
            <maxFileSize>20MB</maxFileSize>
            <maxHistory>60</maxHistory>
            <totalSizeCap>2GB</totalSizeCap>
        </rollingPolicy>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <appender name="DYEING" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/dyeing.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- rollover daily -->
            <fileNamePattern>${LOG_HOME}/dyeing-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
            <maxFileSize>20MB</maxFileSize>
            <maxHistory>60</maxHistory>
            <totalSizeCap>2GB</totalSizeCap>
        </rollingPolicy>

        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <appender name="ASYNC_DYEING" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="DYEING"/>
    </appender>

    <logger name="dyeingLogger" level="INFO">
        <appender-ref ref="ASYNC_DYEING"/>
    </logger>

     <!--给具体的包绑定appender -->
    <logger name="com.package.aaa" level="INFO">
        <appender-ref ref="APP"/>
    </logger>


    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="ERROR"/>
    </root>
</configuration>

5.4 log4j2异步日志的四种队列

ArrayBlockingQueue -- 默认的队列,通过 java 原生的 ArrayBlockingQueue 实现。
DisruptorBlockingQueue -- disruptor 包实现的高性能队列。
JCToolsBlockingQueue -- JCTools 实现的无锁队列。
LinkedTransferQueue -- 通过 java7 以上原生支持的 LinkedTransferQueue 实现。

6.Disruptor源码解析

6.1 循环缓冲区RingBuffer

讲解mysql的文章中,有说到mysql的redolog 是通过一个环形的存储区域实现其循环写入的,以保障性能和一致性。

在 linux 内核中,进程间通信所使用的 fifo 也是通过环形存储区域来实现的。

RingBuffer的好处:

  1. 基于数组实现,内存被循环使用,减少了内存分配、回收扩容等操作。
  2. 对于只有单个读取和写入进程的场景下,读取写入分别在环的不同位置进行,因此,读写过程无需加锁,从而能够让缓存的读写更为高效。

disruptor 正是借鉴这一思想,使用环形队列实现缓冲,由于 RingBuffer 实现了空间的循环利用,一次开辟,即可一直驻留在内存中,降低了 GC 的压力,从而提升缓冲的性能。同时,disruptor 基于RingBuffer,提供了单生产者、多生产者、单消费者、多消费者组等多种模型供不同的场景中可以灵活使用,在这些模式下,disruptor 尽量通过 Unsafe 包中的 CAS 操作结合自旋的方式避免了锁的使用,从而让整个实现十分简洁而高效。

6.2 单生产者模型

相对于多生产者模型而言,单生产者模型显然更为简单,我们来看看他是怎么实现的:

// Disruptor
public <A> void publishEvent(final EventTranslatorOneArg<T, A> eventTranslator, final A arg) {
  ringBuffer.publishEvent(eventTranslator, arg);
}

// RingBuffer
public <A> void publishEvent(EventTranslatorOneArg<E, A> 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 方法发布数据。

6.3 多生产者模型

多生产者模型下,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;
  }
}

多生产者模型下,通过自旋与缓存切片的方式,成功避免了锁的使用,实现了高效的生产操作。

6.4 消费者实现

EventProcessor 是整个消费者事件处理框架,EventProcessor 接口继承了 Runnable 接口,主要有两种实现:
单线程批量处理 BatchEventProcessor
多线程处理 WorkProcessor

针对单消费者和多消费者,实现模式区别:
广播模式 – 使用 handleEventsWith 方法传入多个 EventHandler,内部使用多个 BatchEventProcessor 关联多个线程执行,是典型的发布订阅模式,同一事件会被多个消费者并行消费,适用于同一事件触发多种操作。每个 BatchEventProcessor 是单线程的任务链,任务执行有序且非常快。
集群消费模式 – 使用 handleEventsWithWorkerPool 方法传入多个WorkHandler时,内部使用多个 WorkProcessor 关联多个线程执行,类似于 JMS 的点对点模式,同一事件会被一组消费者其中之一消费,适用于提升消费者并行处理能力,每个 WorkProcessor 内部实现是多线程的,无法保证任务执行的顺序

6.5 消费者等待策略

BlockingWaitStrategy:这是默认的策略。使用锁和条件进行数据的监控和线程的唤醒。因为涉及到线程的切换,是最节省CPU,但在高并发下性能表现最糟糕的一种等待策略。
SleepingWaitStrategy:会自旋等待数据,如果不成功,才让出cpu,最终进行线程休眠,以确保不占用太多的CPU数据,因此可能产生比较高的平均延时。比较适合对延时要求不高的场合,好处是对生产者线程的影响最小。典型的应用场景是异步日志。
YieldingWaitStrategy:用于低延时的场合。消费者线程不断循环监控缓冲区变化,在循环内部,会使用Thread.yield()让出cpu给别的线程执行时间。
BusySpinWaitStrategy:开启的是一个死循环监控,消费者线程会尽最大努力监控缓冲区变化,因此,CPU负担比较大

7. logback和log4j2对比

7.1 log4j2更多的特性支持

  1. 丢数据这种情况少,可以用来做审计功能。而且自身内部报的exception会被发现,但是logback和log4j不会;
  2. log4j2使用了disruptor技术,在多线程环境下,性能高于logback等10倍以上;
  3. (garbage free)之前的版本会产生非常多的临时对象,会造成GC频繁,log4j2则在这方面上做了优化,减少产生临时对象。尽可能少的GC,其实也是基于disruptor技术;
  4. 支持lambda表达式;
  5. 对filter的功能支持的更强大;
  6. 系统日志(Syslog)协议支持TCP 和 UDP
  7. 支持kafka queue

7.2 总结

  1. 在线上环境关闭控制台输出会对性能有所提升;
  2. 相同情况下对比,log4j2性能更优;
  3. 在对日志输出场景比较多的情况下可以考虑将logback更换为log4j2;
  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值