一、MySQL 安装与架构
1.1 安装方式
Docker 安装(推荐开发环境)
# 拉取 MySQL 8.0 镜像
docker pull mysql:8.0
# 启动 MySQL 容器
docker run -d \
--name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=root123 \
-v /opt/mysql/data:/var/lib/mysql \
-v /opt/mysql/conf:/etc/mysql/conf.d \
mysql:8.0
# 进入容器连接 MySQL
docker exec -it mysql8 mysql -uroot -proot123apt/yum 安装(生产环境)
# Ubuntu / Debian
apt update
apt install mysql-server-8.0 -y
systemctl start mysql
systemctl enable mysql
# CentOS / Rocky Linux
yum install mysql-server -y
systemctl start mysqld
systemctl enable mysqld
# 安全初始化
mysql_secure_installation1.2 存储引擎对比
MySQL 中最常用的三大存储引擎:
| 特性 | InnoDB | MyISAM | Memory |
|---|---|---|---|
| 事务支持 | ✅ 支持 ACID | ❌ 不支持 | ❌ 不支持 |
| 行级锁 | ✅ | ❌(表锁) | ❌(表锁) |
| 外键 | ✅ | ❌ | ❌ |
| 聚簇索引 | ✅ | ❌ | ❌ |
| 全文索引 | ✅(8.0+) | ✅ | ❌ |
| 崩溃恢复 | ✅(redo log) | ❌ | ❌ |
| 默认引擎 | ✅(5.5+) | ❌ | ❌ |
-- 查看当前存储引擎
SHOW ENGINES;
-- 查看表引擎
SHOW TABLE STATUS WHERE Name = 'users';
-- 建表时指定引擎
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 修改已有表引擎
ALTER TABLE users ENGINE=InnoDB;选择建议:
- InnoDB:绝大多数场景首选,支持事务、行锁、崩溃恢复
- MyISAM:只读数据仓库、日志表(写入不频繁),8.0 后官方不建议使用
- Memory:临时表、缓存表,重启后数据丢失
1.3 InnoDB 架构详解
InnoDB 的核心内存组件:
┌─────────────────────────────────────────┐
│ InnoDB 内存架构 │
├──────────────────┬──────────────────────┤
│ Buffer Pool │ Change Buffer │
│ (数据页 + 索引页) │ (二级索引变更缓存) │
├──────────────────┴──────────────────────┤
│ Log Buffer (redo log 缓存) │
├─────────────────────────────────────────┤
│ Adaptive Hash Index │
└─────────────────────────────────────────┘缓冲池(Buffer Pool):InnoDB 访问数据时的核心缓存,按 16KB 页管理。建议设置为物理内存的 60%-80%:
-- 查看缓冲池大小
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
-- 设置缓冲池大小(单位:字节)
SET GLOBAL innodb_buffer_pool_size = 4 * 1024 * 1024 * 1024; -- 4GBRedo Log(重做日志):保证事务持久性(Durability)。数据变更先写 redo log,再写数据页(WAL 技术):
-- 查看 redo log 配置
SHOW VARIABLES LIKE 'innodb_log_file_size';
SHOW VARIABLES LIKE 'innodb_log_files_in_group';Undo Log(撤销日志):实现事务回滚和 MVCC(多版本并发控制)。每条 INSERT/UPDATE/DELETE 都会生成 undo log。
Change Buffer(变更缓冲):缓存二级索引的变更操作,在索引页被读入缓冲池时合并,减少随机 I/O:
-- 查看 change buffer 大小
SHOW VARIABLES LIKE 'innodb_change_buffer_max_size'; -- 默认25(占缓冲池的25%)1.4 连接管理与查询流程
MySQL 处理一条 SQL 的完整流程:
客户端连接 → 连接管理 → 查询缓存(8.0已移除)
→ 解析器(词法分析+语法分析) → 预处理器
→ 优化器(选择执行计划) → 执行器
→ 存储引擎 → 返回结果-- 查看当前连接数
SHOW STATUS LIKE 'Threads_connected';
-- 查看最大连接数
SHOW VARIABLES LIKE 'max_connections';
-- 查看慢查询日志状态
SHOW VARIABLES LIKE 'slow_query_log';查询缓存(Query Cache,MySQL 8.0 已完全移除):5.7 及之前版本中,对完全相同的 SELECT 语句缓存结果集。由于缓存失效过于频繁(表有更新即清除),性能反而下降,8.0 彻底移除此功能。
二、数据类型
2.1 整数类型
| 类型 | 存储空间 | 有符号范围 | 无符号范围 |
|---|---|---|---|
| TINYINT | 1 字节 | -128 ~ 127 | 0 ~ 255 |
| SMALLINT | 2 字节 | -32,768 ~ 32,767 | 0 ~ 65,535 |
| MEDIUMINT | 3 字节 | -8,388,608 ~ 8,388,607 | 0 ~ 16,777,215 |
| INT | 4 字节 | -2^31 ~ 2^31-1 | 0 ~ 4,294,967,295 |
| BIGINT | 8 字节 | -2^63 ~ 2^63-1 | 0 ~ 18,446,744,073,709,551,615 |
-- 正确用法:使用无符号 + AUTO_INCREMENT
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
amount DECIMAL(10,2) NOT NULL
);
-- 注意:ZEROFILL 已废弃,不要使用
-- 显示宽度(INT(11))不影响存储,仅用于 ZEROFILL
CREATE TABLE t (a INT(11)); -- 显示宽度无实际作用2.2 浮点与定点数
-- FLOAT / DOUBLE:近似存储,存在精度损失
CREATE TABLE prices_approx (
product_id INT,
price FLOAT(10,2) -- 不精确,避免用于金额
);
-- DECIMAL:精确存储,用于金额
CREATE TABLE prices_exact (
product_id INT,
price DECIMAL(10,2) -- 精确,推荐用于金额
);
-- 演示精度问题
INSERT INTO prices_approx VALUES (1, 0.1);
INSERT INTO prices_exact VALUES (1, 0.1);
SELECT * FROM prices_approx WHERE price = 0.1; -- 可能查不到!
SELECT * FROM prices_exact WHERE price = 0.1; -- 正常查到结论:金额永远使用 DECIMAL,绝不使用 FLOAT/DOUBLE。
2.3 字符串类型
| 类型 | 最大长度 | 特点 |
|---|---|---|
| CHAR(N) | 255 字符 | 定长,不足补空格,检索快 |
| VARCHAR(N) | 65,535 字节 | 变长,额外 1-2 字节记录长度 |
| TINYTEXT | 255 字节 | 无默认值,无排序规则 |
| TEXT | 65,535 字节 (64KB) | 有额外 2 字节开销 |
| MEDIUMTEXT | 16MB | 有额外 3 字节开销 |
| LONGTEXT | 4GB | 有额外 4 字节开销 |
| BLOB | 同 TEXT | 二进制,区分大小写 |
CREATE TABLE strings_demo (
-- CHAR: 定长,适合固定长度的字段
country_code CHAR(2) NOT NULL, -- 'CN', 'US'
-- VARCHAR: 变长,适合长度不固定的字段
email VARCHAR(255) NOT NULL,
username VARCHAR(64) NOT NULL,
-- TEXT: 长文本,不设默认值
bio TEXT,
-- BLOB: 存储二进制数据(一般用文件系统替代)
avatar BLOB,
-- VARCHAR vs TEXT 选择原则:
-- 1. 长度 < 255 且长度稳定 → CHAR
-- 2. 长度 < 5000 且需要建立索引 → VARCHAR
-- 3. 超长文本 → TEXT(但索引需要前缀索引)
INDEX idx_bio (bio(100)) -- 前缀索引
);VARCHAR 与 TEXT 的核心区别:
- VARCHAR 可以设默认值,TEXT 不能
- VARCHAR 可以创建完整索引,TEXT 必须指定前缀长度
- VARCHAR 存储在行内,TEXT 可能存储在行外(超过 768 字节时)
2.4 日期时间类型
| 类型 | 范围 | 存储空间 | 特点 |
|---|---|---|---|
| DATE | '1000-01-01' ~ '9999-12-31' | 3 字节 | 仅日期 |
| TIME | '-838:59:59' ~ '838:59:59' | 3 字节 | 仅时间 |
| DATETIME | '1000-01-01 00:00:00' ~ '9999-12-31 23:59:59' | 8 字节 | 日期+时间,不受时区影响 |
| TIMESTAMP | '1970-01-01 00:00:01' UTC ~ '2038-01-19 03:14:07' UTC | 4 字节 | 受时区影响,2038 年问题 |
CREATE TABLE events (
id INT PRIMARY KEY,
-- 创建时间:自动设为当前时间
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- 更新时间:每次修改自动更新
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- 生日:无时区概念,用 DATE
birthday DATE,
-- 会议时间:精确到秒,用 DATETIME
meeting_time DATETIME(3), -- 毫秒精度
-- 仅时间:用 TIME
daily_alarm TIME
);
-- 时区影响演示
SET time_zone = '+00:00';
INSERT INTO events (id, meeting_time) VALUES (1, '2024-09-12 10:00:00');
SET time_zone = '+08:00';
SELECT meeting_time FROM events WHERE id = 1; -- TIMESTAMP 会转换,DATETIME 不变选择建议:
- 业务无关的创建/更新时间 →
TIMESTAMP(更省空间) - 未来日期或跨时区业务 →
DATETIME - 生日等纯日期 →
DATE - 需要毫秒/微秒精度 →
DATETIME(3)/DATETIME(6)
2.5 JSON 类型(5.7+)
MySQL 5.7 开始原生支持 JSON 类型,8.0 增加了 JSON 函数和索引支持。
CREATE TABLE products (
id INT PRIMARY KEY,
name VARCHAR(100),
attributes JSON,
CHECK (JSON_VALID(attributes))
);
INSERT INTO products VALUES (1, '手机',
'{"color": "黑色", "storage": "256GB", "price": 5999}');
-- JSON 查询
SELECT
name,
JSON_EXTRACT(attributes, '$.color') AS color,
attributes->>'$.storage' AS storage
FROM products
WHERE JSON_EXTRACT(attributes, '$.price') > 5000;
-- JSON_TABLE 展开为行(8.0+)
SELECT p.name, jt.*
FROM products p,
JSON_TABLE(attributes, '$' COLUMNS (
color VARCHAR(10) PATH '$.color',
storage VARCHAR(10) PATH '$.storage',
price INT PATH '$.price'
)) AS jt
WHERE jt.price > 5000;JSON 索引:MySQL 不能直接索引 JSON 列,但可以通过虚拟列建立索引:
ALTER TABLE products
ADD COLUMN price INT GENERATED ALWAYS AS (attributes->>'$.price') STORED,
ADD INDEX idx_price (price);2.6 如何选择最优类型
核心原则:小而精
- 够小就行:能用
TINYINT不用INT,能用SMALLINT不用BIGINT - 优先定点:金额用
DECIMAL,不用FLOAT/DOUBLE - 定长优先:固定长度的值用
CHAR不用VARCHAR - 不存文件:图片/文件存路径或 OSS URL,不用
BLOB - NOT NULL:能用
NOT NULL则用,减少存储和查询开销 - 合理默认值:数字用
0,字符串用'',时间用CURRENT_TIMESTAMP
-- 好的设计
CREATE TABLE user_profile (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
status TINYINT UNSIGNED NOT NULL DEFAULT 1, -- 1-正常 2-禁用
gender TINYINT UNSIGNED NOT NULL DEFAULT 0, -- 0-未知 1-男 2-女
age TINYINT UNSIGNED NOT NULL DEFAULT 0,
balance DECIMAL(12,2) NOT NULL DEFAULT 0.00,
email VARCHAR(255) NOT NULL DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;三、索引
3.1 B+Tree 索引原理
B+Tree 是 MySQL InnoDB 默认的索引结构,核心特性:
- 非叶子节点只存储键值,不存数据,单节点可存储大量键(一般为 1200+ 个键)
- 叶子节点存储完整的数据记录(聚簇索引)或主键值(二级索引)
- 叶子节点通过双向链表连接,支持范围查询
B+Tree 结构示意(阶数=3,简化版):
[50, 80]
/ | \
[10,30] [60,70] [90,100]
/ | / | | \
(1-9) (31-49) ... ... ...
↑ 叶子节点双向链表连接 ↑为什么 B+Tree 适合数据库:
- 树矮:三到四层就能存储上亿条数据,查询只需要 3-4 次 I/O
- 范围查询高效:叶子节点链表直接遍历
- 磁盘预读友好:节点大小 = 磁盘页大小(16KB),一次 I/O 读一页
3.2 聚簇索引 vs 二级索引
聚簇索引(Clustered Index):
- InnoDB 表必有且只有一个聚簇索引
- 主键即聚簇索引(若无主键,则选用第一个 UNIQUE NOT NULL 列;若无,则隐式创建
row_id) - 叶子节点存储整行数据
二级索引(Secondary Index,也称为辅助索引/普通索引):
- 叶子节点存储主键值
- 通过二级索引查询需要回表(先查二级索引得到主键,再查聚簇索引拿完整数据)
-- 创建表时自动建立聚簇索引
CREATE TABLE users (
id INT PRIMARY KEY, -- 聚簇索引
name VARCHAR(50),
email VARCHAR(100),
INDEX idx_name (name) -- 二级索引
);
-- 查询流程对比
EXPLAIN SELECT * FROM users WHERE id = 1;
-- → type: const, key: PRIMARY(直接走聚簇索引,无需回表)
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- → type: ref, key: idx_name, Extra: NULL(走二级索引,然后回表)
EXPLAIN SELECT id, name FROM users WHERE name = '张三';
-- → type: ref, key: idx_name, Extra: Using index(覆盖索引,无需回表)3.3 联合索引(复合索引)
联合索引遵循最左前缀原则(Leftmost Prefix Principle):
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT NOT NULL,
status TINYINT NOT NULL,
created_at DATETIME NOT NULL,
INDEX idx_user_status_time (user_id, status, created_at)
);
-- ✅ 走索引
EXPLAIN SELECT * FROM orders WHERE user_id = 100; -- user_id
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 1; -- user_id + status
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND created_at > '2024-01-01'; -- user_id
EXPLAIN SELECT * FROM orders WHERE user_id = 100 AND status = 1 AND created_at > '2024-01-01'; -- 全部
-- ❌ 不走索引(跳过了最左列)
EXPLAIN SELECT * FROM orders WHERE status = 1; -- 无 user_id
EXPLAIN SELECT * FROM orders WHERE created_at > '2024-01-01'; -- 无 user_id联合索引设计原则:
- 等值条件放前面:
user_id = ? AND status = ? AND created_at > ?建议(user_id, status, created_at) - 区分度大的列放前面:
gender(只有 2 种值)不如email(几乎都不同)区分度大 - 排序字段考虑在内:如果
ORDER BY created_at,将created_at放入联合索引可避免文件排序
-- 排序优化
CREATE INDEX idx_user_time ON orders (user_id, created_at);
EXPLAIN SELECT * FROM orders
WHERE user_id = 100
ORDER BY created_at DESC;
-- Extra: Using index condition(无 filesort,排序利用索引)3.4 覆盖索引与回表
覆盖索引(Covering Index):查询所需的所有列都在索引中,无需回表访问数据行。
-- 表结构
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(50),
department VARCHAR(50),
salary DECIMAL(10,2),
INDEX idx_dept_name (department, name, salary)
);
-- 覆盖索引查询:所需列全部在 idx_dept_name 中
EXPLAIN SELECT department, name, salary
FROM employees
WHERE department = '研发部';
-- Extra: Using index(无需回表)
-- 非覆盖索引查询:所需列不在索引中,需要回表
EXPLAIN SELECT department, name, salary, id
FROM employees
WHERE department = '研发部';
-- Extra: Using index(id是主键,也在二级索引叶子节点中,所以仍是覆盖索引)
-- 非覆盖:需要回表
EXPLAIN SELECT *
FROM employees
WHERE department = '研发部';
-- Extra: NULL(需要回表读取完整数据行)覆盖索引的收益:
- 减少 I/O(只需读取索引页)
- 减少随机 I/O(索引页顺序性好于数据页)
3.5 索引下推(ICP - Index Condition Pushdown)
MySQL 5.6+ 引入,将 WHERE 条件中属于索引列的部分「下推」到存储引擎层过滤,减少回表次数。
CREATE TABLE people (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT,
INDEX idx_name_age (name, age)
);
-- 无 ICP:先根据 name 查出所有记录(回表),再用 age 过滤
-- 有 ICP:在索引遍历时同时用 name 和 age 过滤,减少回表
EXPLAIN SELECT * FROM people
WHERE name LIKE '张%' AND age = 30;
-- Extra: Using index condition(表示使用了 ICP)ICP 生效条件:
- 使用二级索引
- WHERE 条件中部分列在索引中
- 存储引擎层可以过滤
3.6 索引失效场景
以下常见场景会导致索引失效:
-- 场景 1:对索引列使用函数
EXPLAIN SELECT * FROM users WHERE DATE(created_at) = '2024-01-01';
-- ❌ 不走索引,应改为:
EXPLAIN SELECT * FROM users WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02';
-- 场景 2:隐式类型转换
EXPLAIN SELECT * FROM users WHERE phone = 13900001111; -- phone 是 VARCHAR
-- ❌ 类型转换导致索引失效,应改为:
EXPLAIN SELECT * FROM users WHERE phone = '13900001111';
-- 场景 3:模糊匹配以通配符开头
EXPLAIN SELECT * FROM users WHERE name LIKE '%张三';
-- ❌ 无法使用前缀索引,应改为:
EXPLAIN SELECT * FROM users WHERE name LIKE '张三%'; -- ✅
-- 场景 4:OR 条件中有非索引列
EXPLAIN SELECT * FROM users WHERE id = 1 OR name = '张三';
-- ❌ 如果 name 无索引,全表扫描,应改为:
EXPLAIN SELECT * FROM users WHERE id = 1
UNION ALL
SELECT * FROM users WHERE name = '张三';
-- 场景 5:联合索引违反最左前缀
-- 假设索引 (a, b, c)
EXPLAIN SELECT * FROM t WHERE b = 1 AND c = 2; -- ❌ 跳过了 a
EXPLAIN SELECT * FROM t WHERE a = 1 AND c = 2; -- ⚠️ 只用到了 a 列
-- 场景 6:NOT IN / != / <>
EXPLAIN SELECT * FROM users WHERE status != 1;
-- ❌ 一般不走索引(除非比例很小),可考虑拆为两次查询或业务规避
-- 场景 7:IS NOT NULL 可能失效
EXPLAIN SELECT * FROM users WHERE email IS NOT NULL;
-- ⚠️ 取决于数据分布,NULL 值占比大时可能走索引,否则全表扫描四、SQL 优化
4.1 EXPLAIN 解读
EXPLAIN 是 SQL 优化的第一工具,输出解读至关重要:
EXPLAIN SELECT u.name, o.amount, o.created_at
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.status = 1
ORDER BY o.created_at DESC
LIMIT 10;输出列解读:
| 列名 | 关键取值 | 含义 |
|---|---|---|
| id | 1, 2 | SELECT 标识符,id 越大越先执行 |
| select_type | SIMPLE, PRIMARY, SUBQUERY, DERIVED | 查询类型 |
| table | u, o | 表名或别名 |
| type | system > const > eq_ref > ref > range > index > ALL | 访问类型,性能从好到差 |
| possible_keys | idx_status, PRIMARY, idx_user_id | 可能使用的索引 |
| key | idx_status | 实际使用的索引 |
| key_len | 1 | 索引使用的字节数 |
| rows | 100 | 预估扫描行数 |
| Extra | Using index, Using filesort, Using temporary | 额外信息 |
type 详解:
system:表只有一行(极少见)const:主键或唯一索引等值匹配eq_ref:JOIN 时使用主键/唯一索引等值匹配ref:普通索引等值匹配range:索引范围扫描(>、<、BETWEEN、IN)index:全索引扫描ALL:全表扫描(最差)
Extra 的关键信息:
Using index:✅ 覆盖索引,无需回表Using index condition:✅ 使用了 ICPUsing where:⚠️ 存储引擎返回后 MySQL 再做 WHERE 过滤Using filesort:❌ 需要额外排序(无法利用索引排序)Using temporary:❌ 使用了临时表(GROUP BY 无索引等)Using where; Using index:✅ 好现象
4.2 慢查询日志
-- 查看慢查询配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 2; -- 超过2秒的查询
SET GLOBAL log_queries_not_using_indexes = ON; -- 记录未用索引的查询
-- 慢查询日志文件位置
SHOW VARIABLES LIKE 'slow_query_log_file';# 使用 mysqldumpslow 分析慢查询日志
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# -s t: 按查询时间排序
# -t 10: 显示前10条
# 使用 pt-query-digest(Percona Toolkit)
pt-query-digest /var/log/mysql/slow.log4.3 分页优化
传统 LIMIT + OFFSET 在大偏移量时性能极差:
-- ❌ 糟糕:OFFSET 越大越慢
SELECT * FROM orders ORDER BY id DESC LIMIT 10 OFFSET 1000000;
-- MySQL 会扫描 1000010 行,然后丢弃前 1000000 行方案一:游标分页(推荐)
-- ✅ 第一次查询
SELECT id, order_no, amount, created_at
FROM orders
ORDER BY id DESC
LIMIT 10;
-- ✅ 后续查询:带上上一页最后一条的 id
SELECT id, order_no, amount, created_at
FROM orders
WHERE id < 100 -- 上一页最后一条记录的 id
ORDER BY id DESC
LIMIT 10;方案二:子查询优化
-- ✅ 利用覆盖索引减少回表
SELECT *
FROM orders
WHERE id >= (
SELECT id FROM orders
ORDER BY id
LIMIT 1000000, 1
)
ORDER BY id
LIMIT 10;方案三:JOIN 优化
-- ✅ JOIN 方式
SELECT o.*
FROM orders o
JOIN (
SELECT id FROM orders
ORDER BY id
LIMIT 1000000, 10
) AS tmp ON o.id = tmp.id;4.4 COUNT(*) 优化
-- InnoDB 中 COUNT(*) 需要扫描行数,无法像 MyISAM 那样直接读取
-- ❌ 慢:扫描大量数据
SELECT COUNT(*) FROM orders;
-- ✅ 使用二级索引覆盖扫描(比聚簇索引小)
EXPLAIN SELECT COUNT(id) FROM orders;
EXPLAIN SELECT COUNT(*) FROM orders;
-- 优化器会自动选择最小的二级索引
-- 不同条件统计
SELECT COUNT(*) FROM orders WHERE status = 1; -- 需要过滤
SELECT COUNT(*) FROM orders WHERE status = 2; -- 需要过滤
-- ✅ 使用汇总表实时缓存
CREATE TABLE order_stats (
status TINYINT PRIMARY KEY,
cnt BIGINT UNSIGNED NOT NULL
);
-- 通过触发器或应用层在 INSERT/DELETE 时更新
INSERT INTO order_stats (status, cnt) VALUES (1, 0)
ON DUPLICATE KEY UPDATE cnt = cnt + 1;
-- 查询时直接读取汇总表
SELECT cnt FROM order_stats WHERE status = 1;4.5 大表 ALTER TABLE 优化
对大表执行 DDL(如 ALTER TABLE ... ADD COLUMN)会重建整张表,导致长时间锁表。
方案一:MySQL 8.0 即时 DDL
MySQL 8.0.12+ 支持 ALGORITHM=INSTANT,仅修改数据字典,不锁表:
-- ✅ 8.0.12+:秒级完成,不锁表
ALTER TABLE orders ADD COLUMN remark VARCHAR(200) DEFAULT '',
ALGORITHM=INSTANT;
-- 不支持 INSTANT 的操作会回退到 INPLACE 或 COPY方案二:pt-online-schema-change(Percona Toolkit)
对于 MySQL 5.7 或复杂 DDL,使用在线工具:
# 安装 Percona Toolkit
apt install percona-toolkit
# 在线修改表结构
pt-online-schema-change --alter "ADD COLUMN remark VARCHAR(200) DEFAULT ''" \
D=db_name,t=orders \
--execute \
--check-replication-filters工作原理:
- 创建一张空的目标表(带新结构)
- 在原表上创建触发器(INSERT/UPDATE/DELETE),同步写入新表
- 分批拷贝原表数据到新表
- 交换表名,删除旧表
方案三:先创建新表再交换
-- 1. 创建新表
CREATE TABLE orders_new LIKE orders;
ALTER TABLE orders_new ADD COLUMN remark VARCHAR(200) DEFAULT '';
-- 2. 分批迁移数据(使用工具或手动)
INSERT INTO orders_new SELECT * FROM orders WHERE id BETWEEN 1 AND 10000;
-- ... 继续分批
INSERT INTO orders_new SELECT * FROM orders WHERE id > 9990000;
-- 3. 锁表并交换
LOCK TABLES orders WRITE, orders_new WRITE;
-- 检查数据一致性
ALTER TABLE orders RENAME TO orders_old;
ALTER TABLE orders_new RENAME TO orders;
UNLOCK TABLES;五、事务与锁
5.1 InnoDB 锁类型
锁分类:
├── 共享锁(S Lock):读锁,允许多个事务同时读
├── 排他锁(X Lock):写锁,互斥
├── 意向共享锁(IS Lock):表级锁,标记事务准备加 S 锁
├── 意向排他锁(IX Lock):表级锁,标记事务准备加 X 锁
├── 行锁(Record Lock):锁定单行记录
├── 间隙锁(Gap Lock):锁定范围,防止幻读
└── 临键锁(Next-Key Lock):行锁 + 间隙锁的组合-- 共享锁(S锁):SELECT ... LOCK IN SHARE MODE(8.0 前)/ FOR SHARE(8.0+)
START TRANSACTION;
SELECT * FROM users WHERE id = 1 FOR SHARE; -- S锁,其他事务可读不可写
-- COMMIT;
-- 排他锁(X锁):SELECT ... FOR UPDATE
START TRANSACTION;
SELECT * FROM users WHERE id = 1 FOR UPDATE; -- X锁,其他事务不可读也不可写
UPDATE users SET name = '新名字' WHERE id = 1;
-- COMMIT;
-- 行锁演示
CREATE TABLE demo_lock (
id INT PRIMARY KEY,
name VARCHAR(50)
) ENGINE=InnoDB;
INSERT INTO demo_lock VALUES (1, 'A'), (2, 'B'), (3, 'C');
-- 事务1
BEGIN;
UPDATE demo_lock SET name = 'A1' WHERE id = 1; -- 锁住 id=1
-- 事务2
BEGIN;
UPDATE demo_lock SET name = 'B1' WHERE id = 2; -- ✅ 可以执行,不同行不冲突
UPDATE demo_lock SET name = 'A2' WHERE id = 1; -- ❌ 阻塞,等待事务1释放间隙锁与临键锁(解决幻读):
CREATE TABLE t_lock (
id INT PRIMARY KEY,
val INT,
INDEX idx_val (val)
) ENGINE=InnoDB;
INSERT INTO t_lock VALUES (1, 10), (3, 30), (5, 50);
-- 事务1
BEGIN;
SELECT * FROM t_lock WHERE val BETWEEN 20 AND 40 FOR UPDATE;
-- 锁定范围:(10, 30], (30, 50] 即 val=30 的行锁 + 两边的间隙锁
-- 事务2:以下操作均被阻塞(间隙锁阻止插入)
INSERT INTO t_lock VALUES (2, 20); -- ❌ val=20 在(10,30)间隙内
INSERT INTO t_lock VALUES (4, 40); -- ❌ val=40 在(30,50)间隙内
-- 事务1
COMMIT; -- 释放锁,事务2的插入才继续执行5.2 死锁检测与处理
-- 模拟死锁
-- 事务1
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- 锁住 id=1
UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- 等待 id=2
-- 事务2
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 2; -- 锁住 id=2
UPDATE accounts SET balance = balance + 100 WHERE id = 1; -- 等待 id=1
-- ❌ 死锁!InnoDB 会自动检测并回滚其中一个事务
-- 查看死锁日志
SHOW ENGINE INNODB STATUS\G
-- 查看最近一次死锁的详细信息死锁检测机制:
- InnoDB 通过
wait-for graph算法检测死锁 - 检测到死锁后,选择回滚代价较小的事务(undo log 较小的)
- 返回
ERROR 1213 (40001): Deadlock found when trying to get lock
减少死锁的策略:
-- 1. 统一访问顺序
-- ✅ 所有事务都按 id 从小到大更新
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- 2. 缩短事务时间
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- ... 避免在事务中做耗时操作(RPC、文件读写等)
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- 3. 降低隔离级别(如果可以接受)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- RC 级别没有间隙锁,减少死锁概率
-- 4. 合理选择锁范围
-- 用 UPDATE ... WHERE id = ? 代替 SELECT ... FOR UPDATE + UPDATE5.3 隔离级别
SQL 标准定义了四种隔离级别,InnoDB 默认是 REPEATABLE READ:
-- 查看当前隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';
-- 设置会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 用途 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ 可能 | ✅ 可能 | ✅ 可能 | 极少使用 |
| READ COMMITTED | ❌ 避免 | ✅ 可能 | ✅ 可能 | PostgreSQL 默认,多数业务 OK |
| REPEATABLE READ | ❌ 避免 | ❌ 避免 | ✅ 可能 | MySQL 默认,MVCC + 间隙锁避免幻读 |
| SERIALIZABLE | ❌ 避免 | ❌ 避免 | ❌ 避免 | 强一致性场景,性能差 |
MVCC 实现原理:
InnoDB 通过每一行的两个隐藏列实现 MVCC:
DB_TRX_ID:最后修改该行的事务 IDDB_ROLL_PTR:指向 undo log 的指针
-- 演示 MVCC(需要两个会话)
-- 会话 A
BEGIN; -- 开启事务,事务ID=100
SELECT * FROM accounts WHERE id = 1; -- 看到 balance=1000
-- 会话 B
BEGIN; -- 事务ID=101
UPDATE accounts SET balance = 500 WHERE id = 1;
COMMIT;
-- 会话 A
SELECT * FROM accounts WHERE id = 1;
-- REPEATABLE READ: 依然看到 balance=1000(读取的是事务100的快照)
-- READ COMMITTED: 看到 balance=500(读取的是最新已提交数据)
COMMIT;六、高可用架构
6.1 主从复制原理
MySQL 主从复制基于 binlog(二进制日志),异步复制流程:
Master → binlog dump thread → binlog
↓ 网络传输
Slave → I/O thread → relay log → SQL thread → 数据更新# master 配置 (my.cnf)
[mysqld]
server-id=1
log-bin=mysql-bin
binlog-format=ROW # ROW 格式推荐(最精确)
binlog-do-db=myapp # 要复制的数据库
expire-logs-days=7 # 日志保留7天
max-binlog-size=500M # 每个 binlog 文件大小
# slave 配置 (my.cnf)
[mysqld]
server-id=2
relay-log=relay-bin
read-only=1 # 从库只读
log-replica-updates=ON # 记录从库自己的 binlog(级联复制需要)-- 1. Master 创建复制用户
CREATE USER 'repl'@'%' IDENTIFIED BY 'Repl@123';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
-- 2. Master 查看当前 binlog 位置
SHOW MASTER STATUS;
-- File: mysql-bin.000001, Position: 157
-- 3. 备份 Master 数据并恢复到 Slave
mysqldump -uroot -p --all-databases --master-data=2 > backup.sql
# 在 Slave 上导入
mysql -uroot -p < backup.sql
-- 4. Slave 配置复制
CHANGE MASTER TO
MASTER_HOST='192.168.1.10',
MASTER_PORT=3306,
MASTER_USER='repl',
MASTER_PASSWORD='Repl@123',
MASTER_LOG_FILE='mysql-bin.000001',
MASTER_LOG_POS=157;
-- 5. 启动复制
START SLAVE;
-- 6. 检查复制状态
SHOW SLAVE STATUS\G
-- 关键字段:
-- Slave_IO_Running: Yes
-- Slave_SQL_Running: Yes
-- Seconds_Behind_Master: 0 (延迟秒数)6.2 读写分离
实现方式:
方案一:应用层框架(推荐)
// Spring Boot + ShardingSphere-JDBC,代码零侵入
// application.yml
spring:
shardingsphere:
datasource:
names: write-ds,read-ds-1,read-ds-2
write-ds:
url: jdbc:mysql://master:3306/myapp
username: root
password: root123
read-ds-1:
url: jdbc:mysql://slave1:3306/myapp
username: root
password: root123
read-ds-2:
url: jdbc:mysql://slave2:3306/myapp
username: root
password: root123
rules:
readwrite-splitting:
data-sources:
myapp:
write-data-source-name: write-ds
read-data-source-names: read-ds-1,read-ds-2
load-balancer-name: round_robin方案二:中间件层(Proxy 模式)
# 使用 ProxySQL
docker run -d --name proxysql -p 6033:6033 -p 6032:6032 proxysql/proxysql
# 配置读写分离
mysql -uadmin -padmin -h127.0.0.1 -P6032
INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (0, 'master', 3306);
INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (1, 'slave1', 3306);
INSERT INTO mysql_servers (hostgroup_id, hostname, port) VALUES (1, 'slave2', 3306);
INSERT INTO mysql_query_rules (rule_id, active, match_digit, match_pattern, destination_hostgroup)
VALUES (1, 1, 1, '^SELECT.*', 1); -- SELECT 走读库
INSERT INTO mysql_query_rules (rule_id, active, match_digit, match_pattern, destination_hostgroup)
VALUES (2, 1, 1, '.*', 0); -- 其他走写库
LOAD MYSQL SERVERS TO RUNTIME;
LOAD MYSQL QUERY RULES TO RUNTIME;
SAVE MYSQL SERVERS TO DISK;
SAVE MYSQL QUERY RULES TO DISK;6.3 MGR(MySQL Group Replication)
MySQL Group Replication 是原生多主/单主复制方案,基于 Paxos 协议。
# MGR 配置(每个节点)
[mysqld]
server-id=1
gtid_mode=ON
enforce_gtid_consistency=ON
log-bin=mysql-bin
binlog-format=ROW
master_info_repository=TABLE
relay_log_info_repository=TABLE
binlog_checksum=NONE
# MGR 特定配置
plugin_load_add='group_replication.so'
group_replication_group_name='aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
group_replication_start_on_boot=off
group_replication_bootstrap_group=off
group_replication_local_address='192.168.1.10:33061'
group_replication_group_seeds='192.168.1.10:33061,192.168.1.11:33061,192.168.1.12:33061'
group_replication_ip_whitelist='192.168.1.0/24'
group_replication_single_primary_mode=ON # 单主模式-- 在第一个节点上启动组
SET GLOBAL group_replication_bootstrap_group=ON;
START GROUP_REPLICATION;
SET GLOBAL group_replication_bootstrap_group=OFF;
-- 在后续节点上加入组
CHANGE MASTER TO MASTER_USER='repl', MASTER_PASSWORD='Repl@123' FOR CHANNEL 'group_replication_recovery';
START GROUP_REPLICATION;
-- 查看组状态
SELECT * FROM performance_schema.replication_group_members;
SELECT * FROM performance_schema.replication_group_member_stats\G6.4 分库分表
当单表数据量过大(一般超过 5000 万行)时,需要分库分表。
水平分表策略:
-- 取模分片(简单但扩容困难)
-- 创建两张分表
CREATE TABLE orders_0 LIKE orders;
CREATE TABLE orders_1 LIKE orders;
-- 应用层根据 user_id % 2 决定写入哪张表
-- 或用 ShardingSphere 自动路由
-- 范围分片(适合有自然范围的数据)
CREATE TABLE orders_2024_q1 LIKE orders;
CREATE TABLE orders_2024_q2 LIKE orders;ShardingSphere 配置示例:
# ShardingSphere-JDBC 配置
spring:
shardingsphere:
rules:
sharding:
tables:
orders:
actual-data-nodes: ds$->{0..1}.orders_$->{0..1}
database-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: db-mod
table-strategy:
standard:
sharding-column: id
sharding-algorithm-name: table-mod
sharding-algorithms:
db-mod:
type: MOD
props:
sharding-count: 2
table-mod:
type: MOD
props:
sharding-count: 2分库分表带来的挑战:
- 跨节点 JOIN:需在应用层或中间件中做,性能下降
- 分布式事务:使用 Seata、ShardingSphere 分布式事务、TCC 等
- 全局主键:使用雪花算法(Snowflake)、美团 Leaf、UUID
- 跨节点分页:需要在各分片查询并合并排序
- 扩容难题:取模分片扩容需迁移数据,建议使用一致性哈希或范围分片
七、备份与恢复
7.1 mysqldump 逻辑备份
mysqldump 是 MySQL 自带的逻辑备份工具,导出 SQL 语句。
# 备份单个数据库
mysqldump -uroot -p --single-transaction --routines --triggers --events myapp > myapp.sql
# 备份所有数据库
mysqldump -uroot -p --single-transaction --all-databases --master-data=2 > all.sql
# 备份特定表
mysqldump -uroot -p myapp users orders > user_orders.sql
# 只导出表结构,不导数据
mysqldump -uroot -p --no-data myapp > schema.sql
# 只导出数据,不导表结构
mysqldump -uroot -p --no-create-info myapp > data.sql
# 压缩备份
mysqldump -uroot -p --single-transaction myapp | gzip > myapp_$(date +%F).sql.gz
# 恢复备份
mysql -uroot -p myapp < myapp.sql
# 或从压缩文件恢复
gunzip < myapp_2024-09-12.sql.gz | mysql -uroot -p myapp参数说明:
--single-transaction:在 InnoDB 中开启事务,获得一致性快照,不锁表(关键参数!)--master-data=2:记录 binlog 位置(注释形式),用于搭建从库--routines:包含存储过程和函数--triggers:包含触发器--events:包含事件
7.2 XtraBackup 物理备份
Percona XtraBackup 支持物理热备份,速度快,适合大数据库。
# 安装 XtraBackup 8.0
wget https://repo.percona.com/apt/percona-release_latest.generic_all.deb
dpkg -i percona-release_latest.generic_all.deb
apt update
apt install percona-xtrabackup-80 -y
# 全量备份
xtrabackup --backup \
--target-dir=/backup/mysql/full/$(date +%F) \
--user=root \
--password=root123
# 准备备份(应用 redo log,使数据一致)
xtrabackup --prepare \
--target-dir=/backup/mysql/full/2024-09-12
# 恢复备份(需要 MySQL 已停止且数据目录为空)
systemctl stop mysql
rm -rf /var/lib/mysql/*
xtrabackup --copy-back \
--target-dir=/backup/mysql/full/2024-09-12
chown -R mysql:mysql /var/lib/mysql
systemctl start mysql
# 增量备份
xtrabackup --backup \
--target-dir=/backup/mysql/inc1 \
--incremental-basedir=/backup/mysql/full/2024-09-12 \
--user=root \
--password=root123
# 增量恢复(需要与全量合并)
xtrabackup --prepare \
--apply-log-only \
--target-dir=/backup/mysql/full/2024-09-12
xtrabackup --prepare \
--apply-log-only \
--target-dir=/backup/mysql/full/2024-09-12 \
--incremental-dir=/backup/mysql/inc1
xtrabackup --prepare \
--target-dir=/backup/mysql/full/2024-09-12mysqldump vs XtraBackup 对比:
| 对比项 | mysqldump | XtraBackup |
|---|---|---|
| 备份方式 | 逻辑备份(SQL) | 物理备份(文件) |
| 速度 | 慢(大数据量) | 快 |
| 备份大小 | 较小 | 较大(含数据和索引) |
| 恢复速度 | 慢(逐条执行 SQL) | 快(直接复制文件) |
| 粒度 | 库/表/行 | 实例级别 |
| 适用场景 | 小到中型数据库(<50GB) | 大型数据库(>50GB) |
7.3 Binlog 恢复
binlog 可用于时间点恢复(Point-in-Time Recovery,PITR):
# 1. 查看 binlog 列表
mysql -uroot -p -e "SHOW BINARY LOGS;"
# 2. 将 binlog 导出为 SQL
mysqlbinlog --start-datetime="2024-09-12 10:00:00" \
--stop-datetime="2024-09-12 12:00:00" \
/var/lib/mysql/mysql-bin.000005 > recovery.sql
# 或通过位置恢复
mysqlbinlog --start-position=1000 \
--stop-position=5000 \
/var/lib/mysql/mysql-bin.000005 > recovery.sql
# 3. 恢复
mysql -uroot -p myapp < recovery.sql
# 完整恢复流程(从全量备份恢复到指定时间点)
# 1) 恢复最近一次全量备份
mysql -uroot -p < full_backup.sql
# 2) 应用之后的 binlog(到灾难发生前一刻)
mysqlbinlog --stop-datetime="2024-09-12 14:30:00" \
/var/lib/mysql/mysql-bin.000005 \
/var/lib/mysql/mysql-bin.000006 | mysql -uroot -p备份策略推荐(生产环境):
每日计划:
凌晨 02:00 - XtraBackup 全量备份(周日)或增量备份(周一~周六)
每小时 - binlog 定时同步到备份服务器
实时 - binlog 保留至少 7 天
每周验证:
在测试环境恢复最近一次全量 + binlog,验证数据完整性
灾难恢复 SLA:
全量备份 + binlog → RPO < 1 小时,RTO < 4 小时总结
本文从 MySQL 的安装与架构出发,系统性地介绍了:
- 安装与架构:Docker 和 apt/yum 安装方式,InnoDB 核心架构(缓冲池、redo log、undo log、change buffer),SQL 执行流程
- 数据类型:整数、浮点、字符串、日期时间、JSON 类型的选择原则
- 索引:B+Tree 原理、聚簇索引与二级索引、联合索引最左前缀、覆盖索引、索引下推、索引失效场景
- SQL 优化:EXPLAIN 解读、慢查询日志、分页优化、COUNT 优化、大表 DDL
- 事务与锁:行锁/间隙锁/临键锁、死锁处理、MVCC 与隔离级别
- 高可用架构:主从复制、读写分离、MGR 组复制、分库分表
- 备份与恢复:mysqldump 逻辑备份、XtraBackup 物理备份、binlog 时间点恢复
掌握这些知识点,配合实践操作,足以应对绝大多数 MySQL 生产环境的开发与运维工作。建议读者动手执行文中每个 SQL 示例,在实践中加深理解。