锁介绍
锁是一种数据库的信号量管理机制,作用是保证SQL执行上不同事务的隔离性。
事务的四个特性(ACID):
- Atomicity:原子性,要求一个包含多个操作的事务的结果,要么全部提交,要么全部不提交;
- Consistency:一致性,要求事务提交后,数据库从一个一致状态转化到另一个一致状态,或事务失败回滚后,数据库恢复到事务执行前的一致状态。这里举的最多的例子就是多个用户银行转账,假设这些用户的总余额是500元,那么不管如何转账,有多少并发事务执行,结束后总余额应该还是500元。
- Isolation:隔离性,要求多个事务并发执行时,一个事务不会看到其他事务的中间结果。
- Durability:持久性,要求事务提交后,该事务对数据库的更改就持久化保存在数据库中;
当一个query发起时,在Hologres中其经过的链路如下:Frontend解析SQL,Query Engine生成执行计划等,然后Storage Engine读取数据,整个链路的锁有两种:
- FE Locks(前端锁):Frontend为接入层,兼容Postgres协议,因此锁也会兼容Postgres的部分锁,主要用来管理FE元数据信息。
- BE Locks(后端锁):Backend是指Query Engine和Fixed Plan,将会享有Hologres自带的锁,主要用于管理Storage Engine的schema和数据
下面将会对Hologres两种锁的具体介绍。
FE Locks
Hologres接入层Frontend兼容PostgreSQL,因此在接入层的锁与PG兼容。PostgreSQL提供了几种锁模式来控制并发的数据访问:Table-level Lock,Row-level Lock,Advisory Lock。 目前Hologres兼容的是Table-level Lock和Advisory Lock
说明:Hologres目前不支持显示的设置锁命令以及Advisory Lock相关的 UDF
Table-level Locks
1、锁分类
Table-level Lock是指表锁,表锁包含的锁种类有:
锁分类 |
锁介绍 |
备注 |
ACCESS SHARE |
一般情况下只有 SELECT 命令会获取相关表的这个锁 |
|
ROW SHARE |
只有 SELECT FOR UPDATE 和 SELECT FOR SHARE 这两个命令需要获取目标表的这个锁,非目标表(比如 JOIN 关联的其他表)仅获取 ACCESS SHARE 锁 |
Hologres并不支持这两个命令,因此无需额外关注 |
ROW EXCLUSIVE |
UPDATE,DELETE,以及 INSERT 这种修改数据的命令需要获取这个锁 |
需要结合BE Locks一起关注 |
SHARE UPDATE EXCLUSIVE |
为了防止 vacuum 和并发的 schema 变更发生冲突的锁,需要拿这个锁的命令:
|
重点关注analyze命令 |
SHARE |
只有 非 concurrently 的 create index 需要这个锁 |
关注create index命令 |
SHARE ROW EXCLUSIVE |
为了防止并发的数据修改的锁,需要拿这个锁的命令:
|
Hologres 不支持这个命令,无需关注 |
EXCLUSIVE |
仅有 REFRESH MATERIALIZED VIEW CONCURRENTLY 命令需要这个锁 |
Hologres 不支持这个命令,无需关注 |
ACCESS EXCLUSIVE |
完全独占访问需要的锁,与其他所有的锁都冲突,需要拿这个锁的命令有:
|
需要重点关注的锁,在Hologres中,执行的DDL操作都需该锁,会与其他锁冲突 |
2、锁超时时间
FE Locks没有默认超时时间,需要业务设置超时时间,防止锁等待时间过长。
3、锁冲突关系
锁之间的冲突关系如下表所示,其中叉号表示冲突,表示一个操作拿着锁,其余操作都需要等锁:
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 |
Advisory Locks
大部分情况业务无需额外关注,因此本文不再做详细介绍。
BE Locks
1、锁分类
在Hologres中,BE Locks锁(后端锁)分类如下:
锁分类 |
锁介绍 |
备注 |
Exclusive(X) |
排他锁(也叫互斥锁),当事务需要修改一批或一条数据时申请排他锁,例如DML语句(DELETE、INSERT、UPDATE)。排他锁申请成功的前提是同一资源上没有其他的共享锁或排他锁,当排他锁申请成功后,锁资源上将不能同时有其他锁; |
|
Shared(S) |
共享锁,当事务需要读取一批或一条数据时申请共享锁,以免其他事务对将要读取的数据做修改。同一个资源上允许存在多个共享锁,即允许DQL之间并发执行,因为DQL不会改变资源本身。 |
|
Intent(I) |
意向锁,这种类型的锁通常用于表达锁的层次结构,同一个资源上允许存在多个意向锁,当申请成功后,该资源上就不能有排他锁。例如,当事务申请一行的排他锁时,它会同时申请表的意向锁(表是比行更高层次的资源),以防止其他事务申请表的排他锁。 |
2、超时设置
BE Locks超时时间默认为5min,超过该时间则报错
3、锁冲突关系
BE Locks的冲突关系如下,其中叉号表示冲突,表示一个操作拿着锁,其余操作都需要等锁:
DDL分类 |
DROP |
ALTER |
SELECT |
UPDATE |
DETELE |
INSERT (包含INSERT ON CONFLICT) |
DROP |
X |
X |
X |
X |
X |
X |
ALTER |
X |
X |
X |
X |
X |
|
SELECT |
X |
|||||
UPDATE |
X |
X |
X |
X |
X |
|
DETELE |
X |
X |
X |
X |
X |
|
INSERT (包含INSERT ON CONFLICT) |
X |
X |
X |
X |
X |
锁的作用范围
锁的作用范围
根据锁的分类,不同的锁作用范围不同,其中:
1、FE Locks
FE Locks只会作用在表对象上,无关数据,要么成功或者卡住,卡住就代表有锁冲突,在等待锁。
2、BE Locks
BE Locks会作用在数据或者表结构上,因此在作用范围上会分为表数据锁、行数据锁、表结构(schema)锁,其中:
- 表数据锁:代表整个表的数据都需要拿锁,如果多个任务同时都需要拿表锁,会造成任务等待锁现象,从而任务延迟
- 行数据锁:行锁是指整行数据拿锁,在执行效率上会更高,其中走fixed plan的Query都是行锁或者表schema
- 表schema锁:表结构锁,当事务需要读取或修改表结构时申请Schema锁,绝大部分的事务都会申请Schema锁。目前主要有如下类型的Schema锁:
- SchX:Schema排他锁,用于DDL语句,目前只有DROP TABLE;
- SchU:Schema更新锁,用于修改表结构的DDL语句(ALTER TABLE或set_table_property);
- SchS:Schema稳定锁,用于DML语句,确保数据插入期间表结构不会发生改变;
- SchE:Schema存在锁,用户DML和DQL语句,确保读写数据期间表不会被删除;
说明:
1、SchU是DDL锁更细粒度的管控,允许ALTER TABLE期间DQL正常运行,无需等待;SchX是最粗粒度的DDL排他锁,所有的DDL、DML、DQL都会等待;
2、如果start query耗时较长,则可能在等待BE Locks。
在Hologres中,其中常见命令作用的锁范围如下:
说明:
1、非fixed plan的写入/更新/删除都为bulkload
2、create index命令目前是指创建json相关索引,见文档。
3、DDL命令包括CREATE、DROP、ALTER等
操作/锁范围 |
FE Locks |
BE Locks |
||
表锁 |
表数据锁 |
行数据锁 |
表Schema锁 |
|
CREATE |
✅ |
不涉及 |
||
DROP |
✅ 说明:一旦DROP拿锁,无法执行其他命令,其他命令会等锁,直到发现表被drop了,则命令失败 |
✅ 与其他操作都冲突 |
||
ALTER |
✅ 大部分行为与drop一致 |
✅ 拿schema更新锁,在拿锁中途可以对表进行select |
||
SELECT |
✅ 拿锁的同时可以执行insert、update、delete,只与DDL锁冲突 |
✅ 说明:select时除了不能Drop,其余操作都可以做 |
||
INSERT (包含INSERT ON CONFLICT) |
✅ 与create index、DDL冲突 |
如果是bulkload写入则拿表锁 说明:fixed plan和bulkload是相互冲突 |
如果是fixed plan则是行锁 说明:如果有离线写入,则不能同一个时刻同一个表既有离线又有实时fixed plan写入 |
✅ 与DDL冲突,与DML冲突 |
UPDATE |
✅ 与create index、DDL冲突 |
✅ 与DDL冲突,与DML冲突 |
||
DELETE |
✅ 与create index、DDL冲突 |
✅ 与DDL冲突,与DML冲突 |
事务相关的锁说明
Hologres 目前仅支持 DDL 的显式事务,并不支持单纯的 DML 的事务,也不支持 DDL 和 DML 混合的事务:
- Hologres 不支持嵌套子事务
- 单纯的 DML 事务虽然语法上可以通过,但是实际不支持原子提交和回滚。
如下DML即使 insert 成功,如果之后的 update 失败了,insert 的数据也不会被回滚。
begin;insertinto t1(id, salary)values(1,0);update t1 set salary =0.1where id =1;commit;
- 纯 DDL事务可以按预期的方式工作
其中任何一行 DDL 命令失败则会整个事务被回滚。 比如下面第4行 alter 命令失败时,前面 create 和 drop 动作会被回滚。
begin;createtable t1(i int);droptable if exists t2;altertable t1 add column n text;commit;
- DDL 和 DML 命令混合的事务会被禁止
如下示例,当事务中包含DDL和DML时,DML命令就会报错。
begin;createtable t1(i int);update t1 set i =1where i =1;ERROR:UPDATEin ddl transaction isnot supported now.
- 显式事务中任何一个命令获得的锁都只会在整个事务结束(提交或回滚)时才统一释放
如下示例,当对父表做alter操作时,会同时获取父表(login_history)和子表(login_history_202001)的 ACCESS EXCLUSIVE锁, 但是这个命令执行完不会立即释放对应的锁,而是要等待最后的第 10 行的 commit 执行完(不管成功还是失败)才会释放锁。若是一直不commit,则会一直拿着锁,这时若有其他对这个表的DDL操作,则会锁表并报错。
-- suppose we have three tablescreatetable temp1(i int, t text);createtable login_history(ds text, user_id bigint, ts timestamptz) partition by list (ds);createtable login_history_202001 partition of login_history for valuesin('202001');begin;altertable login_history_s1 add column user_id bigint;droptable temp1;createtable tx2(i int);commit;
锁排查
上面介绍的两种锁,FE Locks在实际业务中是最常见也最容易引起问题的锁。当锁住时,可以通过系统表查询锁住的表以及SQL,下面将会具体介绍如何操作。
在具体的锁排查介绍之前,先了解需要用到的Postgres系统视图或者表,通过这些信息的结合来查出锁信息:
- pg_stat_activity: 查看当前实例正在运行的SQL。
- pg_locks: 查看当前实例正在运行中的进程或者正在等待的锁。
步骤1:查询运行时间较长的Query(可选)
通过pg_stat_activity查看运行时间较长的Query以及对应的状态,若是有Query长时间不结束,可能是这个表可能已经有其他的操作被拿锁,导致这个Query等锁。
说明:客户端可以通过参数设置 application_name,更容易排查问题。 例如在通过命令行 psql 连接时,可以通过环境变量 PGAPPNAME 来指定: PGAPPNAME=mac-sql psql -h holo-url -p 80 -d postgres
select datname, pid, application_name, wait_event_type, state, backend_xid, backend_start, xact_start, query_start, query from pg_stat_activity where backend_type in('client backend');
步骤2:查询是否有锁
通过查询是否有锁,排查长时间运行的query是否被锁住。通过以下命令查看是否有锁,f代表false,表示当前 pid 进程正在等待其他进程释放锁。
-- 查看所有在等的锁,持有的锁,及持有/等待的进程等信息select*from pg_locks where granted ='f';
若是知道哪个query运行时间比较长,可以通过pid反查是否有锁住,若是出现“f”则说明在等待锁:
select*from pg_locks where pid =<pid>;
步骤3:排查哪个进程在拿锁
通过步骤2查看知道哪个pid进程在等锁,根据其oid(即表关系)通过以下查看是否有拿锁,t代表true,代表正在拿锁。
-- 查看拿到了表锁的进程信息select pid from pg_locks where relation =<OID>and granted ='t';
步骤4:排查拿锁的query
通过步骤3结果的PID,查询真正拿锁的query
select*from pg_stat_activity where pid =<PID>;
步骤5:释放锁
找到拿锁的query之后,可以直接将query进行kill,以释放锁
select pg_cancel_backend(<pid>);
常见问题
报错:internal error: Cannot acquire lock in time, current owners: [(Transaction =302xxxx, Lock Mode = SchS|SchE|X)]."
报错原因:执行query的表有被其他query拿住BE Locks(如报错Lock Mode = SchS|SchE|X,则为Schema稳定锁、Schema存在锁、排他锁),导致query等BE Locks超时(5min)从而报错
解决办法:报错中的transaction id即Transaction =302xxxx对应query id,可以通过transaction id在慢query日志或者活跃Query中查对应拿锁的query。
报错:ERROR: Operation timed out: Wait schema version timeout.: Server current target schema version:xxx is late from request schema version: yyy
报错原因:执行DDL后,会先在Frontend(FE)节点执行,再异步在Storage Engine(SE)执行,当FE执行DDL结束后会对节点Version(版本)进行更新,若SE的DDL还没执行完成,导致SE的版本比FE的版本低,此时query会等待SE执行DDL,如果超过5min后SE仍然没有执行完成,就会报错等待schema版本超时。
解决办法:
- 先Kill掉等锁的DML,然后重试Query
- 重启实例(极端手段)
报错:The requested table name: xxx (id: 10, version: 26) mismatches the version of the table (id: 10, version: 28) from server
报错原因:执行DDL后,会先在Frontend(FE)单个节点执行,再异步在Storage Engine(SE)执行。SE已经完成DDL并更新版本,但是因为FE节点较多,节点间Replay未完成,导致部分FE节点的版本低于SE,而Query正好在版本较低的FE中执行,从而出现报错。
解决办法:
- 多重试几次query
- 若是超过几分钟还报错建议重启实例(极端手段)
报错:internal error: Cannot find index full ID: 86xxx (table id: 20yy, index id: 1) in storages or it is deleting!
报错原因:执行DROP Table或者Truncate Table时,对这个表进行DML/select,DROP/Truncate需要拿DDL大锁,导致query需要等锁,直到发现表被Drop了从而报错
解决办法:执行DROP/Truncate时,不对表进行Query