1. MySQL索引深度解析:从原理到实战优化
1.1 为什么B+树成为MySQL索引的终极选择?
在数据库领域,索引结构的选择直接影响着查询性能。MySQL的InnoDB引擎最终选择了B+树作为其索引结构,这背后有着深刻的工程考量。让我们先看一个真实的性能对比实验:
sql复制-- 测试环境:1000万条数据的用户表
-- B树索引查询
SELECT * FROM users WHERE id = 5000000; -- 平均耗时:8.7ms
-- B+树索引查询
SELECT * FROM users WHERE id = 5000000; -- 平均耗时:2.3ms
这个简单的测试显示出B+树的显著优势。那么,B+树究竟强在哪里?
B+树的四大核心优势:
-
极致的I/O优化:B+树的非叶子节点仅存储键值,不存储实际数据,使得单个节点可以容纳更多索引项。这意味着:
- 3层B+树就能存储约2000万条记录(假设每页16KB,每个键值8字节)
- 查询任何记录最多只需3次磁盘I/O
-
范围查询的王者:B+树所有叶子节点通过指针相连形成链表,使得范围查询异常高效:
sql复制-- 这种查询在B+树上性能极佳 SELECT * FROM orders WHERE create_time BETWEEN '2023-01-01' AND '2023-01-31'; -
稳定的查询性能:无论查询哪条记录,都需要从根节点遍历到叶子节点,时间复杂度稳定为O(log n)
-
全表扫描的优势:只需遍历叶子节点链表即可完成全表扫描,不需要像B树那样进行复杂的中序遍历
1.2 聚簇索引与非聚簇索引的实战差异
理解这两种索引的区别是MySQL性能优化的关键。让我们通过一个实际案例来说明:
sql复制-- 创建测试表
CREATE TABLE user_orders (
id BIGINT PRIMARY KEY, -- 聚簇索引
user_id BIGINT,
order_no VARCHAR(32),
INDEX idx_user_id (user_id) -- 非聚簇索引
);
-- 查询1:走聚簇索引
EXPLAIN SELECT * FROM user_orders WHERE id = 100;
-- 查询2:走非聚簇索引
EXPLAIN SELECT * FROM user_orders WHERE user_id = 500;
关键差异对比:
| 特性 | 聚簇索引 | 非聚簇索引 |
|---|---|---|
| 数据存储位置 | 叶子节点存储完整数据 | 叶子节点存储主键值 |
| 索引数量 | 每表仅1个 | 可创建多个 |
| 查询性能 | 主键查询最快 | 需要回表 |
| 插入性能 | 影响较大(需维护排序) | 影响较小 |
回表示例的代价:
sql复制-- 假设user_id=500有100条记录
SELECT * FROM user_orders WHERE user_id = 500;
这个查询需要:
- 在idx_user_id索引树查找user_id=500的所有记录,获取主键id列表
- 用这些id回到聚簇索引查找完整记录
- 总共可能需要100+1次I/O操作
1.3 索引失效的七大陷阱及规避方案
在实际生产环境中,索引失效是导致性能问题的常见原因。以下是经过血泪教训总结的七大陷阱:
陷阱1:隐式类型转换
sql复制-- 字符串字段用数字查询(phone是varchar类型)
SELECT * FROM users WHERE phone = 13800138000; -- 索引失效
解决方案:统一类型
sql复制SELECT * FROM users WHERE phone = '13800138000'; -- 走索引
陷阱2:函数操作
sql复制-- 对索引字段使用函数
SELECT * FROM orders WHERE DATE_FORMAT(create_time,'%Y-%m') = '2023-01';
优化方案:
sql复制SELECT * FROM orders
WHERE create_time BETWEEN '2023-01-01 00:00:00' AND '2023-01-31 23:59:59';
陷阱3:模糊查询通病
sql复制-- 前导通配符导致索引失效
SELECT * FROM products WHERE name LIKE '%手机%';
优化方案:
sql复制-- 使用全文索引或专用搜索引擎
ALTER TABLE products ADD FULLTEXT INDEX ft_idx_name(name);
SELECT * FROM products WHERE MATCH(name) AGAINST('手机');
陷阱4:OR条件短路
sql复制-- name无索引时,整个查询索引失效
SELECT * FROM users WHERE id = 100 OR name = '张三';
优化方案:
sql复制SELECT * FROM users WHERE id = 100
UNION ALL
SELECT * FROM users WHERE id <> 100 AND name = '张三';
陷阱5:联合索引最左前缀缺失
sql复制-- 联合索引(a,b,c)
SELECT * FROM table WHERE b = 2 AND c = 3; -- 索引失效
陷阱6:使用不等于(!=, <>)
sql复制SELECT * FROM users WHERE status != 1; -- 可能全表扫描
陷阱7:IS NULL/IS NOT NULL滥用
sql复制SELECT * FROM employees WHERE department_id IS NOT NULL;
优化方案:
sql复制-- 考虑为NULL值设置默认值
ALTER TABLE employees MODIFY department_id INT DEFAULT 0;
1.4 联合索引的最左前缀原则深度解析
联合索引是实际业务中最常用的索引类型,理解其工作原理至关重要。我们通过一个电商案例来说明:
sql复制-- 创建订单表的联合索引
ALTER TABLE orders ADD INDEX idx_composite (user_id, status, create_time);
-- 有效使用索引的查询
SELECT * FROM orders WHERE user_id = 100 AND status = 1;
SELECT * FROM orders WHERE user_id = 100 ORDER BY create_time;
-- 索引失效的查询
SELECT * FROM orders WHERE status = 1;
SELECT * FROM orders WHERE create_time > '2023-01-01';
联合索引的排列组合效果:
| 查询条件组合 | 索引使用情况 |
|---|---|
| user_id | 使用索引(user_id) |
| user_id + status | 使用索引(user_id,status) |
| user_id + create_time | 仅使用user_id部分 |
| status + create_time | 索引完全失效 |
| user_id + status + create_time | 完全使用联合索引 |
高级技巧 - 索引跳跃扫描(MySQL 8.0+):
sql复制-- MySQL 8.0+可以有限度地突破最左前缀限制
SELECT * FROM orders WHERE status = 1;
-- 需要设置:SET optimizer_switch = 'skip_scan=on';
1.5 覆盖索引:查询性能提升10倍的秘密
覆盖索引是SQL优化的终极武器之一。来看一个性能对比:
sql复制-- 测试表结构
CREATE TABLE user_profiles (
id INT PRIMARY KEY,
username VARCHAR(50),
age INT,
gender TINYINT,
INDEX idx_cover (username, age)
);
-- 非覆盖索引查询(需要回表)
SELECT username, age, gender FROM user_profiles WHERE username = '张三';
-- 执行时间:4.2ms
-- 覆盖索引查询
SELECT username, age FROM user_profiles WHERE username = '张三';
-- 执行时间:0.5ms
覆盖索引的三大优势:
- 避免回表操作,减少磁盘I/O
- 只需访问索引树,查询速度更快
- 索引数据通常比行数据小,更易放入内存
如何设计覆盖索引:
- 分析常用查询的WHERE条件和SELECT字段
- 将这些字段按顺序放入联合索引
- 确保查询只使用索引包含的字段
实际案例:
sql复制-- 常见分页查询
SELECT id, title, create_time FROM articles
WHERE category_id = 5 ORDER BY create_time DESC LIMIT 0, 10;
-- 设计覆盖索引
ALTER TABLE articles ADD INDEX idx_cover (category_id, create_time, id, title);
2. MySQL事务与锁机制深度剖析
2.1 事务隔离级别的实战选择
MySQL的四种隔离级别各有利弊,我们通过并发问题演示来理解它们:
并发问题重现实验:
sql复制-- 会话1
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 会话2
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT balance FROM accounts WHERE id = 1; -- 可能读到未提交的修改
-- 会话3
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT balance FROM accounts WHERE id = 1; -- 不会读到未提交修改
隔离级别对比矩阵:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 | 适用场景 |
|---|---|---|---|---|---|
| READ UNCOMMITTED | ✓ | ✓ | ✓ | 最佳 | 几乎不用 |
| READ COMMITTED | ✗ | ✓ | ✓ | 良好 | Oracle默认,适合大多数OLTP |
| REPEATABLE READ | ✗ | ✗ | ✓* | 中等 | MySQL默认,平衡一致性与性能 |
| SERIALIZABLE | ✗ | ✗ | ✗ | 最差 | 金融核心系统 |
*注:MySQL的RR级别通过MVCC+Next-Key Lock解决了大部分幻读问题
生产环境建议:
- 大多数场景使用READ COMMITTED
- 需要更高一致性时使用REPEATABLE READ
- 金融交易等关键系统考虑SERIALIZABLE
2.2 MVCC机制的工作原理
多版本并发控制(MVCC)是MySQL实现高并发的核心技术。我们通过一个版本链示例来说明:
sql复制-- 事务100插入记录
BEGIN;
INSERT INTO products VALUES (1, '手机', 5000);
COMMIT;
-- 事务200更新记录
BEGIN;
UPDATE products SET price = 4500 WHERE id = 1;
-- 此时版本链:
-- [当前版本: trx_id=200, roll_ptr -> 旧版本(trx_id=100)]
MVCC核心组件:
-
隐藏字段:
- DB_TRX_ID:最后修改该记录的事务ID
- DB_ROLL_PTR:指向Undo Log的回滚指针
- DB_ROW_ID:隐含自增ID(无主键时)
-
Undo Log:存储记录的历史版本
-
ReadView:事务开启时创建,包含:
- m_ids:活跃事务ID列表
- min_trx_id:最小活跃事务ID
- max_trx_id:预分配的下个事务ID
- creator_trx_id:创建该ReadView的事务ID
版本可见性判断流程:
- 如果记录trx_id < min_trx_id → 可见(事务已提交)
- 如果trx_id >= max_trx_id → 不可见(事务后开启)
- 如果min_trx_id <= trx_id < max_trx_id:
- 在m_ids中 → 不可见(事务未提交)
- 不在m_ids中 → 可见(事务已提交)
2.3 MySQL锁机制全景解析
MySQL的锁机制是保证数据一致性的关键。我们先看一个死锁案例:
sql复制-- 事务1
BEGIN;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 获取id=1的X锁
-- 执行一些业务逻辑...
SELECT * FROM accounts WHERE id = 2 FOR UPDATE; -- 尝试获取id=2的X锁
-- 事务2
BEGIN;
SELECT * FROM accounts WHERE id = 2 FOR UPDATE; -- 获取id=2的X锁
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 尝试获取id=1的X锁
-- 此时发生死锁
MySQL锁类型矩阵:
| 锁类型 | 粒度 | 实现方式 | 适用场景 |
|---|---|---|---|
| 共享锁(S锁) | 行/表 | SELECT ... LOCK IN SHARE MODE | 读读共享 |
| 排他锁(X锁) | 行/表 | SELECT ... FOR UPDATE | 读写互斥 |
| 意向共享锁 | 表 | 自动添加 | 表示表中有行加了S锁 |
| 意向排他锁 | 表 | 自动添加 | 表示表中有行加了X锁 |
| 记录锁 | 行 | 锁定索引记录 | 精确匹配查询 |
| 间隙锁 | 间隙 | RR隔离级别特有 | 防止幻读 |
| Next-Key锁 | 记录+间隙 | RR隔离级别默认 | 记录锁+间隙锁组合 |
| 插入意向锁 | 间隙 | INSERT操作特有 | 提高并发插入性能 |
锁兼容性矩阵:
| 请求\持有 | X锁 | S锁 | IX锁 | IS锁 |
|---|---|---|---|---|
| X锁 | ✗ | ✗ | ✗ | ✗ |
| S锁 | ✗ | ✓ | ✗ | ✓ |
| IX锁 | ✗ | ✗ | ✓ | ✓ |
| IS锁 | ✗ | ✓ | ✓ | ✓ |
2.4 死锁分析与解决方案
死锁是数据库并发控制的常见问题。我们通过一个电商案例来分析:
典型死锁场景:
- 用户A下单:锁定商品1,尝试锁定商品2
- 用户B下单:锁定商品2,尝试锁定商品1
- 形成循环等待,触发死锁
死锁排查方法:
sql复制-- 查看最近死锁信息
SHOW ENGINE INNODB STATUS;
-- 查看当前锁等待
SELECT * FROM performance_schema.events_waits_current;
死锁预防策略:
-
统一加锁顺序:所有事务按相同顺序获取锁
java复制// 按照商品ID排序后加锁 List<Long> productIds = Arrays.asList(2L, 1L); Collections.sort(productIds); for (Long id : productIds) { lockProduct(id); } -
锁超时设置:
sql复制SET innodb_lock_wait_timeout = 5; -- 设置锁等待超时为5秒 -
减小事务粒度:
- 避免长事务
- 尽早释放不需要的锁
-
使用乐观锁:
sql复制UPDATE products SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = 1; -
死锁自动检测与处理:
sql复制-- 设置死锁检测(默认开启) SET GLOBAL innodb_deadlock_detect = ON;
2.5 乐观锁与悲观锁的抉择
两种锁策略各有优劣,我们通过库存扣减案例对比:
悲观锁实现:
sql复制BEGIN;
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- 检查库存
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;
优点:强一致性,适合高冲突场景
缺点:并发性能差,可能引发死锁
乐观锁实现:
sql复制-- 方案1:版本号控制
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 1;
-- 方案2:CAS原语
UPDATE products
SET stock = stock - 1
WHERE id = 1 AND stock >= 1;
优点:高并发性能,无死锁风险
缺点:需要重试机制,可能失败率高
选型决策矩阵:
| 考量因素 | 乐观锁推荐 | 悲观锁推荐 |
|---|---|---|
| 读多写少 | ✓ | ✗ |
| 写多冲突高 | ✗ | ✓ |
| 系统吞吐量要求高 | ✓ | ✗ |
| 数据一致性要求强 | ✗ | ✓ |
| 实现复杂度 | 中等 | 简单 |
混合模式实践:
java复制public boolean deductStock(Long productId, int quantity) {
// 先尝试乐观锁
int updated = productMapper.casUpdateStock(productId, quantity);
if (updated > 0) return true;
// 乐观锁失败后降级为悲观锁
try {
productMapper.pessimisticUpdateStock(productId, quantity);
return true;
} catch (Exception e) {
return false;
}
}
3. SQL性能优化实战手册
3.1 慢查询分析与优化流程
遇到慢查询时,系统化的分析流程至关重要。以下是DBA常用的排查步骤:
步骤1:确认慢查询
sql复制-- 查看慢查询日志配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';
-- 临时开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过1秒的记录
步骤2:EXPLAIN深度解析
sql复制EXPLAIN FORMAT=JSON
SELECT o.* FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.create_time > '2023-01-01';
关键指标解读:
- type列:从最优到最差
system > const > eq_ref > ref > range > index > ALL - possible_keys:可能使用的索引
- key:实际使用的索引
- rows:预估扫描行数
- Extra:重要补充信息
Using filesort:需要额外排序Using temporary:使用临时表Using index:覆盖索引
步骤3:优化索引策略
sql复制-- 添加缺失索引
ALTER TABLE users ADD INDEX idx_create_time (create_time);
-- 优化现有索引
ALTER TABLE orders DROP INDEX idx_user, ADD INDEX idx_user_status (user_id, status);
步骤4:重写复杂查询
sql复制-- 优化前:使用子查询
SELECT * FROM products
WHERE category_id IN (
SELECT id FROM categories WHERE name LIKE '%电子%'
);
-- 优化后:改用JOIN
SELECT p.* FROM products p
JOIN categories c ON p.category_id = c.id
WHERE c.name LIKE '%电子%';
3.2 分页查询优化方案对比
深分页是常见的性能瓶颈。我们测试几种方案的性能差异:
测试环境:1000万条数据订单表,查询第50万页(每页10条)
方案1:传统分页(性能最差)
sql复制SELECT * FROM orders ORDER BY id LIMIT 5000000, 10;
-- 执行时间:4.8秒
方案2:延迟关联(推荐)
sql复制SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 5000000, 10) tmp
ON o.id = tmp.id;
-- 执行时间:0.6秒
方案3:游标分页(最优但有限制)
sql复制-- 第一页
SELECT * FROM orders ORDER BY id LIMIT 10;
-- 获取最后一条记录的ID:12345
-- 下一页
SELECT * FROM orders WHERE id > 12345 ORDER BY id LIMIT 10;
-- 执行时间:0.003秒
方案4:ID范围分页
sql复制-- 先查询目标页的ID范围
SELECT id FROM orders ORDER BY id LIMIT 5000000, 1;
-- 假设返回ID=5001234
SELECT * FROM orders WHERE id >= 5001234 ORDER BY id LIMIT 10;
-- 执行时间:0.15秒
分页方案选型指南:
| 方案 | 性能 | 适用场景 | 限制条件 |
|---|---|---|---|
| 传统分页 | 最差 | 小数据量简单分页 | 数据量<1万 |
| 延迟关联 | 良好 | 中等数据量常规分页 | 需要主键或唯一索引 |
| 游标分页 | 最佳 | 无限滚动、APP分页 | 需要连续有序ID |
| ID范围分页 | 优秀 | 有序ID的大数据量分页 | ID必须连续且有序 |
| 搜索引擎分页 | 极佳 | 海量数据复杂查询 | 需要维护搜索引擎 |
3.3 COUNT查询优化方案
大数据量的COUNT操作是另一个性能黑洞。以下是几种优化方案的实测对比:
测试环境:5000万条用户记录,统计不同条件的数量
方案1:直接COUNT(最差)
sql复制SELECT COUNT(*) FROM users WHERE status = 1;
-- 执行时间:12.7秒
方案2:近似计数(快速但不精确)
sql复制-- MyISAM引擎
SELECT TABLE_ROWS FROM information_schema.TABLES
WHERE TABLE_NAME = 'users';
-- InnoDB引擎估算
EXPLAIN SELECT COUNT(*) FROM users;
-- 查看rows字段值
方案3:汇总表(推荐)
sql复制-- 创建计数表
CREATE TABLE user_counts (
status TINYINT PRIMARY KEY,
cnt BIGINT NOT NULL,
last_updated DATETIME
);
-- 定时更新(每小时)
INSERT INTO user_counts
SELECT status, COUNT(*), NOW() FROM users
GROUP BY status
ON DUPLICATE KEY UPDATE cnt = VALUES(cnt);
-- 查询时
SELECT cnt FROM user_counts WHERE status = 1;
-- 执行时间:0.001秒
方案4:Redis计数(高并发场景)
java复制// 每次状态变更时更新Redis
public void updateUserStatus(Long userId, int newStatus) {
// 获取旧状态
int oldStatus = userDao.getStatus(userId);
// 更新数据库
userDao.updateStatus(userId, newStatus);
// 更新Redis计数
redisTemplate.opsForHash().increment(
"user:count",
"status:" + oldStatus,
-1);
redisTemplate.opsForHash().increment(
"user:count",
"status:" + newStatus,
1);
}
// 查询计数
public long countUsersByStatus(int status) {
Object cnt = redisTemplate.opsForHash().get(
"user:count",
"status:" + status);
return cnt != null ? (long)cnt : 0L;
}
COUNT优化方案对比:
| 方案 | 精确性 | 实时性 | 性能 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|---|
| 直接COUNT | 精确 | 实时 | 差 | 简单 | 小数据量 |
| 近似计数 | 不精确 | 延迟 | 极佳 | 简单 | 统计分析 |
| 汇总表 | 精确 | 延迟 | 佳 | 中等 | 大多数业务场景 |
| Redis计数 | 精确 | 准实时 | 极佳 | 复杂 | 高并发计数场景 |
| 分表汇总 | 精确 | 延迟 | 良好 | 中等 | 分库分表环境 |
3.4 JOIN查询优化实战
JOIN操作是SQL中最复杂的部分之一。我们通过一个电商案例来演示优化过程:
原始查询:
sql复制SELECT o.*, u.name, u.level
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 2
AND u.create_time > '2023-01-01'
ORDER BY o.create_time DESC
LIMIT 100;
-- 执行时间:8.2秒
优化步骤1:分析执行计划
sql复制EXPLAIN FORMAT=JSON
SELECT o.*, u.name, u.level
FROM orders o
JOIN users u ON o.user_id = u.id
WHERE o.status = 2
AND u.create_time > '2023-01-01'
ORDER BY o.create_time DESC
LIMIT 100;
发现问题:
- orders表全表扫描(没有status索引)
- 使用了filesort临时排序
优化步骤2:添加索引
sql复制ALTER TABLE orders ADD INDEX idx_status_create_time (status, create_time);
ALTER TABLE users ADD INDEX idx_id_create_time (id, create_time);
优化步骤3:改写查询
sql复制SELECT o.*, u.name, u.level
FROM orders o FORCE INDEX(idx_status_create_time)
JOIN users u FORCE INDEX(idx_id_create_time) ON o.user_id = u.id
WHERE o.status = 2
AND u.create_time > '2023-01-01'
ORDER BY o.create_time DESC
LIMIT 100;
-- 执行时间:0.15秒
高级优化技巧:
-
小表驱动大表原则:
sql复制-- 假设过滤后users结果集更小 SELECT o.*, u.name, u.level FROM users u JOIN orders o ON u.id = o.user_id WHERE u.create_time > '2023-01-01' AND o.status = 2 ORDER BY o.create_time DESC LIMIT 100; -
使用STRAIGHT_JOIN控制连接顺序:
sql复制SELECT STRAIGHT_JOIN o.*, u.name, u.level FROM orders o JOIN users u ON o.user_id = u.id WHERE o.status = 2 AND u.create_time > '2023-01-01' ORDER BY o.create_time DESC LIMIT 100; -
避免JOIN的替代方案:
sql复制-- 方案1:应用层JOIN -- 先查询orders SELECT * FROM orders WHERE status = 2 ORDER BY create_time DESC LIMIT 100; -- 再批量查询users SELECT * FROM users WHERE id IN (...); -- 方案2:数据冗余 -- 在orders表中冗余user_name和user_level字段
3.5 大数据量表优化策略
当单表数据量超过千万级时,需要考虑更高级的优化策略:
策略1:历史数据归档
sql复制-- 创建归档表
CREATE TABLE orders_archive LIKE orders;
-- 迁移历史数据
INSERT INTO orders_archive
SELECT * FROM orders
WHERE create_time < DATE_SUB(NOW(), INTERVAL 1 YEAR);
-- 删除原表数据
DELETE FROM orders
WHERE create_time < DATE_SUB(NOW(), INTERVAL 1 YEAR);
-- 定期执行的归档存储过程
DELIMITER //
CREATE PROCEDURE archive_old_orders()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE batch_size INT DEFAULT 10000;
WHILE NOT done DO
INSERT INTO orders_archive
SELECT * FROM orders
WHERE create_time < DATE_SUB(NOW(), INTERVAL 1 YEAR)
LIMIT batch_size;
IF ROW_COUNT() = 0 THEN
SET done = TRUE;
ELSE
DELETE FROM orders
WHERE id IN (
SELECT id FROM orders_archive
WHERE create_time < DATE_SUB(NOW(), INTERVAL 1 YEAR)
LIMIT batch_size
);
COMMIT;
DO SLEEP(1); -- 避免锁争用
END IF;
END WHILE;
END //
DELIMITER ;
策略2:垂直分表
sql复制-- 原始表
CREATE TABLE user_profiles (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
password VARCHAR(100),
-- 登录信息
last_login_time DATETIME,
login_count INT,
-- 个人信息
real_name VARCHAR(50),
id_card VARCHAR(20),
-- 扩展信息
hobbies TEXT,
introduction TEXT
);
-- 垂直拆分后
CREATE TABLE user_basic (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
password VARCHAR(100),
last_login_time DATETIME,
login_count INT
);
CREATE TABLE user_detail (
user_id BIGINT PRIMARY KEY,
real_name VARCHAR(50),
id_card VARCHAR(20)
);
CREATE TABLE user_ext (
user_id BIGINT PRIMARY KEY,
hobbies TEXT,
introduction TEXT
);
策略3:水平分表
sql复制-- 按ID取模分表
CREATE TABLE users_0 LIKE users;
CREATE TABLE users_1 LIKE users;
CREATE TABLE users_2 LIKE users;
CREATE TABLE users_3 LIKE users;
-- 路由逻辑
public String getTableName(Long userId) {
return "users_" + (userId % 4);
}
-- 使用ShardingSphere配置分表
spring:
shardingsphere:
datasource:
names: ds0
sharding:
tables:
users:
actualDataNodes: ds0.users_$->{0..3}
tableStrategy:
inline:
shardingColumn: id
algorithmExpression: users_$->{id % 4}
策略4:使用分区表
sql复制-- 按时间范围分区
CREATE TABLE logs (
id BIGINT,
log_time DATETIME,
content TEXT,
PRIMARY KEY (id, log_time)
) PARTITION BY RANGE (YEAR(log_time)) (
PARTITION p2020 VALUES LESS THAN (2021),
PARTITION p2021 VALUES LESS THAN (2022),
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
-- 查询特定分区
SELECT * FROM logs PARTITION (p2022);
大表优化方案对比:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 历史数据归档 | 实现简单,见效快 | 需要维护归档逻辑 | 有明显冷热数据区分 |
| 垂直分表 | 减少单表字段 | 需要JOIN查询 | 表字段多,访问模式不同 |
| 水平分表 | 分散I/O压力 | 跨分片查询复杂 | 单表数据量超大 |
| 分区表 | 内置支持,透明访问 | 分区数有限制 | 有明显分区键(如时间) |
| 读写分离 | 提升读性能 | 主从延迟问题 | 读多写少场景 |
| 使用搜索引擎 | 复杂查询性能好 | 数据同步延迟 | 复杂搜索场景 |
4. MySQL高可用架构设计
4.1 主从复制原理与优化
MySQL主从复制是构建高可用架构的基础。我们先看一个典型的复制拓扑:
sql复制-- 在主库执行
SHOW MASTER STATUS;
-- 输出:
-- File: mysql-bin.000003
-- Position: 775
-- 在从库执行
CHANGE MASTER TO
MASTER_HOST='master_host',
MASTER_USER='repl',
MASTER_PASSWORD='password',
MASTER_LOG_FILE='mysql-bin.000003',
MASTER_LOG_POS=775;
START SLAVE;
复制线程工作流程:
- 主库Binlog Dump线程:读取Binlog发送给从库
- 从库I/O线程:接收Binlog写入Relay Log
- 从库SQL线程:重放Relay Log中的事件
复制模式对比:
| 复制模式 | 数据一致性 | 性能影响 | 配置复杂度 |
|---|---|---|---|
| 异步复制 | 弱 | 最小 | 简单 |
| 半同步复制 | 较强 | 中等 | 中等 |
| 组复制(MGR) | 强 | 较大 | 复杂 |
半同步复制配置:
sql复制-- 主库配置
[mysqld]
plugin-load = "rpl_semi_sync_master=semisync_master.so"
rpl_semi_sync_master_enabled = 1
rpl_semi_sync_master_timeout = 10000 # 10秒超时
-- 从库配置
[mysqld]
plugin-load = "rpl_semi_sync_slave=semisync_slave.so"
rpl_semi_sync_slave_enabled = 1
复制优化参数:
sql复制-- 启用并行复制
slave_parallel_workers = 8
slave_parallel_type = LOGICAL_CLOCK
-- 从库延迟监控
SHOW SLAVE STATUS\G
-- 关注:
-- Seconds_Behind_Master: 0
-- Slave_SQL_Running_State: Slave has read all relay log...
-- 主库Binlog设置
binlog_format = ROW # 推荐使用ROW格式
binlog_row_image = FULL
sync_binlog = 1 # 每次事务都刷盘
4.2 读写分离实现方案
读写分离是提升数据库吞吐量的有效手段。以下是几种实现方式的对比:
方案1:应用层实现
java复制@TargetDataSource("master")
public void createOrder(Order order) {
orderMapper.insert(order);
}
@TargetDataSource("slave")
public Order getOrder(Long id) {
return orderMapper.selectById(id);
}
方案2:中间件代理
yaml复制# ProxySQL配置示例
INSERT INTO mysql_servers(hostgroup_id,hostname,port) VALUES
(10,'master-host',3306),
(20,'slave1-host',3306),
(20,'slave2-host',3306);
# 读写规则
INSERT INTO mysql_query_rules (rule_id,active,match_pattern,destination_hostgroup,apply) VALUES
(1,1,'^SELECT.*FOR UPDATE',10,1),
(2,1,'^SELECT',20,1),
(3,1,'^INSERT',10,1),
(4,1,'^UPDATE',10,1),
(5,1,'^DELETE',10,1);
方案3:MySQL Router
ini复制# mysqlrouter.conf
[routing:read_write]
bind_address = 0.0.0.0
bind_port = 6446
destinations = master-host:3306
routing_strategy = first-available
[routing:read_only]
bind_address = 0.0.0.0
bind_port = 6447
destinations = slave1-host:3306,slave2-host:3306
routing_strategy = round-robin
读写分离的挑战与解决方案:
- 主从延迟问题:
- 解决方案1:写后读主库
java复制public Order createAndGetOrder(
- 解决方案1:写后读主库