源码探究PostgreSQL中的Fast Path Locking

374 阅读9分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

\

1、概述

pg_locks视图中有个fastpath字段,官方文档中对其解释是“True if lock was taken via fast path, false if taken via main lock table”,那么我们不禁要问,什么样的lock是通过fast path获得的?这个fast path lock又是什么呢?

首先我们要知道,在数据库启动的时候会初始化共享内存区域,共享内存是一个统称,实际上有很多共享内存区,比如锁也是一块。这也就决定了锁其实是数据库中很宝贵的一部分资源,在pg中可以同时获取的锁的数量也是有上限的,其最大为:

max_locks_per_transaction * (max_connections + max_prepared_transactions)

例如:

bill@bill=>show max_locks_per_transaction ;
 max_locks_per_transaction
---------------------------
 10
(1 row)

bill@bill=>show max_connections ;
 max_connections
-----------------
 50
(1 row)

bill@bill=>show max_prepared_transactions;
 max_prepared_transactions
---------------------------
 0
(1 row)

那么我们可以获取的锁数量最大为:10 * 50 =500个,如果超过这个值就会出现以下报错:

bill@bill=>do language plpgsql $$
bill$# declare
bill$# begin
bill$#   for i in 1..20000 loop
bill$#     execute 'create table tbl_'||i||'(id int)';
bill$#   end loop;
bill$# end;
bill$# $$;
ERROR:  out of shared memory
HINT:  You might need to increase max_locks_per_transaction.
CONTEXT:  SQL statement "create table tbl_1889(id int)"
PL/pgSQL function inline_code_block line 5 at EXECUTE

说了这么多,让我们言归正传。正因为锁是很宝贵的资源,同时其加锁解锁这个操作的开销也很大。
而一般加锁的时候还需要去判断要获取锁的对象上是不是有互斥的锁了,但事实上,有很多锁之间是不互斥的,完全没必要去进行这个判断,如下图所示:
在这里插入图片描述
可以看到,在ShareUpdateExclusiveLock这个级别下面的锁之间是不会互斥的。那么我们是不是可以通过某张方式去判断,如果在某个对象上要加的锁全是该级别以下的,就略过这个判断的步骤呢?这便是我们今天要说的Fast Path Locking。

2、Lock相关概念介绍

2.1、PostgreSQL中的锁类型

在PostgreSQL中主要有4种类型的锁:

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

2.2、锁相关的结构

LockMethodData:指定加锁的方法。

typedef struct LockMethodData
{
	int			numLockModes;
	const LOCKMASK *conflictTab;
	const char *const * lockModeNames;
	const bool *trace_flag;
} LockMethodData;
  • numLockModes:锁模式数量,读/写等。
  • conflictTab:不同锁模式间的冲突关系。
  • lockModeNames:LockMethod名。
  • trace_flag:用户debug信息输出。

需要说明的是,pg中LockMethod分为3种,分别是:

  • DEFAULT_LOCKMETHOD:系统默认锁方法,例如表锁、行锁。
  • USER_LOCKMETHOD:用户定义的锁方法,例如advisory lock。
  • RESOURCE_LOCKMETHOD:资源锁。

至于锁冲突的关系就不再赘述,如前面的图片中所示。

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 */
	bool		holdTillEndXact;     /* flag for global deadlock detector */
} LOCK;

其中tag表示锁的类型,例如表锁、行锁等等。是用来在内存中搜索lock的关键信息。例如表锁,就会记录db oid+relation oid,详见:

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_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_RELATION_APPENDONLY_SEGMENT_FILE,	/* Segment file within an Append-Only relation */
	/* ID info for a relation is DB OID + REL OID + (LOGICAL) SEGMENT FILE # */
	LOCKTAG_OBJECT,				/* non-relation database object */
	/* ID info for an object is DB OID + CLASS OID + OBJECT OID + SUBID */

	/*
	 * 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_RESOURCE_QUEUE,		/* ID info for resource queue is QUEUE ID */
	LOCKTAG_DISTRIB_TRANSACTION,/* CDB: distributed transaction (for waiting for distributed xact done) */
	LOCKTAG_USERLOCK,			/* reserved for old contrib/userlock code */
	LOCKTAG_ADVISORY			/* advisory user locks */
} LockTagType;

PROCLOCK:在内存中除了lock结构外,另一个主要记录的请求锁的进程的PROCLOCK 结构。
其中PROCLOCKTAG主要存储的是持有该锁的backend进程的相关信息。

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

	/* data */
	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 */
	int			nLocks;			/* total number of times lock is held by 
								   this process, used by resource scheduler */
	SHM_QUEUE	portalLinks;	/* list of ResPortalIncrements for this 
								   proclock, used by resource scheduler */
} PROCLOCK;

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;

LOCALLOCK:对于每个进程来说,都会维护一个本地的hash表,主要记录一些已经存在的锁的信息,这样对于同一个进程来说,如果需要多次申请同一个锁,那么就没必要去共享内存中额外申请了。

typedef struct LOCALLOCKTAG
{
	LOCKTAG		lock;			/* identifies the lockable object */
	LOCKMODE	mode;			/* lock mode for this table entry */
} LOCALLOCKTAG;

typedef struct LOCALLOCKOWNER
{
	/*
	 * Note: if owner is NULL then the lock is held on behalf of the session;
	 * otherwise it is held on behalf of my current transaction.
	 *
	 * Must use a forward struct reference to avoid circularity.
	 */
	struct ResourceOwnerData *owner;
	int64		nLocks;			/* # of times held by this owner */
} LOCALLOCKOWNER;

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 */
	bool		istemptable;	/* MPP: During prepare we set this if the lock is on a temp table, to avoid MPP-1094 */
	int64		nLocks;			/* total number of times lock is held */
	int			numLockOwners;	/* # of relevant ResourceOwners */
	int			maxLockOwners;	/* allocated size of array */
	bool		holdsStrongLockCount;	/* bumped FastPathStrongRelationLocks */
	bool		lockCleared;	/* we read all sinval msgs for lock */
	LOCALLOCKOWNER *lockOwners; /* dynamically resizable array */
} LOCALLOCK;

3、锁的申请与释放

3.1、加锁与fast path locking

介绍了这么多,我们来看看在加锁的过程中什么时候会用到fast path locking呢?

加锁的操作主要通过LockAcquire函数来实现:

LockAcquireResult
LockAcquire(const LOCKTAG *locktag,
			LOCKMODE lockmode,
			bool sessionLock,
			bool dontWait)
{
	return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait,
							   true, NULL);
}

该函数的主要逻辑大致为:
1、查找backend本地的LOCALLOCK,如果存在该lock,并且当前backend已经持有了该锁,则直接赋予该锁,granted的次数+1。

2、如果1中不符合授予条件,则通过fastpath来获取锁。接下来我们主要看下什么情况下会使用fastpath来获取锁呢?

条件1:加锁的LockMethod为default方法;
条件2:加锁的对象为relation;
条件3:加锁的模式必须小于ShareUpdateExclusiveLock,因为前面我们也说了,这了该模式下的模式才会互不相斥;
条件4:每个backend,最多只能有16把锁通过fastpath获取得到,如果超过这个值则不能;
条件5:当前请求的锁对象上没有被加上大于ShareUpdateExclusiveLock模式的锁,如果有则不能使用fastpath。这也很好理解,如果有大于该模式的锁,怎么保证互不相斥呢?

相关的判断如下所示:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
这里需要说明下,通过使用长度为1024的数组来记录获取的级别大于等于ShareUpdateExclusiveLock的锁数量,若不等于0则表示该对象上已经存在这些级别的锁,那么便不会使用fastpath。

3、接下来,如果锁模式是高于ShareUpdateExclusiveLock的,那么进行标记,下次在对其锁对象加锁时,禁止走fastpath。对与其它backend的fastpath,如果已经加了该对象的锁,则需要把该锁从fastpath中清除,然后将已经加锁的对象放入到shared hash table中记录。这是为了保证加模式更强的锁,能够通过shared hash table检测到其它backend在这个对象上加的模式更低的锁。

static bool
FastPathTransferRelationLocks(LockMethod lockMethodTable, const LOCKTAG *locktag,
							  uint32 hashcode)
{
	LWLock	   *partitionLock = LockHashPartitionLock(hashcode);
	Oid			relid = locktag->locktag_field2;
	uint32		i;

	for (i = 0; i < ProcGlobal->allProcCount; i++)
	{
		PGPROC	   *proc = &ProcGlobal->allProcs[i];
		uint32		f;

		LWLockAcquire(proc->backendLock, LW_EXCLUSIVE);

		if (proc->databaseId != locktag->locktag_field1)
		{
			LWLockRelease(proc->backendLock);
			continue;
		}

		for (f = 0; f < FP_LOCK_SLOTS_PER_BACKEND; f++)
		{
			uint32		lockmode;

			/* Look for an allocated slot matching the given relid. */
			if (relid != proc->fpRelId[f] || FAST_PATH_GET_BITS(proc, f) == 0)
				continue;

			/* Find or create lock object. */
			LWLockAcquire(partitionLock, LW_EXCLUSIVE);
			for (lockmode = FAST_PATH_LOCKNUMBER_OFFSET;
			lockmode < FAST_PATH_LOCKNUMBER_OFFSET + FAST_PATH_BITS_PER_SLOT;
				 ++lockmode)
			{
				PROCLOCK   *proclock;

				if (!FAST_PATH_CHECK_LOCKMODE(proc, f, lockmode))
					continue;
				proclock = SetupLockInTable(lockMethodTable, proc, locktag,
											hashcode, lockmode);
				if (!proclock)
				{
					LWLockRelease(partitionLock);
					LWLockRelease(proc->backendLock);
					return false;
				}
				/* Set holdTillEndXact of proclock */
				proclock->tag.myLock->holdTillEndXact = \
					FAST_PATH_GET_HOLD_TILL_END_XACT_BITS(proc, f) > 0;
				GrantLock(proclock->tag.myLock, proclock, lockmode);
				FAST_PATH_CLEAR_LOCKMODE(proc, f, lockmode);
			}
			LWLockRelease(partitionLock);

			/* No need to examine remaining slots. */
			break;
		}
		LWLockRelease(proc->backendLock);
	}
	return true;
}

4、然后在共享内存中进行加锁。为此,需要首先在共享内存中创建LOCK,PROLOCK。

5、判断当前加锁是否与正在等锁的请求冲突,当前加锁不成功需要等待,否则,检查是否与当前已经持有(其它backend持有)的锁冲突,如果冲突,同样也需要等待。如果都不冲突,则加锁成功。

6、如果加锁的参数dontWait为true,那么当有冲突时,不会等待,清除掉当前请求的信息,返回LOCKACQUIRE_NOT_AVAIL。否则,会将加锁请求放入等待队列中,然后休眠,直到有信号量(有释放锁)唤醒,然后在此尝试加锁。当然,在等待锁的时候,后台会进行死锁检测,检测到死锁时,可能会终止此时加锁。
在这里插入图片描述

3.2、锁释放

锁释放的过程就相对比较简单了,大致流程如下:

  1. 查找backend本地的LOCALLOCK,如果存在该lock,并且当前backend已经持有了该锁,将对应ResourceOwner的锁计数减1,如果减到了0,则ResourceOwner需要释放该锁。另外,将总的granted数量-1。
  2. 如果释放的锁的锁模式小于ShareUpdateExclusiveLock,从fastpath中查找该锁,如果找到,释放对应模式的锁。
  3. 如果locallock中的lock为空,那么从shared hash
    table中查找该锁,因为有可能该锁被其它backend从fastpath放入到了shared hash table中(加锁逻辑中)。
  4. 释放该锁,如果有等待的锁请求,那么唤醒其它等待的进程。

4、总结

fast path locking其本质上就是为了避免对于某些不必要的锁模式进行锁冲突比较。

在加锁的过程中,满足下列几点便可使用fast path locking:

  • 加锁的LockMethod为default方法;
  • 加锁的对象为relation;
  • 加锁的模式必须小于ShareUpdateExclusiveLock;
  • 每个backend,最多只能有16把锁通过fastpath获取得到,如果超过这个值则不能;
  • 当前请求的锁对象上没有被加上大于ShareUpdateExclusiveLock模式的锁。

参考链接:
src/backend/storage/lmgr/README
www.postgresql.org/docs/13/vie…
blog.csdn.net/qq_41032824…