【问题标题】:Prevent cached objects to end up in the database with Entity Framework使用 Entity Framework 防止缓存对象最终进入数据库
【发布时间】:2014-10-09 14:51:08
【问题描述】:

我们有一个带有实体框架和 SQL Azure 的 ASP.NET 项目。

我们的大部分数据每天只需要更新几次,其他数据非常不稳定。

  • 几乎没有变化的数据我们在启动时缓存在内存中,从上下文中分离出来,而不是主要用于读取,从而大大减少了我们必须执行的数据库请求量。
  • 每次 Http 请求时,DbContext 都会请求易失性数据。
  • 当我们对缓存数据进行更新时,我们会向所有实例发送一条消息,以从 SQL 服务器捕获所有数据的新版本。

到目前为止,一切都很好。

直到我们引入了一个错误,将这些“缓存”对象之一链接到“易失性”数据,并执行了 SaveChanges。

好吧,那真是一团糟。

每次更新都会再次再次添加整个数据树,从而用大量重复数据破坏整个数据库。

作为一个完整的 hack,我在其中一个根表上添加了一个带有 UniqueConstraint 和一些乱码数据的完全任意列;希望下次我们引入这样的错误时,SaveChanges() 会失败,因为它会违反唯一约束。

但它当然是 hacky,我还是很害怕 ;P 有没有更好的方法来防止整个树的缓存对象最终进入数据库?

更多信息

  • 项目是 ASP.NET MVC
  • 我缓存了这些数据,因为它主要是只读的,这样可以为每个 http 请求节省大量额外的数据库调用
  • 这是一个高流量的网站,有很多个人定制的观点。将 POCO 数据保存在内存中非常适合我想要的东西。除了我提到的问题。
  • 这有点复杂,但一个简化的版本是我通过单例缓存对象:所以即:

EntityCache.Instance.LolCats = new DbContext().LolCats.AsNoTracking().ToList();

我将此缓存依赖注入到我的控制器中。

【问题讨论】:

  • 是的,设置外键值而不是对象。
  • 可能并不总是可行,因为某些业务逻辑可能依赖于 Navigation 属性。
  • 你在哪里缓存这些数据?你的项目是 ASP.NET WebForms 还是 ASP.NET MVC?
  • 为什么需要脱离上下文?
  • 我正在缓存这些数据,因为我几乎每个 Http 请求都需要它,但主要是静态数据(每天只更改一次)。我正在使用 ASP.NET MVC。

标签: c# sql entity-framework caching


【解决方案1】:

你可以这样解决:

1) 创建这样的接口:

public interface IIsReadOnly
{
    bool IsReadOnly { get; set; }
}

2) 在所有可以缓存的实体中实现这个接口。当您读取和缓存它们时,将IsReadOnly 属性设置为true。调用SaveChanges 时将使用此标志。请记住使用[NotMapped] 属性来装饰此属性,或使用任何其他方式使EF 忽略它。

public class ACacheableEntitySample
   : IIsReadOnly
{
    [NotMapped]
    public bool IsReadOnly { get; set; }

    // define the "regular" entity properties
}

注意:您可以将属性直接包含在类定义中(如果使用 Code First),或使用部分类(对于 Db First、Model First 或 Code First)。

注意:或者,您可以使用 Fluent API 让 EF 忽略 IsReadOnly 属性,甚至更好的 a custom convention (EF 6+)

3) 覆盖您继承的DbContext.SaveChanges 方法。在覆盖的方法中,review all the entries with pending changes,如果它们是只读的,则将那里的状态更改为Unchanged

if (entry is IIsReadOnly) // if it's a cacheable entity
{
    if (entry.IsReadOnly) // and it was marked as readonly when caching
    {
         // change the entry state to unchanged here, so that it's not updated
    }
}

注意:这是解释您需要做什么的示例代码。在您的最终实现中,您可以使用一个简单的 LINQ 语句来获取所有 IsReadOnly 设置为 true 的 IIsReadOnly 实体,并将它们的状态设置为 Unchanged

您可以在另一个DbContext 中使用IIsReadOnly 实体并以通常的方式操作它们。例如,如果您获得这些实体之一,对其进行更新并调用SaveChanges,则更改将被保存,因为IsReadOnly 将具有默认的false 值。但是您可以轻松避免意外保存对缓存数据的更改,只需在缓存时将IsReadOnly 属性设置为true。

【讨论】:

  • 绝妙的解决方案!你是为了我的问题才解决这个问题的,还是在你的应用程序中使用了类似的缓存机制?
  • 只是为了你的问题。我已经为其他目的重载了SaveChanges。不要忘记让你的单例线程安全:即锁定读取,直到缓存准备好。您可以使用任何可用的线程安全集合来避免自己做(并犯一些错误!)。你可以找到很多关于这个集合的信息,例如:msdn.microsoft.com/en-us/library/dd997305(v=vs.110).aspx
【解决方案2】:

原来的答案被删除了,因为这是浪费时间。

您的帖子和进行中的 cmets 是 the XY Problem 的完美示例。

你说:

我真的需要一个解决问题的方法,而不是架构

如果架构问题怎么办?


你提出的问题

您实施的违反至少六种最佳实践的缓存解决方案(令人惊讶!)在您的脸上炸开了锅。您已经设法通过壮观的(不是以一种好的方式)hack 来阻止它再次爆炸,但您想知道如何以不需要如此壮观的 hack 的方式来做到这一点。

你遇到的问题

您需要缓存一些数据,因为每次请求都访问数据库的成本太高。


提供的答案

使用外键代替导航属性

这是一个完全有效的答案,而且令人惊讶的是,这是一个最佳实践。导航属性可以在您重新生成实体数据模型中的代码时随时更改,并且通常是模棱两可的。稍加努力,您就可以使用它,而不必再担心 EF 对对象关系的处理。

缓存模型而不是实体对象

另一个有效的答案,并且需要最少的实际工作量。 MVC 应用程序通常需要在视图模型和实体对象之间进行一些冗余,如果您编写过适当的多层应用程序,您实际上会淹没在冗余对象中。并且没有人会意外地将这些对象再次添加到 DbContext 中——因为它们不能。

批评

您提供的有用信息很少。据我所知,您从一开始就采取的方法是错误的。

首先,在 App_Start 将整个表转储到内存中充其量只是一个临时解决方案。如果表太大而无法满足每个请求,那么它太大而无法满足 App_Start。如果在人们使用您的应用程序时出现重要问题并且您需要尽快部署错误修复,会发生什么情况?当您的表变得真正很大并且您在尝试将它们转储到内存时开始从 EF 获得超时时会发生什么?如果你的 95% 的用户真的只需要你转储到内存中的那个大表的 10%,会发生什么?您的 Web/缓存服务器上的内存是否足以容纳不断增加的表大小?多长时间?

其次,实体对象在其原始 DbContext 被释放后不应保留在任何地方。实体对象在其 DbContext 处于范围内时以一种方便的方式运行,而在超出范围时成为麻烦的 POCO。我说很麻烦,因为 DbContext 对更改跟踪所做的“魔术”往往会使不熟悉 EF 内部工作的人误以为实体对象直接连接到数据库中的表行。您遇到的问题完美地说明了这一点。

第三,您似乎需要删除整个表并将其重新转储到内存中,即使您只更新单行中的单个列也是如此。这对 Web 服务器上的内存和 CPU 以及 Azure SQL 实例都是极大的浪费。当少量数据出现错误并需要快速更新时会发生什么?如果您的一项夜间更新作业失败但您需要在早上获得新数据怎么办?

您现在可能不会担心这些事情,但您的解决方案在您面前爆炸应该至少会引发一些危险信号。在过去几年我从事的项目中,我不得不处理尽可能多的缓存,我在这里所说的一切都来自经验。

建议的解决方案 - 按需缓存

如果您在组织代码方面付出了一些努力,那么您对数据库的所有 CRUD 操作都应该在专门的帮助类中,我称之为存储库。您的控制器调用其专用存储库(StuffController - StuffRepository),接收模型并将该模型绑定到视图,有点像这样:

public class StuffController : Controller
{
    private MyDbContext _db;
    private StuffRepository _repo;

    public StuffController()
    {
        _db = new MyDbContext();
        _repo = new StuffRepository(_db);
    }

    // ...

    public ActionResult Details(int id)
    {
        var model = _repo.ReadDetails(id);
        // ...
        return View(model);
    }

    protected override void Dispose(bool disposing)
    {
        _db.Dispose();

        base.Dispose(disposing);
    }
}

按需缓存的作用是以这样一种方式包装对存储库的调用,如果该方法的结果已经在缓存中并且它不是陈旧的,它将从缓存中返回它。否则它会命中数据库。

这是一个简化的(可能是非功能性的)CacheWrapper 类示例,以便您了解它的作用,使用 HttpRuntime.Cache:

public static class CacheWrapper
{
    private static List<string> _keys = new List<string>();
    public static List<string> Keys
    {
        get { lock(_keys) { return _keys.ToList(); } }
    }

    public static T Fetch<T>(string key, Func<T> dlgt, bool refresh = false) where T : class
    {
        var result = HttpRuntime.Cache.Get(key) as T;

        if(result != null && !refresh) return result;

        lock(HttpRuntime.Cache)
        {
            lock(_keys)
            {
                _keys.Add(key);
            }

            result = dlgt();

            HttpRuntime.Cache.Add(key, result, /* some other params */);
        }

        return result;
    }
}

以及从控制器调用事物的新方法:

public ActionResult Details(int id)
{
    var model = CacheWrapper.Fetch("StuffDetails_" + id, () => _repo.ReadDetails(id));
    // ...
    return View(model);
}

正如我们所说,它的一个稍微复杂一点的版本正在公共 Web 应用程序上进行生产,并且运行良好。

【讨论】:

  • 嗨@RaduPorumb,每次请求都无法再次获取所有数据。我们谈论的那部分数据几乎是静态的:我们谈论的是每天更新。对于每个请求,无论从哪种存储中反序列化所有这些数据,都会浪费大量性能。
  • 你指的是我的 PS 还是帖子的其余部分?
  • 两者 :) (ps。感谢您的扩展回答,但我真的需要解决问题的方法,而不是架构)
  • @DirkBoer 你在说什么反序列化?谁说每次请求都必须重新获得它?如果您不理解我的帖子的任何部分,请向我询问。与其假设错误的事情,我更愿意你让我澄清一下,这样我才能找到你问题的根源。如果这能让事情更清楚,我会非常乐意提供代码示例。
  • 啊抱歉:我非常关注这部分:'...任何与 EF 相关的超过一个请求的生命周期都是自找麻烦。 .但是您正在谈论更改课程本身。在某种程度上,分离的 POCO 实体与任何 DbContext 无关。我理解你的方法,但这意味着为我所有的“缓存”实体复制类。而且您甚至需要以一种或另一种方式复制逻辑,因为它们不能相互继承。
猜你喜欢
  • 2011-03-31
  • 2015-04-30
  • 1970-01-01
  • 1970-01-01
  • 2022-06-14
  • 2019-05-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多