26

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

我认为我必须在非索引字段 x 上创建一个“SELECT ... WHERE x <= 888”,上限 888 不存在,然后在另一个连接上插入一个值略低于 888 的新行。

除非它不起作用。我需要一张很大的桌子吗?或者是其他东西?

4

6 回答 6

23

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

  1. 创建表 ab(a int 主键,b int);

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

  3. Tx2:
    开始;
    插入 ab 值(1,1);
    犯罪;
  4. Tx1:
    从ab中选择*;// 空集,预期的幻像读取丢失。
    更新 ab 设置 b = 2 其中 a = 1; // 1 行受影响。
    从 ab 中选择 *;// 1 行。幻读在这里!!!!
    犯罪;
于 2016-12-16T06:26:19.323 回答
18

Erik,

I come just from test it with a very large number of rows.

You will never found phantoms on InnoDB mysql with read commited or more restricted isolation level. It is explained on documentation:

REPEATABLE READ: For consistent reads, there is an important difference from the READ COMMITTED isolation level: All consistent reads within the same transaction read the snapshot established by the first read. This convention means that if you issue several plain (nonlocking) SELECT statements within the same transaction, these SELECT statements are consistent also with respect to each other. See Section 13.6.8.2, “Consistent Nonlocking Reads”.

But you can't also found phantoms in read commited isolation level: This is necessary because “phantom rows” must be blocked for MySQL replication and recovery to work.

More detailed information: http://dev.mysql.com/doc/refman/5.1/en/set-transaction.html

I think you will need to move to another database brand to show phantoms to your students. I use both MSSQLSERVER and Oracle.

Well ... its a pity for your first question.

于 2011-11-28T21:51:55.007 回答
7

正如其他人所写的那样,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;
于 2016-12-16T06:44:34.717 回答
5

为隔离级别 REPEATABLE READ 重现 InnoDB 引擎的幻读的可能性值得怀疑,因为 InnoDB 使用多版本并发控制- 对于每一行,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 应该再看到一行;

于 2011-11-28T12:21:05.840 回答
0

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

Sql Server 在可重复读取隔离级别中显示幻读,如此处文档所述

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

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

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

于 2013-12-10T17:04:52.210 回答
0

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

线程1

交易一

更新 TableN 设置 X=2 其中 X=1

等待(s1)
选择 X=1 的 TableN

犯罪

线程2

交易2:

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

在维基百科中还有另一个幻读的例子:幻读|维基百科

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

使用 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);

BEGIN TRANSACTION; Update TableN set X=2 where X=1 SELECT SLEEP(30) FROM DUAL; select TableN from where X=1; COMMIT;

--In other thread, before 20 secs;

BEGIN TRANSACTION; insert into TableN(id, X) values(55,1);

COMMIT;

于 2011-03-26T21:37:30.897 回答