在可重复读隔离级别下,事务中的数据是否可能发生更改?

3

我有一些.NET代码包含在可重复读取事务中,代码如下:

using (
                var transaction = new TransactionScope(
                    TransactionScopeOption.Required,
                    new TransactionOptions { IsolationLevel = IsolationLevel.RepeatableRead },
                    TransactionScopeAsyncFlowOption.Enabled))
            {
                int theNextValue = GetNextValueFromTheDatabase();
                var entity = new MyEntity
                               {
                                   Id = Guid.NewGuid(),
                                   PropertyOne = theNextValue, //An identity column
                                   PropertyTwo = Convert.ToString(theNextValue),
                                   PropertyThree = theNextValue,
                                   ...
                               };
                DbSet<MyEntity> myDbSet = GetEntitySet();
                myDbSet.Add(entity);
                await this.databaseContext.Entities.SaveChangesAsync();

                transaction.Complete();
            }

第一种方法GetNextValueFromTheDatabase,从数据库中的表中检索存储在列中的最大值。我使用可重复读取,因为我不希望两个用户读取并使用相同的值。然后,我只需在内存中创建一个实体Entity并调用SaveChangesAsync()将值写入数据库。
偶尔,我会发现实体.PropertyOne、实体.PropertyTwo和实体.PropertyThree的值不匹配。例如,实体.PropertyOne的值为500,但实体.PropertyTwo和实体.PropertyThree的值为499。这是怎么可能的?即使代码没有包含在事务中,我也希望这些值匹配(如果两个用户同时运行,则可能在实体中重复)。
我正在使用Entity Framework 6和Sql Server 2008R2。 编辑:
以下是GetNextValueFromTheDatabase的代码:
public async Task<int> GetNextValueFromTheDatabase()
{
    return await myQuerable
        .OrderByDescending(x => x.PropertyOne) //PropertyOne is an identity column (surprise!)
        .Select(x => x.PropertyOne)
        .Take(1)
        .SingleAsync() + 1;
}

这个问题变得太奇怪了,所以简单的答案是“是”。 - chris544
3个回答

0

我认为你基本上正在经历幻读

考虑两个事务T1、T2,按照下面的方式进行执行。问题在于,在T1的第一次读取中,您没有得到从事务T2插入的值(X)。在第二次读取时,您在SELECT语句中确实获得了该值(X)。这就是可重复读的可怕之处。如果从表中读取了某些行,它不会阻止整个表的插入。它只锁定现有的行。

T1                                     T2

SELECT A.X FROM WeirdTable

                                       INSERT INTO WeirdTable TABLE (A) VALUES (X)
SELECT A.X FROM WeirdTable

.

更新

看起来这个回答与特定问题无关。它与可重复读隔离级别有关,与此问题的关键词匹配,并且在概念上也没有错误,因此我会把它留在这里。


谢谢您的回复,但这并不能解释为什么在同一表格中不同列会有不同的值。 - what evAR

0

所以这个问题无法明确回答,因为代码中没有显示GetNextValueFromTheDatabase。我根据你所说的来回答:

在SQL Server中,REPEATABLE READ会对你读取的行进行S锁定。当你从索引中读取当前最大值时,该行被S锁定。现在,如果出现新的最大值,该行不受锁定的影响。这就是为什么锁定不会阻止其他竞争的最大值出现。

如果你通过从表中读取最大值来获得最大值,则需要使用SERIALIZABLE隔离级别。这将导致在你的特定情况下发生死锁。可以通过锁定提示或重试来解决这个问题。

你也可以保留一个单独的表来存储当前的最大值。REPEATABLE READ在这里已经足够了,因为你总是访问该表的同一行。即使没有锁定提示,你在这里也会看到死锁。

重试是解决死锁的有效方法。


但是再次查看查询语句让我感到困惑。由于可重复读和幻象读取的发生,这些值可能是错误的。但是它们怎么可能不匹配呢? 如果函数只被调用一次并且该值存储在变量中,那么匹配不应该发生,因为其他事务中发生了一些事情吗? - chris544
不确定您的意思。您可能会获得10,另一笔交易插入11。现在您插入11。这是一个冲突。 - usr
我本人有过疏忽,但我曾经看到实体框架事务发生了一些奇怪的事情。如果真的没有其他事务修改这些行,那会很好笑。 - chris544
属性未在事务内的其他地方写入。我有点怀疑EF是否按照我的预期将事务传播到数据库。我明天会运行SQL分析器来查看。即使EF没有正确传播事务,我也只从数据库检索最大值一次,并且我仅通过单个SaveChangesAsync()调用写回数据库。 - what evAR
@whatevAR 嗯,如果代码是正确的,那么这不可能发生。所以肯定有意外情况发生。分析是下一步很好的选择。你为什么认为事务是问题所在呢?当你插入 (x, x) 时,无论 tran 配置如何,你都希望看到两次 x,还要检查映射。 - usr
显示剩余3条评论

0

我终于弄明白了。正如usr's response所描述的那样,多个事务可以同时读取相同的最大值(S-Lock)。问题在于其中一个列是identity列。EF允许您在插入时指定标识列的值,但会忽略您指定的值。因此,标识列似乎大部分时间都更新为预期值,但实际上域实体中指定的值恰好与数据库内部生成的值匹配。

例如,假设当前的最大数字是499,事务A和事务B都读取499。当事务A完成时,它成功地将500写入所有三个属性。事务B尝试将500写入所有3个列。非标识列成功更新为500,但标识列的值会自动递增到下一个可用值(而不会抛出错误)

几个解决方案

我使用的解决方案是在插入记录时不设置任何列的值。一旦记录被插入,使用数据库分配的标识列的值更新其他两个列。

另一个选择是将列的选项更改为.HasDatabaseGeneratedOption(DatabaseGeneratedOption.None),这比第一个选项执行得更好,但需要采取usr建议的更改来缓解锁定问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接