2.6 常规锁的内存结构
常规锁也就是事务锁,它用于协调各种不同事务对相同对象的并发访问。在数据库启动阶段,PostgreSQL通过InitLocks函数来初始化保存锁对象的共享内存空间,在共享内存中,有两个“锁表”被用来保存锁对象,分别是主锁表(LockMethodLockHash)和进程锁表(LockMethodProcLockHash)。
主锁表被用来保存当前数据库中所有事务的锁对象,也就是LOCK结构体。
进程锁表被用来保存当前进程(会话)的事务锁的状态,它保存的是PROCLOCK结构体,这个结构体的主要作用就是建立锁和会话(锁的申请者)的关系。
在InitLocks函数中,还会在共享内存中初始化一个FastPathStrongRelationLocks变量,用来记录是否有事务已经获得了这个对象的“强锁”。
那么什么是强锁呢?从锁的相容性矩阵可以看出,AccessShareLock、RowShareLock、RowExclusiveLock这3个锁是不互相冲突的,是相容的,而且这几个锁主要用于事务中的DML操作(增删改查),数据库中最常用的操作就是DML操作,因此可以把这几个锁定义为“弱锁”,而其他5个锁则定义为“强锁”(实际上ShareUpdateExclusiveLock由于和自身冲突,不被认为是强锁)。也就是说,弱锁和弱锁之间是相容的,而弱锁和强锁之间是冲突的,如表2-4和表2-5所示。
表2-4 强锁和弱锁列表
表2-5 强锁和弱锁的相容性矩阵
如果一个事务对某个对象申请弱锁,同时能够查阅到其他事务在这个对象上没有申请过“强锁”,则可以在事务所在的会话上记录这个弱锁,而不必把这个弱锁保存到共享内存(主锁表)。
那么一个事务如何知道其他事务是否在一个对象上申请了强锁呢?如果一个事务在某个对象上申请了强锁,则会在共享内存中做一个标识,这样当一个事务申请弱锁时就会出现以下两种情况。
• 如果其他事务已经获得了这个对象的强锁,则本事务不会使用锁的Fast Path,它会按照常规的方法将锁保存到主锁表。
• 如果没有其他事务获得过这个对象的强锁,则本事务将本次的弱锁保存到本会话,也就是进入Fast Path模式。
由于DML操作是常规操作,DDL操作或DCL操作的频率不高,那么数据库大部分时间都只会使用弱锁,这就避免了频繁访问主锁表,只将弱锁保存到当前会话,提高了数据库的性能(虽然判断是否有强锁也需要访问共享内存中FastPathStrongRelationLocks中的标记,但这种访问的粒度比较小)。
在InitLocks函数中,还会为会话创建一个LockMethodLocalHash锁表,我们可以叫它本地锁表。在一个事务中,对同一个对象可能会多次申请同类型的锁。当第一次申请这个类型的锁时,我们需要保证它和其他事务不冲突,然后才可以获得这个锁。同时,一旦事务获得了某个锁,根据严格两阶段锁协议,这个锁要到事务结束才会被释放,因此当事务重复在同一个锁对象上申请同类型的锁时,就无须做冲突检测,可以认为自己已经获得了这个锁,只要将这个锁记录在本地就可以了。
总而言之,在PostgreSQL中,常规锁主要保存在以下4个位置。
• 本地锁表:对重复申请的锁进行计数,避免频繁访问主锁表和进程锁表,相当于一层“缓存”。
• 快速路径(Fast Path):将对“弱锁”的访问保存到本进程,避免频繁访问主锁表和进程锁表。
• 主锁表:保存一个锁对象的所有相关信息。
• 进程锁表:保存一个锁对象中与当前会话(进程)相关的信息。