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.h,atomic.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);
}
}
锁模式
/* 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 */

从加锁路径角度来看,
- 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 锁。原因是该模式虽与自身冲突,却不与 AccessShareLock、RowShareLock、RowExclusiveLock 三种 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)