为什么 MariaDB 上的 REPETEABLE_READ 不产生幻读?

cod*_*ent 5 mysql spring jdbc isolation-level mariadb

在我的测试中,我发现当使用MariaDB时,在 REPETEABLE_READ 隔离中执行相同的查询不会产生幻读,而它应该产生幻读

例如:

我的bank_account 表中有两行:

  ID |  OWNER | MONEY
------------------------
  1  |  John  | 1000
  2  |  Louis | 2000
Run Code Online (Sandbox Code Playgroud)

预期的流程应如下所示:

THREAD 1 (REPETEABLE_READ)                THREAD 2 (READ_UNCOMMITED)
  |                                         |
findAll()->[1|John|1000,2|Louis|2000]       |          
  |                                         |
  |                                       updateAccount(1, +100)
  |                                       createAccount("Charles", 3000)                 
  |                                       flush()
  |                                         |
  |                                         commitTx()
  |                                         |_
  |                                         
findAll()->[1|John|1000,2|Louis|2000,       
  |         3|Charles|3000]                 
  |                                         
  |                                         
 commitTx()                               
  |_                                        
Run Code Online (Sandbox Code Playgroud)

总而言之,在Thread2.createAccount("Charles", 3000);刷新之后,Thread1 将搜索所有行并得到

  ID |  OWNER   | MONEY
------------------------
  1  |  John    | 1000
  2  |  Louis   | 2000
  3  |  Charles | 3000
Run Code Online (Sandbox Code Playgroud)

Thread1 受到保护,[1, John, 1000]不会看到未提交的更改[1, John, 1100],但它应该看到新插入的行。

然而,Thread1 在第二个 findAll 中检索到的结果与第一个 findAll() 中的结果完全相同:

  ID |  OWNER   | MONEY
------------------------
  1  |  John    | 1000
  3  |  Charles | 3000
Run Code Online (Sandbox Code Playgroud)

它没有幻读。为什么?????

这是Thread1执行的代码:

@Transactional(readOnly=true, isolation=Isolation.REPEATABLE_READ)
@Override
public Iterable<BankAccount> findAllTwiceRepeteableRead(){
    printIsolationLevel();
    Iterable<BankAccount> accounts = baDao.findAll();
    logger.info("findAllTwiceRepeteableRead() 1 -> {}", accounts);
    //PAUSE HERE
    ...
}
Run Code Online (Sandbox Code Playgroud)

我在它所说的地方暂停执行//PAUSE HERE

然后Thread2执行:

bankAccountService.addMoneyReadUncommited(ba.getId(), 200);
bankAccountService.createAccount("Carlos", 3000);
Run Code Online (Sandbox Code Playgroud)

然后 Thread1 继续:

//PAUSE HERE
...
Iterable<BankAccount> accounts = baDao.findAll();
logger.info("findAllTwiceRepeteableRead() 2 -> {}", accounts);
Run Code Online (Sandbox Code Playgroud)

更新: 我已经用我真正在做的事情更新了线程事务流(我在新行插入后提交第二个事务)。

根据维基百科的说法,这与幻读相符,我认为这是完全相同的场景。所以我还是不明白为什么我没有读到幻像[3|Charles,3000]

当在事务过程中执行两个相同的查询并且第二个查询返回的行集合与第一个查询不同时,就会发生幻读。

当执行 SELECT ... WHERE 操作时未获取范围锁时,可能会发生这种情况。幻读异常是不可重复读取的一种特殊情况,当事务 1 重复范围 SELECT ... WHERE 查询时,事务 2 在两个操作之间创建(即 INSERT)满足该 WHERE 的新行(在目标表中)条款。

Transaction 1                             Transaction 2
/* Query 1 */
SELECT * FROM users
WHERE age BETWEEN 10 AND 30;
                                          /* Query 2 */
                                          INSERT INTO users(id,name,age) VALUES ( 3, 'Bob', 27 );
                                          COMMIT;
/* Query 1 */
SELECT * FROM users
WHERE age BETWEEN 10 AND 30;
COMMIT;
Run Code Online (Sandbox Code Playgroud)

Sha*_*dow 3

您所描述的实际行为实际上是 的正确行为repeatable_read。您期望的行为可以通过使用来实现read_committed

\n\n

正如mariadb关于repeatable_read的文档所说(粗体是我的):

\n\n
\n

与 READ COMMITTED 隔离级别有一个重要的区别:同一事务中的所有一致读取都会读取由第一次读取建立的快照

\n
\n\n

在线程 1 中,FindAll()返回 John 和 Louis 的第一个调用建立了快照。第二个FindAll()只是使用相同的快照。

\n\n

Percona 博客文章READ-COMMITTED 和 REPEATABLE-READ 事务隔离级别之间的差异进一步证实了这一点:

\n\n
\n

在 REPEATBLE READ 中,在事务开始时创建 \xe2\x80\x98read 视图\xe2\x80\x99 ( trx_no does not see trx_id >= ABC,\n sees < ABB ),并且此\n 读视图 ( Oracle 术语中的一致性快照)在事务持续时间内保持打开状态。如果您在凌晨 5 点执行 SELECT 语句,并在下午 5 点返回打开的事务,那么当您运行相同的 SELECT 时,您将看到与上午 5 点看到的结果集完全相同的结果集。这称为 MVCC(多版本并发控制),它是使用行版本控制和 UNDO 信息来完成的。

\n
\n\n

更新

\n\n

警告:以下参考资料来自 MySQL 文档。但是,由于这些引用与 innodb 存储引擎相关,因此我坚信它们也适用于 mariadb 的 innodb 存储引擎。

\n\n

所以,在可重复读隔离级别下的innodb存储引擎中,非锁定选择在同一事务内从第一次读建立的快照中读取。无论在并发提交的事务中插入/更新/删除多少条记录,读取都将是一致的。时期。

\n\n

这是OP在问题中描述的场景。这意味着可重复读隔离级别中的非锁定读将无法产生幻像读,对吧?嗯,不完全是。

\n\n

正如 MySQL 关于InnoDB 一致性非锁定读取的文档所述:

\n\n
\n

数据库状态的快照适用于事务内的 SELECT 语句,而不一定适用于 DML 语句。如果您插入或修改某些行,然后提交该事务,则从另一个并发 REPEATABLE READ 事务发出的 DELETE 或 UPDATE 语句可能会影响那些刚刚提交的行,即使会话无法查询它们。如果某个事务确实更新或删除了由不同事务提交的行,则这些更改对于当前事务确实可见。例如,您可能会遇到如下情况:\n

\n\n
SELECT COUNT(c1) FROM t1 WHERE c1 = \'xyz\';\n-- Returns 0: no rows match. DELETE FROM t1 WHERE c1 = \'xyz\';\n-- Deletes several rows recently committed by other transaction.\n\nSELECT COUNT(c2) FROM t1 WHERE c2 = \'abc\';\n-- Returns 0: no rows match. UPDATE t1 SET c2 = \'cba\' WHERE c2 = \'abc\';\n-- Affects 10 rows: another txn just committed 10 rows with \'abc\' values. \nSELECT COUNT(c2) FROM t1 WHERE c2 = \'cba\';\n-- Returns 10: this txn can now see the rows it just updated.\n
Run Code Online (Sandbox Code Playgroud)\n
\n\n

总结一下:如果使用可重复读隔离模式的innodb,那么并发提交事务中的数据修改语句与当前事务中的数据修改语句交互时,可能会出现幻读。

\n\n

链接的有关隔离级别的维基百科文章描述了一个通用的理论模型。您始终需要阅读实际的产品手册,了解某个功能的实现方式,因为可能存在差异。

\n\n

在维基百科文章中,仅将锁描述为防止幻读的方法。然而,innodb在大多数情况下通过创建快照来防止幻读,因此不需要依赖锁。

\n