定義
DBMS 提供了事務(Transactions)支持。事務是作爲 DBMS 中的邏輯單元分組執行的一系列數據庫操作。與在DBMS之外執行程序(例如,C程序)在許多方面都不同!
數據庫應用程序通常通過事務而不是單個操作訪問數據庫。例如,大型數據庫和數百個併發用戶:銀行、超市收銀臺、機票預訂、在線購買等。
之所以使用事務是因爲它們可以在以下情況下實施數據完整性:多個用戶可以同時修改和共享數據;事務、系統和媒體故障可能不時發生。
- 從高級語言的角度(如 SQL)
- 插入(INSERT)、選擇(SELECT)、更新(UPDATE)、刪除(DELETE)
- 開始(BEGIN)、提交(COMMIT)、中止(ABORT)/ 回滾(ROLLBACK)等;
BEGIN TRANSACTION
SELECT balance FROM Account WHERE name = `Steve';
UPDATE Account
SET balance = balance-500 WHERE name=`Steve';
SELECT balance FROM Account WHERE name = `Bob';
UPDATE Account
SET balance = balance+500 WHERE name = `Bob';
COMMIT
- 內部進程級別:操作對象爲數據表、行、單元或者內存頁。
- 讀(read)、寫(write)
- 開始(begin)、提交(commit)
- 中止(abort):表示事務未成功結束,撤消事務的所有操作
步驟 | Transactions |
1 | read(A) |
2 | write(A) (A := A - 500) |
3 | read(B) |
4 | write(B) (B:= B + 500) |
5 | commit |
其中,A 是 Steve 的賬戶餘額;B 是 Bob 的賬戶餘額。
ACID 屬性
DBMS 確保了事務的以下屬性:
- 原子性(Atomicity)
- 每個事務的執行都是原子性的,即要麼所有的操作都完成了(ALL),要麼根本沒有完成(NONE)。
- 如果一個事務因爲某些原因而不能完成,那麼 DBMS 必須消除部分事務的影響,以確保原子性。
- 一致性(Consistency)
- 數據庫的狀態在每個事務之前和之後是一致的。
- 中間狀態可能是不一致的。
- 隔離(Isolation)
- 每個事務的執行結果應該不受其他併發執行事務的影響。
- 其他事務不會看到目前事務下所有對象的信息,直至該事務完成。
- 耐用性(Durability)
- 一旦事務成功完成,它的效果應該會保存在數據庫中。
- 一旦提交,事務就不能恢復爲 abort,這種改變是持久的。
需要注意的是,這些屬性不是相互獨立的,但原子性是中心屬性。
一致性(Consistency)由應用開發者實現,因爲這要求開發者理解應用程序的數據模型,並且確保每個事務都以一種與現實世界中的合法更改相一致的方式更改數據。
而其他三種屬性則由事務管理器實現,最常用的技術有兩種:
- 鎖(Locking)—— 用於併發控制,如兩階段鎖(2PL:Two-phase Locking)
- 日誌(Logging)—— 用於恢復,如預寫式日誌(WAL:Write-Ahead Log)
鎖(Locking)
鎖是一種用於併發控制的技術,可保證事務的隔離性。鎖在數據庫中一般作用在對象上,如文件、表、記錄、頁等。
鎖的用法分成兩類:
- 共享鎖:多個事務可以同時獲取它。
- 互斥鎖:只有一個事務可以獲得它,導致其他試圖獲取它的事務等待。持有鎖的事務完成後,它釋放鎖,允許一個等待的事務獲取鎖。
兩階段鎖(2PL:Two-phase Locking)
鎖的使用和移除分爲兩個階段:
- 展開:獲取鎖,不釋放鎖。
- 收縮:釋放鎖,不獲取鎖。
2PL 基本協議使用兩種類型的鎖:
- 共享鎖(read-lock):在讀取對象之前通過事務與對象關聯。
- 互斥鎖(write-lock):在寫對象之前通過事務與對象關聯。
當讀寫鎖同時出現在一個對象身上的時候,鎖之間的兼容滿足以下關係:
鎖的類型 | 讀鎖(read-lock) | 寫鎖(write-lock) |
讀鎖(read-lock) | 兼容 | 不兼容 |
寫鎖(write-lock) | 不兼容 | 不兼容 |
缺點:在某些情況下,2PL可以從根本上限制事務間的交叉
優點:2PL使交叉變得安全,比如保證事務的可串行性。
可串行化(Serializability)意味着產生的數據庫狀態等於以串行方式運行事務的數據庫狀態。
可串行性是併發事務的主要正確性標準。
但是,2PL可能會出現死鎖,即,兩個或多個事務的相互阻塞,比如一下的情況:
T1 | T2 | 解釋 |
lock-r(A) | T1獲取了A的讀鎖 | |
read(A) | ||
lock-r(B) | T2獲取了B的讀鎖,與A的讀鎖相互不影響 | |
read(B) | ||
lock-w(B) | T1想獲取B的寫鎖,但是需要等待T2釋放B的讀鎖才能獲取到 | |
write(B) | ||
lock-w(A) | ||
write(A) | T2想獲取A的寫鎖,但是需要等待T1釋放A的讀鎖才能獲取到 |
如何使用鎖定技術提高併發性?
- 細化鎖類型
- 區分讀鎖和寫鎖。
- 讀操作通常比寫操作更頻繁,而且讀同一個對象的多個事務不會相互干擾
- 鎖定粒度
- 數據庫 > 表 > 記錄 > 頁面 > 數據庫表頁記錄
- Table-level locks > Record-level locks
- 放鬆隔離(Isolation)的概念
日誌(Logging)
日誌是一種用於恢復的技術,可保證事務的原子性和持續性。日誌是一個僅追加(append-only)的文件,它記錄對對象的建議更改。當多個事務併發運行時,日誌記錄是交錯的。
恢復相當於撤銷或重做日誌中的更改:
- 撤銷(Undo)尚未提交的操作。
- 重新執行(Redo)已提交但尚未寫入磁盤的操作。
原子性(Atomicity)是通過定義當且僅當日誌中記錄了更改時才提交事務來實現的。
持久性(Durability)是通過在系統啓動時讀取日誌並確保每個提交的事務都在數據庫中應用了其更改來實現的。
預寫式日誌(WAL:Write-Ahead Log)
預寫式日誌(WAL)是最基本的規則,它確保在嘗試從崩潰中恢復時,數據庫的每個更改記錄都是可用的。
主要思想:
- 對對象的任何更改都首先記錄在日誌中,即,包含該對象的舊值和新值的記錄。
- 在提交事務之前,必須將日誌中的記錄寫入持久存儲。
因此,提交事務的定義爲:“日誌記錄(包括提交記錄)已寫入持久存儲的事務”。
對於一個日誌記錄來說,其典型的字段爲:
- LSN:每一個日誌記錄都有一個獨一無二的日誌序列碼(Log Sequence Number)
- prevLSN:同一個事物中上一個日誌記錄的序列碼
- transID
- type:日誌內容的類型,包括 update,commit,abort,end 等等
- 對於更新的日誌記錄,可能還會有 pageID,length,offset,before-image,after-image
WAL 對性能的提升體現在:
- 通常會顯著減少磁盤寫的數量
- 支持對日誌文件進行一次同步,而不是對數據文件進行多次同步
- 支持在線備份和時間點恢復
併發事務(Concurrent Transactions)
交錯處理:事務交錯在單個CPU中。
並行處理:事務在多個cpu中並行執行。
使用併發同時執行事務將提高數據庫性能,具體包括:
- 增加吞吐量(已完成事務的平均數量)
- 例如,當一個事務正在等待從磁盤讀取一個對象時,CPU可以處理另一個事務
- (因爲 I/O 活動可以與 CPU 活動並行執行)
- 減少延遲(完成事務的平均時間)
- 例如,短事務與長事務的交錯執行通常允許短事務更快地完成。
但是 DBMS 必須保證事務的交錯不會導致數據庫的不一致,也就是說要實現併發控制。
併發控制的實現主要是爲了要防止以下的問題:
- 丟失更新問題(The lost update problem)
- 寫入衝突(write-write conflicts)
- 當兩個事務更新同一對象時發生,且一個事務可能會覆蓋已由另一個事務更新的對象的值
- 骯髒讀取問題(The dirty read problem)
- 寫讀衝突(write-read conflicts)
- 當一個事務可以讀取另一個事務更新但尚未提交的對象的值時發生
- 不可重複讀取問題(The unrepeated read problem)
- 讀寫衝突(read-write conflicts)
- 一個事務可以更改一個對象的值,該對象已被另一個事務讀取,但仍在進行中(可以爲該對象發出兩個read,或在讀取該對象後發出一個write)
- 幽靈讀取問題(The phantom read problem)
- 當一個事務 T1 更新的元組滿足另一個事務的搜索條件時發生,因此通過相同的搜索條件,事務在不同的時間獲得不同的結果。
如果使用串行執行事務的話,就不會出現 骯髒讀取、不可重複讀取問題 和 幽靈讀取問題 這幾種情況了。
未重複讀取與幽靈讀取的差別 | |
不可重複讀取 | 幽靈讀取 |
執行相同的選擇(SELECT)兩次會產生相同的元組集合,但是屬性值可能不同 | 執行相同的選擇(SELECT)兩次會產生兩組不同的元組 |
讀取受另一個事務更新(UPDATE)影響的對象時可能發生 | 當查詢一組從另一個事務中插入(INSERT)、刪除(DELETE)、更新(UPDATE)中受影響的元組時可能發生 |
可以使用記錄級鎖定(record-level lock)來防止 | 可以使用表格級鎖定(table-level lock)來防止 |
也就是說,對於由更新(UPDATE)造成的讀取不一致的情況,可以分別通過記錄級鎖定(record-level lock)和表格級鎖定(table-level lock)兩種類型的鎖來防止相應的問題。
SQL 對事務的支持
- SQL標準沒有強制實施特定的鎖定方案或強制執行特定的行爲
- 顯式事務可能沒有 BEGIN TRANSACTION 語句,但必須以 COMMIT 或 ABORT (ROLLBACK) 語句結束。
- 當沒有給出顯式的事務語句時,每個SQL語句都被認爲是一個事務。
- 爲了讓程序員對事務開銷有更多的控制,SQL允許他們指定隔離級別,即。事務對併發事務準備容忍的干擾程度。
核心思想就是,權衡性能(更好的併發訪問)與一致性(數據庫的完整性)
SQL-92 定義了事務隔離的4個級別:
- 未提交讀(Read Uncommitted)
- 已提交讀(Read Committed)
- 可重複讀(Repeatable Read)
- 串行(Serializable)
通過以下命令可以指定事務隔離的級別:
SET TRANSACTION ISOLATION LEVEL serializable;
不同的隔離級別排除了不同的問題:(限制由弱到強)
隔離級別 | 含義 | 骯髒讀取 | 不可重複讀取 | 幽靈讀取 |
未提交讀(Read Uncommitted) | 一個事務可以看到尚未提交的其他事務所做的更改。這可能相當危險。在對只讀數據執行查詢時使用它,或者在查詢是否返回未提交數據無關緊要的情況下使用它。 | 出現 | 出現 | 出現 |
已提交讀(Read Committed) | 一個事務只看到其他事務提交的更改。它是數據庫應用程序中最常用的隔離級別。 當希望最大化應用程序之間的併發性,但不希望查詢看到未提交的數據時,可以使用它。 |
不出現 | 出現 | 出現 |
可重複讀(Repeatable Read) | 事務所觸及的對象被鎖定,並且不能被併發事務更新或刪除。 當希望應用程序之間具有某種程度的併發性,但不希望在事務期間更改單個對象時,可以使用它 |
不出現 | 不出現 | 出現 |
串行(Serializable) | 所有事務都與其他事務完全隔離。它是安全的,但可能會導致顯著的性能下降。 當希望應用程序之間具有某種程度的併發性,但不希望在不同的時間運行查詢時返回不同的結果集時,可以使用它。 |
不出現 | 不出現 | 不出現 |
更高的隔離級別減少了用戶可能遇到的併發問題的類型,但是需要更多的系統資源,並且增加了一個事務阻塞其他事務的機會。
不同的 DBMS 實現隔離級別的方式非常不同。丟失更新所需的隔離級別取決於數據庫管理系統的實現。但一般來說,它可能需要最高的水平序列化來防止它。
較低的隔離級別增加了許多用戶同時訪問數據的能力,但也增加了用戶可能會遇到併發性影響的數量
相反,更高的隔離級別減少了用戶可能遇到的併發影響的類型,但是需要更多的系統資源,並增加了一個事務阻塞另一個事務的機會。
因此,選擇適當的隔離級別取決於平衡應用程序的數據完整性需求和每個隔離級別的開銷。