spin lock
使用 cas 去获取锁,先获取 spins_per_delay 次数,如果还失败,则每次获取失败将 delay 时长延长至 1~2倍 delay 值加 0.5 us,spins_per_delay 的值在获取锁后会做更新,如果这次没有等待,则下次可以多尝试 100 次(最多不超过1000次),如果这次第一次尝试是失败的,则下次尝试少一次,(最少 10 次)
fast path 加锁失败路径
首先将 fast path 锁转移至 hash 表
锁的链接
lock 指锁实例。
lock.procLocks 指获取锁的进程对应这个锁的 procLock 链表,可以通过 (PROCLOCK *)(lock.procLocks.next - 32) 来还原。
lock.waitProcs 指等待这一锁的进程结构的链表。可以 通过 (PGPROC *)lock.waitProcs.links.next 来还原
注:链接为空的条件不是next 和 prev 为空,而是 next 和 prev 都指向链表自己(lock.procLocks.next == &lock.procLocks)
regular 锁授予
每当请求一个 lock 的一个 mode 时,lock->requested[mode] 就会加1,当成功授予一个锁时,lock 上的 lock->granted[lockmode] 计数会加一(如果 grant[mode] 数与 request[mode] 数一样时,还会把 waitmask[mode] 置为 false),且 grantMask[mode] 置为 true
锁请求与等待进程的冲突检查
锁冲突矩阵:
| Requested Lock Mode | ACCESS SHARE | ROW SHARE | ROW EXCLUSIVE | SHARE UPDATE EXCLUSIVE | SHARE | SHARE ROW EXCLUSIVE | EXCLUSIVE | ACCESS EXCLUSIVE |
|---|---|---|---|---|---|---|---|---|
| ACCESS SHARE | X | |||||||
| ROW SHARE | X | X | ||||||
| ROW EXCLUSIVE | X | X | X | X | ||||
| SHARE UPDATE EXCLUSIVE | X | X | X | X | X | |||
| SHARE | X | X | X | X | X | |||
| SHARE ROW EXCLUSIVE | X | X | X | X | X | X | ||
| EXCLUSIVE | X | X | X | X | X | X | X | |
| ACCESS EXCLUSIVE | X | X | X | X | X | X | X | X |
锁请求与等待进程的冲突检查:
lock->waitMask 表示等待锁的人等的 lock mode,如果自己即将获取的锁与其它等待进程的 lock mode 冲突(比如我要拿 shared 锁,但有人拿了 exclusive 锁),意味着我拿这个锁后会有人等我,即有冲突。
锁请求与拿锁进程的冲突检查:
如果我要拿的锁与已经拿锁人的 mode 不冲突(比如有人拿了share update,有人在等 share,而我要拿的是 row share,与两者都不冲突),则检查无冲突
如果我要拿的锁与我自己的锁冲突,或与我自己所在组拿的锁冲突,则不算冲突,检查如下:
- 将我的 proclock→holdmask 中的mode 从 lock→granted[mode]中减1,看是否还有 lock→granted 与我将要拿的mode 冲突
- 将已经拿锁的 lock->procLocks 中属于我的锁组的找出来,将这部分从 lock→granted[mode] 中减去1后,看是否还有 lock→granted 与 我要的 mode 冲突
检查到冲突时等待队列位置判断:
如果有冲突,则要等锁 WaitOnLock→ProcSleep,这里需要判断自己加到等待队列的什么位置,这里的判断需要找到冲突的锁组,将自己加在冲突锁组的前面
- 找出所有我在锁组获取的 lock mode
- 从前往后找出其它锁组的等待进程,检查它们是否等待我在的锁组(它们等的锁 mode 与我的锁组拿的锁 mode 冲突)
- 如果它们在等我的锁组,我也在等它们的锁组(它们拿的锁 mode 与我等的锁 mode 冲突),则发现死锁,这种情况不用等,直接 error
- 如果它们不需要等我(我与它无冲突),则我排在它后面
- 如果找到了等我的进程,而我又不需要等它们,则我要的等待要排在它前面
- 这里代码里有一个特殊情况(我感觉,事实上这种情况应该永远不存在,因为前面已经过滤掉了)如果排在前面的人,它们等我的锁与我要等的锁不冲突,意味它们虽然等我的锁组,但等的不是我,而是等我的锁组其它成员(比如前面都等 shared row exclusive 锁,我要等的是 share 锁与前面等待的锁不冲突,但与我们组拿的row exclusive 锁冲突,则我也可以直接拿这个 share 锁),这种情况要再判断一次是否是与我自己所在组拿的锁冲突,如果是这种情况,不用 sleep,直接获取。
以后每次唤醒,我需要去检查一次死锁,如果是被 auto vacuum 卡了,则允许我 kill 它一次。
锁的使用
我们可以人为将数据分为三类:表的 schema,表内 data,表数据的 index。对 index 加锁前一定要先锁表,以避免死锁,释放时也是先放index锁,再放数据锁
系统表的锁倾向于用完就释放,普通表的锁倾向于持有到 transaction 结束。
锁的选择主要围绕两个问题:
- 写是否影响读
- 写是否影响写
pg 中的 8 种锁使用并非绝对,很可能随着新功能的增加,或者新约束的增加而增减锁的等级。
代码中的使用:
| Lock Mode | 说明 |
|---|---|
| AccessShareLock(5) | 最弱锁,允许有人在写数据,pg 用 MVCC 保证读的一致性,任何 data、index 的写都不影响结果,用于 select |
| RowShareLock(6) | 用于读数据,但有写的意向,用的比较窄,代码中几乎找不到它的存在,它主要是为了支持添加谓词锁 SELECT FOR SHARE/UPDATE,它不影响别人读 data 和创建 index,但不允许变更 schema |
| RowExclusiveLock(4-1) | 用于写数据,用于对系统表 update 和对普通表做 DML 的场景,很多 recovery 阶段可能不需要这个锁(但要保证 readonly 或者只有一个人修改) |
| ShareUpdateExclusiveLock(4-2) | 允许其它人多写数据,用于单写 index和单做 analyze,与 RowExclusiveLock 是一对,ShareLock 与SharerowExclusiveLock 的 concurrent 版本,允许多写,但要保证多写之间没有冲突,且不影响多读。 |
| 相比于 ShareRowExclusiveLock,它允许修改表的数据,但要保证 index 同一时间只有一个人改 index,代码中它的使用比较混乱(我感觉很多场景用 RowExclusiveLock 也是 ok 的,只是为了防止以后添加新功能时会受到影响,索性直接升级到了ShareUpdateExclusiveLock)。 | |
| 它可以当作辅助性的 RowExclusiveLock 来使用,即一段代码可能被多进程并发访问,且不容易处理正确的场景,如 analyze,create index(其中 index 是个很典型场景,因为他介于 schema 与 data 之间),如果能保证一段 index 相关代码不会被并发访问到,则可以用 RowExclusiveLock 来代替,如果一个表 data 的修改会影响到另一个表的 data,则需要升级到更高的ShareRowExclusiveLock,比如一个表的数据实际是另一个表的外键的场景。 | |
| ShareRowExclusiveLock 加在 index 还是加在表上没有太懂,似乎做 analyze 时加在表上,做 build index 时加在表和 index 上。 | |
| ShareLock(1-1) | 用于读数据,与 ExclusiveLock 对应, 有两种含义: |
| • 加在表上:与 AccessShareLock,RowShareLock 一样允许多读,不同点在于完全不允许写表。可以作为一个 pause 动作出现,即等所有修改表数据的行为完成再读,读期间不允许别人再改数据。如果读期间,数据的修改并不影响它的可重读,则可以降级为最弱的 AccessShareLock。 | |
| • 加在其它对应上:如 advisory 锁,page buffer 锁 | |
| ShareRowExclusiveLock(3) | 用于单写数据,只允许一个人写,但写不影响别人读时,可以拿 ACCESS SHARE lock。 |
| ExclusiveLock(1-2) | ExclusiveLock 有两种含义(这两者含义不同,但用了同一个名词): |
| • 加在表上:用于写数据,写期间会影响读,但可以应用户需求允许读,它比 AccessExclusiveLock 锁弱一点,允许在改 schema 和 data 的同时访问表的数据。在表上用 Exclusive 锁,pg 只支持了并发访问 matview | |
| • 加在其它对象上:如 advisory 锁,namespace 锁等,因为对于非表的场景,share 与 exclusive 已经能说明 read 和 write 的语义了。 | |
| AccessExclusiveLock(2) | 用于写schema,最强的锁,大部分 schema 的修改会影响读的方式,必须独占进行,如 drop table,add column 等 |
锁举例:
AccessExclusiveLock:
- cluster_rel 要换掉表对应的文件,如果有人在这期间读到了表,那就会有可能读到旧的文件,这肯定是错的
- ExecRefreshMatView:创建一个 matview 后,要对 mat view 写数据,这期间无法确实数据最终能否提交,因而不允许别人读
- ATController:add column、drop column 等期间,不允许有人访问表的数据
ExclusiveLock:
- LockPage 锁加一页
- LockRelationForExtension 添加一页
- pg_try_advisory_lock_int4 添加 advisory 锁,如 holo ddl version 锁
- RenameEnumLabel:对于 pg_enum 的 label 的修改,对 pg_type 的 对应 enum 对象拿 ExclusiveLock, 以防有人删了这个 pg_enum,然后拿 pg_enum 的 RowExclusiveLock 用于修改 enum 的 label
- RangeVarGetRelidExtended(惟一用到 Exclusive 的表锁)matview
ShareRowExclusiveLock
- 增删 trigger :不允许别人对这个表做写,因为写会触发 trigger,但此时我要增删的 trigger 是否存在还不确定
- 添加 foreign key :不允许别人修改外键在的表的数据,同时会影响 trigger
- CollationCreate:创建前会先判断是否存在,为了防止多个进程创建同一个 collation,只允许一个人写,但这个写不会影响读
- AlterSequence:不太理解,它后面还会再拿一次行排他锁
ShareLock
- AlterDomainNotNull:找基于这个 domain(带check 的列类型)的所有列,这期间不允许有人写domain相关的表,要拿这些表的 share lock
- ReindexTable:reindex 期间不允许写源表
- bt_check_every_level:读 index,期间不允许写,因为写会破坏 btree 结构,可能使check拿到假结果
ShareUpdateExclusiveLock
- analyze_rel:防止多人 analyze 同一表,但 analyze 期间允许别人用 rowexclusive 锁来写表
- DefineIndex(concurrently):允许创建 index 期间写数据,只允许一个进程对 index 写数据
- index_drop:只能一个人drop index,期间可以写数据,在源表上加 ShareUpdateExclusiveLock,在索引上加 AccessExclusiveLock
- AlterPublicationTables:对要添加入或移除出 publication 表的源表加 ShareUpdateExclusiveLock,因为这事不影响修改源表的数据
锁在内存中的组织


2537

被折叠的 条评论
为什么被折叠?



