【问题标题】:How to produce phantom reads?如何产生幻读?
【发布时间】:2011-07-23 15:04:34
【问题描述】:

使用“可重复读取”应该可以产生幻读,但是如何实现呢?我需要它作为教授 CS 学生的示例。

我认为我必须在非索引字段 x 上创建一个“SELECT ... WHERE x

除非它不起作用。我需要一张很大的桌子吗?还是别的什么?

【问题讨论】:

    标签: mysql sql database isolation-level


    【解决方案1】:

    MySQL 中 RR 隔离级别的“幻读”隐藏得很深,但仍然可以重现它。步骤如下:

    1. 创建表 ab(a int primary key, b int);

    2. Tx1:
      开始;
      从 ab 中选择 *; // 空集

    3. Tx2:
      开始;
      插入 ab 值(1,1);
      犯罪;
    4. Tx1:
      从 ab 中选择 *; // 空集,预期的幻像读取丢失。
      更新 ab 集 b = 2 其中 a = 1; // 1 行受影响。
      从 ab 中选择 *; // 1 行。 幻读在这里!!!!
      提交;

    【讨论】:

      【解决方案2】:

      埃里克,

      我只是通过大量行来测试它。

      您将永远不会在 InnoDB mysql 上找到具有已提交读取或更受限制的隔离级别的幻象。文档中有解释:

      REPEATABLE READ:对于一致性读取,与 READ COMMITTED 隔离级别有一个重要区别:同一事务中的所有一致性读取读取第一次读取建立的快照。此约定意味着如果您在同一事务中发出多个普通(非锁定)SELECT 语句,则这些 SELECT 语句彼此之间也是一致的。请参阅第 13.6.8.2 节,“一致的非锁定读取”。

      但您也不能在已提交读隔离级别中找到幻像:这是必要的,因为必须阻止“幻像行”才能使 MySQL 复制和恢复正常工作。

      更多详细信息:http://dev.mysql.com/doc/refman/5.1/en/set-transaction.html

      我认为您需要转移到另一个数据库品牌才能向您的学生展示幻影。我同时使用MSSQLSERVEROracle

      嗯……很遗憾你的第一个问题。

      【讨论】:

      • 谢谢。这或多或少地解释了它。我需要在一两个月内调查这个“问题”。而所谓的问题只是表明它可能发生在......在另一种数据库中。
      • @deFreitas 我没有写这个答案。我刚刚编辑了它。您应该将您的评论重定向到撰写答案的 danihp。
      • 我的错,评论被定向到@danihp
      • @danihp “你永远不会在 InnoDB mysql 上找到具有读取提交或更受限制的隔离级别的幻象。”根据我的测试,READ COMMITTED 不是这样,只是对于 REPEATABLE READ,运行一个 SELECT,然后在另一个会话上插入/更新一条记录,然后在第一个会话上再次运行 select,记录已经改变,我的 MySQL 版本是 5.7。 20 我的桌子正在使用 innoBD
      • 作为文档,幻读发生在可重复读取时。你能让你的陈述更清楚吗? dev.mysql.com/doc/refman/8.0/en/…
      【解决方案3】:

      InnoDB 应该像其他人写的那样防止幻读。

      但是 InnoDB 有一个不同的与锁定相关的奇怪行为。当查询获取锁时,它总是获取行的最新版本上的锁。所以试试下面的

      CREATE TABLE foo (i INT PRIMARY KEY, val INT);
      INSERT INTO foo (i, val) VALUES (1, 10), (2, 20), (3, 30);
      

      然后在两个并发会话中(打开两个终端窗口):

      -- window 1                               -- window 2
      START TRANSACTION;
                                                START TRANSACTION;
      
                                                 SELECT * FROM foo;
      
       UPDATE foo SET val=35 WHERE i=3;
      
                                                 SELECT * FROM foo;
      

      这应该在两个 SELECT 中显示 val = 10, 20, 30,因为 REPEATABLE-READ 意味着第二个窗口只看到其事务开始时存在的数据。

      但是:

                                                 SELECT * FROM foo FOR UPDATE;
      

      第二个窗口等待获取第 3 行的锁。

      COMMIT;
      

      现在第二个窗口中的 SELECT 完成,并显示 val = 10、20、35 的行,因为锁定该行会导致 SELECT 看到最近提交的版本。 InnoDB 中的锁定操作就像它们在 READ-COMMITTED 下运行一样,无论事务的隔离级别如何。

      你甚至可以来回切换:

                                                 SELECT * FROM foo;
      
                                                 SELECT * FROM foo FOR UPDATE;
      
                                                 SELECT * FROM foo;
      
                                                 SELECT * FROM foo FOR UPDATE;
      

      【讨论】:

      • 当我在 MySQL 中读取文档时,InnoDB 默认引擎使用可重复读取,因此会发生幻读。 dev.mysql.com/doc/refman/8.0/en/…
      • @hqt 可重复读取是可重复的,除非您执行锁定读取。
      • "InnoDB 的默认隔离级别。它防止查询到的任何行被其他事务更改,从而阻止不可重复读取但不阻止幻读"。这是在上述文件中所述。我理解你的观点,但是你能为我解释一下为什么mysql文档会这样说吗?
      • 文档不准确,或者至少不清楚。其他事务对数据的更改不会被阻止。你可以自己测试一下,使用类似于我上面所做的技术。打开两个窗口,启动事务并在第一个窗口中查询数据。在第二个窗口中更改数据。在第一个窗口中再次查询(与之前相同的事务),您会看到未更改的数据。
      【解决方案4】:

      对于隔离级别 REPEATABLE READ 为 InnoDB 引擎重现幻读的可能性值得怀疑,因为 InnoDB 使用Multiversion concurrency control - 对于每一行,MVCC 引擎在插入和删除行时知道事务编号,并且可以重现行更新的历史记录。

      因此,所有后续的 SELECT 语句将在事务开始时显示表的状态,但由同一事务插入、删除或更新的行除外。不会出现其他事务提交的新行,因为它们的插入事务数会比这个事务大,而行的范围在这里没有问题。

      我能够为 Apache Derby 数据库的隔离级别 REPEATABLE READ 重现 PHANTOM READS,因为它不使用多版本并发控制(在撰写此答案时为 10.8.2.2 版)。

      要重现,请设置适当的事务级别(在 ij - Derby 的 SQL 客户端中):

      -- Set autocommit off
      autocommit off;
      -- Set isolation level corresponding to ANSI REPEATABLE READ
      set isolation rs;
      

      T1:

      SELECT * FROM TableN;
      

      T2:

      INSERT INTO TableN VALUES(55, 1);
      COMMIT;
      

      再次T1:

      SELECT * FROM TableN;
      

      现在 T1 应该再看到一行;

      【讨论】:

      • 也谢谢你!在 MySQL 中似乎是不可能的 - 所以我可能需要使用 Derby 或其他一些来进行演示。
      【解决方案5】:

      可能会发生幻读,因为不存在范围锁,那么一个例子是(伪代码):

      线程1

      交易一 更新 TableN 设置 X=2 其中 X=1 等待(s1) 选择 X=1 的 TableN 犯罪

      线程2

      交易2: 插入表N(id,X)值(55,1) 犯罪; 通知(s1)

      在维基百科中还有另一个幻读示例:Phantom Reads|wikipedia

      这里重要的是事务同步,你可以使用同步点。

      编辑 使用 mysql 睡眠功能的示例(未测试):

      --on thread 1
      Create TableN(id int, x int);
      insert into TableN(id, X) values(1,1);
      insert into TableN(id, X) values(2,1);
      insert into TableN(id, X) values(3,1);

      开始交易; 更新 TableN 设置 X=2 其中 X=1 从 DUAL 中选择 SLEEP(30); 从中选择 X=1 的 TableN; 犯罪;

      --在其他线程中,20秒前;

      开始交易; 插入 TableN(id, X) 值(55,1);

      提交;

      【讨论】:

      • 我认为 OP 正在寻找可以在 MySQL 中执行的实际代码来证明这一点。
      • 你说得对,马丁。我知道几种方法,理论上可以进行幻读,但我从未能够展示出来。我的一些学生努力了,但无济于事。
      • 使用 java 线程,使用 autocommit=false 和线程的同步,你可以产生这个。
      • 不,我不能。并且不需要让 Java 进入场景,因为这只是一个数据库问题。
      • 然后尝试使用睡眠功能(答案已编辑以显示未经测试的示例)。
      【解决方案6】:

      为了补充 Dani 的好答案,您可以使用 Microsoft Sql Server 向您的学生展示这种行为。

      Sql Server 在文档here 声明的可重复读取隔离级别中显示幻读。

      Postgres 订阅与 InnoDb 相同的概念,如 here 所述。使用 Postgres,在可重复读取中不会发生幻读,因此也不适合您的教学目的。

      Sql Server 提供了另一种隔离级别,即快照,它执行 MySql InnoDb 和 Postgres 在可重复读取中所做的工作(这是一种无锁、基于版本的可重复读取实现,无需幻读,但不可序列化)。

      Sql Server Express 是免费的,但您确实需要一台 Windows 机器。您还可以为自己获取一个 Windows Azure 帐户,并在线使用 Sql Azure 展示该行为。

      【讨论】:

        猜你喜欢
        • 2017-08-05
        • 2017-02-07
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多