Files
examination/topics/database/redis-persistence/code_reading.json
T

254 lines
18 KiB
JSON
Raw Normal View History

{
"topic": "redis-persistence",
"type": "code_reading",
"schema_version": "1.0.0",
"generated": "2026-09-07T00:00:00+08:00",
"questions": [
{
"id": "cr-001",
"type": "code_reading",
"difficulty": 2,
"tags": [
"AOF",
"AOF重写",
"命令序列",
"状态快照"
],
"question": "请阅读下面这段 AOF 文件里按时间顺序追加的命令流。假设这些命令都已被 Redis 执行并写入 AOF,随后触发了一次 AOF 重写(BGREWRITEAOF)。",
"code": "SET user:1001 zhangsan\nSET user:1001 zhangsanfeng\nSET user:1002 lisi\nDEL user:1001\nINCR view:homepage\nSET counter 5\nINCRBY counter 1\nDEL counter",
"language": "plaintext",
"source": null,
"related": [],
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "经过 AOF 重写后,新 AOF 文件里针对 user:1001 这条 key 记录的命令是什么?",
"options": {
"A": "SET user:1001 zhangsan 和 SET user:1001 zhangdanfeng(保留两条)",
"B": "DEL user:1001(因为最后被删除了)",
"C": "SET user:1001 zhangdanfeng(最终值)",
"D": "不写该 key(因为最终不存在)"
},
"answer": "D",
"explanation": "AOF 重写不再按命令逐条重放,而是遍历当前内存数据库的状态,为每个 key 生成一条能重建其最终值的命令。user:1001 的最后一条命令是 DEL,即该 key 当前已不存在,因此重写后根本不生成任何关于 user:1001 的命令。A 保留了被覆盖的旧命令,B 生成一条 DEL 是错误的(key 已无,无需再删),C 误以为留下中间值,都错。"
},
{
"index": 2,
"type": "short_answer",
"question": "重写后,counter 和 view:home 这两个 key 对应的命令分别是什么?为什么 AOF 重写能把这些命令从 8 条大幅精简?",
"answer": "重写后:counter 被最后一条 DEL 删除,key 已不存在,所以不生成命令;view:home 只剩 INCRBY view:home 1。\n\n能大幅精简的根本原因:AOF 存的是『命令序列』(append-only log),大量早期命令的中间结果会被后续命令覆盖(user:1001 被反复覆盖后删除、counter 被 INC/INCRBY 后再 DEL)。AOF 重写遍历当前内存哈希表,为每个 key 只生成一条『重建当前最终状态』的最小命令,把存储的具体『操作时间线』等价转换为『键值状态快照』,冗余的历史操作被丢弃,文件因此变小,恢复也更快。",
"explanation": "本题考察 AOF 重写的本质:将『命令序列』重新组织为『状态快照』。counter 终态不存在故无命令;只有仍存在的 key 才生成最小重建命令。压缩的核心依据是:大量命令对同一 key 是互相覆盖的,只保留最终值就能恢复一致状态。",
"keywords": [
"不存在",
"状态快照",
"命令序列",
"重建最终值",
"最小命令",
"覆盖",
"压缩"
],
"scoring_rubric": "①答出 counter 不存在、view 为 INCRBY 得 1 分;②答出 counter key 已删不生成命令得 1 分;③说明重写是把命令序列转换为键值状态快照、丢弃被覆盖的中间命令、文件变小得 3 分。"
}
],
"explanation": "AOF 本身是一条一条 append 命令的时间线(log 结构),AOF 重写则遍历当前内存把时间线精简为哈希的 key→最终状态。凡是被删除或被后续命令覆盖的中间命令全部丢弃,达到『压缩』。这是理解 AOF 能压缩的本质(log 结构→状态快照)。"
},
{
"id": "cr-002",
"type": "code_reading",
"difficulty": 3,
"tags": [
"AOF",
"RDB",
"恢复",
"持久化配置"
],
"question": "下面是一台 Redis 实例的持久化配置(精简展示)。请阅读配置并回答进程崩溃重启时的数据恢复行为。",
"code": "# redis.conf 片段\nsave 900 1\nsave 300 10\nappendonly no",
"language": "bash",
"source": null,
"related": [],
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "以上配置下,若 Redis 距离上一次 RDB 快照 10 分钟时进程宕机,最多可能丢失多少数据?",
"options": {
"A": "0,因为 Redis 每次写都实时落盘",
"B": "最近 5 分钟内写入的所有数据(有 save 300 的兜底)",
"C": "从上一次成功完成 RDB 快照之后到宕机前写操作的所有数据",
"D": "最后 30 秒的数据"
},
"answer": "C",
"explanation": "本配置中 appendonly 为 no(关闭),只靠 RDB 快照持久化。RDB 是『定时的全量快照』,save 300 1 表示 300 秒内至少有 1 次写才触发一次生成点。宕机时只恢复到最近一次成功快照,快照之后的所有写操作全部丢失。所以『最长可能丢失上一次快照到宕机之间的全部写数据』(选 C)。A 错误,Redis 不是每次写都落盘;B 混淆了 save 触发间隔与丢数据范围;D 是 AOF 场景不适用于这里。"
},
{
"index": 2,
"type": "short_answer",
"question": "同样的业务若想几乎不丢数据,应如何调整配置?为什么?",
"answer": "开启 AOF(将 appendonly 改为 yes),并根据数据重要程度选择同步策略,例如 appendfsync always(每条写命令都 fsync,最安全但性能低,几乎不丢)或 everysec(每秒同步,最多丢约 1 秒数据)。原因:RDB 是周期快照,快照间隔之间的数据没有任何日志兜底;AOF 是追加写命令日志,崩溃后通过重放命令恢复,丢失窗口被压缩到 appendfsync 决定的粒度(always=几乎不丢,everysec≤1s,如果需要可用 appendfsync no 让系统决定)。",
"explanation": "本题考察 RDB vs AOF 在数据安全上的取舍。开启 AOF 后,丢失窗口会从『上一次快照点』缩小到『appendfsync 策略决定的粒度』。平常推荐 everysec(性能/安全平衡),绝对不可丢的选 always。",
"keywords": [
"appendonly yes",
"appendfsync",
"everysec",
"always",
"重放",
"丢失窗口"
],
"scoring_rubric": "①答出开启 appendonly/AOF 得 1 分;②答出 appendfsync 取值及对应丢失窗口得 2 分;③说明 AOF 基于命令重放还原、丢失窗口从 RDB 快照周期缩小到同步粒度得 2 分。"
}
],
"explanation": "RDB 快照与 AOF 日志在『丢数据范围』上差异极大:RDB 丢到上次快照,AOF 丢到 appendfsync 粒度。本题考查在给定持久化配置下计算最大丢数据量、以及如何调整降低丢失。"
},
{
"id": "cr-003",
"type": "code_reading",
"difficulty": 3,
"tags": [
"redo log",
"环形缓冲",
"崩溃恢复",
"循环复用"
],
"question": "下面用伪代码描述 InnoDB redo log 的『环形』复用思想。阅读并回答:为什么 redo log 不像 AOF 那样无限膨胀、也不像 AOF 需要主动重写压缩。",
"code": "# 伪代码:redo 环形缓冲区覆盖逻辑\ndef write_redo(head_lsn, tail_lsn, capacity):\n # 环形缓冲区大小固定(如 1GB)\n while (head_lsn - tail_lsn) >= capacity:\n flush_dirty_page_to_disk() # 强制刷脏页,推进 rev per tail/checkpoint\n append_record(page_lsn)\n head_lsn = head_lsn + 1",
"language": "readtext",
"source": null,
"related": [],
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "redo log 采用『环形固定大小』覆盖最旧记录,覆盖旧记录最典型的前提条件是什么?",
"options": {
"A": "只要文件满了就直接覆盖最旧记录,不考虑数据页是否已落盘",
"B": "被覆盖的记录所对应的脏页必须先成功刷回磁盘(checkpoint 推进),否则崩溃会丢数据",
"C": "先把 redo log 文件长度翻倍再覆盖",
"D": "每次启动都清空整个 redo 文件"
},
"answer": "B",
"explanation": "redo log 用于崩溃后重放:它记录的是尚未安全刷盘的数据页修改。覆盖旧记录的前提是,这些旧记录对应的脏页已经成功刷到数据文件(即 checkpoint 位置推进),否则一旦被覆盖而对应页面还没落盘,崩溃后就无法重放、造成永久丢失。所以先触发脏页刷盘(推进 checkpoint)再覆盖。A 直接覆盖会丢数据,C/D 与场景无关。"
},
{
"index": 2,
"type": "short_answer",
"question": "从『数据结构(覆盖 vs 追加)』角度,说明 redo log 为何天然不膨胀、不需要像 AOF 那样主动重写;它们对中间过程的保留策略分别是什么?",
"answer": "redo log 是固定大小的环形缓冲(记录按 LSN 排序),当旧记录对应的脏页已落盘后,新记录就覆盖旧记录,天然丢弃了『已安全落盘、无需再重放』的物理页变更,只保留尚未落盘的部分,文件体积固定、不膨胀,无需主动重写。AOF 是末尾追加、累进的命令序列,命令一 append 就永久保留,会随时间无限膨胀,才需要 BGREWRITEAOF 主动把命令序列压缩成状态快照。\n对中间过程:redo 是物理层覆盖合并,不保留每次中间变更,重放只针对尚未落盘的最终页状态;AOF 则保留全部命令历史,直到重写时才丢弃被覆盖/删除的命令。",
"explanation": "二者数据结构(环形覆盖 vs 末尾追加)决定了是否需要主动压缩。redo『环形+checkpoint 推进』→ 天然不膨胀;AOF『append 追加』→ 需显式重写。这正是题目伪代码(后半是覆盖)与 AOF(后半是追加)的对照。",
"keywords": [
"环形缓冲",
"覆盖",
"checkpoint",
"脏页落盘",
"LSN",
"不膨胀",
"追加",
"重写",
"状态快照"
],
"scoring_rubric": "①答出 redo 是环形固定大小、覆盖已落盘旧页、天然不膨胀得 2 分;②答出 AOF 是末尾追加、积累全部命令、才需显式重写得 2 分;③说到中间过程差异(物理覆盖合并 vs 保留历史到重写)得 1 分。"
}
],
"explanation": "关键在『覆盖 vs 追加』的数据结构差异:redo 环形覆盖天然合并中间过程、体积固定,AOF 追加增长→需要主动重写。这正是 AOF 为何要压缩而 redo 不用压缩的根因。"
},
{
"id": "cr-004",
"type": "code_reading",
"difficulty": 2,
"tags": [
"binlog",
"PITR",
"时间点恢复",
"mysqlbinlog"
],
"question": "某运维在 14:00 误删除了一张表,希望恢复到 13:59 的状态。已有 12:00 的全量备份。阅读下面的恢复思路并回答。",
"code": "# 1. 先恢复 12:00 的全量备份\ntar -xzf backup_1200.tar.gz && mysql < backup_1200.sql\n\n# 2. 再用 binlog 做增量重放,停在误操作前\nmysqlbinlog --start-position=<pos1> --stop-position=<pos2> binlog.000123 | mysql",
"language": "bash",
"source": null,
"related": [],
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "binlog 做时间点恢复时,'' 为什么用 '--stop-position'(文件+偏移)比单纯用 '--stop-datetime'(时间字符串)更能保证精确?",
"options": {
"A": "--stop-datetime 只对 ROW 格式生效,对 statement 格式失效",
"B": "时间戳是按 SQL 语句粗略过滤的,可能在一条事务/语句内部截止,导致该事务只执行了一半;而 position 是 binlog 里精确的字节偏移,可对准完整事件边界",
"C": "--stop-position 参数并不存在,MySQL 只支持 --stop-datetime",
"D": "--stop-position 会把日志里的所有事务一起截断,无法只保留部分"
},
"answer": "B",
"explanation": "binlog 中能唯一定位重放位置的是文件的物理偏移(position),而非时间。--stop-datetime 按语句/事务的记录时间过滤,可能把一个未结束的事务切在中间(一半重放一半不重放),破坏事务原子性;position 能精确对准一个事件边界,所以做 PITR 用它最安全。A、D 说法无依据,C 错误(--stop-position 存在)。"
},
{
"index": 2,
"type": "short_answer",
"question": "『从全量备份 + binlog 增量重放』这一整套恢复操作官方叫什么?为什么 AOF 做不到像 binlog 这样精准重放到任意历史点?",
"answer": "这套操作叫 point-in-time recovery(PITR,时间点恢复)。binlog 为 PITR 设计:逐条记录事务(含文件号+字节位置),配合全量备份可精确拼到任何一个历史提交点;mysqlbinlog 支持 --start-position/--stop-position(字节精确)和 --stop-datetime(时间近似)。AOF 是 Redis 的持久化日志,虽然记录了全部写命令,但它是『从文件头按顺序重放到文件尾』,命令本身没有内建可精确到事务边界的 position 切片接口,也没有标准的『从中间某条开始/停止』重放工具,要从历史某一时刻恢复需要手工截断文件再重放,非常不便,因此只说『可大致回到历史某个时间点、但需手动截断』,不像 binlog 原生支持精确切片。",
"explanation": "PITR 是 binlog 的核心能力,靠每条记录的位置+完整事务边界。AOF 虽也是命令日志,但其定位是『崩溃后从头把内存状态重放到当前』,而非『精确停在历史某一事务横切』,加上缺少原生 position/时间点切片命令,所以只能手动截断。",
"keywords": [
"PITR",
"时间点恢复",
"position",
"全量备份",
"增量重放",
"从头顺序重放",
"无时间切片"
],
"scoring_rubric": "①答出 PITR 得 1 分;②说明 binlog 用备份+position 增量重放实现精确到点得 2 分;③点出 AOF 只能从头顺序重放到末尾、缺少原生时间点重放而需要手动截断得 2 分。"
}
],
"explanation": "考察 binlog 与 AOF 在时间点恢复上的差异。核心:binlog 支持 position/位点(PITR 精确),AOF 只能从头到末尾顺序重放、无原生时间点切片。"
},
{
"id": "cr-005",
"type": "code_reading",
"difficulty": 3,
"tags": [
"AOF",
"appendfsync",
"丢失窗口",
"AOF重写",
"配置"
],
"question": "下图为某 Redis 的 AOF 相关配置片段。请理解其含义并回答相应问题。",
"code": "appendonly yes\nappendfsync everysec\nauto-aof-rewrite-percentage 100\nauto-aof-rewrite-min-size 64mb",
"language": "bash",
"source": null,
"related": [],
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "在 appendfsync everysec 下,当进程宕机(不是主机断电)时,最多大约丢失多少数据?",
"options": {
"A": "1 秒的写入数据",
"B": "0 数据,一条都不丢",
"C": "距离上一次自动重写之间的全部写入数据",
"D": "最多 64MB 数据"
},
"answer": "A",
"explanation": "everysec 表示每 1 秒把 AOF 缓冲 fsync 到磁盘一次,因此最多丢失『最后一次 fsync 到崩溃之间』这一秒的写入。'always' 才是一条不丢(B 错);C 混淆了 AOF 重写间隔与丢失窗口;D 把 auto-aof-rewrite-min-size 当成丢数据量,错误。"
},
{
"index": 2,
"type": "short_answer",
"question": "auto-aof-rewrite-percentage 100 和 auto-aof-rewrite-min-size 64mb 这两个参数控制什么行为?这里的『重写』做了什么?",
"answer": "它们控制 AOF 自动重写(BGREWRITEAOF)的触发条件:当当前 AOF 文件体积相对上次重写后的文件增长幅度超过 100%(即翻倍),并且当前文件大小已经超过 64MB 的下限时,Redis 自动在后台触发一次 AOF 重写。\n重写的行为:遍历当前内存数据库,为每个 key 只重新生成一条重建其最终状态的最小命令(可配合前缀批量,尽量小段),丢弃掉大量被覆盖或已删除的旧命令,把膨胀的命令日志压缩为接近『状态快照』的小体积日志,文件变小,重启重放更快。",
"explanation": "auto-aof-rewrite-percentage/min-size 是 AOF 自动重写的双阈值:相对上一轮的增长百分比 + 最小文件下限,两者同时满足才自动触发。重写本质是把『命令日志 → 状态快照』压缩,避免 AOF 无限膨胀。",
"keywords": [
"auto-aof-rewrite-percentage",
"100%增长",
"64MB下限",
"BGREWRITEAOF",
"状态快照",
"压缩"
],
"scoring_rubric": "①解释 percentage 是相对上一轮增长幅度、min-size 是最小下限、需要同时满足得 2 分;②说明重写把命令日志转成最小状态快照、避免膨胀得 3 分。"
}
],
"explanation": "考查 AOF 配置阈值与后台重写。appendfsync 决定丢失窗口,rewrite 阈值决定何时压缩 AOF 文件,两者结合掌握 AOF 的核心运维。"
}
]
}