index_rebuild.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. package core
  2. import (
  3. "bytes"
  4. "encoding/binary"
  5. "fmt"
  6. "git.ycl.pub/yl32940/TsDB2/DB/core/index"
  7. "git.ycl.pub/yl32940/TsDB2/DB/core/index/btree"
  8. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  9. )
  10. // buildSecondaryFromEntries 用"已按 secKey 排好序"的条目 [secKey + pkID(8B BE)] 构建一个二级索引。
  11. //
  12. // 页布局决定做法, 不能一律批量建树:
  13. // - B+ 树二级索引 (含 IndexTypeCompose —— ComposeChainHandler 底层即 SBT): 批量建树;
  14. // - 哈希二级索引: 页不是 B+ 节点, 必须走 handler.Insert
  15. // (详见 rebuildSecondaryViaHandler 注释)。
  16. //
  17. // 调用方负责 m.EntryPage 的旧根归还 (整表重建走 releaseOldTree); 新建索引回填时
  18. // 旧根本就是 0, 无需归还。
  19. func buildSecondaryFromEntries(handle page.HandleDb, m *IndexMeta, entries [][]byte) error {
  20. if isBTreePageIndex(m) {
  21. m.EntryPage = btree.SBTBuildFromSorted(entries, handle)
  22. return nil
  23. }
  24. return rebuildSecondaryViaHandler(handle, m, entries)
  25. }
  26. // releaseIndexPages 归还一棵已彻底不可达的索引结构 (内部节点 + 叶子 / 桶链) 的全部页。
  27. //
  28. // 这是 releaseOldTree 与 DROP INDEX 的共用实现: 两者都换掉/摘掉了根, 旧结构从此没人读,
  29. // 必须把它的页还给空闲链, 否则文件只涨不缩。遍历方式按索引类型的页布局分流:
  30. // - B+ 树 (主键 / 二级 / 旧 Str): CollectAllPageIDs 按内部条目 BFS 下降;
  31. // - 哈希: 桶页 next 已统一到 PageHdr.NextPage, 一条链只登记一次, 提交后一次
  32. // FreeChain 还链 (O(1));
  33. // - 复合 / 时序: 扁平条目链, 走处理器自己的 PageCollector。
  34. //
  35. // 一定要在"新结构就位 (已 saveMeta)"之后调用, 否则归还的页会被 NewPage 立刻分给新结构,
  36. // 而这里还要按旧结构遍历 —— 会取到半截结构。
  37. func releaseIndexPages(table *Table, itype page.IndexType, rootPID uint32) {
  38. if rootPID == 0 {
  39. return
  40. }
  41. if isBTreePageType(itype) {
  42. for _, pid := range btree.CollectAllPageIDs(table.handle, rootPID) {
  43. table.deferFreePage(pid)
  44. }
  45. return
  46. }
  47. h, ok := index.Compose.GetHandler(itype)
  48. if !ok {
  49. return
  50. }
  51. // 整链延迟回收优先: 哈希索引的桶页 next 已统一到 PageHdr.NextPage, 且每条桶链的
  52. // (head, tail, count) 就持久化在目录槽里 —— 一条链只登记一次, 提交后一次 FreeChain
  53. // 还链 (O(1)), 不必逐页查询、逐页改写空闲链。
  54. if cc, ok := h.(index.ChainCollector); ok && table.handle.DeferFreeChain != nil {
  55. for _, ch := range cc.CollectChains(table.handle, rootPID) {
  56. if ch.Head == 0 || ch.Count == 0 {
  57. continue
  58. }
  59. if ch.Head == ch.Tail {
  60. table.deferFreePage(ch.Head) // 单页链 (例如元数据页)
  61. continue
  62. }
  63. table.handle.DeferFreeChain(ch.Head, ch.Tail, ch.Count)
  64. }
  65. return
  66. }
  67. pc, ok := h.(index.PageCollector)
  68. if !ok {
  69. return
  70. }
  71. for _, pid := range pc.CollectPageIDs(table.handle, rootPID) {
  72. table.deferFreePage(pid)
  73. }
  74. }
  75. // isBTreePageIndex 判断该索引的页是不是 B+ 节点页 (page.IndexHead + 内部条目结构)。
  76. //
  77. // 只有 B+ 树页能用 CollectAllPageIDs 安全遍历, 也只有它们能用
  78. // BuildFromSorted / SBTBuildFromSorted 批量构建。其余类型的页布局都不同:
  79. // - HashHandler: 元数据页 + 目录页链 + 桶页链。按 IndexHead 解析会把
  80. // BucketID / NextPage 当成 EntryLen / IsLeaf, 遍历出一串垃圾页 ID, 进而把
  81. // 不属于本索引的页归还掉 —— 那是数据损坏, 不是泄漏;
  82. // - TimeIndexHandler: 扁平条目链 + TimeDataPage, 没有内部节点。
  83. //
  84. // IndexTypeCompose 的处理器 (ComposeChainHandler) 底层就是 SBT 二级 B+ 树
  85. // (Insert/Search/RangeScan 全部委托 btree.SBT*), 页即 B+ 节点页 —— 与 BTree 同列。
  86. //
  87. // 注意时序索引 (IndexTypeTime): 它同样是行存储索引, 但页是扁平链而非 B+ 树。
  88. // 因此这里**不能**用 indexStoresRows 来判断 —— 那样会把时序索引当成 B+ 树, 既在
  89. // 回收时漏收页链上的其余页, 又让重建拿 B+ 树页去覆盖它 (见 bulkRebuildSupported)。
  90. func isBTreePageIndex(m *IndexMeta) bool {
  91. if m == nil {
  92. return false
  93. }
  94. return isBTreePageType(m.IndexType)
  95. }
  96. // isBTreePageType 仅按类型判断页布局是否为 B+ 节点页 (与 isBTreePageIndex 同源)。
  97. func isBTreePageType(t page.IndexType) bool {
  98. switch t {
  99. case page.IndexTypePrimary, page.IndexTypeBTree:
  100. return true
  101. case page.IndexTypeStr:
  102. // IndexTypeStr 是 IndexTypeBTree 的旧值, 页格式相同。
  103. return true
  104. case page.IndexTypeCompose:
  105. // ComposeChainHandler 底层即 SBT 二级 B+ 树, 页格式与 BTree 相同。
  106. return true
  107. default:
  108. return false
  109. }
  110. }
  111. // bulkRebuildSupported 校验一个索引能否参与批量删除的"重建"流程。
  112. //
  113. // 批量删除统一采用「存活行写进全新 DataPage + 索引从零重建」, 因此要求每种索引
  114. // 都具备从零批量构建的能力。页布局决定做法:
  115. // - 主键 B+ 树 / B+ 树二级索引 (含 IndexTypeCompose, 底层即 SBT): BuildFromSorted 批量建树
  116. // - 哈希二级索引: 清空根后走 handler.Insert 逐条重建
  117. //
  118. // 时序索引 (IndexTypeTime) 是"扁平链 + TimeDataPage": 行本身就住在按时间分段的
  119. // TimeDataPage 里, 重建必须重新分段, 与本流程的模型完全不同 —— 明确拒绝。
  120. // 旧实现没有这个校验, 会默默用 btree.BuildFromSorted 把时序索引整体改写成 B+ 树
  121. // (元数据仍写着 IndexTypeTime, 页却成了 B+ 节点), 之后 Search / RangeScan 全部错乱。
  122. func bulkRebuildSupported(m *IndexMeta) error {
  123. if m == nil {
  124. return nil
  125. }
  126. name := string(bytes.TrimRight(m.IndexName[:], "\x00"))
  127. if indexStoresRows(m) && m.IndexType != page.IndexTypePrimary {
  128. return fmt.Errorf(
  129. "批量删除不支持索引 %q (行存储索引类型 %d): 时序索引的行数据按时间分段存放, "+
  130. "批量重建需要重新分段, 请改用逐行删除", name, m.IndexType)
  131. }
  132. switch m.IndexType {
  133. case page.IndexTypePrimary, page.IndexTypeBTree, page.IndexTypeStr,
  134. page.IndexTypeCompose, page.IndexTypeHash:
  135. return nil
  136. default:
  137. return fmt.Errorf("批量删除不支持索引 %q 的类型 %d", name, m.IndexType)
  138. }
  139. }
  140. // rebuildSecondaryViaHandler 用索引自己的 Insert 从零重建哈希二级索引
  141. // (目录 + 桶链 —— 唯一的非 B+ 树二级索引)。
  142. //
  143. // 为什么不能沿用 SBTBuildFromSorted: 它产出的页是 B+ 节点布局, 而哈希索引的页
  144. // 是元数据页 + 目录页链 + 桶页链 —— 元数据仍写着 IndexTypeHash, 页却成了 B+ 树,
  145. // readHashMeta 在 page.IndexHeadSize 处的魔数校验直接失败
  146. // (errHashFormatIncompatible: 等值查找恒为 not found, 插入直接报错), 等于把索引写坏。
  147. //
  148. // 走 handler.Insert 的好处是正确性由正常写入路径背书: 目录扩容 / 桶分裂自动走通。
  149. // 代价是这条路径为 O(n log n), 只影响哈希 —— B+ 树二级索引 (含复合索引) 仍走
  150. // 批量建树, 性能没有回退。哈希不要求 entries 有序, 不受影响。
  151. func rebuildSecondaryViaHandler(handle page.HandleDb, m *IndexMeta, entries [][]byte) error {
  152. name := string(bytes.TrimRight(m.IndexName[:], "\x00"))
  153. h, ok := index.Compose.GetHandler(m.IndexType)
  154. if !ok {
  155. return fmt.Errorf("重建索引 %q: 类型 %d 没有注册处理器", name, m.IndexType)
  156. }
  157. // 从空根开始重建; 旧结构由 releaseOldTree 按 PageCollector 单独归还。
  158. m.EntryPage = 0
  159. for _, e := range entries {
  160. if len(e) < 8 {
  161. continue
  162. }
  163. keyLen := len(e) - 8
  164. root, err := h.Insert(handle, &m.EntryPage, e[:keyLen],
  165. binary.BigEndian.Uint64(e[keyLen:]), nil, keyLen)
  166. if err != nil {
  167. return fmt.Errorf("重建索引 %q: %w", name, err)
  168. }
  169. m.EntryPage = root
  170. }
  171. return nil
  172. }
  173. // compareBytes 按字典序比较两个字节切片
  174. func compareBytes(a, b []byte) int {
  175. ml := len(a)
  176. if len(b) < ml {
  177. ml = len(b)
  178. }
  179. for i := 0; i < ml; i++ {
  180. if a[i] < b[i] {
  181. return -1
  182. }
  183. if a[i] > b[i] {
  184. return 1
  185. }
  186. }
  187. if len(a) < len(b) {
  188. return -1
  189. }
  190. if len(a) > len(b) {
  191. return 1
  192. }
  193. return 0
  194. }