postgreSQL——并发控制实现3

2021@SDUSC

目录

概述

RegularLock

 RegularLock 的存储

 RegularLock 的数据结构

(1)锁方法表

(2)加锁对象标识

(3)加锁对象描述体

(4)锁持有者信息描述体

(5)本地锁表

RegularLock的主要操作

(1) RegularLock 的空间计算

(2) RegularLock 的初始化

(3) RegularLock 加锁

(4) RegularLock 的释放

(5) RegularLock 的申请

(6) RegularLock 的注销

(7)锁的冲突检测

(8)两阶段提交

(9)锁的清理


概述

这周分析了postgreSQL并发控制中RegularLock锁的

RegularLock

 RegularLock 的存储

共享存储器里有三种基本的锁结构:LOCK结构、PROCLOCK结构以及LOCALLOCK结构。LOCK结构的存在是为了存储每个可锁的对象、可以保持锁或者请求锁。PROCLOCK用于存储进程与锁之间的关系,通过该结构可以查询到当前进程阻塞了哪些进程,在死锁检测和消除中将频繁使用到该结构。

 RegularLock 的数据结构

static const LOCKMASK LockConflicts[] = {
	0,

	/* AccessShareLock */
	LOCKBIT_ON(AccessExclusiveLock),

	/* RowShareLock */
	LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock),

	/* RowExclusiveLock */
	LOCKBIT_ON(ShareLock) | LOCKBIT_ON(ShareRowExclusiveLock) |
	LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock),

	/* ShareUpdateExclusiveLock */
	LOCKBIT_ON(ShareUpdateExclusiveLock) |
	LOCKBIT_ON(ShareLock) | LOCKBIT_ON(ShareRowExclusiveLock) |
	LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock),

	/* ShareLock */
	LOCKBIT_ON(RowExclusiveLock) | LOCKBIT_ON(ShareUpdateExclusiveLock) |
	LOCKBIT_ON(ShareRowExclusiveLock) |
	LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock),

	/* ShareRowExclusiveLock */
	LOCKBIT_ON(RowExclusiveLock) | LOCKBIT_ON(ShareUpdateExclusiveLock) |
	LOCKBIT_ON(ShareLock) | LOCKBIT_ON(ShareRowExclusiveLock) |
	LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock),

	/* ExclusiveLock */
	LOCKBIT_ON(RowShareLock) |
	LOCKBIT_ON(RowExclusiveLock) | LOCKBIT_ON(ShareUpdateExclusiveLock) |
	LOCKBIT_ON(ShareLock) | LOCKBIT_ON(ShareRowExclusiveLock) |
	LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock),

	/* AccessExclusiveLock */
	LOCKBIT_ON(AccessShareLock) | LOCKBIT_ON(RowShareLock) |
	LOCKBIT_ON(RowExclusiveLock) | LOCKBIT_ON(ShareUpdateExclusiveLock) |
	LOCKBIT_ON(ShareLock) | LOCKBIT_ON(ShareRowExclusiveLock) |
	LOCKBIT_ON(ExclusiveLock) | LOCKBIT_ON(AccessExclusiveLock)

};

显然,系统中锁模式的个数不能比锁掩码的位数多。下面介绍几种RegularLock 实现中所涉及
的数据结构。

(1)锁方法表

一个锁方法表的控制结构定义在LockMethodData中,它存在于共享内存中。

static const LockMethodData user_lockmethod = {
	AccessExclusiveLock,		/* highest valid lock mode number */
	LockConflicts,
	lock_mode_names,
#ifdef LOCK_DEBUG
	&Trace_userlocks
#else
	&Dummy_trace
#endif
};

其中,numLockModes 指示在锁表上定义的锁类型数量,该数值必须小于或等于MAX_LOCKMODES。如果transactional为TRUE,说明这些锁会在事务结束后自动释放。confilict-Tab显示锁类型冲突的位掩码数组,如果锁类型i和j冲突,那么conflictTab[i]的第j位为1;锁模式的取值为1至numLockModes, 所以conflictTab[0]未使用。lockModeNames 用于Debug,而trace_ flag 是指向本锁方法GUC traceflag的指针。

(2)加锁对象标识

LOCKTAG 是在锁的Hash表格( hashtable)中寻找一个锁项所必须的码信息。一个LOCKTAG的值唯一标识 一个可加锁对象。

typedef struct LOCKTAG
{
uint32 locktag_ field1 ;
uint32 1ocktag_ fie1d2;
uint32 locktag_ field3 ;
uint1 6 1ocktag_ fie1d4;
uint8 1ocktag_ type;//锁类型标识
uint8 locktag_ lockme thodid;//镇方法ID
} LOCKTAG;

LOCKTAG结构中定义了一些lockag field, 但没有具体指明这些变量的用途,因为针对不同的
LockTagType,这些locktag_ field 中保存的数据不一定相同。例如,LockTagType 为LOCKTAG TUPLE时,要求的locktag field 包括DBOID + RELOID + BlockNumber + OffsetNumber。如果LockTagType为LOCKTAG _TRANSACTION,则只需要该事务的XID。

(3)加锁对象描述体

LOCK结构用于表示已经加锁的资源或是请求加锁的可锁资源。

typedef struct LOCK
{
LOCKTAG tag;//加锁对象的标识符*/
LOCKMASK grantMask;//当前在该对象上分配的所有锁类型的掩码*/
LOCKMASK wai tMask;//当前在该对象.上等待的所有锁类型的掩码*/
SHM_QUEUE procLocks;//与锁相关联的PROCLOCK对象队列*/
PROC _QUEUE waitProcs;//等待该对象上镇的进程等待队列*/
int requested[ MAX_ LOCKMODES ];//记录每种模式的锁被要求的次数*/
int nReques ted;//请求队列长度,即所有锁类型的锁请求的总的数量
int granted[ MAX_ LOCKMODES];//每一种锁类型上的已分配锁的数量*/
int nGranted;//granted数組中元素的个数
} LOCK;

其中,grantMask 、waitMask用于判断已持锁与请求锁是否冲突。

(4)锁持有者信息描述体

对于同一可加锁对象,可能有几个不同事务持有或等待锁。我们需要为每一个这样的锁持有者(或即将持有者)存储持有者/等待者信息。这些信息保存在PROCLOCK中。

typedef struct PROCLOCK
{
PROCLOCKTAG tag;//可加锁对象的唯一标识
LOCKMASK holdMask;//显示该PROCLOCK所表示的已经分配的锁
LOCKMASK releaseMask;//显示该PROCLOCK已释放的锁
/*在Lock对象中有一个PROCLOCK链表,
*记录的是所有持有该锁的PROCLOCK对象,
由这里的lockLink就是记录本PROCLOCK对象在该链表中的位置*/
SHM_QUEUE lockLink; 
/*每一个PGPROC对象中都有一个PROCLOCK链表,
*记录的是所有当前进程持有锁或即将持有锁的PROCLOCK对象,
*这里的procLink就是记录本PROCLOCK对象在该链表中的位置*/
SHM_ QUEUE proclink;
} PROCLOCK;

在一个PROCLOCK Hash表中查找一个PROCLOCK项需要一个码信息PROCLOCKTAG。PRO-
CLOCKTAG值为该对象唯-标识-个可加锁对象和-一个持有者/等待者组合。
PROCLOCK的所有者可能有两种:事务(通过运行它的后端的PGPROC和该事务的事务ID标
识)或者会话(由后端PGPROC和事务ID InvalidTransactionId 标识)。
当前会话PROCLOCK为用户锁和VACUUM持有的跨事务锁所使用。一个单独的后端可以一次在多个不同事务下持有多个锁(包括会话锁)。我们把这样的锁都当成从不冲突(一个后端不会阻塞自己,即一个后端中不同事务所持有的各种锁相互间不冲突)。变量holdMask显示该PROCLOCK所表示的已经分配的锁。注意,如果-一个进程当前正在等待锁,那么该进程和所等待的锁构成的PROCLOCK对象中的holdMask为0。如果该PROCLOCK表明不是一一个进程在等待锁,那么holdMasks为0的PROCLOCK对象将在方便的时候被回收。
每一个PROCLOCK对象被链接到关联的L0CK对象和所有者的PGPROC对象的列表中。PROCLOCK在创建的时候被马上插人到这些列表中,即使还没有锁被分配。一个正在等待分配锁的PGPROC也会被链接到该锁的进程等待队列中。

(5)本地锁表

每一个后台进程也维护一个当前它感兴趣的( 持有的或者希望持有的)每一个锁的本地Hash表。本地表LocalLock记录了已经获取的锁的次数,这允许在不需要另外对共享内存进行访问的情况下对同一锁的多次加锁请求。

typedef struct LOCALLOCK{
LOCALLOCKTAG tag;//LocalLock唯一标识
LOCK *lock;//关联LOCK结构
PROCLOCK * proclock;//关联后端PROCLOCK结构
uint32 hashcode;//LocalTag的Hash值
int nLocks;//持锁数量
int numLockOwners ;//锁持有者数量
int maxLockOwners;//已分配的存储空间
LOCALLOCKOWNER* lockOwners;//锁持有者列表
} LOCALLOCK;

PostgreSQL通过三张锁表联合管理系统的加锁情况。
●LockMethodLockHash: 由锁方法ID到锁表数据结构的映射。
●LockMethodProcLockHash: 锁方法对应的PROCLOCK对象的Hash表指针。
●LockMethodLocalHash: 锁方法对应的LOCALLOCK对象的Hash表指针。

RegularLock的主要操作

对于RegularLock的各种操作,相关函数定义在lock. c文件中。下面分别介绍这些操作,主要包括锁的空间计算、初始化、锁的申请、注销以及如何加锁、解锁等。

(1) RegularLock 的空间计算

锁空间计算操作由函数LockShmemSize实现,用于估计锁表所需共享内存空间大小。这些空间括:
●“Lock Hash" 表所需空间。
●“ProcLock Hash"表所需空间。
●为安全考虑多分配10%的空间。

(2) RegularLock 的初始化

锁的初始化操作由函数InitLocks 实现,该函数初始化锁管理器的数据结构,主要工作有初始化
LockMethodLockHash、LockMethodProcLockHash 和LockMethodLocalHash 这三个Hash表。

(3) RegularLock 加锁

加锁操作在函数LockAcquire中定义,它有四个参数LockTag、LockMode、SessionLock、dontWait:
●LockTage 是被锁对象的唯一标识。
●LockMode 指示要获得的锁模式( SHARE/EXCLUSIVE)。
●SessionLock 表示加锁的模式。如果为TRUE,表示为会话加锁;如果为FALSE,则为当前事务申请锁。
●DontWait表示申请锁是否允许等待。如果为TRUE,则在检查到无法获得锁之后不等待;如果为FALSE,则可以等待。
该函数的返回值Lock AcquireResult 表示加锁是否成功等结果信息。

typedef enum{
LOCKACQUIRE_ NOT_ AVAIL,//不能获得锁,且不能等待
LOCKACQUIRE_ OK,//成果获取到锁
LOCKACQUIRE_ ALREADY_ HELD//对巳获得锁增加引用计数
} LockAcquireResult;

申请加锁的流程如下:
1)首先用LOCKTAG和加锁模式得到具体的加锁类型,然后在本地表查找此加锁类型的信息。
2)如果没有,则在本地表插人此信息;否则,分配空间以记录锁拥有者的信息。
3)如果当前事务已经持有过此类型的锁,在本地表的计数器上加1,然后直接退出。否则在
全局的锁表( LockMethodLockHash)中查找这个锁。
4)如果在“Lock Hash”中找不到,则在“Lock Hash”中插人一个新元素。然后在ProcLock-Hash表里也查找对应的ProcLock。
5)如果在ProcLock Hash 表找不到,则插人该ProcLock。
6)检查这个类型的锁会不会与已加的锁发生冲突,如果不会,则加锁;否则,根据函数参数
决定等待还是退出。如果退出,还需清除锁表中相应的元素以保持一致性。

(4) RegularLock 的释放

与加锁相对应的操作是解锁,RegularLock 的解锁操作定义在函数LockRelease 中,该函数在本地锁表(LockMethodLocalHash) 中查找锁标记为LockTag 的锁,并释放该锁。如果SessionLock为
TRUE,则释放一个会话锁(SessionLock),否则,释放一个常规的事务锁。如果发现任何等待进程现在是可以被唤醒的,将请求的锁赋予它们并将其唤醒。

bool
LockRelease(const LOCKTAG *locktag, LOCKMODE lockmode, bool sessionLock)
{
	LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid;
	LockMethod	lockMethodTable;
	LOCALLOCKTAG localtag;
	LOCALLOCK  *locallock;
	LOCK	   *lock;
	PROCLOCK   *proclock;
	LWLock	   *partitionLock;
	bool		wakeupNeeded;

	if (lockmethodid <= 0 || lockmethodid >= lengthof(LockMethods))
		elog(ERROR, "unrecognized lock method: %d", lockmethodid);
	lockMethodTable = LockMethods[lockmethodid];
	if (lockmode <= 0 || lockmode > lockMethodTable->numLockModes)
		elog(ERROR, "unrecognized lock mode: %d", lockmode);

#ifdef LOCK_DEBUG
	if (LOCK_DEBUG_ENABLED(locktag))
		elog(LOG, "LockRelease: lock [%u,%u] %s",
			 locktag->locktag_field1, locktag->locktag_field2,
			 lockMethodTable->lockModeNames[lockmode]);
#endif

	
	MemSet(&localtag, 0, sizeof(localtag)); /* must clear padding */
	localtag.lock = *locktag;
	localtag.mode = lockmode;

	locallock = (LOCALLOCK *) hash_search(LockMethodLocalHash,
										  (void *) &localtag,
										  HASH_FIND, NULL);

	if (!locallock || locallock->nLocks <= 0)
	{
		elog(WARNING, "you don't own a lock of type %s",
			 lockMethodTable->lockModeNames[lockmode]);
		return false;
	}

	/*
	 * Decrease the count for the resource owner.
	 */
	{
		LOCALLOCKOWNER *lockOwners = locallock->lockOwners;
		ResourceOwner owner;
		int			i;

		/* Identify owner for lock */
		if (sessionLock)
			owner = NULL;
		else
			owner = CurrentResourceOwner;

		for (i = locallock->numLockOwners - 1; i >= 0; i--)
		{
			if (lockOwners[i].owner == owner)
			{
				Assert(lockOwners[i].nLocks > 0);
				if (--lockOwners[i].nLocks == 0)
				{
					if (owner != NULL)
						ResourceOwnerForgetLock(owner, locallock);
					/* compact out unused slot */
					locallock->numLockOwners--;
					if (i < locallock->numLockOwners)
						lockOwners[i] = lockOwners[locallock->numLockOwners];
				}
				break;
			}
		}
		if (i < 0)
		{
			/* don't release a lock belonging to another owner */
			elog(WARNING, "you don't own a lock of type %s",
				 lockMethodTable->lockModeNames[lockmode]);
			return false;
		}
	}


	locallock->nLocks--;

	if (locallock->nLocks > 0)
		return true;

	
	locallock->lockCleared = false;

	/* Attempt fast release of any lock eligible for the fast path. */
	if (EligibleForRelationFastPath(locktag, lockmode) &&
		FastPathLocalUseCount > 0)
	{
		bool		released;

		
		LWLockAcquire(&MyProc->backendLock, LW_EXCLUSIVE);
		released = FastPathUnGrantRelationLock(locktag->locktag_field2,
											   lockmode);
		LWLockRelease(&MyProc->backendLock);
		if (released)
		{
			RemoveLocalLock(locallock);
			return true;
		}
	}


	partitionLock = LockHashPartitionLock(locallock->hashcode);

	LWLockAcquire(partitionLock, LW_EXCLUSIVE);


	lock = locallock->lock;
	if (!lock)
	{
		PROCLOCKTAG proclocktag;

		Assert(EligibleForRelationFastPath(locktag, lockmode));
		lock = (LOCK *) hash_search_with_hash_value(LockMethodLockHash,
													(const void *) locktag,
													locallock->hashcode,
													HASH_FIND,
													NULL);
		if (!lock)
			elog(ERROR, "failed to re-find shared lock object");
		locallock->lock = lock;

		proclocktag.myLock = lock;
		proclocktag.myProc = MyProc;
		locallock->proclock = (PROCLOCK *) hash_search(LockMethodProcLockHash,
													   (void *) &proclocktag,
													   HASH_FIND,
													   NULL);
		if (!locallock->proclock)
			elog(ERROR, "failed to re-find shared proclock object");
	}
	LOCK_PRINT("LockRelease: found", lock, lockmode);
	proclock = locallock->proclock;
	PROCLOCK_PRINT("LockRelease: found", proclock);

	
	if (!(proclock->holdMask & LOCKBIT_ON(lockmode)))
	{
		PROCLOCK_PRINT("LockRelease: WRONGTYPE", proclock);
		LWLockRelease(partitionLock);
		elog(WARNING, "you don't own a lock of type %s",
			 lockMethodTable->lockModeNames[lockmode]);
		RemoveLocalLock(locallock);
		return false;
	}

	
	wakeupNeeded = UnGrantLock(lock, lockmode, proclock, lockMethodTable);

	CleanUpLock(lock, proclock,
				lockMethodTable, locallock->hashcode,
				wakeupNeeded);

	LWLockRelease(partitionLock);

	RemoveLocalLock(locallock);
	return true;
}


该函数的流程如下: 
1)用LOCKTAG和加锁模式得到具体的锁类型,然后在本地表查找此类型的锁的信息。
2)找到此类型锁的拥有者,在它持有锁的计数器上减1。如果它已经不再持有此锁,则删除这个拥有者的信息。
3)如果这个类型的锁并没有真正释放,只是计数器减1,直接退出。
4)否则在全局的"Lock Hash"和“ProcLock Hash”表里查找此锁对应的Lock和Proclock,调用UnGrantLock修改其信息。唤醒可以被唤醒的进程,并从"Local Hash”里移除该类型锁。如果只要释放一个本地锁,PostgreSQL 用函数RemoveLocallock 来完成。
 

(5) RegularLock 的申请

该操作在函数GrantlocalLock中定义,如果当前进程已经获得所需的锁,那么只需要对当前该锁的引用计数加1,即更新LOCALLOCK结构表示所需的锁已经被授予了。

(6) RegularLock 的注销

该操作为RegularLock的申请操作的逆过程,定义在函数UnGrantLock中。它取消对一个锁的分配,即更新LOCK和PROCLOCK数据结构以显示该锁不再由当前持有者持有或请求。如果在该锁
上存在任何等待者则需要由ProcLockWakeup唤醒。

(7)锁的冲突检测

该操作用来检测当前后端所请求的锁和已持有的锁是否冲突,它定义在函数LockCheckConflicts中。如果存在冲突,返回STATUS. FOUND,否则返回STATUS.OK。

int
LockCheckConflicts(LockMethod lockMethodTable,
				   LOCKMODE lockmode,
				   LOCK *lock,
				   PROCLOCK *proclock)
{
	int			numLockModes = lockMethodTable->numLockModes;
	LOCKMASK	myLocks;
	int			conflictMask = lockMethodTable->conflictTab[lockmode];
	int			conflictsRemaining[MAX_LOCKMODES];
	int			totalConflictsRemaining = 0;
	int			i;
	SHM_QUEUE  *procLocks;
	PROCLOCK   *otherproclock;

	if (!(conflictMask & lock->grantMask))
	{
		PROCLOCK_PRINT("LockCheckConflicts: no conflict", proclock);
		return STATUS_OK;
	}

	
	myLocks = proclock->holdMask;
	for (i = 1; i <= numLockModes; i++)
	{
		if ((conflictMask & LOCKBIT_ON(i)) == 0)
		{
			conflictsRemaining[i] = 0;
			continue;
		}
		conflictsRemaining[i] = lock->granted[i];
		if (myLocks & LOCKBIT_ON(i))
			--conflictsRemaining[i];
		totalConflictsRemaining += conflictsRemaining[i];
	}

	/* If no conflicts remain, we get the lock. */
	if (totalConflictsRemaining == 0)
	{
		PROCLOCK_PRINT("LockCheckConflicts: resolved (simple)", proclock);
		return STATUS_OK;
	}

	/* If no group locking, it's definitely a conflict. */
	if (proclock->groupLeader == MyProc && MyProc->lockGroupLeader == NULL)
	{
		Assert(proclock->tag.myProc == MyProc);
		PROCLOCK_PRINT("LockCheckConflicts: conflicting (simple)",
					   proclock);
		return STATUS_FOUND;
	}


	procLocks = &(lock->procLocks);
	otherproclock = (PROCLOCK *)
		SHMQueueNext(procLocks, procLocks, offsetof(PROCLOCK, lockLink));
	while (otherproclock != NULL)
	{
		if (proclock != otherproclock &&
			proclock->groupLeader == otherproclock->groupLeader &&
			(otherproclock->holdMask & conflictMask) != 0)
		{
			int			intersectMask = otherproclock->holdMask & conflictMask;

			for (i = 1; i <= numLockModes; i++)
			{
				if ((intersectMask & LOCKBIT_ON(i)) != 0)
				{
					if (conflictsRemaining[i] <= 0)
						elog(PANIC, "proclocks held do not match lock");
					conflictsRemaining[i]--;
					totalConflictsRemaining--;
				}
			}

			if (totalConflictsRemaining == 0)
			{
				PROCLOCK_PRINT("LockCheckConflicts: resolved (group)",
							   proclock);
				return STATUS_OK;
			}
		}
		otherproclock = (PROCLOCK *)
			SHMQueueNext(procLocks, &otherproclock->lockLink,
						 offsetof(PROCLOCK, lockLink));
	}

	/* Nope, it's a real conflict. */
	PROCLOCK_PRINT("LockCheckConflicts: conflicting (group)", proclock);
	return STATUS_FOUND;
}


一个进程所有锁之间并不冲突,即使是由不同事务ID所持有的(例如会话锁和事务锁并不冲突)。所以在决定请求的新锁是否和已持有锁之间是否冲突时,我们需要减去我们本身所持有的锁。

(8)两阶段提交

两阶段提交中涉及以下三种关于锁的操作:
●获取一个Preparcd事务的锁。该操作定义在函数lock. _twophase_ recover中。该函数在数据库启动的时候被调用,这个重新获取锁的过程不会和其他事务有冲突。
●分布式数据库预提交COMMIT PREPARED时释放该两阶段提交记录指示的锁。该函数定义在lock. _twophase. postcommit 中。
●分布式数据库进行两阶段提交ROLLBACK PREPARED时释放锁操作,该操作定义在函数
lock_ ltwophase_ postabort 中。

(9)锁的清理

该操作定义在函数CleanUpLock中,在释放锁之后执行,主要是清理ProcLock队列入口。
 

总结

分析完regularlock锁这项大工程之后,准备分析死锁管理机制相关代码。欢迎批评指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值