# 資料庫 Transaction & Lock 筆記 ###### tags: `DB` DB 四大重要特性(ACID) - Atomicity: transaction 不可分割性,transaction 內所有操作全部完成或全部不完成,中間錯誤會被 rollback - Consistency: 保持資料完整一致性 - Isolation: 允許多筆 transaction 同時對資料讀寫跟修改能力,且避免多筆 transaction 執行時由於交叉執行而導致數據的不一致(race condition)。 - Durability: 對資料的修改是永久的,即便系統故障也不會丟失 基本要求 - transaction 執行過程有 error 將會自動 rollback,不必自行實作。 - transaction 執行成功完成 commit 即存入資料庫,除非儲存空間受損,否則資料不會流失。 - 完善的 lock 管理機制,避免 race condition 情形,有 deadlock resolution。 ## Race condition 多筆 transaction 同時對同一筆資料讀寫跟修改時可能會出現的狀況 - Dirty Read: 讀取到其他 transaction 尚未 commit 的資料 | Transaction 1 | Transaction 2 | | ------ | ----------- | | 修改商品價格(100元 -> 0元) | | | | 取得商品價格(0元) | | Rollback || | | 新增訂單(0元) | | | Commit | **Transaction 2 不該讀取到 Transaction 1 尚未 commit 的資料** - Non-repeatable reads: 同一個 transaction 中,執行多次相同的 query 條件,讀取欄位的資料不同 | Transaction 1 | Transaction 2 | | ------ | ----------- | | 取得 A 商品價格(3000元) | | | (其他指令...) | 修改 A 商品價格(4000元) | | (其他指令...) | Commit| | 取得 A 商品價格(4000元) || | 新增訂單(4000元)|| | Commit|| **Transaction 1 中取得 A 商品的價格應該要一致** - Phantom reads: 同一個 transaction 中,執行多次相同的 query 條件,取出的資料數目不同 | Transaction 1 | Transaction 2 | | ------ | ----------- | | 取得 A 商品的積木數(30個) || | (其他指令...) | 新增 A 商品積木 | | (其他指令...) | Commit| | 取得 A 商品的積木數(31個) || | 新增訂單(31個)|| | Commit|| **Transaction 1 中取得 A 商品的積木數應該要一致** - Write Skew: 不同的 transactions 執行某樣 query 條件操作驗證,然後再去做新增/修改動作,卻造成資料衝突 EX: 同一張票造成多人訂票 | Transaction 1 | Transaction 2 | | ------ | ----------- | | 取得票剩餘數(1 張) || | (其他指令...) | 取得票剩餘數(1 張) | | (其他指令...) | (其他指令...) | | 寫入訂票資料 | (其他指令...) | | Commit | 寫入訂票資料 | | | Commit | EX: 點數超支 | Transaction 1 | Transaction 2 | Transaction 3 | Transaction 4 | | ------ | ------ | ------ | ------ | | 取得商品資料(10000點)|| | 取得剩餘點數(11000點) | 取得商品資料(10000點)| | (其他指令...) | 取得剩餘點數(11000點) | 取得商品資料(10000點)| | (其他指令...) | (其他指令...) | 取得剩餘點數(11000點) | 取得商品資料(10000點)| | (其他指令...) | (其他指令...) | (其他指令...) |取得剩餘點數(11000點) | | 新增訂單(10000點) | (其他指令...) | (其他指令...) | (其他指令...) | | Commit | 新增訂單(10000點) | (其他指令...) | (其他指令...) | | | Commit | 新增訂單(10000點) | (其他指令...) | | | | Commit | 新增訂單(10000點) | | | | | Commit | ## Isolation Level Isolation Levels 有四種等級,各家資料庫皆會宣稱支援到何種等級,一般需要 transaction 情境至少會要求到 Repeatable Read 的等級。選擇 DB System 要考量其支援 Isolation Levels,否則要自行處理 race condition 問題。 1. Read Uncommitted: 代表 transaction 可以讀到別的 transaction 尚未 commit 的資料,在這個等級中 race condition 三個問題都沒有解決。 2. Read Committed: 代表 transaction 只能讀到別的 transaction 已經 commit 的資料,沒有 commit 的話就不會讀到,在這個等級解決了 Dirty Read 的問題。 3. Repeatable Read: 代表每次 transaction 要讀取特定欄位的資料時,只要 query 條件相同,讀取到的資料內容就會相同。在這個等級解決了 Non-repeatable reads 的問題。 4. Serializable: 代表在多個 transaction 同時執行時,只要 transaction 的順序相同時,得到的結果一定相同。比如說 Transaction A 先執行了接下來再執行 Transaction B,在同樣的條件下,每次執行都會得到一樣的結果。在這個等級下連同 Phantom reads 也會一併被解決。 ## 如何解決 race condition 問題 ### MYSQL InnoDB - InnoDB 預設 Isolation Level 為 Repeatable Read #### Record-level locks - 分成 shared (S) locks 和 exclusive (X) locks,該筆資料(index records) 被讀取時發行 S lock,修改/刪除時發行 X lock - S lock 可以發給多個 trancaction,所以多個 trancactions 可以讀取同一筆資料。 - X lock 只會發給一個 trancaction,且如果該筆資料已發行 S locks 給其他 trancaction 時,將不會發行 X lock。 - SX locks 機制即 transaction 讀取時其他 transactions 都可以讀,但 transaction 修改時其他 transactions 將無法讀取。 - InnoDB SX locks 提供不同粒度的 lock 種類: - Record lock: This is a lock on an index record. - Gap lock: This is a lock on a gap between index records, or a lock on the gap before the first or after the last index record - Next-key lock: This is a combination of a record lock on the index record and a gap lock on the gap before the index record. - 遇到 conflict 時 trancaction 會等待以取得相應 locks,lock 種類越多或 lock 範圍越大,越容易引起 Lock wait timeout。 - 提升到 Serializable 才會加入 Gap lock & Next-key lock,但容易引起 Lock wait timeout or Deadlock 及大大降低使用效能。 #### Intention Locks - 分成 Intention shared (IS) 和 Intention exclusive (IX),是自行上鎖的概念 - 加上 IS 會使其他 insert/update 會被檔下來,但取值不會被擋下 ``` SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 LOCK IN SHARE MODE #此時其他 transation 如果想在10~20內 insert/update 會被檔下來 #此時其他 transation 如果想在10~20內取值不會被擋下 ``` - 加上 XS 會使其他 insert/update 會被檔下來,但取值也會被擋下, 可防止 Phantom reads 及 Write Skew ``` SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE #此時其他 transation 如果想在10~20內 insert/update 會被檔下來 #此時其他 transation 如果想在10~20內取值會被檔下來 ``` - lock type compatibility || X | IX | S | IS | | ------ | ------ | ------ | ------ | ------ | | X |Conflict | Conflict | Conflict | Conflict | | IX |Conflict | Compatible | Conflict | Compatible | | S |Conflict | Conflict | Compatible | Compatible | | IS |Conflict | Compatible | Compatible | Compatible | #### Undo log 當對資料做變更時 innoDB 會寫入 Undo log,紀錄每個 transaction 變更內容,可依此作為 rollback 依據,並宣稱有 MVCC 功能,其實資料沒有多版本並存,如果 A transaction 出錯要 rollback 時,還原的資料會加上排他鎖(其他 transaction 無法讀取跟寫入),仍會出現無法讀取。 ### PostgreSQL - PostgreSQL 預設是 Read Committed 的 isolation level #### MVCC (Multiversion Concurrency Control) - 每一筆資料的變動都會增加額外 snapshot 並標記 version,太舊的版本會自動刪除,讀取時會取最新的 commit version - 資料只有 X lock,如同 SX lock 架構,更新資料必須沒有其他 X lock 存在。 - 資料帶有 snapshot 版本,讀取不需關注是否有 X lock,即使修改時也能讀取 - lock 種類較少,降低 deadlock 發生機率。 - 因為加入 version 管理機制,就單一 transaction 而言,效能會比 SX locks 差。 #### Predicate locking - transaction 讀取時會加入 predicate lock,若有其他 transaction 執行 predicate lock 修改並成功 commit (有新版的 commit version),則這次 transaction 將會回傳錯誤並 rollback - 可達到 Serializable isolation level - 不會讓任何 transaction 等待 - 降低效能 #### Table and row locks - PostgreSQL 提供多樣可自行加入的 table and row locks,可參考 [Explicit Locking](https://www.postgresql.org/docs/9.5/explicit-locking.html),以下舉 Row-level Lock Modes 為例 - `FOR UPDATE`: 類似 MYSQL InnoDB 的 IX, row 在 `FOR UPDATE` mode 將不能執行 `UPDATE`、`DELETE`、其他 Row-level Lock Modes 操作 - `FOR NO KEY UPDATE`: 除了允許 `SELECT FOR KEY SHARE` 與 `FOR UPDATE` 相同 - `FOR SHARE`: row 在 `FOR SHARE` mode 不能執行 `UPDATE`, `DELETE`, `SELECT FOR UPDATE` or `SELECT FOR NO KEY UPDATE` - `FOR KEY SHARE`: row 在 `FOR KEY SHARE` mode 不能執行 `UPDATE`, `DELETE`, `SELECT FOR UPDATE` ### DynamoDB - 遇 race condition 情形會 throw TransactionConflictException, 可能要視情況自行實作 auto-retries 機制 #### Isolation Level DynamoDB 提供 TransactWriteItems & TransactGetItems 來執行 transaction, 其宣稱的 Isolation Level 可參考 [DynamoDB transaction-isolation](https://docs.aws.amazon.com/en_us/amazondynamodb/latest/developerguide/transaction-apis.html#transaction-isolation) | Operation | Isolation Level | | ------ | ----------- | | DeleteItem | Serializable | | PutItem | Serializable | | UpdateItem | Serializable | | GetItem | Serializable | | BatchGetItem | Read-committed | | BatchWriteItem | NOT Serializable | | Query | Read-committed | | Scan | Read-committed | | Other transactional operation | Serializable | #### failed 情境 TransactWriteItems 失敗情境 1. 其中一個 condition expressions 沒有吻合 2. 同一 item 正在被其他 request 更新時( 其他的 PutItem, UpdateItem, or DeleteItem or TransactWriteItems) 3. Table 在不同的 account or region 時 4. 單一 item 超過 400 KB, local secondary index too large 時 5. Items 總量大於 4MB 6. data format 錯誤 TransactGetItems 失敗情境 1. 同一 item 當有其他 PutItem, UpdateItem, DeleteItem or TransactWriteItems request 在處理時 2. Table 在不同的 account or region 時 3. Items 總量大於 4MB 4. 超過 25 個 unique item 5. data format 錯誤 ## 參考資料 - [Transaction Isolation Levels for MySQL](https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html) - [Transaction Isolation for PostgreSQL](https://www.postgresql.org/docs/9.5/transaction-iso.html) - [InnoDB Locking](https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-intention-locks) - [PostgreSQL Explicit Locking](https://www.postgresql.org/docs/9.5/explicit-locking.html) - [Amazon DynamoDB Transactions](https://docs.aws.amazon.com/en_us/amazondynamodb/latest/developerguide/transaction-apis.html) - [DynamoDB Transactions Limits](https://docs.aws.amazon.com/en_us/amazondynamodb/latest/developerguide/Limits.html#limits-dynamodb-transactions) - [TritonHo RDBMS Course](https://github.com/TritonHo/slides/blob/master/Taipei%202019-04%20course/lesson1.pdf)