Qida's Blog

纸上得来终觉浅,绝知此事要躬行。

排序流程

四种排序情况的流程(参考《极客时间》专栏):

order_by_process

order_by_process

总结

排序总结:

order_by

外部排序总结:

  • MySQL 会给每个线程分配一块内存用于排序,称为 sort_buffer。对 sort_buffer 中的数据按照排序字段做快速排序
  • 排序可能在内存中完成,也可能需要使用磁盘排序,取决于排序所需的内存和参数 sort_buffer_size
  • 内存放不下时,使用外部磁盘排序,外部磁盘排序一般使用归并排序算法。可以简单理解,MySQL 将需要排序的数据分成 N 份,每一份单独排序后存在这些临时文件中。然后把这 N 个有序文件再合并成一个有序的大文件。

优化方式:

  • WHEREORDER BY 子句用到的字段,添加联合索引(注意字段顺序);
  • 如果 GROUP BY 结果无需排序,可以加上 ORDER BY NULL

参考

https://dev.mysql.com/doc/refman/5.7/en/order-by-optimization.html

索引优化应该是对查询性能优化最有效的手段了。索引能够轻易将查询性能提高几个数量级,“最优”的索引有时比一个“好的”索引性能要好两个数量级。

索引是存储引擎用于快速找到记录的一种数据结构。在 MySQL 中,索引是在存储引擎层而不是服务器层实现的(如下图)。所以,并没有统一的索引标准:不同存储引擎的索引的工作方式并不一样,也不是所有的存储引擎都支持所有类型的索引。即使多个存储引擎支持同一种类型的索引,其底层的实现也可能不同。

MySQL Architecture

常见索引类型

MySQL 支持的常见索引类型:

  • B+Tree 索引
  • Hash index(哈希索引)
  • R-Tree index(空间数据索引)
  • Full-text index(全文索引)
  • ……

本文只探讨最常用的 B-Tree 索引。

B+Tree 数据结构

B+Tree 索引特性

当我们谈论索引的时候,如果没有特别指明类型,那多半说的是 B+Tree 索引。InnoDB 存储引擎默认使用的也是 B+Tree 数据结构来存储数据。
索引可以包含一个或多个列的值。如果包含多个列(称为“联合索引”),那么列的顺序至关重要,因为 MySQL 只能高效地使用索引的 最左前缀列。创建一个包含两个列的索引,和创建两个只包含一列的索引是大不相同的,下面看个例子:

1
2
3
4
5
6
7
CREATE TABLE People (
last_name varchar(50) not null,
first_name varchar(50) not null,
dob date not null,
gender enum('m', 'f') not null,
key(last_name, first_name, dob)
);

可以使用该 B-Tree 索引的查询类型:

全值匹配

全值匹配(Match the full value):

1
2
3
SELECT * 
FROM people
WHERE last_name = 'Wu' AND first_name = 'Qida' AND dob = '2018-01-01';

匹配最左前缀

匹配最左前缀(Match a leftmost prefix):

1
2
3
4
5
6
7
SELECT * 
FROM people
WHERE last_name = 'Wu';

SELECT *
FROM people
WHERE last_name = 'Wu' AND first_name = 'Qida';

匹配列前缀

匹配列前缀(Match a column prefix):

1
2
3
SELECT * 
FROM people
WHERE last_name LIKE 'W%';

注意 MySQL LIKE 的限制:

MySQL can’t perform the LIKE operation in the index. This is a limitation of the low-level storage engine API, which in MySQL 5.5 and earlier allows only simple comparisons (such as equality, inequality, and greater-than) in index operations.

MySQL can perform prefix-match LIKE patterns in the index because it can convert them to simple comparisons, but the leading wildcard in the query makes it impossible for the storage engine to evaluate the match. Thus, the MySQL server itself will have to fetch and match on the row’s values, not the index’s values.

匹配范围值

匹配范围值(Match a range of values):

1
2
3
SELECT * 
FROM people
WHERE last_name BETWEEN 'Wu' AND 'Li';

精确匹配某一列,并范围匹配另外一列

精确匹配某一列,并范围匹配另外一列(Match one part exactly and match a range on another part):

1
2
3
SELECT * 
FROM people
WHERE last_name = 'Wu' And first_name LIKE 'Q%';

覆盖索引

只访问索引列的查询(Index-only queries):

1
2
3
SELECT last_name, first_name, dob
FROM people
WHERE last_name = 'Wu' AND first_name = 'Qida' AND dob = '2018-01-01';

高性能的索引策略

选择合适的索引列顺序

联合索引列按区分度从高到低排列。例如 idx(ctime_status) 而不是 idx(status_ctime)

使用独立的列

“独立的列”是指不在索引列上做任何操作,包括:

例如,下面这个查询无法使用 actor_id 列的索引:

1
2
3
4
5
6
7
8
9
-- 错误示范
SELECT *
FROM people
WHERE id + 1 = 5;

-- 正确示范
SELECT *
FROM people
WHERE id = 4;

凭肉眼很容易看出 WHERE 中的表达式其实等价于 id = 4 ,但是 MySQL 无法自动解析这个方程式。这完全是用户行为。我们应该养成简化 WHERE 条件的习惯,始终将索引列单独放在比较符号的一侧。

下面是另一个常见的错误,将索引列作为函数的参数:

1
2
3
4
5
6
7
-- 错误示范
SELECT ...
WHERE DATE(create_time) = '2000-01-01';

-- 正确示范
SELECT ...
WHERE create_time BETWEEN '2000-01-01 00:00:00' AND '2000-01-01 23:59:59';

另一个常见错误,merchant_no 为 VARCHAR 类型且加了索引,但由于隐式类型转换为数字类型,导致全表扫描:

1
2
3
4
5
6
7
8
9
-- 错误示范
SELECT *
FROM t_order
WHERE merchant_no = 2016;

-- 正确示范
SELECT *
FROM t_order
WHERE merchant_no = '2016';

字符串索引优化

常规方式

  • 直接创建完整索引
    • 优点:可以使用覆盖索引
    • 缺点:比较占用空间
  • 创建前缀索引
    • 优点:节省空间
    • 缺点:
      • 需要计算好区分度,以定义合适的索引长度,否则会增加回表次数
      • 无法使用覆盖索引

区分度计算方法:

1
2
3
4
5
6
select 
count(distinct left(email,4)) / count(distinct email) as L4,
count(distinct left(email,5)) / count(distinct email) as L5,
count(distinct left(email,6)) / count(distinct email) as L6,
count(distinct left(email,7)) / count(distinct email) as L7,
from t1;

其它方式一

  • 倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题。查询时使用 reverse 函数。
  • 创建额外的 hash 字段并创建索引,查询性能稳定(散列冲突的概率更小),有额外的存储和计算消耗,查询时使用 crc32 函数并二次比较。

这两种方式都不支持范围扫描,只支持等值查询。

其它方式二

改为使用更合适的数据类型,例如:

  • 使用日期与时间类型,而不是字符串来存储日期和时间。
  • 使用整型,而不是字符串来存储 IP 地址。
  • 使用定长二进制类型(如 binary),而不是字符串来存储散列值。

索引选择性

了解两个概念:

  • 基数(Cardinality)也称为区分度,是指数据列所包含的不同值的数量。例如,某个数据列包含值:1、2、3、4、5、1,则基数为 5。可以通过 show index 查看。
  • 索引选择性(Index Selectivity)是指基数(Cardinality)和数据表的记录总数(#T)的比值,范围从 1/#T1 之间。

索引的选择性越高则查询效率越高,因为选择性高的索引可以让 MySQL 在查找时过滤掉更多的行。唯一索引的选择性是 1,这是最好的索引选择性,性能也是最好的。

下面显示如何计算某列的平均选择性

1
2
3
4
SELECT COUNT(DISTINCT last_name) / COUNT(*) AS Selectivity
FROM people;

Selectivity: 0.0312

只看平均选择性有时是不够的,需考虑最坏或特殊情况下的选择性(即值的分布,是否有某些值占比过多?)。

如果索引的选择性低(基数/总数的比值),可能会导致优化器生成执行计划时,不走这个索引。

有时 MySQL 会选错索引,解决方案如下:

  • 通过 show index 语句查看索引的“基数”。对于由于索引统计信息不准确导致的问题,可以用 analyze table 来重新统计索引信息。
  • force index 强行选择一个索引。
  • 修改语句,引导 MySQL 使用我们期望的索引。
  • 新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引。

三星索引评价系统

评估某个索引是否适合某个查询的“三星评价系统”(three-star system):

Lahdenmaki and Leach’s book also introduces a three-star system for grading how suitable an index is for a query:

  • The index earns one star if it places relevant rows adjacent to each other,
  • a second star if its rows are sorted in the order the query needs,
  • and a final star if it contains all the columns needed for the query.
  • 一星:索引列满足查询所需的条件。如果是多个查询条件,则利用联合索引及其最左前缀匹配特性。
  • 二星:索引行排序符合查询所需的排序,没有额外的 ORDER BY(避免 filesort)。
  • 三星:索引列满足查询所需的全部列,不再需要回表查询(即利用覆盖索引 covering index)。

相关命令

下面介绍一些查看索引的常用命令:

DESC

DESC 命令查看表结构时,可以看到索引列 Key ,共有三种类型:

  • PRI 表示主键索引(PRIMARY KEY)。
  • UNI 表示唯一索引(UNIQUE KEY),值不能重复。
  • MUL 表示普通索引(MULTIPLE KEY) ,值可重复。
1
2
3
4
5
6
7
8
9
10
11
$ DESC table_name;

+---------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------+--------------+------+-----+---------+-------+
| FID | int(11) | NO | PRI | NULL | |
| FKEY | varchar(50) | NO | UNI | NULL | |
| FVALUE | varchar(500) | YES | MUL | NULL | |
| FDESC | varchar(50) | YES | MUL | NULL | |
| FCACHED | int(1) | NO | | 1 | |
+---------+--------------+------+-----+---------+-------+

SHOW INDEX

SHOW INDEX 可以以列为单位,查看该表索引的具体信息,例如:

  • 表名 Table
  • 索引唯一性 Non_unique
  • 索引名 Key_name
  • 联合索引中的顺序 Seq_in_index
  • 列名 Column_name
  • 基数 Cardinality
  • 索引类型 Index_type
1
2
3
4
5
6
7
8
9
10
$ SHOW INDEX FROM table_name;

+------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| table_name | 0 | PRIMARY | 1 | FID | A | 105 | NULL | NULL | | BTREE | | |
| table_name | 0 | UK_FKEY | 1 | FKEY | A | 105 | NULL | NULL | | BTREE | | |
| table_name | 1 | IDX_V_D | 1 | FVALUE | A | 105 | NULL | NULL | | BTREE | | |
| table_name | 1 | IDX_V_D | 2 | FDESC | A | 105 | NULL | NULL | | BTREE | | |
+------------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+

SHOW CREATE TABLE

SHOW CREATE TABLE 可以查看该表的建表语句,留意最后几行:

1
2
3
4
5
6
7
8
9
10
11
12
SHOW CREATE TABLE table_name;

CREATE TABLE `table_name` (
`FID` int(11) NOT NULL,
`FKEY` varchar(50) NOT NULL,
`FVALUE` varchar(200) NOT NULL,
`FDESC` varchar(50) DEFAULT NULL,
`FCACHED` int(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`FID`),
UNIQUE KEY `UK_FKEY` (`FKEY`),
KEY `IDX_V_D` (`FVALUE`, `FDESC`)
)

参考

《高性能 MySQL》

https://dev.mysql.com/doc/refman/5.7/en/innodb-indexes.html

https://dev.mysql.com/doc/refman/5.7/en/optimization-indexes.html

https://dev.mysql.com/doc/refman/5.7/en/show-columns.html

MySQL索引背后的数据结构及算法原理

慢 SQL 治理经典案例分享 | 阿里技术

EXPLAIN 语句提供有关 MySQL 优化器如何执行语句的信息。能够用于 SELECTDELETEINSERTREPLACEUPDATE 语句。

EXPLAINSELECT 语句中使用到的每张表输出一行信息 。它按照 MySQL 在处理 SELECT 语句时的读取顺序来列出各张表。

MySQL 使用嵌套循环连接算法(NLJ)来解析所有的表连接(MySQL resolves all joins using a nested-loop join method)。详见另一篇。

EXPLAIN 输出列如下:

Column JSON Name Meaning
id SELECT 标识符 The SELECT identifier
select_type SELECT 类型 The SELECT type
table 引用的表名 The table for the output row
partitions 匹配的分区 The matching partitions
type 连接类型 The join type
possible_keys 可选的索引 The possible indexes to choose
key 实际选择的索引 The index actually chosen
key_len 实际所选 key 的长度 The length of the chosen key
ref 与索引比较的列 The columns compared to the index
rows 扫描行数 Estimate of rows to be examined
filtered 按表条件过滤的行百分比 Percentage of rows filtered by table condition
Extra 附加信息 Additional information

id

id 列的编号是 SELECT 的序列号,有几个 SELECT 就有几个 idid 值越大执行优先级越高,id 值相同则从上往下执行,id 值为 NULL 则最后执行。

select_type

表示查询类型是简单查询还是复杂查询。常见 SELECT 类型如下:

select_type Value Meaning
SIMPLE Simple SELECT (not using UNION or subqueries)
PRIMARY Outermost SELECT
UNION Second or later SELECT statement in a UNION
UNION RESULT Result of a UNION
SUBQUERY First SELECT in subquery
DERIVED Derived table
MATERIALIZED Materialized subquery

table

表示输出行所引用的表名,特殊情况如下:

  • <union*M*,*N*>:该行指的是 id 值为 MN 的并集。当有 UNION 时,UNION RESULTtable 列的值为 <union*M*,*N*>,表示参与并集的 id 查询编号为 MN
  • <derived*N*>:当 FROM 子句中有子查询时, table 列为 <derived*N*>,表示当前查询依赖于 id=N 的查询结果,于是先执行 id=N 的查询。
  • <subquery*N*>

type

单表查询的性能对比:const > ref > range > index > ALL。一般来说,得保证查询达到 range 级别,最好达到 ref 级别。

system

该表只有一行。是 const 连接类型的特例。

const

该表最多只有一个匹配行,该行在查询开始时读取。因为只有一行,所以优化器的其余部分可以将这一行中列的值视为常量。const 表非常快,因为它们只能读取一次。

当主键索引(PRIMARY KEY )或唯一索引(UNIQUE KEY)与常量值比较时使用 const 类型。如下:

1
2
3
4
5
6
7
8
9
10
11
SELECT * FROM tbl_name WHERE primary_key = 1;

SELECT * FROM tbl_name WHERE primary_key_part1 = 1 AND primary_key_part2 = 2;

SELECT * FROM tbl_name WHERE unique_key = '001';

SELECT * FROM tbl_name WHERE unique_key_part1 = '001' AND unique_key_part2 = '002';

SELECT * FROM ref_table,other_table
WHERE ref_table.unique_key_column = other_table.unique_key_column
AND other_table.unique_key_column = '001';

eq_ref

对于 other_table 中的每行,仅从 ref_table 中读取唯一一行。eq_ref 类型用于主键索引(PRIMARY KEY )或 NOT NULL 的唯一索引(UNIQUE KEY),且索引被表连接所使用时。除了 systemconst 类型之外,这是最好的连接类型。select_type=SIMPLE 简单查询类型不会出现这种类型。

例子:

1
2
3
4
5
6
SELECT * FROM ref_table,other_table
WHERE ref_table.unique_key_column = other_table.column;

SELECT * FROM ref_table,other_table
WHERE ref_table.unique_key_column_part1 = other_table.column
AND ref_table.unique_key_column_part2 = 1;

ref

对于 other_table 中的每行,从 ref_table 中读取所有匹配行。ref 类型用于普通索引或联合索引的最左前缀列(leftmost prefix of the key),即无法根据键值查询到唯一一行。如果使用的索引仅匹配几行结果,则也是一种很好的连接类型。

例子:

1
2
3
4
5
6
7
8
9
10
SELECT * FROM ref_table WHERE key_column = expr;

SELECT * FROM ref_table WHERE key_column_part1 = expr;

SELECT * FROM ref_table,other_table
WHERE ref_table.key_column = other_table.column;

SELECT * FROM ref_table,other_table
WHERE ref_table.key_column_part1 = other_table.column
AND ref_table.key_column_part2 = 1;

range

使用索引进行范围查询时,例如:=, <>, >, >=, <, <=, <=>, IS NULL, BETWEEN, LIKE, IN()

1
2
3
4
5
6
7
8
9
10
11
SELECT * FROM tbl_name
WHERE key_column = 10;

SELECT * FROM tbl_name
WHERE key_column BETWEEN 10 and 20;

SELECT * FROM tbl_name
WHERE key_column IN (10,20,30);

SELECT * FROM tbl_name
WHERE key_part1 = 10 AND key_part2 IN (10,20,30);

index

索引扫描,类似于 ALL 全表扫描。以下情况发生:

  • 覆盖索引(covering index)。此时 Extra 列显示 Using index。覆盖索引扫描通常比全表扫描速度更快,因为其存储空间更小。例子:

    1
    2
    3
    4
    5
    6
    7
    SELECT primary_key FROM tbl_name;

    SELECT unique_key FROM tbl_name;

    SELECT COUNT(primary_key) FROM tbl_name;

    SELECT COUNT(unique_key) FROM tbl_name;

ALL

全表扫描。此时必须增加索引优化查询。

全表扫描发生的情况如下:

  • 小表,此时全表扫描比二级索引扫描再回表的速度要快;
  • ONWHERE 子句没有可用的索引;
  • 查询的字段虽然使用了索引,但查询条件覆盖的范围太大以至于还不如全表扫描。优化方式详见:Section 8.2.1.1, “WHERE Clause Optimization”
  • 使用了区分度(cardinality)低的索引,索引扫描范围太大以至于还不如全表扫描。如果是统计不准,可以用 ANALYZE TABLE 语句优化:Section 13.7.2.1, “ANALYZE TABLE Syntax”

possible_keys

表示 MySQL 可选的索引。

如果此列为 NULL,表示 MySQL 没有可选的索引。此时,可以检查 WHERE 子句是否引用了某些适合建立索引的列,建立索引以提升查询性能。

key

表示 MySQL 实际选择的索引。

  • 如果此列为 NULL,表示 MySQL 没有找到可用于提高查询性能的索引。
  • 如果 possible_keys NOT NULL,但 key NULL,可能是因为表中数据不多,MySQL 认为索引对此查询帮助不大,选择了全表扫描。

如需强制 MySQL 使用或忽略 possible_keys 中列出的索引,可以在查询中使用 FORCE INDEXUSE INDEXIGNORE INDEX。详见:索引提示

key_len

表示 MySQL 实际选择的索引长度,单位为 Byte。如果该索引为联合索引,可用于判断 MySQL 实际使用了联合索引中的多少个字段。如果 key 列为 NULLkey_len 列也为 NULL

key_len 计算规则如下:

  • 使用 NULL 需要额外增加 1 Byte 记录是否为 NULL。并且进行比较和计算时要对 NULL 值做特别的处理,因此尽可能把所有列定义为 NOT NULL

  • 各个类型:

    • 整数类型
      • TINYINT 1 Byte
      • SMALLINT 2 Bytes
      • MEDIUMINT 3 Bytes
      • INT 4 Bytes
      • BIGINT 8 Bytes
    • 日期与时间类型
      • DATE 3 Bytes
      • TIMESTAMP 4 Bytes
      • DATETIME 8 Bytes
    • 字符串类型,实际字节存储长度取决于使用的字符集
      • 字符集(Character encoding) M L
        latin1 1 Char 1 Byte
        gbk 1 Char 2 Bytes
        utf8 1 Char 3 Bytes
        utf8mb4 1 Char 4 Bytes
      • CHAR(M):如果字符集为 utf8,则长度为 3 * M Bytes

      • VARCHAR(M):如果字符集为 utf8,则长度为 3 * M Bytes + 1 or 2 Bytes。额外 1 or 2 Byte(s) 用于存储长度。

  • 创建索引的时候可以指定索引的长度,例如:alter table test add index idx_username (username(30));。长度 30 指的是字符的个数。

  • InnoDB 索引最大长度为 767 Bytes,引自官方文档

    key_part:
    col_name [(length)] [ASC | DESC]

    index_type:
    USING {BTREE | HASH}

    Prefixes, defined by the length attribute, can be up to 767 bytes long for InnoDB tables or 3072 bytes if the innodb_large_prefix option is enabled. For MyISAM tables, the prefix length limit is 1000 bytes.

举个例子,在字符集为 utf8 的情况下,n 最大只能为 (767 - 2 (存储长度)) / 3 = 765 / 3 = 255 个字符。因此当字符串过长时,MySQL 最多会将开头 255 个字符串截取出来作为索引。一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
CREATE TABLE `student` (
`id` int(11) NOT NULL,
`username` varchar(256) DEFAULT NULL,
`password` varchar(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_username` (`username`(255)) USING BTREE,
KEY `idx_password` (`password`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- key_len: 255 * 3 + 2 + 1 = 768 Bytes (额外增加 1 Byte 记录是否为 NULL)
mysql> explain select username from student where username = 'pete';
+----+-------------+---------+------+---------------+--------------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+------+---------------+--------------+---------+-------+------+-------------+
| 1 | SIMPLE | student | ref | idx_username | idx_username | 768 | const | 1 | Using where |
+----+-------------+---------+------+---------------+--------------+---------+-------+------+-------------+

-- key_len: 1 * 3 + 2 + 1 = 6 Bytes
mysql> explain select password from student where password = 'pete';
+----+-------------+---------+------+---------------+--------------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+---------+------+---------------+--------------+---------+-------+------+--------------------------+
| 1 | SIMPLE | student | ref | idx_password | idx_password | 6 | const | 1 | Using where; Using index |
+----+-------------+---------+------+---------------+--------------+---------+-------+------+--------------------------+

如果使用过长的索引,例如修改了字符串编码类型、增加联合索引列,则报错如下:

1
[Err] 1071 - Specified key was too long; max key length is 767 bytes

ref

ref 显示与 key 列(实际选择的索引)比较的内容,可选值:

  • 列名
  • const:常量值
  • func:值为某些函数的结果
  • NULL:范围查询(type=range

例如联合索引如下,使用三个索引列查询时,执行计划如下(注意 key_lenref):

1
2
3
4
5
6
7
8
9
10
`channel_task_no` varchar(60) NOT NULL,
`reconciliation_code` tinyint(4) unsigned NOT NULL DEFAULT '0',
`reconciliation_status` tinyint(4) unsigned NOT NULL DEFAULT '0',
KEY `idx_taskno_rcode_rstatus` (`channel_task_no`,`reconciliation_code`,`reconciliation_status`) USING BTREE

+----+-------------+-------+------+--------------------------+--------------------------+---------+-------------------+------+-----------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+--------------------------+--------------------------+---------+-------------------+------+-----------------------+
| 1 | SIMPLE | t_xxx | ref | idx_taskno_rcode_rstatus | idx_taskno_rcode_rstatus | 184 | const,const,const | 1 | Using index condition |
+----+-------------+-------+------+--------------------------+--------------------------+---------+-------------------+------+-----------------------+

rows

表示 MySQL 认为执行查询必须扫描的行数。

对于 InnoDB 表,此数字是估计值,可能并不总是准确。

prossible_keys 存在多个可选索引时,优化器会选择一个认为最优的执行方案,以最小的代价去执行语句。其中,这个扫描行数就是影响执行代价的因素之一。扫描的行数越少,意味着访问磁盘数据的 IO 次数越少,消耗的 CPU 资源也越少。

当然,扫描行数并不是唯一的判断标准,优化器还会结合是否使用临时表、是否排序等因素进行综合判断。

所以在实践中,如果你发现 explain 的结果预估的 rows 值跟实际情况差距比较大,可以采用执行 analyze table 重新统计信息。

在数据库的慢查询日志中看到一个 rows_examined 的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。

在有些场景下,执行器调用一次,在引擎内部则扫描了多行,因此引擎扫描行数跟 rows_examined 并不是完全相同的。

Extra

这一列显示的是额外信息。如果想要查询越快越好,需要特别留意 Extra 列是否出现以下情况:

Extra 缓冲区 大小配置 数据结构 备注
Using filesort sort_buffer sort_buffer_size 有序数组 使用了“外部排序”(全字段排序或 rowid 排序)
Using join buffer (Block Nested Loop) join_buffer join_buffer_size 无序数组 使用了“基于块的嵌套循环连接”算法(Block Nested-Loop Join(BNL))
Using temporary 临时表 小于 tmp_table_size 为内存临时表,否则为磁盘临时表(可以使用 SQL_BIG_RESULT 直接指定) 二维表结构(类似于 Map,Key-Value) 如果执行逻辑需要用到二维表特性,就会优先考虑使用临时表。例如:DISTINCTGROUP BYUNION

这三个数据结构都是用来存放 SELECT 语句执行过程中的中间数据,以辅助 SQL 语句的执行的。这些情况通常都能通过索引优化。

各种常见的重要值如下:

Using index

使用了覆盖索引。

Using where

使用 WHERE 条件过滤结果,但查询的列未被索引覆盖。

Using index condition

查询的列不完全被索引覆盖。

例如:索引下推优化(ICP)

EXPLAIN output shows Using index condition in the Extra column when Index Condition Pushdown is used. It does not show Using index because that does not apply when full table rows must be read.

  • ICP is used for the range, ref, eq_ref, and ref_or_null access methods when there is a need to access full table rows.
  • For InnoDB tables, ICP is used only for secondary indexes. The goal of ICP is to reduce the number of full-row reads and thereby reduce I/O operations. For InnoDB clustered indexes, the complete record is already read into the InnoDB buffer. Using ICP in this case does not reduce I/O.

ICP can reduce the number of times the storage engine must access the base table and the number of times the MySQL server must access the storage engine.

查询 ICP 是否开启:SELECT @@GLOBAL.optimizer_switch,注意 index_condition_pushdown 标记:

index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,derived_merge=on

Using temporary

MySQL 需要创建一张临时表来处理查询。通常发生于查询包含 DISTINCTGROUP BYORDER BY 子句等需要数据去重的场景。出现这种情况一般是要进行优化的,首先想到的是用索引进行优化。

Using join buffer

使用 BNL 算法进行表连接。这种情况下一般考虑使用索引对被驱动表的表连接字段进行优化,以使用更高效的 NLJ 算法。

Using filesort

将用外部排序而不是索引排序,数据较少时从内存排序,否则需要在磁盘完成排序。这种情况下一般考虑使用索引进行优化。

优化参考:Section 8.2.1.14, “ORDER BY Optimization”

参考

https://dev.mysql.com/doc/refman/5.7/en/execution-plan-information.html

https://dev.mysql.com/doc/refman/5.7/en/analyze-table.html

MySQL Workbench has a Visual Explain capability that provides a visual representation of EXPLAIN output. See Tutorial: Using Explain to Improve Query Performance.

Monitoring Tools and Commands

Monitoring Tools and Commands

https://www.saashub.com/compare-visualvm-vs-jconsole

命令/工具 全称 作用
jps JVM Process Status Tool You use the jps command to list the instrumented JVMs on the target system.
显示正在运行的所有 HotSpot VM 进程。
jstat JVM Statistics Monitoring Tool You use the jstat command to monitor JVM statistics.
用于监视本地或远程 HotSpot VM 各方面的运行数据,例如类加载/卸载、运行时数据区、GC、JIT。
jconsole You use the jconsole command to start a graphical console to monitor and manage Java applications.
JDK 5 起免费提供。一款基于 JMX (Java Management Extensions) 的可视化监视、管理工具。它的主要功能是通过 JMX 的 MBean (Managed Bean) 对系统进行信息收集和参数动态调整。
VisualVM VisualVM is a visual tool integrating commandline JDK tools and lightweight profiling capabilities.

jps

jps 命令的功能与 ps 类似,用于列出正在运行的 JVM 进程状态。

常用参数:

  • -q 只输出 LVMID,省略主类的名称。
  • -l 输出主类的全名,如果进程执行的是 JAR 包,则输出 JAR 路径。
  • -m 输出虚拟机进程启动时传递给主类 main() 函数的参数。
  • -v 输出虚拟机进程启动时的 JVM 参数。

jstat

jstat 命令用于监视当前 JVM 的各种运行状态信息。在用户体验上也许不如 JMC、VisualVM 等可视化监控工具以图表形式展示那样直观,但在实际生产环境中不一定可以使用 GUI 图形界面,因此在没有 GUI、只提供命令行界面的服务器上,仍是运行期定位虚拟机性能问题的常用工具。

命令格式:

1
$ jstat options vmid [interval[s|ms] [count]]

常用参数:

  • options,要查询的虚拟机信息,主要分为三类:
    • 类加载
      • -class 监视类加载、卸载数量、总空间以及类加载所耗费的时间
    • 运行时数据区、GC
      • -gccapacity 查看 GC 情况和 JVM 各区的容量(字节)
      • -gc 查看 GC 情况和 JVM 各区的容量使用量(字节)
      • -gcutil 查看 GC 情况和 JVM 各区的使用率(%)
    • JIT
      • -compiler 输出即时编译器编译过的方法、耗时等信息
      • -printcompilation 输出已经被即时编译的方法
  • vmid,如果是本地虚拟机进程,VMID 与 LVMID 一致;如果是远程虚拟机进程,则 VMID 的格式为:[protocol:][//]lvmid[@hostname[:port]/servername]
  • interval,间隔时间,单位为秒或者毫秒
  • count,打印次数,如果缺省则打印无数次

示例展示:

此示例连接到 lvmid 21891,并以 250 毫秒的间隔获取 7 个样本,每 6 行显示一次标题([-h<lines>]),并显示由 -gcutil 选项指定的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ jstat -gcutil -h6 21891 250 7

S0 S1 E O P YGC YGCT FGC FGCT GCT

12.44 0.00 27.20 9.49 96.70 78 0.176 5 0.495 0.672

12.44 0.00 62.16 9.49 96.70 78 0.176 5 0.495 0.672

12.44 0.00 83.97 9.49 96.70 78 0.176 5 0.495 0.672

0.00 7.74 0.00 9.51 96.70 79 0.177 5 0.495 0.673

0.00 7.74 23.37 9.51 96.70 79 0.177 5 0.495 0.673

0.00 7.74 43.82 9.51 96.70 79 0.177 5 0.495 0.673

S0 S1 E O P YGC YGCT FGC FGCT GCT

0.00 7.74 58.11 9.51 96.71 79 0.177 5 0.495 0.673

该示例结果显示,对象首先都在 Eden 区中创建,在第 3 和第 4 个样本之间由于 Eden 区装满,发生了 Young GC, gc 耗时 0.001 秒,并将对象从 Eden 区(E)提升到 Old 区(O),导致 Old 区的使用率从 9.49% 增加到 9.51%。

-gcutil 选项每列说明:

列名 描述
S0 Survivor space 0 区占用率
S1 Survivor space 1 区占用率
E Eden space 区占用率
O Old space 区占用率
P Perm space 区占用率
列名 描述
YGC 从应用程序启动到采样时发生 Young GC 的次数,E 区满后触发
YGCT 从应用程序启动到采样时 Young GC 所用的时间(单位秒)
FGC 从应用程序启动到采样时发生 Full GC 的次数, O 区满后触发
FGCT 从应用程序启动到采样时 Full GC 所用的时间(单位秒)
GCT 从应用程序启动到采样时用于垃圾回收的总时间(单位秒)

VisualVM

VisualVM 脱胎于自 JDK 6 起免费提供的 jvisualvm,目前已经从 Oracle JDK 中分离出来,成为一个独立发展的开源项目:https://visualvm.github.io/

功能:

  • 监控进程的 CPU 使用率、内存(运行时数据区)、类加载、线程等统计;

  • 监控线程状态。如下图:

  • VisualVM 拥有丰富的插件扩展。例如:

    • Visual GC

      Integration of the Visual GC tool into VisualVM. Visual GC user interface is displayed for each local or remote application with performance counters available via jvmstat API.

      The Visual GC tool attaches to an instrumented HotSpot JVM and collects and graphically displays garbage collection, class loader, and HotSpot compiler performance data.

    • Threads Inspector

      Threads Inspector adds a new section to the Threads tab showing stack traces for the selected live threads.

    • TDA Plugin

      Thread Dump Analyzer is a GUI for analyzing thread dumps generated by the Java VM.

Troubleshooting Tools and Commands

Troubleshooting Tools and Commands

命令 全称 作用 备注
jinfo Configuration Info for Java You use the jinfo command to generate Java configuration information for a specified Java process.
实时显示或修改虚拟机配置信息。
在 JDK 9 中已集成到 jhsdb
jstack Stack Trace for Java You use the jstack command to print Java stack traces of Java threads for a specified Java process.
显示虚拟机当前时刻的线程快照(thread dump/javacore 文件)。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成堆栈快照的目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂起等,都是导致线程长时间停顿的常见原因。
在 JDK 9 中已集成到 jhsdb
jmap Memory Map for Java You use the jmap command to print details of a specified process.
用于实时生成 JVM 堆内存转储快照(heap dump/hprof 文件),或查看堆内存信息。其它转储方法:
-XX:+HeapDumpOnOutOfMemoryError
-XX:+HeapDumpOnCtrlBreak
在 JDK 9 中已集成到 jhsdb
jhat JVM Heap Dump Browser 用于分析 heap dump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果。分析结果默认以包为单位进行分组显示,分析内存泄漏问题主要会使用到其中的 Heap Histogram(与 jmap -histo 功能一样)与 OQL 页签功能,前者可以找到内存中总容量最大的对象,后者是标准的对象查询语言,使用类似 SQL 的语法堆内存中的对象进行查询统计。 在 JDK 9 中已被 jhsdb 替代
jhsdb Java HotSpot Debugger 一个基于 Serviceability Agent 的 HotSpot 进程调试器。 自 JDK 9 起免费提供

jstack

jstack 命令用于 thread dump(生成虚拟机的线程堆栈快照),根据堆栈信息我们可以定位到具体代码,所以它在 JVM 性能调优中使用得非常多。

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
$ jstack 21090 > /tmp/threaddump
$ less /tmp/localfile

Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode):

"Attach Listener" daemon prio=10 tid=0x00007f67e03b4800 nid=0x7bb9 waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE

"catalina-exec-8000" daemon prio=10 tid=0x00007f67ba4a0000 nid=0x795a waiting on condition [0x00007f6558c0a000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000007886ab360> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2043)
at org.apache.http.pool.PoolEntryFuture.await(PoolEntryFuture.java:139)
at org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking(AbstractConnPool.java:307)
at org.apache.http.pool.AbstractConnPool.access$000(AbstractConnPool.java:65)
at org.apache.http.pool.AbstractConnPool$2.getPoolEntry(AbstractConnPool.java:193)
at org.apache.http.pool.AbstractConnPool$2.getPoolEntry(AbstractConnPool.java:186)
at org.apache.http.pool.PoolEntryFuture.get(PoolEntryFuture.java:108)
at org.apache.http.impl.conn.PoolingClientConnectionManager.leaseConnection(PoolingClientConnectionManager.java:212)
at org.apache.http.impl.conn.PoolingClientConnectionManager$1.getConnection(PoolingClientConnectionManager.java:199)
at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:424)
at org.apache.http.impl.client.AbstractHttpClient.doExecute(AbstractHttpClient.java:884)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:107)
at com.xxx.xxx.xxx.HttpClientService.doPost(HttpClientService.java:103)
......

由于导出的 threaddump 文件非常大,可以先统计下所有线程、或关注的线程分别处于什么状态:

1
2
3
4
5
6
7
8
$ grep /tmp/threaddump | awk '{print $2$3$4$5}' | sort | uniq -c | sort

39 RUNNABLE
21 TIMED_WAITING (onobjectmonitor)
6 TIMED_WAITING (parking)
51 TIMED_WAITING (sleeping)
3 WAITING (onobjectmonitor)
305 WAITING (parking)

发现有大量 WAITING (parking) 状态的线程。重新打开 threaddump 文件排查,根据堆栈可以定位到具体的问题代码,可以初步判断是 HTTP 连接耗尽资源导致的问题。

jmap

常用参数:

  • -dump 实时生成 JVM 堆内存转储快照(heap dump/hprof 文件)。格式为 -dump:[live,] format=b, file=<filename>,其中 live 子参数表示是否只 dump 出存活的对象。
  • -histo[:live] 显示堆中对象统计信息,包括类、实例数量、合计容量。
  • -heap 查看当前堆内存的详细信息,如 Heap ConfigurationHeap Usage
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
$ jmap -heap 7059
Attaching to process ID 7059, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.221-b11

using thread-local object allocation.
Parallel GC with 4 thread(s)

Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 7516192768 (7168.0MB)
NewSize = 5368709120 (5120.0MB)
MaxNewSize = 5368709120 (5120.0MB)
OldSize = 2147483648 (2048.0MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
capacity = 5249171456 (5006.0MB)
used = 1933544816 (1843.9720306396484MB)
free = 3315626640 (3162.0279693603516MB)
36.835238326800805% used
From Space:
capacity = 59768832 (57.0MB)
used = 44812496 (42.73652648925781MB)
free = 14956336 (14.263473510742188MB)
74.97636226185581% used
To Space:
capacity = 59768832 (57.0MB)
used = 0 (0.0MB)
free = 59768832 (57.0MB)
0.0% used
PS Old Generation
capacity = 2147483648 (2048.0MB)
used = 80178568 (76.46424102783203MB)
free = 2067305080 (1971.535758972168MB)
3.733605518937111% used

注意:

Heap Configuration Heap Usage
JDK 7 及以下版本 PermSizeMaxPermSize Heap 中包含 Perm Generation
JDK 8 及以上版本 MetaspaceSizeMaxMetaspaceSize Heap 不再包含 Perm Generation,取而代之的是在 Heap 之外有一块 Metaspace

可以进一步分析对象分布。

反汇编工具

大多数情况下,通过诸如javap等反编译工具来查看源码的字节码已经能够满足我们的日常需求,但是不排除在有些特定场景下,我们需要通过反汇编来查看相应的汇编指令。两个很好用的工具——HSDIS、JITWatch

工具 描述
HSDIS (HotSpot disassembler) 一款 HotSpot 虚拟机 JIT 编译代码的反汇编插件。
JITWatch 用于可视化分析。

https://zhuanlan.zhihu.com/p/158168592?from_voters_page=true

参考

《深入理解 Java 虚拟机》

https://docs.oracle.com/en/java/javase/11/tools/index.html

https://openjdk.java.net/tools/

JDK 内置实用工具:监视、故障排除

使用 VisualVM 进行性能分析及调优

JConsole、VisualVM 依赖的 JMX 技术到底是什么?

本文总结下垃圾收集涉及的一些重点:

gc_summary

基于分代收集算法的垃圾收集器组合,总结如下图,常用于 JDK 8 及之前的版本:

generational_collection

GC 选项配置

https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html

  • DefNew – single-threaded mark-copy stop-the-world garbage collector and is what is used to clean the Young generation(单线程 (single-threaded), 采用标记-复制 (mark-copy) 算法的,使整个 JVM 暂停运行 (stop-the-world) 的新生代 (Young generation) 垃圾收集器 (garbage collector))
Advanced Garbage Collection Options GC Configuration Description
-XX:+UseSerialGC Serial + Serial Old Enables the use of the serial garbage collector. This is generally the best choice for small and simple applications that do not require any special functionality from garbage collection.
By default, this option is disabled and the collector is chosen automatically based on the configuration of the machine and type of the JVM.
-XX:+UseParNewGC ParNew + SerialOld Enables the use of parallel threads for collection in the young generation.
By default, this option is disabled. It is automatically enabled when you set the -XX:+UseConcMarkSweepGC option. Using the -XX:+UseParNewGC option without the -XX:+UseConcMarkSweepGC option was deprecated in JDK 8.
Java 8 JEP 173: Retire Some Rarely-Used GC Combinations
Java 9 JEP 214: Remove GC Combinations Deprecated in JDK 8
-XX:+UseConcMarkSweepGC ParNew + CMS Enables the use of the CMS garbage collector for the old generation. Oracle recommends that you use the CMS garbage collector when application latency requirements cannot be met by the throughput (-XX:+UseParallelGC) garbage collector. The G1 garbage collector (-XX:+UseG1GC) is another alternative.
By default, this option is disabled and the collector is chosen automatically based on the configuration of the machine and type of the JVM.
When this option is enabled, the -XX:+UseParNewGC option is automatically set and you should not disable it, because the following combination of options has been deprecated in JDK 8: -XX:+UseConcMarkSweepGC -XX:-UseParNewGC. (Serial + CMS)
Java 9 JEP 291: Deprecate the Concurrent Mark Sweep (CMS) Garbage Collector
Java 14 JEP 363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector
-XX:+UseParallelGC Parallel Scavenge + Parallel Old Enables the use of the parallel scavenge garbage collector (also known as the throughput collector) to improve the performance of your application by leveraging multiple processors.
By default, this option is disabled and the collector is chosen automatically based on the configuration of the machine and type of the JVM. If it is enabled, then the -XX:+UseParallelOldGC option is automatically enabled, unless you explicitly disable it.
Java 14 JEP 366: Deprecate the ParallelScavenge + SerialOld GC Combination
-XX:+UseParallelOldGC Parallel Scavenge + Parallel Old Enables the use of the parallel garbage collector for full GCs.
By default, this option is disabled. Enabling it automatically enables the -XX:+UseParallelGC option.
-XX:+UseG1GC Enables the use of the garbage-first (G1) garbage collector. It is a server-style garbage collector, targeted for multiprocessor machines with a large amount of RAM. It meets GC pause time goals with high probability, while maintaining good throughput. The G1 collector is recommended for applications requiring large heaps (sizes of around 6 GB or larger) with limited GC latency requirements (stable and predictable pause time below 0.5 seconds).
By default, this option is disabled and the collector is chosen automatically based on the configuration of the machine and type of the JVM.
-XX:UseZGC

G1

Garbage First Garbage Collector Tuning - Oracle

GC 日志

Java 9 JEP 158: Unified JVM Logging

Java 9 JEP 271: Unified GC Logging

在线 GC 日志分析工具:https://gceasy.io/

1
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -Xloggc:E:/logs/gc-default.log

参考

HotSpot Virtual Machine Garbage Collection Tuning Guide - Java 17

Available Collectors

  • Serial Collector
  • Parallel Collector
  • Garbage-First (G1) Garbage Collector
  • The Z Garbage Collector

https://www.baeldung.com/java-verbose-gc

《深入理解 Java 虚拟机》

CMS 和 G1 改用三色标记法,可达性分析到底做错了什么?

背景

最近为了做春节大型活动,研究了下性能压测和 JVM 调优,先来看一张 JVM 监控图(硬件:4 核 8G)。

JVM 监控

6 小时的吞吐量为:(21600s - Young GC 35s + Old GC 0s) / 21600s = 99.8%,总吞吐量还是不错的(吞吐量就是 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值,即 吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC 时间)。如果虚拟机总共运行了 100 分钟,其中 GC 总耗时 1 分钟,那么吞吐量就是 99%。)。同时单次 Young GC 的平均耗时仅为 35s / 639 = 55 ms,停顿时间较短。

如果还需要进一步优化,思路如下:

  • 合理调整 Old Gen 与 Young Gen 大小比例,以减少 Young GC 次数,但单次 GC 耗时可能会相应延长,具体需测试。
  • 更换垃圾收集器,并对垃圾收集器参数进行调优。

下面介绍一些基础知识。

运行时数据区

手绘的运行时数据区如下:

jvm

JVM 定义了在程序执行期间使用的各种运行时数据区:

  • 其中一些数据区是在 JVM 启动时创建、仅在 JVM 退出时才被销毁。
  • 另外一些数据区是随每个线程创建及销毁。

java Command

java [options] classname [args]

java [options] -jar filename [args]

  • options: Command-line options separated by spaces.
  • classname: The name of the class to be launched.
  • filename: The name of the Java Archive (JAR) file to be called. Used only with the -jar option.
  • args: The arguments passed to the main() method separated by spaces.

java 命令用于启动 Java 应用程序。它通过启动 JRE,加载指定类并调用其 main() 方法来实现启动。main() 方法声明如下:

1
public static void main(String[] args)

java 命令支持以下几类选项:

说明:

  • 所有 JVM 实现都需要保证支持标准选项。标准选项用于执行常见操作,例如检查 JRE 版本、设置类路径、启用详细输出等。
  • 非标准选项是针对 Java HotSpot VM 的通用选项,因此不能保证所有 JVM 实现都能支持,并且随时可能改变。非标组选项以 -X 开头。
  • 高级选项不建议随意使用。这些是开发人员用于调整 Java HotSpot VM 特定区域的选项。这些区域通常具有特定的系统要求,并且可能需要对系统配置参数的访问权限。这些选项也不能保证所有 JVM 实现都能支持,并且随时可能改变。高级选项以 -XX 开头。

想跟踪最新版本中被弃用或删除的选项,参考已废弃与已移除的选项(JDK 8)。

布尔类型的选项无需参数,格式如下:

  • -XX:+OptionName 用于启用 默认情况下禁用的功能;
  • -XX:-OptionName 用于禁用 默认情况下启用的功能。

对于需要参数的选项,每个选项的确切语法有所差异:

  • 参数可以用空格、冒号(:)或等号(=)与选项名分开,或者参数可以直接跟在选项后面,具体参考文档。

如果需要指定字节大小,可以使用以下几种格式:

  • no suffix
  • k or K for kilobytes (KB)
  • m or M for megabytes (MB)
  • g or G for gigabytes (GB)

例如:

  • 大小为 8 GB,参数可以设为 8g, 8192m, 8388608k, 8589934592
  • 大小为 1.5 GB,参数不能设为 1.5g,可以设为 1536m
  • 如果需要指定百分比,使用 0 到 1 之间的数字(例如, 0.25 for 25%)。

JVM Stack

-Xsssize

Sets the thread stack size (in bytes). Append the letter k or K to indicate KB, m or M to indicate MB, g or G to indicate GB. The default value depends on the platform:

  • Linux/ARM (32-bit): 320 KB
  • Linux/i386 (32-bit): 320 KB
  • Linux/x64 (64-bit): 1024 KB
  • OS X (64-bit): 1024 KB
  • Oracle Solaris/i386 (32-bit): 320 KB
  • Oracle Solaris/x64 (64-bit): 1024 KB

The following examples set the thread stack size to 1024 KB in different units:

1
2
3
-Xss1m
-Xss1024k
-Xss1048576

This option is equivalent to -XX:ThreadStackSize.

Heap

参数 描述
-Xms-XX:InitialHeapSize
-Xmx-XX:MaxHeapSize
设置 Heap 堆区的初始值和最大值,Server 端 JVM 建议将 -Xms-Xmx 设为相同值。
参数 描述
-Xmn 设置 Heap 堆内 Young Generation,而 Old Generation 等于:堆区减去 -Xmn
设置 -Xmn 等同于设置了相同的 Young Generation 初始值 -XX:NewSize 和最大值 -XX:MaxNewSize
参数 描述
-XX:NewRatio Sets the ratio between young and old generation sizes. By default, this option is set to 2 (Young Gen can get up to 1/3 (Y=H/(R+1)) of the Heap).
-XX:SurvivorRatio Sets the ratio between eden and survivor space sizes. By default, this option is set to 8 (S0/S1 can get up to 1/10 (S=Y/(R+2)) of Young Gen).

-Xms-Xmx

-Xmssize

Sets the initial size (in bytes) of the heap. This value must be a multiple of 1024 and greater than 1 MB. Append the letter k or K to indicate kilobytes, m or M to indicate megabytes, g or G to indicate gigabytes.

The following examples show how to set the size of allocated memory to 6 MB using various units:

1
2
3
-Xms6291456
-Xms6144k
-Xms6m

If you do not set this option, then the initial size will be set as the sum of the sizes allocated for the old generation and the young generation.

The -Xms option is equivalent to -XX:InitialHeapSize.

-Xmxsize

Specifies the maximum size (in bytes) of the memory allocation pool in bytes. This value must be a multiple of 1024 and greater than 2 MB. Append the letter k or K to indicate kilobytes, m or M to indicate megabytes, g or G to indicate gigabytes. The default value is chosen at runtime based on system configuration.

For server deployments, -Xms and -Xmx are often set to the same value. See the section “Ergonomics” in Java SE HotSpot Virtual Machine Garbage Collection Tuning Guide at http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/index.html.

The following examples show how to set the maximum allowed size of allocated memory to 80 MB using various units:

1
2
3
-Xmx83886080
-Xmx81920k
-Xmx80m

The -Xmx option is equivalent to -XX:MaxHeapSize.

-Xmn

-Xmnsize

Sets the initial and maximum size (in bytes) of the heap for the young generation (nursery).

The young generation region of the heap is used for new objects. GC is performed in this region more often than in other regions.

  • If the size is too small, then a lot of minor garbage collections will be performed.
  • If the size is too large, then only full garbage collections will be performed, which can take a long time to complete.

⚠️ Oracle recommends that you keep the size for the young generation between a half and a quarter of the overall heap size.

Append the letter k or K to indicate kilobytes, m or M to indicate megabytes, g or G to indicate gigabytes. The following examples show how to set the initial and maximum size of young generation to 256 MB using various units:

1
2
3
-Xmn256m
-Xmn262144k
-Xmn268435456

Instead of the -Xmn option to set both the initial and maximum size of the heap for the young generation, you can use -XX:NewSize to set the initial size and -XX:MaxNewSize to set the maximum size.

-XX:NewRatio

-XX:NewRatio=ratio

Sets the ratio between young and old generation sizes. By default, this option is set to 2.

The NewRatio is the ratio of old generation to young generation (e.g. value 2 means max size of old will be twice the max size of young, i.e. young can get up to 1/3 of the heap).

1
Y=H/(R+1)

设置 Young Generation 和 Old Generation 的比值,例如该值默认为 2,则表示 Young Generation 和 Old Generation 比值为1:2。

-XX:SurvivorRatio

-XX:SurvivorRatio=ratio

Sets the ratio between eden and survivor space sizes. By default, this option is set to 8.

The following formula can be used to calculate the initial size of survivor space (S) based on the size of the young generation (Y), and the initial survivor space ratio (R):

1
S=Y/(R+2)

The 2 in the equation denotes two survivor spaces. The larger the value specified as the initial survivor space ratio, the smaller the initial survivor space size.

By default, the initial survivor space ratio is set to 8. If the default value for the young generation space size is used (2 MB), the initial size of the survivor space will be 0.2 MB.

Method Area

PermGen

JDK 7 及以下版本:

参数 描述
-XX:PermSize Perm 的初始值
-XX:MaxPermSize Perm 的最大值

JVM 的永久代(PermGen)主要用于存放 Class 的 meta-data,Class 在被 Loader 加载时就会被放到 PermGen space,GC 在主程序运行期间不会对该区进行清理,默认是 64M 大小,当程序需要加载的对象比较多时,超过 64M 就会报这部分内存溢出了,需要加大内存分配。

Metaspace

JDK 8 及以上版本,永久代(PermGen)的概念被废弃掉了,参考 JEP 122: Remove the Permanent Generation

The proposed implementation will allocate class meta-data in native memory and move interned Strings and class static variables to the Java heap.

Hotspot will explicitly allocate and free the native memory for the class meta-data. Allocation of new class meta-data would be limited by the amount of available native memory rather than fixed by the value of -XX:MaxPermSize, whether the default or specified on the command line.

取而代之的是一个称为 Metaspace 的存储空间。Metaspace 使用的是本地内存,而不是堆内存,也就是说在默认情况下 Metaspace 的大小只与本地内存大小有关。可以通过以下的几个参数对 Metaspace 进行控制:

参数 描述
-XX:MetaspaceSize Metaspace 的初始值
-XX:MaxMetaspaceSize Metaspace 的最大值

Direct Memory

-XX:MaxDirectMemorySize=size

Sets the maximum total size (in bytes) of the New I/O (the java.nio package) direct-buffer allocations. Append the letter k or K to indicate kilobytes, m or M to indicate megabytes, g or G to indicate gigabytes. By default, the size is set to 0, meaning that the JVM chooses the size for NIO direct-buffer allocations automatically.

The following examples illustrate how to set the NIO size to 1024 KB in different units:

1
2
3
-XX:MaxDirectMemorySize=1m
-XX:MaxDirectMemorySize=1024k
-XX:MaxDirectMemorySize=1048576

异常

java.lang.StackOverflowError

java.lang.StackOverflowError:线程栈溢出,要么是方法调用层次过多(比如存在无限递归调用):

StackOverflow

要么是线程栈太小,可调整 -Xss 参数增加线程栈大小。

java.lang.OutOfMemoryError: Java heap space

java.lang.OutOfMemoryError: Java heap space:这种是堆内存不够,一个原因是真不够,另一个原因是程序中有死循环,例如:

OutOfMemory

如果是堆内存不足,可调整 -Xms-Xmx,或者新老生代的比例。

java.lang.OutOfMemoryError: PermGen space

java.lang.OutOfMemoryError: PermGen space:这种是P区内存不够,可调整:-XX:PermSize-XX:MaxPermSize

参考

Java 虚拟机规范(Java SE 8 版 - 中文版)

Java 虚拟机规范(Java SE 8 版 - 英文版)

JEP 122: Remove the Permanent Generation - Release on JDK 8

Command Line Options - JDK 8 HotSpot VM

写博客难免会引用图片资源,这里提供一种不用图床解决图片资源上传的思路:将图片资源作为源文件一并上传仓库。

新建 img 目录

首先,在 hexo 博客 source 目录下新建 img 目录,即:hexo/source/img

然后,在文章的图片引用处使用该路径即可,例如:![example](/img/example.png)

最后,hexo g 构建出 ./public 目录,发现 img 在该目录之中。hexo s 启动服务后,确认能够成功引用图片。

解决 Typora 实时预览

通过上述方法能够解决部署后图片引用问题,但带来一个新的问题就是 Typora 无法实时预览。解决办法:在文章顶部加上 typora-root-url: ..

hexo_with_img

可以将该路径加入到 hexo 模板之中,这样每次 hexo n 新建文稿都会带上该配置:

hexo_with_img_2

配置如下:

1
2
3
4
5
6
7
---
title: {{ title }}
date: {{ date }}
updated: {{ updated }}
tags:
typora-root-url: ..
---

数据结构

Strings

单个 批量
设值 SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]
SETNX key value
SETEX key seconds value
PSETEX key milliseconds value
GETSET key value
SETRANGE key offset value
MSET key value [key value …]
MSETNX key value [key value …]
取值 GET key
GETDEL key
GETRANGE key start end
STRLEN key
MGET key [key …]
原子递增、递减 INCR key
INCRBY key increment
INCRBYFLOAT key increment
DECR key
DECRBY key decrement
追加 APPEND key value
位操作 SETBIT key offset value
GETBIT key offset
BITCOUNT key [start end]
BITOP operation destkey key [key …]
BITFIELD
BITPOS key bit [start] [end]

使用场景:

  • WEB 集群下的 Session 共享:

    1
    $ SET key value
  • 分布式锁:

    1
    2
    3
    4
    5
    6
    -- 返回 1 表示加锁成功,0 表示加锁失败
    $ SET key value NX
    $ SETNX key value

    -- 解锁
    $ DEL key
  • 全局计数器:

    1
    2
    $ INCR key
    $ DECR key
  • 分布式流水号:

    1
    2
    $ INCR key
    $ INCRBY key

分布式流水号 Java 伪代码如下,单机一次性取 1000 个 ID,以降低网络开销和 Redis 负载:

1
2
3
4
5
6
7
8
9
10
11
12
13
private int id;
private int maxId;
private int INCR_BY = 1000;

public synchronized int nextId() {
if (id == 0 || id == maxId) {
maxId = eval(incrby id INCR_BY);
id = maxId - INCR_BY + 1;
return id;
} else {
return id++;
}
}

Hashes

散列表,一种通过散列函数计算对应数组下标,并通过下标随机访问数据时,时间复杂度为 O(1) 的特性快速定位数据的数据结构。Redis 散列表使用这种数据结构来快速获取指定 field。

单个 批量
设置 field 的 value HSET key field value [field value …]
HSETNX key field value
HMSET key field value [field value …]
获取 field 的 value HGET key field
HSCAN key cursor [MATCH pattern] [COUNT count]
HSTRLEN key field
HMGET key field [field …]
获取所有 fields HKEYS key
获取所有 values HVALS key
获取所有 fields 和 values HGETALL key
获取 field 的个数 HLEN key
判断 field 是否存在 HEXISTS key field
删除 field HDEL key field [field …]
原子递增、递减指定 field HINCRBY key field increment
HINCRBYFLOAT key field increment

Lists

双端队列。

redis_lists

队列操作
入队 LPUSH key element [element …]
LPUSHX key element [element …]
RPUSH key element [element …]
RPUSHX key element [element …]
出队 LPOP key RPOP key
阻塞出队 BLPOP key [key …] timeout BRPOP key [key …] timeout
插队 LINSERT key BEFORE|AFTER pivot element
获取指定索引的元素 LINDEX key index
获取指定范围的元素 LRANGE key start stop
获取列表长度 LLEN key
覆盖元素 LSET key index element
移除元素 LREM key count element
移除指定范围的元素 LTRIM key start stop
非阻塞 阻塞
出队并重新入队另一个队列 RPOPLPUSH source destination BRPOPLPUSH source destination timeout

使用场景:

  • Stack (FILO): LPUSH + LPOP
  • Queue (FIFO): LPUSH + RPOP,实现简单的消息队列
  • Unbounded Blocking Queue (FIFO): LPUSH + BRPOP

Sets

无序集合(散列表实现)。

集合操作:

集合操作 命令
添加元素(去重) SADD key member [member …]
移除元素 SREM key member [member …]
判断指定元素是否存在 SISMEMBER key member
获取元素个数 SCARD key
增量式遍历集合元素 SSCAN key cursor [MATCH pattern] [COUNT count]
获取所有元素 SMEMBERS key
获取指定个数的随机元素 SRANDMEMBER key [count]
移除指定个数的随机元素,并返回 SPOP key [count]

集合运算:

集合运算 数学符号 命令
移动指定元素到另一个集合 SMOVE source destination member
求交集(Intersection) SINTER key [key …]
求交集(Intersection),并保存结果 SINTERSTORE destination key [key …]
求并集(Union) SUNION key [key …]
求并集(Union),并保存结果 SUNIONSTORE destination key [key …]
求差集(Difference) SDIFF key [key …]
求差集(Difference),并保存结果 SDIFFSTORE destination key [key …]

使用场景:

  • 抢红包、抽奖、秒杀 —— 本质上都是同一类问题,解决思路类似。为了减少对临界资源的竞争,避免使用各种锁进行并发控制,可以预先对临界资源进行拆分,以提升性能:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # 预拆红包,放入集合
    $ SADD key 子红包ID1 [子红包ID2 …]
    # 查看所有红包
    $ SMEMBERS key
    # 随机抽取红包
    $ SPOP key [count]

    # 预先将奖品放入奖池
    $ SADD key member [member …]
    # 查看所有奖品
    $ SMEMBERS key
    # 随机抽奖(只抽一次)
    $ SRANDMEMBER key [count]

    # 登记参与抽奖的候选人
    $ SADD key member [member …]
    # 查看所有候选人
    $ SMEMBERS key
    # 随机抽取一二三等奖的获得者
    $ SPOP key [count]
  • 社交应用的关注模型:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 我关注的人
    $ SMEMBERS key

    # 求共同关注
    $ SINTER key [key ...]

    # 我关注的人也关注 ta
    foreach(member in 我_关注的人) {
    # 我每个关注的人,他们关注的人中,是否有 ta
    $ SISMEMBER member_关注的人 ta
    }

    # 我可能认识的人
    foreach(member in 我_关注的人) {
    # 我每个关注的人,他们关注的人中,有我还没关注的
    $ SDIFF member_关注的人 我_关注的人
    }
  • 商品筛选

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    # 1、分类筛选维度,每个维度都为一个集合
    # 2、将商品按维度加入所属集合
    # 3、多选筛选条件时,求交集
    $ SINTER key [key ...]

    # 4、根据交集 member,获取商品详情(O(1) 时间复杂度)
    foreach member {
    # 每个 field 为商品属性
    $ HGETALL member
    }

Sorted Sets

有序集合(复合数据结构实现:散列表+跳表)。

使用场景:

  • Top K(例如排行榜)。实现思路:利用集合的三大特性之一——互异性,进行去重,相同元素只进行计数,形成一个二元组集合(key 为元素,value 为计数)。最后按计数结果对集合进行倒序排序,取前 N 个元素。

其它命令

命令
删除 key DEL
设值 key 的过期时间(秒) EXPIRE

参考

https://redis.io/commands

Hexo 博客使用好多年了,总结下日常使用的一些内容。

Hexo 博客搭建

安装配置

1
2
3
4
5
6
7
8
9
10
11
# -g 参数全局安装 Hexo 命令行工具,安装后才可以使用下述 hexo 命令
$ npm install -g hexo-cli

# 初始化本地仓库及 hexo 文件,适用于第一次使用
$ hexo init

# hexo 基础配置、主题、插件配置等等,详细配置参考官网
$ vim _config.yml

# 根据 package.json 的声明(hexo 版本及 dependencies 版本)安装所需依赖到当前目录 node_modules
$ npm install

主题

NexT

https://github.com/next-theme/hexo-theme-next

https://theme-next.js.org/pisces/

https://hexo-next.readthedocs.io/zh_CN/latest/

常用命令

依赖安装完毕,开始使用 hexo,常用命令如下:

Hexo 常用命令

自动化构建 & 部署

推荐使用 GitHub Actions,简单、免费,而 Travis CI 收费了。

GitHub Actions

为了方便随时随地可以编写博客,搭建好的本地仓库及其源文件一般会推送到 GitHub 远程仓库中保管,并自动构建 & 部署到 GitHub Pages 服务。步骤如下:

  1. 创建 GitHub Pages 服务所需的仓库 yourname.github.io,注意该仓库必须是 public 权限。
  2. 创建 .github/workflows/pages.yml 配置文件。
  3. 推送源文件至该仓库。

参考:https://hexo.io/docs/github-pages

Travis CI

完成上面两步就可以开始创作了。但毕竟命令还是有些繁琐,因此可以利用持续集成服务代替人工来做重复的事情。引入 Travis CI 后,整体流程如下:

GitHub Pages with CI

从上述流程来看,作者只需要完成创作并推送即可,其它构建、部署的事则由 Travis CI 来完成,非常简单。

下面来看下如何配置:

GitHub 创建 access token

登录 GitHub - Settings - Developer Settings 选项,找到 Personal access tokens 页面,创建个人 access token,创建时权限 repo 权限和 user:email 权限。

Travis CI 仓库配置

  1. 使用 GitHub 账户登录 Travis CI 官网并进行 OAuth 授权
  2. 同步仓库一,过程中会在 GitHub 账户下安装 Travis CI 的 GitHub App,用于触发持续集成
  3. 为仓库一设置环境变量:
    • GH_TOKEN 值为 GitHub access token
    • GH_REF 值为 GitHub 仓库二地址

创建 .travis.yml 配置

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
# 设置语言
language: node_js
# 设置相应的版本,可以指定版本 10,或者使用稳定版 stable
node_js: stable
# 设置只监听哪个分支
branches:
only:
- master
# 缓存 node_modules 目录,可以节省持续集成的时间
cache:
directories:
- node_modules
before_install:
- npm install -g hexo-cli
install:
- npm install
script:
- hexo clean
- hexo g
after_script:
- cd ./public
- git init
- git config user.name "yourname" # 修改name
- git config user.email "youremail" # 修改email
- git add .
- git commit -m "Travis CI Auto Builder"
- git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:master

自动构建

创作并推送到仓库一即可,其它构建、部署的事都由 Travis CI 来完成,过程如下:

Travis CI