目录
4.MySQL 中 count(*)、count(1) 和 count(字段名) 有什么区别?(易)
6.如何使用 MySQL 的 EXPLAIN 语句进行查询分析?(中)
8.什么是 MySQL 的主从同步机制?它是如何实现的?(中)
1.MySQL 中有哪些锁类型?(中)
MySQL的锁机制主要围绕并发控制和数据一致性设计。核心分类按粒度有全局锁(影响整个实例)、表级锁(包括基本表锁、元数据锁MDL和InnoDB特有的意向锁IS/IX)、以及InnoDB支持的行级锁(记录锁、间隙锁、临键锁、插入意向锁)。按读写属性则分为共享锁S(允许多读)和排他锁X(独占读写)。
拓展:
I.详细区分:
-
按粒度划分:
-
全局锁: 锁定整个数据库实例(
FLUSH TABLES WITH READ LOCK
)。主要用于全库逻辑备份,阻塞所有写操作和大部分DDL,影响非常大。 -
表级锁:
-
表锁: 最基本的锁,锁定整张表(
LOCK TABLES ... READ/WRITE
)。MyISAM引擎默认使用。写锁阻塞其他所有读写,读锁阻塞写但不阻塞读。InnoDB通常避免使用。 -
元数据锁: 在访问表时自动加锁(MDL)。保证表结构不被修改(如DDL操作)。读操作加读锁,写操作加写锁。DDL操作需要排他MDL锁,会阻塞读写操作,也容易被长事务阻塞。
-
意向锁: InnoDB特有。表级锁,表示事务稍后会在表中的某些行上需要哪种类型的锁(共享或独占)。
-
意向共享锁: 表示事务打算在某些行上加共享锁。
-
意向排他锁: 表示事务打算在某些行上加排他锁。
-
作用: 快速判断表内是否有行被锁定(避免逐行检查),实现行锁与表锁的共存。例如,事务A对行加了S锁(行级),同时会对表加IS锁(表级)。事务B想加表锁时,看到表上有IS锁,就知道表内有行被S锁锁定了,如果事务B想加的是独占表锁(X锁),它就需要等待。
-
-
-
行级锁: InnoDB引擎支持。锁定索引记录(即使表没有显式索引,InnoDB也会创建隐藏聚簇索引)。是支持高并发的基础。细分为:
-
记录锁: 锁定索引中的单条记录。其他事务无法修改或删除(取决于锁类型)这条记录。
-
间隙锁: 锁定索引记录之间的间隙(一个开区间)。防止其他事务在这个间隙中插入新记录,从而解决幻读问题(在
REPEATABLE READ
隔离级别下)。 -
临键锁: 记录锁 + 间隙锁的组合。锁定一条记录及其之前的间隙(一个左开右闭区间)。InnoDB行锁的默认加锁方式(在
REPEATABLE READ
下)。同样用于防止幻读。 -
插入意向锁: 一种特殊的间隙锁。在INSERT操作前设置。表示事务想在某个间隙插入新记录,但正在等待。允许多个事务在同一个间隙的不同位置等待插入(只要不冲突),提高并发插入性能。
-
-
-
按读写属性划分:
-
共享锁: 也称为读锁(
S Lock
)。允许多个事务同时读取同一资源(行/表),但阻止任何事务获取该资源的排他锁(即阻止写)。 -
排他锁: 也称为写锁(
X Lock
)。只允许一个事务读写资源。阻止其他事务获取该资源的任何共享锁或排他锁(即阻止读和写)。
-
-
其他相关概念:
-
自增锁: 一种特殊的表级锁,用于处理具有
AUTO_INCREMENT
列的表上的并发插入操作。保证自增值的唯一性和连续性。 -
死锁: 两个或多个事务互相等待对方释放锁,导致都无法继续执行。InnoDB有死锁检测和回滚机制。
-
锁等待超时: 事务在获取锁时等待超过配置的时间(
innodb_lock_wait_timeout
)会报错。
-
II.表格总结
锁类型 | 级别 | 锁模式/意图 | 主要应用场景/触发方式 | 冲突对象 (主要) |
---|---|---|---|---|
全局锁 | 全局 | 只读 | FLUSH TABLES WITH READ LOCK | 所有其他会话的写操作 |
表级锁 | ||||
表共享读锁 | 表 | 共享 (S) | LOCK TABLES table_name READ | 表独占写锁 |
表独占写锁 | 表 | 排他 (X) | LOCK TABLES table_name WRITE | 表共享读锁、表独占写锁 |
意向共享锁 (IS) | 表 | 意向共享 | 事务准备给某些行加 S 锁时自动获得 | 表独占写锁 |
意向排他锁 (IX) | 表 | 意向排他 | 事务准备给某些行加 X 锁时自动获得 | 表共享读锁、表独占写锁 |
元数据锁 (MDL) | 表 | 读/写 | 访问表时自动加读锁,修改结构时自动加写锁 | 读锁间兼容;写锁阻塞所有读写 |
AUTO-INC 锁 | 表 | 特殊 | 向有 AUTO_INCREMENT 列的表插入数据时 | 其他向同一表插入的事务 |
行级锁 (InnoDB) | ||||
记录锁 (Record Lock) | 行 | 共享 (S) | SELECT ... LOCK IN SHARE MODE | 同一行的 X 锁 |
行 | 排他 (X) | SELECT ... FOR UPDATE , UPDATE , DELETE , INSERT | 同一行的 S 锁、X 锁 | |
间隙锁 (Gap Lock) | 间隙 | 特殊 | 在 REPEATABLE READ 下,防止幻读 | 试图在该间隙插入的插入意向锁 |
临键锁 (Next-Key Lock) | 行+间隙 | 组合 | InnoDB REPEATABLE READ 的默认行锁 | 覆盖记录锁和间隙锁的冲突规则 |
插入意向锁 | 间隙 | 特殊意向 | INSERT 操作前 | 同一确切插入位置的插入意向锁,该位置的间隙锁 |
III.锁兼容性矩阵(表级):
IS和IX的作用就是在上表级锁的时候,可以快速判断是否可以上锁,而不需要遍历表中的所有记录。IS和IX相互之间是不会冲突的,因为它们的作用只是打个标记。
X | IX | S | IS | |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
2.MySQL 的乐观锁和悲观锁是什么?(中)
(乐观锁和悲观锁都不是具体的锁,而是一种设计思想)
悲观锁的核心思想是‘防患于未然’,它在操作数据前就通过 MySQL 的锁机制(如 SELECT ... FOR UPDATE
)显式锁定数据,确保独占访问,适合写冲突严重的场景,但牺牲了并发性。乐观锁的核心思想是‘事后校验’,它利用版本号或时间戳机制,在提交更新时才检查数据是否被修改(WHERE ... AND version=...
),无锁读取提升了并发性能,适合读多写少的低冲突场景,但需要应用层处理提交失败(如重试)。
拓展:
I.表格总结
特性 | 悲观锁 (Pessimistic Lock) | 乐观锁 (Optimistic Lock) |
---|---|---|
核心思想 | “先锁再改” - 假设冲突很可能发生 | “先改再验” - 假设冲突不太可能发生 |
实现机制 | 依赖数据库锁 (SELECT ... FOR UPDATE ) | 使用版本号/时间戳 + WHERE 条件检查 |
加锁时机 | 操作数据前立即加锁 | 操作数据期间不加锁 |
冲突检测 | 通过加锁阻塞其他事务来避免冲突 | 在提交更新时检测版本冲突 |
性能 | 开销大,并发度低 (锁竞争) | 开销小,并发度高 (无锁读) |
失败处理 | 较少发生 (锁保证成功) | 可能发生冲突导致更新失败,需重试/处理 |
适用场景 | 写多读少,强一致性要求高,冲突概率高 | 读多写少,高并发要求,冲突概率低 |
II.悲观锁示例:
START TRANSACTION;
-- 查询商品库存并锁定该行记录 (id=1)
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- ... 在应用层判断库存是否足够 ...
-- 更新库存 (在锁的保护下)
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT; -- 提交时释放锁
III.乐观锁示例:
-- 1. 读取数据 (假设当前 version=5)
SELECT stock, version FROM products WHERE id = 1;
-- ... 在应用层判断库存足够,并计算新库存 new_stock ...
-- 2. 更新数据 (同时检查版本号并更新它)
UPDATE products
SET stock = new_stock, version = version + 1
WHERE id = 1 AND version = 5; -- 关键:WHERE 条件包含之前读到的 version
-- 3. 检查 affected_rows (应用层代码判断)
-- 如果返回 1:更新成功。
-- 如果返回 0:更新失败(数据已被修改),触发重试逻辑或报错。
3.MySQL 中如果发生死锁应该如何解决?(中)
MySQL InnoDB 在检测到死锁时会自动回滚代价较小的事务来解除死锁(报错1213),这是最常见的解决方式。开发者也可通过 SHOW ENGINE INNODB STATUS
分析 LATEST DETECTED DEADLOCK
信息定位根本原因(如冲突 SQL、锁资源、加锁顺序),必要时用 KILL
命令人工干预。
拓展:
I.死锁排查步骤
1)查看最新死锁日志
SHOW ENGINE INNODB STATUS; -- 查看日志中的 "LATEST DETECTED DEADLOCK" 部分
关键信息解读:
LATEST DETECTED DEADLOCK
------------------------
2024-07-30 12:00:00 0x7f8e1c0b6700
*** (1) TRANSACTION: # 事务1信息
TRANSACTION 123456, ACTIVE 10 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 1001, OS thread handle 12345, query id 20001 localhost root updating
UPDATE table_a SET col1=1 WHERE id=10 # 正在执行的SQL
*** (1) HOLDS THE LOCK(S): # 事务1当前持有的锁
RECORD LOCKS space id 10 page no 5 n bits 72 index PRIMARY of table `db`.`table_a`
trx id 123456 lock_mode X locks rec but not gap # 持有主键X锁
*** (1) WAITING FOR THIS LOCK: # 事务1等待的锁
RECORD LOCKS space id 10 page no 6 n bits 72 index idx_col of table `db`.`table_b`
trx id 123456 lock_mode X waiting # 等待table_b的索引X锁
*** (2) TRANSACTION: # 事务2信息
TRANSACTION 123457, ACTIVE 8 sec starting index read
UPDATE table_b SET col2=2 WHERE id=20 # 事务2的SQL
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 10 page no 6 n bits 72 index idx_col of table `db`.`table_b`
trx id 123457 lock_mode X # 持有table_b的索引X锁
*** (2) WAITING FOR THIS LOCK:
RECORD LOCKS space id 10 page no 5 n bits 72 index PRIMARY of table `db`.`table_a`
trx id 123457 lock_mode X waiting # 等待table_a的主键X锁
2)分析死锁原因(根据日志)
组件 | 说明 |
---|---|
事务冲突路径 | 事务1 持有 A 锁 → 请求 B 锁 ↓ 事务2 持有 B 锁 → 请求 A 锁 ↑ (循环等待) |
锁类型 | 记录锁(X)、间隙锁(GAP)、Next-Key 锁等 |
涉及表/索引 | 死锁发生的表、索引位置 |
SQL 语句 | 导致死锁的具体 SQL(重点关注 UPDATE/DELETE/SELECT...FOR UPDATE) |
3)监控死锁发生频率
-- 开启全局死锁日志记录(MySQL 5.6+)
SET GLOBAL innodb_print_all_deadlocks = ON; -- 记录到错误日志中
-- 查询死锁统计信息
SELECT * FROM information_schema.INNODB_METRICS
WHERE NAME LIKE 'lock_deadlocks%';
4)解决方案:
- 事务重试(最常用)
# 伪代码示例(应用层重试)
max_retries = 3
for attempt in range(max_retries):
try:
execute_transaction() # 执行事务
break
except DeadlockError:
if attempt == max_retries - 1:
raise
sleep(random.uniform(0.1, 0.3)) # 随机等待避免集体重试
- 手动 kill 事务
# 使用INFORMATION_SCHEMA中的INNODBLOCKS和INNODB_LOCK_WAITS表,查看当前锁和锁等待情况,得到事务ID
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
# 确定线程id后终止线程
KILL <thread_id>;
II.预防死锁
-
约定一致的访问顺序: 在应用程序中,确保不同的事务总是以相同的顺序访问表或行。例如,总是先更新表A再更新表B;或者在更新同一组行时,总是按主键升序处理。这破坏了死锁的“循环等待”条件。
-
合理使用索引: 确保查询(尤其是UPDATE/DELETE)能有效利用索引。没有索引或索引不当会导致锁升级(如行锁升级为表锁)或锁定过多的行(间隙锁范围过大),大大增加死锁概率。
-
降低事务隔离级别: 如果业务允许,将隔离级别从
REPEATABLE READ
降到READ COMMITTED
,可以减少间隙锁(Gap Locks)的使用(尽管不能完全消除),从而降低某些类型死锁的发生概率。但需评估对一致性的影响。 -
避免长事务: 长时间运行的事务会长时间持有锁,增加与其他事务冲突的机会。
-
使用乐观锁: 对于冲突概率不高的场景,考虑使用版本号/时间戳机制实现乐观锁,避免在数据库层面长时间持有悲观锁。
-
重试机制: 在应用层代码中,捕获死锁错误(
1213
)或锁等待超时错误(1205
),并进行有限次数的重试。这是处理偶发死锁的一种常见且有效的补偿策略。
4.MySQL 中 count(*)、count(1) 和 count(字段名) 有什么区别?(易)
COUNT(*)
和 COUNT(1)
在 MySQL 中性能完全等价,都是高效统计查询结果总行数的最佳选择,COUNT(*)
语义更清晰故更推荐。而 COUNT(字段名)
的本质是统计该字段非 NULL
值的数量,语义和结果与前两者截然不同。性能上,COUNT(字段名)
依赖该字段索引(有索引效率较高,无索引需全表扫描判空),且当字段允许 NULL
时结果会小于总行数。因此,COUNT(*)
/COUNT(1)
用于统计总记录数,COUNT(字段名)
用于统计特定字段的有效值个数。
形式 | 统计内容 | 是否统计 NULL 行 | 性能考虑 | 使用场景 |
---|---|---|---|---|
COUNT(*) | 结果集总行数 | 是 | 通常最优 (引擎优化) | 需要知道总共有多少行记录 |
COUNT(1) | 结果集总行数 | 是 | 等同于 COUNT(*) | 同 COUNT(*) (风格偏好) |
COUNT(字段名) | 字段非 NULL 值的个数 | 否 | 依赖索引 (有索引好,无索引差) | 需要统计某字段有效(非空)值的数量 |
5.MySQL 中如何进行 SQL 调优?(中)
MySQL SQL 调优是一个数据驱动、迭代分析的过程。核心步骤是:
1) 启用慢日志定位问题 SQL。
2) 使用 EXPLAIN/EXPLAIN ANALYZE 深入分析执行计划,识别瓶颈(如全表扫描、文件排序)。
3) 针对性优化索引(覆盖索引、复合索引最左前缀、高区分度),这是最高效的手段。
4) 优化 SQL 写法(避免 SELECT *, 优化 JOIN/WHERE/ORDER BY/GROUP BY, 谨慎处理分页)。
5) 必要时考虑数据库设计调整(反范式、分库分表)和配置优化(重点 Buffer Pool)。
拓展:
经典调优场景案例:
问题场景: 分页查询越来越慢
原始SQL:
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10; -- 耗时 2.3s
优化方案:
-- 方案1:游标分页(需记录上次ID)
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 10; -- 0.01s
-- 方案2:延迟关联
SELECT * FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 10) AS tmp
ON o.id = tmp.id; -- 0.15s
6.如何使用 MySQL 的 EXPLAIN 语句进行查询分析?(中)
使用时,重点关注 type
(避免 ALL
,追求 const/ref/range
)、key
(是否使用正确索引)、rows
(预估扫描行数)和 Extra
列(警惕 Using filesort
/Using temporary
,欢迎 Using index
/Using index condition
)。
拓展:
I.使用方法
在你要分析的 SELECT
语句前加上 EXPLAIN
关键字:EXPLAIN SELECT ... FROM ... WHERE ...
II.EXPLAIN 结果的核心字段及其优化意义:
字段 | 描述 | 理想值 | 优化意义 |
---|---|---|---|
id | 查询标识符 | 数字 | 执行顺序(id相同从上到下,id不同从大到小) |
select_type | 查询类型 | SIMPLE, PRIMARY | 查询复杂度级别 |
table | 访问的表名 | - | 显示查询涉及的表 |
partitions | 匹配的分区 | NULL | 分区表使用情况 |
type | 访问类型 | const > ref > range > index > ALL | 核心性能指标 |
possible_keys | 可能使用的索引 | 非空 | 可选的索引列表 |
key | 实际使用的索引 | 非空 | 实际选择的索引 |
key_len | 使用的索引长度 | 越小越好 | 索引使用效率 |
ref | 索引比较值 | const, 列名 | 索引使用方式 |
rows | 预估扫描行数 | <1000 | 关键性能指标 |
filtered | 过滤后剩余百分比 | 接近100% | WHERE 条件效率 |
Extra | 附加信息 | Using index | 优化关键线索 |
III.type 访问类型(性能从优到劣)
- const/system:通过主键或唯一索引直接定位单行(最佳性能)
- ref:使用非唯一索引查找
- range:索引范围扫描
- index:全索引扫描(比ALL好)
- ALL:全表扫描(需优化)
IV.Extra 字段关键值解析
值 | 含义 | 优化建议 |
---|---|---|
Using index | 覆盖索引 | 理想状态,无需回表 |
Using where | 服务器层过滤 | 考虑索引优化 |
Using temporary | 使用临时表 | 优化GROUP BY/ORDER BY |
Using filesort | 额外排序 | 添加索引避免排序 |
Select tables optimized away | 优化器直接返回值 | 无需优化 |
Impossible WHERE | WHERE条件永不成立 | 检查业务逻辑 |
Using join buffer | 使用连接缓冲区 | 增大join_buffer_size |
7.MySQL 中如何解决深度分页的问题?(中)
解决 MySQL 深度分页(大 OFFSET
)性能问题的核心在于避免扫描和丢弃大量无用记录。首选方案是延迟关联:通过子查询利用覆盖索引快速定位目标页的主键 ID(SELECT id ... LIMIT offset, size
),再通过主键精确关联回表获取完整数据行(SELECT * FROM ... JOIN ...
),大幅减少 I/O。对于连续分页(如无限滚动),基于书签(上一页末记录值)查询(WHERE order_col > bookmark LIMIT size
)效率最高,但无法跳页。业务上可限制最大页码或优化交互。在数据量极大时,分库分表是终极方案。
拓展:
I.视频讲解:limit 10000000深分页为什么慢,怎么优化?_哔哩哔哩_bilibili
II.具体解决
-
延迟关联 / 子查询优化 (最常用且推荐):
-
思路: 先利用覆盖索引快速定位到目标分页的主键 ID 集合,再根据这些 ID 回表查询完整数据行。
-
SQL 示例:
SELECT * FROM your_table AS t1 JOIN ( SELECT id FROM your_table WHERE [some_condition] -- 可选,利用索引过滤 ORDER BY [order_column] -- 必须与主查询一致 LIMIT 1000000, 20 -- 大偏移量 ) AS t2 ON t1.id = t2.id ORDER BY t1.[order_column]; -- 通常需要再次排序,但数据量很小
-
为什么有效:
-
子查询
SELECT id ...
通常只需要扫描索引(尤其是覆盖索引),避免了回表操作,扫描offset + size
行的代价显著降低(索引文件比数据文件小得多)。 -
外层查询
SELECT * ... JOIN ...
只需要根据 20 个 ID 进行精确查找(主键查找非常快)。
-
-
关键: 确保
ORDER BY
和WHERE
子句能高效利用索引。通常需要在(order_column, id)
或(filter_column, order_column, id)
上建立索引。
-
-
基于“书签”记录位置 (适用于连续分页):
-
思路: 记录上一页最后一条记录的排序字段值(和唯一标识,如 ID),下一页直接查询排序字段值大于(或小于)该“书签”的记录。
-
SQL 示例 (下一页):
SELECT * FROM your_table WHERE [some_condition] AND [order_column] > [last_value_of_prev_page] -- "书签" -- AND id > [last_id_of_prev_page] -- 如果 order_column 可能重复,加上唯一ID条件 ORDER BY [order_column] ASC LIMIT 20;
-
SQL 示例 (上一页类似):
SELECT * FROM your_table WHERE [some_condition] AND [order_column] < [first_value_of_current_page] -- AND id < [first_id_of_current_page] ORDER BY [order_column] DESC LIMIT 20 ORDER BY [order_column] ASC; -- 可能需要反转结果
-
为什么有效: 完全避免了
offset
,只需基于索引查找指定范围的下size
条记录,效率极高(type=range
)。 -
缺点:
-
无法直接跳转到任意页码(如第 100 页),只能“上一页”、“下一页”导航。
-
需要客户端维护“书签”状态(上一页/下一页的边界值)。
-
如果排序字段值不唯一,需要额外处理(通常结合唯一 ID)。
-
-
-
业务妥协方案:
-
限制最大可访问页码/偏移量: 避免用户请求过深的分页(如只允许查看前 100 页)。
-
近似分页 / 预加载:
-
使用
COUNT(*)
估算总行数(可能不精确)。 -
只加载前 N 页(如 10 页)到缓存/前端,用户翻页时快速响应。超出范围提示或使用其他方式。
-
-
优化数据展示: 提供更好的搜索/筛选功能,减少用户到达深度分页的需求。
-
-
终极方案:分库分表:
-
思路: 当单表数据量极其庞大(亿级以上),深度分页成为系统性瓶颈时,考虑水平分片(Sharding)。将数据分散到多个物理库/表中。
-
影响:
-
分页逻辑变复杂(可能需要在每个分片查询后合并结果)。
-
显著增加架构复杂度和维护成本。
-
-
适用场景: 超大规模数据,其他优化手段已无法满足性能要求。
-
8.什么是 MySQL 的主从同步机制?它是如何实现的?(中)
MySQL 主从同步(Replication)是将主库(Master)的数据变更同步到一个或多个从库(Slave)的过程。
MySQL 主从同步基于异步复制机制实现,核心是利用主库的二进制日志 (Binlog) 记录数据变更。从库通过 I/O 线程 拉取主库 Binlog 并写入本地中继日志 (Relay Log),再由 SQL 线程 重放 Relay Log 中的事件使数据最终一致。其核心价值在于读写分离提升性能、提供数据冗余保障高可用。默认异步复制存在延迟,可通过半同步复制增强数据安全,或组复制实现强一致。
拓展:
I.核心组件
1)二进制日志(Binlog)
-
作用:主库记录所有数据变更的日志(DDL/DML)
-
格式:
-
STATEMENT
:记录SQL语句(可能因函数导致主从不一致) -
ROW
(推荐):记录数据行变化(默认格式,安全) -
MIXED
:混合模式
-
2)主库线程
-
Binlog Dump Thread:
-
每个从库对应一个线程
-
将Binlog推送给从库的I/O线程
-
3)从库线程
线程类型 | 职责 | 关键参数 |
---|---|---|
I/O Thread | 接收主库Binlog → 写入Relay Log | relay_log |
SQL Thread | 读取Relay Log → 重放SQL | relay_log_recovery |
4)中继日志(Relay Log)
-
从库的临时存储:结构与Binlog相同
-
作用:解耦网络传输与SQL重放
II.主从同步全流程
-
主库数据变更
UPDATE users SET balance=100 WHERE id=1; -- 生成Binlog(ROW格式)
Binlog内容示例:
### UPDATE `test`.`users` ### WHERE ### @1=1 /* INT meta=0 nullable=0 */ ### @2='Alice' /* STRING(20) meta=65044 nullable=0 */ ### @3=50 /* INT meta=0 nullable=1 */ ### SET ### @3=100 /* INT meta=0 nullable=1 */
-
从库拉取日志
-
I/O Thread 通过
COM_BINLOG_DUMP
命令请求Binlog -
主库通过
Binlog Dump Thread
推送日志
-
-
日志中继存储
-
I/O Thread 将收到的Binlog写入
relay-log.000002
-
更新
master.info
文件记录读取位置
-
-
从库重放日志
SQL Thread 顺序执行Relay Log中的事件:UPDATE users SET balance=100 WHERE id=1; -- 在从库执行
-
状态同步
-
从库更新
relay-log.info
记录重放位置 -
主库通过
SHOW SLAVE STATUS
监控延迟
-
III.主从同步模式对比
同步模式 | 原理 | 数据安全性 | 性能 | 应用场景 |
---|---|---|---|---|
异步复制 | 主库提交后立即响应客户端 | 可能丢数据 | 最高 | 非金融业务 |
半同步复制 | 至少一个从库确认后才响应 | 较高 | 中等 | 通用生产环境 |
组复制(MGR) | 基于Paxos协议的多主一致性 | 最高 | 较低 | 金融级高可用 |
9.如何处理 MySQL 的主从同步延迟?(中)
主从延迟是不可避免的,只能尽量降低延迟。
1)二次查询:当从库查找不到数据时,回到主库查找,但是这相当于把压力转移到了主库上,当有大量的查询找不到数据时,会对主库造成冲击。
2)将写入后需要马上读的操作转移到主库:这相当于把操作写死了,不推荐。
3)关键事务在主库查询:只有比较关键的数据在主库查询,非关键数据读写分离。
4)将数据存储到缓存:可以先通过缓存来读取数据,但是容易引起不一致的问题。
拓展:
MySQL 主从延迟的常见原因及优化方案
原因分类 | 具体表现 | 优化方案 |
---|---|---|
从库单线程复制 | SQL Thread 单线程重放Binlog导致积压 | - 启用多线程复制(MTS):sql<br> slave_parallel_workers = 8; # 并行线程数<br> slave_parallel_type = LOGICAL_CLOCK;<br> - MySQL 8.0+ 使用 WRITESET 并行复制 |
网络延迟 | 跨机房/跨地域部署时网络抖动 | - 主从同机房部署(延迟≤0.1ms) - 使用专线或VPN隧道 - 启用Binlog压缩: ini<br> slave_compressed_protocol = ON;<br> |
从库性能不足 | 从库硬件配置低于主库,I/O或CPU瓶颈 | - 升级为与主库同规格硬件(SSD/NVMe) - 调整内存参数: ini<br> innodb_buffer_pool_size = 12G; # 物理内存的70%<br> - 隔离从库读流量 |
长事务 | 主库大事务阻塞Binlog传输,从库重放耗时 | - 拆分事务(如每1000行提交一次) - 避免 ALTER TABLE 直接操作大表- 设置事务超时: sql<br> SET SESSION max_execution_time = 30000; # 30秒超时<br> |
从库数量过多 | 主库需为每个从库维护Binlog Dump线程,消耗资源 | - 控制从库数量(建议≤5个) - 使用级联复制架构: <br> 主库 → 中继从库 → 二级从库<br> - 非关键业务从库设置 delay=1h |
从库读负载过高 | 从库处理大量查询,挤占SQL Thread资源 | - 增加从库实例横向扩展 - 使用ProxySQL路由读请求 - 缓存热点数据(Redis) - 识别并优化慢查询: sql<br> SELECT * FROM slow_log WHERE exec_time > 2s;<br> |
10.什么是分库分表?分库分表有哪些类型(或策略)?(易)
分库分表是将一个数据库/表的数据分散存储到多个数据库/表中的技术方案,主要解决单点性能瓶颈(如数据量过大、高并发、磁盘 I/O 限制),提升系统扩展性和查询效率
可以分为垂直拆分和水平拆分两种类型。垂直拆分(按业务维度)是按字段维度拆分,不同业务字段分离到不同表/库,水平拆分(按数据维度)是按数据行拆分,相同表结构分布在多个库/表。
拓展:
I.拆分原则
-
先垂直后水平
-
单表不超过500万行
-
分片键选择高基数字段
II.垂直拆分
1)垂直分表
-- 原始用户表
CREATE TABLE users (
id BIGINT,
name VARCHAR(50),
age INT,
credit_card_no VARCHAR(20), -- 敏感信息
login_history JSON, -- 大字段
PRIMARY KEY(id)
);
-- 拆分后
CREATE TABLE users_basic (
id BIGINT,
name VARCHAR(50),
age INT,
PRIMARY KEY(id)
);
CREATE TABLE users_private (
user_id BIGINT, -- 关联字段
credit_card_no VARCHAR(20),
login_history JSON
);
适用场景:
-
表字段超过50个
-
存在不常用大字段(如TEXT/BLOB)
-
需要冷热数据分离
2)垂直分库
优势:
-
业务解耦(不同微服务对应不同库)
-
降低单库连接数压力
III.水平拆分
1)水平分表
-- 原始订单表
CREATE TABLE orders (
id BIGINT,
user_id INT,
amount DECIMAL(10,2),
create_time DATETIME
);
-- 按月分表
CREATE TABLE orders_202301 (
id BIGINT,
user_id INT,
amount DECIMAL(10,2),
create_time DATETIME
) PARTITION BY RANGE (MONTH(create_time)) (
PARTITION p1 VALUES LESS THAN (2),
PARTITION p2 VALUES LESS THAN (3)
);
分片策略:
策略 | 实现方式 | 优缺点 |
---|---|---|
范围分片 | 按ID范围/时间区间 | 易扩容,但可能热点不均匀 |
哈希取模 | user_id % 1024 | 数据均匀,但扩容需要迁移 |
一致性哈希 | 虚拟节点环 | 扩容影响小,实现复杂 |
目录分片 | 路由表维护映射关系 | 灵活但需维护元数据 |
2)水平分库
典型分片键选择:
-
用户ID(保证同一用户数据在同一库)
-
订单ID(离散分布)
-
地理区域(如华北、华东库)
本文到此结束,如果对你有帮助,可以点个赞~
后续会在合集里持续更新 MySQL 相关的面试题,欢迎关注~
祝各位都能拿到满意的offer~