跳转至

数据库面试题

40 道数据库面试高频题 + 详细解答,覆盖 MySQL 索引、事务与锁、 SQL 优化、 Redis 缓存等核心知识点。


一、 MySQL 索引( 10 题)

1. B+树的原理是什么?为什么 MySQL 选择 B+树作为索引结构

B+树的基本特点:

  1. 非叶子节点只存储索引键,不存储数据,可以容纳更多键值,降低树高度
  2. 叶子节点存储所有索引键和数据指针,通过双向链表连接
  3. 所有查询都走到叶子节点,查询性能稳定( O(log n))
  4. 叶子节点的链表结构支持高效范围查询

B+树的结构示意:

Text Only
                    [30 | 60]                     ← 根节点(非叶子)
                   /    |    \
          [10|20]    [40|50]    [70|80]            ← 非叶子节点
          / | \      / | \      / | \
        [10][20] [30][40][50][60][70][80][90]      ← 叶子节点(含数据)
         ↔    ↔    ↔    ↔    ↔    ↔    ↔          ← 双向链表

与其他数据结构的对比:

数据结构 MySQL 选择原因分析
B 树 非叶子也存数据→每个节点存的 key 少→树高→磁盘 IO 多
Hash 等值查询 O(1)但不支持范围查询、排序、最左前缀
红黑树 二叉树→树高度大→磁盘 IO 过多
跳表 有序链表+多级索引,范围查询好,但空间开销大,实际被 Redis 的 ZSet 使用

为什么 MySQL 选 B+树? 1. 磁盘 IO 优化: B+树矮胖(一般 3-4 层),非叶子节点不存数据所以能存更多 Key ,一次磁盘 IO 读取一个节点(一页 16KB )可以包含更多索引信息 2. 范围查询高效:叶子节点形成有序链表,范围查询只需定位起点后顺序遍历 3. 查询稳定:所有查询都走到叶子节点,路径长度一致 4. 排序友好:有序结构天然支持 ORDER BY

InnoDB 与 MyISAM 的 B+树区别: - InnoDB:聚簇索引,叶子节点直接存储行数据(数据即索引) - MyISAM:非聚簇索引,叶子节点存储数据文件的行指针

2. MySQL 有哪些索引类型?分别有什么特点

按数据结构分: - B+树索引:最常用,支持范围查询( InnoDB 默认) - Hash 索引:等值查询 O(1), Memory 引擎支持 - 全文索引( FULLTEXT ):文本搜索( MySQL 5.6+ InnoDB 支持) - R-Tree 索引:空间数据索引

按功能/逻辑分:

索引类型 说明 示例
主键索引 唯一且非空,一表一个, InnoDB 聚簇索引 PRIMARY KEY (id)
唯一索引 值唯一,允许 NULL UNIQUE KEY idx_email (email)
普通索引 最基本的索引,无限制 KEY idx_name (name)
联合索引(复合索引) 多列组合索引 KEY idx_age_name (age, name)
全文索引 全文检索 FULLTEXT KEY idx_content (content)
前缀索引 索引列的前 N 个字符 KEY idx_name (name(10))

特殊概念:

覆盖索引( Covering Index ): - 查询的所有列都在索引中,无需回表 - Extra 显示Using index

SQL
-- 联合索引: (name, age)
SELECT name, age FROM users WHERE name = '张三';
-- name和age都在索引中,不需要回表查其他列

聚簇索引 vs 非聚簇索引:

Text Only
聚簇索引(主键):     非叶子节点[主键值]
                    叶子节点[主键值 + 完整行数据]

非聚簇索引(二级索引): 非叶子节点[索引列值]
                      叶子节点[索引列值 + 主键值]
                       ↓ 回表
                      通过主键值在聚簇索引中查找完整行

3. 什么是最左前缀匹配原则

最左前缀原则: 联合索引按照从左到右的顺序匹配。查询条件必须包含索引最左边的列,才能使用该索引。

SQL
-- 创建联合索引
ALTER TABLE users ADD INDEX idx_a_b_c (a, b, c);

-- 索引结构: 先按a排序,a相同按b排序,b相同按c排序

各查询能否使用索引:

查询条件 是否使用索引 说明
WHERE a = 1 ✅ 使用 a 最左列
WHERE a = 1 AND b = 2 ✅ 使用 a,b 满足最左前缀
WHERE a = 1 AND b = 2 AND c = 3 ✅ 使用 a,b,c 完整匹配
WHERE b = 2 ❌ 不使用 缺少最左列 a
WHERE b = 2 AND c = 3 ❌ 不使用 缺少最左列 a
WHERE a = 1 AND c = 3 ✅ 使用 a b 缺失, c 无法使用
WHERE a = 1 AND b > 2 AND c = 3 ✅ 使用 a,b 范围查询后的列©不能使用索引
WHERE a = 1 ORDER BY b ✅ a 查找+b 排序 避免 filesort
WHERE a > 1 AND b = 2 ✅ 使用 a a 是范围查询, b 无法使用

核心规则总结: 1. 必须从最左列开始匹配 2. 遇到范围查询(>、<、 BETWEEN 、 LIKE )后,右侧列无法使用索引 3. MySQL 优化器会自动调整 WHERE 条件顺序(所以WHERE b=2 AND a=1也能用索引)

联合索引设计原则: - 等值查询多的列放左边 - 区分度高( Cardinality )的列放左边 - 排序和分组用到的列考虑放入联合索引

4. 索引失效的场景有哪些

常见的 8 种索引失效场景:

1. 对索引列使用函数或计算

SQL
-- ❌ 索引失效
SELECT * FROM users WHERE YEAR(created_at) = 2024;
SELECT * FROM users WHERE id + 1 = 10;

-- ✅ 改写
SELECT * FROM users WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
SELECT * FROM users WHERE id = 9;

2. 隐式类型转换

SQL
-- phone是varchar类型
-- ❌ 索引失效(字符串转数字)
SELECT * FROM users WHERE phone = 13800138000;

-- ✅ 正确
SELECT * FROM users WHERE phone = '13800138000';

3. LIKE 以通配符开头

SQL
-- ❌ 索引失效
SELECT * FROM users WHERE name LIKE '%张';
SELECT * FROM users WHERE name LIKE '%张%';

-- ✅ 可以使用索引
SELECT * FROM users WHERE name LIKE '张%';

4. OR 条件中有非索引列

SQL
-- name有索引,age无索引
-- ❌ 索引失效(整个查询全表扫描)
SELECT * FROM users WHERE name = '张三' OR age = 25;

-- ✅ 改用UNION
SELECT * FROM users WHERE name = '张三'
UNION
SELECT * FROM users WHERE age = 25;

5. NOT IN / NOT EXISTS / !=

SQL
-- ❌ 可能导致索引失效(优化器判断全表扫描更快时)
SELECT * FROM users WHERE id NOT IN (1, 2, 3);
SELECT * FROM users WHERE name != '张三';
SELECT * FROM users WHERE name IS NOT NULL;

6. 联合索引不满足最左前缀

SQL
-- 索引: (a, b, c)
-- ❌ 不满足最左前缀
SELECT * FROM users WHERE b = 2 AND c = 3;

7. 数据量小或区分度低时优化器选择全表扫描

SQL
-- gender只有男/女两个值,区分度太低
-- 优化器可能认为全表扫描更快
SELECT * FROM users WHERE gender = '男';

8. 使用 SELECT * 导致无法覆盖索引

SQL
-- 联合索引: (name, age)
-- ❌ 需要回表
SELECT * FROM users WHERE name = '张三';

-- ✅ 覆盖索引,不需要回表
SELECT name, age FROM users WHERE name = '张三';

5. 如何使用 EXPLAIN 分析 SQL 执行计划?各字段含义是什么

SQL
EXPLAIN SELECT * FROM users WHERE name = '张三';

EXPLAIN 核心字段详解:

字段 含义 重点关注
id 查询序号 id 大的先执行,相同自上而下
select_type 查询类型 SIMPLE/PRIMARY/SUBQUERY/DERIVED
table 访问的表
partitions 匹配的分区
type 访问类型 ⭐ 最重要的优化指标
possible_keys 可能使用的索引
key 实际使用的索引 为 NULL 表示没用索引
key_len 使用的索引长度 联合索引中实际用了多少列
ref 索引引用列
rows 预估扫描行数 ⭐ 越小越好
filtered 过滤比例
Extra 额外信息 ⭐ 重要优化提示

type 字段(性能从好到差):

SQL
system > const > eq_ref > ref > range > index > ALL

system: 表只有一行(系统表)
const:  通过主键或唯一索引查一行(WHERE id = 1
eq_ref: 联表查询,每次匹配一行(主键/唯一索引join
ref:    非唯一索引的等值查询(WHERE name = '张三'
range:  索引范围查询(WHERE id > 10, BETWEEN, IN
index:  全索引扫描(遍历整个索引树)
ALL:    全表扫描 ⚠️ 需要优化

Extra 字段常见值:

SQL
Using index        : 覆盖索引  
Using where        : 使用了WHERE过滤
Using index condition: 索引下推(ICP)  
Using temporary    : 使用了临时表 ⚠️ 需要优化
Using filesort     : 使用了文件排序 ⚠️ 需要优化
Using join buffer  : 使用了连接缓冲 ⚠️ 考虑加索引

优化目标: - type 至少达到 ref 或 range 级别,避免 ALL 和 index - Extra 中避免出现 Using temporary 和 Using filesort - rows 尽可能小

6. 什么是索引下推( ICP )

索引下推( Index Condition Pushdown, ICP ): MySQL 5.6 引入的优化,将 WHERE 条件中与索引相关的条件过滤推到存储引擎层执行,减少回表次数。

SQL
-- 联合索引: idx_name_age (name, age)
SELECT * FROM users WHERE name LIKE '张%' AND age = 25;

无 ICP ( MySQL 5.6 之前):

Text Only
1. 存储引擎使用索引找到 name LIKE '张%' 的所有记录的主键
2. 全部回表读取完整行
3. Server层再过滤 age = 25

有 ICP ( MySQL 5.6+):

Text Only
1. 存储引擎使用索引找到 name LIKE '张%' 的记录
2. 在存储引擎层直接用索引中的age字段判断 age = 25
3. 只有满足条件的才回表
4. 大幅减少回表次数

EXPLAIN 中 Extra 显示 Using index condition 表示使用了 ICP 。

7. 什么是回表查询?如何避免

回表查询: 使用非聚簇索引(二级索引)查询时,先在二级索引 B+树中找到主键,再通过主键到聚簇索引 B+树中査找完整行数据。

SQL
-- 假设name列有普通索引idx_name
SELECT * FROM users WHERE name = '张三';

查询过程:
1. idx_name索引树中找到name='张三'的叶子节点  获得主键id=123
2. id=123到聚簇索引(主键)树中查找完整行数据     这一步就是回表

避免回表的方法 — 覆盖索引:

SQL
-- 联合索引: idx_name_age (name, age)

-- ❌ 需要回表 (SELECT * 需要索引中没有的列)
SELECT * FROM users WHERE name = '张三';

-- ✅ 覆盖索引,无需回表 (所有查询列都在索引中)
SELECT id, name, age FROM users WHERE name = '张三';
-- id是主键,name和age在联合索引中,Extra: Using index

设计原则: - 频繁查询的列考虑建联合索引实现覆盖索引 - 避免使用SELECT *,只查需要的列 - 联合索引中将频繁查询的列加入

8. 聚簇索引和非聚簇索引有什么区别

维度 聚簇索引 非聚簇索引(二级索引)
存储内容 叶子节点存完整行数据 叶子节点存主键值
数量 每表只有一个 可以有多个
物理排序 数据按索引排序存储 不影响数据物理存储
查询速度 直接获取数据 可能需要回表
默认 InnoDB 主键索引 InnoDB 非主键索引

InnoDB 主键选择策略: 1. 有显式 PRIMARY KEY → 使用它作为聚簇索引 2. 没有主键,选第一个 NOT NULL 的 UNIQUE 索引 3. 都没有 → InnoDB 生成一个隐式的 6 字节 ROWID

主键设计建议: - 使用自增 ID 而非 UUID :自增是顺序插入,避免页分裂 - UUID 作为主键会导致随机插入,产生大量页分裂,性能差 - 主键尽量小:二级索引的叶子节点都存储了主键值

9. 如何设计一个好的索引?索引优化有哪些原则

索引设计原则:

1. 选择合适的列建索引 - WHERE 、 JOIN 、 ORDER BY 、 GROUP BY 中频繁使用的列 - 区分度(Cardinality)高的列:SELECT COUNT(DISTINCT col)/COUNT(*) FROM table - 区分度>0.3 适合建索引

2. 联合索引设计 - 等值查询列放左边,范围查询列放右边 - 区分度高的列放左边 - 尽可能覆盖常用查询的列(覆盖索引)

3. 避免过度索引 - 索引会降低 INSERT/UPDATE/DELETE 速度 - 每个索引占用额外存储空间 - 建议单表索引不超过 5-6 个

4. 前缀索引

SQL
-- 对长字符串列使用前缀索引
ALTER TABLE users ADD INDEX idx_email (email(6));  -- INDEX索引加速查询
-- 缺点:无法用于ORDER BY和覆盖索引

5. 避免索引失效 - 不在索引列上做函数运算 - 避免隐式类型转换 - LIKE 不以%开头

10. 什么是索引合并( Index Merge )

索引合并: MySQL 在某些情况下可以同时使用多个索引来处理一个查询,然后合并结果。

三种类型:

1. Index Merge Intersection (交集)

SQL
-- name和age各有独立索引
SELECT * FROM users WHERE name = '张三' AND age = 25;
-- 分别使用两个索引查找,取交集

2. Index Merge Union (并集)

SQL
-- name和age各有独立索引
SELECT * FROM users WHERE name = '张三' OR age = 25;
-- 分别使用两个索引查找,取并集

3. Index Merge Sort-Union (排序后并集)

SQL
SELECT * FROM users WHERE name < '张' OR age > 25;

注意: 索引合并并不总是最优的,如果经常出现索引合并,考虑建一个联合索引替代。


二、事务与锁( 10 题)

11. 数据库事务的 ACID 特性分别是什么

特性 英文 含义 实现机制
原子性 Atomicity 事务中的操作要么全部成功,要么全部失败回滚 undo log
一致性 Consistency 事务前后数据库从一个一致状态转到另一个一致状态 其他三个特性共同保证
隔离性 Isolation 并发事务之间互不干扰 锁 + MVCC
持久性 Durability 事务提交后结果永久存储 redo log

详细说明:

原子性( undo log 实现): - InnoDB 通过 undo log (回滚日志)记录事务修改前的数据 - 如果事务需要回滚,使用 undo log 恢复数据到修改前的状态

持久性( redo log 实现): - 事务提交时,先写 redo log 到磁盘( WAL, Write-Ahead Logging ) - 即使数据页还没有刷到磁盘,通过 redo log 也可以恢复数据 - 保证了即使数据库崩溃,已提交的事务不会丢失

隔离性(锁 + MVCC 实现): - 写-写冲突:通过锁机制控制 - 写-读冲突:通过 MVCC (多版本并发控制)实现

12. MySQL 的四种隔离级别分别是什么?各级别会出现什么问题

隔离级别 脏读 不可重复读 幻读
读未提交(Read Uncommitted) ✅可能 ✅可能 ✅可能
读已提交(Read Committed) ❌解决 ✅可能 ✅可能
可重复读(Repeatable Read) ❌解决 ❌解决 ✅可能*
串行化(Serializable) ❌解决 ❌解决 ❌解决

*InnoDB 在 RR 级别通过 MVCC+间隙锁基本解决了幻读

三种并发问题:

脏读: 读到其他事务未提交的数据

SQL
事务A: UPDATE balance SET amount=0 WHERE id=1;  (未提交)
事务B: SELECT amount FROM balance WHERE id=1;    读到0 (脏数据)
事务A: ROLLBACK;  (回滚了)
事务B读到了一个不存在的值

不可重复读: 同一事务中两次读取同一数据结果不一致

SQL
事务A: SELECT amount FROM balance WHERE id=1;    100
事务B: UPDATE ... SET amount=200 WHERE id=1; COMMIT;
事务A: SELECT amount FROM balance WHERE id=1;    200 (同一行数据变了)

幻读: 同一事务中两次查询结果的行数不一致

SQL
事务A: SELECT * FROM users WHERE age > 20;        3
事务B: INSERT INTO users (age) VALUES (25); COMMIT;
事务A: SELECT * FROM users WHERE age > 20;        4 (多了一行)

MySQL 默认隔离级别: Repeatable Read (可重复读)

13. MVCC 的原理是什么

MVCC ( Multi-Version Concurrency Control ,多版本并发控制): 通过维护数据的多个版本,使得读写操作不冲突,提高并发性能。

核心组件:

1. 隐藏列 每行数据有两个隐藏列: - DB_TRX_ID:最后修改该行的事务 ID - DB_ROLL_PTR:回滚指针,指向 undo log 中的旧版本

2. Undo Log 版本链

Text Only
当前版本: [name='王五', trx_id=300, roll_ptr] →
旧版本:   [name='李四', trx_id=200, roll_ptr] →
更旧版本: [name='张三', trx_id=100, roll_ptr] → NULL

3. ReadView (快照读) 事务执行第一个 SELECT 时创建 ReadView ,包含: - m_ids:创建 ReadView 时活跃(未提交)的事务 ID 列表 - min_trx_id:活跃事务中最小的 ID - max_trx_id:下一个要分配的事务 ID (当前最大 ID+1 ) - creator_trx_id:创建该 ReadView 的事务 ID

可见性判断规则:

Text Only
对于版本链中的每个版本(trx_id):

1. trx_id == creator_trx_id → 可见(自己修改的)
2. trx_id < min_trx_id → 可见(事务在ReadView创建前已提交)
3. trx_id >= max_trx_id → 不可见(事务在ReadView创建后才开始)
4. min_trx_id <= trx_id < max_trx_id:
   - trx_id 在 m_ids 中 → 不可见(事务还未提交)
   - trx_id 不在 m_ids 中 → 可见(事务已提交)

如果当前版本不可见,沿roll_ptr找上一个版本继续判断

RC 和 RR 的 MVCC 区别: - RC (读已提交):每次 SELECT 都创建新的 ReadView - RR (可重复读):只在事务的第一个 SELECT 时创建 ReadView ,之后复用

14. MySQL 有哪些锁类型

按粒度分:

锁类型 粒度 特点
表锁 锁整张表 开销小,加锁快,并发低
行锁 锁单行 开销大,加锁慢,并发高
页锁 锁数据页 介于表锁和行锁之间( BDB 引擎)

InnoDB 行级锁的类型:

1. 记录锁( Record Lock ): - 锁住索引中的一条记录

SQL
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 锁住id=1这一行

2. 间隙锁( Gap Lock ): - 锁住索引记录之间的间隙,防止插入 - 只在 RR 隔离级别下存在

SQL
-- 假设表中id有: 1, 5, 10
SELECT * FROM users WHERE id BETWEEN 5 AND 10 FOR UPDATE;
-- 间隙锁锁住 (5,10) 区间,防止在该区间插入新记录

3. 临键锁( Next-Key Lock ): - 记录锁 + 间隙锁 = 左开右闭区间 - InnoDB 默认的行锁类型

SQL
-- id有: 1, 5, 10
SELECT * FROM users WHERE id = 5 FOR UPDATE;
-- Next-Key Lock: (1, 5]

4. 意向锁( Intention Lock ): - 表级锁,用于表明事务将要对表中的行加什么锁 - 意向共享锁( IS ):表明将加行共享锁 - 意向排他锁( IX ):表明将加行排他锁 - 作用:快速判断表级锁与行级锁是否冲突

按模式分:

锁模式 说明 兼容性
共享锁(S 锁) 读锁,多个事务可同时持有 S 与 S 兼容
排他锁(X 锁) 写锁,独占 X 与任何锁不兼容
SQL
-- 共享锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;  -- MySQL 5.x
SELECT * FROM users WHERE id = 1 FOR SHARE;           -- MySQL 8.0+

-- 排他锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- INSERT/UPDATE/DELETE 自动加排他锁

15. 什么是死锁?如何检测和避免

死锁: 两个或多个事务互相等待对方持有的锁,形成循环等待。

SQL
事务A:
  1. UPDATE users SET name='X' WHERE id=1;  -- 锁住id=1
  2. UPDATE users SET name='Y' WHERE id=2;  -- 等待id=2的锁 (被事务B持有)

事务B:
  1. UPDATE users SET name='X' WHERE id=2;  -- 锁住id=2
  2. UPDATE users SET name='Y' WHERE id=1;  -- 等待id=1的锁 (被事务A持有)

结果: A等BB等A  死锁

MySQL 死锁检测: - InnoDB 内置等待图( wait-for graph )检测死锁 - 检测到死锁后,自动回滚代价最小的事务(持有锁最少的) - SHOW ENGINE INNODB STATUS; 查看最近的死锁信息

避免死锁的策略:

  1. 固定加锁顺序:所有事务按相同顺序访问资源
SQL
-- 总是按id升序加锁
UPDATE users SET ... WHERE id = 1;
UPDATE users SET ... WHERE id = 2;
  1. 减小事务粒度:事务尽量短小,减少持锁时间

  2. 尽量使用索引:避免行锁升级为表锁

  3. 设置超时

SQL
SET innodb_lock_wait_timeout = 5;  -- 等锁超时时间(秒)
  1. 乐观锁替代悲观锁:适合读多写少的场景
SQL
-- 乐观锁: 使用版本号
UPDATE users SET name='X', version=version+1 WHERE id=1 AND version=5;

16. 乐观锁和悲观锁的区别是什么

维度 乐观锁 悲观锁
思想 假设不会冲突 假设会冲突
实现 版本号/CAS 数据库锁(FOR UPDATE)
加锁时机 更新时检查 操作前加锁
适用场景 读多写少,冲突少 写多,冲突频繁
性能 冲突少时性能好 锁开销大,但稳定

乐观锁实现:

SQL
-- 方式1: 版本号
-- 1. 查询时获取版本号
SELECT id, name, version FROM users WHERE id = 1;  -- version=5

-- 2. 更新时检查版本号
UPDATE users SET name='新名字', version=6 WHERE id = 1 AND version = 5;
-- 影响0行说明被其他事务修改了,需要重试

-- 方式2: CAS (Compare And Swap)
UPDATE stock SET count = count - 1 WHERE id = 1 AND count > 0;

悲观锁实现:

SQL
BEGIN;
SELECT * FROM users WHERE id = 1 FOR UPDATE;  -- 加排他锁
-- 其他事务无法修改id=1的记录
UPDATE users SET name = '新名字' WHERE id = 1;
COMMIT;  -- 释放锁

17. InnoDB 的行锁是如何实现的

关键点: InnoDB 的行锁是基于索引实现的!

  • 如果查询条件使用了主键索引 → 锁住主键索引对应的行
  • 如果使用了二级索引 → 先锁二级索引,再锁主键索引
  • 如果没有使用索引(全表扫描)→ 锁住所有行(相当于表锁)
SQL
-- 有索引: 只锁一行
SELECT * FROM users WHERE id = 1 FOR UPDATE;

-- 无索引: 锁全表!
SELECT * FROM users WHERE name = '张三' FOR UPDATE;
-- 如果name没有索引,会扫描全表,锁住所有行

这也是为什么强调要在 WHERE 条件列上建索引的重要原因之一。

18. 什么是当前读和快照读

快照读( Consistent Nonlocking Read ): - 读取的是 MVCC 版本链中某个快照版本 - 不加锁,并发性能好

SQL
-- 普通的SELECT就是快照读
SELECT * FROM users WHERE id = 1;

当前读( Current Read ): - 读取的是数据的最新版本 - 加锁(共享锁或排他锁)

SQL
-- 以下都是当前读
SELECT * FROM users WHERE id = 1 FOR UPDATE;        -- 排他锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE; -- 共享锁
INSERT INTO users VALUES (...);    -- 排他锁
UPDATE users SET ... WHERE id = 1; -- 排他锁
DELETE FROM users WHERE id = 1;    -- 排他锁

19. MySQL 的两阶段锁是什么

两阶段锁协议( 2PL, Two-Phase Locking ):

在 InnoDB 事务中,行锁是在需要时才加,但并不是不需要时立即释放,而是要等到事务提交时统一释放。

SQL
事务A:
BEGIN;
UPDATE t1 SET ... WHERE id=1;  -- 加锁
UPDATE t2 SET ... WHERE id=2;  -- 加锁
       加锁阶段(Growing Phase) 
COMMIT;                        -- 释放所有锁
       解锁阶段(Shrinking Phase) 

实践意义: 如果事务中需要锁多行,把最可能造成锁冲突、影响并发的锁操作放在事务最后,减少持锁时间。

SQL
-- 好的做法: 并发高的操作放最后
BEGIN;  -- 事务保证操作原子性
INSERT INTO order_log ...;     -- 并发低的操作先做
UPDATE user_balance ...;       -- 并发高的操作放后面,减少持锁时间
COMMIT;

20. binlog 、 redo log 和 undo log 的区别是什么

日志 作用 层次 写入时机
binlog 主从复制 / 数据恢复 MySQL Server 层 事务提交时
redo log 崩溃恢复(保证持久性) InnoDB 引擎层 事务执行中(WAL)
undo log 事务回滚 / MVCC InnoDB 引擎层 事务执行前

redo log : - 物理日志,记录"在某个数据页上做了什么修改" - 循环写入(固定大小的文件组) - WAL ( Write-Ahead Logging ):数据修改先写 redo log 再改内存 - 崩溃恢复时 redo log 重放已提交事务

undo log : - 逻辑日志,记录修改前的数据(回滚用) - 同时用于 MVCC 的版本链 - INSERT → undo log 记录 DELETE - UPDATE → undo log 记录旧值

binlog : - 逻辑日志,记录 SQL 语句( Statement 格式)或行变更( Row 格式) - 追加写入(不覆盖) - 用于主从复制和基于时间点的数据恢复 - 三种格式: Statement ( SQL 语句)、 Row (行变更)、 Mixed (混合)


三、 SQL 与优化( 8 题)

21. 如何分析慢查询

步骤 1 :开启慢查询日志

SQL
-- 查看是否开启
SHOW VARIABLES LIKE 'slow_query_log%';

-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL long_query_time = 1;   -- 超过1秒记录

步骤 2 :使用 mysqldumpslow 分析

Bash
# 按查询时间排序,显示前10条
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

# 按锁时间排序
mysqldumpslow -s l -t 10 /var/log/mysql/slow.log

步骤 3 : EXPLAIN 分析执行计划

SQL
EXPLAIN SELECT * FROM orders WHERE user_id = 123 ORDER BY created_at;  -- EXPLAIN查看查询执行计划
-- 检查type、key、rows、Extra

步骤 4 : SHOW PROFILE 分析详细耗时

SQL
SET profiling = 1;
SELECT ...;  -- 执行查询
SHOW PROFILES;  -- 查看所有查询耗时
SHOW PROFILE FOR QUERY 1;  -- 查看某次查询的详细阶段耗时

步骤 5 :优化 SQL

22. SQL 优化有哪些常用技巧

15 条 SQL 优化技巧:

**1. 避免 SELECT ***

SQL
-- ❌
SELECT * FROM users WHERE id = 1;
-- ✅ 只查需要的列,可能命中覆盖索引
SELECT id, name, age FROM users WHERE id = 1;

2. 用 EXISTS 代替 IN (大表)

SQL
-- ❌ IN子查询
SELECT * FROM orders WHERE user_id IN (SELECT id FROM users WHERE status = 1);  -- 子查询:嵌套在另一个查询中
-- ✅ EXISTS
SELECT * FROM orders o WHERE EXISTS (SELECT 1 FROM users u WHERE u.id = o.user_id AND u.status = 1);

3. 避免在 WHERE 中对列做运算

SQL
-- ❌
SELECT * FROM users WHERE YEAR(created_at) = 2024;
-- ✅
SELECT * FROM users WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';

4. 小表驱动大表( JOIN 优化)

SQL
-- 小表做驱动表(LEFT JOIN左边放小表)
SELECT * FROM small_table s LEFT JOIN big_table b ON s.id = b.s_id;

5. LIMIT 优化深分页

SQL
-- ❌ 深分页很慢(OFFSET越大越慢)
SELECT * FROM orders ORDER BY id LIMIT 1000000, 10;

-- ✅ 使用游标分页
SELECT * FROM orders WHERE id > 1000000 ORDER BY id LIMIT 10;

-- ✅ 延迟关联
SELECT * FROM orders o INNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 10) t ON o.id = t.id;  -- JOIN连接多个表

6. 合理使用联合索引

7. 避免隐式类型转换

8. 批量操作

SQL
-- ❌ 逐行插入
INSERT INTO users VALUES (1, 'a');
INSERT INTO users VALUES (2, 'b');
-- ✅ 批量插入
INSERT INTO users VALUES (1, 'a'), (2, 'b'), (3, 'c');

9. 使用 UNION ALL 替代 UNION

SQL
-- UNION会去重(有排序开销),UNION ALL不去重
SELECT id FROM t1 UNION ALL SELECT id FROM t2;

10. GROUP BY 优化

SQL
-- 确保GROUP BY的列有索引
-- 使用WITH ROLLUP获取汇总行

11. 避免大事务

12. 使用连接(JOIN)代替子查询

13. 合理使用索引提示

SQL
SELECT * FROM users FORCE INDEX (idx_name) WHERE name = '张三';

14. 拆分复杂 SQL 为多个简单 SQL

15. 使用临时表缓存中间结果

23. 分库分表的方案和策略有哪些

什么时候需要分库分表? - 单表数据量超过 2000 万行(经验值) - 单库连接数成为瓶颈 - 读写压力过大

垂直拆分:

垂直分表: 将表中的列拆分到多个表

Text Only
users表(字段太多)
users_base(id, name, age, phone)      ← 常用字段
users_detail(id, address, bio, avatar) ← 不常用字段

垂直分库: 按业务将不同表拆分到不同数据库

Text Only
电商数据库
用户库(users, addresses)
订单库(orders, order_items)
商品库(products, categories)

水平拆分:

水平分表: 将同一表的数据按行拆分到多个表

Text Only
orders表(数据量太大)
orders_0 (id % 4 = 0)
orders_1 (id % 4 = 1)
orders_2 (id % 4 = 2)
orders_3 (id % 4 = 3)

水平分库: 将数据分散到多个数据库实例

分片策略:

策略 方式 优点 缺点
Hash 取模 id % N 数据均匀 扩容需要迁移数据
范围分片 id 1-1000W 在库 1 扩容方便 可能热点不均
时间维度 按月/年分表 适合时序数据 查询跨表复杂
一致性 Hash 虚拟节点 扩容迁移数据少 实现复杂

全局唯一 ID 方案: - UUID:简单,但无序,影响索引性能 - 数据库自增:简单,但有性能瓶颈 - Snowflake 雪花算法:时间戳+机器 ID+序列号,趋势递增 - Redis 自增:利用 Redis 的 INCR 命令 - 号段模式:每次从数据库获取一段 ID 缓存在本地(如 Leaf 、 Tinyid )

24. MySQL 主从复制的原理是什么

主从复制流程:

Text Only
主库(Master)                          从库(Slave)
   |                                     |
   | 1. 数据变更写入binlog                |
   |------ binlog ---------------------->| 2. IO线程读取binlog
   |                                     |    写入relay log(中继日志)
   |                                     |
   |                                     | 3. SQL线程读取relay log
   |                                     |    重放SQL语句
   |                                     |    更新从库数据

三个线程: - 主库:Binlog Dump Thread — 发送 binlog 给从库 - 从库:IO Thread — 接收 binlog ,写入 relay log - 从库:SQL Thread — 读取 relay log ,执行 SQL

binlog 三种格式:

格式 记录内容 优点 缺点
Statement SQL 语句 日志量小 函数/变量可能导致不一致
Row 行数据变更 数据一致性好 日志量大
Mixed 混合 折中 复杂

主从延迟问题: - 原因:从库单线程重放,主库并行写入 - 解决: - MySQL 5.7+ 多线程复制( MTS ) - 半同步复制(主库等待至少一个从库确认才返回) - 强制读主库(关键业务)

25. 什么是读写分离?有什么注意事项

读写分离: 写操作发往主库,读操作发往从库,减轻主库压力。

Text Only
应用程序
  ├── 写操作 → 主库(Master)
  └── 读操作 → 从库(Slave1, Slave2, ...)

实现方式: 1. 代码层:根据 SQL 类型路由到主/从库 2. 中间件: MyCat 、 ShardingSphere 、 ProxySQL 3. 云服务: RDS 读写分离代理

注意事项: - 主从延迟导致数据不一致:写入后立即读取可能读到旧数据 - 解决:关键业务强制读主库(写完立即查的场景) - 解决:延迟监控 + 半同步复制 - 从库可能被大查询阻塞 - 需要监控主从延迟SHOW SLAVE STATUS\G 中的 Seconds_Behind_Master


四、 Redis ( 12 题)

26. Redis 有哪些数据类型?各自的应用场景是什么

数据类型 底层结构 应用场景
String SDS(Simple Dynamic String) 缓存、计数器、分布式锁、 Session
Hash listpack/hashtable(旧版本也可能看到 ziplist ) 对象存储(用户信息)
List quicklist 消息队列、最新列表
Set intset/hashtable 标签、共同好友、去重
ZSet(Sorted Set) listpack/skiplist+hashtable(旧版本也可能看到 ziplist ) 排行榜、延迟队列

扩展数据类型: - Bitmap:签到、活跃用户统计 - HyperLogLog: UV 统计(基数估计) - GEO:附近的人、地理位置 - Stream:消息队列( Redis 5.0+)

常用命令示例:

Text Only
# String
SET key value EX 3600        # 设置,过期时间3600秒
GET key                      # 获取
INCR counter                 # 自增
SETNX lock_key value         # 不存在才设置(分布式锁)

# Hash
HSET user:1 name "张三" age 25
HGET user:1 name
HGETALL user:1

# List
LPUSH queue task1 task2       # 左边入队
RPOP queue                    # 右边出队
LRANGE list 0 -1              # 获取所有

# Set
SADD tags:article1 "Java" "Redis"
SMEMBERS tags:article1
SINTER tags:article1 tags:article2  # 交集

# ZSet
ZADD ranking 100 "user1" 200 "user2"
ZREVRANGE ranking 0 9 WITHSCORES    # Top10
ZRANK ranking "user1"               # 排名

27. Redis 的持久化方式有哪些? RDB 和 AOF 的区别

RDB ( Redis DataBase )快照: - 将某个时间点的内存数据以二进制形式保存到磁盘 - 生成 dump.rdb 文件

Text Only
# 配置触发条件
save 900 1      # 900秒内有1次修改就触发
save 300 10     # 300秒内有10次修改就触发
save 60 10000   # 60秒内有10000次修改就触发

# 手动触发
SAVE            # 阻塞式保存(不推荐)
BGSAVE          # 后台异步保存(fork子进程)

AOF ( Append Only File ): - 记录每一个写命令,追加到 AOF 文件 - 重启时重放 AOF 文件恢复数据

Text Only
# 开启AOF
appendonly yes
appendfilename "appendonly.aof"

# 同步策略
appendfsync always    # 每次写入都同步(最安全, 最慢)
appendfsync everysec  # 每秒同步一次(默认, 推荐)
appendfsync no        # 由操作系统决定何时同步(最快, 不安全)

AOF 重写( Rewrite ): - AOF 文件过大时,自动重写压缩(删除冗余命令) - 例: 100 次 INCR counter → 合并为 SET counter 100

RDB vs AOF 对比:

维度 RDB AOF
持久化方式 快照 日志追加
数据安全 可能丢失最后一次快照后的数据 最多丢 1 秒数据(everysec)
文件大小 压缩二进制, 文件小 文本格式, 文件大
恢复速度 快(直接加载) 慢(重放命令)
性能影响 fork 子进程, 可能有延迟 追加写入, 影响较小

混合持久化( Redis 4.0+): - AOF 重写时,将 RDB 数据写入 AOF 文件头部,增量部分仍用 AOF 格式 - 兼具 RDB 快速恢复和 AOF 数据安全的优点

Text Only
aof-use-rdb-preamble yes

28. Redis 的内存淘汰策略有哪些

当 Redis 使用内存超过maxmemory设置时,触发内存淘汰。

8 种淘汰策略:

策略 说明
noeviction 不淘汰,写入操作返回错误(默认)
allkeys-lru 所有 key 中淘汰最近最少使用的(推荐)
allkeys-lfu 所有 key 中淘汰最不经常使用的( Redis 4.0+)
allkeys-random 所有 key 中随机淘汰
volatile-lru 设置了过期时间的 key 中淘汰 LRU
volatile-lfu 设置了过期时间的 key 中淘汰 LFU
volatile-random 设置了过期时间的 key 中随机淘汰
volatile-ttl 淘汰最快过期的 key

LRU vs LFU : - LRU ( Least Recently Used ):淘汰最久没被访问的 - LFU ( Least Frequently Used ):淘汰访问频率最低的

推荐使用: - 通用场景:allkeys-lru - 有热点数据:allkeys-lfu(更精确识别热点)

Redis 过期键删除策略: - 惰性删除:访问 key 时检查是否过期,过期则删除 - 定期删除:每隔一段时间随机检查一批 key ,删除过期的 - 两者结合使用

29. 缓存穿透、缓存击穿、缓存雪崩分别是什么?如何解决

1. 缓存穿透 — 查不存在的数据

问题:大量请求查询不存在的数据,缓存 miss→全部打到数据库

Text Only
请求 key=99999 → Redis(miss) → 数据库(不存在) → 返回null → 不缓存
再次请求 key=99999 → Redis(miss) → 数据库... (每次都打到DB)

解决方案:

方案 1 :缓存空值

Python
def get_user(user_id):
    value = redis.get(f"user:{user_id}")
    if value is not None:
        return None if value == "" else json.loads(value)  # json.loads将JSON字符串转为Python对象

    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    if user is None:
        redis.setex(f"user:{user_id}", 300, "")  # 缓存空值5分钟
        return None

    redis.setex(f"user:{user_id}", 3600, json.dumps(user))
    return user

方案 2 :布隆过滤器( Bloom Filter )

Python
# 提前将所有合法ID加入布隆过滤器
bloom_filter.add(all_user_ids)

def get_user(user_id):
    if not bloom_filter.exists(user_id):  # 一定不存在
        return None
    # 正常查缓存和数据库

2. 缓存击穿 — 热点 key 过期

问题:某个热点 key 过期的瞬间,大量并发请求同时打到数据库

Text Only
热点key过期 → 1000个并发请求同时到来
→ 全部缓存miss → 全部查DB → DB可能崩溃

解决方案:

方案 1 :互斥锁(分布式锁)

Python
def get_hot_data(key):
    value = redis.get(key)
    if value:
        return value

    lock_key = f"lock:{key}"
    if redis.setnx(lock_key, 1):  # 获取锁
        redis.expire(lock_key, 10)
        try:  # try/except捕获异常
            value = db.query(...)
            redis.setex(key, 3600, value)
        finally:
            redis.delete(lock_key)  # 释放锁
        return value
    else:
        time.sleep(0.1)           # 等待
        return get_hot_data(key)   # 重试

方案 2 :逻辑过期(不设 TTL ,由程序判断)

Python
def get_hot_data(key):
    data = redis.get(key)  # 永不过期
    if data and data['expire_time'] > time.time():
        return data['value']

    # 逻辑过期,异步更新
    if try_lock(key):
        thread_pool.submit(refresh_cache, key)

    return data['value']  # 返回旧值

方案 3 :热点数据永不过期


3. 缓存雪崩 — 大量 key 同时过期

问题:大量缓存 key 在同一时间过期,或 Redis 宕机,请求全部打到数据库

解决方案:

  1. 过期时间加随机值
Python
# 避免大量key同时过期
ttl = base_ttl + random.randint(0, 300)  # 加0-5分钟随机
redis.setex(key, ttl, value)
  1. Redis 高可用(主从 + 哨兵/集群)

  2. 限流降级

Python
# 使用限流器,超出阈值走降级逻辑
if rate_limiter.allow():
    return query_db()
else:
    return default_response  # 降级返回默认值
  1. 多级缓存:本地缓存(Caffeine/Guava) + Redis + 数据库

30. Redis 集群方案有哪些

方案 1 :主从复制

Text Only
Master(读写) --复制--> Slave1(读)
                 --复制--> Slave2(读)
  • 从库只读,主库读写
  • 主库宕机需要手动切换

方案 2 :哨兵( Sentinel )

Text Only
Sentinel1  Sentinel2  Sentinel3
    ↓          ↓          ↓      (监控)
  Master → Slave1, Slave2

Master宕机 → Sentinel选举 → 自动将Slave提升为新Master
  • 自动故障检测和转移
  • 客户端通过 Sentinel 获取 Master 地址
  • 仍然是单 Master ,写入有瓶颈

方案 3 : Redis Cluster (官方集群方案)

Text Only
Node1(Master) ←→ Node2(Master) ←→ Node3(Master)
  ↕                 ↕                 ↕
Node4(Slave)    Node5(Slave)    Node6(Slave)

16384个hash slot分配给3个Master:
Node1: slot 0-5460
Node2: slot 5461-10922
Node3: slot 10923-16383
  • 数据分布在多个节点,通过 hash slot 分片
  • key 的 slot = CRC16(key) % 16384
  • 每个 Master 可以有 Slave ,自动 failover
  • 支持水平扩展
维度 主从 哨兵 Cluster
故障转移 手动 自动 自动
写扩展 不支持 不支持 支持
数据分布 全量复制 全量复制 分片存储
适用场景 简单读扩展 高可用 大规模分布式

31. Redis 分布式锁怎么实现?有什么问题

基础实现: SETNX

Text Only
# 加锁
SET lock_key unique_value NX EX 30
# NX: 只在key不存在时设置 (互斥)
# EX 30: 30秒过期 (防止死锁)

# 解锁 (Lua脚本保证原子性)
if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

为什么用 Lua 脚本解锁? - GET 和 DEL 是两步操作,不是原子的 - 可能出现: A 的锁超时释放了 → B 获取了锁 → A 删除了 B 的锁 - Lua 脚本在 Redis 中原子执行,先判断 value 是否是自己的再删除

存在的问题: 1. 锁过期,业务未完成:业务执行时间超过锁的过期时间 - 解决:看门狗( Watchdog )自动续期

  1. Redis 主从切换锁丢失:主库加锁后,还未同步到从库就宕机,新主库没有锁

Redisson 实现(推荐):

Java
RLock lock = redissonClient.getLock("myLock");
try {  // try/catch捕获异常
    lock.lock();  // 加锁(自动续期,看门狗每10秒续30秒)
    // 业务逻辑
} finally {
    lock.unlock();  // 解锁
}

Redisson 特性: - 看门狗自动续期( Watchdog, 默认 30 秒锁,每 10 秒续期一次) - 可重入锁 - 公平锁 - 联锁( MultiLock ) - 红锁( RedLock )

RedLock 算法(解决主从切换问题): - 使用 N 个(建议 5 个)独立的 Redis 实例 - 向所有实例请求加锁 - 超过半数(N/2+1)实例加锁成功才算获取锁 - 开销大,争议性大,实际使用不多

32. Redis 为什么这么快

1. 纯内存操作 - 数据存储在内存中,读写速度远快于磁盘

2. 单线程模型(核心操作) - 避免了多线程的上下文切换和锁竞争开销 - Redis 6.0+引入多线程 IO ,但命令执行仍是单线程

3. 高效的数据结构 - SDS (动态字符串): O(1)获取长度,预分配空间 - ziplist / listpack :紧凑内存布局;较新的 Redis 版本更多使用 listpack 取代 ziplist - skiplist (跳表): O(log n)查找 - hashtable : O(1)查找 - quicklist (快速列表):链表 + 紧凑列表节点的组合结构;不同版本内部节点实现会有差异

4. IO 多路复用( epoll ) - 一个线程处理大量 Socket 连接 - 非阻塞 IO ,高效的事件驱动模型

5. 通信协议简单 - RESP ( Redis Serialization Protocol )协议简单高效 - 文本协议,易于解析

6. 渐进式 rehash - 扩容时不一次性迁移所有数据,而是分步进行

33. Redis 大 Key 问题如何解决

什么是大 Key ? - String 类型 value > 10KB - Hash/List/Set/ZSet 元素数量 > 5000 或总大小 > 10MB

大 Key 的危害: 1. 内存不均:集群中某个节点内存远大于其他节点 2. 阻塞:大 key 的读写、删除操作可能阻塞 Redis 3. 网络流量:获取大 key 消耗大量网络带宽 4. 主从延迟:大 key 的同步影响复制延迟

发现大 Key :

Bash
# 扫描大key
redis-cli --bigkeys

# 或使用SCAN + MEMORY USAGE
redis-cli MEMORY USAGE big_key_name

解决方案:

1. 拆分大 Key

Text Only
# 大Hash拆分
user:1 {name, age, email, address, ...100个字段}
user:1:base {name, age, email}
user:1:detail {address, bio, avatar}

# 大List拆分
list:big → list:0, list:1, list:2 (按范围拆分)

2. 压缩 Value

Python
import zlib
compressed = zlib.compress(json.dumps(data).encode())  # json.dumps将Python对象转为JSON字符串
redis.set(key, compressed)

3. 异步删除大 Key

Text Only
# Redis 4.0+ 异步删除
UNLINK big_key  # 后台线程删除,不阻塞主线程

# 或者渐进式删除
# 对于Hash: HSCAN + HDEL
# 对于List: LTRIM
# 对于Set: SSCAN + SREM

4. 设置合理的过期时间

5. 监控告警:定期检测大 Key 并告警

34. Redis 的过期键删除策略是什么

Redis 使用两种策略结合删除过期键:

1. 惰性删除( Lazy Deletion ) - 访问 key 时才检查是否过期 - 过期则删除并返回 nil - 优点: CPU 友好 - 缺点:大量过期 key 不被访问时会占用内存

2. 定期删除( Periodic Deletion ) - Redis 默认每秒 10 次( hz 配置)随机检查一批键 - 每次检查时: 1. 从设置了过期时间的键中随机取 20 个 2. 删除其中过期的键 3. 如果过期键比例>25%,重复步骤 1 4. 否则等待下一次检查 - 优点:平衡了 CPU 和内存 - 缺点:可能有延迟删除

RDB 和 AOF 对过期键的处理: - RDB 生成时不保存过期键 - AOF 写入时过期键的 DEL 命令追加到 AOF - 主从复制中,从库的过期键由主库的 DEL 命令控制

35. Redis 事务和 Lua 脚本有什么区别

Redis 事务( MULTI/EXEC ):

Text Only
MULTI                    # 开始事务
SET key1 value1          # 入队
SET key2 value2          # 入队
EXEC                     # 执行所有命令

特点: - 不支持回滚(某条命令失败,其他命令仍然执行) - 弱原子性:命令按顺序执行,但不保证全部成功 - 可以配合 WATCH 实现乐观锁

Lua 脚本:

Text Only
EVAL "redis.call('SET', KEYS[1], ARGV[1]); redis.call('SET', KEYS[2], ARGV[2])" 2 key1 key2 val1 val2

特点: - 真正的原子性:脚本中的所有操作原子执行 - 可以包含条件判断和复杂逻辑 - 性能好:减少网络往返

推荐使用 Lua 脚本处理需要原子性的操作,如分布式锁的获取和释放。

36. Redis 的 Pipeline 是什么?有什么好处

Pipeline (管道): 一次性发送多个命令给 Redis ,减少网络 RTT ( Round-Trip Time )。

Python
# 不使用Pipeline(3次网络往返)
redis.set("key1", "val1")  # RTT
redis.set("key2", "val2")  # RTT
redis.set("key3", "val3")  # RTT

# 使用Pipeline(1次网络往返)
pipe = redis.pipeline()
pipe.set("key1", "val1")
pipe.set("key2", "val2")
pipe.set("key3", "val3")
pipe.execute()  # 一次性发送,一次性接收

Pipeline 的特点: - 显著减少网络延迟( RTT ),提升吞吐量 - 非原子操作(命令之间可能插入其他客户端的命令) - 命令在服务端按序执行 - 适合批量操作(批量 SET/GET )

Pipeline vs 事务 vs Lua : | 维度 | Pipeline | 事务(MULTI) | Lua 脚本 | |------|----------|-------------|---------| | 网络往返 | 1 次 | 2 次(MULTI+EXEC) | 1 次 | | 原子性 | 无 | 弱 | 强 | | 复杂逻辑 | 不支持 | 不支持 | 支持 |

37. Redis 的内存优化有哪些方法

1. 选择合适的编码 - Hash: 当元素少且值小时,Redis 会优先使用紧凑编码(新版本常见为 listpack,旧资料里常写 ziplist )

Text Only
hash-max-listpack-entries 512    # listpack 最大元素数(旧版本参数名常见为 ziplist)
hash-max-listpack-value 64       # listpack 单个值最大字节

2. 合理使用数据结构 - 存储简单对象用 Hash 而非 String ( Hash 更省内存) - 使用 Bitmap 、 HyperLogLog 等节省空间的结构

3. 设置过期时间 - 临时数据设置合理的 TTL - 避免永不过期的冗余数据

4. 压缩 Value

5. 使用 Redis 对象共享 - Redis 对 0-9999 的整数对象进行共享 - 使用整数作为 value 更节省内存

6. 避免大 Key ,定期清理


面试答题技巧

  1. 索引问题:先讲 B+树原理,再结合 EXPLAIN 实际分析,最后给出优化建议
  2. 事务问题:从 ACID 出发,结合隔离级别和 MVCC 解释
  3. Redis 问题:数据结构→持久化→集群→缓存问题,层层深入
  4. 优化问题:先分析(慢查询/EXPLAIN ),再优化(索引/SQL 改写/分库分表),有数据支撑
  5. 结合项目:举出实际项目中的缓存穿透、慢查询优化等案例

⚠️ 核验说明(2026-04-03):本页已完成 2026-04-03 人工复核。Redis 编码与配置项已补上版本边界,避免把旧版 ziplist 资料直接写成当前通用事实。


最后更新日期: 2026-04-03