为什么MariaDB生成幻像读取时没有REPETEABLE_READ?

时间:2016-09-29 11:20:46

标签: mysql spring jdbc mariadb isolation-level

在我的测试中,我看到当使用 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
    ...
}

我暂停执行sais //PAUSE HERE

然后Thread2执行:

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]

  

在交易过程中发生两次虚拟读取   执行相同的查询,以及返回的行集合   第二个查询与第一个查询不同。

     

执行a时未获取范围锁定时可能会发生这种情况   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;

1 个答案:

答案 0 :(得分:3)

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

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

  

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

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

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

  

在REPEATBLE READ中,'读取视图'(trx_no看不到trx_id&gt; = ABC,   看到&lt; ABB)是在交易开始时创建的   读取视图(Oracle术语中的一致快照)保持打开状态   交易的持续时间。如果在凌晨5点执行SELECT语句,   并在下午5点进行公开交易,当你运行相同的时候   SELECT,然后您将看到完全相同的结果集   上午05时。这称为MVCC(多版本并发控制)和   它是使用行版本控制和UNDO信息完成的。

更新

警告:以下引用来自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和可重复读取隔离模式,那么如果并发提交事务中的数据修改语句与当前事务中的数据修改语句交互,则可能会发生幻像读取。

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

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