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