WAL机制学习
背景在学习游戏常见场景开发遇到这个概念看目的是为了解决缓存写入数据库的过程失败后未落库的数据能够落地的方案。但是在实际工作中遇到比较少故展开学习。学习目标了解该机制原理应用场景原理WAL 的核心就是先写日志再写数据崩溃后通过回放日志来恢复。一个典型的 WAL 方案包含三个部分日志文件追加写入的文本文件或二进制文件每条记录代表一个操作。写入器负责将操作序列化为日志并刷盘。回放器启动时或崩溃后读取日志与数据库对比执行缺失的操作。关键细节以下用一些示例代码讲解下定义日志记录结构每条日志包含唯一ID、操作类型、时间戳和具体数据。importjsonimportosimportuuidimporttimefromtypingimportCallable,Dict,AnyclassWalRecord:单条WAL记录def__init__(self,op_type:str,data:Dict[str,Any]):self.record_idstr(uuid.uuid4())self.op_typeop_type# 例如 add_friend, remove_friendself.datadata# 例如 {user_id: 1, friend_id: 2}self.timestamptime.time()defto_json(self)-str:returnjson.dumps({id:self.record_id,op:self.op_type,data:self.data,ts:self.timestamp})\nstaticmethoddeffrom_json(line:str)-WalRecord:objjson.loads(line.strip())recWalRecord(obj[op],obj[data])rec.record_idobj[id]rec.timestampobj[ts]returnrecWAL 管理器负责写入、回放和清理。classWalManager:def__init__(self,file_path:str/var/log/friend_wal.log):self.file_pathfile_path self._fileNonedefopen(self):打开日志文件以追加模式self._fileopen(self.file_path,a)# a 可读可追加defclose(self):ifself._file:self._file.close()defappend(self,record:WalRecord):追加一条记录并强制刷盘linerecord.to_json()self._file.write(line)self._file.flush()# 刷新到内核缓冲区os.fsync(self._file.fileno())# 强制刷入磁盘关键defread_all(self)-list:读取所有记录用于回放records[]self._file.seek(0)# 回到文件开头forlineinself._file:ifline.strip():records.append(WalRecord.from_json(line))returnrecordsdeftruncate(self,upto_record_id:str):截断到指定记录之后清理已处理的日志# 简单实现重写文件只保留大于upto_record_id的记录all_recordsself.read_all()keep_records[rforrinall_recordsifr.record_id!upto_record_idandall_records.index(r)all_records.index([xforxinall_recordsifx.record_idupto_record_id][0])]self._file.close()withopen(self.file_path,w)asf:forrecinkeep_records:f.write(rec.to_json())self._fileopen(self.file_path,a)为什么需要 os.fsyncflush() 只是将数据从用户空间缓冲区移到内核缓冲区但内核可能还未写入磁盘。os.fsync() 强制内核将数据刷入物理磁盘确保即使系统掉电也不会丢失。这是 WAL 可靠性的核心。幂等恢复回放时不能简单地重放所有日志因为有些操作可能已经成功写入数据库例如第一次运行正常但后来服务又崩溃了。因此需要先检查数据库中是否已经存在该操作的结果。常见做法使用 INSERT IGNORE 或 ON DUPLICATE KEY UPDATE让数据库自动跳过重复。或者维护一张 processed_log 表记录已处理的日志ID回放时跳过这些ID。日志清理WAL 文件会无限增长需要定期清理。清理策略标记-截断记录一个“安全水位”表示该ID之前的日志都已处理完毕可以删除。分段管理按时间或大小切分文件只保留最近N个文件。使用数据库表代替文件利用数据库自身的清理机制.应用场景MySQL InnoDB 的 Redo LogMySQL 的 InnoDB 存储引擎内部就有 WAL 机制称为 Redo Log。每次事务提交时先写 Redo Log顺序IO再异步刷脏页到数据文件。如果数据库崩溃重启时通过 Redo Log 恢复未写入数据文件的修改。这是 WAL 最经典的工业实现。Kafka 的 Partition LogKafka 的每个分区本质上就是一个 WAL 文件。生产者写入消息时先追加到分区日志顺序写消费者从日志中读取。Kafka 的高吞吐和可靠性正依赖于这种 WAL 设计。RocksDB / LevelDB 的 WALFacebook 开发的 RocksDB用于 MySQL MyRocks 引擎、TiKV 等也有独立的 WAL 文件。每次写入先写 WAL再写 MemTable崩溃后通过 WAL 恢复。分布式系统中的 WALZooKeeper每个事务先写事务日志WAL再更新内存状态。etcd使用 BoltDB 的 WAL 机制保证一致性。TiDB 的 TiKV 节点使用 Raft 协议底层也是 WALRaft Log。