【问题标题】:Why isn't REPETEABLE_READ on MariaDB producing phantom reads?为什么 MariaDB 上的 REPETEABLE_READ 不产生幻读?
【发布时间】:2017-02-07 16:40:09
【问题描述】:

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

例如:

bank_account 表中有两行:

  ID |  OWNER | MONEY
------------------------
  1  |  John  | 1000
  2  |  Louis | 2000

预期的流程应该如下所示:

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()                               
  |_                                        

综上所述,在Thread2.createAccount("Charles", 3000); 及其刷新之后,Thread1 将搜索所有行并得到

  ID |  OWNER   | MONEY
------------------------
  1  |  John    | 1000
  2  |  Louis   | 2000
  3  |  Charles | 3000

Thread1 可以防止看到 [1, John, 1000] 而不是 [1, John, 1100] 的未提交更改,但它应该会看到新插入的行。

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

  ID |  OWNER   | MONEY
------------------------
  1  |  John    | 1000
  3  |  Charles | 3000

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

这是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
    ...
}

我在 //PAUSE HERE 处暂停执行。

然后线程2执行:

bankAccountService.addMoneyReadUncommited(ba.getId(), 200);
bankAccountService.createAccount("Carlos", 3000);

然后 Thread1 恢复:

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

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

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

当在事务过程中,两个 执行相同的查询,并返回由 第二个查询与第一个不同。

当执行一个范围锁时,可能会发生这种情况 SELECT ... WHERE 操作。幻读异常是一个特殊的 事务 1 重复一个范围内的不可重复读取的情况 SELECT ... WHERE 查询以及在两个操作之间的事务 2 创建(即插入)新行(在目标表中)满足 那个 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;

【问题讨论】:

    标签: mysql spring jdbc mariadb isolation-level


    【解决方案1】:

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

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

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

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

    Differences between READ-COMMITTED and REPEATABLE-READ transaction isolation levels 上的 Percona 博客文章进一步证实了这一点:

    在 REPEATBLE READ 中,“读取视图”( trx_no 看不到 trx_id >= ABC, sees

    更新

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

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

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

    正如InnoDB Consistent Nonlocking Reads 上的 MySQL 文档所说:

    数据库状态的快照适用于其中的 SELECT 语句 一个事务,不一定是 DML 语句。如果您插入或 修改一些行,然后提交该事务,DELETE 或 UPDATE 从另一个并发 REPEATABLE READ 事务发出的语句 可能会影响那些刚刚提交的行,即使会话可能 不要查询他们。如果事务确实更新或删除已提交的行 通过不同的交易,这些更改确实对 当前交易。例如,您可能会遇到类似的情况 以下:

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

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

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

    在 Wikipedia 文章中,只有锁被描述为防止幻读的一种手段。但是,innodb 在大多数情况下使用快照的创建来防止幻读,因此不需要依赖锁。

    【讨论】:

      猜你喜欢
      • 2011-07-23
      • 2017-08-05
      • 2019-08-12
      • 2012-05-02
      • 2017-08-02
      • 2012-04-25
      • 2012-06-08
      相关资源
      最近更新 更多