分类: java

  • 某团面试题:JVM 堆内存溢出后,其他线程是否可继续工作?

    快乐分享,Java干货及时送达👇

    文章来源:http://t.csdn.cn/g8GN1


    最近网上出现一个美团面试题:“一个线程OOM后,其他线程还能运行吗?”。

    我看网上出现了很多不靠谱的答案。

    这道题其实很有难度,涉及的知识点有jvm内存分配、作用域、gc等,不是简单的是与否的问题。

    由于题目中给出的OOM,java中OOM又分很多类型;比如:

    • 堆溢出(“java.lang.OutOfMemoryError: Java heap space”)

    • 永久带溢出(“java.lang.OutOfMemoryError:Permgen space”)

    • 不能创建线程(“java.lang.OutOfMemoryError:Unable to create new native thread”)

    等很多种情况。

    本文主要是分析堆溢出对应用带来的影响。

    先说一下答案,答案是还能运行 。

    代码如下:

    public class JvmThread {


        public static void main(String[] args) {
            new Thread(() -> {
                Listbyte[]> list = new ArrayListbyte[]>();
                while (true) {
                    System.out.println(new Date().toString() + Thread.currentThread() + "==");
                    byte[] b = new byte[1024 * 1024 * 1];
                    list.add(b);
                    try {
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();

            // 线程二
            new Thread(() -> {
                while (true) {
                    System.out.println(new Date().toString() + Thread.currentThread() + "==");
                    try {
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

    结果展示:

    Wed Nov 07 14:42:18 CST 2018Thread[Thread-1,5,main]==
    Wed Nov 07 14:42:18 CST 2018Thread[Thread-0,5,main]==
    Wed Nov 07 14:42:19 CST 2018Thread[Thread-1,5,main]==
    Wed Nov 07 14:42:19 CST 2018Thread[Thread-0,5,main]==
    Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
     at com.gosaint.util.JvmThread.lambda$main$0(JvmThread.java:21)
     at com.gosaint.util.JvmThread$$Lambda$1/521645586.run(Unknown Source)
     at java.lang.Thread.run(Thread.java:748)
    Wed Nov 07 14:42:20 CST 2018Thread[Thread-1,5,main]==
    Wed Nov 07 14:42:21 CST 2018Thread[Thread-1,5,main]==
    Wed Nov 07 14:42:22 CST 2018Thread[Thread-1,5,main]==

    JVM启动参数设置:

    上图是JVM堆空间的变化。我们仔细观察一下在14:42:05~14:42:25之间曲线变化,你会发现使用堆的数量,突然间急剧下滑!

    这代表这一点,当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉,从而不会影响其他线程的运行!

    讲到这里大家应该懂了,此题的答案为:一个线程溢出后,进程里的其他线程还能照常运行

    注意了,这个例子我只演示了堆溢出的情况。如果是栈溢出,结论也是一样的,大家可自行通过代码测试。

    总结:

    其实发生OOM的线程一般情况下会死亡,也就是会被终结掉,该线程持有的对象占用的heap都会被gc了,释放内存。因为发生OOM之前要进行gc,就算其他线程能够正常工作,也会因为频繁gc产生较大的影响。

  • 灵魂一问:SELECT COUNT(*) 会造成全表扫描吗?

    快乐分享,Java干货及时送达👇


    来源:程序员大彬

    • 前言
    • SQL 选用索引的执行成本如何计算
    • 实例说明
    • 总结

    前言

    SELECT COUNT(*)会不会导致全表扫描引起慢查询呢?

    SELECT COUNT(*) FROM SomeTable  

    网上有一种说法,针对无 where_clauseCOUNT(*),MySQL 是有优化的,优化器会选择成本最小的辅助索引查询计数,其实反而性能最高,这种说法对不对呢

    针对这个疑问,我首先去生产上找了一个千万级别的表使用  EXPLAIN 来查询了一下执行计划

    EXPLAIN SELECT COUNT(*) FROM SomeTable  

    结果如下

    图片

    如图所示: 发现确实此条语句在此例中用到的并不是主键索引,而是辅助索引,实际上在此例中我试验了,不管是 COUNT(1),还是 COUNT(*),MySQL 都会用成本最小 的辅助索引查询方式来计数,也就是使用 COUNT(*) 由于 MySQL 的优化已经保证了它的查询性能是最好的!随带提一句,COUNT(*)是 SQL92 定义的标准统计行数的语法,并且效率高,所以请直接使用COUNT(*)查询表的行数!

    所以这种说法确实是对的。但有个前提,在 MySQL 5.6 之后的版本中才有这种优化。

    那么这个成本最小该怎么定义呢,有时候在 WHERE 中指定了多个条件,为啥最终 MySQL 执行的时候却选择了另一个索引,甚至不选索引?

    本文将会给你答案,本文将会从以下两方面来分析

    • SQL 选用索引的执行成本如何计算
    • 实例说明

    SQL 选用索引的执行成本如何计算

    就如前文所述,在有多个索引的情况下, 在查询数据前,MySQL 会选择成本最小原则来选择使用对应的索引,这里的成本主要包含两个方面。

    • IO 成本: 即从磁盘把数据加载到内存的成本,默认情况下,读取数据页的 IO 成本是 1,MySQL 是以页的形式读取数据的,即当用到某个数据时,并不会只读取这个数据,而会把这个数据相邻的数据也一起读到内存中,这就是有名的程序局部性原理,所以 MySQL 每次会读取一整页,一页的成本就是 1。所以 IO 的成本主要和页的大小有关
    • CPU 成本:将数据读入内存后,还要检测数据是否满足条件和排序等 CPU 操作的成本,显然它与行数有关,默认情况下,检测记录的成本是 0.2。

    实例说明

    为了根据以上两个成本来算出使用索引的最终成本,我们先准备一个表(以下操作基于 MySQL 5.7.18)

    CREATE TABLE `person` (  
      `id` bigint(20) NOT NULL AUTO_INCREMENT,  
      `name` varchar(255) NOT NULL,  
      `score` int(11) NOT NULL,  
      `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,  
      PRIMARY KEY (`id`),  
      KEY `name_score` (`name`(191),`score`),  
      KEY `create_time` (`create_time`)  
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;  

    这个表除了主键索引之外,还有另外两个索引, name_scorecreate_time。然后我们在此表中插入 10 w 行数据,只要写一个存储过程调用即可,如下:

    CREATE PROCEDURE insert_person()  
    begin  
        declare c_id integer default 1;  
        while c_iddo  
        insert into person values(c_id, concat('name',c_id), c_id+100, date_sub(NOW(), interval c_id second));  
        set c_id=c_id+1;  
        end while;  
    end  

    插入之后我们现在使用 EXPLAIN 来计算下统计总行数到底使用的是哪个索引

    EXPLAIN SELECT COUNT(*) FROM person  

    图片

    从结果上看它选择了 create_time 辅助索引,显然 MySQL 认为使用此索引进行查询成本最小,这也是符合我们的预期,使用辅助索引来查询确实是性能最高的!

    我们再来看以下 SQL 会使用哪个索引

    SELECT * FROM person WHERE NAME >'name84059' AND create_time>'2020-05-23 14:39:18'   

    图片

    用了全表扫描!理论上应该用 name_score 或者 create_time 索引才对,从 WHERE 的查询条件来看确实都能命中索引,那是否是使用 SELECT * 造成的回表代价太大所致呢,我们改成覆盖索引的形式试一下

    SELECT create_time FROM person WHERE NAME >'name84059' AND create_time > '2020-05-23 14:39:18'   

    结果 MySQL 依然选择了全表扫描!这就比较有意思了,理论上采用了覆盖索引的方式进行查找性能肯定是比全表扫描更好的,为啥 MySQL 选择了全表扫描呢,既然它认为全表扫描比使用覆盖索引的形式性能更好,那我们分别用这两者执行来比较下查询时间吧

    -- 全表扫描执行时间: 4.0 ms  
    SELECT create_time FROM person WHERE NAME >'name84059' AND create_time>'2020-05-23 14:39:18'   
      
    -- 使用覆盖索引执行时间: 2.0 ms  
    SELECT create_time FROM person force index(create_time) WHERE NAME >'name84059' AND create_time>'2020-05-23 14:39:18'   

    从实际执行的效果看使用覆盖索引查询比使用全表扫描执行的时间快了一倍!说明 MySQL 在查询前做的成本估算不准!我们先来看看 MySQL 做全表扫描的成本有多少。

    前面我们说了成本主要 IO 成本和 CPU 成本有关,对于全表扫描来说也就是分别和聚簇索引占用的页面数和表中的记录数。执行以下命令

    SHOW TABLE STATUS LIKE 'person'  

    图片

    可以发现

    1. 行数是 100264,我们不是插入了 10 w 行的数据了吗,怎么算出的数据反而多了,其实这里的计算是估算 ,也有可能这里的行数统计出来比 10 w 少了,估算方式有兴趣大家去网上查找,这里不是本文重点,就不展开了。得知行数,那我们知道 CPU 成本是 100264 * 0.2 = 20052.8
    2. 数据长度是 5783552,InnoDB 每个页面的大小是 16 KB,可以算出页面数量是 353。

    也就是说全表扫描的成本是 20052.8 + 353 = 20406

    这个结果对不对呢,我们可以用一个工具验证一下。在 MySQL 5.6 及之后的版本中,我们可以用 optimizer trace 功能来查看优化器生成计划的整个过程 ,它列出了选择每个索引的执行计划成本以及最终的选择结果,我们可以依赖这些信息来进一步优化我们的 SQL。

    optimizer_trace 功能使用如下

    SET optimizer_trace="enabled=on";  
    SELECT create_time FROM person WHERE NAME >'name84059' AND create_time > '2020-05-23 14:39:18';  
    SELECT * FROM information_schema.OPTIMIZER_TRACE;  
    SET optimizer_trace="enabled=off";  

    执行之后我们主要观察使用 name_scorecreate_time 索引及全表扫描的成本。

    先来看下使用 name_score 索引执行的的预估执行成本:

    {  
        "index""name_score",  
        "ranges": [  
          "name84059   
        ],  
        "index_dives_for_eq_ranges"true,  
        "rows": 25372,  
        "cost": 30447  
    }  

    可以看到执行成本为 30447,高于我们之前算出来的全表扫描成本:20406。所以没选择此索引执行

    注意:这里的 30447 是查询二级索引的 IO 成本和 CPU 成本之和,再加上回表查询聚簇索引的 IO 成本和 CPU 成本之和。

    再来看下使用 create_time 索引执行的的预估执行成本:

    {  
        "index""create_time",  
        "ranges": [  
          "0x5ec8c516   
        ],  
        "index_dives_for_eq_ranges"true,  
        "rows": 50132,  
        "cost": 60159,  
        "cause""cost"  
    }  

    可以看到成本是 60159,远大于全表扫描成本 20406,自然也没选择此索引。

    再来看计算出的全表扫描成本:

    {  
        "considered_execution_plans": [  
          {  
            "plan_prefix": [  
            ],  
            "table""`person`",  
            "best_access_path": {  
              "considered_access_paths": [  
                {  
                  "rows_to_scan": 100264,  
                  "access_type""scan",  
                  "resulting_rows": 100264,  
                  "cost": 20406,  
                  "chosen"true  
                }  
              ]  
            },  
            "condition_filtering_pct": 100,  
            "rows_for_plan": 100264,  
            "cost_for_plan": 20406,  
            "chosen"true  
          }  
        ]  
    }  

    注意看 cost:20406,与我们之前算出来的完全一样!这个值在以上三者算出的执行成本中最小,所以最终 MySQL 选择了用全表扫描的方式来执行此 SQL。

    实际上 optimizer trace 详细列出了覆盖索引,回表的成本统计情况,有兴趣的可以去研究一下。

    从以上分析可以看出, MySQL 选择的执行计划未必是最佳的,原因有挺多,就比如上文说的行数统计信息不准,再比如 MySQL 认为的最优跟我们认为不一样,我们可以认为执行时间短的是最优的,但 MySQL 认为的成本小未必意味着执行时间短。

    总结

    本文通过一个例子深入剖析了 MySQL 的执行计划是如何选择的,以及为什么它的选择未必是我们认为的最优的,这也提醒我们,在生产中如果有多个索引的情况,使用 WHERE 进行过滤未必会选中你认为的索引,我们可以提前使用  EXPLAIN, optimizer trace 来优化我们的查询语句。

  • 一次由热部署导致的 OOM 排查经历 !

    快乐分享,Java干货及时送达👇

    来源:juejin.cn/post/7079761581546373127

    • 查看 JVM 的内存使用情况
    • 分析 MetaSpace OOM 原因
    • 快速止血
    • 代码分析
    • 总结

    开发中,将代码部署到 test 环境测试的时候,容器经常因为 OOM 重启,而相同的代部署在 prod 环境则没有问题,怀疑是因为近期 test 环境更换了热部署基础镜像包导致的(由于我们的服务只能在 test 环境进行测试,因此使用了公司的代码修改热部署插件)。于是想通过一些 JVM 工具排查下 OOM 原因,最终发现了历史代码中存在的 bug,同时新的热部署基础镜像包放大了这种影响,导致 metaspace 内存泄漏,排查过程如下:

    查看 JVM 的内存使用情况

    观察JVM的内存使用情况,主要用了 Arthasdashboard 命令和jdk自带的 jstat 工具。首先,查看 arthasdashboard 数据面板的 Memory 区域,发现 metaspace 区内存使用率一致在增加,直到超过 MaxMetaspaceSize 设置的值,发生 metaspace 的OOM。

    观察JVM的内存使用情况

    进一步,通过jdk的 jstat 工具查看gc情况,上一次gc是因为 metaspace 内存占用超过了gc 阈值,同时 metaspace 的使用率一直在90%以上,更加验证了 metaspace OOM:

    观察JVM的内存使用情况

    分析 MetaSpace OOM 原因

    JDK8 之后,metaspace 对应于java运行时数据区的 方法区 ,用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据,采用本地内存(Native Memory)来实现 方法区 ,本身没有大小限制(受物理内存大小限制),但可以使用 -XX:MaxMetaspaceSize 参数设置上限,我们这边设置的是2G。

    既然是因为类的元数据信息撑爆了元空间,那就看一下类的装载和卸载情况吧,使用 jstat 工具看下类加载情况,并对比 testprod 两个环境的差异:

    「test」

    分析 MetaSpace OOM 原因

    「prod」

    分析 MetaSpace OOM 原因

    prod 环境的服务加载了很多类也卸载了很多类(这其实是个线上问题,通过这次排查才发现的,后面分析原因),而 test 更加严重,加载了很多类,但几乎没有卸载过类。

    重点看下 test 服务的类加载情况,使用 Arthasclassloader 命令:

    分析 MetaSpace OOM 原因

    第一眼就觉得 AviatorClassLoader 比较可疑,其他加载器都是spring和jdk的,这个是谷歌的,同时这个类加载器的实例数量和加载的类的数量非常大,同时随着服务的运行在不断的增长。仅仅是类加载器的实例数量大倒还好,毕竟它的 Class 对象就一份,不会撑爆元空间,但它加载的 Class 会有问题,因为判断是不是同一个类,是由 「加载它的类加载器+全限定类名」 一起决定,于是乎,可以从这个类加载器入手,代码中全局搜索一下,果然找到了引入点。

    快速止血

    我们的项目是接手的一个报表类的服务,项目中用到了一个表达式计算引擎 AviatorEvaluator ,用来根据表达式字符串计算复合指标,其伪代码如下表示:

    public static synchronized Object process(Map eleMap, String expression) {
        // 表达式引擎实例
        AviatorEvaluatorInstance instance = AviatorEvaluator.newInstance();
        // 生产表达式对象
        Expression compiledExp = instance.compile(expression, true);
        // 计算表达式结果
        return compiledExp.execute(eleMap);
    }

    其中 expression 是表达式字符串,eleMap 是表达式中变量名和具体值的键值对,比如 expression 对应 a+b ,eleMap对应的键值对为 {"a":10, "b":20} ,则process方法的结果为30

    之前从未仔细看过这段代码,现在看下来似乎有点不太对劲:synchronizednewInstance 是重复了吗?synchronized 是用来做同步的,说明这段代码中有共享资源竞争的情况,应该就是 AviatorEvaluator 实例了,目前的逻辑每次执行 process 都会实例化一个 AviatorEvaluator 对象,这已经不仅仅是线程私有了,而是每一个线程每次调用这个方法都会实例化一个对象,已经属于 「线程封闭」 的场景了,不需要用 synchronized 关键字做同步。我们的业务场景对这个方法的调用量非常大,每个指标的计算都会调用这个方法。至此,有以下结论:

    1. synchronized 同步和线程封闭每个线程私有一个对象二者选其一就行;
    2. 如果 AviatorEvaluator 是线程安全的话,使用单例模式就行,可以减轻堆区的内存压力;

    谷歌出品的工具类不是线程安全的?不会吧?

    查阅资料得知,AviatorEvaluatorexecute 方法是线程安全的,代码里的使用姿势不对,修改代码如下,重新发布,不再OOM了。

    // 删除 synchronized
    public static Object process(Map eleMap, String expression) {
       AviatorEvaluator.execute(expression, eleMap, true); // true 表示使用缓存
    }

    代码分析

    但是,还是有两点难以解释:a) 为什么是 metaspace ?b) 为什么使用热部署镜像的 test 环境出现 OOM,而 prod 没有出现 OOM?

    如果是因为 AviatorEvaluator 对象太多导致的,那也应该是堆区 OOM;同时,prod 环境的请求量远大于 test 环境,如果是像目前 test 这种 metaspace 的膨胀速度,线上肯定也是会 OOM 的,差异在于 test 用的热部署的基础镜像包。

    「首先探寻第一个问题的答案,为什么是 metaspace ?」

    热部署?ClassLoader? 方法区?此时,脑海里回想起一道经典八股文:动态代理的两种方式?(JDK动态代理和CGLIB动态代理的区别?AOP的实现原理?运行期、类加载期的字节码增强?)

    感觉这次 OOM 很大可能是 ** 「ClassLoader」 和 ** 「热部署(字节码增强)」** 撞出的火花 …**

    通过阅读 AviatorEvaluator 的源码,发现调用链简化后大概是这样:

    public Object execute(final String expression, final Map env, final boolean cached) {
      Expression compiledExpression = compile(expression, expression, cached); // 编译生成 Expression 对象
      return compiledExpression.execute(env);  // 执行表达式,输出结果
    }
    private Expression compile(final String cacheKey, final String exp, final String source, final boolean cached) {
     return innerCompile(expression, sourceFile, cached);  // 编译生成 Expression 对象
    }
    private Expression innerCompile(final String expression, final String sourceFile, final boolean cached) {
      ExpressionLexer lexer = new ExpressionLexer(this, expression);
      CodeGenerator codeGenerator = newCodeGenerator(sourceFile, cached); //!!这个方法 new AviatorClassLoader 的实例
      return new ExpressionParser(this, lexer, codeGenerator).parse(); 
    }
      public CodeGenerator newCodeGenerator(final String sourceFile, final boolean cached) {
        // 每个 AviatorEvaluatorInstance 一个 AviatorClassLoader 的实例作为成员变量
        AviatorClassLoader classLoader = this.aviatorClassLoader;
        //!!这个方法通过上面的类加载器不断生成并加载新的Class对象
        return newCodeGenerator(classLoader, sourceFile);
      }
    public CodeGenerator newCodeGenerator(final AviatorClassLoader classLoader, final String sourceFile) {
     ASMCodeGenerator asmCodeGenerator = 
        // 使用字节码工具ASM生成内部类
        new ASMCodeGenerator(this, sourceFile, classLoader, this.traceOutputStream);
    }
    public ASMCodeGenerator(final AviatorEvaluatorInstance instance, final String sourceFile,
        final AviatorClassLoader classLoader, final OutputStream traceOut) {
      // 生成了唯一的内部类
      this.className = "Script_" + System.currentTimeMillis() + "_" + CLASS_COUNTER.getAndIncrement();
    }

    这时候原因差不多清晰了,使用 AviatorEvaluatorInstance 对象计算表达式会使用成员变量里的一个 AviatorClassLoader 加载自定义的字节码生成 CodeGenerator 对象。AviatorEvaluatorInstance 用单例模式使用固然没什么问题,但是如果每次都 new 一个AviatorEvaluatorInstance 对象的话,就会有成百上千的 AviatorClassLoader 对象,这也解释了上面通过 Arthas 查看 classloader 会有这么多对应的实例,但 metaspceClass 还是只有一份的,问题不大。同时看到生成的字节码对象都是 Script_ 开头的,使用 arthassc 命令看下满足条件的类(数量非常多,只截取了部分),果然找到了 OOM 的元凶:

    OOM 的元凶

    「第二个疑问:为什么 prod 没有OOM?」

    上面通过 jstat 发现 prod 卸载了很多类,而 test 几乎不卸载类,两个环境唯一的区别是 test 用了热部署的基础镜像。

    咨询了负责热部署的同事,了解到热部署 agent 会对 classloader 有一些 强引用,监听 classloader 的加载的一些类来监听热更新,这会导致内存泄漏的问题。同时得到反馈,热部署后面会用 弱引用 优化。这里引用下《深入理解java虚拟机》中的解释:

    弱引用优化

    因为大量的 AviatorEvaluatorInstance 创建了大量的 AviatorClassLoader ,并被热部署 agent 强引用了,得不到回收,那么这些类加载器加载的 Script_* 的Class对象也得不到卸载,直到 metaspace OOM。

    通过 JVM 参数 -Xlog:class+load=info-Xlog:class+unload=info 看下 prod 环境的类加载和类卸载日志,确实有大量 Script_* 类的加载和卸载:

    类的加载和卸载

    类的加载和卸载

    test 环境这没有此种类的卸载,这边就不再贴图了。

    此刻,我比较好奇的是热部署包里的什么类型的对象强引用了我们的自定义类加载器?使用 jprofiler 看下两个环境的堆转储文件,不看不知道,一看吓一跳,问题比想象的更加严重:

    「prod」

    堆转储文件

    「test」

    堆转储文件

    对比 prodtest 环境的引用情况,test 环境存在热更新类 WatchHandler 对象到 AviatorClassLoader 的强引用,而 prod 环境不存在;进一步,选择一个具体的 AviatorClassLoader 实例看下引用情况,又有了重大发现:

    「prod」

    AviatorClassLoader

    「test」

    AviatorClassLoader

    prod 环境的 AviatorClassLoader 除了加载我们业务需要的自定义类 Script_*,还加载了很多热更新相关的类,同时不同 AviatorClassLoader 实例加载的热更新相关的类的 hashcode 也是不同了,说明每个 AviatorClassLoader 实例都加载了一轮,这才是元空间占用内存的大头。

    当然,在正确使用 AviatorEvaluator 的情况下(使用单例模式),就不会出现这么严重的问题了,但依然存在热部署 agent 对自定义 classloader 的强引用问题。

    总结

    这是我第一次系统地排查 OOM 问题,把之前一些零散模糊的知识点给串起来的,下面总结了一些本次排查涉及到的 JVM 基础概念和工具:

    「元空间:」

    • https://segmentfault.com/a/1190000012577387

    「类加载器:」

    • https://segmentfault.com/a/1190000037574626
    • https://segmentfault.com/a/1190000023666707

    「JDK工具:」

    • jps:JVM Process Status Tool 进程状况工具
    • jstat:JVM Statistics M onitoring Tool 统计信息监视工具
    • jinfo:Configuration Info for Java Java配置信息工具
    • jmap:Memory Map for Java 内存映像工具
    • jhat:JVM Heap Analysis Tool 堆转储快照分析工具
    • jstack:Stack Trace for Java 堆栈跟踪工具
    • Jcmd:多功能诊断命令行工具
    
    

  • 针对 jar 和 vue 的一键自动化部署工具,界面友好,操作简单(已开源)

    前言

    easy-jenkins是一款对vue和jar的部署工具,操作简单,实行一键部署,内部结构采用流水线形式架构,每次部署,时时提供部署过程,部署记录,界面友好简洁,使用方便,符合用户常规操作

    easy-jenkins面向分支形式,无需登录,默认分支为jenkins,每个分支可以配置多个数据源,切换不同分支可以管理不同数据源

    easy-jenkins采用本地存储的结构无需配置数据库,简单易上手

    提示:以下是本篇文章正文内容,下面案例可供参考

    一、项目地址

    开源地址:

    • https://gitee.com/susantyp/easy-jenkins

    二、使用步骤

    先把代码拉入你的本地

    1.项目结构

    2.启动主类 EasyJenkinsApplication

    3.安装

    启动后弹出当前窗体 点击下一步

    来到这里后,填写相应的信息

    • 安装路径
    • maven路径 打包需要
    • 以及项目端口的启动,避免不要和本地端口冲突,我们可以设置 8332 8899 9900 等端口

    点击安装并启动, 点击确认 等待几秒,项目自动启动

    4.项目启动图

    三、功能点介绍

    • 部署列表
    • 部署记录
    • 数据分支
    • 基本设置

    1.部署列表

    部署列表主要显示我们的连接信息

    1.添加连接(部署jar)

    我们点击按钮,添加连接

    添加本地项目地址

    添加本地项目地址后,它下面的文本款会根据本地项目地址自动生成,如图

    在这边需要确保:

    • 你的本地项目地址是正确的
    • jar名称正确的
    • pom.xml文件是正确的

    我部署项目的端口为8080

    根据你自己的项目设置端口

    添加服务器相关信息

    在我们的右边填写我们的服务器信息

    • 服务器ip
    • 账号
    • 密码
    • 端口

    上传的位置 后面不需要带 /

    上传的位置 默认生成一个命令

    如果上传位置是 /home/springboot

    则生成如下

    nohup java -jar /home/springboot/wall.jar & tailf /home/springboot/nohup.out

    可以修改为你自己需要运行的命令

    或者直接使用当前命令

    2.部署jar

    我们点击部署按钮即可

    部署过程效果图

    后台会实时返回部署的消息,返回给前端显示

    3.部署成功

    部署成功返回:Successfully deployed

    4.删除

    点击table 直接删除

    5.编辑

    编辑小伙伴可以自己玩一下

    2.部署记录

    部署记录主要记录了,最近部署的情况和统计信息

    3.数据分支

    easy-jenkins 是面向分支的

    不同分支存储不同的连接,默认分支为jenkins

    1.创建分支

    创建一个root的分支

    2.切换分支

    切换完成后,可以查看当前分支的状态

    当前我们就是root分支的环境下

    我们点击部署记录

    部署记录此时为空的,刚刚创建分支下面是没有连接数据的,需要重新添加连接

    我们切换为jenkins分支后,前面我们在jenkins添加了一条连接数据,下面就显示数据了,同时上面会标注当前的环境为jenkins

    注意了,正在使用的分支是不可以删除

    4.基本设置

    • 安装路径
    • maven路径
    • 项目端口号

    当前这三个值,是我们最初刚刚开始安装的时候的需要录入的值,我们可以点击编辑操作

    5.启动

    • 第一次启动会启动安装向导程序
    • 第二次启动直接启动浏览器,则不再启动安装向导程序

    6.如何部署vue

    dist 是vue项目默认build的位置

    同样 后面 不需要 ‘/’

    需要注意

    上传位置名字保持跟本地相同的名字,如图:

    然后填写你相应的服务器信息即可

    7.exe启动项目

    在我们exe文件夹下面,有一个easy-jenkins.exe文件

    可以将他拷贝到桌面,直接点击它运行即可,不需要每次启动springboot程序

    总结

    此部署工具主要针对于个人本地的部署

    针对于小型项目的部署,轻量级的,一键部署,操作简单

    作者:来自上海的这位朋友

    来源:blog.csdn.net/Susan003/article/

    details/128223343

  • Springboot代码混淆,别再让代码在线上进行裸奔

    快乐分享,Java干货及时送达👇

    来源:blog.csdn.net/qq_35387940/article/details/127426354

    • 编译
    • 反编译
    • 混淆
    • 正文

    编译

    简单就是把代码跑一哈,然后我们的代码 .java文件 就被编译成了 .class 文件

    图片

    反编译

    就是针对编译生成的 jar/war 包 里面的 .class 文件 逆向还原回来,可以看到你的代码写的啥。

    比较常用的反编译工具 JD-GUI ,直接把编译好的jar丢进去,大部分都能反编译看到源码:

    图片

    那如果不想给别人反编译看自己写的代码呢?

    怎么做?

    混淆

    该篇玩的代码混淆 ,是其中一种手段。

    我给你看,但你反编译看到的不是真正的代码。

    先看一张效果示例图 :

    图片

    开搞

    正文

    先看一下我们混淆一个项目代码,要做啥?

    图片

    一共就两步

    第一步, 在项目路径下,新增一份文件 proguard.cfg :

    proguard.cfg

    #指定Java的版本
    -target 1.8
    #proguard会对代码进行优化压缩,他会删除从未使用的类或者类成员变量等
    -dontshrink
    #是否关闭字节码级别的优化,如果不开启则设置如下配置
    -dontoptimize
    #混淆时不生成大小写混合的类名,默认是可以大小写混合
    -dontusemixedcaseclassnames
    # 对于类成员的命名的混淆采取唯一策略
    -useuniqueclassmembernames
    #混淆时不生成大小写混合的类名,默认是可以大小写混合
    -dontusemixedcaseclassnames
    #混淆类名之后,对使用Class.forName('className')之类的地方进行相应替代
    -adaptclassstrings
     
    #对异常、注解信息予以保留
    -keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod
    # 此选项将保存接口中的所有原始名称(不混淆)-->
    -keepnames interface ** { *; }
    # 此选项将保存所有软件包中的所有原始接口文件(不进行混淆)
    #-keep interface * extends * { *; }
    #保留参数名,因为控制器,或者Mybatis等接口的参数如果混淆会导致无法接受参数,xml文件找不到参数
    -keepparameternames
    # 保留枚举成员及方法
    -keepclassmembers enum * { *; }
    # 不混淆所有类,保存原始定义的注释-
    -keepclassmembers class * {
                            @org.springframework.context.annotation.Bean *;
                            @org.springframework.beans.factory.annotation.Autowired *;
                            @org.springframework.beans.factory.annotation.Value *;
                            @org.springframework.stereotype.Service *;
                            @org.springframework.stereotype.Component *;
                            }
     
    #忽略warn消息
    -ignorewarnings
    #忽略note消息
    -dontnote
    #打印配置信息
    -printconfiguration
    -keep public class com.example.myproguarddemo.MyproguarddemoApplication {
            public static void main(java.lang.String[]);
        }

    注意点:

    图片

    其余的看注释,可以配置哪些类不参与混淆,哪些枚举保留,哪些方法名不混淆等等。

    第二步,在pom文件上 加入proguard 混淆插件 :

    build标签里面改动加入一下配置


        
            
                com.github.wvengen
                proguard-maven-plugin
                2.6.0
                
                    
                    
                        package
                        
                            proguard
                        

                    

                

                
                    
                    ${project.build.finalName}.jar
                    
                    ${project.build.finalName}.jar
                    
                    true
                    
                    ${project.basedir}/proguard.cfg
                    
                    
                        ${java.home}/lib/rt.jar
                        ${java.home}/lib/jce.jar
                        ${java.home}/lib/jsse.jar
                    

                    
                    !META-INF/**,!META-INF/versions/9/**.class
                    
                    ${project.basedir}/target
                    
                    
                        
                    

                

            

            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            repackage
                        

                        
                            com.example.myproguarddemo.MyproguarddemoApplication
                        

                    

                

            

        


    注意点:

    图片

    图片

    然后可以看到:

    图片

    然后点击package,正常执行编译打包流程就可以 :

    图片

    然后可以看到jar的生成:

    图片

    看看效果:

    图片

    好了,该篇就到这。

    
    

  • 切记,任何时候都不要在 for 循环中删除 List 集合元素!!!

    前言

    首先说结论:无论什么场景,都不要对List使用for循环的同时,删除List集合元素,因为这么做就是不对的。

    阿里开发手册也明确说明禁止使用foreach删除、增加List元素。

    正确删除元素的方式是使用迭代器(Iterator),代码如下:

    List list = new ArrayList();
    Iterator iterator = list.iterator();
    while (iterator.hasNext()) {
        // 删除元素
        iterator.remove();
    }

    JDK8后lambda写法:list.removeIf(s -> s.contains("a"));

    不想知道为什么不能使用for循环删除List集合元素的,看完前言就可以关闭本页面了,想知道原因的继续往下看

    实例

    下面举个实例场景,看一下为什么不能使用for循环。

    需求

    一个List集合,元素类型为String,有N个元素,删除这些元素中包含字符”a”的元素。

    假设集合内容如下:

    List list = new ArrayList(4);
    list.add("a");
    list.add("ab");
    list.add("abc");
    list.add("abcd");

    正确答案

    先上正确答案

    public static void main(String[] args) {
        List list = new ArrayList(4);
        list.add("a");
        list.add("ab");
        list.add("abc");
        list.add("abcd");

        Iterator iterator = list.iterator();
        while (iterator.hasNext()) {
            if (iterator.next().contains("a")) {
                // 删除元素
                iterator.remove();
            }
        }
        System.out.println(list);
    }

    输出结果为

    []
    错误答案1:普通for循环(for-i)
    public static void main(String[] args) {
        List list = new ArrayList(4);
        list.add("a");
        list.add("ab");
        list.add("abc");
        list.add("abcd");

        for (int i = 0; i         if (list.get(i).contains("a")) {
                list.remove(i);
            }
        }
        System.out.println(list);
    }

    输出结果为

    [ab, abcd]

    分析

    普通for循环遍历List集合的同时,删除List中的元素是可以运行的代码,但在大多数场景下,不能使用这种方式,上边的结果也印证了这一点,虽然你的代码不会报错,运行也正常,但在本实例中,这么写就是BUG。

    BUG原因:索引为i的元素删除后,后边元素的索引自动向前补位,即原来索引为i+1的元素,变为了索引为i的元素,但是下一次循环取的索引是i+1,此时你以为取到的是原来索引为i+1的元素,其实取到是原来索引为i+2的元素。

    如下图示例:

    看图可以发现,只要每删除一个元素,就会漏掉下一个元素,所以这种方式从逻辑上来说是存在bug的,无论什么需求场景,都不建议用这种方式,因为不可控因素太多(鬼知道生产环境中他会删掉多少元素,同时漏掉多少元素)。

    既然这么写不报错,那么个别特殊场景确实可以使用这种普通for循环删除元素的,比如我们把实例的需求变动一下,改为:一个List集合,元素类型为String,有N个元素,删除这些元素中包含字符’a’的元素,如果有连续两个或以上元素包含’a’,那么只删除当前连续元素中的奇数位元素。虽然这种场景适用,但仍然不推荐,还是因为太不可控。

    错误答案2:增强for循环(foreach)
    public static void main(String[] args) {
        List list = new ArrayList(4);
        list.add("a");
        list.add("ab");
        list.add("abc");
        list.add("abcd");

        for (String str : list) {
            if (str.contains("a")) {
                list.remove(str);
            }
        }
        System.out.println(list);
    }

    运行报错:

    Exception in thread "main" java.util.ConcurrentModificationException
     at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
     at java.util.ArrayList$Itr.next(ArrayList.java:859)
     at top.oldmoon.learn.test.ListTest.main(ListTest.java:24)

    使用百度翻译可以知道:Concurrent Modification Exception:并发修改异常

    分析

    其实这里没啥好分析的,直接报错了,你还这么写干嘛?没事找罪受吗。。。

    可以简单的理解为:foreach就不支持对集合中的元素进行增删操作,但是可以修改。

    作者:DingDangDog

    来源:https://oldmoon.top/post/12

    
    

  • 阿里终面:每天100w次登陆请求, 8G 内存该如何设置JVM参数?

    快乐分享,Java干货及时送达👇

    就在上周,一个同学在阿里云技术面终面的时候被问到这么一个问题:假设一个每天100w次登陆请求的平台,一个服务节点 8G 内存,该如何设置JVM参数? 。
    下面以面试题的形式给大家梳理出来,做到一箭双雕: 
    • 既供大家实操参考
    • 又供大家面试参考
    大家要学习的,除了 JVM 配置方案 之外,是其 分析问题的思路、思考问题的视角。这些思路和视角,能帮助大家走更远、更远。
    接下来,进入正题。

    每天100w次登陆请求, 8G 内存该如何设置JVM参数?

    每天100w次登陆请求, 8G 内存该如何设置JVM参数,大概可以分为以下8个步骤

    Step1:新系统上线如何规划容量?

    1.套路总结

    任何新的业务系统在上线以前都需要去估算服务器配置和JVM的内存参数,这个容量与资源规划并不仅仅是系统架构师的随意估算的,需要根据系统所在业务场景去估算,推断出来一个系统运行模型,评估JVM性能和GC频率等等指标。以下是我结合大牛经验以及自身实践来总结出来的一个建模步骤:
    • 计算业务系统每秒钟创建的对象会佔用多大的内存空间,然后计算集群下的每个系统每秒的内存佔用空间(对象创建速度)
    • 设置一个机器配置,估算新生代的空间,比较不同新生代大小之下,多久触发一次MinorGC。
    • 为了避免频繁GC,就可以重新估算需要多少机器配置,部署多少台机器,给JVM多大内存空间,新生代多大空间。
    • 根据这套配置,基本可以推算出整个系统的运行模型,每秒创建多少对象,1s以后成为垃圾,系统运行多久新生代会触发一次GC,频率多高。
    2.套路实战——以登录系统为例
    有些同学看到这些步骤还是发憷,说的好像是那么回事,一到实际项目中到底怎麽做我还是不知道!
    光说不练假把式,以登录系统为例模拟一下推演过程:
    • 假设每天100w次登陆请求,登陆峰值在早上,预估峰值时期每秒100次登陆请求。
    • 假设部署3台服务器,每台机器每秒处理30次登陆请求,假设一个登陆请求需要处理1秒钟,JVM新生代里每秒就要生成30个登陆对象,1s之后请求完毕这些对象成为了垃圾。
    • 一个登陆请求对象假设20个字段,一个对象估算500字节,30个登陆佔用大约15kb,考虑到RPC和DB操作,网络通信、写库、写缓存一顿操作下来,可以扩大到20-50倍,大约1s产生几百k-1M数据。
    • 假设2C4G机器部署,分配2G堆内存,新生代则只有几百M,按照1s1M的垃圾产生速度,几百秒就会触发一次MinorGC了。
    • 假设4C8G机器部署,分配4G堆内存,新生代分配2G,如此需要几个小时才会触发一次MinorGC。
    所以,可以粗略的推断出来一个每天100w次请求的登录系统,按照4C8G的3实例集群配置,分配4G堆内存、2G新生代的JVM,可以保障系统的一个正常负载。
    基本上把一个新系统的资源评估了出来,所以搭建新系统要每个实例需要多少容量多少配置,集群配置多少个实例等等这些,并不是拍拍脑袋和胸脯就可以决定的下来的。

    Step2:该如何进行垃圾回收器的选择?

    吞吐量还是响应时间

    首先引入两个概念:吞吐量和低延迟
    吞吐量 = CPU在用户应用程序运行的时间 / (CPU在用户应用程序运行的时间 + CPU垃圾回收的时间)
    响应时间 = 平均每次的GC的耗时
    通常,吞吐优先还是响应优先这个在JVM中是一个两难之选。
    堆内存增大,gc一次能处理的数量变大,吞吐量大;但是gc一次的时间会变长,导致后面排队的线程等待时间变长;相反,如果堆内存小,gc一次时间短,排队等待的线程等待时间变短,延迟减少,但一次请求的数量变小(并不绝对符合)。
    无法同时兼顾,是吞吐优先还是响应优先,这是一个需要权衡的问题。

    垃圾回收器设计上的考量

    • JVM在GC时不允许一边垃圾回收,一边还创建新对象(就像不能一边打扫卫生,还在一边扔垃圾)。
    • JVM需要一段Stop the world的暂停时间,而STW会造成系统短暂停顿不能处理任何请求;
    • 新生代收集频率高,性能优先,常用复制算法;老年代频次低,空间敏感,避免复制方式。
    • 所有垃圾回收器的涉及目标都是要让GC频率更少,时间更短,减少GC对系统影响!

    CMS和G1

    目前主流的垃圾回收器配置是新生代采用ParNew,老年代采用CMS组合的方式,或者是完全采用G1回收器,
    从未来的趋势来看,G1是官方维护和更为推崇的垃圾回收器。

    业务系统:

    • 延迟敏感的推荐CMS;
    • 大内存服务,要求高吞吐的,采用G1回收器!

    CMS垃圾回收器的工作机制

    CMS主要是针对老年代的回收器,老年代是标记-清除,默认会在一次FullGC算法后做整理算法,清理内存碎片。

    CMS GC 描述 Stop the world 速度
    1.开始标记 初始标记仅标记GCRoots能直接关联到的对象,速度很快 Yes 很快
    2.并发标记 并发标记阶段就是进行GCRoots Tracing的过程 No
    3.重新标记 重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。 Yes 很快
    4.垃圾回收 并发清理垃圾对象(标记清除算法) No
    • 优点:并发收集、主打“低延时” 。在最耗时的两个阶段都没有发生STW,而需要STW的阶段都以很快速度完成。
    • 缺点:1、消耗CPU;2、浮动垃圾;3、内存碎片
    • 适用场景:重视服务器响应速度,要求系统停顿时间最短。

    总之:

    业务系统,延迟敏感的推荐CMS;

    大内存服务,要求高吞吐的,采用G1回收器!

    Step3:如何对各个分区的比例、大小进行规划

    一般的思路为:
    首先,JVM最重要最核心的参数是去评估内存和分配,第一步需要指定堆内存的大小,这个是系统上线必须要做的,-Xms 初始堆大小,-Xmx 最大堆大小,后台Java服务中一般都指定为系统内存的一半,过大会佔用服务器的系统资源,过小则无法发挥JVM的最佳性能。
    其次,需要指定-Xmn新生代的大小,这个参数非常关键,灵活度很大,虽然sun官方推荐为3/8大小,但是要根据业务场景来定,针对于无状态或者轻状态服务(现在最常见的业务系统如Web应用)来说,一般新生代甚至可以给到堆内存的3/4大小;而对于有状态服务(常见如IM服务、网关接入层等系统)新生代可以按照默认比例1/3来设置。服务有状态,则意味著会有更多的本地缓存和会话状态信息常驻内存,应为要给老年代设置更大的空间来存放这些对象。
    最后,是设置-Xss栈内存大小,设置单个线程栈大小,默认值和JDK版本、系统有关,一般默认512~1024kb。一个后台服务如果常驻线程有几百个,那麽栈内存这边也会佔用了几百M的大小。
    JVM参数 描述 默认 推荐
    -Xms Java堆内存的大小 OS内存64/1 OS内存一半
    -Xmx Java堆内存的最大大小 OS内存4/1 OS内存一半
    -Xmn Java堆内存中的新生代大小,扣除新生代剩下的就是老年代的内存大小了 跌认堆的1/3 sun推荐3/8
    -Xss 每个线程的栈内存大小 和idk有关 sun
    对于8G内存,一般分配一半的最大内存就可以了,因为机器本上还要占用一定内存,一般是分配4G内存给JVM,
    引入性能压测环节,测试同学对登录接口压至1s内60M的对象生成速度,采用ParNew+CMS的组合回收器,
    正常的JVM参数配置如下:
    -Xms3072M -Xmx3072M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8 
    这样设置可能会由于动态对象年龄判断原则导致频繁full gc。为啥呢?
    压测过程中,短时间(比如20S后)Eden区就满了,此时再运行的时候对象已经无法分配,会触发MinorGC,
    假设在这次GC后S1装入100M,马上过20S又会触发一次MinorGC,多出来的100M存活对象+S1区的100M已经无法顺利放入到S2区,此时就会触发JVM的动态年龄机制,将一批100M左右的对象推到老年代保存,持续运行一段时间,系统可能一个小时候内就会触发一次FullGC。
    按照默认8:1:1的比例来分配时,  survivor区只有 1G的 10%左右,也就是几十到100M,
    如果  每次minor GC垃圾回收过后进入survivor对象很多,并且survivor对象大小很快超过 Survivor 的 50% ,  那么会触发动态年龄判定规则,让部分对象进入老年代.
    而一个GC过程中,可能部分WEB请求未处理完毕,  几十兆对象,进入survivor的概率,是非常大的,甚至是一定会发生的.

    如何解决这个问题呢?为了让对象尽可能的在新生代的eden区和survivor区, 尽可能的让survivor区内存多一点,达到200兆左右,

    于是我们可以更新下JVM参数设置:

    -Xms3072M -Xmx3072M -Xmn2048M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M  -XX:SurvivorRatio=8  

    说明:
    ‐Xmn2048M ‐XX:SurvivorRatio=8 
    年轻代大小2g,eden与survivor的比例为8:1:1,也就是1.6g:0.2g:0.2g

    survivor达到200m,如果几十兆对象到底survivor, survivor 也不一定超过 50%

    这样可以防止每次垃圾回收过后,survivor对象太早超过 50% ,

    这样就降低了因为对象动态年龄判断原则导致的对象频繁进入老年代的问题,

    什么是JVM动态年龄判断规则呢?

    对象进入老年代的动态年龄判断规则(动态晋升年龄计算阈值):Minor GC 时,Survivor 中年龄 1 到 N 的对象大小超过 Survivor 的 50% 时,则将大于等于年龄 N 的对象放入老年代。

    核心的优化策略是:是让短期存活的对象尽量都留在survivor里,不要进入老年代,这样在minor gc的时候这些对象都会被回收,不会进到老年代从而导致full gc

    应该如何去评估新生代内存和分配合适?

    这里特别说一下,JVM最重要最核心的参数是去评估内存和分配,

    第一步需要指定堆内存的大小,这个是系统上线必须要做的,-Xms 初始堆大小,-Xmx 最大堆大小,

    后台Java服务中一般都指定为系统内存的一半,过大会佔用服务器的系统资源,过小则无法发挥JVM的最佳性能。

    其次需要指定-Xmn新生代的大小,这个参数非常关键,灵活度很大,虽然sun官方推荐为3/8大小,但是要根据业务场景来定:

    • 针对于无状态或者轻状态服务(现在最常见的业务系统如Web应用)来说,一般新生代甚至可以给到堆内存的3/4大小;
    • 而对于有状态服务(常见如IM服务、网关接入层等系统)新生代可以按照默认比例1/3来设置。

    服务有状态,则意味著会有更多的本地缓存和会话状态信息常驻内存,应为要给老年代设置更大的空间来存放这些对象。

    step4:栈内存大小多少比较合适?

    -Xss栈内存大小,设置单个线程栈大小,默认值和JDK版本、系统有关,一般默认512~1024kb。一个后台服务如果常驻线程有几百个,那麽栈内存这边也会佔用了几百M的大小。

    step5:对象年龄应该为多少才移动到老年代比较合适?

    假设一次minor gc要间隔二三十秒,并且,大多数对象一般在几秒内就会变为垃圾,

    如果对象这么长时间都没被回收,比如2分钟没有回收,可以认为这些对象是会存活的比较长的对象,从而移动到老年代,而不是继续一直占用survivor区空间。

    所以,可以将默认的15岁改小一点,比如改为5,

    那么意味着对象要经过5次minor gc才会进入老年代,整个时间也有一两分钟了(5*30s= 150s),和几秒的时间相比,对象已经存活了足够长时间了。

    所以:可以适当调整JVM参数如下:

    ‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8 ‐XX:MaxTenuringThreshold=5 

    step6:多大的对象,可以直接到老年代比较合适?

    对于多大的对象直接进入老年代(参数-XX:PretenureSizeThreshold),一般可以结合自己系统看下有没有什么大对象 生成,预估下大对象的大小,一般来说设置为1M就差不多了,很少有超过1M的大对象,

    所以:可以适当调整JVM参数如下:

    ‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8 ‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M

    step7:垃圾回收器CMS老年代的参数优化

    JDK8默认的垃圾回收器是-XX:+UseParallelGC(年轻代)和-XX:+UseParallelOldGC(老年代),

    如果内存较大(超过4个G,只是经验 值),还是建议使用G1.

    这里是4G以内,又是主打“低延时” 的业务系统,可以使用下面的组合:

    ParNew+CMS(-XX:+UseParNewGC -XX:+UseConcMarkSweepGC)

    新生代的采用ParNew回收器,工作流程就是经典复制算法,在三块区中进行流转回收,只不过采用多线程并行的方式加快了MinorGC速度。

    老生代的采用CMS。再去优化老年代参数:比如老年代默认在标记清除以后会做整理,还可以在CMS的增加GC频次还是增加GC时长上做些取舍,

    如下是响应优先的参数调优:

    XX:CMSInitiatingOccupancyFraction=70

    设定CMS在对内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC)

    XX:+UseCMSInitiatinpOccupancyOnly

    和上面搭配使用,否则只生效一次

    -XX:+AlwaysPreTouch

    强制操作系统把内存真正分配给IVM,而不是用时才分配。

    综上,只要年轻代参数设置合理,老年代CMS的参数设置基本都可以用默认值,如下所示:

    ‐Xms3072M ‐Xmx3072M ‐Xmn2048M ‐Xss1M ‐XX:MetaspaceSize=256M ‐XX:MaxMetaspaceSize=256M ‐XX:SurvivorRatio=8  ‐XX:MaxTenuringThreshold=5 ‐XX:PretenureSizeThreshold=1M ‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC ‐XX:CMSInitiatingOccupancyFraction=70 ‐XX:+UseCMSInitiatingOccupancyOnly ‐XX:+AlwaysPreTouch

    参数解释

    1.‐Xms3072M ‐Xmx3072M 最小最大堆设置为3g,最大最小设置为一致防止内存抖动

    2.‐Xss1M 线程栈1m

    3.‐Xmn2048M ‐XX:SurvivorRatio=8 年轻代大小2g,eden与survivor的比例为8:1:1,也就是1.6g:0.2g:0.2g

    4.-XX:MaxTenuringThreshold=5 年龄为5进入老年代 5.‐XX:PretenureSizeThreshold=1M 大于1m的大对象直接在老年代生成

    6.‐XX:+UseParNewGC ‐XX:+UseConcMarkSweepGC 使用ParNew+cms垃圾回收器组合

    7.‐XX:CMSInitiatingOccupancyFraction=70 老年代中对象达到这个比例后触发fullgc

    8.‐XX:+UseCMSInitiatinpOccupancyOnly  老年代中对象达到这个比例后触发fullgc,每次

    9.‐XX:+AlwaysPreTouch 强制操作系统把内存真正分配给IVM,而不是用时才分配。

    step8:配置OOM时候的内存dump文件和GC日志

    额外增加了GC日志打印、OOM自动dump等配置内容,帮助进行问题排查

    -XX:+HeapDumpOnOutOfMemoryError

    在Out Of Memory,JVM快死掉的时候,输出Heap Dump到指定文件。

    不然开发很多时候还真不知道怎么重现错误。

    路径只指向目录,JVM会保持文件名的唯一性,叫java_pid${pid}.hprof。

    -XX:+HeapDumpOnOutOfMemoryError 
    -XX:HeapDumpPath=${LOGDIR}/

    因为如果指向特定的文件,而文件已存在,反而不能写入。

    输出4G的HeapDump,会导致IO性能问题,在普通硬盘上,会造成20秒以上的硬盘IO跑满,

    需要注意一下,但在容器环境下,这个也会影响同一宿主机上的其他容器。

    GC的日志的输出也很重要:

    -Xloggc:/dev/xxx/gc.log 
    -XX:+PrintGCDateStamps 
    -XX:+PrintGCDetails

    GC的日志实际上对系统性能影响不大,打日志对排查GC问题很重要。

    一份通用的JVM参数模板

    一般来说,大企业或者架构师团队,都会为项目的业务系统定制一份较为通用的JVM参数模板,但是许多小企业和团队可能就疏于这一块的设计,如果老板某一天突然让你负责定制一个新系统的JVM参数,你上网去搜大量的JVM调优文章或博客,结果发现都是零零散散的、不成体系的JVM参数讲解,根本下不了手,这个时候你就需要一份较为通用的JVM参数模板了,不能保证性能最佳,但是至少能让JVM这一层是稳定可控的,

    在这里给大家总结了一份模板:

    基于4C8G系统的ParNew+CMS回收器模板(响应优先),新生代大小根据业务灵活调整!

    -Xms4g
    -Xmx4g
    -Xmn2g
    -Xss1m
    -XX:SurvivorRatio=8
    -XX:MaxTenuringThreshold=10
    -XX:+UseConcMarkSweepGC
    -XX:CMSInitiatingOccupancyFraction=70
    -XX:+UseCMSInitiatingOccupancyOnly
    -XX:+AlwaysPreTouch
    -XX:+HeapDumpOnOutOfMemoryError
    -verbose:gc
    -XX:+PrintGCDetails
    -XX:+PrintGCDateStamps
    -XX:+PrintGCTimeStamps
    -Xloggc:gc.log

    如果是GC的吞吐优先,推荐使用G1,基于8C16G系统的G1回收器模板:

    G1收集器自身已经有一套预测和调整机制了,因此我们首先的选择是相信它,

    即调整-XX:MaxGCPauseMillis=N参数,这也符合G1的目的——让GC调优尽量简单!

    同时也不要自己显式设置新生代的大小(用-Xmn或-XX:NewRatio参数),

    如果人为干预新生代的大小,会导致目标时间这个参数失效。

    -Xms8g
    -Xmx8g
    -Xss1m
    -XX:+UseG1GC
    -XX:MaxGCPauseMillis=150
    -XX:InitiatingHeapOccupancyPercent=40
    -XX:+HeapDumpOnOutOfMemoryError
    -verbose:gc
    -XX:+PrintGCDetails
    -XX:+PrintGCDateStamps
    -XX:+PrintGCTimeStamps
    -Xloggc:gc.log
    G1参数 描述 默认值
    XX:MaxGCPauseMillis=N 最大GC停顿时间。柔性目标,JVM满足90%,不保证100%。 200
    -XX:nitiatingHeapOccupancyPercent=n 当整个堆的空间使用百分比超过这个值时,就会融发MixGC 45

    针对-XX:MaxGCPauseMillis来说,参数的设置带有明显的倾向性:调低↓:延迟更低,但MinorGC频繁,MixGC回收老年代区减少,增大Full GC的风险。调高↑:单次回收更多的对象,但系统整体响应时间也会被拉长。

    针对InitiatingHeapOccupancyPercent来说,调参大小的效果也不一样:调低↓:更早触发MixGC,浪费cpu。调高↑:堆积过多代回收region,增大FullGC的风险。

    调优总结

    系统在上线前的综合调优思路:

    1、业务预估:根据预期的并发量、平均每个任务的内存需求大小,然后评估需要几台机器来承载,每台机器需要什么样的配置。

    2、容量预估:根据系统的任务处理速度,然后合理分配Eden、Surivior区大小,老年代的内存大小。

    3、回收器选型:响应优先的系统,建议采用ParNew+CMS回收器;吞吐优先、多核大内存(heap size≥8G)服务,建议采用G1回收器。

    4、优化思路:让短命对象在MinorGC阶段就被回收(同时回收后的存活对象

    5、到目前为止,总结到的调优的过程主要基于上线前的测试验证阶段,所以我们尽量在上线之前,就将机器的JVM参数设置到最优!

    JVM调优只是一个手段,但并不一定所有问题都可以通过JVM进行调优解决,大多数的Java应用不需要进行JVM优化,我们可以遵循以下的一些原则:

    • 上线之前,应先考虑将机器的JVM参数设置到最优;
    • 减少创建对象的数量(代码层面);
    • 减少使用全局变量和大对象(代码层面);
    • 优先架构调优和代码调优,JVM优化是不得已的手段(代码、架构层面);
    • 分析GC情况优化代码比优化JVM参数更好(代码层面);

    通过以上原则,我们发现,其实最有效的优化手段是架构和代码层面的优化,而JVM优化则是最后不得已的手段,也可以说是对服务器配置的最后一次“压榨”。

    什么是ZGC?

    ZGC (Z Garbage Collector)是一款由Oracle公司研发的,以低延迟为首要目标的一款垃圾收集器。

    它是基于动态Region内存布局,(暂时)不设年龄分代,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的收集器。

    在 JDK 11 新加入,还在实验阶段,

    主要特点是:回收TB级内存(最大4T),停顿时间不超过10ms。

    优点:低停顿,高吞吐量, ZGC 收集过程中额外耗费的内存小

    缺点:浮动垃圾

    目前使用的非常少,真正普及还是需要写时间的。

    如何选择垃圾收集器?

    在真实场景中应该如何去选择呢,下面给出几种建议,希望对你有帮助:

    1、如果你的堆大小不是很大(比如 100MB ),选择串行收集器一般是效率最高的。参数:-XX:+UseSerialGC 。

    2、如果你的应用运行在单核的机器上,或者你的虚拟机核数只有 单核,选择串行收集器依然是合适的,这时候启用一些并行收集器没有任何收益。参数:-XX:+UseSerialGC 。

    3、如果你的应用是“吞吐量”优先的,并且对较长时间的停顿没有什么特别的要求。选择并行收集器是比较好的。参数:-XX:+UseParallelGC 。

    4、如果你的应用对响应时间要求较高,想要较少的停顿。甚至 1 秒的停顿都会引起大量的请求失败,那么选择 G1 、 ZGC 、 CMS 都是合理的。虽然这些收集器的 GC 停顿通常都比较短,但它需要一些额外的资源去处理这些工作,通常吞吐量会低一些。参数:-XX:+UseConcMarkSweepGC 、 -XX:+UseG1GC 、 -XX:+UseZGC 等。从上面这些出发点来看,我们平常的 Web 服务器,都是对响应性要求非常高的。

    选择性其实就集中在 CMS、G1、ZGC 上。而对于某些定时任务,使用并行收集器,是一个比较好的选择。

    Hotspot为什么使用元空间替换了永久代?

    什么是元空间?什么是永久代?为什么用元空间代替永久代?

    我们先回顾一下方法区吧,看看虚拟机运行时数据内存图,如下:

    方法区和堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。

    什么是永久代?它和方法区有什么关系呢?

    如果在HotSpot虚拟机上开发、部署,很多程序员都把方法区称作永久代。

    可以说方法区是规范,永久代是Hotspot针对该规范进行的实现。

    在Java7及以前的版本,方法区都是永久代实现的。

    什么是元空间?它和方法区有什么关系呢?

    对于Java8,HotSpots取消了永久代,取而代之的是元空间(Metaspace)。

    换句话说,就是方法区还是在的,只是实现变了,从永久代变为元空间了。

    为什么使用元空间替换了永久代?

    永久代的方法区,和堆使用的物理内存是连续的。

    永久代是通过以下这两个参数配置大小的~

    • -XX:PremSize:设置永久代的初始大小
    • -XX:MaxPermSize: 设置永久代的最大值,默认是64M

    对于永久代,如果动态生成很多class的话,就很可能出现java.lang.OutOfMemoryError:PermGen space错误,因为永久代空间配置有限嘛。最典型的场景是,在web开发比较多jsp页面的时候。

    JDK8之后,方法区存在于元空间(Metaspace)。

    物理内存不再与堆连续,而是直接存在于本地内存中,理论上机器内存有多大,元空间就有多大

    可以通过以下的参数来设置元空间的大小:

    • -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么在不超过MaxMetaspaceSize时,适当提高该值。
    • -XX:MaxMetaspaceSize,最大空间,默认是没有限制的。
    • -XX:MinMetaspaceFreeRatio,在GC之后,最小的Metaspace剩余空间容量的百分比,减少为分配空间所导致的垃圾收集
    • -XX:MaxMetaspaceFreeRatio,在GC之后,最大的Metaspace剩余空间容量的百分比,减少为释放空间所导致的垃圾收集

    所以,为什么使用元空间替换永久代?

    表面上看是为了避免OOM异常。

    因为通常使用PermSize和MaxPermSize设置永久代的大小就决定了永久代的上限,但是不是总能知道应该设置为多大合适, 如果使用默认值很容易遇到OOM错误。

    当使用元空间时,可以加载多少类的元数据就不再由MaxPermSize控制, 而由系统的实际可用空间来控制啦。

    什么是Stop The World ? 什么是OopMap?什么是安全点?

    进行垃圾回收的过程中,会涉及对象的移动。

    为了保证对象引用更新的正确性,必须暂停所有的用户线程,像这样的停顿,虚拟机设计者形象描述为Stop The World。也简称为STW。

    在HotSpot中,有个数据结构(映射表)称为OopMap

    一旦类加载动作完成的时候,HotSpot就会把对象内什么偏移量上是什么类型的数据计算出来,记录到OopMap。

    在即时编译过程中,也会在特定的位置生成 OopMap,记录下栈上和寄存器里哪些位置是引用。

    这些特定的位置主要在:1.循环的末尾(非 counted 循环)

    2.方法临返回前 / 调用方法的call指令后

    3.可能抛异常的位置

    这些位置就叫作安全点(safepoint)。

    用户程序执行时并非在代码指令流的任意位置都能够在停顿下来开始垃圾收集,而是必须是执行到安全点才能够暂停。

    
    

  • 腾讯:@Bean 与 @Component 用在同一个类上,会怎么样?

    快乐分享,Java干货及时送达👇

    来源:cnblogs.com/youzhibing/p/15354706.html

    • 疑虑背景
      • 疑虑描述
      • Spring Boot 版本
    • 结果验证
    • 源码解析
    • Spring 升级优化
    • 总结
      • 补充

    疑虑背景

    疑虑描述

    最近,在进行开发的过程中,发现之前的一个写法,类似如下

    图片

    以我的理解,@Configuration@Bean 会创建一个 userName 不为 null 的 UserManager 对象,而 @Component 也会创建一个 userName 为 null 的 UserManager 对象

    那么我们在其他对象中注入 UserManager 对象时,到底注入的是哪个对象?

    因为项目已经上线了很长一段时间了,所以这种写法没有编译报错,运行也没有出问题

    后面去找同事了解下,实际是想让

    图片

    生效,而实际也确实是它生效了

    那么问题来了:Spring 容器中到底有几个 UserManager 类型的对象?

    Spring Boot 版本

    项目中用的 Spring Boot 版本是:2.0.3.RELEASE

    对象的 scope 是默认值,也就是 singleton

    结果验证

    验证方式有很多,可以 debug 跟源码,看看 Spring 容器中到底有几个 UserManager 对象,也可以直接从 UserManager 构造方法下手,看看哪几个构造方法被调用,等等

    我们从构造方法下手,看看 UserManager 到底实例化了几次

    图片

    只有有参构造方法被调用了,无参构造方法岿然不动(根本没被调用)

    既然 UserManager 构造方法只被调用了一次,那么前面的问题:到底注入的是哪个对象

    答案也就清晰了,没得选了呀,只能是 @Configuration@Bean 创建的 userName 不为 null 的 UserManager 对象

    问题又来了:为什么不是 @Component 创建的 userName 为 null 的 UserManager 对象?

    源码解析

    @Configuration@Component 关系很紧密

    图片

    所以@Configuration 能够被 component scan

    其中 ConfigurationClassPostProcessor@Configuration 息息相关,其类继承结构图如下:

    图片

    它实现了 BeanFactoryPostProcessor 接口和 PriorityOrdered 接口,关于 BeanFactoryPostProcessor

    那么我们从 AbstractApplicationContext 的 refresh 方法调用的 invokeBeanFactoryPostProcessors(beanFactory)开始,来跟下源码

    图片

    此时完成了 com.lee.qsl 包下的 component scancom.lee.qsl 包及子包下的 UserConfig 、 UserController 和 UserManager 都被扫描出来

    注意,此刻@Bean 的处理还未开始, UserManager 是通过@Component 而被扫描出来的;此时 Spring 容器中 beanDefinitionMap 中的 UserManager 是这样的

    图片

    接下来一步很重要,与我们想要的答案息息相关

    图片

    图片

    循环递归处理 UserConfig 、 UserController 和 UserManager ,把它们都封装成 ConfigurationClass ,递归扫描 BeanDefinition

    循环完之后,我们来看看 configClasses

    图片

    UserConfig bean 定义信息中 beanMethods 中有一个元素 [BeanMethod:name=userManager,declaringClass=com.lee.qsl.config.UserConfig]

    然后我们接着往下走,来仔细看看答案出现的环节

    是不是有什么发现?@Component 修饰的 UserManager 定义直接被覆盖成了 @Configuration + @Bean 修饰的 UserManager 定义

    Bean 定义类型也由 ScannedGenericBeanDefinition 替换成了 ConfigurationClassBeanDefinition

    后续通过 BeanDefinition 创建实例的时候,创建的自然就是 @Configuration + @Bean 修饰的 UserManager ,也就是会反射调用 UserManager 的有参构造方法

    自此,答案也就清楚了

    Spring 其实给出了提示

    2021-10-03 20:37:33.697  INFO 13600 --- [           
    main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'userManager' with a different definition: replacing [Generic bean: class [com.lee.qsl.manager.UserManager]; scope=singleton; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in file [D:qsl-projectspring-boot-bean-componenttargetclassescomleeqslmanagerUserManager.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=userConfig; factoryMethodName=userManager; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [com/lee/qsl/config/UserConfig.class]]

    只是日志级别是 info ,太不显眼了

    Spring 升级优化

    可能 Spring 团队意识到了 info 级别太不显眼的问题,或者说意识到了直接覆盖的处理方式不太合理

    所以在 Spring 5.1.2.RELEASE (Spring Boot 则是 2.1.0.RELEASE )做出了优化处理

    我们来具体看看

    图片

    启动直接报错,Spring 也给出了提示

    The bean 'userManager', defined in class path resource [com/lee/qsl/config/UserConfig.class], could not be registered. A bean with that name has already been defined in file [D:qsl-projectspring-boot-bean-componenttargetclassescomleeqslmanagerUserManager.class] and overriding is disabled.

    我们来跟下源码,主要看看与 Spring 5.0.7.RELEASE 的区别

    新增了配置项 allowBeanDefinitionOverriding 来控制是否允许 BeanDefinition 覆盖,默认情况下是不允许的

    我们可以在配置文件中配置:spring.main.allow-bean-definition-overriding=true ,允许 BeanDefinition 覆盖

    这种处理方式是更优的,将选择权交给开发人员,而不是自己偷偷的处理,已达到开发者想要的效果

    总结

    Spring 5.0.7.RELEASESpring Boot 2.0.3.RELEASE ) 支持@Configuration + @Bean@Component 同时作用于同一个类

    启动时会给 info 级别的日志提示,同时会将@Configuration + @Bean 修饰的 BeanDefinition 覆盖掉@Component 修饰的 BeanDefinition

    也许 Spring 团队意识到了上述处理不太合适,于是在 Spring 5.1.2.RELEASE 做出了优化处理

    增加了配置项:allowBeanDefinitionOverriding ,将主动权交给了开发者,由开发者自己决定是否允许覆盖

    补充

    关于 allowBeanDefinitionOverriding ,前面讲的不对,后面特意去翻了下源码,补充如下

    Spring 1.2 引进 DefaultListableBeanFactory 的时候就有了 private boolean allowBeanDefinitionOverriding = true;,默认是允许 BeanDefinition 覆盖

    图片

    Spring 4.1.2 引进了 isAllowBeanDefinitionOverriding()方法

    图片

    Spring 自始至终默认都是允许 BeanDefinition 覆盖的,变的是 Spring Boot , Spring Boot 2.1.0 之前没有覆盖 Spring 的 allowBeanDefinitionOverriding 默认值,仍是允许 BeanDefinition 覆盖的

    Spring Boot 2.1.0 中 SpringApplication 定义了私有属性:allowBeanDefinitionOverriding

    图片

    没有显示的指定值,那么默认值就是 false ,之后在 Spring Boot 启动过程中,会用此值覆盖掉 Spring 中的 allowBeanDefinitionOverriding 的默认值

    关于 allowBeanDefinitionOverriding ,我想大家应该已经清楚了

    
    

  • MySQL 批量操作,一次插入多少行数据效率最高?

    MySQL 批量操作,一次插入多少行数据效率最高?来源:blog.csdn.net/LJFPHP/article/details/99708888

    • 一、前言
    • 二、批量插入前准备
      • 1、插入到数据表的字段
      • 2、计算一行字段占用的空间
      • 3、在数据里做插入操作的时候,整体时间的分配
    • 三、批量插入数据测试
      • 1、SQL语句的大小限制
      • 2、查看服务器上的参数:
      • 3、计算一次能插入的最大行记录
      • 4、测试插入数据比对
      • 5、如果插入的值就是sql语句限制的最大值,那么性能真的好吗?
    • 四、其他影响插入性能的因素
      • 1、首先是插入的时候,要注意缓冲区的大小使用情况
      • 2、插入缓存
      • 3、使用事务提升效率
      • 4、通过配置提升读写性能
      • 5、索引影响插入性能
    • 五、总结

    一、前言

    我们在操作大型数据表或者日志文件的时候经常会需要写入数据到数据库,那么最合适的方案就是数据库的批量插入。只是我们在执行批量操作的时候,一次插入多少数据才合适呢?

    假如需要插入的数据有百万条,那么一次批量插入多少条的时候,效率会高一些呢?这里博主和大家一起探讨下这个问题,应用环境为批量插入数据到临时表。

    二、批量插入前准备

    博主本地原本是循环查出来的数据,然后每1000条插入一次,直至完成插入操作。但是为什么要设置1000条呢,实不相瞒,这是因为项目里的其他批量插入都是一次插1000条。。汗,博主不服,所以想要测试下。

    首先是查看当前数据库的版本,毕竟各个版本之间存在差异,脱离版本讲数据库就是耍流氓(以前没少耍啊):

    mysql> select version();
    +------------+
    | version()  |
    +------------+
    | 5.6.34-log |
    +------------+
    1 row in set (0.00 sec)
    

    1、插入到数据表的字段

    对于手动创建的临时表来说,字段当然是越少越好,而且字段占用的空间要尽量小一些,这样临时表不至于太大,影响表操作的性能。这里需要插入的字段是:

    字段1 int(10)
    字段2 int(10)
    字段3 int(10)
    字段4 varchar(10)
    

    我们一共插入四个字段,分别是3个int类型的,一个varchar类型的,整体来说这些字段都比较小,占用的内存空间会小一些。

    2、计算一行字段占用的空间

    对于innodb引擎来说,int类型可以存储4个字节,里面的Int(M)并不会影响存储字节的大小,这个M只是数据的展示位数,和mysql的ZEROFILL属性有关,即在数字长度不够的数据前面填充0,以达到设定的长度。此处不多说,想要了解的朋友可以百度一下,还是很有意思的。

    varchar(10)代表可以存储10个字符,不管是英文还是中文,最多都是10个,这部分假设存储的是中文,在utf-8mb4下,10个中文占用10*4 = 40个字节那么一行数据最多占用:4+4+4+40 = 52字节

    3、在数据里做插入操作的时候,整体时间的分配

    链接耗时 (30%)
    发送query到服务器 (20%)
    解析query (20%)
    插入操作 (10% * 词条数目)
    插入index (10% * Index的数目)
    关闭链接 (10%)
    

    从这里可以看出来,真正耗时的不是操作,而是链接,解析的过程。单条sql的话,会在链接,解析部分耗费大量的时间,因此速度会很慢,所以我们一般都是采用批量插入的操作,争取在一次链接里面写入尽可能多的数据,以此来提升插入的速度。但是这个尽可能多的数据是多少呢?一次到底插入多少才合适呢?

    三、批量插入数据测试

    开始测试,但是一开始插入多少是合适的呢,是否有上限?查询mysql手册,我们知道sql语句是有大小限制的。

    1、SQL语句的大小限制

    my.ini 里有 max_allowed_packet 这个参数控制通信的 packet 大小。mysql默认的sql语句的最大限制是1M(mysql5.7的客户端默认是16M,服务端默认是4M),可以根据设置查看。官方解释是适当增大 max_allowed_packet 参数可以使client端到server端传递大数据时,系统能够分配更多的扩展内存来处理。

    官方手册:https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html

    2、查看服务器上的参数:

    mysql> show variables like '%max_allowed_packet%';
    +--------------------------+------------+
    | Variable_name            | Value      |
    +--------------------------+------------+
    | max_allowed_packet       | 33554432   |
    | slave_max_allowed_packet | 1073741824 |
    +--------------------------+------------+
    2 rows in set (0.00 sec)
    

    33554432字节 = 32M ,也就是规定大小不能超过32M。

    3、计算一次能插入的最大行记录

    1M计算的话,(1024*1024)/52 ≈ 20165 ,为了防止溢出,最大可一次性插入20000条(根据自己的配置和sql语句大小计算)。那么32M的话就是:20000 *32 = 640000 也就是64W条。

    4、测试插入数据比对

    (1)插入11W条数据,按照每次10,600,1000,20000,80000来测试:
    +---------------+
    | count(c1.uin) |
    +---------------+
    |         110000 |
    +---------------+
    

    有个博客说一次插入10条最快,,我觉得一次插的有点少,咱们试试

    参考:https://www.cnblogs.com/aicro/p/3851434.html

    这个博主测试后,认为一次插10条是性能最快的,他的每条记录是3kb,相当于我的59行数据,取个整数60,那么对于这个博主是插入10条,对我来说插入:600,这几个值都试试。

    耗时:

    11W的数据,每次插入10条。耗时:2.361s
    11W的数据,每次插入600条。耗时:0.523s
    11W的数据,每次插入1000条。耗时:0.429s
    11W的数据,每次插入20000条。耗时:0.426s
    11W的数据,每次插入80000条。耗时:0.352s
    

    从这部分看,随着批量插入的增加,速度略有提升,最起码一次插10条应该不是最佳的。插入数据量多,减少了循环的次数,也就是在数据库链接部分的耗时有所减少,只是这个8W并不是极限数据,具体一次插入多少条,还有待参考。

    (2)加大数据量到24w
    +---------------+
    | count(c1.uin) |
    +---------------+
    |        241397 |
    +---------------+
    

    耗时:

    24W的数据,每次插入10条。耗时:4.445s
    24W的数据,每次插入600条。耗时:1.187s
    24W的数据,每次插入1000条。耗时:1.13s
    24W的数据,每次插入20000条。耗时:0.933s
    24W的数据,每次插入80000条。耗时:0.753s
    

    一次插入24W反而性能最佳,这么代表我们的测试数据量依然不够。

    (3)加大测试量到42W
    +---------------+
    | count(c1.uin) |
    +---------------+
    |        418859 |
    

    耗时:

    42W的数据,每次插入1000条。耗时:2.216s
    42W的数据,每次插入80000条。耗时:1.777s
    42W的数据,每次插入16W条。耗时:1.523s
    42W的数据,每次插入20W条。耗时:1.432s
    42W的数据,每次插入30W条。耗时:1.362s
    42W的数据,每次插入40W条。耗时:1.764s
    

    随着插入量的增加,批量插入条数多了之后,性能是有所提升的。但是在达到30W以上之后,效率反而有所下降。这部分我的理解是mysql是要分配一定的内存给传过来的数据包使用,当批量插入的数据量到达一定程度之后,一次插入操作的开销就很耗费内存了。

    个人感觉,最佳大小是max_allowed_packet的一半,也就是极限能插入64W,选用32W也许性能会更好一些,同时也不会对mysql的其他操作产生太大的影响。

    5、如果插入的值就是sql语句限制的最大值,那么性能真的好吗?

    博主疯狂谷歌百度,都没有找到有人来具体的说一下这个问题,不过在高性能mysql里面发现一句话:

    客户端用一个单独的数据包将查询请求发送给服务器,所以当查询语句很长的时候,需要设置max_allowed_packet参数。但是需要注意的是,如果查询实在是太大,服务端会拒绝接收更多数据并抛出异常。与之相反的是,服务器响应给用户的数据通常会很多,由多个数据包组成。但是当服务器响应客户端请求时,客户端必须完整的接收整个返回结果,而不能简单的只取前面几条结果,然后让服务器停止发送。因而在实际开发中,尽量保持查询简单且只返回必需的数据,减小通信间数据包的大小和数量是一个非常好的习惯,这也是查询中尽量避免使用SELECT *以及加上LIMIT限制的原因之一。

    后面通过各种百度,博主觉得最大只是代表传输数据包的最大长度,但性能是不是最佳就要从各个方面来分析了。比如下面列出的插入缓冲,以及插入索引时对于缓冲区的剩余空间需求,以及事务占有的内存等,都会影响批量插入的性能。

    四、其他影响插入性能的因素

    1、首先是插入的时候,要注意缓冲区的大小使用情况

    在分析源码的过程中,有一句话:如果buffer pool余量不足25%,插入失败,返回DB_LOCK_TABLE_FULL。这个错误并不是直接报错:max_allowed_packet 不够大之类的,这个错误是因为对于innodb引擎来说,一次插入是涉及到事务和锁的,在插入索引的时候,要判断缓冲区的剩余情况,所以插入并不能仅仅只考虑max_allowed_packet的问题,也要考虑到缓冲区的大小。

    参考淘宝的数据库日报:http://mysql.taobao.org/monthly/2017/09/10/

    2、插入缓存

    另外对于innodb引擎来说,因为存在插入缓存(Insert Buffer)这个概念,所以在插入的时候也是要耗费一定的缓冲池内存的。当写密集的情况下,插入缓冲会占用过多的缓冲池内存,默认最大可以占用到1/2的缓冲池内存,当插入缓冲占用太多缓冲池内存的情况下,会影响到其他的操作。

    也就是说,插入缓冲受到缓冲池大小的影响,缓冲池大小为:

    mysql> show variables like 'innodb_buffer_pool_size';
    +-------------------------+-----------+
    | Variable_name           | Value     |
    +-------------------------+-----------+
    | innodb_buffer_pool_size | 134217728 |
    +-------------------------+-----------+
    

    换算后的结果为:128M,也就是说,插入缓存最多可以占用64M的缓冲区大小。这个大小要超过咱们设置的sql语句大小,所以可以忽略不计。

    详细解释:

    我们都知道,在InnoDB引擎上进行插入操作时,一般需要按照主键顺序进行插入,这样才能获得较高的插入性能。当一张表中存在非聚簇的且不唯一的索引时,在插入时,数据页的存放还是按照主键进行顺序存放,但是对于非聚簇索引叶节点的插入不再是顺序的了,这时就需要离散的访问非聚簇索引页,由于随机读取的存在导致插入操作性能下降。

    InnoDB为此设计了Insert Buffer来进行插入优化。对于非聚簇索引的插入或者更新操作,不是每一次都直接插入到索引页中,而是先判断插入的非聚集索引是否在缓冲池中,若在,则直接插入;若不在,则先放入到一个Insert Buffer中。

    看似数据库这个非聚集的索引已经查到叶节点,而实际没有,这时存放在另外一个位置。然后再以一定的频率和情况进行Insert Buffer和非聚簇索引页子节点的合并操作。这时通常能够将多个插入合并到一个操作中,这样就大大提高了对于非聚簇索引的插入性能。

    参考:https://cloud.tencent.com/developer/article/1200824

    参考:mysql技术内幕 Innodb篇

    3、使用事务提升效率

    还有一种说法,使用事务可以提高数据的插入效率,这是因为进行一个INSERT操作时,MySQL内部会建立一个事务,在事务内才进行真正插入处理操作。通过使用事务可以减少创建事务的消耗,所有插入都在执行后才进行提交操作。大概如下:

    START TRANSACTION;
    INSERT INTO `insert_table` (`datetime`, `uid`, `content`, `type`) 
        VALUES ('0''userid_0''content_0', 0);
    INSERT INTO `insert_table` (`datetime`, `uid`, `content`, `type`) 
        VALUES ('1''userid_1''content_1', 1);
    ...
    COMMIT;
    

    参考:https://my.oschina.net/songhongxu/blog/163063

    事务需要控制大小,事务太大可能会影响执行的效率。MySQL有innodb_log_buffer_size配置项,超过这个值会把innodb的数据刷到磁盘中,这时,效率会有所下降。所以比较好的做法是,在数据达到这个这个值前进行事务提交。

    查看:show variables like '%innodb_log_buffer_size%';

    +------------------------+----------+
            | Variable_name          | Value    |
            +------------------------+----------+
            | innodb_log_buffer_size | 67108864 |
            +------------------------+----------+
    

    大概是:64M

    这种写法和批量写入的效果差不多,只不过sql语句还是单句的,然后统一提交。一个瓶颈是SQL语句的大小,一个瓶颈是事务的大小。当我们在提交sql的时候,首先是受到sql大小的限制,其次是受到事务大小的限制。在开启事务的情况下使用批量插入,会节省不少事务的开销,如果要追求极致的速度的话,建议是开着事务插入的。

    不过需要注意一下,内存是有限且共享的,如果批量插入占用太多的事务内存,那么势必会对其他的业务操作等有一定的影响。

    4、通过配置提升读写性能

    也可以通过增大innodb_buffer_pool_size 缓冲区来提升读写性能,只是缓冲区是要占用内存空间的,内存很珍贵,所以这个方案在内存富裕,而性能瓶颈的时候,可以考虑下。

    参考:https://my.oschina.net/anuodog/blog/3002941

    5、索引影响插入性能

    如果表中存在多个字段索引,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护。这样就降低了数据的插入速度。对于普通的数据表,主键索引是肯定要有的,想要加快性能的话,就是要有序插入,每次插入记录都在索引的最后面,索引的定位效率很高,并且对索引调整较小。如果插入的记录在索引中间,需要B+tree进行分裂合并等处理,会消耗比较多计算资源,并且插入记录的索引定位效率会下降,数据量较大时会有频繁的磁盘操作。

    五、总结

    博主经过测试+谷歌,最终是选用的一次批量插入数据量为max_allowed_packet大小的一半。只是在不断的搜索中,发现影响插入性能的地方挺多的,如果仅仅是拿max_allowed_packet这个参数作为分析,其实是没有意义的,这个参数只是设置最大值,但并不是最佳性能。

    不过需要注意,由于sql语句比较大,所以才执行完插入操作之后,一定要释放变量,不要造成无谓的内存损耗,影响程序性能。

    对于我们的mysql来说也是一样的,mysql的最佳性能是建立在各个参数的合理设置上,这样协同干活儿的效果最佳。如果其他设置不到位的话,就像是木桶原理一样,哪怕内存缓冲区设置的很大,但是性能取决的反而是设置最差的那个配置。关于mysql的配置调优,我们都在路上,加油!

  • 用上这几个开源管理系统做项目,领导看了直呼专业!

    SCUI Admin 中后台前端解决方案

    SCUI 是一个中后台前端解决方案,基于 VUE3和 elementPlus 实现。使用最新的前端技术栈,提供各类实用的组件方便在业务开发时的调用,并且持续性的提供丰富的业务模板帮助你快速搭建企业级中后台前端任务。

    • 项目地址:https://gitee.com/lolicode/scui
    用上这几个开源管理系统做项目,领导看了直呼专业!
    用上这几个开源管理系统做项目,领导看了直呼专业!

    Vue + .NetCore 前后端分离的快速发开框架

    框架内置了大量的通用组件可直接使用,并内置了基于本框架定制开发的代码生成器,尽量避免重复性代码编写。支持前端、后台自定义业务代码扩展,后台提供了大量常用扩展与通用类;前端、后台提供了近300个扩展方法与属性,开发人员可在此功能上编写扩展自定义业务代码

    • 项目地址:https://gitee.com/x_discoverer/Vue.NetCore
    用上这几个开源管理系统做项目,领导看了直呼专业!
    用上这几个开源管理系统做项目,领导看了直呼专业!

    next-admin 适配移动端、pc的后台模板

    基于 vue3.x + CompositionAPI + typescript + vite + element plus + vue-router-next + next.vuex,适配手机、平板、pc 的后台开源免费模板,希望减少工作量,帮助大家实现快速开发。

    • 项目地址:https://gitee.com/lyt-top/vue-next-admin
    用上这几个开源管理系统做项目,领导看了直呼专业!
    用上这几个开源管理系统做项目,领导看了直呼专业!

    django-vue-admin-pro 快速开发平台

    django-vue-admin-pro 是一套全部开源的快速开发平台,毫无保留给个人及企业免费使用。前端采用 D2Admin 、Vue。后端采用 Python 语言 Django 框架。权限认证使用 Jwt,支持多终端认证系统。支持加载动态权限菜单,多方式轻松实现权限控制。

    • 项目地址:https://gitee.com/dvadmin/django-vue-admin-pro
    用上这几个开源管理系统做项目,领导看了直呼专业!

    Admin.NET 通用管理平台

    基于 Furion/.NET 6实现的通用管理平台。整合最新技术,模块插件式开发,前后端分离,开箱即用。集成EF Core、多租户、缓存、数据校验、鉴权、事件总线、动态 API、通讯、远程请求、任务调度、gRPC 等众多黑科技。代码简洁、易扩展,让开发更简单、更通用、更流行!

    • 项目地址:https://gitee.com/zuohuaijun/Admin.NET
    用上这几个开源管理系统做项目,领导看了直呼专业!
    用上这几个开源管理系统做项目,领导看了直呼专业!

    RuoYi 若依权限管理系统

    基于 SpringBoot 的权限管理系统 易读易懂、界面简洁美观。核心技术采用Spring、MyBatis、Shiro 没有任何其它重度依赖,直接运行即可用。

    • 项目地址:https://gitee.com/y_project/RuoYi
    用上这几个开源管理系统做项目,领导看了直呼专业!
    用上这几个开源管理系统做项目,领导看了直呼专业!

    Vue3.2 + Element-Plus 后台管理框架

    Geeker Admin,基于 Vue3.2、TypeScript、Vite3、Pinia、Element-Plus 开源的一套后台管理框架。

    • 项目地址:https://github.com/HalseySpicy/Geeker-Admin
    用上这几个开源管理系统做项目,领导看了直呼专业!
    用上这几个开源管理系统做项目,领导看了直呼专业!

    Pig RABC权限管理系统

    基于 Spring Boot 2.6、 Spring Cloud 2021 & Alibaba、 OAuth2 的微服务RBAC 权限管理系统。基于数据驱动视图的理念封装 element-ui,即使没有 vue 的使用经验也能快速上手。

    • 项目地址:https://gitee.com/log4j/pig
    用上这几个开源管理系统做项目,领导看了直呼专业!
    用上这几个开源管理系统做项目,领导看了直呼专业!

    zheng 分布式敏捷开发系统架构

    基于 Spring+SpringMVC+Mybatis 分布式敏捷开发系统架构,提供整套公共微服务服务模块:集中权限管理(单点登录)、内容管理、支付中心、用户管理(支持第三方登录)、微信平台、存储系统、配置中心、日志分析、任务和通知等,支持服务治理、监控和追踪,努力为中小型企业打造全方位 J2EE 企业级开发解决方案。

    • 项目地址:https://gitee.com/shuzheng/zheng
    用上这几个开源管理系统做项目,领导看了直呼专业!

    Guns 快速开发平台

    Guns 基 于Spring Boot2,致力于做更简洁的后台管理系统。支持单体和微服务架构。

    • 项目地址:https://gitee.com/stylefeng/guns
    用上这几个开源管理系统做项目,领导看了直呼专业!
    用上这几个开源管理系统做项目,领导看了直呼专业!

    D2Admin 企业中后台产品前端集成方案

    D2Admin 是一个完全 开源免费 的企业中后台产品前端集成方案,使用最新的前端技术栈,小于 60kb 的本地首屏 js 加载,已经做好大部分项目前期准备工作,并且带有大量示例代码,助力管理系统敏捷开发。

    • 项目地址:https://github.com/d2-projects/d2-admin
    用上这几个开源管理系统做项目,领导看了直呼专业!

    JeeSpringCloud 权限管理系统

    基于 SpringBoot2.0的后台权限管理系统界面简洁美观敏捷开发系统架构。核心技术采用 Spring、MyBatis、Shiro 没有任何其它重度依赖。互联网云快速开发框架,微服务分布式代码生成的敏捷开发系统架构。项目代码简洁,注释丰富,上手容易,还同时集中分布式、微服务,同时包含许多基础模块和监控、服务模块。

    • 项目地址:https://gitee.com/JeeHuangBingGui/jeeSpringCloud
    用上这几个开源管理系统做项目,领导看了直呼专业!
    用上这几个开源管理系统做项目,领导看了直呼专业!

    BootstrapAdmin 后台管理框架

    基于 RBAC 的 Net6 后台管理框架,权限管理,前后台分离,支持多站点单点登录,兼容所有主流浏览器,内置微信、支付宝、QQ等多种登录方式,内置多种样式,可切换至 Blazor 多 Tabs 模式,权限控制细化到网页内任意元素(按钮、表格、文本框等等)

    • 项目地址:https://gitee.com/LongbowEnterprise/BootstrapAdmin
    用上这几个开源管理系统做项目,领导看了直呼专业!
    用上这几个开源管理系统做项目,领导看了直呼专业!

    eladmin 后台管理系统

    EL-ADMIN (opens new window)一个基于 Spring Boot 2.1.0 、 Spring Boot Jpa、 JWT、Spring Security、Redis、Vue、Element-UI 的前后端分离的后台管理系统。

    • 项目地址:https://gitee.com/elunez/eladmin
    用上这几个开源管理系统做项目,领导看了直呼专业!
    用上这几个开源管理系统做项目,领导看了直呼专业!

    FastAdmin 基于 ThinkPHP 和 Bootstrap

    FastAdmin 是一款基于 ThinkPHP 和 Bootstrap 的极速后台开发框架,基于 Auth 验证的权限管理系统,一键生成 CRUD,自动生成控制器、模型、视图、JS、语言包、菜单、回收站。

    • 项目地址:https://gitee.com/karson/fastadmin
    用上这几个开源管理系统做项目,领导看了直呼专业!
    用上这几个开源管理系统做项目,领导看了直呼专业!

    Vben-Admin 中大型项目后台解决方案

    Vue-Vben-Admin 是一个基于 Vue3.0、Vite、 Ant-Design-Vue、TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。包括二次封装组件、utils、hooks、动态菜单、权限校验、按钮级别权限控制等功能。项目会使用前端较新的技术栈,可以作为项目的启动模板,以帮助你快速搭建企业级中后台产品原型。

    • 项目地址:https://github.com/vbenjs/vue-vben-admin
    用上这几个开源管理系统做项目,领导看了直呼专业!
    用上这几个开源管理系统做项目,领导看了直呼专业!

    smart-admin 通用型中后台解决方案

    SmartAdmin 由河南·洛阳 1024创新实验室团队研发的一套互联网企业级的通用型中后台解决方案!使用最前沿的前后台技术栈 SpringBoot 和 Vue,前后端分离,我们开源一套漂亮的代码和一套整洁的代码规范,让大家在这浮躁的代码世界里感受到一股把代码写好的清流!同时又让开发者节省大量的时间,减少加班,快乐工作,热爱生活。SmartAdmin 让你从认识到忘不了,绝对是你最想要的!

    • 项目地址:https://gitee.com/lab1024/smart-admin
    用上这几个开源管理系统做项目,领导看了直呼专业!
    用上这几个开源管理系统做项目,领导看了直呼专业!

    erupt 低代码全栈类框架

    Erupt 是一个低代码 全栈类 框架,它使用 Java 注解 动态生成页面以及增、删、改、查、权限控制等后台功能。零前端代码、零 CURD、自动建表,仅需 一个类文件 + 简洁的注解配置,快速开发企业级 Admin 管理后台。高扩展性,支持 CURD 自由扩展 @DataProxy 、自定义数据源、逻辑删除、LDAP、OSS。

    • 项目地址:https://gitee.com/erupt/erupt
    用上这几个开源管理系统做项目,领导看了直呼专业!
    用上这几个开源管理系统做项目,领导看了直呼专业!

    ID:GitHubKY

    来源:开源技术专栏