SQL Server 並發控制 第三篇:隔離級別和行版本(2)
隔離級別定義事務處理數據讀取操作的隔離程度,在悲觀並發模式下,隔離級別只會影響讀操作申請的共享鎖(Shared Lock),而不會影響寫操作申請的互斥鎖(Exclusive Lock),隔離級別控制讀操作的行為:
- 在讀數據時是否使用共享鎖,申請何種類型的鎖;
- 事務持有共享鎖的時間;
- 讀操作引用被其他事務更新,但尚未提交的數據行時,控制讀操作的行為:
- 被阻塞,等待其他事務釋放互斥鎖;
- 讀沒有提交的數據,獲取更新之后的數據值;
在樂觀並發模式下,讀操作引用被其他事務更新,但尚未提交的數據行時,控制讀操作的行為:
- 獲取更新之前的數據值,從tempdb中讀取行版本(row version),該行版本在事務開始時已經提交;
在執行寫操作時,事務總是持有互斥鎖,直到事務結束才釋放,互斥鎖不受事務隔離級別的影響。在悲觀並發模式中,互斥鎖和任意鎖都不兼容,在同一時刻,同一個數據行上,只能有一個事務持有互斥鎖,就是說,寫操作是順序進行的,完全隔離的,不能並發執行。隔離和並發,此消彼長。
一,事務運行時的隔離級別
SQL Server 數據庫級別默認的事務隔離級別是Read Committed,用戶不能修改Database-Level默認的隔離級別,但是,用戶能夠修改Session-Level默認的事務隔離級別。
每一個事務都運行在一個特定的隔離級別內,該隔離級別是由會話(session)決定的。SQL Server定義的五個隔離級別,都是為了定義讀數據的行為:
- READ UNCOMMITTED :允許臟讀、數據不可重復讀和數據范圍不可重復讀
- READ COMMITTED:防止臟讀(讀取未提交的數據更新),允許數據不可重復讀和數據范圍不可重復讀
- REPEATABLE READ:防止臟讀和數據不可重復讀,允許數據范圍不可重復讀
- SERIALIZABLE:防止臟讀、數據不可重復讀和數據范圍不可重復讀
- SNAPSHOT:快照隔離級別,主要是為了避免讀寫相互阻塞
事務的隔離級別共有5個,使用SET命令修改Session-Level的隔離級別,使用DBCC UserOptions 查看當前Session的隔離級別:
SET TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SNAPSHOT | SERIALIZABLE }
注意:隔離級別只能定義讀操作的行為,無法定義寫操作的行為,寫操作跟寫操作與讀操作都是互斥的。
在任何隔離級別下,事務在執行寫操作時都申請互斥鎖(exclusive lock),持有互斥鎖直到事務結束,互斥鎖不受隔離級別的控制;而共享鎖(Shared Lock)受到隔離級別的控制,隔離級別影響Shared Lock的申請和釋放:
- 在 Read Uncommitted隔離級別下,讀操作不會申請Shared Lock;
- 在 Read Committed(不使用row-versioning),Repeatable Read 和 Serializable隔離級別下,都會申請Shared Lock;
- 在 Read Committed(不使用row-versioning) 隔離級別下,在讀操作執行時,申請和持有Share Lock;一旦讀操作完成,釋放Shared Lock;
- 在 Repeatable Read 和 Serializable隔離級別下,事務會持有Shared Lock,直到事務結束(提交或回滾);
- 在Serializable隔離級別下,事務會持有范圍Shared Lock(Range Lock),鎖定一個范圍,在事務活躍期間,其他事務不允許在該范圍中進行更新(Insert 或 delete)操作;
二,悲觀並發模式下的隔離級別
在悲觀並發模式下,可以設置4個隔離級別,分別是READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ和SERIALIZABLE,其中READ UNCOMMITTED、REPEATABLE READ和SERIALIZABLE是悲觀並發模式獨有的。
1,臟讀
當事務運行在 READ UNCOMMITTED 隔離級別下時,有可能會讀取到其他事務未提交的數據更新。
- 例如,事務A修改了數據,把數據由1修改2,但是還沒有提交,
- 之后,另一個事務B讀取了該數據,結果是2,
- 在事務B讀取數據之后,事務A回滾,那么該數據的最終值是1。
出現臟讀的原因是讀操作不加共享鎖,讀操作不會被寫操作阻塞,使得讀操作可能會讀取到寫事務未提交的值。
2,數據不可重復讀
當事務運行在 READ COMMITTED 隔離級別下時,在同一個事務內,兩次讀取同一個數據,可能讀取到不同的結果,這就是數據不可重復讀現象。
- 例如,事務A讀取了一個數據,值是1,事務未提交
- 之后,事務B修改了該數據,把數據值由1修改為2,並提交事務。
- 在事務B提交之后,事務A重新讀取該書,值為2
出現數據不可重復讀的原因是事務在讀取數據時申請了共享鎖,但是在語句執行完成之后,就立馬釋放了共享鎖。要想避免出現數據不可重復讀的現象,事務必須一直持有共享鎖,直到事務提交或回滾。
3,數據范圍不可重復讀
當使用where子句限定了數據范圍之后,事務在兩次執行相同的查詢,獲得的數據范圍不同,也就是說,在相同的數據范圍內,增加了新的數據。
- 例如,事務A查詢Age在10和20之間的男生,共有7個,事務未提交
- 事務B向數據表中插入一條新的數據,Age是15,事務提交
- 事務A重新查詢Age在10和20之間的男生,共有8個
出現數據范圍不可重復讀的原因是事務只申請了共享鎖,沒有申請范圍鎖,這就導致其他寫事務可以向特定的數據范圍內新增數據。
三,鎖
在悲觀並發模式下,事務使用鎖來實現數據的一致性。鎖是一種機制,鎖施加在資源上,表示鎖定該資源。鎖的所有者是事務,事務的特性決定了鎖的特性。
1,鎖定的資源
事務可以在數據行、數據頁、鍵范圍、分區和表上加鎖,
鎖定的資源類型,按照粒度的級別,從低向高分別是:
- RID:堆中的一行
- KEY:聚集索引中的鍵
- KEY Range:鍵范圍
- PAGE:堆表的數據頁或B-Tree的索引頁
- EXTENT:分區,連續的8個Page
- HoBT:堆或B-Tree, 用於保護堆(沒有聚集索引的表)和堆中的B 樹
- IDX:索引中的數據行
- OBJECT:表(整個表)
2,鎖模式
共享鎖(S鎖):讀操作在讀取的數據上施加共享鎖,用於select子句中
互斥鎖(X鎖):寫操作在更新的數據上施加互斥鎖,一個數據上,只能施加一個互斥鎖,X鎖和任意鎖都是互斥的。
更新鎖(U鎖):U鎖和S鎖是兼容的,在寫操作中,數據的更新過程分為兩步,第一步是找到更新的數據,對於找到的數據施加U鎖;第二步是在真正執行數據更新時,把U鎖轉換為X鎖。
意向鎖(I鎖):意向鎖有 IS、IX和IU 三種類型,當事務申請低粒度鎖時,事務會在相同的對象上,依次申請高粒度資源的意向鎖,直到表上的意向鎖。例如,事務A申請在數據行R上施加X鎖,同時,事務A會在包含該數據行R的數據頁上申請IX鎖,同時申請表級的IX鎖,也就是說,在一行上申請X鎖,那么該行所在的頁和表都會申請IX鎖。在這種情況下,其他事務就很容易探測到有事務在更新表中的數據,避免鎖住整個表。
鍵范圍鎖(Key-Range):鍵范圍鎖出現在SERIALIZABLE隔離級別,如果事務需要掃描一個范圍的數據,事務使用鍵范圍鎖,鎖定表中的特定范圍,避免其他事務向范圍中插入新的數據,鍵范圍鎖與特定的索引鍵關聯。
3,鎖持續的時間
X鎖持續到事務結束,S鎖的持續時間受到事務隔離級別的影響。
- READ UNCOMMITTED :不申請S鎖
- READ COMMITTED:申請S鎖,S鎖在讀取操作完成時就立馬釋放,S鎖持續的時間是語句執行的時間
- REPEATABLE READ:申請S鎖,當事務結束之后,立馬釋放S鎖,S鎖持續的時間是事務執行的時間
- SERIALIZABLE:申請S鎖,鎖定的資源是鍵范圍,當事務結束之后,立馬釋放S鎖,S鎖持續的時間是事務執行的時間
4,鎖的兼容性
自行百度
5,鎖升級
鎖升級是指鎖施加的資源的粒度增加,從低粒度升級到高粒度,這會使數據庫系統並發度降低,增加阻塞和死鎖的風險,但是帶來的好處是降低鎖管理的開銷、保證查詢執行的速度。
鎖升級的第一種方式是鎖占用的內存達到閾值。通常來說,一個鎖結構大概需要96B的內存空間,當數據庫中存在大量的鎖結構時,鎖結構會占用較多的內存空間。當SQL Server使用超過24%的Buffer Poll用於存儲鎖結構時,SQL Server 引擎會選擇一些正在持有的鎖的會話,把鎖升級到高層次的級別,使鎖鎖定的資源粒度變大,降低鎖的數量。
鎖升級的第二種方式是鎖的數量達到閾值。在一個表上,當一個會話申請的鎖的數量達到一定的閾值,閾值的默認值是5000,也就是說,當一個會話持有鎖的數量超過5000時,SQL Server就自動把鎖升級。
鎖升級是自動進行的,也可以使用LOCK HINT,手動控制鎖的粒度。
參考文檔: