如何正确管理DataContext的释放?

7
我有一个Web服务,它对数据库的访问非常频繁。在测试中它运行得很好,但是一旦我将其投入生产并增加负载,它就开始产生错误,这些错误是在某个方法调用DataContext时引发的。错误通常是以下之一:
Object reference not set to an instance of an object
Cannot access a disposed object. Object name: 'DataContext accessed after Dispose.'.
但并不总是如此。
任何单个Web服务请求都可能导致多达10或15个数据库查询和1或2个更新。
我设计了一个数据访问层,其中包含代表数据库表并保存所有业务逻辑的一堆对象。这是一个与Web GUI共享的单独项目,不同于我的Web服务。
数据访问对象派生自一个基类,该基类具有GetDataContext()方法,以便在需要时启动数据上下文的实例。
在我的所有数据访问对象中,我都写了这个:
using (db = GetDataContext())
{
    // do some stuff
}

我很高兴地为每个数据库交互创建/使用/处理我的DataContext(由sqlmetal.exe创建)对象。

经过多小时的头痛,我认为错误的原因是在负载下,datacontext对象被创建和处理得太多了,我需要改变事物,使同一个数据请求共享相同的datacontext。

我在互联网上找到了这篇文章,其中有一个DataContextFactory,似乎正是我所需要的。

然而,现在我已经实现了这一点,并将DataContext保存为HttpContext中的一个项目,当我的datacontext被多次使用时,我会得到以下错误信息:

Cannot access a disposed object.

Object name: 'DataContext accessed after Dispose.'

这是因为我的using (...) {}代码在第一次使用后就处理了我的datacontext。

所以,我的问题是... 在我遍历整个数据访问层并删除大量usings之前,什么是正确的方法?我不想通过去除usings来引起内存泄漏,但同时我希望在不同的数据访问对象之间共享我的datacontext。

我应该只删除usings,并在从Web服务请求返回之前手动调用dispose方法吗?如果是这样,那么我该如何确保捕获所有内容,考虑到我有几个try-catch块,可能会变得混乱。

是否有更好的方法来做到这一点?我应该忘记处理并希望所有东西都被隐式清除吗?

更新

问题似乎不是性能问题...请求处理非常快,不超过200毫秒。实际上,我通过生成大量的虚假请求进行了负载测试,没有任何问题。

据我所见,它与负载有关,原因有以下两种:

  • 高数量的请求会导致并发请求相互影响
  • 问题更频繁地发生只是因为有很多请求。

当问题发生时,应用程序池进入错误状态,并需要重新启动才能使其正常工作。


@abatishchev:感谢您重新标记...但是为什么要撤销我所有的错别字修正呢? - BG100
1
每个请求使用17个DataContext实例对我来说似乎不算太多。创建一个DataContext非常轻量级。它可能会提高一些性能,因为它缓存了实体,但仅此而已。您是否分析过发送到数据库的查询?在一个请求期间有多少查询被触发? - Steven
@Steven:所有的查询都是通过存储过程完成的,每个存储过程可能包含几个查询。每个请求总共执行约24个 SQL 查询,性能并不是问题,请求在大约200毫秒内完成(由日志文件时间戳测量)。问题在于当超过20个请求快速连续到达时,就会出现错误。 - BG100
你为什么认为错误“对象引用未设置为对象的实例”与数据上下文的释放有关呢?我更倾向于认为这可能与延迟加载相关实体的问题有关,或者类似的情况。 - Pleun
@Steven:这可能是一种竞态条件,但我不知道如何调试它!不,我正在为每个涉及它的数据库交互创建一个新的DataContext,并将其包含在using块中...因此我考虑将其更改为在查询之间共享DataContext...但目前为止我还没有这样做。 - BG100
显示剩余4条评论
3个回答

3

我成功地解决了这个问题...

我有一个基类,其中有一个方法会创建DataContext实例,就像这样:

public abstract class MyBase {

    protected static DataContext db = null;

    protected static DataContext GetDataContext() {
        return new DataContext("My Connection String");
    }

    // rest of class
}

然后,在继承 MyBase 的类中,我想要执行查询操作,我的语句如下:

using (db = GetDataContext()) { ... }

事实上,我想从静态方法和非静态方法中访问数据库,因此在我的基类中,我将db变量声明为静态的...这是一个大错误!
如果DataContext变量被声明为静态的,在负载繁重时,当很多事情同时发生时,DataContext会在请求之间共享,如果在DataContext上恰好发生了一些事情,它会破坏DataContext的实例,并且存储在应用程序池中的数据库连接会一直保留到下一次回收,然后数据库连接会被刷新。
因此,简单的解决方法是更改如下:
protected static DataContext db = null;

转换为:

protected DataContext db = null;

这会破坏所有静态方法中的using语句。但是,可以通过在using语句中声明DataContext变量来轻松解决此问题,如下所示:

using (DataContext db = GetDataContext()) { ... }

3
虽然我更喜欢使用 "using" 的工作单元方法,但有时它并不总是适合您的设计。理想情况下,您希望确保在完成后释放您的 "SqlConnection",以便另一个请求有机会从池中获取该连接。如果不可能,您需要确保每个请求结束后都处置了上下文。这可以通过以下几种方式来实现:
1. 如果您正在使用 WebForms,则可以将 "DataContext" 的处理绑定到页面生命周期的结尾。检查 "HttpContext.Items" 集合以确定上一页是否具有数据上下文,如果是,则处置它。 2. 创建一个专用的 "IHttpModule",它将事件附加到请求的末尾,在那里执行与上述相同的操作。
以上两种解决方案的问题是,如果您负载过重,您会发现许多请求等待连接可用,很可能超时。您必须权衡风险。
总的来说,工作单元方法仍然是首选,因为一旦不再需要资源,就会立即释放它。

0
这种情况发生在您有一个引用另一个对象的对象(例如,两个表之间的连接)并且您尝试在上下文已被处理掉后访问所引用的对象。类似于这样:
IEnumerable<Father> fathers;
using (var db = GetDataContext())
{
  // Assume a Father as a field called Sons of type IEnumerable<Son>
  fathers = db.Fathers.ToList();
}

foreach (var father in fathers)
{
  // This will get the exception you got
  Console.WriteLine(father.Sons.FirstOrDefault());
}

可以通过强制加载所有引用对象来避免这种情况:

IEnumerable<Father> fathers;
using (var db = GetDataContext())
{
  var options = new System.Data.Linq.DataLoadOptions();
  options.LoadWith<Father>(f => f.Sons);
  db.LoadOptions = options;
  fathers = db.Fathers.ToList();
}

foreach (var father in fathers)
{
  // This will no longer throw
  Console.WriteLine(father.Sons.FirstOrDefault());
}

以下是我最初对问题的想法,然而我已经检查过了,在使用块之外并没有返回自 datacontext 的对象的引用。这是一个无法在开发环境中复现的非确定性问题。如果你提供的建议是问题的原因,那么我应该可以轻松地复制这个问题。 - BG100
@BG100:确实。祝你好运。Heisenbugs总是很让人头疼的。 - ssarabando

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