使用datareader读取数百万个数据时,如何避免数据库连接丢失的问题?

时间:2019-03-07 11:54:14

标签: c# ado.net oracle-sqldeveloper

我有一个从数据库表中读取数据的类库。现在该数据库表是客户端数据库,我的应用程序仅具有连接字符串和sql查询来打开连接,执行sql查询,读取数据并执行一些操作。 此操作是什么,有点复杂(基本上是业务规则)。

现在,用户以特定格式提交sql查询,即我的类库知道从该sql查询结果中选择哪些列。

我不知道类库要处理的记录数。也可能是 100,200或数百万个数据

当前,类库正在处理90 millions of data which resides on oracle。我正在使用SQLDATAREADER读取这些数据。

现在的问题是避免内存异常,我正在使用sql数据读取器读取数据,但要1读取1千万个数据,然后对每个记录执行一些操作,连接将保持打开状态,目前我正面临连接丢失的问题:

ORA-03135: connection lost contact

1解决方案可能是分块读取数据,但是正如我所说,我不知道我可能要处理的记录数,而且SQL查询也不在我手中,因为它是由用户提交的,并由我的类库提取

我有什么办法可以避免连接问题?

更新:

public class LongRunningTask : IDisposable
{
        public void Start(DbConnection connection, string sql)
        {
            using (var cmd = connection.CreateCommand())
            {
                cmd.CommandText = sql;
                cmd.CommandTimeout = 0;
                connection.Open();
                using (var dr = cmd.ExecuteReader(CommandBehavior.CloseConnection))
                {
                    //read 1 by 1 record and pass it to algorithm to do some complex processing
                }
            }
        }
}

算法并不慢,这也不是问题。主要问题是读取部分内容,如果当前从ORACLE获得9000万数据,则读取速度慢

我已经针对SQL SERVER对1亿个数据进行了测试,尽管这个过程花费了很多时间,但我还没有遇到这个问题(尽管有时会出现传输层错误)。我仅在ORACLE中遇到了这个问题。

8 个答案:

答案 0 :(得分:4)

将数据读取器打开很多小时不是一个好主意。即使一切配置正确,线路上的某些地方也可能存在瞬态错误(例如您提到的传输层错误)。

您可以在客户端代码中添加重试逻辑,以使其更强大。一种方法是跟踪最后处理的记录,并尝试在连接失败时从该位置重新连接并“恢复”。

private const int MAX_RETRY = 10;
private const int RETRY_INTERVAL_MS = 1000;
private string lastProcessedPosition = null;

public void Start(string connectionString, string sql)
{
    var exceptions = new List<Exception>();
    for (var i = 0; i < MAX_RETRY; i++)
    {
        try
        {
            if (Process(connString, sql, lastProcessedPosition)) return;
        }
        catch(Exception ex)
        {
            exceptions.Add(ex);
        }
        System.Threading.Thread.Sleep(RETRY_INTERVAL_MS);
    }
    throw new AggregateException(exceptions);
}

您的Process()方法将重新连接并跳过已处理的行:

public bool Process(string connString, string sql, string resumeFromPosition = null)
{
    using ()// init your connection, command, reader
    {
        if (resumeFromPosition != null)
        {
            while (dr.Read() && dr.ToPositionString() != resumeFromPosition)
            {
                // skipping already processed records
            }
        }
        while (dr.Read)
        {
            // Do your complex processing

            // You can do this every N records if accuracy is not critical
            lastProcessedPosition = dr.ToPositionString();
        }
    }
    return true;
}

dr.ToPositionString()是您创建的扩展方法,用于根据表模式使行唯一。

答案 1 :(得分:3)

此解决方案是我过去用来从数据库中读取大数据集,但以大块方式处理它们的方法:

首先,我选择实现一种获取数据库连接的方法。请注意,我将ConnectionTimeout设置为0,因为我知道此过程将长期运行。

private static OracleConnection GetConnection()
{
    return new OracleConnection(new OracleConnectionStringBuilder
    {
        //TODO: Set other connection string properties
        ConnectionTimeout = 0
    }.ConnectionString);
}

接下来,我想使用一些通用的“ GetData”方法从数据库中读取数据。请注意,其返回类型明确为“ IEnumerable”。您可以强烈地键入它而不是使其具有通用性,但是它需要保持返回IEnumerable以便利用“收益率返回”。

还要注意,我已经将CommandTimeout设置为0,因为我知道此过程将长期运行。

public static IEnumerable<T> GetData<T>(string sql)
{
    using (var conn = GetConnection())
    {
        if (ConnectionState.Closed == conn.State) conn.Open();

        using (var cmd = conn.CreateCommand())
        {
            cmd.CommandTimeout = 0;
            cmd.CommandType = CommandType.Text;
            cmd.CommandText = sql; //TODO: Make sure you do standard sql injection prevention

            using (var reader = cmd.ExecuteReader())
            {
                //We want to optimize the number of round trips to the DB our reader makes.
                //Setting the FetchSize this way will make the reader bring back 5000 records
                //with every trip to the DB
                reader.FetchSize = reader.RowSize * 5000;

                while (reader.Read())
                {
                    var values = new object[reader.FieldCount];
                    reader.GetValues(values);
                    //This assumes that type T has a constructor that takes in an object[]
                    //and the mappings of object[] to properties is done in that constructor
                    yield return (T)Activator.CreateInstance(typeof(T), new object[] { values });
                }
            }
        }
    }
}

接下来,我想拥有一些实现幻想业务逻辑/算法的方法:

public static void ProcessBusinessLogic<T>(IEnumerable<T> data)
{
    //TODO Implement fancy business logic here
}

最后,我需要一个包装这两种方法并一起使用的方法。我还需要一种方法来确保以“块”形式处理记录,以便不要尝试将数百万条记录加载到内存中并使系统崩溃。

为了处理块中的数据,我将来自MoreLinq nuget库的类用于MoreEnumerable.Batch。这将使我可以将来自GetData方法的结果“分块”为大小更合适的“批次”。利用这一点,我将能够确保不会将比我设置为批处理大小的记录更多的记录加载到内存中。

public static void Main(string[] args)
{
    foreach (var batch in GetData<string>("hello world").Batch(50000))
    {
        ProcessBusinessLogic(batch);
    }
}

因此,要将整个内容放在一起,此测试应用程序具有2个类:

using System;
using System.Collections.Generic;
using System.Data;
using MoreLinq;
using Oracle.ManagedDataAccess.Client;

namespace ReadLargeDataset
{
    public class Program
    {
        public static void Main(string[] args)
        {
            foreach (var batch in GetData<string>("hello world").Batch(50000))
            {
                ProcessBusinessLogic(batch);
            }
        }

        public static void ProcessBusinessLogic<T>(IEnumerable<T> data)
        {
            //TODO Implement fancy business logic here
        }

        public static IEnumerable<T> GetData<T>(string sql)
        {
            using (var conn = GetConnection())
            {
                if (ConnectionState.Closed == conn.State) conn.Open();

                using (var cmd = conn.CreateCommand())
                {
                    cmd.CommandTimeout = 0;
                    cmd.CommandType = CommandType.Text;
                    cmd.CommandText = sql; //TODO: Make sure you do standard sql injection prevention

                    using (var reader = cmd.ExecuteReader())
                    {
                        //We want to optimize the number of round trips to the DB our reader makes.
                        //Setting the FetchSize this way will make the reader bring back 5000 records
                        //with every trip to the DB
                        reader.FetchSize = reader.RowSize * 5000;

                        while (reader.Read())
                        {
                            var values = new object[reader.FieldCount];
                            reader.GetValues(values);
                            //This assumes that type T has a constructor that takes in an object[]
                            //and the mappings of object[] to properties is done in that constructor
                            yield return (T)Activator.CreateInstance(typeof(T), new object[] { values });
                        }
                    }
                }
            }
        }

        private static OracleConnection GetConnection()
        {
            return new OracleConnection(new OracleConnectionStringBuilder
            {
                //TODO: Set other connection string properties
                ConnectionTimeout = 0
            }.ConnectionString);
        }
    }
}

并且:

#region License and Terms
// MoreLINQ - Extensions to LINQ to Objects
// Copyright (c) 2009 Atif Aziz. All rights reserved.
// 
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// 
//     http://www.apache.org/licenses/LICENSE-2.0
// 
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#endregion

// ReSharper disable CheckNamespace
namespace MoreLinq
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;

    public static class MoreEnumerable
    {
        /// <summary>
        /// Batches the source sequence into sized buckets.
        /// </summary>
        /// <typeparam name="TSource">Type of elements in <paramref name="source"/> sequence.</typeparam>
        /// <param name="source">The source sequence.</param>
        /// <param name="size">Size of buckets.</param>
        /// <returns>A sequence of equally sized buckets containing elements of the source collection.</returns>
        /// <remarks> This operator uses deferred execution and streams its results (buckets and bucket content).</remarks>

        public static IEnumerable<IEnumerable<TSource>> Batch<TSource>(this IEnumerable<TSource> source, int size)
        {
            return Batch(source, size, x => x);
        }

        /// <summary>
        /// Batches the source sequence into sized buckets and applies a projection to each bucket.
        /// </summary>
        /// <typeparam name="TSource">Type of elements in <paramref name="source"/> sequence.</typeparam>
        /// <typeparam name="TResult">Type of result returned by <paramref name="resultSelector"/>.</typeparam>
        /// <param name="source">The source sequence.</param>
        /// <param name="size">Size of buckets.</param>
        /// <param name="resultSelector">The projection to apply to each bucket.</param>
        /// <returns>A sequence of projections on equally sized buckets containing elements of the source collection.</returns>
        /// <remarks> This operator uses deferred execution and streams its results (buckets and bucket content).</remarks>

        public static IEnumerable<TResult> Batch<TSource, TResult>(this IEnumerable<TSource> source, int size,
            Func<IEnumerable<TSource>, TResult> resultSelector)
        {
            if (source == null) throw new ArgumentNullException(nameof(source));
            if (size <= 0) throw new ArgumentOutOfRangeException(nameof(size));
            if (resultSelector == null) throw new ArgumentNullException(nameof(resultSelector));
            return BatchImpl(source, size, resultSelector);
        }

        private static IEnumerable<TResult> BatchImpl<TSource, TResult>(this IEnumerable<TSource> source, int size,
            Func<IEnumerable<TSource>, TResult> resultSelector)
        {
            Debug.Assert(source != null);
            Debug.Assert(size > 0);
            Debug.Assert(resultSelector != null);

            TSource[] bucket = null;
            var count = 0;

            foreach (var item in source)
            {
                if (bucket == null)
                {
                    bucket = new TSource[size];
                }

                bucket[count++] = item;

                // The bucket is fully buffered before it's yielded
                if (count != size)
                {
                    continue;
                }

                // Select is necessary so bucket contents are streamed too
                yield return resultSelector(bucket.Select(x => x));

                bucket = null;
                count = 0;
            }

            // Return the last bucket with all remaining elements
            if (bucket != null && count > 0)
            {
                yield return resultSelector(bucket.Take(count));
            }
        }
    }
}

答案 2 :(得分:1)

简短答案:

我以前曾经遇到过这个问题,这是因为公司网络中存在防火墙规则。

长答案和不请自来的建议

我认为您的主要问题是应用程序设计。如果您要处理数百万条记录,则可能需要很长时间……根据您的工作需要很长的时间。
我开发了一个用于加密数据库中1亿张静态卡号的应用程序,此过程花了3周的时间。处理真正的大数据非常棘手。我遇到了各种各样的问题。 这是我的一些建议。

1)您将听到问题在于超时设置。可能不是。在我工作的地方,我们有防火墙规则,该规则会在一段时间(我不记得15或30分钟)后终止数据库连接,并且花了我们数周的时间才弄清楚为什么我们的连接会断开。

2)一次撤回数百万个记录并不是一个好主意。

3)您应该在代码中添加一些SQL注入防护。

4)我建议使用像实体框架这样的ORM,这样可以使循环和分块更加容易。

答案 3 :(得分:1)

无法获取所有数据并将所有数据保存在某个内存对象中,然后释放与DB的连接;完成该流程后,您便会处理复杂的subiness规则,然后需要将该数据更新回数据库,再次打开连接并进行批量更新。

希望我有道理。

答案 4 :(得分:1)

无论连接状态如何,我都绝不提倡将那么多数据存储在内存中。一方面,我不确定一个实体有多大?在创建列时使用哪种数据类型。

请确保以下内容:

  1. 您真的需要整个实体来执行复杂的逻辑来执行(即运行业务规则)吗?除此之外,单个整个实体中还存在多少列?

  2. 是否可以仅获取所需的数据? (例如,仅根据您需要在其中映射或执行业务规则的列?除非确定每列都参与,否则请避免加载整个数据库行。)

  3. 是否可以将这种复杂的逻辑直接关联到数据库记录?如果是这样,请将这些规则转移到数据库中,并使用存储过程在内存中执行和计算这些规则。

如果我要在你家,我会把事情混在一起。问题的某些部分将保留在数据库中,在我认为有必要在内存中执行操作的地方,我将在那里执行操作。

发布一些具体而具体的逻辑和数据实体,以便我们可以进一步分析问题。

答案 5 :(得分:1)

看起来您遇到的情况是 到Oracle的连接很粗略,或者有某种策略会在某种超时后终止您的进程(而不是在您这边-在服务器上)。 SQL Server和Oracle都具有查询调控器的概念,可以用不同的方式对其进行配置。

有一些策略可以克服这些障碍...它们都围绕着批处理查询。问题是如何确保从上次停下来的地方重新接机。

Oracle和SQL Server都在查询中使用offsetfetch first n的想法。专门用于进行分页查询,这可能会对您有帮助。

基本设置为:

select 
  columns... 
from 
  data sources...
where 
  some conditions...
offset @offset
fetch first @pageSize rows

..然后您可以安排进程重复运行选择,并随即传递@offset和@pageSize的新值。

如果查询有所不同,但是它是常规的选择语句类型,则可以在处理代码中将offsetfetch子句附加到查询中。

面对实际连接问题,您必须使过程更加健壮,但这很简单。

您还希望保持连接时间尽可能短。您要执行此操作的原因是大多数数据库都支持连接池,因此如果让轮询器对其进行回收,则创建连接便宜且容易。另外,DBA之所以在他们的查询上使用调控器是因为开发人员永远在做cmd.CommandTimeout = 0;

之类的事情。
public class LongRunningTask
{
  const long pageSize = 100000L; //--> ...or whatever the market will bear
  const int retryLimit = 3;
  public void Start( ConnectionFactory factory, string sql )
  {
    var done = false;
    var page = 0L;
    var index = 0L;
    var retries = 0;
    var retrying = false;
    while ( !done )
    {
      try
      {
        using ( var connection = factory.CreateConnection( ) )
        {
          using ( var cmd = connection.CreateCommand( ) )
          {
            cmd.CommandType = CommandType.Text;
            cmd.CommandText = sql;
            cmd.Parameters.Add( factory.CreateParameter( "@pageSize", SqlDbType.BigInt ) );
            cmd.Parameters.Add( factory.CreateParameter( "@offset", SqlDbType.BigInt ) );
            cmd.Parameters[ "@pageSize" ].Value = pageSize - ( retrying ? index : 0 );
            cmd.Parameters[ "@offset" ].Value = page + ( retrying ? index : 0 );
            connection.Open( );
            using ( var dr = cmd.ExecuteReader( ) )
            {
              index = retrying ? index : 0;
              retrying = false;
              done = !dr.HasRows; //--> didn't get anything, we're done!
              while ( dr.Read( ) )
              {
                //read 1 by 1 record and pass it to algorithm to do some complex processing
                index++;
              }
            }
          }
        }
        page++;
      }
      catch ( Exception ex )
      {
        Console.WriteLine( ex );
        if ( retryLimit < retries++ ) throw;
        retrying = true;
      }
    }
  }
}

public  class ConnectionFactory
{
  public DbConnection CreateConnection( )
  {
    return //... a DbConnection
  }
  public DbParameter CreateParameter( string parameterName, SqlDbType type, int length = 0 )
  {
    return //... a DbParameter
  }
}

答案 6 :(得分:0)

您可以这样设置连接超时限制:

command.CommandTimeout = 60; //The time in seconds to wait for the command to execute. The default is 30 seconds.

答案 7 :(得分:0)

我正在阅读这篇文章,试图找到另一个问题的答案。你有一个有趣的问题。

如果您不显示正在检索的数据并且对某些最终结果感兴趣,那么最好的方法是编写 Oracle 服务器端 PL/SQL 并在 PL/SQL 包中实现您的逻辑。< /p>

我之前用 PL/SQL 编写了服务器端代码,其中包含复杂的业务逻辑并生成不同级别的摘要信息。然后客户端程序将只读取生成的集合。