TiDB 性能优化面试题:索引、缓存、异步与并发问题解析
·
一、TiDB 面试高频题全解
TiDB 是后端开发岗必考技术之一,面试中对索引原理、事务隔离、分布式架构、SQL优化四大方向的考察深度逐年提升。本文精选10道必考题,配有完整代码和原理分析。
二、索引原理必考题
2.1 Q:TiDB 索引的底层数据结构?B+ Tree 的优势?
参考答案:
-- 验证索引使用情况
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE user_id = 123 AND status = 'paid'
ORDER BY created_at DESC
LIMIT 20;
-- 输出分析:
-- 好:Index Scan using idx_user_status on orders
-- 差:Seq Scan on orders(全表扫描)
-- 联合索引:最左匹配原则
CREATE INDEX idx_user_status_date
ON orders(user_id, status, created_at DESC);
-- 覆盖索引:避免回表
CREATE INDEX idx_covering
ON users(id, name, email); -- 查询只包含这三列时,不需要回表
SELECT id, name, email FROM users WHERE id BETWEEN 1 AND 100;
-- 完全走覆盖索引,不回表
B+ Tree 的优势(vs B Tree vs Hash):
- 非叶节点只存键,不存数据,同一页面能存更多键 → 树更矮
- 叶节点形成双向链表 → 范围查询高效(B Tree 需要回溯)
- Hash 索引只支持等值查询,不支持范围
2.2 Q:什么情况下索引会失效?
-- ❌ 索引失效场景
-- 1. 对索引列使用函数
SELECT * FROM users WHERE YEAR(created_at) = 2024; -- 失效
SELECT * FROM users WHERE created_at >= '2024-01-01'; -- ✅ 有效
-- 2. 隐式类型转换(phone 是 varchar,传入 number)
SELECT * FROM users WHERE phone = 13800138000; -- 失效(全表扫描)
SELECT * FROM users WHERE phone = '13800138000'; -- ✅ 有效
-- 3. 联合索引未遵守最左匹配
-- 索引:(a, b, c)
SELECT * FROM t WHERE b = 1 AND c = 2; -- ❌ a 未出现,失效
SELECT * FROM t WHERE a = 1 AND c = 2; -- ⚠️ 只用到 a 列索引
SELECT * FROM t WHERE a = 1 AND b = 2 AND c = 3; -- ✅ 完整使用
-- 4. LIKE 以通配符开头
SELECT * FROM users WHERE name LIKE '%张%'; -- ❌ 失效
SELECT * FROM users WHERE name LIKE '张%'; -- ✅ 有效
-- 5. OR 条件一侧无索引(MySQL)
SELECT * FROM users WHERE id = 1 OR age = 25;
-- 如果 age 无索引,全表扫描
三、事务隔离级别(必考)
3.1 Q:四种隔离级别分别解决了什么问题?
-- 查看/设置隔离级别
SELECT @@transaction_isolation;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 四种隔离级别与并发问题对照表:
-- READ UNCOMMITTED:脏读✓ 不可重复读✓ 幻读✓(几乎不用)
-- READ COMMITTED:脏读✗ 不可重复读✓ 幻读✓(Oracle默认)
-- REPEATABLE READ:脏读✗ 不可重复读✗ 幻读✓(MySQL默认,MVCC+间隙锁基本解决幻读)
-- SERIALIZABLE:脏读✗ 不可重复读✗ 幻读✗(性能最差)
-- 演示不可重复读(READ COMMITTED)
-- 事务A:
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- 结果:1000
-- 事务B(此时并发执行):
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT;
-- 事务A继续:
SELECT balance FROM accounts WHERE id = 1; -- 结果:900(不可重复读!)
COMMIT;
-- REPEATABLE READ 下事务A第二次仍然读到 1000(MVCC 保证)
3.2 Q:MVCC 的实现原理?
-- MySQL InnoDB MVCC:每行数据包含隐藏字段
-- DB_TRX_ID:最后一次修改的事务ID
-- DB_ROLL_PTR:指向 undo log 中旧版本
-- DB_ROW_ID:隐式主键
-- 读操作:快照读(不加锁,通过 ReadView 决定可见性)
SELECT * FROM orders WHERE id = 1; -- 快照读
-- 当前读(加锁,读最新版本)
SELECT * FROM orders WHERE id = 1 FOR UPDATE; -- X锁
SELECT * FROM orders WHERE id = 1 LOCK IN SHARE MODE; -- S锁
UPDATE orders SET status='paid' WHERE id = 1; -- 当前读
-- ReadView 可见性规则(REPEATABLE READ):
-- 1. 事务ID < min_trx_id:已提交,可见
-- 2. 事务ID > max_trx_id:未提交,不可见
-- 3. 在范围内:判断是否在 m_ids(活跃事务列表)中
四、性能调优实战
4.1 Q:慢查询排查完整流程?
-- 1. 开启慢查询日志(线上谨慎操作)
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1; -- 超过1秒记录
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
-- 2. 用 EXPLAIN 分析执行计划
EXPLAIN SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01'
GROUP BY u.id
ORDER BY order_count DESC
LIMIT 10;
-- 关注 type 列(效率从高到低):
-- system > const > eq_ref > ref > range > index > ALL(全表扫描)
-- 关注 Extra 列:
-- Using index(覆盖索引,最好)
-- Using filesort(需要优化ORDER BY)
-- Using temporary(需要优化GROUP BY)
-- 3. 针对性优化
-- 问题:全表扫描 + filesort
CREATE INDEX idx_users_created ON users(created_at, id);
CREATE INDEX idx_orders_user ON orders(user_id);
-- 优化后:range扫描 + 覆盖索引,性能提升10x
五、高可用与分布式
5.1 Q:主从复制原理?主库宕机如何切换?
-- 主从复制三个线程:
-- Master: binlog dump 线程(发送 binlog)
-- Slave: IO 线程(接收写relay log)+ SQL线程(重放relay log)
-- 查看主从延迟
SHOW SLAVE STATUS\G
-- 关注 Seconds_Behind_Master:延迟秒数
-- 关注 Exec_Master_Log_Pos:已执行的位置
-- 主库故障切换步骤:
-- 1. 确认主库故障(心跳超时)
-- 2. 选举延迟最小的从库(MHA/Orchestrator自动选举)
-- 3. 从库执行剩余 relay log
-- 4. 将选举的从库提升为主库
-- 5. 其他从库切换到新主库
-- 6. 应用层更新连接配置(VIP漂移 或 DNS切换)
-- 半同步复制:保证至少一个从库收到binlog再提交
SET GLOBAL rpl_semi_sync_master_enabled = ON;
SET GLOBAL rpl_semi_sync_master_timeout = 10000; -- 等待超时降级为异步
六、手写题
6.1 实现分页查询(大数据量优化版)
-- ❌ 传统分页:OFFSET 越大越慢
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 1000000;
-- 需要扫描 1000020 行,丢弃前 1000000 行
-- ✅ 游标分页:利用索引快速定位
SELECT * FROM orders
WHERE id > :last_id -- 上次返回的最后一个 id
ORDER BY id ASC
LIMIT 20;
-- 直接从 last_id 开始,O(log n) 定位
-- ✅ 延迟关联:先查主键,再关联
SELECT o.*
FROM (
SELECT id FROM orders
WHERE status = 'paid'
ORDER BY created_at DESC
LIMIT 20 OFFSET 10000
) AS temp
JOIN orders o ON temp.id = o.id;
-- 子查询只用覆盖索引,减少回表次数
七、面试汇总
| 题目 | 难度 | 考察重点 |
|---|---|---|
| B+ Tree 原理 | ⭐⭐ | 存储引擎 |
| 索引失效场景 | ⭐⭐⭐ | 优化意识 |
| MVCC 实现 | ⭐⭐⭐⭐ | 深度原理 |
| 慢查询排查流程 | ⭐⭐⭐ | 实战能力 |
| 主从切换流程 | ⭐⭐⭐ | 高可用 |
| 大数据量分页优化 | ⭐⭐⭐⭐ | 性能优化 |
💬 收藏+关注!TiDB 高频面试题持续更新,助力 Offer 到手!
三、实战进阶:TiDB 最佳实践
3.1 错误处理与异常设计
在生产环境中,完善的错误处理是系统稳定性的基石。以下是 TiDB 的推荐错误处理模式:
-- 错误处理:使用事务保证数据一致性
CREATE OR REPLACE FUNCTION safe_transfer(
from_id BIGINT,
to_id BIGINT,
amount DECIMAL(10,2)
) RETURNS BOOLEAN AS $$
DECLARE
from_balance DECIMAL(10,2);
BEGIN
-- 加锁查询余额(防止并发问题)
SELECT balance INTO from_balance
FROM accounts
WHERE id = from_id
FOR UPDATE;
IF from_balance < amount THEN
RAISE EXCEPTION '余额不足: 当前余额 %, 需要 %', from_balance, amount;
END IF;
UPDATE accounts SET balance = balance - amount WHERE id = from_id;
UPDATE accounts SET balance = balance + amount WHERE id = to_id;
INSERT INTO transfer_logs(from_id, to_id, amount, created_at)
VALUES(from_id, to_id, amount, NOW());
RETURN TRUE;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE NOTICE '转账失败: %', SQLERRM;
RETURN FALSE;
END;
$$ LANGUAGE plpgsql;
3.2 性能监控与可观测性
现代系统必须具备三大可观测性:Metrics(指标)、Logs(日志)、Traces(链路追踪)。
-- 慢查询监控与分析
-- 开启慢查询日志
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5; -- 超过 500ms 的查询记录
-- 实时查看正在执行的慢查询(MySQL)
SELECT
id,
user,
host,
db,
command,
time AS seconds,
LEFT(info, 100) AS query_preview
FROM information_schema.processlist
WHERE command != 'Sleep'
AND time > 1
ORDER BY time DESC;
-- 查询统计(PostgreSQL)
SELECT
query,
calls,
total_exec_time / 1000 AS total_seconds,
mean_exec_time AS avg_ms,
rows / calls AS avg_rows
FROM pg_stat_statements
WHERE calls > 10
ORDER BY mean_exec_time DESC
LIMIT 20;
-- 表大小与索引使用情况
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS total_size,
seq_scan AS full_scans,
idx_scan AS index_scans
FROM pg_stat_user_tables
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
3.3 测试策略:单元测试 + 集成测试
高质量代码离不开完善的测试覆盖。以下是 TiDB 推荐的测试实践:
-- 数据库测试:使用事务回滚保证测试幂等
-- PostgreSQL 测试模式(每次测试在事务中执行,结束后回滚)
BEGIN;
-- 测试数据插入
INSERT INTO users (username, email, password_hash)
VALUES ('test_user', 'test@example.com', 'hashed_password');
-- 验证插入结果
DO $$
DECLARE
user_count INT;
BEGIN
SELECT COUNT(*) INTO user_count FROM users WHERE email = 'test@example.com';
IF user_count != 1 THEN
RAISE EXCEPTION 'Test failed: expected 1 user, got %', user_count;
END IF;
RAISE NOTICE 'Test passed: user inserted correctly';
END $$;
-- 测试唯一约束
DO $$
BEGIN
BEGIN
INSERT INTO users (username, email, password_hash)
VALUES ('test_user2', 'test@example.com', 'hash2');
RAISE EXCEPTION 'Test failed: unique constraint not triggered';
EXCEPTION WHEN unique_violation THEN
RAISE NOTICE 'Test passed: unique constraint works correctly';
END;
END $$;
ROLLBACK; -- 回滚所有测试数据,不影响正式数据库
3.4 生产部署清单
上线前必检:
| 检查项 | 具体内容 | 优先级 |
|---|---|---|
| 配置安全 | 密钥不在代码中,用环境变量或 Vault | P0 |
| 错误处理 | 所有 API 有 fallback,不暴露内部错误 | P0 |
| 日志规范 | 结构化 JSON 日志,含 traceId | P0 |
| 健康检查 | /health 接口,K8s readiness/liveness probe | P0 |
| 限流保护 | API 网关或应用层限流 | P1 |
| 监控告警 | 错误率/响应时间/CPU/内存 四大指标 | P1 |
| 压测验证 | 上线前跑 10 分钟压测,确认 QPS/延迟 | P1 |
| 回滚预案 | 蓝绿部署或金丝雀发布,问题 1 分钟回滚 | P1 |
四、常见问题排查
4.1 TiDB 内存占用过高?
排查步骤:
- 确认泄漏存在:观察内存是否持续增长(而非偶发峰值)
- 生成内存快照:使用对应工具(Chrome DevTools / heapdump / memory_profiler)
- 比对两次快照:找到两次快照间"新增且未释放"的对象
- 溯源代码:找到对象创建的调用栈,确认是否被缓存/全局变量/闭包持有
常见原因:
- 全局/模块级变量无限增长(缓存无上限)
- 事件监听器添加但未移除
- 定时器/interval 未清理
- 闭包意外持有大对象引用
4.2 性能瓶颈在哪里?
通用排查三板斧:
- 数据库:explain 慢查询,加索引,缓存热点数据
- 网络 IO:接口耗时分布(P50/P90/P99),N+1 查询问题
- CPU:火焰图(flamegraph)找热点函数,减少不必要计算
五、总结与最佳实践
学习 TiDB 的正确姿势:
- 先跑通,再优化:先让代码工作,再根据性能测试数据做针对性优化
- 了解底层原理:知道框架帮你做了什么,才知道什么时候需要绕过它
- 从错误中学习:每次线上问题都是提升的机会,认真做 RCA(根因分析)
- 保持代码可测试:依赖注入、单一职责,让每个函数都能独立测试
- 关注社区动态:订阅官方博客/Release Notes,及时了解新特性和 Breaking Changes
💬 觉得有帮助?点赞+收藏+关注!持续更新 TiDB 实战系列。
💬 觉得有用的话,点个赞+收藏,关注我,持续更新优质技术内容!
标签:TiDB | 性能优化 | 面试 | 索引 | 并发
更多推荐

所有评论(0)