[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 あなたが使用していない理由はいくつかは疑問 テオ=ローを挿入. であるため 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);
    }
}

あなたがよく想像させるためにここに来て, 上の保つために、ログの他のタイプを被ったとき 他のテーブル, または 資格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();
}

必要なとき 豆のLogManager これはちょうどある場合は Autowired いつものように 他の可能な用途は、.

@Autowired
LogManager logManager;

このデモには、あなたはモジュールを作成しました 非同期でログインデータベース 適切かつ安全な. あなたは完全に呼び出すことによって、ロギングの機能を補完することができます APIのLUUログ その他, ログを送信 歌っ サーバ 介して他の FTP,.... 代わりに書き込みます データベース, メインアプリケーションに影響を与えることなく、使用目的に応じて.
私はあなたに成功をお祈り

ダウンロードコードLogDBExample.zip