【问题标题】:There is already an open DataReader ... even though it is not已经有一个打开的 DataReader ...即使它不是
【发布时间】:2014-09-27 11:54:05
【问题描述】:

注意:当问题没有正确处理阅读器/连接,或者错误是由于处理不当的延迟加载时,我已经经历了数百万个问题。我相信这个问题是另一个问题,可能与 MySQL 的 .NET 连接器有关。

我通过其 .NET 连接器 (6.8.3) 广泛使用 MySQL 服务器 (5.6) 数据库。出于性能原因,所有表都是使用 MyISAM 引擎创建的。我只有 一个进程一个线程update: 事实上,这不是真的,见下文)访问数据库是顺序的,所以不需要事务和并发。

今天,在处理了以下一段代码之后:

public IEnumerable<VectorTransition> FindWithSourceVector(double[] sourceVector)
{
    var sqlConnection = this.connectionPool.Take();

    this.selectWithSourceVectorCommand.Connection = sqlConnection;

    this.selectWithSourceVectorCommand.Parameters["@epsilon"].Value
        = this.epsilonEstimator.Epsilon.Min() / 10;

    for (int d = 0; d < this.dimensionality; ++d)
    {
        this.selectWithSourceVectorCommand.Parameters["@source_" + d.ToString()]
        .Value = sourceVector[d];
    }

    // *** the following line (201) throws the exception presented below
    using (var reader = this.selectWithSourceVectorCommand.ExecuteReader())
    {
        while (reader.Read())
        {
            yield return ReaderToVectorTransition(reader);
        }
    }

    this.connectionPool.Putback(sqlConnection);
}

抛出以下异常:

MySqlException: 已经有一个打开的 DataReader 与此 Connection 关联,必须先关闭。

这是堆栈跟踪的相关部分:

在 MySql.Data.MySqlClient.ExceptionInterceptor.Throw(Exception 异常) 在 MySql.Data.MySqlClient.MySqlConnection.Throw(异常前) 在 MySql.Data.MySqlClient.MySqlCommand.CheckState() 在 MySql.Data.MySqlClient.MySqlCommand.ExecuteReader(CommandBehavior 行为) 在 MySql.Data.MySqlClient.MySqlCommand.ExecuteReader() 在 implementation.VectorTransitionsMySqlTable.d__27.MoveNext() 在 C:\Users\bartoszp...\implementation\VectorTransitionsMySqlTable.cs:line 201

在 System.Linq.Enumerable.d__3a1.MoveNext() at System.Linq.Buffer1..ctor(IEnumerable1 source) at System.Linq.Enumerable.ToArray[TSource](IEnumerable1 源) 在 implementation.VectorTransitionService.Add(VectorTransition vectorTransition) 在 C:\Users\bartoszp...\implementation\VectorTransitionService.cs:line 38

在 Program.Go[T](Environment`2 p, Space parentSpace, EpsilonEstimator epsilonEstimator, ThresholdEstimator thresholdEstimator, TransitionTransformer transitionTransformer, AmbiguityCalculator ac, VectorTransitionsTableFactory vttf, AxesTableFactory atf, ​​NeighbourhoodsTableFactory ntf, AmbiguitySamplesTableFactory astf, AmbiguitySampleMatchesTableFactory asmtf, MySql rejectDuplicates, Boolean addNew) 在 C:\Users\bartoszp...\Program.cs:line 323

connectionPool.Take 返回满足以下谓词的第一个连接:

private bool IsAvailable(MySqlConnection connection)
{
    var result = false;

    try
    {
        if (connection != null
            && connection.State == System.Data.ConnectionState.Open)
        {
            result = connection.Ping();
        }
    }
    catch (Exception e)
    {
        Console.WriteLine("Ping exception: " + e.Message);
    }

    return result && connection.State == System.Data.ConnectionState.Open;
}

(这与我之前的问题有关,当时我解决了一个不同但类似的问题:MySQL fatal error during information_schema query (software caused connection abort)

FindWithSourceVector 方法由以下代码调用:

var existing
    = this.vectorTransitionsTable
        .FindWithSourceVector(vectorTransition.SourceVector)
        .Take(2)
        .ToArray();

(我最多需要找到两个重复的向量) - 这是堆栈跟踪的 VectorTransitionService.cs:line 38 部分。

现在最有趣的部分:当调试器在异常发生后停止执行时,我调查了sqlConnection 对象发现,它没有与之关联的阅读器(下图)!

为什么会发生这种情况(显然是“随机的”——在过去的 ~20 小时内几乎每分钟都会调用此方法)?我可以避免这种情况(以其他方式猜测 - 当Ping 抛出异常并祈祷它会有所帮助时添加一些睡眠)?


关于连接池实现的附加信息:

Get 适用于仅调用简单查询且不使用读取器的方法,因此返回的连接可以以可重入的方式使用。本例中没有直接使用(因为涉及到读者):

public MySqlConnection Get()
{
    var result = this.connections.FirstOrDefault(IsAvailable);

    if (result == null)
    {
        Reconnect();

        result = this.connections.FirstOrDefault(IsAvailable);
    }

    return result;
}

Reconnect 方法只是遍历整个数组并重新创建和打开连接。

Take 使用Get,但也会从可用连接列表中删除返回的连接,因此如果某些方法在使用阅读器期间调用其他也需要连接的方法,则不会共享该连接。这里也不是这种情况,因为FindSourceVector 方法很简单(不调用使用数据库的其他方法)。但是,为了约定,使用Take - 如果有读者,请使用Take

public MySqlConnection Take()
{
    var result = this.Get();

    var index = Array.IndexOf(this.connections, result);

    this.connections[index] = null;

    return result;
}

Putback 只是将连接放置到第一个空点,或者如果连接池已满,则忽略它:

public void Putback(MySqlConnection mySqlConnection)
{
    int index = Array.IndexOf(this.connections, null);

    if (index >= 0)
    {
        this.connections[index] = mySqlConnection;
    }
    else if (mySqlConnection != null)
    {
        mySqlConnection.Close();
        mySqlConnection.Dispose();
    }
}

【问题讨论】:

  • 你还没有展示你对方法的返回值做了什么。考虑到迭代器块的执行方式,肯定有一些方法可以让你最终不关闭阅读器。
  • @JonSkeet 感谢您的宝贵时间。我已添加通话。
  • 这是多线程吗?也许一个计时器和过程花费的时间比间隔时间长?似乎是某种竞争条件。
  • @TyCobb 不,正如我所说,它只是一个线程,目前没有其他进程正在使用这个数据库。感谢您对此进行调查,这对我来说非常重要:)
  • @BartoszKP:即使我的回答有效,您也应该绝对回到正常的连接池。这么说吧:每天都有数以百万计的程序使用内置的连接池。与连接池代码中的错误相比,它更有可能是代码中的错误,因此添加 more 代码很可能会添加更多错误......看起来就是这样在这里。

标签: c# mysql .net database datareader


【解决方案1】:

我怀疑这是问题所在,在方法结束时:

this.connectionPool.Putback(sqlConnection);

您只获取来自迭代器的两个元素 - 因此您永远不会完成 while 循环,除非实际上只有一个从读取器返回的值。现在您正在使用 LINQ,它将自动在迭代器上调用 Dispose(),因此您的 using 语句仍将处理读取器 - 但您不会将连接放回池中。如果你在 finally 块中这样做,我想你会没事的:

var sqlConnection = this.connectionPool.Take();
try
{
    // Other stuff here...

    using (var reader = this.selectWithSourceVectorCommand.ExecuteReader())
    {
        while (reader.Read())
        {
            yield return ReaderToVectorTransition(reader);
        }
    }
}
finally
{
    this.connectionPool.Putback(sqlConnection);
}

或者理想情况下,如果您的连接池是您自己的实现,则使 Take 返回实现 IDisposable 的内容,并在完成后将连接返回到池中。

这是一个简短但完整的程序来演示正在发生的事情,不涉及任何实际数据库:

using System;
using System.Collections.Generic;
using System.Linq;

class DummyReader : IDisposable
{
    private readonly int limit;
    private int count = -1;
    public int Count { get { return count; } }

    public DummyReader(int limit)
    {
        this.limit = limit;
    }

    public bool Read()
    {
        count++;
        return count < limit;
    }

    public void Dispose()
    {
        Console.WriteLine("DummyReader.Dispose()");
    }
}

class Test
{    
    static IEnumerable<int> FindValues(int valuesInReader)
    {
        Console.WriteLine("Take from the pool");

        using (var reader = new DummyReader(valuesInReader))
        {
            while (reader.Read())
            {
                yield return reader.Count;
            }
        }
        Console.WriteLine("Put back in the pool");
    }

    static void Main()
    {
        var data = FindValues(2).Take(2).ToArray();
        Console.WriteLine(string.Join(",", data));
    }
}

正如所写 - 对读者的情况进行建模,只找到两个值 - 输出是:

Take from the pool
DummyReader.Dispose()
0,1

请注意,读者已被处置,但我们永远不会从池中返回任何东西。如果你改Main来模拟reader只有一个值的情况,像这样:

var data = FindValues(1).Take(2).ToArray();

然后我们一直通过while循环,所以输出改变了:

Take from the pool
DummyReader.Dispose()
Put back in the pool
0

我建议你复制我的程序并进行试验。确保你了解正在发生的一切......然后你可以将它应用到你自己的代码中。您可能还想阅读我在iterator block implementation details 上的文章。

【讨论】:

  • @BartoszKP:是的,using 语句有一个 finally 语句 - 所以它调用 reader.Dispose(),但这是不同的。是的,如果有超过 2 个结果,则最后一个 yield 后面的代码不会在您的情况下执行 - Take 方法将在不要求第三个的情况下处理迭代器结果,因此没有理由执行while 循环之后的代码。阅读器只会被关闭,因为它实际上位于 finally 块中。
  • @BartoszKP:是的,读者正在被处置——但我认为这不是问题所在。我相信这个错误实际上是你没有将连接返回到你的池。我怀疑这会在您的连接池中显示一个错误 - 完全有可能是来自MySqlException 的诊断消息中的一个错误。如果不查看更多连接池代码,很难判断,但我建议您在此处添加有关您返回的内容等的诊断信息。
  • @BartoszKP:我建议您不要将其添加到这个问题中,而是花一点时间来诊断发生了什么,并就连接提出一个 问题如有必要,池代码。虽然正如我之前提到的,如果我是你,我会放弃它:)
  • @BartoszKP:你有没有一次“打开”多个迭代器块?例如,在另一个方法中调用一个方法?如果是这样,那 可能 可以用您的 Reconnect 方法的工作方式来解释它。但实际上,我需要逐行检查才能确定。
  • @BartoszKP:您无法删除答案已获好评的问题。我不认为它会造成任何伤害。我很高兴你发现了问题:)
【解决方案2】:

TyCobbJon Skeet 猜对了,问题出在池实现和多线程上。我忘记了实际上我确实在Reconnect 方法中启动了一些微小的Tasks。第一个连接是同步创建和打开的,但所有其他连接都是异步打开的。

我的想法是,因为我一次只需要一个连接,所以其他人可以在不同的线程中重新连接。然而,因为我并不总是把连接放回去(如Jon's answer 中所解释的那样),重新连接非常频繁地发生,并且由于系统负载很大,这些重新连接线程不够快,最终导致了竞争条件。解决方法是以更简单直接的方式重新连接:

private void Reconnect()
{
    for (int i = 0; i < connections.Length; ++i)
    {
        if (!IsAvailable(this.connections[i]))
        {
            this.ReconnectAt(i);
        }
    }
}

private void ReconnectAt(int index)
{
    try
    {
        this.connections[index] = new MySqlConnection(this.connectionString);
        this.connections[index].Open();
    }
    catch (MySqlException mse)
    {
        Console.WriteLine("Reconnect error: " + mse.Message);
        this.connections[index] = null;
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-09-10
    • 2013-06-22
    • 1970-01-01
    相关资源
    最近更新 更多