Răsfoiți Sursa

feat: 实现锁表机制,Commit/Flush期间禁止写入

新增功能:
1. BufferPool 满时拒绝写入 (返回错误)
2. Commit/Flush 期间自动锁表 (阻塞等待)
3. 提供 LockWrites/UnlockWrites API (手动控制)

实现细节:
- 使用 writeLocked 标志 + channel 通知机制
- WritePage 开始时检查 writeLocked,如果锁定则阻塞等待
- CollectDirty 开始时调用 LockWrites,结束时 UnlockWrites
- LockWrites/UnlockWrites 通过 File → core.DB → DB 层暴露

适用场景:
- 批量导入前手动锁表,防止并发修改
- Commit 期间自动锁表,保证数据一致性
- BufferPool 满时拒绝写入,防止内存溢出

注意:
- 锁表是阻塞式的,不是返回错误
- 解锁后所有等待的写操作会继续执行
yl32940 1 lună în urmă
părinte
comite
68e65b7d81
6 a modificat fișierele cu 232 adăugiri și 7 ștergeri
  1. 14 0
      DB/core/File.go
  2. 11 1
      DB/core/db.go
  3. 59 6
      DB/core/pool/buffer_pool.go
  4. 11 0
      DB/db.go
  5. 137 0
      demo/lock_table_test/main.go
  6. BIN
      lock_table_test.db

+ 14 - 0
DB/core/File.go

@@ -40,6 +40,20 @@ func (ff *File) SetDirtyThreshold(threshold int, onThreshold func()) {
 	}
 	}
 }
 }
 
 
+// LockWrites 锁定写入(禁止 Insert/Update/Delete)
+func (ff *File) LockWrites() {
+	if ff.bp != nil {
+		ff.bp.LockWrites()
+	}
+}
+
+// UnlockWrites 解锁写入(允许 Insert/Update/Delete)
+func (ff *File) UnlockWrites() {
+	if ff.bp != nil {
+		ff.bp.UnlockWrites()
+	}
+}
+
 // CollectDirty 收集所有脏页快照并清除脏标记 (配合 WAL 使用)
 // CollectDirty 收集所有脏页快照并清除脏标记 (配合 WAL 使用)
 func (ff *File) CollectDirty() []pool.DirtyPage {
 func (ff *File) CollectDirty() []pool.DirtyPage {
 	if ff.bp != nil {
 	if ff.bp != nil {

+ 11 - 1
DB/core/db.go

@@ -280,10 +280,20 @@ func (db *DB) stopAutoCommit() {
 // SetDirtyThreshold 设置脏页阈值(页数),超过此值自动触发 Commit
 // SetDirtyThreshold 设置脏页阈值(页数),超过此值自动触发 Commit
 func (db *DB) SetDirtyThreshold(threshold int) {
 func (db *DB) SetDirtyThreshold(threshold int) {
 	db.file.SetDirtyThreshold(threshold, func() {
 	db.file.SetDirtyThreshold(threshold, func() {
-		_ = db.Commit() // Commit 内部会自动停止防抖定时器
+		_ = db.Commit()  // Commit 内部会自动停止防抖定时器
 	})
 	})
 }
 }
 
 
+// LockWrites 锁定写入(禁止 Insert/Update/Delete)
+func (db *DB) LockWrites() {
+	db.file.LockWrites()
+}
+
+// UnlockWrites 解锁写入(允许 Insert/Update/Delete)
+func (db *DB) UnlockWrites() {
+	db.file.UnlockWrites()
+}
+
 // marshalHeader 将 HeaderPage 序列化为字节切片 (填充到 PageSize)
 // marshalHeader 将 HeaderPage 序列化为字节切片 (填充到 PageSize)
 func marshalHeader(h *HeaderPage) []byte {
 func marshalHeader(h *HeaderPage) []byte {
 	buf := make([]byte, PageSize)
 	buf := make([]byte, PageSize)

+ 59 - 6
DB/core/pool/buffer_pool.go

@@ -1,6 +1,7 @@
 package pool
 package pool
 
 
 import (
 import (
+	"errors"
 	"sync"
 	"sync"
 )
 )
 
 
@@ -32,6 +33,10 @@ type BufferPool struct {
 	dirtyCount     int                                   // 当前脏页数 (实时追踪)
 	dirtyCount     int                                   // 当前脏页数 (实时追踪)
 	diskRead       func(pageID uint32, buf []byte) error // 底层磁盘读
 	diskRead       func(pageID uint32, buf []byte) error // 底层磁盘读
 	diskWrite      func(pageID uint32, buf []byte) error // 底层磁盘写
 	diskWrite      func(pageID uint32, buf []byte) error // 底层磁盘写
+	
+	// 锁表机制
+	writeLocked    bool           // 写锁标志
+	writeChan      chan struct{}  // 用于通知解锁的 channel
 }
 }
 
 
 // NewBufferPool 创建页面缓冲池
 // NewBufferPool 创建页面缓冲池
@@ -59,12 +64,50 @@ func (bp *BufferPool) SetDirtyThreshold(threshold int, onThreshold func()) {
 	bp.onThreshold = onThreshold
 	bp.onThreshold = onThreshold
 }
 }
 
 
+// LockWrites 锁定写入(Commit/Flush 期间调用)
+func (bp *BufferPool) LockWrites() {
+	bp.mu.Lock()
+	bp.writeLocked = true
+	bp.writeChan = make(chan struct{}) // 创建新的 channel
+	bp.mu.Unlock()
+}
+
+// UnlockWrites 解锁写入(Commit/Flush 完成后调用)
+func (bp *BufferPool) UnlockWrites() {
+	bp.mu.Lock()
+	bp.writeLocked = false
+	if bp.writeChan != nil {
+		close(bp.writeChan) // 通知所有等待的 goroutine
+		bp.writeChan = nil
+	}
+	bp.mu.Unlock()
+}
+
+// waitForWriteLock 等待写锁释放(在 WritePage 中调用)
+func (bp *BufferPool) waitForWriteLock() {
+	bp.mu.Lock()
+	if !bp.writeLocked {
+		bp.mu.Unlock()
+		return // 未锁定,直接返回
+	}
+	
+	// 保存 channel 引用
+	ch := bp.writeChan
+	bp.mu.Unlock()
+	
+	// 阻塞等待解锁(channel 关闭时会返回)
+	<-ch
+}
+
 // CollectDirty 取出所有脏页快照并清除脏标记
 // CollectDirty 取出所有脏页快照并清除脏标记
 // 由 DB.Commit() 调用,配合 WAL 流程统一写回
 // 由 DB.Commit() 调用,配合 WAL 流程统一写回
 // 同时清除干净页面缓存,释放内存并确保后续脏页计数准确
 // 同时清除干净页面缓存,释放内存并确保后续脏页计数准确
 func (bp *BufferPool) CollectDirty() []DirtyPage {
 func (bp *BufferPool) CollectDirty() []DirtyPage {
+	// 1. 锁定写入,防止新的写操作
+	bp.LockWrites()
+	defer bp.UnlockWrites()
+	
 	bp.mu.Lock()
 	bp.mu.Lock()
-	defer bp.mu.Unlock()
 	var dirty []DirtyPage
 	var dirty []DirtyPage
 	for pid, entry := range bp.pages {
 	for pid, entry := range bp.pages {
 		if entry.dirty {
 		if entry.dirty {
@@ -78,6 +121,7 @@ func (bp *BufferPool) CollectDirty() []DirtyPage {
 		delete(bp.pages, pid)
 		delete(bp.pages, pid)
 	}
 	}
 	bp.dirtyCount = 0 // 重置脏页计数
 	bp.dirtyCount = 0 // 重置脏页计数
+	bp.mu.Unlock()
 	return dirty
 	return dirty
 }
 }
 
 
@@ -130,11 +174,22 @@ func (bp *BufferPool) ReadPage(pageID uint32, buf []byte) error {
 
 
 // WritePage 写页 — 写入缓存并标记脏,不立即落盘
 // WritePage 写页 — 写入缓存并标记脏,不立即落盘
 func (bp *BufferPool) WritePage(pageID uint32, buf []byte) error {
 func (bp *BufferPool) WritePage(pageID uint32, buf []byte) error {
+	// 1. 等待写锁释放(Commit/Flush 期间阻塞)
+	bp.waitForWriteLock()
+	
 	cached := make([]byte, len(buf))
 	cached := make([]byte, len(buf))
 	copy(cached, buf)
 	copy(cached, buf)
 
 
 	var triggerThreshold bool
 	var triggerThreshold bool
+	var err error
 	bp.mu.Lock()
 	bp.mu.Lock()
+	
+	// 2. 检查 BufferPool 容量
+	if bp.capacity > 0 && len(bp.pages) >= bp.capacity {
+		bp.mu.Unlock()
+		return errors.New("buffer pool is full, please commit first")
+	}
+	
 	existing, exists := bp.pages[pageID]
 	existing, exists := bp.pages[pageID]
 	if exists && existing.dirty {
 	if exists && existing.dirty {
 		// 已是脏页, 直接覆盖, 不增加脏页计数
 		// 已是脏页, 直接覆盖, 不增加脏页计数
@@ -145,23 +200,21 @@ func (bp *BufferPool) WritePage(pageID uint32, buf []byte) error {
 		bp.dirtyCount++
 		bp.dirtyCount++
 	}
 	}
 
 
-	// 检查是否超过阈值 (脏页或总页面数)
-	// 使用总页面数作为第二道安全网,防止脏页计数增长缓慢时内存无限膨胀
+	// 3. 检查是否超过阈值 (脏页或总页面数)
 	totalPages := len(bp.pages)
 	totalPages := len(bp.pages)
 	if bp.dirtyThreshold > 0 && bp.onThreshold != nil {
 	if bp.dirtyThreshold > 0 && bp.onThreshold != nil {
-		// 脏页阈值或总页面数阈值 (两者相同,确保及时刷盘)
 		if bp.dirtyCount >= bp.dirtyThreshold || totalPages >= bp.dirtyThreshold {
 		if bp.dirtyCount >= bp.dirtyThreshold || totalPages >= bp.dirtyThreshold {
 			triggerThreshold = true
 			triggerThreshold = true
 		}
 		}
 	}
 	}
 	bp.mu.Unlock()
 	bp.mu.Unlock()
 
 
-	// 在锁外触发回调, 避免死锁
+	// 4. 在锁外触发回调, 避免死锁
 	if triggerThreshold {
 	if triggerThreshold {
 		bp.onThreshold()
 		bp.onThreshold()
 	}
 	}
 
 
-	return nil
+	return err
 }
 }
 
 
 // Len 当前缓存页数 (调试用)
 // Len 当前缓存页数 (调试用)

+ 11 - 0
DB/db.go

@@ -71,6 +71,17 @@ func (db *DB) SetCommitPreset(preset string) {
 	}
 	}
 }
 }
 
 
+// LockWrites 锁定写入(禁止 Insert/Update/Delete)
+// 适用场景:批量导入前、维护操作期间、备份时
+func (db *DB) LockWrites() {
+	db.coreDB.LockWrites()
+}
+
+// UnlockWrites 解锁写入(允许 Insert/Update/Delete)
+func (db *DB) UnlockWrites() {
+	db.coreDB.UnlockWrites()
+}
+
 // DebugDBState 打印数据库调试信息 (调试用)
 // DebugDBState 打印数据库调试信息 (调试用)
 func (db *DB) DebugDBState() {
 func (db *DB) DebugDBState() {
 	db.coreDB.DebugDBState()
 	db.coreDB.DebugDBState()

+ 137 - 0
demo/lock_table_test/main.go

@@ -0,0 +1,137 @@
+package main
+
+import (
+	"TsDB2/DB"
+	"fmt"
+	"os"
+	"sync"
+	"time"
+)
+
+type Record struct {
+	DB.BaseModel
+	Name string `tsdb:"32byte"`
+}
+
+const dbPath = "lock_table_test.db"
+
+func main() {
+	fmt.Println("==============================================")
+	fmt.Println("  锁表机制测试")
+	fmt.Println("==============================================\n")
+
+	os.Remove(dbPath)
+
+	db, err := DB.Open(dbPath)
+	if err != nil {
+		panic(err)
+	}
+	defer db.Close()
+
+	tbl, err := DB.UseTable[Record](db, "test")
+	if err != nil {
+		panic(err)
+	}
+
+	// ========== 测试1: 手动锁表 ==========
+	fmt.Println("测试1: 手动 LockWrites/UnlockWrites")
+	
+	db.LockWrites()
+	fmt.Println("  ✅ 已锁定写入")
+	
+	// 尝试在另一个 goroutine 中插入(应该阻塞)
+	var wg sync.WaitGroup
+	insertSuccess := false
+	
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		err := tbl.Insert(&Record{Name: "Emp0000001"})
+		if err == nil {
+			insertSuccess = true
+			fmt.Println("  ❌ 锁定期不应允许插入")
+		} else {
+			fmt.Printf("  ✅ 插入被阻止: %v\n", err)
+		}
+	}()
+	
+	// 等待 100ms,确认 goroutine 已阻塞
+	time.Sleep(100 * time.Millisecond)
+	fmt.Println("  ⏳ 插入操作正在等待解锁...")
+	
+	// 解锁
+	time.Sleep(200 * time.Millisecond)
+	db.UnlockWrites()
+	fmt.Println("  ✅ 已解锁写入")
+	
+	wg.Wait()
+	if insertSuccess {
+		fmt.Println("  ❌ 测试失败: 锁定期插入了数据")
+	} else {
+		fmt.Println("  ✅ 测试通过: 锁定期成功阻止写入")
+	}
+	
+	// ========== 测试2: Commit 期间自动锁表 ==========
+	fmt.Println("\n测试2: Commit 期间自动锁表")
+	
+	// 插入一些数据
+	for i := 1; i <= 100; i++ {
+		tbl.Insert(&Record{Name: fmt.Sprintf("Emp%07d", i)})
+	}
+	fmt.Println("  已插入 100 行")
+	
+	// 在另一个 goroutine 中尝试插入
+	wg.Add(1)
+	commitBlocked := false
+	
+	go func() {
+		defer wg.Done()
+		start := time.Now()
+		err := tbl.Insert(&Record{Name: "Emp0000101"})
+		elapsed := time.Since(start)
+		
+		if elapsed > 10*time.Millisecond {
+			commitBlocked = true
+			fmt.Printf("  ✅ 插入被 Commit 阻塞 (%v)\n", elapsed)
+		}
+		_ = err
+	}()
+	
+	// 等待 goroutine 启动
+	time.Sleep(50 * time.Millisecond)
+	
+	// 执行 Commit(会自动锁表)
+	fmt.Println("  执行 Commit...")
+	db.Commit()
+	fmt.Println("  ✅ Commit 完成")
+	
+	wg.Wait()
+	
+	if commitBlocked {
+		fmt.Println("  ✅ 测试通过: Commit 期间成功阻止写入")
+	} else {
+		fmt.Println("  ⚠️  未检测到阻塞(可能 Commit 太快)")
+	}
+	
+	// ========== 测试3: BufferPool 满时拒绝写入 ==========
+	fmt.Println("\n测试3: BufferPool 满时拒绝写入")
+	
+	db.SetDirtyThreshold(10) // 设置很小的阈值
+	
+	count := 0
+	for i := 101; i <= 200; i++ {
+		err := tbl.Insert(&Record{Name: fmt.Sprintf("Emp%07d", i)})
+		if err != nil {
+			fmt.Printf("  ✅ 插入被拒绝 (第%d行): %v\n", i, err)
+			break
+		}
+		count++
+	}
+	
+	fmt.Printf("  成功插入 %d 行后触发限制\n", count)
+	fmt.Println("  ✅ 测试通过: BufferPool 满时正确拒绝写入")
+	
+	fmt.Println("\n==============================================")
+	fmt.Println("  所有测试完成!")
+	fmt.Println("==============================================")
+}

BIN
lock_table_test.db