事務隔離

事務隔離（Transaction Isolation）定義了資料庫系統中一個事務中操作的結果在何時以何種方式對其他並發事務操作可見。隔離是事務ACID（原子性、一致性、隔離性、持久性）四大屬性之一。

## 並發控制

並發控制描述了資料庫事務隔離以保證數據正確性的機制。為了保證並行事務執行的準確執行，資料庫和存儲引擎在設計的時候著重強調了並發控制這一點。典型的事務相關機制限制數據的訪問順序（執行調度）以滿足可序列化和可恢復性。限制數據訪問意味著降低了執行的性能，並發控制機制就是要保證在滿足這些限制的前提下提供儘可能高的性能。在不損害正確性的情況下，可序列化的要求經常會為了性能而妥協，但是為了避免數據一致性的破壞，可恢復性不能夠妥協。

兩階段鎖是關係資料庫中最常見的提供了可序列化和可恢復性的並發控制機制，為了訪問一個資料庫對象，事務首先要獲得這個對象的鎖。對於不同的訪問類型（如對對象的讀或寫操作）和鎖的類型，如果另外一個事務正持有這個對象的鎖，獲得鎖的過程會被阻塞或者延遲。

## 讀現象舉例

ANSI/ISO SQL 92標準描述了三種不同的一個事務讀取另外一個事務可能修改的數據的「讀現象」。

下面的例子中，我們假設有兩個事務，事務1執行語句1。接著，事務2執行語句2並且提交，最後事務1再執行語句1。

查詢使用如下的數據表。

id｜name｜age

1｜Joe｜20

2｜Jill｜25

### 髒讀

當一個事務允許讀取另外一個事務修改但未提交的數據時，就可能發生髒讀（dirty reads）。

髒讀和不可重複讀類似，不同點在於事務2不需要提交就能造成語句1兩次執行的結果不同。在未提交讀隔離級別唯一禁止的是更新混亂，即早期的更新可能出現在後來更新之前的結果集中。

在我們的例子中，事務2修改了一行，但是沒有提交，事務1讀了這個沒有提交的數據。現在如果事務2回滾了剛才的修改或者做了另外的修改的話，事務1中查到的數據就是不正確的了。

事務 1｜事務 2

```sql
/* Query 1 */
SELECT age FROM users WHERE id = 1;
/* will read 20 */
```

```sql
/* Query 2 */
UPDATE users SET age = 21 WHERE id = 1;
/* No commit here */
```

```sql
/* Query 1 */
SELECT age FROM users WHERE id = 1;
/* will read 21 */
```

`ROLLBACK; /* lock-based DIRTY READ */`

在這個例子中，事務2回滾後就沒有id是1，age是21的數據行了。

### 不可重複讀

在一次事務中，當一行數據獲取兩遍得到不同的結果表示發生了不可重複讀（non-repeatable reads）.

在基於鎖的並發控制中「不可重複讀」現象發生在當執行SELECT操作時沒有獲得讀鎖或者SELECT操作執行完後馬上釋放了讀鎖；多版本並發控制中當沒有要求一個提交衝突（commit conflict）的事務回滾也會發生「不可重複讀」現象。

事務 1｜事務 2

```sql
/* Query 1 */
SELECT * FROM users WHERE id = 1;
```

```sql
/* Query 2 */
UPDATE users SET age = 21 WHERE id = 1;
COMMIT; /* in multiversion concurrency
   control, or lock-based READ COMMITTED */
```

```sql
/* Query 1 */
SELECT * FROM users WHERE id = 1;
COMMIT; /* lock-based REPEATABLE READ */
```

在這個例子中，事務2提交成功，因此他對id為1的行的修改就對其他事務可見了。但是事務1在此前已經從這行讀到了另外一個「age」的值。在可序列化（SERIALIZABLE）和可重複讀的隔離級別中，資料庫在第二次SELECT請求的時必須返回更新之前的值。在提交讀和未提交讀中，返回的是更新之後的值，這個現象就是不可重複讀。

有兩種策略可以避免不可重複讀。一個是要求事務2延遲到事務1提交或者回滾之後再執行。這種方式實現了T1, T2 的串行化調度。串行化調度可以支持可重複讀。

另一種被用在多版本並發控制的策略是允許事務2先提交，這樣能得到更好的並發性能。但因為事務1在事務2之前開始，事務1必須在其開始執行時間點的資料庫的快照上面操作。當事務1最終提交時候，資料庫會檢查其結果是否等價於T1, T2串行調度。如果等價，則允許事務1提交，如果不等價，事務1需要回滾並拋出個串行化失敗的錯誤。

使用基於鎖的並發控制，在可重複讀的隔離級別中，ID=1的行會被鎖住，在事務1提交或回滾前一直阻塞語句2的執行。在提交讀的級別，語句1第二次執行，age已經被修改了。

在多版本並發控制機制下，可序列化（SERIALIZABLE）級別，兩次SELECT語句讀到的數據都是事務1開始的快照，因此返回同樣的數據。但是，如果事務1試圖UPDATE這行數據，事務1會被要求回滾並拋出一個串行化失敗的錯誤。

在提交讀隔離級別，每個語句讀到的是語句執行前的快照，因此讀到更新前後不同的值。在這種級別不會有串行化的錯誤（因為這種級別不要求串行化），事務1也不要求重試。

### 幻影讀

在事務執行過程中，當兩個完全相同的查詢語句執行得到不同的結果集。這種現象稱為「幻影讀（phantom read）」

當事務沒有獲取範圍鎖的情況下執行SELECT ... WHERE操作可能會發生「幻影讀」。

「幻影讀」是不可重複讀的一種特殊場景：當事務1兩次執行SELECT ... WHERE檢索一定範圍內數據的操作中間，事務2在這個表中創建了（如INSERT）了一行新數據，這條新數據正好滿足事務1的「WHERE」子句。

事務 1｜事務 2

```sql
/* Query 1 */
SELECT * FROM users
WHERE age BETWEEN 10 AND 30;
```

```sql
/* Query 2 */
INSERT INTO users VALUES ( 3, 'Bob', 27 );
COMMIT;
```

```sql
/* Query 1 */
SELECT * FROM users
WHERE age BETWEEN 10 AND 30;
```

需要指出的是事務1執行了兩遍同樣的查詢語句。如果設了最高的隔離級別，兩次會得到同樣的結果集，這也正是資料庫在可序列化（SERIALIZABLE）隔離級別上需要滿足的。但是在較低的隔離級別上，第二次查詢可能會得到不同的結果集。

在可序列化隔離級別，查詢語句1在age從10到30的記錄上加鎖，事務2隻能阻塞直至事務1提交。在可重複讀級別，這個範圍不會被鎖定，允許記錄插入，因此第二次執行語句1的結果中會包括新插入的行。

## 隔離級別

在資料庫事務的ACID四個屬性中，隔離性是一個限制最寬鬆的。為了獲取更高的隔離等級，資料庫系統的通常使用鎖機制或者多版本並發控制機制。應用軟體也需要額外的邏輯來使其正常工作。

很多資料庫管理系統（DBMS）定義了不同的「事務隔離等級」來控制鎖的程度。在很多資料庫系統中，多數的事務都避免高等級的隔離等級（如可串行化）從而減少鎖的開銷。程式設計師需要小心的分析資料庫訪問部分的代碼來保證隔離級別的降低不會造成難以發現的代碼bug。相反的，更高的隔離級別會增加死鎖發生的機率，同樣需要編程過程中去避免。

由於更高的隔離級別中不存在被一個更低的隔離級別禁止的操作，DBMS被允許使用一個比請求的隔離級別更高的隔離級別。

ANSI/ISO SQL定義的標準隔離級別如下：

### 可串行化

可串行化（SERIALIZABLE）是最高的隔離級別。

在基於鎖機制並發控制的DBMS上，可串行化要求在選定對象上的讀鎖和寫鎖直到事務結束後才能釋放。在SELECT的查詢中使用一個「WHERE」子句來描述一個範圍時應該獲得一個「範圍鎖」（range-locks）。這種機制可以避免「幻影讀」現象。

當採用不基於鎖的並發控制時不用獲取鎖。但當系統探測到幾個並發事務有「寫衝突」的時候，只有其中一個是允許提交的。這種機制的詳細描述見快照隔離。

### 可重複讀

可重複讀（REPEATABLE READS）確保在同一事務中多次讀取相同數據時，結果始終一致。在可重複讀隔離級別中，基於鎖機制並發控制的DBMS需要對選定對象的讀鎖（read locks）和寫鎖（write locks）一直保持到事務結束，但不要求「範圍鎖」，因此可能會發生「幻影讀」。

### 提交讀

在提交讀（READ COMMITTED）級別中，基於鎖機制並發控制的DBMS需要對選定對象的寫鎖一直保持到事務結束，但是讀鎖在SELECT操作完成後馬上釋放（因此「不可重複讀」現象可能會發生，見下面描述）。和前一種隔離級別一樣，也不要求「範圍鎖」。

### 未提交讀

未提交讀（READ UNCOMMITTED）是最低的隔離級別。允許「髒讀」（dirty reads），事務可以看到其他事務「尚未提交」的修改。

通過比低一級的隔離級別要求更多的限制，高一級的級別提供更強的隔離性。標準允許事務運行在更強的事務隔離級別上。（如在可重複讀隔離級別上執行提交讀的事務是沒有問題的）

## 默認隔離級別

不同的DBMS默認隔離級別也不同。大多資料庫允許用戶設置隔離級別。有些DBMS在執行一個SELECT語句時使用額外的語法來獲取鎖（如SELECT ... FOR UPDATE來獲得在訪問的數據行上的排他鎖）。

## 隔離級別、讀現象和鎖

### 隔離級別vs讀現象

隔離級別｜髒讀｜不可重複讀｜幻影讀

未提交讀｜可能發生｜可能發生｜可能發生

提交讀｜可能發生｜可能發生

可重複讀｜可能發生

可序列化

「可能發生」表示這個隔離級別會發生對應的現象，「-」表示不會發生。

值得一提的是，避免以上三種現象雖然可以滿足 ANSI 對可串行化（Serializable）級別的定義，但是其並非真正的可串行化，不能保證執行效果和串行執行完全一致，可能會出現串行化異常，例如寫偏差和只讀事務偏差。

### 隔離級別vs 鎖持續時間

在基於鎖的並發控制中，隔離級別決定了鎖的持有時間。"C"-表示鎖會持續到事務提交。

"S" –表示鎖持續到當前語句執行完畢。如果鎖在語句執行完畢就釋放則另外一個事務就可以在這個事務提交前修改鎖定的數據，從而造成混亂。

隔離級別｜寫操作｜讀操作｜範圍操作 (...where...)

未提交讀｜S｜S｜S

提交讀｜C｜S｜S

可重複讀｜C｜C｜S

可序列化｜C｜C｜C

## 外部連結

- Oracle® Database Concepts, chapter 13 Data Concurrency and Consistency, Preventable Phenomena and Transaction Isolation Levels
- Oracle® Database SQL Reference, chapter 19 SQL Statements: SAVEPOINT to UPDATE, SET TRANSACTION
- in JDBC: Connection constant fields, Connection.getTransactionIsolation, Connection.setTransactionIsolation(int)
- in Spring Framework: @Transactional, Isolation