mirror of
https://github.com/MariaDB/server.git
synced 2026-10-23 18:49:29 +03:00
MDEV-36832 UPDATE...IN(SELECT) acquires spurious gap locks on FK index
`UPDATE ticket SET is_valid=0 WHERE ticket_id IN
(SELECT ticket_id FROM ticket WHERE booking_id=1)` acquires S next-key
and gap locks on the FK secondary index `fk_ticket_2_booking`, causing
deadlocks between concurrent transactions operating on non-overlapping
`booking_id` values. The expected behavior — matching
`UPDATE ... WHERE ticket_id IN (1,2)` (explicit PK list) — is to only
lock the PRIMARY key records being updated.
**Root cause**: the optimizer converts the `IN(SELECT)` to a semi-join,
which creates two table references for the same table. The SQL layer
then processes the statement as `SQLCOM_UPDATE_MULTI`. In
`ha_innobase::store_lock()`, the read-only (subquery) table handle
receives `TL_READ`, but the existing `LOCK_NONE` optimization for DML
read tables was gated on `isolation_level <= READ_COMMITTED` and did
not include `SQLCOM_UPDATE_MULTI` at all. In `REPEATABLE READ` (the
default), the subquery's secondary index scan therefore acquired
`LOCK_S` next-key locks, including gap locks spanning into adjacent key
ranges.
**Fix**: add `SQLCOM_UPDATE_MULTI` to the `LOCK_NONE` (consistent read)
condition in `store_lock()`:
1. For `TL_READ` (binlog off or ROW format): use consistent read at
all isolation levels below `SERIALIZABLE`. This is safe because:
- The MVCC snapshot is stable within a transaction in
`REPEATABLE READ`
- Rows being modified still acquire X locks via the write-side
table handle (`F_WRLCK` / `LOCK_X`)
`SERIALIZABLE` is excluded: the `::external_lock()` upgrade of
`LOCK_NONE` to `LOCK_S` applies only to non-autocommit
transactions, so an autocommit multi-table UPDATE would otherwise
read its read-side tables via a consistent read at `SERIALIZABLE`,
weakening its guarantees.
2. For `TL_READ_NO_INSERT` (statement-based binlog): extend the
existing `READ COMMITTED` optimization to also cover
`SQLCOM_UPDATE_MULTI`
The fix is deliberately limited to `SQLCOM_UPDATE_MULTI` (not
`SQLCOM_UPDATE`) because single-table UPDATE with scalar subqueries on
other tables traditionally uses S locks to block concurrent reads of the
subquery table, and broadening the change there would alter observable
behavior for existing applications.
**Lock count before/after** (semi-join UPDATE on `booking_id=1`):
- Before: 5 lock structs, 5 row locks (IS + S×3 on FK index + IX +
X×2 on PK)
- After: 2 lock structs, 2 row locks (IX + X×2 on PK) — identical to
explicit `WHERE ticket_id IN (1,2)`
**Tests**:
`update_subquery_fk_gap_lock.test` — verifies that concurrent
`UPDATE...IN(SELECT)` operations on non-overlapping `booking_id` values
do not block each other. Two connections each update their own
`booking_id`'s tickets via semi-join subquery, then insert new tickets
for their `booking_id`. Both operations must succeed without lock wait
timeout, confirming that the spurious FK index gap locks are eliminated.
`update_subquery_fk_gap_lock_isolation.test` — ACID isolation regression
tests (requires ROW binlog format) verifying that replacing S locks with
consistent read (`LOCK_NONE`) on the semi-join read table does not cause
isolation violations at `REPEATABLE READ`:
1. **Phantom insert**: T1 updates `booking_id=1` tickets via semi-join,
T2 concurrently inserts a new ticket with `booking_id=1`. T2 must
not block (no S gap locks). T1's MVCC snapshot does not see the new
row, so T1 updates only the 2 original tickets. Correct under
snapshot isolation.
2. **Concurrent UPDATE on filter column**: T1 updates `booking_id=1`
tickets, T2 changes `ticket_id=2`'s `booking_id` from 1 to 2. Both
need X lock on `ticket_id=2`'s PK record — serialized via PK X
lock alone, proving FK index S locks are unnecessary.
3. **Concurrent DELETE**: T1 updates `booking_id=1` tickets, T2 deletes
`ticket_id=1`. Both need PK X lock — serialized, no isolation
violation.
4. **SERIALIZABLE S locks**: At `SERIALIZABLE`, the read-side table
keeps `LOCK_S`, so T2's INSERT into the subquery range must block
with `ER_LOCK_WAIT_TIMEOUT`. Confirms the fix does not weaken
`SERIALIZABLE` guarantees.
5. **Multi-table UPDATE on different tables**: T1 does
`UPDATE t_write JOIN t_read ... SET t_write.val = t_read.val`, T2
concurrently modifies `t_read`. T2 must not block (T1 holds no S
locks on `t_read`). T1's snapshot values are used. Correct under
snapshot isolation.
6. **MVCC snapshot consistency**: T1 establishes snapshot, T2 modifies
`t_read`, T1 does multi-table UPDATE using snapshot then re-reads.
Re-read must return the same values the UPDATE used, confirming
MVCC snapshot stability at `REPEATABLE READ`.
7. **SERIALIZABLE with autocommit**: the `::external_lock()` upgrade
of `LOCK_NONE` to `LOCK_S` does not apply to autocommit statements,
so `store_lock()` itself must not choose `LOCK_NONE` at
`SERIALIZABLE`. An autocommit `UPDATE...IN(SELECT)` stalled
mid-scan on a PK X lock must hold S next-key locks on the FK index
entries it has already scanned: an INSERT whose FK index entry
falls inside that range must block with `ER_LOCK_WAIT_TIMEOUT`.
`mdev-14846.test` depends on the multi-table UPDATEs' read-only
tables being read with S locks to produce the deadlock whose error
handling it verifies. Both transactions taking part in that deadlock
now run at `SERIALIZABLE`, where the read-only tables keep their S
locks, preserving the original deadlock scenario and its
`ER_LOCK_DEADLOCK` expectation unchanged. Tying this to the binary
log format instead would not work everywhere: the embedded server
has no binary log, so its read side always gets `TL_READ`.
This commit is contained in:
committed by
Georgi Kodinov
parent
d5813edd08
commit
f4a3b69bdf
@@ -22,6 +22,7 @@ UPDATE t1 SET t1.pk = -109 WHERE t1.f1 IN ( SELECT 'a' FROM t4 WHERE f >= 1 );
|
||||
SET DEBUG_SYNC='now SIGNAL con1_dml';
|
||||
connect con1,localhost,root,,test;
|
||||
SET DEBUG_SYNC='now WAIT_FOR con1_dml';
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
||||
begin;
|
||||
SELECT * FROM t1 for update;
|
||||
pk f1 f2 f3
|
||||
@@ -34,6 +35,7 @@ SET DEBUG_SYNC='now SIGNAL default_dml';
|
||||
SET DEBUG_SYNC='now SIGNAL con2_dml';
|
||||
connection default;
|
||||
SET DEBUG_SYNC='now WAIT_FOR default_dml';
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
||||
UPDATE t3 AS alias1 LEFT JOIN t3 AS alias2 ON ( alias1.f1 <> alias1.f2 ) SET alias1.f3 = 59 WHERE ( EXISTS ( SELECT t1.f3 FROM t1 WHERE t1.f1 = alias1.f1 ) ) OR alias2.f1 = 'h';
|
||||
connect con2,localhost,root,,test;
|
||||
set debug_sync='now WAIT_FOR con2_dml';
|
||||
|
||||
72
mysql-test/suite/innodb/r/update_subquery_fk_gap_lock.result
Normal file
72
mysql-test/suite/innodb/r/update_subquery_fk_gap_lock.result
Normal file
@@ -0,0 +1,72 @@
|
||||
#
|
||||
# MDEV-36832: UPDATE...IN(SELECT) acquires spurious gap locks on FK index
|
||||
#
|
||||
CREATE TABLE booking (
|
||||
booking_id INT PRIMARY KEY
|
||||
) ENGINE=InnoDB;
|
||||
CREATE TABLE ticket (
|
||||
ticket_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
booking_id INT NOT NULL,
|
||||
is_valid INT DEFAULT 1,
|
||||
KEY fk_ticket_2_booking (booking_id),
|
||||
FOREIGN KEY (booking_id) REFERENCES booking(booking_id)
|
||||
) ENGINE=InnoDB;
|
||||
INSERT INTO booking VALUES (1), (2);
|
||||
INSERT INTO ticket (booking_id) VALUES (1), (1), (2), (2);
|
||||
connect con1,localhost,root;
|
||||
SET SESSION innodb_lock_wait_timeout=1;
|
||||
connect con2,localhost,root;
|
||||
SET SESSION innodb_lock_wait_timeout=1;
|
||||
#
|
||||
# Con1: UPDATE tickets for booking_id=1 via subquery
|
||||
#
|
||||
connection con1;
|
||||
BEGIN;
|
||||
UPDATE ticket SET is_valid=0
|
||||
WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id=1);
|
||||
#
|
||||
# Con2: UPDATE tickets for booking_id=2 via subquery
|
||||
#
|
||||
connection con2;
|
||||
BEGIN;
|
||||
UPDATE ticket SET is_valid=0
|
||||
WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id=2);
|
||||
#
|
||||
# Con1: INSERT into booking_id=1 (should not be blocked by Con2)
|
||||
#
|
||||
connection con1;
|
||||
INSERT INTO ticket (booking_id) VALUES (1);
|
||||
#
|
||||
# Con2: INSERT into booking_id=2 (should succeed immediately)
|
||||
#
|
||||
connection con2;
|
||||
INSERT INTO ticket (booking_id) VALUES (2);
|
||||
#
|
||||
# Con1: reap INSERT — must succeed without lock wait timeout
|
||||
#
|
||||
connection con1;
|
||||
#
|
||||
# Commit both transactions
|
||||
#
|
||||
connection con1;
|
||||
COMMIT;
|
||||
connection con2;
|
||||
COMMIT;
|
||||
#
|
||||
# Verify: each booking has 2 updated + 1 new ticket
|
||||
#
|
||||
connection default;
|
||||
SELECT booking_id, is_valid, COUNT(*) c FROM ticket
|
||||
GROUP BY booking_id, is_valid ORDER BY booking_id, is_valid;
|
||||
booking_id is_valid c
|
||||
1 0 2
|
||||
1 1 1
|
||||
2 0 2
|
||||
2 1 1
|
||||
#
|
||||
# Cleanup
|
||||
#
|
||||
DROP TABLE ticket;
|
||||
DROP TABLE booking;
|
||||
disconnect con1;
|
||||
disconnect con2;
|
||||
@@ -0,0 +1,285 @@
|
||||
#
|
||||
# MDEV-36832: Negative isolation tests for UPDATE...IN(SELECT)
|
||||
# consistent read (LOCK_NONE) at REPEATABLE READ
|
||||
#
|
||||
# These tests verify that replacing S locks with consistent read
|
||||
# (LOCK_NONE) on the semi-join read table does NOT cause ACID
|
||||
# isolation violations at REPEATABLE READ.
|
||||
#
|
||||
CREATE TABLE booking (
|
||||
booking_id INT PRIMARY KEY
|
||||
) ENGINE=InnoDB;
|
||||
CREATE TABLE ticket (
|
||||
ticket_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
booking_id INT NOT NULL,
|
||||
is_valid INT DEFAULT 1,
|
||||
KEY fk_ticket_2_booking (booking_id),
|
||||
FOREIGN KEY (booking_id) REFERENCES booking(booking_id)
|
||||
) ENGINE=InnoDB;
|
||||
INSERT INTO booking VALUES (1), (2);
|
||||
INSERT INTO ticket (booking_id, is_valid) VALUES
|
||||
(1, 1), (1, 1),
|
||||
(2, 1), (2, 1);
|
||||
connect con1,localhost,root;
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
SET SESSION innodb_lock_wait_timeout=3;
|
||||
connect con2,localhost,root;
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
SET SESSION innodb_lock_wait_timeout=3;
|
||||
#
|
||||
# Test 1: Phantom insert — concurrent INSERT into subquery range
|
||||
#
|
||||
# T1 updates tickets for booking_id=1 via semi-join.
|
||||
# T2 concurrently inserts a new ticket with booking_id=1.
|
||||
# Without S gap locks (LOCK_NONE), T2 is NOT blocked.
|
||||
# T1's MVCC snapshot does not see the new row, so T1 updates
|
||||
# exactly the 2 original tickets. The new ticket remains is_valid=1.
|
||||
# This is correct under snapshot isolation (RR).
|
||||
#
|
||||
connection con1;
|
||||
BEGIN;
|
||||
UPDATE ticket SET is_valid=0
|
||||
WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id=1);
|
||||
connection con2;
|
||||
BEGIN;
|
||||
INSERT INTO ticket (booking_id, is_valid) VALUES (1, 1);
|
||||
COMMIT;
|
||||
connection con1;
|
||||
COMMIT;
|
||||
connection default;
|
||||
# Expect: ticket_ids 1,2 updated to is_valid=0; new ticket is_valid=1
|
||||
SELECT ticket_id, booking_id, is_valid FROM ticket
|
||||
WHERE booking_id=1 ORDER BY ticket_id;
|
||||
ticket_id booking_id is_valid
|
||||
1 1 0
|
||||
2 1 0
|
||||
5 1 1
|
||||
DELETE FROM ticket WHERE ticket_id > 4;
|
||||
UPDATE ticket SET is_valid=1;
|
||||
#
|
||||
# Test 2: Concurrent UPDATE on subquery filter column
|
||||
#
|
||||
# T1 updates tickets for booking_id=1. T2 changes ticket_id=2's
|
||||
# booking_id from 1 to 2. Both need X lock on ticket_id=2's PK
|
||||
# record, so they serialize regardless of FK index locks.
|
||||
# Proves: PK X locks alone ensure write-write serialization.
|
||||
#
|
||||
connection con1;
|
||||
BEGIN;
|
||||
UPDATE ticket SET is_valid=0
|
||||
WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id=1);
|
||||
connection con2;
|
||||
BEGIN;
|
||||
UPDATE ticket SET booking_id=2 WHERE ticket_id=2;
|
||||
connection con1;
|
||||
COMMIT;
|
||||
connection con2;
|
||||
COMMIT;
|
||||
connection default;
|
||||
# Both committed. Serialized via PK X lock.
|
||||
# ticket_id=1: is_valid=0 (T1), booking_id=1 (unchanged)
|
||||
# ticket_id=2: is_valid=0 (T1 ran first), booking_id=2 (T2 ran second)
|
||||
SELECT ticket_id, booking_id, is_valid FROM ticket ORDER BY ticket_id;
|
||||
ticket_id booking_id is_valid
|
||||
1 1 0
|
||||
2 2 0
|
||||
3 2 1
|
||||
4 2 1
|
||||
#
|
||||
# Test 3: Concurrent DELETE of a subquery-matched row
|
||||
#
|
||||
# T1 updates tickets for booking_id=1. T2 deletes ticket_id=1.
|
||||
# Both need PK X lock → serialized.
|
||||
#
|
||||
connection default;
|
||||
UPDATE ticket SET booking_id=1 WHERE ticket_id <= 2;
|
||||
UPDATE ticket SET is_valid=1;
|
||||
connection con1;
|
||||
BEGIN;
|
||||
UPDATE ticket SET is_valid=0
|
||||
WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id=1);
|
||||
connection con2;
|
||||
BEGIN;
|
||||
DELETE FROM ticket WHERE ticket_id=1;
|
||||
connection con1;
|
||||
COMMIT;
|
||||
connection con2;
|
||||
COMMIT;
|
||||
connection default;
|
||||
# ticket_id=1 deleted by T2 (ran second), ticket_id=2 updated by T1.
|
||||
SELECT ticket_id, booking_id, is_valid FROM ticket ORDER BY ticket_id;
|
||||
ticket_id booking_id is_valid
|
||||
2 1 0
|
||||
3 2 1
|
||||
4 2 1
|
||||
INSERT INTO ticket (ticket_id, booking_id, is_valid) VALUES (1, 1, 1);
|
||||
UPDATE ticket SET is_valid=1;
|
||||
#
|
||||
# Test 4: SERIALIZABLE must still acquire S locks
|
||||
#
|
||||
# At SERIALIZABLE, store_lock() keeps LOCK_S for the read-side
|
||||
# table, so the subquery's read MUST block concurrent writes.
|
||||
# T1 does UPDATE...IN(SELECT) at SERIALIZABLE.
|
||||
# T2 tries to INSERT into same booking_id range — must block.
|
||||
#
|
||||
connection con1;
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
||||
connection con2;
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
||||
connection con1;
|
||||
BEGIN;
|
||||
UPDATE ticket SET is_valid=0
|
||||
WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id=1);
|
||||
connection con2;
|
||||
BEGIN;
|
||||
INSERT INTO ticket (booking_id, is_valid) VALUES (1, 1);
|
||||
ERROR HY000: Lock wait timeout exceeded; try restarting transaction
|
||||
COMMIT;
|
||||
connection con1;
|
||||
COMMIT;
|
||||
connection default;
|
||||
UPDATE ticket SET is_valid=1;
|
||||
#
|
||||
# Test 5: Multi-table UPDATE on different tables — read table
|
||||
# uses consistent read (LOCK_NONE), no S locks
|
||||
#
|
||||
# T1: UPDATE t_write JOIN t_read SET t_write.val = t_read.val
|
||||
# WHERE t_read.status = 'active'
|
||||
# T2: UPDATE t_read SET status = 'cancelled' WHERE id = 2
|
||||
#
|
||||
# With LOCK_NONE on t_read, T2 must NOT block.
|
||||
# T1's MVCC snapshot sees 'active' for id=2 and uses val=20.
|
||||
# This is correct under snapshot isolation: equivalent to T1
|
||||
# running before T2 in a serial schedule.
|
||||
#
|
||||
CREATE TABLE t_read (
|
||||
id INT PRIMARY KEY,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
val INT NOT NULL
|
||||
) ENGINE=InnoDB;
|
||||
CREATE TABLE t_write (
|
||||
id INT PRIMARY KEY,
|
||||
val INT NOT NULL DEFAULT 0
|
||||
) ENGINE=InnoDB;
|
||||
INSERT INTO t_read VALUES (1, 'active', 10), (2, 'active', 20);
|
||||
INSERT INTO t_write VALUES (1, 0), (2, 0);
|
||||
connection con1;
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
BEGIN;
|
||||
SET DEBUG_SYNC='execute_command_after_close_tables SIGNAL t1_updated WAIT_FOR t2_done';
|
||||
UPDATE t_write w JOIN t_read r ON w.id = r.id SET w.val = r.val WHERE r.status = 'active';
|
||||
connection con2;
|
||||
SET DEBUG_SYNC='now WAIT_FOR t1_updated';
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
BEGIN;
|
||||
UPDATE t_read SET status='cancelled' WHERE id=2;
|
||||
COMMIT;
|
||||
SET DEBUG_SYNC='now SIGNAL t2_done';
|
||||
connection con1;
|
||||
COMMIT;
|
||||
connection default;
|
||||
# t_write.val reflects T1's snapshot: val=10 (id=1), val=20 (id=2).
|
||||
# t_read.id=2 is now 'cancelled' (by T2) but T1's snapshot saw
|
||||
# 'active'. Correct under snapshot isolation.
|
||||
SELECT w.id, w.val, r.status AS current_read_status
|
||||
FROM t_write w JOIN t_read r ON w.id = r.id ORDER BY w.id;
|
||||
id val current_read_status
|
||||
1 10 active
|
||||
2 20 cancelled
|
||||
#
|
||||
# Test 6: MVCC snapshot consistency — re-read within same
|
||||
# transaction after UPDATE must see consistent data
|
||||
#
|
||||
# T1 establishes snapshot, T2 modifies t_read, T1 does
|
||||
# multi-table UPDATE using snapshot, then T1 re-reads t_read.
|
||||
# The re-read must see the same data as the UPDATE used
|
||||
# (MVCC snapshot stability at RR).
|
||||
#
|
||||
connection default;
|
||||
UPDATE t_read SET status='active';
|
||||
UPDATE t_write SET val=0;
|
||||
connection con1;
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
BEGIN;
|
||||
SELECT * FROM t_read WHERE status='active';
|
||||
id status val
|
||||
1 active 10
|
||||
2 active 20
|
||||
connection con2;
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
BEGIN;
|
||||
UPDATE t_read SET status='cancelled', val=99 WHERE id=2;
|
||||
COMMIT;
|
||||
connection con1;
|
||||
UPDATE t_write w JOIN t_read r ON w.id = r.id
|
||||
SET w.val = r.val WHERE r.status = 'active';
|
||||
SELECT id, status, val FROM t_read WHERE status='active' ORDER BY id;
|
||||
id status val
|
||||
1 active 10
|
||||
2 active 20
|
||||
COMMIT;
|
||||
connection default;
|
||||
# t_write must have snapshot values: val=10 and val=20 (NOT val=99).
|
||||
# T2's change is invisible to T1's snapshot.
|
||||
SELECT id, val FROM t_write ORDER BY id;
|
||||
id val
|
||||
1 10
|
||||
2 20
|
||||
#
|
||||
# Test 7: SERIALIZABLE with autocommit must also acquire S locks
|
||||
#
|
||||
# The external_lock() upgrade of LOCK_NONE to LOCK_S at
|
||||
# SERIALIZABLE applies only to non-autocommit transactions, so
|
||||
# store_lock() must not hand out LOCK_NONE at SERIALIZABLE.
|
||||
# T1 (autocommit=1, SERIALIZABLE) runs UPDATE...IN(SELECT) and is
|
||||
# stalled on the PK X lock of the last matching row, held by T2.
|
||||
# By that point T1 has scanned the FK index entries (1,10),(1,20)
|
||||
# and must hold S next-key locks on them. T3 then inserts a ticket
|
||||
# whose FK entry (1,15) falls inside that locked range - it must
|
||||
# block.
|
||||
#
|
||||
connection default;
|
||||
DROP TABLE ticket;
|
||||
CREATE TABLE ticket (
|
||||
ticket_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
booking_id INT NOT NULL,
|
||||
is_valid INT DEFAULT 1,
|
||||
KEY fk_ticket_2_booking (booking_id),
|
||||
FOREIGN KEY (booking_id) REFERENCES booking(booking_id)
|
||||
) ENGINE=InnoDB;
|
||||
INSERT INTO ticket (ticket_id, booking_id, is_valid) VALUES
|
||||
(10, 1, 1), (20, 1, 1),
|
||||
(30, 2, 1), (40, 2, 1);
|
||||
connection con1;
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
BEGIN;
|
||||
UPDATE ticket SET is_valid=is_valid WHERE ticket_id=20;
|
||||
connection con2;
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
||||
SET SESSION innodb_lock_wait_timeout=30;
|
||||
UPDATE ticket SET is_valid=0 WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id=1);
|
||||
connection default;
|
||||
SET SESSION innodb_lock_wait_timeout=1;
|
||||
# FK entry (1,15) lands inside con2's S next-key locked range:
|
||||
INSERT INTO ticket (ticket_id, booking_id) VALUES (15, 1);
|
||||
ERROR HY000: Lock wait timeout exceeded; try restarting transaction
|
||||
SET SESSION innodb_lock_wait_timeout=default;
|
||||
connection con1;
|
||||
COMMIT;
|
||||
connection con2;
|
||||
connection default;
|
||||
SELECT ticket_id, booking_id, is_valid FROM ticket ORDER BY ticket_id;
|
||||
ticket_id booking_id is_valid
|
||||
10 1 0
|
||||
20 1 0
|
||||
30 2 1
|
||||
40 2 1
|
||||
#
|
||||
# Cleanup
|
||||
#
|
||||
DROP TABLE t_write, t_read;
|
||||
DROP TABLE ticket;
|
||||
DROP TABLE booking;
|
||||
disconnect con1;
|
||||
disconnect con2;
|
||||
SET DEBUG_SYNC='RESET';
|
||||
@@ -4,6 +4,14 @@
|
||||
|
||||
--source include/innodb_stable_estimates.inc
|
||||
|
||||
# This test verifies that a deadlock error during join evaluation of a
|
||||
# multi-table UPDATE is properly propagated (originally the error was
|
||||
# ignored, tripping an assertion on trx->state). Both UPDATEs below wait
|
||||
# on a table they only read, so the deadlock exists only if those reads
|
||||
# take S locks. At REPEATABLE READ they are consistent (non-locking)
|
||||
# reads, hence the two transactions involved in the deadlock run at
|
||||
# SERIALIZABLE, where the read-only tables keep their S locks.
|
||||
|
||||
--disable_query_log
|
||||
call mtr.add_suppression("InnoDB: Transaction was aborted due to ");
|
||||
--enable_query_log
|
||||
@@ -38,6 +46,7 @@ SET DEBUG_SYNC='now SIGNAL con1_dml';
|
||||
|
||||
--connect (con1,localhost,root,,test)
|
||||
SET DEBUG_SYNC='now WAIT_FOR con1_dml';
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
||||
begin;
|
||||
SELECT * FROM t1 for update; # Holds x lock of all records in the table t1
|
||||
SET DEBUG_SYNC='now SIGNAL default_dml';
|
||||
@@ -45,6 +54,7 @@ SET DEBUG_SYNC='now SIGNAL con2_dml';
|
||||
|
||||
--connection default
|
||||
SET DEBUG_SYNC='now WAIT_FOR default_dml';
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
||||
--send UPDATE t3 AS alias1 LEFT JOIN t3 AS alias2 ON ( alias1.f1 <> alias1.f2 ) SET alias1.f3 = 59 WHERE ( EXISTS ( SELECT t1.f3 FROM t1 WHERE t1.f1 = alias1.f1 ) ) OR alias2.f1 = 'h'
|
||||
# It holds the lock of all record in t3 and tries to acquire record lock for the table t1.
|
||||
|
||||
|
||||
85
mysql-test/suite/innodb/t/update_subquery_fk_gap_lock.test
Normal file
85
mysql-test/suite/innodb/t/update_subquery_fk_gap_lock.test
Normal file
@@ -0,0 +1,85 @@
|
||||
--source include/have_innodb.inc
|
||||
|
||||
--echo #
|
||||
--echo # MDEV-36832: UPDATE...IN(SELECT) acquires spurious gap locks on FK index
|
||||
--echo #
|
||||
|
||||
CREATE TABLE booking (
|
||||
booking_id INT PRIMARY KEY
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE ticket (
|
||||
ticket_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
booking_id INT NOT NULL,
|
||||
is_valid INT DEFAULT 1,
|
||||
KEY fk_ticket_2_booking (booking_id),
|
||||
FOREIGN KEY (booking_id) REFERENCES booking(booking_id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
INSERT INTO booking VALUES (1), (2);
|
||||
INSERT INTO ticket (booking_id) VALUES (1), (1), (2), (2);
|
||||
|
||||
--connect (con1,localhost,root)
|
||||
SET SESSION innodb_lock_wait_timeout=1;
|
||||
|
||||
--connect (con2,localhost,root)
|
||||
SET SESSION innodb_lock_wait_timeout=1;
|
||||
|
||||
--echo #
|
||||
--echo # Con1: UPDATE tickets for booking_id=1 via subquery
|
||||
--echo #
|
||||
--connection con1
|
||||
BEGIN;
|
||||
UPDATE ticket SET is_valid=0
|
||||
WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id=1);
|
||||
|
||||
--echo #
|
||||
--echo # Con2: UPDATE tickets for booking_id=2 via subquery
|
||||
--echo #
|
||||
--connection con2
|
||||
BEGIN;
|
||||
UPDATE ticket SET is_valid=0
|
||||
WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id=2);
|
||||
|
||||
--echo #
|
||||
--echo # Con1: INSERT into booking_id=1 (should not be blocked by Con2)
|
||||
--echo #
|
||||
--connection con1
|
||||
--send INSERT INTO ticket (booking_id) VALUES (1)
|
||||
|
||||
--echo #
|
||||
--echo # Con2: INSERT into booking_id=2 (should succeed immediately)
|
||||
--echo #
|
||||
--connection con2
|
||||
INSERT INTO ticket (booking_id) VALUES (2);
|
||||
|
||||
--echo #
|
||||
--echo # Con1: reap INSERT — must succeed without lock wait timeout
|
||||
--echo #
|
||||
--connection con1
|
||||
--reap
|
||||
|
||||
--echo #
|
||||
--echo # Commit both transactions
|
||||
--echo #
|
||||
--connection con1
|
||||
COMMIT;
|
||||
|
||||
--connection con2
|
||||
COMMIT;
|
||||
|
||||
--echo #
|
||||
--echo # Verify: each booking has 2 updated + 1 new ticket
|
||||
--echo #
|
||||
--connection default
|
||||
SELECT booking_id, is_valid, COUNT(*) c FROM ticket
|
||||
GROUP BY booking_id, is_valid ORDER BY booking_id, is_valid;
|
||||
|
||||
--echo #
|
||||
--echo # Cleanup
|
||||
--echo #
|
||||
DROP TABLE ticket;
|
||||
DROP TABLE booking;
|
||||
|
||||
--disconnect con1
|
||||
--disconnect con2
|
||||
@@ -0,0 +1,351 @@
|
||||
--source include/have_innodb.inc
|
||||
--source include/have_debug_sync.inc
|
||||
# The MDEV-36832 fix only takes effect when the read-side table handle
|
||||
# gets TL_READ (not TL_READ_NO_INSERT). read_lock_type_for_table()
|
||||
# returns TL_READ only when binlog is OFF or ROW format. With
|
||||
# STATEMENT or MIXED, TL_READ_NO_INSERT is used and the fix's
|
||||
# all-isolation-level LOCK_NONE branch does not apply (S locks are
|
||||
# required for statement-based replication serialization).
|
||||
--source include/have_binlog_format_row.inc
|
||||
|
||||
--echo #
|
||||
--echo # MDEV-36832: Negative isolation tests for UPDATE...IN(SELECT)
|
||||
--echo # consistent read (LOCK_NONE) at REPEATABLE READ
|
||||
--echo #
|
||||
--echo # These tests verify that replacing S locks with consistent read
|
||||
--echo # (LOCK_NONE) on the semi-join read table does NOT cause ACID
|
||||
--echo # isolation violations at REPEATABLE READ.
|
||||
--echo #
|
||||
|
||||
CREATE TABLE booking (
|
||||
booking_id INT PRIMARY KEY
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE ticket (
|
||||
ticket_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
booking_id INT NOT NULL,
|
||||
is_valid INT DEFAULT 1,
|
||||
KEY fk_ticket_2_booking (booking_id),
|
||||
FOREIGN KEY (booking_id) REFERENCES booking(booking_id)
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
INSERT INTO booking VALUES (1), (2);
|
||||
INSERT INTO ticket (booking_id, is_valid) VALUES
|
||||
(1, 1), (1, 1),
|
||||
(2, 1), (2, 1);
|
||||
|
||||
--connect (con1,localhost,root)
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
SET SESSION innodb_lock_wait_timeout=3;
|
||||
|
||||
--connect (con2,localhost,root)
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
SET SESSION innodb_lock_wait_timeout=3;
|
||||
|
||||
--echo #
|
||||
--echo # Test 1: Phantom insert — concurrent INSERT into subquery range
|
||||
--echo #
|
||||
--echo # T1 updates tickets for booking_id=1 via semi-join.
|
||||
--echo # T2 concurrently inserts a new ticket with booking_id=1.
|
||||
--echo # Without S gap locks (LOCK_NONE), T2 is NOT blocked.
|
||||
--echo # T1's MVCC snapshot does not see the new row, so T1 updates
|
||||
--echo # exactly the 2 original tickets. The new ticket remains is_valid=1.
|
||||
--echo # This is correct under snapshot isolation (RR).
|
||||
--echo #
|
||||
|
||||
--connection con1
|
||||
BEGIN;
|
||||
UPDATE ticket SET is_valid=0
|
||||
WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id=1);
|
||||
|
||||
--connection con2
|
||||
BEGIN;
|
||||
# With the fix (ROW binlog), this INSERT must NOT block.
|
||||
# The read-side FK index scan uses LOCK_NONE, so no S gap locks exist.
|
||||
INSERT INTO ticket (booking_id, is_valid) VALUES (1, 1);
|
||||
COMMIT;
|
||||
|
||||
--connection con1
|
||||
COMMIT;
|
||||
|
||||
--connection default
|
||||
--echo # Expect: ticket_ids 1,2 updated to is_valid=0; new ticket is_valid=1
|
||||
SELECT ticket_id, booking_id, is_valid FROM ticket
|
||||
WHERE booking_id=1 ORDER BY ticket_id;
|
||||
|
||||
DELETE FROM ticket WHERE ticket_id > 4;
|
||||
UPDATE ticket SET is_valid=1;
|
||||
|
||||
--echo #
|
||||
--echo # Test 2: Concurrent UPDATE on subquery filter column
|
||||
--echo #
|
||||
--echo # T1 updates tickets for booking_id=1. T2 changes ticket_id=2's
|
||||
--echo # booking_id from 1 to 2. Both need X lock on ticket_id=2's PK
|
||||
--echo # record, so they serialize regardless of FK index locks.
|
||||
--echo # Proves: PK X locks alone ensure write-write serialization.
|
||||
--echo #
|
||||
|
||||
--connection con1
|
||||
BEGIN;
|
||||
UPDATE ticket SET is_valid=0
|
||||
WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id=1);
|
||||
|
||||
--connection con2
|
||||
BEGIN;
|
||||
# T2 needs X lock on ticket_id=2 PK → blocks on T1's X lock.
|
||||
--send UPDATE ticket SET booking_id=2 WHERE ticket_id=2
|
||||
|
||||
# T1 commits, releasing X locks → T2 proceeds.
|
||||
--connection con1
|
||||
COMMIT;
|
||||
|
||||
--connection con2
|
||||
--reap
|
||||
COMMIT;
|
||||
|
||||
--connection default
|
||||
--echo # Both committed. Serialized via PK X lock.
|
||||
--echo # ticket_id=1: is_valid=0 (T1), booking_id=1 (unchanged)
|
||||
--echo # ticket_id=2: is_valid=0 (T1 ran first), booking_id=2 (T2 ran second)
|
||||
SELECT ticket_id, booking_id, is_valid FROM ticket ORDER BY ticket_id;
|
||||
|
||||
--echo #
|
||||
--echo # Test 3: Concurrent DELETE of a subquery-matched row
|
||||
--echo #
|
||||
--echo # T1 updates tickets for booking_id=1. T2 deletes ticket_id=1.
|
||||
--echo # Both need PK X lock → serialized.
|
||||
--echo #
|
||||
|
||||
--connection default
|
||||
UPDATE ticket SET booking_id=1 WHERE ticket_id <= 2;
|
||||
UPDATE ticket SET is_valid=1;
|
||||
|
||||
--connection con1
|
||||
BEGIN;
|
||||
UPDATE ticket SET is_valid=0
|
||||
WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id=1);
|
||||
|
||||
--connection con2
|
||||
BEGIN;
|
||||
# T2 needs X lock on ticket_id=1 PK → blocks on T1's X lock.
|
||||
--send DELETE FROM ticket WHERE ticket_id=1
|
||||
|
||||
--connection con1
|
||||
COMMIT;
|
||||
|
||||
--connection con2
|
||||
--reap
|
||||
COMMIT;
|
||||
|
||||
--connection default
|
||||
--echo # ticket_id=1 deleted by T2 (ran second), ticket_id=2 updated by T1.
|
||||
SELECT ticket_id, booking_id, is_valid FROM ticket ORDER BY ticket_id;
|
||||
|
||||
INSERT INTO ticket (ticket_id, booking_id, is_valid) VALUES (1, 1, 1);
|
||||
UPDATE ticket SET is_valid=1;
|
||||
|
||||
--echo #
|
||||
--echo # Test 4: SERIALIZABLE must still acquire S locks
|
||||
--echo #
|
||||
--echo # At SERIALIZABLE, store_lock() keeps LOCK_S for the read-side
|
||||
--echo # table, so the subquery's read MUST block concurrent writes.
|
||||
--echo # T1 does UPDATE...IN(SELECT) at SERIALIZABLE.
|
||||
--echo # T2 tries to INSERT into same booking_id range — must block.
|
||||
--echo #
|
||||
|
||||
--connection con1
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
||||
|
||||
--connection con2
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
||||
|
||||
--connection con1
|
||||
BEGIN;
|
||||
UPDATE ticket SET is_valid=0
|
||||
WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id=1);
|
||||
|
||||
--connection con2
|
||||
BEGIN;
|
||||
# At SERIALIZABLE, T1 holds S locks on the FK index.
|
||||
# T2's INSERT needs an insert-intention lock that conflicts.
|
||||
--error ER_LOCK_WAIT_TIMEOUT
|
||||
INSERT INTO ticket (booking_id, is_valid) VALUES (1, 1);
|
||||
COMMIT;
|
||||
|
||||
--connection con1
|
||||
COMMIT;
|
||||
|
||||
--connection default
|
||||
UPDATE ticket SET is_valid=1;
|
||||
|
||||
--echo #
|
||||
--echo # Test 5: Multi-table UPDATE on different tables — read table
|
||||
--echo # uses consistent read (LOCK_NONE), no S locks
|
||||
--echo #
|
||||
--echo # T1: UPDATE t_write JOIN t_read SET t_write.val = t_read.val
|
||||
--echo # WHERE t_read.status = 'active'
|
||||
--echo # T2: UPDATE t_read SET status = 'cancelled' WHERE id = 2
|
||||
--echo #
|
||||
--echo # With LOCK_NONE on t_read, T2 must NOT block.
|
||||
--echo # T1's MVCC snapshot sees 'active' for id=2 and uses val=20.
|
||||
--echo # This is correct under snapshot isolation: equivalent to T1
|
||||
--echo # running before T2 in a serial schedule.
|
||||
--echo #
|
||||
|
||||
CREATE TABLE t_read (
|
||||
id INT PRIMARY KEY,
|
||||
status VARCHAR(20) NOT NULL,
|
||||
val INT NOT NULL
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
CREATE TABLE t_write (
|
||||
id INT PRIMARY KEY,
|
||||
val INT NOT NULL DEFAULT 0
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
INSERT INTO t_read VALUES (1, 'active', 10), (2, 'active', 20);
|
||||
INSERT INTO t_write VALUES (1, 0), (2, 0);
|
||||
|
||||
--connection con1
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
BEGIN;
|
||||
SET DEBUG_SYNC='execute_command_after_close_tables SIGNAL t1_updated WAIT_FOR t2_done';
|
||||
--send UPDATE t_write w JOIN t_read r ON w.id = r.id SET w.val = r.val WHERE r.status = 'active'
|
||||
|
||||
--connection con2
|
||||
SET DEBUG_SYNC='now WAIT_FOR t1_updated';
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
BEGIN;
|
||||
# Must NOT block — T1 holds no S locks on t_read.
|
||||
UPDATE t_read SET status='cancelled' WHERE id=2;
|
||||
COMMIT;
|
||||
SET DEBUG_SYNC='now SIGNAL t2_done';
|
||||
|
||||
--connection con1
|
||||
--reap
|
||||
COMMIT;
|
||||
|
||||
--connection default
|
||||
--echo # t_write.val reflects T1's snapshot: val=10 (id=1), val=20 (id=2).
|
||||
--echo # t_read.id=2 is now 'cancelled' (by T2) but T1's snapshot saw
|
||||
--echo # 'active'. Correct under snapshot isolation.
|
||||
SELECT w.id, w.val, r.status AS current_read_status
|
||||
FROM t_write w JOIN t_read r ON w.id = r.id ORDER BY w.id;
|
||||
|
||||
--echo #
|
||||
--echo # Test 6: MVCC snapshot consistency — re-read within same
|
||||
--echo # transaction after UPDATE must see consistent data
|
||||
--echo #
|
||||
--echo # T1 establishes snapshot, T2 modifies t_read, T1 does
|
||||
--echo # multi-table UPDATE using snapshot, then T1 re-reads t_read.
|
||||
--echo # The re-read must see the same data as the UPDATE used
|
||||
--echo # (MVCC snapshot stability at RR).
|
||||
--echo #
|
||||
|
||||
--connection default
|
||||
UPDATE t_read SET status='active';
|
||||
UPDATE t_write SET val=0;
|
||||
|
||||
--connection con1
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
BEGIN;
|
||||
# Establish MVCC snapshot
|
||||
SELECT * FROM t_read WHERE status='active';
|
||||
|
||||
--connection con2
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
BEGIN;
|
||||
# T2 modifies t_read AFTER T1's snapshot is established
|
||||
UPDATE t_read SET status='cancelled', val=99 WHERE id=2;
|
||||
COMMIT;
|
||||
|
||||
--connection con1
|
||||
# T1's multi-table UPDATE uses snapshot — still sees id=2 as active/val=20
|
||||
UPDATE t_write w JOIN t_read r ON w.id = r.id
|
||||
SET w.val = r.val WHERE r.status = 'active';
|
||||
|
||||
# T1's re-read must still see 'active' for both rows (MVCC snapshot)
|
||||
SELECT id, status, val FROM t_read WHERE status='active' ORDER BY id;
|
||||
|
||||
COMMIT;
|
||||
|
||||
--connection default
|
||||
--echo # t_write must have snapshot values: val=10 and val=20 (NOT val=99).
|
||||
--echo # T2's change is invisible to T1's snapshot.
|
||||
SELECT id, val FROM t_write ORDER BY id;
|
||||
|
||||
--echo #
|
||||
--echo # Test 7: SERIALIZABLE with autocommit must also acquire S locks
|
||||
--echo #
|
||||
--echo # The external_lock() upgrade of LOCK_NONE to LOCK_S at
|
||||
--echo # SERIALIZABLE applies only to non-autocommit transactions, so
|
||||
--echo # store_lock() must not hand out LOCK_NONE at SERIALIZABLE.
|
||||
--echo # T1 (autocommit=1, SERIALIZABLE) runs UPDATE...IN(SELECT) and is
|
||||
--echo # stalled on the PK X lock of the last matching row, held by T2.
|
||||
--echo # By that point T1 has scanned the FK index entries (1,10),(1,20)
|
||||
--echo # and must hold S next-key locks on them. T3 then inserts a ticket
|
||||
--echo # whose FK entry (1,15) falls inside that locked range - it must
|
||||
--echo # block.
|
||||
--echo #
|
||||
|
||||
--connection default
|
||||
# Recreate the table: DELETE would leave delete-marked records, which
|
||||
# make the UPDATE's clustered index scan take next-key (gap) X locks.
|
||||
# The probe INSERT below would then queue behind the UPDATE's waiting
|
||||
# next-key lock on the PRIMARY index, masking what this test measures
|
||||
# (S locks on the FK index).
|
||||
DROP TABLE ticket;
|
||||
CREATE TABLE ticket (
|
||||
ticket_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
booking_id INT NOT NULL,
|
||||
is_valid INT DEFAULT 1,
|
||||
KEY fk_ticket_2_booking (booking_id),
|
||||
FOREIGN KEY (booking_id) REFERENCES booking(booking_id)
|
||||
) ENGINE=InnoDB;
|
||||
INSERT INTO ticket (ticket_id, booking_id, is_valid) VALUES
|
||||
(10, 1, 1), (20, 1, 1),
|
||||
(30, 2, 1), (40, 2, 1);
|
||||
|
||||
--connection con1
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
|
||||
BEGIN;
|
||||
# Hold a PK X lock on the last row matching booking_id=1
|
||||
UPDATE ticket SET is_valid=is_valid WHERE ticket_id=20;
|
||||
|
||||
--connection con2
|
||||
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
||||
SET SESSION innodb_lock_wait_timeout=30;
|
||||
# autocommit=1: no BEGIN. The UPDATE stalls on con1's PK X lock after
|
||||
# having scanned the FK index entries for booking_id=1.
|
||||
--send UPDATE ticket SET is_valid=0 WHERE ticket_id IN (SELECT ticket_id FROM ticket WHERE booking_id=1)
|
||||
|
||||
--connection default
|
||||
let $wait_condition=
|
||||
SELECT COUNT(*)=1 FROM information_schema.innodb_trx
|
||||
WHERE trx_state='LOCK WAIT';
|
||||
--source include/wait_condition.inc
|
||||
SET SESSION innodb_lock_wait_timeout=1;
|
||||
--echo # FK entry (1,15) lands inside con2's S next-key locked range:
|
||||
--error ER_LOCK_WAIT_TIMEOUT
|
||||
INSERT INTO ticket (ticket_id, booking_id) VALUES (15, 1);
|
||||
SET SESSION innodb_lock_wait_timeout=default;
|
||||
|
||||
--connection con1
|
||||
COMMIT;
|
||||
|
||||
--connection con2
|
||||
--reap
|
||||
|
||||
--connection default
|
||||
SELECT ticket_id, booking_id, is_valid FROM ticket ORDER BY ticket_id;
|
||||
|
||||
--echo #
|
||||
--echo # Cleanup
|
||||
--echo #
|
||||
DROP TABLE t_write, t_read;
|
||||
DROP TABLE ticket;
|
||||
DROP TABLE booking;
|
||||
--disconnect con1
|
||||
--disconnect con2
|
||||
|
||||
SET DEBUG_SYNC='RESET';
|
||||
@@ -16872,31 +16872,75 @@ ha_innobase::store_lock(
|
||||
unexpected if an obsolete consistent read view would be
|
||||
used. */
|
||||
|
||||
/* Use consistent read for checksum table */
|
||||
/* Use consistent read for checksum table
|
||||
and for read-only table accesses in multi-table
|
||||
UPDATE.
|
||||
|
||||
Multi-table UPDATE modifies some tables and only
|
||||
reads others. The read-only tables do not need
|
||||
S locks: the MVCC snapshot provides read
|
||||
consistency, and rows being modified still get
|
||||
X locks via the write-side table handle.
|
||||
|
||||
For TL_READ we use consistent read (LOCK_NONE) at
|
||||
all isolation levels below SERIALIZABLE. This is
|
||||
safe because:
|
||||
- The MVCC snapshot is stable within a transaction
|
||||
in REPEATABLE READ.
|
||||
- Rows being modified still get X locks via the
|
||||
write-side table handle (F_WRLCK / LOCK_X).
|
||||
SERIALIZABLE must be excluded here: the
|
||||
::external_lock() upgrade of LOCK_NONE to LOCK_S
|
||||
applies only to non-autocommit transactions, so an
|
||||
autocommit multi-table UPDATE would otherwise do a
|
||||
consistent read at SERIALIZABLE.
|
||||
|
||||
We limit this to SQLCOM_UPDATE_MULTI (not
|
||||
SQLCOM_UPDATE) because single-table UPDATE with
|
||||
scalar subqueries on other tables traditionally
|
||||
uses S locks to block concurrent modifications to
|
||||
those tables, and changing that would alter
|
||||
observable behavior for existing applications.
|
||||
|
||||
For TL_READ_NO_INSERT we keep the pre-existing
|
||||
behavior: consistent read only at READ COMMITTED
|
||||
and below, S locks at REPEATABLE READ and above,
|
||||
because statement-based replication needs locking
|
||||
reads for serializable execution ordering. */
|
||||
|
||||
if (sql_command == SQLCOM_CHECKSUM
|
||||
|| sql_command == SQLCOM_CREATE_SEQUENCE
|
||||
|| (sql_command == SQLCOM_ANALYZE && lock_type == TL_READ)
|
||||
|| (lock_type == TL_READ
|
||||
&& trx->isolation_level != TRX_ISO_SERIALIZABLE
|
||||
&& sql_command == SQLCOM_UPDATE_MULTI)
|
||||
|| (trx->isolation_level <= TRX_ISO_READ_COMMITTED
|
||||
&& (lock_type == TL_READ
|
||||
|| lock_type == TL_READ_NO_INSERT)
|
||||
&& (sql_command == SQLCOM_INSERT_SELECT
|
||||
|| sql_command == SQLCOM_REPLACE_SELECT
|
||||
|| sql_command == SQLCOM_UPDATE
|
||||
|| sql_command == SQLCOM_UPDATE_MULTI
|
||||
|| sql_command == SQLCOM_CREATE_SEQUENCE
|
||||
|| sql_command == SQLCOM_CREATE_TABLE))) {
|
||||
|
||||
/* If the transaction isolation level is
|
||||
READ UNCOMMITTED or READ COMMITTED and we are executing
|
||||
INSERT INTO...SELECT or REPLACE INTO...SELECT
|
||||
or UPDATE ... = (SELECT ...) or CREATE ...
|
||||
SELECT... without FOR UPDATE or IN SHARE
|
||||
MODE in select, then we use consistent read
|
||||
for select. */
|
||||
DBUG_PRINT("ib_lock",
|
||||
("consistent read for DML: "
|
||||
"lock_type=%d sql_command=%d "
|
||||
"isolation_level=%u",
|
||||
lock_type, sql_command,
|
||||
trx->isolation_level));
|
||||
|
||||
m_prebuilt->select_lock_type = LOCK_NONE;
|
||||
m_prebuilt->stored_select_lock_type = LOCK_NONE;
|
||||
} else {
|
||||
DBUG_PRINT("ib_lock",
|
||||
("LOCK_S for DML read: "
|
||||
"lock_type=%d sql_command=%d "
|
||||
"isolation_level=%u",
|
||||
lock_type, sql_command,
|
||||
trx->isolation_level));
|
||||
|
||||
m_prebuilt->select_lock_type = LOCK_S;
|
||||
m_prebuilt->stored_select_lock_type = LOCK_S;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user