searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

PostgreSQL 锁机制

2023-08-14 09:40:58
42
0

锁的定义

       PostgreSQL 实现并发控制的基本方法是使用锁来控制临界区互斥访问。后台进程对磁盘文件进行访问操作时,首先要获取锁。如果成功获得目标锁则进入临界区执行磁盘读写访问,访问完成后退出临界区并释放锁;否则,进程睡眠直到被别的后台进程唤醒后重试。

PostgreSQL 中的 3 种锁

      PostgreSQL 中定义了三种锁,分别是 SpinLock、LWLock 和 RegularLock。

SpinLock

      SpinLock 是最底层的锁,使用互斥信号量实现,分为硬件依赖的实现方法(定义在 /src/backend/storage/lmgr/s_lock.c 中)和与机器不相关的实现方法(定义在 /src/backend/storage/lmgr/spin.c 中)。

      SpinLock 的特点是:封锁时间很短,没有等待队列和死锁检测机制,事务结束时不能自动释放。依赖于硬件的 SpinLock 机制比不依赖于硬件的 SpinLock 机制速度快,因为不依赖于硬件的 SpinLock 机制需要使用 PG 信号量来仿真 SpinLock。

LWLock

      LWLock(轻量级锁)主要提供对共享存储器的互斥访问。LWLock 有两种锁模式,一种为排他模式,另一种为共享模式。LWLock 利用 SpinLock 实现。
      LWLock 的特点是:有等待队列、无死锁检测、能自动释放锁。当一个进程阻塞在一个轻量级锁上时,相当于它阻塞在一个信号量上,所以不会消耗 CPU 时间,等待的进程将会以先来后到的顺序被授予锁。
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;

        其中 tranche 是 LWLock 的 ID,是一个轻量级锁的唯一标识。state 是状态值,waiters 是锁的等待队列。

LWLock 相关操作定义在 /src/backend/storage/lmgr/lwlock.c 中

Lock

      Lock就是一般数据库事务管理中所知的锁, 由 LWLock实现。

      Lock 的特点是:有等待队列,有死锁检测,能自动释放锁。

      Lock 支持的锁模式有八种,按排他(Exclusive)级别从低到高分别是:
  • 访问共享锁(AccessShareLock):一个内部锁模式,进行查询(SELECT 操作)时自动施加在被查询的表上 。
  • 行共享锁(RowShareLock):当语句中采用了 SELECT...FOR UPDATE 和 FOR SHARE 时将使用行共享锁对表加锁。
  • 行排他锁(RowExclusiveLock):使用 UPDATE、DELETE、INSERT 语句时将使用行排他锁对表加锁。
  • 共享更新排他锁(ShareUpdateExclusiveLock):使用 VACUUM(不带 FULL 选项)、ANALYZE 或 CREATE lNDEX CONCURRENTLY 语句时使用共享更新排他锁对表加锁。
  • 共享锁(ShareLock):使用不带 CONCURRENTLY 选项的 CREATE INDEX 语句请求时用共享锁对表加锁。
  • 共享行排他锁(ShareRowExclusiveLock):类似于排他锁,但是允许行共享。
  • 排他锁(ExclusiveLock):阻塞行共享和 SELECT... FOR UPDATE。
  • 访问排他锁(AccessExclusiveLock):被 ALTER TABLE、DROP TABLE 以及 VACUUM FULL 操作要求。

      排他模式的锁(ShareRowExclusiveLock、ExclusiveLock、AccessExclusiveLock)表示在事务执行期间阻止其他任何类型锁作用于此表;共享模式的锁(非排他模式的锁)表示允许其他用户同时共享此锁,但在事务执行期间阻止排他型锁的使用。排他模式和共享模式的锁都可以工作在以下授权级别上:Access(锁定整个表模式)、Rows(仅锁定单独的元组)。

编号 锁模式 用途 冲突关系
1 AccessShareLock SELECT 8
2 RowShareLock
SELECT FOR UPDATE/FOR SHARE
7|8
3 RowExclusiveLock
INSERT, UPDATE, DELETE
5|6|7|8
4 ShareUpdateExclusiveLock
VACUUM (non-FULL),ANALYZE, CREATE INDEX CONCURRENTLY
4|5|6|7|8
5 ShareLock
CREATE INDEX (WITHOUT CONCURRENTLY)
3|4|6|7|8
6 ShareRowExclusiveLock ROW SELECT ... FOR UPDATE 3|4|5|6|7|8
7 ExclusiveLock
blocks ROW SHARE/SELECT ... FOR UPDATE
2|3|4|5|6|7|8
8 AccessExclusiveLock
ALTER TABLE, DROP TABLE, VACUUM FULL
1|2|3|4|5|6|7|8
     
      Lock 的数据结构较为复杂,比较重要的几个如下:

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

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

 LOCKTAG :在锁的 Hash 表格(hashtable)中寻找一个锁项所必须的码信息,一个 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;
LOCK :用于标识已经加锁的资源或是请求加锁的可锁资源。
typedef struct LOCK
{
	/* hash key */
	LOCKTAG		tag;			/* unique identifier of lockable object */

	/* data */
	LOCKMASK	grantMask;		/* bitmask for lock types already granted */
	LOCKMASK	waitMask;		/* 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;

PROCLOCK:同一可加锁对象可能有几个不同事务持有或等待锁。PROCLOCK 为每一个这样的锁持有者(或即将持有者)存储持有者/等待者信息。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;

LocalLock:每一个后台进程也维护一个当前它感兴趣的(持有的或者希望持有的)每一个锁的本地 Hash 表。本地表 LocalLock 记录了已经获取的锁的次数,则允许在不需要另外对共享内存进行访问的情况下对同一锁的多次加锁请求。同时也跟踪每一个资源所有者获得锁的数量,所以我们可以只释放属于某一资源所有者的锁。

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

	/* data */
	LOCK	   *lock;			/* associated LOCK object, if any */
	PROCLOCK   *proclock;		/* associated PROCLOCK object, if any */
	uint32		hashcode;		/* copy of LOCKTAG's hash value */
	int64		nLocks;			/* total number of times lock is held */
	bool		holdsStrongLockCount;	/* bumped FastPathStrongRelationLocks */
	bool		lockCleared;	/* we read all sinval msgs for lock */
	int			numLockOwners;	/* # of relevant ResourceOwners */
	int			maxLockOwners;	/* allocated size of array */
	LOCALLOCKOWNER *lockOwners; /* dynamically resizable array */
} LOCALLOCK;

      此外,PostgreSQL 通过三张锁表联合管理系统的加锁情况。

  • LockMethodLockHash:由锁方法 ID 到锁表数据结构的映射。
  • LockMethodProcLockHash:锁方法对应的 PROCLOCK 对象的 Hash 表指针。
  • LockMethodLocalHash:锁方法对应的 LOCALLOCK 对象的 Hash 表指针。
      RegularLock 相关操作相关函数定义在 /src/backend/storage/lmgr/lock.c 中。

死锁

      死锁是一种特殊的状态,它发生在多任务系统中,当一个或多个进程因竞争共享资源而处于永远等待的状态时,就会发生死锁。由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程无法继续运行,而无外力协助也无法分配到必需的资源,这就是死锁。

死锁的产生条件

      死锁的产生条件包括 4 点:
  • 互斥:临界资源是独占资源,进程应互斥且排他的使用这些资源。
  • 占有和等待:进程在请求资源得不到满足而等待时,不释放已占有资源。
  • 不剥夺:已获资源只能由进程自愿释放,不允许被其他进程剥夺。
  • 循环等待:存在循环等待链,其中,每个进程都在等待链中等待下一个进程所持有的资源,造成这组进程处于永远等待状态。

死锁预防

      PostgreSQL 对于死锁的预防分为两步:
  • 当进程请求加锁时,如果失败, 会进入等待队列。如果在队列中已经存在一些进程要求本进程中已经持有的锁,那么为了尽量避免死锁,可以简单地把本进程插入到它们的前面。
  • 当一个锁被释放时,将会试图唤醒等待队列中的进程。如果其中的某个进程的要求与排在它前面但由于某些原因不能被唤醒的进程冲突,这个进程将不被唤醒。这么做可以保证互相冲突的加锁请求按照到达的先后被处理。

死锁检测

      PostgreSQL 在未获得锁之后进入睡眠状态,随计时器时限到达后进行死锁检测。以上为死锁检测的触发条件。

      PostgreSQL 使用等待图(WFG)来进行死锁检测,WFG 是一个有向图,图中的顶点表示申请加锁的进程,而图中的有向边表示依赖关系(等待关系)。
      
      如果进程 A 在等待进程 B (进程 A 申请的锁 a 与进程 B 持有的锁 b 冲突,或者进程 A 申请的锁 a 与进程 B 申请的锁 b 冲突并且进程 B 比进程 A 申请锁的时间更早) ,那么从顶点 A 到顶点 B 就有一条有向边。系统出现死锁当且仅当 WFG 中出现环。
  • Soft Edge:进程 A 和进程 B 都在某个锁的等待队列中,进程 A 在进程 B 的后面。如果进程 A 和进程 B 的加锁要求冲突,即进程 A 在等待进程 B,这时候顶点 A 到 顶点 B 会有一条有向边。
  • Hard Edge:如果进程 A 的加锁要求和进程 B 已持有的锁冲突,这时候顶点 A 到顶点 B 也有一条有向边。
如果 WFG 中的环是有 Soft Edge 的环,可以通过拓扑排序对队列进行重排,尝试消除死锁。

死锁消除

      PostgreSQL 死锁消除算法如下:
  • 从每一个顶点开始出发,沿 WFG 中的有向边走,看能不能回到此顶点。如果找到一条路径满足此条件,则说明出现死锁。
  • 如果此路径中没有 Soft Edge,直接终止这个事务。
  • 否则记录所有出现的 Soft Edge 。对于这个集合,递归地枚举它的所有子集,尝试进行调整。
  • 对于上述每个子集,使用拓扑排序来排出一个调整方案,并逐一加以测试(一旦找到一个可行的调整方案,这个测试过程就结束了,PostgreSQL 并没有要求这个调整方案一定是最优的)。如果找不到一个可以消除死锁的方案,则死锁消除失败,然后终止这个事务。

死锁处理相关数据结构

EDGE:依赖图中的边。

typedef struct Edge
{
	Gene		edge_list[4];	/* list of edges */
	int			total_edges;
	int			unused_edges;
} Edge;

 WAIT_ORDER:锁的等待队列,在死锁消除时需要对一个锁的等待队列重新排序。

typedef struct
{
	LOCK	   *lock;			/* the lock whose wait queue is described */
	PGPROC	  **procs;			/* array of PGPROC *'s in new wait order */
	int			nProcs;
} WAIT_ORDER;

 DEADLOCK_INFO:记录每一个已检测到的死锁环中的每一条边信息。

typedef struct
{
	LOCKTAG		locktag;		/* ID of awaited lock object */
	LOCKMODE	lockmode;		/* type of lock we're waiting for */
	int			pid;			/* PID of blocked backend */
} DEADLOCK_INFO;
DeadLockState:枚举类型,表示 DeadLockCheck 函数的执行结果。
typedef enum
{
	DS_NOT_YET_CHECKED,			/* no deadlock check has run yet */
	DS_NO_DEADLOCK,				/* no deadlock detected */
	DS_SOFT_DEADLOCK,			/* deadlock avoided by queue rearrangement */
	DS_HARD_DEADLOCK,			/* deadlock, no way out but ERROR */
	DS_BLOCKED_BY_AUTOVACUUM	/* no deadlock; queue blocked by autovacuum
								 * worker */
} DeadLockState;

死锁处理相关函数入口

      死锁处理相关操作位于 src/backend/storage/lmgr/deadlock.c 文件中。
  • 死锁处理相关数据结构的初始化:每个服务进程启动的时候都需要初始化死锁检测器,即给死锁检测时需要用到的数据结构分配内存空间,由 InitDeadLockChecking 函数完成。
  • 死锁检测入口函数:当进程不能获得锁进入等待队列时,就会触发死锁检测的操作,死锁检测入口是 DeadLockCheck 函数。
  • 死锁检测调整函数:定义在 DeadLockCheckRecurse 函数中,它递归地调整等待队列的排序,寻找无死锁的队列。如果等待队列的任何拓扑序列都无法消除死锁,则该函数返回 TRUE。如果发现可用的新等待序列,则保存这个新序列,并返回 FALSE。
  • 等待图的环检测函数:定义在 FindLockCycle 函数中,它调用 FindLockCycleRecurse 函数检测等待图中是否有死锁环。如果发现了死锁环,则通过调用参数返回一个 Soft Edge 的链表。
0条评论
0 / 1000
LiangYi
3文章数
0粉丝数
LiangYi
3 文章 | 0 粉丝
LiangYi
3文章数
0粉丝数
LiangYi
3 文章 | 0 粉丝
原创

PostgreSQL 锁机制

2023-08-14 09:40:58
42
0

锁的定义

       PostgreSQL 实现并发控制的基本方法是使用锁来控制临界区互斥访问。后台进程对磁盘文件进行访问操作时,首先要获取锁。如果成功获得目标锁则进入临界区执行磁盘读写访问,访问完成后退出临界区并释放锁;否则,进程睡眠直到被别的后台进程唤醒后重试。

PostgreSQL 中的 3 种锁

      PostgreSQL 中定义了三种锁,分别是 SpinLock、LWLock 和 RegularLock。

SpinLock

      SpinLock 是最底层的锁,使用互斥信号量实现,分为硬件依赖的实现方法(定义在 /src/backend/storage/lmgr/s_lock.c 中)和与机器不相关的实现方法(定义在 /src/backend/storage/lmgr/spin.c 中)。

      SpinLock 的特点是:封锁时间很短,没有等待队列和死锁检测机制,事务结束时不能自动释放。依赖于硬件的 SpinLock 机制比不依赖于硬件的 SpinLock 机制速度快,因为不依赖于硬件的 SpinLock 机制需要使用 PG 信号量来仿真 SpinLock。

LWLock

      LWLock(轻量级锁)主要提供对共享存储器的互斥访问。LWLock 有两种锁模式,一种为排他模式,另一种为共享模式。LWLock 利用 SpinLock 实现。
      LWLock 的特点是:有等待队列、无死锁检测、能自动释放锁。当一个进程阻塞在一个轻量级锁上时,相当于它阻塞在一个信号量上,所以不会消耗 CPU 时间,等待的进程将会以先来后到的顺序被授予锁。
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;

        其中 tranche 是 LWLock 的 ID,是一个轻量级锁的唯一标识。state 是状态值,waiters 是锁的等待队列。

LWLock 相关操作定义在 /src/backend/storage/lmgr/lwlock.c 中

Lock

      Lock就是一般数据库事务管理中所知的锁, 由 LWLock实现。

      Lock 的特点是:有等待队列,有死锁检测,能自动释放锁。

      Lock 支持的锁模式有八种,按排他(Exclusive)级别从低到高分别是:
  • 访问共享锁(AccessShareLock):一个内部锁模式,进行查询(SELECT 操作)时自动施加在被查询的表上 。
  • 行共享锁(RowShareLock):当语句中采用了 SELECT...FOR UPDATE 和 FOR SHARE 时将使用行共享锁对表加锁。
  • 行排他锁(RowExclusiveLock):使用 UPDATE、DELETE、INSERT 语句时将使用行排他锁对表加锁。
  • 共享更新排他锁(ShareUpdateExclusiveLock):使用 VACUUM(不带 FULL 选项)、ANALYZE 或 CREATE lNDEX CONCURRENTLY 语句时使用共享更新排他锁对表加锁。
  • 共享锁(ShareLock):使用不带 CONCURRENTLY 选项的 CREATE INDEX 语句请求时用共享锁对表加锁。
  • 共享行排他锁(ShareRowExclusiveLock):类似于排他锁,但是允许行共享。
  • 排他锁(ExclusiveLock):阻塞行共享和 SELECT... FOR UPDATE。
  • 访问排他锁(AccessExclusiveLock):被 ALTER TABLE、DROP TABLE 以及 VACUUM FULL 操作要求。

      排他模式的锁(ShareRowExclusiveLock、ExclusiveLock、AccessExclusiveLock)表示在事务执行期间阻止其他任何类型锁作用于此表;共享模式的锁(非排他模式的锁)表示允许其他用户同时共享此锁,但在事务执行期间阻止排他型锁的使用。排他模式和共享模式的锁都可以工作在以下授权级别上:Access(锁定整个表模式)、Rows(仅锁定单独的元组)。

编号 锁模式 用途 冲突关系
1 AccessShareLock SELECT 8
2 RowShareLock
SELECT FOR UPDATE/FOR SHARE
7|8
3 RowExclusiveLock
INSERT, UPDATE, DELETE
5|6|7|8
4 ShareUpdateExclusiveLock
VACUUM (non-FULL),ANALYZE, CREATE INDEX CONCURRENTLY
4|5|6|7|8
5 ShareLock
CREATE INDEX (WITHOUT CONCURRENTLY)
3|4|6|7|8
6 ShareRowExclusiveLock ROW SELECT ... FOR UPDATE 3|4|5|6|7|8
7 ExclusiveLock
blocks ROW SHARE/SELECT ... FOR UPDATE
2|3|4|5|6|7|8
8 AccessExclusiveLock
ALTER TABLE, DROP TABLE, VACUUM FULL
1|2|3|4|5|6|7|8
     
      Lock 的数据结构较为复杂,比较重要的几个如下:

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

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

 LOCKTAG :在锁的 Hash 表格(hashtable)中寻找一个锁项所必须的码信息,一个 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;
LOCK :用于标识已经加锁的资源或是请求加锁的可锁资源。
typedef struct LOCK
{
	/* hash key */
	LOCKTAG		tag;			/* unique identifier of lockable object */

	/* data */
	LOCKMASK	grantMask;		/* bitmask for lock types already granted */
	LOCKMASK	waitMask;		/* 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;

PROCLOCK:同一可加锁对象可能有几个不同事务持有或等待锁。PROCLOCK 为每一个这样的锁持有者(或即将持有者)存储持有者/等待者信息。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;

LocalLock:每一个后台进程也维护一个当前它感兴趣的(持有的或者希望持有的)每一个锁的本地 Hash 表。本地表 LocalLock 记录了已经获取的锁的次数,则允许在不需要另外对共享内存进行访问的情况下对同一锁的多次加锁请求。同时也跟踪每一个资源所有者获得锁的数量,所以我们可以只释放属于某一资源所有者的锁。

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

	/* data */
	LOCK	   *lock;			/* associated LOCK object, if any */
	PROCLOCK   *proclock;		/* associated PROCLOCK object, if any */
	uint32		hashcode;		/* copy of LOCKTAG's hash value */
	int64		nLocks;			/* total number of times lock is held */
	bool		holdsStrongLockCount;	/* bumped FastPathStrongRelationLocks */
	bool		lockCleared;	/* we read all sinval msgs for lock */
	int			numLockOwners;	/* # of relevant ResourceOwners */
	int			maxLockOwners;	/* allocated size of array */
	LOCALLOCKOWNER *lockOwners; /* dynamically resizable array */
} LOCALLOCK;

      此外,PostgreSQL 通过三张锁表联合管理系统的加锁情况。

  • LockMethodLockHash:由锁方法 ID 到锁表数据结构的映射。
  • LockMethodProcLockHash:锁方法对应的 PROCLOCK 对象的 Hash 表指针。
  • LockMethodLocalHash:锁方法对应的 LOCALLOCK 对象的 Hash 表指针。
      RegularLock 相关操作相关函数定义在 /src/backend/storage/lmgr/lock.c 中。

死锁

      死锁是一种特殊的状态,它发生在多任务系统中,当一个或多个进程因竞争共享资源而处于永远等待的状态时,就会发生死锁。由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程无法继续运行,而无外力协助也无法分配到必需的资源,这就是死锁。

死锁的产生条件

      死锁的产生条件包括 4 点:
  • 互斥:临界资源是独占资源,进程应互斥且排他的使用这些资源。
  • 占有和等待:进程在请求资源得不到满足而等待时,不释放已占有资源。
  • 不剥夺:已获资源只能由进程自愿释放,不允许被其他进程剥夺。
  • 循环等待:存在循环等待链,其中,每个进程都在等待链中等待下一个进程所持有的资源,造成这组进程处于永远等待状态。

死锁预防

      PostgreSQL 对于死锁的预防分为两步:
  • 当进程请求加锁时,如果失败, 会进入等待队列。如果在队列中已经存在一些进程要求本进程中已经持有的锁,那么为了尽量避免死锁,可以简单地把本进程插入到它们的前面。
  • 当一个锁被释放时,将会试图唤醒等待队列中的进程。如果其中的某个进程的要求与排在它前面但由于某些原因不能被唤醒的进程冲突,这个进程将不被唤醒。这么做可以保证互相冲突的加锁请求按照到达的先后被处理。

死锁检测

      PostgreSQL 在未获得锁之后进入睡眠状态,随计时器时限到达后进行死锁检测。以上为死锁检测的触发条件。

      PostgreSQL 使用等待图(WFG)来进行死锁检测,WFG 是一个有向图,图中的顶点表示申请加锁的进程,而图中的有向边表示依赖关系(等待关系)。
      
      如果进程 A 在等待进程 B (进程 A 申请的锁 a 与进程 B 持有的锁 b 冲突,或者进程 A 申请的锁 a 与进程 B 申请的锁 b 冲突并且进程 B 比进程 A 申请锁的时间更早) ,那么从顶点 A 到顶点 B 就有一条有向边。系统出现死锁当且仅当 WFG 中出现环。
  • Soft Edge:进程 A 和进程 B 都在某个锁的等待队列中,进程 A 在进程 B 的后面。如果进程 A 和进程 B 的加锁要求冲突,即进程 A 在等待进程 B,这时候顶点 A 到 顶点 B 会有一条有向边。
  • Hard Edge:如果进程 A 的加锁要求和进程 B 已持有的锁冲突,这时候顶点 A 到顶点 B 也有一条有向边。
如果 WFG 中的环是有 Soft Edge 的环,可以通过拓扑排序对队列进行重排,尝试消除死锁。

死锁消除

      PostgreSQL 死锁消除算法如下:
  • 从每一个顶点开始出发,沿 WFG 中的有向边走,看能不能回到此顶点。如果找到一条路径满足此条件,则说明出现死锁。
  • 如果此路径中没有 Soft Edge,直接终止这个事务。
  • 否则记录所有出现的 Soft Edge 。对于这个集合,递归地枚举它的所有子集,尝试进行调整。
  • 对于上述每个子集,使用拓扑排序来排出一个调整方案,并逐一加以测试(一旦找到一个可行的调整方案,这个测试过程就结束了,PostgreSQL 并没有要求这个调整方案一定是最优的)。如果找不到一个可以消除死锁的方案,则死锁消除失败,然后终止这个事务。

死锁处理相关数据结构

EDGE:依赖图中的边。

typedef struct Edge
{
	Gene		edge_list[4];	/* list of edges */
	int			total_edges;
	int			unused_edges;
} Edge;

 WAIT_ORDER:锁的等待队列,在死锁消除时需要对一个锁的等待队列重新排序。

typedef struct
{
	LOCK	   *lock;			/* the lock whose wait queue is described */
	PGPROC	  **procs;			/* array of PGPROC *'s in new wait order */
	int			nProcs;
} WAIT_ORDER;

 DEADLOCK_INFO:记录每一个已检测到的死锁环中的每一条边信息。

typedef struct
{
	LOCKTAG		locktag;		/* ID of awaited lock object */
	LOCKMODE	lockmode;		/* type of lock we're waiting for */
	int			pid;			/* PID of blocked backend */
} DEADLOCK_INFO;
DeadLockState:枚举类型,表示 DeadLockCheck 函数的执行结果。
typedef enum
{
	DS_NOT_YET_CHECKED,			/* no deadlock check has run yet */
	DS_NO_DEADLOCK,				/* no deadlock detected */
	DS_SOFT_DEADLOCK,			/* deadlock avoided by queue rearrangement */
	DS_HARD_DEADLOCK,			/* deadlock, no way out but ERROR */
	DS_BLOCKED_BY_AUTOVACUUM	/* no deadlock; queue blocked by autovacuum
								 * worker */
} DeadLockState;

死锁处理相关函数入口

      死锁处理相关操作位于 src/backend/storage/lmgr/deadlock.c 文件中。
  • 死锁处理相关数据结构的初始化:每个服务进程启动的时候都需要初始化死锁检测器,即给死锁检测时需要用到的数据结构分配内存空间,由 InitDeadLockChecking 函数完成。
  • 死锁检测入口函数:当进程不能获得锁进入等待队列时,就会触发死锁检测的操作,死锁检测入口是 DeadLockCheck 函数。
  • 死锁检测调整函数:定义在 DeadLockCheckRecurse 函数中,它递归地调整等待队列的排序,寻找无死锁的队列。如果等待队列的任何拓扑序列都无法消除死锁,则该函数返回 TRUE。如果发现可用的新等待序列,则保存这个新序列,并返回 FALSE。
  • 等待图的环检测函数:定义在 FindLockCycle 函数中,它调用 FindLockCycleRecurse 函数检测等待图中是否有死锁环。如果发现了死锁环,则通过调用参数返回一个 Soft Edge 的链表。
文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
1
1