1. 程式人生 > 其它 >PostgreSQL死鎖案例分析(二)

PostgreSQL死鎖案例分析(二)

PostgreSQL死鎖案例分析(二)

原作者:陳雁飛 創作時間:2019-09-26 15:28:14+08   採編:wangliyun

釋出時間:2019-09-27 08:08:14

歡迎大家踴躍投稿,投稿信箱:[email protected]

 評論:1    瀏覽:4684

作者介紹

陳雁飛,開源PostgreSQL愛好者,一直從事PostgreSQL資料庫運維工作

問題現象

接前一篇文章,這裡繼續介紹在工作中遇到的一個死鎖案例。經過對業務模型的抽取分析(後面會介紹表結構和資料,業務模型來源於開源元件的實際業務),模擬得到的死鎖日誌資訊如下:

2019-09-01 21:01:08.359 CST [1482] ERROR:  deadlock detected
2019-09-01 21:01:08.359 CST [1482] DETAIL:  Process 1482 waits for ShareLock on transaction 523; blocked by process 1610.
        Process 1610 waits for ShareLock on transaction 524; blocked by process 1482.
        Process 1482: select test2.a,test2.b,test2.c from test2 join test1 on test2.a = test1.a where test2.b = 2 and test1.c = 3 for update;
        Process 1610: delete from test1 where a = 1;
2019-09-01 21:01:08.359 CST [1482] HINT:  See server log for query details.
2019-09-01 21:01:08.359 CST [1482] CONTEXT:  while locking tuple (0,1) in relation "test1"
2019-09-01 21:01:08.359 CST [1482] STATEMENT:  select test2.a,test2.b,test2.c from test2 join test1 on test2.a = test1.a where test2.b = 2 and test1.c = 3 for update;

從資料庫日誌上看,記錄的SQL語句涉及兩張表TEST1和TEST2,其中一個事務執行的SQL是SEELCT … FOR UPDATE用於獲取行級鎖操作。

流程梳理

經分析,事務操作涉及兩張表,簡化後的表結構以及操作邏輯如下:

create table test1(a int primary key, b int, c int);
create table test2(a int references test1 on delete cascade,b int, c int);

insert into test1 values(1,2,3),(2,3,4),(3,4,5);
insert into test2 values(1,2,3),(2,3,4),(3,4,5);

表TEST1

表TEST2

從表結構上可以看到,表test2和test1構成外來鍵約束關係,並且是級聯刪除的關係,導致在刪除TEST1表中的時候,資料庫會自動請求對TEST2表中對應行的刪除操作。根據業務操作模型,整理得到的執行SQL邏輯如下(這裡僅僅列舉出事務中涉及鎖相關的操作,其他查詢操作未列舉出):

從整理的SQL操作上看,事務一僅僅涉及到對TEST1表的操作,但是由於存在外來鍵級聯刪除的關係,在delete語句的執行的時候,會請求TEST2表相應的行進行刪除。事務二主要是一個SELECT .. FOR UPDATE操作,但是查詢語句中涉及兩表join關聯,且最後鎖定的行和事務一中請求的TEST2表相同。因此,可以推測事務二執行的時候,依次涉及對TEST2表和TEST1表的加鎖操作。

由於是涉及到行級鎖的操作,需要藉助gdb工具進行除錯,控制事務2獲取鎖的邏輯順序。首先,根據執行計劃資訊,找到載入的行級鎖的函式。

在資料庫中,SQL的執行按照生成的執行計劃完成的,該執行計劃中最頂層運算元是LockRows,對應到執行器中的函式是ExecLockRows,結合程式碼,對行級元素加鎖的操作如下:

test = table_tuple_lock(erm->relation, &tid, estate->es_snapshot,
                    markSlot, estate->es_output_cid,
                    lockmode, erm->waitPolicy,
                    lockflags,
                    &tmfd);

因此,使用gdb除錯工具在該處函式加上斷點,erm記錄請求行級鎖對應的表資訊。操作結果如下:

可以看到對應表OID為16389,查詢資料庫該對應的表為TEST2,表明此時事務二已經獲取到TEST2對應結果的行級鎖資訊,此時繼續執行事務一中的刪除操作,該事務將被阻塞。

除錯工具中繼續執行事務二操作之後,出現前文中描述的死鎖資訊,如下:

表明事務二中執行的語句先獲取TEST2的行鎖,然後獲取TEST1的行鎖,與事務一種的操作獲取鎖順序正好相反,由於操作的是相同行,從而導致了死鎖發生。

進一步分析

這裡查詢TEST2對應的語句返回結果只有一條記錄,且不需要獲取TEST1表中的結果,在手冊上對FOR UPDATE鎖的解釋是“FOR UPDATE causes the rows retrieved by the SELECT statement to be locked as though for update. ”,表示對查詢語句檢索的行請求鎖,這裡沒有請求TEST1表的資料,為什麼也請求該表上的行級鎖呢?從直觀感覺上看是沒有必要的。

其實這個和資料庫的執行器獲取資料邏輯有關,處理函式為ExecutePlan,在該函式中迴圈處理每顆Plan並獲取對應的所有元組,然後根據前面儲存的junk filter資訊(ExecFilterJunk函式),獲取需要的目標元組。這樣,在執行的時候,需要檢索Plan中所有表中滿足條件的行。

總結

死鎖案例比較簡單,並且一般出現在高併發情況下,如果不對業務修改,單純從資料庫角度加鎖消除死鎖,可能會犧牲併發效能,這一點需要注意。同時,由於行級鎖從系統層面無法檢視,可以藉助gdb等除錯工具方式控制行級鎖的載入順序,從而構造出死鎖的情況。