2014-10-06 41 views
5

在爲PostgreSQL編寫一些SQL查詢時,我發現了一些不尋常的行爲,這讓我感到有點不安。具有顯式鎖定和併發事務的錯誤PostgreSQL查詢結果

假設我們有如下表 「測試」:

+----+-------+---------------------+ 
| id | value |  created_at  | 
+----+-------+---------------------+ 
| 1 | A  | 2014-01-01 00:00:00 | 
| 2 | A  | 2014-01-02 00:00:00 | 
| 3 | B  | 2014-01-03 00:00:00 | 
| 4 | B  | 2014-01-04 00:00:00 | 
| 5 | A  | 2014-01-05 00:00:00 | 
| 6 | B  | 2014-01-06 00:00:00 | 
| 7 | A  | 2014-01-07 00:00:00 | 
| 8 | B  | 2014-01-08 00:00:00 | 
+----+-------+---------------------+ 

有兩筆交易,A和B,並行運行。

A: begin;   /* Begin transaction A */ 
B: begin;   /* Begin transaction B */ 
A: select * from test where id = 1 for update; /* Lock one row */ 
B: select * from test where value = 'B' order by created_at limit 3 for update; /* This query returns immediately since it does not need to return row with id=1 */ 
B: select * from test where value = 'A' order by created_at limit 3 for update; /* This query blocks because row id=1 is locked by transaction A */ 
A: update test set created_at = '2014-01-09 00:00:00' where id = 1; /* Modify the locked row */ 
A: commit; 

一旦事務A提交併釋放與ID = 1,交易的阻塞查詢乙返回以下結果行:

+----+-------+---------------------+ 
| id | value |  created_at  | 
+----+-------+---------------------+ 
| 1 | A  | 2014-01-09 00:00:00 | 
| 2 | A  | 2014-01-02 00:00:00 | 
| 5 | A  | 2014-01-05 00:00:00 | 
+----+-------+---------------------+ 

這些行肯定不是「created_at有序「並且id = 1的行甚至不應該在返回的行之間。事務A和B同時運行的事實導致了事務B中的錯誤結果,如果事務一個接一個地執行,則不會發生這種結果。這似乎違反了事務隔離。

這是一個錯誤?

如果這不是一個錯誤,並且這些結果是預期的,那麼這對數據庫返回的結果的可靠性意味着什麼?如果我有一個高度併發的環境,並且後面的代碼依賴於實際按日期排序的行,則會出現錯誤。

但是,如果我們運行相同的指令序列如上,但具有下列替換更新語句:

update test set value = 'B', created_at = '2014-01-09 00:00:00' where id = 1; 

...然後阻塞查詢返回正確的結果:

+----+-------+---------------------+ 
| id | value |  created_at  | 
+----+-------+---------------------+ 
| 2 | A  | 2014-01-02 00:00:00 | 
| 5 | A  | 2014-01-05 00:00:00 | 
| 7 | A  | 2014-01-07 00:00:00 | 
+----+-------+---------------------+ 

在這種情況下,阻止的查詢會執行兩次,因爲它的初始結果會失效,因此會執行兩次

我對PostgreSQL最感興趣,但我也想知道是否支持行級鎖定的其他RDBMS,如Oracle,SQL Server和MySQL。

+0

在MySQL中,會話b中的第一個選擇已被阻止,並等待會話A提交或回滾。在Oracle中這很難重現,因爲沒有'LIMIT'子句,通常使用具有'rownum'的派生表和order by將完全改變查詢的含義。 – 2014-10-06 18:18:36

回答

5

這裏有一些事情正在進行。首先,這是有記錄的行爲。其次,你沒有看到整個故事,因爲你沒有嘗試更新會話「B」中的任何東西。

這似乎是違反事務隔離。

取決於您正在運行的隔離級別。 PostgreSQL's default transaction isolation levelREAD COMMITTED

這是PostgreSQL中的documented behavior

這是可能的在讀運行SELECT命令COMMITTED 事務隔離級別,並使用ORDER BY和鎖定子句 返回行亂序。這是因爲ORDER BY首先被應用。 該命令對結果進行排序,但可能會阻止嘗試獲取一個或多個行上的 鎖。一旦SELECT解除阻塞,排序列值中的一些可能已被修改,導致那些行 看起來無序(儘管它們按照原始列值的順序排列)。

一種解決方法(也記錄,相同的鏈接)是將FOR UPDATE移動到子查詢中,但這需要表鎖。

要查看PostgreSQL 確實在這種情況下所做的操作,請在會話「B」中運行更新。

create table test (
    id integer primary key, 
    value char(1) not null, 
    created_at timestamp not null 
); 
insert into test values 
(1, 'A', '2014-01-01 00:00:00'), 
(2, 'A', '2014-01-02 00:00:00'), 
(3, 'B', '2014-01-03 00:00:00'), 
(4, 'B', '2014-01-04 00:00:00'), 
(5, 'A', '2014-01-05 00:00:00'), 
(6, 'B', '2014-01-06 00:00:00'), 
(7, 'A', '2014-01-07 00:00:00'), 
(8, 'B', '2014-01-08 00:00:00'); 
 
A: begin;   /* Begin transaction A */ 
B: begin;   /* Begin transaction B */ 
A: select * from test where id = 1 for update; /* Lock one row */ 
B: select * from test where value = 'B' order by created_at limit 3 for update; /* This query returns immediately since it does not need to return row with id=1 */ 
B: select * from test where value = 'A' order by created_at limit 3 for update; /* This query blocks because row id=1 is locked by transaction A */ 
A: update test set created_at = '2014-01-09 00:00:00' where id = 1; /* Modify the locked row */ 
A: commit; 
B: update test set value = 'C' where id in (select id from test where value = 'A' order by created_at limit 3); /* Updates 3 rows */ 
B: commit; 

現在,看看錶。

 
scratch=# select * from test order by id; 
id | value |  created_at  
----+-------+--------------------- 
    1 | A  | 2014-01-09 00:00:00 
    2 | C  | 2014-01-02 00:00:00 
    3 | B  | 2014-01-03 00:00:00 
    4 | B  | 2014-01-04 00:00:00 
    5 | C  | 2014-01-05 00:00:00 
    6 | B  | 2014-01-06 00:00:00 
    7 | C  | 2014-01-07 00:00:00 
    8 | B  | 2014-01-08 00:00:00 

會話「A」成功更新id爲1的行到'2014-01-09'。會話「B」成功地更新了其餘三個值爲「A」的行。更新語句獲得了對號碼2,5和7的鎖定;我們知道這是因爲那些是實際更新的行。前面的select語句鎖定了不同的行 - 第1,2行和第5行。

如果您啓動了一個第三個終端會話,並鎖定第7行進行更新,則可以阻止會話B的更新。

+0

謝謝你的澄清,我不知道這種行爲是記錄。我還是PostgreSQL的新手,但從來不會爲它提供的所有功能和可能性而驚歎! – Jaan 2014-10-08 08:56:11