锁
一、概述
在GP中,定义了三种锁
- 自旋锁(Spinlocks)
- 轻量级锁(LWLocks)
- 普通锁(Regular locks,也叫重量级锁)
自旋锁
与互斥锁有点类似。针对某一项资源,在任何时刻,最多只能有一个保持者。对于互斥锁,如果资源已经被占用,资源申请者进入睡眠状态。但自旋锁会一直循环查看当前保持者是否已经释放锁,这个循环查看的状态就被形象地称之为自旋。
自旋锁一般是短期持有的,等待竞争的锁不需要做内核态和用户态之间的切换,他们只需要等一等,等到持有自旋锁的保持者释放即可获取。所以长时间上锁是非常耗费性能的,它阻止了其他进程或线程的运行和调度。所以会有一个自旋时间,时间一到立即释放自旋锁。自旋锁没有死锁检测和出错释放,一般由内核调用。
轻量级锁
为共享内存中需要并发访问的结构体提供锁保护。 轻量级锁通过做一个标记来记录此时是否有人在使用该资源。在没有锁竞争的情况下,获取和释放一个轻量级锁都是很快的。当一个进程必须等待一个轻量级锁时,会阻塞在一个SysV信号量上,因此等待过程并不消耗CPU时间。等待进程按照申请锁的先后顺序获得授权,没有超时机制,也没有死锁检测机制。
普通锁
也叫做重量级锁,用于对数据库对象,比如表、数据记录等加锁。普通锁支持多种不同的加锁模式,同时也支持死锁检测以及在事务结束时自动释放。
我们平时接触到的都是普通锁,简单总结下:自旋锁等待时间短,消耗CPU;轻量级锁通过信号量控制,不消耗CPU,是一种乐观锁,在竞争时会升级为重量级锁;普通锁可以长期持有,是一种悲观锁。
二、普通锁的数据结构
1.锁方法
在Greenplum数据库中,有三种锁方法:DEFAULT、USER和RESOURCE。
- DEFAULT锁方法是系统默认的加锁方法,用于对常见数据对象加锁
- USER锁方法主要用于意向锁(Advisory Locks)
- RESOURCE锁方法用于对资源队列的访问加锁
锁方法由结构体LockMethodData来表示。 其中有个叫锁模式冲突表的属性值得注意。锁模式冲突表定义了各个锁模式之间的冲突关系。理论上,各个锁方法可以定义自己的锁模式以及锁模式冲突表,但目前这三种锁方法使用相同的锁模式和锁模式冲突表。
锁模式:
锁模式冲突表:
2.锁结构体
内存中,普通锁由机构体LOCK表示,记录了一个可加锁对象的锁信息。
除了LOCK结构体外,还会使用另一个结构体PROCLOCK 。多个进程可能会同时持有或是等待同一个可加锁对象,对于每一个锁持有者或是等待者,我们会将其信息保存在一个结构体PROCLOCK中。
死锁
一、单节点死锁(本地死锁)
常规的本地死锁非常容易理解,两个并发的会话1和2,对应着两个后端进程。进程1持有锁A,进程2持有锁B。接着进程1要获取锁B,而进程2要获取锁A。由于锁通常在事务结束才释放,两个进程互相竞争于是发生本地死锁。
二、PostgreSQL死锁检测器解决单节点死锁
Postgres 有一个死锁检测器,负责检测死锁并在发生死锁时打破死锁。检测器使用等待图(wait-for graph)为进程之间的等待关系建模,图节点由进程标识符pid标识,图的一条边表示一个锁的等待关系。
工作过程:
- 进程获取锁失败,进入睡眠模式等待
- 如果等待超时(GP中该计时器是一秒),运行死锁检测算法
- SIGALARM 处理程序构建等待图,以当前进程为起点,检查等待图是否存在环,环意味着发生死锁。当前进程主动退出打破死锁
三、分布式集群中的死锁(全局死锁)
全局死锁也容易理解,两个分布式事务1和2,分别运行在A,B两个节点上。接下来,1要去B节点上运行,但B节点正被2阻塞,因此事务1被挂起,等待2的执行完成;不巧事务2也要申请A节点的资源,由于A节点正在执行事务1,事务2也被挂起,等待1的执行完成。这时候发生死锁。与本地死锁不同的是,节点A和节点B上都没有死锁,但在整个集群看来,死锁真实发生了。
全局死锁检测器仍然使用等待图为锁等待关系建模,但是是基于整个集群。将每个节点上的本地等待图合并起来生成全局图。图中的节点不再是进程ID,而是一个进程组,代表一个分布式事务。
GP的WFG图还存在另外一种边,如果进程1和2同时在一个锁的等待队列里,并且1在2的后面,那么2总是先被唤醒,此时就存在1等待2的情况,这种等待边为soft edge。由soft edge构成的环是假死锁,可以尝试调整等待队列里的顺序,使得环不存在,那么就不需要撤销事务来破锁了。