一、 简介
我们平时说的表锁、页锁、咨询锁等等(行锁除外),实际上都是常规锁根据不同锁定对象划分的子类。
有关常规锁的类型、等级、兼容性、查看方法等,属于DBA的日常运维知识,可以参考 PG锁和阻塞发现与处理_Hehuyi_In的博客-CSDN博客_pg查看阻塞 这里不重复列了。
然后介绍一个重要的结构体LockMethodData,它会在后面函数中反复出现
locks.h文件
//该结构体定义了与"lock method"关联的锁的语义
typedef struct LockMethodData
{
int numLockModes; // number of lock modes (READ,WRITE,etc) that
are defined in this lock method. Must be less than MAX_LOCKMODES. 锁的模式数量
const LOCKMASK *conflictTab; // 标志冲突的(二维)数组,mode i和mode j冲突则conflictTab [i]的第j位 = 1,conflictTab[0]未使用
const char *const *lockModeNames; // ID strings for debug printouts.
const bool *trace_flag; // pointer to GUC trace flag for this lock method.
} LockMethodData;
typedef const LockMethodData *LockMethod;
//Lock methods的id,由于LOCKTAG对应字段为8位,所以最多指定256个
typedef uint16 LOCKMETHODID;
//两个预定义的lock methods
#define DEFAULT_LOCKMETHOD 1
#define USER_LOCKMETHOD 2
lockdefs.h文件
这里可以看到LOCKMODE是当整型用的,而LOCKMASK是当位图用的,所以前面的const LOCKMASK *conflictTab,这个冲突数组其实相当于是个二维数组。
/*
* LOCKMODE is an integer (1..N) indicating a lock type. LOCKMASK is a bit
* mask indicating a set of held or requested lock types (the bit 1<<mode
* corresponds to a particular lock mode).
*/
typedef int LOCKMASK;
typedef int LOCKMODE;
二、 常规锁的加锁对象
如果要对一个表进行操作,通常会通过heap_open打开这个表,并在打开时指定需要的锁模式。之后会有一系列函数将锁模式传递下去,最终通过LockRelationOid函数将表的Oid和lockmode联系在一起。
# define heap_open(r,l)
Relation table_open(Oid relationId, LOCKMODE lockmode)
Relation relation_open(Oid relationId, LOCKMODE lockmode)
void LockRelationOid(Oid relid, LOCKMODE lockmode)
当然,常规锁不仅可以对表加锁,也可以对各类对象加锁。LOCKTAG结构体的成员变量没有特定的含义,完全取决于具体类型。
typedef struct LOCKTAG
{
uint32 locktag_field1; /* a 32-bit ID field */
uint32 locktag_field2; /* a 32-bit ID field */
uint32 locktag_field3; /* a 32-bit ID field */
uint16 locktag_field4; /* a 16-bit ID field */
uint8 locktag_type; /* see enum LockTagType */
uint8 locktag_lockmethodid; /* lockmethod indicator */
} LOCKTAG;
LOCKTAG具体类型如下
typedef enum LockTagType
{
LOCKTAG_RELATION, /* 表锁*/
LOCKTAG_RELATION_EXTEND, /* 对表进行extend时加锁 */
LOCKTAG_DATABASE_FROZEN_IDS, /* 更新 pg_database.datfrozenxid 时加锁 */
LOCKTAG_PAGE, /* 页锁 */
LOCKTAG_TUPLE, /* 行锁 */
LOCKTAG_TRANSACTION, /* 事务锁,在分配事务id时对这个事务id加锁,由于行并发更新时做事务等待 */
LOCKTAG_VIRTUALTRANSACTION, /* 虚拟事务id */
LOCKTAG_SPECULATIVE_TOKEN, /* upsert语句中需要等待confirm的行加锁 */
LOCKTAG_OBJECT, /* 对非表对象类型加锁 */
LOCKTAG_USERLOCK, /* 用户自定义的锁 */
LOCKTAG_ADVISORY /* 咨询锁,目前可以跨事务存在 */
} LockTagType;
如果加锁对象是一个表,就可以通过SET_LOCKTAG_RELATION生成对应LOCKTAG(这部分代码就紧跟在LOCKTAG结构体定义后面):
#define SET_LOCKTAG_RELATION(locktag,dboid,reloid) \
((locktag).locktag_field1 = (dboid), \
(locktag).locktag_field2 = (reloid), \
(locktag).locktag_field3 = 0, \
(locktag).locktag_field4 = 0, \
(locktag).locktag_type = LOCKTAG_RELATION, \
(locktag).locktag_lockmethodid = DEFAULT_LOCKMETHOD)
三、 常规锁加锁步骤
在pg中,锁获取最重要的函数是LockAcquire(核心是调用LockAcquireExtended),这是非常高频的操作,性能相当重要。因此在LockAcquire中,对封锁的性能做了大量优化。
常规锁主要保存在以下4个位置:
- 本地锁表(LOCALLOCK结构体):对于重复申请的锁进行计数,避免频繁访问主锁表和进程锁表,相当于一层缓存
- 快速路径(fast path):对弱锁的访问保存到本进程,避免频繁访问主锁表和进程锁表
- 主锁表(LOCK结构体):保存一个锁对象所有相关信息
- 进程锁表(PROCLOCK结构体):保存一个锁对象中与当前会话(进程)相关的信息
本篇我们先只看第一项,本地锁表。
四、 本地锁表
1. 结构体定义
每个会话都保存了一个本地锁表 —— 当事务重复在同一个对象上申请同类型锁时,无须做冲突检测,只要将这个锁记录在本地即可,避免频繁访问主锁表和进程锁表。
typedef struct LOCALLOCK
{
/* tag */
LOCALLOCKTAG tag; /* 本地锁唯一id:LOCKTAG + LOCKMODE */
/* data */
uint32 hashcode; /* LOCKTAG的hash值 */
LOCK *lock; /* 关联主锁表对象 */
PROCLOCK *proclock; /*关联进程锁表对象*/
int64 nLocks; /* 本地持有该锁的次数(类似计数器) */
int numLockOwners; /* 相关的 ResourceOwners 数量,lockOwners数组的实际大小 */
int maxLockOwners; /* ResourceOwners数组的最大长度 */
LOCALLOCKOWNER *lockOwners; /* dynamically resizable array,长度可以动态变化的数组 */
bool holdsStrongLockCount; /* bumped FastPathStrongRelationLocks,fast path需要的标记,是否持有了强锁 */
bool lockCleared; /* we read all sinval msgs for lock,防止递归处理失效消息 */
} LOCALLOCK;
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;
本地锁表的名字是LockMethodLocalHash,这是个hash表,以下代码在lock.c
/*
* Pointers to hash tables containing lock state
*
* The LockMethodLockHash and LockMethodProcLockHash hash tables are in
* shared memory; LockMethodLocalHash is local to each backend.
*/
static HTAB *LockMethodLockHash;
static HTAB *LockMethodProcLockHash;
static HTAB *LockMethodLocalHash;
2. LockAcquireExtended函数
本地锁表的判断和获取在LockAcquireExtended函数,下面通过gdb调试跟踪一下执行情况。
模拟方法:LockAcquireExtended打断点,跟踪事务第一次select,和第二次select。
- gdb测试
会话1
会话2
- 具体代码与跟踪(第一次执行)
调用栈如下
LockAcquireResult
LockAcquireExtended(const LOCKTAG *locktag,
LOCKMODE lockmode,
bool sessionLock, // if true, acquire lock for session not current transaction。如果为true,为会话而不是当前事务获得锁
bool dontWait, // if true, don't wait to acquire lock。如果为true,不需要等待获得锁
bool reportMemoryError,// specifies whether a lock request that fills the lock table should generate an ERROR or not。填充lock table的锁请求是否需要生成一个错误信息
LOCALLOCK **locallockp) //若locallockp非空,*locallockp为指向LOCALLOCK的指针,否则为空
{
LOCKMETHODID lockmethodid = locktag->locktag_lockmethodid;
LockMethod lockMethodTable;
LOCALLOCKTAG localtag;
LOCALLOCK *locallock;
LOCK *lock;
PROCLOCK *proclock;
bool found;
ResourceOwner owner;
uint32 hashcode;
LWLock *partitionLock;
bool found_conflict;
bool log_lock = false;
// lock method判断,必须>0且< LockMethods数组长度
if (lockmethodid <= 0 || lockmethodid >= lengthof(LockMethods))
elog(ERROR, "unrecognized lock method: %d", lockmethodid);
lockMethodTable = LockMethods[lockmethodid];
// lock mode判断,必须>0且<8,因为一共就8级锁
if (lockmode <= 0 || lockmode > lockMethodTable->numLockModes)
elog(ERROR, "unrecognized lock mode: %d", lockmode);
//是否为恢复模式,恢复模式不能获取RowExclusiveLock以上的锁
if (RecoveryInProgress() && !InRecovery &&
(locktag->locktag_type == LOCKTAG_OBJECT ||
locktag->locktag_type == LOCKTAG_RELATION) &&
lockmode > RowExclusiveLock)
ereport(ERROR,
(errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE),
errmsg("cannot acquire lock mode %s on database objects while recovery is in progress",
lockMethodTable->lockModeNames[lockmode]),
errhint("Only RowExclusiveLock or less can be acquired on database objects during recovery.")));
#ifdef LOCK_DEBUG
if (LOCK_DEBUG_ENABLED(locktag))
elog(LOG, "LockAcquire: lock [%u,%u] %s",
locktag->locktag_field1, locktag->locktag_field2,
lockMethodTable->lockModeNames[lockmode]);
#endif
/* Identify owner for lock,函数入参之一 */
if (sessionLock)
owner = NULL;
else
owner = CurrentResourceOwner;
从下面开始正式是本地表锁的判断和获取。本地锁表的名字是LockMethodLocalHash,这是个hash表,其查询标签是 locktag+lockmode。
/*
* Find or create a LOCALLOCK entry for this lock and lockmode
*/
MemSet(&localtag, 0, sizeof(localtag)); /* must clear padding */
localtag.lock = *locktag;
localtag.mode = lockmode;
locallock = (LOCALLOCK *) hash_search(LockMethodLocalHash, //这就是前面提到的hash表
(void *) &localtag,
HASH_ENTER, &found);
下面是一个判断,如果没有从hash_search检查到锁,说明这是本事务第一次申请该锁。此时需要初始化locallock对象,例如在locallock->nLocks中填充值是0,证明还没有持有这个锁。
/*
* if it's a new locallock object, initialize it
*/
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
{
/* Make sure there will be room to remember the lock,如果检查到了锁,需要判断是否还有位置存放该锁。如果numLockOwners>= maxLockOwners了,则需要提高maxLockOwners上限值,并重新lockOwners数组调整大小 */
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;
if (locallockp) //是否设置了locallockp参数
*locallockp = locallock;
if (locallock->nLocks > 0) //第一次因为没有检查到锁,locallock->nLocks=0,不会执行到这步
{
GrantLockLocal(locallock, owner);
if (locallock->lockCleared)
return LOCKACQUIRE_ALREADY_CLEAR;
else
return LOCKACQUIRE_ALREADY_HELD;
}
/*
再下面就是与fast path、进程锁相关,我们后面再看
*/
if (EligibleForRelationFastPath(locktag, lockmode) &&
FastPathLocalUseCount < FP_LOCK_SLOTS_PER_BACKEND)
{
…
}
- 具体代码与跟踪(第二次执行)
会话1
会话2
前面跟开始差不多
注意这次 if (!found) 为假,说明本地找到了对应锁,进到了else部分。
如果检查到了对应锁(locallock->nLocks一定大于0),说明这个锁对象和模式已经授予了本事务,给本地锁表对应锁增加引用计数即可,这个工作由GrantLockLocal函数完成。
if (locallock->nLocks > 0) //并且nLocks=1,会进到GrantLockLocal函数
{
GrantLockLocal(locallock, owner);
if (locallock->lockCleared)
return LOCKACQUIRE_ALREADY_CLEAR;
else
return LOCKACQUIRE_ALREADY_HELD;
}
3. GrantLockLocal函数
负责给本地锁表对应锁增加引用计数,一个简单的小函数。GrantLockLocal函数首先增加locallock->nLocks计数,然后增加ResourceOwner中锁的计数,这次申请锁的任务就完成了。
再加一个gdb断点,重新update一下
函数调用栈如下
/*
* GrantLockLocal -- update the locallock data structures to show
* the lock request has been granted.
*
* We expect that LockAcquire made sure there is room to add a new
* ResourceOwner entry.
*/
static void
GrantLockLocal(LOCALLOCK *locallock, ResourceOwner owner)
{
LOCALLOCKOWNER *lockOwners = locallock->lockOwners;
int i;
Assert(locallock->numLockOwners < locallock->maxLockOwners);
/* Count the total */
locallock->nLocks++;
/* Count the per-owner lock,增加对应owner持锁数 */
for (i = 0; i < locallock->numLockOwners; i++)
{
if (lockOwners[i].owner == owner)
{
lockOwners[i].nLocks++;
return; //如果匹配到,然后return,函数就结束了
}
}
// 如果循环完还是没匹配到,例如第一次的时候(即第二次申请相同对象锁的时候),需要初始化lockOwners数组值
lockOwners[i].owner = owner;
lockOwners[i].nLocks = 1;
locallock->numLockOwners++;
if (owner != NULL)
ResourceOwnerRememberLock(owner, locallock);
/* Indicate that the lock is acquired for certain types of locks. */
CheckAndSetLockHeld(locallock, true);
}
gdb测试时由于已经是第三次申请该锁,if中的owner已经能匹配到,因此增加计数后就直接return了。
参考
《PostgreSQL技术内幕:事务处理深度探索》第2章
《PostgreSQL数据库内核分析》第7章