已经有一个开放的DataReader ......即使它没有

时间:2014-08-04 22:05:23

标签: c# mysql .net database datareader

注意:当问题没有正确处理读取器/连接时,或者错误是由于延迟处理不当导致错误时,我已经经历了数百万个问题。我认为这个问题与众不同,可能与MySQL的.NET连接器有关。

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

今天,经过数小时处理以下代码:

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:已经有一个与此Connection关联的打开DataReader,必须先关闭它。

以下是堆栈跟踪的相关部分:

  

at MySql.Data.MySqlClient.ExceptionInterceptor.Throw(异常异常)      at MySql.Data.MySqlClient.MySqlConnection.Throw(Exception ex)      at MySql.Data.MySqlClient.MySqlCommand.CheckState()      at MySql.Data.MySqlClient.MySqlCommand.ExecuteReader(CommandBehavior behavior)      at MySql.Data.MySqlClient.MySqlCommand.ExecuteReader()      at implementation.VectorTransitionsMySqlTable.d__27.MoveNext()in C:\ Users \ bartoszp ... \ implementation \ VectorTransitionsMySqlTable.cs:line 201

     

在System.Linq.Enumerable.d__3a 1.MoveNext() at System.Linq.Buffer 1..ctor(IEnumerable 1 source) at System.Linq.Enumerable.ToArray[TSource](IEnumerable 1来源)      在C:\ Users \ bartoszp ... \ implementation \ VectorTransitionService.cs中的implementation.VectorTransitionService.Add(VectorTransition vectorTransition):第38行

     

在Program.Go [T](环境`2 p,空间parentSpace,EpsilonEstimator epsilonEstimator,ThresholdEstimator thresholdEstimator,TransitionTransformer transitionTransformer,AmbiguityCalculator ac,VectorTransitionsTableFactory vttf,AxesTableFactory atf,NeighbourhoodsTableFactory ntf,AmbiguitySamplesTableFactory astf,AmbiguitySampleMatchesTableFactory asmtf,MySqlConnectionPool connectionPool,Boolean 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:第38行部分。

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

enter image description here

为什么会发生这种情况(显然是在&#34;随机&#34; - 这种方法几乎每分钟被调用一次,直到最后~20h)?我是否可以避免这种情况(以其他方式猜测 - 在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();
    }
}

2 个答案:

答案 0 :(得分:3)

我怀疑这是问题,在方法的最后:

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以模拟读者只有一个值的情况,例如:

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

然后我们一直通过while循环,因此输出会发生变化:

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

我建议你复制我的程序并进行实验。确保您了解所发生的事情......然后您可以将其应用到您自己的代码中。您可能也想在iterator block implementation details上阅读我的文章。

答案 1 :(得分:0)

TyCobbJon Skeet已经正确猜到,问题是池实现和多线程。我忘了实际上我确实在Task方法中开了一些小Reconnect个。第一个连接是同步创建和打开的,但所有其他连接都是异步打开的。

这个想法是因为我只需要一次连接,其他人可以在不同的线程中重新连接。但是,因为我并不总是把连接放回去(如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;
    }
}