Browse Source

fix(core): 自增主键游标全面 pkMu 保护 — AllocPKID 与显式大号顶起互斥

AllocPKID 整体移入锁内; InsertRowWithID/批量 insertRows 的 pendingPK 消费与
显式 pkID 顶起游标的读-改-写也并入同一临界区, 并发写者不再可能游标回退发重号
yl32940 3 weeks ago
parent
commit
cff695da80
2 changed files with 26 additions and 14 deletions
  1. 7 0
      DB/core/Table.go
  2. 19 14
      DB/core/table_crud.go

+ 7 - 0
DB/core/Table.go

@@ -214,6 +214,9 @@ type Table struct {
 	nextPKInit   bool
 	pendingPK    uint64 // AllocPKID 刚预留、等待 InsertRowWithID 消费的号
 	hasPendingPK bool
+	// pkMu 保护自增主键游标的读-改-写 (ensureNextPK / nextPK++ / pendingPK 预留):
+	// 多写者并发 AllocPKID 时, 无锁的 nextPK++ 会给两行发出同一个号 (重复主键)。
+	pkMu sync.Mutex
 
 	// fieldIdxCache: 列名 → table.Field 下标的惰性缓存。条件求值
 	// (Eq/In/Gt... 的 Match) 每行每条件都查一次字段位置, 线性扫是 O(列数)/行;
@@ -372,6 +375,10 @@ func (table *Table) fieldParam(masterIdx int) byte {
 // 分配即推进游标并预留该号, 紧随其后的 InsertRowWithID 会跳过查重。
 // 若最终未插入, 该号只是被跳过 —— 游标只增不减, 不会与后续插入冲突。
 func (table *Table) AllocPKID() uint64 {
+	// 整个"惰性初始化 → 取号 → 游标推进 → 预留登记"在锁内完成:
+	// 并发写者各拿各的号, 号段互不重叠。
+	table.pkMu.Lock()
+	defer table.pkMu.Unlock()
 	table.ensureNextPK()
 	pk := table.nextPK
 	table.nextPK++

+ 19 - 14
DB/core/table_crud.go

@@ -34,17 +34,21 @@ func (table *Table) InsertRowWithID(pkID uint64, values []types.FieldValue) (uin
 	if pkID == 0 {
 		pkID = table.AllocPKID()
 	}
+	// 游标状态 (预留消费 + 顶起) 统一在 pkMu 内读写, 与 AllocPKID 的
+	// 取号/预留登记互斥: 并发写者交错时避免游标回退发重号或预留错位。
+	table.pkMu.Lock()
 	reserved := table.hasPendingPK && table.pendingPK == pkID
 	table.hasPendingPK = false
+	// 显式指定更大的主键时把游标顶到它之后, 避免后续自增再发一次同样的号
+	if pkID+1 > table.nextPK {
+		table.nextPK = pkID + 1
+	}
+	table.pkMu.Unlock()
 	if !reserved {
 		if table.indexMeta.rowStoreHasPK(pkID) {
 			return 0, fmt.Errorf("%w: pkID=%d", ErrDuplicatePK, pkID)
 		}
 	}
-	// 显式指定更大的主键时把游标顶到它之后, 避免后续自增再发一次同样的号
-	if pkID+1 > table.nextPK {
-		table.nextPK = pkID + 1
-	}
 
 	rowData, err := table.serializeRow(values, nil)
 	if err != nil {
@@ -114,7 +118,17 @@ func (table *Table) insertRows(pkIDs []uint64, values [][]types.FieldValue, allR
 			auto[i] = true
 		}
 	}
+	// 预留槽消费与游标顶起都在 pkMu 内, 与 AllocPKID/单行路径互斥
+	table.pkMu.Lock()
 	table.hasPendingPK = false // 预留号已被本批整体消费
+	if !allReserved {
+		for i := 0; i < n; i++ {
+			if !auto[i] && pkIDs[i]+1 > table.nextPK {
+				table.nextPK = pkIDs[i] + 1
+			}
+		}
+	}
+	table.pkMu.Unlock()
 
 	// 2) 批内主键查重 (显式 vs 显式, 以及显式撞上自增号段)
 	{
@@ -137,16 +151,7 @@ func (table *Table) insertRows(pkIDs []uint64, values [][]types.FieldValue, allR
 			}
 		}
 	}
-	// 4) 显式大号顶起自增游标 (与单行路径一致; 预留号本就来自游标, 无需)
-	if !allReserved {
-		for i := 0; i < n; i++ {
-			if !auto[i] && pkIDs[i]+1 > table.nextPK {
-				table.nextPK = pkIDs[i] + 1
-			}
-		}
-	}
-
-	// 5) 逐行序列化 (动态堆写入在这一步发生)
+	// 4) 逐行序列化 (动态堆写入在这一步发生)
 	rowDatas := make([][]byte, n)
 	for i := 0; i < n; i++ {
 		rowData, err := table.serializeRow(values[i], nil)