[Java的日志] P3: 异步日志记录使用的ExecutorService数据库

从博客东黄长发发表的文章, 是作者的同意.

大家好, 在 2 以前在伐木
[Java的日志] P1: 测井软件开发中的重要性
[Java的日志] P2: Log4j的集成到软件
本人提到的日志记录的重要性, 登录时的一些原则, 并就如何整合指示 Log4j的 在Java程序中.
本文中,我将引导您创建一个功能 异步日志 在 数据库.
用于登录到数据库功能 Log4j 完全能够满足这可. 但是,在这一部分,我将指导如何创建一个模块记录到数据库中,这样就可以更好地理解 日志机制异步.
第一, 登录 数据库 应该有 表 保存日志, 并创建 顺序 来获取值日志表的ID

CREATE TABLE LOGS
( 
  LOGS_ID               NUMBER NOT NULL PRIMARY KEY,
  LEVEL_LOG             VARCHAR2(10),
  CREATE_TIME           DATE,
  CONTENT               CLOB
);
/
CREATE SEQUENCE LOGS_SEQ;

在应用程序, 我将创建一个 实体 对应 表 是为了方便 插入 数据表 日志.

package net.tunghuynh.logging.core;
 
public class Logs {
    private Integer logsId;
    private String levelLog;
    private String content;
 
    public Integer getLogsId(){
        return logsId;
    }
    
    public void setLogsId(Integer logsId){
        this.logsId = logsId;
    }
    
    public String getLevelLog() {
        return levelLog;
    }
 
    public void setLevelLog(String levelLog) {
        this.levelLog = levelLog;
    }
 
    public String getContent() {
        return content;
    }
 
    public void setContent(String content) {
        this.content = content;
    }
}

如果正常记录的目的只许成功,你绝对可以做一个直接的声明 insert into 日志数据到表中以下列方式

public static void main(String[] args) {
    Logs logs = new Logs();
    logs.setLevelLog("INFO");
    logs.setContent("Log by insert into database");
    new Main().save(logs);
}
public void save(Logs item) {
    PreparedStatement stmt = null;
    Connection con = null;
    try {
        con = DatabaseUtils.getConnection();
        stmt = con.prepareStatement("INSERT INTO LOGS (LOGS_ID, LEVEL_LOG, CONTENT, CREATE_TIME)" +
                " VALUES(LOGS_SEQ.NEXTVAL, ?, ?, SYSDATE)");
        stmt.setString(1, item.getLevelLog());
        stmt.setString(2, item.getContent());
        stmt.execute();
    } catch (Exception ex) {
        logger.error(ex.getMessage(), ex);
    } finally {
        DatabaseUtils.closeObject(stmt);
        DatabaseUtils.closeObject(con);
    }
}

是这么认为的, 我在手柄上拆分代码作为本文的范围之外 数据库连接, 关闭的连接,... 在 1 命名其他类 DatabaseUtils 以避免稀释文章. 你可以看到的那部分细节 源全 底!.
但是,像他以前的文章中提到的原则,当日志记录 不影响时间 治疗原发性服务. 因此犯有代码这一原则, 因为它可以工作 连接数据库 和 执行 命令 插入 其他将占据 1 某些兼职.
为了解决这个问题,我们需要处理的数据库,并在执行连接插入 1 线 私人. 最简单的是 new Thread 新执行 Runnable.

Thread saveLog = new Thread(new Runnable() {
    @Override
    public void run() {
        new Main().save(item);
    }
});
saveLog.start();

但是,这样一来会相当遇到一个问题 危险 即当业务功能被称为连续, 导致需要创造过 多线程 在应用程序 不解放 得到, 这将影响到很多 性能 应用程序并在服务器的影响. 因此,你应该使用窗体 池 管理和 限制 分娩 Thread 虚张声势以上. 在这里,我通常使用 ExecutorService 来承担这项工作, 当或任何需要,只要登录 提交日志在和货架男孩 ExecutorService 手柄日志逐渐, 所以不会影响主要业务的处理时间.
首先,我创建了一个 abstract class Task 管理 对象日志列表 必要 保存 在 数据库, abstract class Task 得到 implements Callable 到 执行 业务.

package net.tunghuynh.logging.core;
 
import java.util.List;
import java.util.concurrent.Callable;
 
public abstract class Task<E> implements Callable<Integer> {
    private List<E> items;
    public void setItems(List<E> items){
        this.items = items;
    }
    public List<E> getItems(){
        return items;
    }
}

然后,我创建了一个 abstract class ThreadManager 声明和定义的框架用于记录的过程.

package net.tunghuynh.logging.core;
 
import org.apache.logging.log4j.Logger;
 
import java.util.ArrayList;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
 
public abstract class ThreadManager {
    public final int BATCH_SIZE = 10;//Chương trình sẽ thực hiện doProcess khi hàng đợi vượt quá 10 phần tử
    public final long WAIT_TIME_OUT = 1000; //ms
    public final long TIME_OUT = 2 * 1000; //ms. Chương trình sẽ thực hiện doProcess với chu kỳ 2 giây 
    private final Logger logger = org.apache.logging.log4j.LogManager.getLogger(ThreadManager.class);
    private final BlockingQueue sourceQueue = new LinkedBlockingQueue();
    protected ArrayList items = new ArrayList(BATCH_SIZE);
    protected AtomicBoolean shouldWork = new AtomicBoolean(true);
    protected AtomicBoolean isRunning = new AtomicBoolean(true);
    private boolean listening = false;
    private String name = "DB LOGGER";
    protected ExecutorService executorService = Executors.newFixedThreadPool(5);
    private Thread mainThread;
 
    public ThreadManager() {
        logger.debug("Start task manager named: " + name);
        mainThread = new Thread(new Runnable() {
            @Override
            public void run() {
                logger.info("Queued job manager " + name + " is running and watching for queue... ");
                isRunning.set(true);
                int recNum = 0;
                long lgnStart = System.currentTimeMillis();
                while (shouldWork.get()) {
                    try {
                        Object item = sourceQueue.poll(WAIT_TIME_OUT, TimeUnit.MILLISECONDS);
                        if (item != null) {
                            items.add(item);
                            recNum++;
                        }
 
                        if (recNum >= BATCH_SIZE || timedOut(lgnStart)) {
                            if (items.size() > 0) {
                                logger.info(String.format("Thread %s: %s submits %d item(s)",
                                        Thread.currentThread().getName(), name, items.size()));
                                doProcess(items);
                                items = new ArrayList(BATCH_SIZE);
                                lgnStart = System.currentTimeMillis();
                                recNum = 0;
                            }
                        }
                    } catch (Exception e) {
                        logger.error(e.getMessage());
                    }
                    isRunning.set(false);
                }
                logger.info("Taskmanager " + name + " is stopped!!");
            }
 
            private boolean timedOut(Long startTime) {
                return System.currentTimeMillis() - startTime > TIME_OUT;
            }
        });
 
    }
 
    /**
     * abstract method xử lý nghiệp vụ
     * @param items
     */
    public abstract void doProcess(ArrayList items);
 
    /**
     * Bắt đầu lắng nghe dữ liệu cần xử lý từ hàng đợi
     */
    public synchronized void listen() {
        if (!listening) {
            mainThread.start();
            listening = true;
        }
    }
 
    public BlockingQueue getSourceQueue() {
        return sourceQueue;
    }
 
    public void stop() {
        logger.info(String.format("%s received a termination signal, stopping ... ", name));
        this.shouldWork.set(false);
        int tryTime = 0;
        while (isRunning.get() &amp;&amp; tryTime < 50) {
            try {
                Thread.currentThread().sleep(50L);
            } catch (Exception ex) {
 
            }
            tryTime++;
        }
    }
 
    /**
     * Submit một đối tượng cần xử lý vào hàng đợi
     * @param item
     */
    public void submit(Object item) {
        sourceQueue.offer(item);
    }
}

本节将相对复杂的你是谁没有多少接触到手柄多线程 在Java核心. 我会形容过去如下:
- BlockingQueue sourceQueue: 排队对象日志 必要插入, 外部将一个元素添加到队列经由方法submit()
- ExecutorService executorService: 服务实现, 已固定配置只允许创建5 线同时, 尽管被称为几次,只有最大 5 创建线程, 避免产生太大的影响线程高性能服务器
- ArrayList items: 列表,以保存对象被从队列中删除sourceQueue 指挥doProcess 到插入 在数据库
- 流处理器: 在这里你有1线 创建和开始 1 在所述方法中的唯一时间listen(), 这个线程的应用程序的运行过程中存在的. 内侧把手的连续螺纹读数据 从队列sourceQueue 要添加到列表项目. 这里最重要的是满足 1 在 2 条件: 名单项目 超过BATCH_SIZE 或元素存储在项目列表太TIME_OUT 将推动项目 去插入. 条件忌 插入太多记录 1 时间, 和忌在全存储数据太长 没有插入. 发送后doProcess 他们必须明确名单项目 和重启 可变计时器暂停. 你可以看到下面的图片直观更容易.

创建后, 2 抽象 一般以上, 我将创建类处理 记录.
第一个是 class LogThread 得到 extends Task, 任务 插入 1 名单 对象 日志 在 数据库. 这部分是相似于上述的例子,但另一种是插入 1 只有列表.

package net.tunghuynh.logging.core;
 
import net.tunghuynh.preparestatement.DatabaseUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.List;
 
public class LogThread extends Task {
    private final Logger logger = LogManager.getLogger(LogThread.class);
 
    @Override
    public Integer call() throws Exception {
        List lstLog = getItems();
        try {
            if (lstLog != null &amp;&amp; !lstLog.isEmpty()) {
                save(lstLog);
            }
        } catch (Exception e) {
            logger.error(e.toString(), e);
            return 0;
        }
        return 1;
    }
 
    public void save(List<Logs> lst) {
        PreparedStatement stmt = null;
        Connection con = null;
        try {
            con = DatabaseUtils.getConnection();
            stmt = con.prepareStatement("INSERT INTO AD_SCHEDULE_LOG " +
                    " (LOGS_ID, LEVEL_LOG, CONTENT, CREATE_TIME)" +
                    " VALUES(LOGS_SEQ.NEXTVAL, ?, ?, SYSDATE)");
            for(Logs item : lst){
                stmt.setString(1, item.getLevelLog());
                stmt.setString(2, item.getContent());
                stmt.execute();
            }
        } catch (Exception ex) {
            logger.error(ex.getMessage(), ex);
        } finally {
            DatabaseUtils.closeObject(stmt);
            DatabaseUtils.closeObject(con);
        }
    }
}

接下来,我将创建 class LogManager 得到 extends ThreadManager, 任务是让对象列表,以保持原木 队列 在 ThreadManager 到 提交 在 ExecutorService.
这段话可以有 1 一些人怀疑你为什么不使用 插入西奥Lô. 因为在 ThreadManager 从队列中读取出来, 和最多只 10 元素 被推入名单 doProcess, 手段本质上是 批量 并且不需要治疗在这里了.

package net.tunghuynh.logging.core;
 
import java.util.ArrayList;
 
public class LogManager extends ThreadManager {
    @Override
    public void doProcess(ArrayList items) {
        LogThread logThread = new LogThread();
        logThread.setItems(items);
        executorService.submit(logThread);
    }
}

来到这里让你想像, 当发生的其他类型的日志保持对 其他表, 或 发送QUA FTP,...或必要的处理任何其他任务 异步 它可以被重用 abstract class ThreadManager 骨架, 刚刚重新定义其他同级 LogThread 处理主要的商业和 LogManager 从队列中读取的数据. 所以,你有一个基本的流程框架.
最后,使用异步日志成以上创建的数据库,我写 1 段 测试在 Main.java

package net.tunghuynh.logging;
 
import net.tunghuynh.logging.core.Logs;
import net.tunghuynh.preparestatement.DatabaseUtils;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
 
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
 
public class Main {
    static Logger logger = LogManager.getLogger(Main.class);
    public static void main(String[] args) {
        //Tạo đối tượng test để ghi log
        Logs logs = new Logs();
        logs.setLevelLog("INFO");
        logs.setContent("Content");
        long start = new Date().getTime();
        //Khởi tạo LogManager để ghi log bất đồng bộ
        net.tunghuynh.logging.core.LogManager logManager = new net.tunghuynh.logging.core.LogManager();
        logManager.listen();
        //Submit vào hàng đợi
        logManager.submit(logs);
        logger.info("Async: " + (new Date().getTime()-start) + "ms");
        start = new Date().getTime();
        //Ghi log không dùng thread
        new Main().save(logs);
        logger.info("No-async: " + (new Date().getTime()-start) + "ms");
    }
    public void save(Logs item) {
        PreparedStatement stmt = null;
        Connection con = null;
        try {
            con = DatabaseUtils.getConnection();
            stmt = con.prepareStatement("INSERT INTO LOGS (LOGS_ID, LEVEL_LOG, CONTENT, CREATE_TIME)" +
                    " VALUES(LOGS_SEQ.NEXTVAL, ?, ?, SYSDATE)");
            stmt.setString(1, item.getLevelLog());
            stmt.setString(2, item.getContent());
            stmt.execute();
        } catch (Exception ex) {
            logger.error(ex.getMessage(), ex);
        } finally {
            DatabaseUtils.closeObject(stmt);
            DatabaseUtils.closeObject(con);
        }
    }
}

在这个测试期间单独联合编写 不使用线程 初始和写作 异步 新建筑测量处理时间
尝试运行和查看结果

2019-07-12 16:34:58 PM INFO  [Thread-1] net.tunghuynh.logging.core.ThreadManager.run : Queued job manager ACTION/API LOGGER is running and watching for queue... 
2019-07-12 16:34:58 PM INFO  [main] net.tunghuynh.logging.Main.main : Async: 4ms
2019-07-12 16:35:00 PM INFO  [main] net.tunghuynh.logging.Main.main : No-async: 1898ms
2019-07-12 16:35:00 PM INFO  [Thread-1] net.tunghuynh.logging.core.ThreadManager.run : Thread Thread-1: ACTION/API LOGGER submits 1 item(s)
2019-07-12 16:35:29 PM INFO  [Thread-1] net.tunghuynh.logging.core.ThreadManager.run : Queued job manager ACTION/API LOGGER is running and watching for queue... 
2019-07-12 16:35:29 PM INFO  [main] net.tunghuynh.logging.Main.main : Async: 7ms
2019-07-12 16:35:31 PM INFO  [main] net.tunghuynh.logging.Main.main : No-async: 1519ms
2019-07-12 16:35:31 PM INFO  [Thread-1] net.tunghuynh.logging.core.ThreadManager.run : Thread Thread-1: ACTION/API LOGGER submits 1 item(s)

你看,我们看到的结果 区别 巨大. 随着日志 异步 只有处理时间 下面 10 毫秒, 因为事情已经运行,就可以为其他人处理它. 还 插入常见 他们必须采取 1.5 到 2 秒 (长于 500 时间), 非常显著使用.
在对自己的测试只是放置到主要功能测试,方便, 你可以 在里面 该 ThreadManager在 1 单位 static 使用始终函数 提交 而无需重置多次, 或者如果你使用 弹簧 应该创建 1 豆角,扁豆 给 LogManager 同 构造函数 是 listen() 和 破坏 是 stop() 使用

@Bean(name = "logManager", initMethod = "listen", destroyMethod = "stop")
public LogManager getLogManager() {
    return new LogManager();
}

需要的时候 Bean logManager 这就是 Autowired 像其他任何人一样 豆角,扁豆 另一个可用.

@Autowired
LogManager logManager;

在本演示中,您已经创建了一个模块 异步日志 在 数据库 足够使用和安全. 您可以通过调用以下命令来完全添加日志记录功能 该API保存日志 其他, 发送日志 唱 服务器 不同 的FTP,.... 而不是只写 数据库, 取决于使用目的而不会影响主要应用.
祝你成功

下载代码LogDBExample.zip