deleter.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. package core
  2. import (
  3. "cmp"
  4. "encoding/binary"
  5. "slices"
  6. "sync"
  7. "git.ycl.pub/yl32940/TsDB2/DB/core/index"
  8. "git.ycl.pub/yl32940/TsDB2/DB/core/index/btree"
  9. "git.ycl.pub/yl32940/TsDB2/DB/core/index/datapage"
  10. "git.ycl.pub/yl32940/TsDB2/DB/core/page"
  11. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  12. )
  13. // asyncFreeMemory 把 FreeOSMemory() 丢到后台 goroutine 执行, 不阻塞 VACUUM 主流程。
  14. //
  15. // 之前每 rebuild 完一个索引就同步 asyncFreeMemory() —— 大表 (百万行 + 多索引)
  16. // VACUUM 时 GC 峰值可达数秒, 用户侧直接超时。后台化后 VACUUM 主线程立即返回,
  17. // GC 由 Go scheduler 在空闲时调度; 若未来需要可加参数开关禁用。
  18. func asyncFreeMemory() {
  19. go func() {
  20. defer func() { _ = recover() }()
  21. asyncFreeMemory()
  22. }()
  23. }
  24. // Deleter 删除操作对象
  25. type Deleter struct {
  26. table *Table
  27. where Condition
  28. }
  29. // Delete 返回删除操作对象
  30. func (table *Table) Delete() *Deleter {
  31. return &Deleter{table: table}
  32. }
  33. // Where 设置删除条件
  34. func (d *Deleter) Where(cond Condition) *Deleter {
  35. d.where = cond
  36. return d
  37. }
  38. // Exec 执行删除
  39. // SQLite 风格优化: 先软删除 DataPage 行 (极快), 再重建索引 (流式, 低内存)
  40. // 避免逐行级联删除 B+ 树的高昂开销
  41. func (d *Deleter) Exec() (int64, error) {
  42. defer d.table.db.lockOp()() // 批量删除全程串行; 内部 scanWithPKID/重建索引同 goroutine 重入
  43. defer d.table.db.beginOp()() // 软删 + 全索引重建是一个操作, 中途阈值提交会切成半成品
  44. // 找到主键索引
  45. var pkMeta *IndexMeta
  46. for i := range d.table.indexMeta.metas {
  47. m := &d.table.indexMeta.metas[i]
  48. if m.Flag&0x80 != 0 {
  49. continue
  50. }
  51. if m.IndexType == page.IndexTypePrimary || m.IndexType == page.IndexTypeTime {
  52. pkMeta = &d.table.indexMeta.metas[i]
  53. break
  54. }
  55. }
  56. if pkMeta == nil {
  57. return 0, nil
  58. }
  59. // 本流程靠"重建索引"落地, 前提是每种索引都支持从零批量构建。
  60. // 必须在 Phase 1 之前校验: 那里已经把行标记成删除, 事后再报错会留下
  61. // "行没了、索引还是旧的" 的半途状态, 比直接拒绝更糟。
  62. if err := d.checkRebuildSupported(pkMeta); err != nil {
  63. return 0, err
  64. }
  65. // 用 Scan 流式收集匹配行的 pkID (避免 Find 反序列化全行)。
  66. // 必须用 scanWithPKID 拿真实 pkID: 行数据里没有 pkID 字段 (它是行的 8 字节前缀),
  67. // 旧实现退化成"取第一个 uint64 字段值当 pkID", 主键列不是行内第一个 8 字节列时
  68. // 会删掉另一个主键指向的行, 而真正匹配的行一条都没删 —— 静默删错数据。
  69. pkIDs := make(map[uint64]bool)
  70. err := d.table.Query().Where(d.where).scanWithPKID(func(pkID uint64, _ []types.FieldValue) bool {
  71. pkIDs[pkID] = true
  72. return true
  73. })
  74. if err != nil {
  75. return 0, err
  76. }
  77. if len(pkIDs) == 0 {
  78. return 0, nil
  79. }
  80. // Phase 1: 软删除 DataPage 中的行 (只标记 DeletedFlag, 极快!)
  81. pkHandler, _ := index.Compose.GetHandler(pkMeta.IndexType)
  82. pkStore, _ := pkHandler.(index.RowStoreHandler)
  83. if pkStore == nil {
  84. return 0, nil // 主键索引不承载行数据, 软删除无从谈起 (正常不会发生)
  85. }
  86. dpIDs := pkStore.GetDataPageIDs(d.table.handle, pkMeta.EntryPage)
  87. var count int64
  88. pkKeyBE := make([]byte, 8) // 复用缓冲区
  89. for _, dpID := range dpIDs {
  90. dp, err := datapage.Load(d.table.handle, dpID)
  91. if err != nil {
  92. continue
  93. }
  94. changed := false
  95. rows := dp.GetAllRows()
  96. for _, storeRow := range rows {
  97. if len(storeRow) < 8 {
  98. continue
  99. }
  100. pkID := binary.BigEndian.Uint64(storeRow[:8])
  101. if pkIDs[pkID] {
  102. binary.BigEndian.PutUint64(pkKeyBE, pkID)
  103. if dp.DeleteByPK(pkKeyBE) {
  104. changed = true
  105. count++
  106. // 行已作废 → 释放它占用的动态字段堆块引用。
  107. // storeRow 是 GetAllRows 的独立拷贝, storeRow[8:] 即该行
  108. // 完整行数据(首字节是 schemaVer, 决定按哪个版本定位引用)。
  109. // 不释放的话堆页 RefCount 只增不减, 永不归零 → 堆页永久泄漏。
  110. d.table.heapReleaseRow(storeRow[8:])
  111. }
  112. }
  113. }
  114. if changed {
  115. dp.Save()
  116. }
  117. }
  118. // 释放 pkIDs 内存 (Phase 2 不需要)
  119. pkIDs = nil
  120. // Phase 2: 流式重建所有索引 (逐个索引从 DataPage 读取, 不缓存全部行)
  121. if err := d.rebuildIndexes(pkMeta, dpIDs); err != nil {
  122. return count, err
  123. }
  124. // Phase 3: 存活行已全部重建到全新的 DataPage, 旧 dpIDs 整个作废
  125. // (pkMeta.EntryPage 已指向新树, 二级索引只存 PKID, 都不再引用旧页)。
  126. // 必须在这里回收, 否则每执行一次批量删除就漏掉整批数据页。
  127. // 走延迟回收: 事务中途直写盘会让"旧页被清空"先于 WAL 落盘, 回滚即丢数据。
  128. // 顺序关键 —— 必须等上面两组读取循环都跑完, 否则会读到已还链的页。
  129. if d.table.handle.DeferFreePage != nil {
  130. for _, dpID := range dpIDs {
  131. d.table.handle.DeferFreePage(dpID)
  132. }
  133. }
  134. // 更新行数
  135. d.table.info.TotalRows -= uint64(count)
  136. d.table.saveTableInfo()
  137. // 失效缓存
  138. d.table.indexMeta.InvalidateRowCache()
  139. return count, nil
  140. }
  141. // rebuildIndexes 批量重建所有索引
  142. // 主键索引: 顺序创建 DataPage + 批量构建 B+ 树 (避免逐行搜索)
  143. // 二级索引: 收集条目 → 排序 → 批量构建 B+ 树
  144. func (d *Deleter) rebuildIndexes(pkMeta *IndexMeta, dpIDs []uint32) error {
  145. fields := d.table.Field
  146. // 行缓冲区按"所有 schema 版本中最宽的行" + 8B pkID 前缀分配。
  147. // 不能用 info.RowSize: AddField/DropField 之后它小于实际行长,
  148. // 沿用会让 GetAllRowsPooled 里的 copy(buf[:rowLen]) 越界 panic。
  149. rowSize := 8 + d.table.maxRowDataSize()
  150. // 内存池: 复用行缓冲区
  151. rowPool := &sync.Pool{
  152. New: func() interface{} {
  153. b := make([]byte, rowSize)
  154. return &b
  155. },
  156. }
  157. // ===== 重建主键索引: 顺序扫描 + 批量构建 =====
  158. var leafEntries [][]byte
  159. var curDP *datapage.DataPage
  160. var curDPID uint32
  161. for _, dpID := range dpIDs {
  162. dp, err := datapage.Load(d.table.handle, dpID)
  163. if err != nil {
  164. continue
  165. }
  166. rows := dp.GetAllRowsPooled(rowPool)
  167. for _, storeRow := range rows {
  168. if len(storeRow) < 8 {
  169. continue
  170. }
  171. rowData := storeRow[8:]
  172. indexKey := extractIndexKey(rowData, pkMeta, fields, d.table.versions)
  173. if indexKey == nil {
  174. continue
  175. }
  176. // 确保有可用的 DataPage
  177. if curDP == nil {
  178. curDP = datapage.New(d.table.handle)
  179. curDPID = curDP.PageID()
  180. }
  181. // 当前 DataPage 已满则创建新的
  182. if curDP.IsFull(uint32(len(storeRow))) {
  183. curDP = datapage.New(d.table.handle)
  184. curDPID = curDP.PageID()
  185. }
  186. slot := curDP.AppendRowSlot(storeRow)
  187. curDP.Save()
  188. if slot == 0 {
  189. // IsFull 预判与实际容量出现出入时的兜底: 宁可跳过, 也不要写出 slot=0 的坏叶条目
  190. continue
  191. }
  192. // 收集叶条目: [pkID(8B BE) + dpID(4B BE) + slot(2B BE)] —— 带 slot 才能 O(1) 取行
  193. pkID := binary.BigEndian.Uint64(storeRow[:8])
  194. pkKeyBE := make([]byte, 8)
  195. binary.BigEndian.PutUint64(pkKeyBE, pkID)
  196. entry := make([]byte, 14)
  197. copy(entry[:8], pkKeyBE)
  198. binary.BigEndian.PutUint32(entry[8:12], curDPID)
  199. binary.BigEndian.PutUint16(entry[12:14], uint16(slot))
  200. leafEntries = append(leafEntries, entry)
  201. }
  202. // 归还缓冲区到池
  203. for i := range rows {
  204. rowPool.Put(&rows[i])
  205. }
  206. }
  207. // 按 pkID 升序排序后再批量构建。
  208. // BuildFromSorted 假设输入有序, 不能依赖 DataPage 扫描顺序:
  209. // 更新导致的行搬迁会把新 DataPage 排到扫描序列末尾,
  210. // 从而产生 1,2,3,4,6,...,100,5 这样的失序序列, 破坏 B+ 树不变式。
  211. slices.SortFunc(leafEntries, func(a, b []byte) int {
  212. return cmp.Compare(binary.BigEndian.Uint64(a[:8]), binary.BigEndian.Uint64(b[:8]))
  213. })
  214. // 批量构建主键 B+ 树 (一次创建所有节点, 避免逐行搜索)
  215. oldPKRoot := pkMeta.EntryPage
  216. pkMeta.EntryPage = btree.BuildFromSorted(leafEntries, d.table.handle)
  217. d.table.indexMeta.saveMeta(pkMeta)
  218. // 新树已建好、元数据已落盘, 旧主键树至此彻底不可达 → 归还其全部页。
  219. // 必须晚于建树: 否则这些页会先被 NewPage 拿去分给新树, 而这里还要按旧树结构遍历。
  220. d.releaseOldTree(pkMeta, oldPKRoot)
  221. leafEntries = nil // 释放内存
  222. asyncFreeMemory()
  223. // ===== 重建每个二级索引: 收集 → 排序 → 批量构建 =====
  224. for i := range d.table.indexMeta.metas {
  225. m := &d.table.indexMeta.metas[i]
  226. if m.Flag&0x80 != 0 {
  227. continue
  228. }
  229. if indexStoresRows(m) {
  230. // 行存储索引: 主键索引已在上一步重建; 时序索引在 Exec 开头的
  231. // checkRebuildSupported 里已被拒绝, 走不到这里。
  232. continue
  233. }
  234. // 收集所有二级索引条目
  235. var secEntries [][]byte
  236. for _, dpID := range dpIDs {
  237. dp, err := datapage.Load(d.table.handle, dpID)
  238. if err != nil {
  239. continue
  240. }
  241. rows := dp.GetAllRowsPooled(rowPool)
  242. for _, storeRow := range rows {
  243. if len(storeRow) < 8 {
  244. continue
  245. }
  246. pkID := binary.BigEndian.Uint64(storeRow[:8])
  247. rowData := storeRow[8:]
  248. secKey := extractIndexKey(rowData, m, fields, d.table.versions)
  249. if secKey == nil {
  250. continue
  251. }
  252. // 构建二级条目: [secKey + pkID(8B BE)]
  253. secEntry := make([]byte, len(secKey)+8)
  254. copy(secEntry[:len(secKey)], secKey)
  255. binary.BigEndian.PutUint64(secEntry[len(secKey):], pkID)
  256. secEntries = append(secEntries, secEntry)
  257. }
  258. for i := range rows {
  259. rowPool.Put(&rows[i])
  260. }
  261. }
  262. // 按复合键 (secKey, pkID) 排序: 二级树的复合分隔键与叶内复合二分都要求
  263. // 全树按复合键全局有序 —— 只按 secKey 排序不够 (同 key 条目的 pkID 顺序
  264. // 无法靠"稳定排序保持物理序"保证, 数据页按主键序而非 pkID 序)。
  265. slices.SortStableFunc(secEntries, func(a, b []byte) int {
  266. ka, kb := a[:len(a)-8], b[:len(b)-8]
  267. if c := compareBytes(ka, kb); c != 0 {
  268. return c
  269. }
  270. return cmp.Compare(binary.BigEndian.Uint64(a[len(a)-8:]), binary.BigEndian.Uint64(b[len(b)-8:]))
  271. })
  272. oldSecRoot := m.EntryPage
  273. if err := buildSecondaryFromEntries(d.table.handle, m, secEntries); err != nil {
  274. return err
  275. }
  276. d.table.indexMeta.saveMeta(m)
  277. // 同上: 新结构就位后归还旧结构的全部页。
  278. d.releaseOldTree(m, oldSecRoot)
  279. secEntries = nil
  280. asyncFreeMemory()
  281. }
  282. return nil
  283. }
  284. // releaseOldTree 归还一棵已被换根、彻底不可达的旧索引结构 (内部节点 + 叶子节点)。
  285. //
  286. // 调用前提:
  287. // - EntryPage 已改指新结构且已 saveMeta;
  288. // - 新结构已构建完成 —— 否则归还的页会被 NewPage 立刻分给新结构, 而这里还要
  289. // 按旧结构遍历收集页 ID;
  290. // - 此后不再有任何代码读这棵旧结构。
  291. //
  292. // 走 table.deferFreePage: 事务中途直写盘会让"页已清零"先于 WAL 落盘, 回滚/崩溃
  293. // 后旧根指针还在, 却指向一张空页。与 Phase 3 回收旧 DataPage 是同一套时序约定。
  294. //
  295. // 遍历方式按页布局分流: B+ 树节点结构统一, CollectAllPageIDs 按内部条目下降即可;
  296. // 哈希 / 复合索引没有共同形状, 走处理器自己实现的 index.PageCollector。
  297. //
  298. // 不回收的后果: 每执行一次批量删除就漏掉整个旧索引结构 (实测 2 万行两级索引
  299. // 约 40 页), 文件只涨不缩。
  300. func (d *Deleter) releaseOldTree(m *IndexMeta, rootPID uint32) {
  301. if m == nil {
  302. return
  303. }
  304. releaseIndexPages(d.table, m.IndexType, rootPID)
  305. }
  306. // checkRebuildSupported 在动任何数据之前确认全部活跃索引都能被重建。
  307. func (d *Deleter) checkRebuildSupported(pkMeta *IndexMeta) error {
  308. if err := bulkRebuildSupported(pkMeta); err != nil {
  309. return err
  310. }
  311. for i := range d.table.indexMeta.metas {
  312. m := &d.table.indexMeta.metas[i]
  313. if m.Flag&0x80 != 0 {
  314. continue
  315. }
  316. if err := bulkRebuildSupported(m); err != nil {
  317. return err
  318. }
  319. }
  320. return nil
  321. }