在現(xiàn)代數(shù)據(jù)庫應(yīng)用程序中,并發(fā)是不可避免的,因為多個用戶或多個應(yīng)用程序?qū)嵗龝瑫r訪問和更新相同的數(shù)據(jù)。高效地處理并發(fā)是數(shù)據(jù)庫管理系統(tǒng)(DBMS)必須面對的核心挑戰(zhàn)之一。若處理不當,將可能導致數(shù)據(jù)不一致或無法滿足性能需求。本文將詳細討論 SQL Server 中的并發(fā)、常見的并發(fā)問題,以及 SQL Server 提供的事務(wù)隔離級別來平衡性能與數(shù)據(jù)一致性。
什么是并發(fā)
并發(fā)是指多個事務(wù)(Transaction)或操作同時訪問或修改相同的數(shù)據(jù)。數(shù)據(jù)庫需要通過鎖(Lock)、事務(wù)隔離級別(Isolation Level)等機制,盡量減少并發(fā)沖突并確保數(shù)據(jù)一致性。
舉個簡單的例子:
為了避免此類沖突或保證沖突可控,需要了解 SQL Server 提供的多種隔離級別以及具體的并發(fā)問題類型。
常見的并發(fā)問題
當兩個或更多事務(wù)對同一行或同一數(shù)據(jù)集進行讀寫時,就可能出現(xiàn)以下幾種并發(fā)問題:
丟失更新(Lost Update)
當兩個事務(wù)讀取到相同的記錄并先后修改該記錄時,最后一個提交的事務(wù)覆蓋了前一個事務(wù)提交的結(jié)果,導致前一個事務(wù)的更新內(nèi)容被“丟失”。
場景示例:
1) 事務(wù) A 讀取某產(chǎn)品價格為 100 元,并打算減價 10 元。
2) 事務(wù) B 幾乎同一時間也讀取該產(chǎn)品價格為 100 元,并打算減價 5 元。
3) 事務(wù) A 將更新后的價格(90 元)寫回并提交。
4) 事務(wù) B 將更新后的價格(95 元)寫回并提交,把 A 的減價覆蓋掉,導致 A 的更新丟失。
臟讀(Dirty Read)
當一個事務(wù)讀取到另一個事務(wù)尚未提交(或已經(jīng)回滾)的數(shù)據(jù)時,就會產(chǎn)生臟讀。
若該事務(wù)最終回滾,則第一個事務(wù)讀到的那條“更新”實際上從未正式存在過,導致數(shù)據(jù)可能出現(xiàn)不一致。
場景示例:
1) 事務(wù) A 更新某客戶的信用額度為 10000 元,但尚未提交。
2) 此時事務(wù) B 讀取該客戶信用額度并發(fā)現(xiàn)是 10000 元,基于這個信息進行后續(xù)邏輯。
3) 如果事務(wù) A 最后回滾,信用額度繼續(xù)保持原有值 5000 元。B 相當于用了一條 “不存在的更新” 做決策。
不可重復讀(Non-Repeatable Read)
在同一事務(wù)中,兩次讀取同一行時,若中間有別的事務(wù)更新了該行,就會出現(xiàn)前后讀取數(shù)據(jù)不一致的情況,即“同一個查詢在同一事務(wù)里,前后兩次讀取結(jié)果不一致”。
場景示例:
1) 事務(wù) A 兩次讀取同一個客戶的地址信息。
2) 在 A 的兩次讀取之間,事務(wù) B 修改了該地址信息。
3) 事務(wù) A 第一次讀到的是“北京市朝陽區(qū)”,第二次讀到的是“上海市浦東新區(qū)”,產(chǎn)生不可重復讀。
幻讀(Phantom Read)
指在同一事務(wù)里,一次查詢和下一次“同樣的查詢”獲取到的結(jié)果集行數(shù)不一致,因為中間可能有其他事務(wù)插入或刪除符合查詢條件的新數(shù)據(jù)行,導致事務(wù)產(chǎn)生“幻覺”——仿佛多了一行或少了一行數(shù)據(jù)。
場景示例:
1) 事務(wù) A 使用 SELECT * FROM Orders WHERE Amount > 1000
讀取符合金額大于 1000 的訂單列表。
2) 在事務(wù) A 二次執(zhí)行相同查詢之前,事務(wù) B 插入了一條新訂單,金額也大于 1000。
3) 事務(wù) A 再次執(zhí)行相同的查詢時,就會“發(fā)現(xiàn)”一條新行,好像出現(xiàn)了“幻影”數(shù)據(jù)。
事務(wù)隔離級別
為了應(yīng)對上述并發(fā)問題,SQL Server 提供了多種隔離級別(Isolation Level),它們在性能與數(shù)據(jù)一致性之間做出了不同程度的權(quán)衡。常用的事務(wù)隔離級別包括:
未提交讀(READ UNCOMMITTED)
已提交讀(READ COMMITTED)(SQL Server 默認)
可重復讀(REPEATABLE READ)
可序列化(SERIALIZABLE)
快照(SNAPSHOT)
如何選擇合適的隔離級別
事務(wù)隔離級別的選擇往往需要在“讀一致性需求”和“性能需求”之間做出平衡:
如果對數(shù)據(jù)一致性要求極高(如財務(wù)系統(tǒng)),往往需要使用更嚴格的隔離級別(如 SERIALIZABLE 或 SNAPSHOT)。
如果對讀性能要求很高、對一致性容忍度相對較寬(如統(tǒng)計報表類場景),則可在 READ COMMITTED 或 READ UNCOMMITTED 之間進行考慮。
SNAPSHOT 隔離級別在許多實際應(yīng)用中是一個較平衡的方案,既減少鎖爭用,又避免大部分并發(fā)問題。
下表簡要概括了常見隔離級別與會遇到的問題:
隔離級別 | 臟讀 | 不可重復讀 | 幻讀 | 丟失更新 |
---|
READ UNCOMMITTED | 可能 | 可能 | 可能 | 可能 |
READ COMMITTED (默認) | 否 | 可能 | 可能 | 可能 |
REPEATABLE READ | 否 | 否 | 可能 | 否 |
SERIALIZABLE | 否 | 否 | 否 | 否 |
SNAPSHOT | 否 | 否 | 否 | 否(*) |
說明:
在 SNAPSHOT 隔離級別下,通過行版本控制機制避免大多數(shù)并發(fā)沖突,但一些業(yè)務(wù)邏輯層面的“邏輯沖突”仍有可能發(fā)生,需要進一步使用樂觀并發(fā)控制或顯式鎖來處理。
示例:理解并發(fā)讀取、更新與回滾
假設(shè)我們有一個簡單的 Customer
表,包含以下字段:
CustomerID | CustomerCode | CustomerName |
---|
1 | Code_1 | 張三 |
2 | Code_2 | 李四 |
... | ... | ... |
測試數(shù)據(jù)
-- 1. 首先創(chuàng)建測試表
CREATE TABLE Customer (
CustomerID INT PRIMARY KEY,
CustomerCode VARCHAR(10),
CustomerName VARCHAR(50)
);
-- 2. 插入測試數(shù)據(jù)
INSERT INTO Customer VALUES (1, 'Code_1', '張三');
INSERT INTO Customer VALUES (2, 'Code_2', '李四');
-- 窗口 1 (事務(wù) 1):
BEGIN TRANSACTION;
-- 先讀取初始值
SELECT CustomerCode FROM Customer WHERE CustomerID = 1;
-- 應(yīng)該顯示 Code_1
-- 第一次更新
UPDATE Customer
SET CustomerCode = 'Code_101'
WHERE CustomerID = 1;
-- 模擬長時間操作
WAITFOR DELAY '00:00:10';
-- 第二次更新
UPDATE Customer
SET CustomerCode = 'Code_1101'
WHERE CustomerID = 1;
-- 最后回滾所有操作
ROLLBACK TRANSACTION;
-- 或者 COMMIT TRANSACTION; 如果想要提交更改
-- 窗口 2 (事務(wù) 2):
-- 可以設(shè)置不同的隔離級別來觀察行為差異
-- 使用 READ UNCOMMITTED (會看到未提交的更改)
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN TRANSACTION;
-- 第一次讀取
SELECT CustomerCode FROM Customer WHERE CustomerID = 1;
-- 等待幾秒后再次讀取
WAITFOR DELAY '00:00:05';
-- 第二次讀取
SELECT CustomerCode FROM Customer WHERE CustomerID = 1;
-- 再等待幾秒
WAITFOR DELAY '00:00:05';
-- 第三次讀取
SELECT CustomerCode FROM Customer WHERE CustomerID = 1;
COMMIT TRANSACTION;
-- 或者使用 READ COMMITTED (默認級別,只能看到已提交的更改)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION;
-- 重復上述查詢操作
SELECT CustomerCode FROM Customer WHERE CustomerID = 1;
WAITFOR DELAY '00:00:05';
SELECT CustomerCode FROM Customer WHERE CustomerID = 1;
WAITFOR DELAY '00:00:05';
SELECT CustomerCode FROM Customer WHERE CustomerID = 1;
COMMIT TRANSACTION;
-- 使用 SNAPSHOT 隔離級別前需要先啟用數(shù)據(jù)庫的SNAPSHOT功能
ALTER DATABASE testdb
SET ALLOW_SNAPSHOT_ISOLATION ON;
-- 然后可以使用 SNAPSHOT 隔離級別
SET TRANSACTION ISOLATION LEVEL SNAPSHOT;
BEGIN TRANSACTION;
-- 重復上述查詢操作
SELECT CustomerCode FROM Customer WHERE CustomerID = 1;
WAITFOR DELAY '00:00:05';
SELECT CustomerCode FROM Customer WHERE CustomerID = 1;
WAITFOR DELAY '00:00:05';
SELECT CustomerCode FROM Customer WHERE CustomerID = 1;
COMMIT TRANSACTION;
當兩個事務(wù)同時訪問 CustomerID = 1
的記錄時可能發(fā)生如下場景:
事務(wù) 1 先讀取到 CustomerCode = Code_1
。
事務(wù) 1 更新 CustomerCode
到 Code_101
并在此事務(wù)中保持鎖,執(zhí)行耗時約 10 秒。
事務(wù) 2 此時讀取到更新后的 Code_101
(若在 READ COMMITTED 級別下,需要事務(wù) 1 提交后才可見,否則就需要 SNAPSHOT 或其他機制)。
事務(wù) 1 在 10 秒后再次更新 CustomerCode
為 Code_1101
。
事務(wù) 2 重新讀取數(shù)據(jù),得到了新的 Code_1101
。
事務(wù) 1 最后決定回滾(ROLLBACK),會將 CustomerCode
恢復到初始值 Code_1
。
事務(wù) 2 若再次讀取,就會發(fā)現(xiàn) Code_1
。
如果應(yīng)用不想讓普通用戶見到“中間的更新值”,就需要讓讀取操作只讀取已經(jīng)提交的數(shù)據(jù),這就依賴于我們選擇的事務(wù)隔離級別。如果隔離級別過低,例如 READ UNCOMMITTED,就可能出現(xiàn)事務(wù) 2 先讀到事務(wù) 1 的未提交更新,最后又發(fā)現(xiàn)實際已經(jīng)回滾的數(shù)據(jù),從而產(chǎn)生數(shù)據(jù)不一致的問題。
小結(jié)
并發(fā)是多用戶環(huán)境下數(shù)據(jù)庫系統(tǒng)必須解決的問題。
不同的并發(fā)問題包括:丟失更新、臟讀、不可重復讀和幻讀。它們在不同場景下給數(shù)據(jù)一致性帶來不同挑戰(zhàn)。
SQL Server 提供了多種事務(wù)隔離級別(從 READ UNCOMMITTED 到 SERIALIZABLE 以及 SNAPSHOT),分別在性能和一致性上做出了不同的權(quán)衡。
實際應(yīng)用中需要結(jié)合具體業(yè)務(wù)需求和系統(tǒng)壓力,選擇恰當?shù)母綦x級別與并發(fā)控制技術(shù)(如鎖管理、行版本控制、樂觀并發(fā)控制等),才能在高并發(fā)環(huán)境下既保證數(shù)據(jù)一致性又維持良好的性能表現(xiàn)。
希望本文能幫助你更深入地理解 SQL Server 中的并發(fā)原理和常見問題。在后續(xù)探討中,可以結(jié)合實際業(yè)務(wù)邏輯與測試案例來進一步驗證哪種隔離級別或并發(fā)控制手段最為合適。祝你在數(shù)據(jù)庫并發(fā)處理方面取得更加理想的性能與一致性!
該文章在 2024/12/24 9:49:04 編輯過