MySQL經過MVCC和鎖來實現併發控制,在4個隔離級別中,讀寫數據方式及加鎖方式有所不一樣,以知足不一樣的業務需求。html
而在MSSQL中,也是經過鎖和MVCC的行版原本實現併發控制。
每一個事務中,鎖的類型、級別、加鎖、釋放的狀況,由事務的隔離級別控制,在MSSQL中,有6個隔離級別,不一樣的隔離級別對鎖的應用不同。而這兩個隔離級別中,有2個應用 MVCC的機制,也就是 快照類的隔離級別:Read Commmitted Snapshot 跟 Snapshot。
1 併發控制理論
在MSSQL中,常常用到的併發控制理論是 悲觀併發控制跟樂觀併發控制。
1.1 悲觀併發控制
悲觀併發,默認在事務操做過程當中,必定會有其餘事務跟它爭奪資源,因此在事務操做過程當中,會根據不一樣的狀況對數據添加鎖,避免操做期間其餘事務對該數據的修改或讀取,保證數據的一致性。
悲觀併發控制,因爲歸入了鎖機制,很大程度會影響到併發規模。主要應用於數據頻繁修改、而且回滾事務的成本要大於鎖數據的成本 的系統中。
1.2 樂觀併發控制
樂觀控制,默認事務在讀取數據的時候,其餘事務並無在操做這些數據,因此不會加鎖,直接修改數據,修改後查看讀取數據期間是否有其餘用戶也修改了數據,若是有,則回滾自己的修改事務。
樂觀併發控制,應用於數據修改不頻繁、而且 回滾事務成本要小於鎖數據成本 的系統中。
2 隔離級別
在每個事務中,都指定了一個隔離級別,該隔離級別定義了這個事務跟其餘事務之間的隔離程度。
在MSSQL中,有6種隔離級別,4個常規隔離級別跟2個快照隔離級別:Read UnCommitted、Read Committed、Read Commmitted (行版本)、Read Repeattable、Snapshot跟Read Serializeble。Read Commmitted (行版本)跟Snapshot 可能接觸狀況比較少,不過仍會說明。
在MySQL中,默認的隔離級別是RR,而在SQL SERVER中,默認的隔離級別是RC,讀已提交。
2.1 隔離級別說明
如何設置整個數據庫的默認隔離級別?
下文中說S鎖,並非所有加鎖過程(MSSQL中仍是IS鎖的申請)。
- Read UnCommitted
- 簡稱 RU,讀未提交記錄,始終是讀最新記錄
- 可能存在髒讀、不可重複讀、幻讀等問題
- 讀的過程不加S鎖,等同於 SELECT * FROM tbname with(nolock)
- Read Committed
- 簡稱 RC ,讀已提交記錄
- 可能存在不可重複讀、幻讀等問題
- 讀的過程加 S鎖,不管事務是否結束,SELECT 語句一旦結束,立馬釋放S鎖,不會等到事務結束才釋放鎖,遵循的是 Strict 2-PL
- Read Commmitted (行版本)
- 簡稱 RCSI
- 應用MVCC原理,版本讀,讀已提交記錄,可是讀取到的不必定是最新的記錄
- 同個事務中,讀取數據都是同一個版本
- 不存在髒讀、不可重複讀問題,可能存在幻讀問題
- 行版本控制隔離級別 中的版本數據,不存在與數據庫自己,而是存在 tempdb ,下文會詳細描述這一隔離級別
- Read Repeattable
- 簡稱 RR ,可重複讀記錄
- 可能存在幻讀等問題
- 讀的過程加S鎖,直到事務結束,才釋放S鎖,遵循的是 Stong Strict 2-PL
- Snapshot
- Read Serializeble
- 簡稱 RS,序列化讀記錄
- 不存在 髒讀、不可重複讀、幻讀等問題
- 讀的過程當中除了添加S鎖,還添加範圍鎖;修改數據的過程當中,除了添加 X 鎖,也會添加範圍鎖,避免在符合條件的數據在操做過程當中,有其餘符合條件的數據INSERT進來
- 併發度最差,除非明確業務需求及性能影響才使用,曾經遇到過某個短信業務的框架默認使用這個隔離級別,上線後爆發死鎖上K個,立刻分析緊急修復....
2.2 Read Commmitted Snapshot Isolation 與 Snapshot Isolation
Read Commmitted Snapshot Isolation 使用行版本控制
語句級的快照,在事務中當數據發生修改或者刪除時,調用寫入複製機制,保證寫入的行數據的舊版本知足事務操做前的一致性。 RCSI 保證的是語句級的 讀一致性。
Snapshot Isolation 使用行版本控制
事務級的快照,當事務開始的時候,調用寫入複製機制。 SI 保證的是事務級 的讀取一致性。
如何管理行版本信息呢?
二者的行版本的信息均存儲在tempdb數據庫內,並不是存儲在自己的數據庫,這就要求tempdb要有足夠的空間存儲版本信息,若是tempdb空間不足,則行版本寫入失敗,形成該隔離級別沒法正常使用。
存儲引擎對使用 RCSI 或者 SI 隔離級別的事務,在 SI事務開始的時候,分配一個事務序列號 XLN,每次分配遞增1,以此實現事務級的一致性,這裏注意 RCSI的 事務序列號 並非一個事務一個序列號,而是事務內每條SQL一個事務序列號,以此來實現語句級別的快照。這兩個隔離級別下,須要維護全部執行過數據修改的邏輯副本(即行版本),這些邏輯副本存儲在tempdb內,每一個邏輯副本(行版本)都有標記本次的事務的事務序列號XLN。即 最新的行值存儲在當前的數據庫中,而歷史行版本信息包括最新版本,存儲在tempdb中。這裏注意一下,事務內的修改數據寫行版本信息的時候,先寫入到緩存池中,在刷新到tempdb文件,避免性能形成太大的影響。
這個時候,可能會問?那豈不是tempdb要存儲很是多的歷史版本數據,有沒有刪除機制呢?
這個是有的,一方面,行版本信息不會即時刪除,由於要保證基於行版本控制隔離級別下運行的事務要求,保證並行的事務若是正在使用tempdb的行版本信息 不會受到影響。另外一方面,數據庫的存儲引擎 會跟蹤最先可用的事務序列號,而後按期刪除比序列號更小的 XLN的全部行版本。
如何讀取行版本信息呢?
兩個快照隔離級別下的 的事務讀數據的時候,不會獲取正在讀取數據上的共享鎖,所以不會堵塞正在修改的事務,因爲減小了鎖的申請及數量,能夠提供其DB併發能力。不過會獲取所在表格的架構鎖,若是表格正在發現架構修改(如列增長修改等),則會被堵塞。
如何讀取合適的行版本,RCSI 跟 SI 之間是有區別的。
RCSI:
每次啓動語句時,提交全部數據,同時讀取tempdb中的
最新事務序列,這使 RCSI 下事務內的每一個語句 均可以查看
每一個語句啓動時存在的
最新數據的快照,也就是 事務內多個SQL查詢間隙中有其餘事務修改了數據,那麼同個事務的屢次相同SQL查詢結果就會出現不一致的狀況。
SI:每次
啓動事務時,提交全部數據,讀取 最接近但低於 自己的 快照事務序列號,也就是 事務內的多個SQL 查詢,讀到的數據都是同一個版本,即便屢次查詢間隙有其餘事務修改數據,讀到的結果也是一致的。
如何修改行版本信息呢 ?
在使用 RCSI 事務中,使用阻塞性掃描(其中讀取數據值時將在數據行上採用更新鎖(U 鎖)完成選擇要更新的行,知足條件的行記錄將升級更新鎖到排它鎖,注意,這裏掃描的不是tempdb裏邊的行版本信息,而是實際數據庫裏邊的最新行記錄,修改數據的機制跟 RC 相同。 若是數據行不符合更新條件,則在該行上將釋放更新鎖,同時鎖定下一行並對其進行掃描。持有鎖以後,則進行數據更新,事務結束後,釋放鎖。
在使用 SI 事務中,對數據修改採用樂觀方法:使用行版本的數據,進行數據修改,直到數據修改完成是,才獲取實際數據上的鎖, 當數據行符合更新標準時,則提交修改的數據行。
若是數據行已在快照事務之外修改,則將出現更新衝突,同時快照事務也將終止。 更新衝突由數據庫引擎處理,沒法禁用更新衝突檢測。
從簡單的SQL來分析,WHERE條件均爲主鍵(僅爲我的測試推測):
- 同個事務,屢次 SELECT * FROM tbname WHERE id=2
- RCSI,在同個事務中,每一個SQL啓動的時候,提交數據到tempdb表格(我的推測,應該是會分配一個相似hash字符串之類的,若是同個事務中的屢次查詢結果一致,應該不用在每一個SQL開始的時候,重複提交行版本到tempdb),從tempdb中讀取最新版本信息,若是tempdb沒有版本信息,則從 數據庫中讀取,並把讀取到的記錄存儲在 tempdb。會存在同個事務中,屢次讀取數據結果不一致的狀況。
- SI,在同個事務中,同個事務內的相同SQL 從tempdb中讀取距離當前事務最新的版本,整個事務內部的SQL都是用這個版本數據,若是tempdb沒有版本信息,則從 數據庫中讀取,並把讀取到的記錄存儲在 tempdb。同個事務中,不會存在 屢次讀取數據結果不一致的狀況。
- UPDATE tbname SET colname='xinysu' WHERE id=18
- RCSI,直接讀取數據庫中的數據,根據主鍵加上X鎖,更新數據,這個操做跟 RC 隔離級別是同樣的。
- SI,讀取 行版本 數據,在行版本上選擇須要更新的行,修改爲功後把數據 修改到實際的數據庫中去,若是 實際數據庫中的數據在這段操做期間已被其餘事務修改了數值,則會出現更新衝突,該事務將報錯中止。即,SI 在 UPDATE 的時候,有更新衝突檢測。
- 爲啥要先在行版本上更新,最後在更新到實際數據上?
- 假設一個UPDATE運行須要3s,可是隻更新了1條行記錄,若是直接在實際數據上更新,則須要鎖定掃描記錄3s,最後更新,中間會堵塞到其餘事務對該數據的查詢,可是若是在行版本上更新,則不須要鎖住 實際數據,最後更新1行記錄的時候,很是快,避免長時間的堵塞,提升併發能力。
屬性
|
使用行版本控制的已提交讀隔離級別
|
快照隔離級別
|
數據庫級選項啓動
|
READ_COMMITTED_SNAPSHOT
|
ALLOW_SNAPSHOT_ISOLATION
|
事務設置
|
使用默認的已提交讀隔離級別,或運行 SET TRANSACTION ISOLATION LEVEL 語句來指定 READ COMMITTED 隔離級別
|
SET TRANSACTION ISOLATION LEVEL 來在事務啓動前指定 SNAPSHOT 隔離級別
|
行版本處理
|
在每條語句啓動前提交的全部數據。
|
在每一個事務啓動前提交的全部數據。
|
更新處理
|
從行版本恢復到實際的數據,以選擇要更新的行並使用選擇的數據行上的更新鎖。 獲取要修改的實際數據行上的排他鎖。 沒有更新衝突檢測。
|
使用行版本選擇要更新的行。 嘗試獲取要修改的實際數據行上的排他鎖,若是數據已被其餘事務修改,則出現更新衝突,同時快照事務也將終止。
|
更新衝突檢測
|
無
|
集成支持。 沒法禁用。
|
3 隔離級別測試
查看當前會話的數據庫隔離級別:DBCC USEROPTIONS ,查看[set options] = 'isolation level',便可查看當前事務的隔離級別。
設置數據庫隔離級別:
- RU,事務開始的時候,設置 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
- RC,事務開始的時候,設置 SET TRANSACTION ISOLATION LEVEL READ COMMITTED
- RCSI,整個數據庫級設置 READ_COMMITTED_SNAPSHOT 爲ON,注意,設置的這個的時候須要獲取數據庫的獨佔權,也就是當前不容許有用戶線程鏈接數據庫,否者這個設置SQL會一直處於堵塞狀況。若是當前數據庫的默認隔離級別是 RC,則設置後,默認爲RCSI,否者,須要在事務開始的時候,設置 SET TRANSACTION ISOLATION LEVEL READ COMMITTED
- 數據庫設置:當前數據庫下,執行 ALTER DATABASE dbname SET READ_COMMITTED_SNAPSHOT ON
- 事務設置:SET TRANSACTION ISOLATION LEVEL READ COMMITTED
- RR,事務開始的時候,設置 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
- RS,事務開始的時候,設置 SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
- SI,整個數據庫級設置 ALLOW_SNAPSHOT_ISOLATION 爲ON,同時設置事務的隔離級別爲 SNAPSHOT。注意,這裏的 ALLOW_SNAPSHOT_ISOLATION 設置也是須要獲取數據的獨佔鎖。
- 數據庫設置:當前數據庫下,執行 ALTER DATABASE dbname SET ALLOW_SNAPSHOT_ISOLATION ON
- 事務設置:SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
測試過程當中,分爲3個表格:無索引、有索引、有惟一索引。
CREATE TABLE tb_no_index ( id int primary key not null identity(1,1), age int not null, name varchar(100) );
CREATE TABLE tb_index ( id int primary key not null identity(1,1), age int not null, name varchar(100) );
CREATE TABLE tb_unique_index ( id int primary key not null identity(1,1), age int not null,name varchar(100) );
CREATE INDEX IX_age ON tb_index(age)
CREATE INDEX IX_unique_age ON tb_index(age)
INSERT INTO tb_no_index(age) values(2),(9),(21),(4),(7),(25);
INSERT INTO tb_index(age) values(2),(9),(21),(4),(7),(25);
INSERT INTO tb_unique_index(age) values(2),(9),(21),(4),(7),(25);
3.1 Read Uncommitted
- 數據不一致狀況測試截圖
- RU測試結論
- 在RU隔離級別下
- 不會出現更新丟失狀況(鎖機制),可是會出現 髒讀、不可重複讀及幻讀的狀況。
- 讀不加行鎖,能夠讀未提交數據
3.2 Read Committed
- 數據不一致狀況測試截圖
- 讀狀況測試
- RC測試結論
- 在RC隔離級別下
- 不會出現更新丟失狀況(鎖機制)、髒讀現象,可是會出現 不可重複讀及幻讀的狀況
- 讀須要申請鎖,故不會出現髒讀狀況
- 遵循 強2-PL模式,事務內的讀鎖讀完即刻釋放,寫鎖等到事務提交的時候才釋放。
3.3 Read Commit Snapshot Isolation
- 測試環境設置
- 實現設置數據庫隔離級別爲:
![](http://static.javashuo.com/static/loading.gif)
- 檢查當前會話的默認隔離級別:
![](http://static.javashuo.com/static/loading.gif)
- 數據不一致狀況測試截圖
- 更新衝突測試
- RCSI 測試結論
- 讀不加鎖,但申請表格的架構鎖,讀行版本數據
- 不存在丟失更新、髒讀狀況,可是存在不可重複讀及幻讀狀況
- 沒有更新衝突檢測,RCSI跟RC的更新處理方式同樣
3.4 Read Reaptable
- 數據不一致狀況測試截圖
- RR測試結論
- 讀加S鎖,事務結束後才釋放S鎖
- 不存在丟失更新、髒讀及不可重複讀狀況,可是存在幻讀狀況
3.5 Read Serializable
- 數據不一致狀況測試截圖
- RS 測試結論
- 讀加S鎖,事務結束後才釋放S鎖
- 增長了範圍鎖
- 不存在丟失更新、髒讀、不可重複讀、幻讀狀況
- 併發能力最差
3.6 Snapshot Isolation
- 數據不一致狀況測試截圖
- 更新衝突測試
- SI 測試結論
- 不存在 丟失更新、髒讀、幻讀等數據不一致狀況
- 讀不加鎖,爲讀行版本數據
- 具備衝突監測,沒法禁用,若是使用這個隔離級別,程序要作更新衝突的回滾處理
4 總結
隔離級別
|
說明
|
髒讀
|
不可重複讀
|
幻影
|
併發控制模型
|
Read UnCommitted
|
未提交讀
|
YES
|
YES
|
YES
|
悲觀
|
Read Committed
|
已提交讀
|
NO
|
YES
|
YES
|
悲觀
|
Read Commmitted (行版本)
|
已提交讀(快照)
|
NO
|
YES
|
YES
|
樂觀
|
Read Repeattable
|
可重複讀
|
NO
|
NO
|
YES
|
悲觀
|
Snapshot
|
快照
|
NO
|
NO
|
NO
|
樂觀
|
Read Serializeble
|
可串行化
|
NO
|
NO
|
NO
|
悲觀
|