数据存储 - 面试题库
一、基础题 ⭐
Q1. MySQL 中 InnoDB 引擎的索引结构是什么?
答案:InnoDB 使用 B+ 树作为索引结构。B+ 树的特点是所有数据都存储在叶子节点上,叶子节点之间通过双向链表连接,这使得范围查询和排序操作非常高效。非叶子节点只存储键值和指向子节点的指针,不存储实际数据,这样可以在相同内存下存储更多键值,降低树的高度。InnoDB 的索引分为主键索引(聚簇索引)和普通索引(二级索引),二级索引的叶子节点存储的是主键值而非行数据,因此通过二级索引查询需要”回表”。
关联知识点:B+ 树结构、聚簇索引、二级索引、回表查询
Q2. 什么是数据库事务的 ACID 特性?
答案:ACID 是事务的四个核心特性:原子性(Atomicity)指事务中的操作要么全部成功,要么全部失败回滚,通过 undo log 实现;一致性(Consistency)指事务执行前后数据库状态保持一致,满足所有约束条件;隔离性(Isolation)指多个事务并发执行时互不干扰,通过锁和 MVCC 实现;持久性(Durability)指事务提交后对数据的修改是永久性的,即使系统崩溃也不会丢失,通过 redo log 实现。这四个特性共同保证了数据库在复杂场景下的数据正确性。
关联知识点:事务管理、undo log、redo log、MVCC
Q3. MySQL 的事务隔离级别有哪些?分别解决了什么问题?
答案:MySQL 有四种隔离级别:读未提交(Read Uncommitted)最低级别,可能出现脏读、不可重复读、幻读;读已提交(Read Committed)解决了脏读,但仍有不可重复读和幻读问题,Oracle 默认级别;可重复读(Repeatable Read)解决了脏读和不可重复读,配合 MVCC 基本解决幻读,MySQL 默认级别;串行化(Serializable)最高级别,通过加锁完全解决所有并发问题,但性能最低。MySQL 默认使用可重复读级别,在实际应用中已能满足大多数场景需求。
关联知识点:脏读、不可重复读、幻读、MVCC、并发控制
Q4. 什么是数据库的聚簇索引和非聚簇索引?
答案:聚簇索引(Clustered Index)的叶子节点存储的是完整的行数据,数据行的物理存储顺序与索引顺序一致,InnoDB 中主键就是聚簇索引。一张表只能有一个聚簇索引。非聚簇索引(Secondary Index)也叫二级索引,其叶子节点存储的是索引列的值和对应的主键值,而不是完整的行数据。通过非聚簇索引查询时,先找到主键值,再通过主键索引查找完整数据,这个过程称为”回表”。覆盖索引可以避免回表,提高查询效率。
关联知识点:聚簇索引、二级索引、回表、覆盖索引
Q5. Redis 支持哪些基本数据类型?
答案:Redis 支持五种基本数据类型:字符串(String)是最基础的类型,可以存储字符串、整数或浮点数,支持 SET/GET/INCR 等操作;列表(List)是双向链表结构,支持从两端插入和弹出,常用于消息队列;集合(Set)是无序且不重复的元素集合,支持交集、并集、差集运算;有序集合(Sorted Set/ZSet)在集合基础上增加了分数(score),元素按分数排序,常用于排行榜;哈希(Hash)是键值对集合,适合存储对象,可以单独操作某个字段。
关联知识点:Redis 数据结构、应用场景
Q6. 什么是 Redis 的持久化机制?
答案:Redis 提供两种持久化方式:RDB(Redis Database)是在指定时间间隔内生成数据集快照,通过 fork 子进程将内存数据写入临时文件后替换旧文件,优点是文件紧凑恢复快,缺点是可能丢失最后一次快照后的数据;AOF(Append Only File)记录每个写操作命令,重启时重放命令恢复数据,通过 appendfsync 配置同步策略(always、everysec、no),everysec 是性能和安全的平衡点。Redis 4.0+ 支持混合持久化,结合两者优点,先写 RDB 快照再追加 AOF 增量。
关联知识点:RDB、AOF、数据恢复、fork 机制
Q7. 什么是 ORM 框架?它的作用是什么?
答案:ORM(Object-Relational Mapping)是对象关系映射框架,用于在面向对象的编程语言和关系型数据库之间建立映射关系。它的核心作用包括:将数据库表映射为类,表记录映射为对象,字段映射为对象属性;提供链式 API 替代手写 SQL,提高开发效率;自动处理 SQL 注入防护、类型转换、连接管理等底层细节;支持多种数据库方言,便于数据库迁移。常见的 ORM 框架有 Java 的 MyBatis/Hibernate、Python 的 SQLAlchemy、Go 的 GORM 等。缺点是复杂查询性能可能不如手写 SQL,且存在一定的学习成本。
关联知识点:对象关系映射、SQL 注入、数据库抽象层
Q8. 什么是缓存穿透?如何解决?
答案:缓存穿透是指查询一个不存在的数据,缓存中没有,数据库中也没有,导致每次请求都打到数据库。常见于恶意攻击或数据 id 被非法篡改的场景。解决方案有三种:布隆过滤器(Bloom Filter)在请求到达缓存前先判断数据是否存在,不存在则直接返回;缓存空值将查询结果为空的记录也缓存起来,设置较短的过期时间(如 30 秒);参数校验在接口层对请求参数进行合法性校验,拦截明显不合法的请求。布隆过滤器方案性能最优但存在误判率,缓存空值方案最简单但可能浪费内存。
关联知识点:布隆过滤器、缓存空值、安全防护
Q9. 什么是缓存击穿?如何解决?
答案:缓存击穿是指某个热点 key 在过期瞬间,大量并发请求同时到达,导致请求全部打到数据库,造成数据库压力骤增。与缓存穿透不同,击穿针对的是真实存在的数据。解决方案主要有两种:互斥锁(Mutex Lock)在缓存失效时,只允许一个线程去查询数据库并重建缓存,其他线程等待后重新从缓存获取,使用 SETNX 实现分布式锁;逻辑过期在数据中不设置物理过期时间,而是在值内部维护一个逻辑过期时间戳,发现过期后异步触发重建,当前请求返回旧数据。互斥锁保证数据一致性但影响性能,逻辑过期性能好但可能返回短暂过期数据。
关联知识点:分布式锁、SETNX、热点数据
Q10. 什么是缓存雪崩?如何解决?
答案:缓存雪崩是指大量缓存在同一时间集中过期,或者 Redis 服务宕机,导致大量请求直接打到数据库,造成数据库压力过大甚至崩溃。解决方案包括:过期时间加随机值在设置缓存过期时间时增加一个随机值(如 1-5 分钟),避免大量 key 同时过期;多级缓存架构引入本地缓存(如 Caffeine)作为一级缓存,Redis 作为二级缓存,降低单点故障影响;高可用部署使用 Redis Sentinel 或 Redis Cluster 保证 Redis 服务可用性;限流降级在数据库层配置限流策略,超过阈值时触发降级,保护数据库不被压垮。
关联知识点:高可用、限流降级、多级缓存
Q11. MyBatis 中 #{} 和 ${} 的区别是什么?
答案:#{} 是预编译参数占位符,MyBatis 会将其转换为 SQL 预编译语句的 ? 占位符,通过 PreparedStatement 设置参数值,能有效防止 SQL 注入,适用于大多数参数传递场景。${} 是字符串替换占位符,MyBatis 会直接将变量值拼接到 SQL 语句中,不做任何转义或预编译处理,存在 SQL 注入风险,仅适用于动态表名、列名等无法使用预编译的场景。例如 ORDER BY ${columnName} 中列名不能用 #{} 因为预编译会将列名当作字符串值而非列标识。开发中应优先使用 #{},仅在确实需要动态 SQL 片段时才使用 ${}。
关联知识点:SQL 注入、预编译、动态 SQL
Q12. Redis 的过期策略是什么?
答案:Redis 采用惰性删除和定期删除相结合的策略。惰性删除是指当客户端访问某个 key 时,检查其是否过期,如果过期则删除并返回空,这种方式节省 CPU 但可能导致过期 key 占用内存;定期删除是指 Redis 每隔一段时间(默认 100ms)随机抽取一定数量的设置了过期时间的 key 进行检查,删除其中已过期的 key,通过控制采样数量和执行时间避免阻塞主线程。两种策略配合使用,既保证了内存的有效回收,又避免了过多的 CPU 开销。当内存达到上限时,还会根据配置的淘汰策略(如 LRU、LFU)进行额外淘汰。
关联知识点:内存管理、LRU、LFU、淘汰策略
二、进阶题 ⭐⭐
Q13. MySQL 中什么是覆盖索引?为什么它能提高查询效率?
答案:覆盖索引(Covering Index)是指一个索引包含了查询所需的所有字段,查询可以直接从索引中获取数据,无需回表查询聚簇索引。例如表中有联合索引 (name, age),执行 SELECT name, age FROM user WHERE name = ‘zhangsan’ 时,索引已经包含了所有需要的字段,这就是覆盖索引。覆盖索引能提高效率的原因:减少 IO 操作,避免了从聚簇索引中读取数据行的随机 IO;索引文件通常比数据文件小,可以加载更多索引到内存;对于 InnoDB,二级索引的叶子节点比数据行紧凑得多。使用 EXPLAIN 时,Extra 列显示 “Using index” 即表示使用了覆盖索引。
关联知识点:二级索引、回表、EXPLAIN、索引优化
Q14. 什么是 MySQL 的 MVCC?它是如何实现的?
答案:MVCC(Multi-Version Concurrency Control)是多版本并发控制机制,用于在读写并发时不加锁也能保证数据一致性。InnoDB 通过 undo log 和隐藏字段实现 MVCC:每行数据有两个隐藏字段,trx_id 记录最后一次修改的事务 ID,roll_pointer 指向 undo log 中的历史版本链。每个事务启动时生成一个 ReadView,包含当前活跃事务 ID 列表。查询时根据 ReadView 判断哪个版本对当前事务可见:如果行的 trx_id 在 ReadView 中,说明是活跃事务修改的,不可见;如果小于最小活跃事务 ID,说明已提交,可见。MVCC 使得读操作不需要加锁,实现了读写不阻塞,大幅提高了并发性能。
关联知识点:ReadView、undo log、事务隔离、并发控制
Q15. MySQL 中什么是慢查询?如何分析和优化?
答案:慢查询是指执行时间超过指定阈值的 SQL 语句。MySQL 通过慢查询日志(slow_query_log)记录这些 SQL,通过 long_query_time 参数设置阈值(默认 10 秒)。分析慢查询的步骤:开启慢查询日志,收集慢 SQL;使用 EXPLAIN 分析执行计划,关注 type(访问类型)、key(使用的索引)、rows(扫描行数)、Extra(额外信息)等字段;检查是否全表扫描(type=ALL)、是否使用了索引、是否有 filesort 或 using temporary。优化方法包括:添加合适的索引,避免 SELECT *,优化 JOIN 条件,拆分复杂查询,调整表结构。对于已添加索引但仍慢的查询,检查是否出现索引失效的情况(如对索引列使用函数、类型转换、左模糊匹配等)。
关联知识点:慢查询日志、EXPLAIN、索引失效、SQL 优化
Q16. Redis 的内存淘汰策略有哪些?
答案:当 Redis 内存达到 maxmemory 限制时,会根据配置的淘汰策略处理数据。共有八种策略:noeviction 默认策略,写操作返回错误,不淘汰任何数据;allkeys-lru 从所有 key 中使用 LRU 算法淘汰最近最少使用的 key,最常用的策略;volatile-lru 仅从设置了过期时间的 key 中使用 LRU 淘汰;allkeys-lfu 从所有 key 中使用 LFU 算法淘汰访问频率最低的 key;volatile-lfu 仅从设置了过期时间的 key 中使用 LFU 淘汰;allkeys-random 随机淘汰所有 key;volatile-random 随机淘汰设置了过期时间的 key;volatile-ttl 淘汰 TTL 值最小的 key,即即将过期的 key。LRU 是近似算法,通过采样实现,LFU 适合缓存热点数据场景。
关联知识点:LRU、LFU、内存管理、maxmemory
Q17. 什么是数据库的死锁?如何检测和避免?
答案:死锁是指两个或多个事务在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉,这些事务都将无法推进。例如事务 A 持有行 1 的锁等待行 2,事务 B 持有行 2 的锁等待行 1。MySQL InnoDB 通过 wait-for graph 检测死锁,发现后立即回滚代价较小的事务(通常是 undo log 量最少的那个)。避免死锁的方法:保持固定的加锁顺序,所有事务按相同顺序访问资源;一次性申请所有需要的锁,减少持有锁等待的时间;使用较低的隔离级别,减少锁的范围;设置锁等待超时(innodb_lock_wait_timeout),超时后自动回滚;合理设计索引,避免锁升级导致锁范围扩大。发生死锁时应记录日志以便后续分析。
关联知识点:锁机制、wait-for graph、innodb_lock_wait_timeout
Q18. Redis 中如何实现分布式锁?有什么注意事项?
答案:Redis 实现分布式锁的核心命令是 SET key value NX PX milliseconds,NX 保证只有一个客户端能设置成功(互斥),PX 设置过期时间防止死锁。释放锁时使用 Lua 脚本保证判断和删除的原子性:先 GET 判断锁是否属于自己,再 DEL 删除。注意事项:过期时间设置要合理,需大于业务执行时间,但也不能过长;锁的 value 应使用唯一标识(如 UUID),释放时验证归属;主从切换可能导致锁丢失,因为 Redis 异步复制,主节点设置锁后宕机,从节点可能没有该锁;高安全场景建议使用 Redlock 算法,在多个独立 Redis 节点上获取锁,提高可靠性;对于复杂场景可考虑使用 ZooKeeper 或 etcd 等强一致性方案。
关联知识点:SETNX、Lua 脚本、Redlock、主从复制
Q19. ORM 框架中的 N+1 查询问题是什么?如何解决?
答案:N+1 查询问题是 ORM 框架中常见的性能问题。当查询一个列表(1 次查询),然后遍历列表中的每个对象并访问其关联对象时(N 次查询),就会产生 N+1 次数据库查询。例如查询 100 个订单,然后遍历每个订单获取其用户信息,会产生 1 + 100 = 101 次查询。解决方案包括:预加载(Eager Loading)使用 JOIN 或 IN 查询一次性加载关联数据,如 SQLAlchemy 的 joinedload 或 selectinload;批量加载先收集所有关联 ID,用一次 IN 查询批量获取关联对象,再在内存中组装;投影查询只查询需要的字段,避免加载完整对象;延迟加载优化对于确实需要懒加载的场景,使用批量懒加载(batch lazy loading)减少查询次数。关键是在 ORM 层面理解查询行为,必要时查看生成的 SQL。
关联知识点:关联查询、预加载、批量加载、性能优化
Q20. 什么是数据库的索引下推(ICP)?
答案:索引条件下推(Index Condition Pushdown, ICP)是 MySQL 5.6 引入的优化特性。在没有 ICP 时,存储引擎根据索引定位到数据行后,将整行数据返回给 Server 层,由 Server 层判断 WHERE 条件是否满足;启用 ICP 后,存储引擎在索引遍历过程中直接对索引中包含的字段进行 WHERE 条件判断,只有满足条件的行才回表获取完整数据。这样可以减少回表次数和 Server 层与存储引擎层之间的数据传输。例如联合索引 (name, age),查询 WHERE name LIKE ‘张%’ AND age > 20,ICP 可以在索引层面先过滤 age 条件,减少不必要的回表。ICP 适用于二级索引的范围查询场景,EXPLAIN 的 Extra 列显示 “Using index condition” 表示使用了 ICP。
关联知识点:索引优化、存储引擎架构、EXPLAIN
Q21. Redis 的发布订阅(Pub/Sub)模式有什么特点?与消息队列有什么区别?
答案:Redis 的 Pub/Sub 是一种消息通信模式:发布者(PUBLISH)发送消息到指定频道,订阅者(SUBSCRIBE)接收该频道的消息。特点是消息即时投递,不持久化,订阅者只能收到订阅之后的消息,无法获取历史消息;如果订阅者离线,消息会丢失,没有消费确认机制;支持模式匹配订阅(PSUBSCRIBE)。与消息队列(如 RabbitMQ、Kafka)的区别:Pub/Sub 是广播模式,消息被所有订阅者接收,而消息队列是点对点模式,消息只被一个消费者消费;Pub/Sub 没有消息持久化和确认机制,可靠性低;消息队列支持消息确认、重试、死信队列等高级特性。Redis Pub/Sub 适合实时性要求高、允许丢失的场景,如实时通知、聊天室,不适合需要可靠投递的业务场景。
关联知识点:消息队列、广播模式、消息可靠性
Q22. 什么是数据库的读写分离?如何实现?
答案:读写分离是将数据库的读操作和写操作分别路由到不同的数据库实例,主库负责写操作,从库负责读操作,通过主从复制同步数据。目的是分担主库的读压力,提高系统的整体吞吐能力。实现方式有三种:客户端代理应用代码中根据 SQL 类型自动路由,如 ShardingSphere-JDBC,优点是灵活但侵入业务代码;中间件代理部署独立的代理层(如 MyCat、ProxySQL、ShardingSphere-Proxy),应用连接代理层,代理层负责路由,对业务透明;ORM 框架集成部分 ORM 框架支持读写分离配置,如 SQLAlchemy 的绑定机制。读写分离需要注意主从延迟问题,刚写入的数据可能无法立即读到,对于强一致性读请求需要路由到主库,或使用延迟感知路由策略。
关联知识点:主从复制、主从延迟、中间件、ShardingSphere
Q23. MyBatis 的一级缓存和二级缓存有什么区别?
答案:MyBatis 提供两级缓存机制。一级缓存是 SqlSession 级别的缓存,默认开启,同一个 SqlSession 中执行相同的 SQL 查询时,第一次从数据库查询并缓存结果,第二次直接从缓存获取,不同 SqlSession 之间缓存不共享,执行增删改操作或手动 clearCache 会清空一级缓存;二级缓存是 Mapper 级别的缓存,需要手动开启(在 mapper.xml 中配置
关联知识点:SqlSession、缓存一致性、分布式缓存
Q24. Redis 的 Pipeline 是什么?有什么使用场景?
答案:Pipeline(管道)是 Redis 提供的一种批量操作机制,允许客户端将多个命令打包后一次性发送给 Redis,Redis 执行完毕后一次性返回所有结果。正常情况下每个命令都需要一次网络往返(RTT),使用 Pipeline 可以将 N 个命令的网络往返减少到 1 次,大幅提高批量操作的效率。使用场景包括:批量写入或读取大量数据;初始化数据时批量设置缓存;需要原子性地执行多个操作但不需要事务的场景。注意事项:Pipeline 不保证原子性,中途失败的命令不会影响其他命令的执行;打包的命令数量不宜过多,建议分批(如每批 1000 条),避免单次网络包过大导致内存溢出;Pipeline 与事务(MULTI/EXEC)可以结合使用,在事务中批量执行命令。
关联知识点:网络 RTT、批量操作、原子性
三、高级题 ⭐⭐⭐
Q25. MySQL 中什么是索引失效?列举常见的索引失效场景。
答案:索引失效是指虽然建立了索引,但查询时 MySQL 优化器选择全表扫描而非索引扫描的情况。常见场景包括:对索引列使用函数或表达式,如 WHERE YEAR(create_time) = 2024,应改为范围查询;隐式类型转换,如字符串字段不加引号 WHERE phone = 13800138000,MySQL 会转换类型导致索引失效;左模糊匹配 LIKE ‘%value’,只有右模糊 LIKE ‘value%’ 能使用索引;使用 OR 条件时如果 OR 两边的字段不都有索引,会全表扫描;联合索引不满足最左前缀原则,如索引 (a, b, c) 查询 WHERE b = 1 无法使用;范围查询右边的列无法使用索引,如 WHERE a > 1 AND b = 2,b 无法使用索引;使用 != 或 NOT IN 通常不走索引;数据区分度低时优化器可能选择全表扫描。
关联知识点:最左前缀原则、查询优化、执行计划
Q26. MySQL 的 redo log、undo log、binlog 分别是什么?有什么区别?
答案:三种日志各司其职:redo log(重做日志)是 InnoDB 引擎层的物理日志,记录数据页的物理修改,用于崩溃恢复(crash-safe),采用循环写入方式,大小固定,保证事务的持久性;undo log(回滚日志)是 InnoDB 引擎层的逻辑日志,记录数据修改前的值,用于事务回滚和 MVCC 多版本控制,保证事务的原子性;binlog(归档日志)是 MySQL Server 层的逻辑日志,记录所有修改数据的 SQL 语句或行变化,用于主从复制和数据恢复,采用追加写入方式,文件达到上限后自动切换。执行流程:事务提交时先写 redo log(prepare 状态),再写 binlog,最后将 redo log 设为 commit 状态,通过两阶段提交保证两者一致性。
关联知识点:两阶段提交、崩溃恢复、主从复制、WAL 机制
Q27. 什么是 Redis 的 BigKey 问题?如何发现和解决?
答案:BigKey 是指 Redis 中某个 key 对应的 value 非常大(如 String 类型超过 10KB,或 Hash/List/Set/ZSet 包含数百万个元素)。危害包括:内存分布不均,单个 key 占用过多内存;操作阻塞,DEL、EXPIRE 等命令执行时间长,阻塞主线程影响其他请求;网络拥塞,传输大 value 占用大量带宽;主从同步延迟,大 key 同步耗时长。发现方法:使用 redis-cli –bigkeys 扫描;通过 INFO keyspace 分析内存分布;使用 MEMORY USAGE 命令估算 key 大小。解决方案:拆分大 key,将大 Hash 拆分为多个小 Hash(如 hash:1, hash:2);对于集合类型,使用分段存储;删除时使用 UNLINK 异步删除,避免阻塞;定期清理过期数据;在业务层面限制单个 key 的数据量。
关联知识点:内存管理、UNLINK、主从同步、性能优化
Q28. 什么是数据库的分库分表?有哪些策略?
答案:分库分表是将单个大数据库或大表拆分为多个小数据库或表的技术,用于解决单库存储瓶颈和性能问题。分库是将数据分散到多个数据库实例,解决单库连接数和 IO 瓶颈;分表是将单张表的数据拆分到多张表,解决单表数据量过大导致的性能下降。分片策略包括:范围分片按 ID 范围划分(如 1-1000 在表 1,1001-2000 在表 2),优点是简单,缺点是数据分布可能不均;哈希分片对分片键取模(如 user_id % 4),数据分布均匀但扩容需要数据迁移;一致性哈希分片减少扩容时的数据迁移量;时间分片按时间维度划分(如按月分表),适合时序数据。分库分表带来的问题包括:跨库 JOIN 困难、分布式事务复杂、全局唯一 ID 生成、分页查询性能差等,需要配合中间件解决。
关联知识点:分片策略、一致性哈希、全局 ID、中间件
Q29. Redis Cluster 的工作原理是什么?
答案:Redis Cluster 是 Redis 官方的分布式解决方案,采用去中心化的架构。核心原理:将 16384 个哈希槽(hash slot)分配到所有节点,每个节点负责一部分槽位;客户端通过 CRC16(key) % 16384 计算 key 对应的槽位,然后路由到对应节点;节点之间通过 Gossip 协议通信,维护集群元数据和节点状态;客户端首次连接时获取集群拓扑,后续直接连接目标节点,如果连接错误节点会返回 MOVED 重定向。数据迁移时通过 MIGRATE 命令在节点间迁移槽位和对应的 key。Redis Cluster 不支持多 key 操作(如 MGET、SUNION),除非这些 key 通过 hash tag 落在同一槽位。每个主节点可以配置从节点,实现高可用,主节点宕机时从节点自动选举升级。
关联知识点:哈希槽、Gossip 协议、MOVED 重定向、hash tag
Q30. ORM 框架中如何处理数据库迁移(Migration)?
答案:数据库迁移是管理数据库 schema 变更的机制,通过版本化的迁移文件记录每次结构变更。工作流程:创建迁移文件包含 up(应用变更)和 down(回滚变更)两个方法,如添加表、修改列等;执行迁移时框架按版本号顺序执行 up 方法,并在迁移记录表中记录已执行的版本;回滚时执行对应版本的 down 方法。主流方案:Flyway 基于纯 SQL 脚本,按文件命名顺序执行,简单直观;Liquibase 支持 XML/YAML/JSON/SQL 多种格式,功能更丰富;ORM 内置如 SQLAlchemy 的 Alembic、Django 的 migrations、Rails 的 migration。最佳实践:迁移文件一旦提交不应修改,需要变更应创建新的迁移文件;迁移应幂等可重复执行;生产环境迁移前备份数据,在低峰期执行;大表结构变更(如加索引)使用 ONLINE DDL 避免锁表。
关联知识点:Flyway、Liquibase、Alembic、ONLINE DDL
Q31. 什么是缓存与数据库的双写一致性问题?有哪些解决方案?
答案:双写一致性是指同时使用缓存和数据库时,如何保证两者的数据保持一致。常见场景是先更新数据库再更新缓存,但并发情况下可能出现脏数据。解决方案包括:先删缓存再更新数据库简单但有并发问题,更新数据库过程中其他线程可能读到旧数据并写入缓存;先更新数据库再删缓存(Cache Aside Pattern)推荐方案,删除缓存后下次查询会回源加载最新数据,但仍存在极端并发情况下读到旧数据;延迟双删先删缓存,更新数据库,再延迟一段时间(大于主从延迟)后再次删缓存,解决主从架构下的并发问题;订阅 binlog 通过 Canal 等工具订阅数据库 binlog,异步更新或删除缓存,保证最终一致性,对业务无侵入;设置缓存过期时间作为兜底,即使出现不一致也会在过期后自动修复。没有完美的强一致性方案,需根据业务容忍度选择。
关联知识点:Cache Aside、Canal、binlog 订阅、最终一致性
Q32. 什么是数据库的分页优化?大数据量下如何优化分页查询?
答案:分页查询 LIMIT offset, size 在 offset 很大时性能很差,因为 MySQL 需要扫描并丢弃前 offset 条数据。优化方案包括:延迟关联先通过覆盖索引查询出主键 ID,再通过主键 JOIN 回原表获取完整数据,如 SELECT * FROM table t INNER JOIN (SELECT id FROM table LIMIT 100000, 10) tmp ON t.id = tmp.id,避免回表大量数据;游标分页记住上一页最后一条记录的 ID,下一页查询 WHERE id > last_id LIMIT size,性能稳定但不支持跳页;业务层面限制最大页数,如只允许查看前 100 页,超过提示用户精确搜索;记录总数缓存将 COUNT(*) 结果缓存,减少每次分页都统计总数;搜索引擎对于复杂搜索和分页场景,考虑使用 Elasticsearch 等搜索引擎,分页性能更好。选择方案需结合业务场景,如是否需要跳页、数据量级等。
关联知识点:覆盖索引、游标分页、Elasticsearch、延迟关联
Q33. Redis 中 ZSet 的底层实现原理是什么?
答案:ZSet(有序集合)的底层使用两种数据结构:跳表(SkipList)和压缩列表(ziplist,Redis 7.0 后为 listpack)。当元素数量较少(默认少于 128 个)且所有元素长度小于 64 字节时,使用 ziplist 存储以节省内存;满足任一条件时转换为 skiplist。跳表是一种多层链表结构,每层都是有序链表,最底层包含所有元素,上层是下层的索引,通过逐层查找实现 O(log N) 的复杂度。跳表相比平衡树的优势:实现简单,不需要旋转等复杂操作;范围查询效率高,找到起点后直接遍历底层链表;并发友好,易于实现并发控制。ZSet 的每个元素关联一个 score(双精度浮点数),按 score 排序,score 相同时按元素字典序排序。常见应用包括排行榜、延迟队列、范围查询等。
关联知识点:跳表、ziplist、listpack、范围查询
Q34. 什么是分布式事务?常见的解决方案有哪些?
答案:分布式事务是指跨越多个数据库或服务的事务,需要保证多个数据源的操作要么全部成功,要么全部失败。常见解决方案:2PC(两阶段提交)协调者先询问所有参与者是否可以提交(Prepare 阶段),全部同意后才通知提交(Commit 阶段),强一致性但性能差、阻塞性强;TCC(Try-Confirm-Cancel)业务层面实现三个接口,Try 预留资源,Confirm 确认执行,Cancel 取消回滚,性能好但侵入性强,每个业务需实现三个方法;本地消息表在本地事务中写入消息表,定时任务扫描消息并发送到 MQ,消费者处理成功后回调确认,实现最终一致性;可靠消息服务使用 RocketMQ 等支持事务消息的中间件,先发送半消息,本地事务成功后确认消息,失败则回滚消息;Saga 将长事务拆分为多个本地事务,每个事务有对应的补偿操作,失败时依次执行补偿。选择方案需权衡一致性要求和性能。
关联知识点:2PC、TCC、RocketMQ 事务消息、最终一致性、Saga
Q35. 什么是数据库的在线 DDL?MySQL 如何实现?
答案:在线 DDL(Online DDL)是指在执行表结构变更(如添加索引、修改列类型)时不阻塞 DML 操作(INSERT、UPDATE、DELETE)。MySQL 5.6+ 引入,通过 ALGORITHM 和 LOCK 参数控制。实现原理:创建新的临时表结构,在 DDL 执行过程中将原表的 DML 操作记录到在线日志(online log)中,DDL 完成后将日志中的变更应用到新表,最后切换表名。支持三种算法:INPLACE 在原表上直接修改,避免拷贝数据,支持大多数操作;COPY 拷贝数据到新表,阻塞 DML,旧版本的方式;INSTANT 仅修改元数据,瞬间完成,如添加列(MySQL 8.0.12+)。LOCK 参数控制并发级别:DEFAULT 最小锁定,NONE 不阻塞读写和 DDL,SHARED 阻塞写入但不阻塞读取,EXCLUSIVE 完全阻塞。大表 DDL 建议使用 pt-online-schema-change 或 gh-ost 工具,通过触发器或 binlog 实现无锁变更。
关联知识点:INPLACE、pt-online-schema-change、gh-ost、元数据锁