如果轉載,請注明博文來源:
www.cnblogs.com/xinysu/ ,版權歸 博客園 蘇家小蘿卜 所有。望各位支持!
MySQL通過MVCC和鎖來實現並發控制,在4個隔離級別中,讀寫數據方式及加鎖方式有所不同,以滿足不同的業務需求。
而在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 隔離級別說明
如何設置整個數據庫的默認隔離級別?
數據不一致的說明詳見之前博文:
http://www.cnblogs.com/xinysu/p/7260227.html 中的第四章:數據不一致情況。
下文中說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
- 簡稱 SI
- 下文會詳細描述這一隔離級別
- 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',即可查看當前事務的隔離級別。
數據不一致的說明詳見之前博文:
http://www.cnblogs.com/xinysu/p/7260227.html 中的第四章:數據不一致情況。
2-PL鎖申請釋放的說明詳見之前博文:
http://www.cnblogs.com/xinysu/p/7260227.html 中的第3章:數據不一致情況。
設置數據庫隔離級別:
- 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
- 測試環境設置
- 實現設置數據庫隔離級別為:
- 檢查當前會話的默認隔離級別:
- 數據不一致情況測試截圖
- 更新沖突測試
- 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
|
悲觀
|