一、 pg中的锁
pg中的锁可以分为3个层次:
- 自旋锁(Spin Lock):是一种和硬件结合的互斥锁,借用了硬件提供的原子操作的原语来对一些共享变量进行封锁,通常适用于临界区比较小的情况。特点是:封锁时间很短、无死锁检测机制和等待队列、事务结束时不会自动释放SpinLock。
- 轻量锁(Lightweight Lock):负责保护共享内存中的数据结构,有共享和排他两种模式,类似Oracle中的latch。特点是:封锁时间较短、无死锁检测机制、有等待队列、事务结束时会自动释放。
- 常规锁(Regular Lock):就是通常说的对数据库对象的锁。按照锁粒度,可以分为表锁、页锁、行锁等;按照等级,pg锁一共有8个等级。特点是:封锁时间可以很长、有死锁检测机制和等待队列、事务结束时会自动释放。
二、自旋锁
自旋锁顾名思义就是一直原地旋转等待的锁。一个进程如果想要访问临界区,必须先获得锁资源,如果不能获得,就会一直自旋,直到获取到锁资源。
显然这种自旋会造成CPU浪费,但是通常它保护的临界区非常小,封锁时间很短,因此通常自旋比释放CPU资源带来的上下文切换消耗要小。
如果机器拥有TAS(test-and-set)指令集,则pg会使用s_lock.h和s_lock.c中定义的实现机制。如果没有,则要用pg定义的信号量PGSemaphore来仿真SpinLock(在Spin.c文件中),显然后者效率不如前者。
看了下源码介绍,自旋锁的实现太偏底层,并且有不少汇编语言,有兴趣请参考原书了。
三、 轻量锁
如前所述,轻量锁负责保护共享内存中的数据结构,它正式的名字叫做Individual LWLocks。
1. 轻量锁类型
pg中的轻量锁类型定义在lwlocknames.h文件中(这个文件是在编译时由lwlocknames.txt生成的),在pg 14中,目前有45种轻量锁。
#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)
…
可以看出,Individual LWLocks 被保存在MainLWLockArray数组中(前48个值),每种Individual LWLocks都有自己固定要保护的对象,使用方式如下(shmem.c 文件ShmemInitStruct函数):
LWLockAcquire(ShmemIndexLock, LW_EXCLUSIVE);
LWLockRelease(ShmemIndexLock);
每个builtin tranches代表一组LWLocks,这组LWLocks虽然封锁各自的内容,但它们的功能相同。以下定义在lwlock.h文件中
typedef enum BuiltinTrancheIds
{
LWTRANCHE_XACT_BUFFER = NUM_INDIVIDUAL_LWLOCKS,
LWTRANCHE_COMMITTS_BUFFER,
LWTRANCHE_SUBTRANS_BUFFER,
LWTRANCHE_MULTIXACTOFFSET_BUFFER,
LWTRANCHE_MULTIXACTMEMBER_BUFFER,
LWTRANCHE_NOTIFY_BUFFER,
LWTRANCHE_SERIAL_BUFFER,
LWTRANCHE_WAL_INSERT,
LWTRANCHE_BUFFER_CONTENT,
LWTRANCHE_REPLICATION_ORIGIN_STATE,
LWTRANCHE_REPLICATION_SLOT_IO,
LWTRANCHE_LOCK_FASTPATH,
LWTRANCHE_BUFFER_MAPPING,
LWTRANCHE_LOCK_MANAGER,
LWTRANCHE_PREDICATE_LOCK_MANAGER,
LWTRANCHE_PARALLEL_HASH_JOIN,
LWTRANCHE_PARALLEL_QUERY_DSA,
LWTRANCHE_PER_SESSION_DSA,
LWTRANCHE_PER_SESSION_RECORD_TYPE,
LWTRANCHE_PER_SESSION_RECORD_TYPMOD,
LWTRANCHE_SHARED_TUPLESTORE,
LWTRANCHE_SHARED_TIDBITMAP,
LWTRANCHE_PARALLEL_APPEND,
LWTRANCHE_PER_XACT_PREDICATE_LIST,
LWTRANCHE_FIRST_USER_DEFINED
} BuiltinTrancheIds;
这些builtin tranches对应的一部分被保存在MainLWLockArray数组中,另一部分在使用它们的结构体中。
/* 初始化NamedLWLockTrancheRequests个轻量锁 */
for (int i = 0; i < NamedLWLockTrancheRequests; i++)
LWLockRegisterTranche(NamedLWLockTrancheArray[i].trancheId,
NamedLWLockTrancheArray[i].trancheName);
2. 轻量锁定义
轻量锁的结构体定义如下:
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; /* 等待者数量 */
struct PGPROC *owner; /* last exclusive owner of the lock,最近一次排他锁的拥有者 */
#endif
} LWLock;
State变量有32位,其中低24位作为共享锁的计数器,因此一个轻量锁最多可以有2^24个共享锁持锁者。有1位作为排他锁的标记,因为同一时间最多只能有一个持锁者。
3. 轻量锁的申请
假设一个新会话申请共享锁(如果持续有共享锁申请插队,排他锁申请者可能会饿死)
假设一个新会话申请排他锁
如果等待队列中第一个申请的是排他锁,则只有这一个申请者被唤醒,其他申请者继续等待。
如果等待队列中第一个申请的是共享锁,则所有共享锁申请者都被唤醒,其他排他锁申请者继续等待。
参考
《PostgreSQL技术内幕:事务处理深度探索》第2章
《PostgreSQL数据库内核分析》第7章