锁的定义
PostgreSQL 中的 3 种锁
SpinLock
SpinLock 是最底层的锁,使用互斥信号量实现,分为硬件依赖的实现方法(定义在 /src/backend/storage/lmgr/s_lock.c 中)和与机器不相关的实现方法(定义在 /src/backend/storage/lmgr/spin.c 中)。
SpinLock 的特点是:封锁时间很短,没有等待队列和死锁检测机制,事务结束时不能自动释放。依赖于硬件的 SpinLock 机制比不依赖于硬件的 SpinLock 机制速度快,因为不依赖于硬件的 SpinLock 机制需要使用 PG 信号量来仿真 SpinLock。
LWLock
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 是锁的等待队列。
Lock
Lock就是一般数据库事务管理中所知的锁, 由 LWLock实现。
Lock 的特点是:有等待队列,有死锁检测,能自动释放锁。
-
访问共享锁(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 |
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;
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 表指针。
死锁
死锁的产生条件
-
互斥:临界资源是独占资源,进程应互斥且排他的使用这些资源。
-
占有和等待:进程在请求资源得不到满足而等待时,不释放已占有资源。
-
不剥夺:已获资源只能由进程自愿释放,不允许被其他进程剥夺。
-
循环等待:存在循环等待链,其中,每个进程都在等待链中等待下一个进程所持有的资源,造成这组进程处于永远等待状态。
死锁预防
-
当进程请求加锁时,如果失败, 会进入等待队列。如果在队列中已经存在一些进程要求本进程中已经持有的锁,那么为了尽量避免死锁,可以简单地把本进程插入到它们的前面。
-
当一个锁被释放时,将会试图唤醒等待队列中的进程。如果其中的某个进程的要求与排在它前面但由于某些原因不能被唤醒的进程冲突,这个进程将不被唤醒。这么做可以保证互相冲突的加锁请求按照到达的先后被处理。
死锁检测
PostgreSQL 在未获得锁之后进入睡眠状态,随计时器时限到达后进行死锁检测。以上为死锁检测的触发条件。
-
Soft Edge:进程 A 和进程 B 都在某个锁的等待队列中,进程 A 在进程 B 的后面。如果进程 A 和进程 B 的加锁要求冲突,即进程 A 在等待进程 B,这时候顶点 A 到 顶点 B 会有一条有向边。
-
Hard Edge:如果进程 A 的加锁要求和进程 B 已持有的锁冲突,这时候顶点 A 到顶点 B 也有一条有向边。
死锁消除
-
从每一个顶点开始出发,沿 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;
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;
死锁处理相关函数入口
-
死锁处理相关数据结构的初始化:每个服务进程启动的时候都需要初始化死锁检测器,即给死锁检测时需要用到的数据结构分配内存空间,由 InitDeadLockChecking 函数完成。
-
死锁检测入口函数:当进程不能获得锁进入等待队列时,就会触发死锁检测的操作,死锁检测入口是 DeadLockCheck 函数。
-
死锁检测调整函数:定义在 DeadLockCheckRecurse 函数中,它递归地调整等待队列的排序,寻找无死锁的队列。如果等待队列的任何拓扑序列都无法消除死锁,则该函数返回 TRUE。如果发现可用的新等待序列,则保存这个新序列,并返回 FALSE。
-
等待图的环检测函数:定义在 FindLockCycle 函数中,它调用 FindLockCycleRecurse 函数检测等待图中是否有死锁环。如果发现了死锁环,则通过调用参数返回一个 Soft Edge 的链表。