最近發現在 isolation 都設為 repeatable read 時,MySQL 與 PostgreSQL 在操作上有些不一樣的行為,以下會分成兩個實驗來比較差別。
先複習一下 Read Committed 與 Repeatable Read 的分別避免了什麼事情發生:
Read Committed 避免了兩個 transactions 同時執行時,後者讀取到未完成的修改 (dirty read),所以在 transaction 未完成的情況下,彼此之間的修改不會互相影響,直到其中一個 transaction commit 成功,其他 transaction 才會獲取修改後的值。
Repeatable read 則是禁止 transaction 在自身沒有做修改的情況下,同樣的查詢取得不同結果(non-repeatable),簡單來說 read committed 不會讀取到未送出的修改,repeatable read 則是不論其他 transaction 完成送出與否,都不會改變讀取的值。
# PostgreSQL 實驗 (記得切換 isolation level)
# 建立測試資料
CREATE TABLE "public"."accounts" ("id" serial, PRIMARY KEY ("id"));
ALTER TABLE "public"."accounts" ADD COLUMN "own" varchar;
ALTER TABLE "public"."accounts" ADD COLUMN "balance" int4;
INSERT INTO "public"."accounts" ("own", "balance") VALUES ('A', '100');
/*
SELECT * FROM accounts;
id | own | balance
----+-----+---------
1 | A | 100
*/
# 發起兩筆交易 (transaction)
-- 交易一
BEGIN;
UPDATE accounts SET balance = balance - 10;
SELECT * FROM accounts;
/*
id | own | balance
----+-----+---------
1 | A | 90
*/
-- 交易二
SELECT * FROM accounts;
/*
id | own | balance
----+-----+---------
1 | A | 100
*/
由上可以看出,repeatable read 避免了 dirty read 的問題,接著進行將交易一送出
-- 交易一
COMMIT;
-- 交易二
SELECT * FROM accounts;
/*
id | own | balance
----+-----+---------
1 | A | 100
*/
可以看出在 isolation 層級為 repeatable read 下不會發生 non-repeatable read,那我們再嘗試在交易二去修改值
-- 交易二
UPDATE accounts SET balance = balance - 10;
/*
ERROR: could not serialize access due to concurrent update
*/
-- 交易二
UPDATE accounts SET balance = 10;
/*
ERROR: could not serialize access due to concurrent update
*/
可以發現 postgres 會禁止改變已經在其他交易做過更動的欄位(需要在交易二做過 read 的動作)
# MySQL 實驗
# 建立測試資料
CREATE TABLE `accounts` (`id` serial, PRIMARY KEY (id));
ALTER TABLE `accounts` ADD COLUMN `own` varchar(255) NULL;
ALTER TABLE `accounts` ADD COLUMN `balance` int NULL;
INSERT INTO `accounts` (`own`, `balance`) VALUES ('A', '100');
SELECT * FROM accounts;
/*
+----+------+---------+
| id | own | balance |
+----+------+---------+
| 1 | A | 100 |
+----+------+---------+
*/
# 發起兩筆交易 (transaction)
-- 交易一
BEGIN;
UPDATE accounts SET balance = balance - 10;
SELECT * FROM accounts;
/*
+----+------+---------+
| id | own | balance |
+----+------+---------+
| 1 | A | 90 |
+----+------+---------+
*/
-- 交易二
BEGIN;
SELECT * FROM accounts;
/*
+----+------+---------+
| id | own | balance |
+----+------+---------+
| 1 | A | 100 |
+----+------+---------+
*/
由上確認 MySQL 在 repeatable read 下理所當然也不會有 dirty read 問題
-- 交易一
COMMIT;
-- 交易二
SELECT * FROM accounts;
/*
+----+------+---------+
| id | own | balance |
+----+------+---------+
| 1 | A | 100 |
+----+------+---------+
*/
MySQL 在 repeatable read 一樣避免了 non-repeatable read
-- 交易二
UPDATE accounts SET balance = balance - 10;
SELECT * FROM accounts;
/*
+----+------+---------+
| id | own | balance |
+----+------+---------+
| 1 | A | 80 |
+----+------+---------+
*/
可以看到 MySQL 在一樣對同欄位做 atomic operation 操作是可以成功的,但是會發生上一步餘額還在 100 元,扣掉 10 元後瞬間變成 80 元的狀況。
MySQL 跟 PostgreSQL 都是藉由 ReadView(snapshot) 的建立時間點,來區分 read-committed 和 repeatable read,不過在 MVCC 的實作跟限制上看起來還是有很多不同,這次實驗可以發現 PostgreSQL 在 repeatable read 下可以偵測並阻止 Lost Update 的發生,感覺似乎更加嚴謹一些。
# references
https://www.postgresql.org/docs/current/transaction-iso.html (opens new window)
https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html (opens new window)