最近,當開發人員David Glasser了解MongoDB默認執行髒讀的糟糕方式時,MongoDB再次成為Reddit的佼佼者。 在本文中,我們將解釋什麼是隔離級別和髒讀以及如何在流行的資料庫中實現它們。
在ANSI SQL中,有四個標準隔離級別:可序列化,可重複讀取,已提交讀取和未提交讀取。
許多資料庫的默認設置為「讀取已提交」,它僅保證在進行該事務時您不會看到過渡中的數據。它通過在讀取期間短暫地獲取鎖來實現此目的,同時保持寫入鎖直到事務被提交。
如果您需要在一個事務中多次重複相同的讀取操作,並且想要合理地確定它總是返回相同的值,則需要在整個持續時間內保持讀取鎖定。使用「可重複讀取」隔離級別時,將自動為您完成此操作。
我們說「可重複讀」是「合理肯定的」,因為可能存在「幻讀」。使用where子句(例如「 WHERE Status = 1」)執行查詢時,可能會發生幻像讀取。這些行將被鎖定,但是沒有什麼阻止添加符合條件的新行。術語「幻像」適用於第二次執行查詢時出現的行。
為了絕對確保同一事務中的兩次讀取返回相同的數據,可以使用Serializable隔離級別。這使用「範圍鎖」,如果新行與打開的事務中的WHERE子句匹配,則可以防止添加這些行。
通常,隔離級別越高,由於鎖爭用而導致的性能越差。因此,為了提高讀取性能,某些資料庫還支持「讀取未提交」。此隔離級別忽略鎖(實際上在SQL Server中稱為NOLOCK)。結果,它會執行髒讀。
在討論髒讀之前,您必須了解表實際上並不存在於資料庫中。表只是一個邏輯構造。實際上,您的數據存儲在一個或多個索引中。在大多數關係數據庫中,主索引被稱為「聚集索引」或「堆」。 (對於NoSQL資料庫,術語有所不同。)因此,在執行插入操作時,它需要在每個索引中插入一行。執行更新時,資料庫引擎僅需要觸摸引用正在更改的列的索引。但是,它通常必須對每個索引執行兩次操作,即從舊位置刪除和向新位置插入。
在下圖中,您可以看到一個簡單的表和一個執行計劃,其中更新了兩個對象IX_Customer_State和PK_Customer。由於全名未更改,因此跳過了IX_Customer_FullName索引。
注意:在SQL Server中,PK前綴是指主鍵,它通常也是用於聚集索引的鍵。 IX用於非聚集索引。 其他資料庫有其自己的約定。
通過這種方式,讓我們看一下髒讀可能導致數據不一致的多種方式。
未提交的讀取最容易理解。 通過忽略寫鎖定,使用「讀未提交」的SELECT語句可以在事務完全提交之前看到新插入或更新的行。 如果該轉換然後被回滾,那麼從邏輯上講,SELECT操作將返回從不存在的數據。
在更新操作期間移動數據時,會發生兩次讀取。 假設您正在按州讀取所有客戶記錄。 如果上述更新語句是在您加州記錄的時間與您閱讀德克薩斯州記錄的時間之間執行的,則您可以看到客戶1253兩次; 一次使用舊值,一次使用新值。
漏讀的發生方式相同。 如果我們將客戶1253移到德克薩斯州到阿拉斯加,再按州選擇數據,則可能會完全錯過該記錄。 這就是David Glasser的MongoDB資料庫所發生的事情。 通過在更新操作期間從索引讀取,查詢會丟失記錄。
根據資料庫的設計方式和特定的執行計劃,髒讀也會干擾排序。例如,如果執行引擎收集指向所有感興趣的行的一組指針,然後更新一行,然後執行引擎實際上使用所述指針從原始位置複製數據,則可能發生這種情況。
為了提供良好的性能同時避免髒讀問題,許多資料庫都支持快照隔離語義。在快照隔離下運行時,當前事務無法查看在當前事務之前啟動的任何其他事務的結果。
這是通過製作要修改的行的臨時副本來完成的,而不是僅僅依靠鎖。這通常稱為「行級版本控制」。
當請求讀取提交隔離時,大多數支持快照隔離語義的資料庫都會自動使用它。
SQL Server支持所有四個ANSI SQL隔離級別以及一個顯式的快照級別。取決於使用READ_COMMITTED_SNAPSHOT選項配置資料庫的方式,「已提交讀」也可以使用快照語義。
在啟用此選項之前和之後,請徹底測試資料庫。雖然它可以提高讀取性能,但可能會減慢寫入速度。如果您的tempdb處於慢速驅動器上,則尤其如此,因為這是行的舊版本存儲的地方。
臭名昭著的NOLOCK指令(可應用於SELECT語句)與在設置為「讀取未提交」的事務中運行具有相同的效果。由於SQL Server 2000和更早版本尚未提供行級版本控制,因此該版本已大量使用。儘管不再需要或不建議使用,但該習慣仍然存在。
有關更多信息,請參見SET TRANSACTION ISOLATION LEVEL(Transact-SQL)。
雖然PostgreSQL正式支持所有四個ANSI隔離級別,但實際上它只有三個。每當查詢請求「讀取未提交」時,PostgreSQL都會以靜默方式將其升級為「讀取已提交」。因此PostgreSQL不允許髒讀。
當選擇級別Read Uncommitted時,您實際上會獲得Read Committed,並且在Repeatable Read的PostgreSQL實現中不可能進行幻像讀取,因此實際的隔離級別可能比您選擇的嚴格。這是SQL標準所允許的:四個隔離級別僅定義了哪些現象一定不能發生,它們沒有定義哪些現象必須發生。
PostgreSQL沒有明確提供快照隔離。而是在使用「讀取已提交」時自動發生。這是因為PostgreSQL從一開始就設計為具有多版本並發控制。
在9.1版之前,PostgreSQL不提供可序列化的事務,並且會靜默地將它們降級為「可重複讀」。當前沒有支持的PostgreSQL版本仍然具有此限制。
有關更多信息,請參見13.2。事務隔離。
InnoDB默認為「可重複讀取」,但提供所有四個ANSI SQL隔離級別。讀取已提交使用快照隔離語義。
有關InnoDB的更多信息,請參見15.3.2.1事務隔離級別。
使用MyISAM存儲引擎時,根本不支持事務。相反,它在表級別使用一個讀寫器鎖。 (儘管在某些情況下,插入操作可以繞過鎖。)
Oracle僅支持3個事務級別:讀已提交,可序列化和只讀。在Oracle中,「默認值為讀已提交」,它使用快照語義。
像PostgreSQL一樣,Oracle不提供「讀未提交」。絕對不允許髒讀。
列表中還缺少「可重複讀取」。如果您在Oracle中需要這種行為,則需要將隔離級別設置為Serializable。
Oracle唯一的隔離級別是只讀。它沒有很好的文檔記錄,手冊只說:
只讀事務僅查看那些在事務開始時提交的更改,並且不允許INSERT,UPDATE和DELETE語句。
有關其他兩個隔離級別的更多信息,請參閱13數據並發性和一致性。
DB 2具有4個隔離級別,分別稱為重複讀取,讀取穩定性,游標穩定性和未提交讀取。但是,它們並不直接映射到ANSI術語。
可重複讀是ANSI SQL稱為可序列化的。也就是說,幻像讀取是不可能的。
讀取穩定性映射到ANSI SQL的可重複讀取。
默認情況下,「游標穩定性」用於「讀取已提交」。從9.7版開始,快照語義已生效。以前,它將使用類似於SQL Server的鎖。
未提交讀允許進行髒讀,就像SQL Server的未提交讀一樣。該手冊僅建議將其用於只讀表,或者「在查看其他應用程式未提交的數據沒有問題時」。
有關更多信息,請參見隔離級別。
如前所述,MongoDB不支持事務。從手冊中
由於MongoDB僅單文檔操作是原子操作,因此兩階段提交只能提供類似於事務的語義。在兩階段提交或回滾期間,應用程式有可能在中間點返回中間數據。
實際上,這意味著MongoDB使用髒讀語義,其中包括記錄可能翻倍或丟失的可能性。
CouchDB也不支持交易。但是與MongoDB不同,它確實使用多版本並發控制來防止髒讀。
讀取請求在請求開始時始終會看到您資料庫的最新快照。
這使CouchDB等效於具有Snapshot語義的Read Committed隔離級別。
有關更多信息,請參見最終一致性。
儘管經常與CouchDB混淆,但Couchbase Server是一個非常不同的產品。對於索引,它沒有隔離的概念。
在執行更新時,它僅更新主索引,如果您願意,也可以更新「真實表」。所有二級索引均會延遲更新。
該文檔尚不清楚,但在建立索引時似乎使用快照。如果是這樣,髒讀應該不是問題。但是由於延遲索引更新,您仍然無法獲得真正的「讀取已提交」隔離級別。
與許多NoSQL資料庫一樣,它不直接支持事務。但是,您確實可以使用顯式鎖。這些只能保留30秒,然後自動丟棄。
有關更多信息,請參閱鎖定項目,您需要了解的有關Couchbase體系結構的所有信息以及Couchbase View Engine內部。
在Cassandra 1.0中,甚至沒有隔離寫入單個行。欄位是一一更新的,因此您最終可能會讀取包含新舊值的記錄。
從1.1版開始,Cassandra提供「行級隔離」。這使其達到與其他資料庫稱為「讀取未提交」的相同隔離級別。更高級別的隔離是不可能的。
有關更多信息,請參見關於事務和並發控制。
從上面的示例中可以看到,僅將資料庫視為ACID或非ACID是不夠的。 您確實需要知道它支持什麼隔離級別以及在什麼情況下。
原文:https://www.infoq.com/articles/Isolation-Levels/
本文:http://jiagoushi.pro/node/918
討論:請加入知識星球或者微信圈子【首席架構師圈】