Postgresql源码(69)常规锁细节分析
相关:
《Postgresql源码(40)Latch的原理分析和应用场景》
《Postgresql源码(67)LWLock锁的内存结构与初始化》
《Postgresql源码(68)virtualxid锁的原理和应用场景》
《Postgresql源码(69)常规锁简单分析》
《Postgresql源码(73)两阶段事务PrepareTransaction事务如何与会话解绑(上)》
《Postgresql源码(74)两阶段事务PrepareTransaction事务如何与会话解绑(下)》
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都查不到,不管弱锁、强锁都去主锁表中申请,申请后如果主锁表中没有,则创建;如果有,则判断相容性。
- 本地锁表的key是tag(包括锁对象类型)+mode(锁级别),不论下面申请什么锁,都会存一份本地锁(包括fastpath,fastpath没有Lock结构,本地锁的lock记录为空)
加锁过程需要注意的是:
- 【本地】先查本地锁表,注意本地锁表的TAG是LOCKTAG+锁级别。
- 【fastpath查询】如满足加锁对象是表 && 【弱锁】 && fastpath坑位没满(PGPROC中可记录16个表OID)可以走fastpath。去遍历MYPROC的数组的16个位置,查到了或上当前申请的锁级别就返回(同时记录到本地锁表)。
- 【fastpath维护】【共享内存强锁数组】如满足加锁对象是表 && 【强锁】,需要维护fastpath,把强锁的tag的hashcode%1024找到fastpath共享内存数组位置,对位置++表示有强锁申请过这个对象了,后面即使在来弱锁上一步也过不了了。
- 【fastpath维护】【fastpath锁换到主锁表】如果有强锁申请了,需要把所有PGPROC的记录的16个fastpath OID全部查一遍,如果有,需要把fastpath弱锁清理,在主锁表中重建弱锁,便于死锁检测、锁冲突检查。
- 【主锁表进程锁表查找/新建】用tag去主锁表中查询,如果没有新建,如果有返回LOCK结构。
- 【冲突检查】【等锁队列检查】检查要加的锁级别,和当前锁的等待队列中的锁级别是否有冲突,如果冲突需要等待。
- 【冲突检查】【锁相容性检查】检查申请的级别和锁本身已经GRANT的级别是否相容。
- 【等待】如果冲突,开始等待。
关于第六点举例,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);
关键数据结构:
- 数据结构的设计主要还是为了便于查询,本来只需要一个main lock table即可(用【LOCKTAG】查【LOCK】结构)。但是因为有锁队列的存在,有时需要知道哪些会话在等锁、某个锁有哪些会话在等这样一些问题,所以增加了PROCLOCK结构,用来维护锁和后台进程的关系。
- 使用进程锁表LockMethodProcLockHash可以做到用【LOCK+PGPROC】查询【PROCLOCK】结构,维持锁和会话的关系。
logtype
locktag_lockmethodid
目前只有两套,2是专门给咨询锁使用的,其他都是1。
/* These identify the known lock methods */
#define DEFAULT_LOCKMETHOD 1
#define USER_LOCKMETHOD 2
PROCLOCK的procLinks的作用
- 当前进程的PGPROC中有一个16个链表头组成的锁链数组,每一个位置上是一个PROCLOCK链表头。
- 数组16个位置对应主锁表的16个分组,遍历全部主锁表
等价与
遍历这个数组上每个位置的链表。
2.2 fastpath强锁表FastPathStrongRelationLocks(共享内存)
数据库最常发生的增删改查正常都需要去主锁表中申请常规锁,但是DML操作其实只需要弱锁,且弱锁之间是相容的;所以PG在1-3级锁上做了一层优化:如果事务对某个对象申请弱锁,且对象上没有别人申请的强锁,则可以在会话本地记录弱锁,不走主锁表,不写共享内存。
走fastpath必须满足三个条件:
- 必须是表锁(已知)
- 必须是弱锁(已知)
- 申请对象上没有别人加强锁(需要查询)
对象上有没有别人申请过强锁这个信息,记录到下面共享内存结构中的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);
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 */...
}
关于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;
初始化
InitLocks...LockMethodLocalHash = hash_create("LOCALLOCK hash",16,&info,HASH_ELEM | HASH_BLOBS);
3 加锁流程分析
API
LockAcquireResult
LockAcquire(const LOCKTAG *locktag, :锁tagLOCKMODE lockmode, :申请锁模式bool sessionLock, :会话锁 或 事务锁bool dontWait) :是否等待
{return LockAcquireExtended(locktag, lockmode, sessionLock, dontWait,true, NULL);
}
LockAcquireExtended
LockAcquireResult
LockAcquireExtended(const LOCKTAG *locktag,LOCKMODE lockmode,bool sessionLock,bool dontWait,bool reportMemoryError,LOCALLOCK **locallockp)
{...
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){
没查到构造一条放入本地锁表
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{
查到了,如果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;
如果locallock->nLocks,说明这个锁之前已经拿过了,就不用再走下面逻辑了。
由GrantLockLocal负责更新本地锁的内容。
1:locallock->nLocks++。
2:更新ResourceOwner,增加锁计数。
if (locallock->nLocks > 0){GrantLockLocal(locallock, owner);if (locallock->lockCleared)return LOCKACQUIRE_ALREADY_CLEAR;elsereturn LOCKACQUIRE_ALREADY_HELD;}
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){
用tag算的hashcode在%1024,落到1024数组FastPathStrongRelationLocks->count
的某个位置上。
uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);bool acquired;LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
查共享内存fastpath锁表FastPathStrongRelationLocks,检查是否有强锁。
if (FastPathStrongRelationLocks->count[fasthashcode] != 0)acquired = false;else
没发现强锁,可以用fastpath加锁,下面要判断是否有位置能加fastpath锁。
(注意这里只把locktag_field2传进去了,表锁的tag组成是:
[dbid, redid](SET_LOCKTAG_RELATION)
,所以这里只把表的OID传进去了)
FastPathGrantRelationLock函数:
- 使用PGPROC的数组
Oid fpRelId[16];
来保存OID。 - 使用PGPROC的变量
uint64 fpLockBits
当做位图来记录锁级别。
FastPathGrantRelationLock逻辑:
- 3个bit一组按顺序查位图是不是空的,是空的就记录下来位置,不是空的就看下oid里面记的是不是需要的,如果正好Oid也是需要的,把当前请求锁模式或进去就可以返回了。
- 如果查了一遍位图,所有Oid都不是需要的,那就找一个空的位置,把锁级别记录到位图,OID记录到数组,然后返回。
- 如果查了一遍位图,没有一个空余位置,就返回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;
}
回到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;}}
3.3 外部请求强锁;标记fastpath共享内存冲突表;检查fastpath,如果存在需要换出到主锁表
ConflictsWithRelationFastPath检查:是强锁!
- 非咨询锁
locktag_lockmethodid == DEFAULT_LOCKMETHOD
- 表锁
locktag_type == LOCKTAG_RELATION
- 锁等级大于4级
(mode) > ShareUpdateExclusiveLock
if (ConflictsWithRelationFastPath(locktag, lockmode)){uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
开始处理强锁: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)){// 加锁失败处理,共享内存不足。}}
3.4 插入/查找主锁表
本地锁表没有、fastpath不能用(满了或者是强锁),到这里开始操作主锁表。
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;
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
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;
}
Postgresql源码(69)常规锁细节分析相关推荐
- PostgreSQL源码分析
PostgreSQL源码结构 PostgreSQL的使用形态 PostgreSQL采用C/S(客户机/服务器)模式结构.应用层通过INET或者Unix Socket利用既定的协议与数据库服务器进行通信 ...
- postgreSQL源码分析——索引的建立与使用——Hash索引(3)
2021SC@SDUSC 上一篇博客讲了关于Hash索引创建与插入的相关函数,这一篇博客讲述关于溢出页的操作函数以及Hash表的扩展相关的函数. 目录 溢出页的分配和回收 _hash_addovflp ...
- postgreSQL源码分析——索引的建立与使用——各种索引类型的管理和操作(2)
2021SC@SDUSC 目录 上层操作函数 index_open index_beginscan() index_create() indexcmd.c 下层接口函数 IndexScanDescDa ...
- Postgresql源码(82)SPI模块拆解分析一:执行简单SQL获取结果
相关 <Postgresql源码(76)执行器专用元组格式TupleTableSlot> <Postgresql源码(82)SPI模块拆解分析一:执行简单SQL获取结果> &l ...
- Postgresql源码(85)查询执行——表达式解析器分析(select 1+1如何执行)
相关 <Postgresql源码(61)查询执行--最外层Portal模块> <Postgresql源码(62)查询执行--子模块ProcessUtility> <Pos ...
- java ipc pgsql_[转]PostgreSQL源码结构
PostgreSQL采用C/S(客户机/服务器)模式结构.应用层通过INET或者Unix Socket利用既定的协议与数据库服务器进行通信. 另外,还有一种'Standalone Backend'使用 ...
- Postgresql源码(34)Btree索引读——_bt_first搜索部分分析
阅读顺序 <Postgresql源码(30)Postgresql索引基础B-linked-tree> <Postgresql源码(31)Btree索引相关系统表和整体结构> & ...
- postgresql源码学习(52)—— vacuum①-准备工作与主要流程
关于vacuum的基础知识,参考,本篇从源码层继续学习 https://blog.csdn.net/Hehuyi_In/article/details/102992065 https://blog.c ...
- PostgreSQL源码结构
PostgreSQL的使用形态 PostgreSQL采用C/S(客户机/服务器)模式结构.应用层通过INET或者Unix Socket利用既定的协议与数据库服务器进行通信. 另外,还有一种'Stand ...
最新文章
- 用Python分析了1w场吃鸡数据,原来吃鸡要这么玩!
- WICC上了全球很多家交易所,它是一种资产和流动性的代表
- 如何使用Android studio创建签名
- python socket能做什么_用python写一个聊天小程序!和女朋友的专属聊天工具!
- 在建工程的管理是怎样在总帐中实现
- web----DNS解析
- [网络安全自学篇] 二十九.文件上传和IIS6.0解析问题及防御原理(二)
- 十余位权威专家深度解读,达摩院2019十大科技趋势点燃科技热情
- 测试范围不统一,引发的冲突问题
- mit oracle 350平衡线,MIT MI-350 CVT 平衡线_已售器材欣赏区_信谊音响_二手音响_音响发烧站_高级音响 - 音响贵族网...
- 更好地使用 ViewController
- yolov5论文叫什么_熬夜写论文是一种怎样的体验
- [转载红鱼儿]kbmmw 开发点滴:kbmMW 命名查询(Named Query)
- 智能驾驶的深度神经网络模型嵌入式部署的线路思考
- 微信小程序点餐页面实现完整版
- CTF—古典密码(凯撒密码、维吉尼亚密码、培根密码等)
- Android 最常用的设计模式二 安卓源码分析——组合模式(component)
- 从浏览器中获取headers
- python压缩_Python札记 -- 文件压缩
- 干货来了,JAVA代码实现图片分割、合并工具类