Assume that there is a table which controls stock amount information.
CREATE TABLE products(
id INTEGER PRIMARY KEY,
remaining_amount INTEGER NOT NULL
);
INSERT INTO products(id, remaining_amount) VALUES (1, 1);
Now, user A and B try to take the last stock at the same time.
A/B: UPDATE products
SET remaining_amount = remaining_amount - 1
WHERE id = 1 and remaining_amount > 0;
The questions are:
remaining_amount
never be negative values? Do we need any explicit pessimistic row locking?READ COMMITTED
, REPEATABLE READ
, SERIALIZABLE
or READ UNCOMMITTED
(only for MySQL)?We noticed that each RDBMS(MySQL/Postgres/Oracle) ALWAYS, AUTOMATICALLY locks updating rows WITH ANY TRANSACTION ISOLATION LEVELS. It means that MySQL(InnoDB)'s READ UNCOMMITTED
works well.
A | B |
---|---|
SET TRANSACTION ISOLATIONLEVEL READ UNCOMMITTED; BEGIN; |
SET TRANSACTION ISOLATIONLEVEL READ UNCOMMITTED; BEGIN; |
UPDATE products SET remaining_amount = remaining_amount - 1 WHERE remaining_amount > 0; |
|
Query OK, 1 row affected Rows matched: 1 Changed: 1 Warnings: 0 |
|
UPDATE products SET remaining_amount = remaining_amount - 1 WHERE remaining_amount > 0; |
|
BLOCKED!!! | |
COMMIT; |
|
Query OK, 0 rows affected Rows matched: 0 Changed: 0 Warnings: 0 |
|
COMMIT; |
Special Thanks: @zyake
Other evidences on Postgres: https://twitter.com/zyake/status/1543229753968041984
When checking if a specific tuple satisfies the update condition or when performing an update operation, an exclusive lock per page is acquired, so it is serialized. https://github.com/postgres/postgres/blob/e3fcca0d0d2414f3a50d6fd40eddf48b7df81475/src/backend/access/heap/heapam.c#L3215 https://github.com/postgres/postgres/blob/e3fcca0d0d2414f3a50d6fd40eddf48b7df81475/src/backend/access/heap/heapam.c#L3447
mpyw-yattemita/mysql-postgres-update-locking-and-isolation-levels
However, it is not guaranteed to be conflict-free whenever a SELECT
subquery is included. Our investigation revealed that the results vary depending on the transaction isolation level, as follows:
UPDATE t SET v=v-1 WHERE id=1 AND v>0
UPDATE t SET v=v-1 WHERE EXISTS(SUBQUERY)
UPDATE t SET v=(SUBQUERY)-1 WHERE id=1 AND v>0
SET
, use READ COMMITED
.WHERE
, use REPEATABLE READ
and retry on serialization errors.Simple | Subquery WHERE | Subquery SET | |
---|---|---|---|
READ COMMITTED | ✅ | ❌ 5/6 Broken | ✅ |
REPEATABLE READ | ❗ Serialization Error | ❗ Serialization Error | ❗ Serialization Error |
SERIALIZABLE | ❗ Serialization Error | ❗ Serialization Error | ❗ Serialization Error |
READ COMMITED
will be broken:[B] Latter \ [A] Former | Before-Read Delay | Pre-Write Delay | Post-Write Delay |
---|---|---|---|
Act before A's commit | ❌ Broken | ❌ Broken | ❌ Broken |
Act after A's commit | ✅ | ❌ Broken | ❌ Broken |
SET
, any transaction isolation level works well. READ UNCOMMITTED
or READ COMMITTED
are recommended.WHERE
, use REPEATABLE READ
and retry on deadlock errors.Simple | Subquery WHERE | Subquery SET | |
---|---|---|---|
READ UNCOMMITTED | ✅ | ❌ 4/6 Broken | ✅ |
READ COMMITTED | ✅ | ❌ 5/6 Broken | ✅ |
REPEATABLE READ | ✅ | ❗ 1/6 Deadlock | ✅ |
SERIALIZABLE | ✅ | ❗ 1/6 Deadlock | ✅ |
READ UNCOMMITED
will be broken:[B] Latter \ [A] Former | Before-Read Delay | Pre-Write Delay | Post-Write Delay |
---|---|---|---|
Act before A's commit | ✅ | ❌ Broken | ❌ Broken |
Act after A's commit | ✅ | ❌ Broken | ❌ Broken |
READ COMMITED
will be broken:[B] Latter \ [A] Former | Before-Read Delay | Pre-Write Delay | Post-Write Delay |
---|---|---|---|
Act before A's commit | ❌ Broken | ❌ Broken | ❌ Broken |
Act after A's commit | ✅ | ❌ Broken | ❌ Broken |
REPEATABLE READ
will get deadlocks:[B] Latter \ [A] Former | Before-Read Delay | Pre-Write Delay | Post-Write Delay |
---|---|---|---|
Act before A's commit | ✅ | ✅ | ✅ |
Act after A's commit | ✅ | ✅ | ❗ Deadlock |
SERIALIZABLE
will get deadlocks:[B] Latter \ [A] Former | Before-Read Delay | Pre-Write Delay | Post-Write Delay |
---|---|---|---|
Act before A's commit | ✅ | ✅ | ❗ Deadlock |
Act after A's commit | ✅ | ✅ | ✅ |