wal.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. package core
  2. import (
  3. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  4. "encoding/binary"
  5. "hash/crc32"
  6. )
  7. // =============================================================================
  8. // WAL (Write-Ahead Log) 实现
  9. //
  10. // WAL 页布局 (16KB), 权威定义见 page.WALPageHdr:
  11. // [WALPageHdr(40B)] [WALEntry...]
  12. // 首页与续页头部等长 —— 续页的 TxnID/TotalEntries/State 填 0, 换来整条链能用
  13. // 同一个结构体解析, 读写都不再按 isFirst 分叉。
  14. //
  15. // ⭐ 页间链指针用 PageHdr.NextPage (与空闲链同一个字段), 不再自定义 NextWalPage ——
  16. // 这样整条 WAL 链本身就是一条合法空闲链, 回收时一次拼接即可, 不必逐页遍历 (见 recycleChain)。
  17. //
  18. // WALEntry (见 page.WALEntryMeta): [TargetPageID(4B)] [DataLen(4B)] [Checksum(4B)] [OpType(1B)] [Data(DataLen)]
  19. // OpType == WALOpFullPage(0) → Data 为整页镜像 (DataLen = 页有效长度)
  20. // OpType == Insert/Delete/Update → Data 为行级数据
  21. // Checksum = CRC32(OpType || Data)
  22. //
  23. // ⚠️ OpType 必须显式存储: 早期实现靠"Data 的首字节"猜类型, 但整页镜像的首字节
  24. // 就是 PageHdr.Type, 而 DataPage/TablePage/HeapPage 的 Type 恰好是 2/1/3,
  25. // 与行级 OpType 冲突, 整页记录会被误判成行级记录而丢失。
  26. //
  27. // WAL 状态机 (State 字段):
  28. // 0 = Mem (内存中,未写盘)
  29. // 1 = Written (已 write(),未 fsync())
  30. // 2 = Persisted (已 fsync(),但未标记提交)
  31. // 3 = Committed (已提交,可安全恢复)
  32. //
  33. // 打包: 一个 WAL 页可容纳**多条** Entry —— 写端按「当前页剩余空间 >= 13B + DataLen」
  34. // 紧凑追加, 放不下才开新页。整页镜像 (≈16KB) 仍会独占一页; 瘦页 (点更新/小表) 则能
  35. // 在一页里合并几十条, 页尾不再被零填充浪费。
  36. // 读端本就按 EntryCount 逐条解析 (见 wal_recovery.go), 所以打包只改写端。
  37. // =============================================================================
  38. // DataPage 磁盘布局见 page.DataPageHdr (头 40B), 行数据区从 page.DataPageHdrSize 开始。
  39. // WAL 状态常量 (对应 State 字段)
  40. const (
  41. WALStateMem = 0 // 内存中,未写盘
  42. WALStateWritten = 1 // 已 write(),未 fsync()
  43. WALStatePersisted = 2 // 已 fsync(),但未标记提交
  44. WALStateCommitted = 3 // 已提交,可安全恢复
  45. )
  46. // =============================================================================
  47. // WAL 条目操作类型 (每个条目都显式带 1B OpType)
  48. //
  49. // INSERT: Data = storeRow [pkID(8B BE) + rowData]
  50. // DELETE: Data = pkID(8B BE)
  51. // UPDATE: Data = storeRow [pkID(8B BE) + rowData] (新行数据)
  52. // =============================================================================
  53. const (
  54. WALOpFullPage = 0 // 整页镜像 (Data = 页内容)
  55. WALOpInsert = 1 // 插入行
  56. WALOpDelete = 2 // 删除行
  57. WALOpUpdate = 3 // 更新行
  58. )
  59. // RowEntry 行级 WAL 记录
  60. type RowEntry struct {
  61. PageID uint32 // 目标 DataPage ID
  62. OpType byte // WALOpInsert / WALOpDelete / WALOpUpdate
  63. StoreRow []byte // INSERT/UPDATE: [pkID(8B BE) + rowData]; DELETE: pkID(8B BE)
  64. }
  65. // WALWriter 事务 WAL 写入器
  66. type WALWriter struct {
  67. txnID uint64
  68. entries []walEntry
  69. // pageIndex: pageID → entries 下标。RecordPage 的"同页只留最新镜像"去重用,
  70. // 免掉每次提交 O(条目数²) 的线性扫 (默认阈值 500 页/提交时是 12.5 万次比较)。
  71. pageIndex map[uint32]int
  72. file *File
  73. header *HeaderPage
  74. state byte // 当前 WAL 状态 (Mem/Written/Persisted/Committed)
  75. }
  76. type walEntry struct {
  77. pageID uint32
  78. data []byte // 完整页镜像 (isRowLevel=false 时使用)
  79. isRowLevel bool // 是否为行级记录(true=存行数据,false=存整页)
  80. opType byte // 行级操作类型 (WALOpInsert/WALOpDelete/WALOpUpdate)
  81. rowEntry *RowEntry // 行级记录
  82. }
  83. // BeginTxn 开始新事务
  84. func (db *DB) BeginTxn() *WALWriter {
  85. // NextTxnID 是头页里的全局游标, 提交 / 事务提交都可能并发取号, 必须在 headerMu 内自增。
  86. var txnID uint64
  87. db.file.withHeader(func() {
  88. txnID = db.header.NextTxnID
  89. db.header.NextTxnID++
  90. })
  91. return &WALWriter{
  92. txnID: txnID,
  93. entries: make([]walEntry, 0, 16),
  94. pageIndex: make(map[uint32]int, 16),
  95. file: db.file,
  96. header: db.header,
  97. state: WALStateMem, // 初始状态: Mem (内存中)
  98. }
  99. }
  100. // RecordPage 记录一个被修改的页 (同一 pageID 只保留最新镜像)。
  101. // 去重走 pageIndex 映射, O(1); 旧实现对每条 entry 线性扫全部 entries,
  102. // 一次 500 页的提交就是 12.5 万次比较。
  103. func (w *WALWriter) RecordPage(pageID uint32, data []byte) {
  104. d := make([]byte, len(data))
  105. copy(d, data)
  106. if i, ok := w.pageIndex[pageID]; ok {
  107. w.entries[i].data = d
  108. return
  109. }
  110. w.pageIndex[pageID] = len(w.entries)
  111. w.entries = append(w.entries, walEntry{
  112. pageID: pageID,
  113. data: d,
  114. isRowLevel: false, // 原有逻辑:存整页
  115. })
  116. }
  117. // Rollback 回滚: 丢弃所有条目
  118. func (w *WALWriter) Rollback() {
  119. w.entries = nil
  120. w.pageIndex = nil
  121. w.state = WALStateMem // 重置状态
  122. }
  123. // Commit 原子提交流程:
  124. // 1. 将 entries 写入 WAL 页链 (State: Mem → Written)
  125. // 2. 标记 WAL 首页 Committed = 1 (State → Committed)
  126. // 3. fsync 一次: WAL 页与提交标记随这次 fsync 一起落盘
  127. // (排序要求的是"标记 durable ⇒ 数据已 durable", 同一次 fsync 前的顺序写出即满足)
  128. // 4. 回写实际页 (redo; 整页镜像, 重复执行幂等)
  129. // 5. 回收所有 WAL 页 + 清空 Header 链指针 + fsync
  130. //
  131. // ⭐ 关键: 「提交标记」必须先于「回写真实页」落盘。
  132. // 若顺序反过来, 断电落在两者之间时真实页已被原地覆盖, 而提交标记仍是旧值,
  133. // 恢复会丢弃整个 WAL, 留下半应用(损坏)的页 —— 这里没有 undo, 无法自愈。
  134. //
  135. // 缓冲池的脏标记【不在这里】收尾: 提交方拿到 success 之后调用 File.MarkClean
  136. // (按版本号逐页判定), 因为只有提交方知道哪些页属于这次快照, 也才知道窗口里有没有
  137. // 人把这些页又改了一遍。
  138. func (w *WALWriter) Commit() error {
  139. if len(w.entries) == 0 {
  140. return nil
  141. }
  142. // === 步骤 1+2+3: 写 WAL 页链 → 标记首页已提交 → 一次 fsync ===
  143. // WAL 数据与提交标记在同一次 fsync 前依次写出: fsync 保证"标记 durable ⇒
  144. // 数据先于它 durable"的顺序不变 (排序要求的是先后持久, 不是两次 fsync)。
  145. // 旧实现这里分两次 fsync (写完数据一次、标记后再一次), 每次提交白付一轮 fsync 延迟。
  146. walPageIDs, firstBuf, err := w.writeWALPages()
  147. if err != nil {
  148. return err
  149. }
  150. // === 步骤 3: 标记 WAL 首页为已提交 (State: Written → Committed) ===
  151. // 这一步之后, 无论发生什么崩溃, 恢复都能重放整笔事务 (WAL 已完整且被标记)。
  152. if err := w.markCommitted(walPageIDs[0], firstBuf); err != nil {
  153. return err
  154. }
  155. // 状态转换: Written → Persisted → Committed (随下面这次 fsync 落盘)
  156. if err := w.file.sync(); err != nil {
  157. return err
  158. }
  159. w.state = WALStateCommitted
  160. // === 步骤 4: 回写实际页 (直接写盘, 绕过 BufferPool) ===
  161. // 放在提交标记之后: 中途断电可由恢复阶段重做 (整页镜像, 幂等)。
  162. //
  163. // 用 WritePageHome 而不是 WritePageDirect: 这里写出去的正是缓存快照本身, 缓存里
  164. // 那一份的内容只会等于它或新于它 (并发写入时), 无差别失效会丢掉更新的那一份。
  165. // 缓存状态由提交方在 Commit() 返回后用 File.MarkClean 按版本号收尾 —— 这也是
  166. // 这里不再 ClearCache 的原因: ClearAll 会把提交窗口里被重新改脏的页一并标干净,
  167. // 那些页并不在本笔 WAL 里, 标干净等于把它们的新内容永久丢掉。
  168. for _, e := range w.entries {
  169. if e.pageID == 0 {
  170. continue
  171. }
  172. if err := w.file.WritePageHome(e.pageID, e.data); err != nil {
  173. return err
  174. }
  175. }
  176. // 步骤 4.5: 回写后立刻 fsync, 钉死"数据页先于头页(步骤 5 清 WAL 指针)落盘"。
  177. // 没有这一次 fsync, OS 后台回刷可能把步骤 5 的头页先刷下去、数据页留在缓存 —
  178. // 断电后头页说"没有 WAL 可重放", 数据页却是旧值, 已提交数据静默丢失。
  179. if err := w.file.sync(); err != nil {
  180. return err
  181. }
  182. // === 步骤 5: 回收整条 WAL 链 (O(1) 拼接, 与链长无关) ===
  183. // 链内每页的 next 都写在 PageHdr.NextPage, 与空闲链同字段, 所以整条链
  184. // head…tail 可以直接挂到空闲链尾, 不必逐页遍历回收。
  185. // 链首/链尾/链长都已在 writeWALPages 中记录 (walPageIDs), 尾页 NextPage 必为 0。
  186. //
  187. // 还链 (改空闲链指针) 与清空 WAL 链指针是一次头页上的复合修改, 必须整体落在
  188. // headerMu 内, 否则并发写入线程可能看到"WAL 链已清空、空闲链却还没接好"的中间态。
  189. var recycleErr error
  190. w.file.withHeader(func() {
  191. recycleErr = recycleChainWithCache(w.file, w.header,
  192. walPageIDs[0], walPageIDs[len(walPageIDs)-1], uint32(len(walPageIDs)))
  193. if recycleErr != nil {
  194. return
  195. }
  196. w.header.FirstWALPage = 0
  197. w.header.LastWALPage = 0
  198. recycleErr = w.file.writeHeaderLocked(w.header)
  199. })
  200. if recycleErr != nil {
  201. return recycleErr
  202. }
  203. // 收尾必须 fsync, 它钉死一个关键的持久化顺序: "忘掉日志"(头页: WAL 链回收
  204. // + 指针清零) 不得先于"数据就位"(步骤 4 回写的真实数据页) 落盘。
  205. // 没有这次 fsync, 断电时 OS 可能只把头页刷下去、数据页留在缓存里 —— 重开时
  206. // 头页说"没有 WAL", 已提交的数据却是旧内容, 且 WAL 页已被标成空闲页即将被
  207. // 覆盖, 连重放救回的机会都没有 (静默丢已提交数据)。
  208. // 顺带的效果: 头页若仍指着已提交的 WAL, 下次打开会重复重放 —— 整页镜像幂等
  209. // 无害, 行级记录则会重复插入。
  210. if err := w.file.sync(); err != nil {
  211. return err
  212. }
  213. w.entries = nil
  214. w.pageIndex = nil
  215. return nil
  216. }
  217. // writeWALPages 将所有 entry 紧凑写入 WAL 页链, 返回 (分配的页 ID 列表, 首页缓冲区)。
  218. // 状态转换: Mem → Written
  219. //
  220. // 打包策略: 一条 Entry 占 [13B 元数据 + DataLen]。当前页剩余空间够就继续追加,
  221. // 不够才定型落盘并开新页 —— 一条 200B 的记录原本独占 16KB, 现在一页可放约 76 条。
  222. //
  223. // 所有 WAL 页头部等长 (page.WALPageHdrSize); walEntryPayload 已把单条记录截断到
  224. // 「一定能放进空页」的长度, 因此开新页后必定写得下, 不会出现空页仍放不下的情况。
  225. func (w *WALWriter) writeWALPages() ([]uint32, []byte, error) {
  226. var pageIDs []uint32
  227. var firstBuf []byte // 首页缓冲区: markCommitted 直接改它打提交标记, 免一次整页读回
  228. // 当前正在填充的 WAL 页
  229. var curBuf []byte
  230. var curPID uint32
  231. var curIsFirst bool
  232. var curOff uint32 // 已用字节数 (从页头之后开始)
  233. var curEntries uint16 // 已写入的条目数
  234. var opTypeBuf [1]byte // CRC 的 opType 段缓冲 (分段 Update 免每条一次临时分配)
  235. // flushCurrent 定型当前页 (回填 UseSize / EntryCount) 并落盘。
  236. // 链指针不在这里填 —— 只有"开新页"时才知道下一页是谁, 由调用处先回填再 flush。
  237. // ⭐ 全程用内存缓冲记录"是否首页", 不从磁盘读回上一页判定 —— 那种实现依赖
  238. // TxnID != 0, 而首个事务的 TxnID 恰好是 0, 会把链指针写到错误偏移。
  239. flushCurrent := func() error {
  240. if curBuf == nil {
  241. return nil
  242. }
  243. wh := page.WalHdrOf(curBuf)
  244. wh.UseSize = curOff
  245. wh.EntryCount = curEntries
  246. return w.file.WritePageDirect(curPID, curBuf)
  247. }
  248. for i := range w.entries {
  249. opType, targetPID, payload := w.walEntryPayload(w.entries[i])
  250. dataLen := uint32(len(payload))
  251. need := page.WALEntryMeta + dataLen
  252. // 当前页放不下 → 开新页
  253. if curBuf == nil || uint32(PageSize)-curOff < need {
  254. // 分配新页: headerMu 内摘空闲链 / 递增游标并推进头页。
  255. // erase=false —— 页紧接着就在内存里被整页重造 (curBuf 是全 0 的新切片,
  256. // flushCurrent 一次写满 16KB), 不需要擦页; 且此刻正处在 Commit 内
  257. // (持有 commitMu), 擦页会重入 autoCommit → Commit → 自死锁在 commitMu 上。
  258. pid := w.file.allocPage(w.header, false)
  259. // 先回填上一页的链指针, 再把上一页定型落盘。
  260. // 链指针统一写 PageHdr.NextPage (偏移 12) —— 与空闲链表同一个字段,
  261. // 所以整条 WAL 链本身就是一条合法空闲链, 回收时可一次拼接 (见 Commit 步骤 5)。
  262. if curBuf != nil {
  263. page.HdrOf(curBuf).NextPage = pid
  264. if err := flushCurrent(); err != nil {
  265. return nil, nil, err
  266. }
  267. }
  268. pageIDs = append(pageIDs, pid)
  269. curBuf = make([]byte, PageSize)
  270. if len(pageIDs) == 1 {
  271. // 首页缓冲留一份给 markCommitted: 它的内容在"开第二页回填链指针 +
  272. // flush"后就定稿了, 之后不再被本函数改动 —— 提交标记直接改这份内存
  273. // 缓冲写回, 免一次"整页读回只为改 1 字节 State"。
  274. firstBuf = curBuf
  275. }
  276. curPID = pid
  277. curIsFirst = len(pageIDs) == 1
  278. curEntries = 0
  279. var prevPID uint32
  280. if len(pageIDs) > 1 {
  281. prevPID = pageIDs[len(pageIDs)-2]
  282. }
  283. wh := page.WalHdrOf(curBuf)
  284. wh.Init(pid, prevPID)
  285. if curIsFirst {
  286. // === 首页: TxnID + TotalEntries + State ===
  287. wh.TxnID = w.txnID
  288. wh.TotalEntries = uint32(len(w.entries))
  289. wh.State = WALStateWritten // State = Written (已写盘,未 fsync)
  290. }
  291. // 首页与续页头部等长, 条目区起点一致
  292. curOff = page.WALPageHdrSize
  293. }
  294. // 追加当前 entry
  295. // 统一格式: [TargetPageID(4B)][DataLen(4B)][Checksum(4B)][OpType(1B)][Data]
  296. // CRC32 覆盖 OpType + Data, 整页/行级一致, 类型显式存储不再靠首字节猜测。
  297. // crc32.Update 分段计算与"拼成一条再 ChecksumIEEE"完全等价, 省掉每条一次
  298. // 1+DataLen 的临时缓冲分配。
  299. opTypeBuf[0] = opType
  300. crc := crc32.Update(0, crc32.IEEETable, opTypeBuf[:])
  301. crc = crc32.Update(crc, crc32.IEEETable, payload)
  302. binary.LittleEndian.PutUint32(curBuf[curOff:], targetPID)
  303. binary.LittleEndian.PutUint32(curBuf[curOff+4:], dataLen)
  304. binary.LittleEndian.PutUint32(curBuf[curOff+8:], crc)
  305. curBuf[curOff+12] = opType
  306. curOff += page.WALEntryMeta
  307. copy(curBuf[curOff:], payload)
  308. curOff += dataLen
  309. curEntries++
  310. }
  311. // 定型最后一页
  312. if err := flushCurrent(); err != nil {
  313. return nil, nil, err
  314. }
  315. // 更新 HeaderPage 的 WAL 链指针
  316. // 发布链指针是为了崩溃恢复能找回这笔事务, 必须在 headerMu 内对"首页是否已存在"
  317. // 与"尾页指向"做一次原子更新, 否则并发提交可能互相覆盖链尾。
  318. if len(pageIDs) > 0 {
  319. w.file.withHeader(func() {
  320. if w.header.FirstWALPage == 0 {
  321. w.header.FirstWALPage = pageIDs[0]
  322. }
  323. w.header.LastWALPage = pageIDs[len(pageIDs)-1]
  324. _ = w.file.writeHeaderLocked(w.header)
  325. })
  326. }
  327. // 状态转换: Mem → Written (已写盘,未 fsync)
  328. w.state = WALStateWritten
  329. return pageIDs, firstBuf, nil
  330. }
  331. // walEntryPayload 解析一条 entry 的目标页 / OpType / 载荷, 并把载荷截断到
  332. // 「一定能放进一张空 WAL 页」的长度 (首页/续页头等长, 只需一个限制)。
  333. //
  334. // 这个截断是 writeWALPages 打包逻辑的前提: 开新页后必定能写下当前这条记录。
  335. func (w *WALWriter) walEntryPayload(e walEntry) (byte, uint32, []byte) {
  336. opType := byte(WALOpFullPage)
  337. targetPID := e.pageID
  338. var payload []byte
  339. switch {
  340. case e.isRowLevel && e.rowEntry != nil:
  341. opType = e.rowEntry.OpType
  342. targetPID = e.rowEntry.PageID
  343. payload = e.rowEntry.StoreRow
  344. case e.isRowLevel:
  345. // 行级条目缺少 rowEntry: 视作空行级记录
  346. opType = e.opType
  347. payload = nil
  348. default:
  349. // 整页记录: 只存有效前缀 (整页 16KB 无法与条目元数据同放一页)
  350. storeLen := uint32(len(e.data))
  351. if e.pageID == 0 {
  352. // HeaderPage 只有前 4KB 有效; 必须与 recoverHeader 的解析长度一致
  353. storeLen = PageSize4K
  354. } else if len(e.data) >= int(page.PageHdrSize) {
  355. useSize := page.HdrOf(e.data).UseSize
  356. if useSize > 0 && useSize < uint32(len(e.data)) {
  357. storeLen = useSize
  358. }
  359. }
  360. if storeLen > uint32(len(e.data)) {
  361. storeLen = uint32(len(e.data))
  362. }
  363. payload = e.data[:storeLen]
  364. }
  365. // 防御: 单条记录不得超过空首页容量 (行级记录同样受此保护)
  366. if maxLen := uint32(PageSize) - page.WALPageHdrSize - page.WALEntryMeta; uint32(len(payload)) > maxLen {
  367. payload = payload[:maxLen]
  368. }
  369. return opType, targetPID, payload
  370. }
  371. // markCommitted 标记 WAL 首页为已提交 (State → Committed)。
  372. // firstBuf 是 writeWALPages 手里刚写盘的首页内存缓冲 (内容与盘上逐字节一致),
  373. // 直接改它的 State 字节整页写回 —— 旧实现为此整页读回 16KB, 只为改 1 字节。
  374. func (w *WALWriter) markCommitted(firstPageID uint32, firstBuf []byte) error {
  375. if len(firstBuf) != int(PageSize) {
  376. // 防御: 缓冲缺失/长度不符时退回读盘路径 (正常不会发生)
  377. firstBuf = make([]byte, PageSize)
  378. // 必须直接读盘: WAL 页是用 WritePageDirect 写的, 走 BufferPool 可能读到陈旧缓存
  379. if err := w.file.readPageDisk(firstPageID, firstBuf); err != nil {
  380. return err
  381. }
  382. }
  383. page.WalHdrOf(firstBuf).State = WALStateCommitted // State = Committed
  384. if err := w.file.WritePageDirect(firstPageID, firstBuf); err != nil {
  385. return err
  386. }
  387. return nil
  388. }
  389. // sync 刷盘 (fsync)
  390. func (ff *File) sync() error {
  391. return ff.f.Sync()
  392. }
  393. // =============================================================================
  394. // applyRowToPage 将行级 WAL 记录应用到页缓冲区 (用于崩溃恢复)
  395. //
  396. // 操作类型:
  397. //
  398. // WALOpInsert: 在 DataPage 中追加一行
  399. // WALOpDelete: 在 DataPage 中软删除指定 pkID 的行
  400. // WALOpUpdate: 在 DataPage 中按 pkID 原地更新行数据
  401. //
  402. // storeRow 格式: [pkID(8B BE) + rowData] (INSERT/UPDATE)
  403. // storeRow 格式: pkID(8B BE) (DELETE)
  404. // =============================================================================
  405. func applyRowToPage(buf []byte, opType byte, storeRow []byte) {
  406. const delFlag = uint32(0x80000000)
  407. if len(buf) < int(page.DataPageHdrSize) {
  408. return
  409. }
  410. // 读取 DataPage 元数据 (Count/Min/Max 紧随 PageHdr)
  411. dh := page.DataHdrOf(buf)
  412. count := dh.Count
  413. dataEnd := dh.UseSize
  414. if dataEnd < page.DataPageHdrSize || dataEnd > uint32(len(buf)) {
  415. return
  416. }
  417. switch opType {
  418. case WALOpInsert:
  419. // 追加行: [len(4B)] [storeRow]
  420. off := dataEnd
  421. if off+4+uint32(len(storeRow)) > uint32(len(buf)) {
  422. return // 页满, 放弃 (整页镜像已包含该行)
  423. }
  424. binary.LittleEndian.PutUint32(buf[off:], uint32(len(storeRow)))
  425. off += 4
  426. copy(buf[off:], storeRow)
  427. off += uint32(len(storeRow))
  428. // 更新 count 和 dataEnd (UseSize); 追加的是活行, 存活计数同步 +1
  429. dh.Count = count + 1
  430. dh.LiveCnt++
  431. dh.UseSize = off
  432. case WALOpDelete:
  433. // storeRow = pkID(8B BE)
  434. if len(storeRow) < 8 {
  435. return
  436. }
  437. keyPK := binary.BigEndian.Uint64(storeRow[:8])
  438. off := page.DataPageHdrSize
  439. for i := uint32(0); i < count; i++ {
  440. if off+4 > dataEnd {
  441. break
  442. }
  443. rawLen := binary.LittleEndian.Uint32(buf[off:])
  444. deleted := rawLen&delFlag != 0
  445. rowLen := rawLen & ^delFlag
  446. rowStart := off + 4
  447. if rowStart+rowLen > dataEnd {
  448. break
  449. }
  450. if !deleted && rowLen >= 8 {
  451. rowPK := binary.BigEndian.Uint64(buf[rowStart : rowStart+8])
  452. if rowPK == keyPK {
  453. // 设置软删除标志; 存活/失效统计同步维护 (页头持久化, 见 page.DataPageHdr)
  454. binary.LittleEndian.PutUint32(buf[off:], rowLen|delFlag)
  455. dh.LiveCnt--
  456. dh.Dead += 4 + rowLen
  457. return
  458. }
  459. }
  460. off = rowStart + rowLen
  461. }
  462. case WALOpUpdate:
  463. // storeRow = [pkID(8B BE) + rowData]
  464. if len(storeRow) < 8 {
  465. return
  466. }
  467. keyPK := binary.BigEndian.Uint64(storeRow[:8])
  468. off := page.DataPageHdrSize
  469. for i := uint32(0); i < count; i++ {
  470. if off+4 > dataEnd {
  471. break
  472. }
  473. rawLen := binary.LittleEndian.Uint32(buf[off:])
  474. deleted := rawLen&delFlag != 0
  475. rowLen := rawLen & ^delFlag
  476. rowStart := off + 4
  477. if rowStart+rowLen > dataEnd {
  478. break
  479. }
  480. if !deleted && rowLen >= 8 {
  481. rowPK := binary.BigEndian.Uint64(buf[rowStart : rowStart+8])
  482. if rowPK == keyPK {
  483. // 原地覆盖行数据
  484. copy(buf[rowStart:], storeRow)
  485. // 剩余字节置零
  486. for j := uint32(len(storeRow)); j < rowLen; j++ {
  487. buf[rowStart+j] = 0
  488. }
  489. return
  490. }
  491. }
  492. off = rowStart + rowLen
  493. }
  494. }
  495. }