讲真,第一次看 Cantian 持久化与恢复源码的时候,我也是一脸懵------Redo 日志、Undo 日志、Checkpoint、崩溃恢复、归档管理、日志复制,这么多机制,到底谁保护谁?直到有一次线上遇到数据库宕机,需要紧急恢复数据,我才真正去翻源码,搞明白这套机制是怎么做到"数据不丢、快速恢复"的。
这篇文章就是帮你建立 Cantian 持久化与恢复的"全局地图"。你会看到:
- Redo 日志:Log Buffer 双缓冲 → LGWR 组提交 → 日志文件组切换
- Undo 日志:段分配、记录生成、回滚机制
- Checkpoint:四类触发条件与三种刷盘模式
- 崩溃恢复:Analysis → Redo → Undo 三阶段恢复
- 归档管理:日志归档与备份恢复
- 日志复制:发送/接收/回放三线程架构
核心要解决的问题就三个:
- 数据怎么保护:Redo/Undo 是怎么做到数据不丢的?
- 崩溃怎么恢复:三阶段恢复是怎么做到快速恢复的?
- 主备怎么同步:日志复制是怎么做到数据一致的?
先搞清楚 Cantian 持久化与恢复是个啥
Cantian 的持久化与恢复,说白了就是保证数据库在宕机后能够快速恢复到一致状态。
你想啊,数据库运行过程中随时可能宕机:断电、硬件故障、内核崩溃......怎么保证已提交的事务不丢失?怎么保证未提交的事务能回滚?怎么保证数据页面能恢复到一致状态?这就是持久化与恢复要解决的问题。
它的核心设计原则就这几个:
| 原则 | 说明 |
|---|---|
| WAL 日志 | 先写日志再写数据,保证持久性 |
| 双缓冲设计 | Log Buffer 双缓冲提高并发写入 |
| 三阶段恢复 | Analysis → Redo → Undo 保证一致性 |
| 组提交 | LGWR 批量提交减少 I/O 开销 |
持久化与恢复功能域总览:六大机制是怎么保护的?
持久化与恢复功能域覆盖三个核心模块:kernel/persist(恢复与持久化)、kernel/replication(日志复制)、kernel/xact(Undo 部分)。它保障 ACID 中 Durability(持久性)与 Atomicity(原子性),在高并发事务与潜在宕机场景下确保已提交事务不丢失、未提交事务可回滚、数据页面在崩溃后可恢复至一致状态。
#mermaid-svg-IwhNwI5fu9hRZzQw{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-IwhNwI5fu9hRZzQw .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-IwhNwI5fu9hRZzQw .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-IwhNwI5fu9hRZzQw .error-icon{fill:#552222;}#mermaid-svg-IwhNwI5fu9hRZzQw .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-IwhNwI5fu9hRZzQw .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-IwhNwI5fu9hRZzQw .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-IwhNwI5fu9hRZzQw .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-IwhNwI5fu9hRZzQw .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-IwhNwI5fu9hRZzQw .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-IwhNwI5fu9hRZzQw .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-IwhNwI5fu9hRZzQw .marker{fill:#333333;stroke:#333333;}#mermaid-svg-IwhNwI5fu9hRZzQw .marker.cross{stroke:#333333;}#mermaid-svg-IwhNwI5fu9hRZzQw svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-IwhNwI5fu9hRZzQw p{margin:0;}#mermaid-svg-IwhNwI5fu9hRZzQw .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-IwhNwI5fu9hRZzQw .cluster-label text{fill:#333;}#mermaid-svg-IwhNwI5fu9hRZzQw .cluster-label span{color:#333;}#mermaid-svg-IwhNwI5fu9hRZzQw .cluster-label span p{background-color:transparent;}#mermaid-svg-IwhNwI5fu9hRZzQw .label text,#mermaid-svg-IwhNwI5fu9hRZzQw span{fill:#333;color:#333;}#mermaid-svg-IwhNwI5fu9hRZzQw .node rect,#mermaid-svg-IwhNwI5fu9hRZzQw .node circle,#mermaid-svg-IwhNwI5fu9hRZzQw .node ellipse,#mermaid-svg-IwhNwI5fu9hRZzQw .node polygon,#mermaid-svg-IwhNwI5fu9hRZzQw .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-IwhNwI5fu9hRZzQw .rough-node .label text,#mermaid-svg-IwhNwI5fu9hRZzQw .node .label text,#mermaid-svg-IwhNwI5fu9hRZzQw .image-shape .label,#mermaid-svg-IwhNwI5fu9hRZzQw .icon-shape .label{text-anchor:middle;}#mermaid-svg-IwhNwI5fu9hRZzQw .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-IwhNwI5fu9hRZzQw .rough-node .label,#mermaid-svg-IwhNwI5fu9hRZzQw .node .label,#mermaid-svg-IwhNwI5fu9hRZzQw .image-shape .label,#mermaid-svg-IwhNwI5fu9hRZzQw .icon-shape .label{text-align:center;}#mermaid-svg-IwhNwI5fu9hRZzQw .node.clickable{cursor:pointer;}#mermaid-svg-IwhNwI5fu9hRZzQw .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-IwhNwI5fu9hRZzQw .arrowheadPath{fill:#333333;}#mermaid-svg-IwhNwI5fu9hRZzQw .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-IwhNwI5fu9hRZzQw .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-IwhNwI5fu9hRZzQw .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IwhNwI5fu9hRZzQw .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-IwhNwI5fu9hRZzQw .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IwhNwI5fu9hRZzQw .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-IwhNwI5fu9hRZzQw .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-IwhNwI5fu9hRZzQw .cluster text{fill:#333;}#mermaid-svg-IwhNwI5fu9hRZzQw .cluster span{color:#333;}#mermaid-svg-IwhNwI5fu9hRZzQw div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-IwhNwI5fu9hRZzQw .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-IwhNwI5fu9hRZzQw rect.text{fill:none;stroke-width:0;}#mermaid-svg-IwhNwI5fu9hRZzQw .icon-shape,#mermaid-svg-IwhNwI5fu9hRZzQw .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-IwhNwI5fu9hRZzQw .icon-shape p,#mermaid-svg-IwhNwI5fu9hRZzQw .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-IwhNwI5fu9hRZzQw .icon-shape .label rect,#mermaid-svg-IwhNwI5fu9hRZzQw .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-IwhNwI5fu9hRZzQw .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-IwhNwI5fu9hRZzQw .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-IwhNwI5fu9hRZzQw :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 崩溃恢复
备份恢复
检查点
Redo 日志
事务执行
日志复制
Redo 发送
Redo 接收
Redo 回放
Undo 管理
Undo 记录生成
Undo 段管理
Undo 清理 PURGE
事务修改数据页
Log Buffer 日志缓冲
LGWR 日志写入线程
日志文件组
Checkpoint 检查点
DBWR 脏页刷写
双写区保护
Archive 归档管理
物理备份
基于时间点恢复 PITR
分析阶段
前滚 Redo
回滚未完成事务
模块职责矩阵
| 模块路径 | 核心文件 | 职责 |
|---|---|---|
| kernel/persist | knl_log.c/h knl_ckpt.c/h knl_recovery.c/h knl_archive.c/h knl_abr.c/h knl_log_file.c/h knl_log_type.h knl_ddl_log_file.c/h |
Redo 日志管理、检查点、崩溃恢复、归档管理、ABR 自动块修复、DDL 逻辑日志 |
| kernel/xact | knl_undo.c/h knl_tran.c/h knl_xact_log.c/h knl_xa.c/h knl_lock.c/h knl_alck.c/h |
Undo 日志管理、事务管理、XA 分布式事务、锁管理 |
| kernel/replication | repl_log_send.c/h repl_log_recv.c/h repl_log_replay.c/h repl_raft.c/h repl_arch_fetch.c/h knl_gbp.c/h repl_msg.c/h |
Redo 发送/接收/回放、Raft 共识、归档拉取、GBP 全局缓冲池 |
| kernel/backup | knl_backup.c/h bak_restore.c/h bak_paral.c/h bak_build.c/h |
物理备份、增量备份、PITR 恢复、并行备份 |
Redo 日志管理
Redo 日志是 Cantian 持久化的核心,记录所有对数据页面的物理修改,用于崩溃后前滚恢复。
Redo 日志格式(物理 Redo)
Redo 采用物理日志 格式,每条日志记录描述一个页面级别的修改操作。日志类型枚举在 knl_log_type.h:
c
typedef enum en_log_type {
RD_TYPE_END = 0, // 日志结束标记
// 事务相关 Redo
RD_ENTER_PAGE, // 进入页面
RD_LEAVE_PAGE, // 离开页面
// 物理修改 Redo
RD_PHYSICAL_WRITE, // 物理写
RD_PHYSICAL_PUNCH, // 物理打孔(释放空间)
// 逻辑操作 Redo
RD_LOGIC_OPERATION, // 逻辑操作(DDL)
// ... 更多 Redo 类型
} log_type_t;
每条日志通过 log_entry_t 结构承载:
c
typedef struct st_log_entry {
uint32 size; // 日志条目大小
log_type_t type; // 日志类型
uint32 rmid; // 资源管理器 ID(标识所属模块)
uint64 lsn; // 日志序列号
uint64 prev_lsn; // 前一条日志 LSN
uint32 space_id; // 表空间 ID
uint32 file_id; // 文件 ID
uint32 page_id; // 页面 ID
uint16 slot; // 页面内槽位 ID
} log_entry_t;
日志缓冲区(Log Buffer)
日志缓冲区采用两级设计:会话私有缓冲 + 内核公共双缓冲。
会话 1 会话 2 会话 N
│ │ │
[私有的 Log Buffer] [私有的 Log Buffer] [私有的 Log Buffer]
│ │ │
└──────────────┬┴───────────────┘
│
┌─────────▼─────────┐
│ 公共双缓冲 (A/B) │
│ log_dual_buffer_t │
└─────────┬─────────┘
│
LGWR 刷盘
核心结构定义在 knl_log.h:
c
typedef struct st_log_buffer {
spinlock_t lock;
uint32 buf_size; // 缓冲区大小
uint32 write_pos; // 当前写位置
uint32 flush_pos; // 已刷盘位置
uint32 max_pos; // 最大使用位置
char *addr; // 缓冲区地址
} log_buffer_t;
typedef struct st_log_dual_buffer {
log_buffer_t buffers[2]; // 双缓冲 A/B
uint32 active_idx; // 当前活跃缓冲索引
} log_dual_buffer_t;
LGWR 日志写入线程
LGWR(Log Writer)是负责将日志缓冲刷入磁盘文件的后台线程。核心流程:
- 批量化写入 :以
log_batch_t为单位批量写日志 - LSN 推进 :每次成功刷盘后推进
flushed_lsn - 组提交:多个事务的日志合并为一批写入
- 等待唤醒机制:LGWR 在无日志时进入睡眠,由提交事务唤醒
c
typedef struct st_log_batch {
spinlock_t lock;
uint32 size; // 批次大小
log_point_t point; // 日志指针
bool32 flushed; // 是否已刷盘
} log_batch_t;
日志文件组与切换
日志文件管理在 knl_log_file.c:
| 概念 | 说明 |
|---|---|
log_file_t |
单个日志文件,包含文件头、状态、路径 |
log_group_t |
日志文件组(多副本镜像) |
log_file_head_t |
日志文件头部(magic、LSN 范围、reset_log 信息) |
| 日志切换 | 当前文件写满后自动切换到下一文件,触发归档 |
c
typedef struct st_log_file_ctrl {
uint32 asn; // 日志归档序列号
uint32 rst_id; // 重置 ID
uint64 start_lsn; // 起始 LSN
uint64 end_lsn; // 结束 LSN
uint64 block_size; // 块大小
char name[GS_FILE_NAME_BUFFER_SIZE];// 文件名
} log_file_ctrl_t;
typedef struct st_logfile {
log_file_ctrl_t ctrl;
int32 handle; // 文件句柄
logfile_status_t status; // 状态
uint64 size; // 文件大小
uint32 write_pos; // 当前写位置
} logfile_t;
日志 LSN 分配
LSN(Log Sequence Number)是全局单调递增的日志序列号:
- 分配粒度:每事务持有当前最大 LSN
- 全局 LSN :
log_manager_t中curr_lsn字段 - 刷盘 LSN :
flushed_lsn表示已持久化到磁盘的 LSN 上限 - 用途:崩溃恢复起点、主备复制水位、检查点标记
Undo 日志管理
Undo 日志管理代码在 knl_undo.c。
Undo 记录格式
c
typedef enum en_undo_type {
UNDO_INSERT = 1, // INSERT 对应的 Undo
UNDO_DELETE, // DELETE 对应的 Undo
UNDO_UPDATE, // UPDATE 对应的 Undo
UNDO_MULTI_UPDATE, // 多行更新 Undo
UNDO_DML_BATCH, // 批量 DML Undo
} undo_type_t;
typedef struct st_undo_row {
undo_type_t type; // Undo 类型
uint32 rowid_size; // RowID 大小
uint32 data_size; // 数据大小
char *data; // Undo 数据(旧值)
} undo_row_t;
typedef struct st_undo_data {
undo_row_t row; // Undo 行数据
uint32 page_id; // 页面 ID
uint32 slot; // 槽位
uint32 ssn; // SSN
bool32 is_final; // 是否最终 Undo
} undo_data_t;
Undo 段空间管理
Undo 段是独立分配的表空间段,每个段可服务多个事务:
c
typedef struct st_undo_segment {
uint32 seg_id; // 段 ID
uint32 file_id; // 文件 ID
uint32 page_id; // 起始页面
uint32 page_count; // 页面数
undo_seg_stat_t stat; // 段状态
} undo_segment_t;
typedef struct st_undo_set {
spinlock_t lock;
uint32 count; // 段数量
undo_segment_t *segments; // 段数组
} undo_set_t;
typedef struct st_undo_context {
undo_set_t undo_set; // Undo 段集合
undo_stat_t stat; // 全局 Undo 统计
} undo_context_t;
- Undo 表空间:独立的 Undo 表空间,支持多表空间与在线扩充
- 段分配策略:Round-Robin 分配到各段,支持段回收与自动收缩
Undo 生成
| DML 操作 | Undo 类型 | 记录内容 |
|---|---|---|
| INSERT | UNDO_INSERT | 插入行的 RowID(用于回滚时删除) |
| DELETE | UNDO_DELETE | 被删除行的完整旧值 |
| UPDATE | UNDO_UPDATE(或组合 Undo) | 被修改列的旧值 |
| 批量操作 | UNDO_DML_BATCH | 批量 DML 的聚合 Undo 信息 |
Undo 回滚
回滚时按逆序应用 Undo 记录:
- 从事务 Slot 定位最后一条 Undo 记录
- 逐条读取 Undo 数据
- 对于 INSERT Undo → 执行物理删除
- 对于 DELETE Undo → 执行物理插入(恢复旧行)
- 对于 UPDATE Undo → 恢复旧值
- 标记事务为 Aborted
回滚上下文:
c
typedef struct st_rollback_ctx {
bool32 working; // 是否正在回滚
uint32 rollback_num; // 回滚记录数
uint32 rollback_proc; // 已处理记录数
} rollback_ctx_t;
Undo 清理(PURGE)
已提交事务的 Undo 记录不被立即清除,而是由后台 PURGE 进程异步回收:
- 触发条件:事务提交后、Undo 段空间压力
- 清理方式:标记 Undo 页面可复用、重新格式化 Undo 页面
- SMON 职责:SMON 守护进程定期检查并回收过期 Undo 段
检查点(Checkpoint)
检查点代码在 knl_ckpt.c。
检查点触发条件
| 触发类型 | 条件 | 说明 |
|---|---|---|
| 周期触发 | ckpt_interval 参数(默认 3 秒) |
定时器周期性触发 |
| 日志量触发 | 累计日志量超过阈值 | 防止恢复时间过长 |
| 手动触发 | ALTER SYSTEM CHECKPOINT |
DBA 手动执行 |
| 关机触发 | 数据库正常关闭 | 全量检查点 |
| 日志切换触发 | 日志文件切换 | 保证切换时一致性 |
c
typedef enum e_ckpt_mode {
CKPT_MODE_IDLE = 0,
CKPT_MODE_FUZZY, // 模糊检查点(正常模式)
CKPT_MODE_FULL, // 全量检查点
CKPT_MODE_INCREMENTAL, // 增量检查点
} ckpt_mode_t;
脏页刷写策略
检查点将脏页从 Buffer Pool 刷写到数据文件:
- 收集脏页:遍历 BP,按表空间/文件分组
- 排序优化:按页面 ID 排序以减少随机 IO
- 批量写盘:DBWR 线程批量异步写盘
- 双写区保护:先写双写区(Double Write Area),再写正式位置
c
typedef struct st_dbwr_context {
thread_t thread; // DBWR 线程
uint32 batch_size; // 批量大小
uint32 write_speed; // 写速度控制
ckpt_asyncio_ctx_t asyncio_ctx; // 异步 IO 上下文
} dbwr_context_t;
检查点 LSN 推进
- ckpt_lsn:检查点完成时对应的 LSN,恢复从此 LSN 开始
- 推进机制 :检查点线程完成脏页刷写后更新
ckpt_lsn - 心跳 LSN:定期写入控制文件,用于判断实例存活
c
typedef struct st_ckpt_ctx {
thread_t thread; // 检查点线程
uint64 ckpt_lsn; // 当前检查点 LSN
uint64 last_ckpt_time; // 上次检查点时间
ckpt_mode_t mode; // 检查点模式
dbwr_context_t dbwr_ctx; // DBWR 上下文
ckpt_stat_t stat; // 检查点统计
} ckpt_ctx_t;
双写区保护
双写区(Double Write Area)是防止部分页面写失败(torn page)的保护机制:
- 脏页先写入双写区
- 双写区写成功后,再写正式数据文件位置
- 崩溃恢复时,如果正式位置页面不完整,从双写区恢复
检查点与恢复时间关系
- 检查点间隔越短 → 恢复扫描的日志越少 → 恢复越快
- 但过于频繁的检查点 → 增加 IO 负载 → 影响正常事务吞吐
- 通过
ckpt_interval和日志量阈值两个参数平衡
崩溃恢复
崩溃恢复代码在 knl_recovery.c。
事务表 Undo 段 Buffer Pool Redo 日志文件 检查点记录 事务表 Undo 段 Buffer Pool Redo 日志文件 检查点记录 #mermaid-svg-gQU1QmDHBVeREMje{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gQU1QmDHBVeREMje .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gQU1QmDHBVeREMje .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gQU1QmDHBVeREMje .error-icon{fill:#552222;}#mermaid-svg-gQU1QmDHBVeREMje .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gQU1QmDHBVeREMje .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gQU1QmDHBVeREMje .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gQU1QmDHBVeREMje .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gQU1QmDHBVeREMje .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gQU1QmDHBVeREMje .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gQU1QmDHBVeREMje .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gQU1QmDHBVeREMje .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gQU1QmDHBVeREMje .marker.cross{stroke:#333333;}#mermaid-svg-gQU1QmDHBVeREMje svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gQU1QmDHBVeREMje p{margin:0;}#mermaid-svg-gQU1QmDHBVeREMje .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gQU1QmDHBVeREMje text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-gQU1QmDHBVeREMje .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-gQU1QmDHBVeREMje .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-gQU1QmDHBVeREMje .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-gQU1QmDHBVeREMje .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-gQU1QmDHBVeREMje #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-gQU1QmDHBVeREMje .sequenceNumber{fill:white;}#mermaid-svg-gQU1QmDHBVeREMje #sequencenumber{fill:#333;}#mermaid-svg-gQU1QmDHBVeREMje #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-gQU1QmDHBVeREMje .messageText{fill:#333;stroke:none;}#mermaid-svg-gQU1QmDHBVeREMje .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gQU1QmDHBVeREMje .labelText,#mermaid-svg-gQU1QmDHBVeREMje .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-gQU1QmDHBVeREMje .loopText,#mermaid-svg-gQU1QmDHBVeREMje .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-gQU1QmDHBVeREMje .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-gQU1QmDHBVeREMje .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-gQU1QmDHBVeREMje .noteText,#mermaid-svg-gQU1QmDHBVeREMje .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-gQU1QmDHBVeREMje .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gQU1QmDHBVeREMje .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gQU1QmDHBVeREMje .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-gQU1QmDHBVeREMje .actorPopupMenu{position:absolute;}#mermaid-svg-gQU1QmDHBVeREMje .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-gQU1QmDHBVeREMje .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-gQU1QmDHBVeREMje .actor-man circle,#mermaid-svg-gQU1QmDHBVeREMje line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-gQU1QmDHBVeREMje :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} === 阶段一:分析(Analysis) === === 阶段二:前滚(Redo/Replay) === === 阶段三:回滚(Undo) === 读取检查点 LSN 从 ckpt_lsn 开始顺序扫描 收集所有脏页信息 构建事务状态表 (识别未完成事务) 从 ckpt_lsn 到崩溃 LSN 逐条重放 Redo 日志 恢复所有页面到最新状态 获取未完成事务列表 按逆序回滚未提交事务 标记所有事务处理完毕
恢复阶段:分析(Analysis)
分析阶段的核心任务:
- 定位检查点 LSN :从控制文件读取
ckpt_lsn - 扫描 Redo 日志 :从
ckpt_lsn开始扫描所有在线日志文件 - 收集脏页信息 :建立页面恢复哈希表(
rcy_page_bucket_t) - 构建事务状态表:识别所有活动事务,区分已提交与未完成
c
typedef struct st_rcy_context {
uint64 ckpt_lsn; // 检查点 LSN
uint64 rcy_lsn; // 当前恢复 LSN
uint64 max_lsn; // 最大日志 LSN
rcy_paral_group_t paral_group; // 并行恢复分组
rcy_page_bucket_t page_buckets[]; // 页面哈希桶
} rcy_context_t;
前滚(Redo)
从检查点 LSN 到崩溃 LSN 之间的所有 Redo 日志逐条重放:
- PAGE 级别过滤:同一页面的多次修改合并
- 并行恢复:按文件/页面分桶,多线程并行重放
- 页面加载:如果页面不在 BP 中,从数据文件加载
- LSN 校验:重放前比对页面 LSN,防止重复恢复
c
typedef struct st_rcy_page {
uint32 space_id;
uint32 file_id;
uint32 page_id;
uint64 lsn;
uint32 buf_id;
bool32 dirty;
} rcy_page_t;
未完成事务回滚
分析阶段识别出的未完成事务在前滚完成后进行回滚:
- 遍历事务表,找到状态为 ACTIVE 的事务
- 读取每个事务的 Undo 链
- 逆序逐条回滚 Undo 记录
- 释放事务资源
恢复过程中的资源管理
- 内存管理:恢复期间需要额外内存存储页面哈希表和事务表
- 锁管理:恢复期间不持有正常业务锁,避免阻塞
- 日志管理:恢复不产生新的 Redo,但会更新控制文件
归档管理
归档代码在 knl_archive.c。
归档模式 vs 非归档
| 模式 | 配置 | 行为 | 适用场景 |
|---|---|---|---|
| ARCHIVELOG | LOG_ARCHIVE_DEST 已配置 |
在线日志写满后自动归档 | 生产环境、需要 PITR |
| NOARCHIVELOG | 未配置归档目标 | 日志循环覆盖,不保留历史 | 测试环境、可接受数据丢失 |
归档日志文件命名
归档日志文件名包含关键元信息:
arch_<db_id>_<rst_id>_<asn>.log
db_id:数据库 IDrst_id:重置 ID(每次 OPEN RESETLOGS 递增)asn:归档序列号(全局递增)
c
typedef struct st_arch_file {
uint32 asn; // 归档序列号
uint32 rst_id; // 重置 ID
uint64 start_lsn; // 起始 LSN
uint64 end_lsn; // 结束 LSN
char name[GS_FILE_NAME_BUFFER_SIZE];
} arch_file_t;
归档发送线程
归档支持多目标(本地 + 远程):
c
typedef enum en_arch_dest_state {
ARCH_DEST_VALID = 0,
ARCH_DEST_DEFER, // 暂停
ARCH_DEST_FAILURE, // 故障
} arch_dest_state_t;
typedef struct st_arch_proc_context {
thread_t thread; // 归档线程
uint32 dest_id; // 目标 ID
arch_dest_state_t state; // 目标状态
uint64 last_asn; // 最后完成的 ASN
uint32 fail_count; // 失败计数
} arch_proc_context_t;
归档空间管理
- 归档空间配额 :
ARCH_CLEAN_ATTR管理归档文件保留策略 - 自动清理:超出保留时间/数量的归档文件自动删除
- 空间告警:归档空间不足时发出告警,必要时暂停日志切换
备份与恢复
备份代码在 kernel/backup/。
物理备份
物理备份直接复制数据文件到备份目录:
- 全量备份:备份全部数据文件、控制文件、归档日志
- 一致性要求 :备份期间需要将表空间置于备份模式(
BEGIN BACKUP) - 并行备份 :多线程并行拷贝,提高备份速度(
bak_paral.c)
增量备份
增量备份只备份自上次备份以来变更的数据块:
- 差异增量:基于上次全量备份的变化
- 累积增量:基于上次增量备份的变化
- 块变更追踪:通过日志或位图追踪变更块
基于时间点恢复(PITR)
PITR 使用备份集 + 归档日志恢复到指定时间点:
- 恢复数据文件:从备份集还原全量数据文件
- 应用归档日志:从归档日志顺序重放到目标时间点/SCN
- OPEN RESETLOGS:重置日志序列,创建新化身(Incarnation)
备份一致性
- 备份开始 SCN:记录备份开始时的全局 SCN
- 备份结束 SCN:记录备份结束时的全局 SCN
- 归档日志范围:备份期间产生的归档日志一并保存
- 交叉校验:恢复前校验备份集完整性
日志复制(Redo Replication)
日志复制代码在 kernel/replication/。
#mermaid-svg-N5oKvcWFVKfvxdvD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-N5oKvcWFVKfvxdvD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-N5oKvcWFVKfvxdvD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-N5oKvcWFVKfvxdvD .error-icon{fill:#552222;}#mermaid-svg-N5oKvcWFVKfvxdvD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-N5oKvcWFVKfvxdvD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-N5oKvcWFVKfvxdvD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-N5oKvcWFVKfvxdvD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-N5oKvcWFVKfvxdvD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-N5oKvcWFVKfvxdvD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-N5oKvcWFVKfvxdvD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-N5oKvcWFVKfvxdvD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-N5oKvcWFVKfvxdvD .marker.cross{stroke:#333333;}#mermaid-svg-N5oKvcWFVKfvxdvD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-N5oKvcWFVKfvxdvD p{margin:0;}#mermaid-svg-N5oKvcWFVKfvxdvD .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-N5oKvcWFVKfvxdvD .cluster-label text{fill:#333;}#mermaid-svg-N5oKvcWFVKfvxdvD .cluster-label span{color:#333;}#mermaid-svg-N5oKvcWFVKfvxdvD .cluster-label span p{background-color:transparent;}#mermaid-svg-N5oKvcWFVKfvxdvD .label text,#mermaid-svg-N5oKvcWFVKfvxdvD span{fill:#333;color:#333;}#mermaid-svg-N5oKvcWFVKfvxdvD .node rect,#mermaid-svg-N5oKvcWFVKfvxdvD .node circle,#mermaid-svg-N5oKvcWFVKfvxdvD .node ellipse,#mermaid-svg-N5oKvcWFVKfvxdvD .node polygon,#mermaid-svg-N5oKvcWFVKfvxdvD .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-N5oKvcWFVKfvxdvD .rough-node .label text,#mermaid-svg-N5oKvcWFVKfvxdvD .node .label text,#mermaid-svg-N5oKvcWFVKfvxdvD .image-shape .label,#mermaid-svg-N5oKvcWFVKfvxdvD .icon-shape .label{text-anchor:middle;}#mermaid-svg-N5oKvcWFVKfvxdvD .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-N5oKvcWFVKfvxdvD .rough-node .label,#mermaid-svg-N5oKvcWFVKfvxdvD .node .label,#mermaid-svg-N5oKvcWFVKfvxdvD .image-shape .label,#mermaid-svg-N5oKvcWFVKfvxdvD .icon-shape .label{text-align:center;}#mermaid-svg-N5oKvcWFVKfvxdvD .node.clickable{cursor:pointer;}#mermaid-svg-N5oKvcWFVKfvxdvD .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-N5oKvcWFVKfvxdvD .arrowheadPath{fill:#333333;}#mermaid-svg-N5oKvcWFVKfvxdvD .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-N5oKvcWFVKfvxdvD .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-N5oKvcWFVKfvxdvD .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N5oKvcWFVKfvxdvD .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-N5oKvcWFVKfvxdvD .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N5oKvcWFVKfvxdvD .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-N5oKvcWFVKfvxdvD .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-N5oKvcWFVKfvxdvD .cluster text{fill:#333;}#mermaid-svg-N5oKvcWFVKfvxdvD .cluster span{color:#333;}#mermaid-svg-N5oKvcWFVKfvxdvD div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-N5oKvcWFVKfvxdvD .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-N5oKvcWFVKfvxdvD rect.text{fill:none;stroke-width:0;}#mermaid-svg-N5oKvcWFVKfvxdvD .icon-shape,#mermaid-svg-N5oKvcWFVKfvxdvD .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-N5oKvcWFVKfvxdvD .icon-shape p,#mermaid-svg-N5oKvcWFVKfvxdvD .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-N5oKvcWFVKfvxdvD .icon-shape .label rect,#mermaid-svg-N5oKvcWFVKfvxdvD .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-N5oKvcWFVKfvxdvD .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-N5oKvcWFVKfvxdvD .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-N5oKvcWFVKfvxdvD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 备库 Standby
网络传输
主库 Primary
日志批次
LGWR 写日志
log_send
Redo 发送线程
Raft 共识模块
TCP/UDP/UCX
log_recv
Redo 接收线程
log_replay
Redo 回放线程
GBP 全局缓冲池
Redo 发送
repl_log_send.c 实现日志发送:
c
typedef struct st_lsnd {
thread_t thread; // 发送线程
uint32 dest_id; // 目标备库 ID
uint64 send_lsn; // 已发送 LSN
uint64 flush_lsn; // 备库已刷盘 LSN
lsnd_status_t status; // 发送状态
uint32 comp_threshold; // 压缩阈值
} lsnd_t;
typedef struct st_lsnd_context {
spinlock_t lock;
lsnd_t lsnd[MAX_LSND_COUNT]; // 多目标发送
uint32 count; // 活跃目标数
} lsnd_context_t;
- 多目标发送:支持向多个备库并发发送
- 压缩传输:大日志批次自动压缩
- 同步/异步模式 :
LOG_SYNC_MODE控制主备同步级别
Redo 接收
repl_log_recv.c 实现日志接收:
c
typedef struct st_lrcv_context {
thread_t thread; // 接收线程
uint64 recv_lsn; // 已接收 LSN
uint64 write_lsn; // 已写入 LSN
uint64 flush_lsn; // 已刷盘 LSN
log_buffer_t recv_buf; // 接收缓冲
} lrcv_context_t;
Redo 回放
repl_log_replay.c 实现日志回放:
- 并行回放:按页面分区并行回放日志
- 延迟控制 :支持
APPLY_DELAY参数延迟回放 - 读取一致性:回放过程中备库可对外提供只读服务
主备日志一致性
- Raft 共识 :
repl_raft.c实现 Raft 协议保障日志一致性 - 日志位置确认:主库通过 Heartbeat 确认备库已收到日志
- ABR 块修复 :
knl_abr.c自动从备库修复坏块 - GBP 全局缓冲 :
knl_gbp.c跨节点页面共享
复制延迟
复制延迟 = 主库当前 LSN - 备库已应用 LSN:
send_latency:发送环节延迟replay_latency:回放环节延迟apply_lag:总延迟(send_latency + replay_latency)- 可通过动态视图查询:
SELECT * FROM DV_REPLICATION_STATUS
关键数据结构与代码入口
核心数据结构
| 数据结构 | 定义位置 | 用途 |
|---|---|---|
log_manager_t |
knl_log.h | 日志管理器(全局 LSN、日志组、双缓冲) |
log_batch_t |
knl_log.h | 日志批次(组提交基本单元) |
log_entry_t |
knl_log.h | 单条日志条目 |
log_buffer_t |
knl_log.h | 日志缓冲区 |
logfile_t |
knl_log.h | 日志文件 |
ckpt_ctx_t |
knl_ckpt.h | 检查点上下文 |
rcy_context_t |
knl_recovery.h | 恢复上下文 |
arch_proc_context_t |
knl_archive.h | 归档线程上下文 |
undo_context_t |
knl_undo.h | Undo 上下文 |
undo_segment_t |
knl_undo.h | Undo 段 |
undo_data_t |
knl_undo.h | Undo 数据 |
txn_t |
knl_tran.h | 事务描述符 |
lsnd_t |
repl_log_send.h | 日志发送端 |
lrcv_context_t |
repl_log_recv.h | 日志接收端 |
lrpl_context_t |
repl_log_replay.h | 日志回放上下文 |
关键代码入口
| 功能 | 文件 | 入口函数 |
|---|---|---|
| 日志写入 | knl_log.c | log_append_entry() → log_write() |
| LGWR 线程 | knl_log.c | lgwr_proc() |
| 检查点线程 | knl_ckpt.c | ckpt_proc() |
| 崩溃恢复 | knl_recovery.c | rcy_recover() → rcy_analysis() → rcy_redo() → rcy_undo() |
| 归档线程 | knl_archive.c | arch_proc() |
| ABR 块修复 | knl_abr.c | abr_repair_page() |
| Undo 生成 | knl_undo.c | undo_write() |
| Undo 回滚 | knl_undo.c | undo_rollback() |
| Redo 发送 | repl_log_send.c | lsnd_proc() |
| Redo 接收 | repl_log_recv.c | lrcv_proc() |
| Redo 回放 | repl_log_replay.c | lrpl_proc() |
| 物理备份 | knl_backup.c | bak_backup() |
| PITR 恢复 | bak_restore.c | bak_restore() |
| DDL 逻辑日志 | knl_ddl_log_file.c | ddl_log_append() |
总结
Cantian 持久化与恢复体系以 Redo 日志为核心,通过 Log Buffer 双缓冲设计 + LGWR 组提交批量刷盘机制保证已提交事务不丢失。Undo 日志按 INSERT/DELETE/UPDATE 分类记录前镜像,采用 Round-Robin 段分配策略,回滚时逆序应用逆向操作。Checkpoint 通过模糊/全量/增量三种模式控制脏页刷写时机,双写区防止页断裂。崩溃恢复遵循 Analysis→Redo→Undo 三阶段流程:Analysis 扫描 Redo 构建脏页表与事务表,Redo 从 ckpt_lsn 前滚到崩溃点,Undo 回滚未完成事务。归档管理在 ARCHIVELOG 模式下保留在线日志历史,支持 PITR 时间点恢复。日志复制通过 log_send→log_recv→log_replay 三线程架构与 Raft 共识协议保障主备数据一致性。
关键源码索引
| 文件路径 | 关键函数 | 功能说明 |
|---|---|---|
pkg/src/kernel/persist/knl_log.c |
log_append_entry() / log_write() / lgwr_proc() |
Redo 日志写入与 LGWR 线程 |
pkg/src/kernel/persist/knl_log.h |
log_manager_t / log_batch_t / log_buffer_t |
日志管理器核心数据结构 |
pkg/src/kernel/persist/knl_ckpt.c |
ckpt_proc() |
Checkpoint 检查点线程主循环 |
pkg/src/kernel/persist/knl_recovery.c |
rcy_recover() → rcy_analysis() → rcy_redo() → rcy_undo() |
崩溃恢复三阶段核心 |
pkg/src/kernel/persist/knl_archive.c |
arch_proc() |
归档日志管理线程 |
pkg/src/kernel/persist/knl_abr.c |
abr_repair_page() |
自动块修复 ABR |
pkg/src/kernel/persist/knl_log_file.c |
log_file_switch() |
日志文件组管理与切换 |
pkg/src/kernel/persist/knl_ddl_log_file.c |
ddl_log_append() |
DDL 逻辑日志 |
pkg/src/kernel/xact/knl_undo.c |
undo_write() / undo_rollback() |
Undo 生成与回滚 |
pkg/src/kernel/replication/repl_log_send.c |
lsnd_proc() |
Redo 日志发送线程 |
pkg/src/kernel/replication/repl_log_recv.c |
lrcv_proc() |
Redo 日志接收线程 |
pkg/src/kernel/replication/repl_log_replay.c |
lrpl_proc() |
Redo 日志回放线程 |
pkg/src/kernel/backup/knl_backup.c |
bak_backup() |
物理备份执行 |
pkg/src/kernel/backup/bak_restore.c |
bak_restore() |
PITR 恢复执行 |