C# 客户端 |连接池 |打开第二个连接时出错

C# SqlClient | Connection Pooling | Error When Opening Second Connection

我正在尝试弄清楚如何在使用 SqlClient 访问 Microsoft SQL 服务器时正确设置我的数据库访问权限。在大多数情况下它是有效的,但有一个特定的场景给我带来了麻烦。即:试图在同一个线程中同时使用两个连接;一个打开数据 reader,另一个执行删除操作。

以下代码演示了我的难题:

public class Database { 
   ...
   internal SqlConnection CreateConnection() => new SqlConnection(connectionString);
   ...
}
public IEnumerable<Model> GetModel() {
   var cmd = new SqlCommand() { ... };
   using(var conn = db.CreateConnection()) {
      conn.Open();
      cmd.Connection = conn;
      using(var reader = cmd.ExecuteReader()) {
         while(reader.Read()) {
            var m = new Model();
            // deserialization logic
            yield return m;
         }
      }
   }
}

public void Delete(int id) {
   var cmd = new SqlCommand() { ... }
   using(var conn = db.CreateConnection()) {
      conn.Open(); // throwing the error here
      cmd.Connection = conn;
      cmd.ExecuteNonQuery();
   }
}

申请代码:

using(var scope = new TransactionScope()) {
   var models = GetModels();
   foreach(var m in models) {
      Delete(m.Id); // throws an exception
   }
   scope.Complete();
}

无论出于何种原因,上述代码在尝试执行删除操作时抛出异常:

quote System.Transactions.TransactionAbortedException: The transaction has aborted. ---> System.Transactions.TransactionPromotionException: Failure while attempting to promote transaction. ---> System.Data.SqlClient.SqlException: There is already an open DataReader associated with this Command which must be closed first. ---> System.ComponentModel.Win32Exception: The wait operation timed out quote

现在,我已经确认,如果我在 ConnectionString 上设置 MultipleActiveResultSets=truePooling=false,则上述应用程序代码将正常运行。但是,我似乎不需要设置其中任何一个。如果我同时打开两个连接,它们不应该是单独的连接吗?为什么我从删除连接中收到一条错误消息,说有一个打开的 DataReader?

请帮助<3

据我了解,这里的主要原因是您的屈服迭代。

因此数据库连接尚未调用处置,因为它仍在您的迭代 (foreach) 中使用。例如,如果您当时调用了 .ToList(),它应该 return 所有条目,然后处理连接。

有关 yield 如何在迭代中工作的更好解释,请参阅此处:

到目前为止,这里最简单的解决方法是在删除任何模型之前简单地加载事务外的所有模型。例如

var models = GetModels().ToList();
using(var scope = new TransactionScope()) {
 
   foreach(var m in models) {
      Delete(m.Id); // throws an exception
   }
   scope.Complete();
}

甚至在事务shold 工作中获取模型

using(var scope = new TransactionScope()) {
   var models = GetModels().ToList(); 
   foreach(var m in models) {
      Delete(m.Id); // throws an exception
   }
   scope.Complete();
}

只要您在迭代期间不让连接保持打开状态。如果您允许 GetModels() 中的连接关闭,它将返回到连接池,并可用于同一事务中登记的后续方法。

在当前代码中,GetModels() 中的连接在 foreach 循环期间保持打开状态,并且 Delete(id) 必须打开第二个连接并尝试创建分布式事务,但失败了。

如果没有 MultipleActiveResultsets,GetModels 连接无法在返回查询结果的过程中提升为分布式事务。设置 pooling=false 不会使这个错误消失。

这里有一个简化的再现:

using Microsoft.Data.SqlClient;
using System.Collections.Generic;
using System.Transactions;

namespace SqlClientTest
{

    class Program
    {

        
        static void Main(string[] args)
        {

            Setup();

            var topt = new TransactionOptions();
            topt.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;
            using (new TransactionScope(TransactionScopeOption.Required,  topt ))
            {
                foreach (var id in GetIds())
                {
                    Delete(id);
                }

            }

        }

        static string constr = @"server=.;database=tempdb;Integrated Security=true;TrustServerCertificate=true;";


        public static void Setup()
        {
            using (var con = new SqlConnection(constr))
            {
                con.Open();
                var cmd = con.CreateCommand();
                cmd.CommandText = "drop table if exists ids; select object_id id into ids from sys.objects";
                
                cmd.ExecuteNonQuery();
            }
        }

        public static IEnumerable<int> GetIds()
        {
            using (var con = new SqlConnection(constr))
            {
                con.Open();
                var cmd = con.CreateCommand();
                cmd.CommandText = "select object_id id from sys.objects";
                using (var reader = cmd.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        yield return reader.GetInt32(0);
                    }
                }
            }
        }
        public static void Delete(int id)
        {
            using (var con = new SqlConnection(constr))
            {
                con.Open();
                var cmd = con.CreateCommand();
                cmd.CommandText = "insert into ids(id) values (@id)";
                cmd.Parameters.Add(new SqlParameter("@id", id));

                cmd.ExecuteNonQuery();
            }
        }

    }

}

下面是 Profiler 在 运行:

时显示的内容