目录

[TOC]

一致性状态机

对于一个无限增长的序列a[1, 2, 3…], 如果对于任意整数i, a[i]的值满足分布式一致性,这个系统就满足一致性状态机的要求。

基本上所有的系统都会有源源不断的操作, 这时候单独对某个特定的值达成一致是不够的。为了真实系统保证所有的副本的一致性,通常会把操作转化为write-ahead-log(简称WAL). 然后让系统的所有副本对WAL保持一致, 这样每个进程按照顺序执行WAL里的操作,就能保证最终的状态是一致的。

预写日志记录(Write-Ahead Logging, WAL)

在计算机科学中,预写日志记录 (WAL) 是一系列技术,用于在数据库系统中提供原子性和持久性(ACID 的两个属性)

预写日志是一种仅用于附加的辅助磁盘驻留结构,用于崩溃和事务恢复。更改首先记录在日志中,必须先将日志写入稳定存储,然后再将更改写入数据库

让我们通过一个简单的银行转账例子来理解WAL的重要性:

  1. Alice账户余额:1000元
  2. Bob账户余额:500元

不使用WAL的情况:

1. 从Alice账户扣除100元 (Alice: 900元)
2. 系统崩溃
3. Bob没有收到钱,但Alice的钱已经扣除!

使用WAL的情况:

1. 写入日志:[事务开始, Alice扣款100元, Bob收款100元]
2. 系统确认日志写入磁盘
3. 执行实际操作:
   - 从Alice账户扣除100元
   - 给Bob账户增加100元
4. 写入日志:[事务完成]

如果在任何步骤发生崩溃,系统都可以通过查看日志来恢复正确状态。

主要功能

预写日志的主要功能可以概括为:

    工作原理

    详细的工作流程

    1. 事务开始

      -- 示例:转账操作
      BEGIN TRANSACTION;
      UPDATE accounts SET balance = balance - 100 WHERE user = 'Alice';
      UPDATE accounts SET balance = balance + 100 WHERE user = 'Bob';
      COMMIT;
      
    2. 日志记录生成

      LSN: 1001
      TransID: T123
      Type: BEGIN
      Timestamp: 2024-03-21 10:00:00
      
      LSN: 1002
      TransID: T123
      Type: UPDATE
      Table: accounts
      Before: {user: 'Alice', balance: 1000}
      After: {user: 'Alice', balance: 900}
      
      LSN: 1003
      TransID: T123
      Type: UPDATE
      Table: accounts
      Before: {user: 'Bob', balance: 500}
      After: {user: 'Bob', balance: 600}
      
      LSN: 1004
      TransID: T123
      Type: COMMIT
      
    3. 恢复示例

      如果系统在LSN 1002后崩溃:

      1. 分析日志:发现未完成的事务T123
      2. 回滚操作:将Alice的余额恢复到1000
      3. 记录回滚日志(CLR)
      

    日志记录格式

    典型的WAL日志记录包含以下字段:

    • LSN (Log Sequence Number): 唯一标识日志记录
    • 事务ID:标识产生该日志记录的事务
    • 操作类型:INSERT、UPDATE、DELETE等
    • 表ID:受影响的表标识
    • 前镜像:修改前的数据
    • 后镜像:修改后的数据
    • 时间戳:日志记录的创建时间

    示例说明

    想象一下,当运行它的计算机断电时,一个程序正在执行某些操作。重新启动后,该程序可能需要知道它正在执行的操作是成功、部分成功还是失败。如果使用预写日志,程序可以检查此日志,并将意外断电时应该做什么与实际做什么进行比较。基于这种比较,程序可以决定撤消它已开始的内容、完成已开始的内容或保持原样

    图解检查点过程

    sequenceDiagram participant DB as 数据库 participant WAL as WAL日志 participant Disk as 磁盘 Note over DB,Disk: 正常操作 DB->>WAL: 写入操作日志 WAL->>Disk: 定期刷新日志 Note over DB,Disk: 检查点开始 DB->>DB: 暂停新事务(精确检查点) DB->>Disk: 将脏页写入磁盘 DB->>WAL: 记录检查点位置 DB->>DB: 恢复处理新事务

    时间点1: 系统正常运行
    - WAL大小:1GB
    - 脏页数量:1000页
    - 活跃事务:10个
    
    时间点2: 触发检查点
    - 将1000个脏页写入磁盘
    - 记录10个活跃事务的状态
    - 标记检查点位置
    
    时间点3: 检查点完成
    - 可以清理旧的WAL日志
    - 系统恢复只需要从该检查点开始
    

    检查点类型

    1. 模糊检查点:允许在检查点过程中继续处理新的事务
    2. 精确检查点:暂停所有新事务直到检查点完成
    3. 增量检查点:将脏页分批写入磁盘,减少I/O峰值

    WAL vs 其他方案

    WAL 允许就地完成数据库的更新。实现原子更新的另一种方法是使用影子分页,它不是就地的。就地更新的主要优点是它减少了修改索引和阻止列表的需要。

    WAL的优势

    1. 写入性能更好(顺序写入)
    2. 空间利用率高
    3. 实现相对简单
    4. 支持细粒度的并发控制

    WAL的劣势

    1. 需要定期执行检查点操作
    2. 恢复时间可能较长
    3. 日志管理的开销
    4. 写放大问题

    应用场景

    数据库系统

    1. PostgreSQL
    2. SQLite
    3. MySQL (InnoDB引擎)

    分布式系统

    1. 分布式一致性协议(如Raft)
    2. 消息队列系统
    3. 分布式缓存

    文件系统

    1. 日志结构文件系统
    2. 事务性文件系统

    ARIES算法

    ARIES (Algorithms for Recovery and Isolation Exploiting Semantics) 是WAL最著名的实现之一,是IBM研究院开发的一种数据库恢复算法。

    核心原则

    1. 写前日志(Write-Ahead Logging)
      • 所有修改必须先写入日志,再写入数据页
      • 日志记录必须在事务提交前写入稳定存储
    2. 重复历史(Repeating History)
      • 系统崩溃后的恢复过程必须重做所有操作
      • 即使某些事务最终会被回滚,也需要先重做
    3. 存储前后镜像(Store Before- and After-Images)
      • 每个更新操作都记录修改前的值(前镜像)和修改后的值(后镜像)
      • 用于支持事务回滚和恢复操作

    日志记录类型

    1. 更新日志记录

      
      
    2. 补偿日志记录(CLR)

      <LSN, TransID, PageID, UndoNextLSN, RedoInfo>
      

        详细恢复步骤

        1. 分析阶段(Analysis Pass)
          • 从最后一个检查点开始扫描日志
          • 重建事务表(Transaction Table)和脏页表(Dirty Page Table)
          • 确定活跃事务集合和脏页集合
          • 确定RedoLSN(重做起点)
        2. 重做阶段(Redo Pass)
          • 从RedoLSN开始扫描日志
          • 对所有日志记录(包括CLR)进行重做
          • 重做条件:
            • 页面LSN < 日志记录LSN
            • 页面在脏页表中或当前LSN >= RecLSN
        3. 回滚阶段(Undo Pass)
          • 回滚所有分析阶段确定的未完成事务
          • 从后向前处理,生成补偿日志记录(CLR)
          • 按照UndoNextLSN链进行回滚

        graph TD A[系统崩溃/启动恢复] --> B[分析阶段] B --> C[重建事务表和脏页表] C --> D[确定RedoLSN] D --> E[重做阶段] E --> F{是否需要重做?} F -->|是| G[重做操作] F -->|否| H[跳过] G --> I[下一个日志记录] H --> I I --> J{日志结束?} J -->|否| F J -->|是| K[回滚阶段] K --> L[处理未完成事务] L --> M{需要回滚?} M -->|是| N[生成CLR并回滚] M -->|否| O[恢复完成] N --> P[更新UndoNextLSN] P --> L

        示例场景

        假设有以下日志记录序列:

        LSN TransID Type PrevLSN PageID Data

        100 T1 BEGIN NULL - -

        110 T1 UPDATE 100 P1 x=10->20

        120 T2 BEGIN NULL - -

        130 T2 UPDATE 120 P2 y=5->15

        140 T1 COMMIT 110 - -

        如果在LSN 135时系统崩溃:

        1. 分析阶段:
          • T1处于提交状态
          • T2处于活跃状态
          • P1和P2在脏页表中
        2. 重做阶段:
          • 重做LSN 110和130的更新操作
        3. 回滚阶段:
          • 只需回滚T2的操作
          • 生成CLR回滚LSN 130的操作

        最佳实践

        1. 日志管理
          • 合理配置日志大小
          • 实现日志压缩/归档机制
          • 监控日志增长速度
        2. 性能优化
          • 批量写入日志
          • 异步刷盘策略
          • 使用预写缓冲区
        3. 监控与维护
          • 监控检查点频率
          • 跟踪日志空间使用
          • 定期验证恢复功能

        Python实现简单的WAL

        这里的实现比较简单,并且有些逻辑上的漏洞

        1、只考虑了update的情况

        2、commit是假的commit,只要进行了update都会写wal.log,所以就都认为是一定需要recover的,没有区分undo.log和redo.log,可以留着有空再改进

        一些疑问

        归档之前记录检查点

        在归档之前记录检查点是WAL机制中的一个重要设计,主要有以下几个原因:
        1. 确保数据一致性

        def create_checkpoint(self) -> Dict[str, Any]:
            """创建检查点,标记当前系统的一致性状态"""
            checkpoint = {
                "timestamp": time.time(),
                "lsn": self.current_lsn,      # 记录当前的LSN
                "records_count": self.records_count
            }
            # ... 保存检查点信息 ...
        
        1. 优化恢复过程
        • 没有检查点的情况:
        • 有检查点的情况
        sequenceDiagram participant DB as 数据库 participant WAL as WAL日志 participant Disk as 磁盘 Note over DB,Disk: 系统崩溃后重启 DB->>WAL: 读取最近的检查点 WAL->>Disk: 只需读取检查点之后的日志 Note over DB: 只需重放检查点之后的操作 Note over DB: 显著提高恢复效率
        1. 安全归档的前提
        def archive_logs(self, checkpoint_lsn: int):
            """归档检查点之前的日志"""
            # 1. 检查点确保了这些数据已经写入磁盘
            # 2. 检查点之前的日志可以安全归档
            for record in records:
                if record.lsn <= checkpoint_lsn:  # 检查点之前的记录
                    archive_records.append(record)
                else:
                    keep_records.append(record)
        
        # 检查点提供了安全归档的界限
        if self.records_count >= self.checkpoint_size:
            checkpoint = self.create_checkpoint()  # 先创建检查点
            self.archive_logs(checkpoint["lsn"])  # 再根据检查点LSN归档
        
        1. 数据库恢复的保障
        def recover_from_wal(self):
            """从WAL恢复数据库状态"""
            # 1. 找到最近的检查点
            checkpoint = self.find_latest_checkpoint()
            
            # 2. 只需要处理检查点之后的日志
            records = self.wal.recover_with_archives()
            recent_records = [r for r in records if r.lsn > checkpoint["lsn"]]
            
            # 3. 重放操作
            for record in recent_records:
                if record.operation == "UPDATE":
                    self.apply_update(record)
        

        检查点机制在归档前是必要的,因为它

        1. 标记了一个已知的一致性状态
        2. 提供了安全归档的边界
        3. 优化了恢复流程
        4. 保证了数据的完整性
        5. 提供了系统状态的快照
        6. 支持增量备份和恢复

        这就是在WAL中归档操作总是与检查点机制配合使用

        设计

        prompt

        请你结合database.py和wal.py实现,用mermaid语法绘制1. 展示系统的静态结构
        2. 展示更新操作的流程
        3. 展示恢复操作的流程:
        4. 展示WAL记录的生命周期
        

        1. 展示系统的静态结构

        classDiagram class Database { -Dict data -WAL wal +update(trans_id, table, key, value) +get(table, key) +commit() +recover_from_wal() } class WAL { -str log_path -int current_lsn -int checkpoint_size -str archive_dir -str checkpoint_dir -int records_count +append(trans_id, operation, table, before, after) +flush() +recover() +recover_with_archives() +create_checkpoint() +archive_logs(checkpoint_lsn) } class WALRecord { +int lsn +str trans_id +str operation +str table +Dict before +Dict after +float timestamp +to_dict() +from_dict() } Database --> WAL : uses WAL --> WALRecord : creates

        2. 展示更新操作的流程

        sequenceDiagram participant Client participant Database participant WAL participant Disk Client->>Database: update(trans_id, table, key, value) Database->>WAL: append(trans_id, "UPDATE", table, before, after) WAL->>Disk: 写入WAL记录 Disk-->>WAL: 确认写入 WAL-->>Database: 返回LSN Database->>Database: 更新内存数据 alt records_count >= checkpoint_size WAL->>WAL: create_checkpoint() WAL->>Disk: 写入检查点 WAL->>WAL: archive_logs() WAL->>Disk: 写入归档文件 WAL->>WAL: 清理WAL文件 end Database-->>Client: 完成更新

        3. 展示恢复操作的流程:

        sequenceDiagram participant Database participant WAL participant Archive participant Checkpoint participant WALFile Database->>WAL: recover_from_wal() WAL->>Archive: 读取最新归档状态 Archive-->>WAL: 返回归档状态 WAL->>Checkpoint: 查找最新检查点 Checkpoint-->>WAL: 返回检查点信息 WAL->>WALFile: 读取增量记录 WALFile-->>WAL: 返回WAL记录 WAL->>WAL: 合并状态 WAL->>Database: 返回恢复记录 Database->>Database: 重建内存数据

        4. 展示WAL记录的生命周期:

        stateDiagram-v2 [*] --> 活跃记录: 写入WAL文件 活跃记录 --> 检查点前记录: 达到checkpoint_size 检查点前记录 --> 归档状态: archive_logs() 归档状态 --> 最终状态: 合并到归档文件 活跃记录 --> 恢复数据: 系统重启 归档状态 --> 恢复数据: 系统重启 恢复数据 --> [*]: 加载到内存 note right of 活跃记录: WAL文件中的记录 note right of 检查点前记录: LSN <= checkpoint_lsn note right of 归档状态: 转换为最终数据状态 note right of 最终状态: state_{timestamp}.json

        代码

        database.py

        from typing import Dict, Any
        from wal import WAL
        import os
        
        class Database:
            """简单的内存数据库实现,支持WAL机制
            
            主要功能:
            1. 基本的键值对存储
            2. 使用WAL确保数据持久性
            3. 系统重启时的数据恢复
            """
            def __init__(self, wal_path: str, checkpoint_dir=None):
                """初始化数据库
                
                Args:
                    wal_path: WAL日志文件路径
                    checkpoint_dir: checkpoint目录路径(可选)
                """
                self.wal_path = wal_path
                self.checkpoint_dir = checkpoint_dir or os.path.dirname(wal_path)
                self.data: Dict[str, Dict[str, Any]] = {}  # 内存中的数据存储
                self.wal = WAL(wal_path)  # WAL管理器
                self.recover_from_wal()  # 启动时从WAL恢复数据
            
            def recover_from_wal(self):
                """从WAL(包括归档)恢复数据库状态"""
                # 使用recover_with_archives替代recover
                records = self.wal.recover_with_archives()
                for record in records:
                    if record.operation == "UPDATE":
                        if record.table not in self.data:
                            self.data[record.table] = {}
                        self.data[record.table].update(record.after)
            
            def update(self, trans_id: str, table: str, key: str, value: Any):
                """更新数据库中的值
                
                遵循WAL原则:先写日志,再更新数据
                
                Args:
                    trans_id: 事务ID
                    table: 表名
                    key: 键
                    value: 值
                """
                if table not in self.data:
                    self.data[table] = {}
                
                # 记录修改前的值
                before = {key: self.data[table].get(key)}
                after = {key: value}
                
                # 先写日志
                self.wal.append(trans_id, "UPDATE", table, before, after)
                
                # 再更新数据
                self.data[table][key] = value
            
            def get(self, table: str, key: str) -> Any:
                """获取数据库中的值
                
                Args:
                    table: 表名
                    key: 键
                    
                Returns:
                    对应的值,如果不存在则返回None
                """
                return self.data.get(table, {}).get(key)
            
            def commit(self):
                """提交事务,确保日志写入磁盘"""
                self.wal.flush()
        

        wal.py

        import os
        import json
        import time
        from typing import Dict, Any, List, Optional
        from dataclasses import dataclass, asdict, field
        @dataclass
        class WALRecord:
            """使用dataclass简化日志记录类的定义"""
            lsn: int
            trans_id: str
            operation: str
            table: str
            before: Dict[str, Any]
            after: Dict[str, Any]
            timestamp: float = field(default_factory=time.time)
        
            def to_dict(self) -> Dict[str, Any]:
                return asdict(self)
        
            @classmethod
            def from_dict(cls, data: Dict[str, Any]) -> 'WALRecord':
                return cls(**data)
        
        class WAL:
            """WAL管理器"""
            def __init__(self, log_path: str):
                """初始化WAL管理器
                
                Args:
                    log_path: 日志文件路径
                """
                self.log_path = log_path
                self.current_lsn = 0
                self.checkpoint_size = 200  # 修改为较小的值以便测试
                # 将archive_dir和checkpoint_dir放在log_path同级目录下
                base_dir = os.path.dirname(log_path)
                self.archive_dir = os.path.join(base_dir, "archive")
                self.checkpoint_dir = os.path.join(base_dir, "checkpoint")
                self.records_count = 0
                
                # 确保目录存在
                self._init_directories()
                
                # 创建文件(如果不存在)
                if not os.path.exists(self.log_path):
                    with open(self.log_path, 'w') as f:
                        pass
                
                self._init_records_count()
            
            def _write_to_log(self, data: str):
                """同步写入日志文件
                
                Args:
                    data: 要写入的数据
                """
                with open(self.log_path, 'a') as f:
                    f.write(data + '\n')
                    f.flush()
                    os.fsync(f.fileno())
            
            def append(self, trans_id: str, operation: str, 
                      table: str, before: Dict[str, Any], after: Dict[str, Any]) -> int:
                """追加新的日志记录"""
                self.current_lsn += 1
                record = WALRecord(self.current_lsn, trans_id, operation, 
                                    table, before, after)
                
                # 同步写入
                self._write_to_log(json.dumps(record.to_dict()))
                
                # 更新计数并检查是否需要创建检查点
                self.records_count += 1
                if self.records_count >= self.checkpoint_size:
                    self._create_checkpoint_and_archive()
                
                return self.current_lsn
            
            def flush(self):
                """刷新日志到磁盘(现在是空操作,因为每次写入都是同步的)"""
                pass
        
            def recover(self) -> List[WALRecord]:
                """从日志文件中恢复记录"""
                try:
                    # 首先检查最新的检查点
                    checkpoint = self._find_latest_checkpoint()
                    start_lsn = checkpoint["lsn"] if checkpoint else 0
                    
                    # 获取归档的状态
                    final_state = {}
                    archive_files = sorted([f for f in os.listdir(self.archive_dir) 
                                         if f.startswith("state_") and f.endswith(".json")])
                    if archive_files:
                        latest_archive = archive_files[-1]
                        with open(os.path.join(self.archive_dir, latest_archive)) as f:
                            archive_data = json.load(f)
                            final_state = archive_data["state"]
                    
                    # 从检查点开始读取增量记录
                    with open(self.log_path, 'r') as f:
                        for line in f:
                            if line.strip():
                                record = WALRecord.from_dict(json.loads(line))
                                if record.lsn > start_lsn:
                                    # 更新状态
                                    if record.operation == "UPDATE":
                                        if record.table not in final_state:
                                            final_state[record.table] = {}
                                        final_state[record.table].update(record.after)
                    
                    # 将最终状态转换为WAL记录
                    result_records = []
                    for table, table_data in final_state.items():
                        for key, value in table_data.items():
                            record = WALRecord(
                                lsn=self.current_lsn + 1,
                                trans_id="RECOVERY",
                                operation="UPDATE",
                                table=table,
                                before={},
                                after={key: value}
                            )
                            result_records.append(record)
                    
                    return result_records
                
                except Exception as e:
                    print("Error during recovery: {}".format(e))
                    return []
        
            def _create_checkpoint_and_archive(self):
                """异步创建检查点和归档"""
                try:
                    checkpoint = self.create_checkpoint()
                    self.archive_logs(checkpoint["lsn"])
                    self.records_count = 0
                except Exception as e:
                    print(f"Error during checkpoint/archive: {e}")
            
            def create_checkpoint(self) -> Dict[str, Any]:
                """创建检查点,包含更多信息"""
                checkpoint = {
                    "timestamp": time.time(),
                    "lsn": self.current_lsn,
                    "records_count": self.records_count,
                    "pending_writes": 0
                }
                
                # 在checkpoint_dir下创建checkpoint文件
                checkpoint_path = os.path.join(self.checkpoint_dir, 
                                             "checkpoint_{}.json".format(checkpoint['lsn']))
                
                with open(checkpoint_path, 'w') as f:
                    json.dump(checkpoint, f)
                
                return checkpoint
            
            def _find_latest_checkpoint(self) -> Optional[Dict[str, Any]]:
                """查找最新的检查点"""
                # 在checkpoint_dir下查找checkpoint文件
                checkpoint_files = [f for f in os.listdir(self.checkpoint_dir) 
                                  if f.startswith("checkpoint_")]
                
                if not checkpoint_files:
                    return None
                
                latest_checkpoint = max(checkpoint_files)
                with open(os.path.join(self.checkpoint_dir, latest_checkpoint)) as f:
                    return json.load(f)
            
            def __del__(self):
                """清理资源(不再需要停止线程)"""
                pass
        
            def recover_with_archives(self) -> List[WALRecord]:
                """从归档状态和当前日志中恢复所有记录
            
                恢复流程:
                1. 加载最新的归档状态
                2. 读取当前WAL文件中的记录
                3. 按LSN顺序应用所有更新
                
                Returns:
                    转换后的WAL记录列表,表示最终状态
                """
                try:
                    # 1. 获取最新归档状态
                    final_state = {}  # {table: {key: value}}
                    last_archive_lsn = 0
                    
                    archive_files = sorted([f for f in os.listdir(self.archive_dir) 
                                         if f.startswith("state_") and f.endswith(".json")])
                    if archive_files:
                        latest_archive = archive_files[-1]
                        with open(os.path.join(self.archive_dir, latest_archive)) as f:
                            archive_data = json.load(f)
                            final_state = archive_data["state"]
                            last_archive_lsn = archive_data["metadata"]["checkpoint_lsn"]
                    
                    # 2. 读取并应用当前WAL文件中的所有记录
                    if os.path.exists(self.log_path):
                        with open(self.log_path, 'r') as f:
                            for line in f:
                                if line.strip():
                                    record = WALRecord.from_dict(json.loads(line))
                                    if record.operation == "UPDATE":
                                        if record.table not in final_state:
                                            final_state[record.table] = {}
                                        final_state[record.table].update(record.after)
                    
                    # 3. 将最终状态转换为WAL记录
                    result_records = []
                    for table, table_data in final_state.items():
                        for key, value in table_data.items():
                            record = WALRecord(
                                lsn=self.current_lsn + 1,
                                trans_id="RECOVERY",
                                operation="UPDATE",
                                table=table,
                                before={},
                                after={key: value}
                            )
                            result_records.append(record)
                    
                    return result_records
                
                except Exception as e:
                    print("Error during recovery with archives: {}".format(e))
                    return []
        
            def _get_relevant_archives(self, checkpoint_lsn: int) -> List[str]:
                """获取与恢复相关的归档文件列表
            
                Args:
                    checkpoint_lsn: 检查点的LSN
                
                Returns:
                    相关归档文件的完整路径列表
                """
                if not os.path.exists(self.archive_dir):
                    return []
                
                archive_files = []
                for filename in os.listdir(self.archive_dir):
                    if not filename.startswith("wal_") or not filename.endswith(".log"):
                        continue
                    
                    # 从文件名解析LSN范围
                    try:
                        # 文件名格式:wal_START-LSN_END-LSN.log
                        _, start_lsn, end_lsn = filename.replace(".log", "").split("_")
                        end_lsn = int(end_lsn)
                        
                        # 只选择包含检查点LSN之后记录的文件
                        if end_lsn >= checkpoint_lsn:
                            archive_files.append(os.path.join(self.archive_dir, filename))
                    except ValueError:
                        continue
                
                # 按文件名排序,确保按时间顺序处理
                return sorted(archive_files)
        
            def _read_log_file(self, file_path: str) -> List[WALRecord]:
                """读取日志文件中的所有记录
            
                Args:
                    file_path: 日志文件路径
                
                Returns:
                    文件中的WALRecord列表
                """
                records = []
                if not os.path.exists(file_path):
                    return records
                
                with open(file_path, 'r') as f:
                    for line in f:
                        if line.strip():
                            try:
                                data = json.loads(line)
                                record = WALRecord.from_dict(data)
                                records.append(record)
                            except json.JSONDecodeError:
                                print(f"Error decoding log record: {line}")
                            except Exception as e:
                                print(f"Error processing log record: {e}")
                
                return records
        
            def _sort_and_deduplicate_records(self, records: List[WALRecord]) -> List[WALRecord]:
                """对记录进行排序和去重
            
                Args:
                    records: 原始记录列表
                
                Returns:
                    排序和去重后的记录列表
                """
                # 按LSN排序
                sorted_records = sorted(records, key=lambda r: r.lsn)
                
                # 使用字典去重,保留最新的记录
                unique_records = {}
                for record in sorted_records:
                    unique_records[record.lsn] = record
                
                return list(unique_records.values())
        
            def _init_records_count(self):
                """初始化记录计数
            
                包括当前WAL文件和所有归档文件中的记录
                """
                self.records_count = 0
                
                # 计算归档文件中的记录数
                if os.path.exists(self.archive_dir):
                    for filename in os.listdir(self.archive_dir):
                        if filename.startswith("wal_") and filename.endswith(".log"):
                            file_path = os.path.join(self.archive_dir, filename)
                            with open(file_path, 'r') as f:
                                self.records_count += sum(1 for line in f if line.strip())
                
                # 计算当前WAL文件中的记录数
                if os.path.exists(self.log_path):
                    with open(self.log_path, 'r') as f:
                        self.records_count += sum(1 for line in f if line.strip())
        
            def archive_logs(self, checkpoint_lsn: int):
                """归档检查点之前的日志记录,将其转换为最终数据状态
            
                Args:
                    checkpoint_lsn: 检查点的LSN
                """
                try:
                    # 读取当前WAL文件中需要归档的记录
                    records_to_archive = []
                    with open(self.log_path, 'r') as f:
                        for line in f:
                            if line.strip():
                                record = WALRecord.from_dict(json.loads(line))
                                if record.lsn <= checkpoint_lsn:
                                    records_to_archive.append(record)
                    
                    if not records_to_archive:
                        return
                    
                    # 将WAL记录转换为最终数据状态
                    final_state = {}  # 格式: {table: {key: value}}
                    for record in records_to_archive:
                        if record.operation == "UPDATE":
                            if record.table not in final_state:
                                final_state[record.table] = {}
                            # 遍历after字典中的所有键值对
                            for key, value in record.after.items():
                                final_state[record.table][key] = value
                    
                    # 创建归档文件(使用JSON格式存储最终状态)
                    timestamp = int(time.time())
                    archive_filename = "state_{}.json".format(timestamp)
                    archive_path = os.path.join(self.archive_dir, archive_filename)
                    
                    # 保存最终状态和元数据
                    archive_data = {
                        "metadata": {
                            "checkpoint_lsn": checkpoint_lsn,
                            "timestamp": timestamp,
                            "record_count": len(records_to_archive)
                        },
                        "state": final_state
                    }
                    
                    # 写入归档文件
                    with open(archive_path, 'w') as f:
                        json.dump(archive_data, f, indent=2)
                    
                    # 清理原WAL文件中已归档的记录
                    remaining_records = []
                    with open(self.log_path, 'r') as f:
                        for line in f:
                            if line.strip():
                                record = WALRecord.from_dict(json.loads(line))
                                if record.lsn > checkpoint_lsn:
                                    remaining_records.append(line)
                    
                    # 重写WAL文件,只保留未归档的记录
                    with open(self.log_path, 'w') as f:
                        for line in remaining_records:
                            f.write(line)
                
                except Exception as e:
                    print("Error during log archiving: {}".format(e))
        
            def _init_directories(self):
                """初始化所需的目录"""
                # 确保WAL文件所在目录存在
                os.makedirs(os.path.dirname(self.log_path), exist_ok=True)
                # 确保归档目录存在
                os.makedirs(self.archive_dir, exist_ok=True)
                # 确保checkpoint目录存在
                os.makedirs(self.checkpoint_dir, exist_ok=True)
        
        

        UT测试

        import unittest
        import os
        import json
        from database import Database
        
        class TestWAL(unittest.TestCase):
            """WAL机制的单元测试类
            
            测试内容:
             1. 基本的数据更新操作
             2. 系统崩溃后的数据恢复
             3. 归档功能
            """
            def __init__(self, *args, **kwargs):
                """初始化测试类,添加清理标志"""
                super().__init__(*args, **kwargs)
                # 默认不清理文件,方便观察
                self.should_cleanup = False
        
            def setUp(self):
                """测试前的准备工作
                
                1. 设置测试用的目录结构
                2. 确保测试开始前相关文件不存在
                3. 创建新的数据库实例
                """
                # 使用当前目录作为基础目录
                self.test_dir = os.getcwd()
                
                # 创建WAL日志目录
                self.wal_dir = os.path.join(self.test_dir, "wal_logs")
                if not os.path.exists(self.wal_dir):
                    os.makedirs(self.wal_dir)
                self.wal_path = os.path.join(self.wal_dir, "test_wal.log")
                
                # 清理之前的测试文件
                if os.path.exists(self.wal_path):
                    os.remove(self.wal_path)
                
                # 清理之前的归档目录
                archive_dir = os.path.join(self.test_dir, "archive")
                if os.path.exists(archive_dir):
                    for f in os.listdir(archive_dir):
                        os.remove(os.path.join(archive_dir, f))
                    os.rmdir(archive_dir)
                
                # 创建数据库实例时传入checkpoint目录
                self.db = Database(self.wal_path)
            
            def tearDown(self):
                """测试后的清理工作
                
                只有在should_cleanup为True时才清理文件
                """
                if not self.should_cleanup:
                    return
                
                try:
                    # 清理WAL日志文件和目录
                    if os.path.exists(self.wal_path):
                        os.remove(self.wal_path)
                    if os.path.exists(self.wal_dir):
                        os.rmdir(self.wal_dir)
                    
                    # 清理checkpoint文件和目录
                    if os.path.exists(self.checkpoint_dir):
                        for f in os.listdir(self.checkpoint_dir):
                            os.remove(os.path.join(self.checkpoint_dir, f))
                        os.rmdir(self.checkpoint_dir)
                    
                    # 清理归档目录
                    archive_dir = os.path.join(self.test_dir, "archive")
                    if os.path.exists(archive_dir):
                        for f in os.listdir(archive_dir):
                            os.remove(os.path.join(archive_dir, f))
                        os.rmdir(archive_dir)
                except Exception as e:
                    print("Error during cleanup: {}".format(e))
            
            def test_basic_operations(self):
                """测试基本的更新操作,使用更多数据
                
                验证:
                1. 数据更新是否成功
                2. 更新后的值是否正确
                3. 多表操作
                4. 多种数据类型
                """
                # 账户表操作
                accounts_data = {
                    "Alice": 1000,
                    "Bob": 500,
                    "Charlie": 2000,
                    "David": 1500,
                    "Eve": 3000
                }
                
                # 用户信息表操作
                user_info_data = {
                    "Alice": {"age": 25, "city": "New York", "active": True},
                    "Bob": {"age": 30, "city": "London", "active": False},
                    "Charlie": {"age": 35, "city": "Paris", "active": True},
                    "David": {"age": 28, "city": "Tokyo", "active": True},
                    "Eve": {"age": 32, "city": "Berlin", "active": True}
                }
                
                # 商品表操作
                products_data = {
                    "prod1": {"name": "Laptop", "price": 1200, "stock": 50},
                    "prod2": {"name": "Phone", "price": 800, "stock": 100},
                    "prod3": {"name": "Tablet", "price": 600, "stock": 75},
                    "prod4": {"name": "Watch", "price": 300, "stock": 150},
                    "prod5": {"name": "Headphones", "price": 200, "stock": 200}
                }
                
                # 更新账户表
                for user, balance in accounts_data.items():
                    self.db.update("T1", "accounts", user, balance)
                
                # 更新用户信息表
                for user, info in user_info_data.items():
                    self.db.update("T1", "user_info", user, info)
                
                # 更新商品表
                for prod_id, info in products_data.items():
                    self.db.update("T1", "products", prod_id, info)
                
                self.db.commit()
                
                # 验证账户表数据
                for user, balance in accounts_data.items():
                    self.assertEqual(self.db.get("accounts", user), balance)
                
                # 验证用户信息表数据
                for user, info in user_info_data.items():
                    self.assertEqual(self.db.get("user_info", user), info)
                
                # 验证商品表数据
                for prod_id, info in products_data.items():
                    self.assertEqual(self.db.get("products", prod_id), info)
            
            def test_recovery(self):
                """测试数据库恢复功能
                
                验证:
                1. 写入数据后系统崩溃
                2. 重启后是否能正确恢复数据
                """
                self.db.update("T1", "accounts", "Alice", 1000)
                self.db.update("T1", "accounts", "Bob", 500)
                self.db.commit()
                
                # 创建新的数据库实例,模拟重启
                new_db = Database(self.wal_path)
                
                # 验证数据是否正确恢复
                self.assertEqual(new_db.get("accounts", "Alice"), 1000)
                self.assertEqual(new_db.get("accounts", "Bob"), 500)
            
            def test_archiving(self):
                """测试归档功能,使用更多数据触发归档
                
                验证:
                1. 写入大量数据触发归档
                2. 检查归档文件是否正确生成
                3. 验证归档的数据是否正确
                """
                print("\nStarting archiving test...")
                print("Test directory:", self.test_dir)
                
                # 生成大量测试数据
                for i in range(100):  # 增加到100个用户
                    # 账户数据
                    self.db.update("T1", "accounts", f"user_{i}", 1000 + i * 100)
                    
                    # 用户信息
                    user_info = {
                        "age": 20 + i % 40,
                        "city": ["New York", "London", "Paris", "Tokyo", "Berlin"][i % 5],
                        "active": i % 3 == 0,
                        "score": i * 10,
                        "registration_date": f"2024-{(i % 12) + 1:02d}-{(i % 28) + 1:02d}"
                    }
                    self.db.update("T1", "user_info", f"user_{i}", user_info)
                    
                    # 交易记录
                    transaction = {
                        "amount": 100 + i * 50,
                        "type": ["deposit", "withdrawal", "transfer"][i % 3],
                        "timestamp": f"2024-03-{i % 30 + 1:02d} {i % 24:02d}:{i % 60:02d}:00"
                    }
                    self.db.update("T1", "transactions", f"tx_{i}", transaction)
                
                self.db.commit()
                print("Large dataset committed")
                
                # 检查归档目录
                archive_dir = os.path.join(self.test_dir, "wal_logs", "archive")
                print("Looking for archives in:", archive_dir)
                
                # 确保目录存在
                self.assertTrue(os.path.exists(archive_dir), "Archive directory does not exist")
                
                archive_files = [f for f in os.listdir(archive_dir) 
                                if f.startswith("state_") and f.endswith(".json")]
                print("Found archive files:", archive_files)
                
                # 验证是否生成了归档文件
                self.assertTrue(len(archive_files) > 0, "No archive file was created")
                
                # 读取最新的归档文件
                latest_archive = max(archive_files)
                archive_path = os.path.join(archive_dir, latest_archive)
                print("Reading archive file:", archive_path)
                
                with open(archive_path) as f:
                    archive_data = json.load(f)
                    print("Archive content sample:", json.dumps(
                        {k: list(v.keys())[:5] for k, v in archive_data["state"].items()}, 
                        indent=2
                    ))
                
                # 验证归档的数据是否正确(抽样检查)
                self.assertIn("accounts", archive_data["state"])
                self.assertIn("user_info", archive_data["state"])
                self.assertIn("transactions", archive_data["state"])
                
                # 检查几个随机样本
                for i in [0, 42, 66]:
                    user_key = f"user_{i}"
                    self.assertEqual(
                        archive_data["state"]["accounts"][user_key], 
                        1000 + i * 100
                    )
                    self.assertEqual(
                        archive_data["state"]["user_info"][user_key]["age"], 
                        20 + i % 40
                    )
        
        if __name__ == '__main__':
            unittest.main()
        

        测试显然是成功的

        性能优化建议

        1. 日志写入优化

        # 不好的做法:每个操作都刷盘
        for record in records:
            write_log(record)
            flush_to_disk()
        
        # 好的做法:批量写入
        batch = []
        for record in records:
            batch.append(record)
            if len(batch) >= BATCH_SIZE:
                write_log_batch(batch)
                flush_to_disk()
                batch = []
        

        2. 检查点优化

          3. 监控指标

          • WAL生成速率
          • 检查点耗时
          • 恢复时间
          • 日志空间使用率