Browse Source

feat: 实现基础事务支持(简化版)

核心功能:
- BeginTransaction(): 开启新事务
- Commit(): 收集脏页 + WAL + fsync 落盘
- Rollback(): 标记回滚(依赖 WAL 恢复)

实现思路:
1. 事务内操作正常写 BufferPool(标记 dirty)
2. Commit 时走原有 WAL 流程,确保 fsync 落盘
3. Rollback 时仅标记状态,不清除 BufferPool
4. 实际回滚依赖下次打开数据库时的 WAL 恢复

文件改动:
- DB/core/transaction.go: 事务管理器
- DB/core/db.go: 添加 currentTxn 字段
- DB/db.go: 暴露 BeginTransaction() API
- demo/txn_test/: 事务功能测试

已知限制:
⚠️ Rollback 不立即生效(BufferPool 中的脏页未清除)
⚠️ 需要重启数据库才能看到回滚效果
⚠️ 适合单线程场景,无并发隔离

TODO:
- 实现 rollbackBufferPool() 立即回滚
- 添加 File.GetDirtyPages() 方法
- 支持多线程并发事务隔离
yl32940 1 month ago
parent
commit
b95d1c0cce
5 changed files with 269 additions and 0 deletions
  1. 3 0
      DB/core/db.go
  2. 119 0
      DB/core/transaction.go
  3. 22 0
      DB/db.go
  4. 125 0
      demo/txn_test/main.go
  5. BIN
      txn_test.db

+ 3 - 0
DB/core/db.go

@@ -35,6 +35,9 @@ type DB struct {
 	autoCommitDelay time.Duration
 	autoCommitTimer *time.Timer
 	autoCommitMu    sync.Mutex
+	
+	// 当前活跃事务(nil 表示无事务)
+	currentTxn *Transaction
 }
 
 func Open(path string) (*DB, error) {

+ 119 - 0
DB/core/transaction.go

@@ -0,0 +1,119 @@
+package core
+
+import (
+	"fmt"
+)
+
+// Transaction 事务管理器(极简版)
+// 
+// 核心思想:
+// - 事务内操作正常写 BufferPool(标记 dirty)
+// - Commit 时收集脏页 + WAL + fsync 落盘(保证持久化)
+// - Rollback 时仅标记回滚,实际回滚依赖 WAL 恢复机制
+//
+// 注意:
+// - 这是简化实现,适合单线程场景
+// - 多线程并发事务需要更复杂的隔离机制
+type Transaction struct {
+	db         *DB
+	committed  bool
+	rolledBack bool
+}
+
+// BeginTransaction 开启新事务
+func (db *DB) BeginTransaction() *Transaction {
+	txn := &Transaction{
+		db:         db,
+		committed:  false,
+		rolledBack: false,
+	}
+	
+	// 设置当前活跃事务
+	db.currentTxn = txn
+	
+	return txn
+}
+
+// Commit 提交事务
+// 1. 收集所有 BufferPool 脏页
+// 2. 写入 WAL
+// 3. ⭐ 关键:fsync 确保落盘 ⭐
+// 4. 回收 WAL 页
+func (txn *Transaction) Commit() error {
+	if txn.committed {
+		return fmt.Errorf("transaction already committed")
+	}
+	if txn.rolledBack {
+		return fmt.Errorf("transaction already rolled back")
+	}
+	
+	// 1. 收集所有脏页
+	dirty := txn.db.file.CollectDirty()
+	if len(dirty) == 0 {
+		txn.committed = true
+		txn.db.currentTxn = nil
+		return nil // 无脏页,直接返回
+	}
+	
+	// 2. 创建 WAL 事务
+	wal := txn.db.BeginTxn()
+	
+	// 3. 记录所有脏页到 WAL
+	for _, d := range dirty {
+		wal.RecordPage(d.PageID, d.Data)
+	}
+	
+	// 4. ⭐ 关键:Commit 会触发 fsync,确保落盘 ⭐
+	if err := wal.Commit(); err != nil {
+		return fmt.Errorf("WAL commit failed: %v", err)
+	}
+	
+	// 5. 清理事务状态
+	txn.committed = true
+	txn.db.currentTxn = nil
+	
+	return nil
+}
+
+// Rollback 回滚事务(简化版)
+// 当前实现:仅标记回滚,不清除 BufferPool
+// 实际回滚依赖:
+// 1. 下次打开数据库时的 WAL 恢复机制
+// 2. 或者手动调用 db.RecoverFromWAL()
+//
+// TODO: 未来可以实现立即回滚(从磁盘 reload 脏页)
+func (txn *Transaction) Rollback() error {
+	if txn.committed {
+		return fmt.Errorf("transaction already committed, cannot rollback")
+	}
+	if txn.rolledBack {
+		return fmt.Errorf("transaction already rolled back")
+	}
+	
+	// 标记回滚
+	txn.rolledBack = true
+	txn.db.currentTxn = nil
+	
+	// 注意:这里没有清除 BufferPool 中的脏页
+	// 如果需要立即回滚,可以调用 txn.rollbackBufferPool()
+	
+	return nil
+}
+
+// rollbackBufferPool 从磁盘重新加载所有脏页(可选实现)
+// 这个方法可以在 Rollback 时调用,实现立即回滚
+// TODO: 需要 File 层提供 GetDirtyPages() 方法
+func (txn *Transaction) rollbackBufferPool() error {
+	// TODO: 实现立即回滚逻辑
+	return fmt.Errorf("rollbackBufferPool not yet implemented")
+}
+
+// IsCommitted 检查事务是否已提交
+func (txn *Transaction) IsCommitted() bool {
+	return txn.committed
+}
+
+// IsRolledBack 检查事务是否已回滚
+func (txn *Transaction) IsRolledBack() bool {
+	return txn.rolledBack
+}

+ 22 - 0
DB/db.go

@@ -92,6 +92,28 @@ func (db *DB) SetBufferPoolCapacity(capacity int) {
 	db.coreDB.SetBufferPoolCapacity(capacity)
 }
 
+// BeginTransaction 开启新事务
+// 返回 Transaction 对象,用于 Commit/Rollback
+// 
+// 使用示例:
+//   txn := db.BeginTransaction()
+//   defer func() {
+//     if err := recover(); err != nil {
+//       txn.Rollback() // 异常时回滚
+//       panic(err)
+//     }
+//   }()
+//   
+//   tbl.Insert(&Record{...})
+//   tbl.Update(...)
+//   
+//   if err := txn.Commit(); err != nil {
+//     return err // 提交失败
+//   }
+func (db *DB) BeginTransaction() *core.Transaction {
+	return db.coreDB.BeginTransaction()
+}
+
 // DebugDBState 打印数据库调试信息 (调试用)
 func (db *DB) DebugDBState() {
 	db.coreDB.DebugDBState()

+ 125 - 0
demo/txn_test/main.go

@@ -0,0 +1,125 @@
+package main
+
+import (
+	"TsDB2/DB"
+	"fmt"
+	"os"
+)
+
+type Record struct {
+	DB.BaseModel
+	Name string `tsdb:"32byte"`
+	Age  int64  `tsdb:"8byte"`
+}
+
+const dbPath = "txn_test.db"
+
+func main() {
+	os.Remove(dbPath)
+	
+	fmt.Println("==============================================")
+	fmt.Println("  TsDB2 事务功能测试")
+	fmt.Println("==============================================\n")
+	
+	// 打开数据库
+	db, err := DB.Open(dbPath)
+	if err != nil {
+		panic(err)
+	}
+	defer db.Close()
+	
+	// 禁用自动提交,完全手动控制
+	db.SetAutoCommitDelay(0)
+	
+	// 创建表
+	tbl, err := DB.UseTable[Record](db, "users")
+	if err != nil {
+		panic(err)
+	}
+	
+	// ========== 测试1: 正常事务提交 ==========
+	fmt.Println("========== 测试1: 正常事务提交 ==========")
+	
+	txn1 := db.BeginTransaction()
+	
+	for i := 1; i <= 5; i++ {
+		err := tbl.Insert(&Record{
+			Name: fmt.Sprintf("User%03d", i),
+			Age:  int64(20 + i),
+		})
+		if err != nil {
+			panic(err)
+		}
+	}
+	
+	if err := txn1.Commit(); err != nil {
+		panic(err)
+	}
+	
+	count, _ := tbl.Count()
+	fmt.Printf("✅ 插入 5 行并提交,当前行数: %d\n", count)
+	
+	// ========== 测试2: 事务回滚 ==========
+	fmt.Println("\n========== 测试2: 事务回滚 ==========")
+	
+	countBefore, _ := tbl.Count()
+	fmt.Printf("回滚前行数: %d\n", countBefore)
+	
+	txn2 := db.BeginTransaction()
+	
+	for i := 6; i <= 10; i++ {
+		err := tbl.Insert(&Record{
+			Name: fmt.Sprintf("User%03d", i),
+			Age:  int64(20 + i),
+		})
+		if err != nil {
+			panic(err)
+		}
+	}
+	
+	countDuring, _ := tbl.Count()
+	fmt.Printf("插入 5 行后(未提交): %d\n", countDuring)
+	
+	// 回滚事务
+	if err := txn2.Rollback(); err != nil {
+		panic(err)
+	}
+	
+	// 注意:简化版 Rollback 不立即清除 BufferPool
+	// 需要重启数据库才能看到效果
+	fmt.Println("⚠️  已调用 Rollback()(简化版不立即清除 BufferPool)")
+	fmt.Println("   实际回滚依赖 WAL 恢复机制")
+	
+	// ========== 测试3: 验证数据持久化 ==========
+	fmt.Println("\n========== 测试3: 验证数据持久化 ==========")
+	
+	db.Close()
+	
+	// 重新打开数据库
+	db2, err := DB.Open(dbPath)
+	if err != nil {
+		panic(err)
+	}
+	defer db2.Close()
+	
+	tbl2, _ := DB.UseTable[Record](db2, "users")
+	countAfterReopen, _ := tbl2.Count()
+	fmt.Printf("重开后行数: %d(应为 5,回滚的 5 行不应存在)\n", countAfterReopen)
+	
+	// 验证具体数据
+	row, err := tbl2.Find(1)
+	if err == nil {
+		fmt.Printf("Row#1: Name=%s, Age=%d\n", row.Name, row.Age)
+	}
+	
+	row, err = tbl2.Find(6)
+	if err != nil {
+		fmt.Println("✅ Row#6 不存在(已回滚)")
+	} else {
+		fmt.Printf("❌ Row#6 存在: Name=%s(回滚失败!)\n", row.Name)
+	}
+	
+	fmt.Println("\n==============================================")
+	fmt.Println("  测试完成!")
+	fmt.Println("==============================================")
+}

BIN
txn_test.db