HeadPage.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. package core
  2. import (
  3. "fmt"
  4. "time"
  5. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  6. )
  7. // 数据库全局头页(PageID=0 固定)
  8. type HeaderPage struct {
  9. Magic [4]byte // 4 魔术字,识别文件类型
  10. Version uint32 // 4 版本
  11. PageSize uint32 // 4 每页大小(比如 4096)
  12. NextFreePage uint32 // 4 下一个可用空闲页
  13. TablePage uint32 // 4 【唯一重要】表链起始页ID
  14. TableCount uint32 // 4 表数量
  15. CreateTime int64 // 8
  16. ModifyTime int64 // 8
  17. FreePageStart uint32 // 4 空闲页链表头
  18. FreePageCount uint32 // 4 空闲页数量
  19. GlobalFlags uint64 // 8 全局状态
  20. FirstWALPage uint32 // 4 未恢复的 WAL 链首页 ID (0=无待恢复WAL)
  21. LastWALPage uint32 // 4 WAL 链尾页 ID (追加写入用)
  22. NextTxnID uint64 // 8 下一个事务 ID
  23. ExtConfig [256]byte // 256 扩展配置
  24. Comment [512]byte // 512 备注
  25. // FreePageEnd 空闲链表尾页 ID (0 = 空闲链为空)。
  26. //
  27. // 放在 Comment 之后而不是紧跟 FreePageStart: 这样前 840 字节的既有字段偏移一个
  28. // 都不动 (盘上头部布局零位移), 只吃掉原本的对齐填充。不变量:
  29. // FreePageStart == 0 当且仅当 FreePageEnd == 0, 两者必须同生同灭。
  30. FreePageEnd uint32 // 4 空闲页链表尾
  31. // 填充对齐到整页(PageSize - 已用字节)
  32. _ [PageSize4K - 844]byte // 自动补齐
  33. }
  34. // newPageWithReader 分配新页:优先从空闲链表头取, 否则递增分配。
  35. // 必须带读盘能力 —— 空闲链的 next 存在页头 PageHdr.NextPage, 不读页就摘不下来。
  36. //
  37. // writePage 用来把摘到的页擦成"干净空白页"(可为 nil, 表示调用方自己保证整页覆写)。
  38. //
  39. // 为什么擦页必须发生在这个入口:
  40. // - 空闲链上的页是回收来的, 而 recycleChain 是 O(1) 拼接 —— 只改写链尾 1 页,
  41. // 链内页的正文一个字节都不动 (逐页覆写就把 O(1) 打回 O(n), DropTable 的堆链、
  42. // 提交后的整条 WAL 链正是靠它才不慢)。于是这些页在盘上留着它上一世的内容:
  43. // 典型是 WAL 页 (Type=7、UseSize=16384、NextPage 还指着旧 WAL 链)。
  44. // - 分配方拿到页号后只要有一次"先读后写", 读到的就是这些残存字节。历史上
  45. // IndexMetaPage.Init 正是这么把整页 WAL 残留当成 IndexMeta 数组解析出 91 条垃圾
  46. // 元数据 (EntryPage=16777216), 最终让 DROP TABLE 拿着垃圾页号去 FreePage:
  47. // 写到几十 GB 偏移、稀疏文件暴涨到 40GB、DROP 假死 52s。
  48. // - 这条不变量以前靠"每个调用点自己记得整页覆写"维持 (当时十几个调用点确实都做到了),
  49. // 但它散在十几个地方, 违约的代价是静默的数据错乱而不是报错。现在收到唯一入口上:
  50. // 凡是 NewPage 交出的页, 盘上/池里一定是"全 0 + 自述页号"。
  51. //
  52. // 为什么用 writePage (= File.WritePage, 走缓冲池) 而不是直写盘:
  53. // 调用方紧接着的整页写入会覆盖池里同一份缓存, 因此不额外产生物理 IO, 顺带把池里
  54. // 可能残留的陈旧副本一并覆盖; 未启用缓冲池时退化为一次直写 (每页一次, 不是每行一次)。
  55. func (header *HeaderPage) newPageWithReader(readPage func(uint32, []byte) error,
  56. writePage func(uint32, []byte) error) uint32 {
  57. var pid uint32
  58. if header.FreePageStart != 0 {
  59. pid = header.FreePageStart
  60. buf := make([]byte, PageSize)
  61. if err := readPage(pid, buf); err == nil {
  62. // 空闲页与其它页一样符合 PageHdr: 下一个空闲页 ID 存在页头的 NextPage
  63. next := page.HdrOf(buf).NextPage
  64. header.FreePageStart = next
  65. if next == 0 {
  66. // 摘完链表就空了, 尾指针必须跟着清零, 否则留下悬垂尾指针
  67. header.FreePageEnd = 0
  68. }
  69. header.FreePageCount--
  70. } else {
  71. // 读失败,回退到递增分配
  72. pid = header.NextFreePage
  73. header.NextFreePage++
  74. }
  75. } else {
  76. pid = header.NextFreePage
  77. header.NextFreePage++
  78. }
  79. if writePage != nil {
  80. // 页头只留自述页号 (Type=0 未定型、UseSize=页头长), 其余字节全 0。
  81. // 自述页号不是可有可无的: loadMetas / Init 这类"读盘重建"的路径会校验
  82. // Hdr.PageID == pageID, 靠它挡住"页号复用错了"的误读。
  83. clean := make([]byte, PageSize)
  84. page.HdrOf(clean).Init(0, pid)
  85. _ = writePage(pid, clean)
  86. }
  87. return pid
  88. }
  89. // recyclePage 回收一个页到空闲链表(头插, 只写 1 页)。
  90. //
  91. // 页统一符合 PageHdr: 空闲链指针写在页头的 NextPage (偏移 12), 不再占用偏移 0
  92. // 的 Type/RefCount 字段。这样空闲页仍是一张"正规页", 而且堆页/索引链的 NextPage
  93. // 与空闲链的 NextPage 是同一个字段 —— 整条链可以 O(1) 拼接到空闲链上。
  94. func (header *HeaderPage) recyclePage(pageID uint32, savePage func(uint32, []byte) error) {
  95. buf := make([]byte, PageSize)
  96. hdr := page.HdrOf(buf)
  97. hdr.PageID = pageID
  98. hdr.NextPage = header.FreePageStart
  99. header.FreePageStart = pageID
  100. if header.FreePageEnd == 0 {
  101. // 原空闲链为空: 这一页既是头也是尾
  102. header.FreePageEnd = pageID
  103. }
  104. header.FreePageCount++
  105. _ = savePage(pageID, buf)
  106. }
  107. // recycleChain 把一条已经用 PageHdr.NextPage 串好的整链 (head…tail) 整体挂到
  108. // 空闲链表尾。O(1): 只改写「原空闲链尾」1 页 + 更新头页, 既不逐页遍历也不逐页覆零。
  109. //
  110. // 前提: head…tail 每页的 next 都写在 PageHdr.NextPage, 且 tail.NextPage == 0。
  111. // count 是该链的页数, 由构造方 (WAL 写入时) 或遍历方 (崩溃恢复时) 顺手统计而来 ——
  112. // 正因为链长已知, 这里才能不遍历就把 FreePageCount 加对。
  113. //
  114. // 为什么挂链尾而不是头插: 刚回收的大块 (比如整张被 DROP 的表) 排在空闲链后段,
  115. // 空闲链头的分配前沿不动, 语义也更接近「把这些页排队等着复用」。
  116. func (header *HeaderPage) recycleChain(head, tail uint32, count uint32,
  117. readPage func(uint32, []byte) error, savePage func(uint32, []byte) error) error {
  118. if head == 0 || tail == 0 || count == 0 {
  119. return nil
  120. }
  121. if header.FreePageEnd != 0 {
  122. // 原空闲链尾 → 新链头, 两条链就此接成一条
  123. buf := make([]byte, PageSize)
  124. if err := readPage(header.FreePageEnd, buf); err != nil {
  125. return err
  126. }
  127. page.HdrOf(buf).NextPage = head
  128. if err := savePage(header.FreePageEnd, buf); err != nil {
  129. return err
  130. }
  131. } else {
  132. // 原空闲链为空: 这条链就是整条空闲链
  133. header.FreePageStart = head
  134. }
  135. header.FreePageEnd = tail
  136. header.FreePageCount += count
  137. return nil
  138. }
  139. // recycleChainWithCache 整链还链 + 刷新被改写页的缓冲池副本。
  140. //
  141. // recycleChain 用 savePage (=WritePageDirect) 改写「原空闲链尾」, 绕过了 BufferPool;
  142. // 那一页在池里可能还留着干净旧副本, 之后任何走 ReadPage 的读取都会拿到错内容 ——
  143. // 所以从盘读回、同步一次缓存。链内其它页的 next 没被改动, 池里的副本仍然准确,
  144. // 不必逐页刷新 (逐页刷新就等于把 O(1) 又变回 O(n) 了)。
  145. func recycleChainWithCache(ff *File, header *HeaderPage, head, tail, count uint32) error {
  146. if count == 0 {
  147. return nil
  148. }
  149. oldTail := header.FreePageEnd
  150. if err := header.recycleChain(head, tail, count, ff.readPageDisk, ff.WritePageDirect); err != nil {
  151. return err
  152. }
  153. if oldTail != 0 {
  154. syncBuf := make([]byte, PageSize)
  155. if err := ff.readPageDisk(oldTail, syncBuf); err == nil {
  156. ff.SyncCache(oldTail, syncBuf)
  157. }
  158. }
  159. return nil
  160. }
  161. func newHeader(desc string) (*HeaderPage, error) {
  162. var comment [512]byte
  163. // 1. 安全判断:防止描述超长
  164. if len(desc) > len(comment) {
  165. return nil, fmt.Errorf("描述字符串长度 %d 超过最大限制 %d", len(desc), len(comment))
  166. }
  167. copy(comment[:], desc)
  168. // 2. 初始化头部(全部正确赋值,无遗漏、无坑)
  169. return &HeaderPage{
  170. Magic: Magic, // 魔数
  171. Version: Version, // 版本
  172. PageSize: PageSize, // 页大小
  173. NextFreePage: 2, // 下一个分配页从 2 开始(0=header, 1=table page 已预留)
  174. TablePage: 0, // 初始无表
  175. TableCount: 0, // 表数量=0
  176. CreateTime: time.Now().UnixNano(), // 纳秒时间戳
  177. ModifyTime: time.Now().UnixNano(),
  178. FreePageStart: 0, // 初始无空闲页
  179. FreePageCount: 0, // 空闲页数量=0
  180. GlobalFlags: 0, // 无标志
  181. NextTxnID: 1, // 事务 ID 从 1 开始 (0 保留, 便于 WAL 页首页判定)
  182. ExtConfig: [256]byte{}, // 清空扩展配置
  183. Comment: comment, // 数据库描述
  184. }, nil
  185. }