來自http://www.infoq.com/cn/articles/Isolation-Levels
> ## 關鍵要點
>
> * 僅從ACID或非ACID角度考慮問題是不夠的,你應知道你的數據庫支持何種事務隔離級別。
> * 一些數據庫宣稱自己具有“最終一致性”,但卻可能對重復查詢返回不一致的結果。
> * 相比于你所尋求的數據庫,一些數據庫提供更高的事務隔離級別。
> * 臟讀可導致同一記錄得到兩個版本,或是完全地丟失一條記錄。
> * 在同一事務中多次重新運行同一查詢后,可能會出現幻讀。
最近MongoDB登上了Reddit的頭條,因為MongoDB的核心開發者David Glasser痛苦地認識到[MongoDB默認會執行臟讀](https://engineering.meteor.com/mongodb-queries-dont-always-return-all-matching-documents-654b6594a827)。
在本文中,我們將解釋什么是事務隔離級別和臟讀,并給出一些廣受歡迎的數據庫是如何實現它們的。
ANSI SQL給出了四種標準的事務隔離級別:可序列化(Serializable)、可重復讀(Repeatable reads)、提交讀(Read committed)和未提交讀(Read uncommitted)。
許多數據庫缺省是提交讀的,這保證了在事務運行期間用戶看不到轉變中的數據。提交讀的實現通過在讀取時暫時性地獲取鎖,并持有寫入鎖直至事務提交。
如果在一個事務中需要多次重復同一讀取,并想要“合理地確定”所有的讀取總是會得到同樣的結果,這要在整個過程期間持有讀取鎖。在使用可重復讀事務隔離級別時,上述操作是自動完成的。
我們這里所說的“合理地確定”可重復讀,是因為存在“幻讀”(phantom reads)的可能性。當執行使用了WHERE語句的查詢時,類似于“WHERE Status=1”,就有可能發生幻讀。雖然所涉及的行將被鎖上,但是這并不能阻止匹配WHERE條件的新行被添加進來。“幻”(phantom)一詞指在查詢第二次執行時所出現的行。
為確保在同一事務中的兩次讀取會返回同樣的數據,可使用可序列化事務隔離級別。可序列化使用了“范圍鎖”,避免了匹配WHERE條件的新行添加到一個開放的事務中。
一般情況下,由于鎖競爭的存在,事務隔離級別越高,性能越差。因此為了改進讀取性能,一些數據庫還支持未提交讀。該事務隔離級別將無視鎖的存在(事實上其在SQL Server中被稱為“NOLOCK”),因此該級別下可執行臟讀。
## 臟讀所存在的問題
在探討臟讀問題之前,你必須要理解表并非是真實存在于數據庫中的,表只是一個邏輯結構。事實上你的數據是按一個或多個索引進行存儲的。主索引在大多數數據庫中被稱為“聚束索引”或“堆”(該術語在各NoSQL數據庫中各不相同)。因而當執行插入操作時,需要在每個索引中插入一行。當執行更新操作時,數據庫引擎僅需訪問指到被改變列的索引。但更新操作常常必須要在每個索引上執行兩個操作,即從舊的位置刪除并在新的位置插入。
在下圖中,你可看見一個普通的表,還有表中IX_Customer_State和PK_Customer對象更新操作的執行計劃。鑒于表的FullName列并未改變,所以可以跳過IX_Customer_FullName索引。

(點擊放大圖像)
[](http://cdn3.infoqstatic.com/statics_s2_20170810-0346u3/resource/articles/Isolation-Levels/zh/resources/11.jpg)
注意在SQL Server中,PK前綴指代主鍵,通常也是用于聚束索引的鍵。IX用于指代非聚束索引。其它的數據具有它們自己的命名規范。
解決了上述問題,讓我們看一下臟讀導致不一致數據的多種途徑。
未提交讀問題易于理解。在事務被完全提交之前,如果無視寫入鎖的存在,使用“未提交讀”的SELECT語句就可以就看到新插入或更新的行。如果這些轉變操作這時被回滾,從邏輯上說,SELECT操作將返回并不存在的數據。
如果數據在更新操作過程中被移動了,這就產生了雙重讀取。例如,你正在讀取所有的客戶記錄的狀態。如果在你讀取“California”記錄和讀取“Texas”記錄之間,上面所說的更新語句被執行了,你就能看見“客戶1253”記錄兩次。一次是舊值,一次是新值。

記錄丟失發生的方式相同。如果我們提取“客戶1253”記錄并將其從“Texas”記錄移動到“Alaska”記錄,并再次使用狀態去選擇數據,你可能會完全地丟失該記錄。這就是發生在David Glasser的MongoDB數據庫中的事情。由于在更新操作期間讀取了索引,查詢丟失了記錄。

臟讀也會妨礙到排序操作,該問題的出現取決于數據庫的設計方式及特定的執行計劃。例如,臟讀可能發生于執行計劃對所有候選數據行采集指針信息時,如果在其后一行數據被更新了,但實際上執行引擎還是會使用已被采集的指針信息從原始位置拷貝數據。
## 快照隔離,或被稱為“行級版本控制”
為在避免臟讀問題的同時提供好的性能,許多數據庫支持快照隔離語義。運行于快照隔離狀態下,當前的事務不能看到任何先于其啟動的其它事務的結果。
快照隔離的實現是通過做被改變行的臨時拷貝,而非僅依靠于鎖機制,因此它也常被稱為“行級版本控制”。
很多支持快照隔離語義的數據庫在被請求使用“提交讀”事務隔離時,會自動使用快照隔離。
## SQL Server中的事務隔離級別
SQL Server支持所有四種ANSI SQL事務隔離級別,外加一種顯式的快照隔離級別。提交讀可能也使用快照語義,這取決于數據庫中READ_COMMITTED_SNAPSHOT選項的配置方式。
在開關該選項前,你的數據庫需要做充分的測試。雖然提交讀可以提升讀取性能,但它也同時降低了寫入性能。尤其是tempdb被部署在慢速磁盤上時,因為這存儲了行的舊版本。
在SELECT語句中可以使用臭名昭著的NOLOCK指示符。NOLOCK的作用等同于將事務運行設置為未提交讀。這在SQL Server 2000及更早期的版本中被大量地使用,因為那時并沒有提供行級版本控制。盡管現在不再必要或不建議這樣做,但是該習慣仍然保留著。
更多信息參見“[設置事務隔離級別 (Transact-SQL)](https://msdn.microsoft.com/en-us/library/ms173763.aspx)”.
## PostgreSQL中的事務隔離級別
雖然官方宣稱PostgreSQL支持所有四種ANSI事務隔離級別,但事實上PostgreSQL中只有三種事務隔離級別。每當查詢請求“未提交讀”時,PostgreSQL就默默地將其升級為“提交讀”。因此PostgreSQL不允許臟讀。
> 當你選取“未提交讀”級別時,事實上你得到了“提交讀”,在PostgreSQL對可重復讀的實現中,臟讀是不可能發生的,因此實際的事務隔離級別可能比你所選取的要更加嚴格。這是被SQL標準所允許的,因為四種事務隔離級別僅定義了事務中一定不能發生的現象,它們并未定義應該發生哪種現象。
PostgreSQL并未顯式地提供快照隔離。當然快照隔離是在使用提交讀時自動發生的。這是因為PostgreSQL的設計從一開始就考慮了[多版本并發控制](https://en.wikipedia.org/wiki/Multiversion_concurrency_control)。
在9.1版本之前,PostgreSQL不提供可序列化事務,會將它們靜默降級為可重復讀。但當前所有仍在支持的PostgreSQL版本中都不再有這個限制了。
更多的信息參見PostgreSQL官方文檔的[13.2節,“ 事務隔離”](https://www.postgresql.org/docs/9.1/static/transaction-iso.html).
## MySQL中的事務隔離級別
InnoDB默認為可重復讀,但是提供所有四種ANSI SQL事務隔離級別。提交讀使用快照隔離語義。
更多InnoDB相關的信息,參見MySQL官方文檔的[15.3.2.1節“ 事務隔離等級”](https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html)
事務在使用MyISAM存儲引擎時是完全不被支持的,這里使用了表一級的單一讀寫鎖(雖然在某些情況下,插入操作是可以繞過鎖的。)
## Oracle中的事務隔離等級
Oracle只支持三種事務隔離級別,即提交讀、可序列化和只讀。在Oracle中,提交讀是默認的,它使用快照語義。
類似于PostgreSQL,Oracle并不提供未提交讀,永不允許臟讀。
可重復讀并不在Oracle的支持列表中。如果你需要在Oracle中具有該行為,你的事務隔離級別需要被設置為可序列化。
只讀是Oracle所獨有的事務隔離級別。但是對此并沒有很好的文檔,手冊中只有如下描述:
> 只讀事務只能看見那些在事務開始階段就被提交的改變,不允許INSERT、UPDATE和DELETE語言。
對其它兩種事務隔離級別的更多信息,參見Oracle官方文檔[第13章“數據并發和一致性”](http://docs.oracle.com/cd/B14117_01/server.101/b10743/consist.htm#i17856)。
## DB2中的事務隔離級別
DB2具有四種隔離級別,分別稱為可重復讀、讀穩定性、游標穩定性和未提交讀。這四種級別并不與上述四種ANSI術語一一對應。
可重復讀對應于ANSI SQL中的可序列化,意味著不可能存在臟讀。
讀穩定性對應于ANSI SQL中的可重復讀。
游標穩定性用于提交讀,是DB2的默認設置配置。對于9.7版快照語義生效。而在9.7的前期版本中,DB2使用類似于SQL Server的鎖機制。
未提交讀在很大程度上類似于SQL Server中的未提交讀,也允許臟讀。手冊中推薦僅在只讀表上使用未提交讀,或是用在“可以看到未被其它應用提交的數據時”。
更多信息參見“[事務隔離級別](http://www.ibm.com/support/knowledgecenter/SSEPGG_9.7.0/com.ibm.db2.luw.admin.perf.doc/doc/c0004121.html)”。
## MongoDB中的事務隔離級別
正如前文所提到的,MongoDB不支持事務。在其手冊中對此是這樣描述的:
> 因為在MongoDB中對單一文檔的操作是原子的,兩階段提交只能提供類事務語義。在兩階段提交或回滾期間,應用可在中間點返回中間數據。
事實上這意味著MongoDB使用臟讀語義,具有雙倍或丟失記錄的可能性。
## CouchDB中的事務隔離等級
CouchDB也不支持事務。但是不同于MongoDB的是,它使用了多版本并發控制去避免臟讀。
> 讀取請求將總是在請求開始時就能看到數據庫的最新快照。
這所給予CouchDB的事務隔離等級,等價于具有快照語義的提交讀。
更多的信息參見“[最終一致性](http://docs.couchdb.org/en/1.6.1/intro/consistency.html)”。
## Couchbase Server的事務隔離級別
Couchbase Server常被混淆為CouchDB,但它是一種完全不同的產品。就索引而言,它并未提供任何形式的隔離。
當執行更新操作時,Couchbase Server僅更新主索引,或稱其為“真實的表”。所有的二級索引將被延遲更新。
雖然在Couchbase Server文檔并沒有明確說明,看上去它在構建索引時使用了快照,如果確是如此,臟讀應該不成為問題。但是由于索引的延遲更新,在Couchbase Server中仍不能獲得真正的提交讀事務隔離級別。
和許多的NoSQL數據庫一樣,Couchbase Server并不直接支持事務。但是你確實可以使用顯式鎖,但鎖只能在被自動丟棄前維持30秒的時間。
更多的信息參見“[對條目上鎖](http://docs.couchbase.com/developer/dev-guide-3.0/lock-items.html)”、“[你所應知道的關于Couchbase架構的所有事情](https://dzone.com/articles/couchbase-architecture-deep)”和“[Couchbase視圖引擎的內幕](http://www.couchbase.com/wiki/display/couchbase/Couchbase+View+Engine+Internals)”。
## Cassandra中的事務隔離級別
Cassandra 1.0隔離了甚至是對一行的寫入操作。因為字段是被逐一更新的,所以可以終止對舊值和新值混合在一起的記錄的讀取。
從1.1版本開始,Cassandra提供了“行級隔離”。這讓Cassandra具有等同于其它的數據庫中被稱為“未提交讀”的隔離級別。Cassandra并未提供更高級別的隔離。
更多的信息參見“[關于事務和并發控制](https://docs.datastax.com/en/cassandra/2.0/cassandra/dml/dml_about_transactions_c.html)”。
## 了解你的數據庫的事務隔離級別
正如從上述實例中可看到的,僅從ACID和非ACID角度考慮你的數據庫是不夠的。你的確需要去知道你的數據庫應在何種情況下支持何種的事務隔離級別。
## 關于作者
Jonathan Allen的首份工作是在上世紀九十年代末做診所的MIS項目,Allen將項目逐步由Access和Excel升級到企業級的解決方法。在從事為財政部門編寫自動交易系統代碼的工作五年之后,他成為項目顧問,參與了包括機器人倉庫UI、癌癥研究軟件中間層、主要房地產保險企業的大數據需求等在內的各種行業項目。在閑暇時間,他喜歡研究源于16世紀的武術,并為其撰寫文章。
查看英文原文:[A Quick Primer on Isolation Levels and Dirty Reads](https://www.infoq.com/articles/Isolation-Levels)
* * *
感謝[冬雨](http://www.infoq.com/cn/author/%E5%86%AC%E9%9B%A8)對本文的審校。
給InfoQ中文站投稿或者參與內容翻譯工作,請郵件至[editors@cn.infoq.com](mailto:editors@cn.infoq.com)。也歡迎大家通過新浪微博([@InfoQ](http://www.weibo.com/infoqchina),[@丁曉昀](http://weibo.com/u/1451714913)),微信(微信號:[InfoQChina](http://www.geekbang.org/ivtw))關注我們。
- 數據庫
- CAP定理
- 關系模型
- 關系數據庫
- NoSQL
- ODBC
- JDBC
- ODBC、JDBC和四種驅動類型
- mysql
- 安裝與配置
- CentOS 7 安裝 MySQL
- 優化
- 比較全面的MySQL優化參考
- 1、硬件層相關優化
- 1.1、CPU相關
- 1.2、磁盤I/O相關
- 2、系統層相關優化
- 2.1、文件系統層優化
- 2.2、其他內核參數優化
- 3、MySQL層相關優化
- 3.1、關于版本選擇
- 3.2、關于最重要的參數選項調整建議
- 3.3、關于Schema設計規范及SQL使用建議
- 3.4、其他建議
- 后記
- Mysql設計與優化專題
- ER圖,數據建模與數據字典
- 數據中設計中的范式與反范式
- 字段類型與合理的選擇字段類型
- 表的垂直拆分和水平拆分
- 詳解慢查詢
- mysql的最佳索引攻略
- 高手詳解SQL性能優化十條經驗
- 優化SQL查詢:如何寫出高性能SQL語句
- MySQL索引原理及慢查詢優化
- 數據庫SQL優化大總結之 百萬級數據庫優化方案
- 數據庫性能優化之SQL語句優化1
- 【重磅干貨】看了此文,Oracle SQL優化文章不必再看!
- MySQL 對于千萬級的大表要怎么優化?
- MySQL 數據庫設計總結
- MYSQL性能優化的最佳20+條經驗
- 數據操作
- 數據語句操作類型
- DCL
- 修改Mysql數據庫名的5種方法
- DML
- 連接
- 連接2
- DDL
- 數據類型
- 字符集
- 表引擎
- 索引
- MySQL理解索引、添加索引的原則
- mysql建索引的幾大原則
- 淺談mysql的索引設計原則以及常見索引的區別
- 常用工具簡介
- QA
- MySQL主機127.0.0.1與localhost區別總結
- 視圖(view)
- 觸發器
- 自定義函數和存儲過程的使用
- 事務(transaction)
- 范式與反范式
- 常用函數
- MySQL 數據類型 詳解
- Mysql數據庫常用分庫和分表方式
- 隔離級別
- 五分鐘搞清楚MySQL事務隔離級別
- mysql隔離級別及事務傳播
- 事務隔離級別和臟讀的快速入門
- 數據庫引擎中的隔離級別
- 事務隔離級別
- Innodb中的事務隔離級別和鎖的關系
- MySQL 四種事務隔離級的說明
- Innodb鎖機制:Next-Key Lock 淺談
- SQL函數和存儲過程的區別
- mongo
- MongoDB設置訪問權限、設置用戶
- redis
- ORM
- mybatis
- $ vs #
- mybatis深入理解(一)之 # 與 $ 區別以及 sql 預編譯
- 電商設計
- B2C電子商務系統研發——概述篇
- B2C電子商務系統研發——商品數據模型設計
- B2C電子商務系統研發——商品模塊E-R圖建模
- B2C電子商務系統研發——商品SKU分析和設計(一)
- B2C電子商務系統研發——商品SKU分析和設計(二)
- 數據庫命名規范--通用