数据库热门面试题
使用说明
本篇共 72 道题。
数据库题要从数据模型、访问模式、一致性和运维回答。不要只背概念,应说明什么查询会受益、什么故障需要防范。
Q1: SQL 和 NoSQL 数据库怎么选?
答案:
关系型数据库适合结构明确、关联查询和强事务;NoSQL 是多类模型的统称,文档、键值、列族等各自在灵活结构、水平扩展或特定访问模式上有优势。
选择依据是数据关系、查询、事务、一致性、规模和团队能力。一个系统可以组合使用,但每增加一种存储都增加一致性和运维成本。
Q2: MySQL 索引为什么常用 B+Tree?
答案:
B+Tree 分支多、树高低,适合磁盘/页式存储;数据按键有序,既支持等值也支持范围和顺序扫描,叶子节点形成有序链。
哈希适合等值但不支持范围,二叉树层级可能更深。具体叶子存主键还是整行取决于存储引擎和索引类型。
- B+Tree 的内部节点只存键和子指针,单页能容纳更多键,树高通常很低;查询只需少量随机 I/O。
- 所有数据在叶子层且叶子按序相连,所以点查、范围查和排序都高效。哈希索引虽擅长等值查找,却不支持范围和有序遍历。
Q3: 聚簇索引和二级索引有什么区别?
答案:
聚簇索引的叶子直接组织完整行,一张表通常只有一个;二级索引叶子保存索引键和定位主记录的信息。
在 InnoDB 中二级索引通常保存主键,查询非覆盖列需要回表,所以主键过大也会放大所有二级索引。
- InnoDB 的聚簇索引叶子直接存整行,一张表只有一个,通常由主键组织;二级索引叶子存索引列加主键值。
- 通过二级索引查询非覆盖列要先找到主键再“回表”。因此主键应短且稳定,二级索引设计要考虑回表和覆盖能力。
Q4: 联合索引的最左前缀怎么理解?
答案:
联合索引按列顺序排序,查询只有在前导列条件能确定或缩小有序范围时,后续列才更容易继续用于定位或排序。
不是“必须出现第一列”这么机械,优化器还可能跳跃扫描或仅用于覆盖。索引顺序应结合等值、范围、排序和选择性用执行计划验证。
- 联合索引
(a,b,c)按 a、再 b、再 c 排序,所以查询必须从最左连续列建立可利用的查找范围。跳过 a 通常无法直接定位 b。 - 范围条件后的列常不能继续缩小扫描范围,但可能用于索引条件下推或覆盖。最终是否使用仍以
EXPLAIN和数据分布为准。
Q5: 什么是覆盖索引?
答案:
查询需要的筛选和返回列都能从某个索引获得,不必回到主记录读取整行,就是覆盖索引。
它能减少随机 I/O,但为了覆盖把大量列塞进索引会增加空间和写放大。只针对高频关键查询设计。
- 如果查询所需的过滤、排序和返回列都能从同一索引取得,就不必回到聚簇索引读取整行,这就是覆盖索引。
EXPLAIN常出现Using index。覆盖能显著减少随机 I/O,但为了覆盖把大量低价值列塞进索引,会增加存储和写放大。
Q6: 索引越多越好吗?
答案:
不是。索引加速读取,但每次写入要维护更多树,占用磁盘和缓存,也会增加优化器选择复杂度。
根据真实慢查询、访问频率和基数设计,定期找重复/未使用索引。低选择性列单独索引未必有效,但与其他列组合可能有价值。
- 每个索引都会占空间,并让 INSERT、UPDATE、DELETE 维护更多 B+Tree;相似或低选择性索引还可能干扰优化器选择。
- 应从真实查询和慢日志出发设计联合索引,定期清理重复与未使用索引,同时保留主键、唯一性等正确性约束。
Q7: 事务的 ACID 是什么?
答案:
原子性保证事务内操作整体成功或回滚;一致性是约束从一个合法状态到另一个;隔离性控制并发事务相互影响;持久性保证提交结果在故障后可恢复。
它们由日志、锁/MVCC、约束和应用共同实现,不意味着任何跨服务业务天然都有 ACID。
- 原子性保证事务要么全成要么回滚;一致性是约束和业务不变量在事务前后成立;隔离性控制并发事务互相可见;持久性保证提交后故障也能恢复。
- ACID 需要数据库日志、锁/MVCC、约束和应用正确使用共同实现,不等于“用了事务就不会有业务并发问题”。
Q8: 事务隔离级别解决哪些并发问题?
答案:
常讨论脏读、不可重复读和幻读。隔离越强,并发控制成本通常越高。
数据库对标准隔离级别的具体实现可能不同,例如快照和锁策略。选级别要结合业务不变量,并用唯一约束或显式锁保护关键写入。
- 读未提交可能脏读;读已提交避免脏读但同一事务两次读取可能不同;可重复读提供稳定快照;串行化最强但并发成本最高。
- 还要区分快照读和当前读。隔离级别不是越高越好,应根据不变量、冲突率和吞吐选择,并用锁或版本号保护关键写入。
Q9: MVCC 是什么?
答案:
多版本并发控制为记录保留可见版本,让读事务基于快照读取,减少读写互相阻塞;写冲突仍需要锁或检测。
可见性由事务时间/ID和版本链决定。长事务会阻碍旧版本清理,导致膨胀和复制延迟。
- MVCC 给行维护多个版本,事务根据快照和可见性规则读取合适版本,让读写多数情况下不用互相阻塞。
- 旧版本通常由 undo/版本链保存,长事务会阻碍清理并造成膨胀。MVCC 不替代写锁,也不能自动解决所有丢失更新问题。
Q10: 乐观锁和悲观锁怎么选?
答案:
| 维度 | 悲观锁 | 乐观锁 |
|---|---|---|
| 思路 | 先加锁再操作 | 先操作再检查冲突 |
| 实现 | SELECT ... FOR UPDATE | 版本号 / CAS |
| 适用 | 写多读少、冲突频繁 | 读多写少、冲突较少 |
-- 悲观锁
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 乐观锁(版本号)
UPDATE accounts SET balance = 200, version = version + 1
WHERE id = 1 AND version = 5;
Q11: 数据库死锁如何产生和处理?
答案:
多个事务按不同顺序持有资源并等待对方,形成循环依赖。数据库会检测并回滚其中一个事务。
统一访问顺序、缩短事务、正确索引减少锁范围,并对可重试错误做有限退避。死锁日志是定位证据,不能只增加超时。
- 两个事务以不同顺序持有并等待对方资源会形成环。数据库通常检测等待图并回滚其中一个事务。
- 应统一访问顺序、缩短事务、建立合适索引减少锁范围,并对死锁错误做有限重试;排查时查看死锁日志和具体 SQL,而不是只调大超时。
Q12: 如何分析一条慢 SQL?
答案:
先确认参数、频率和耗时分布,再看执行计划中的访问方式、估算/实际行数、索引、Join 顺序、排序和临时结果。
检查统计信息、返回列和网络,不要一看到全表扫描就盲目加索引;小表全扫可能是最优,索引也可能因数据分布失效。
- 先确认慢发生在连接等待、锁等待、执行还是返回大量数据,再看慢查询日志和
EXPLAIN/EXPLAIN ANALYZE。 - 重点检查扫描行数、访问类型、索引选择、排序/临时表和估算偏差;优化可能是改 SQL、索引或数据模型,改完要在真实数据分布下对比。
Q13: 深分页为什么慢?
答案:
LIMIT offset, size 往往仍要扫描并丢弃前面大量行,offset 越大成本越高,且并发写入会让页结果漂移。
稳定排序场景使用基于最后一个排序键的游标分页,并给排序列建立合适索引。确需随机跳页再评估缓存或延迟方案。
LIMIT offset, size往往仍要扫描并丢弃前 offset 行,offset 越大成本越高;涉及回表时还会产生大量随机 I/O。- 稳定排序下优先用基于最后一个排序键的游标分页,并用唯一键打破并列。确需跳页时可先覆盖索引取 ID 再关联原表。
Q14: 数据库范式和反范式如何取舍?
答案:
范式减少重复和更新异常,适合作为事务数据基础;反范式用冗余换读性能或简化查询,但需要明确同步和修复机制。
先保持正确模型,再由真实查询和容量证明是否冗余。冗余字段要定义来源、更新顺序和一致性级别。
- 范式减少重复和更新异常,适合事实来源与强一致核心数据;反范式用冗余换取更少 JOIN 和更快读取,适合读多且可控更新的场景。
- 冗余字段必须定义维护者、同步方式和修复机制。订单快照是合理冗余,但无责任边界的复制字段会快速失真。
Q15: 数据库连接池解决什么问题?
答案:
复用建立好的连接,限制并发进入数据库并管理超时、健康和回收,避免每个请求重复握手。
池不是越大越好。总连接要按数据库容量和应用实例数计算,设置获取超时并监控等待队列、使用率和泄漏。
- 建立数据库连接包含 TCP/TLS、认证和会话初始化,连接池复用这些昂贵资源,并对并发查询施加上限。
- 池太小会排队,太大会把数据库压垮。要配置获取超时、空闲回收和健康检查,并监控活跃/空闲/等待数,确保每个代码路径归还连接。
Q16: Redis 常用数据结构如何选?
答案:
String 适合值和计数;Hash 适合对象字段;List 适合简单队列;Set 做去重;Sorted Set 做排名和按分数范围;Stream 适合带消费组的消息流。
先看操作语义和复杂度,再考虑内存编码与大 Key,不能所有数据都序列化成一个巨大字符串。
Q17: Redis 持久化 RDB 和 AOF 有什么区别?
答案:
| 维度 | RDB | AOF |
|---|---|---|
| 方式 | 定时快照(fork 子进程生成二进制文件) | 追加写命令日志 |
| 数据安全 | 可能丢失两次快照之间的数据 | 最多丢 1 秒(everysec) |
| 文件大小 | 紧凑 | 较大(但有重写机制) |
| 恢复速度 | 快(直接加载二进制) | 较慢(重放命令) |
| 适用场景 | 备份、灾难恢复 | 数据安全要求高 |
推荐:Redis 4.0+ 使用混合持久化(aof-use-rdb-preamble yes),AOF 重写时前半部分用 RDB 格式、后半部分用 AOF 增量,兼具 RDB 的快速恢复和 AOF 的数据安全。
Q18: MongoDB 适合什么场景?
答案:
文档模型适合数据天然聚合、结构演进快、按文档读取写入的场景,嵌套结构可减少部分 Join。
设计要根据访问模式决定嵌入还是引用,并设置 Schema 校验和索引。灵活 Schema 不代表可以没有数据治理。
- MongoDB 适合文档结构、字段演进快、数据常按聚合根整体读取的场景;嵌套文档可减少跨表 JOIN。
- 它并不意味着无需 Schema 和索引。复杂关联、强关系约束和大量跨文档事务通常更适合关系数据库,选择应看访问模式而非“数据是 JSON”。
Q19: PostgreSQL 有哪些常见优势?
答案:
标准 SQL 和事务能力完整,支持丰富类型、JSONB、窗口函数、CTE、扩展和多种索引,适合关系与部分半结构化数据组合。
选择仍要结合团队运维、云服务、复制和现有生态,不应只用功能列表断言它适合所有系统。
- PostgreSQL 在标准 SQL、复杂查询、扩展类型、JSONB、窗口函数、GIS 和可扩展性方面很强,适合关系数据与多种查询混合。
- 选型还要看团队经验、托管服务、复制运维和既有生态;不能只列功能表。实际性能取决于 Schema、索引和工作负载。
Q20: 主从复制和读写分离有哪些问题?
答案:
主库写入后从库异步重放,能扩展读取和提供副本,但会有复制延迟,刚写完读从库可能看不到。
需要读己之写、会话粘主、延迟监控和故障切换。复制不是备份,逻辑误删也会复制到从库。
- 异步复制会有延迟,写后立即读从库可能看不到数据;故障切换还要处理复制位点、脑裂和旧主重新加入。
- 关键读可暂时走主库或携带一致性 token,普通读允许最终一致。读写分离也会增加连接路由、事务和监控复杂度。
Q21: 分库分表的关键难点是什么?
答案:
除了选择稳定且均匀的分片键,还要处理跨分片查询、分页、聚合、事务、全局 ID、热点、扩容迁移和路由治理。
优先通过索引、缓存、读副本和垂直拆分延后分片。必须分片时让常用查询尽量落在单分片。
- 难点不只是路由,还包括全局唯一 ID、跨分片查询与排序、事务、扩容迁移、热点分片和运维工具。
- 应先通过索引、归档、读副本和垂直拆分延后分片。确需分片时选择能让主要查询单分片完成的分片键,并预留在线迁移方案。
Q22: 如何保证缓存与数据库一致性?
答案:
常见 Cache Aside 是写数据库后删除缓存,读时未命中回源。它提供最终一致性,但仍需处理并发读写和删除失败。
可通过短 TTL、重试队列、订阅变更日志和版本号增强。强一致关键数据不应把缓存作为唯一判断来源。
- 常用 Cache Aside:先更新数据库,再删除缓存;删除而非直接更新能降低并发覆盖风险,但仍存在短暂窗口。
- 高一致要求可用事务 Outbox/CDC 可靠发送失效事件,加版本号防旧值回写。所有方案都要定义允许不一致时长、重试和缓存兜底。
Q23: 分布式事务有哪些方案?
答案:
强一致可用 2PC 等协调协议,但可用性和性能成本高;业务常用 Saga/补偿、Outbox + 消息和幂等状态机实现最终一致。
选择依据是业务不变量、可补偿性和故障恢复。重点不是背名词,而是能说明中间状态、重试和人工修复。
- 2PC 提供较强原子性但协调和阻塞成本高;Saga 把长事务拆成本地事务加补偿;TCC 用 Try/Confirm/Cancel 显式预留资源;Outbox 适合可靠事件驱动。
- 选择取决于一致性、时延和可补偿性。无论哪种都要幂等、状态持久化、超时重试和人工修复。
Q24: 向量数据库和普通索引有什么区别?
答案:
向量数据库按高维距离做近似最近邻检索,适合语义相似;传统 B+Tree/倒排索引处理精确、范围和关键词更强。
实际 RAG 常组合向量、关键词和元数据过滤,再重排。索引参数在召回、延迟、内存和更新成本之间权衡,仍需离线评测。
- 普通 B+Tree/倒排索引擅长精确值、范围和关键词;向量索引按距离找语义近邻,常用 HNSW、IVF 等近似算法在召回率、延迟和内存间取舍。
- 向量库还需处理 embedding 版本、元数据过滤和重建索引。它通常与关系/关键词检索混合,而不是替代主数据库。
Q25: WHERE 和 HAVING 的区别?
答案:
WHERE:在分组前过滤,不能用聚合函数HAVING:在分组后过滤,可以用聚合函数
SELECT user_id, SUM(amount)
FROM orders
WHERE amount > 10 -- 先过滤金额 > 10 的订单
GROUP BY user_id
HAVING SUM(amount) > 1000; -- 再过滤总金额 > 1000 的用户
Q26: 索引失效的常见场景?
答案:
- 对索引列使用函数:
WHERE YEAR(created_at) = 2024 - 隐式类型转换:
WHERE phone = 13800138000(phone 是 VARCHAR) - LIKE 左模糊:
WHERE name LIKE '%Tom' - OR 条件未全部建索引
- 使用
!=或NOT IN - 联合索引不满足最左前缀
Q27: SELECT ... FOR UPDATE 什么时候会锁住更多数据?
答案:
它会对查询命中的记录获取排他锁,但实际范围取决于存储引擎、隔离级别、访问路径和索引。条件没有合适索引时,数据库可能扫描并锁住远多于预期的记录;范围查询在某些隔离级别下还可能涉及间隙锁。
使用前要:
- 让条件命中唯一或高选择性索引。
- 保持事务短小,避免锁内网络请求和用户交互。
- 所有竞争流程按一致顺序获取锁,降低死锁概率。
- 设置锁等待超时并对可重试事务做有限重试。
- 通过执行计划和锁监控确认真实范围。
Q28: 一对多、多对多怎么设计?
答案:
- 一对多:在"多"的一方加外键。如用户→订单,订单表加
user_id - 多对多:中间表。如学生↔课程,创建
student_courses(student_id, course_id)关联表
Q29: MongoDB 的索引和 MySQL 有什么区别?
答案:
MongoDB 也使用 B 树索引,支持:
- 单字段索引、复合索引(同样遵循最左前缀)
- 文本索引(全文搜索)
- 地理空间索引(2dsphere)
- TTL 索引(自动过期删除)
Q30: Redis 单线程为什么还这么快?
答案:
Redis 的"单线程"指的是命令执行在单线程中完成,但这并不意味着整个 Redis 只有一个线程。
快的原因:
- 纯内存操作:数据在内存中,读写速度是纳秒级,远快于磁盘
- 单线程避免锁竞争:不需要加锁、不需要上下文切换,消除了多线程的同步开销
- IO 多路复用(epoll/kqueue):一个线程监听多个 socket,不需要为每个连接创建线程
- 高效数据结构:跳表、压缩列表、intset 等都是精心优化的
- 简单的执行模型:没有查询解析、执行计划等开销(相比 SQL 数据库)
Redis 6.0 引入了多线程 IO——网络数据的读取和写入可以由多个 IO 线程并行处理,但命令执行仍然是单线程。这样既利用了多核处理网络 IO 瓶颈,又保持了命令执行的简单性和无锁特性。
IO 线程 1 ─┐
IO 线程 2 ──┼─→ [命令队列] → 主线程串行执行 → [响应队列] ──┼→ IO 线程写回
IO 线程 3 ─┘ └→
Q31: 为什么推荐 Prisma?
答案:
- 类型安全:从 Schema 自动生成 TypeScript 类型
- 迁移:
prisma migrate自动管理数据库变更 - 直观 API:声明式查询,学习成本低
- Prisma Studio:可视化数据查看
缺点:复杂查询不如原生 SQL 灵活,有运行时 overhead(Query Engine)。
Q32: 数据库中的近似计数什么时候比精确 COUNT(*) 更合适?
答案:
大表上按复杂条件做精确计数可能需要扫描大量索引或数据,而首页展示的“约 120 万条”往往不要求事务级精确。此时可以使用数据库统计信息、搜索引擎估算、预聚合表或 HyperLogLog 等近似结构降低成本。
选择前要明确误差和时效要求:
- 结算、库存、账务等正确性场景必须使用可验证的精确结果。
- 运营看板、搜索结果数量和基数分析可接受近似,但应标注口径与更新时间。
- 预聚合要设计增量更新、失败补偿和定期校准。
优化前先看执行计划和真实数据量,小表或合适索引上的 COUNT(*) 未必有问题。
Q33: 主从延迟怎么处理?
答案:
- 关键业务读主库:写后立即读走主库
- 半同步复制:主库等至少一个从库确认
- 延迟监控:
SHOW SLAVE STATUS查看Seconds_Behind_Master - 业务层缓存:写入后更新缓存
Q34: MySQL 和 PostgreSQL 的默认事务隔离级别有什么差异?为什么不能只背默认值?
答案:
常见配置下,InnoDB 默认是 Repeatable Read,PostgreSQL 默认是 Read Committed;但云数据库、连接代理或应用会话可以修改它,所以排查并发问题时必须查询当前实例和当前连接的实际设置。
两者即使隔离级别名称相同,实现行为也可能不同,例如快照建立时机、锁策略、冲突检测和 Serializable 实现。回答时应先说明业务要避免的是脏读、不可重复读、写偏差还是丢失更新,再判断是否还需要显式锁、版本号、唯一约束或重试。
隔离级别越高不代表业务自动正确,也可能增加冲突和重试成本。
Q35: 迁移文件要不要提交到 Git?
答案:
必须提交。 迁移文件是数据库版本的历史记录,和代码一样需要版本管理。团队成员拉取代码后执行迁移即可同步数据库结构。
Q36: 最小权限原则怎么实践?
答案:
- 应用使用专用数据库用户,只授予必要的 SELECT/INSERT/UPDATE 权限
- 不给应用 DROP/ALTER/GRANT 权限
- 生产数据库禁止直接登录,通过审计工具操作
Q37: 为什么不用 MySQL 存时序数据?
答案:
- MySQL 按行存储,聚合查询需要扫描大量无关列
- 数据量大时写入性能下降(索引维护)
- 缺少自动数据过期(TTL)
- 缺少时序专用聚合函数
Q38: Elasticsearch 和 MySQL LIKE 的区别?
答案:
| 维度 | ES | MySQL LIKE |
|---|---|---|
| 原理 | 倒排索引 | 全表扫描 |
| 中文 | 分词后搜索 | 模糊匹配 |
| 性能 | 亿级数据毫秒返回 | 百万级就很慢 |
| 相关性 | 自动评分排序 | 无 |
Q39: RPO 和 RTO 是什么?
答案:
- RPO(Recovery Point Objective):可容许的数据丢失量。RPO = 1 小时意味着最多丢 1 小时数据
- RTO(Recovery Time Objective):可容许的恢复时间。RTO = 30 分钟意味着 30 分钟内必须恢复
Q40: RAG 中向量数据库的作用?
答案:
RAG(检索增强生成)中,向量数据库存储知识库文档的 Embedding。用户提问时,先将问题转为向量,从向量数据库中检索相关文档,再将文档作为上下文发给 LLM。
Q41: 什么时候不该用 SQLite?
答案:
- 高并发写入(SQLite 写锁是库级别的)
- 需要远程访问(SQLite 是嵌入式的)
- 数据量超过几十 GB
- 需要多进程写入
Q42: 树形评论怎么实现?
答案:
| 方案 | 写入 | 查询 | 适用 |
|---|---|---|---|
| 邻接表(parent_id) | 递归查询 | 层级浅 | |
| 路径枚举(path) | LIKE 前缀 | 层级深 | |
| 闭包表 | 查询频繁 |
推荐:邻接表 + 路径枚举组合使用。
Q43: 连接池满了怎么办?
答案:
- 排查:是否有连接泄漏(未释放)
- 优化:减少查询耗时,快速归还连接
- 扩容:适当增大连接池
- 降级:设置请求排队超时,超时返回错误
Q44: 先更新 DB 还是先删缓存?
答案:
推荐先更新 DB,再删缓存。 原因:
- 先删缓存再更新 DB:删缓存后、更新 DB 前,其他请求读到旧数据并写入缓存
- 先更新 DB 再删缓存:即使删缓存失败,下次缓存过期后也会读到新数据
Q45: Saga 的编排模式和协调模式有什么区别?
答案:
| 维度 | 编排(Orchestration) | 协调(Choreography) |
|---|---|---|
| 协调方式 | 中心协调者管理 | 各服务通过事件通信 |
| 耦合度 | 协调者知道所有步骤 | 服务间松耦合 |
| 可观测性 | 好(流程集中) | 差(流程分散) |
| 适用 | 流程复杂 | 流程简单(2-3 步) |
Q46: 线上数据库慢了,你会怎么排查?
答案:
按照「定位 → 分析 → 优化 → 验证」四步走:
- 看慢查询日志:开启
slow_query_log,找到耗时最长、执行最频繁的 SQL - SHOW PROCESSLIST:看当前有没有锁等待、长事务
- EXPLAIN 分析:看是否全表扫描、索引是否命中
- 监控指标:Buffer Pool 命中率、磁盘 I/O、连接数
- 针对性优化:加索引、改写 SQL、调参数、加缓存
- 压测验证:优化后在测试环境验证效果
Q47: Redis 的 Hot Key 和 Big Key 分别有什么风险?
答案:
Hot Key 是访问量集中在少数键,可能打满单个分片、网络或 CPU;Big Key 是单个值或集合过大,读取、删除、复制和迁移会形成延迟尖峰。
治理方式不同:
- Hot Key:本地缓存、请求合并、读副本、拆键或业务分片,并防缓存击穿。
- Big Key:限制单值大小、拆分集合、渐进扫描/删除,避免在线使用阻塞式全量命令。
- 两者都要通过命令延迟、流量、键大小采样和分片倾斜监控发现。
不要只根据固定 KB/MB 阈值下结论,应结合实例规格、命令复杂度、网络和延迟 SLO。
Q48: UNION 和 UNION ALL 的区别?
答案:
UNION:合并结果并去重(多一次排序操作)UNION ALL:合并结果不去重(性能更好)
Q49: 为什么用 B+ 树而不是哈希表?
答案:
| 维度 | B+ 树 | 哈希表 |
|---|---|---|
| 等值查询 | ||
| 范围查询 | ✅ 叶子链表支持 | ❌ 不支持 |
| 排序 | ✅ 天然有序 | ❌ 无序 |
| 模糊查询 | ✅ 前缀匹配 | ❌ 不支持 |
数据库多数场景需要范围查询和排序,所以 B+ 树更适合。
Q50: InnoDB 的 Gap Lock 和 Next-Key Lock 解决什么问题?
答案:
Record Lock 锁已有索引记录;Gap Lock 锁索引记录之间的间隙;Next-Key Lock 通常是“记录锁 + 前方间隙锁”的组合。
在可重复读等条件下,它们用于阻止其他事务向查询范围插入新记录,从而支持范围条件的并发一致性。锁的是索引范围,不是抽象业务条件;没有合适索引会扩大扫描和锁范围。
代价是并发插入受阻并更容易死锁。排查时查看执行计划、实际索引、事务隔离级别和锁等待,不要用“加了行锁所以只锁一行”简单判断。
Q51: 字段类型怎么选?
答案:
| 数据 | 推荐类型 | 说明 |
|---|---|---|
| 自增主键 | INT / BIGINT | 大表用 BIGINT |
| 金额 | DECIMAL(10,2) | 绝不用 FLOAT |
| 时间 | DATETIME / TIMESTAMP | TIMESTAMP 占用更小 |
| 布尔 | TINYINT(1) | MySQL 无原生 BOOLEAN |
| 枚举 | ENUM 或 TINYINT | TINYINT 更灵活 |
| UUID | CHAR(36) 或 BINARY(16) | BINARY 性能更好 |
Q52: MongoDB 支持事务吗?
答案:
MongoDB 4.0+ 支持多文档事务,4.2+ 支持分布式事务。但过度使用事务会降低性能,应优先通过文档设计减少事务需求。
Q53: 缓存穿透、击穿、雪崩是什么?怎么解决?
答案:
| 问题 | 本质 | 场景 | 解决方案 |
|---|---|---|---|
| 穿透 | 查询不存在的数据,每次都打到 DB | 恶意攻击、大量无效 ID | 1. 布隆过滤器拦截不存在的 key 2. 缓存空值( set key "" EX 60) |
| 击穿 | 热点 key 过期瞬间大量请求打到 DB | 秒杀商品的缓存过期 | 1. 互斥锁(SET NX,只有一个请求去查 DB)2. 逻辑过期(value 中存过期时间,异步更新) |
| 雪崩 | 大量 key 同时过期,DB 瞬间压力暴增 | 缓存批量设置相同 TTL | 1. 过期时间加随机偏移(TTL + random(0, 300))2. 多级缓存(本地缓存 + Redis) 3. 限流降级 |
// 缓存穿透 — 缓存空值
async function getUser(id: string): Promise<User | null> {
const cached = await redis.get(`user:${id}`);
if (cached === '') return null; // 空值缓存命中,直接返回
if (cached) return JSON.parse(cached);
const user = await db.findUser(id);
if (!user) {
// 缓存空值,短 TTL 防止长期占用
await redis.set(`user:${id}`, '', 'EX', 60);
return null;
}
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
return user;
}
// 缓存击穿 — 互斥锁
async function getHotProduct(id: string): Promise<Product> {
const cached = await redis.get(`product:${id}`);
if (cached) return JSON.parse(cached);
// 获取互斥锁
const lockKey = `lock:product:${id}`;
const locked = await redis.set(lockKey, '1', 'PX', 5000, 'NX');
if (locked) {
try {
const product = await db.findProduct(id);
await redis.set(`product:${id}`, JSON.stringify(product), 'EX', 3600);
return product;
} finally {
await redis.del(lockKey);
}
} else {
// 没拿到锁,短暂等待后重试
await new Promise(r => setTimeout(r, 100));
return getHotProduct(id);
}
}
Q54: Drizzle 有什么独特优势?
答案:
- 零运行时开销:直接生成 SQL
- SQL-like 语法:会写 SQL 就会用 Drizzle
- Bundle 小:适合 Serverless
Q55: 联合索引的列顺序怎么确定?
答案:
遵循「区分度高的列在前」原则:
- 等值条件的列放前面
- 范围条件放后面
- 排序字段放最后
- 区分度高的列优先
Q56: 分库分表的分片键应该怎样选择?
答案:
分片键要同时考虑路由、数据分布和业务查询:
- 高频请求能根据它直接定位分片,避免全库广播。
- 值分布足够均匀,避免热点租户或时间段压垮单分片。
- 生命周期稳定,尽量不发生需要跨分片迁移的修改。
- 与事务、唯一约束、排序分页和关联查询的边界一致。
- 预留扩容与再分片方案,不能只对当前数据量取模。
按用户 ID 分片适合用户域查询,但跨用户统计会变难;按时间分片利于归档,却容易产生最新分片热点。通常要结合全局索引、汇总系统或异步数据平台补足跨分片能力。
Q57: JSONB 和 MongoDB 怎么选?
答案:
- PostgreSQL JSONB:关系型 + 文档的混合,既有 SQL 查询能力又支持灵活 JSON
- MongoDB:纯文档数据库,大量文档操作、水平扩展更方便
少量 JSON 字段 → PostgreSQL;整体数据模型都是文档 → MongoDB。
Q58: 如何回滚迁移?
答案:
- 每个迁移文件都写
down方法 - 生产环境尽量不回滚,而是创建新的迁移修复
- 回滚前确认数据兼容性
Q59: 密码应该怎么存储?
答案:
使用 bcrypt(自动加盐,防彩虹表):
import bcrypt from 'bcrypt';
const hash = await bcrypt.hash(password, 12);
const match = await bcrypt.compare(inputPassword, hash);
永远不存明文,不用 MD5/SHA256(无盐易被破解)。
Q60: ClickHouse 为什么查询这么快?
答案:
- 列式存储:只读需要的列,减少 IO
- 向量化执行:批量处理数据
- 数据压缩:同列数据压缩率极高
- 稀疏索引:分区 + 排序键快速定位
Q61: 分词器是什么?
答案:
分词器将文本切分为词条(Term)。如「React 性能优化」:
- 标准分词器:
react、性、能、优、化(按字切分) - IK 分词器(中文):
react、性能、优化(按语义切分)
Q62: 逻辑备份和物理备份怎么选?
答案:
- 逻辑备份(mysqldump):SQL 可读、跨版本兼容,但慢,适合小数据量(< 50GB)
- 物理备份(xtrabackup):直接拷贝文件,速度快,适合大数据量
Q63: 余弦相似度和欧氏距离怎么选?
答案:
- 余弦相似度:衡量方向相似性,适合文本搜索(推荐)
- 欧氏距离:衡量绝对距离,适合图像搜索
Q64: 边缘数据库的优势?
答案:
- 低延迟:数据在离用户最近的边缘节点
- 离线支持:本地副本可离线工作
- 成本低:减少中心数据库请求
- 简单部署:随应用部署,无需独立服务
Q65: 为什么订单要做快照?
答案:
商品信息(名称、价格)可能随时修改,但历史订单必须保留下单时的数据。所以在 order_items 中冗余商品名和价格,形成快照。
Q66: 连接泄漏怎么排查?
答案:
// 检查活跃连接数
// PostgreSQL
SELECT count(*) FROM pg_stat_activity;
// MySQL
SHOW PROCESSLIST;
常见原因:事务未提交/回滚、未使用 try/finally 释放连接。
Q67: 缓存过期时间怎么设?
答案:
- 变化频繁的数据:短 TTL(1-5 分钟)
- 变化不频繁:长 TTL(1-24 小时)
- 添加随机偏移防止缓存雪崩:
baseTime + random(0, 300)
Q68: 补偿操作失败怎么办?
答案:
- 重试:补偿操作设计为幂等,可安全重试
- 记录:持久化到数据库/日志
- 人工介入:告警通知,人工处理
Q69: InnoDB Buffer Pool 的作用是什么?怎么设置大小?
答案:
Buffer Pool 是 InnoDB 的核心内存缓存,存放数据页和索引页。查询时先在 Buffer Pool 中查找,命中则直接返回(逻辑读),未命中才从磁盘读取(物理读)。
大小设置:
- 专用数据库服务器:物理内存的 60%-80%
- 共享服务器:预留足够内存给操作系统和其他进程
- 监控命中率:目标
> 99%,低于 95% 说明太小
-- 计算命中率
SELECT
(1 - (
(SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_reads')
/
(SELECT VARIABLE_VALUE FROM performance_schema.global_status WHERE VARIABLE_NAME = 'Innodb_buffer_pool_read_requests')
)) * 100 AS hit_rate;
Q70: 什么时候不该用 NoSQL?
答案:
- 需要复杂事务(如转账)
- 需要复杂 JOIN 查询
- 数据一致性要求极高
- 已有成熟的 SQL 团队和基础设施
Q71: SQL 的执行顺序?
答案:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
这就是为什么 WHERE 不能用 SELECT 中的别名,但 ORDER BY 可以。
Q72: RR 级别下如何解决幻读?
答案:
InnoDB 用间隙锁(Gap Lock) 锁住索引之间的间隙,阻止其他事务在间隙中插入新行。