• Postgresql源码(69)常规锁简单分析


    相关:
    《Postgresql源码(40)Latch的原理分析和应用场景》
    《Postgresql源码(67)LWLock锁的内存结构与初始化》
    《Postgresql源码(68)virtualxid锁的原理和应用场景》
    《Postgresql源码(69)常规锁简单分析》

    0 总结 & 速查

    数据结构需要注意的是:

    • LOCKTAG唯一值组成:四个公共区域可以放四个唯一标识;一个锁类型记录锁住的对象类型;一个锁method(咨询锁一类、其他锁一类)。
    • 本地锁表:相当于缓存,记录已经申请过得锁,包括fastpath锁和主锁表的锁,都会在本地锁表中记录。
    • fastpath
      • 记录在PGPROC中,使用一个uint64位图(3位一组16组共48位,每组记录一个锁级别)和一个16个oid的数组记录16个表的Oid。
      • fastpath进入需要两个条件:1 弱锁、2 共享内存fastpath数组,对应锁位置(锁tag哈希%1024到一个位置)上为0(表示别人没加过强锁)。
      • 如果共享内存fastpath数组对应tag位置上是1,则不能使用fastpath。
    • 主锁表:本地锁表、fastpath都查不到,不管弱锁、强锁都去主锁表中申请,申请后如果主锁表中没有,则创建;如果有,则判断相容性。

    加锁过程需要注意的是:

    1. 【本地】先查本地锁表,注意本地锁表的TAG是LOCKTAG+锁级别。
    2. 【fastpath查询】如满足加锁对象是表 && 【弱锁】 && fastpath坑位没满(PGPROC中可记录16个表OID)可以走fastpath。去遍历MYPROC的数组的16个位置,查到了或上当前申请的锁级别就返回(同时记录到本地锁表)。
    3. 【fastpath维护】【共享内存强锁数组】如满足加锁对象是表 && 【强锁】,需要维护fastpath,把强锁的tag的hashcode%1024找到fastpath共享内存数组位置,对位置++表示有强锁申请过这个对象了,后面即使在来弱锁上一步也过不了了
    4. 【fastpath维护】【fastpath锁换到主锁表】如果有强锁申请了,需要把所有PGPROC的记录的16个fastpath OID全部查一遍,如果有,需要把fastpath弱锁清理,在主锁表中重建弱锁,便于死锁检测、锁冲突检查。
    5. 【主锁表进程锁表查找/新建】用tag去主锁表中查询,如果没有新建,如果有返回LOCK结构。
    6. 【冲突检查】【等锁队列检查】检查要加的锁级别,和当前锁的等待队列中的锁级别是否有冲突,如果冲突需要等待。
    7. 【冲突检查】【锁相容性检查】检查申请的级别和锁本身已经GRANT的级别是否相容。
    8. 【等待】如果冲突,开始等待。

    关于第六点举例,PG经典的锁排队场景的成因(具体看3.5)

    • 第一个人拿1级锁(select长事务)
    • 第二个人等着拿8级锁(vacuum full)
    • 又来一个1级锁,必须要排队!因为强锁来了把fastpath的共享内存强锁位置标记了,后来的1级锁都不能在走fastpath了,走不成fastpath只能走主锁表,就会到这里等待。

    关于lockmode和locktype:

    • lockmode:1-8级锁,1-3弱锁,4,5-8强锁。
    • locktype:在tag中维护,记录锁对象类型。

    1 常规锁概念

    常规锁相容性复习:https://www.postgresql.org/docs/14/explicit-locking.html

    • 常规锁共8把锁,常称为1-8级锁,便于记忆:
      • 1-3级锁为弱锁,内部相容(增删改查)。
      • 5-8级锁为强锁,内部不相容(建索引、vacuum full、alter table等)。
      • 4级锁比较特殊,和弱锁相容,但和自己不相容(vacuum、analyaze、create index concurrently)。
        在这里插入图片描述

    2 常规锁关键数据结构

    • 【共享内存】主锁表:LockMethodLockHash(存储所有锁对象
    • 【共享内存】锁进程关系表:LockMethodProcLockHash(查询当前进程阻塞了哪些进程,死锁检测
    • 【本地内存】本地锁表:对重复申请的锁计数,避免频繁访问(本地快速加锁,锁缓存
    • 【共享内存】本地弱锁fastpath(需要共享内存中查询是否有强锁FastPathStrongRelationLockData

    2.1 主锁表LockMethodLockHash(共享内存)

    共享内存中维护两个核心哈希表:

    • 主锁表:LockMethodLockHash (key: LOCKTAG)->(value:LOCK)
    • 进程锁表:LockMethodProcLockHash (key: PROCLOCKTAG)->(value:PROCLOCK)
    InitLocks
    	...
    	static HTAB *LockMethodLockHash;
    	LockMethodLockHash = ShmemInitHash("LOCK hash",
    									   init_table_size,
    									   max_table_size,
    									   &info,
    									   HASH_ELEM | HASH_BLOBS | HASH_PARTITION);
    
    	static HTAB *LockMethodProcLockHash;
    	LockMethodProcLockHash = ShmemInitHash("PROCLOCK hash",
    										   init_table_size,
    										   max_table_size,
    										   &info,
    										   HASH_ELEM | HASH_FUNCTION | HASH_PARTITION);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    关键数据结构:

    1. 数据结构的设计主要还是为了便于查询,本来只需要一个main lock table即可(用【LOCKTAG】查【LOCK】结构)。但是因为有锁队列的存在,有时需要知道哪些会话在等锁、某个锁有哪些会话在等这样一些问题,所以增加了PROCLOCK结构,用来维护锁和后台进程的关系。
    2. 使用进程锁表LockMethodProcLockHash可以做到用【LOCK+PGPROC】查询【PROCLOCK】结构,维持锁和会话的关系。
      在这里插入图片描述

    logtype
    在这里插入图片描述

    locktag_lockmethodid
    目前只有两套,2是专门给咨询锁使用的,其他都是1。

    /* These identify the known lock methods */
    #define DEFAULT_LOCKMETHOD	1
    #define USER_LOCKMETHOD		2
    
    • 1
    • 2
    • 3

    2.2 fastpath强锁表FastPathStrongRelationLocks(共享内存)

    数据库最常发生的增删改查正常都需要去主锁表中申请常规锁,但是DML操作其实只需要弱锁,且弱锁之间是相容的;所以PG在1-3级锁上做了一层优化:如果事务对某个对象申请弱锁,且对象上没有别人申请的强锁,则可以在会话本地记录弱锁,不走主锁表,不写共享内存。

    这里有两个条件:

    1. 自己申请弱锁(已知)
    2. 申请对象上没有别人加强锁(需要查询)

    对象上有没有别人申请过强锁这个信息,记录到下面共享内存结构中的count中,如果有加过强锁,对应位置的计数加1。注意这个数据结构是放在共享内存中的。但是访问他的代价,比每次都去主锁表中加锁要低很多。

    共享内存结构:

    lock.c
    typedef struct
    {
    	slock_t		mutex;
    	uint32		count[FAST_PATH_STRONG_LOCK_HASH_PARTITIONS]; // tag算hash值后%1024到这里1024个位置,是强锁就+1
    } FastPathStrongRelationLockData;
    
    static volatile FastPathStrongRelationLockData *FastPathStrongRelationLocks;
    
    初始化:
    InitLocks
    	...
    	FastPathStrongRelationLocks =
    		ShmemInitStruct("Fast Path Strong Relation Lock Data",
    						sizeof(FastPathStrongRelationLockData), &found);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    fastpath锁信息记录位置:

    PGPROC
    {
    	...
    	// fastpath使用
    	LWLock		fpInfoLock;		/* protects per-backend fast-path state */
    	uint64		fpLockBits;		/* lock modes held for each fast-path slot */
    	Oid			fpRelId[FP_LOCK_SLOTS_PER_BACKEND]; // 最多能存16个oid
    	
    	// 下面是VXID专用
    	bool		fpVXIDLock;		/* are we holding a fast-path VXID lock? */
    	LocalTransactionId fpLocalTransactionId;	/* lxid for fast-path VXID
    												 * lock */
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    关于fpLockBits、fpLockBits下面有具体介绍。
    在这里插入图片描述

    2.3 本地锁表LockMethodLocalHash(本地内存)

    本地内存中维护本地锁表:LockMethodLockHash

    在事务内第一次申请锁需要检查冲突,拿到锁后,如果同事务内继续申请这把锁,就可以直接获取到了。因为事务内的锁是在事务结束时统一释放的。

    在申请成功后,将锁信息存到下面本地锁表结构中,供后面使用:

    typedef struct LOCALLOCKTAG
    {
    	LOCKTAG		lock;			/* identifies the lockable object */
    	LOCKMODE	mode;			/* lock mode for this table entry */
    } LOCALLOCKTAG;
    typedef struct LOCALLOCK
    {
    	/* tag */
    	LOCALLOCKTAG tag;			/* unique identifier of locallock entry */
    
    	/* data */
    	uint32		hashcode;		/* copy of LOCKTAG's hash value */
    	LOCK	   *lock;			// 锁
    	PROCLOCK   *proclock;		// 锁进程关系
    	int64		nLocks;			// 本地多少次持有这个锁
    	int			numLockOwners;	// ResourceOwners的数量
    	int			maxLockOwners;	// 保存ResourceOwners的数组长度
    	LOCALLOCKOWNER *lockOwners; /* dynamically resizable array */
    	bool		holdsStrongLockCount;	// 是否持有强锁
    	bool		lockCleared;	/* we read all sinval msgs for lock */
    } LOCALLOCK;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    初始化

    InitLocks
    	...
    	LockMethodLocalHash = hash_create("LOCALLOCK hash",
    									  16,
    									  &info,
    									  HASH_ELEM | HASH_BLOBS);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3 加锁流程分析

    API

    LockAcquireResult
    LockAcquire(const LOCKTAG *locktag,				:锁tag
    			LOCKMODE lockmode,					:申请锁模式
    			bool sessionLock,					:会话锁 或 事务锁
    			bool dontWait)						:是否等待
    {
    	return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait,
    							   true, NULL);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    LockAcquireExtended

    LockAcquireResult
    LockAcquireExtended(const LOCKTAG *locktag,
    					LOCKMODE lockmode,
    					bool sessionLock,
    					bool dontWait,
    					bool reportMemoryError,
    					LOCALLOCK **locallockp)
    {
    	...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3.1 查本地锁表

    	MemSet(&localtag, 0, sizeof(localtag)); /* must clear padding */
    	localtag.lock = *locktag;
    	localtag.mode = lockmode;
    	locallock = (LOCALLOCK *) hash_search(LockMethodLocalHash,
    										  (void *) &localtag,
    										  HASH_ENTER, &found);							  
    	if (!found)
    	{
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    没查到构造一条放入本地锁表

    		locallock->lock = NULL;
    		locallock->proclock = NULL;
    		locallock->hashcode = LockTagHashCode(&(localtag.lock));
    		locallock->nLocks = 0;                    // 没人持锁
    		locallock->holdsStrongLockCount = false;  // 没有强锁
    		locallock->lockCleared = false;
    		locallock->numLockOwners = 0;
    		locallock->maxLockOwners = 8;
    		locallock->lockOwners = NULL;	/* in case next line fails */
    		locallock->lockOwners = (LOCALLOCKOWNER *)
    			MemoryContextAlloc(TopMemoryContext,
    							   locallock->maxLockOwners * sizeof(LOCALLOCKOWNER));
    	}
    	else
    	{
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    查到了,如果ResourcesOwner数组不够了,扩展数组

    	 	if (locallock->numLockOwners >= locallock->maxLockOwners)
    		{
    			int			newsize = locallock->maxLockOwners * 2;
    
    			locallock->lockOwners = (LOCALLOCKOWNER *)
    				repalloc(locallock->lockOwners,
    						 newsize * sizeof(LOCALLOCKOWNER));
    			locallock->maxLockOwners = newsize;
    		}
    	}
    	hashcode = locallock->hashcode;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    如果locallock->nLocks,说明这个锁之前已经拿过了,就不用再走下面逻辑了。

    由GrantLockLocal负责更新本地锁的内容。
    1:locallock->nLocks++。
    2:更新ResourceOwner,增加锁计数。

    	if (locallock->nLocks > 0)
    	{
    		GrantLockLocal(locallock, owner);
    		if (locallock->lockCleared)
    			return LOCKACQUIRE_ALREADY_CLEAR;
    		else
    			return LOCKACQUIRE_ALREADY_HELD;
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3.2 查fastpath

    进入条件一:EligibleForRelationFastPath

    • 必须是非咨询锁locktag_lockmethodid == DEFAULT_LOCKMETHOD
    • 必须是表锁locktag_type == LOCKTAG_RELATION
    • 必须是当前数据库的锁locktag_field1 == MyDatabaseId
    • 锁级别需要小于4(mode) < ShareUpdateExclusiveLock

    进入条件二:

    • 本地fastpath锁不能超过16个FastPathLocalUseCount < FP_LOCK_SLOTS_PER_BACKEND
    /*
    #define EligibleForRelationFastPath(locktag, mode) \
    	((locktag)->locktag_lockmethodid == DEFAULT_LOCKMETHOD && \
    	(locktag)->locktag_type == LOCKTAG_RELATION && \
    	(locktag)->locktag_field1 == MyDatabaseId && \
    	MyDatabaseId != InvalidOid && \
    	(mode) < ShareUpdateExclusiveLock)
    */
    	if (EligibleForRelationFastPath(locktag, lockmode) &&
    		FastPathLocalUseCount < FP_LOCK_SLOTS_PER_BACKEND)
    	{
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    用tag算的hashcode在%1024,落到1024数组FastPathStrongRelationLocks->count的某个位置上。

    		uint32		fasthashcode = FastPathStrongLockHashPartition(hashcode);
    		bool		acquired;
    
    		LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
    
    • 1
    • 2
    • 3
    • 4

    查共享内存fastpath锁表FastPathStrongRelationLocks,检查是否有强锁。

    		if (FastPathStrongRelationLocks->count[fasthashcode] != 0)
    			acquired = false;
    		else
    
    • 1
    • 2
    • 3

    没发现强锁,可以用fastpath加锁,下面要判断是否有位置能加fastpath锁。

    (注意这里只把locktag_field2传进去了,表锁的tag组成是:[dbid, redid](SET_LOCKTAG_RELATION),所以这里只把表的OID传进去了)

    FastPathGrantRelationLock函数:

    • 使用PGPROC的数组Oid fpRelId[16];来保存OID。
    • 使用PGPROC的变量uint64 fpLockBits当做位图来记录锁级别。
      在这里插入图片描述

    FastPathGrantRelationLock逻辑:

    1. 3个bit一组按顺序查位图是不是空的,是空的就记录下来位置,不是空的就看下oid里面记的是不是需要的,如果正好Oid也是需要的,把当前请求锁模式或进去就可以返回了。
    2. 如果查了一遍位图,所有Oid都不是需要的,那就找一个空的位置,把锁级别记录到位图,OID记录到数组,然后返回。
    3. 如果查了一遍位图,没有一个空余位置,就返回false了。
    static bool
    FastPathGrantRelationLock(Oid relid, LOCKMODE lockmode)
    {
    	uint32		f;
    	uint32		unused_slot = FP_LOCK_SLOTS_PER_BACKEND;
    	for (f = 0; f < FP_LOCK_SLOTS_PER_BACKEND; f++)
    	{
    		if (FAST_PATH_GET_BITS(MyProc, f) == 0)
    			unused_slot = f;
    		else if (MyProc->fpRelId[f] == relid)
    		{
    			FAST_PATH_SET_LOCKMODE(MyProc, f, lockmode);
    			return true;
    		}
    	}
    	if (unused_slot < FP_LOCK_SLOTS_PER_BACKEND)
    	{
    		MyProc->fpRelId[unused_slot] = relid;
    		FAST_PATH_SET_LOCKMODE(MyProc, unused_slot, lockmode);
    		++FastPathLocalUseCount;
    		return true;
    	}
    	return false;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    回到LockAcquireExtended,如果拿到了fastpath,就可以直接返回了。

    返回前要把locallock的lock和proclock置空,但是本地锁表还是有对应的项在的!区别就是locallock->lock字段是不是空的。

    			acquired = FastPathGrantRelationLock(locktag->locktag_field2,
    												 lockmode);
    		LWLockRelease(&MyProc->fpInfoLock);
    		if (acquired)
    		{
    			locallock->lock = NULL;
    			locallock->proclock = NULL;
    			GrantLockLocal(locallock, owner);
    			return LOCKACQUIRE_OK;
    		}
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    3.3 外部请求强锁;标记fastpath共享内存冲突表;检查fastpath,如果存在需要换出到主锁表

    ConflictsWithRelationFastPath检查:是强锁!

    • 非咨询锁locktag_lockmethodid == DEFAULT_LOCKMETHOD
    • 表锁locktag_type == LOCKTAG_RELATION
    • 锁等级大于4级(mode) > ShareUpdateExclusiveLock
    	if (ConflictsWithRelationFastPath(locktag, lockmode))
    	{
    		uint32		fasthashcode = FastPathStrongLockHashPartition(hashcode);
    
    • 1
    • 2
    • 3

    开始处理强锁:BeginStrongLockAcquire

    • 1 (重要!相当于禁用了后面这个对象后来的所有fastpath)共享内存强锁表对应位置++FastPathStrongRelationLocks->count[fasthashcode]++;
    • 2 本地锁记录强锁存在locallock->holdsStrongLockCount = true
    • 3 全局记录本地强锁StrongLockInProgress = locallock

    fastpath锁转换到主锁表:FastPathTransferRelationLocks

    • 1 轮询PGPROC,用表oid在fpRelId[16]数组中查询,如果找到了,用找到的锁级别去主锁表中加锁。
    • 2 清理fastpath对应的位置。
    • 3 完成转换:PGPROC记录的fastpath锁换成主锁表的锁,便于死锁检查。
    • 4 注意这里只是把fastpath中已经存在弱锁换到主锁表了,并没有给当前请求LockAcquire的对象加锁。
    		BeginStrongLockAcquire(locallock, fasthashcode);
    		if (!FastPathTransferRelationLocks(lockMethodTable, locktag,
    										   hashcode))
    		{
    			// 加锁失败处理,共享内存不足。
    		}
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    3.4 插入/查找主锁表

    1. 本地锁表没有、fastpath不能用(满了或者是强锁),到这里开始操作主锁表。

    2. SetupLockInTable构造 LOCK 和 PROCLOCK结构,完成主锁表、进程锁表插入

    	partitionLock = LockHashPartitionLock(hashcode);
    	LWLockAcquire(partitionLock, LW_EXCLUSIVE);
    	// 构造 LOCK 和 PROCLOCK结构,完成主锁表、进程锁表插入
    	proclock = SetupLockInTable(lockMethodTable, MyProc, locktag,
    								hashcode, lockmode);
    	...
    	locallock->proclock = proclock;
    	lock = proclock->tag.myLock;
    	locallock->lock = lock;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    3.5 查到主锁表中的LOCK,先检查申请锁是否与等待队列中的锁不相容

    lockMethodTable->conflictTab[lockmode]获得是与申请锁级别冲突的级别,例如申请1级锁:

    • lockmode = 1
    • lockMethodTable->conflictTab[lockmode] = 100000000
    • 1级锁和8级锁冲突。

    lockMethodTable->conflictTab[lockmode] & lock->waitMask表示如果当前申请的锁级别(比如我需要1级锁),和别人等在这个锁上的锁级别(别人需要8级锁,在等)不相容,那么当前申请的锁必须进入等待状态(我需要的1级锁必须等锁)。

    所以就造成了PG经典的锁排队场景:

    • 第一个人拿1级锁(select长事务)
    • 第二个人等着拿8级锁(vacuum full)
    • 后面再来一个1级锁就必须要排队了,因为强锁来了走3.3把fastpath的共享内存锁表标记了,后来的1级锁都不能在走fastpath了,走不成fastpath只能走主锁表,就会到这里等待。
    	if (lockMethodTable->conflictTab[lockmode] & lock->waitMask)
    		found_conflict = true;
    	else
    
    
    • 1
    • 2
    • 3
    • 4

    3.6 再按锁相容表检查确定是否需要等待

    		found_conflict = LockCheckConflicts(lockMethodTable, lockmode,
    											lock, proclock);
    
    	if (!found_conflict)
    	{
    		/* No conflict with held or previously requested locks */
    		GrantLock(lock, proclock, lockmode);
    		GrantLockLocal(locallock, owner);
    	}
    	else
    	{
    		...
    		WaitOnLock(locallock, owner);
    		...
    	}
    
    	/*
    	 * Lock state is fully up-to-date now; if we error out after this, no
    	 * special error cleanup is required.
    	 */
    	FinishStrongLockAcquire();
    
    	LWLockRelease(partitionLock);
    	return LOCKACQUIRE_OK;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
  • 相关阅读:
    linux:一切都是文件
    爬虫 — 正则案例
    FastAPI 学习之路(二)
    5.ARP地址解析协议
    C++如何取出打印std::vector<cv::KeyPoint>的第0号元素
    第06章 Tableau仪表板和故事
    Python爬虫抓取网站模板的完整版实现
    i5 13600KF参数 酷睿i53600KF什么水平i5 13600KF核显相当于什么显卡
    k8s-svc外界访问pod容器服务-4
    2023年中国雷达设备市场规模及市场份额分析[图]
  • 原文地址:https://blog.csdn.net/jackgo73/article/details/126260915