乐趣区

关于线程池:不看后悔的项目中线程池实际应用

前言:

最近在看线程池方面的内容,联合源码学习完其外部原理后,心想本人在我的项目中有理论应用过线程池吗?想了想,的确在我的项目中很多中央应用到了线程池;上面来简略聊下最近在日志方面中多线程的利用:

  • 服务接口日志异步线程池化入库解决
  • 定时工作中应用多线程进行日志清理

本文主线:

①、线程池基本原理解读;

②、线程池理论利用例子:

  • 线程池利用 Demo 我的项目构造形容
  • 服务接口日志异步线程池化入库解决
  • 定时工作中应用多线程进行日志清理

线程池基本原理解读:

啥也不说,先贴一张脑图,通过脑图对线程池疾速的进行理解;

除了看图外,也能够通过此文章《ava 线程池实现原理及其在美团业务中的实际》对线程池进行具体的理解;

线程池理论利用例子:

上面就来聊聊最近在我的项目日志中线程池的利用;

线程池利用 Demo 我的项目形容:

Demo 地址:https://github.com/leishen6/s…

下面说的两种日志方面线程池利用曾经写好了 Demo,是一个 SpringBoot 我的项目,我的项目构造如下图:

服务接口日志异步线程池化入库解决:

后盾服务接口我的项目中,常常须要对接口的申请报文和响应报文日志做入库保留;

上面将通过比照 一般形式入库操作和线程池形式入库操作 ,来说说为什么线程池式入库更加优雅;

一般形式入库操作:

一般入库就是间接进行完业务逻辑解决并构建好响应后同时将日志进行入数据库,入库胜利后再将响应返回;

流程图如下:

然而这样存在一个很大的弊病就是因为多了一次数据库操作(日志入库),进而可能会导致响应速度比较慢;

上面就聊聊怎么通过线程池对日志入库进行优化,晋升接口的响应速度;

线程池形式入库操作:

线程池形式入库,能够将日志间接放入到队列中,而后就间接返回响应,最初应用线程池中的线程取出队列中的日志数据异步做入库操作;

流程图如下:

应用线程池形式,解决申请的主线程能够将日志放入到队列后,间接将响应返回,而后再应用线程池中的线程取出队列中的日志数据异步的将其进行入库;因为缩小了一次数据库操作,会极大的晋升接口响应速度。

上面来看看代码实现:

1、下面说到的寄存申请报文和响应报文日志的队列:LinkedBlockingDeque

// 基于链表的双向阻塞队列,在队列的两端都能够插入和移除元素,是线程平安的,多线程并发下效率更高
BlockingQueue<TestLogBean> queue = new LinkedBlockingDeque<TestLogBean>(MAX_QUEUE_SIZE);

除了 LinkedBlockingDeque 阻塞队列外,还有一些其它常常会用到的阻塞队列,如下图:

2、我的项目中进行日志入库操作的线程池: 单线程的线程池 + 固定数线程的线程池

  • 单线程的线程池:用来循环的监听队列中的日志数量以及决策什么时候将队列中的日志取出交由固定数线程的线程池做入库操作;
  • 固定数线程的线程池:次要用来进行日志的入库操作;

局部代码实现如下:

/**
 *  初始化
 */
public void init(){
    // 基于链表的双向阻塞队列,在队列的两端都能够插入和移除元素,是线程平安的,多线程并发下效率更高
    queue = new LinkedBlockingDeque<TestLogBean>(MAX_QUEUE_SIZE);
    lastExecuteTime = System.currentTimeMillis();

    logger.info("LogPoolManager init successfully......");

    logManagerThreadPool.execute(new Runnable() {
        @Override
        public void run() {while (run.get()){
                try {
                    // 线程休眠,具体工夫依据我的项目的理论状况配置
                    Thread.sleep(SLEEP_TIME);
                } catch (InterruptedException e) {logger.error("log Manager Thread sleep fail", e);
                }
                // 满足寄存了 10 个日志  或  满足工夫距离曾经大于设置的最大工夫距离时  执行日志插入
                if (logCount.get() >= BATCH_SIZE || (System.currentTimeMillis() - lastExecuteTime) > MAX_EXE_TiME) {if (logCount.get() > 0) {logger.info("begin drain log queue to database...");
                        List<TestLogBean> list = new ArrayList<TestLogBean>();
                        /**
                             *  drainTo (): 一次性从 BlockingQueue 获取所有可用的数据对象(还能够指定获取数据的个数),*  通过该办法,能够晋升获取数据效率;不须要屡次分批加锁或开释锁。*  将取出的数据放入指定的 list 汇合中
                             */
                        queue.drainTo(list);
                        // 工作队列 中工作数量置为 0
                        logCount.set(0);
                        // 从线程池中取出线程执行日志插入
                        logWorkerThreadPool.execute(new InsertThread(testLogService, list));
                        logger.info("end drain log queue to database...");
                    }
                    // 获取以后执行的工夫
                    lastExecuteTime = System.currentTimeMillis();}
            }
            logger.info("LogPoolManager shutdown successfully");
        }
    });
}

本我的项目中测试服务接口日志异步线程池化入库解决,我的项目启动后,在浏览器输出上面 URL,并刷新页面即可:

http://127.0.0.1:8081/v1/api/log/test

定时工作中应用多线程进行日志清理:

当日志表中的数据量过多,占用了太多的磁盘空间,导致磁盘一直的告警,此时须要对日志表进行瘦身;

此时能够应用多线程将日志表中局部数据清理掉,开释磁盘空间;

日志表中哪些数据须要清理掉呢?上面这个场景就可能会在需要中呈现:

领导说须要保留日志表中最新一年的数据,就是从以后日期向前推 365 天;例如:明天 2020-12-30,向前推 365 天的日期是 2019-12-30,所以 2019-12-30 之前生成的日志都须要清理掉;

上面来看代码,因为须要尽可能保障程序的灵活性,所以须要将删除的表名,依据删除的字段等进行灵便的配置;配置参数如下图:application-cleanLog.properties 配置文件

## 线程池大小
threads.pool.num=8

## 须要进行清理的表
log.clean.table=t_test_log

## 清理时依据的字段
log.clean.filed=createts

## 每次清理的数据量大小
log.clean.batchCount=1000

## 每次定时清理时 循环清理的次数
log.clean.batchNum=6

## 需保留据以后多少天的最新数据,其余的数据才能够被清理掉
log.clean.dateNum=1

留神:

  • 线程池大小须要联合服务器硬件配置和理论的业务日志量大小进行合理配置;如果设置的过大,可能会占用过多的内存和频繁的进行上下文切换,从而可能导致效率变低;
  • 本我的项目中是依据日期字段进行数据清理的,如果表曾经依据日期进行了分区,能够间接依据分区进行清理,分区清理速度更放慢些,然而按分区进行删除本我的项目中暂未实现;
  • 每次清理的数据量大小:指的是进行一次 delete 时须要删除的数据量;倡议不要设置的太大,因为删除的数据量太大的话,可能会导致锁表,从而影响表的失常查问、新增等操作;
  • 定时工作执行时循环清理的次数:指的是定时工作执行时进行几次 delete 操作;下面有说道 每次 delete 的数据量别设置太大,那么在总清理的数据量不变的状况下,就须要将清理的次数设置大些;

    总清理的数据量 = 每次清理的数据量大小 * 清理的次数

多线程进行日志清理的流程图如下所示:

上面来看看代码实现:

局部代码实现如下所示:

/**
 *  多线程清理日志启动
 */
public void cleanLogStart(){

    // 循环进行日志清理的次数
    int whileNum = props.getInt("log.clean.batchNum");
    LogCleanBean logClean = null;

    while (whileNum > 0){

        // 查问合乎每次删除数据量的时间段
        List<String> list = logCleanService.selectTime(logCleanBean);
        if (list != null && list.size() > 0){logClean = new LogCleanBean();
            logClean.setTableName(logCleanBean.getTableName());
            logClean.setFieldName(logCleanBean.getFieldName());
            // 获取能够删除日志的最小生成工夫
            logClean.setMinTime(list.get(list.size()-1));
            // 获取能够删除日志的最大生成工夫
            logClean.setMaxTime(list.get(0));
            logCleanBean.setMinTime(logClean.getMinTime());

            // 此次查问曾经不满足设置的每次清理的数据量大小了,阐明曾经清理洁净了
            if (list.size() < logCleanBean.getBatchCleanCount()){whileNum = 0;}else {
                // 清理次数进行递加
                --whileNum;
            }
        }else {break;}
        // 进行多线程解决
        cleanManagerThreadPool.execute(new CleanThread(this.logCleanService, logClean));
    }
}

扩大:

本我的项目中应用 delete 并且依据工夫字段进行的数据删除,如果是 MySql 数据库 的话,则在删除数据后去查看磁盘空间的话,发现可用磁盘空间并没有增多,并且可用磁盘空间还有可能缩小了呢,why 为什么?

  • 因为在 InnoDB 存储引擎中,delete 其实并不会真的把数据删除,mysql 实际上只是给删除的数据打了个标记为已删除,因而 delete 删除表中的数据时,表文件在磁盘上所占空间不会变小,存储空间不会被开释,只是把删除的数据行设置为不可见。然未开释磁盘空间,然而下次插入数据的时候,依然能够重用这部分空间(重用 → 笼罩)。
  • 并且因为在 delete 删除数据时,会记录 binlog 日志 等,如果删除的数据中存在 Text 和 BLOB 等大字段,可能日志文件会变得额定的大,占用局部磁盘空间,这就会导致 free 磁盘空间的进一步缩小;

解决方案:

  • 如果删除的数据中不存在 Text 和 BLOB 等大字段,能够间接不必管,间接笼罩应用,并且期待 MySql 的主动清理操作即可,然而须要肯定工夫;
  • 能够在 delete 操作当前应用 optimize table table_name 会立即开释磁盘空间。然而,因为 optimize 执行时会将表锁住,所以不要在高峰期应用,也不要常常应用;因为其会阻塞失常的查问、更新等操作的。

也能够应用 truncate、drop 进行数据的删除, 疾速开释磁盘空间 ;具体联合以后我的项目的理论状况进行抉择。

— END —

本文中介绍了在日志解决场景中线程池的应用场景,除此之外,还有很多场景应用到了线程池,并且在很多框架中也应用到了线程池,大家也能够通过浏览框架源码学习线程池的应用办法。

❤ 点赞 + 评论 + 转发 哟

如果本文对您有帮忙的话,请挥动下您爱发财的小手点下赞呀,您的反对就是我一直创作的能源,谢谢啦!

您能够微信搜寻【木子雷】公众号,大量 Java 学习干货文章,您能够来瞧一瞧哟!

退出移动版