這是基礎的最後一篇了,要來講鎖
任何 RDBMS 都會有鎖,這是為了並行使用同時避免彼此更新發生衝突,進而造成資料完整性問題。
合理作法是盡可能讓必定發生的鎖只在最低的層級發生,例如可以 page 鎖就不要 table 鎖、可以 row 鎖就不要 page 鎖。
但是,取得鎖,這個動作,本身也會消耗資源,如果硬要用 row 鎖,那有可能會發生一個操作要數百萬的鎖,這樣會沒效率,在這種狀況,會考慮使用更高層級的鎖。
鎖有以下幾種層級
| 層級 | 說明 |
|---|---|
| RID / KEY | 堆積資料表中的資料列識別碼,或索引鍵。於可序列化交易中,會使用索引鍵上的鎖定來鎖住資料列範圍。可序列化交易會在本章稍後討論。 |
| PAGE | 資料頁或索引頁。 |
| EXTENT | 連續的八個頁面。 |
| HoBT(Heap or B-Tree) | 單一索引的堆積或 B-Tree。 |
| TABLE | 整張資料表,包含所有索引。 |
| FILE | 資料庫中的一個檔案。 |
| METADATA | 中繼資料資源。 |
| ALLOCATION_UNIT | 資料表會被分成三種配置單元:資料列資料、資料列溢位資料,以及 LOB(Large Object,大型物件)資料。配置單元上的鎖定,會鎖住資料表的三種配置單元之一。 |
| DATABASE | 整個資料庫。 |
為了避免前面說的那種取一百萬鎖的狀況,SQL Server 再鎖定資料表內的某資源的時候,會在那個資源的上一層取得一個叫做意圖鎖定。
例如 : 如果 SQL Server 需要鎖定一個 RID 或 KEY,他也會在包含該資料列的 PAGE 上取得意圖鎖定,如果今天 Lock Manager 判斷再 page 鎖的話會更有效率,那他就會把鎖定層級往上到 page 層。
上述說的升級鎖,要注意 row 鎖不會升級成 page 鎖;會直接升級成 table 鎖。
如果 table 有進行分割,那 SQL Server 可以鎖定分割區。
SQL Server 升級鎖的門檻
但是我可以用 LOCK_ESCALATION 選項,針對特定 TABLE 去變更這個門檻
| 值 | 說明 |
|---|---|
| TABLE | 即使使用分割資料表,鎖定也會升級到資料表層級。 |
| AUTO | 在分割資料表上,此值允許鎖定升級到分割區層級,而不是整張資料表。 |
| DISABLE | 此值會停用鎖定升級到資料表層級的行為;但當必須使用資料表鎖定來保護資料完整性時除外。 |
| 選項 | 說明 |
|---|---|
MAX_DURATION |
線上索引重建或 SWITCH 操作在觸發 ABORT_AFTER_WAIT 動作之前,所等待的時間長度,以分鐘為單位指定。 |
ABORT_AFTER_WAIT |
可用的動作如下:• NONE 表示操作會以一般優先權繼續等待。• SELF 表示該操作會被終止。• BLOCKERS 表示目前正在封鎖該操作的所有使用者交易都會被終止。 |
WAIT_AT_LOW_PRIORITY |
功能上等同於 MAX_DURATION = 0, ABORT_AFTER_WAIT = NONE。 |
接下來要示範操作,重建一個非叢集索引,然後設定 LOCK_ESCALATION,並且同時指定如果有任何操作封鎖索引重建超過一分鐘,就終止這個封鎖。
--Create the database
CREATE DATABASE [Lock]
ON PRIMARY
(
NAME = N'Lock',
FILENAME = 'C:\Program Files\Microsoft SQL Server\MSSQL17.PRIMARYREPLICA\MSSQL\DATA\Lock.mdf'
),
FILEGROUP MemOpt CONTAINS MEMORY_OPTIMIZED_DATA DEFAULT
(
NAME = N'MemOpt',
FILENAME = 'C:\Program Files\Microsoft SQL Server\MSSQL17.PRIMARYREPLICA\MSSQL\DATA\MemOpt'
)
LOG ON
(
NAME = N'Lock_log',
FILENAME = 'C:\Program Files\Microsoft SQL Server\MSSQL17.PRIMARYREPLICA\MSSQL\DATA\Lock_log.ldf'
);
GO
USE [Lock];
GO
--Create and populate numbers table
DECLARE @Numbers TABLE
(
[數字] INT
);
;WITH CTE([數字])
AS
(
SELECT 1 AS [數字]
UNION ALL
SELECT [數字] + 1
FROM CTE
WHERE [數字] < 100
)
INSERT INTO @Numbers
SELECT [數字] FROM CTE;
--Create and populate name pieces
DECLARE @Names TABLE
(
[名字] VARCHAR(30),
[姓氏] VARCHAR(30)
);
INSERT INTO @Names
VALUES
('Peter', 'Carter'),
('Michael', 'Smith'),
('Danielle', 'Mead'),
('Reuben', 'Roberts'),
('Iris', 'Jones'),
('Sylvia', 'Davies'),
('Finola', 'Wright'),
('Edward', 'James'),
('Marie', 'Andrews'),
('Jennifer', 'Abraham');
--Create and populate Addresses table
CREATE TABLE dbo.[地址]
(
[地址編號] INT NOT NULL IDENTITY PRIMARY KEY,
[地址列1] NVARCHAR(50),
[地址列2] NVARCHAR(50),
[地址列3] NVARCHAR(50),
[郵遞區號] NCHAR(8)
);
INSERT INTO dbo.[地址]
VALUES
('1 Carter Drive', 'Hedge End', 'Southampton', 'SO32 6GH'),
('10 Apress Way', NULL, 'London', 'WC10 2FG'),
('12 SQL Street', 'Botley', 'Southampton', 'SO32 8RT'),
('19 Springer Way', NULL, 'London', 'EC1 5GG');
--Create and populate Customers table
CREATE TABLE dbo.[客戶]
(
[客戶編號] INT NOT NULL IDENTITY PRIMARY KEY,
[名字] VARCHAR(30) NOT NULL,
[姓氏] VARCHAR(30) NOT NULL,
[帳單地址編號] INT NOT NULL,
[送貨地址編號] INT NOT NULL,
[信用額度] MONEY NOT NULL,
[餘額] MONEY NOT NULL
);
SELECT * INTO #Customers
FROM
(
SELECT
(SELECT TOP 1 [名字] FROM @Names ORDER BY NEWID()) AS [名字],
(SELECT TOP 1 [姓氏] FROM @Names ORDER BY NEWID()) AS [姓氏],
(SELECT TOP 1 [數字] FROM @Numbers ORDER BY NEWID()) AS [帳單地址編號],
(SELECT TOP 1 [數字] FROM @Numbers ORDER BY NEWID()) AS [送貨地址編號],
(
SELECT TOP 1 CAST(RAND() * [數字] AS INT) * 10000
FROM @Numbers
ORDER BY NEWID()
) AS [信用額度],
(
SELECT TOP 1 CAST(RAND() * [數字] AS INT) * 9000
FROM @Numbers
ORDER BY NEWID()
) AS [餘額]
FROM @Numbers a
CROSS JOIN @Numbers b
) a;
INSERT INTO dbo.[客戶]
SELECT * FROM #Customers;
GO
--This table will be used later in the chapter
CREATE TABLE dbo.[記憶體客戶]
(
[客戶編號] INT NOT NULL IDENTITY
PRIMARY KEY NONCLUSTERED HASH WITH(BUCKET_COUNT = 20000),
[名字] VARCHAR(30) NOT NULL,
[姓氏] VARCHAR(30) NOT NULL,
[帳單地址編號] INT NOT NULL,
[送貨地址編號] INT NOT NULL,
[信用額度] MONEY NOT NULL,
[餘額] MONEY NOT NULL
)
WITH(MEMORY_OPTIMIZED = ON);
GO
INSERT INTO dbo.[記憶體客戶]
(
[名字],
[姓氏],
[帳單地址編號],
[送貨地址編號],
[信用額度],
[餘額]
)
SELECT
[名字],
[姓氏],
[帳單地址編號],
[送貨地址編號],
[信用額度],
[餘額]
FROM dbo.[客戶];
GO
CREATE INDEX idx_姓氏 ON dbo.[客戶]([姓氏]);
GO
--Set LOCK_ESCALATION to AUTO
ALTER TABLE dbo.[客戶] SET (LOCK_ESCALATION = AUTO);
GO
-- 如果資料表有分割,SQL Server 可以把鎖升級到「分割區層級」,不一定直接升級到整張表。
--Set WAIT_AT_LOW_PRIORITY
ALTER INDEX idx_姓氏 ON dbo.[客戶] REBUILD
WITH
(
ONLINE = ON
(
WAIT_AT_LOW_PRIORITY
(
MAX_DURATION = 1 MINUTES,
ABORT_AFTER_WAIT = BLOCKERS
--線上重建索引時,盡量低優先權等待,不要馬上去擋別人。
--但如果等超過 1 分鐘還是被別的交易擋住,就把那些 blocker 殺掉,讓索引重建繼續。
)
)
);
GO
當一個交易已經在某個資源上拿到某種鎖的時候,另一個交易還能不能在同一個資源上拿另一種鎖?
這時候就會牽涉到各種鎖的態樣
最常見最大宗的鎖有三種,共享、排他、更新鎖。
共享鎖 S:
只看資料,不改資料
所以這種鎖不太會有什麼影響,可以跟很多人一起看資料一起同時持有這種鎖。
排他鎖 X:
要改這筆資料,其他人不要讀,不要改
這會用在例如 update、delete、insesrt 上
只要有這個鎖存在,其他人都不准來拿鎖
更新鎖 U:
這個比較特別,這意思是我現在需要更新這筆資料,但還沒開始更新,先卡位,避免等等升級成排他鎖而發生死結。
Sch-S 鎖 :
這個在統計資訊更新的時候有說過,這是結構鎖,我當時說的是,為了穩定讀取與使用物件結構,所以拿來鎖結構確保不會有人來修改物件結構。
但後面沒有細說
這不是哪來鎖一般資料的 Insert、Update、Delete 的
這是用來鎖統計資訊、鎖引、欄位定義、資料表結構、metadata等等會用到的東西的。
中文可以理解成 : 結構描述穩定鎖,所以她最重要的是為了穩定。
例如 :
SELECT *
FROM TABLENAME
很多人會以為,SELECT 只拿 S 鎖,但實際上SQL Server 在查詢的時候,也會需要確認這張表的 metadata 是穩定的,所以其實還會多拿一個 Sch-S 鎖。
按照我前面的說法可以猜想到,這個鎖不是拿來擋 DML 的所以持有這個鎖的時候,還是可以進行 DML 的操作。
他只會去擋另外一個等等會說明的 Sch-M 鎖,也就是 DDL 指令。
所以就可以理解到,有一個 SELECT 的交易還沒有 COMMIT,但是卻不能做 ALTER TABLE 的原因,就是因為那個 SELECT 現在還持有 Sch-S 鎖。
Sch-M 鎖 :
這跟 Sch-S 很像,中文叫做結構描述修改鎖。
這個鎖很強,不只會擋 DDL 也會擋 DML。
這是當有一個交易正在做 DML 的時候,就會持有這個鎖,擋住其他想要查詢想要修改的人。
這最常發生在更新統計資訊、重建索引
一個查詢在正在執行的時候,會去讀取統計資訊以便置做出執行計畫,讀取統計資訊這個動作,就是實際上發出 Sch-S 鎖的時候,所以才會有 SELECT 也會拿 Sch-S 鎖的現象。
而更新統計資訊,因為這是改 metadata,要把新的 metadata 寫回去的時候拿 Sch-M 鎖。
這時候就會發生,背景在更新統計資訊,阿前面的 SELECT 不能查的狀況。
這狀況我在統計資訊那邊也有說過怎麼解
就是把統計資訊設定成非同步更新,但副作用是那一次的查尋會因為沒有更新統計資訊,而使用爛的執行計畫,好處就是不會被這個鎖擋住。
重建索引的時候,當然一定鎖
不過也有一種 online 的重建索引方式,但儘管是這種方式也不代表全成完全不鎖
online 的方式是,會先短暫的拿 Sch-M,然後開始建置此時允許 DML,最後切換再一次短暫 Sch-M。
那這會有問題,如果再建置的時候進行一個很長時間的 DML,就會導致最後要切換的時候重建索引一直拿不到 Sch-M 鎖,線上重建索引就會很久,是卡在這。
這也是我在上一個線上維護鎖定選項中,設定一個
WAIT_AT_LOW_PRIORITY
(
MAX_DURATION = 1 MINUTES,
ABORT_AFTER_WAIT = BLOCKERS
)
的用意。
Online index rebuild 需要 Sch-M 的時候,如果被其他交易擋住,就先低優先權等。
等超過 1 分鐘,就殺掉 blocker。
一般鎖
| 類型 | 說明 |
|---|---|
| Shared(S) | 用於讀取操作。 |
| Update(U) | 取得於可能會被更新的資源上。 |
| Exclusive(X) | 當資料被修改時使用。 |
| Schema Modification(Sch-M)/ Schema Stability(Sch-S) | 當 DDL 陳述式在資料表上執行時,會取得結構描述修改鎖定。當查詢正在編譯與執行時,會取得結構描述穩定鎖定。穩定鎖定只會封鎖需要結構描述修改鎖定的操作;而結構描述修改鎖定則會封鎖對資料表的所有存取。 |
| Bulk Update(BU) | 大量更新鎖定會在大量載入操作期間使用,允許多個執行緒平行將資料載入到資料表,同時封鎖其他程序。 |
| Key-range | 使用悲觀式隔離層級時,會在某個資料列範圍上取得鍵範圍鎖定。隔離層級會在本章稍後討論。 |
| Intent | 意圖鎖定用來保護鎖定階層中較低層級的資源,方式是發出訊號,表示它們有意取得共享鎖定或排他鎖定。 |
意圖鎖
| 類型 | 說明 |
|---|---|
| Intent shared(IS) | 保護鎖定階層中較低層級部分資源上的共享鎖定。 |
| Intent exclusive(IX) | 保護鎖定階層中較低層級部分資源上的共享鎖定與排他鎖定。 |
| Shared with intent exclusive(SIX) | 保護所有資源上的共享鎖定,以及鎖定階層中較低層級部分資源上的排他鎖定。 |
| Intent update(IU) | 保護鎖定階層中較低層級所有資源上的更新鎖定。 |
| Shared intent update(SIU) | S 鎖定與 IU 鎖定組合後產生的鎖定集合。 |
| Update intent exclusive(UIX) | X 鎖定與 IU 鎖定組合後產生的鎖定集合。 |
鎖定相容性
| Shared | Update | Exclusive | |
|---|---|---|---|
| Shared | Yes | Yes | No |
| Update | Yes | No | No |
| Exclusive | No | No | No |
如果兩個不同的程序已經在不同的資源上取的鎖,但兩邊都被封鎖,並且都在等待對方完成,這種情況就叫做死鎖。
一般來說遇到封鎖,絕大多數狀況都可以自動完成只是要等,但是死鎖不行,一定要親自介入,並且通常只有一邊的查詢可以完成。
SQL Server 有一個東西去監控死鎖,deadlock monitor,這東西遇到死鎖時,會檢查這些程序是否已經被指派交易優先權。
如果這些程序有不同的交易優先權,SQL Server 會終止優先權最低的程序。
如果它們的優先權相同,SQL Server 會根據資源使用量,終止成本最低的程序。
如果兩個程序的成本也相同,SQL Server 會隨機挑選其中一個程序並終止它。
遵循以下準則
意思是交易開始後,不要停下來等人按按鈕互動、輸入資料、確認畫面、選選項等等
任何會造成資料或物件被修改的動作,都是在交易的脈絡中發生的。
SQL Server 支援三種交易
交易最大的重點是 ACID
這意思是交易不可能只提交一部份,要馬全部成功,要馬全部失敗
但是 SQL Server 在這個部分有提供一個 save point,標記點,當發生 roll back的時候,save point 之前的內容會被提交,而 save point 之後的可以繼續提交或回覆。
這作用是,如果說今天有大量新增 table,但是中途有一個新增失敗,但是我不想要全部失敗,所以有一個 Save point 可以用,避免因為這個失敗導致其他已經大量做好的東西要全部重做。
下面範例就是我再 customers 做大量 insert,中間插入一個 addresses 的 insert 然後故意讓他失敗。
SELECT COUNT(*) InitialCustomerCount
FROM dbo.Customers;
SELECT COUNT(*) InitialAddressesCount
FROM dbo.Addresses;
BEGIN TRANSACTION;
DECLARE @Numbers TABLE
(
Number INT
);
;WITH CTE(Number)
AS
(
SELECT 1 Number
UNION ALL
SELECT Number + 1
FROM CTE
WHERE Number < 100
)
INSERT INTO @Numbers
SELECT Number
FROM CTE;
--Create and populate name pieces
DECLARE @Names TABLE
(
FirstName VARCHAR(30),
LastName VARCHAR(30)
);
INSERT INTO @Names
VALUES
('Peter', 'Carter'),
('Michael', 'Smith'),
('Danielle', 'Mead'),
('Reuben', 'Roberts'),
('Iris', 'Jones'),
('Sylvia', 'Davies'),
('Finola', 'Wright'),
('Edward', 'James'),
('Marie', 'Andrews'),
('Jennifer', 'Abraham');
--Populate Customers table
SELECT * INTO #Customers
FROM
(
SELECT
(SELECT TOP 1 FirstName FROM @Names ORDER BY NEWID()) FirstName,
(SELECT TOP 1 LastName FROM @Names ORDER BY NEWID()) LastName,
(SELECT TOP 1 Number FROM @Numbers ORDER BY NEWID()) BillingAddressID,
(SELECT TOP 1 Number FROM @Numbers ORDER BY NEWID()) DeliveryAddressID,
(
SELECT TOP 1 CAST(RAND() * Number AS INT) * 10000
FROM @Numbers
ORDER BY NEWID()
) CreditLimit,
(
SELECT TOP 1 CAST(RAND() * Number AS INT) * 9000
FROM @Numbers
ORDER BY NEWID()
) Balance
FROM @Numbers a
CROSS JOIN @Numbers b
) a;
INSERT INTO dbo.Customers
SELECT *
FROM #Customers;
SAVE TRANSACTION CustomerInsert;
BEGIN TRY
--Populate Addresses table - Will fail, due to length of Post Code
INSERT INTO dbo.Addresses
VALUES
(
'1 Apress Towers',
'Hedge End',
'Southampton',
'SA206 2BQ'
);
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION CustomerInsert;
END CATCH;
COMMIT TRANSACTION;
SELECT COUNT(*) FinalCustomerCount
FROM dbo.Customers;
SELECT COUNT(*) FinalAddressesCount
FROM dbo.Addresses;
交易的時候,必須把資料庫從一個一致的狀態,在交易完成的時候,仍然保持一致。
所有交易的資料都必須符合所有資料規則、條件約束、資料型別等等。
在條件約束的方面上,有變通方式,CHECK 可以用 NOCHECK 去暫停,等到暫停結束在把她調整回來,但是這樣做會讓 SQL Server 把條件約束標記為不信任,然後查詢優化器就會去忽略這個條件約束,一直到用下列與法去命令驗證資料。
ALTER TABLE MyTable WITH CHECK CHECK CONSTRAINT ALL;
這裡的紅字是一個效能調教的方向。之後會說
隔離性 S並行交易是否能夠看到另一個交易在提交之前所做的資料修改。
隔離交易可以避免交易異常現象,SQL Server 透過兩種方式來控制隔離性
每個交易都會以某個已經定義的隔離層級執行
隔離層級是一個很重要的項目,他影響甚廣,但是在說明隔離層級之前,要先說明交易可能會發生的異常現象。
以下三種現象我們稱為異常
當一個交易讀取到,實際上從未存在於資料庫中的資料時,就稱為 Dirty Reads
這個在 checkpoint 那一個地方有說過類似的東西。
在 checkpoint 之前,資料尚未被寫入實體檔案,仍然存在於記憶體中。
可是別人卻查的到,因為他是存在記憶體,這個可以視作一種 Dirty Pages。
但注意這是 Dirty Pages 不是 Dirty Reads。
另一種狀況是交易A 正在執行 INSERT,交易 B 就去查詢這筆 INSERT,結果最後交易 A ROLL BACK,那交易 B 查到的這筆 INSERT 就是 Dirty Reads。
這種狀況預設下
SQL SERVER 會發給 交易A 排他鎖
而交易B 取得的是共享鎖,所以交易 B 會被 A的排他鎖擋住
當一個交易讀取同一筆資料列兩次,但每次得到不同結果時,就會發生不可重複讀。
這種狀況預設如下
交易A 正在進行
取得 S 鎖 SELECT ID = 1 放掉 S 鎖
取得 S 鎖 SELECT ID = 2 放掉 S 鎖
取得 S 鎖 SELECT ID = 3 放掉 S 鎖 ….
此時 交易 B 去 UPDATE ID = 1,而且 COMMIT,因為 S鎖已經放掉了所以預設會讓他改
交易 A 尚未結束,他在最後一個 SELECT 還需要再去 SELECT ID = 1
這時候就會發生,在同一個交易裡,第一次讀 ID = 1 資料跟第二次 ID = 1 的資料不一樣
雖然這個案例在邏輯上是異常,但是在預設的 SQL Server 狀態下,會判定這是正常查詢。
這跟不可重複讀很像,只不過不可重複讀是針對 row 的值,而幻讀是在說 row 的數量。
但是有些時候,上面說的那三種異常,對我業務邏輯來說不一定就是異常。
所以 sql server 提供幾種隔離等級,去處理這些問題。
一共有四種悲觀、兩種樂觀等級。
悲觀隔離等級會使用鎖定來防止交易異常現象
樂觀隔離等級會使用資料列版本控制來做
這是限制最少的等級
它的運作方式是:寫入操作會取得鎖定,但讀取操作不會取得任何鎖定。
這表示在這個隔離層級下,讀取操作不會封鎖其他讀取者或寫入者。結果是,前面各節所描述的所有交易異常現象都有可能發生。
這是前面一直說的預設等級
它的運作方式是:讀取操作會取得共享鎖定,寫入操作也會取得鎖定。
一旦該資料已經被讀取,鎖釋放。
可以防止 dirty read,但是不可重複、幻讀仍然會發生。
這是 Read Committed 的延伸,接觸到的所有資料列取得取得共享鎖,但是跟 Read Committed 不同的是,這些鎖會持有到交易結束為止。
所以這種等級,dirty read 跟不可重複讀都不可能發生,但是幻讀還是有可能會發生。
還有由於持有鎖的時間變得更長,鎖以死鎖會更有可能發生。
這是最嚴格的隔離等級,也是最容易發生死鎖的等級。
它的運作方式不只是為寫入操作取得鎖定,也會為讀取操作取得鍵範圍鎖定,並且將這些鎖定持有到交易結束。
因為鍵範圍鎖定會以這種方式被持有,所以所有交易異常現象都不可能發生,包括幻讀。
這個跟悲觀隔離等級不一樣,悲觀隔離等級他可以在單一交易中去設定,但是樂觀隔離等級一定要在資料庫層級上做設定,並且 RCSI 會直接預設取代 Read Committed。
這裡的快照,跟前面擴展覆寫說的快照是不一樣的東西,這裡是只交易的快照。
一個交易開始之後,這個交易裡面的所有 SELECT,都會看到交易開始那一刻,已經提交的資料版本。
跟資料庫快照很像,他也是版本紀錄,當有人去更動資料時,會去紀錄舊版本的資料,而不是整份複製。
差別是,交易快照只存在這次交易。
如果是用這個快照隔離,那就不會發生悲觀的髒讀、不可重複讀、幻讀,而且在讀的時候也不會有鎖的問題。
沒有鎖死結機率就下降。
看似都很完美但這是有代價得
更準確來說,如果2019以後的有啟用 ADR,那他確實會存在使用者資料庫裏面的PVS,但沒啟用的話那一樣還是存在 tempDB。
讀取這個 SELECT statement 開始時,最後一個已提交版本。在 READ COMMITTED 隔離語意下,用 row versioning 取代讀取時的 S 鎖等待。
原本 Read Committed :
SELECT 用 S 鎖,遇到 UPDATE 的 X 鎖會等。
RCSI :
SELECT 遇到 X 鎖時,不等,直接去讀 UPDATE 之前的版本。
這跟快照隔離最大差異是,快照隔離的範圍是一個交易,RCSI 的範圍是一個語法。
上面都只是理論,用實際案例比較好理解
首先,隔離等級是在決定,當交易 A 正在處理資料時,交易 B 到底可以看到什麼、要不要等、看哪一個版本。
可以拆解成兩派
我們現在先設定一個場景
假設資料現在是 :
ID = 1
Balance = 1000
此時有一個交易 A
BEGIN TRAIN
UPDATE Account
SET Balance = 500
WHERE ID = 1;
-- 還沒 COMMIT
現在 A 已經把 1000 改成 500,但還沒提交。
此時交易B
SELECT Balance
FROM Account
WHERE ID = 1
不同隔離等級的差異就是 :
B 現在是要看到 500? 還是等 A? 還是直接看到以前的 1000?
Read Uncommitted
最鬆。
B 不管 A 有沒有提交,都直接看到 500。
但因為 A 還沒 COMMIT,所以他是有可能 ROLLBACK 的,那B就會看到一個其實從來沒有正式存在過的資料。
而這個就叫做髒讀。
Read Committed
這是SQL Server 預設。
這個規則是:
只能讀到已經提交的資料。
剛才 A 還有 COMMIT,所以因為規則,此時 B 只能等 A COMMIT,之後才看到 500。
這種狀況下髒讀不會發生。
但是會有下面這種狀況發生
--狀況是 交易 A 已經 COMMIT
--交易 B 開始交易
BEGIN TRAN
SELECT Balance
FROM Account
WHERE ID = 1
--這時候會看到 500
--但是交易 B 還沒結束他還在做別的事情
--這時候交易 C 又同時開始啟動交易
BEGIN TRAN
UPDATE Account
SET Balance = 900
WHERE ID = 1
--交易 C 還沒 COMMIT
--然後交易 B 再最後,出於業務需要他再一次的
SELECT Balance
FROM Account
WHERE ID = 1
--因為交易 C 還沒 COMMIT,這時候交易 B 會被鎖擋住
--接下來交易 C COMMIT 了
-- 那這時候交易 B 會看到 Balance = 900
變成對交易 B 來說,他從頭到尾都在同一個交易裡面,
可是第一次找是 500、
第二次找卻變成 900 了
而這就叫做不可重複讀。
發生的原因就是,在交易 B 第一次 SELECT 拿到 S 鎖之後,他馬上就放掉了,所以交易 C 才可以去改資料。
這個層級會直接解決剛剛交易 B 一拿到 S 鎖之後馬上放掉的問題,他會繼續持有到整個交易 B 結束。
因此他不會發生髒讀、不可重複讀。
但如果在改一下
現在改成交易 B 是看
BEGIN TRAN
SELECT Balance
FROM Account
WHERE ID > 1
假設結果是看到有兩筆
ID = 1
ID = 2
因為 Repeatable Read 有一個重點是,讀過的 ROW。
但他沒說那沒讀過的呢?
在交易 B 結束之前,又有一個交易 D 去寫
INSERT INTO Account .......
然後交易 B 因為還沒結束,然後也是因為業務需要,在COMMIT 又查一次 ID > 1
就會發現比第一次讀的時候多了一行資料。
這就是幻讀。
所以延續上面的例子
BEGIN TRAN
SELECT Balance
FROM Account
WHERE ID > 1
如果是這個等級,那就會把 ID > 1 都鎖住,交易 D 要 INSERT 那就只能等交易 B COMMIT。
那這個層級,就不會發生髒讀、不可重複讀、幻讀。
但是當然,這代價最大,因為他鎖最多,那死鎖的風險也就最高。
以上四個是悲觀隔離
接下來是樂觀隔離
在樂觀隔離中,想法就換了,他可以接受,去讀之前的版本。
Snapshot
回到 Read Uncommitted 的案例中
交易 A 去 UPDATE Balance = 500
交易 B 去查,在那邊說會是 500,但這裡不一樣
當在這個隔離等級的狀況下
交易 B 啟動的瞬間,他會去快照現在的資料樣貌
所以只要接下來交易 B 都還沒有結束,他就會一直用這個瞬間的資料來處理。
那這樣的話就不管那些什麼髒讀幻讀什麼讀了,因為之後資料不管怎樣變動,只要交易 B 還沒有結束,他都是看當下快照的結果,後續資料怎麼改都跟交易 B 無關。
RCSI
這跟 Read Committed 很像,也結合 Snapshot 的概念
交易 A UPDATE Balance = 500 還沒 commit
交易 B 去查會得到還沒 COMMIT 前的資料就是 1000
交易 A COMMIT,換交易 C UPDATE Balance = 900 還沒 COMMIT
交易 B 在接著查,會查到交易 A 提交的 500。
可以看出他跟 Snapshot 的差異是,Snapshot 的快照是綁在整個交易,而 RCSI 則是只限定在一個陳述式裡面。
最後,樂觀隔離是有代價的,因為要快照,就一定要有硬碟空間,有硬碟空間又會有I/O問題,然後就會有記憶體、CPU的資源要消耗。
交易的時間越長,裡面的快照就會越亂越肥,因為妳想用這種隔離方式就代表你會有同時交易的問題,同時交易每一個都要快照一次,如果時間又很長,快照就要存很久,這對硬體來說會有壓力。
然後如果在快照裡面兩個交易同時去 UPDATE 同一筆,例如說
交易A 去 UPDATE ID = 1 的
交易B 也去 UPDATE ID = 1的
悲觀式的只會鎖定,就等
樂觀式的會直接噴錯。
RCSI 的話會等,不會噴錯。
對於樂觀式的快照方式我寫的很籠統,再更細一點是他只會快照 row 的版本,可是這樣就太多了,有些東西要寫很細是真的可以寫很多,可是一些比較沒有實際用途的就不想寫出來增加負擔
交易一旦 COMMIT 成功,就算 SQL Server 當機、重開機、斷電等等,這筆交易也不能消失,稱為持久性。
正常交易流程
其中第三點,叫做 write-ahead logging(WAL,預寫式記錄) 。
預寫的意思是相對實際資料來說,因為 COMMIT 的時候實際資料不一定已經寫回 MDF 了。
但是後來 SQL SERVER 有透過一種功能放關這個規則,叫做 delayed durability。
這功能運作方式是 : 延後 log 快取 flush 到磁碟,直到下列其中一件事情發生
sp_flush_log 系統預存程序。這好處是 commit 會變快、log flush 等待時間變少、大量的小交易吞吐量會提升
缺點就是,沒有辦法保證一致性,突發的當機會讓已經成功的交易消失。
這東西是微軟提供的 DMV
-- 用來觀察超過 10 分鐘的交易
SELECT
name AS 交易名稱
,transaction_begin_time AS 交易開始時間
,CASE transaction_type
WHEN 1 THEN N'讀取/寫入'
WHEN 2 THEN N'唯讀'
WHEN 3 THEN N'系統'
WHEN 4 THEN N'分散式'
END AS 交易類型
,CASE transaction_state
WHEN 0 THEN N'初始化中'
WHEN 1 THEN N'已初始化但尚未開始'
WHEN 2 THEN N'作用中'
WHEN 3 THEN N'已結束'
WHEN 4 THEN N'提交中'
WHEN 5 THEN N'已準備'
WHEN 6 THEN N'已提交'
WHEN 7 THEN N'回復中'
WHEN 8 THEN N'已回復'
END AS 交易狀態
,SUBSTRING
(
TXT.text,
(er.statement_start_offset / 2) + 1,
(
(
CASE
WHEN er.statement_end_offset = -1
THEN LEN(CONVERT(NVARCHAR(MAX), TXT.text)) * 2
ELSE er.statement_end_offset
END - er.statement_start_offset
) / 2
) + 1
) AS 目前執行語句
,TXT.text AS 完整批次語句
,es.host_name AS 主機名稱
,CASE tat.transaction_type
WHEN 1 THEN N'讀取/寫入交易'
WHEN 2 THEN N'唯讀交易'
WHEN 3 THEN N'系統交易'
WHEN 4 THEN N'分散式交易'
ELSE N'未知'
END AS 交易類型說明
,SUSER_SNAME(es.security_id) AS 執行交易的登入帳號
,es.memory_usage * 8 AS 記憶體使用量KB
,es.reads AS 讀取次數
,es.writes AS 寫入次數
,es.cpu_time AS CPU時間
FROM sys.dm_tran_active_transactions tat
INNER JOIN sys.dm_tran_session_transactions st
ON tat.transaction_id = st.transaction_id
INNER JOIN sys.dm_exec_sessions es
ON st.session_id = es.session_id
INNER JOIN sys.dm_exec_requests er
ON er.session_id = es.session_id
CROSS APPLY sys.dm_exec_sql_text(er.sql_handle) TXT
WHERE st.is_user_transaction = 1
AND tat.transaction_begin_time < DATEADD(MINUTE, -10, GETDATE());
這個 DVM 用來觀察目前鎖定的資訊
通常會搭配
用來看目前等待資源的工作
SELECT
DB_NAME(tl.resource_database_id) AS 資料庫名稱
,tl.resource_type AS 資源類型
,tl.resource_subtype AS 資源子類型
,tl.resource_description AS 鎖定資源描述
,tl.request_mode AS 要求鎖定模式
,tl.request_status AS 要求狀態
,os.session_id AS 被封鎖Session
,os.blocking_session_id AS 封鎖來源Session
,os.resource_description AS 等待資源描述
,OBJECT_NAME(
CAST(
SUBSTRING(os.resource_description,
CHARINDEX('objid=', os.resource_description, 0) + 6, 9)
AS INT)
) AS 被鎖定資料表
FROM sys.dm_os_waiting_tasks os
INNER JOIN sys.dm_tran_locks tl
ON os.session_id = tl.request_session_id
WHERE tl.request_owner_type IN ('TRANSACTION', 'SESSION', 'CURSOR');
這兩個東西會擷取死鎖的資訊,然後寫成 ERROR LOG,不過已經漸漸淘汰不需要使用了
可以透過這個東西去查看死結,並且回溯查詢死結的詳細資訊。