ExecuteReader需要一个已打开且可用的连接。该连接当前状态为连接中。

137
在尝试通过ASP.NET在线连接MSSQL数据库时,当有两个或更多人同时连接时,我会收到以下错误信息:

ExecuteReader需要一个打开且可用的连接。该连接的当前状态为Connecting。

在我的本地主机服务器上,网站正常运行。
以下是大致代码。
public Promotion retrievePromotion()
{
    int promotionID = 0;
    string promotionTitle = "";
    string promotionUrl = "";
    Promotion promotion = null;
    SqlOpenConnection();
    SqlCommand sql = SqlCommandConnection();

    sql.CommandText = "SELECT TOP 1 PromotionID, PromotionTitle, PromotionURL FROM Promotion";

    SqlDataReader dr = sql.ExecuteReader();
    while (dr.Read())
    {
        promotionID = DB2int(dr["PromotionID"]);
        promotionTitle = DB2string(dr["PromotionTitle"]);
        promotionUrl = DB2string(dr["PromotionURL"]);
        promotion = new Promotion(promotionID, promotionTitle, promotionUrl);
    }
    dr.Dispose();
    sql.Dispose();
    CloseConnection();
    return promotion;
}

我能知道可能出了什么问题,以及如何解决吗?

编辑:不要忘记,我的连接字符串和连接都是静态的。我相信这就是原因。请给予建议。

public static string conString = ConfigurationManager.ConnectionStrings["dbConnection"].ConnectionString;
public static SqlConnection conn = null;

26
不要在多线程环境(如ASP.NET)中使用共享/静态连接,因为这会导致锁定或异常(例如打开太多的连接)。把你的DB类扔进垃圾桶里,在需要时创建、打开、使用、关闭和处理ado.net对象。还要注意using语句。 - Tim Schmelter
2
你能详细介绍一下 SqlOpenConnection() 和 sql.ExecuteReader() 函数吗? - ankit rajput
1
私有无返回值方法 SqlOpenConnection() { 尝试 { conn = new SqlConnection(); conn.ConnectionString = conString; conn.Open(); } 捕获 (SqlException ex) { 抛出 ex; } } - Guo Hong Lim
@GuoHongLim:我忘了提到即使是静态的 conString 从性能方面来说也没有任何加成,因为默认情况下它会被缓存,就像当前应用程序中的每个配置值一样。 - Tim Schmelter
...只是为了让它成为一个已知的未知数:确保您也正确处理数据库事务处理/工作单元,这留给读者作为练习。 - mwardm
2个回答

254

很抱歉一开始只留下了评论,但我几乎每天都会发表类似的评论,因为许多人认为将 ADO.NET 功能封装到 DB-Class(我在10年前也是这样认为)是明智的选择。大多数人决定使用静态/共享对象,因为它似乎比为每个操作创建一个新对象更快。

从性能和故障安全性来看,这既不明智也不可取。

不要侵犯连接池的领地

ADO.NET 内部管理与DBMS之间底层连接的原因有很好的解释: ADO-NET 连接池

实际上,大多数应用程序仅使用一个或几个不同的连接配置。这意味着在应用程序执行期间,将重复打开和关闭许多相同的连接。为了最小化打开连接的成本,ADO.NET使用一种名为连接池的优化技术。 连接池可以减少必须打开新连接的次数。池管理器维护物理连接的所有权。它通过保持每个给定连接配置的一组活动连接来管理连接。每当用户在连接上调用Open时,池管理器会在池中查找可用的连接。如果有可用的池连接,则返回给调用者而不是打开新连接。当应用程序在连接上调用Close时,池管理器将其返回到活动连接的池集合而不是关闭它。一旦连接返回到池中,它就可以在下一次Open调用上被重用。
很明显,没有理由避免创建、打开或关闭连接,因为实际上它们根本没有被创建、打开和关闭。这只是一个标志,用于连接池知道何时可以重复使用连接。但这是非常重要的标志,因为如果一个连接正在使用中(连接池认为),必须打开一个新的物理连接到数据库管理系统,这是非常昂贵的。

所以你不会获得任何性能改进,反之亦然。如果达到了指定的最大池大小(默认值为100),甚至会出现异常(太多的打开连接...)。这不仅会极大地影响性能,还会成为令人讨厌的错误和(没有使用事务)数据转储区域。

如果您甚至使用静态连接,则为尝试访问此对象的每个线程创建锁定。ASP.NET本质上是一个多线程环境。因此,这些锁定可能导致性能问题。实际上,迟早会出现许多不同的异常(如ExecuteReader需要打开并可用的连接)。

结论

  • 不要重用连接或任何ADO.NET对象。
  • 不要使它们静态/共享(在VB.NET中)
  • 在您需要它们的地方(例如在方法中),始终创建、打开(在连接的情况下)、使用、关闭和处理它们
  • 使用using-statement隐式释放和关闭(在连接的情况下)

这不仅适用于连接(尽管最为明显)。实现IDisposable接口的每个对象都应该被处理掉(最简单的方式是使用using-statement),特别是在System.Data.SqlClient命名空间中。

上述所有内容都反对自定义DB-Class,该类封装并重复使用所有对象。这就是我建议废弃它的原因。它只会带来问题。


编辑:这是您的retrievePromotion方法的可能实现:

public Promotion retrievePromotion(int promotionID)
{
    Promotion promo = null;
    var connectionString = System.Configuration.ConfigurationManager.ConnectionStrings["MainConnStr"].ConnectionString;
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        var queryString = "SELECT PromotionID, PromotionTitle, PromotionURL FROM Promotion WHERE PromotionID=@PromotionID";
        using (var da = new SqlDataAdapter(queryString, connection))
        {
            // you could also use a SqlDataReader instead
            // note that a DataTable does not need to be disposed since it does not implement IDisposable
            var tblPromotion = new DataTable();
            // avoid SQL-Injection
            da.SelectCommand.Parameters.Add("@PromotionID", SqlDbType.Int);
            da.SelectCommand.Parameters["@PromotionID"].Value = promotionID;
            try
            {
                connection.Open(); // not necessarily needed in this case because DataAdapter.Fill does it otherwise 
                da.Fill(tblPromotion);
                if (tblPromotion.Rows.Count != 0)
                {
                    var promoRow = tblPromotion.Rows[0];
                    promo = new Promotion()
                    {
                        promotionID    = promotionID,
                        promotionTitle = promoRow.Field<String>("PromotionTitle"),
                        promotionUrl   = promoRow.Field<String>("PromotionURL")
                    };
                }
            }
            catch (Exception ex)
            {
                // log this exception or throw it up the StackTrace
                // we do not need a finally-block to close the connection since it will be closed implicitly in an using-statement
                throw;
            }
        }
    }
    return promo;
}

1
这对于提供连接工作范例非常有用。感谢这个解释。 - aminvincent
写得很好,解释了许多人意外发现的事情,我希望更多的人知道这一点。(+1) - Andrew Hill
1
谢谢您,先生。我认为这是我读过的关于这个主题的最好的解释,这是一个非常重要的主题,许多新手都会犯错误。我必须赞扬您出色的写作能力。 - Sasino
@Tim Schmelter,我该如何使用您建议的方法使在不同线程上运行的查询利用单个事务进行提交/回滚? - geeko

2

我几天前遇到了这个错误。

在我的情况下,是因为我在单例模式中使用了事务。

.Net对单例模式不太友好,正如上面所述。

我的解决方案是:

public class DbHelper : DbHelperCore
{
    public DbHelper()
    {
        Connection = null;
        Transaction = null;
    }

    public static DbHelper instance
    {
        get
        {
            if (HttpContext.Current is null)
                return new DbHelper();
            else if (HttpContext.Current.Items["dbh"] == null)
                HttpContext.Current.Items["dbh"] = new DbHelper();

            return (DbHelper)HttpContext.Current.Items["dbh"];
        }
    }

    public override void BeginTransaction()
    {
        Connection = new SqlConnection(Entity.Connection.getCon);
        if (Connection.State == System.Data.ConnectionState.Closed)
            Connection.Open();
        Transaction = Connection.BeginTransaction();
    }
}

我使用HttpContext.Current.Items作为我的实例。这个类DbHelper和DbHelperCore是我自己的类。

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