鎖是一種數據庫的信號量管理機制,作用是保證SQL執行上不同事務的隔離性。本文為您介紹Hologres中的鎖及如何排查鎖。
背景信息
當一個Query發起時,在Hologres中其經過的鏈路如下圖所示。Frontend解析Query,Query Engine生成執行計劃,然后Storage Engine讀取數據,整個鏈路的鎖有如下兩種。
Frontend Locks(前端鎖)
Frontend為接入層,兼容PostgreSQL協議,因此鎖也會兼容PostgreSQL的部分鎖,主要用來管理Frontend(FE)元數據信息。
Backend Locks(后端鎖)
Backend是指Query Engine和Fixed Plan,將會享有Hologres自帶的鎖,主要用于管理Storage Engine的Schema和數據。
鎖行為變更說明
自Hologres V2.0版本開始,FE默認啟用了無鎖機制。當表的DDL、DQL發生沖突時(例如,在表A發生DDL變更期間有新的查詢請求),新的請求會直接報錯。如果您希望在沖突發生時不報錯,而是等待鎖釋放后再執行新的請求,您可以執行以下命令來設置GUC參數,關閉無鎖機制。
ALTER database <db_name> SET hg_experimental_disable_pg_locks = off;
從Hologres V2.1版本開始,對于無主鍵表的Bulkload操作進行了優化,改為僅獲取行鎖,而不再獲取表鎖。
鎖介紹
Frontend Locks(FE Locks)
Hologres接入層Frontend兼容PostgreSQL,因此在接入層的鎖與PostgreSQL兼容。PostgreSQL提供了Table-level Lock、Row-level Lock和Advisory Lock三種鎖模式來控制并發的數據訪問。 目前Hologres兼容的是Table-level Lock和Advisory Lock。
說明Hologres目前不支持顯式的設置鎖命令以及Advisory Lock相關的UDF。
Table-level Lock
分類
Table-level Lock是指表鎖,表鎖包含如下種類。
鎖名稱
說明
備注
ACCESS SHARE
一般情況下只有
SELECT
命令會獲取相關表的這個鎖。不涉及。
ROW SHARE
只有
SELECT FOR UPDATE
和SELECT FOR SHARE
這兩個命令需要獲取目標表的這個鎖,非目標表(比如JOIN關聯的其他表)僅獲取ACCESS SHARE鎖。Hologres不支持
SELECT FOR UPDATE
和SELECT FOR SHARE
命令,因此無需關注。ROW EXCLUSIVE
UPDATE
、DELETE
和INSERT
修改數據的命令需要獲取此鎖。需要結合Backend Locks一起關注。
SHARE UPDATE EXCLUSIVE
為了防止Vacuum和并發的Schema變更發生沖突的鎖,如下命令需要獲取此鎖。
lazy VACUUM
即非Vacuum Full
。ANALYZE
。CREATE INDEX CONCURRENTLY
。說明Hologres使用此命令時不會獲取 SHARE UPDATE EXCLUSIVE鎖,而是與非 CONCURRENTLY形式的
CREATE INDEX
一樣獲取SHARE
鎖。CREATE STATISTICS
,此命令Hologres不支持。COMMENT ON
。ALTER TABLE VALIDATE CONSTRAINT
,此命令Hologres不支持。ALTER TABLE SET/RESET (storage_parameter)
,Hologres僅支持使用此命令設置自己擴展的屬性以及PostgreSQL原生的 autovacuum_enabled屬性,不支持設置其他屬性;且設置上述屬性時不會獲取表的任何鎖。修改PostgreSQL內置的其他某些storage parameter參數需要獲取此鎖,詳情請參見ALTER TABLE。ALTER TABLE ALTER COLUMN SET/RESET options
。ALTER TABLE SET STATISTICS
,此命令Hologres不支持。ALTER TABLE CLUSTER ON
,此命令Hologres不支持。ALTER TABLE SET WITHOUT CLUSTER
,此命令Hologres不支持。
重點關注
ANALYZE
命令。SHARE
只有非concurrently的
CREATE INDEX
需要獲取此鎖。說明Hologres中創建JSON索引時會需要拿此鎖。
關注
CREATE INDEX
命令(創建JSON相關索引)。SHARE ROW EXCLUSIVE
為了防止并發的數據修改的鎖,需要獲取此鎖的命令如下。
CREATE COLLATION
,此命令Hologres不支持。CREATE TRIGGER:
,此命令Hologres不支持。部分
ALTER TABLE
命令。DISABLE/ENABLE [ REPLICA | ALWAYS ] TRIGGER
,此命令Hologres不支持。ADD table_constraint
,此命令Hologres不支持。
Hologres不支持獲取此鎖的命令,所以無需關注。
EXCLUSIVE
僅有
REFRESH MATERIALIZED VIEW CONCURRENTLY
命令需要獲取此鎖。Hologres不支持
REFRESH MATERIALIZED VIEW CONCURRENTLY
命令,無需關注。ACCESS EXCLUSIVE
完全獨占訪問需要的鎖,與其他所有的鎖都沖突,需要獲取此鎖的命令如下。
DROP TABLE
TRUNCATE TABLE
REINDEX
,此命令Hologres不支持。CLUSTER
,此命令Hologres不支持。VACUUM FULL
REFRESH MATERIALIZED VIEW (without CONCURRENTLY)
,此命令Hologres不支持。LOCK
,顯式的LOCK命令,如果不指明具體的鎖類型,默認需要這個鎖;此命令Hologres不支持。ALTER TABLE
,除去上面明確提到獲取特定鎖的ALTER TABLE
形式以外,其他的ALTER TABLE
命令形式都默認獲取這個鎖。
需要重點關注的鎖,在Hologres中執行的DDL操作都會獲取該鎖,與其他鎖沖突。
超時時間
FE Locks沒有默認超時時間,需要業務設置超時時間,防止鎖等待時間過長,詳情請參見Query管理。
沖突關系
鎖之間的沖突關系如下表所示,沖突意味著一個操作獲取鎖,其他操作獲取的鎖都需要等待。
說明表示不沖突,表示沖突。
Requested Lock Mode
ACCESS SHARE
ROW SHARE
ROW EXCLUSIVE
SHARE UPDATE EXCLUSIVE
SHARE
SHARE ROW EXCLUSIVE
EXCLUSIVE
ACCESS EXCLUSIVE
ACCESS SHARE
ROW SHARE
ROW EXCLUSIVE
SHARE UPDATE EXCLUSIVE
SHARE
SHARE ROW EXCLUSIVE
EXCLUSIVE
ACCESS EXCLUSIVE
Advisory Lock
Advisory Lock指咨詢鎖,PostgreSQL提供了一種方法創建由應用定義其含義的鎖。Hologres大部分業務情況無需額外關注。
Backend Locks(BE Locks)
分類
在Hologres中BE Locks鎖分類如下。
鎖分類
鎖介紹
Exclusive(X)
排他鎖(也稱互斥鎖),當事務需要修改一批或一條數據時申請排他鎖,例如DML語句(DELETE、INSERT、UPDATE)。排他鎖申請成功的前提是同一資源上沒有其他的共享鎖或排他鎖,當排他鎖申請成功后,鎖資源上將不能同時有其他鎖。
Shared(S)
共享鎖,當事務需要讀取一批或一條數據時申請共享鎖,以免其他事務對將要讀取的數據做修改。同一個資源上允許存在多個共享鎖,即允許DQL之間并發執行,因為DQL不會改變資源本身。
Intent(I)
意向鎖,這種類型的鎖通常用于表達鎖的層次結構,同一個資源上允許存在多個意向鎖,當申請成功后,該資源上就不能有排他鎖。例如,當事務申請一行的排他鎖時,它會同時申請表的意向鎖(表是比行更高層次的資源),以防止其他事務申請表的排他鎖。
超時時間
BE Locks超時時間默認為5min,超過該時間則報錯。
沖突關系
BE Locks的沖突關系如下,沖突表示一個操作獲取鎖,其余操作都需要等待鎖。
說明表示不沖突,表示沖突。
操作
DROP
ALTER
SELECT
UPDATE
DETELE
INSERT(包含INSERT ON CONFLICT)
DROP
ALTER
SELECT
UPDATE
DETELE
INSERT(包含INSERT ON CONFLICT)
鎖的作用范圍
根據鎖的分類,不同的鎖作用范圍不同。
FE Locks
FE Locks只會作用在表對象上,與表數據無關,且鎖只有成功或者卡住兩種狀態,卡住就代表有鎖沖突在等待鎖。
BE Locks
BE Locks會作用在數據或者表結構上,因此在作用范圍上會分為表數據鎖、行數據鎖、表結構(Schema)鎖。
表數據鎖:代表整個表的數據都需要獲取鎖,如果多個任務同時都需要獲取表數據鎖,會造成任務等待鎖現象,從而任務延遲。
行數據鎖:指整行數據獲取鎖,在執行效率上會更高,其中走Fixed Plan加速SQL執行的Query都是行數據鎖或者表Schema鎖。
表Schema鎖:即表結構鎖,當事務需要讀取或修改表結構時申請Schema鎖,大部分的事務都會申請Schema鎖。目前主要有如下類型的Schema鎖。
SchX:Schema排他鎖,用于DDL語句,目前只支持
DROP TABLE
命令。SchU:Schema更新鎖,用于修改表結構的DDL語句包含
ALTER TABLE
和set_table_property
命令。SchE:Schema存在鎖,用戶DML和DQL語句,確保讀寫數據期間表不會被刪除。
說明SchU是DDL鎖更細粒度的管控,允許ALTER TABLE期間DQL正常運行,無需等待;SchX是最粗粒度的DDL排他鎖,所有的DDL、DML、DQL都會等待。
如果Start Query耗時較長,則可能在等待BE Locks。
在Hologres中,其中常見命令作用的鎖范圍如下。表示操作命令會獲取該鎖。
非Fixed Plan的寫入、更新、刪除都為Bulkload。
CREATE INDEX
命令目前是指創建JSON相關索引。DDL命令包括
CREATE
、DROP
、ALTER
等。
操作/鎖范圍 | 表鎖 | 表數據鎖 | 行數據鎖 | 表Schema鎖 |
CREATE | 不涉及 | |||
DROP | 說明 一旦DROP命令獲取鎖,無法執行其他命令,其他命令會等待鎖,直到發現表被刪除了,則其他命令失敗。 說明: | 不涉及 | 不涉及 | 說明 與其他操作都沖突。 |
ALTER | 說明 與DROP命令一致。 | 不涉及 | 不涉及 | 說明 在獲取表Schema鎖的同時可以對表執行 |
SELECT | 說明 在獲取表鎖的同時可以對表執行 | 不涉及 | 不涉及 | 說明 執行 |
INSERT (包含INSERT ON CONFLICT) | 說明
|
說明 Bulkload與Fixed Plan相互沖突。 | 如果是通過Fixed Plan方式則獲取行數據鎖。 Hologres V2.1版本起,無主鍵表的Bulkload也改為僅獲取行數據鎖。 說明
| 說明 與DDL、DML沖突。 |
UPDATE | 說明 與 | 說明 與DDL、DML沖突。 | ||
DELETE | 說明 與 | 說明 與DDL、DML沖突。 |
事務相關的鎖說明
Hologres目前僅支持DDL的顯式事務,并不支持單純的DML事務,也不支持DDL和DML混合的事務。
Hologres不支持嵌套子事務。
單純的DML事務雖然語法上可以通過,但是實際不支持原子提交和回滾。
如下DML即使
insert
成功,如果之后的update
失敗了,insert
的數據也不會被回滾。begin; insert into t1(id, salary) values (1, 0); update t1 set salary = 0.1 where id = 1; commit;
純DDL事務可以按預期的方式工作
其中任何一行DDL命令失敗則會整個事務被回滾。例如下面
alter
命令失敗時,前面create
和drop
操作會被回滾。begin; create table t1(i int); drop table if exists t2; alter table t1 add column n text; commit;
DDL和DML命令混合的事務會被禁止
如下示例,當事務中包含DDL和DML命令時,DML命令就會報錯。
begin; create table t1(i int); update t1 set i = 1 where i = 1; -- DML命令報錯 ERROR: UPDATE in ddl transaction is not supported now.
顯式事務中任何一個命令獲得的鎖都只會在整個事務結束(提交或回滾)時才統一釋放。
如下示例,當對父表做
alter
操作時,會同時獲取父表(login_history)和子表(login_history_202001)的ACCESS EXCLUSIVE鎖, 但是這個命令執行完不會立即釋放對應的鎖,而是要等待最后的commit
執行完(不管成功還是失敗)才會釋放鎖。若是一直不執行commit
,則會一直保持鎖,這時若有其他對這個表的DDL操作,則會鎖表并報錯。-- suppose we have three tables create table temp1(i int, t text); create table login_history(ds text, user_id bigint, ts timestamptz) partition by list (ds); create table login_history_202001 partition of login_history for values in ('202001'); begin; alter table login_history_s1 add column user_id bigint; drop table temp1; create table tx2(i int); commit;
FE鎖排查
可以通過如下步驟排查是否有FE的鎖,以及出現鎖之后如何解決:
如果Query耗時較長,可以通過
wait_event_type
字段查詢當前Query是否有鎖。如下命令示例,如果結果中
wait_event_type
字段值為lock
,說明該Query有FE的鎖。--2.0及其以上版本 select query,state,query_id,transaction_id,pid,wait_event_type,wait_event,running_info,extend_info FROM hg_stat_activity where query_id = 200640xxxx; --返回結果 ----------------+---------------------------------------------------------------- query | drop table test_order_table1; state | active query_id | 200640xxxx pid | 123xx transaction_id | 200640xxxx wait_event_type | Lock wait_event | relation running_info | {"current_stage":{"stage_duration_ms":47383,"stage_name":"PARSING"},"fe_id":1,"warehouse_id":0}+ | extend_info | {} + --1.3及以下版本 select datname, pid, application_name, wait_event_type, state,query_start, query from pg_stat_activity where backend_type in ('client backend'); --返回結果 ----------------+---------------------------------------------------------------- datname | holo_poc pid | 321xxx application_name | PostgreSQL JDBC Driver wait_event_type | lock state | active query_start |2023-04-20 14:31:46.989+08 query | delete from xxx
查看鎖的持有者。
使用如下命令查看鎖的持有者:
select * from pg_locks where pid = <pid>;
pid為步驟1中返回結果中的
pid
。排查哪個進程在拿鎖。
通過步驟2判斷出當前SQL有鎖,根據其
oid
(即表關系)通過以下命令查看是否有拿鎖,t
代表true
,代表正在拿鎖。-- 查看拿到了表鎖的進程信息 select pid from pg_locks where relation = <OID> and granted = 't';
排查拿鎖的Query。
通過步驟3返回結果的PID,使用如下命令查詢真正拿鎖的Query。
select * from pg_stat_activity where pid = <PID>;
釋放鎖。
找到拿鎖的Query之后,使用如下命令可以直接將Query結束,以釋放鎖。
select pg_cancel_backend(<pid>);
BE鎖排查
如果在hg_stat_activity視圖的be_lock_waiters
和be_lock_converting_waiters
字段有數據,說明Query在后端有鎖或者被鎖,可以通過如下步驟排查。
情況1:當前Query產生鎖,其余Query在等該鎖釋放。
通過以下SQL可以看到當前SQL是否有BE鎖,以及哪些Query被該SQL鎖住,等待其釋放鎖。
select query_id, transaction_id, ((extend_info::json)->'be_lock_waiters'->>0)::text as be_lock_waiters FROM hg_stat_activity as h where h.state = 'active' and ((extend_info::json)->'be_lock_waiters')::text != ''; --返回結果 ----------------+------------------ query_id | 10005xxx transaction_id | 10005xxx be_lock_waiters | 13235xxx
情況2:查看當前Query被哪個Query鎖住,等待它釋放鎖。
通過如下SQL,查詢當前Query被誰拿著鎖,等待這個Query釋放鎖。
select query_id, transaction_id, ((extend_info::json)->'be_lock_waiters')::text as be_lock_waiters FROM hg_stat_activity as h where h.state = 'active' and ((extend_info::jsonb)->'be_lock_waiters')::jsonb ? '10005xxx'; -[ RECORD 1 ]---+------------------------------------------ query_id | 200740017664xxxx transaction_id | 200740017664xxxx be_lock_waiters | ["200640051468xxxx","200540035746xxxx"]
常見問題
報錯:
internal error: Cannot acquire lock in time, current owners: [(Transaction =302xxxx, Lock Mode = SchS|SchE|X)].
。報錯:
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)掉等待鎖的DDL,然后重試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、DELETE等)需要獲取DDL鎖,導致Query需要等鎖,直到發現表被刪除了從而報錯。解決方法:執行
DROP
和TRUNCATE
命令時,不對表執行其他Query。