兜兜零元 发表于 2024-7-22 01:15:17

记载些Spring+题集(12)

11种API性能优化方法

一、索引优化

接口性能优化时,大家第一个想到的通常是:优化索引,优化索引的本钱是最小的。
你可以通过查看线上日志或监控陈诉,发现某个接口使用的某条SQL语句耗时较长。
   这条SQL语句是否已经加了索引?
加的索引是否收效了?
MySQL是否选择了错误的索引?
1.1 没加索引

在SQL语句中,忘记为WHERE条件的关键字段或ORDER BY后的排序字段加索引是项目中常见的题目。在项目初期,由于表中的数据量较小,加不加索引对SQL查询性能影响不大。然而,随着业务的发展,表中的数据量不断增加,这时就必须加索引了。
可以通过以下下令查看/添加索引:
show index from `table_name`
CREATE INDEX index_name ON table_name (column_name);
这种方式能够明显进步查询性能,尤其是在数据量庞大的环境下。
1.2 索引充公效

通过上述下令我们已经确认索引是存在的,但它是否收效呢?
   那么,如何查看索引是否收效呢?
可以使用 EXPLAIN 下令,查看 MySQL 的执行筹划,它会表现索引的使用环境。
EXPLAIN SELECT * FROM `order` WHERE code='002';
效果:
https://img-blog.csdnimg.cn/img_convert/9011661ad44a3691d4c412086d93a15c.png
这个下令将表现查询的执行筹划,包罗使用了哪些索引。如果索引收效,你会在输出效果中看到相干的信息。通过这几列可以判断索引使用环境,执行筹划包含列的寄义如下图所示:
https://img-blog.csdnimg.cn/img_convert/b0721aac22bf145b10c97253317a1416.png
说真话,SQL语句没有使用索引,除去没有建索引的环境外,最大的大概性是索引失效了。
以下是索引失效的常见缘故原由:
https://img-blog.csdnimg.cn/img_convert/c26cc0673c8864b5d2d996ec95651979.png
相识这些缘故原由,可以帮助你在查询优化时制止索引失效的题目,确保数据库查询性能保持最佳。
1.3 选错索引

此外,你是否遇到过这样一种环境:明显是同一条SQL语句,只是入参不同。有时候使用的是索引A,有时候却使用索引B?
   没错,有时候MySQL会选错索引。
须要时可以使用 FORCE INDEX 来欺压查询SQL使用某个索引。
比方:
SELECT * FROM `order` FORCE INDEX (index_name) WHERE code='002';
至于为什么MySQL会选错索引,缘故原由大概有以下几点:
https://img-blog.csdnimg.cn/img_convert/1789cced256ed5c321720f2a2e4194fc.png
相识这些缘故原由,可以帮助你更好地理解和控制MySQL的索引选择行为,确保查询性能的稳固性。
二、SQL优化

如果优化了索引之后效果不明显,接下来可以实验优化一下SQL语句,由于相对于修改Java代码来说,改造SQL语句的本钱要小得多。以下是SQL优化的15个小技巧:
https://img-blog.csdnimg.cn/img_convert/7cb7c602b0d0a3decf0024161c235eef.png
三、远程调用

多时候,我们需要在一个接口中调用其他服务的接口。
在用户信息查询接口中需要返回以下信息:用户名称、性别、等级、头像、积分和发展值。此中,用户名称、性别、等级和头像存储在用户服务中,积分存储在积分服务中,发展值存储在发展值服务中。为了将这些数据同一返回,我们需要提供一个额外的对外接口服务。因此,用户信息查询接口需要调用用户查询接口、积分查询接口和发展值查询接口,然后将数据汇总并同一返回。
调用过程如下图所示:
https://img-blog.csdnimg.cn/img_convert/460077d2735c7a370e0a53156439f4ff.png
调用远程接口总耗时 530ms = 200ms + 150ms + 180ms
显然这种串行调用远程接口性能黑白常不好的,调用远程接口总的耗时为所有的远程接口耗时之和。
   那么如何优化远程接口性能呢?
3.1 串行改并行

上面说到,既然串行调用多个远程接口性能很差,为什么不改成并行呢?调用远程接口的总耗时为200ms,这即是耗时最长的那次远程接口调用时间。
在Java 8之前,可以通过实现Callable接口来获取线程的返回效果。在Java 8之后,可以通过CompletableFuture类来实现这一功能。
以下是一个使用CompletableFuture的示例:
public class RemoteServiceExample {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 调用用户服务接口
        CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
            // 模拟远程调用
            simulateDelay(200);
            return "User Info";
        });
        // 调用积分服务接口
        CompletableFuture<String> pointsFuture = CompletableFuture.supplyAsync(() -> {
            // 模拟远程调用
            simulateDelay(150);
            return "Points Info";
        });
        // 调用成长值服务接口
        CompletableFuture<String> growthFuture = CompletableFuture.supplyAsync(() -> {
            // 模拟远程调用
            simulateDelay(100);
            return "Growth Info";
        });
        // 汇总结果
        CompletableFuture<Void> allOf = CompletableFuture.allOf(userFuture, pointsFuture, growthFuture);
        // 等待所有异步操作完成
        allOf.join();
        // 获取结果
        String userInfo = userFuture.get();
        String pointsInfo = pointsFuture.get();
        String growthInfo = growthFuture.get();
    }
}

3.2 数据异构

为了提升接口性能,尤其在高并发场景下,可以考虑数据冗余,将用户信息、积分和发展值的数据同一存储在一个地方,比如Redis。
这样,通过用户ID可以直接从Redis中查询所需的数据,从而制止远程接口调用
https://img-blog.csdnimg.cn/img_convert/739e491b4fa35544fec0bbc4d71427f5.png
但需要注意的是,如果使用了数据异构方案,就大概会出现数据一致性题目。用户信息、积分和发展值有更新的话,大部门环境下,会先更新到数据库,然后同步到redis。但这种跨库的操纵,大概会导致双方数据不一致的环境产生。
   那如何解决数据一致性题目呢?
《亿级电商流量,高并发下Redis与MySQL的数据一致性如何保证》
四、重复调用

在我们的一样平常工作代码中,重复调用非常常见,但如果没有控制好,会严重影响接口的性能。
4.1 循环查数据库 有时候,我们需要从指定的用户聚会合查询出哪些用户已经存在于数据库中。
public List<User> findExistingUsers(List<String> userIds) {
    List<User> existingUsers = new ArrayList<>();
    for (String userId : userIds) {
        User user = userRepository.findById(userId);
        if (user != null) {
            existingUsers.add(user);
        }
    }
    return existingUsers;
}

上述代码会对每个用户ID执行一次数据库查询,这在用户聚集较大时会导致性能题目。
   那么,我们如何优化呢?
我们可以通过批量查询来优化性能,镌汰数据库的查询次数。
public List<User> findExistingUsers(List<String> userIds) {
    // 批量查询数据库
    List<User> users = userRepository.findByIds(userIds);
    return users;
}

   这里有个需要注意的地方是:id聚集的巨细要做限制,最好一次不要哀求太多的数据。要根据现实环境而定,发起控制每次哀求的记载条数在500以内。
五、异步处置处罚

在进行接口性能优化时,有时候需要重新梳理业务逻辑,查抄是否存在设计不公道的地方。假设有一个用户哀求接口,需要执行以下操纵:

[*] 业务操纵
[*] 发送站内关照
[*] 记载操纵日志 为了实现方便,通常会将这些逻辑放在接口中同步执行,但这会对接口性能造成一定影响。
https://img-blog.csdnimg.cn/img_convert/fd5fcbad179c7932bb7d6a69b54f1531.png
这个接口表面上看起来没有题目,但如果你仔细梳理一下业务逻辑,会发现只有业务操纵才是核心逻辑,其他的功能都黑白核心逻辑。在这里有个原则就是:
   核心逻辑可以同步执行,同步写库。非核心逻辑,可以异步执行,异步写库。
上面这个例子中,发站内关照和用户操纵日志功能,对实时性要求不高,即使晚点写库,用户无非是晚点收到站内关照,或者运营晚点看到用户操纵日志,对业务影响不大,所以完全可以异步处置处罚。
   异步处置处罚通常有两种主要方式:多线程和消息队列(MQ)
5.1 线程池异步优化

使用线程池改造之后,接口逻辑如下
https://img-blog.csdnimg.cn/img_convert/2b10b3c008ecbdfe2a50f082f32a4f29.png
5.2 MQ异步

使用线程池有个小题目就是:如果服务器重启了,或者是需要被执行的功能出现异常了,无法重试,会丢数据。
为了制止使用线程池处置处罚异步任务时出现数据丢失的题目,可以考虑使用更加结实和可靠的异步处置处罚方案,如消息队列(MQ)。消息队列不仅可以异步处置处罚任务,还能够保证消息的持久化和可靠性,支持重试机制。
使用mq改造之后,接口逻辑如下:
https://img-blog.csdnimg.cn/img_convert/e355585f7b3d087e4289e12c8761d344.png
六、制止大事务

使用@Transactional注解这种声明式事务的方式提供事务功能,确实能少写许多代码,提升开发效率。但也容易造成大事务,引发性能的题目。
https://img-blog.csdnimg.cn/img_convert/af6a99a12f7fb37bf3ff8468a375c167.png
   那么我们该如何优化大事务呢?
为了制止大事务引发的题目,可以考虑以下优化发起:

[*] 少用@Transactional注解
[*] 将查询(select)方法放到事务外
[*] 事务中制止远程调用
[*] 事务中制止一次性处置处罚太多数据
[*] 有些功能可以非事务执行
[*] 有些功能可以异步处置处罚
七、锁粒度

在一些业务场景中,为了制止多个线程并发修改同一共享数据而引发数据异常,通常我们会使用加锁的方式来解决这个题目。然而,如果锁的设计不当,导致锁的粒度过粗,也会对接口性能产生明显的负面影响。
7.1 synchronized

在Java中,我们可以使用synchronized关键字来为代码加锁。
通常有两种写法:在方法上加锁和在代码块上加锁。
1. 方法上加锁
public synchronized void doSave(String fileUrl) {
    mkdir();
    uploadFile(fileUrl);
    sendMessage(fileUrl);
}
在方法上加锁的目的是为了防止并发环境下创建相同的目次,制止第二次创建失败而影响业务功能。但这种直接在方法上加锁的方式,锁的粒度较粗。
由于doSave方法中的文件上传和消息发送并不需要加锁,只有创建目次的方法需要加锁。我们知道,文件上传操纵非常耗时,如果将整个方法加锁,那么需要比及整个方法执行完之后才气开释锁。显然,这会导致该方法的性能降落,得不偿失。
2. 代码块上加锁我们可以将加锁改在代码块上,从而缩小锁的粒度, 如下:
public void doSave(String path, String fileUrl) {
    synchronized(this) {
        if (!exists(path)) {
            mkdir(path);
        }
    }
    uploadFile(fileUrl);
    sendMessage(fileUrl);
}
这样改造后,锁的粒度变小了,只有并发创建目次时才加锁。创建目次是一个非常快的操纵,即使加锁对接口性能的影响也不大。最重要的是,其他的文件上传和消息发送功能仍然可以并发执行。
多节点环境中的题目 在单机版服务中,这种做法没有题目。但在生产环境中,为了保证服务的稳固性,同一个服务通常会摆设在多个节点上。如果某个节点挂掉,其他节点的服务仍然可用。
多节点摆设制止了某个节点挂掉导致服务不可用的环境,同时也能分摊整个体系的流量,制止体系压力过大。
但这种摆设方式也带来了新的题目:synchronized只能保证一个节点加锁有效。
   如果有多个节点,如何加锁呢?
7.2 Redis分布式锁

在分布式体系中,由于Redis分布式锁的实现相对简单且高效,因此它在许多现实业务场景中被广泛采取。
使用Redis分布式锁的伪代码如下:
public boolean doSave(String path, String fileUrl) {
    try {
        String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
        if ("OK".equals(result)) {
            if (!exists(path)) {
                mkdir(path);
                uploadFile(fileUrl);
                sendMessage(fileUrl);
            }
            return true;
        }
    } finally {
        unlock(lockKey, requestId);
    }
    return false;
}
与之前使用synchronized关键字加锁时一样,这里的锁的范围也太大了,换句话说,锁的粒度太粗。这会导致整个方法的执行效率很低。
现实上,只有在创建目次时才需要加分布式锁,别的代码不需要加锁。于是,我们需要优化代码:
public void doSave(String path, String fileUrl) {
    if (tryLock()) {
        try {
            if (!exists(path)) {
                mkdir(path);
            }
        } finally {
            unlock(lockKey, requestId);
        }
    }
    uploadFile(fileUrl);
    sendMessage(fileUrl);
}

private boolean tryLock() {
    String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
    return "OK".equals(result);
}

private void unlock(String lockKey, String requestId) {
    // 解锁逻辑
}
上面的代码将加锁的范围缩小了,只有在创建目次时才加锁。这样的简单优化后,接口性能可以得到明显提升。
7.3 数据库锁

MySQL数据库中的三种锁

[*] 表锁:

[*] 优点:加锁快,不会出现死锁。
[*] 缺点:锁定粒度大,锁辩论的概率高,并发度最低。

[*] 行锁:

[*] 优点:锁定粒度最小,锁辩论的概率低,并发度最高。
[*] 缺点:加锁慢,会出现死锁。

[*] 间隙锁:

[*] 优点:锁定粒度介于表锁和行锁之间。
[*] 缺点:开销和加锁时间介于表锁和行锁之间,并发度一样平常,也会出现死锁。

锁与并发度
并发度越高,接口性能越好。因此,数据库锁的优化方向是:

[*] 优先使用行锁
[*] 其次使用间隙锁
[*] 末了使用表锁
八、分页处置处罚

有时候,我需要调用某个接口来批量查询数据,比方,通过用户ID批量查询用户信息,然后为这些用户赠予积分。但是,如果一次性查询的用户数目太多,比方一次查询2000个用户的数据,传入2000个用户的ID进行远程调用时,用户查询接口经常会出现超时的环境。
调用代码如下:
List<User> users = remoteCallUser(ids);
众所周知,调用接口从数据库获取数据需要经过网络传输。如果数据量过大,无论是数据获取速率还是网络传输速率都会受到带宽限制,从而导致耗时较长。
   那么,这种环境下该如何优化呢?
答案是:分页处置处罚。
将一次性获取所有数据的哀求,改为分多次获取,每次只获取一部门用户的数据,末了进行合并和汇总。其实,处置处罚这个题目可以分为两种场景:同步调用和异步调用。
8.1 同步调用

如果在job中需要获取2000个用户的信息,它要求只要能正确获取到数据即可,对获取数据的总耗时要求不高。但对每一次远程接口调用的耗时有要求,不能大于500ms,否则会有邮件预警。这时,我们可以同步分页调用批量查询用户信息接口。
具体示例代码如下:
List<List<Long>> allIds = Lists.partition(ids, 200);

for (List<Long> batchIds : allIds) {
    List<User> users = remoteCallUser(batchIds);
}
代码中我使用了Google Guava工具中的Lists.partition方法,用它来做分页简直太好用了,不然要写一大堆分页的代码。 8.2 异步调用 如果是在某个接口中需要获取2000个用户的信息,需要考虑的因素更多。
除了远程调用接口的耗时,还需要考虑该接口本身的总耗时,也不能超过500ms。这时,使用上面的同步分页哀求远程接口的方法肯定是行不通的。那么,只能使用异步调用了。
代码如下:
List<List<Long>> allIds = Lists.partition(ids, 200);

final List<User> result = Lists.newArrayList();
allIds.stream().forEach(batchIds -> {
    CompletableFuture.supplyAsync(() -> {
        result.addAll(remoteCallUser(batchIds));
        return Boolean.TRUE;
    }, executor);
});

使用CompletableFuture类,通过多个线程异步调用远程接口,末了汇总效果同一返回。
九、加缓存

通常环境下,我们最常用的缓存是:Redis和Memcached。但对于Java应用来说,绝大多数环境下使用的是Redis,所以接下来我们以Redis为例。
在关系型数据库(比方:MySQL)中,菜单通常有上下级关系。某个四级分类是某个三级分类的子分类,三级分类是某个二级分类的子分类,而二级分类又是某个一级分类的子分类。
这种存储结构决定了,想一次性查出整个分类树并非易事。这需要使用程序递归查询,而如果分类许多,这个递归操纵会非常耗时。因此,如果每次都直接从数据库中查询分类树的数据,会是一个非常耗时的操纵。
这时我们可以使用缓存。在大多数环境下,接口直接从缓存中获取数据。操纵Redis可以使用成熟的框架,比如:Jedis和Redisson等。 使用Jedis的伪代码如下:
String json = jedis.get(key);
if (StringUtils.isNotEmpty(json)) {
    CategoryTree categoryTree = JsonUtil.toObject(json);
    return categoryTree;
}
return queryCategoryTreeFromDb();
   注意引入缓存之后,我们的体系复杂度就上升了,这时候就会存在数据不一致的题目
《亿级电商流量,高并发下Redis与MySQL的数据一致性如何保证》
十、分库分表

有时候,接口性能受限的并不是其他方面,而是数据库。
当体系发展到一定阶段,用户并发量增加,会有大量的数据库哀求,这不仅需要占用大量的数据库连接,还会带来磁盘IO的性能瓶颈题目。此外,随着用户数目的不断增加,产生的数据量也越来越大,一张表大概无法存储所有数据。由于数据量太大,即使SQL语句使用了索引,查询数据时也会非常耗时。那么,这种环境下该怎么办呢?
答案是:需要进行分库分表。
如下图所示:
https://img-blog.csdnimg.cn/img_convert/bd7627bf67e674f23ae6edd2087e8850.png
图中将用户库拆分成了三个库,每个库都包含了四张用户表。如果有用户哀求过来,先根据用户ID路由到此中一个用户库,然后再定位到某张表。路由的算法有许多:

[*] 根据ID取模:

[*] 比方:ID=7,有4张表,则7%4=3,模为3,路由到用户表3。

[*] 给ID指定一个区间范围:

[*] 比方:ID的值是0-10万,则数据存在用户表0;ID的值是10-20万,则数据存在用户表1。

[*] 一致性Hash算法。 分库分表主要有两个方向:垂直和程度。
1. 垂直分库分表
垂直分库分表(即业务方向)更简单,将不同的业务数据存储在不同的库或表中。
比方,将用户数据和订单数据存储在不同的库中。
2. 程度分库分表
程度分库分表(即数据方向)上,分库和分表的作用有区别,不能混为一谈。
分库



[*] 目的:解决数据库连接资源不足题目和磁盘IO的性能瓶颈题目。
分表



[*] 目的:解决单表数据量太大,SQL语句查询数据时,即使走了索引也非常耗时的题目。此外,还可以解决消耗CPU资源的题目。
分库分表



[*] 目的:综合解决数据库连接资源不足、磁盘IO性能瓶颈、数据检索耗时和CPU资源消耗等题目。
业务场景中的应用


[*] 只分库:

[*] 用户并发量大,但需要保存的数据量很少。

[*] 只分表:

[*] 用户并发量不大,但需要保存的数据量许多。

[*] 分库分表:

[*] 用户并发量大,并且需要保存的数据量也许多。

十一、监控功能

优化接口性能题目,除了上面提到的这些常用方法之外,还需要配合使用一些辅助功能,由于它们真的可以帮我们提升查找题目的效率。
11.1 开启慢查询日志

通常环境下,为了定位SQL的性能瓶颈,我们需要开启MySQL的慢查询日志。把超过指定时间的SQL语句单独记载下来,方便以后分析和定位题目。
开启慢查询日志需要重点关注三个参数:


[*] slow_query_log:慢查询开关
[*] slow_query_log_file:慢查询日志存放的路径
[*] long_query_time:超过多少秒才会记载日志
通过MySQL的SET下令可以设置:
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/usr/local/mysql/data/slow.log';
SET GLOBAL long_query_time = 2;
设置完之后,如果某条SQL的执行时间超过了2秒,会被自动记载到slow.log文件中。
固然,也可以直接修改配置文件my.cnf:

slow_query_log = ON
slow_query_log_file = /usr/local/mysql/data/slow.log
long_query_time = 2

但这种方式需要重启MySQL服务。
许多公司每天早上都会发一封慢查询日志的邮件,开发人员根据这些信息优化SQL。
11.2 加监控

为了在出现SQL题目时能够实时发现,我们需要对体系做监控。目前业界使用比较多的开源监控体系是:Prometheus。它提供了监控和预警的功能。
架构图如下:
https://img-blog.csdnimg.cn/img_convert/75f4453be81e5352222dcf36ad28d318.png
我们可以用它监控如下信息:


[*] 接口相应时间
[*] 调用第三方服务耗时
[*] 慢查询sql耗时
[*] cpu使用环境
[*] 内存使用环境
[*] 磁盘使用环境
[*] 数据库使用环境
[*] 等等。。。
它的界面大概长这样子:
https://img-blog.csdnimg.cn/img_convert/befe8dd909ee5bbfff8af32201c4b369.png
可以看到MySQL的当前QPS、活跃线程数、连接数、缓存池的巨细等信息。如果发现连接池占用的数据量太多,肯定会对接口性能造成影响。这时大概是由于代码中开启了连接却忘记关闭,或者并发量太大导致的,需要进一步排查和体系优化
链路跟踪
有时候,一个接口涉及的逻辑非常复杂,比方查询数据库、查询Redis、远程调用接口、发送MQ消息以及执行业务代码等等。这种环境下,接口的一次哀求会涉及到非常长的调用链路。如果逐一排查这些题目,会泯灭大量时间,此时我们已经无法用传统的方法来定位题目。
有没有办法解决这个题目呢?
答案是使用分布式链路跟踪体系:SkyWalking。
SkyWalking的架构图如下:
https://img-blog.csdnimg.cn/img_convert/7accbba7d32651e5daa0586fb5fdcaba.png
https://img-blog.csdnimg.cn/img_convert/730b77b0ad3bb217929829f0bb7657a8.png
在SkyWalking中,可以通过traceId(全局唯一的ID)来串联一个接口哀求的完整链路。你可以看到整个接口的耗时、调用的远程服务的耗时、访问数据库或者Redis的耗时等,功能非常强大。
之前没有这个功能时,为了定位线上接口性能题目,我们需要在代码中加日志,手动打印出链路中各个环节的耗时环境,然后再逐一排查。这种方法不仅费时费力,而且容易遗漏细节。
如果你用过SkyWalking来排查接口性能题目,你会不自觉地爱上它的功能。如果你想相识更多功能,可以访问SkyWalking的官网:skywalking.apache.org。
CPU飙升到100%,GC频繁,故障排查

发现题目

下面是线上呆板的cpu使用率,可以看到从4月8日开始,随着时间cpu使用率在逐步增高,终极使用率达到100%导致线上服务不可用,后面重启了呆板后恢复。
https://img-blog.csdnimg.cn/img_convert/4a75070b18a1c51a888ee1e0fc4a0f22.png
排查思绪

简单分析下大概出题目的地方,分为5个方向:

[*] 体系本身代码题目。
[*] 内部下游体系的题目导致的雪崩效应。
[*] 上游体系调用量突增。
[*] http哀求第三方的题目。
[*] 呆板本身的题目。
初步定位题目

1.查看日志,没有发现会合的错误日志,初步排除代码逻辑处置处罚错误。
2.首先接洽了内部下游体系观察了他们的监控,发现一起正常。可以排除下游体系故障对我们的影响。
3.查看provider接口的调用量,对比7天没有突增,排除业务方调用量的题目。
4.查看tcp监控,TCP状态正常,可以排除是http哀求第三方超时带来的题目。
5.查看呆板监控,6台呆板cpu都在上升,每个呆板环境一样。排除单点呆板故障题目。
即通过上述方法没有直接定位到题目。
定位CPU题目

1.重启了6台中题目比较严重的5台呆板,先恢复业务。保留一台现场,用来分析题目。
2.查看当前的tomcat线程pid。
https://img-blog.csdnimg.cn/img_convert/bfd3dc5e2119e146bfb2d0e22ef68374.png
3.查看该pid下线程对应的体系占用环境。
top -Hp 384
https://img-blog.csdnimg.cn/img_convert/c7ee8298edaaaa780171cd61c7a919a9.png
4.发现pid 4430 4431 4432 4433 线程分别占用了约40%的cpu。
https://img-blog.csdnimg.cn/img_convert/9cafca51f515c4101e9a764e34027f37.png
5.将这几个pid转为16进制,分别为114e 114f 1150 1151
6.下载当前的java线程栈 sudo -u tomcat jstack -l 384>/1.txt
7.查询5中对应的线程环境,发现都是gc线程导致的
https://img-blog.csdnimg.cn/img_convert/823636e37ec7784d9d5a423d7e2d7fda.png
8.dump java堆数据 sudo -u tomcat jmap -dump:live,format=b,file=/dump201612271310.dat 384 9.使用MAT加载堆文件,可以看到javax.crypto.JceSecurity对象占用了95%的内存空间,初步定位到题目。MAT下载地址:http://www.eclipse.org/mat/
https://img-blog.csdnimg.cn/img_convert/2fafa7ad7a3c0a02b69deda649d47d9d.png
10.查看类的引用树,看到BouncyCastleProvider对象持有过多。即我们代码中对该对象的处置处罚方式是错误的,定位到题目。
https://img-blog.csdnimg.cn/img_convert/9462f1a8044cae03cd6e40a5f43035f5.png
代码分析

我们代码中有一块是这样写的
https://img-blog.csdnimg.cn/img_convert/0bae54fceda4c61d6cf21ebffb505389.png
这是加解密的功能,每次运行加解密都会new一个BouncyCastleProvider对象,放倒Cipher.getInstance()方法中。看下Cipher.getInstance()的实现,这是jdk的底层代码实现,追踪到JceSecurity类中
https://img-blog.csdnimg.cn/img_convert/197025b2a5e14f9f6d135ce0a1b2bc84.png
verifyingProviders每次put后都会remove,verificationResults只会put,不会remove.
https://img-blog.csdnimg.cn/img_convert/ae0c7f70324129497aff2f8e02b569de.png
看到verificationResults是一个static的map,即属于JceSecurity类的。所以每次运行到加解密都会向这个map put一个对象,而这个map属于类的维度,所以不会被GC回收。这就导致了大量的new的对象不被回收。
代码改进

将有题目的对象置为static,每个类持有一个,不会多次新建对象。
https://img-blog.csdnimg.cn/img_convert/c0c0209a40696797b4ac3a03e75f0509.png
本文总结

遇到线上CPU升高题目不要慌,首先确认排查题目的思绪:

[*] 查看日志。
[*] 观察上下游接口调用环境。
[*] 查看TCP环境。
[*] 查看呆板CPU环境。
[*] 查看java线程,jstack。
[*] 查看java堆,jmap。
[*] 通过MAT分析堆文件,寻找无法被回收的对象。
高并发下数据读写的安全性

3.1、元数据锁(Meta Data Lock)

Meta Data Lock元数据锁,也被简称为MDL锁,这是基于表的元数据加锁,表锁是基于整张表加锁,行锁是基于一条数据加锁,那这个表的元数据是什么?所有存储引擎的表都会存在一个.frm文件,这个文件中主要存储表的结构(DDL语句),而MDL锁就是基于.frm文件中的元数据加锁的。
   对于这种锁是在MySQL5.5版本后再开始支持的,这个锁主要是用于:更改表结构时使用,比如你要向一张表创建/删除一个索引、修改一个字段的名称/数据范例、增加/删除一个表字段等这类环境。
由于毕竟当你的表结构正在发生更改,假设此时有其他事务来对表做CRUD操纵,天然就会出现题目,比如我刚删了一个表字段,效果另一个事务中又按原来的表结构插入了一条数据,这显然会存在风险,因此MDL锁在加锁后,整张表不允许其他事务做任何操纵。
3.2、自增锁(AUTO-INC Lock)

自增锁,这个是专门为了提升自增ID的并发插入性能而设计的,通常环境下咱们在建表时,都会对一张表的主键设置自增特性,如下:
CREATE TABLE `table_name` (
    `xx_id` NOT NULL AUTO_INCREMENT,
    .....
) ENGINE = InnoDB; 当对一个字段设置AUTO_INCREMENT自增后,意味着后续插入数据时无需为其赋值,体系会自动赋上顺序自增的值。比如目前表中最大的ID=88,如果两个并发事务一起对表执行插入语句,由于是并发执行的缘故原由,所以有大概会导致插入两条ID=89的数据。因此这里必须要加上一个排他锁,确保并发插入时的安全性,但也由于锁的缘故原由,插入的效率也就因此降低了,毕竟将所有写操纵串行化了。
   为了改善插入数据时的性能,自增锁诞生了,自增锁也是一种特殊的表锁,但它仅为具备AUTO_INCREMENT自增字段的表服务,同时自增锁也分成了不同的级别,可以通过innodb_autoinc_lock_mode参数控制。


[*] • innodb_autoinc_lock_mode = 0:传统模式。
[*] • innodb_autoinc_lock_mode = 1:连续模式(MySQL8.0以前的默认模式)。
[*] • innodb_autoinc_lock_mode = 2:交错模式(MySQL8.0之后的默认模式)。
固然,这三种模式又是什么寄义呢?得先弄明白MySQL中大概出现的三种插入范例:


[*] • 普通插入:指通过INSERT INTO table_name(...) VALUES(...)这种方式插入。
[*] • 批量插入:指通过INSERT ... SELECT ...这种方式批量插入查询出的数据。
[*] • 混合插入:指通过INSERT INTO table_name(id,...) VALUES(1,...),(NULL,...),(3,...)这种方式插入,此中一部门指定ID,一部门不指定。
自增锁主要负责维护并发事务下自增列的顺序,也就是说,每当一个事务想向表中插入数据时,都要先获取自增锁先分配一个自增的顺序值,但不同模式下的自增锁也会有些许不同。
   传统模式:事务T1获取自增锁插入数据,事务T2也要插入数据,此时事务T2只能阻塞等待,也就是传统模式下的自增锁,同时只允许一条线程执行,这种形式显然性能较低。
    连续模式:这个模式主要是由于传统模式存在性能短板而研发的,在这种模式中,对于能够提前确定数目的插入语句,则不会再获取自增锁,啥意思呢?也就是对于“普通插入范例”的语句,由于在插入之前就已经确定了要插入多少条数据,由于会直接分配范围自增值。好比目前事务T1要通过INSERT INTO...语句插入十条数据,目前表中存在的最大ID=88,那在连续模式下,MySQL会直接将89~98这十个自增值分配给T1,因此T1无需再获取自增锁,但不获取自增锁不代表不获取锁了,而是改为使用一种轻量级锁Mutex-Lock来防止自增值重复分配。

对于普通插入范例的操纵,由于可以提前确定插入的数据量,因此可以采取“预分配”思想,但如若对于批量插入范例的操纵,由于批量插入的数据是基于SELECT语句查询出来的,所以在执行之前也无法确定究竟要插入多少条,所以仍旧会获取自增锁执行。也包罗对于混合插入范例的操纵,有一部门指定了自增值,但有一部门需要MySQL分配,因此“预分配”的思想也用不上,因此也要获取自增锁执行。
    交错模式:在交错插入模式中,对于INSERT、REPLACE、INSERT…SELECT、REPLACE…SELECT、LOAD DATA等一系列插入语句,都不会再使用表级别的自增锁,而是全都使用Mutex-Lock来确保安全性,为什么在这个模式中,批量插入也可以不获取自增锁呢?这跟它的名字有关,目前这个模式叫做交错插入模式,也就是不同事务之间插入数据时,自增列的值是交错插入的。

好比事务T1、T2都要执行批量插入的操纵,由于不确定各自要插入多少条数据,所以之前那种“连续预分配”的思想用不了了,但虽然无法做“连续的预分配”,那能不能交错预分配呢?好比给T1分配{1、3、5、7、9....},给T2分配{2、4、6、8、10.....},然后两个事务交错插入,这样岂不是做到了自增值即不重复,也能支持并发批量插入?答案是Yes,但由于两个事务执行的都是批量插入的操纵,因此事先不确定插入行数,所以有大概导致“交错预分配”的顺序值,有大概不会使用,比如T1只插入了四条数据,只用了1、3、5、7,T2插入了五条数据,因此表中的自增值有大概出现清闲,即{1、2、3、4、5、6、8、10},此中9就并未使用。
3.3、全局锁

全局锁其实是一种尤为特殊的表锁,其实将它称之为库锁也许更符合,由于全局锁是基于整个数据库来加锁的,加上全局锁之后,整个数据库只能允许读,不允许做任何写操纵,一样平常全局锁是在对整库做数据备份时使用。
-- 获取全局锁的命令
FLUSH TABLES WITH READ LOCK;

-- 释放全局锁的命令
UNLOCK TABLES; 从上述的下令也可以看出,为何将其归纳到表锁范围,由于获取锁以及开释锁的下令都是表锁的下令。
   PS:表中横向(行)表示已经持有锁的事务,纵向(列)表示正在哀求锁的事务。
行级锁对比共享临键锁排他临键锁间隙锁插入意向锁共享临键锁兼容辩论兼容辩论排他临键锁辩论辩论兼容辩论间隙锁兼容兼容兼容辩论插入意向锁辩论辩论辩论兼容 由于临建锁也会锁定相应的行数据,因此上表中也不再重复赘述记载锁,临建锁兼容的 记载锁都兼容,同理,辩论的记载锁也会辩论,再来看看标记别的锁对比:
表级锁对比共享意向锁排他意向锁元数据锁自增锁全局锁共享意向锁兼容辩论辩论辩论辩论排他意向锁辩论辩论辩论辩论辩论元数据锁辩论辩论辩论辩论辩论自增锁辩论辩论辩论辩论辩论全局锁兼容辩论辩论辩论辩论 会发现表级别的锁,会有许多许多辩论,由于锁的粒度比较大,因此许多时候都会出现辩论,但对于表级锁,咱们只需要关注共享意向锁和共享排他锁即可,其他的大多数为MySQL的隐式锁(在这里,共享意向锁和排他意向锁,也可以理解为MyISAM中的表读锁和表写锁)。
   末了再简单的说一下,表中的辩论和兼容究竟是啥意思?辩论的意思是当一个事务T1持有某个锁时,另一个事务T2来哀求相同的锁,T2会由于锁排斥会陷入阻塞等待状态。反之同理,兼容的意思是指允许多个事务一同获取同一个锁。
MySQL5.7 共享排他锁

在MySQL5.7之前的版本中,数据库中仅存在两种范例的锁,即共享锁与排他锁,但是在MySQL5.7.2版本中引入了一种新的锁,被称之为(SX)共享排他锁,这种锁是共享锁与排他锁的杂交范例,至于为何引入这种锁呢?
   SMO题目(Split-Merge Overflow)通常出现在数据库管理体系中,特别是在B+Tree索引结构的操纵过程中。以下是对SMO题目的表明:
**B+Tree**:
B+Tree是一种自平衡的树数据结构,通常用于数据库和操纵体系的索引。在数据库中,B+Tree用于快速检索记载,由于它们允许对数据进行排序并支持对数时间复杂度的搜刮、顺序访问、插入和删除操纵。
**SMO题目**:
1. **Split(分裂)**:当向B+Tree中的一个页(通常是磁盘上的一个块)插入一个新的键,而这个页已经满了时,就会发生分裂。这时,页中的键和指针会被分成两个部门,一部门保留在原页,另一部门被移动到一个新的页中。这种操纵大概导致树的其他部门也发生分裂,以保持B+Tree的性质。
2. **Merge(合并)**:删除操纵大概导致页中的键的数目镌汰,如果某个页中的键的数目低于某个阈值,这个页大概需要与它的兄弟页合并,以保持B+Tree的效率。
**Overflow(溢出)**:
SMO题目中的“Overflow”指的是在分裂或合并操纵中,由于页分裂或合并导致的额外开销和处置处罚,大概会引起体系性能的降落。特别是在高并发的数据库环境中,这种开销大概会更加明显。
**减小锁定的B+Tree粒度**:
在多用户并发访问数据库时,锁定是用于保持数据一致性的机制。减小锁定粒度意味着锁定的范围更小,比方,不是锁定整个B+Tree页,而是只锁定页中的某个部门。这样做可以镌汰并发操纵中的锁争用,从而进步体系的并发性能。具体到SMO题目,减小粒度可以帮助镌汰因分裂或合并操纵而需要锁定的大量资源,从而减轻性能降落的题目。

总结来说,SMO题目是数据库中B+Tree索引结构在维护其平衡和效率时,因分裂和合并操纵导致的一系列性能挑衅。通过优化锁定策略,可以镌汰SMO题目对数据库性能的影响。
    在SQL执行期间一旦更新操纵触发B+Tree叶子节点分裂,那么就会对整棵B+Tree加排它锁,这不但阻塞了后续这张表上的所有的更新操纵,同时也制止了所有试图在B+Tree上的读操纵,也就是会导致所有的读写操纵都被阻塞,其影响巨大。因此,这种大粒度的排它锁成为了InnoDB支持高并发访问的主要瓶颈,而这也是MySQL 5.7版本中引入SX锁要解决的题目。
那想一下该如何解决这个题目呢?最简单的方式就是减小SMO题目发生时,锁定的B+Tree粒度,当发生SMO题目时,就只锁定B+Tree的某个分支,而并不是锁定整颗B+树,从而做到不影响其他分支上的读写操纵。
   那MySQL5.7中引入共享排他锁后,究竟是如何实现的这点呢?首先要弄清楚SX锁的特性,它不会阻塞S锁,但是会阻塞X、SX锁。
在聊之前首先得搞清楚SQL执行时的几个概念:


[*] • 读取操纵:基于B+Tree去读取某条或多条行记载。
[*] • 乐观写入:不会改变B+Tree的索引键,仅会更改索引值,比如主键索引树中不修改主键字段,只修改其他字段的数据,不会引起节点分裂。
[*] • 悲观写入:会改变B+Tree的结构,也就是会造成节点分裂,比如无序插入、修改索引键的字段值。
在MySQL5.6版本中,一旦有操纵导致了树结构发生变化,就会对整棵树加上排他锁,阻塞所有的读写操纵,而MySQL5.7版本中,为相识决该题目,对于不同的SQL执行,流程就做了调整。
MySQL5.7中读操纵的执行流程



[*] • ①读取数据之前首先会对B+Tree加一个共享锁。
[*] • ②在基于树检索数据的过程中,对于所有走过的叶节点会加一个共享锁。
[*] • ③找到需要读取的目标叶子节点后,先加一个共享锁,开释步骤②上加的所有共享锁。
[*] • ④读取终极的目标叶子节点中的数据,读取完成后开释对应叶子节点上的共享锁。
MySQL5.7中乐观写入的执行流程



[*] • ①乐观写入之前首先会对B+Tree加一个共享锁。
[*] • ②在基于树检索修改位置的过程中,对于所有走过的叶节点会加一个共享锁。
[*] • ③找到需要写入数据的目标叶子节点后,先加一个排他锁,开释步骤②上加的所有共享锁。
[*] • ④修改目标叶子节点中的数据后,开释对应叶子节点上的排他锁。
MySQL5.7中悲观写入的执行流程



[*] • ①悲观更新之前首先会对B+Tree加一个共享排他锁。
[*] • ②由于①上已经加了SX锁,因此当前事务执行过程中会阻塞其他实验更改树结构的事务。
[*] • ③遍历查找需要写入数据的目标叶子节点,找到后对其分支加上排他锁,开释①中加的SX锁。
[*] • ④执行SMO操纵,也就是执行悲观写入操纵,完成后开释步骤③中在分支上加的排他锁。
如果需要修改多个数据时,会在遍历查找的过程中,记载下所有要修改的目标节点。
MySQL5.7中并发事务辩论分析

观察上述讲到的三种执行环境,对于读操纵、乐观写入操纵而言,并不会加SX锁,共享排他锁仅针对于悲观写入操纵会加,由于读操纵、乐观写入执行前对整颗树加的是S锁,因此悲观写入时加的SX锁并不会阻塞乐观写入和读操纵,但当另一个事务实验执行SMO操纵变动树结构时,也需要先对树加上一个SX锁,这时两个悲观写入的并发事务就会出现辩论,新来的事务会被阻塞。
   但是要注意:当第一个事务寻找到要修改的节点后,会对其分支加上X锁,紧接着会开释B+Tree上的SX锁,这时另外一个执行SMO操纵的事务就能获取SX锁啦!
其实从上述中大概得知一点:MySQL5.7版本引入共享排他锁之后,解决了5.6版本发生SMO操纵时阻塞统统读写操纵的题目,这样能够在一定程度上提升了InnoDB表的并发性能。
   末了要注意:虽然一个执行悲观写入的事务,找到了要更新/插入数据的节点后会开释SX锁,但是会对其上级的叶节点(叶分支)加上排他锁,因此正在发生SMO操纵的叶分支,仍旧是会阻塞所有的读写行为!
当一个要读取的数据,位于正在执行SMO操纵的叶分支中时,仍旧会被阻塞。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 记载些Spring+题集(12)