【问题标题】:Is it possible to produce phantom read in single SQL Server query?是否可以在单个 SQL Server 查询中产生幻读?
【发布时间】:2018-08-03 23:53:33
【问题描述】:

我设法找到的所有幻读解释都通过在一个事务中运行 2 个选择语句来演示幻读(例如https://blobeater.blog/2017/10/26/sql-server-phantom-reads/

BEGIN TRAN

    SELECT #1

    DELAY DURING WHICH AN INSERT TAKES PLACE IN A DIFFERENT TRANSACTION

    SELECT #2

END TRAN

是否可以在一个 select 语句中重现幻读?这意味着 select 语句从事务 #1 开始。然后插入在事务#2 上运行并提交。最后,来自事务 #1 的 select 语句完成,但没有返回事务 #2 插入的行。

【问题讨论】:

  • 不,为了体验幻读,您需要至少两个独立的事务和语句
  • 运行默认READ COMMITTED 隔离级别的单个选择语句可能会丢失其他事务插入的行。但是,这不是幻读。

标签: sql-server tsql


【解决方案1】:

SQL Server Transaction Isolation Levels documentation 将幻像行定义为“与搜索条件匹配但最初没有看到”(强调我的)。因此,发生幻读需要不止一个SELECT 语句。

在执行SELECT 语句执行期间插入的数据可能不会在READ COMMITTED 隔离级别中返回,具体取决于时间,但这不是定义上的幻读。下面的示例显示了这种行为。

--create table with enough data for a long-running SELECT query
CREATE TABLE dbo.PhantomReadExample(
      PhantomReadExampleID int NOT NULL
        CONSTRAINT PK_PhantomReadExample PRIMARY KEY
    , PhantomReadData char(8000) NOT NULL
    );
--insert 100K rows
WITH 
     t10 AS (SELECT n FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) t(n))
    ,t1k AS (SELECT 0 AS n FROM t10 AS a CROSS JOIN t10 AS b CROSS JOIN t10 AS c)
    ,t1m AS (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 0)) AS num FROM t1k AS a CROSS JOIN t1k AS b)
INSERT INTO dbo.PhantomReadExample WITH(TABLOCKX) (PhantomReadExampleID, PhantomReadData)
SELECT num*2, 'data'
FROM t1m
WHERE num <= 100000;
GO

--run this on connection 1
SELECT *
FROM dbo.PhantomReadExample
ORDER BY PhantomReadExampleID;
GO
--run this on connection 2 while the connection 1 SELECT is running
INSERT INTO dbo.PhantomReadExample(PhantomReadExampleID, PhantomReadData) 
    VALUES(1, 'data');
GO

SELECT 查询扫描期间读取行时会在行上获取共享锁,以确保只读取已提交的数据,但一旦读取数据就会立即释放这些锁以提高并发性。这允许其他会话在 SELECT 查询运行时插入、更新和删除行。

在这种情况下不返回插入的行,因为有序聚集索引扫描已经超过了插入点。

【讨论】:

  • 使用 Read Commited,我曾认为共享锁会阻止更新正在读取的行。这意味着即使将插入的行添加到尚未扫描的位置,也不会读取它?如果我错了,我会很高兴今天学到了一些东西。
  • 我认为缺少的部分是在插入完成时保持选择运行的一种方法。我通过按以下顺序运行语句来做到这一点:(con. 3) begin tran, update; (con. 1) 选择; (con 2) 插入; (con 3) 提交。
  • @JustinasSimanavicius,我忘了添加表创建和加载脚本。这将为长时间运行的SELECT 查询插入足够的数据,该查询可以通过自动提交事务重现问题。
  • @JoeC 我相信共享锁在读取后会立即释放(stackoverflow.com/a/941960/2128373),这意味着它们可以在读取后更新。在我的实验中,我设法让选择保持运行,然后在尚未扫描的位置插入一行,最后在选择结果中得到它。
  • @JoeC,在读取的行上获取共享锁以实现读取一致性,但在查询不再需要时立即释放这些锁。我在答案中添加了这个细节。
【解决方案2】:

下面是幻读的wikipedia definition

当在事务过程中出现新行时,就会发生幻读 被另一个事务添加到正在读取的记录中。

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

这当然可以在单个读取查询中重现(当然,其他数据库活动也必须发生以产生幻像行)。

设置

CREATE TABLE Test(X INT PRIMARY KEY);

连接 1(保持运行)

SET NOCOUNT ON;
WHILE 1 = 1
INSERT INTO Test VALUES (CRYPT_GEN_RANDOM(4))

连接 2

如果在读取提交锁隔离级别运行,这极有可能返回一些行(对于内部部署产品的默认设置,并通过下面的表提示强制执行)

WITH CTE AS
(
SELECT *
FROM Test WITH (READCOMMITTEDLOCK)
WHERE X BETWEEN 0 AND 2147483647
)
SELECT *
FROM CTE c1
FULL OUTER HASH JOIN CTE c2 ON c1.X = c2.X
WHERE (c1.X IS NULL OR c2.X IS NULL)

返回的行是在表的第一次和第二次读取之间添加的与WHERE X BETWEEN 0 AND 2147483647 谓词匹配的行的值。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-11-19
    • 2016-10-03
    相关资源
    最近更新 更多