7

我们遇到了一些显然以错误的隔离级别执行的数据库代码的问题。在代码的这个特定部分,它应该使用“READ UNCOMMITTED”执行以最小化锁。在这一点上,不一致的数据是可以的。

然而,代码实际上是用 READ COMMITTED 读取的,我们不知道为什么。

这是我们所做的:

  1. 打开连接
  2. 在此连接上执行“SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED”
  3. 打断点
  4. 执行 SQL

在断点上,我们向数据库发出以下命令:

select s.session_id, s.transaction_isolation_level, st.text from sys.dm_exec_sessions s
inner join sys.sysprocesses sp on (sp.spid = s.session_id) 
CROSS APPLY sys.dm_exec_sql_text(sp.sql_handle) st

此 SQL 现在报告 4 个池连接,其中一个是我们的连接,我们可以超越断点来执行我们的 SQL,它具有以下状态:

53  2   SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

IE。会话 53 具有隔离级别 2(已提交读),在此会话上执行的最后一个 SQL 是“SET TRANSACTION ...”命令。

怎么会这样?

我们用 SQL Profiler 验证了这个连接在我们的 .NET 代码打开它之前没有存在,所以它没有从连接池中重用。

然而,有了一个新的连接,并且在其上执行的唯一且第一个 SQL 明确告诉它使用 READ UNCOMMITTED,连接如何仍然是 READ COMMITTED?

我们应该在这里看什么?

连接字符串(已编辑位)如下所示:

SERVER=hostname;DATABASE=dbname;Integrated Security=false;USER ID=sa;PASSWORD=****;Application Name=appname;Type System Version=SQL Server 2000;Workstation ID=hostname;

连接是正常SqlConnection连接,以正常方式打开。

不幸的是,如果我们编写打开 SqlConnection 的普通代码,我们将无法重现该问题,因此应用程序状态肯定有问题,但由于 SqlProfiler 和 Sql Server 都告诉我们是的,SQL 已执行,但没有,我不在乎。

什么会影响这一点?

完全相同的代码也会打开其他连接,也就是说,代码会执行多次并打开许多连接,因此池中会出现多个连接,但只有第一个连接会出现此问题。

这是 SQL Server 2008 R2,我们在 2012 年也重现了这个问题。

编辑

好的,还有更多信息。

首先,我们启用池化,或者更确切地说,我们没有明确禁用它,也没有调整连接字符串来创建“N”个池。

但是,此连接是使用此特定连接字符串打开的第一个连接,因此不会从池中检索它。另请参阅下面关于它永久“生病”的说明。

这个连接是这样建立的:

var conn = new SqlConnection(...);
conn.StateChance += connection_StateChange;

private void connection_StateChange(Object sender, StateChangeEventArgs e)
{
    if (e.CurrentState == ConnectionState.Open)
    {
        using (IDbCommand cmd = ((SqlConnection)sender).CreateCommand())
        {
            cmd.CommandText = "SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED";
            cmd.ExecuteNonQuery();
        }

在此之前我们没有执行任何其他 SQL。

请注意,此代码在应用程序的生命周期中被多次使用,只有它打开的第一个连接最终会出错。

这种联系也变得永远病态。因为每次我们打开连接(即使我们可能把它从连接池中取出),上面的状态改变事件都会执行,试图再次设置隔离级别。这也失败了,但仅针对此单个连接。

此外,自从我发布此问题以来,我们发现了一件事会影响这一点。

通过更改我在上面发布的连接字符串:

...;Type System Version=SQL Server 2000;...

对此:

...;Type System Version=SQL Server 2008;MultipleActiveResultSets=true;...

然后这个问题就消失了,在前面列出的断点处,连接现在处于“READ UNCOMMITTED”状态。

这是一条红鲱鱼,在我们实际执行代码之前,我们的概述中不再报告连接。

我们正在继续调试。

4

1 回答 1

5

这里的问题是,不带参数的SqlConnection.BeginTransaction默认是读取提交的。我想我们不明白该页面上的“默认隔离级别”文本是什么。

该页面有以下文字:

如果不指定隔离级别,则使用默认隔离级别。要使用 BeginTransaction 方法指定隔离级别,请使用采用 iso 参数 (BeginTransaction) 的重载。为事务设置的隔离级别在事务完成后一直存在,直到连接关闭或释放。在未启用快照隔离级别的数据库中将隔离级别设置为快照不会引发异常。事务将使用默认隔离级别完成。

(我的亮点)

这是一个演示的 LINQPad 脚本:

void Main()
{
    using (var conn = new SqlConnection("Data Source=.;Initial Catalog=master;Integrated security=true"))
    {
        conn.Open();
        Dump(conn, "after open");

        using (var cmd = new SqlCommand())
        {
            cmd.Connection = conn;
            cmd.CommandText = "SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED";
            cmd.ExecuteNonQuery();
        }

        Dump(conn, "after set iso");

        using (var cmd = new SqlCommand())
        {
            cmd.Connection = conn;
            cmd.CommandText = "BEGIN TRANSACTION";
            cmd.ExecuteNonQuery();
        }

        Dump(conn, "after sql-based begin transaction");

        using (var cmd = new SqlCommand())
        {
            cmd.Connection = conn;
            cmd.CommandText = "COMMIT";
            cmd.ExecuteNonQuery();
        }

        Dump(conn, "after sql-based commit");

        var trans = conn.BeginTransaction();

        Dump(conn, "after .net begin transaction", trans);

        trans.Commit();

        Dump(conn, "after .net commit");
    }
}

public static void Dump(SqlConnection connection, string title, SqlTransaction transaction = null)
{
    using (var cmd = new SqlCommand())
    {
        cmd.Connection = connection;
        if (transaction != null)
            cmd.Transaction = transaction;
        cmd.CommandText = "SELECT transaction_isolation_level FROM sys.dm_exec_sessions WHERE session_id = @@SPID";
        Debug.WriteLine(title + "=" + Convert.ToInt32(cmd.ExecuteScalar()));
    }
}

它将输出:

after open=2
after set iso=1
after sql-based begin transaction=1
after sql-based commit=1
after .net begin transaction=2
after .net commit=2

在这里您可以看到,通过 SQL 手动启动和提交事务不会更改隔离级别,但在 .NET 中启动事务而不显式说明隔离级别仍然会将其更改为已提交读。

由于在我们阅读的任何地方,在没有明确说明隔离级别的情况下启动事务就表示它继承了会话的隔离级别,我想我们不明白.NET 不会做同样的事情。

于 2013-06-10T11:59:08.473 回答