1
0
mirror of https://github.com/MariaDB/server.git synced 2025-08-08 11:22:35 +03:00

MDEV-36330: SERIALIZABLE read inconsistency

At TRANSACTION ISOLATION LEVEL SERIALIZABLE, InnoDB would fail to flag
a write/read conflict, which would be a violation already at the more
relaxed REPEATABLE READ level when innodb_snapshot_isolation=ON.

Fix: Create a read view and start the transaction at the same time.
Thus, lock checks will be able to consult the correct read view
to flag ER_CHECKREAD if we are about to lock a record that was committed
after the start of our transaction.

innobase_start_trx_and_assign_read_view(): At any other isolation level
than READ UNCOMMITTED, do create a read view. This is needed for the
correct operation of START TRANSACTION WITH CONSISTENT SNAPSHOT.

ha_innobase::store_lock(): At SERIALIZABLE isolation level, if the
transaction was not started yet, start it and open a read view.
An alternative way to achieve this would be to make trans_begin()
treat START TRANSACTION (or BEGIN) in the same way as
START TRANSACTION WITH CONSISTENT SNAPSHOT when the isolation level
is SERIALIZABLE.

innodb_isolation_level(const THD*): A simpler version of
innobase_map_isolation_level(). Compared to earlier, we will return
READ UNCOMMITTED also if the :newraw option is set for the
InnoDB system tablespace.

Reviewed by: Vladislav Lesin
This commit is contained in:
Marko Mäkelä
2025-07-11 16:07:08 +03:00
parent f73ffd1150
commit 7fbbbc983f
6 changed files with 128 additions and 75 deletions

View File

@@ -166,7 +166,6 @@ SELECT * FROM t FORCE INDEX (b) FOR UPDATE;
a b a b
1 NULL 1 NULL
COMMIT; COMMIT;
disconnect con_weird;
connection consistent; connection consistent;
SELECT * FROM t FORCE INDEX (b) FOR UPDATE; SELECT * FROM t FORCE INDEX (b) FOR UPDATE;
a b a b
@@ -230,9 +229,58 @@ UPDATE t SET b=4 WHERE a=1;
connection consistent; connection consistent;
SELECT * FROM t WHERE a=1 FOR UPDATE; SELECT * FROM t WHERE a=1 FOR UPDATE;
ERROR HY000: Record has changed since last read in table 't' ERROR HY000: Record has changed since last read in table 't'
disconnect consistent;
disconnect disable_purging; disconnect disable_purging;
connection default; connection default;
SET DEBUG_SYNC="RESET"; SET DEBUG_SYNC="RESET";
DROP TABLE t; DROP TABLE t;
CREATE TABLE t1(a INT) ENGINE=InnoDB STATS_PERSISTENT=0;
CREATE TABLE t2(a INT) ENGINE=InnoDB STATS_PERSISTENT=0;
BEGIN;
INSERT INTO t1 SET a=1;
connection con_weird;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
INSERT INTO t2 SET a=1;
connection consistent;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
INSERT INTO t2 SET a=2;
connection default;
COMMIT;
connection con_weird;
SELECT * FROM t1;
a
1
COMMIT;
connection consistent;
SELECT * FROM t1;
ERROR HY000: Record has changed since last read in table 't1'
COMMIT;
connection default;
BEGIN;
INSERT INTO t1 SET a=2;
connection con_weird;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION WITH CONSISTENT SNAPSHOT;
INSERT INTO t2 SET a=3;
connection consistent;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION WITH CONSISTENT SNAPSHOT;
INSERT INTO t2 SET a=2;
connection default;
COMMIT;
connection con_weird;
SELECT * FROM t1;
a
1
2
COMMIT;
disconnect con_weird;
connection consistent;
SELECT * FROM t1;
ERROR HY000: Record has changed since last read in table 't1'
COMMIT;
disconnect consistent;
connection default;
DROP TABLE t1,t2;
# End of 10.6 tests # End of 10.6 tests

View File

@@ -174,7 +174,6 @@ ROLLBACK;
--reap --reap
SELECT * FROM t FORCE INDEX (b) FOR UPDATE; SELECT * FROM t FORCE INDEX (b) FOR UPDATE;
COMMIT; COMMIT;
--disconnect con_weird
--connection consistent --connection consistent
SELECT * FROM t FORCE INDEX (b) FOR UPDATE; SELECT * FROM t FORCE INDEX (b) FOR UPDATE;
@@ -246,12 +245,55 @@ UPDATE t SET b=4 WHERE a=1;
--connection consistent --connection consistent
--error ER_CHECKREAD --error ER_CHECKREAD
SELECT * FROM t WHERE a=1 FOR UPDATE; SELECT * FROM t WHERE a=1 FOR UPDATE;
--disconnect consistent
--disconnect disable_purging --disconnect disable_purging
--connection default --connection default
SET DEBUG_SYNC="RESET"; SET DEBUG_SYNC="RESET";
DROP TABLE t; DROP TABLE t;
CREATE TABLE t1(a INT) ENGINE=InnoDB STATS_PERSISTENT=0;
CREATE TABLE t2(a INT) ENGINE=InnoDB STATS_PERSISTENT=0;
BEGIN; INSERT INTO t1 SET a=1;
--connection con_weird
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN; INSERT INTO t2 SET a=1;
--connection consistent
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN; INSERT INTO t2 SET a=2;
--connection default
COMMIT;
--connection con_weird
SELECT * FROM t1;
COMMIT;
--connection consistent
--disable_ps2_protocol
--error ER_CHECKREAD
SELECT * FROM t1;
--enable_ps2_protocol
COMMIT;
--connection default
BEGIN; INSERT INTO t1 SET a=2;
--connection con_weird
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION WITH CONSISTENT SNAPSHOT; INSERT INTO t2 SET a=3;
--connection consistent
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION WITH CONSISTENT SNAPSHOT; INSERT INTO t2 SET a=2;
--connection default
COMMIT;
--connection con_weird
SELECT * FROM t1;
COMMIT;
--disconnect con_weird
--connection consistent
--disable_ps2_protocol
--error ER_CHECKREAD
SELECT * FROM t1;
--enable_ps2_protocol
COMMIT;
--disconnect consistent
--connection default
DROP TABLE t1,t2;
--source include/wait_until_count_sessions.inc --source include/wait_until_count_sessions.inc
--echo # End of 10.6 tests --echo # End of 10.6 tests

View File

@@ -116,8 +116,6 @@ simple_thread_local ha_handler_stats *mariadb_stats;
#include <limits> #include <limits>
#include <myisamchk.h> // TT_FOR_UPGRADE #include <myisamchk.h> // TT_FOR_UPGRADE
#define thd_get_trx_isolation(X) ((enum_tx_isolation)thd_tx_isolation(X))
extern "C" void thd_mark_transaction_to_rollback(MYSQL_THD thd, bool all); extern "C" void thd_mark_transaction_to_rollback(MYSQL_THD thd, bool all);
unsigned long long thd_get_query_id(const MYSQL_THD thd); unsigned long long thd_get_query_id(const MYSQL_THD thd);
void thd_clear_error(MYSQL_THD thd); void thd_clear_error(MYSQL_THD thd);
@@ -821,14 +819,16 @@ innodb_tmpdir_validate(
return(0); return(0);
} }
/******************************************************************//** /** @return the current transaction isolation level */
Maps a MySQL trx isolation level code to the InnoDB isolation level code static inline uint innodb_isolation_level(const THD *thd) noexcept
@return InnoDB isolation level */ {
static inline static_assert(ISO_REPEATABLE_READ == TRX_ISO_REPEATABLE_READ, "");
uint static_assert(ISO_SERIALIZABLE == TRX_ISO_SERIALIZABLE, "");
innobase_map_isolation_level( static_assert(ISO_READ_COMMITTED == TRX_ISO_READ_COMMITTED, "");
/*=========================*/ static_assert(ISO_READ_UNCOMMITTED == TRX_ISO_READ_UNCOMMITTED, "");
enum_tx_isolation iso); /*!< in: MySQL isolation level code */ return high_level_read_only
? ISO_READ_UNCOMMITTED : (thd_tx_isolation(thd) & 3);
}
/** Gets field offset for a field in a table. /** Gets field offset for a field in a table.
@param[in] table MySQL table object @param[in] table MySQL table object
@@ -4470,21 +4470,18 @@ innobase_start_trx_and_assign_read_view(
trx_start_if_not_started_xa(trx, false); trx_start_if_not_started_xa(trx, false);
/* Assign a read view if the transaction does not have it yet. /* Assign a read view if the transaction does not have one yet.
Do this only if transaction is using REPEATABLE READ isolation Skip this for the READ UNCOMMITTED isolation level. */
level. */ trx->isolation_level = innodb_isolation_level(thd) & 3;
trx->isolation_level = innobase_map_isolation_level(
thd_get_trx_isolation(thd)) & 3;
if (trx->isolation_level == TRX_ISO_REPEATABLE_READ) { if (trx->isolation_level != TRX_ISO_READ_UNCOMMITTED) {
trx->read_view.open(trx); trx->read_view.open(trx);
} else { } else {
push_warning_printf(thd, Sql_condition::WARN_LEVEL_WARN, push_warning_printf(thd, Sql_condition::WARN_LEVEL_WARN,
HA_ERR_UNSUPPORTED, HA_ERR_UNSUPPORTED,
"InnoDB: WITH CONSISTENT SNAPSHOT" "InnoDB: WITH CONSISTENT SNAPSHOT"
" was ignored because this phrase" " is ignored at READ UNCOMMITTED"
" can only be used with" " isolation level.");
" REPEATABLE READ isolation level.");
} }
/* Set the MySQL flag to mark that there is an active transaction */ /* Set the MySQL flag to mark that there is an active transaction */
@@ -16031,31 +16028,6 @@ ha_innobase::start_stmt(
DBUG_RETURN(0); DBUG_RETURN(0);
} }
/******************************************************************//**
Maps a MySQL trx isolation level code to the InnoDB isolation level code
@return InnoDB isolation level */
static inline
uint
innobase_map_isolation_level(
/*=========================*/
enum_tx_isolation iso) /*!< in: MySQL isolation level code */
{
if (UNIV_UNLIKELY(srv_force_recovery >= SRV_FORCE_NO_UNDO_LOG_SCAN)
|| UNIV_UNLIKELY(srv_read_only_mode)) {
return TRX_ISO_READ_UNCOMMITTED;
}
switch (iso) {
case ISO_REPEATABLE_READ: return(TRX_ISO_REPEATABLE_READ);
case ISO_READ_COMMITTED: return(TRX_ISO_READ_COMMITTED);
case ISO_SERIALIZABLE: return(TRX_ISO_SERIALIZABLE);
case ISO_READ_UNCOMMITTED: return(TRX_ISO_READ_UNCOMMITTED);
}
ut_error;
return(0);
}
/******************************************************************//** /******************************************************************//**
As MySQL will execute an external lock for every new table it uses when it As MySQL will execute an external lock for every new table it uses when it
starts to process an SQL statement (an exception is when MySQL calls starts to process an SQL statement (an exception is when MySQL calls
@@ -16520,19 +16492,29 @@ ha_innobase::store_lock(
Be careful to ignore TL_IGNORE if we are going to do something with Be careful to ignore TL_IGNORE if we are going to do something with
only 'real' locks! */ only 'real' locks! */
/* If no MySQL table is in use, we need to set the isolation level /* If no table handle is open, we need to set the isolation level
of the transaction. */ of the transaction. */
if (lock_type != TL_IGNORE if (lock_type != TL_IGNORE
&& trx->n_mysql_tables_in_use == 0) { && trx->n_mysql_tables_in_use == 0) {
trx->isolation_level = innobase_map_isolation_level( switch ((trx->isolation_level
(enum_tx_isolation) thd_tx_isolation(thd)) & 3; = innodb_isolation_level(thd) & 3)) {
case ISO_REPEATABLE_READ:
if (trx->isolation_level <= TRX_ISO_READ_COMMITTED) { break;
case ISO_READ_COMMITTED:
case ISO_READ_UNCOMMITTED:
/* At low transaction isolation levels we let /* At low transaction isolation levels we let
each consistent read set its own snapshot */ each consistent read set its own snapshot */
trx->read_view.close(); trx->read_view.close();
break;
case ISO_SERIALIZABLE:
auto trx_state = trx->state;
if (trx_state == TRX_STATE_NOT_STARTED) {
trx_start_if_not_started(trx, false);
trx->read_view.open(trx);
} else {
ut_ad(trx_state == TRX_STATE_ACTIVE);
}
} }
} }

View File

@@ -1,14 +1,6 @@
--- suite/storage_engine/trx/cons_snapshot_serializable.result --- suite/storage_engine/trx/cons_snapshot_serializable.result
+++ suite/storage_engine/trx/cons_snapshot_serializable.reject +++ suite/storage_engine/trx/cons_snapshot_serializable.reject
@@ -5,12 +5,15 @@ @@ -11,6 +11,7 @@
CREATE TABLE t1 (a <INT_COLUMN>) ENGINE=<STORAGE_ENGINE> <CUSTOM_TABLE_OPTIONS>;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION WITH CONSISTENT SNAPSHOT;
+Warnings:
+Warning 138 InnoDB: WITH CONSISTENT SNAPSHOT was ignored because this phrase can only be used with REPEATABLE READ isolation level.
connection con2;
INSERT INTO t1 (a) VALUES (1);
connection con1;
# If consistent read works on this isolation level (SERIALIZABLE), the following SELECT should not return the value we inserted (1) # If consistent read works on this isolation level (SERIALIZABLE), the following SELECT should not return the value we inserted (1)
SELECT a FROM t1; SELECT a FROM t1;
a a

View File

@@ -1,11 +0,0 @@
--- suite/storage_engine/trx/level_read_committed.result
+++ suite/storage_engine/trx/level_read_committed.reject
@@ -77,6 +77,8 @@
CREATE TABLE t1 (a <INT_COLUMN>) ENGINE=<STORAGE_ENGINE> <CUSTOM_TABLE_OPTIONS>;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION WITH CONSISTENT SNAPSHOT;
+Warnings:
+Warning 138 InnoDB: WITH CONSISTENT SNAPSHOT was ignored because this phrase can only be used with REPEATABLE READ isolation level.
connection con2;
INSERT INTO t1 (a) VALUES (1);
connection con1;

View File

@@ -5,7 +5,7 @@
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION WITH CONSISTENT SNAPSHOT; START TRANSACTION WITH CONSISTENT SNAPSHOT;
+Warnings: +Warnings:
+Warning 138 InnoDB: WITH CONSISTENT SNAPSHOT was ignored because this phrase can only be used with REPEATABLE READ isolation level. +Warning 138 InnoDB: WITH CONSISTENT SNAPSHOT is ignored at READ UNCOMMITTED isolation level.
connection con2; connection con2;
INSERT INTO t1 (a) VALUES (1); INSERT INTO t1 (a) VALUES (1);
connection con1; connection con1;