一次性全讲透 GaussDB(DWS)锁的问题
本文分享自华为云社区《GaussDB(DWS)锁问题全解》,作者: yd_211043076。
一、gaussdb 有哪些锁
1、常规锁:常规锁主要用于业务访问数据库对象的加锁,保护并发操作的对象,保持数据一致性;常见的常规锁有表锁(relation)和行锁(tuple)。
表锁:当对表进行 DDL、DML 操作时,会对操作的对象表加锁,在事务结束释放。
行锁:使用 select for share 语句时持有该模式锁,后台会对 tuple 加 5 级锁;使用 select for update, delete, update 等操作时,后台会对 tuple 加 7 级锁(ExclusiveLock)。
2、轻量级锁:轻量级锁主要用于数据库内部共享资源访问的保护,比如内存结构、共享内存分配控制等。
二、锁冲突矩阵
1、常规锁按照粒度可分为 8 个等级,各操作对应的锁等级及锁冲突情况参照下表:
2、几种锁冲突的场景:
ACCESS SHARE 与 ACCESS EXCLUSIVE 锁冲突例子:session 1 在事务内对表进行 truncate,且 lockwait_timeout 参数设置为 10s;session 2 查询该表,此时会一直等到 session 1 释放锁,直到等锁超时。
ROW SHARE(行锁冲突的例子):并发 insert/update/copy;session 1 在事务内对有主键约束的行存表进行更新;session 2 对同一主键的行进行更新,会一直等待 session 1 释放锁,直到行锁超时;
并发更新列存表出现等锁超时,该现象一般为并发更新同一 CU 造成的;
场景构造:session 1 在事务内对列存表进行更新,不提交事务;session 2 同样对列存表更新,会等锁超时;(只有更新的为同一 CU 时才会出现此场景)
列存表并发等锁原理:https://bbs.huaweicloud.com/blogs/255895 ;
三、锁相关视图
pg_locks 视图存储各打开事务所持有的锁信息,需关注的字段:locktype(被锁定对象的类型)、relation(被锁定对象关系的 OID)、pid(持锁或等锁的线程 ID)、mode(持锁或等锁模式)、granted(t:持锁,f:等锁)。
pgxc_lock_conflicts 视图提供集群中有冲突的锁的信息(适合锁冲突现场还在是使用),目前只收集 locktype 为 relation、partition、page、tuple 和 transactionid 的锁的信息,需要关注的字段 nodename(被锁定对象节点的名字)、queryid(申请锁的查询 ID)、query(申请锁的查询语句)、pid、mode、granted。
pgxc_deadlock 视图获取导致分布式死锁产生的锁等待信息,只收集 locktype 为 relation、partition、page、tuple 和 transactionid 的锁等待信息。
四、锁相关参数介绍
lockwait_timeout:控制单个锁的最长等待时间。当申请的锁等待时间超过设定值时,系统会报错,即等锁超时,一般默认值为 20min。
deadlock_timeout:死锁检测的超时时间,当申请的锁超过该设定值仍未获取到时,触发死锁检测,系统会检查是否产生死锁,一般默认值为 1s。
update_lockwait_timeout:允许并发更新参数开启时,控制并发更新同一行单个锁的最长等待时间,超过该设定值,会报错,一般默认值为 2min。
以上参数的单位均为毫秒,请保证 deadlock_timeout 的值大于 lockwait_timeout,否则将不会触发死锁检测。
五、锁等待超时排查
https://bbs.huaweicloud.com/blogs/280354
六、为什么会死锁(单节点死锁)
1、死锁:两个及以上不同的进程实体在运行时因为竞争资源而陷入僵局,除非外力作用,否则双发都无法继续推进;而数据库事务可针对资源按照任意顺序加锁,就有一定几率因不同的加锁顺序而产生死锁。
2、死锁场景模拟:
锁表顺序不同,常见于存储过程中
第一时刻:session 1:先拿到 lock_table2 的 8 级锁,此时 session 2 拿到 lock_table1 的 8 级锁;第二时刻:session 1:再尝试申请 lock_table1 的 1 级锁; session 2 :尝试申请 lock_table2 的 1 级锁;两个会话都持锁并等待对方手里的锁释放。
GaussDB(DWS)会自动处理单点死锁,当单节点死锁发生时,数据库会自动回滚其中一条事务,以消除死锁现象。
3、一些死锁场景
vacuum full 与 delete select 语句造成的死锁(等同一对象的不同锁);部分业务场景下,存在查询时间窗在白天,而业务跑批删除只能在晚上执行,同样为了保证查询效率降低脏页率,对业务表的 vacuum full 操作也在晚上,时间窗重合,升锁过程便可能产生死锁;
上述场景下 vacuum full 语句申请 1:ExclusiveLock 并持有,后续 delete from 语句申请 2:cessShareLock 并持有;vacuum full 升级锁 3:AccessExclusiveLock 失败;delete from 升级锁 4:RowExclusiveLock 失败;两个语句形成死锁。
ater 列存表与 select max(a)的死锁,两条语句只涉及一张表,但仍旧会产生死锁,列存表有 CUdesc 表及 delta 表,语句在行时拿锁顺序不同,便可能产生死锁
列存表查询 max(col)时,尽管并没有开启 delta 表,也会获取 delta 表的锁,alter table 也一样,此时同一个操作对象变存在两个独立的资源(主表与 delta 表,其实还应该包含 CUdesc 表),不同拿锁顺序变产生这种两个语句操作同一张表死锁的现象。
upsert 的死锁现象:行存带主键约束或列存表场景下并发 upsert,并发更新重复的数据,且不同事务内部更新的相同数据的顺序不同;
该场景主要为分别从两个数据源做并发导数(upsert 方式)时,时间窗未区分开,且数据也存在重复的可能性,此时便可能存在以不同的顺序分别更新相同数据(行)的现象,就会引发死锁现象,导致某一次导数任务失败,可选择业务侧将两个任务区分到不同时间窗去执行来规避该死锁现象。
七、分布式死锁
DWS 的 share nothing 结构,使得一条语句可能在不同的节点上执行,在这些节点上都要对操作对象申请锁,且同样存在以不同顺序申请锁的可能,因此便存在分布式死锁的场景
1、如何排查分布式死锁:
先构造一个分布式死锁场景,如下图,session 1 在 CN 1 上开启事务并先查询 lock_table1;此时 session 2 在 CN 2 上开启事务并查询 lock_table1,然后两个会话分别执行 truncate 表:
通过查询分布式死锁视图:select * from pgxc_deadlock order by nodename,dbname,locktype,nspname,relname;
根据查询结果,可以看出在构造的该场景下:
CN_5001 的 truncate 语句线程号为:139887210493696;在等待线程号为:139887432832768 的 truncate 语句释放 lock_table1 的 AccessShareLock(事务中 select 语句持有的锁),同时该线程:139887210493696,持有 lock_table1 的 AccessExclusiveLock;
CN_5004 的 truncate 语句线程号为:139887432832768;在等待线程号为:139887210493696 的 truncate 语句释放 lock_table1 的 AccessExclusiveLock;同时该线程:139887432832768 持有 lock_table1 的 AccessShareLock;这种 场景下在不同实例上分布式的等待关系,便形成了分布式死锁。
2、消除分布式死锁:
对于分布式死锁的场景,一般在一个事务因为等锁超时后事务回滚,另一个未超时的事务便能继续进行下去;人为干预的情况,则需要调用 select pg_terminate_backend(pid),查杀掉一个持锁语句,破坏环形等待条件,便可让另一个事务继续执行下去。
版权声明: 本文为 InfoQ 作者【华为云开发者联盟】的原创文章。
原文链接:【http://xie.infoq.cn/article/db21a0583f75e39082ec93e6a】。文章转载请联系作者。
评论