header_page.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. package core
  2. import (
  3. "encoding/binary"
  4. "fmt"
  5. "time"
  6. "unsafe"
  7. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  8. )
  9. // 编译期断言: 钉死 HeaderPage 的盘上布局 —— 字段重排/增删都会在这里编译失败,
  10. // 而不是让 analyzer 的硬编码偏移静默读错。
  11. //
  12. // 布局分组 (v11 重排, 全部标量字段 8 字节对齐、零对齐填充):
  13. //
  14. // [0,16) 标识与分配: Magic/Version/PageSize/NextFreePage
  15. // [16,48) 时间与事务: CreateTime/ModifyTime/NextTxnID/GlobalFlags (8 字节标量)
  16. // [48,76) 表/空闲链/WAL: TablePage/TableCount/FreePageStart/FreePageEnd/
  17. // FreePageCount/FirstWALPage/LastWALPage (4 字节标量, 两两紧邻无填充)
  18. // [76,332) 扩展: ExtConfigHdr[8] (预留) + HeaderCrc uint32 (偏移 84,
  19. // 整页 XXH32 校验, 写入前由 PrepareWrite 生成) + ExtConfig[244]
  20. // [332,4096) 介绍区: Comment[3764] — 占满头页余下全部空间 (建库默认写入
  21. // 当前版本的功能介绍与修复点, 见 VersionComment; 用户可用
  22. // DB.SetComment 改写, 上限即本字段容量)
  23. //
  24. // 8 字节标量全部排在前半段 (偏移 16~48), 4 字节标量排在后半段 (偏移 48~76),
  25. // 任何字段后面都不留填充字节。
  26. var (
  27. // 整体头页结构体必须严格等于4K
  28. _ [PageSize4K - unsafe.Sizeof(HeaderPage{})]struct{}
  29. _ [unsafe.Sizeof(HeaderPage{}) - PageSize4K]struct{}
  30. // FreePageEnd 紧跟 FreePageStart,间隔4字节
  31. _ [unsafe.Offsetof(HeaderPage{}.FreePageEnd) - (unsafe.Offsetof(HeaderPage{}.FreePageStart) + 4)]struct{}
  32. // 锁定各个字段偏移
  33. _ [16]byte = [unsafe.Offsetof(HeaderPage{}.CreateTime)]byte{}
  34. _ [24]byte = [unsafe.Offsetof(HeaderPage{}.ModifyTime)]byte{}
  35. _ [32]byte = [unsafe.Offsetof(HeaderPage{}.NextTxnID)]byte{}
  36. _ [40]byte = [unsafe.Offsetof(HeaderPage{}.GlobalFlags)]byte{}
  37. _ [84]byte = [unsafe.Offsetof(HeaderPage{}.HeaderCrc)]byte{}
  38. _ [332]byte = [unsafe.Offsetof(HeaderPage{}.Comment)]byte{}
  39. // Comment 区域大小校验
  40. _ [PageSize4K - 332]byte = [unsafe.Sizeof(HeaderPage{}.Comment)]byte{}
  41. )
  42. // 数据库全局头页(PageID=0 固定)
  43. type HeaderPage struct {
  44. Magic [4]byte // 0 魔术字,识别文件类型
  45. Version uint32 // 4 版本
  46. PageSize uint32 // 8 每页大小(比如 4096)
  47. NextFreePage uint32 // 12 下一个可用空闲页
  48. CreateTime int64 // 16 建库时间 (纳秒)
  49. ModifyTime int64 // 24 最近修改时间 (纳秒)
  50. NextTxnID uint64 // 32 下一个事务 ID
  51. GlobalFlags uint64 // 40 全局状态
  52. TablePage uint32 // 48 【唯一重要】表链起始页ID
  53. TableCount uint32 // 52 表数量
  54. FreePageStart uint32 // 56 空闲页链表头
  55. FreePageEnd uint32 // 60 空闲页链表尾 (0 = 空闲链为空)。
  56. // 不变量: FreePageStart == 0 当且仅当 FreePageEnd == 0,
  57. // 两者必须同生同灭 (摘链/还链处维护)。
  58. FreePageCount uint32 // 64 空闲页数量
  59. FirstWALPage uint32 // 68 未恢复的 WAL 链首页 ID (0=无待恢复WAL)
  60. LastWALPage uint32 // 72 WAL 链尾页 ID (追加写入用)
  61. //将来以后可以是 前 4 字节:`ExtVer` 扩展区版本号 后 4 字节:`ExtFlags` 扩展特性标记位 `ExtVer == 0`:旧库,
  62. //`ExtConfig`区域未初始化,忽略解析 `ExtVer >=1`:按对应版本解析 244 字节扩展配置 ExtFlags- `bit0`:数据库只读标记
  63. //- `bit1`:启用页级独立校验(除头页 CRC 外,普通数据页单独校验)
  64. //- `bit2`:启用数据压缩
  65. //- `bit3`:启用加密,密钥索引放在 ExtConfig 里面
  66. //- `bit4`:WAL 持久化模式标记
  67. //- 其余 bit 预留,未来新增特性
  68. ExtConfigHdr [8]byte // 76 扩展配置: 头 8 字节 (预留, 保持 84 前不动) 扩展配置
  69. HeaderCrc uint32 // 84 整页 XXH32 校验 (写入前由 PrepareWrite 生成;
  70. // 旧 v11 库该字段为 0 = 无校验, 首次写头页自动生成)
  71. ExtConfig [244]byte // 88 扩展配置其余
  72. Comment [PageSize4K - 332]byte // 332 数据库介绍区 (3764B, 占满整页余下空间;
  73. // 建库默认写入 VersionComment, 可用 DB.SetComment 改写)
  74. }
  75. // CalcHeaderCrc 计算整页 4096 字节的 XXH32。计算期间临时把 HeaderCrc 字段
  76. // 清零 (CRC 的覆盖范围是"除 CRC 自身外的整页"), 算完恢复原值 —— 本方法
  77. // 不改变结构体状态, 可随意调用。
  78. //
  79. // 序列化方式与 ReadHeader/writeHeaderLocked 一致 (unsafe 直映, 小端机
  80. // 内存布局即盘上布局), 所以这里算出的值与盘上字节严格对应。
  81. func (h *HeaderPage) CalcHeaderCrc() uint32 {
  82. buf := (*[PageSize4K]byte)(unsafe.Pointer(h))[:]
  83. off := int(unsafe.Offsetof(HeaderPage{}.HeaderCrc))
  84. saved := binary.LittleEndian.Uint32(buf[off : off+4])
  85. binary.LittleEndian.PutUint32(buf[off:off+4], 0)
  86. crc := XXH32(buf)
  87. binary.LittleEndian.PutUint32(buf[off:off+4], saved)
  88. return crc
  89. }
  90. // Validate 打开数据库时的头页校验。
  91. //
  92. // - Magic 必查;
  93. // - CRC: 版本 11 且 HeaderCrc == 0 视为【旧 v11 库】—— 该字段在旧库里是
  94. // ExtConfig 预留区, 恒为 0 —— 跳过校验, 首次 PrepareWrite 会自动生成
  95. // CRC (版本号不变, 同号文件自动升级出校验能力);
  96. // 其余情况 (v11 已带 CRC, 或未来 v12+) 一律校验, 不匹配返回错误。
  97. func (h *HeaderPage) Validate() error {
  98. if h.Magic != Magic {
  99. return fmt.Errorf("invalid database file: bad magic %v, expected %v", h.Magic, Magic)
  100. }
  101. if h.Version == Version && h.HeaderCrc == 0 {
  102. return nil // 旧 v11: 无 CRC, 跳过
  103. }
  104. if got := h.CalcHeaderCrc(); got != h.HeaderCrc {
  105. return fmt.Errorf("头页 CRC 校验失败: 期望 %08x, 实际 %08x —— 文件可能损坏或不是 VRTX 数据库", h.HeaderCrc, got)
  106. }
  107. return nil
  108. }
  109. // PrepareWrite 头页落盘前调用: 计算整页 XXH32 写入 HeaderCrc 字段。
  110. //
  111. // 所有头页写都汇聚在 File.writeHeaderLocked 一处, 该方法在那里被统一调用,
  112. // 因此任何路径 (Commit / 页分配 / SetComment / VACUUM ...) 出盘的头页
  113. // CRC 都是最新的。对旧 v11 库, 这一步同时就是"自动生成 CRC"的升级点。
  114. func (h *HeaderPage) PrepareWrite() {
  115. h.HeaderCrc = h.CalcHeaderCrc()
  116. }
  117. // newPageWithReader 分配新页:优先从空闲链表头取, 否则递增分配。
  118. // 必须带读盘能力 —— 空闲链的 next 存在页头 PageHdr.NextPage, 不读页就摘不下来。
  119. //
  120. // writePage 用来把摘到的页擦成"干净空白页"(可为 nil, 表示调用方自己保证整页覆写)。
  121. //
  122. // 为什么擦页必须发生在这个入口:
  123. // - 空闲链上的页是回收来的, 而 recycleChain 是 O(1) 拼接 —— 只改写链尾 1 页,
  124. // 链内页的正文一个字节都不动 (逐页覆写就把 O(1) 打回 O(n), DropTable 的堆链、
  125. // 提交后的整条 WAL 链正是靠它才不慢)。于是这些页在盘上留着它上一世的内容:
  126. // 典型是 WAL 页 (Type=7、UseSize=16384、NextPage 还指着旧 WAL 链)。
  127. // - 分配方拿到页号后只要有一次"先读后写", 读到的就是这些残存字节。历史上
  128. // IndexMetaPage.Init 正是这么把整页 WAL 残留当成 IndexMeta 数组解析出 91 条垃圾
  129. // 元数据 (EntryPage=16777216), 最终让 DROP TABLE 拿着垃圾页号去 FreePage:
  130. // 写到几十 GB 偏移、稀疏文件暴涨到 40GB、DROP 假死 52s。
  131. // - 这条不变量以前靠"每个调用点自己记得整页覆写"维持 (当时十几个调用点确实都做到了),
  132. // 但它散在十几个地方, 违约的代价是静默的数据错乱而不是报错。现在收到唯一入口上:
  133. // 凡是 NewPage 交出的页, 盘上/池里一定是"全 0 + 自述页号"。
  134. //
  135. // 为什么用 writePage (= File.WritePage, 走缓冲池) 而不是直写盘:
  136. // 调用方紧接着的整页写入会覆盖池里同一份缓存, 因此不额外产生物理 IO, 顺带把池里
  137. // 可能残留的陈旧副本一并覆盖; 未启用缓冲池时退化为一次直写 (每页一次, 不是每行一次)。
  138. func (header *HeaderPage) newPageWithReader(readPage func(uint32, []byte) error,
  139. writePage func(uint32, []byte) error) uint32 {
  140. var pid uint32
  141. if header.FreePageStart != 0 {
  142. pid = header.FreePageStart
  143. buf := make([]byte, PageSize)
  144. if err := readPage(pid, buf); err == nil {
  145. // 空闲页与其它页一样符合 PageHdr: 下一个空闲页 ID 存在页头的 NextPage
  146. next := page.HdrOf(buf).NextPage
  147. header.FreePageStart = next
  148. if next == 0 {
  149. // 摘完链表就空了, 尾指针必须跟着清零, 否则留下悬垂尾指针
  150. header.FreePageEnd = 0
  151. }
  152. header.FreePageCount--
  153. } else {
  154. // 读失败,回退到递增分配
  155. pid = header.NextFreePage
  156. header.NextFreePage++
  157. }
  158. } else {
  159. pid = header.NextFreePage
  160. header.NextFreePage++
  161. }
  162. if writePage != nil {
  163. // 页头只留自述页号 (Type=0 未定型、UseSize=页头长), 其余字节全 0。
  164. // 自述页号不是可有可无的: loadMetas / Init 这类"读盘重建"的路径会校验
  165. // Hdr.PageID == pageID, 靠它挡住"页号复用错了"的误读。
  166. clean := make([]byte, PageSize)
  167. page.HdrOf(clean).Init(0, pid)
  168. _ = writePage(pid, clean)
  169. }
  170. return pid
  171. }
  172. // recyclePage 回收一个页到空闲链表(头插, 只写 1 页)。
  173. //
  174. // 页统一符合 PageHdr: 空闲链指针写在页头的 NextPage (偏移 12), 不再占用偏移 0
  175. // 的 Type/RefCount 字段。这样空闲页仍是一张"正规页", 而且堆页/索引链的 NextPage
  176. // 与空闲链的 NextPage 是同一个字段 —— 整条链可以 O(1) 拼接到空闲链上。
  177. func (header *HeaderPage) recyclePage(pageID uint32, savePage func(uint32, []byte) error) {
  178. buf := make([]byte, PageSize)
  179. hdr := page.HdrOf(buf)
  180. hdr.PageID = pageID
  181. hdr.NextPage = header.FreePageStart
  182. header.FreePageStart = pageID
  183. if header.FreePageEnd == 0 {
  184. // 原空闲链为空: 这一页既是头也是尾
  185. header.FreePageEnd = pageID
  186. }
  187. header.FreePageCount++
  188. _ = savePage(pageID, buf)
  189. }
  190. // recycleChain 把一条已经用 PageHdr.NextPage 串好的整链 (head…tail) 整体挂到
  191. // 空闲链表尾。O(1): 只改写「原空闲链尾」1 页 + 更新头页, 既不逐页遍历也不逐页覆零。
  192. //
  193. // 前提: head…tail 每页的 next 都写在 PageHdr.NextPage, 且 tail.NextPage == 0。
  194. // count 是该链的页数, 由构造方 (WAL 写入时) 或遍历方 (崩溃恢复时) 顺手统计而来 ——
  195. // 正因为链长已知, 这里才能不遍历就把 FreePageCount 加对。
  196. //
  197. // 为什么挂链尾而不是头插: 刚回收的大块 (比如整张被 DROP 的表) 排在空闲链后段,
  198. // 空闲链头的分配前沿不动, 语义也更接近「把这些页排队等着复用」。
  199. func (header *HeaderPage) recycleChain(head, tail uint32, count uint32,
  200. readPage func(uint32, []byte) error, savePage func(uint32, []byte) error) error {
  201. if head == 0 || tail == 0 || count == 0 {
  202. return nil
  203. }
  204. if header.FreePageEnd != 0 {
  205. // 原空闲链尾 → 新链头, 两条链就此接成一条
  206. buf := make([]byte, PageSize)
  207. if err := readPage(header.FreePageEnd, buf); err != nil {
  208. return err
  209. }
  210. page.HdrOf(buf).NextPage = head
  211. if err := savePage(header.FreePageEnd, buf); err != nil {
  212. return err
  213. }
  214. } else {
  215. // 原空闲链为空: 这条链就是整条空闲链
  216. header.FreePageStart = head
  217. }
  218. header.FreePageEnd = tail
  219. header.FreePageCount += count
  220. return nil
  221. }
  222. // recycleChainWithCache 整链还链 + 刷新被改写页的缓冲池副本。
  223. //
  224. // recycleChain 用 savePage (=WritePageDirect) 改写「原空闲链尾」, 绕过了 BufferPool;
  225. // 那一页在池里可能还留着干净旧副本, 之后任何走 ReadPage 的读取都会拿到错内容 ——
  226. // 所以从盘读回、同步一次缓存。链内其它页的 next 没被改动, 池里的副本仍然准确,
  227. // 不必逐页刷新 (逐页刷新就等于把 O(1) 又变回 O(n) 了)。
  228. func recycleChainWithCache(ff *File, header *HeaderPage, head, tail, count uint32) error {
  229. if count == 0 {
  230. return nil
  231. }
  232. oldTail := header.FreePageEnd
  233. if err := header.recycleChain(head, tail, count, ff.readPageDisk, ff.WritePageDirect); err != nil {
  234. return err
  235. }
  236. if oldTail != 0 {
  237. syncBuf := make([]byte, PageSize)
  238. if err := ff.readPageDisk(oldTail, syncBuf); err == nil {
  239. ff.SyncCache(oldTail, syncBuf)
  240. }
  241. }
  242. return nil
  243. }
  244. func newHeader(desc string) (*HeaderPage, error) {
  245. // 未提供介绍 → 默认写入当前版本的功能介绍与修复点 (VersionComment)
  246. if desc == "" {
  247. desc = VersionComment
  248. }
  249. var comment [PageSize4K - 332]byte
  250. // 1. 安全判断:防止描述超长
  251. if len(desc) > len(comment) {
  252. return nil, fmt.Errorf("描述字符串长度 %d 超过最大限制 %d", len(desc), len(comment))
  253. }
  254. copy(comment[:], desc)
  255. // 2. 初始化头部(全部正确赋值,无遗漏、无坑)
  256. return &HeaderPage{
  257. Magic: Magic, // 魔数
  258. Version: Version, // 版本
  259. PageSize: PageSize, // 页大小
  260. NextFreePage: 2, // 下一个分配页从 2 开始(0=header, 1=table page 已预留)
  261. TablePage: 0, // 初始无表
  262. TableCount: 0, // 表数量=0
  263. CreateTime: time.Now().UnixNano(), // 纳秒时间戳
  264. ModifyTime: time.Now().UnixNano(),
  265. FreePageStart: 0, // 初始无空闲页
  266. FreePageCount: 0, // 空闲页数量=0
  267. GlobalFlags: 0, // 无标志
  268. NextTxnID: 1, // 事务 ID 从 1 开始 (0 保留, 便于 WAL 页首页判定)
  269. ExtConfigHdr: [8]byte{0}, // 扩展配置 前 4 字节:`ExtVer` 扩展区版本号 后 4 字节:`ExtFlags` 扩展特性标记位
  270. ExtConfig: [244]byte{0}, // 清空扩展配置
  271. Comment: comment, // 数据库描述
  272. }, nil
  273. }