文章导航
本页目录

OpenGauss锁管理

Locking Overview

锁类型 保护对象 运行机制
自旋锁 (Spinlock) 极短时间的共享内存变量/状态位 基于硬件原子指令(如 CAS/TAS),忙等(Busy-loop),无死锁检测,超时约 1 分钟报 Error
轻量级锁 (LWLock) 共享内存数据结构(如 Buffer 缓冲区、CLOG、ProcArray 等) 支持共享 (S) 与排他 (X) 模式;无死锁检测;冲突时线程挂起睡眠,按到达顺序(FIFO)唤醒
常规锁 (Heavyweight Lock) 用户可见的数据库对象(表、分区、页面、元组、Advisory 锁) 8 种锁模式(如 AccessShare 到 AccessExclusive);由 Lock Manager 管理,带完整死锁检测与事务结束自动释放
谓词锁 (Predicate Lock) 串行化隔离级别 (SSI) SIReadLock 机制,记录事务间的“读-写”依赖

https://gitcode.com/opengauss/openGauss-server/blob/master/src/gausskernel/storage/lmgr/README

自旋锁

s_lock.hatomic.h。OpenGauss针对鲲鹏ARM架构写了一些硬件指令。

常规锁(Heavyweight Lock)

类型

/*
 * LOCKTAG is the key information needed to look up a LOCK item in the
 * lock hashtable.	A LOCKTAG value uniquely identifies a lockable object.
 *
 * The LockTagType enum defines the different kinds of objects we can lock.
 * We can handle up to 256 different LockTagTypes.
 */
typedef enum LockTagType {
    LOCKTAG_RELATION, /* whole relation */
    /* ID info for a relation is DB OID + REL OID; DB OID = 0 if shared */
    LOCKTAG_RELATION_EXTEND, /* the right to extend a relation */
    /* same ID info as RELATION */
    LOCKTAG_PARTITION,          /*partition*/
    LOCKTAG_PARTITION_SEQUENCE, /*partition sequence*/
    LOCKTAG_PAGE,               /* one page of a relation */
    /* ID info for a page is RELATION info + BlockNumber */
    LOCKTAG_TUPLE, /* one physical tuple */
    /* ID info for a tuple is PAGE info + OffsetNumber */
    LOCKTAG_TRANSACTION, /* transaction (for waiting for xact done) */
    /* ID info for a transaction is its TransactionId */
    LOCKTAG_VIRTUALTRANSACTION, /* virtual transaction (ditto) */
    /* ID info for a virtual transaction is its VirtualTransactionId */
    LOCKTAG_OBJECT, /* non-relation database object */
    /* ID info for an object is DB OID + CLASS OID + OBJECT OID + SUBID */
    LOCKTAG_CSTORE_FREESPACE, /* cstore free space */

    /*
     * Note: object ID has same representation as in pg_depend and
     * pg_description, but notice that we are constraining SUBID to 16 bits.
     * Also, we use DB OID = 0 for shared objects such as tablespaces.
     */
    LOCKTAG_USERLOCK, /* reserved for old contrib/userlock code */
    LOCKTAG_ADVISORY, /* advisory user locks */
    /* same ID info as spcoid, dboid, reloid */
    LOCKTAG_RELFILENODE, /* relfilenode */
    LOCKTAG_SUBTRANSACTION, /* subtransaction (for waiting for subxact done) */
    /* ID info for a transaction is its TransactionId + SubTransactionId */
    LOCKTAG_UID,
    LOCKTAG_TABLESPACE,
    LOCKTAG_PLPY_GIL,
    LOCK_EVENT_NUM
} LockTagType;

数据库对象锁

  • LOCKTAG_RELATION
    • relation_open
  • LOCKTAG_PARTITION
  • LOCKTAG_PAGE

咨询锁(LOCKTAG_ADVISORY)

事务ID锁(LOCKTAG_TRANSACTION)

构造死锁场景

Docker安装OpenGauss参考1

创建表并插入数据

CREATE TABLE t1 (id int PRIMARY KEY, value int);
INSERT INTO t1 VALUES (1, 111), (2, 222);

按照以下步骤在两个独立的数据库会话(Session 1 和 Session 2)中操作。

步骤 会话1 会话2
1 BEGIN; BEGIN;
2 UPDATE t1 SET value = 444 WHERE id = 1; (成功,持有id=1的行锁) UPDATE t1 SET value = 555 WHERE id = 2; (成功,持有id=2的行锁)
3 UPDATE t1 SET value = 666 WHERE id = 2; (被阻塞,等待会话2释放id=2的行锁)
4 UPDATE t1 SET value = 777 WHERE id = 1; (死锁检测触发,会话2报错并回滚)

会话2会报以下的错误:

openGauss=# UPDATE t1 SET value = 777 WHERE id = 1;
ERROR:  deadlock detected
DETAIL:  Process 139219895842304 waits for ShareLock on transaction 14165; blocked by process 139219937785344.
Process 139219937785344 waits for ShareLock on transaction 14166; blocked by process 139219895842304.
HINT:  See server log for query details.
源码加锁分析

关键源码:搜索LOCKTAG_TRANSACTION。事务开始时持有自己xid的ExclusiveLock,其它事务等锁 需要加ShareLock

/*
 *		XactLockTableInsert
 *
 * Insert a lock showing that the given transaction ID is running ---
 * this is done when an XID is acquired by a transaction or subtransaction.
 * The lock can then be used to wait for the transaction to finish.
 */
void XactLockTableInsert(TransactionId xid)
{
    LOCKTAG tag;

    SET_LOCKTAG_TRANSACTION(tag, xid);

    (void)LockAcquire(&tag, ExclusiveLock, false, false);
}

/*
 *		XactLockTableWait
 *
 * Wait for the specified transaction to commit or abort.
 *
 * Note that this does the right thing for subtransactions: if we wait on a
 * subtransaction, we will exit as soon as it aborts or its top parent commits.
 * It takes some extra work to ensure this, because to save on shared memory
 * the XID lock of a subtransaction is released when it ends, whether
 * successfully or unsuccessfully.	So we have to check if it's "still running"
 * and if so wait for its parent.
 */
void XactLockTableWait(TransactionId xid, bool allow_con_update, int waitSec)
{
    LOCKTAG tag;
    CLogXidStatus status = CLOG_XID_STATUS_IN_PROGRESS;

    for (;;) {
        if (!TransactionIdIsValid(xid))
            break;

        Assert(!TransactionIdEquals(xid, GetTopTransactionIdIfAny()) || status == CLOG_XID_STATUS_COMMITTED ||
               status == CLOG_XID_STATUS_ABORTED);

        SET_LOCKTAG_TRANSACTION(tag, xid);

        (void)LockAcquire(&tag, ShareLock, false, false, allow_con_update, waitSec);

        (void)LockRelease(&tag, ShareLock, false);

        if (!TransactionIdIsInProgress(xid))
            break;
        xid = SubTransGetParent(xid, &status, true);
    }
}

GPT-5.6-sol分析

锁模式

/* NoLock is not a lock mode, but a flag value meaning "don't get a lock" */
#define NoLock 0

#define AccessShareLock 1  /* SELECT */
#define RowShareLock 2     /* SELECT FOR UPDATE/FOR SHARE */
#define RowExclusiveLock 3 /* INSERT, UPDATE, DELETE */
#define ShareUpdateExclusiveLock                         \
    4               /* VACUUM (non-FULL),ANALYZE, CREATE \
                     * INDEX CONCURRENTLY */
#define ShareLock 5 /* CREATE INDEX (WITHOUT CONCURRENTLY) */
#define ShareRowExclusiveLock                \
    6 /* like EXCLUSIVE MODE, but allows ROW \
       * SHARE */
#define ExclusiveLock                  \
    7 /* blocks ROW SHARE/SELECT...FOR \
       * UPDATE */
#define AccessExclusiveLock              \
    8 /* ALTER TABLE, DROP TABLE, VACUUM \
       * FULL, and unqualified LOCK TABLE */
GaussDB锁相容矩阵
GaussDB锁相容矩阵2

从加锁路径角度来看,

  • 1-3级属于弱锁,互相不会阻塞,加锁时有机会走fastpath路径。
  • 5-8级属于强锁,可能阻塞1-3级锁。

优化点

  • Hashtable锁表优化
    • Fast-path
    • Hash分区(PG8.2)提高并发度
  • 页内锁

Hashtable锁表实现

在关系型数据库内核中,锁管理器(Lock Manager)主要负责维护当前系统中所有被锁定的资源(表、页、行、事务等)以及正在等待锁的事务队列。

数据结构 所在的 hash table 是否所有后端共享 hash key
LOCK LockMethodLockHash,即 LOCK hash 是,共享内存 LOCKTAG
PROCLOCK LockMethodProcLockHash,即 PROCLOCK hash 是,共享内存 {LOCK *, PGPROC *},即锁对象和后端进程的组合
LOCALLOCK LockMethodLocalHash 否,每个后端一份 LOCALLOCKTAG = {LOCKTAG, LOCKMODE}
typedef struct LOCK {
    /* hash key */
    LOCKTAG tag; /* unique identifier of lockable object */

    /* data */
    LOCKMASK grantMask;           /* bitmask for lock types already granted */
    LOCKMASK waitMask;            /* bitmask for lock types awaited */
    SHM_QUEUE procLocks;          /* list of PROCLOCK objects assoc. with lock */
    LOCKMASK waitMask;            /* bitmask for lock types awaited */
    SHM_QUEUE procLocks;          /* list of PROCLOCK objects assoc. with lock */
    PROC_QUEUE waitProcs;         /* list of PGPROC objects waiting on lock */
    int requested[MAX_LOCKMODES]; /* counts of requested locks */
    int nRequested;               /* total of requested[] array */
    int granted[MAX_LOCKMODES];   /* counts of granted locks */
    int nGranted;                 /* total of granted[] array */
} LOCK;

typedef struct PROCLOCKTAG {
    /* NB: we assume this struct contains no padding! */
    LOCK* myLock;   /* link to per-lockable-object information */
    PGPROC* myProc; /* link to PGPROC of owning backend */
} PROCLOCKTAG;

typedef struct PROCLOCK {
    /* tag */
    PROCLOCKTAG tag; /* unique identifier of proclock object */

    /* data */
    PGPROC  *groupLeader; /* group leader, or NULL if no lock group */	
    LOCKMASK holdMask;    /* bitmask for lock types currently held */
    LOCKMASK releaseMask; /* bitmask for lock types to be released */
    SHM_QUEUE lockLink;   /* list link in LOCK's list of proclocks */
    SHM_QUEUE procLink;   /* list link in PGPROC's list of proclocks */
} PROCLOCK;
typedef struct LOCALLOCK {
    /* tag */
    LOCALLOCKTAG tag; /* unique identifier of locallock entry */

    /* data */
    LOCK* lock;                 /* associated LOCK object, if any */
    PROCLOCK* proclock;         /* associated PROCLOCK object, if any */
    uint32 hashcode;            /* copy of LOCKTAG's hash value */
    int64 nLocks;               /* total number of times lock is held */
    int numLockOwners;          /* # of relevant ResourceOwners */
    int maxLockOwners;          /* allocated size of array */
    bool holdsStrongLockCount;  /* bumped FastPathStrongRelatonLocks */
    bool ssLock;                /* distribute lock in shared storage mode */
    LOCALLOCKOWNER* lockOwners; /* dynamically resizable array */
} LOCALLOCK;

锁调用路径,以relation_open为例

LockRelationOid -> LockAcquire -> LockAcquireExtended -> LockAcquireExtendedXC

查找/创建LOCALLOCK

    /*
     * Find or create a LOCALLOCK entry for this lock and lockmode
     */
    locallock = (LOCALLOCK *)hash_search(t_thrd.storage_cxt.LockMethodLocalHash, (void *)&localtag, HASH_ENTER, &found);

Fast-path优化

观察到弱锁加锁频繁,且很少发生互斥。我们可以让弱锁走fast-path,避免访问共享内存的主锁表。

只有前三级弱锁才能走Fast-path
/*
 * The fast-path lock mechanism is concerned only with relation locks on
 * unshared relations by backends bound to a database.	The fast-path
 * mechanism exists mostly to accelerate acquisition and release of locks
 * that rarely conflict.  Because ShareUpdateExclusiveLock is
 * self-conflicting, it can't use the fast-path mechanism; but it also does
 * not conflict with any of the locks that do, so we can ignore it completely.
 */
#define EligibleForRelationFastPath(locktag, mode)                                                    \
    ((locktag)->locktag_lockmethodid == DEFAULT_LOCKMETHOD &&                                         \
     ((locktag)->locktag_type == LOCKTAG_RELATION || (locktag)->locktag_type == LOCKTAG_PARTITION) && \
     (mode) < ShareUpdateExclusiveLock)
#define ConflictsWithRelationFastPath(locktag, mode)                                                  \
    ((locktag)->locktag_lockmethodid == DEFAULT_LOCKMETHOD &&                                         \
     ((locktag)->locktag_type == LOCKTAG_RELATION || (locktag)->locktag_type == LOCKTAG_PARTITION) && \
     (mode) > ShareUpdateExclusiveLock)
Fast-path取锁成功/失败判断

t_thrd.storage_cxt.FastPathStrongRelationLocks->count[fasthashcode] > 0,表示这个锁已经有强锁了,此时Fast-path取锁失败。

        if (t_thrd.storage_cxt.FastPathStrongRelationLocks->count[fasthashcode] != 0)
            acquired = false;
        else {
            FastPathTag tag = { locktag->locktag_field1, locktag->locktag_field2, locktag->locktag_field3 };
            acquired = FastPathGrantRelationLock(tag, lockmode);
        }

        LWLockRelease(t_thrd.proc->backendLock);
        if (acquired) {
            /*
             * The locallock might contain stale pointers to some old shared
             * objects; we MUST reset these to null before considering the
             * lock to be acquired via fast-path.
             */
            locallock->lock = NULL;
            locallock->proclock = NULL;
            GrantLockLocal(locallock, owner);
            instr_stmt_report_lock(LOCK_END, lockmode);
            return LOCKACQUIRE_OK;
        }

其中t_thrd.storage_cxt.FastPathStrongRelationLocks定义如下,注意hash partitions只有1024,可能会有两个不同的锁被误判为同一个锁,而不必要地进入主锁表。

/*
 * To make the fast-path lock mechanism work, we must have some way of
 * preventing the use of the fast-path when a conflicting lock might be
 * present.  We partition* the locktag space into FAST_PATH_HASH_BUCKETS
 * partitions, and maintain an integer count of the number of "strong" lockers
 * in each partition.  When any "strong" lockers are present (which is
 * hopefully not very often), the fast-path mechanism can't be used, and we
 * must fall back to the slower method of pushing matching locks directly
 * into the main lock tables.
 *
 * The deadlock detector does not know anything about the fast path mechanism,
 * so any locks that might be involved in a deadlock must be transferred from
 * the fast-path queues to the main lock table.
 */
#define FAST_PATH_STRONG_LOCK_HASH_BITS 10
#define FAST_PATH_STRONG_LOCK_HASH_PARTITIONS (1 << FAST_PATH_STRONG_LOCK_HASH_BITS)
#define FastPathStrongLockHashPartition(hashcode) ((hashcode) % FAST_PATH_STRONG_LOCK_HASH_PARTITIONS)

typedef struct FastPathStrongRelationLockData {
    slock_t mutex;
    uint32 count[FAST_PATH_STRONG_LOCK_HASH_PARTITIONS];
} FastPathStrongRelationLockData;
/*
 * We allow a small number of "weak" relation locks (AccesShareLock,
 * RowShareLock, RowExclusiveLock) to be recorded in the PGPROC structure
 * rather than the main lock table.  This eases contention on the lock
 * manager LWLocks.  See storage/lmgr/README for additional details.
 */
#define FP_LOCK_SLOTS_PER_BACKEND ((uint32)g_instance.attr.attr_storage.num_internal_lock_partitions[FASTPATH_PART])
#define FP_LOCK_SLOTS_PER_LOCKBIT 20

...

...

#define FAST_PATH_GET_BITS(proc, n) (((proc)->fpLockBits[n / FP_LOCK_SLOTS_PER_LOCKBIT] \
    >> (FAST_PATH_BITS_PER_SLOT * (n % FP_LOCK_SLOTS_PER_LOCKBIT))) & FAST_PATH_MASK)

分支一:fastpath取锁成功(弱锁)

FastPathGrantRelationLock():在当前后端自己的 PGPROC fast-path 数组中,为一个关系或分区登记一种弱锁模式

每个后端的 PGPROC 中有两组 fast-path 数据:

struct PGPROC {
...
    /* Per-backend LWLock.	Protects fields below. */
    LWLock* backendLock; /* protects the fields below */

    /* Lock manager data, recording fast-path locks taken by this backend. */
    uint64 *fpLockBits;                             /* lock modes held for each fast-path slot */
    FastPathTag *fpRelId;                           /* slots for rel oids */
...
};
  typedef struct FastPathTag {
      uint32 dbid;
      uint32 relid;
      uint32 partitionid;
  } FastPathTag;

逻辑上可以把它们看成一张当前backend私有的小表:

槽位 fpRelId[slot] fpLockBits[slot]
0 {dbid, relid, partitionid} 三种弱锁的 bit
1 {dbid, relid, partitionid} 三种弱锁的 bit

每个槽位使用 3 个 bit 表示三种允许进入 fast-path 的锁模式:

bit 0 → AccessShareLock       = 1
bit 1 → RowShareLock          = 2
bit 2 → RowExclusiveLock      = 3

所以一个关系可以在同一个槽位中同时记录多个弱锁模式,例如:

fpRelId[3] = {dbid, relid, 0}

fpLockBits 对应 slot 3:
  AccessShareLock bit  = 1
  RowShareLock bit     = 0
  RowExclusiveLock bit = 1

这表示当前后端在同一个关系上同时持有:AccessShareLock和RowExclusiveLock。

FastPathGrantRelationLock()的返回值:

  • true:已经成功写入当前后端的 fast-path 槽位
  • false:fast-path 槽位无法容纳,请调用者退回主锁表
设置local hash锁表上的LOCALLOCK,记录Owner
/*
 * GrantLockLocal -- update the locallock data structures to show
 *		the lock request has been granted.
 *
 * We expect that LockAcquire made sure there is room to add a new
 * ResourceOwner entry.
 */
static void GrantLockLocal(LOCALLOCK *locallock, ResourceOwner owner)
{
    LOCALLOCKOWNER *lockOwners = locallock->lockOwners;
    int i;

    Assert(locallock->numLockOwners < locallock->maxLockOwners);
    /* Count the total */
    locallock->nLocks++;
    /* Count the per-owner lock */
    for (i = 0; i < locallock->numLockOwners; i++) {
        if (lockOwners[i].owner == owner) {
            lockOwners[i].nLocks++;
            return;
        }
    }
    lockOwners[i].owner = owner;
    lockOwners[i].nLocks = 1;
    locallock->numLockOwners++;
}

分支二:强锁

ConflictsWithRelationFastPath() 为真。它在进入主锁表前必须先阻止并物化可能冲突的 fast-path 弱锁:

    /*
     * If this lock could potentially have been taken via the fast-path by
     * some other backend, we must (temporarily) disable further use of the
     * fast-path for this lock tag, and migrate any locks already taken via
     * this method to the main lock table.
     */
    if (ConflictsWithRelationFastPath(locktag, lockmode)) {
        uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);

        BeginStrongLockAcquire(locallock, fasthashcode);
        if (!FastPathTransferRelationLocks(lockMethodTable, locktag, hashcode)) {
            AbortStrongLockAcquire();
            SSDmsLockRelease(locallock);
            instr_stmt_report_lock(LOCK_END, NoLock);
            if (reportMemoryError)
                ereport(ERROR, (errcode(ERRCODE_OUT_OF_MEMORY), errmsg("out of shared memory"),
                    errhint("You might need to increase max_locks_per_transaction.")));
            else
                return LOCKACQUIRE_NOT_AVAIL;
        }
    }

t_thrd.storage_cxt.FastPathStrongRelationLocks->count[fasthashcode]++;

/*
 * BeginStrongLockAcquire - inhibit use of fastpath for a given LOCALLOCK,
 * and arrange for error cleanup if it fails
 */
static void BeginStrongLockAcquire(LOCALLOCK *locallock, uint32 fasthashcode)
{
    Assert(t_thrd.storage_cxt.StrongLockInProgress == NULL);
    Assert(locallock->holdsStrongLockCount == FALSE);

     * ensure we don't collide with someone else trying to bump the count at
     * the same time.
     *
     * XXX: It might be worth considering using an atomic fetch-and-add
     * instruction here, on architectures where that is supported.
     */
    SpinLockAcquire(&t_thrd.storage_cxt.FastPathStrongRelationLocks->mutex);
    t_thrd.storage_cxt.FastPathStrongRelationLocks->count[fasthashcode]++;
    locallock->holdsStrongLockCount = TRUE;
    t_thrd.storage_cxt.StrongLockInProgress = locallock;
    SpinLockRelease(&t_thrd.storage_cxt.FastPathStrongRelationLocks->mutex);
}

FastPathTransferRelationLocks()将所有弱锁迁移到全局锁表。

分支三:ShareUpdateExclusiveLock

ShareUpdateExclusiveLock == 4。它既不满足 mode < ShareUpdateExclusiveLock,也不满足 mode > ShareUpdateExclusiveLock,因此:

EligibleForRelationFastPath()  == false
ConflictsWithRelationFastPath() == false

请求直接进入主锁表,但不调用 BeginStrongLockAcquire(),也不扫描所有后端迁移 fast-path 锁。原因是该模式虽与自身冲突,却不与 AccessShareLockRowShareLockRowExclusiveLock 三种 fast-path 模式冲突。

获取锁表分区锁

“锁表分区”是锁管理器对共享 LOCK hash、PROCLOCK hash 做的并发控制划分。(PG8.2引入的性能优化点)

    /*
     * We didn't find the lock in our LOCALLOCK table, and we didn't manage to
     * take it via the fast-path, either, so we've got to mess with the shared
     * lock table.
     */
    partitionLock = LockHashPartitionLock(hashcode);

    LWLockAcquire(partitionLock, LW_EXCLUSIVE);

Before PostgreSQL 8.2, all of the shared-memory data structures used by the lock manager were protected by a single LWLock, the LockMgrLock; any operation involving these data structures had to exclusively lock LockMgrLock. Not too surprisingly, this became a contention bottleneck. To reduce contention, the lock manager’s data structures have been split into multiple “partitions”, each protected by an independent LWLock.

锁等待队列

openGauss 的 heavyweight lock 等待队列是“每个 LOCK 对象一条队列”,不是一条全局等待队列。

LOCK
   ├─ waitProcs                ← 当前锁对象的等待队列
   │    ├─ PGPROC A
   │    ├─ PGPROC B
   │    └─ PGPROC C

   ├─ waitMask                 ← 等待队列中出现过哪些锁模式
   └─ procLocks
        ├─ PROCLOCK(LOCK, PGPROC A)
        ├─ PROCLOCK(LOCK, PGPROC B)
        └─ PROCLOCK(LOCK, PGPROC C)

死锁检测算法

OpenGauss 的 heavyweight lock 死锁检测,核心是构造并搜索“等待图(waits-for graph,WFG)”。

src/gausskernel/storage/lmgr/deadlock.cpp

延迟检测

openGauss 不会在每次锁等待时立即执行完整死锁检测。请求发生冲突后先:

  WaitOnLock()
    → ProcSleep()
         → 插入 LOCK.waitProcs
         → 设置 PGPROC.waitLock / waitLockMode
         → 释放锁表分区 LWLock
         → 等待信号量

通常等待超过 deadlock_timeout 后才触发:

CheckDeadLock()
→ DeadLockCheck(t_thrd.proc)

Footnotes

  1. 容器镜像安装 https://docs.opengauss.org/zh/docs/latest/installation_guide/installing_the_container_image.html

  2. https://support.huawei.com/enterprise/zh/doc/EDOC1100521883/49d463fb?idPath=22658044|7919788|9856606|253384033|265624857