在PostgreSQL里有四种类型的进程间锁:
Spinlocks:自旋锁,其保护的对象一般是数据库内部的一些数据结构,是一种轻量级的锁。
LWLocks:轻量锁,也是主要用于保护数据库内部的一些数据结构,支持独占和共享两种模式。
Regular locks:又叫heavyweight locks,也就是我们常说的表锁、行锁这些。
SIReadLock predicate locks:谓词锁,主要是用来表示数据库对象和事务间的一个特定关系。

一、Spinlocks-自旋锁
自旋锁顾名思义就是一直原地旋转等待的锁。一个进程如果想要访问临界区,必须先获得锁资源,如果不能获得,就会一直自旋,直到获取到锁资源,它是最轻量级的锁,不需要做内存上下文转换的。
所谓临界区(也称为临界段)就是访问和操作共享数据的代码段
这种自旋会造成CPU浪费,但是通常它保护的临界区非常小,封锁时间很短,因此通常自旋比释放CPU资源带来的上下文切换消耗要小。 它是一种和硬件结合的互斥锁,借用了硬件提供的原子操作的原语来对一些共享变量进行封锁,通常适用于临界区比较小的情况。
特点是:持有锁时间很短、无死锁检测机制和等待队列、事务结束时不会自动释放SpinLock。
Spinlocks来自于Linux内核,自旋锁简单来说是一种低级的同步机制,表示了一个变量可能的两个状态: Acquired 、Released 。在PostgreSQL的源码里其实经常可以看到这种spinlocks的用法,拿出PostgreSQL-15beta1的WalRcvForceReply()函数来简单举例的话,
/*
* Wake up the walreceiver main loop.
*
* This is called by the startup process whenever interesting xlog records
* are applied, so that walreceiver can check if it needs to send an apply
* notification back to the primary which may be waiting in a COMMIT with
* synchronous_commit = remote_apply.
*/
void
WalRcvForceReply(void)
{
Latch *latch;
WalRcv->force_reply = true;
/* fetching the latch pointer might not be atomic, so use spinlock */
SpinLockAcquire(&WalRcv->mutex);
latch = WalRcv->latch;
SpinLockRelease(&WalRcv->mutex);
if (latch)
SetLatch(latch);
}
可以看到在latch = WalRcv->latch; 这里想用指针取出结构体中的数据 。 WalRcv->latch,其中WalRcv是指向结构体的指针,latch是这个结构体类型的一个成员。表达式 WalRcv->latch引用了指针WalRcv指向的结构体的成员latch。而我们找到latch对应的结构体,看它的定义(如果是VScode的话直接左键双击,摁F12),在结构体里的定义为 Latch *latch; 表示latch是一个指针,*latch表示latch指针指向的相应的额外的结构体。这里的Latch是另外的一个结构体,如下图所示,但是我们只看上述内容以及Latch *latch的话就可以看出这里想实现的功能是一个赋值的过程。
typedef struct Latch
{
sig_atomic_t is_set;
sig_atomic_t maybe_sleeping;
bool is_shared;
int owner_pid;
#ifdef WIN32
HANDLE event;
#endif
} Latch;
这个过程的上下两侧,有SpinLockAcquire()和SpinLockRelease()两个函数。因为每一个想要获取自旋锁的处理,必须为这个变量写入一个表示自旋锁获取 (spinlock acquire)状态的值,并且为这个变量写入锁释放 (spinlock released)状态。如果一个处理程序尝试执行受自旋锁保护的代码,那么代码将会被锁住,直到占有锁的处理程序释放掉。在这个例子里,为了避免同时访问临界区,阻止竞态条件状态,所以操作必须是原子的。 因此用到了Spinlocks,从注释里也可以看见—— “获取闩锁指针可能不是原子的,所以使用自旋锁”。
竞态条件是指在并发环境中,当有多个事件同时访问同一个临界资源时,由于多个事件的并发执行顺序的不确定,从而导致程序输出结果的不确定,这种情况我们称之为竞态条件 (Race Conditions)或者竞争冒险(race hazard)。
二、LWLocks-轻量锁
LWLocks 负责保护共享内存中的数据结构,通常有共享和排他两种模式。
在几乎所有具有并行处理的软件中,都会采用一种轻量级锁(比如Oracle 叫latch,Mysql叫rw-lock,PG叫LwLock)来做串行化控制 ,这种轻量级锁,我们一般也统称为闩锁 。oracle的latch和PG的LwLocks都是在系统的spinlock之上实现的轻量级锁。
还记得原来维护ORACLE数据库的时候,数据库里的“latch: cache buffers chains ” 等待事件引起了我的好奇,分析逻辑读产生CBC latch分析了好久。
LWLocks一般由Spinlocks来保护。LWLocks比Spinlocks多一种共享模式。因此比Spinlocks稍微重了点,但是和其他的锁相比,还是比较轻的。
特点是:持有锁时间较短、无死锁检测机制、有等待队列、事务结束时会自动释放。
LWLocks通常用于对共享内存中数据结构的联锁访问。LWLocks支持独占和共享锁模式(用于读/写和只读)访问共享对象)。获取或释放LWLock的速度非常快。
看了下PG-15beta1版本src/backend/storage/lmgr/lwlocknames.h里轻量锁类型定义 ,发现和14.2的没有什么调整。
而对于LWLocks 的模式,我说有共享和排他两种模式时,加了个通常,因为我在看源码的时候发现LWLocks 的模式,除了原本LW_EXCLUSIVE和LW_SHARED之外,还有一个LW_WAIT_UNTIL_FREE,表示PGPROC->lwWaitMode中使用的一种特殊模式,是等待锁变空闲时的状态。它是不能用作LWLockAcquire()的参数来请求LWLocks 的,所以大家平时一般说两种模式。
typedef enum LWLockMode
{
LW_EXCLUSIVE,
LW_SHARED,
LW_WAIT_UNTIL_FREE /* A special mode used in PGPROC->lwWaitMode,when waiting for lock to become free. Not to be used as LWLockAcquire argument */
} LWLockMode;
如下,则是使用排他和共享模式LWLocks 的部分的相应举例:
LWLockAcquire(ControlFileLock, LW_EXCLUSIVE);
ControlFile->checkPointCopy.nextXid = checkPoint.nextXid;
LWLockRelease(ControlFileLock);
LWLockAcquire(XidGenLock, LW_SHARED);
ctx->next_fxid = ShmemVariableCache->nextXid;
ctx->oldest_xid = ShmemVariableCache->oldestXid;
LWLockRelease(XidGenLock);
三、Regular locks-普通锁
就是通常说的对数据库对象的锁。按照锁粒度,可以分为表锁、页锁、行锁等,这应该是我们最熟悉的了;其中表级锁按照等级,一共有8个等级。
特点是:持有锁时间可以很长、有死锁检测机制和等待队列、事务结束时会自动释放。
如下,是这八个级别锁的定义,相应的,其特定场景使用锁的情况在注释部分已经列举出来了。
/*
* These are the valid values of type LOCKMODE for all the standard lock
* methods (both DEFAULT and USER).
*/
/* 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 */
#define MaxLockMode 8 /* highest standard lock mode */
如下LOCK的结构体,lock的获取与释放,都有队列来维护 ,
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 */
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;
tag——唯一标识被锁定的对象
grantMask——当前在该对象上授予的所有锁类型的位掩码。
waitMask——当前该对象上等待的所有锁类型的位掩码。
procLocks——该锁的PROCLOCK对象列表。
waitProcs——等待该锁的进程队列。
requested——该锁当前请求的每种锁类型的数量(包括已经被批准的请求)。
nRequested——所有类型请求的锁总数。
granted——当前在该锁上授予的每种锁类型的计数。
nGranted——所有类型被授予的锁总数。
上面结构体里的LOCKTAG,与PG中的数据库,relation等强相关也就是我们可以可以锁定的不同类型的对象,例如表锁、行锁等等。通过注释我们就可以看到可以锁的对象。
typedef enum LockTagType
{
LOCKTAG_RELATION, /* whole relation */
LOCKTAG_RELATION_EXTEND, /* the right to extend a relation */
LOCKTAG_DATABASE_FROZEN_IDS, /* pg_database.datfrozenxid */
LOCKTAG_PAGE, /* one page of a relation */
LOCKTAG_TUPLE, /* one physical tuple */
LOCKTAG_TRANSACTION, /* transaction (for waiting for xact done) */
LOCKTAG_VIRTUALTRANSACTION, /* virtual transaction (ditto) */
LOCKTAG_SPECULATIVE_TOKEN, /* speculative insertion Xid and token */
LOCKTAG_OBJECT, /* non-relation database object */
LOCKTAG_USERLOCK, /* reserved for old contrib/userlock code */
LOCKTAG_ADVISORY /* advisory user locks */
} LockTagType;
下边是一个申请AccessShareLock和释放锁的举例
/* Get tuple descriptor from relation OID */
rel = relation_open(relid, AccessShareLock);
...
relation_close(rel, AccessShareLock);
四、SIReadLock predicate locks-谓词锁
谓词锁,主要是用来表示数据库对象和事务间的一个特定关系 。
PostgreSQL里用PREDICATELOCK结构体代表一个单独的锁。
typedef struct PREDICATELOCK
{
/* hash key */
PREDICATELOCKTAG tag; /* unique identifier of lock */
/* data */
SHM_QUEUE targetLink; /* list link in PREDICATELOCKTARGET's list of
* predicate locks */
SHM_QUEUE xactLink; /* list link in SERIALIZABLEXACT's list of
* predicate locks */
SerCommitSeqNo commitSeqNo; /* only used for summarized predicate locks */
} PREDICATELOCK;
然后用一个对象和事务作为此谓词锁的标识( tag )来标识一个谓词锁,如下PREDICATELOCKTAG结构体用来标识一个单独的谓词锁。
typedef struct PREDICATELOCKTAG
{
PREDICATELOCKTARGET *myTarget;
SERIALIZABLEXACT *myXact;
} PREDICATELOCKTAG;
PREDICATELOCK结构体注释的内容翻译过来如下 “当读取相关的数据库对象时,或者通过提升多个细粒度目标,可以在这里创建一个条目。当可序列化事务被清除时,与该可序列化事务相关的所有条目将被删除。当条目被组合成一个粗粒度的锁条目时,也可以删除它们。”
PREDICATELOCKTAG结构体注释的内容翻译过来如下“它是谓词锁目标(这是一个可锁定对象)和已获得该目标上的锁的可序列化事务的组合。”
这表明谓词锁是数据库对象和事务间的一个特定关系,这样的关系是用以表示读写冲突的。




