暂无图片
暂无图片
6
暂无图片
暂无图片
暂无图片

PostgreSQL的四种进程间锁

原创 阎书利 2022-05-28
1721

在PostgreSQL里有四种类型的进程间锁:

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

image.png

一、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结构体注释的内容翻译过来如下“它是谓词锁目标(这是一个可锁定对象)和已获得该目标上的锁的可序列化事务的组合。”

这表明谓词锁是数据库对象和事务间的一个特定关系,这样的关系是用以表示读写冲突的。

最后修改时间:2023-07-24 14:23:37
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论