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
1 NULL
COMMIT;
disconnect con_weird;
connection consistent;
SELECT * FROM t FORCE INDEX (b) FOR UPDATE;
a b
@@ -230,9 +229,58 @@ UPDATE t SET b=4 WHERE a=1;
connection consistent;
SELECT * FROM t WHERE a=1 FOR UPDATE;
ERROR HY000: Record has changed since last read in table 't'
disconnect consistent;
disconnect disable_purging;
connection default;
SET DEBUG_SYNC="RESET";
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

View File

@@ -174,7 +174,6 @@ ROLLBACK;
--reap
SELECT * FROM t FORCE INDEX (b) FOR UPDATE;
COMMIT;
--disconnect con_weird
--connection consistent
SELECT * FROM t FORCE INDEX (b) FOR UPDATE;
@@ -246,12 +245,55 @@ UPDATE t SET b=4 WHERE a=1;
--connection consistent
--error ER_CHECKREAD
SELECT * FROM t WHERE a=1 FOR UPDATE;
--disconnect consistent
--disconnect disable_purging
--connection default
SET DEBUG_SYNC="RESET";
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
--echo # End of 10.6 tests

View File

@@ -116,8 +116,6 @@ simple_thread_local ha_handler_stats *mariadb_stats;
#include <limits>
#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);
unsigned long long thd_get_query_id(const MYSQL_THD thd);
void thd_clear_error(MYSQL_THD thd);
@@ -821,14 +819,16 @@ innodb_tmpdir_validate(
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 */
/** @return the current transaction isolation level */
static inline uint innodb_isolation_level(const THD *thd) noexcept
{
static_assert(ISO_REPEATABLE_READ == TRX_ISO_REPEATABLE_READ, "");
static_assert(ISO_SERIALIZABLE == TRX_ISO_SERIALIZABLE, "");
static_assert(ISO_READ_COMMITTED == TRX_ISO_READ_COMMITTED, "");
static_assert(ISO_READ_UNCOMMITTED == TRX_ISO_READ_UNCOMMITTED, "");
return high_level_read_only
? ISO_READ_UNCOMMITTED : (thd_tx_isolation(thd) & 3);
}
/** Gets field offset for a field in a table.
@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);
/* Assign a read view if the transaction does not have it yet.
Do this only if transaction is using REPEATABLE READ isolation
level. */
trx->isolation_level = innobase_map_isolation_level(
thd_get_trx_isolation(thd)) & 3;
/* Assign a read view if the transaction does not have one yet.
Skip this for the READ UNCOMMITTED isolation level. */
trx->isolation_level = innodb_isolation_level(thd) & 3;
if (trx->isolation_level == TRX_ISO_REPEATABLE_READ) {
if (trx->isolation_level != TRX_ISO_READ_UNCOMMITTED) {
trx->read_view.open(trx);
} else {
push_warning_printf(thd, Sql_condition::WARN_LEVEL_WARN,
HA_ERR_UNSUPPORTED,
"InnoDB: WITH CONSISTENT SNAPSHOT"
" was ignored because this phrase"
" can only be used with"
" REPEATABLE READ isolation level.");
" is ignored at READ UNCOMMITTED"
" isolation level.");
}
/* Set the MySQL flag to mark that there is an active transaction */
@@ -16031,31 +16028,6 @@ ha_innobase::start_stmt(
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
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
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. */
if (lock_type != TL_IGNORE
&& trx->n_mysql_tables_in_use == 0) {
trx->isolation_level = innobase_map_isolation_level(
(enum_tx_isolation) thd_tx_isolation(thd)) & 3;
if (trx->isolation_level <= TRX_ISO_READ_COMMITTED) {
switch ((trx->isolation_level
= innodb_isolation_level(thd) & 3)) {
case ISO_REPEATABLE_READ:
break;
case ISO_READ_COMMITTED:
case ISO_READ_UNCOMMITTED:
/* At low transaction isolation levels we let
each consistent read set its own snapshot */
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.reject
@@ -5,12 +5,15 @@
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;
@@ -11,6 +11,7 @@
# If consistent read works on this isolation level (SERIALIZABLE), the following SELECT should not return the value we inserted (1)
SELECT a FROM t1;
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;
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.
+Warning 138 InnoDB: WITH CONSISTENT SNAPSHOT is ignored at READ UNCOMMITTED isolation level.
connection con2;
INSERT INTO t1 (a) VALUES (1);
connection con1;