缓存LINQ-SQL对象和DataContext线程安全性

4
我们正在使用LINQ-SQL查询数据库,然后将结果存储在HTTP缓存中的主表对象。稍后,主对象将用于查询其子项,使用延迟加载。以下是相关的代码片段 - 我已经在一个新的概念验证应用程序中重新创建了该场景:
        if (HttpRuntime.Cache["c"] == null)
        {
            LockApp.Models.DBDataContext db = new Models.DBDataContext();

            var master = db.Masters.ToList();
            HttpRuntime.Cache.Add("c", master,
                    null, DateTime.Now.AddMonths(1), 
                    TimeSpan.Zero, CacheItemPriority.Normal, null);

        }

        ViewBag.Data = (List<LockApp.Models.Master>)HttpRuntime.Cache["c"];

这里是遍历主对象和详细对象的 Razor 视图:

    @foreach(var m in ViewBag.Data){
        @m.Id<nbsp></nbsp>
        foreach(var d in m.Details){
            @d.Id<nbsp></nbsp>
        }
        <br />
    }

它的功能非常完美,可以正确地缓存数据。然而,在清除缓存后有多个请求尝试访问网站时,它会失败 - 我正在使用JMeter进行测试,基本上是使用许多(50)并行线程击中网站,然后触摸web.config - 我立即开始看到以下两个错误之一:

索引超出了数组范围

在 foreach(var d in m.Details) 中出现此错误

这个错误永远不会消失,即缓存中的某些数据被损坏了

具有以下堆栈:

  System.Collections.Generic.List`1.Add(T item) +34
   System.Data.Linq.SqlClient.SqlConnectionManager.UseConnection(IConnectionUser user) +305
   System.Data.Linq.SqlClient.SqlProvider.Execute(Expression query, QueryInfo queryInfo, IObjectReaderFactory factory, Object[] parentArgs, Object[] userArgs, ICompiledSubQuery[] subQueries, Object lastResult) +59
   System.Data.Linq.SqlClient.SqlProvider.ExecuteAll(Expression query, QueryInfo[] queryInfos, IObjectReaderFactory factory, Object[] userArguments, ICompiledSubQuery[] subQueries) +118
   System.Data.Linq.SqlClient.CompiledQuery.Execute(IProvider provider, Object[] arguments) +99
   System.Data.Linq.DeferredSourceFactory`1.ExecuteKeyQuery(Object[] keyValues) +402
   System.Data.Linq.DeferredSourceFactory`1.Execute(Object instance) +888
   System.Data.Linq.DeferredSource.GetEnumerator() +51
   System.Data.Linq.EntitySet`1.Load() +107
   System.Data.Linq.EntitySet`1.GetEnumerator() +13
   System.Data.Linq.EntitySet`1.System.Collections.IEnumerable.GetEnumerator() +4
   ASP._Page_Views_Home_Index_cshtml.Execute() in c:\Users\prc0092\Documents\Visual Studio 2012\Projects\LockApp\LockApp\Views\Home\Index.cshtml:16

或者这个错误

ExecuteReader要求打开并可用的连接。连接的当前状态为打开。

在同一行foreach(var d in m.Details)

如果我停止使用并行请求访问该网站,此错误会在一段时间后消失

以下是堆栈跟踪:

   System.Data.SqlClient.SqlConnection.GetOpenConnection(String method) +5316460
   System.Data.SqlClient.SqlConnection.ValidateConnectionForExecute(String method, SqlCommand command) +7
   System.Data.SqlClient.SqlCommand.ValidateCommand(String method, Boolean async) +155
   System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource`1 completion, Int32 timeout, Task&amp; task, Boolean asyncWrite) +82
   System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method) +53
   System.Data.SqlClient.SqlCommand.ExecuteReader(CommandBehavior behavior, String method) +134
   System.Data.SqlClient.SqlCommand.ExecuteDbDataReader(CommandBehavior behavior) +41
   System.Data.Common.DbCommand.ExecuteReader() +12
   System.Data.Linq.SqlClient.SqlProvider.Execute(Expression query, QueryInfo queryInfo, IObjectReaderFactory factory, Object[] parentArgs, Object[] userArgs, ICompiledSubQuery[] subQueries, Object lastResult) +1306
   System.Data.Linq.SqlClient.SqlProvider.ExecuteAll(Expression query, QueryInfo[] queryInfos, IObjectReaderFactory factory, Object[] userArguments, ICompiledSubQuery[] subQueries) +118
   System.Data.Linq.SqlClient.CompiledQuery.Execute(IProvider provider, Object[] arguments) +99
   System.Data.Linq.DeferredSourceFactory`1.ExecuteKeyQuery(Object[] keyValues) +402
   System.Data.Linq.DeferredSourceFactory`1.Execute(Object instance) +888
   System.Data.Linq.DeferredSource.GetEnumerator() +51
   System.Data.Linq.EntitySet`1.Load() +107
   System.Data.Linq.EntitySet`1.GetEnumerator() +13
   System.Data.Linq.EntitySet`1.System.Collections.IEnumerable.GetEnumerator() +4
   ASP._Page_Views_Home_Index_cshtml.Execute() in c:\Users\prc0092\Documents\Visual Studio 2012\Projects\LockApp\LockApp\Views\Home\Index.cshtml:16

我尝试过的不同方法

双重锁定

没有帮助。

    private static object ThisLock = new object();

    public ActionResult Index()
    {

        if (HttpRuntime.Cache["c"] == null)
        {
            lock (ThisLock)
            {
                if (HttpRuntime.Cache["c"] == null)
                {

预加载子数据

虽然有效,但需要不断维护,因为并非所有子数据都应该预加载。请参阅下一个注释。

DataLoadOptions dlo = new DataLoadOptions();
dlo.LoadWith<Master>(b => b.Details);
db.LoadOptions = dlo;

在尝试访问子对象时锁定主对象

同样需要维护,因为需要找到所有子对象被访问的初始位置 - 我们正在努力解决这个问题,因为进入网站的入口路径不同。

    @foreach(var m in ViewBag.Data){
        @m.Id<nbsp></nbsp>
        lock (m){
            foreach(var d in m.Details){
                @d.Id<nbsp></nbsp>
            }
        }
        <br />
    }

切换到Entity Framework

在某些并行请求(核心i7上的50+)下,它仍然存在“打开连接”的问题,但比Linq-SQL好得多 - 正如我之前提到的那样,它会在一段时间后消失,而且我还没有看到数据损坏。

我们可能会完全切换到EF,因为这似乎是唯一可行的路径 - 假设不出现数据损坏 - 这需要在我的实际项目中进行测试。

虽然EF数据上下文也不是线程安全的,而且我认为EF数据对象会随着它们的上下文一起运行。 这可能是我尚未回答的唯一问题。

破解原因的理论

看起来将Linq-SQL对象存储在http缓存中会携带数据上下文。当该上下文稍后被多个线程用于访问子项时,会出现某种类型的并发问题,这表现为子对象的临时连接问题或完全数据损坏。由于无法断开/重新连接上下文与Linq对象,因此看起来唯一的建议是不要缓存需要延迟加载其子项的Linq对象 - 我所做的大量谷歌搜索似乎没有给出这个建议,事实上有时相反。

我已上传了完整的项目(适用于Visual Studio 2012和SQL Server 2012)

https://docs.google.com/file/d/0B8CQRA9dD8POb3U5RGtCV3BMeU0/edit?usp=sharing 以及一个简单的JMeter脚本,将使用并行请求击中您的本地机器: https://docs.google.com/file/d/0B8CQRA9dD8POd1VYdGRDMEFQbEU/edit?usp=sharing

要进行测试,请启动站点并运行测试 - 然后触摸站点上的web.config


你是否在静态函数中执行这些操作?我正在使用实体框架和lambda/linq进行工作,采用单例设计模式(对象只被实例化一次,并保留在内存中直到应用程序结束),但是我遇到了一些并发问题。仅供参考。 - Saturn K
正如我所提到的,这是在应用程序缓存中缓存的,因此实际上与静态相同(Application[]是单例)。 - Artemiy
1个回答

0
LockApp.Models.DBDataContext db = new Models.DBDataContext();

var master = db.Masters.ToList();

在这两个调用之间,您应该调用db.ObjectTrackingEnabled = false。否则,数据上下文将跟踪所有对象,以便可以将更改写回数据库。由于您正在缓存这些对象以供多个线程读取,因此不希望出现这种情况。(即使在单线程情况下跟踪您不会更改的对象也更加昂贵,因此值得在其他地方进行)。

此外,使用LoadWith来急切地加载您可能想要访问的任何属性,以便它们都在初始缓存线程上加载,而不是在(可能是多个)尝试访问它们的线程上加载。


正如我所提到的,由于需要维护站点入口路径,因此LoadWith不可行。但是你认为ObjectTracking会对此有影响吗?我认为在新的.NET世界中,我们必须通过阅读LINQ源代码来查看这个问题。 - Artemiy
对象跟踪肯定会对其产生影响,但是禁用它是否足以帮助解决问题还有待考虑。如果 LoadWith 不可行,则确保不直接访问相关属性,而是从新查询中进行访问。否则,您将把您想要“死亡”的缓存对象视为数据库的“活动”反映,而它们只是缓存记录。 - Jon Hanna

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