情之所起

说起香港,脑海中闪过的第一个念头就是《重庆森林》——重庆大厦里奔走的林青霞、趴在半山扶梯上窥看编号633住处的王菲和她哼唱的《California Dreamin’》。终于要去见识这个憧憬太久的目的地了啊。

阅读全文 »

概述

jmap命令是一个可以输出所有内存中对象的工具,甚至可以将JVM中的heap,以二进制输出成文本。打印出某个java进程(使用pid)内存内的所有对象的情况(如:产生哪些对象,及其数量)。


命令格式

1
2
3
4
5
6
7
8
 jmap [option] <pid>
(to connect to running process) 连接到正在运行的进程

jmap [option] <executable <core>
(to connect to a core file) 连接到核心文件

jmap [option] [server_id@]<remote server IP or hostname>
(to connect to remote debug server) 连接到远程调试服务

基本命令

输出jvm的heap内容到文件

jmap -dump:live,format=b,file=[outputFileName.hprof] [pid]

使用hprof二进制形式输出jvm的heap内容到文件。live子选项是可选的,只输出活的对象到文件。


打印正等候回收的对象的信息

jmap -finalizerinfo [pid]


打印heap的概要信息

JDK 8: jmap -heap [pid]

JDK 9往后:jhsdb jmap --heap --pid [pid]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
root@PC:~# jhsdb jmap --heap --pid 51266
Attaching to process ID 51266, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 17.0.8.1+1-Ubuntu-0ubuntu122.04

using thread-local object allocation.
Garbage-First (G1) GC with 10 thread(s)

Heap Configuration: #堆配置情况,也就是JVM参数配置的结果[平常说的tomcat配置JVM参数,就是在配置这些]
MinHeapFreeRatio = 40 #最小堆可用比例
MaxHeapFreeRatio = 70 #最大堆可用比例
MaxHeapSize = 4169138176 (3976.0MB) #最大堆空间大小
NewSize = 1363144 (1.2999954223632812MB) #新生代分配大小
MaxNewSize = 2499805184 (2384.0MB) #最大新生代分配大小
OldSize = 5452592 (5.1999969482421875MB) #老年代大小
NewRatio = 2 #新生代比例
SurvivorRatio = 8 #新生代与suvivor的比例
MetaspaceSize = 22020096 (21.0MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 2097152 (2.0MB)

Heap Usage: #堆使用情况【堆内存实际的使用情况】
G1 Heap:
regions = 1988
capacity = 4169138176 (3976.0MB)
used = 38374400 (36.5966796875MB)
free = 4130763776 (3939.4033203125MB)
0.9204396299673038% used
G1 Young Generation:
Eden Space:
regions = 3
capacity = 117440512 (112.0MB)
used = 6291456 (6.0MB)
free = 111149056 (106.0MB)
5.357142857142857% used
Survivor Space:
regions = 1
capacity = 2097152 (2.0MB)
used = 2097152 (2.0MB)
free = 0 (0.0MB)
100.0% used
G1 Old Generation:
regions = 16
capacity = 71303168 (68.0MB)
used = 29985792 (28.5966796875MB)
free = 41317376 (39.4033203125MB)
42.05394071691177% used

打印每个class的实例数目、内存占用、类名信息

jmap -histo:live [pid]

OOM的原因

一次性申请过多的对象

更改申请对象的数量。(分页)


内存资源耗尽 未释放

找到未释放的对象进行释放。


本身资源不够

查看堆信息:

JDK 8: jmap -heap [pid]

JDK 11往后:jhsdb jmap --heap --pid [pid]

image-20231024141116327

通过dump定位

系统已经OOM挂掉了

  1. 提前设置-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=[指定的存放路径]

    需要保证服务器的硬盘空间够大,因为该设置会记录系统在整个运行过程中所有的对象信息,占用空间。

    发生OOM后,会生成java_pidxxxxx.hprof的heap dump文件。

  2. 使用JProfiler进行查看

    image-20231024162807712
  3. 找到与业务相关的类 - 右键,使用选定对象

    image-20231024163513439
  4. 选择 传入引用,确定。

    image-20231024163605325
  5. 显示到GC根(GC Root)的路径,确定

    image-20231024163702041
  6. 根据显示的线程堆栈,查看代码进行分析。

    image-20231024163834286

系统运行中,还未OOM

方式一 导出dump文件

jmap -dump:format=b,file=[outputFileName.hprof] [pid]

在系统运行阶段导出dump文件,会造成一次FullGC、STW(Stop The World,所有线程中断)。但不导出dump文件的话,时间成本要增高很多


方式二 Arthas

简介

消息队列提供一个异步通信机制,消息的发送者不必一直等待到消息被成功处理才返回,而是立即返回。消息中间件负责处理网络通信,如果网络连接不可用,消息被暂存于队列当中,当网络畅通时,将消息转发给相应的应用程序或者服务。

如果在商品服务和订单服务之间使用消息中间件,既可以提高并发量,又降低服务之间的耦合度。

RabbitMQ是一个开源的消息代理的队列服务器,用来通过普通协议在完全不同的应用之间共享数据


Docker下相关命令

  • 下载

    1
    2
    # 要选择带management的版本,带管理页面
    docker pull rabbitmq:3.12-management
  • 创建用户

    1
    2
    3
    4
    rabbitmqctl add_user [username] [password]

    # 赋予管理员权限
    rabbitmqctl set_user_tags [username] administrator

架构

image-20231018100910521


交换机 exchange

发布订阅模式 fanout

发布一次,消费多个。

下载

Apache JMeter - Download Apache JMeter

选择二进制文件包。


Windows下

windows下,解压后运行”\apache-jmeter-5.6.2\bin\jmeter.bat”。

Options- choose language - 选择中文。


image-20231012172118257 image-20231012172258211

image-20231012172436339

image-20231012172550539


Linux下

解压后运行apache-jmeter-5.6.2/bin/jmeter.sh -n -t first.jmx -l result.jtl

  • -n 命令行下执行
  • -t 要运行的测试脚本文件
  • -l 记录结果的文件

查看result.jtl文件,要将文件拷出到windows,在监听器 - 聚合报告 下,打开文件浏览

image-20231013013107361

配置不同用户进行测试

image-20231013121242660

cookie管理器中,通过${}获取CSV中设置的变量值

image-20231013121417139

背景

以往的Java Web开发中,异常处理通常是通过try-catch语句块来实现。这种方法在应用程序规模较小的情况下还可以,但是在大型应用中,可能存在大量的代码重复和不一致问题。此外,在抛出未处理的异常时,用户会看到系统生成的默认错误页面,用户体验差。


全局异常统一处理

优点

  • 有助于保持代码整洁和规模化

    如果没有全局异常处理,每个Controller的方法都需要实现自己的异常处理,当程序变得越来越复杂时,这种代码会导致代码冗余和混乱的异常处理逻辑。

  • 提升用户体验

    全局异常处理允许应用程序捕获未处理的异常,并提供更友好的异常提示信息

  • 便于日志记录和监控

    全局异常处理可以帮助应用程序捕获和记录异常信息,在出现问题时快速定位。此外,还可以和监控系统继承,以实时跟踪应用程序中出现的异常情况。

  • 增强安全性

    全局异常处理可以防止应用程序出现潜在的安全漏洞,例如SQL注入和XSS攻击。


实操

1
2
3
4
5
<!--  web组件   -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

自定义一个异常类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GlobalException extends RuntimeException {
private RespBeanEnum respBeanEnum;

public RespBeanEnum getRespBeanEnum() {
return respBeanEnum;
}

public void setRespBeanEnum(RespBeanEnum respBeanEnum) {
this.respBeanEnum = respBeanEnum;
}
}

编写统一异常处理类,统一捕获处理返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestControllerAdvice // 表明统一处理异常
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class) // 希望捕获的异常类
public RespBean exceptionHandler(Exception e) {
if (e instanceof GlobalException) {
GlobalException ex = (GlobalException) e;
return RespBean.error(ex.getRespBeanEnum());
} else if (e instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException ex = (MethodArgumentNotValidException) e;
RespBean respBean = RespBean.error(RespBeanEnum.BIND_ERROR);
respBean.setMessage("参数校验异常:" + ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());
return respBean;
}
return RespBean.error(RespBeanEnum.ERROR);
}
}

优化慢SQL

慢查询日志记录了执行时间超过long_query_time(默认10s通常设置为1s)的所有查询语句,在解决SQL慢查询问题时经常会用到。

  • 查询慢查询日志是否开启 (默认关闭)

    1
    show variables like "slow_query_log;

    开启慢查询日志

    1
    SET GLOBAL slow_query_log=ON;
  • 查看慢查询的超时时间

    1
    show variables like "%long_query_time%";

    修改long_query_time参数:

    1
    SET GLOBAL long_query_time=1;
  • 查询当前慢查询语句的个数

    1
    show global status like "%Slow_queries%";
  • 查询慢查询日志存放位置

    1
    SHOW VARIABLES LIKE "slow_query_log_file";
  • 无论是否超时,未被索引的记录也被记录

    1
    SET GLOBAL log_queries_not_using_indexes = "ON";
  • 慢查询仅记录扫描行数>此参数的SQL

    1
    SET SESSION min_examined_row_limit = 100;

设置完成后,可以用SHOW VARIABLES LIKE "slow%";命令查看。


故意执行一条慢查询并查看日志

1
2
3
4
# 故意在百万数据量的表中执行一条 未使用索引的排序语句
SELECT score, name
FROM cus_order
ORDER BY score DESC;

查看日志路径,并去实际查看日志记录

1
SHOW VARIABLES LIKE "slow_query_log_file";
1
2
3
4
5
6
7
8
# Time: 2023-09-25T15:30:40.620921Z
# User@Host: root[root] @ [172.17.0.1] Id: 18
# Query_time: 0.637583 Lock_time: 0.000002 Rows_sent: 1002000 Rows_examined: 2004000
SET timestamp=1695655839;
# 故意在百万数据量的表中执行一条 未使用索引的排序语句
SELECT score, name
FROM cus_order
ORDER BY score DESC;
  • Query_time 这段代码的运行时长。
  • Lock_time 执行这段代码时,锁定了多久。
  • Rows_sent 慢查询返回的记录数量。
  • Rows_examined 慢查询扫描过的行数。

实际项目中,慢查询日志通常会比较复杂,需要借助一些工具对其进行分析。比如MySQL内置的**mysqldumpslow工具**,可以将相同的SQL归为一类,并统计出归类项的执行次数和每次执行的耗时等一系列对应的情况。

执行计划:一条语句在经过MySQL查询优化器的优化后,具体的执行方式。执行计划通常用于SQL性能分析、优化等场景。

通过EXPAIN命令,可以获取执行计划的相关信息。可以了解到如数据表的查询顺序、数据查询操作的操作类型、哪些索引可以被命中、哪些索引实际会命中、每个数据表有多少行记录被查询等信息。

找到了慢SQL后,可以通过EXPLAIN命令分析对应的语句。

1
2
3
EXPLAIN SELECT score, name
FROM cus_order
ORDER BY score DESC;

EXPLAIN命令分析对应的语句的结果

  • select_type 查询的类型
    • SIMPLE: 普通查询(即没有联合查询、子查询)
    • PRIMARY:主查询
    • UNION (UNION中后面的查询)
    • SUBQUERY
  • table 查询涉及的表或衍生表
  • type 执行方式,判断查询是否高效的重要参考指标,结果值从差到好:
    1. ALL
    2. index
    3. range~index_range
    4. ref
    5. eq_ref
    6. const
    7. system
  • rows SQL要查找到结果集需要扫描读取的数据行数,原则上rows越少越好。

避免使用 SELECT *

  • 需要解析更多的对象、字段、权限、属性等相关内容,会消耗更多的CPU
  • 无用字段增加网络带宽资源消耗、增加数据传输时间,尤其是大字段(varchar、blob、text)
  • 无法使用 MySQL优化器覆盖索引 的优化

分页优化

1
2
3
4
SELECT score, `name`
FROM cus_order
ORDER BY score DESC
LIMIT 1000000, 10;

使用延迟关联来优化这个分页查询语句:

1
2
3
4
5
6
SELECT score, name
FROM cus_order AS a, (SELECT id
FROM cus_order
ORDER BY score DESC
LIMIT 1000000, 10) AS b
WHERE a.id = b.id;
  1. 先提取主键
  2. 再将这个主键表与原数据表关联

尽量避免多表做join

join的效率比较低,主要是因为使用嵌套循环(Nested Loop)来实现关联查询,效率都不是很高。实际业务场景避免多表join常见的作法有2种

  1. 单表查询后,在内存中自己做关联

    对数据库做单表查询,再根据查询结进行二次查询。以此类推,最后再进行关联。

    • 拆分后的单表查询代码可复用性更高
    • 单表查询更利于后续的维护
  2. 数据冗余

    把一些重要的数据在表中做冗余,尽可能避免关联查询。很笨的一种做法,表结构比较稳定的清苦那个下才会考虑。


建议不要使用外键与级联

不建议使用外键的主要原因是对分库分表不友好,性能方面影响较小。


选择合适的字段类型

  1. 某些字符串可以转换成数字类型存储,比如IP地址

    数字是连续的,性能更好,占用空间叶更小。

    MySQL提供了两个方法来处理IP地址:

    1. INET_ATON()把ip转为无符号整型(4-8位)
    2. INET_NTOA()把整型的ip转为地址
  2. 对于非负的数据(自增ID、整型IP、年龄)来说,要优先使用无符号整型来存储

    无符号(SIGNED INT)相对于有符号(UNSIGNED INT)多出了一倍的存储空间

  3. 小数值类型(年龄、状态表示如0/1)优先使用TINYINT类型

  4. 日期类型一定不要用字符串存储,可以考虑DATETIMETIMESTAMP和数值型时间戳

    类型 存储空间 日期格式 日期范围 是否携带时区
    DATETIME 5-8字节 YYYY-MM-DD hh:mm:ss[.fraction] 1000-01-01 00:00:00[.000000] ~ 9999-12-31 23:59:59[.999999]
    TIMESTAMP 4-7字节 YYYY-MM-DD hh:mm:ss[.fraction] 1970-01-01 00:00:01[.000000] ~2038-01-19 03:14:07[.999999]
    数值型时间戳 4字节 全数字 1970-01-01 00:00:01之后的时间
  5. 金额字段用decimal,避免精度丢失

    Java中,对应BigDecimal

  6. 尽量使用自增id作为主键(分布式场景不建议)

    主键如果是自增id,每次都会将数据加在B+树尾部(本质是双向链表),时间复杂度为O(1)。在写满一个数据页的时候,直接申请另一个新数据页接着写就可以了。

    如果主键是非自增id,为了让新加入数据后,B+树的叶子节点还能保持有序,需要往叶子节点的中间找,查找过程的时间复杂度是O($ \log n$)。如果数据页被写满,需要进行页分裂页分裂操作需要加悲观锁,性能非常低。

    但是像分库分表这类场景就不建议使用自增id作为主键,应该使用分布式id,比如uuid

  7. 不建议使用NULL作为列默认值

    • NULL需要占用空间,空字符串''反而不用占用空间。

    • NULL会影响聚合函数的结果。

      SUMAVG等函数会忽略NULL值,COUNT(列名)也会忽略NULL,但COUNT(*)会统计所有的记录数。


尽量用UNION ALL 代替UNION

UNION会把两个结果集放在一个临时表中再进行去重操作,更耗时、消耗CPU资源。


批量操作

批量操作能减少请求数据库的次数,提高性能。

1
2
INSERT INTO cus_order(id, score, name)
VALUES (1, 3512, "user1"), (2, 34335, "user2"), (3, 320985, "user3");

Show PROFILE 分析SQL执行性能

SHOW PROFILESHOW PROFILES已经被弃用,未来的MySQL版本中可能会删除,取代它的是Performance Schema。

SHOW PROFILESHOW PROFILES展示SQL语句的资源使用情况,展示的消息包括CPU的使用、CPU上下文切换、IO等待、内存使用等。

  • 查看是否支持profiling

    1
    SELECT @@have_profiling;
  • 查看profiling是否开启

    1
    2
    3
    4
    # 0是关闭,1是开启
    SELECT @@profiling;

    SET @@profiling = 1;
  • SHOW PROFILES展示当前Session下所有SQL语句的简要信息(Query_ID、Duration)

  • SHOW PROFILE展示一个SQL语句的执行耗时细节

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    SHOW PROFILE [type [, type] ...]
    [FOR QUERY n] # 展示Query_ID为n的执行情况,默认展示最新的一次SQL执行情况
    [LIMIT row_count [OFFSET offset]]

    type: { # 具体某类资源的消耗情况
    ALL
    BLOCK IO
    CONTEXT SWITCHES
    CPU
    IPC
    MEMORY
    PAGE FAULTS
    SOURCE
    SWAPS
    }

正确使用索引

正确的索引可以大大加快数据的检索速度。

选择合适的字段创建索引

  1. 不为NULL的字段

  2. 被频繁查询的字段

  3. 被作为条件查询的字段

  4. 频繁需要排序的字段(索引已经排序

  5. 被频繁用于连接的字段

    提高多表连接查询的效率。


被频繁更新的字段应该慎重建立索引

维护索引的成本不小。


尽可能考虑建立 联合索引 而不是单列索引

可以简单理解为每个索引都对着一棵B+树。如果一个表的字段过多,索引过多,当表数据达到一个体量之后,索引占用的空间也会很多,且修改索引的耗时也会很多。如果是联合索引,会节约很大的磁盘空间,且修改数据的效率也会提升。


避免冗余索引

冗余索引指的是索引功能相同,**能够命中索引(a,b)就肯定能命中索引(a)**。在大多数情况下,都应该尽量扩展已有的索引,而不是创建新索引。


考虑在 字符串类型 的字段上使用 前缀索引 代替普通索引


避免索引失效

索引失效也是慢查询的主要原因之一。常见的导致索引失效的情况:

  1. SELECT *
  2. 创建了组合索引,但查询条件未遵守最左匹配原则 (查询中未使用了该索引的第一列
  3. 在索引列上进行计算、函数、类型转换等操作
  4. %开头的LIKE查询
  5. 查询条件中使用OR,且OR的前后条件中有一个列没有索引,涉及的索引都不会被使用到
  6. 发生隐式转换 (操作符左右两边的数据类型不一致时,左边为字符类型时发生转换会导致索引失效,效率极低

删除长期未使用的索引

可以通过查询sys库的schema_unused_indexes视图来查询哪些索引从未被使用。

概述

MySQL支持多种存储引擎,可以通过SHOW ENGINES命令查看MySQL所支持的所有存储引擎。

MySQL 当前默认的存储引擎是 InnoDB。并且,所有的存储引擎中只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务

MySQL 5.5.5 之前MyISAM 是 MySQL 的默认存储引擎。5.5.5 版本之后,InnoDB 是 MySQL 的默认存储引擎。


MySQL存储引擎架构

MySQL存储引擎采用的是插件式架构,支持多种存储引擎。存储引擎是基于表的,不是基于数据库的


MyISAM和InnoDB有什么区别

MySQL 5.5之前,MyISAM引擎是默认存储引擎,但是MyISAM不支持事务和行级锁,且崩溃后无法安全恢复

InnoDB MyISAM
行级 支持 只有表级锁,在并发写的情况下,性能极差
事务 支持(默认使用可重读隔离级别,可以解决幻读问题的发生) 不支持
外键 支持(外键对于维护数据一致性非常有帮助,但是对性能有一定的损耗,因此通常情况下,不建议在实际生产项目中使用外键,而在业务代码中进行约束 不支持
安全恢复 支持(数据库在异常崩溃后,**重启时会保证数据库恢复到崩溃前的状态,这个过程依赖于redo log**) 不支持
索引实现 使用B+树作为索引结构,数据文件本身就是索引文件 使用B+树,索引文件和数据文件分离。
性能 随着CPU核数的增加,InnoDB的读写能力呈线性增长。 读写不能并发,所以处理能力和CPU核数无关。

InnoDB存储引擎对MVCC的实现

MVCC, Multi-Version Concurrency Control 多版本并发控制。MVCC是一种用于多个并发事务同时读写数据库时,保持数据的一致性和隔离性的机制。它通过在每个数据行上维护多个版本的数据来实现。当一个事务要对数据库中的数据进行修改时,MVCC会为该事务创建一个数据快照,而不是直接修改实际的数据行

概述

MySQL中常见的日志类型主要有下面几类(针对InnoDB存储引擎):

  1. 慢查询日志 (slow query log)
  2. 二进制日志 (binlog)
  3. 事务日志
    • 重做日志 (redo log)
    • 撤销日志 (undo log)
阅读全文 »
0%