D2 · 事务、MVCC 与 Redis
本日安排:8 小时净学习。休息另计,算法可独立安排。
- 2.5 小时:DB04–DB08,事务与快照。
- 2.5 小时:RD01–RD08,先掌握 P0。
- 1 小时:画缓存回填与锁过期时间线。
- 1 小时:复述 AG04/AG05。
- 1 小时:基础模拟问答与纠错。
事务隔离与版本可见性
Section titled “事务隔离与版本可见性”面试里说 ACID,最容易失真的一项是 Consistency。数据库能执行约束、唯一键和事务边界;“订单只能退款一次”“库存不能小于零”这类业务规则,仍需由数据模型和写入条件表达。Isolation 说明并发事务能观察到什么。这里固定讨论 MySQL 8.4 的 InnoDB,避免把某个实现的细节说成所有数据库的通则。InnoDB 默认隔离级别是 Repeatable Read(RR),也提供 Read Uncommitted、Read Committed(RC)和 Serializable。[1]
设事务 T1 查询订单行。普通 SELECT 是一致性读:InnoDB 根据 read view 和行的版本信息选择可见版本;需要旧值时,undo 记录可用于重建历史版本。RR 下,同一事务普通一致性读通常使用第一次一致性读建立的快照;RC 则每次一致性读取得新快照。[1][2][5] 这句话有范围:不是事务开始时数据库就替每条语句冻结了全库,也不意味着本事务的每种读都返回同一时间点的数据。
图中同一行在 T1 快照之后被 T2 改写。T1 的普通读按快照取旧版本;T1 的 SELECT ... FOR UPDATE 是锁定读,读取可用于加锁的最新状态并取得行锁。把两者混在同一个 RR 事务里,可能出现看起来互相矛盾的结果:普通读按旧 read view 返回,锁定读却按当前状态处理。MySQL 文档因此提醒谨慎混用两种读法。[1][2][3]
“RR 就完全没有幻读”也过于绝对。锁定读、UPDATE/DELETE 对唯一索引点查与范围扫描的加锁行为不同;范围查询可能使用 gap lock 或 next-key lock 限制插入,而锁的范围还受执行计划和索引影响。不要只看 SQL 的 WHERE 条件就断言锁了几行。[1] 面试答题时先说明读类型,再说隔离级别、索引和事务边界。
MVCC 降低了普通读与写之间的阻塞,但不是“没有锁”。写写冲突仍要协调,锁定读会获取记录锁,外键或唯一键检查也可能影响并发。旧版本要等不再被任何活动 read view 需要后才能清理;长事务可能让较旧版本继续存活并增加 undo 清理压力。[2][3][5] 排查线上延迟时,不能只看 SQL 执行本身,还要查看事务是否长期未提交、连接是否被占用,以及等待的是行锁还是连接池资源。读到了旧快照不一定是脏读,等待锁也不一定表示隔离级别配置错误。
库存不超卖:把条件与修改放在一个原子写入里
Section titled “库存不超卖:把条件与修改放在一个原子写入里”假设 sku-7 还剩 1 件。两个请求都先 SELECT available,都读到 1,然后分别无条件写 0,结果可能是两个请求都认为扣减成功。事务并不自动修复应用拆开的“先检查、后写入”逻辑。
单行库存可以把条件放在 UPDATE 中:
-- 示意 SQL:展示条件更新边界,不代表已连接数据库实测UPDATE inventorySET available = available - 1WHERE sku_id = 'sku-7' AND available >= 1;应用检查受影响行数:1 行表示本次满足条件并完成扣减;0 行表示不存在该 SKU 或库存条件不成立,需要按实际业务区分。订单记录等相关写入应放在同一个本地事务中:扣减失败则不创建成功订单;提交失败也不能向调用者报告成功。若需要先读取并根据复杂条件决定,可在事务内锁定目标行后再校验和修改;锁定读解决的是并发访问,不替代业务校验。[3]
多个库存行或多个账户一起修改时,仍要考虑死锁。事务越长、锁住的记录越多、不同请求拿锁顺序越不一致,冲突越难控制。常见做法是缩短事务、按稳定顺序访问资源、保证过滤条件有合适索引,并记录死锁信息。若数据库回滚一个死锁牺牲事务,应用应按业务允许重试整个事务;只重发最后一条 SQL,可能丢掉原本的决策上下文。
Agent 场景尤其不应在“查完订单—等待模型—等人工批准”的几分钟内占着数据库事务。保存必要的业务状态后释放连接;执行退款前重新读取授权、订单版本和可退款金额,并用稳定的业务幂等标识处理重复请求。模型生成的旧结论不是当前数据库事实。
Redo、Undo、Binlog:恢复、版本与复制各有职责
Section titled “Redo、Undo、Binlog:恢复、版本与复制各有职责”设事务把余额从 50 改为 30。Undo 保存回滚所需的信息,也让并发一致性读能重建旧版本;redo 记录 InnoDB 修改,崩溃恢复时可重放未能完整写入数据文件的修改;binlog 是 MySQL 层的变更日志,常用于复制与基于备份的时间点恢复。[4][5][6] 它们不是三份等价的“操作日志”:redo 面向存储引擎恢复,undo 面向回滚和旧版本,binlog 服务于服务器层的变更记录与复制。
数据页可以晚于事务提交写入磁盘,因为崩溃恢复能够按 redo 处理尚未完成的数据页更新;但“COMMIT 返回”也不等于抵抗所有磁盘、主机和副本故障。持久性取决于 redo/binlog 的刷盘配置、操作系统和存储设备是否兑现 flush、复制确认条件及故障切换策略。比如 innodb_flush_log_at_trx_commit 会影响 redo 刷盘策略,但单看一项参数不能替系统定义完整的故障保证。备份也必须独立保留并演练恢复。面试回答应说清保证覆盖哪类故障,而不是承诺“绝不丢”。
连接池解决的是复用连接并约束并发,不会增加数据库本身的处理能力。若每个服务实例允许 80 个连接,部署 6 个实例就可能合计申请 480 个;再加上定时任务、管理工具和其他服务,要按数据库连接预算设置上限、等待队列与超时。连接池等待时间、活跃连接和事务时长是容量判断的重要数据。长任务不要长时间占连接,读写分离则要考虑副本延迟;刚提交的写后读、权限撤销与退款校验不能无条件路由到滞后副本。
Redis 缓存:失效顺序仍可能产生旧回填
Section titled “Redis 缓存:失效顺序仍可能产生旧回填”Cache-aside 常见流程是:读缓存,miss 后读数据库再回填;写入时先提交数据库,再删除缓存。这个顺序能处理不少情况,但不是强一致协议。考虑三个事件:R 从数据库读取到 v1;W 提交 v2 并删除缓存;R 把先前拿到的 v1 写回缓存。缓存重新出现旧值。图中每条线只表达先后关系,不表示执行耗时或实测数据。
TTL 能限制缓存保留时间,但不能阻止过期前的陈旧读取;固定延迟的“双删”也无法证明慢请求、进程暂停和消息延迟都已覆盖。允许短暂陈旧的展示类读取,可以结合短 TTL、可靠失效事件、按版本命名的缓存键或回填前检查版本来缩小风险。涉及金额、权限、退款资格的最终决定,应回到权威数据库并校验当前状态。缓存只是派生数据,就不能成为唯一账本。
几个经常混淆的词:缓存穿透是大量查询不存在的数据,导致请求持续落到源站;可用输入校验、有限期负缓存或存在性过滤来降低回源,但负缓存可能暂时遮住刚创建的对象。缓存击穿是一个热点键失效后许多并发请求同时回源,可做请求合并、互斥重建和有界等待。缓存雪崩指很多键同期失效或缓存整体不可用,可通过打散过期、预热、限流和容量隔离来减小冲击。增加 worker 可能只会把更多并发压到数据库。热 key 与大 key 也是两类问题:一个是访问集中,一个是单个对象或集合过大。对于大 key,还应限制单次返回成员数量和删除工作量,避免一次命令占用过长执行时间;对于热 key,应确认分片路由是否让热点集中在单个节点。
Redis 的 key 可以设过期时间,但逻辑过期与物理内存回收不应当作同一瞬间;Redis 还按 maxmemory 和淘汰策略处理内存压力。若任务状态只放在可能淘汰的 Redis key 中,就要说明恢复来源和淘汰配置,不能把缓存直接当任务账本。[11][12]
Redis 持久化、原子操作与锁的边界
Section titled “Redis 持久化、原子操作与锁的边界”RDB 是某时点数据集快照;AOF 记录写操作以便重建状态。两者的恢复点和写入成本不同,不能用“AOF 开了”代替持久性说明。AOF 的 fsync 策略会影响性能和断电后的潜在丢失窗口;副本复制进度则是另一项条件。即使本地文件已持久,故障切换到尚未收到写入的副本仍有数据风险。[7][10]
Pipeline 把命令批量发送,减少网络往返,不因此形成原子事务。MULTI/EXEC 排队执行一组命令;WATCH 监视键变化,发现竞争时由客户端重新读取和决策;Lua 脚本可把短小的多步判断放在 Redis 服务端原子执行。[8] 这些机制不提供跨数据库或 HTTP 服务的 ACID 事务。执行中的错误也不应被描述成数据库式自动回滚;脚本和事务都应限制工作量,避免长逻辑阻塞其他命令。
考虑“计数器首次出现时设置过期时间”的限流逻辑:如果应用分别执行 INCR 和 EXPIRE,进程可能在两条命令之间崩溃,留下没有 TTL 的 key;两个客户端也可能交错修改状态。要把状态判断与更新组成一个明确的原子操作,可以使用短 Lua 脚本或经设计的事务逻辑。原子脚本只保证 Redis 命令执行过程中其他命令不插入该脚本的中间步骤,不会替外部数据库提交,也不代表脚本前已经完成的副作用可以回滚。相关 key 在 Cluster 下还须符合槽位约束。[8][14] 操作结果如果只在 Redis 中暂存,还要说明崩溃后的恢复来源。
分布式锁常见获得方式为 SET lock-key random-token NX PX ttl。随机 token 标识持有者;释放时只有 token 仍匹配才删除,避免 A 错删 B 的锁。可锁定时长到期后,A 可能只是暂停,锁已让给 B,A 恢复仍继续写。随机 token 没有新旧次序,因此不能阻止旧持有者的迟到写入。需要保护正确性的存储端可检查单调递增的 fencing token,只接受较新的操作;如果外部系统不支持这种校验,就不能声称 Redis 锁本身足以保证互斥。[9]
Redis 复制通常是异步的。Sentinel 侧重监控与主从故障转移;Cluster 还负责分片和集群操作。WAIT 可等待指定数量副本确认之前的写传播,但它不是共识协议,也不等于副本已把数据持久刷盘。[10][13][14] 对关键操作仍要有数据库约束、幂等键和对账路径,避免把 Redis 锁或复制确认当业务正确性的唯一依据。
面试时把持久性说成一个故障模型
Section titled “面试时把持久性说成一个故障模型”回答“提交成功后会不会丢”时,先问清楚“成功”指哪一层的确认,以及讨论哪种故障。Redis 返回命令成功、AOF 追加到内核页缓存、AOF 按设置 fsync 到存储、一个或多个副本收到复制流、故障转移选中某个副本,这些并非同一个确认点。RDB 快照能提供某个恢复点,但快照后新增的写入是否能恢复要看配置与故障时点。AOF 记录写入变化,重启时可重放;周期性 fsync 允许一定窗口内写入未到持久介质,always 也不能让已损坏的磁盘或丢失的主从链路变成可靠存储。[7][10]
所以对“Redis 里放队列是否安全”的回答,不能停在“开 AOF”。还要看任务是否允许丢、生产者何时得到成功确认、消费者何时 ACK、故障切换丢失范围、是否有幂等重放和死信/对账记录。可恢复任务通常需要持久任务账本或可重建来源;Redis 可承担队列和加速职责,但业务数据库或其他持久系统要能够回答“这次操作到底完成没有”。配置更强的持久化只减少某些风险,并不会自动消除端到端不确定结果。
缓存策略要按对象的失效后果选
Section titled “缓存策略要按对象的失效后果选”同一个 Redis 集群可以混放缓存、限流计数、临时会话或队列数据,但要先确认 maxmemory 与淘汰策略对每一类数据的含义。过期时间通常说明条目过了业务有效期;淘汰则是在内存达到限制时按策略移除键。设置 TTL 不等于内存到点立即归还,也不保证热点键在内存压力下仍存在。[11][12] 若采用 noeviction,写入可能在内存满时失败;采用基于 LRU/LFU 的策略时,受保护业务数据也可能被淘汰。策略应与数据可重建性和请求失败处理相匹配。
按场景拆分或设置独立实例,可以避免缓存突发写入把任务状态挤走。若共享集群无法拆分,至少要把 key 空间、内存预算和监控指标分清;淘汰事件、内存使用、热点访问、缓存命中率及源站回源量要一起看。只盯命中率会漏掉一种情况:命中率仍高,但少数热点 key 过期后集中回源,源数据库尾延迟已经上升。
InnoDB RR 事务中,普通一致性读与 SELECT ... FOR UPDATE 的关系,哪项最准确?
- A. 两者都只读事务第一次 SELECT 的快照
- B. 普通一致性读按 read view 取可见版本;锁定读读取当前可加锁状态并加锁
- C. 锁定读只锁历史版本,不影响其他事务写入
- D. RR 会把整张表复制到事务私有空间
答案与解析
正确答案:B。
- A 错:普通一致性读与锁定读语义不同。
- B 对:这一区分是分析混合读写时的起点。
- C 错:旧版本由 undo 重建,不能对历史副本加行锁。
- D 错:MVCC 不会为每个事务复制整张表。
两笔请求同时扣减仅剩 1 件库存,哪种方案最直接地保护单行库存不变量?
- A. 应用先查询库存,再无条件写回
- B. Redis 设置 30 秒 TTL 的互斥锁,数据库无需条件
- C. 用
UPDATE ... WHERE available >= 1扣减,并检查受影响行数 - D. 把隔离级别设为 RR 就不必检查写入结果
答案与解析
正确答案:C。
- A 错:两方可能都读到 1。
- B 错:锁失效、客户端暂停或绕过锁都可能破坏保证,数据库仍要守住约束。
- C 对:条件判断与扣减由数据库作为一次写入处理。
- D 错:隔离级别不能替代业务条件和结果检查。
以下对 InnoDB undo 和 redo 的概括哪项正确?
- A. undo 用于构造旧版本与回滚,redo 用于崩溃恢复中的重做
- B. undo 是复制日志,redo 是权限审计记录
- C. 两者都等同于 binlog
- D. redo 只保存 SQL 文本,不参与数据页恢复
答案与解析
正确答案:A。
- A 对:两类日志承担不同职责。
- B 错:undo 不是权限审计日志,redo 也不是复制日志。
- C 错:binlog 是 MySQL 层日志,不能与二者混为一谈。
- D 错:redo 用于恢复未完整写入数据文件的修改。
R 读到数据库旧值后,W 更新数据库并删除缓存,随后 R 才回填旧值。此时说明什么?
- A. 先提交数据库再删缓存已经提供强一致性
- B. 只要缓存有 TTL 就不可能读到旧值
- C. 删除缓存不能阻止已开始的旧读迟到回填
- D. 应把所有请求无限重试直到读到新值
答案与解析
正确答案:C。
- A 错:这是已知竞态窗口。
- B 错:TTL 只能限制缓存寿命,不能阻止存活期间的陈旧命中。
- C 对:旧数据已经由 R 读出,之后仍可能被重新写回。
- D 错:无界重试会放大负载,也不构成一致性方案。
Redis Pipeline 与 Lua 脚本的差别,哪项正确?
- A. Pipeline 自动使命令组不可插入,Lua 只减少网络往返
- B. Pipeline 主要减少往返;Lua 可将短小逻辑放到服务端原子执行
- C. 两者都能跨 Redis、MySQL 和 HTTP 实现原子回滚
- D. Lua 脚本执行错误时总会回滚此前所有修改
答案与解析
正确答案:B。
- A 错:Pipeline 不等于事务;Lua 在服务器执行逻辑。
- B 对:两者解决的问题不同。
- C 错:它们都不提供跨服务事务。
- D 错:不要将脚本原子执行误解为数据库式错误回滚。
Redis 锁持有者 A 暂停,租约到期后 B 获锁并写入,随后 A 恢复。哪项措施能在受保护资源侧拒绝 A 的旧写?
- A. 给 A 使用随机字符串 token
- B. 延长 TTL 到 24 小时
- C. 资源端校验单调递增的 fencing token
- D. 客户端释放锁前先 sleep 一秒
答案与解析
正确答案:C。
- A 错:随机 token 可标识所有者,不能表示新旧顺序。
- B 错:长 TTL 不消除暂停时间超过租约的可能。
- C 对:资源端可以拒绝落后的操作令牌。
- D 错:等待不构成可验证的互斥保证。
T1 在 RR 事务中先普通 SELECT 看到库存 3;T2 把库存改成 2 并提交;T1 再次普通 SELECT 和 SELECT ... FOR UPDATE 分别应如何分析?说明推理时必须确认的读类型。
参考解答与评分
T1 的重复普通一致性读通常仍按已建立的 read view 读取原快照,因此可能看到 3。锁定读则属于当前读语义,需取得当前可用状态并按索引扫描范围加锁,因此可能看到最新提交后的 2。不要把二者说成同一快照,也不要只凭 RR 推断所有语句看到同一状态。具体范围锁还需结合索引和执行路径分析。
评分点:
- 说明 RR 普通一致性读复用快照(1分)。
- 说明锁定读与普通读语义不同(1分)。
- 指出锁定读按当前状态加锁(1分)。
- 提醒范围和锁行为取决于索引/扫描条件(1分)。
为什么“先读剩余配额,再写入剩余配额”会超额?给出一种单条 SQL 修复,并说明应用还需检查什么。
参考解答与评分
两个并发事务可能同时读到同一个旧值,随后分别覆盖写回,两个调用都报告成功但只扣掉一次。可用 UPDATE quota SET remaining = remaining - 1 WHERE scope = ? AND remaining >= 1。应用检查受影响行数,只有成功扣减后才确认本次额度预留;与订单等数据相关的写入放入同一事务。
评分点:
- 描述并发读同一旧值的竞态(1分)。
- 给出带
remaining >= 1的条件更新(1分)。 - 检查受影响行数(1分)。
- 相关业务写入使用本地事务原子提交(1分)。
读写线程发生缓存旧回填时,说明时间顺序,并给出一项改进和一个正确性边界。
参考解答与评分
读线程 R 先从数据库取得旧版本;写线程 W 提交新版本并删除缓存;R 再将旧版本回填到缓存。可按版本写缓存、使用 generation key,或在回填前重新校验版本;TTL 只能限制陈旧时间,不能证明绝不陈旧。金额与权限等关键判定应读权威状态。
评分点:
- 按正确顺序列出旧读、新提交、删缓存、迟到回填(1分)。
- 解释删缓存未取消已进行的读(1分)。
- 给出版本校验、generation 或其他具体控制(1分)。
- 说明关键决策不依赖缓存(1分)。
为什么 SET NX PX 分布式锁仍可能出现两个持有者?token、TTL 与 fencing token 分别做什么?
参考解答与评分
持有者可能暂停到租约过期,随后另一个客户端获锁;旧持有者恢复后仍继续执行。随机 token 用于识别锁所有者并安全释放;TTL 限制锁最长占用时间、避免永久残留。二者都不能阻止旧写。单调递增的 fencing token 需由实际资源端校验,拒绝旧请求。
评分点:
- 给出持有者暂停、租约过期、新持有者接管的场景(1分)。
- 说明唯一 token 用于身份匹配/释放(1分)。
- 说明 TTL 不能终止暂停后恢复的进程(1分)。
- 说明资源端校验 fencing token 拒绝旧写(1分)。
- MySQL 8.4 Reference Manual:隔离级别、一致性读、锁定读、redo、undo、binlog。
- Redis Docs:持久化、事务、分布式锁、复制、淘汰和过期。
Sources
Section titled “Sources”[1] https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html [2] https://dev.mysql.com/doc/refman/8.4/en/innodb-consistent-read.html [3] https://dev.mysql.com/doc/refman/8.4/en/innodb-locking-reads.html [4] https://dev.mysql.com/doc/refman/8.4/en/innodb-redo-log.html [5] https://dev.mysql.com/doc/refman/8.4/en/innodb-undo-logs.html [6] https://dev.mysql.com/doc/refman/8.4/en/binary-log.html [7] https://redis.io/docs/latest/operate/oss_and_stack/management/persistence [8] https://redis.io/docs/latest/develop/using-commands/transactions [9] https://redis.io/docs/latest/develop/clients/patterns/distributed-locks [10] https://redis.io/docs/latest/operate/oss_and_stack/management/replication [11] https://redis.io/docs/latest/develop/reference/eviction [12] https://redis.io/docs/latest/commands/expire [13] https://redis.io/docs/latest/commands/wait [14] https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec
下面保留原报告的题目和口述参考答案。先自行作答,再展开查看。
DB04|ACID 和四种隔离级别怎么讲?〔P0〕
口述答案
Atomicity 是事务内操作整体提交或回滚;Consistency 是事务前后保持声明的约束和业务不变量,数据库不会自动懂所有业务规则;Isolation 描述并发事务之间允许观察到什么;Durability 是提交后的持久性,依赖具体日志、刷盘与故障模型。
常见四级是 RU、RC、RR、Serializable。RC 的一致性读通常每条语句看新快照;InnoDB 默认 RR,普通一致性读通常复用第一次一致性读建立的快照;Serializable 提供更强的串行化效果,但不等于实现上所有事务只能单线程执行。
追问怎么接
“脏读、不可重复读、幻读怎么区分?”脏读看到未提交数据;不可重复读关注同一行前后值变化;幻读关注同一条件返回的行集合变化。要同时说明具体数据库实现:不能用 SQL 标准的粗略现象表替代 InnoDB 锁和快照机制。
不要这样答:“RR 就是锁住我读过的全部行”“ACID 的一致性就是分布式强一致”。
依据与延伸:DB-ACID、DB-Isolation、DB-Snapshot、DB-Flush、Serializable。
DB05|MVCC、快照读、当前读与幻读之间是什么关系?〔P0〕
口述答案
InnoDB 使用事务元数据和 undo 版本信息,让一致性读按可见性规则找到合适版本,减少读写互相阻塞。普通 SELECT 的一致性读和 SELECT ... FOR UPDATE 这类锁定读不是一回事;锁定读/更新处理当前可用状态并加相应锁。
在 RR 下,普通快照读重复查询通常看同一快照;范围锁定读可使用 next-key 锁限制范围内并发插入。不能因此笼统说“RR 在任意混合读写下完全没有幻读或异常”。当前事务自己的写入、锁定读和快照读混用需要单独分析。
追问怎么接
“快照在 BEGIN 时一定建立?”普通 RR 事务通常在第一次一致性读建立,也有显式一致性快照用法。“MVCC 不需要锁?”写写冲突、锁定读仍需要锁。“长事务有何影响?”可能延长旧版本保留,增加 undo/清理压力;还可能长时间持锁。
不要这样答:“MVCC 给整个数据库复制了一份”“RR 下 UPDATE 也只操作事务最初看见的版本”。
依据与延伸:DB-MVCC、DB-Snapshot、DB-Locks。
DB06|两个请求同时扣库存,怎样不超卖?乐观锁与死锁呢?〔P0〕
口述答案
不要在应用先 SELECT 判断,再无条件 UPDATE。简单库存可用条件更新:UPDATE inventory SET available=available-1 WHERE sku_id=? AND available>=1,检查受影响行数;创建订单等关联写放在同一本地事务里,失败整体回滚。复杂约束可锁定目标行后校验,或用版本号做 compare-and-swap。
乐观并发控制用版本条件检测状态是否被别人修改,冲突后重新读取/决策;悲观方式先加锁再操作。并发事务仍可能死锁,应用需要识别并重试整个可重试事务,而不是只重放最后一条 SQL。
追问怎么接
“怎样减少死锁?”事务尽量短、多个资源按统一顺序加锁、用合适索引减少锁范围,并用死锁记录找冲突。“UPDATE 一定只锁一行吗?”取决于索引和扫描范围;条件写得像单行,不代表扫描/加锁一定只有一条记录。
不要这样答:“有事务就不会超卖”“给库存加 Redis 锁后数据库不用约束了”。
依据与延伸:DB-LockingRead、DB-Locks、DB-Deadlock。
DB07|Redo、Undo、Binlog 分别做什么?提交后一定不丢吗?〔P0〕
口述答案
Redo 用于 InnoDB 崩溃恢复,使已记录的页面修改能重新恢复;Undo 支持事务回滚,也用于构造一致性读的旧版本;Binlog 是 MySQL 层的变更日志,用于复制及结合备份做时间点恢复。三者角色不同,不能互相替代。
写成功不等于任何故障下绝不丢失。要看提交确认前是否持久化日志、磁盘是否可靠、复制是否已确认以及故障切换策略。innodb_flush_log_at_trx_commit 影响提交刷盘策略,复制链还涉及 binlog 和副本持久性。面试先说语义,再说配置。
追问怎么接
“数据页没落盘怎么已提交?”WAL 允许先保证必要日志持久化,再异步刷新数据页,崩溃后恢复。“redo 就是 SQL 日志吗?”不应这样概括;不要把 redo 和 statement-format binlog 混成一类。“有日志算备份吗?”仍需独立备份、恢复验证和保留策略。
不要这样答:“commit 返回就能抵抗全部主机/磁盘/集群故障”“undo 只用于回滚,不涉及 MVCC”。
DB08|连接池、读写分离和 Agent 长任务有什么关系?〔P1〕
口述答案
连接池要限制活跃数据库连接,避免每个请求反复建连,也避免并发无限推到数据库;连接池耗尽时应有等待上限和监控。它不是让数据库获得无限吞吐的装置。
Agent 的模型调用或人工审批可能持续很久,不该在这段时间占着连接和事务锁。把状态保存后释放资源,再在下一步开启短事务。读写分离下要考虑副本延迟:刚写完查询、权限撤销和退款执行校验不能无条件依赖可能滞后的副本;具体保障由复制模式与路由契约决定。
追问怎么接
“模型读到订单后过一分钟才退款怎么办?”执行时重新检查业务版本与额度,必要时重新审批,而不是维持一分钟事务。“每个 worker 池开 100 个连接呢?”总量是实例数乘连接上限,还要算其他服务;连接预算必须全局规划。
**不要这样答:**为了保证 Agent 看到同一数据,从开始问模型到人工确认一直持有事务。
RD01|Redis 为什么快?“单线程”到底怎么说?〔P1〕
口述答案
Redis 的常见优势包括内存中的数据、合适的数据结构、事件驱动网络处理以及避免在普通命令执行路径上引入复杂共享并发。不能把 Redis 整个进程概括成只有一个线程:网络 I/O、后台任务和具体版本实现需要分开看。面试更应说明长命令、网络和内存为什么仍会造成尾延迟。
先会按用途选择逻辑类型:String 做简单值/计数,Hash 存字段集合,Set 做去重,Sorted Set 做带分数排序。内部编码可能随对象大小和版本变化,本周不背“某类型永远由某数据结构实现”。
追问怎么接
“单线程为什么能处理很多连接?”等待网络不等于每条连接都占一个持续运行的执行线程;事件就绪时处理。“为什么 KEYS、巨大集合操作危险?”工作量大可能占住执行路径;改用有界扫描和业务侧限制,但扫描本身不提供事务快照。
不要这样答:“Redis 快完全因为单线程”“全部命令都是 O(1)”。
依据与延伸:Redis-Types、Redis-Latency、Redis-Scan。
RD02|过期、淘汰、热 key、大 key 怎么区分?〔P1〕
口述答案
过期是根据 TTL 判断数据是否仍有效;淘汰是在内存压力下按策略移除 key,和 TTL 不是同一个维度。Redis 既会在访问时检查过期,也会做主动过期处理,因此不保证到点立即完成物理删除。内存满时还要看 maxmemory 与 LRU/LFU/noeviction 等策略。
热 key 是访问集中,大 key 是对象体积或成员规模大,可以独立出现。前者常影响单分片负载,后者影响网络、命令时长、删除和迁移。先量化热点与对象大小,再决定本地缓存、拆分或业务聚合。
追问怎么接
“重要任务状态存在 Redis 能不能被淘汰?”取决于配置;不应把可淘汰缓存当作唯一任务账本。“热 key 加节点就解决?”一个 key 的访问可能仍集中到一个分片,不能只做平均容量估算。
不要这样答:“设置了 TTL 就不需要 maxmemory”“过期时间一到,内存立即归还”。
依据与延伸:Redis-Expire、Redis-Evict、Redis-Latency。
RD03|缓存穿透、击穿、雪崩是什么?〔P0〕
口述答案
穿透是反复查询不存在的数据,绕过缓存压向数据库;可以做输入校验、短期负缓存或适合场景的存在性过滤。击穿是热门 key 失效后大量请求同时回源;可用 singleflight/互斥重建、有界等待,允许旧数据时采用 stale-while-revalidate。雪崩是大量 key 同时失效或缓存整体故障,引发集中回源;可打散过期、预热、限流和隔离。
这些方案都要有边界:负缓存可能短暂掩盖新建对象;旧数据策略不适合退款权限与金额校验;缓存故障时不能让所有请求无限绕过限流直接打数据库。
追问怎么接
“互斥重建拿不到锁怎么办?”有限等待或使用允许的旧值,超时降级;不能永久阻塞。“TTL 随机化能救 Redis 宕机吗?”不能,它只处理某些同时过期问题,整机故障仍需要容量和降级设计。
不要这样答:“加分布式锁解决所有缓存问题”“缓存挂了全部回源,数据库应该能撑住”。
依据与延伸:Cache-Aside、Redis-Lock、AWS-Retry。
RD04|数据库更新后,缓存怎么保持一致?〔P0〕
口述答案
常见 cache-aside 做法是先提交数据库,再删除相关缓存;读取 miss 时回源并填充。这是降低不一致概率的实用方案,不是强一致保证。删除可能失败,也可能出现旧读在更新完成后才把旧值写回缓存。
例如 R 先读到 DB 旧值,W 提交新值并删缓存,随后 R 把旧值填回:缓存重新变旧。若业务允许短暂陈旧,用 TTL、可靠失效事件、版本校验及必要的重建协议控制窗口;关键执行仍读权威状态,不让缓存成为支付正确性的依据。
追问怎么接
“为什么不先删再更新 DB?”删除后别的读可能再次加载尚未更新的旧 DB 值。“延迟双删保证一致吗?”不保证,固定延时无法覆盖所有长请求和故障。严格要求需明确序列化/版本机制或放弃该路径缓存,不能靠一句双删宣称强一致。
不要这样答:“先更新数据库再删缓存,所以一致性问题已经解决”。
依据与延伸:Cache-Aside、Outbox。
RD05|RDB、AOF、fsync 的持久性怎么回答?〔P0〕
口述答案
RDB 保存某个时刻的数据快照,恢复点之间的写入可能丢失;AOF 记录写入变化,恢复时重建状态,持久性取决于 fsync 策略。everysec 通常以约一秒的刷盘节奏在性能与丢失窗口之间取舍,不能把它说成所有故障和配置下的严格最大损失保证;always 更强,但仍受存储与部署故障模型约束。
后台快照/重写可能涉及 fork 与写时复制,写多时会增加内存压力。AOF 重写是将当前状态表示得更紧凑,不是完整保留所有历史事件;业务审计不能依赖它。
追问怎么接
“执行成功但 AOF 还没刷盘时宕机?”要区分 Redis 进程退出与 OS/机器故障,页缓存和持久磁盘的存活边界不同。“AOF always 就能保证主从切换不丢?”本机持久性和副本复制进度是两件事。
不要这样答:“AOF 是写前日志,所以命令执行前一定落盘”“开了 AOF 就永不丢数据”。
依据与延伸:Redis-Persist、Redis-Repl。
RD06|Pipeline、MULTI/EXEC、WATCH、Lua 各解决什么?〔P0〕
口述答案
Pipeline 批量发送命令减少往返,并不把这些命令变成不可分割事务。MULTI/EXEC 把一组命令排队后执行,执行期间不被别的客户端命令插入;但不提供关系数据库式的执行错误回滚。WATCH 做乐观检查,监视 key 改变则放弃事务,需要客户端重新决策。
Lua 把多步逻辑移到服务端原子执行,适合计数/限流等小段逻辑;但脚本长时间运行会占用执行路径。原子执行不等于发生错误时自动撤销之前所有写入,也不覆盖 MySQL 或外部 HTTP 操作。
追问怎么接
“INCR 和 EXPIRE 分两次调用呢?”中间可能失败或被插入,导致无 TTL 的计数;可以用经过审查的脚本/事务逻辑。“Lua 能跨 Cluster 随意操作 key 吗?”通常相关 key 需满足同槽等约束,按版本与部署契约判断。
不要这样答:“Pipeline 是事务”“Lua 的原子性等于跨服务 ACID”。
RD07|Redis 分布式锁怎样写?为什么仍可能同时执行?〔P0〕
口述答案
基本模式是原子 SET key unique_token NX PX ttl,释放时原子地比较持有者 token 再删除,不能直接 DEL。TTL 防永久占锁,唯一 token 防误删别人的锁;两者都不保证旧持有者在暂停恢复后停止工作。
例子:A 拿锁后暂停,TTL 到期,B 拿到锁;A 恢复后仍继续写。续租降低概率但不能构成完备保证。对正确性关键资源使用版本条件、唯一约束或下游可强制校验的 fencing token,不能只相信锁。
追问怎么接
“fencing 是什么?”每次授予更高的单调令牌,资源端拒绝旧令牌;必须由实际受保护资源检查。随机持有者 token 只解决身份匹配,不具备单调次序。“那退款用什么?”稳定业务操作 ID、额度约束与下游幂等;锁仅是协调/效率手段。
不要这样答:“锁自动续期就绝对安全”“我在代码里检查了一下锁还在,所以随后远端写一定安全”。
依据与延伸:Redis-Lock、DB-LockingRead、Stripe-Idem。
RD08|Sentinel、Cluster、复制与 WAIT 怎么区分?〔P1〕
口述答案
Sentinel 主要监控主从并协调故障转移;Cluster 还把 key 分布到多个分片,并提供相应的故障处理机制。常见复制是异步的,所以主节点已确认的写在故障切换时仍可能丢失;分片增加容量并不自动消除这个问题。
WAIT 等待一定数量副本确认之前的写传播,能增强某些故障场景的数据安全性,但不把 Redis 转成强一致系统,也不能简单等同于任意副本都已持久落盘。关键数据是否可接受风险,需要结合具体配置与业务契约。
追问怎么接
“任务队列存在 Redis 就永不丢?”还要看持久化、复制、ACK、消费者恢复和淘汰配置。区分临时通知与需要恢复的任务账本。“多 key 原子操作呢?”分片后通常需要同槽或重新设计事务边界,不能继承单实例的直觉。
不要这样答:“WAIT 1 就是分布式事务”“有副本就不会丢已确认写”。