Dunwu Blog

大道至简,知易行难

MySQL 运维

::: info 概述

如果你的公司有 DBA,那么我恭喜你,你可以无视 MySQL 运维。如果你的公司没有 DBA,那你就好好学两手 MySQL 基本运维操作,行走江湖,防身必备。

:::

MySQL 安装

Windows 安装

(1)下载 MySQL 5.7 免安装版

下载地址:https://dev.mysql.com/downloads/mysql/5.7.html#downloads

(2)解压并创建 my.ini 在根目录

my.ini 文件示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[mysqld]
#设置 3306 端口
port = 3306
# 设置 mysql 的安装目录 这块换成自己解压的路径
basedir=D:\\Tools\\DB\\mysql\\mysql-5.7.31
# 允许最大连接数
max_connections=200
# 服务端使用的字符集默认为 8 比特编码的 latin1 字符集
character-set-server=utf8
# 创建新表时将使用的默认存储引擎
default-storage-engine=INNODB

[client]
# 设置 mysql 客户端默认字符集
default-character-set=utf8

(3)执行安装命令

在控制台 CMD 中依次执行以下安装命令

1
2
3
cd D:\\Tools\\DB\\mysql\\mysql-5.7.31
mysqld --initialize
mysqld -install

说明:

  • mysqld --initialize 会自动初始化创建 data 文件夹并初始化 mysql。
  • mysqld -install 会安装 mysql 服务。

(4)启动服务

在控制台执行 net start mysql 启动服务。

CentOS 安装

本文仅介绍 rpm 安装方式

安装 mysql yum 源

官方下载地址:https://dev.mysql.com/downloads/repo/yum/

(1)下载 yum 源

1
wget https://dev.mysql.com/get/mysql80-community-release-el7-1.noarch.rpm

(2)安装 yum repo 文件并更新 yum 缓存

1
rpm -ivh mysql80-community-release-el7-1.noarch.rpm

执行结果:

会在 /etc/yum.repos.d/ 目录下生成两个 repo 文件

1
2
3
$ ls | grep mysql
mysql-community.repo
mysql-community-source.repo

更新 yum:

1
2
yum clean all
yum makecache

(3)查看 rpm 安装状态

1
2
3
4
5
6
$ yum search mysql | grep server
mysql-community-common.i686 : MySQL database common files for server and client
mysql-community-common.x86_64 : MySQL database common files for server and
mysql-community-test.x86_64 : Test suite for the MySQL database server
: administering MySQL servers
mysql-community-server.x86_64 : A very fast and reliable SQL database server

通过 yum 安装 mysql 有几个重要目录:

1
2
3
4
5
6
7
8
9
10
## 配置文件
/etc/my.cnf
## 数据库目录
/var/lib/mysql/
## 配置文件
/usr/share/mysql(mysql.server 命令及配置文件)
## 相关命令
/usr/bin(mysqladmin mysqldump 等命令)
## 启动脚本
/usr/lib/systemd/system/mysqld.service (注册为 systemd 服务)

(4)安装 mysql 服务器

1
yum install mysql-community-server

mysql 服务管理

通过 yum 方式安装 mysql 后,本地会有一个名为 mysqld 的 systemd 服务。

其服务管理十分简便:

1
2
3
4
5
6
7
8
9
10
11
12
## 查看状态
systemctl status mysqld
## 启用服务
systemctl enable mysqld
## 禁用服务
systemctl disable mysqld
## 启动服务
systemctl start mysqld
## 重启服务
systemctl restart mysqld
## 停止服务
systemctl stop mysqld

初始化数据库密码

查看一下初始密码

1
2
$ grep "password" /var/log/mysqld.log
2018-09-30T03:13:41.727736Z 5 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: %:lt+srWu4k1

执行命令:

1
mysql -uroot -p<临时密码>

输入临时密码,进入 mysql,如果要修改密码,执行以下指令:

1
ALTER user 'root'@'localhost' IDENTIFIED BY '你的密码';

注:密码强度默认为中等,大小写字母、数字、特殊符号,只有修改成功后才能修改配置再设置更简单的密码

配置远程访问

1
2
3
4
CREATE USER 'root'@'%' IDENTIFIED BY '你的密码';
GRANT ALL ON *.* TO 'root'@'%';
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '你的密码';
FLUSH PRIVILEGES;

跳过登录认证

1
vim /etc/my.cnf

在 [mysqld] 下面加上 skip-grant-tables

作用是登录时跳过登录认证,换句话说就是 root 什么密码都可以登录进去。

执行 systemctl restart mysqld,重启 mysql

MySQL 管理

客户端连接

语法:mysql -h<主机> -P<端口> -u<用户名> -p<密码>

如果没有显式指定密码,会要求输入密码才能访问。

【示例】连接本地 MySQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ mysql -h 127.0.0.1 -P 3306 -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 13501
Server version: 8.0.19 MySQL Community Server - GPL

Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>

查看连接

连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,你可以在 show processlist 命令中看到它。客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。

img

创建用户

1
CREATE USER 'username'@'host' IDENTIFIED BY 'password';

说明:

  • username:你将创建的用户名
  • host:指定该用户在哪个主机上可以登陆,如果是本地用户可用 localhost,如果想让该用户可以从任意远程主机登陆,可以使用通配符%
  • password:该用户的登陆密码,密码可以为空,如果为空则该用户可以不需要密码登陆服务器

示例:

1
2
3
4
5
CREATE USER 'dog'@'localhost' IDENTIFIED BY '123456';
CREATE USER 'pig'@'192.168.1.101_' IDENDIFIED BY '123456';
CREATE USER 'pig'@'%' IDENTIFIED BY '123456';
CREATE USER 'pig'@'%' IDENTIFIED BY '';
CREATE USER 'pig'@'%';

注意:在 MySQL 8 中,默认密码验证不再是 password。所以在创建用户时,create user 'username'@'%' identified by 'password'; 客户端是无法连接服务的。

所以,需要加上 IDENTIFIED WITH mysql_native_password,例如:CREATE USER 'slave'@'%' IDENTIFIED WITH mysql_native_password BY '123456';

查看用户

1
2
3
-- 查看所有用户
SELECT DISTINCT CONCAT('User: ''', user, '''@''', host, ''';') AS query
FROM mysql.user;

授权

命令:

1
GRANT privileges ON databasename.tablename TO 'username'@'host'

说明:

  • privileges:用户的操作权限,如SELECTINSERTUPDATE等,如果要授予所的权限则使用ALL
  • databasename:数据库名
  • tablename:表名,如果要授予该用户对所有数据库和表的相应操作权限则可用*表示,如*.*

示例:

1
2
3
GRANT SELECT, INSERT ON test.user TO 'pig'@'%';
GRANT ALL ON *.* TO 'pig'@'%';
GRANT ALL ON maindataplus.* TO 'pig'@'%';

注意:

用以上命令授权的用户不能给其它用户授权,如果想让该用户可以授权,用以下命令:

1
2
3
4
-- 为指定用户配置指定权限
GRANT privileges ON databasename.tablename TO 'username'@'host' WITH GRANT OPTION;
-- 为 root 用户分配所有权限
GRANT ALL ON *.* TO 'root'@'%' IDENTIFIED BY '密码' WITH GRANT OPTION;

撤销授权

命令:

1
REVOKE privilege ON databasename.tablename FROM 'username'@'host';

说明:

privilege, databasename, tablename:同授权部分

例子:

1
REVOKE SELECT ON *.* FROM 'pig'@'%';

注意:

假如你在给用户'pig'@'%'授权的时候是这样的(或类似的):GRANT SELECT ON test.user TO 'pig'@'%',则在使用REVOKE SELECT ON *.* FROM 'pig'@'%';命令并不能撤销该用户对 test 数据库中 user 表的SELECT 操作。相反,如果授权使用的是GRANT SELECT ON *.* TO 'pig'@'%';REVOKE SELECT ON test.user FROM 'pig'@'%';命令也不能撤销该用户对 test 数据库中 user 表的Select权限。

具体信息可以用命令SHOW GRANTS FOR 'pig'@'%'; 查看。

查看授权

1
2
-- 查看用户权限
SHOW GRANTS FOR 'root'@'%';

更改用户密码

1
SET PASSWORD FOR 'username'@'host' = PASSWORD('newpassword');

如果是当前登陆用户用:

1
SET PASSWORD = PASSWORD("newpassword");

示例:

1
SET PASSWORD FOR 'pig'@'%' = PASSWORD("123456");

备份与恢复

MySQL 备份数据使用 mysqldump 命令。

mysqldump 将数据库中的数据备份成一个文本文件,表的结构和表中的数据将存储在生成的文本文件中。

备份:

备份一个数据库

语法:

1
mysqldump -h <host> -P<port> -u<username> -p<database> [<table1> <table2> ...] > backup.sql
  • host - MySQL Server 的 host
  • port - MySQL Server 的端口
  • username - 数据库用户
  • dbname - 数据库名称
  • table1 和 table2 参数表示需要备份的表的名称,为空则整个数据库备份;
  • BackupName.sql 参数表设计备份文件的名称,文件名前面可以加上一个绝对路径。通常将数据库被分成一个后缀名为 sql 的文件

备份多个数据库

1
mysqldump -u <username> -p --databases <database1> <database2> ... > backup.sql

备份所有数据库

1
mysqldump -u <username> -p --all-databases > backup.sql

恢复一个数据库

MySQL 恢复数据使用 mysql 命令。

语法:

1
mysql -h <host> -P<port> -u<username> -p<database> < backup.sql

恢复所有数据库

1
mysql -u<username> -p --all-databases < backup.sql

卸载

(1)查看已安装的 mysql

1
2
3
4
5
6
7
$ rpm -qa | grep -i mysql
perl-DBD-MySQL-4.023-6.el7.x86_64
mysql80-community-release-el7-1.noarch
mysql-community-common-8.0.12-1.el7.x86_64
mysql-community-client-8.0.12-1.el7.x86_64
mysql-community-libs-compat-8.0.12-1.el7.x86_64
mysql-community-libs-8.0.12-1.el7.x86_64

(2)卸载 mysql

1
yum remove mysql-community-server.x86_64

主从节点部署

假设需要配置一个主从 MySQL 服务器环境

  • master 节点:192.168.8.10
  • slave 节点:192.168.8.11

主节点上的操作

(1)修改配置并重启

执行 vi /etc/my.cnf ,添加如下配置:

1
2
3
[mysqld]
server-id=1
log_bin=/var/lib/mysql/binlog
  • server-id - 服务器 ID 号。在主从架构中,每台机器的 ID 必须唯一。
  • log_bin - 同步的日志路径及文件名,一定注意这个目录要是 mysql 有权限写入的;

修改后,重启 mysql 使配置生效:

1
systemctl restart mysql

(2)创建用于同步的用户

进入 mysql 命令控制台:

1
2
$ mysql -u root -p
Password:

执行以下 SQL:

1
2
3
4
5
6
7
8
9
10
11
12
-- a. 创建 slave 用户
CREATE USER 'slave'@'%' IDENTIFIED WITH mysql_native_password BY '密码';
-- 为 slave 赋予 REPLICATION SLAVE 权限
GRANT REPLICATION SLAVE ON *.* TO 'slave'@'%';

-- b. 或者,创建 slave 用户,并指定该用户能在任意主机上登录
-- 如果有多个从节点,又想让所有从节点都使用统一的用户名、密码认证,可以考虑这种方式
CREATE USER 'slave'@'%' IDENTIFIED WITH mysql_native_password BY '密码';
GRANT REPLICATION SLAVE ON *.* TO 'slave'@'%';

-- 刷新授权表信息
FLUSH PRIVILEGES;

注意:在 MySQL 8 中,默认密码验证不再是 password。所以在创建用户时,create user 'username'@'%' identified by 'password'; 客户端是无法连接服务的。所以,需要加上 IDENTIFIED WITH mysql_native_password BY 'password'

补充用户管理 SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 查看所有用户
SELECT DISTINCT CONCAT('User: ''', user, '''@''', host, ''';') AS query
FROM mysql.user;

-- 查看用户权限
SHOW GRANTS FOR 'root'@'%';

-- 创建用户
-- a. 创建 slave 用户,并指定该用户只能在主机 192.168.8.11 上登录
CREATE USER 'slave'@'192.168.8.11' IDENTIFIED WITH mysql_native_password BY '密码';
-- 为 slave 赋予 REPLICATION SLAVE 权限
GRANT REPLICATION SLAVE ON *.* TO 'slave'@'192.168.8.11';

-- 删除用户
DROP USER 'slave'@'192.168.8.11';

(3)加读锁

为了主库与从库的数据保持一致,我们先为 mysql 加入读锁,使其变为只读。

1
mysql> FLUSH TABLES WITH READ LOCK;

(4)查看主节点状态

1
2
3
4
5
6
7
mysql> show master status;
+------------------+----------+--------------+---------------------------------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+---------------------------------------------+-------------------+
| mysql-bin.000001 | 4202 | | mysql,information_schema,performance_schema | |
+------------------+----------+--------------+---------------------------------------------+-------------------+
1 row in set (0.00 sec)

注意:需要记录下 FilePosition,后面会用到。

(5)导出 sql

1
mysqldump -u root -p --all-databases --master-data > dbdump.sql

(6)解除读锁

1
mysql> UNLOCK TABLES;

(7)将 sql 远程传送到从节点上

1
scp dbdump.sql root@192.168.8.11:/home

从节点上的操作

(1)修改配置并重启

执行 vi /etc/my.cnf ,添加如下配置:

1
2
3
[mysqld]
server-id=2
log_bin=/var/lib/mysql/binlog
  • server-id - 服务器 ID 号。在主从架构中,每台机器的 ID 必须唯一。
  • log_bin - 同步的日志路径及文件名,一定注意这个目录要是 mysql 有权限写入的;

修改后,重启 mysql 使配置生效:

1
systemctl restart mysql

(2)导入 sql

1
mysql -u root -p < /home/dbdump.sql

(3)在从节点上建立与主节点的连接

进入 mysql 命令控制台:

1
2
$ mysql -u root -p
Password:

执行以下 SQL:

1
2
3
4
5
6
7
8
9
10
-- 停止从节点服务
STOP SLAVE;

-- 注意:MASTER_USER 和
CHANGE MASTER TO
MASTER_HOST='192.168.8.10',
MASTER_USER='slave',
MASTER_PASSWORD='密码',
MASTER_LOG_FILE='binlog.000001',
MASTER_LOG_POS=4202;
  • MASTER_LOG_FILEMASTER_LOG_POS 参数要分别与 show master status 指令获得的 FilePosition 属性值对应。
  • MASTER_HOST 是主节点的 HOST。
  • MASTER_USERMASTER_PASSWORD 是在主节点上注册的用户及密码。

(4)启动 slave 进程

1
mysql> start slave;

(5)查看主从同步状态

1
mysql> show slave status\G;

说明:如果以下两项参数均为 YES,说明配置正确。

  • Slave_IO_Running
  • Slave_SQL_Running

(6)将从节点设为只读

1
2
3
4
5
6
7
8
9
10
11
mysql> set global read_only=1;
mysql> set global super_read_only=1;
mysql> show global variables like "%read_only%";
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| innodb_read_only | OFF |
| read_only | ON |
| super_read_only | ON |
| transaction_read_only | OFF |
+-----------------------+-------+

注:设置 slave 服务器为只读,并不影响主从同步。

慢查询

查看慢查询是否开启

1
show variables like '%slow_query_log';

可以通过 set global slow_query_log 命令设置慢查询是否开启:ON 表示开启;OFF 表示关闭。

1
set global slow_query_log='ON';

查看慢查询时间阈值

1
show variables like '%long_query_time%';

设置慢查询阈值

1
set global long_query_time = 3;

隔离级别

查看隔离级别:

1
2
3
4
5
6
7
8
9
10
11
mysql> show variables like 'transaction_isolation';

+-----------------------+----------------+

| Variable_name | Value |

+-----------------------+----------------+

| transaction_isolation | READ-COMMITTED |

+-----------------------+----------------+

MySQL 配置

大部分情况下,默认的基本配置已经足够应付大多数场景,不要轻易修改 MySQL 服务器配置,除非你明确知道修改项是有益的。

尽量不要使用 MySQL 的缓存功能,因为其要求每次请求参数完全相同,才能命中缓存。这种方式实际上并不高效,还会增加额外开销,实际业务场景中一般使用 Redis 等 key-value 存储来解决缓存问题,性能远高于 MySQL 的查询缓存。

配置文件路径

配置 MySQL 首先要确定配置文件在哪儿。

不同 Linux 操作系统上,MySQL 配置文件路径可能不同。通常的路径为 /etc/my.cnf 或 /etc/mysql/my.cnf 。

如果不知道配置文件路径,可以尝试以下操作:

1
2
3
4
5
# which mysqld
/usr/sbin/mysqld
# /usr/sbin/mysqld --verbose --help | grep -A 1 'Default options'
Default options are read from the following files in the given order:
/etc/my.cnf /etc/mysql/my.cnf /usr/etc/my.cnf ~/.my.cnf

配置项语法

MySQL 配置项设置都使用小写,单词之间用下划线或横线隔开(二者是等价的)。

建议使用固定的风格,这样检索配置项时较为方便。

1
2
3
# 这两种格式等价
/usr/sbin/mysqld --auto-increment-offset=5
/usr/sbin/mysqld --auto_increment_offset=5

基本配置模板

一个基本的 MySQL 配置模板大概如下:

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
[mysqld]
# GENERAL
# -------------------------------------------------------------------------------
datadir = /var/lib/mysql
socket = /var/lib/mysql/mysql.sock
pid_file = /var/lib/mysql/mysql.pid
user = mysql
port = 3306
default_storage_engine = InnoDB
default_time_zone = '+8:00'
character_set_server = utf8mb4
collation_server = utf8mb4_0900_ai_ci

# LOG
# -------------------------------------------------------------------------------
log_error = /var/log/mysql/mysql-error.log
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log

# InnoDB
# -------------------------------------------------------------------------------
innodb_buffer_pool_size = <value>
innodb_log_file_size = <value>
innodb_file_per_table = 1
innodb_flush_method = O_DIRECT

# MyIsam
# -------------------------------------------------------------------------------
key_buffer_size = <value>

# OTHER
# -------------------------------------------------------------------------------
tmp_table_size = 32M
max_heap_table_size = 32M
max_connections = <value>
open_files_limit = 65535

[client]
socket = /var/lib/mysql/mysql.sock
port = 3306

配置项说明

下面是一个较为详尽的 MySQL 配置文件,各配置项有注释说明:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
[mysqld]
# GENERAL
# -------------------------------------------------------------------------------
datadir = /var/lib/mysql
# socket 文件
socket = /var/lib/mysql/mysql.sock
# PID 文件
pid_file = /var/lib/mysql/mysql.pid
# 启动 mysql 服务进程的用户
user = mysql
# 服务端口号,默认 3306
port = 3306
default_storage_engine = InnoDB
# 默认时区
default_time_zone = '+8:00'
character_set_server = utf8mb4
collation_server = utf8mb4_0900_ai_ci

# MySQL 服务 ID,单点服务时没必要设置
server-id = 1

# 事务隔离级别,默认为可重复读(REPEATABLE-READ)。(此级别下可能参数很多间隙锁,影响性能,但是修改又影响主从复制及灾难恢复,建议还是修改代码逻辑吧)
# 隔离级别可选项目:READ-UNCOMMITTED READ-COMMITTED REPEATABLE-READ SERIALIZABLE
transaction_isolation = REPEATABLE-READ

# 目录配置
# -------------------------------------------------------------------------------

# mysql 安装根目录
basedir = /usr/local/mysql-5.7.21

# mysql 数据文件所在目录
datadir = /var/lib/mysql

# 临时目录 比如 load data infile 会用到,一般都是使用/tmp
tmpdir = /tmp

# 数据库引擎配置
# -------------------------------------------------------------------------------

# mysql 5.1 之后,默认引擎是 InnoDB
default_storage_engine = InnoDB

# 内存临时表默认引擎,默认 InnoDB
default_tmp_storage_engine = InnoDB

# mysql 5.7 新增特性,磁盘临时表默认引擎,默认 InnoDB
internal_tmp_disk_storage_engine = InnoDB

# 字符集配置
# -------------------------------------------------------------------------------

# 数据库默认字符集,主流字符集支持一些特殊表情符号(特殊表情符占用 4 个字节)
character_set_server = utf8mb4

# 数据库字符集对应一些排序等规则,注意要和 character_set_server 对应
collation-server = utf8mb4_0900_ai_ci

# 设置 client 连接 mysql 时的字符集,防止乱码
# init_connect='SET NAMES utf8'

# 是否对 sql 语句大小写敏感,默认值为 0,1 表示不敏感
lower_case_table_names = 1

# 数据库连接配置
# -------------------------------------------------------------------------------

# 最大连接数,可设最大值 16384,一般考虑根据同时在线人数设置一个比较综合的数字,鉴于该数值增大并不太消耗系统资源,建议直接设 10000
# 如果在访问时经常出现 Too Many Connections 的错误提示,则需要增大该参数值
max_connections = 10000

# 默认值 100,最大错误连接数,如果有超出该参数值个数的中断错误连接,则该主机将被禁止连接。如需对该主机进行解禁,执行:FLUSH HOST
# 考虑高并发场景下的容错,建议加大。
max_connect_errors = 10000

# MySQL 打开的文件描述符限制,默认最小 1024;
# 当 open_files_limit 没有被配置的时候,比较 max_connections\*5 和 ulimit -n 的值,哪个大用哪个,
# 当 open_file_limit 被配置的时候,比较 open_files_limit 和 max_connections\*5 的值,哪个大用哪个。
# 注意:仍然可能出现报错信息 Can't create a new thread;此时观察系统 cat /proc/mysql 进程号/limits,观察进程 ulimit 限制情况
# 过小的话,考虑修改系统配置表,/etc/security/limits.conf 和 /etc/security/limits.d/90-nproc.conf
open_files_limit = 65535

# 超时配置
# -------------------------------------------------------------------------------

# MySQL 默认的 wait_timeout 值为 8 个小时,interactive_timeout 参数需要同时配置才能生效
# MySQL 连接闲置超过一定时间后(单位:秒,此处为 1800 秒)将会被强行关闭
interactive_timeout = 1800
wait_timeout = 1800

# 在 MySQL 暂时停止响应新请求之前的短时间内多少个请求可以被存在堆栈中
# 官方建议 back_log = 50 + (max_connections / 5),封顶数为 900
back_log = 900

# 数据库数据交换配置
# -------------------------------------------------------------------------------
# 该参数限制服务器端,接受的数据包大小,如果有 BLOB 子段,建议增大此值,避免写入或者更新出错。有 BLOB 子段,建议改为 1024M
max_allowed_packet = 128M

# 内存、cache 与 buffer 设置

# 内存临时表的最大值,默认 16M,此处设置成 64M
tmp_table_size = 64M

# 用户创建的内存表的大小,默认 16M,往往和 tmp_table_size 一起设置,限制用户临时表大小。
# 超限的话,MySQL 就会自动地把它转化为基于磁盘的 MyISAM 表,存储在指定的 tmpdir 目录下,增大 IO 压力,建议内存大,增大该数值。
max_heap_table_size = 64M

# 表示这个 mysql 版本是否支持查询缓存。ps:SHOW STATUS LIKE 'qcache%',与缓存相关的状态变量。
# have_query_cache

# 这个系统变量控制着查询缓存功能的开启和关闭,0 表示关闭,1 表示打开,2 表示只要 select 中明确指定 SQL_CACHE 才缓存。
# 看业务场景决定是否使用缓存,不使用,下面就不用配置了。
# MySQL8 不支持
query_cache_type = 0

# 默认值 1M,优点是查询缓存可以极大的提高服务器速度,如果你有大量的相同的查询并且很少修改表。
# 缺点:在你表经常变化的情况下或者如果你的查询原文每次都不同,查询缓存也许引起性能下降而不是性能提升。
# MySQL8 不支持
query_cache_size = 64M

# 只有小于此设定值的结果才会被缓冲,保护查询缓冲,防止一个极大的结果集将其他所有的查询结果都覆盖。
query_cache_limit = 2M

# 每个被缓存的结果集要占用的最小内存,默认值 4kb,一般不怎么调整。
# 如果 Qcache_free_blocks 值过大,可能是 query_cache_min_res_unit 值过大,应该调小些
# query_cache_min_res_unit 的估计值:(query_cache_size - Qcache_free_memory) / Qcache_queries_in_cache
query_cache_min_res_unit = 4kb

# 在一个事务中 binlog 为了记录 SQL 状态所持有的 cache 大小
# 如果你经常使用大的、多声明的事务,你可以增加此值来获取更大的性能。
# 所有从事务来的状态都将被缓冲在 binlog 缓冲中然后在提交后一次性写入到 binlog 中
# 如果事务比此值大,会使用磁盘上的临时文件来替代。
# 此缓冲在每个连接的事务第一次更新状态时被创建
binlog_cache_size = 1M

# 日志配置
# -------------------------------------------------------------------------------

# 日志文件相关设置,一般只开启三种日志,错误日志,慢查询日志,二进制日志。普通查询日志不开启。
# 普通查询日志,默认值 off,不开启
general_log = 0

# 普通查询日志存放地址
general_log_file = /usr/local/mysql-5.7.21/log/mysql-general.log

# 全局动态变量,默认 3,范围:1 ~ 3
# 表示错误日志记录的信息,1:只记录 error 信息;2:记录 error 和 warnings 信息;3:记录 error、warnings 和普通的 notes 信息。
log_error_verbosity = 2

# 错误日志文件地址
log_error = /usr/local/mysql-5.7.21/log/mysql-error.log

# 开启慢查询
slow_query_log = 1

# 开启慢查询时间,此处为 1 秒,达到此值才记录数据
long_query_time = 3

# 检索行数达到此数值,才记录慢查询日志中
min_examined_row_limit = 100

# mysql 5.6.5 新增,用来表示每分钟允许记录到 slow log 的且未使用索引的 SQL 语句次数,默认值为 0,不限制。
log_throttle_queries_not_using_indexes = 0

# 慢查询日志文件地址
slow_query_log_file = /var/log/mysql/mysql-slow.log

# 开启记录没有使用索引查询语句
log-queries-not-using-indexes = 1

# 开启二进制日志
log_bin = /usr/local/mysql-5.7.21/log/mysql-bin.log

# mysql 清除过期日志的时间,默认值 0,不自动清理,而是使用滚动循环的方式。
expire_logs_days = 0

# 如果二进制日志写入的内容超出给定值,日志就会发生滚动。你不能将该变量设置为大于 1GB 或小于 4096 字节。 默认值是 1GB。
max_binlog_size = 1000M

# binlog 的格式也有三种:STATEMENT,ROW,MIXED。mysql 5.7.7 后,默认值从 MIXED 改为 ROW
# 关于 binlog 日志格式问题,请查阅网络资料
binlog_format = row

# 表示每 N 次写入 binlog 后,持久化到磁盘,默认值 N=1
# 建议设置成 1,这样可以保证 MySQL 异常重启之后 binlog 不丢失。
# sync_binlog = 1

# InnoDB 引擎配置
# -------------------------------------------------------------------------------

# 说明:该参数可以提升扩展性和刷脏页性能。
# 默认值 1,建议值:4-8;并且必须小于 innodb_buffer_pool_instances
innodb_page_cleaners = 4

# 说明:一般 8k 和 16k 中选择,8k 的话,cpu 消耗小些,selcet 效率高一点,一般不用改
# 默认值:16k;建议值:不改,
innodb_page_size = 16384

# 说明:InnoDB 使用一个缓冲池来保存索引和原始数据,不像 MyISAM。这里你设置越大,你在存取表里面数据时所需要的磁盘 I/O 越少。
# 在一个独立使用的数据库服务器上,你可以设置这个变量到服务器物理内存大小的 60%-80%
# 注意别设置的过大,会导致 system 的 swap 空间被占用,导致操作系统变慢,从而减低 sql 查询的效率
# 默认值:128M,建议值:物理内存的 60%-80%
innodb_buffer_pool_size = 512M

# 说明:只有当设置 innodb_buffer_pool_size 值大于 1G 时才有意义,小于 1G,instances 默认为 1,大于 1G,instances 默认为 8
# 但是网络上有评价,最佳性能,每个实例至少 1G 大小。
# 默认值:1 或 8,建议值:innodb_buffer_pool_size/innodb_buffer_pool_instances >= 1G
innodb_buffer_pool_instances = 1

# 说明:mysql 5.7 新特性,defines the chunk size for online InnoDB buffer pool resizing operations。
# 实际缓冲区大小必须为 innodb_buffer_pool_chunk_size*innodb_buffer_pool_instances *倍数,取略大于 innodb_buffer_pool_size
# 默认值 128M,建议值:默认值就好,乱改反而容易出问题,它会影响实际 buffer pool 大小。
innodb_buffer_pool_chunk_size = 128M

# 在启动时把热数据加载到内存。默认值为 on,不修改
innodb_buffer_pool_load_at_startup = 1

# 在关闭时把热数据 dump 到本地磁盘。默认值为 on,不修改
innodb_buffer_pool_dump_at_shutdown = 1

# 说明:影响 Innodb 缓冲区的刷新算法,建议从小到大配置,直到 zero free pages;innodb_lru_scan_depth \* innodb_buffer_pool_instances defines the amount of work performed by the page cleaner thread each second。
# 默认值 1024,建议值: 未知
innodb_lru_scan_depth = 1024

# 说明:事务等待获取资源等待的最长时间,单位为秒,看具体业务情况,一般默认值就好
# 默认值:50,建议值:看业务。
innodb_lock_wait_timeout = 60

# 说明:设置了 MySQL 后台任务(例如页刷新和 merge dadta from buffer pool)每秒 io 操作的上限。
# 默认值:200,建议值:方法一,单盘 sata 设 100,sas10,raid10 设 200,ssd 设 2000,fushion-io 设 50000;方法二,通过测试工具获得磁盘 io 性能后,设置 IOPS 数值/2。
innodb_io_capacity = 2000

# 说明:该参数是所有缓冲区线程 io 操作的总上限。
# 默认值:innodb_io_capacity 的两倍。建议值:例如用 iometer 测试后的 iops 数值就好
innodb_io_capacity_max = 4000

# 说明:控制着 innodb 数据文件及 redo log 的打开、刷写模式,三种模式:fdatasync(默认),O_DSYNC,O_DIRECT
# fdatasync:数据文件,buffer pool->os buffer->磁盘;日志文件,buffer pool->os buffer->磁盘;
# O_DSYNC: 数据文件,buffer pool->os buffer->磁盘;日志文件,buffer pool->磁盘;
# O_DIRECT: 数据文件,buffer pool->磁盘; 日志文件,buffer pool->os buffer->磁盘;
# 默认值为空,建议值:使用 SAN 或者 raid,建议用 O_DIRECT,不懂测试的话,默认生产上使用 O_DIRECT
innodb_flush_method = O_DIRECT

# 说明:mysql5.7 之后默认开启,意思是,每张表一个独立表空间。
# 默认值 1,开启
innodb_file_per_table = 1

# 说明:The path where InnoDB creates undo tablespaces。通常等于 undo log 文件的存放目录。
# 默认值 ./; 自行设置
innodb_undo_directory = /usr/local/mysql-5.7.21/log

# 说明:The number of undo tablespaces used by InnoDB 等于 undo log 文件数量。5.7.21 后开始弃用
# 默认值为 0,建议默认值就好,不用调整了。
innodb_undo_tablespaces = 0

# 说明:定义 undo 使用的回滚段数量。5.7.19 后弃用
# 默认值 128,建议不动,以后弃用了。
innodb_undo_logs = 128

# 说明:5.7.5 后开始使用,在线收缩 undo log 使用的空间。
# 默认值:关闭,建议值:开启
innodb_undo_log_truncate = 1

# 说明:结合 innodb_undo_log_truncate,实现 undo 空间收缩功能
# 默认值:1G,建议值,不改。
innodb_max_undo_log_size = 1G

# 说明:重作日志文件的存放目录
innodb_log_group_home_dir = /usr/local/mysql-5.7.21/log

# 说明:日志文件的大小
# 默认值:48M,建议值:根据你系统的磁盘空间和日志增长情况调整大小
innodb_log_file_size = 128M

# 说明:日志组中的文件数量,mysql 以循环方式写入日志
# 默认值 2,建议值:根据你系统的磁盘空间和日志增长情况调整大小
innodb_log_files_in_group = 3

# 此参数确定些日志文件所用的内存大小,以 M 为单位。缓冲区更大能提高性能,但意外的故障将会丢失数据。MySQL 开发人员建议设置为 1-8M 之间
innodb_log_buffer_size = 16M

# 说明:可以控制 log 从系统 buffer 刷入磁盘文件的刷新频率,增大可减轻系统负荷
# 默认值是 1;建议值不改。系统性能一般够用。
innodb_flush_log_at_timeout = 1

# 说明:参数可设为 0,1,2;
# 参数 0:表示每秒将 log buffer 内容刷新到系统 buffer 中,再调用系统 flush 操作写入磁盘文件。
# 参数 1:表示每次事务提交,redo log 都直接持久化到磁盘。
# 参数 2:表示每次事务提交,隔 1 秒后再将 redo log 持久化到磁盘。
# 建议设置成 1,这样可以保证 MySQL 异常重启之后数据不丢失。
innodb_flush_log_at_trx_commit = 1

# 说明:限制 Innodb 能打开的表的数据,如果库里的表特别多的情况,请增加这个。
# 值默认是 2000,建议值:参考数据库表总数再进行调整,一般够用不用调整。
innodb_open_files = 8192

# innodb 处理 io 读写的后台并发线程数量,根据 cpu 核来确认,取值范围:1-64
# 默认值:4,建议值:与逻辑 cpu 数量的一半保持一致。
innodb_read_io_threads = 4
innodb_write_io_threads = 4

# 默认设置为 0,表示不限制并发数,这里推荐设置为 0,更好去发挥 CPU 多核处理能力,提高并发量
innodb_thread_concurrency = 0

# 默认值为 4,建议不变。InnoDB 中的清除操作是一类定期回收无用数据的操作。mysql 5.5 之后,支持多线程清除操作。
innodb_purge_threads = 4

# 说明:mysql 缓冲区分为 new blocks 和 old blocks;此参数表示 old blocks 占比;
# 默认值:37,建议值,一般不动
innodb_old_blocks_pct = 37

# 说明:新数据被载入缓冲池,进入 old pages 链区,当 1 秒后再次访问,则提升进入 new pages 链区。
# 默认值:1000
innodb_old_blocks_time=1000

# 说明:开启异步 io,可以提高并发性,默认开启。
# 默认值为 1,建议不动
innodb_use_native_aio = 1

# 说明:默认为空,使用 data 目录,一般不改。
innodb_data_home_dir=/usr/local/mysql-5.7.21/data

# 说明:Defines the name,size,and attributes of InnoDB system tablespace data files。
# 默认值,不指定,默认为 ibdata1:12M:autoextend
innodb_data_file_path = ibdata1:12M:autoextend

# 说明:设置了 InnoDB 存储引擎用来存放数据字典信息以及一些内部数据结构的内存空间大小,除非你的数据对象及其多,否则一般默认不改。
# innodb_additional_mem_pool_size = 16M
# 说明:The crash recovery mode。只有紧急情况需要恢复数据的时候,才改为大于 1-6 之间数值,含义查下官网。
# 默认值为 0;
#innodb_force_recovery = 0

# MyISAM 引擎配置
# -------------------------------------------------------------------------------

# 指定索引缓冲区的大小,为 MYISAM 数据表开启供线程共享的索引缓存,对 INNODB 引擎无效。相当影响 MyISAM 的性能。
# 不要将其设置大于你可用内存的 30%,因为一部分内存同样被 OS 用来缓冲行数据
# 甚至在你并不使用 MyISAM 表的情况下,你也需要仍旧设置起 8-64M 内存由于它同样会被内部临时磁盘表使用。
# 默认值 8M,建议值:对于内存在 4GB 左右的服务器该参数可设置为 256M 或 384M。注意:该参数值设置的过大反而会是服务器整体效率降低!
key_buffer_size = 64M

# 为每个扫描 MyISAM 的线程分配参数设置的内存大小缓冲区。
# 默认值 128kb,建议值:16G 内存建议 1M,4G:128kb 或者 256kb 吧
# 注意,该缓冲区是每个连接独占的,所以总缓冲区大小为 128kb *连接数;极端情况 128kb*maxconnectiosns,会超级大,所以要考虑日常平均连接数。
# 一般不需要太关心该数值,稍微增大就可以了,
read_buffer_size = 262144

# 支持任何存储引擎
# MySQL 的随机读缓冲区大小,适当增大,可以提高性能。
# 默认值 256kb;建议值:得参考连接数,16G 内存,有人推荐 8M
# 注意,该缓冲区是每个连接独占的,所以总缓冲区大小为 128kb *连接数;极端情况 128kb*maxconnectiosns,会超级大,所以要考虑日常平均连接数。
read_rnd_buffer_size = 1M

# order by 或 group by 时用到
# 支持所有引擎,innodb 和 myisam 有自己的 innodb_sort_buffer_size 和 myisam_sort_buffer_size 设置
# 默认值 256kb;建议值:得参考连接数,16G 内存,有人推荐 8M。
# 注意,该缓冲区是每个连接独占的,所以总缓冲区大小为 1M *连接数;极端情况 1M*maxconnectiosns,会超级大。所以要考虑日常平均连接数。
sort_buffer_size = 1M

# 此缓冲被使用来优化全联合 (full JOINs 不带索引的联合)
# 类似的联合在极大多数情况下有非常糟糕的性能表现,但是将此值设大能够减轻性能影响。
# 通过 “Select_full_join” 状态变量查看全联合的数量
# 注意,该缓冲区是每个连接独占的,所以总缓冲区大小为 1M *连接数;极端情况 1M*maxconnectiosns,会超级大。所以要考虑日常平均连接数。
# 默认值 256kb; 建议值:16G 内存,设置 8M。
join_buffer_size = 1M

# 缓存 linux 文件描述符信息,加快数据文件打开速度
# 它影响 myisam 表的打开关闭,但是不影响 innodb 表的打开关闭。
# 默认值 2000,建议值:根据状态变量 Opened_tables 去设定
table_open_cache = 2000

# 缓存表定义的相关信息,加快读取表信息速度
# 默认值 1400,最大值 2000,建议值:基本不改。
table_definition_cache = 1400

# 该参数是 myssql 5.6 后引入的,目的是提高并发。
# 默认值 1,建议值:cpu 核数,并且<=16
table_open_cache_instances = 2

# 当客户端断开之后,服务器处理此客户的线程将会缓存起来以响应下一个客户而不是销毁。可重用,减小了系统开销。
# 默认值为 9,建议值:两种取值方式,方式一,根据物理内存,1G —> 8;2G —> 16; 3G —> 32; >3G —> 64;
# 方式二,根据 show status like 'threads%',查看 Threads_connected 值。
thread_cache_size = 16

# 默认值 256k,建议值:16/32G 内存,512kb,其他一般不改变,如果报错:Thread stack overrun,就增大看看,
# 注意,每个线程分配内存空间,所以总内存空间。你懂得。
thread_stack = 512k

[client]
socket = /var/lib/mysql/mysql.sock
port = 3306
  • GENERAL

    • datadir - mysql 数据文件所在目录
    • socket - scoket 文件
    • pid_file - PID 文件
    • user - 启动 mysql 服务进程的用户
    • port - 服务端口号,默认 3306
    • default_storage_engine - mysql 5.1 之后,默认引擎是 InnoDB
    • default_time_zone - 默认时区。中国大部分地区在东八区,即 +8:00
    • character_set_server - 数据库默认字符集
    • collation_server - 数据库字符集对应一些排序等规则,注意要和 character_set_server 对应
  • LOG

    • log_error - 错误日志文件地址
    • slow_query_log - 错误日志文件地址
  • InnoDB

    • innodb_buffer_pool_size - InnoDB 使用一个缓冲池来保存索引和原始数据,不像 MyISAM。这里你设置越大,你在存取表里面数据时所需要的磁盘 I/O 越少。
      • 在一个独立使用的数据库服务器上,你可以设置这个变量到服务器物理内存大小的 60%-80%
      • 注意别设置的过大,会导致 system 的 swap 空间被占用,导致操作系统变慢,从而减低 sql 查询的效率
      • 默认值:128M,建议值:物理内存的 60%-80%
    • innodb_log_file_size - 日志文件的大小。默认值:48M,建议值:根据你系统的磁盘空间和日志增长情况调整大小
    • innodb_file_per_table - 说明:mysql5.7 之后默认开启,意思是,每张表一个独立表空间。默认值 1,开启。
    • innodb_flush_method - 说明:控制着 innodb 数据文件及 redo log 的打开、刷写模式,三种模式:fdatasync(默认),O_DSYNC,O_DIRECT。默认值为空,建议值:使用 SAN 或者 raid,建议用 O_DIRECT,不懂测试的话,默认生产上使用 O_DIRECT
      • fdatasync:数据文件,buffer pool->os buffer->磁盘;日志文件,buffer pool->os buffer->磁盘;
      • O_DSYNC: 数据文件,buffer pool->os buffer->磁盘;日志文件,buffer pool->磁盘;
      • O_DIRECT: 数据文件,buffer pool->磁盘; 日志文件,buffer pool->os buffer->磁盘;
  • MyIsam

    • key_buffer_size - 指定索引缓冲区的大小,为 MYISAM 数据表开启供线程共享的索引缓存,对 INNODB 引擎无效。相当影响 MyISAM 的性能。
      • 不要将其设置大于你可用内存的 30%,因为一部分内存同样被 OS 用来缓冲行数据
      • 甚至在你并不使用 MyISAM 表的情况下,你也需要仍旧设置起 8-64M 内存由于它同样会被内部临时磁盘表使用。
      • 默认值 8M,建议值:对于内存在 4GB 左右的服务器该参数可设置为 256M 或 384M。
      • 注意:该参数值设置的过大反而会是服务器整体效率降低!
  • OTHER

    • tmp_table_size - 内存临时表的最大值,默认 16M,此处设置成 128M
    • max_heap_table_size - 用户创建的内存表的大小,默认 16M,往往和 tmp_table_size 一起设置,限制用户临时表大小。超限的话,MySQL 就会自动地把它转化为基于磁盘的 MyISAM 表,存储在指定的 tmpdir 目录下,增大 IO 压力,建议内存大,增大该数值。
    • query_cache_type - 这个系统变量控制着查询缓存功能的开启和关闭,0 表示关闭,1 表示打开,2 表示只要 select 中明确指定 SQL_CACHE 才缓存。
    • query_cache_size - 默认值 1M,优点是查询缓存可以极大的提高服务器速度,如果你有大量的相同的查询并且很少修改表。缺点:在你表经常变化的情况下或者如果你的查询原文每次都不同,查询缓存也许引起性能下降而不是性能提升。
    • max_connections - 最大连接数,可设最大值 16384,一般考虑根据同时在线人数设置一个比较综合的数字,鉴于该数值增大并不太消耗系统资源,建议直接设 10000。如果在访问时经常出现 Too Many Connections 的错误提示,则需要增大该参数值
    • thread_cache - 当客户端断开之后,服务器处理此客户的线程将会缓存起来以响应下一个客户而不是销毁。可重用,减小了系统开销。默认值为 9,建议值:两种取值方式,
      • 方式一,根据物理内存,1G —> 8;2G —> 16; 3G —> 32; >3G —> 64;
      • 方式二,根据 show status like ‘threads%’,查看 Threads_connected 值。
    • open_files_limit - MySQL 打开的文件描述符限制,默认最小 1024;
      • 当 open_files_limit 没有被配置的时候,比较 max_connections*5 和 ulimit -n 的值,哪个大用哪个,
      • 当 open_file_limit 被配置的时候,比较 open_files_limit 和 max_connections*5 的值,哪个大用哪个
      • 注意:仍然可能出现报错信息 Can’t create a new thread;此时观察系统 cat /proc/mysql 进程号/limits,观察进程 ulimit 限制情况
      • 过小的话,考虑修改系统配置表,/etc/security/limits.conf/etc/security/limits.d/90-nproc.conf

MySQL FAQ

Too many connections

现象

尝试连接 MySQL 时,遇到 Too many connections 错误。

原因

数据库连接线程数超过最大值,访问被拒绝。

解决方案

如果实际连接线程数过大,可以考虑增加服务器节点来分流;如果实际线程数并不算过大,那么可以配置 max_connections 来增加允许的最大连接数。需要注意的是,连接数不宜过大,一般来说,单库每秒有 2000 个并发连接时,就可以考虑扩容了,健康的状态应该维持在每秒 1000 个并发连接左右。

(1)查看最大连接数

1
2
3
4
5
6
7
mysql> show variables like '%max_connections%';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| max_connections | 151 |
| mysqlx_max_connections | 100 |
+------------------------+-------+

(2)查看服务器响应的最大连接数

1
2
3
4
5
6
7
mysql> show global status like 'Max_used_connections';
+----------------------+-------+
| Variable_name | Value |
+----------------------+-------+
| Max_used_connections | 142 |
+----------------------+-------+
1 row in set (0.00 sec)

(3)临时设置最大连接数

1
set GLOBAL max_connections=256;

注意:当服务器重启时,最大连接数会被重置。

(4)永久设置最大连接数

修改 /etc/my.cnf 配置文件,在 [mysqld] 添加以下配置:

1
max_connections=256

重启 mysql 以生效

(5)修改 Linux 最大文件数限制

设置了最大连接数,如果还是没有生效,考虑检查一下 Linux 最大文件数

MySQL 最大连接数会受到最大文件数限制,vim /etc/security/limits.conf,添加 mysql 用户配置

1
2
mysql hard nofile 65535
mysql soft nofile 65535

(6)检查 LimitNOFILE

如果是使用 rpm 方式安装 mysql,检查 mysqld.service 文件中的 LimitNOFILE 是否配置的太小。

时区(time_zone)偏差

现象

数据库中存储的 Timestamp 字段值比真实值少了 13 个小时。

原因

  • 当 JDBC 与 MySQL 开始建立连接时,会获取服务器参数。
  • 当 MySQL 的 time_zone 值为 SYSTEM 时,会取 system_time_zone 值作为协调时区,若得到的是 CST 那么 Java 会误以为这是 CST -0500 ,因此会给出错误的时区信息(国内一般是CST +0800,即东八区)。

查看时区方法:

通过 show variables like '%time_zone%'; 命令查看 MySQL 时区配置:

1
2
3
4
5
6
7
mysql> show variables like '%time_zone%';
+------------------+--------+
| Variable_name | Value |
+------------------+--------+
| system_time_zone | CST |
| time_zone | SYSTEM |
+------------------+--------+

解决方案

方案一

1
2
3
4
5
mysql> set global time_zone = '+08:00';
Query OK, 0 rows affected (0.00 sec)

mysql> set time_zone = '+08:00';
Query OK, 0 rows affected (0.00 sec)

方案二

修改 my.cnf 文件,在 [mysqld] 节下增加 default-time-zone='+08:00' ,然后重启。

数据表损坏如何修复

使用 myisamchk 来修复,具体步骤:

  1. 修复前将 mysql 服务停止。
  2. 打开命令行方式,然后进入到 mysql 的 bin 目录。
  3. 执行 myisamchk –recover 数据库所在路 /*.MYI

使用 repair table 或者 OPTIMIZE table 命令来修复,REPAIR TABLE table_name 修复表 OPTIMIZE TABLE table_name 优化表 REPAIR TABLE 用于修复被破坏的表。 OPTIMIZE TABLE 用于回收闲置的数据库空间,当表上的数据行被删除时,所占据的磁盘空间并没有立即被回收,使用了 OPTIMIZE TABLE 命令后这些空间将被回收,并且对磁盘上的数据行进行重排(注意:是磁盘上,而非数据库)

数据结构

问题现象:ERROR 1071: Specified key was too long; max key length is 767 bytes

问题原因:MySQL 默认情况下单个列的索引不能超过 767 位(不同版本可能存在差异) 。

解决方法:优化索引结构,索引字段不宜过长。

MySQL 运维脚本

这里推荐我写的几个一键运维脚本,非常方便,欢迎使用:

参考资料

Iptables 应用

iptables 是一个配置 Linux 内核 防火墙 的命令行工具,是 netfilter 项目的一部分。 可以直接配置,也可以通过许多前端和图形界面配置。

iptables 也经常代指该内核级防火墙。iptables 用于 ipv4ip6tables 用于 ipv6

nftables 已经包含在 Linux kernel 3.13 中,以后会取代 iptables 成为主要的 Linux 防火墙工具。

环境:CentOS7

简介

iptables 可以检测、修改、转发、重定向和丢弃 IPv4 数据包

过滤 IPv4 数据包的代码已经内置于内核中,并且按照不同的目的被组织成 的集合。 由一组预先定义的 组成,包含遍历顺序规则。每一条规则包含一个谓词的潜在匹配和相应的动作(称为 目标),如果谓词为真,该动作会被执行。也就是说条件匹配。

安装 iptables

(1)禁用 firewalld

CentOS 7 上默认安装了 firewalld 作为防火墙,使用 iptables 建议关闭并禁用 firewalld。

1
2
systemctl stop firewalld
systemctl disable firewalld

(2)安装 iptables

1
yum install -y iptables-services

(3)服务管理

  • 查看服务状态:systemctl status iptables
  • 启用服务:systemctl enable iptables
  • 禁用服务:systemctl disable iptables
  • 启动服务:systemctl start iptables
  • 重启服务:systemctl restart iptables
  • 关闭服务: systemctl stop iptables

命令

基本语法:

1
iptables(选项)(参数)

基本选项说明:

参数 作用
-P 设置默认策略:iptables -P INPUT (DROP
-F 清空规则链
-L 查看规则链
-A 在规则链的末尾加入新规则
-I num 在规则链的头部加入新规则
-D num 删除某一条规则
-s 匹配来源地址 IP/MASK,加叹号”!”表示除这个 IP 外。
-d 匹配目标地址
-i 网卡名称 匹配从这块网卡流入的数据
-o 网卡名称 匹配从这块网卡流出的数据
-p 匹配协议,如 tcp,udp,icmp
–dport num 匹配目标端口号
–sport num 匹配来源端口号

顺序:

1
iptables -t 表名 <-A/I/D/R> 规则链名 [规则号] <-i/o 网卡名> -p 协议名 <-s 源IP/源子网> --sport 源端口 <-d 目标IP/目标子网> --dport 目标端口 -j 动作

iptables 示例

清空当前的所有规则和计数

1
2
3
iptables -F  # 清空所有的防火墙规则
iptables -X # 删除用户自定义的空链
iptables -Z # 清空计数

配置允许 ssh 端口连接

1
2
iptables -A INPUT -s 192.168.1.0/24 -p tcp --dport 22 -j ACCEPT
# 22为你的ssh端口, -s 192.168.1.0/24表示允许这个网段的机器来连接,其它网段的ip地址是登陆不了你的机器的。 -j ACCEPT表示接受这样的请求

允许本地回环地址可以正常使用

1
2
3
iptables -A INPUT -i lo -j ACCEPT
#本地圆环地址就是那个127.0.0.1,是本机上使用的,它进与出都设置为允许
iptables -A OUTPUT -o lo -j ACCEPT

设置默认的规则

1
2
3
iptables -P INPUT DROP # 配置默认的不让进
iptables -P FORWARD DROP # 默认的不允许转发
iptables -P OUTPUT ACCEPT # 默认的可以出去

配置白名单

1
2
3
iptables -A INPUT -p all -s 192.168.1.0/24 -j ACCEPT  # 允许机房内网机器可以访问
iptables -A INPUT -p all -s 192.168.140.0/24 -j ACCEPT # 允许机房内网机器可以访问
iptables -A INPUT -p tcp -s 183.121.3.7 --dport 3380 -j ACCEPT # 允许183.121.3.7访问本机的3380端口

开启相应的服务端口

1
2
3
iptables -A INPUT -p tcp --dport 80 -j ACCEPT # 开启80端口,因为web对外都是这个端口
iptables -A INPUT -p icmp --icmp-type 8 -j ACCEPT # 允许被ping
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT # 已经建立的连接得让它进来

保存规则到配置文件中

1
2
3
cp /etc/sysconfig/iptables /etc/sysconfig/iptables.bak # 任何改动之前先备份,请保持这一优秀的习惯
iptables-save > /etc/sysconfig/iptables
cat /etc/sysconfig/iptables

列出已设置的规则

iptables -L [-t 表名][链名]

  • 四个表名 rawnatfiltermangle
  • 五个规则链名 INPUTOUTPUTFORWARDPREROUTINGPOSTROUTING
  • filter 表包含INPUTOUTPUTFORWARD三个规则链
1
2
3
4
5
6
iptables -L -t nat                  # 列出 nat 上面的所有规则
# ^ -t 参数指定,必须是 raw, nat,filter,mangle 中的一个
iptables -L -t nat --line-numbers # 规则带编号
iptables -L INPUT

iptables -L -nv # 查看,这个列表看起来更详细

清除已有规则

1
2
3
4
iptables -F INPUT  # 清空指定链 INPUT 上面的所有规则
iptables -X INPUT # 删除指定的链,这个链必须没有被其它任何规则引用,而且这条上必须没有任何规则。
# 如果没有指定链名,则会删除该表中所有非内置的链。
iptables -Z INPUT # 把指定链,或者表中的所有链上的所有计数器清零。

删除已添加的规则

1
2
# 添加一条规则
iptables -A INPUT -s 192.168.1.5 -j DROP

将所有 iptables 以序号标记显示,执行:

1
iptables -L -n --line-numbers

比如要删除 INPUT 里序号为 8 的规则,执行:

1
iptables -D INPUT 8

开放指定的端口

1
2
3
4
5
6
7
8
9
iptables -A INPUT -s 127.0.0.1 -d 127.0.0.1 -j ACCEPT               #允许本地回环接口(即运行本机访问本机)
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT #允许已建立的或相关连的通行
iptables -A OUTPUT -j ACCEPT #允许所有本机向外的访问
iptables -A INPUT -p tcp --dport 22 -j ACCEPT #允许访问22端口
iptables -A INPUT -p tcp --dport 80 -j ACCEPT #允许访问80端口
iptables -A INPUT -p tcp --dport 21 -j ACCEPT #允许ftp服务的21端口
iptables -A INPUT -p tcp --dport 20 -j ACCEPT #允许FTP服务的20端口
iptables -A INPUT -j reject #禁止其他未允许的规则访问
iptables -A FORWARD -j REJECT #禁止其他未允许的规则访问

屏蔽 IP

1
2
3
4
5
iptables -A INPUT -p tcp -m tcp -s 192.168.0.8 -j DROP  # 屏蔽恶意主机(比如,192.168.0.8
iptables -I INPUT -s 123.45.6.7 -j DROP #屏蔽单个IP的命令
iptables -I INPUT -s 123.0.0.0/8 -j DROP #封整个段即从123.0.0.1到123.255.255.254的命令
iptables -I INPUT -s 124.45.0.0/16 -j DROP #封IP段即从123.45.0.1到123.45.255.254的命令
iptables -I INPUT -s 123.45.6.0/24 -j DROP #封IP段即从123.45.6.1到123.45.6.254的命令是

指定数据包出去的网络接口

只对 OUTPUT,FORWARD,POSTROUTING 三个链起作用。

1
iptables -A FORWARD -o eth0

查看已添加的规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
iptables -L -n -v
Chain INPUT (policy DROP 48106 packets, 2690K bytes)
pkts bytes target prot opt in out source destination
5075 589K ACCEPT all -- lo * 0.0.0.0/0 0.0.0.0/0
191K 90M ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
1499K 133M ACCEPT tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp dpt:80
4364K 6351M ACCEPT all -- * * 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
6256 327K ACCEPT icmp -- * * 0.0.0.0/0 0.0.0.0/0

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination

Chain OUTPUT (policy ACCEPT 3382K packets, 1819M bytes)
pkts bytes target prot opt in out source destination
5075 589K ACCEPT all -- * lo 0.0.0.0/0 0.0.0.0/0

启动网络转发规则

公网210.14.67.7让内网192.168.188.0/24上网

1
iptables -t nat -A POSTROUTING -s 192.168.188.0/24 -j SNAT --to-source 210.14.67.127

端口映射

本机的 2222 端口映射到内网 虚拟机的 22 端口

1
iptables -t nat -A PREROUTING -d 210.14.67.127 -p tcp --dport 2222  -j DNAT --to-dest 192.168.188.115:22

字符串匹配

比如,我们要过滤所有 TCP 连接中的字符串test,一旦出现它我们就终止这个连接,我们可以这么做:

1
2
3
4
5
6
7
8
9
10
11
12
iptables -A INPUT -p tcp -m string --algo kmp --string "test" -j REJECT --reject-with tcp-reset
iptables -L

# Chain INPUT (policy ACCEPT)
# target prot opt source destination
# REJECT tcp -- anywhere anywhere STRING match "test" ALGO name kmp TO 65535 reject-with tcp-reset
#
# Chain FORWARD (policy ACCEPT)
# target prot opt source destination
#
# Chain OUTPUT (policy ACCEPT)
# target prot opt source destination

阻止 Windows 蠕虫的攻击

1
iptables -I INPUT -j DROP -p tcp -s 0.0.0.0/0 -m string --algo kmp --string "cmd.exe"

防止 SYN 洪水攻击

1
iptables -A INPUT -p tcp --syn -m limit --limit 5/second -j ACCEPT

参考资料

Spring Framework 综述

Spring Framework 简介

Spring Framework 是最受欢迎的企业级 Java 应用程序开发框架。用于构建企业级应用的轻量级、一站式解决方案。

当谈论到大小和透明度时, Spring 是轻量级的。 Spring 框架的基础版本是在 2 MB 左右的。

Spring 框架的核心特性可以用于开发任何 Java 应用程序,但是在 Java EE 平台上构建 web 应用程序是需要扩展的。 Spring 框架的目标是使 J2EE 开发变得更容易使用,通过启用基于 POJO 编程模型来促进良好的编程实践。

Spring Framework 设计理念如下:

  • 力争让选择无处不在
  • 体现海纳百川的精神
  • 保持后向兼容性
  • 专注 API 设计
  • 追求严苛的代码质量

为什么使用 Spring

下面列出的是使用 Spring 框架主要的好处:

  • Spring 可以使开发人员使用 POJOs 开发企业级的应用程序。只使用 POJOs 的好处是你不需要一个 EJB 容器产品,比如一个应用程序服务器,但是你可以选择使用一个健壮的 servlet 容器,比如 Tomcat 或者一些商业产品。
  • Spring 在一个单元模式中是有组织的。即使包和类的数量非常大,你只需要选择你需要的部分,而忽略剩余的那部分。
  • Spring 不会让你白费力气做重复工作,它真正的利用了一些现有的技术,像几个 ORM 框架、日志框架、JEE、Quartz 和 JDK 计时器,其他视图技术。
  • 测试一个用 Spring 编写的应用程序很容易,因为 environment-dependent 代码被放进了这个框架中。此外,通过使用 JavaBean-style POJOs,它在使用依赖注入注入测试数据时变得更容易。
  • Spring 的 web 框架是一个设计良好的 web MVC 框架,它为 web 框架,比如 Structs 或者其他工程上的或者很少受欢迎的 web 框架,提供了一个很好的供替代的选择。
  • 为将特定技术的异常(例如,由 JDBC、Hibernate,或者 JDO 抛出的异常)翻译成一致的, Spring 提供了一个方便的 API,而这些都是未经检验的异常。
  • 轻量级的 IOC 容器往往是轻量级的,例如,特别是当与 EJB 容器相比的时候。这有利于在内存和 CPU 资源有限的计算机上开发和部署应用程序。
  • Spring 提供了一个一致的事务管理界面,该界面可以缩小成一个本地事务(例如,使用一个单一的数据库)和扩展成一个全局事务(例如,使用 JTA)。

核心思想

Spring 最核心的两个技术思想是:IoC 和 Aop

IoC

IoCInversion of Control ,意为控制反转。

Spring 最认同的技术是控制反转的依赖注入(DI)模式。控制反转(IoC)是一个通用的概念,它可以用许多不同的方式去表达,依赖注入仅仅是控制反转的一个具体的例子。

当编写一个复杂的 Java 应用程序时,应用程序类应该尽可能的独立于其他的 Java 类来增加这些类可重用可能性,当进行单元测试时,可以使它们独立于其他类进行测试。依赖注入(或者有时被称为配线)有助于将这些类粘合在一起,并且在同一时间让它们保持独立。

到底什么是依赖注入?让我们将这两个词分开来看一看。这里将依赖关系部分转化为两个类之间的关联。例如,类 A 依赖于类 B。现在,让我们看一看第二部分,注入。所有这一切都意味着类 B 将通过 IoC 被注入到类 A 中。

依赖注入可以以向构造函数传递参数的方式发生,或者通过使用 setter 方法 post-construction。由于依赖注入是 Spring 框架的核心部分,所以我将在一个单独的章节中利用很好的例子去解释这一概念。

Aop

Spring 框架的一个关键组件是面向方面的程序设计(AOP)框架。一个程序中跨越多个点的功能被称为横切关注点,这些横切关注点在概念上独立于应用程序的业务逻辑。有各种各样常见的很好的关于方面的例子,比如日志记录、声明性事务、安全性,和缓存等等。

在 OOP 中模块化的关键单元是类,而在 AOP 中模块化的关键单元是方面。AOP 帮助你将横切关注点从它们所影响的对象中分离出来,然而依赖注入帮助你将你的应用程序对象从彼此中分离出来。

Spring 框架的 AOP 模块提供了面向方面的程序设计实现,允许你定义拦截器方法和切入点,可以实现将应该被分开的代码干净的分开功能。我将在一个独立的章节中讨论更多关于 Spring AOP 的概念。

Spring 体系结构

Spring 当前框架有20个 jar 包,大致可以分为6大模块:

    1. 为什么使用 Spring
    1. 核心思想
    • 2.1. IoC
    • 2.2. Aop
    1. Spring 体系结构
    • 3.1. Core Container
      • 3.1.1. BeanFactory
      • 3.1.2. ApplicationContext
    • 3.2. AOP and Instrumentation
    • 3.3. Messaging
    • 3.4. Data Access / Integaration
    • 3.5. Web
    • 3.6. Test
    1. 术语

Spring 框架提供了非常丰富的功能,因此整个架构也很庞大。
在我们实际的应用开发中,并不一定要使用所有的功能,而是可以根据需要选择合适的 Spring 模块。

img

Core Container

IoC 容器是 Spring 框架的核心。spring 容器使用依赖注入管理构成应用的组件,它会创建相互协作的组件之间的关联。毫无疑问,这些对象更简单干净,更容易理解,也更容易重用和测试。
Spring 自带了几种容器的实现,可归纳为两种类型:

BeanFactory

由 org.springframework.beans.factory.BeanFactory 接口定义。
它是最简单的容器,提供基本的 DI 支持。

ApplicationContext

由 org.springframework.context.ApplicationContext 接口定义。
它是基于 BeanFactory 之上构建,并提供面向应用的服务,例如从属性文件解析文本信息的能力,以及发布应用事件给感兴趣的事件监听者的能力。
注:Bean 工厂对于大多数应用来说往往太低级了,所以应用上下文使用更广泛。推荐在开发中使用应用上下文容器。

Spring 自带了多种应用上下文,最可能遇到的有以下几种:
ClassPathXmlApplicationContext:从类路径下的 XML 配置文件中加载上下文定义,把应用上下文定义文件当做类资源。
FileSystemXmlApplicationContext:读取文件系统下的 XML 配置文件并加载上下文定义。
XmlWebApplicationContext:读取 Web 应用下的 XML 配置文件并装载上下文定义。

范例

1
2
ApplicationContext context = new FileSystemXmlApplicationContext("D:\Temp\build.xml");
ApplicationContext context2 = new ClassPathXmlApplicationContext("build.xml");

可以看到,加载 FileSystemXmlApplicationContextClassPathXmlApplicationContext 十分相似。
差异在于:前者在指定文件系统路径下查找 build.xml 文件;而后在所有类路径(包含 JAR 文件)下查找 build.xml 文件。
通过引用应用上下文,可以很方便的调用 getBean() 方法从 Spring 容器中获取 Bean。

相关 jar 包

  • spring-core, spring-beans, 提供框架的基础部分,包括 IoC 和依赖注入特性。

  • spring-context, 在spring-core, spring-beans基础上构建。它提供一种框架式的访问对象的方法。它也支持类似 Java EE 特性,例如:EJB,JMX 和基本 remoting。ApplicationContext 接口是它的聚焦点。

  • springcontext-support, 集成第三方库到 Spring application context。

  • spring-expression,提供一种强有力的表达语言在运行时来查询和操纵一个对象图。

AOP and Instrumentation

相关 jar 包

  • spring-aop,提供了对面向切面编程的丰富支持。
  • spring-aspects,提供了对 AspectJ 的集成。
  • spring-instrument,提供了对类 instrumentation 的支持和类加载器。
  • spring-instrument-tomcat,包含了 Spring 对 Tomcat 的 instrumentation 代理。

Messaging

相关 jar 包

  • spring-messaging,包含 spring 的消息处理功能,如 Message,MessageChannel,MessageHandler。

Data Access / Integaration

Data Access/Integration 层包含了 JDBC / ORM / OXM / JMS 和 Transaction 模块。

相关 jar 包

  • spring-jdbc,提供了一个 JDBC 抽象层。

  • spring-tx,支持编程和声明式事务管理类。

  • spring-orm,提供了流行的对象关系型映射 API 集,如 JPA,JDO,Hibernate。

  • spring-oxm,提供了一个抽象层以支持对象/XML 映射的实现,如 JAXB,Castor,XMLBeans,JiBX 和 XStream.

  • spring-jms,包含了生产和消费消息的功能。

Web

相关 jar 包

  • spring-web,提供了基本的面向 web 的功能,如多文件上传、使用 Servlet 监听器的 Ioc 容器的初始化。一个面向 web 的应用层上下文。

  • spring-webmvc,包括 MVC 和 REST web 服务实现。

  • spring-webmvc-portlet,提供在 Protlet 环境的 MVC 实现和spring-webmvc功能的镜像。

Test

相关 jar 包

  • spring-test,以 Junit 和 TestNG 来支持 spring 组件的单元测试和集成测试。

术语

  • 应用程序:是能完成我们所需要功能的成品,比如购物网站、OA 系统。
  • 框架:是能完成一定功能的半成品,比如我们可以使用框架进行购物网站开发;框架做一部分功能,我们自己做一部分功能,这样应用程序就创建出来了。而且框架规定了你在开发应用程序时的整体架构,提供了一些基础功能,还规定了类和对象的如何创建、如何协作等,从而简化我们开发,让我们专注于业务逻辑开发。
  • 非侵入式设计:从框架角度可以这样理解,无需继承框架提供的类,这种设计就可以看作是非侵入式设计,如果继承了这些框架类,就是侵入设计,如果以后想更换框架之前写过的代码几乎无法重用,如果非侵入式设计则之前写过的代码仍然可以继续使用。
  • 轻量级及重量级:轻量级是相对于重量级而言的,轻量级一般就是非入侵性的、所依赖的东西非常少、资源占用非常少、部署简单等等,其实就是比较容易使用,而重量级正好相反。
  • POJO:POJO(Plain Old Java Objects)简单的 Java 对象,它可以包含业务逻辑或持久化逻辑,但不担当任何特殊角色且不继承或不实现任何其它 Java 框架的类或接口。
  • 容器:在日常生活中容器就是一种盛放东西的器具,从程序设计角度看就是装对象的的对象,因为存在放入、拿出等操作,所以容器还要管理对象的生命周期。
  • 控制反转:即 Inversion of Control,缩写为 IoC,控制反转还有一个名字叫做依赖注入(Dependency Injection),就是由容器控制程序之间的关系,而非传统实现中,由程序代码直接操控。
  • JavaBean:一般指容器管理对象,在 Spring 中指 Spring IoC 容器管理对象。

计算机网络面试总结

如果你不是从事于通信领域,面试时问及计算机网络的知识,一般也就限定在:HTTP(含 HTTPS、Cookie、Session)、TCP、UDP、Socket 这些

综合

计算机网络如何分层?

❓ 问题:计算机网络如何分层?各层的作用是什么?各层的主要协议、设备分别是什么?

这是学习计算机网络知识宏观层面必须要了解的核心点。知道了这些,对于网络的体系结构就基本上了解了。

img

计算机网络分层一般有三种划分体系:OSI 分层;五层协议分层;TCP/IP 协议分层。

  • OSI 的七层体系结构概念清楚,理论完整,但是比较复杂且不实用,所以并不流行。
  • 五层协议分层是一种折中方案,在现实中更为流行。

img

物理层

物理层(Physical Layer)只接收和发送一串比特(bit)流,不考虑信息的意义和信息结构。

扩展阅读:计算机网络之物理层

  • 关键词:调制、解调、数字信号、模拟信号、通信媒介、信道复用
  • 数据单元:比特流。
  • 典型设备:光纤、同轴电缆、双绞线、中继器和集线器。

数据链路层

网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,数据链路层(Data Link Layer)就是为同一链路的主机提供数据传输服务。数据链路层把网络层传下来的分组封装成帧。

扩展阅读:计算机网络之数据链路层

  • 关键词:点对点信道、广播信道、PPPCSMA/CD、局域网、以太网、MAC、适配器、集线器、网桥、交换机
  • 主要协议:PPPCSMA/CD 等。
  • 数据单元:帧(frame)。
  • 典型设备:二层交换机、网桥、网卡。

网络层

网络层(network layer)为分组交换网上的不同主机提供通信服务。在发送数据时,网络层把运输层产生的报文段或用户数据报封装成分组或包进行传送。

扩展阅读:计算机网络之网络层

  • 关键词:IPICMPARP、路由
  • 主要协议:IP
  • 数据单元:IP 数据报(packet)。
  • 典型设备:网关、路由器。

传输层

传输层(transport layer)为两台主机中进程间的通信提供通用的数据传输服务。

扩展阅读:计算机网络之传输层

  • 关键词:UDPTCP、滑动窗口、拥塞控制、三次握手
  • 主要协议:TCPUDP
  • 数据单元:报文段(segment)或用户数据报。

\会话层~~

~~会话层(Session Layer)不参与具体的传输,它提供包括访问验证和会话管理在内的建立和维护应用之间通信的机制。~~

\表示层~~

~~表示层(Presentation Layer)是为在应用过程之间传送的信息提供表示方法的服务,它关心的只是发出信息的语法与语义。表示层要完成某些特定的功能,主要有不同数据编码格式的转换,提供数据压缩、解压缩服务,对数据进行加密、解密。~~

应用层

应用层(application layer)通过应用进程间的交互来完成特定网络应用。应用层协议定义的是应用进程间通信和交互的规则。

扩展阅读:计算机网络之应用层

  • 关键词:HTTPDNSFTPTELNETDHCP
  • 主要协议:HTTPDNSSMTPTelnetFTPSNMP 等。
  • 数据单元:报文(message)。

HTTP

扩展阅读:超文本传输协议 HTTP

DNS

扩展阅读:域名系统协议 DNS

TCP/UDP

扩展阅读:传输控制协议 TCP用户数据报协议 UDP

什么是 TCP?

TCP(Transmission Control Protocol),即传输控制协议,它是一种面向连接的可靠的基于字节流的传输层通信协议

TCP 的特性是什么?

  • 面向连接的 - 面向连接是指 TCP 需要通过三次握手、四次挥手原则建立和断开双向连接。
  • 可靠的 - 可靠是指 TCP 传输的数据包保证以原始顺序到达目的地,且数据包不被损坏。为了实现这点,TCP 通过以下技术来保证:
    • 数据包的序列号和校验码
    • 确认包和自动重传
      • 如果发送者没有收到正确的响应,它将重新发送数据包。如果多次超时,连接就会断开。
      • TCP 实行流量控制和拥塞控制。这些确保措施会导致延迟,而且通常导致传输效率比 UDP 低。
  • 基于字节流的
    • 虽然应用程序和 TCP 的交互是一次一个数据块(大小不等),但 TCP 把应用程序看成是一连串的无结构的字节流。TCP 有一个缓冲,当应用程序传送的数据块太长,TCP 就可以把它划分短一些再传送。如果应用程序一次只发送一个字节,TCP 也可以等待积累有足够多的字节后再构成报文段发送出去。
    • 在 TCP 建立连接前两次握手的 SYN 报文中选项字段的 MSS 值,通信双方商定通信的最大报文长度。如果应用层交付下来的数据过大,就会对数据分段,然后发送;否则通过滑动窗口来控制通信双发的数据。

TCP 三次握手

❓ 问题:三次握手有什么用?什么是三次握手?为什么需要三次握手?

(1)三次握手有什么用?

  • 三次握手负责建立 TCP 双向连接。

(2)什么是三次握手?

img

如上图所示,三次握手流程如下:

  1. 第一次握手 - 客户端向服务端发送带有 SYN 标志的数据包。
  2. 第二次握手 - 服务端向客户端发送带有 SYN/ACK 标志的数据包。
  3. 第三次握手 - 客户端向服务端发送带有带有 ACK 标志的数据包。

至此,TCP 三次握手完成,客户端与服务端已建立双向连接。

💡 说明:SYN 为 synchronize 的缩写,ACK 为 acknowledgment 的缩写。

(3)为什么需要三次握手?

为了便于说明,假设客户端为 A, 服务端为 B。

  1. 第一次握手,A 向 B 发同步消息。B 收到消息后,B 认为:A 发消息没问题;B 收消息没问题。
  2. 第二次握手,B 向 A 发同步消息和确认消息。A 收到消息后,A 认为:A 发消息、收消息都没问题;B 发消息、收消息都没问题。但是,此时 B 不确定自己发消息是否没问题,所以就需要第三次握手。
  3. 第三次握手,A 向 B 发确认消息。B 收到消息后。B 认为:B 发消息没问题。

TCP 四次挥手

❓ 问题:四次挥手有什么用?什么是四次挥手?为什么建立连接是三次握手,关闭连接确是四次挥手呢?

(1)四次挥手有什么用?

  • 四次挥手负责断开 TCP 连接。

(2)什么是四次挥手?

如上图所示,四次挥手流程如下:

img

  1. 第一次挥手 - 客户端向服务端发送一个 FIN 包,用来关闭客户端到服务端的数据传送。
  2. 第二次挥手 - 服务端收到这个 FIN 包,向客户端发送一个 ACK 包,确认序号为收到的序号加 1。和 SYN 一样,一个 FIN 将占用一个序号。
  3. 第三次挥手 - 服务端关闭与客户端的连接,向客户端发送一个 FIN 包。
  4. 第四次挥手 - 客户端向服务端发送 ACK 包,并将确认序号设置为收到序号加 1。

(3)为什么建立连接是三次握手,关闭连接确是四次挥手呢?

  • 建立连接的时候, 服务器在 LISTEN 状态下,收到建立连接请求的 SYN 报文后,把 ACK 和 SYN 放在一个报文里发送给客户端。
  • 而关闭连接时,服务器收到对方的 FIN 报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送 FIN 报文给对方来表示同意现在关闭连接,因此,己方 ACK 和 FIN 一般都会分开发送,从而导致多了一次。

TCP 滑动窗口

❓ 问题:什么是滑动窗口?滑动窗口原理是什么?

什么是滑动窗口?

滑动窗口是 TCP 的一种控制网络流量的技术。

TCP 必需要解决的可靠传输以及包乱序(reordering)的问题,所以,TCP 必需要知道网络实际的数据处理带宽或是数据处理速度,这样才不会引起网络拥塞,导致丢包。

TCP 头里有一个字段叫 Window,又叫 Advertised-Window,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来

滑动窗口原理是什么?

img

  1. 已发送已确认 - 数据流中最早的字节已经发送并得到确认。这些数据是站在发送端的角度来看的。上图中的 31 个字节已经发送并确认。
  2. 已发送但尚未确认 - 已发送但尚未得到确认的字节。发送方在确认之前,不认为这些数据已经被处理。上图中的 32 ~ 45 字节为第 2 类。
  3. 未发送而接收方已 Ready - 设备尚未将数据发出 ,但接收方根据最近一次关于发送方一次要发送多少字节确认自己有足够空间。发送方会立即尝试发送。上图中的 46 ~ 51 字节为第 3 类。
  4. 未发送而接收方 Not Ready - 由于接收方 not ready,还不允许将这部分数据发出。上图中的 52 以后的字节为第 4 类。

img

这张图片相对于上一张图片,滑动窗口偏移了 5 个字节,意味着有 5 个已发送的字节得到了确认。

TCP 重传机制

❓ 问题:为什么需要重传机制?TCP 有哪些重传机制,原理是什么?

TCP 要保证所有的数据包都可以到达,所以,必需要有重传机制。

TCP 重传机制主要有两种:

  • 超时重传机制
  • 快速重传机制

(1)超时重传机制

超时重传机制是指:发送数据包在一定的时间周期内没有收到相应的 ACK,等待一定的时间,超时之后就认为这个数据包丢失,就会重新发送。这个等待时间被称为 RTO(Retransmission TimeOut),即重传超时时间。

没有确认的数据包不会从窗口中移走,定时器在重传时间到期内,每个片段的位置不变。

这种机制的重点是 RTO 的设置:

  • RTO 设长了,重发就慢,丢了老半天才重发,没有效率,性能差;
  • RTO 设短了,会导致可能并没有丢就重发。于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发

(2)快速重传机制

快速重传机制,实现了另外的一种丢包评定标准,即如果连续收到 3 次重复 ACK,发送方就认为这个 seq 的包丢失了,立刻进行重传。

当接收方收到乱序片段时,需要重复发送 ACK。

SpringBoot 之发送邮件

简介

Spring Boot 收发邮件最简便方式是通过 spring-boot-starter-mail

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

spring-boot-starter-mail 本质上是使用 JavaMail(javax.mail)。如果想对 JavaMail 有进一步了解,可以参考: JavaMail 使用指南

API

Spring Framework 提供了一个使用 JavaMailSender 接口发送电子邮件的简单抽象,这是发送邮件的核心 API。

JavaMailSender 接口提供的 API 如下:

img

配置

Spring Boot 为 JavaMailSender 提供了自动配置以及启动器模块。

如果 spring.mail.host 和相关库(由 spring-boot-starter-mail 定义)可用,则 Spring Boot 会创建默认 JavaMailSender(如果不存在)。可以通过 spring.mail 命名空间中的配置项进一步自定义发件人。
特别是,某些默认超时值是无限的,您可能希望更改它以避免线程被无响应的邮件服务器阻塞,如以下示例所示:

1
2
3
spring.mail.properties.mail.smtp.connectiontimeout=5000
spring.mail.properties.mail.smtp.timeout=3000
spring.mail.properties.mail.smtp.writetimeout=5000

也可以使用 JNDI 中的现有会话配置 JavaMailSender

1
spring.mail.jndi-name=mail/Session

以下为 Spring Boot 关于 Mail 的配置:

有关更多详细信息,请参阅 MailProperties

1
2
3
4
5
6
7
8
9
10
# Email (MailProperties)
spring.mail.default-encoding=UTF-8 # Default MimeMessage encoding.
spring.mail.host= # SMTP server host. For instance, `smtp.example.com`.
spring.mail.jndi-name= # Session JNDI name. When set, takes precedence over other Session settings.
spring.mail.password= # Login password of the SMTP server.
spring.mail.port= # SMTP server port.
spring.mail.properties.*= # Additional JavaMail Session properties.
spring.mail.protocol=smtp # Protocol used by the SMTP server.
spring.mail.test-connection=false # Whether to test that the mail server is available on startup.
spring.mail.username= # Login user of the SMTP server.

实战

引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.github.dozermapper</groupId>
<artifactId>dozer-spring-boot-starter</artifactId>
<version>6.4.0</version>
</dependency>
</dependencies>

配置邮件属性

src/main/resources 目录下添加 application-163.properties 配置文件,内容如下:

1
2
3
4
5
6
7
8
9
10
spring.mail.host = smtp.163.com
spring.mail.username = xxxxxx
spring.mail.password = xxxxxx
spring.mail.properties.mail.smtp.auth = true
spring.mail.properties.mail.smtp.starttls.enable = true
spring.mail.properties.mail.smtp.starttls.required = true
spring.mail.default-encoding = UTF-8

mail.domain = 163.com
mail.from = ${spring.mail.username}@${mail.domain}

注:需替换有效的 spring.mail.usernamespring.mail.password

application-163.properties 配置文件表示使用 163 邮箱时的配置,为了使之生效,需要通过 spring.profiles.active = 163 来激活它。

src/main/resources 目录下添加 application.properties 配置文件,内容如下:

1
spring.profiles.active = 163

Java 代码

首先,需要读取部分配置属性,方法如下:

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
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;

@Validated
@Component
@ConfigurationProperties(prefix = "mail")
public class MailProperties {
private String domain;
private String from;

public String getDomain() {
return domain;
}

public void setDomain(String domain) {
this.domain = domain;
}

public String getFrom() {
return from;
}

public void setFrom(String from) {
this.from = from;
}
}

接着,定义一个邮件参数实体类(使用 lombok 简化了 getter、setter):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import lombok.Data;
import java.util.Date;

@Data
public class MailDTO {
private String from;
private String replyTo;
private String[] to;
private String[] cc;
private String[] bcc;
private Date sentDate;
private String subject;
private String text;
private String[] filenames;
}

接着,实现发送邮件的功能接口:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import com.github.dozermapper.core.Mapper;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;

import javax.mail.MessagingException;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import java.io.IOException;

@Service
public class MailService {

private final Logger log = LoggerFactory.getLogger(this.getClass());

@Autowired
private MailProperties mailProperties;

@Autowired
private JavaMailSender javaMailSender;

@Autowired
private Mapper mapper;

public void sendSimpleMailMessage(MailDTO mailDTO) {
SimpleMailMessage simpleMailMessage = mapper.map(mailDTO, SimpleMailMessage.class);
if (StringUtils.isEmpty(mailDTO.getFrom())) {
mailDTO.setFrom(mailProperties.getFrom());
}
javaMailSender.send(simpleMailMessage);
}

public void sendMimeMessage(MailDTO mailDTO) {

MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper messageHelper;
try {
messageHelper = new MimeMessageHelper(mimeMessage, true);

if (StringUtils.isEmpty(mailDTO.getFrom())) {
messageHelper.setFrom(mailProperties.getFrom());
}
messageHelper.setTo(mailDTO.getTo());
messageHelper.setSubject(mailDTO.getSubject());

mimeMessage = messageHelper.getMimeMessage();
MimeBodyPart mimeBodyPart = new MimeBodyPart();
mimeBodyPart.setContent(mailDTO.getText(), "text/html;charset=UTF-8");

// 描述数据关系
MimeMultipart mm = new MimeMultipart();
mm.setSubType("related");
mm.addBodyPart(mimeBodyPart);

// 添加邮件附件
for (String filename : mailDTO.getFilenames()) {
MimeBodyPart attachPart = new MimeBodyPart();
try {
attachPart.attachFile(filename);
} catch (IOException e) {
e.printStackTrace();
}
mm.addBodyPart(attachPart);
}
mimeMessage.setContent(mm);
mimeMessage.saveChanges();

} catch (MessagingException e) {
e.printStackTrace();
}

javaMailSender.send(mimeMessage);
}
}

示例源码

示例源码:spring-boot-mail

参考资料

SpringBoot 之 Profile

一个应用为了在不同的环境下工作,常常会有不同的配置,代码逻辑处理。Spring Boot 对此提供了简便的支持。

关键词: @Profilespring.profiles.active

区分环境的配置

properties 配置

假设,一个应用的工作环境有:dev、test、prod

那么,我们可以添加 4 个配置文件:

  • applcation.properties - 公共配置
  • application-dev.properties - 开发环境配置
  • application-test.properties - 测试环境配置
  • application-prod.properties - 生产环境配置

applcation.properties 文件中可以通过以下配置来激活 profile:

1
spring.profiles.active = test

yml 配置

与 properties 文件类似,我们也可以添加 4 个配置文件:

  • applcation.yml - 公共配置
  • application-dev.yml - 开发环境配置
  • application-test.yml - 测试环境配置
  • application-prod.yml - 生产环境配置

applcation.yml 文件中可以通过以下配置来激活 profile:

1
2
3
spring:
profiles:
active: prod

此外,yml 文件也可以在一个文件中完成所有 profile 的配置:

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
# 激活 prod
spring:
profiles:
active: prod
# 也可以同时激活多个 profile
# spring.profiles.active: prod,proddb,prodlog
---
# dev 配置
spring:
profiles: dev

# 略去配置

---
spring:
profiles: test

# 略去配置

---
spring.profiles: prod
spring.profiles.include:
- proddb
- prodlog

---
spring:
profiles: proddb

# 略去配置

---
spring:
profiles: prodlog
# 略去配置

注意:不同 profile 之间通过 --- 分割

区分环境的代码

使用 @Profile 注解可以指定类或方法在特定的 Profile 环境生效。

修饰类

1
2
3
4
5
6
7
8
9
10
@Configuration
@Profile("production")
public class JndiDataConfig {

@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}

修饰注解

1
2
3
4
5
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

修饰方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class AppConfig {

@Bean("dataSource")
@Profile("development")
public DataSource standaloneDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.addScript("classpath:com/bank/config/sql/test-data.sql")
.build();
}

@Bean("dataSource")
@Profile("production")
public DataSource jndiDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}

激活 profile

插件激活 profile

1
spring-boot:run -Drun.profiles=prod

main 方法激活 profile

1
--spring.profiles.active=prod

jar 激活 profile

1
java -jar -Dspring.profiles.active=prod *.jar

在 Java 代码中激活 profile

直接指定环境变量来激活 profile:

1
System.setProperty("spring.profiles.active", "test");

在 Spring 容器中激活 profile:

1
2
3
4
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

示例源码

示例源码:spring-boot-profile

参考资料

SpringBoot 教程之处理异步请求

@EnableAsync 注解

要使用 @Async,首先需要使用 @EnableAsync 注解开启 Spring Boot 中的异步特性。

1
2
3
4
@Configuration
@EnableAsync
public class AppConfig {
}

更详细的配置说明,可以参考:AsyncConfigurer

@Async 注解

支持的用法

(1)无入参无返回值方法

您可以用 @Async 注解修饰方法,这表明这个方法是异步方式调用。换句话说,程序在调用此方法时会立即返回,而方法的实际执行发生在已提交给 Spring TaskExecutor 的任务中。在最简单的情况下,您可以将注解应用于返回 void 的方法,如以下示例所示:

1
2
3
4
@Async
void doSomething() {
// this will be executed asynchronously
}

(2)有入参无返回值方法

与使用 @Scheduled 注释注释的方法不同,这些方法可以指定参数,因为它们在运行时由调用者以“正常”方式调用,而不是由容器管理的调度任务调用。例如,以下代码是 @Async 注解的合法应用:

1
2
3
4
@Async
void doSomething(String s) {
// this will be executed asynchronously
}

(3)有入参有返回值方法

甚至可以异步调用返回值的方法。但是,这些方法需要具有 Future 类型的返回值。这仍然提供了异步执行的好处,以便调用者可以在调用 Future 上的 get() 之前执行其他任务。以下示例显示如何在返回值的方法上使用@Async

1
2
3
4
@Async
Future<String> returnSomething(int i) {
// this will be executed asynchronously
}

不支持的用法

@Async 不能与生命周期回调一起使用,例如 @PostConstruct

要异步初始化 Spring bean,必须使用单独的初始化 Spring bean,然后在目标上调用 @Async 带注释的方法,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SampleBeanImpl implements SampleBean {

@Async
void doSomething() {
// ...
}

}

public class SampleBeanInitializer {

private final SampleBean bean;

public SampleBeanInitializer(SampleBean bean) {
this.bean = bean;
}

@PostConstruct
public void initialize() {
bean.doSomething();
}

}

明确指定执行器

默认情况下,在方法上指定 @Async 时,使用的执行器是在启用异步支持时配置的执行器,即如果使用 XML 或 AsyncConfigurer 实现(如果有),则为 annotation-driven 元素。但是,如果需要指示在执行给定方法时应使用默认值以外的执行器,则可以使用 @Async 注解的 value 属性。以下示例显示了如何执行此操作:

1
2
3
4
@Async("otherExecutor")
void doSomething(String s) {
// this will be executed asynchronously by "otherExecutor"
}

在这种情况下,“otherExecutor”可以是 Spring 容器中任何 Executor bean 的名称,也可以是与任何 Executor 关联的限定符的名称(例如,使用 <qualifier> 元素或 Spring 的 @Qualifier 注释指定) )。

管理 @Async 的异常

@Async 方法的返回值类型为 Future 型时,很容易管理在方法执行期间抛出的异常,因为在调用 get 结果时会抛出此异常。但是,对于返回值类型为 void 型的方法,异常不会被捕获且无法传输。您可以提供 AsyncUncaughtExceptionHandler 来处理此类异常。以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {

@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
// handle exception
}
}

默认情况下,仅记录异常。您可以使用 AsyncConfigurer<task:annotation-driven /> XML 元素定义自定义 AsyncUncaughtExceptionHandler

示例源码

示例源码:spring-boot-async

参考资料

Java 虚拟机之字节码

字节码简介

Java 字节码是 Java 虚拟机执行的一种指令格式。之所以被称之为字节码,是因为:Java 字节码文件(.class)是一种以 8 位字节为基础单位的二进制流文件,各个数据项严格按照顺序紧凑地排列在 .class 文件中,中间没有添加任何分隔符。整个 .class 文件本质上就是一张表

Java 能做到 “一次编译,到处运行”,一是因为 JVM 针对各种操作系统、平台都进行了定制;二是因为无论在什么平台,都可以编译生成固定格式的 Java 字节码文件(.class)。

类文件结构

一个 Java 类编译后生成的 .class 文件内容如下图所示,是一堆十六进制数。

Class 文件是一组以 8 个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文 件之中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部是程序运行的必要数 据,没有空隙存在。

图来自 字节码增强技术探索

字节码看似杂乱无序,实际上是由严格的格式要求组成的。

魔数

每个 .class 文件的头 4 个字节称为 **魔数(magic_number)**,它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 .class 文件。魔数的固定值为:0xCAFEBABE(咖啡宝贝)。

版本号

版本号(version)有 4 个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)

Java 的版本号是从 45 开始的,JDK 1.1 之后 的每个 JDK 大版本发布主版本号向上加 1。举例来说,如果版本号为:“00 00 00 34”。那么,次版本号转化为十进制为 0,主版本号转化为十进制为 52,在 Oracle 官网中查询序号 52 对应的主版本号为 1.8,所以编译该文件的 Java 版本号为 1.8.0。

常量池

紧接着主版本号之后的字节为常量池(constant_pool),常量池可以理解为 .class 文件中的资源仓库

常量池整体上分为两部分:常量池计数器以及常量池数据区

  • 常量池计数器(constant_pool_count) - 由于常量的数量不固定,所以需要先放置两个字节来表示常量池容量计数值。

  • 常量池数据区 - 数据区的每一项常量都是一个表,且结构各不相同。

常量池主要存放两类常量:

  • 字面量 - 如文本字符串、声明为 final 的常量值。
  • 符号引用
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符

访问标志

紧接着常量池的 2 个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口的访问信息,描述该 Class 是类还是接口;以及是否被 publicabstractfinal 等修饰符修饰。

类索引、父类索引、接口索引集合

类索引(this_class)和父类索引都是一个 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合,Class 文件中由这三项数据来确定该类型的继承关系

字段表集合

字段表(field_info)用于描述类和接口中声明的变量。Java 语言中的“字段”(Field)包括类级变 量以及实例级变量,但不包括在方法内部声明的局部变量。

字段可以包括的修饰符有字段的作用域(public、private、protected 修饰 符)、是实例变量还是类变量(static 修饰符)、可变性(final)、并发可见性(volatile 修饰符,是否 强制从主内存读写)、可否被序列化(transient 修饰符)、字段数据类型(基本类型、对象、数组)、 字段名称。

方法表集合

Class 文件存储 格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依 次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表 集合(attributes)几项

字段表结束后为方法表,方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。

属性表集合

属性表集合(attribute_info)存放了在该文件中类或接口所定义属性的基本信息。

字节码指令

字节码指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零到多个代表此操作所需参数(Operands)而构成。由于 JVM 采用面向操作数栈架构而不是寄存器架构,所以大多数的指令都不包括操作数,只有一个操作码。

JVM 操作码的长度为 1 个字节,因此指令集的操作码最多只有 256 个。

字节码操作大致分为 9 类:

  • 加载和存储指令
  • 运算指令
  • 类型转换指令
  • 对象创建与访问指令
  • 操作数栈管理指令
  • 控制转移指令
  • 方法调用和返回指令
  • 异常处理指令
  • 同步指令

字节码增强

Asm

对于需要手动操纵字节码的需求,可以使用 Asm,它可以直接生产 .class字节码文件,也可以在类被加载入 JVM 之前动态修改类行为(如下图 17 所示)。

Asm 的应用场景有 AOP(Cglib 就是基于 Asm)、热部署、修改其他 jar 包中的类等。当然,涉及到如此底层的步骤,实现起来也比较麻烦。

Asm 有两类 API:核心 API 和树形 API

核心 API

Asm Core API 可以类比解析 XML 文件中的 SAX 方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用 Core API。在 Core API 中有以下几个关键类:

  • ClassReader:用于读取已经编译好的。class 文件。
  • ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。
  • 各种 Visitor 类:如上所述,CoreAPI 根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的 Visitor,比如用于访问方法的 MethodVisitor、用于访问类变量的 FieldVisitor、用于访问注解的 AnnotationVisitor 等。为了实现 AOP,重点要使用的是 MethodVisitor。

树形 API

Asm Tree API 可以类比解析 XML 文件中的 DOM 方式,把整个类的结构读取到内存中,缺点是消耗内存多,但是编程比较简单。TreeApi 不同于 CoreAPI,TreeAPI 通过各种 Node 类来映射字节码的各个区域,类比 DOM 节点,就可以很好地理解这种编程方式。

Javassist

利用 Javassist 实现字节码增强时,可以无须关注字节码刻板的结构,其优点就在于编程简单。直接使用 java 编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。

其中最重要的是 ClassPool、CtClass、CtMethod、CtField 这四个类:

  • CtClass(compile-time class) - 编译时类信息,它是一个 class 文件在代码中的抽象表现形式,可以通过一个类的全限定名来获取一个 CtClass 对象,用来表示这个类文件。
  • ClassPool - 从开发视角来看,ClassPool 是一张保存 CtClass 信息的 HashTable,key 为类名,value 为类名对应的 CtClass 对象。当我们需要对某个类进行修改时,就是通过 pool.getCtClass(“className”) 方法从 pool 中获取到相应的 CtClass。
  • CtMethodCtField - 这两个比较好理解,对应的是类中的方法和属性。

参考资料

Java 虚拟机之调优

JVM 调优概述

GC 性能指标

对于 JVM 调优来说,需要先明确调优的目标。
从性能的角度看,通常关注三个指标:

  • 吞吐量(throughput) - 指不考虑 GC 引起的停顿时间或内存消耗,垃圾收集器能支撑应用达到的最高性能指标。
  • 停顿时间(latency) - 其度量标准是缩短由于垃圾啊收集引起的停顿时间或者完全消除因垃圾收集所引起的停顿,避免应用运行时发生抖动。
  • 垃圾回收频率 - 久发生一次指垃圾回收呢?通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。所以我们只要适当地增大堆内存空间,保证正常的垃圾回收频率即可。

大多数情况下调优会侧重于其中一个或者两个方面的目标,很少有情况可以兼顾三个不同的角度。

调优原则

GC 优化的两个目标:

  • 降低 Full GC 的频率
  • 减少 Full GC 的执行时间

GC 优化的基本原则是:将不同的 GC 参数应用到两个及以上的服务器上然后比较它们的性能,然后将那些被证明可以提高性能或减少 GC 执行时间的参数应用于最终的工作服务器上。

降低 Minor GC 频率

如果新生代空间较小,Eden 区很快被填满,就会导致频繁 Minor GC,因此我们可以通过增大新生代空间来降低 Minor GC 的频率。

可能你会有这样的疑问,扩容 Eden 区虽然可以减少 Minor GC 的次数,但不会增加单次 Minor GC 的时间吗?如果单次 Minor GC 的时间增加,那也很难达到我们期待的优化效果呀。

我们知道,单次 Minor GC 时间是由两部分组成:T1(扫描新生代)和 T2(复制存活对象)。假设一个对象在 Eden 区的存活时间为 500ms,Minor GC 的时间间隔是 300ms,那么正常情况下,Minor GC 的时间为 :T1+T2。

当我们增大新生代空间,Minor GC 的时间间隔可能会扩大到 600ms,此时一个存活 500ms 的对象就会在 Eden 区中被回收掉,此时就不存在复制存活对象了,所以再发生 Minor GC 的时间为:两次扫描新生代,即 2T1。

可见,扩容后,Minor GC 时增加了 T1,但省去了 T2 的时间。通常在虚拟机中,复制对象的成本要远高于扫描成本。

如果在堆内存中存在较多的长期存活的对象,此时增加年轻代空间,反而会增加 Minor GC 的时间。如果堆中的短期对象很多,那么扩容新生代,单次 Minor GC 时间不会显著增加。因此,单次 Minor GC 时间更多取决于 GC 后存活对象的数量,而非 Eden 区的大小。

降低 Full GC 的频率

Full GC 相对来说会比 Minor GC 更耗时。减少进入老年代的对象数量可以显著降低 Full GC 的频率。

减少创建大对象:如果对象占用内存过大,在 Eden 区被创建后会直接被传入老年代。在平常的业务场景中,我们习惯一次性从数据库中查询出一个大对象用于 web 端显示。例如,我之前碰到过一个一次性查询出 60 个字段的业务操作,这种大对象如果超过年轻代最大对象阈值,会被直接创建在老年代;即使被创建在了年轻代,由于年轻代的内存空间有限,通过 Minor GC 之后也会进入到老年代。这种大对象很容易产生较多的 Full GC。

我们可以将这种大对象拆解出来,首次只查询一些比较重要的字段,如果还需要其它字段辅助查看,再通过第二次查询显示剩余的字段。

增大堆内存空间:在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆内存,也可以降低 Full GC 的频率。

降低 Full GC 的时间

Full GC 的执行时间比 Minor GC 要长很多,因此,如果在 Full GC 上花费过多的时间(超过 1s),将可能出现超时错误。

  • 如果通过减小老年代内存来减少 Full GC 时间,可能会引起 OutOfMemoryError 或者导致 Full GC 的频率升高。
  • 另外,如果通过增加老年代内存来降低 Full GC 的频率,Full GC 的时间可能因此增加。

因此,你需要把老年代的大小设置成一个“合适”的值

GC 优化需要考虑的 JVM 参数

类型 参数 描述
堆内存大小 -Xms 启动 JVM 时堆内存的大小
-Xmx 堆内存最大限制
新生代空间大小 -XX:NewRatio 新生代和老年代的内存比
-XX:NewSize 新生代内存大小
-XX:SurvivorRatio Eden 区和 Survivor 区的内存比

GC 优化时最常用的参数是-Xms,-Xmx-XX:NewRatio-Xms-Xmx参数通常是必须的,所以NewRatio的值将对 GC 性能产生重要的影响。

有些人可能会问如何设置永久代内存大小,你可以用-XX:PermSize-XX:MaxPermSize参数来进行设置,但是要记住,只有当出现OutOfMemoryError错误时你才需要去设置永久代内存。

GC 优化的过程

GC 优化的过程大致可分为以下步骤:

(1)监控 GC 状态

你需要监控 GC 从而检查系统中运行的 GC 的各种状态。

(2)分析 GC 日志

在检查 GC 状态后,你需要分析监控结构并决定是否需要进行 GC 优化。如果分析结果显示运行 GC 的时间只有 0.1-0.3 秒,那么就不需要把时间浪费在 GC 优化上,但如果运行 GC 的时间达到 1-3 秒,甚至大于 10 秒,那么 GC 优化将是很有必要的。

但是,如果你已经分配了大约 10GB 内存给 Java,并且这些内存无法省下,那么就无法进行 GC 优化了。在进行 GC 优化之前,你需要考虑为什么你需要分配这么大的内存空间,如果你分配了 1GB 或 2GB 大小的内存并且出现了OutOfMemoryError,那你就应该执行堆快照(heap dump)来消除导致异常的原因。

🔔 注意:

堆快照(heap dump)是一个用来检查 Java 内存中的对象和数据的内存文件。该文件可以通过执行 JDK 中的jmap命令来创建。在创建文件的过程中,所有 Java 程序都将暂停,因此,不要在系统执行过程中创建该文件。

你可以在互联网上搜索 heap dump 的详细说明。

(3)选择合适 GC 回收器

如果你决定要进行 GC 优化,那么你需要选择一个 GC 回收器,并且为它设置合理 JVM 参数。此时如果你有多个服务器,请如上文提到的那样,在每台机器上设置不同的 GC 参数并分析它们的区别。

(4)分析结果

在设置完 GC 参数后就可以开始收集数据,请在收集至少 24 小时后再进行结果分析。如果你足够幸运,你可能会找到系统的最佳 GC 参数。如若不然,你还需要分析输出日志并检查分配的内存,然后需要通过不断调整 GC 类型/内存大小来找到系统的最佳参数。

(5)应用优化配置

如果 GC 优化的结果令人满意,就可以将参数应用到所有服务器上,并停止 GC 优化。

在下面的章节中,你将会看到上述每一步所做的具体工作。

GC 日志

获取 GC 日志

获取 GC 日志有两种方式:

  • 使用 jstat 命令动态查看
  • 在容器中设置相关参数打印 GC 日志

jstat 命令查看 GC

jstat -gc 统计垃圾回收堆的行为:

1
2
3
jstat -gc 1262
S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT
26112.0 24064.0 6562.5 0.0 564224.0 76274.5 434176.0 388518.3 524288.0 42724.7 320 6.417 1 0.398 6.815

也可以设置间隔固定时间来打印:

1
jstat -gc 1262 2000 20

这个命令意思就是每隔 2000ms 输出 1262 的 gc 情况,一共输出 20 次

打印 GC 的参数

通过 JVM 参数预先设置 GC 日志,通常有以下几种 JVM 参数设置:

1
2
3
4
5
6
-XX:+PrintGC 输出 GC 日志
-XX:+PrintGCDetails 输出 GC 的详细日志
-XX:+PrintGCTimeStamps 输出 GC 的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出 GC 的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行 GC 的前后打印出堆的信息
-verbose:gc -Xloggc:../logs/gc.log 日志文件的输出路径

如果是长时间的 GC 日志,我们很难通过文本形式去查看整体的 GC 性能。此时,我们可以通过GCView工具打开日志文件,图形化界面查看整体的 GC 性能。

【示例】Tomcat 设置示例

1
2
3
4
5
6
JAVA_OPTS="-server -Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m -XX:SurvivorRatio=4
-verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log
-Djava.awt.headless=true
-XX:+PrintGCTimeStamps -XX:+PrintGCDetails
-Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000
-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15"
  • -Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m
    Xms,即为 jvm 启动时得 JVM 初始堆大小,Xmx 为 jvm 的最大堆大小,xmn 为新生代的大小,permsize 为永久代的初始大小,MaxPermSize 为永久代的最大空间。
  • -XX:SurvivorRatio=4
    SurvivorRatio 为新生代空间中的 Eden 区和救助空间 Survivor 区的大小比值,默认是 8,则两个 Survivor 区与一个 Eden 区的比值为 2:8,一个 Survivor 区占整个年轻代的 1/10。调小这个参数将增大 survivor 区,让对象尽量在 survitor 区呆长一点,减少进入年老代的对象。去掉救助空间的想法是让大部分不能马上回收的数据尽快进入年老代,加快年老代的回收频率,减少年老代暴涨的可能性,这个是通过将-XX:SurvivorRatio 设置成比较大的值(比如 65536)来做到。
  • -verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log
    将虚拟机每次垃圾回收的信息写到日志文件中,文件名由 file 指定,文件格式是平文件,内容和-verbose:gc 输出内容相同。
  • -Djava.awt.headless=true Headless 模式是系统的一种配置模式。在该模式下,系统缺少了显示设备、键盘或鼠标。
  • -XX:+PrintGCTimeStamps -XX:+PrintGCDetails
    设置 gc 日志的格式
  • -Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000
    指定 rmi 调用时 gc 的时间间隔
  • -XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15 采用并发 gc 方式,经过 15 次 minor gc 后进入年老代

分析 GC 日志

Young GC 回收日志:

1
2016-07-05T10:43:18.093+0800: 25.395: [GC [PSYoungGen: 274931K->10738K(274944K)] 371093K->147186K(450048K), 0.0668480 secs] [Times: user=0.17 sys=0.08, real=0.07 secs]

Full GC 回收日志:

1
2016-07-05T10:43:18.160+0800: 25.462: [Full GC [PSYoungGen: 10738K->0K(274944K)] [ParOldGen: 136447K->140379K(302592K)] 147186K->140379K(577536K) [PSPermGen: 85411K->85376K(171008K)], 0.6763541 secs] [Times: user=1.75 sys=0.02, real=0.68 secs]

通过上面日志分析得出,PSYoungGen、ParOldGen、PSPermGen 属于 Parallel 收集器。其中 PSYoungGen 表示 gc 回收前后年轻代的内存变化;ParOldGen 表示 gc 回收前后老年代的内存变化;PSPermGen 表示 gc 回收前后永久区的内存变化。young gc 主要是针对年轻代进行内存回收比较频繁,耗时短;full gc 会对整个堆内存进行回城,耗时长,因此一般尽量减少 full gc 的次数

通过两张图非常明显看出 gc 日志构成:

YOUNG GC

img

FULL GC

img

CPU 过高

定位步骤:

(1)执行 top -c 命令,找到 cpu 最高的进程的 id

(2)jstack PID 导出 Java 应用程序的线程堆栈信息。

示例:

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
jstack 6795

"Low Memory Detector" daemon prio=10 tid=0x081465f8 nid=0x7 runnable [0x00000000..0x00000000]
"CompilerThread0" daemon prio=10 tid=0x08143c58 nid=0x6 waiting on condition [0x00000000..0xfb5fd798]
"Signal Dispatcher" daemon prio=10 tid=0x08142f08 nid=0x5 waiting on condition [0x00000000..0x00000000]
"Finalizer" daemon prio=10 tid=0x08137ca0 nid=0x4 in Object.wait() [0xfbeed000..0xfbeeddb8]

at java.lang.Object.wait(Native Method)

- waiting on <0xef600848> (a java.lang.ref.ReferenceQueue$Lock)

at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:116)

- locked <0xef600848> (a java.lang.ref.ReferenceQueue$Lock)

at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:132)

at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:159)

"Reference Handler" daemon prio=10 tid=0x081370f0 nid=0x3 in Object.wait() [0xfbf4a000..0xfbf4aa38]

at java.lang.Object.wait(Native Method)

- waiting on <0xef600758> (a java.lang.ref.Reference$Lock)

at java.lang.Object.wait(Object.java:474)

at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:116)

- locked <0xef600758> (a java.lang.ref.Reference$Lock)

"VM Thread" prio=10 tid=0x08134878 nid=0x2 runnable

"VM Periodic Task Thread" prio=10 tid=0x08147768 nid=0x8 waiting on condition

在打印的堆栈日志文件中,tid 和 nid 的含义:

1
2
nid : 对应的 Linux 操作系统下的 tid 线程号,也就是前面转化的 16 进制数字
tid: 这个应该是 jvm jmm 内存规范中的唯一地址定位

在 CPU 过高的情况下,查找响应的线程,一般定位都是用 nid 来定位的。而如果发生死锁之类的问题,一般用 tid 来定位。

(3)定位 CPU 高的线程打印其 nid

查看线程下具体进程信息的命令如下:

top -H -p 6735

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
top - 14:20:09 up 611 days,  2:56,  1 user,  load average: 13.19, 7.76, 7.82
Threads: 6991 total, 17 running, 6974 sleeping, 0 stopped, 0 zombie
%Cpu(s): 90.4 us, 2.1 sy, 0.0 ni, 7.0 id, 0.0 wa, 0.0 hi, 0.4 si, 0.0 st
KiB Mem: 32783044 total, 32505008 used, 278036 free, 120304 buffers
KiB Swap: 0 total, 0 used, 0 free. 4497428 cached Mem

PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
6800 root 20 0 27.299g 0.021t 7172 S 54.7 70.1 187:55.61 java
6803 root 20 0 27.299g 0.021t 7172 S 54.4 70.1 187:52.59 java
6798 root 20 0 27.299g 0.021t 7172 S 53.7 70.1 187:55.08 java
6801 root 20 0 27.299g 0.021t 7172 S 53.7 70.1 187:55.25 java
6797 root 20 0 27.299g 0.021t 7172 S 53.1 70.1 187:52.78 java
6804 root 20 0 27.299g 0.021t 7172 S 53.1 70.1 187:55.76 java
6802 root 20 0 27.299g 0.021t 7172 S 52.1 70.1 187:54.79 java
6799 root 20 0 27.299g 0.021t 7172 S 51.8 70.1 187:53.36 java
6807 root 20 0 27.299g 0.021t 7172 S 13.6 70.1 48:58.60 java
11014 root 20 0 27.299g 0.021t 7172 R 8.4 70.1 8:00.32 java
10642 root 20 0 27.299g 0.021t 7172 R 6.5 70.1 6:32.06 java
6808 root 20 0 27.299g 0.021t 7172 S 6.1 70.1 159:08.40 java
11315 root 20 0 27.299g 0.021t 7172 S 3.9 70.1 5:54.10 java
12545 root 20 0 27.299g 0.021t 7172 S 3.9 70.1 6:55.48 java
23353 root 20 0 27.299g 0.021t 7172 S 3.9 70.1 2:20.55 java
24868 root 20 0 27.299g 0.021t 7172 S 3.9 70.1 2:12.46 java
9146 root 20 0 27.299g 0.021t 7172 S 3.6 70.1 7:42.72 java

由此可以看出占用 CPU 较高的线程,但是这些还不高,无法直接定位到具体的类。nid 是 16 进制的,所以我们要获取线程的 16 进制 ID:

1
printf "%x\n" 6800
1
输出结果:45cd

然后根据输出结果到 jstack 打印的堆栈日志中查定位:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"catalina-exec-5692" daemon prio=10 tid=0x00007f3b05013800 nid=0x45cd waiting on condition [0x00007f3ae08e3000]
java.lang.Thread.State: TIMED_WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x00000006a7800598> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:226)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2082)
at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)
at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:86)
at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:32)
at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1068)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:745)

GC 配置

详细参数说明请参考官方文档:JavaHotSpot VM Options,这里仅列举常用参数。

堆大小设置

年轻代的设置很关键。

JVM 中最大堆大小有三方面限制:

  1. 相关操作系统的数据模型(32-bt 还是 64-bit)限制;
  2. 系统的可用虚拟内存限制;
  3. 系统的可用物理内存限制。
1
整个堆大小 = 年轻代大小 + 年老代大小 + 持久代大小
  • 持久代一般固定大小为 64m。使用 -XX:PermSize 设置。
  • 官方推荐年轻代占整个堆的 3/8。使用 -Xmn 设置。

JVM 内存配置

配置 描述
-Xss 虚拟机栈大小。
-Xms 堆空间初始值。
-Xmx 堆空间最大值。
-Xmn 新生代空间大小。
-XX:NewSize 新生代空间初始值。
-XX:MaxNewSize 新生代空间最大值。
-XX:NewRatio 新生代与年老代的比例。默认为 2,意味着老年代是新生代的 2 倍。
-XX:SurvivorRatio 新生代中调整 eden 区与 survivor 区的比例,默认为 8。即 eden 区为 80% 的大小,两个 survivor 分别为 10% 的大小。
-XX:PermSize 永久代空间的初始值。
-XX:MaxPermSize 永久代空间的最大值。

GC 类型配置

配置 描述
-XX:+UseSerialGC 使用 Serial + Serial Old 垃圾回收器组合
-XX:+UseParallelGC 使用 Parallel Scavenge + Parallel Old 垃圾回收器组合
-XX:+UseParallelOldGC 使用 Parallel Old 垃圾回收器(JDK5 后已无用)
-XX:+UseParNewGC 使用 ParNew + Serial Old 垃圾回收器
-XX:+UseConcMarkSweepGC 使用 CMS + ParNew + Serial Old 垃圾回收器组合
-XX:+UseG1GC 使用 G1 垃圾回收器
-XX:ParallelCMSThreads 并发标记扫描垃圾回收器 = 为使用的线程数量

垃圾回收器通用参数

配置 描述
PretenureSizeThreshold 晋升年老代的对象大小。默认为 0。比如设为 10M,则超过 10M 的对象将不在 eden 区分配,而直接进入年老代。
MaxTenuringThreshold 晋升老年代的最大年龄。默认为 15。比如设为 10,则对象在 10 次普通 GC 后将会被放入年老代。
DisableExplicitGC 禁用 System.gc()

JMX

开启 JMX 后,可以使用 jconsolejvisualvm 进行监控 Java 程序的基本信息和运行情况。

1
2
3
4
5
-Dcom.sun.management.jmxremote=true
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-Djava.rmi.server.hostname=127.0.0.1
-Dcom.sun.management.jmxremote.port=18888

-Djava.rmi.server.hostname 指定 Java 程序运行的服务器,-Dcom.sun.management.jmxremote.port 指定服务监听端口。

远程 DEBUG

如果开启 Java 应用的远程 Debug 功能,需要指定如下参数:

1
2
3
4
-Xdebug
-Xnoagent
-Djava.compiler=NONE
-Xrunjdwp:transport=dt_socket,address=28888,server=y,suspend=n

address 即为远程 debug 的监听端口。

HeapDump

1
-XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError

辅助配置

配置 描述
-XX:+PrintGCDetails 打印 GC 日志
-Xloggc:<filename> 指定 GC 日志文件名
-XX:+HeapDumpOnOutOfMemoryError 内存溢出时输出堆快照文件

参考资料

分库分表基本原理

什么是分库分表

分库分表是一种数据库水平拆分方案,用于解决单机数据库的存储瓶颈性能瓶颈问题。

  • 分库:将数据分散到不同的数据库实例(如 DB1DB2)。
  • 分表:将数据分散到同一数据库的不同表(如 order_1order_2)。

为何要分库分表

分库分表主要基于以下理由:

  • 并发连接 - 单库超过每秒 2000 个并发时,而一个健康的单库最好保持在每秒 1000 个并发左右,不要太大。
  • 磁盘容量 - 磁盘容量占满,会导致服务器不可用。
  • SQL 性能 - 单表数据量过大,会导致 SQL 执行效率低下。一般,单表超过 1000 万条数据,就可以考虑分表了。
# 分库分表前 分库分表后
并发支撑情况 MySQL 单机部署,扛不住高并发 MySQL 从单机到多机,能承受的并发增加了多倍
磁盘使用情况 MySQL 单机磁盘容量几乎撑满 拆分为多个库,数据库服务器磁盘使用率大大降低
SQL 执行性能 单表数据量太大,SQL 越跑越慢 单表数据量减少,SQL 执行效率明显提升

分库分表原理

数据分片指按照某个维度将存放在单一数据库中的数据分散地存放至多个数据库或表中以达到提升性能瓶颈以及可用性的效果。 数据分片的有效手段是对关系型数据库进行分库和分表。分库和分表均可以有效的避免由数据量超过可承受阈值而产生的查询瓶颈。 除此之外,分库还能够用于有效的分散对数据库单点的访问量;分表虽然无法缓解数据库压力,但却能够提供尽量将分布式事务转化为本地事务的可能,一旦涉及到跨库的更新操作,分布式事务往往会使问题变得复杂。 使用多主多从的分片方式,可以有效的避免数据单点,从而提升数据架构的可用性。

通过分库和分表进行数据的拆分来使得各个表的数据量保持在阈值以下,以及对流量进行疏导应对高访问量,是应对高并发和海量数据系统的有效手段。 数据分片的拆分方式又分为垂直分片和水平分片。

垂直分片

垂直分片有两种拆分考量:业务拆分和访问频率拆分

(1)业务拆分

业务拆分的核心理念是专库专用

在拆分之前,一个数据库由多个数据表构成,每个表对应着不同的业务。而拆分之后,则是按照业务将表进行归类,分布到不同的数据库中,从而将压力分散至不同的数据库。下图展示了根据业务需要,将用户表和订单表垂直分片到不同的数据库的方案。

垂直分片往往需要对架构和设计进行调整。通常来讲,是来不及应对互联网业务需求快速变化的;而且,它也并无法真正的解决单点瓶颈。垂直拆分可以缓解数据量和访问量带来的问题,但无法根治。如果垂直拆分之后,表中的数据量依然超过单节点所能承载的阈值,则需要水平分片来进一步处理

(2)访问频率拆分

访问频率拆分,是 把一个有很多字段的表给拆分成多个表,或者是多个库上去。一般来说,会 将较少的、访问频率较高的字段放到一个表中,然后 将较多的、访问频率较低的字段放到另外一个表中。因为数据库是有缓存的,访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。

image-20200114211639899

一般来说,满足下面的条件就可以考虑扩容了:

  • Mysql 单库超过 5000 万条记录,Oracle 单库超过 1 亿条记录,DB 压力就很大。
  • 单库超过每秒 2000 个并发时,而一个健康的单库最好保持在每秒 1000 个并发左右,不要太大。

在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。

水平分片

水平拆分 又称为 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。当 单表数据量太大 时,会极大影响 SQL 执行的性能 。分表是将原来一张表的数据分布到数据库集群的不同节点上,从而缓解单点的压力。

相对于垂直分片,水平分片不再将数据根据业务逻辑分类,而是通过某个字段(或某几个字段),根据某种规则将数据分散至多个库或表中,每个分片仅包含数据的一部分。 例如:根据主键分片,偶数主键的记录放入 0 库(或表),奇数主键的记录放入 1 库(或表)。

水平分片从理论上突破了单机数据量处理的瓶颈,并且扩展相对自由,是分库分表的标准解决方案。

image-20200114211203589

一般来说,单表有 200 万条数据 的时候,性能就会相对差一些了,需要考虑分表了。但是,这也要视具体情况而定,可能是 100 万条,也可能是 500 万条,SQL 越复杂,就最好让单表行数越少。

读写分离的数据节点中的数据内容是一致的,而水平分片的每个数据节点的数据内容却并不相同。将水平分片和读写分离联合使用,能够更加有效的提升系统性能。

分库分表策略

img

分库分表策略主要有两种:

  • 根据数值范围划分
  • 根据 Hash 划分

数值范围路由

数值范围路由,就是根据 ID、时间范围 这类具有排序性的字段来进行划分。例如:用户 Id 为 1-9999 的记录分到第一个库,10000-20000 的分到第二个库,以此类推。

按这种策略划分出来的数据,具有数据连续性。

优点:数据迁移很简单。

缺点:容易产生热点问题,大量的流量都打在最新的数据上了。

Hash 路由

典型的 Hash 路由,如根据数值取模,当需要扩容时,一般以 2 的幂次方进行扩容(这样,扩容时迁移的数据量会小一些)。例如:用户 Id mod n,余数为 0 的记录放到第一个库,余数为 1 的放到第二个库,以此类推。

一般采用 预分区 的方式,提前根据 数据量 规划好 分区数,比如划分为 5121024 张表,保证可支撑未来一段时间的 数据容量,再根据 负载情况 迁移到其他 数据库 中。扩容时通常采用 翻倍扩容,避免 数据映射 全部被 打乱,导致 全量迁移 的情况。

优点:数据离散分布,不存在热点问题。

缺点:数据迁移、扩容麻烦(之前的数据需要重新计算 hash 值重新分配到不同的库或表)。当 节点数量 变化时,如 扩容收缩 节点,数据节点 映射关系 需要重新计算,会导致数据的 重新迁移

路由表

这种策略,就是用一张独立的表记录路由信息。

优点:简单、灵活,尤其是在扩容、迁移时,只需要迁移指定的数据,然后修改路由表即可。

缺点:每次查询,必须先查路由表,增加了 IO 开销。并且,如果路由表本身太大,也会面临性能瓶颈,如果想对路由表再做分库分表,将出现死循环式的路由算法选择问题。

迁移和扩容

停机迁移/扩容(不推荐)

停机迁移/扩容是最暴力、最简单的迁移、扩容方案。

img

停机迁移/扩容流程

  1. 预估停服时间,发布停服公告;停服,不允许数据访问。
  2. 编写临时的数据导入程序,从老数据库中读取数据。
  3. 将数据写入中间件。
  4. 中间件根据分片规则,将数据分发到分库(分表)中。
  5. 应用程序修改配置,重启。

停机迁移/扩容方案分析

  • 优点:简单、无数据一致性问题。
  • 缺点
    • 停服时间长(数据量大时可能需数小时)。
    • 风险高,失败后难以回滚。

结论:代价过高,不推荐使用。

双写迁移

双写迁移方案核心思想

双写迁移方案核心思想

  • 新旧库同时写入,通过开关控制读写状态(只写旧库、只写新库、双写)。
  • 逐步切换读请求到新库,确保数据一致性。

双写迁移方案关键步骤

  1. 双写阶段:先写旧库,再写新库,以旧库结果为准。记录旧库成功但新库失败的日志,用于补偿。
  2. 数据校验:运行对比程序,检查新旧库数据差异并修复。
  3. 灰度切换读请求:逐步将读流量切至新库,观察稳定性。
  4. 最终切换:读写全部切至新库,清理旧库冗余数据。

双写迁移方案流程

img

  1. 修改应用程序配置,将数据同时写入老数据库和中间件。这就是所谓的双写,同时写俩库,老库和新库。
  2. 编写临时程序,读取老数据库。
  3. 将数据写入中间件。如果数据不存在,直接写入;如果数据存在,比较时间戳,只允许新数据覆盖老数据。
  4. 导入数据后,有可能数据还是存在不一致,那么就对数据进行校验,比对新老库的每条数据。如果存在差异,针对差异数据,执行(3)。循环(3)、(4)步骤,直至数据完全一致。
  5. 修改应用程序配置,将数据只写入中间件。
  6. 中间件根据分片规则,将数据分发到分库(分表)中。

双写迁移方案分析

优点

  • 无需停服,业务影响小。
  • 可灰度验证,风险可控。

缺点

  • 实现复杂,需处理双写一致性和补偿逻辑。

主从替换

生产环境的数据库,为了保证高可用,一般会采用主从架构。主库支持读写操作,从库支持读操作。

img

由于主从节点数据一致,所以将从库升级为主节点,并修改分片配置,将从节点作为分库之一,就实现了扩容。

img

升级从库的流程

  1. 解除主从关系,从库升级为主库。
  2. 应用程序,修改配置,读写通过中间件。
  3. 分库分表中间,修改分片配置。将数据按照新的规则分发。
  4. 编写临时程序,清理冗余数据。比如:原来是一个单库,数据量为 400 万。从节点升级为分库之一后,每个分库都有 400 万数据,其中 200 万是冗余数据。清理完后,进行数据校验。
  5. 为每个分库添加新的从库,保证高可用。

升级从库方案分析

优点:不需要停机,无需数据迁移。

分库分表的问题

分库分表主要存在以下问题:

  • 分布式 ID 问题
  • 分布式事务问题
  • 跨节点 Join 和聚合
  • 跨分片的排序分页

分布式 ID 问题

一旦数据库被切分到多个物理结点上,我们将不能再依赖数据库自身的主键生成机制。一方面,某个分区数据库自生成的 ID 无法保证在全局上是唯一的;另一方面,应用程序在插入数据之前需要先获得 ID,以便进行 SQL 路由。

分布式 ID 的解决方案详见:分布式 ID

分布式事务问题

跨库事务也是分布式的数据库集群要面对的棘手事情。 合理采用分表,可以在降低单表数据量的情况下,尽量使用本地事务,善于使用同库不同表可有效避免分布式事务带来的麻烦。在不能避免跨库事务的场景,有些业务仍然需要保持事务的一致性。 而基于 XA 的分布式事务由于在并发度高的场景中性能无法满足需要,并未被互联网巨头大规模使用,他们大多采用最终一致性的柔性事务代替强一致事务。

分布式事务的解决方案详见:分布式事务

跨节点 Join 和聚合

分库分表后,无法直接跨节点 joincountorder bygroup by 以及聚合。

针对这类问题,普遍做法是二次查询

在第一次查询时,获取各个节点上的结果。

在程序中将这些结果进行合并、筛选。

跨分片的排序分页

一般来讲,分页时需要按照指定字段进行排序。当排序字段就是分片字段的时候,我们通过分片规则可以比较容易定位到指定的分片,而当排序字段非分片字段的时候,情况就会变得比较复杂了。为了最终结果的准确性,我们需要在不同的分片节点中将数据进行排序并返回,并将不同分片返回的结果集进行汇总和再次排序,最后再返回给用户。如下图所示:

img

上面图中所描述的只是最简单的一种情况(取第一页数据),看起来对性能的影响并不大。但是,如果想取出第 10 页数据,情况又将变得复杂很多,如下图所示:

img

有些读者可能并不太理解,为什么不能像获取第一页数据那样简单处理(排序取出前 10 条再合并、排序)。其实并不难理解,因为各分片节点中的数据可能是随机的,为了排序的准确性,必须把所有分片节点的前 N 页数据都排序好后做合并,最后再进行整体的排序。很显然,这样的操作是比较消耗资源的,用户越往后翻页,系统性能将会越差。

那如何解决分库情况下的分页问题呢?有以下几种办法:

如果是在前台应用提供分页,则限定用户只能看前面 n 页,这个限制在业务上也是合理的,一般看后面的分页意义不大(如果一定要看,可以要求用户缩小范围重新查询)。

如果是后台批处理任务要求分批获取数据,则可以加大 page size,比如每次获取 5000 条记录,有效减少分页数(当然离线访问一般走备库,避免冲击主库)。

分库设计时,一般还有配套大数据平台汇总所有分库的记录,有些分页查询可以考虑走大数据平台。

中间件

国内常见分库分表中间件:

  • Cobar - 阿里 b2b 团队开发和开源的,属于 proxy 层方案,就是介于应用服务器和数据库服务器之间。应用程序通过 JDBC 驱动访问 cobar 集群,cobar 根据 SQL 和分库规则对 SQL 做分解,然后分发到 MySQL 集群不同的数据库实例上执行。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库 join 和分页等操作。
  • TDDL - 淘宝团队开发的,属于 client 层方案。支持基本的 crud 语法和读写分离,但不支持 join、多表查询等语法。目前使用的也不多,因为还依赖淘宝的 diamond 配置管理系统。
  • Atlas - 360 开源的,属于 proxy 层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在 5 年前了。所以,现在用的公司基本也很少了。
  • sharding-jdbc - 当当开源的,属于 client 层方案。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且目前推出到了 2.0 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也可以选择的方案
  • Mycat - 基于 cobar 改造的,属于 proxy 层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 sharding jdbc 来说,年轻一些,经历的锤炼少一些。

技术选型建议:

建议使用的是 sharding-jdbc 和 mycat。

  • sharding-jdbc 这种 client 层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合 sharding-jdbc 的依赖。其本质上通过配置多数据源,然后根据设定的分库分表策略,计算路由,将请求发送到计算得到的节点上。
  • Mycat 这种 proxy 层方案的缺点在于需要部署,自己运维一套中间件,运维成本高,但是好处在于对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就行了。

通常来说,这两个方案其实都可以选用,但是我个人建议中小型公司选用 sharding-jdbc,client 层方案轻便,而且维护成本低,不需要额外增派人手,而且中小型公司系统复杂度会低一些,项目也没那么多;但是中大型公司最好还是选用 mycat 这类 proxy 层方案,因为可能大公司系统和项目非常多,团队很大,人员充足,那么最好是专门弄个人来研究和维护 mycat,然后大量项目直接透明使用即可。

参考资料