[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