package docgov import ( "context" "sync" ) // docLockIndex 管理按 doc_id 的细粒度互斥锁。 // 全局锁只保护「分锁表」本身(短临界区);文档间操作互不阻塞。 // 锁序约定:docLocks.meta → docID(如需多文档,按 docID 字典序)。 type docLockIndex struct { meta sync.Mutex locks map[string]*sync.Mutex } func newDocLockIndex() *docLockIndex { return &docLockIndex{locks: map[string]*sync.Mutex{}} } // lock 返回 docID 对应的互斥锁指针;调用方必须在 meta 锁内无其他副作用。 func (d *docLockIndex) lock(docID string) *sync.Mutex { d.meta.Lock() defer d.meta.Unlock() mu, ok := d.locks[docID] if !ok { mu = &sync.Mutex{} d.locks[docID] = mu } return mu } // docLockGuard 绑定一个 docID 的已获取互斥锁,确保按序释放。 type docLockGuard struct { mu *sync.Mutex release sync.Once } func (g *docLockGuard) Release() { if g != nil && g.mu != nil { g.release.Do(func() { g.mu.Unlock() }) } } func (e *GovernanceEngine) acquireDocLock(docID string) docLockGuard { mu := e.docLocks.lock(docID) mu.Lock() return docLockGuard{mu: mu} } // withDocLock 以 docID 粒度执行 fn;fn 内的 DB I/O 仍持该文档锁, // 但不再阻塞其他文档(原实现为全局 RWMutex)。 func (e *GovernanceEngine) withDocLock(docID string, fn func() error) error { g := e.acquireDocLock(docID) defer g.Release() return fn() } // lookupMemDoc 返回内存中的文档记录(需在 doc 锁内调用)。 func (e *GovernanceEngine) lookupMemDoc(docID string) (*DocumentRecord, bool) { doc, ok := e.memDocs[docID] return doc, ok } // ensureDocLockedWithDB 在 doc 锁内做内存查找 + DB 兜底加载。 // 对应原 ensureDocumentLocked,但锁粒度为单文档。 func (e *GovernanceEngine) ensureDocLockedWithDB(ctx context.Context, docID string) (*DocumentRecord, error) { if doc, ok := e.memDocs[docID]; ok { return doc, nil } if e.pool != nil { doc, err := e.loadDocumentFromDB(ctx, docID) if err == nil && doc != nil { return doc, nil } } return nil, ErrDocumentNotFound } // loadDocumentFromDB 在无全局锁状态下从 PG 装载文档记录到内存。 // 这是 T20 B3 的核心:DB RTT 不再进入全局临界区。 func (e *GovernanceEngine) loadDocumentFromDB(ctx context.Context, docID string) (*DocumentRecord, error) { e.mu.Lock() defer e.mu.Unlock() return e.loadDocumentFromDBLocked(ctx, docID) }