数据库面试指南
一、MySQL 索引
1.1 B+ 树原理
B+ 树特点:
- 非叶子节点只存储键值,不存储数据
- 叶子节点存储所有键值和数据,通过链表连接
- 所有叶子节点在同一层
[30|60]
/ | \
[10|20] [40|50] [70|80]
| | |
叶子节点通过链表连接为什么用 B+ 树:
| 对比 | B 树 | B+ 树 |
|——|——|——-|
| 数据存储 | 所有节点 | 仅叶子节点 |
| 范围查询 | 需中序遍历 | 叶子链表直接遍历 |
| 磁盘 IO | 较多 | 较少 |
1.2 聚簇索引与非聚簇索引
类型 | 叶子节点存储 | 数量 |
聚簇索引 | 完整行数据 | 1 个 |
非聚簇索引 | 主键值 | 多个 |
回表:二级索引 → 主键 → 聚簇索引查完整数据
1.3 覆盖索引
查询列都在索引中,无需回表:
-- 索引 idx(name, age)
SELECT name, age FROM user WHERE name = 'Tom'; -- 覆盖索引
SELECT * FROM user WHERE name = 'Tom'; -- 需要回表1.4 最左前缀原则
联合索引
(a, b, c) 的使用:查询条件 | 是否走索引 |
a = 1 | ✅ |
a = 1 AND b = 2 | ✅ |
a = 1 AND b = 2 AND c = 3 | ✅ |
b = 2 | ❌ |
a = 1 AND c = 3 | ✅ 部分(只用 a) |
1.5 索引失效场景
-- 1. 对索引列使用函数
WHERE YEAR(create_time) = 2024 -- ❌
WHERE create_time >= '2024-01-01' -- ✅
-- 2. 隐式类型转换
WHERE phone = 13800138000 -- ❌ phone 是 varchar
WHERE phone = '13800138000' -- ✅
-- 3. LIKE 左模糊
WHERE name LIKE '%Tom' -- ❌
WHERE name LIKE 'Tom%' -- ✅
-- 4. OR 条件(部分列无索引)
WHERE a = 1 OR b = 2 -- b 无索引则全表扫描
-- 5. != 或 NOT IN
WHERE status != 1 -- 可能全表扫描1.6 索引下推(ICP)
Index Condition Pushdown:将 WHERE 条件下推到存储引擎层过滤
-- 索引 idx(name, age)
SELECT * FROM user WHERE name LIKE 'Tom%' AND age = 25;无 ICP | 有 ICP |
回表后再过滤 age | 在索引层直接过滤 age |
回表次数多 | 回表次数少 |
1.7 联合索引设计原则
- 高选择性列在前:区分度高的列放前面
- 等值查询列在前:
=条件的列优先于范围条件
- 覆盖常用查询:避免回表
- 避免冗余索引:
(a, b)已包含(a)的功能
-- 查询:WHERE status = 1 AND create_time > '2024-01-01' ORDER BY id
-- 推荐索引:(status, create_time) 或 (status, id)1.8 前缀索引
对长字符串只索引前 N 个字符:
ALTER TABLE user ADD INDEX idx_email(email(10));选择性计算:
SELECT COUNT(DISTINCT LEFT(email, 10)) / COUNT(*) FROM user;
-- 选择性越接近 1 越好缺点:无法用于 ORDER BY 和覆盖索引
二、MySQL 事务
2.1 ACID 特性
特性 | 说明 | 实现 |
原子性 | 全成功或全失败 | undo log |
一致性 | 状态一致 | 其他三者保证 |
隔离性 | 并发互不干扰 | 锁 + MVCC |
持久性 | 提交后永久保存 | redo log |
2.2 隔离级别
级别 | 脏读 | 不可重复读 | 幻读 |
READ UNCOMMITTED | ✅ | ✅ | ✅ |
READ COMMITTED | ❌ | ✅ | ✅ |
REPEATABLE READ | ❌ | ❌ | ⚠️ |
SERIALIZABLE | ❌ | ❌ | ❌ |
MySQL 默认 REPEATABLE READ
2.3 MVCC 原理
核心组件:
1. 隐藏列:
DB_TRX_ID、DB_ROLL_PTR
2. undo log 版本链
3. ReadViewReadView 可见性判断:
trx_id < min_trx_id → 可见(已提交)
trx_id >= max_trx_id → 不可见(未来事务)
trx_id in m_ids → 不可见(活跃事务)RC vs RR:
- RC:每次 SELECT 新建 ReadView
- RR:首次 SELECT 创建,后续复用
2.4 当前读与快照读
-- 快照读(MVCC)
SELECT * FROM user WHERE id = 1;
-- 当前读(加锁)
SELECT * FROM user WHERE id = 1 FOR UPDATE;
INSERT/UPDATE/DELETE三、MySQL 锁
3.1 锁分类
类型 | 粒度 | 并发度 |
表锁 | 整表 | 低 |
行锁 | 单行 | 高 |
模式 | 说明 |
S 锁 | 共享锁,读锁 |
X 锁 | 排他锁,写锁 |
3.2 行锁类型
类型 | 说明 |
Record Lock | 锁定单行 |
Gap Lock | 锁定间隙,防止插入 |
Next-Key Lock | Record + Gap |
索引值: 10, 20, 30
Next-Key Lock 锁定 (10, 20]3.3 死锁
产生条件:互斥、占有等待、不可剥夺、循环等待
解决:
- 固定加锁顺序
- 设置锁超时
innodb_lock_wait_timeout
- 死锁检测 innodb_deadlock_detect3.4 乐观锁与悲观锁
-- 悲观锁
SELECT * FROM stock WHERE id = 1 FOR UPDATE;
UPDATE stock SET count = count - 1 WHERE id = 1;
-- 乐观锁(版本号)
UPDATE stock SET count = count - 1, version = version + 1
WHERE id = 1 AND version = #{version};四、MySQL 日志
4.1 三大日志
日志 | 作用 | 层级 |
redo log | 崩溃恢复(持久性) | InnoDB |
undo log | 事务回滚(原子性)+ MVCC | InnoDB |
binlog | 主从复制、数据恢复 | Server |
4.2 redo log
WAL(Write-Ahead Logging):先写日志,再写磁盘
事务提交流程:
1. 写 redo log(prepare)
2. 写 binlog
3. 写 redo log(commit)两阶段提交:保证 redo log 和 binlog 一致性
4.3 Buffer Pool
内存缓冲池:缓存数据页和索引页,减少磁盘 IO
Buffer Pool 结构:
- Free List:空闲页链表
- LRU List:已使用页链表(冷热分离)
- Flush List:脏页链表LRU 优化:
- 新页先放入 old 区(3/8 位置)
- 访问超过 1s 后才移到 young 区
- 避免全表扫描污染缓存
4.4 InnoDB vs MyISAM
特性 | InnoDB | MyISAM |
事务 | ✅ | ❌ |
行锁 | ✅ | ❌(表锁) |
外键 | ✅ | ❌ |
MVCC | ✅ | ❌ |
崩溃恢复 | redo log | 无 |
全文索引 | 5.6+ 支持 | ✅ |
存储结构 | 聚簇索引 | 堆表 |
选择建议:
- 需要事务、高并发 → InnoDB
- 只读、统计分析 → MyISAM(已不推荐)
4.5 一条 SQL 执行流程
客户端 → 连接器 → 查询缓存(8.0废弃) → 分析器 → 优化器 → 执行器 → 存储引擎五、SQL 优化
5.1 EXPLAIN 关键字段
字段 | 说明 | 关注点 |
type | 访问类型 | system > const > eq_ref > ref > range > index > ALL |
key | 使用的索引 | NULL 表示未使用索引 |
rows | 扫描行数 | 越小越好 |
Extra | 额外信息 | Using index(覆盖索引)、Using filesort(需优化) |
5.2 慢查询优化
- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;- 分析执行计划
EXPLAIN SELECT ...;- 常见优化:
- 添加合适索引
- 避免 SELECT *
- 分页优化
- 避免索引失效
5.3 分页优化
-- 深分页问题
SELECT * FROM user LIMIT 1000000, 10; -- 慢
-- 优化:延迟关联
SELECT * FROM user
WHERE id >= (SELECT id FROM user LIMIT 1000000, 1)
LIMIT 10;
-- 优化:记录上次 ID
SELECT * FROM user WHERE id > #{lastId} LIMIT 10;5.4 ORDER BY 优化
-- 利用索引排序(索引 idx(status, create_time))
SELECT * FROM orders WHERE status = 1 ORDER BY create_time; -- ✅ 使用索引
-- 无法使用索引排序(需要 filesort)
SELECT * FROM orders WHERE status > 0 ORDER BY create_time; -- ❌ 范围查询后无法用索引排序filesort 优化:
- 增大
sort_buffer_size
- 只 SELECT 需要的列
- 添加合适的联合索引5.5 JOIN 优化
-- 小表驱动大表
SELECT * FROM small_table s
JOIN large_table l ON s.id = l.foreign_id;
-- 确保被驱动表的关联字段有索引
ALTER TABLE large_table ADD INDEX idx_foreign_id(foreign_id);JOIN 算法:
| 算法 | 说明 | 适用场景 |
|——|——|———-|
| Index Nested-Loop | 被驱动表用索引 | 有索引 |
| Block Nested-Loop | 批量匹配 | 无索引 |
| Hash Join | 8.0+ 哈希匹配 | 大表等值连接 |
5.6 子查询 vs JOIN
-- 子查询(可能效率低)
SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE status = 1);
-- 改写为 JOIN
SELECT o.* FROM orders o
JOIN users u ON o.user_id = u.id
WHERE u.status = 1;原则:优先使用 JOIN,避免相关子查询
5.7 COUNT 优化
COUNT(*) ≈ COUNT(1) > COUNT(主键) > COUNT(字段)COUNT(*)和COUNT(1)会被优化器优化
COUNT(字段)不统计 NULL
六、分库分表
6.1 拆分方式
方式 | 说明 | 场景 |
垂直拆分 | 按业务拆表/库 | 表太多、字段太多 |
水平拆分 | 按数据拆分 | 数据量太大 |
6.2 分片策略
策略 | 优点 | 缺点 |
范围分片 | 扩容方便 | 热点问题 |
哈希分片 | 数据均匀 | 扩容困难 |
一致性哈希 | 扩容影响小 | 实现复杂 |
6.3 分布式 ID
雪花算法:
0 | 41位时间戳 | 10位机器ID | 12位序列号- 64 位 long 型
- 趋势递增
- 每毫秒可生成 4096 个 ID
6.4 跨库 JOIN 解决方案
- 全局表:将小表同步到所有分库
- 字段冗余:将常用字段冗余到主表
- 应用层组装:分别查询后在应用层合并
- 数据同步到数仓:通过 ETL 同步后查询
6.5 数据迁移方案
双写方案:
1. 新库创建表结构
2. 开启双写(同时写新旧库)
3. 历史数据迁移
4. 数据校验
5. 切换读流量到新库
6. 停止旧库写入binlog 同步方案:
1. 使用 Canal/DTS 同步 binlog
2. 全量同步 + 增量追平
3. 数据校验
4. 切换流量6.6 分布式事务
方案 | 说明 | 一致性 |
2PC | 两阶段提交 | 强一致 |
TCC | Try-Confirm-Cancel | 最终一致 |
Saga | 补偿事务 | 最终一致 |
本地消息表 | 消息 + 定时任务 | 最终一致 |
七、主从复制
7.1 复制原理
主库 → binlog → IO线程 → 从库relay log → SQL线程 → 从库数据7.2 主从延迟
原因:
- 从库单线程重放
- 大事务
- 网络延迟
解决:
- 并行复制
- 读写分离中间件
- 半同步复制
7.3 读写分离
实现方式:
1. 代码层:根据 SQL 类型路由
2. 中间件:MyCat、ShardingSphere、ProxySQL
注意事项:
- 主从延迟导致读不到最新数据
- 解决:强制读主库、延迟判断、同步等待
// 强制读主库注解
@Master
public User getUser(Long id) {
return userMapper.selectById(id);
}7.4 半同步复制
异步复制:主库提交后立即返回,不等从库确认
半同步复制:至少一个从库确认收到 binlog 后才返回
-- 开启半同步复制
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';
SET GLOBAL rpl_semi_sync_master_enabled = 1;优点:减少数据丢失风险
缺点:增加响应延迟
7.5 主从切换
手动切换:
1. 停止主库写入
2. 等待从库同步完成
3. 从库提升为主库
4. 修改应用配置自动切换(MHA/Orchestrator):
1. 检测主库故障
2. 选择最新的从库
3. 补齐差异日志
4. 提升为新主库
5. 其他从库指向新主库
6. 通知应用层八、Redis 基础
8.1 数据类型与底层结构
类型 | 底层结构 | 应用场景 |
String | SDS | 缓存、计数器 |
List | quicklist | 消息队列 |
Hash | ziplist/hashtable | 对象存储 |
Set | intset/hashtable | 去重、交并集 |
ZSet | ziplist/skiplist | 排行榜 |
8.2 List 底层结构
quicklist(Redis 3.2+):双向链表 + ziplist
quicklist:
head -> [ziplist] <-> [ziplist] <-> [ziplist] <- tail优点:
- ziplist 节省内存
- 链表支持快速两端操作
- 兼顾内存和性能
8.3 Hash 底层结构
条件 | 底层结构 |
元素少且值小 | ziplist |
其他情况 | hashtable |
ziplist 阈值:
-
hash-max-ziplist-entries:512
- hash-max-ziplist-value:64 字节8.4 SDS(Simple Dynamic String)
struct sdshdr {
int len; // 已使用长度
int free; // 剩余空间
char buf[]; // 数据
};优点:
- O(1) 获取长度
- 二进制安全
- 预分配减少内存分配
8.5 跳表(Skip List)
Level 3: 1 -----------------> 9
Level 2: 1 ------> 5 -------> 9
Level 1: 1 -> 3 -> 5 -> 7 -> 9- 查找时间复杂度 O(log n)
- 实现简单,易于范围查询
8.6 Redis 为什么快
- 内存操作
- 单线程避免锁竞争(6.0 后 IO 多线程)
- IO 多路复用(epoll)
- 高效数据结构
九、Redis 持久化
9.1 RDB
快照方式:某时刻全量数据
触发方式:
- save:阻塞
- bgsave:fork 子进程
- 配置自动触发优点:恢复快、文件小
缺点:可能丢失最后一次快照后的数据
9.2 AOF
追加日志:记录写命令
写入策略:
- always:每条命令都写,最安全
- everysec:每秒写一次(默认)
- no:由操作系统决定AOF 重写:压缩 AOF 文件
9.3 混合持久化
RDB + AOF:RDB 做全量,AOF 做增量
十、Redis 过期与淘汰
10.1 过期删除策略
策略 | 说明 |
惰性删除 | 访问时检查是否过期 |
定期删除 | 定时随机抽查删除 |
10.2 内存淘汰策略
策略 | 说明 |
noeviction | 不淘汰,内存满报错 |
allkeys-lru | 所有 key 中 LRU |
volatile-lru | 有过期时间的 key 中 LRU |
allkeys-random | 随机淘汰 |
volatile-ttl | 淘汰 TTL 最短的 |
allkeys-lfu | 所有 key 中 LFU |
volatile-lfu | 有过期时间的 key 中 LFU |
10.3 LRU vs LFU 实现
LRU(Least Recently Used):
- Redis 使用近似 LRU
- 随机采样 N 个 key,淘汰最久未访问的
-
maxmemory-samples 控制采样数量LFU(Least Frequently Used):
- Redis 4.0+ 支持
- 基于访问频率淘汰
- 使用对数计数器 + 衰减因子
LFU 计数器结构:
高 16 位:上次访问时间(分钟级)
低 8 位:对数计数器(0-255)选择建议:
- 访问模式稳定 → LRU
- 有热点数据 → LFU
十一、Redis 高可用
11.1 主从复制
全量同步:RDB + 缓冲区命令
增量同步:offset 偏移量11.2 哨兵模式
功能:
- 监控主从状态
- 自动故障转移
- 通知客户端
选举流程:
1. 主观下线 → 客观下线
2. 哨兵选举 Leader
3. Leader 选择新主库
4. 通知从库和客户端
11.3 Cluster 集群
数据分片:16384 个哈希槽
HASH_SLOT = CRC16(key) mod 16384特点:
- 去中心化
- 自动故障转移
- 支持水平扩展
11.4 故障转移流程
Cluster 故障转移:
1. 节点间定期 PING/PONG 心跳检测
2. 超时未响应标记为 PFAIL(疑似下线)
3. 超过半数主节点认为 PFAIL → FAIL(确认下线)
4. 从节点发起选举(Raft 协议)
5. 获得多数主节点投票的从节点晋升为主节点
6. 新主节点广播 PONG,通知集群配置变更
7. 其他从节点指向新主节点选举优先级:
- 复制偏移量最大的从节点优先
- 运行 ID 最小的从节点次之
十二、Redis 缓存问题
12.1 缓存穿透
问题:查询不存在的数据,直接打到数据库
解决:
1. 缓存空值
2. 布隆过滤器
# 布隆过滤器
if not bloom_filter.exists(key):
return None # 一定不存在12.2 缓存击穿
问题:热点 key 过期,大量请求打到数据库
解决:
1. 互斥锁
2. 逻辑过期
3. 热点 key 永不过期
# 互斥锁
def get_data(key):
data = redis.get(key)
if data is None:
if redis.setnx(lock_key, 1, ex=10):
data = db.query(key)
redis.set(key, data, ex=3600)
redis.delete(lock_key)
else:
time.sleep(0.1)
return get_data(key)
return data12.3 缓存雪崩
问题:大量 key 同时过期
解决:
1. 过期时间加随机值
2. 多级缓存
3. 熔断降级
12.4 热点 Key 问题
问题:单个 key 访问量过大,导致单节点压力过大
解决方案:
1. 本地缓存:L1 缓存热点数据
2. Key 分片:
key_1, key_2… 分散到多个节点
3. 读写分离:从节点分担读压力# Key 分片示例
def get_hot_key(key):
shard = random.randint(0, 9)
return redis.get(f"{key}_{shard}")12.5 大 Key 问题
定义:
- String > 10KB
- Hash/List/Set/ZSet 元素 > 5000
危害:
- 内存不均衡
- 阻塞其他请求
- 网络带宽占用
发现:
redis-cli --bigkeys
redis-cli memory usage <key>解决:
1. 拆分:将大 Hash 拆成多个小 Hash
2. 压缩:对 value 进行压缩
3. 异步删除:
UNLINK 代替 DEL12.6 缓存与数据库一致性
常用方案:先更新数据库,再删除缓存
1. 更新数据库
2. 删除缓存
3. 延迟双删(可选)延迟双删:
db.update(data)
redis.delete(key)
time.sleep(0.5) # 等待主从同步
redis.delete(key)十三、Redis 应用
13.1 分布式锁
SETNX 实现:
-- 加锁
SET lock_key unique_value NX EX 30
-- 解锁(Lua 脚本保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
endRedlock:多节点加锁,过半成功
13.2 延迟队列
使用 ZSet,score 为执行时间戳:
# 添加任务
redis.zadd("delay_queue", {task_id: execute_time})
# 消费任务
while True:
tasks = redis.zrangebyscore("delay_queue", 0, now, limit=10)
for task in tasks:
if redis.zrem("delay_queue", task):
process(task)13.3 限流
滑动窗口:
def is_allowed(user_id, limit, window):
key = f"rate:{user_id}"
now = time.time()
# 移除窗口外的记录
redis.zremrangebyscore(key, 0, now - window)
# 检查数量
count = redis.zcard(key)
if count < limit:
redis.zadd(key, {now: now})
redis.expire(key, window)
return True
return False13.4 排行榜
# 增加分数
redis.zincrby("leaderboard", 10, "user:1001")
# 获取排名
redis.zrevrank("leaderboard", "user:1001")
# 获取 Top 10
redis.zrevrange("leaderboard", 0, 9, withscores=True)13.5 布隆过滤器
原理:使用位数组 + 多个哈希函数判断元素是否存在
# Redis 布隆过滤器(需要 RedisBloom 模块)
# 创建
BF.RESERVE user_filter 0.01 1000000 # 错误率 1%,容量 100 万
# 添加
BF.ADD user_filter user:1001
# 查询
BF.EXISTS user_filter user:1001 # 返回 1 表示可能存在,0 表示一定不存在特点:
- 空间效率高
- 有误判率(假阳性)
- 不支持删除
13.6 HyperLogLog
用途:基数统计(去重计数)
# 添加元素
redis.pfadd("uv:page1", "user1", "user2", "user3")
# 获取基数
redis.pfcount("uv:page1") # 返回 3
# 合并多个 HyperLogLog
redis.pfmerge("uv:total", "uv:page1", "uv:page2")特点:
- 固定 12KB 内存
- 标准误差 0.81%
- 适合大数据量去重计数
13.7 GEO 地理位置
# 添加位置
redis.geoadd("locations", 116.405285, 39.904989, "beijing")
redis.geoadd("locations", 121.472644, 31.231706, "shanghai")
# 计算距离
redis.geodist("locations", "beijing", "shanghai", "km") # 返回 1067.5
# 附近的人(半径 100km)
redis.georadius("locations", 116.405285, 39.904989, 100, "km")
# 获取 geohash
redis.geohash("locations", "beijing")应用场景:
- 附近的人/店铺
- 打车距离计算
- 外卖配送范围
十四、高频面试题与答案
Q1: B+ 树为什么适合做索引?
答案:
- 磁盘 IO 优化:
- 非叶子节点只存键值,不存数据,单节点可存更多键
- 树高度低(通常 3-4 层),减少磁盘 IO 次数
- 范围查询高效:
- 叶子节点通过双向链表连接
- 范围查询只需遍历链表,无需回溯
- 查询稳定:
- 所有数据都在叶子节点,查询路径长度一致
- 时间复杂度稳定 O(log n)
Q2: 聚簇索引和非聚簇索引区别?
答案:
特性 | 聚簇索引 | 非聚簇索引(二级索引) |
数据存储 | 叶子节点存储完整数据行 | 叶子节点存储主键值 |
数量 | 每表只能有一个 | 可以有多个 |
查询 | 直接获取数据 | 需要回表查询 |
排序 | 物理排序 | 逻辑排序 |
回表:通过二级索引找到主键,再通过主键到聚簇索引查找完整数据
Q3: 索引失效场景有哪些?
答案:
- 对索引列使用函数:
WHERE YEAR(date) = 2024
- 隐式类型转换:
WHERE phone = 13800138000(phone 是 varchar)
- LIKE 左模糊:
WHERE name LIKE '%Tom'
- OR 条件部分列无索引
- 使用 != 或 NOT IN
- 联合索引不满足最左前缀
- 范围查询后的列失效
Q4: MVCC 原理是什么?
答案:
Multi-Version Concurrency Control(多版本并发控制)
核心组件:
1. 隐藏列:DB_TRX_ID(事务ID)、DB_ROLL_PTR(回滚指针)
2. undo log:存储历史版本,形成版本链
3. ReadView:事务快照,决定可见性
可见性判断:
- 创建 ReadView 时记录活跃事务列表
- 版本的 trx_id < min_trx_id → 可见
- 版本的 trx_id > max_trx_id → 不可见
- 版本的 trx_id 在活跃列表中 → 不可见
Q5: 事务隔离级别及解决的问题?
答案:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
READ UNCOMMITTED | ❌ | ❌ | ❌ |
READ COMMITTED | ✅ | ❌ | ❌ |
REPEATABLE READ | ✅ | ✅ | ❌(InnoDB 通过间隙锁解决) |
SERIALIZABLE | ✅ | ✅ | ✅ |
- 脏读:读到未提交的数据
- 不可重复读:同一事务内两次读取结果不同
- 幻读:同一事务内两次查询行数不同
Q6: MySQL 锁类型有哪些?
答案:
按粒度:
| 锁类型 | 说明 |
|——–|——|
| 表锁 | 锁整张表,开销小,并发低 |
| 行锁 | 锁单行,开销大,并发高 |
| 间隙锁 | 锁索引间隙,防止幻读 |
| 临键锁 | 行锁 + 间隙锁,左开右闭 |
按模式:
- 共享锁(S):读锁,多个事务可同时持有
- 排他锁(X):写锁,独占
- 意向锁:表级锁,表示事务意图
Q7: redo log 和 binlog 区别?
答案:
特性 | redo log | binlog |
层级 | InnoDB 引擎层 | MySQL Server 层 |
内容 | 物理日志(页修改) | 逻辑日志(SQL 语句) |
写入方式 | 循环写,固定大小 | 追加写,无限增长 |
用途 | 崩溃恢复 | 主从复制、数据恢复 |
写入时机 | 事务执行中 | 事务提交时 |
Q8: 慢查询如何优化?
答案:
- 开启慢查询日志:
slow_query_log = ON
- 使用 EXPLAIN 分析:关注 type、key、rows、Extra
- 优化索引:
- 添加缺失索引
- 优化联合索引顺序
- 使用覆盖索引
- SQL 改写:
- 避免 SELECT *
- 分页优化
- 子查询改 JOIN
- 表结构优化:分库分表、归档历史数据
Q9: Redis 持久化方式?
答案:
方式 | 原理 | 优点 | 缺点 |
RDB | 定时快照 | 恢复快、文件小 | 可能丢失数据 |
AOF | 记录写命令 | 数据安全 | 文件大、恢复慢 |
混合 | RDB + 增量 AOF | 兼顾两者优点 | 4.0+ 支持 |
AOF 重写:压缩 AOF 文件,只保留最终状态
Q10: 缓存穿透/击穿/雪崩的区别和解决方案?
答案:
问题 | 描述 | 解决方案 |
穿透 | 查询不存在的数据,绕过缓存 | 布隆过滤器、缓存空值 |
击穿 | 热点 key 过期,大量请求打到 DB | 互斥锁、永不过期 |
雪崩 | 大量 key 同时过期 | 随机过期时间、多级缓存 |
Q11: 分布式锁如何实现?
答案:
Redis 实现:
# 加锁
SET lock_key unique_value NX PX 30000
# 解锁(Lua 脚本保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
end注意事项:
- 设置合理过期时间
- 解锁时验证持有者
- 考虑锁续期(看门狗)
- 集群环境使用 Redlock
Q12: 缓存一致性如何保证?
答案:
推荐方案:先更新数据库,再删除缓存
1. 更新数据库
2. 删除缓存
3. 读请求时重建缓存延迟双删:
1. 删除缓存
2. 更新数据库
3. 延迟一段时间再删除缓存最终一致性:订阅 binlog,异步更新缓存
Q13: 主从复制原理?
答案:
1. 从库执行 CHANGE MASTER TO,配置主库信息
2. 从库启动 IO 线程,连接主库
3. 主库启动 binlog dump 线程,发送 binlog
4. 从库 IO 线程接收,写入 relay log
5. 从库 SQL 线程读取 relay log,重放 SQL复制方式:
- 异步复制:主库不等待从库确认
- 半同步复制:至少一个从库确认
- 组复制:Paxos 协议,强一致性
最后更新:2026-01-09






Loading Comments...