wal.go 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. package core
  2. import (
  3. "TsDB2/DB/core/page"
  4. "encoding/binary"
  5. "hash/crc32"
  6. "unsafe"
  7. )
  8. // =============================================================================
  9. // WAL (Write-Ahead Log) 实现
  10. //
  11. // WAL 页布局 (16KB):
  12. // 首页: [PageHdr(20B)] [TxnID(8B)] [TotalEntries(4B)] [Committed(1B)]
  13. // [NextWalPage(4B)] [EntryCount(2B)] [WALEntry...]
  14. // 续页: [PageHdr(20B)] [NextWalPage(4B)] [EntryCount(2B)] [WALEntry...]
  15. //
  16. // WALEntry: [TargetPageID(4B)] [DataLen(4B)] [Checksum(4B)] [Data(DataLen)]
  17. //
  18. // 每个 WAL 页存 1 条完整 16KB 页镜像 (因为 entry 12B + 16KB > 16KB - 39B)
  19. // =============================================================================
  20. const (
  21. walPHDR = uint32(unsafe.Sizeof(page.PageHdr{})) // 20
  22. walFirstHdr = walPHDR + 8 + 4 + 1 + 4 + 2 // 39: PageHdr + TxnID + TotalEntries + Committed + NextWalPage + EntryCount
  23. walContHdr = walPHDR + 4 + 2 // 26: PageHdr + NextWalPage + EntryCount
  24. walEntryMeta = 4 + 4 + 4 // 12: TargetPageID + DataLen + Checksum
  25. )
  26. // WALWriter 事务 WAL 写入器
  27. type WALWriter struct {
  28. txnID uint64
  29. entries []walEntry
  30. file *File
  31. header *HeaderPage
  32. readPage func(uint32, []byte) error // 用于分配页时读取空闲链表
  33. }
  34. type walEntry struct {
  35. pageID uint32
  36. data []byte // 完整页镜像
  37. }
  38. // BeginTxn 开始新事务
  39. func (db *DB) BeginTxn() *WALWriter {
  40. txnID := db.header.NextTxnID
  41. db.header.NextTxnID++
  42. return &WALWriter{
  43. txnID: txnID,
  44. entries: make([]walEntry, 0, 16),
  45. file: db.file,
  46. header: db.header,
  47. readPage: db.file.readPageDisk, // 必须直接读盘! 走 BufferPool 会被旧缓存干扰空闲链表
  48. }
  49. }
  50. // RecordPage 记录一个被修改的页 (同一 pageID 只保留最新镜像)
  51. func (w *WALWriter) RecordPage(pageID uint32, data []byte) {
  52. d := make([]byte, len(data))
  53. copy(d, data)
  54. // 同一 pageID 只保留最新镜像
  55. for i := range w.entries {
  56. if w.entries[i].pageID == pageID {
  57. w.entries[i].data = d
  58. return
  59. }
  60. }
  61. w.entries = append(w.entries, walEntry{pageID, d})
  62. }
  63. // Rollback 回滚: 丢弃所有条目
  64. func (w *WALWriter) Rollback() {
  65. w.entries = nil
  66. }
  67. // Commit 原子提交流程:
  68. // 1. 将 entries 写入 WAL 页链 (顺序写盘)
  69. // 2. fsync WAL 页 (确保持久化)
  70. // 3. 回写实际页 (从 WAL 拷贝到真实位置)
  71. // 4. 标记 WAL 首页 Committed = 1
  72. // 5. 回收所有 WAL 页
  73. func (w *WALWriter) Commit() error {
  74. if len(w.entries) == 0 {
  75. return nil
  76. }
  77. // === 步骤 1: 写入 WAL 页链 ===
  78. walPageIDs, err := w.writeWALPages()
  79. if err != nil {
  80. return err
  81. }
  82. // === 步骤 2: fsync WAL 页 (确保持久化) ===
  83. if err := w.file.sync(); err != nil {
  84. return err
  85. }
  86. // === 步骤 3: 回写实际页 (直接写盘, 绕过 BufferPool) ===
  87. for _, e := range w.entries {
  88. if e.pageID == 0 {
  89. continue
  90. }
  91. if err := w.file.WritePageDirect(e.pageID, e.data); err != nil {
  92. return err
  93. }
  94. }
  95. w.file.ClearCache()
  96. // === 步骤 4: 标记 WAL 首页 Committed = 1 ===
  97. if err := w.markCommitted(walPageIDs[0]); err != nil {
  98. return err
  99. }
  100. if err := w.file.sync(); err != nil {
  101. return err
  102. }
  103. // === 步骤 5: 回收所有 WAL 页 (直接写盘 + 同步 BufferPool 缓存) ===
  104. for _, pid := range walPageIDs {
  105. w.header.recyclePage(pid, w.file.WritePageDirect)
  106. // 同步 BufferPool 缓存: recyclePage 通过 WritePageDirect 写盘, 绕过了 BufferPool
  107. // 必须从磁盘读回并更新缓存, 否则 newPageWithReader 从缓存读到旧数据导致页面无法回收
  108. syncBuf := make([]byte, PageSize)
  109. _ = w.file.readPageDisk(pid, syncBuf)
  110. w.file.SyncCache(pid, syncBuf)
  111. }
  112. w.header.FirstWALPage = 0
  113. w.header.LastWALPage = 0
  114. if err := w.file.WriteHeader(w.header); err != nil {
  115. return err
  116. }
  117. w.entries = nil
  118. return nil
  119. }
  120. // writeWALPages 将所有 entry 写入 WAL 页链, 返回分配的页 ID 列表
  121. func (w *WALWriter) writeWALPages() ([]uint32, error) {
  122. var pageIDs []uint32
  123. entryIdx := 0
  124. for entryIdx < len(w.entries) {
  125. // 分配新 WAL 页 (必须用 newPageWithReader 推进空闲链表)
  126. pid := w.header.newPageWithReader(w.readPage)
  127. pageIDs = append(pageIDs, pid)
  128. buf := make([]byte, PageSize)
  129. hdr := (*page.PageHdr)(unsafe.Pointer(&buf[0]))
  130. hdr.Type = page.WALIndex
  131. hdr.PageID = pid
  132. if len(pageIDs) > 1 {
  133. hdr.PrePage = pageIDs[len(pageIDs)-2]
  134. }
  135. off := uint32(0)
  136. if entryIdx == 0 {
  137. // === 首页: 写入 TxnID + TotalEntries + Committed + NextWalPage + EntryCount ===
  138. binary.LittleEndian.PutUint64(buf[walPHDR:], w.txnID)
  139. binary.LittleEndian.PutUint32(buf[walPHDR+8:], uint32(len(w.entries)))
  140. buf[walPHDR+12] = 0 // Committed = 0 (进行中)
  141. binary.LittleEndian.PutUint32(buf[walPHDR+13:], 0) // NextWalPage (稍后填)
  142. binary.LittleEndian.PutUint16(buf[walPHDR+17:], 0) // EntryCount (稍后填)
  143. off = walFirstHdr
  144. } else {
  145. // === 续页: 写入 NextWalPage + EntryCount ===
  146. binary.LittleEndian.PutUint32(buf[walPHDR:], 0) // NextWalPage (稍后填)
  147. binary.LittleEndian.PutUint16(buf[walPHDR+4:], 0) // EntryCount (稍后填)
  148. off = walContHdr
  149. }
  150. // 写入当前 entry (只存 UseSize 字节, 减少 WAL 空间)
  151. e := w.entries[entryIdx]
  152. storeLen := uint32(len(e.data)) // 默认存全部
  153. if len(e.data) >= 20 {
  154. useSize := binary.LittleEndian.Uint32(e.data[16:20])
  155. if useSize > 0 && useSize < uint32(len(e.data)) {
  156. storeLen = useSize
  157. }
  158. // HeaderPage (pageID=0): 至少存 PageSize4K
  159. if e.pageID == 0 && storeLen < PageSize4K {
  160. storeLen = PageSize4K
  161. }
  162. }
  163. checksum := crc32.ChecksumIEEE(e.data[:storeLen])
  164. binary.LittleEndian.PutUint32(buf[off:], e.pageID)
  165. binary.LittleEndian.PutUint32(buf[off+4:], storeLen)
  166. binary.LittleEndian.PutUint32(buf[off+8:], checksum)
  167. copy(buf[off+12:], e.data[:storeLen])
  168. // storeLen 可能 < len(e.data), 剩余字节保持零填充
  169. off += walEntryMeta + storeLen
  170. hdr.UseSize = off
  171. entryIdx++
  172. // 立即设置当前页的 EntryCount = 1 (不依赖后续链接代码)
  173. if entryIdx == 1 {
  174. // 首页: EntryCount 在 walPHDR+17
  175. binary.LittleEndian.PutUint16(buf[walPHDR+17:], 1)
  176. } else {
  177. // 续页: EntryCount 在 walPHDR+4
  178. binary.LittleEndian.PutUint16(buf[walPHDR+4:], 1)
  179. }
  180. // 链接上一页的 NextWalPage
  181. if len(pageIDs) > 1 {
  182. prevPID := pageIDs[len(pageIDs)-2]
  183. prevBuf := make([]byte, PageSize)
  184. if err := w.file.ReadPage(prevPID, prevBuf); err == nil {
  185. if isFirstWALPage(prevBuf) {
  186. binary.LittleEndian.PutUint32(prevBuf[walPHDR+13:], pid)
  187. binary.LittleEndian.PutUint16(prevBuf[walPHDR+17:], 1)
  188. } else {
  189. binary.LittleEndian.PutUint32(prevBuf[walPHDR:], pid)
  190. binary.LittleEndian.PutUint16(prevBuf[walPHDR+4:], 1)
  191. }
  192. _ = w.file.WritePageDirect(prevPID, prevBuf)
  193. }
  194. }
  195. // 写入当前页
  196. if err := w.file.WritePageDirect(pid, buf); err != nil {
  197. return nil, err
  198. }
  199. }
  200. // 更新 HeaderPage 的 WAL 链指针
  201. if len(pageIDs) > 0 {
  202. if w.header.FirstWALPage == 0 {
  203. w.header.FirstWALPage = pageIDs[0]
  204. }
  205. w.header.LastWALPage = pageIDs[len(pageIDs)-1]
  206. _ = w.file.WriteHeader(w.header)
  207. }
  208. return pageIDs, nil
  209. }
  210. // markCommitted 标记 WAL 首页为已提交
  211. func (w *WALWriter) markCommitted(firstPageID uint32) error {
  212. buf := make([]byte, PageSize)
  213. if err := w.file.ReadPage(firstPageID, buf); err != nil {
  214. return err
  215. }
  216. buf[walPHDR+12] = 1 // Committed = 1
  217. return w.file.WritePageDirect(firstPageID, buf)
  218. }
  219. // isFirstWALPage 判断 WAL 页是否为事务首页
  220. func isFirstWALPage(buf []byte) bool {
  221. // 首页在 walPHDR 处有 TxnID (非零), 续页没有
  222. txnID := binary.LittleEndian.Uint64(buf[walPHDR:])
  223. return txnID != 0
  224. }
  225. // sync 刷盘 (fsync)
  226. func (ff *File) sync() error {
  227. return ff.f.Sync()
  228. }