PostgreSQL 随读笔记-事务上

PostgreSQL 随读笔记-事务


7. 事务处理与并发控制

7.1 事务系统简介

  • 事务管理器
    是事务系统的中枢,本事是一个FSM。通过接受外部系统的命令或者信号,再根据当前事务所处的状态决定事务的下一步执行过程。
  • 锁管理器
    读阶段采用了MVCC。保证读写互不阻塞,写阶段需要由各种锁保证事务隔离级别。
  • 日志
    事务提交日志CLOG(记录执行结果和状态)和事务日志XLOG(保持了一定的冗余数据)

PostgreSQL在接到语句后,会fork出一个子进程postgres处理。首先会从总控进入事务块环境中,如果没有一个事务信息,调用底层事务处理函数启动一个事务,返回上层。Begin、RollBack、end会改变事务块状态。

7.2 事务系统上层

事务块实现数据库内部底层事务和终端命令的交互。一般来说,大家所认为的事务对应着数据库理论中的命令概念,反应一个SQL(命令)的执行状态。实际上“事务块”才对应着数据库理论中的“事务”。一个事务块包含着多个事务,所以事务块状态数量要比底层事务的状态数量多得多。

  • pg中事务块:DB理论中的事务
  • pg中事务:事务块中sql语句

执行一条SQL语句前会调用:StratTransactionCommand。执行结束调用CommitTransactionCommand,如果命令执行失败,调用AbortCurrentTransaction。这三个函数根据事务块的状态,执行不同的操作,调用不同的底层事务执行函数。

7.2.1 事务块状态

默认情况下,PostgreSQL中一个事务处理一条SQL语句,事务块使得一个事务能够处理多条SQL语句。每一个Postgres进程中,至始至终只存在一个事务块,当用户开始一个新的事务时,并不进入新的事务块,而是修改事务块的状态使之重新开始记录用户“事务”的状态。

事务块状态是指整个事务系统的状态,反应当前用户输入命令的变化过程和事务底层执行状态的变化。Begin、RollBack、end会改变事务块状态,底层事务提交或者终止也会改变该状态。事务块状态:TBlockState

7.2.2 事务块操作

任何语句通过事务块入口函数进入并执行,执行完毕后,通过事务块出口函数退出。

  1. 事务块基本操作
    3个:
  • StartTransactionCommand
    每条语句执行前,都需要执行该函数,通过判断当前事务块状态进入不同的底层事务执行函数。例如当前事务块状态为TBLOCK_DEFAULT,调用当前函数时,启动一个事务设置事务块状态为TBLOCK_STARTED。
  • CommitTransactionCommand
    每条语句执行后,都需要执行该函数,通过判断当前事务块状态进入不同的底层事务执行函数。例如当前TBLOCK_DEFAULT,调用该函数时说明此事系统没有任何事务信息,由于每个语句都是在事务中执行的,所以调用当前函数处于一个非法状态,打印错误信息并返回。
  • AbortCurrentTransaction
    系统遇到错误时,会返回调用点执行该函数。假设【事务块】当前TBLOCK_DEFAULT,系统执行遇到错误,返回到sigsetjmp函数,然后调用该函数进行出错处理,如果底层事务的状态是TRANS_DEFAULT,说明系统中没有任何事务信息,不需要进行事务出错处理,直接返回;如果当前底层事务状态为TRANS_START,则退出事务块需要调用AbortTransaction,再CleanupTransaction。
  1. 事务块状态改变
  • BeginTransactionBlock (Begin语句执行)
    执行begin命令,会进入事务块,此时调用该函数,将事务块状态从 TBLOCK_STARTED 到 TBLOCK_BEGIN,如果遇到其他状态都是不合理的。
    PostgreSQL 随读笔记-事务上_第1张图片

  • EndTransactionBlock (End语句执行/commit)
    接受到end命令,调用该函数,事务块结束。可能由commit导改致,也可能是rollback,根据返回值来确定系统动作。
    该函数调用:判断当前事务块状态。如果事务块当前状态TBLOCK_INPROGRESS则把当前事务块状态改为TBLOCK_END,返回值设为TRUE;如果当前事务失败,则事务块状态为TBLOCK_ABORT,则需要退出该事务块,事务块状态设置为TBLOCK_ABORT_END,返回false。

PostgreSQL 随读笔记-事务上_第2张图片

  • UserAbortTransactionBlock (Rollback语句执行)
    执行用户提交的一个rollback命令会进入该函数。
    PostgreSQL 随读笔记-事务上_第3张图片
    其他错误场景:

chazhangtu

总结:事务块状态

事务块状态 标志
默认 TBLOCK_DEFAULT
已开始 TBLOCK_STARTED
事务块开启 TBLOCK_BEGIN
事务块运行中 TBLOCK_INPROGRESS
事务块结束 TBLOCK_END
回滚 TBLOCK_ABORT
回滚结束 TBLOCK_ABORT_END
回滚等待 TBLOCK_ABORT_PENDING

在无异常情形下,一个事务块的状态按照默认(TBLOCK_DEFAULT)->已开始(TBLOCK_STARTED)->事务块开启(TBLOCK_BEGIN)->事务块运行中(TBLOCK_INPROGRESS)->事务块结束(TBLOCK_END)->默认(TBLOCK_DEFAULT)循环。剩余的状态机是在上述正常场景下的各个状态点的异常处理分支。

(1) 在进入事务块运行中(TBLOCK_INPROGRESS)之前出错,因为事务还没有开启,直接报错并回滚,清理资源回到默认(TBLOCK_DEFAULT)状态。

(2) 在事务块运行中(TBLOCK_INPROGRESS)出错分为2种情形。事务执行失败:事务块运行中(TBLOCK_INPROGRESS)->回滚(TBLOCK_ABORT)->回滚结束(TBLOCK_ABORT_END)->默认(TBLOCK_DEFAULT);用户手动回滚执行成功的事务:事务块运行中(TBLOCK_INPROGRESS)->回滚等待(TBLOCK_ABORT_PENDING)->默认(TBLOCK_DEFAULT)。

(3) 在用户执行COMMIT语句时出错:事务块结束(TBLOCK_END)->默认(TBLOCK_DEFAULT)。由图2可以看出,事务开始后离开默认(TBLOCK_DEFAULT)状态,事务完全结束后回到默认(TBLOCK_DEFAULT)状态。

(4) 隐式事务块,当客户端执行单条SQL语句时可以自动提交,其状态机相对比较简单:按照默认(TBLOCK_DEFAULT)->已开始(TBLOCK_STARTED)->默认(TBLOCK_DEFAULT)循环。

7.3 事务系统底层

底层事务的状态:也就是事务块中一条sql语句真正的状态:

7.3.1 事务状态

/*
 *	transaction states - transaction state from server perspective
 */
typedef enum TransState
{
	TRANS_DEFAULT,				/* idle */
	TRANS_START,				/* transaction starting */
	TRANS_INPROGRESS,			/* inside a valid transaction */
	TRANS_COMMIT,				/* commit in progress */
	TRANS_ABORT,				/* abort in progress */
	TRANS_PREPARE				/* prepare in progress */
} TransState;

由于子事务的引入,一个事务中可能会有多个层级的子事务。pg使用一个事务栈来保存每个层级子事务的状态,这个事务栈的结构体是 TransactionStateData:

typedef struct TransactionStateData
{
	FullTransactionId fullTransactionId;	/* my FullTransactionId */
	SubTransactionId subTransactionId;	/* my subxact ID */
	char	   *name;			/* savepoint name, if any */
	int			savepointLevel; /* savepoint level */
	TransState	state;			/* low-level state */
	TBlockState blockState;		/* high-level state 事务块状态 */
	int			nestingLevel;	/* transaction nesting depth */
	int			gucNestLevel;	/* GUC context nesting depth */
	MemoryContext curTransactionContext;	/* my xact-lifetime context */
	ResourceOwner curTransactionOwner;	/* my query resources */
	TransactionId *childXids;	/* subcommitted child XIDs, in XID order */
	int			nChildXids;		/* # of subcommitted child XIDs */
	int			maxChildXids;	/* allocated size of childXids[] */
	Oid			prevUser;		/* 记录前一个 CurrentUserId(用户名) */
	int			prevSecContext; /* previous SecurityRestrictionContext */
	bool		prevXactReadOnly;	/* entry-time xact r/o state */
	bool		startedInRecovery;	/* did we start in recovery? */
	bool		didLogXid;		/* has xid been included in WAL record? */
	int			parallelModeLevel;	/* Enter/ExitParallelMode counter */
	bool		chain;			/* start a new block after this one */
	bool		assigned;		/* assigned to top-level XID */
	struct TransactionStateData *parent;	/* back link to parent */
} TransactionStateData;
  • TBlockState blockState:就是这里介绍的上层(事务块状态层),本质就是一个状态机,不做实质性的操作;
  • TransState state:事务真正状态;
  • nestingLevel: 当前事务嵌套级别,顶层为1,开启子事务PushTransaction,子事务的nestingLevel就在父基础上+1;
  • curTransactionOwner:指针,记录当前事务占有资源
  • 其他:略

7.3.2 事务操作函数

事务实际操作函数由StartTransaction、CommitTransaction、AbortTransaction、CleanupTransaction等函数。

1. 启动事务(StartTransaction)

调用该函数一定是顶层事务,子事务启动会调用StartSubTransaction
PostgreSQL 随读笔记-事务上_第4张图片

1)将全局变量CurrentTransactionState指向TopTransactionStateData(一个初始化好的空间)设置底层事务状态state = TRANS_START;

2)重置事务状态变量:

	s->nestingLevel = 1;
	s->gucNestLevel = 1;
	s->childXids = NULL;
	s->nChildXids = 0;
	s->maxChildXids = 0;
	GetUserIdAndSecContext(&s->prevUser, &s->prevSecContext); // 继承context
	初始化事务内计数器等
	...

3)分配内存和资源跟踪器初始化:AtStart_Memory 和 AtStart_ResourceOwner。前者CurrentTransactionState->curTransactionContext指向内存管理器新分配的内存,后者初始化CurrentTransactionState->curTransactionOwner,里面包括打开的锁、snapshot的引用等。

4)产生事务标识XID给当前事务。

通常只读事务不会申请事务ID,只有涉及写操作时才会分配事务ID。事务会在执行第一个含有写操作的语句时分配事务ID。不过,即使没有事务id,事务也会用一个虚拟事务id来代表自己。虚拟事务id由两部分组成:backendId(后台进程id,会话独有)+ localTransactionId(进程维护的本地事务id,也就是说vxid是在本进程中递增,而不是全局进程递增),以下结构体代码在 lock.h:

typedef struct
{
	BackendId	backendId;		/* backendId from PGPROC */
	LocalTransactionId localTransactionId;	/* lxid from PGPROC */
} VirtualTransactionId;

backendId是通过MyBackendId继承(与select pg_backend_pid()一一对应);localTransactionId通过GetNextLocalTransactionId进行分配,就是一个全局变量一直+++。

MyBackendId在初始化Postgres进程被赋值:
PostgreSQL 随读笔记-事务上_第5张图片
创建完成之后把localTransactionId插入当前进程队列PGPROC中:

void
VirtualXactLockTableInsert(VirtualTransactionId vxid)
{
	Assert(VirtualTransactionIdIsValid(vxid));

	LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);

	Assert(MyProc->backendId == vxid.backendId);
	Assert(MyProc->fpLocalTransactionId == InvalidLocalTransactionId);
	Assert(MyProc->fpVXIDLock == false);

	MyProc->fpVXIDLock = true;
	MyProc->fpLocalTransactionId = vxid.localTransactionId;

	LWLockRelease(&MyProc->fpInfoLock);
}

在这里插入图片描述

5)设置时间戳
设置事务开始时间=命令开始时间,并初始化事务结束时间=0

6)初始化GUC,cache等结构

7)把事务状态设置为TRANS_INPROGRESS

2. 提交事务(CommitTransaction)

1)检查事务状态:确保事务状态为TRANS_INPROGRESS
2)触发所有延迟触发器:SQL中after触发器
3)关闭大对象AtEOXact_LargeObject
4)如果改了pg_database、pg_authid、pg_auth_members,则执行更新操作通知Postmaster进程
5)提交事务,TRANS_COMMIT,执行RecordTransactionCommit,将日志写会磁盘,返回最后处理完的当前事务或者子事务的xid
6)清理,略

3. 退出事务(AbortTransaction)

主要是系统遇到错误时调用,关中断,释放所有资源设置TRANS_ABORT等

4. 清理事务

7.4 事务保存点和子事务

savepoint,一种机制,用于回滚部分事务。必要的时候可以rollback transaction to语句回滚到该保存点。

具体来说在执行definesavepoint这个函数调用PushTransaction:

PostgreSQL 随读笔记-事务上_第6张图片

保存点涉及到的函数

xact.c:
static void PushTransaction(void); // 为子事务创建一个TransactionState并压入事务状态堆栈中,currentTransactionState切换到新创建的事务状态
static void PopTransaction(void); //出栈

这里PushTransaction会把block的状态改成 TBLOCK_SUBBEGIN;

子事务涉及函数:

static void StartSubTransaction(void);
static void CommitSubTransaction(void);
static void AbortSubTransaction(void);
static void CleanupSubTransaction(void);

在执行savepoint语句时(CommitTransactionCommand)时会调用StartSubTransaction设置blockstate为TBLOCK_SUBINPROGRESS,TBLOCK_SUBBEGIN这里在pushTransaction的时候设置的。

	/*
	 * We were just issued a SAVEPOINT inside a transaction block.
	 * Start a subtransaction.  (DefineSavepoint already did
	 * PushTransaction, so as to have someplace to put the SUBBEGIN
	 * state.)
	 */
case TBLOCK_SUBBEGIN:
	StartSubTransaction();
	s->blockState = TBLOCK_SUBINPROGRESS;
	break;

通常上层事务在创建过程中可以得到一个XID,用于标志自己,但不对数据库写操作,就没必要单独获得事务或者子事务XID。只有GetCurrentTransactionId函数才分配XID。

根据CurrentTransactionState的vxid决定分配:
GetCurrentTransactionId->AssignTransactionId
注意:子任务分配XID时,会递归分配为父亲分配xid,这样保证子事务的xid大于parent:

AssignTransactionId:
/*
	 * Ensure parent(s) have XIDs, so that a child always has an XID later
	 * than its parent.  Mustn't recurse here, or we might get a stack
	 * overflow if we're at the bottom of a huge stack of subtransactions none
	 * of which have XIDs yet.
	 */
	if (isSubXact && !FullTransactionIdIsValid(s->parent->fullTransactionId))
	{
		TransactionState p = s->parent;
		TransactionState *parents;
		size_t		parentOffset = 0;

		parents = palloc(sizeof(TransactionState) * s->nestingLevel);
		while (p != NULL && !FullTransactionIdIsValid(p->fullTransactionId))
		{
			parents[parentOffset++] = p;
			p = p->parent;
		}

		/*
		 * This is technically a recursive call, but the recursion will never
		 * be more than one layer deep.
		 */
		while (parentOffset != 0)
			AssignTransactionId(parents[--parentOffset]);

		pfree(parents);
	}

7.5 两阶段提交

pg只支持分布式数据库的两阶段提交协议,即提供相关操作接口,并没有实现整个协议。
暂不深入

7.6 PostgreSQL的并发控制

隔离级别,最小实体是元组,对元组需要实施访问控制,通过锁操作和MVCC操作。

7.7 PostgreSQL的三种锁

SpinLocl

s_lock.c

是一种和硬件结合的互斥锁,借用了硬件提供的原子操作的原语来对一些共享变量进行封锁,通常适用于临界区比较小的情况。特点是:封锁时间很短、无死锁检测机制和等待队列、事务结束时不会自动释放SpinLock。

自旋锁顾名思义就是一直原地旋转等待的锁。一个进程如果想要访问临界区,必须先获得锁资源,如果不能获得,就会一直自旋(进程不停止,一直在循环),直到获取到锁资源。

显然这种自旋会造成CPU浪费,但是通常它保护的临界区非常小,封锁时间很短,因此通常自旋比释放CPU资源带来的上下文切换消耗要小。

作为底层锁,利用其来实现LWlock轻量锁

LWLock

负责保护共享内存中的数据结构,有共享和排他两种模式,类似Oracle中的latch。特点是:封锁时间较短、无死锁检测机制、有等待队列、事务结束时会自动释放。

pg中的轻量锁类型定义在lwlocknames.h文件中(这个文件是在编译时由lwlocknames.txt生成的),在pg 14中,目前有48种轻量锁。

#define ShmemIndexLock (&MainLWLockArray[1].lock)
#define OidGenLock (&MainLWLockArray[2].lock)
#define XidGenLock (&MainLWLockArray[3].lock)
#define ProcArrayLock (&MainLWLockArray[4].lock)
#define SInvalReadLock (&MainLWLockArray[5].lock)
#define SInvalWriteLock (&MainLWLockArray[6].lock)
#define WALBufMappingLock (&MainLWLockArray[7].lock)
#define WALWriteLock (&MainLWLockArray[8].lock)
#define ControlFileLock (&MainLWLockArray[9].lock)

可以看出,各锁被保存在MainLWLockArray数组中(前48个值),每种LWLocks都有自己固定要保护的对象,使用方式如下:

LWLockAcquire(BtreeVacuumLock, LW_SHARED);
LWLockRelease(BtreeVacuumLock);

其定义如下:

typedef struct LWLock
{
	uint16		tranche;		/* tranche ID */
	pg_atomic_uint32 state;		/* state of exclusive/nonexclusive lockers */
	proclist_head waiters;		/* list of waiting PGPROCs */
#ifdef LOCK_DEBUG
	pg_atomic_uint32 nwaiters;	/* number of waiters */
	struct PGPROC *owner;		/* last exclusive owner of the lock */
#endif
} LWLock;

State变量有32位,其中低24位作为共享锁的计数器,因此一个轻量锁最多可以有2^24个共享锁持锁者。有1位作为排他锁的标记,因为同一时间最多只能有一个持锁者。

申请:
如果等待队列中第一个申请的是排他锁,则只有这一个申请者被唤醒,其他申请者继续等待。
如果等待队列中第一个申请的是共享锁,则所有共享锁申请者都被唤醒,其他排他锁申请者继续等待。

RegularLock

常规锁(Regular Lock):就是通常说的对数据库对象的锁。按照锁粒度,可以分为表锁、页锁、行锁等;按照等级,pg锁一共有8个等级。特点是:封锁时间可以很长、有死锁检测机制和等待队列、事务结束时会自动释放。

我们平时说的表锁、页锁、咨询锁等等(行锁除外),实际上都是常规锁根据不同锁定对象划分的子类。

1. Regularlock的锁方法

pg提供两种加regular锁的方法:

/*
 * map from lock method id to the lock table data structures
 */
static const LockMethod LockMethods[] = {
	NULL,
	&default_lockmethod,
	&user_lockmethod
};

这是个静态的,postmaster启动时作为,分配一块内存区作为regularlock方法表并初始化。其中 LockMethod 定义:

lock.c

typedef struct LockMethodData
{
	int			numLockModes;
	const LOCKMASK *conflictTab;
	const char *const *lockModeNames;
	const bool *trace_flag;
} LockMethodData;

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<

2. RegularLock支持的锁模式

这里是锁模式(Lockmode):

对应源码:
/*
 * These are the valid values of type LOCKMODE for all the standard lock
 * methods (both DEFAULT and USER).
 */
 
/* NoLock is not a lock mode, but a flag value meaning "don't get a lock" */
#define NoLock					0
 
#define AccessShareLock			1		/* SELECT */
#define RowShareLock			2		/* SELECT FOR UPDATE/FOR SHARE */
#define RowExclusiveLock		3		/* INSERT, UPDATE, DELETE */
#define ShareUpdateExclusiveLock 4		/* VACUUM (non-FULL),ANALYZE, CREATE INDEX CONCURRENTLY */
#define ShareLock				5		/* CREATE INDEX (WITHOUT CONCURRENTLY) */
#define ShareRowExclusiveLock	6		/* like EXCLUSIVE MODE, but allows ROW SHARE */
#define ExclusiveLock			7		/* blocks ROW SHARE/SELECT...FOR UPDATE */
#define AccessExclusiveLock		8		/* ALTER TABLE, DROP TABLE, VACUUM FULL, and unqualified LOCK TABLE */

带exclusive的锁表示在事务执行期间阻止其他任何类型锁作用于词此表,带share的锁表示允许其他用户共享此锁,事务执行期间阻止排他锁的使用。

挨着分析:

  • AccessShareLock:内部锁模式,select自动施加在表上
  • AccessExclusiveLock:被alter table、drop table 或者 vacuum full 使用 (最强的锁)
  • ShareLock:使用不带concurrently选项对create index语句请求时用该锁对表加锁(也就是会停止该表上的所有dml,vacuum等操作,只允许同一张表select,select for update/for share,create index操作)
  • ExclusiveLock:比sharelock多阻塞 sharelock和rowsharelock,即除了阻止dml、vaccum等,同时阻塞在该表上创建索引,阻止select for update/for share操作,只允许查,Q:啥时候用,A:比如向pg_lock系统表插入一个数据,比如去extend FSM等(一般涉及extend)。

下面四个锁是pg特有的锁:

  • RowShareLock 锁用于 select for update/for share语句。
    • select for update 就是将select出来的行被锁住,要更新;所以避免它们这些行在当前事务结束前被其他事务修改或者删除。所以这里需要阻塞其他update、delete、select for update这些行的事务(注意这里没有insert),直到当前事务结束。同时,如果一个其他事务的update、delete、select for update已经锁住了某个或者某些行,select for update会等到那些事务结束,并且随后锁住并返回更新的行。
    • for share类似,只是在每个检索出来的行上要求一个共享锁,而不是一个排他锁?一个共享锁阻塞update、delete、select for update但不阻塞select for share。
      【注意这里为啥只和7、8级冲突,保证一致性靠的是在行上标记事务,并非锁,对1级锁来说多了和7级的冲突】
  • RowExclusiveLock:insert/update/delete操作
  • ShareUpdateExclusiveLock:vacuum,回收已删除行、或update过时的行占用的空间
  • ShareRowExclusiveLock:?
                 
    PostgreSQL 随读笔记-事务上_第7张图片

3. RegularLock的存储

三种基本的锁结构:lock,proclock以及locallock结构。

  • lock:为了存储每个可锁对象、可以保持锁或者请求锁

  • prolock:存储进程和锁之间的关系(pg_locks的grant中的f等)

  • locallock:locallock是对每个backend的锁操作的加速。对于某进程的加锁要求,首先在locallock中查找当前进程是否已获得该锁,已获得就直接引用计数+1。不用LWlock。进对自己进程可见。

三张hashtable

static HTAB *LockMethodLockHash;
static HTAB *LockMethodProcLockHash;
static HTAB *LockMethodLocalHash;

4. RegularLock数据结构

按照上面三个最主要的结构体进行细分(lock.h):

1. Lock结构体:为了存储每个可锁对象、可以保持锁或者请求锁
/*
typedef struct LOCK
{
	/* hash key */
	LOCKTAG		tag;			1. /* unique identifier of lockable object */

	/* data */
	LOCKMASK	grantMask;		2. /* bitmask for lock types already granted */
	LOCKMASK	waitMask;		2. /* 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 */
} LOCK;
  • LOCKTAG 标志唯一被锁对象,是一个hashkey,是key information对应着一个lock item在lock hashtable里面,即描述被锁的是一个什么东西、什么类型、用什么方法锁。
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?

主要掉下面的几个宏:

#define SET_LOCKTAG_RELATION(locktag,dboid,reloid)
#define SET_LOCKTAG_RELATION_EXTEND(locktag,dboid,reloid)
#define SET_LOCKTAG_DATABASE_FROZEN_IDS(locktag,dboid) 
#define SET_LOCKTAG_PAGE(locktag,dboid,reloid,blocknum) 
#define SET_LOCKTAG_TUPLE(locktag,dboid,reloid,blocknum,offnum)
#define SET_LOCKTAG_TRANSACTION(locktag,xid)
#define SET_LOCKTAG_VIRTUALTRANSACTION(locktag,vxid)
...

这里的LOCKTAG中的locktag_type标志被锁对象的类型(可以看到万物都可锁):

typedef enum LockTagType
{
	LOCKTAG_RELATION,			/* whole relation */
	LOCKTAG_RELATION_EXTEND,	/* the right to extend a relation */
	LOCKTAG_DATABASE_FROZEN_IDS,	/* pg_database.datfrozenxid */
	LOCKTAG_PAGE,				/* one page of a relation */
	LOCKTAG_TUPLE,				/* one physical tuple */
	LOCKTAG_TRANSACTION,		/* transaction (for waiting for xact done) */
	LOCKTAG_VIRTUALTRANSACTION, /* virtual transaction (ditto) */
	LOCKTAG_SPECULATIVE_TOKEN,	/* speculative insertion Xid and token */
	LOCKTAG_OBJECT,				/* non-relation database object */
	LOCKTAG_USERLOCK,			/* reserved for old contrib/userlock code */
	LOCKTAG_ADVISORY			/* advisory user locks */
} LockTagType;

这里好奇一下LOCKTAG_RELATION_EXTEND是啥:
PostgreSQL 随读笔记-事务上_第8张图片

  • LOCKMASK grantMask 和 LOCKMASK waitMask(32位);
    前者是这个对象已经被加锁的所有类型,后面是这个对象所有正在被等待加锁的锁类型。这两个标志为了判断已持有锁与请求锁是否冲突。
typedef int LOCKMASK;
  • SHM_QUEUE procLocks; PROC_QUEUE waitProcs;
    下面这两个就是持有该对象锁的proc(进程)队列(信息用PROCLOCK存),和等待该对象锁的proc(进程)队列(信息用PGPROC存)
2. 锁持有者信息描述体

对于一个可加锁的对象,可能有几个不同事物持有或等待锁,我们需要将持有/等待锁和进程联系起来。这些信息应该保存起来PROCLOCK:

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

	/* data */
	PGPROC	   *groupLeader;	/* proc's lock group leader, or proc itself */
	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 */
} PROCLOCK;
  • PROCLOCKTAG tag 同样是为了在HTAB的LockMethodProcLockHash中查找:一个被锁对象和一个等待/持有进程标志唯一一个PROCLOCKTAG。
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;
  • SHM_QUEUE lockLink:在上一个lock结构体中,记录了一个持有该对象的锁的进程的queue,这里的locklink就是记录本ProcLock在queue的位置;

  • SHM_QUEUE procLink:同样每一个pgproc对象都有一个proclock链表,这里的proclink就是记录本proclock在pgproc链表的位置。

所以可以这么理解:proclock是pgproc和lock的链接。

proclock的所有者可能有两种:事务(由后端pgproc+该事务的事务id标识)或者会话(由后端pgproc和事务的invalidTransactionId标识)

注意proclock为用户锁和vacuum持有的跨事务锁使用。变量holdmask标识该proclock锁表示的已经分配的锁,如果一个进程正在等待锁,那么该进程和所等待的锁构成的proclock中holdmask为0.

3. 本地锁表

HTAB *LockMethodLocalHash;
每一个后台进程(会话)维护一个它自己感兴趣的每一个锁的本地hash表,每个都有一个hashtable 当事务重复在同一个对象上申请同类型锁时,无须做冲突检测,只要将这个锁记录在本地即可,避免频繁访问主锁表和进程锁表。

typedef struct LOCALLOCK
{
	/* tag */
	LOCALLOCKTAG tag;			/* unique identifier of locallock entry */

	/* data */
	uint32		hashcode;		/* copy of LOCKTAG's hash value */
	LOCK	   *lock;			/* associated LOCK object, if any */
	PROCLOCK   *proclock;		/* associated PROCLOCK object, if any */
	int64		nLocks;			/* 持锁数量 */
	int			numLockOwners;	/* # 锁持有者数量 */
	int			maxLockOwners;	/* allocated size of array */
	LOCALLOCKOWNER *lockOwners; /* 锁持有者列表 */
	bool		holdsStrongLockCount;	/* bumped FastPathStrongRelationLocks */
	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;

5. Regularlock的主要操作

366页
lock.c文件
(1)空间计算LockShmemSize

#define NLOCKENTS() \
	mul_size(max_locks_per_xact, add_size(MaxBackends, max_prepared_xacts))
	/* lock hash table */
	max_table_size = NLOCKENTS();
	size = add_size(size, hash_estimate_size(max_table_size, sizeof(LOCK)));

	/* proclock hash table */
	max_table_size *= 2;
	size = add_size(size, hash_estimate_size(max_table_size, sizeof(PROCLOCK)));

	/*
	 * Since NLOCKENTS is only an estimate, add 10% safety margin.
	 */
	size = add_size(size, size / 10);

(2)初始化
初始三个hashtable

(3)regularlock加锁

LockAcquire加锁:

LockAcquireResult LockAcquire(const LOCKTAG *locktag,
			LOCKMODE lockmode,
			bool sessionLock,
			bool dontWait);			

四个参数:

  • LOCKTAG :被锁对象唯一标识

  • LOCKMODE :获得的锁模式
    只有三种:

typedef enum LWLockMode
{
	LW_EXCLUSIVE,
	LW_SHARED,
	LW_WAIT_UNTIL_FREE			/* A special mode used in PGPROC->lwWaitMode,
								 * when waiting for lock to become free. Not
								 * to be used as LWLockAcquire argument */
} LWLockMode;
  • sessionlock 表示为会话加锁;如果false是为当前事务申请锁;
  • dontwait 表示申请锁是否允许等待

返回结果

/* Result codes for LockAcquire() */
typedef enum
{
	LOCKACQUIRE_NOT_AVAIL,		/* lock not available, and dontWait=true */
	LOCKACQUIRE_OK,				/* lock successfully acquired */
	LOCKACQUIRE_ALREADY_HELD,	/* incremented count for lock already held */
	LOCKACQUIRE_ALREADY_CLEAR	/* incremented count for lock already clear */
} LockAcquireResult;
  • 申请加锁流程:
    在pg中,锁获取最重要的函数是LockAcquire(核心是调用LockAcquireExtended),这是非常高频的操作,性能相当重要。因此在LockAcquire中,对封锁的性能做了大量优化。

常规锁主要保存在以下4个位置:

本地锁表(LOCALLOCK结构体):对于重复申请的锁进行计数,避免频繁访问主锁表和进程锁表,相当于一层缓存,如果查到了对应锁,也就是这个锁对象和模式已经授予本事务,给本地锁表对应锁增加引用计数即可,这个工作由GrantLockLocal函数完成。
快速路径(fast path):对弱锁的访问保存到本进程,避免频繁访问主锁表和进程锁表
主锁表(LOCK结构体):保存一个锁对象所有相关信息
进程锁表(PROCLOCK结构体):保存一个锁对象中与当前会话(进程)相关的信息

  • 释放:
    LockRelease
    有唤醒

Q:会话 backend 进程 事务这都啥关系
A:前三是一样,包含很多事务?

Q:Lockmode和LWlockmode啥关系啊。。

你可能感兴趣的:(postgresql,数据库,sql)