소스 검색

fix: DropTable 页回收 - 正确释放 B+树/DataPage/Heap 页到空闲链表

- 新增 recycleTablePages: 回收表的所有页(B+树节点、DataPage、Heap、Overflow)
- 修复关键 bug: 二级索引叶子存 PKID(8B) 不是 DataPageID(4B)
  只对主键/时序索引调用 GetDataPageIDs, 避免误释放其他表页面
- 不释放 IndexMeta 页(所有表共享), 仅标记条目为删除
- 用表自己的 indexMeta.metas 收集索引页, 不遍历共享链
- DropTable 前后各清一次 ClearCache 防止 BufferPool 脏数据
- full_test 28/28 全部通过 (之前 27/28)
yl32940 1 개월 전
부모
커밋
b979ac5954
6개의 변경된 파일과 113개의 추가작업 그리고 6개의 파일을 삭제
  1. 74 1
      DB/core/TablePage.go
  2. 4 0
      DB/core/db.go
  3. 30 0
      DB/core/index/btree/primary.go
  4. 1 1
      DB/core/pool/buffer_pool.go
  5. 4 4
      demo/dirty_threshold_debug/main.go
  6. BIN
      fulltest.db

+ 74 - 1
DB/core/TablePage.go

@@ -1,6 +1,7 @@
 package core
 
 import (
+	"TsDB2/DB/core/index/btree"
 	"TsDB2/DB/core/page"
 	"TsDB2/DB/core/types"
 	"TsDB2/DB/core/util"
@@ -249,7 +250,7 @@ func (t *TablePage) Create(name string, fields []types.FieldInfo) (*Table, error
 }
 
 // ===========================================================================
-// ✅ Drop 软删除
+// ✅ Drop 删除表并回收所有页
 // ===========================================================================
 func (t *TablePage) Drop(name string) error {
 	pageID := t.hdr.PageID
@@ -265,6 +266,17 @@ func (t *TablePage) Drop(name string) error {
 		for offset+t.tableInfoSize <= hdr.UseSize {
 			info := (*TableInfo)(unsafe.Pointer(&buf[offset]))
 			if util.BytesToString(info.Name[:]) == name && !util.IsHighBitSet(info.Flag) {
+				// 找到该表在内存中的 Table 对象,获取其索引元数据
+				var tableEntry *Table
+				for _, tb := range t.tables {
+					if util.BytesToString(tb.info.Name[:]) == name && !util.IsHighBitSet(tb.info.Flag) {
+						tableEntry = tb
+						break
+					}
+				}
+				// 回收该表所有页 (用该表自己的 indexMeta.metas,不遍历共享的 IndexMeta 页链)
+				t.recycleTablePages(info, tableEntry)
+				// 软删除标记
 				info.Flag = util.SetHighBit(info.Flag)
 				t.handle.SavePage(pageID, buf)
 				for i := range t.tables {
@@ -282,6 +294,67 @@ func (t *TablePage) Drop(name string) error {
 	return ErrTableNotFound
 }
 
+// recycleTablePages 回收表的所有页 (B+树页、DataPage页、Heap页)
+// 注意: IndexMeta 页是所有表共享的,不能释放!
+func (t *TablePage) recycleTablePages(info *TableInfo, tableEntry *Table) {
+	if t.handle.FreePage == nil {
+		return
+	}
+
+	var pagesToFree []uint32
+
+	// 1. 用该表自己的 indexMeta.metas 收集 B+ 树页和 DataPage
+	//    不遍历共享的 IndexMeta 页链,避免误释放其他表的索引页
+	if tableEntry != nil && tableEntry.indexMeta != nil {
+		for i := range tableEntry.indexMeta.metas {
+			m := &tableEntry.indexMeta.metas[i]
+			if m.Flag&0x80 != 0 || m.EntryPage == 0 {
+				continue
+			}
+			// 收集 B+ 树所有节点页 (内部节点+叶子节点)
+			btPages := btree.CollectAllPageIDs(t.handle, m.EntryPage)
+			pagesToFree = append(pagesToFree, btPages...)
+			// 只有主键/时序索引的叶子节点存 DataPage ID
+			// 二级索引叶子存的是 PKID(8B),不是 DataPage ID!
+			if m.IndexType == page.IndexTypePrimary || m.IndexType == page.IndexTypeTime {
+				dpIDs := btree.GetDataPageIDs(t.handle, m.EntryPage)
+				pagesToFree = append(pagesToFree, dpIDs...)
+			}
+		}
+	}
+
+	// 2. 收集 Heap 页链 (溢出页)
+	heapID := info.FirstHeap
+	for heapID != 0 {
+		buf := make([]byte, PageSize)
+		if err := t.handle.ReadPage(heapID, buf); err != nil {
+			break
+		}
+		hdr := (*page.PageHdr)(unsafe.Pointer(&buf[0]))
+		nextHeap := hdr.NextPage
+		pagesToFree = append(pagesToFree, heapID)
+		heapID = nextHeap
+	}
+
+	// 3. 收集 Overflow 页链
+	oflowID := info.FirstOverflow
+	for oflowID != 0 {
+		buf := make([]byte, PageSize)
+		if err := t.handle.ReadPage(oflowID, buf); err != nil {
+			break
+		}
+		hdr := (*page.PageHdr)(unsafe.Pointer(&buf[0]))
+		nextOflow := hdr.NextPage
+		pagesToFree = append(pagesToFree, oflowID)
+		oflowID = nextOflow
+	}
+
+	// 4. 统一释放所有页
+	for _, pid := range pagesToFree {
+		t.handle.FreePage(pid)
+	}
+}
+
 func (t *TablePage) Find(name string) (*Table, bool) {
 	for i := range t.tables {
 		tb := t.tables[i]

+ 4 - 0
DB/core/db.go

@@ -152,12 +152,16 @@ func (db *DB) CreateTable(name string, fields []types.FieldInfo) (*Table, error)
 }
 
 func (db *DB) DropTable(name string) error {
+	// 先清除 BufferPool 缓存, 确保回收时读取磁盘最新数据
+	db.file.ClearCache()
 	if err := db.tables.Drop(name); err != nil {
 		return err
 	}
 	db.header.TableCount--
 	_ = db.file.WriteHeader(db.header)
 	db.updateTime()
+	// 回收页后再次清除缓存, 防止 stale 数据
+	db.file.ClearCache()
 	return nil
 }
 

+ 30 - 0
DB/core/index/btree/primary.go

@@ -1221,3 +1221,33 @@ func BTreeTraceSearch(rootPID uint32, key []byte, handle page.HandleDb) {
 		depth++
 	}
 }
+
+// CollectAllPageIDs 收集 B+ 树所有页 ID (包括内部节点和叶子节点)
+// 用于 DropTable 时释放所有 B+ 树占用的页
+func CollectAllPageIDs(handle page.HandleDb, rootPID uint32) []uint32 {
+	if rootPID == 0 {
+		return nil
+	}
+	var pages []uint32
+	// BFS 遍历所有节点
+	queue := []uint32{rootPID}
+	for len(queue) > 0 {
+		pid := queue[0]
+		queue = queue[1:]
+		pages = append(pages, pid)
+		n := btLoad(handle, pid)
+		if n == nil {
+			continue
+		}
+		if !n.isLeaf() {
+			// 内部节点: 收集所有子节点
+			for i := 0; i < int(n.info.Count); i++ {
+				childPID := btPID(btEntry(n, i))
+				if childPID != 0 {
+					queue = append(queue, childPID)
+				}
+			}
+		}
+	}
+	return pages
+}

+ 1 - 1
DB/core/pool/buffer_pool.go

@@ -144,7 +144,7 @@ func (bp *BufferPool) WritePage(pageID uint32, buf []byte) error {
 		bp.pages[pageID] = &pageEntry{data: cached, dirty: true, pageLen: len(buf)}
 		bp.dirtyCount++
 	}
-	
+
 	// 检查是否超过阈值 (脏页或总页面数)
 	// 使用总页面数作为第二道安全网,防止脏页计数增长缓慢时内存无限膨胀
 	totalPages := len(bp.pages)

+ 4 - 4
demo/dirty_threshold_debug/main.go

@@ -19,7 +19,7 @@ const dbPath = "dirty_debug.db"
 func main() {
 	fmt.Println("========== 脏页阈值调试 ==========")
 	os.Remove(dbPath)
-	
+
 	db, _ := DB.Open(dbPath)
 	db.SetAutoCommitDelay(0) // 禁用防抖
 	tbl, _ := DB.UseTable[Record](db, "test")
@@ -28,7 +28,7 @@ func main() {
 	// 脏页阈值 = 2000 页
 	// 每行约 72 bytes, 16KB/页 ≈ 228 行/页
 	// 2000 页 ≈ 456000 行应该触发阈值
-	
+
 	fmt.Println("插入数据 (禁用防抖, 仅靠脏页阈值)...")
 	for i := 1; i <= 500000; i++ {
 		tbl.Insert(&Record{
@@ -37,7 +37,7 @@ func main() {
 			Score: int64(i % 1000),
 			Dept:  fmt.Sprintf("dept_%d", i%10),
 		})
-		
+
 		if i%50000 == 0 {
 			fi, _ := os.Stat(dbPath)
 			sizeMB := float64(fi.Size()) / 1024 / 1024
@@ -47,7 +47,7 @@ func main() {
 
 	fmt.Println("\n手动 Commit...")
 	db.Commit()
-	
+
 	fi, _ := os.Stat(dbPath)
 	fmt.Printf("Commit 后文件: %.2f MB\n", float64(fi.Size())/1024/1024)
 

BIN
fulltest.db