MySQL 从入门到精通:SQL、索引、优化与高可用

2024-09-12 后端

一、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 -proot123

apt/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_installation

1.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;  -- 4GB

Redo 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 如何选择最优类型

核心原则:小而精

  1. 够小就行:能用 TINYINT 不用 INT,能用 SMALLINT 不用 BIGINT
  2. 优先定点:金额用 DECIMAL,不用 FLOAT/DOUBLE
  3. 定长优先:固定长度的值用 CHAR 不用 VARCHAR
  4. 不存文件:图片/文件存路径或 OSS URL,不用 BLOB
  5. NOT NULL:能用 NOT NULL 则用,减少存储和查询开销
  6. 合理默认值:数字用 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 适合数据库:

  1. 树矮:三到四层就能存储上亿条数据,查询只需要 3-4 次 I/O
  2. 范围查询高效:叶子节点链表直接遍历
  3. 磁盘预读友好:节点大小 = 磁盘页大小(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

联合索引设计原则:

  1. 等值条件放前面:user_id = ? AND status = ? AND created_at > ? 建议 (user_id, status, created_at)
  2. 区分度大的列放前面:gender(只有 2 种值)不如 email(几乎都不同)区分度大
  3. 排序字段考虑在内:如果 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 索引失效场景

以下常见场景会导致索引失效:

四、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:✅ 使用了 ICP
  • Using 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.log

4.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

工作原理:

  1. 创建一张空的目标表(带新结构)
  2. 在原表上创建触发器(INSERT/UPDATE/DELETE),同步写入新表
  3. 分批拷贝原表数据到新表
  4. 交换表名,删除旧表

方案三:先创建新表再交换

-- 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 + UPDATE

5.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:最后修改该行的事务 ID
  • DB_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(级联复制需要)

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\G

6.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

分库分表带来的挑战:

  1. 跨节点 JOIN:需在应用层或中间件中做,性能下降
  2. 分布式事务:使用 Seata、ShardingSphere 分布式事务、TCC 等
  3. 全局主键:使用雪花算法(Snowflake)、美团 Leaf、UUID
  4. 跨节点分页:需要在各分片查询并合并排序
  5. 扩容难题:取模分片扩容需迁移数据,建议使用一致性哈希或范围分片

七、备份与恢复

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 支持物理热备份,速度快,适合大数据库。

mysqldump 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 的安装与架构出发,系统性地介绍了:

  1. 安装与架构:Docker 和 apt/yum 安装方式,InnoDB 核心架构(缓冲池、redo log、undo log、change buffer),SQL 执行流程
  2. 数据类型:整数、浮点、字符串、日期时间、JSON 类型的选择原则
  3. 索引:B+Tree 原理、聚簇索引与二级索引、联合索引最左前缀、覆盖索引、索引下推、索引失效场景
  4. SQL 优化:EXPLAIN 解读、慢查询日志、分页优化、COUNT 优化、大表 DDL
  5. 事务与锁:行锁/间隙锁/临键锁、死锁处理、MVCC 与隔离级别
  6. 高可用架构:主从复制、读写分离、MGR 组复制、分库分表
  7. 备份与恢复:mysqldump 逻辑备份、XtraBackup 物理备份、binlog 时间点恢复

掌握这些知识点,配合实践操作,足以应对绝大多数 MySQL 生产环境的开发与运维工作。建议读者动手执行文中每个 SQL 示例,在实践中加深理解。