使用Entity Framework防止缓存对象最终进入数据库

时间:2014-08-15 20:25:33

标签: c# sql entity-framework caching

我们有一个包含Entity Framework和SQL Azure的ASP.NET项目。

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

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

到目前为止,这么好。

在我们引入一个链接其中一个“缓存”的错误之前反对' volatile'数据,并做了SaveChanges。

嗯,那真是一团糟。

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

作为一个完整的黑客,我添加了一个完全任意的列,其中包含UniqueConstraint和一个根表上的一些乱码数据;希望在下次我们引入这样的bug时失败SaveChanges(),因为它会违反Unique Constraint。

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

更多信息

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

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

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

2 个答案:

答案 0 :(得分:1)

删除原始答案,因为这是浪费时间。

您的帖子和正在进行的评论是the XY Problem的完美示例。

你说:

  

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

如果架构 问题怎么办?


您提出的问题

您实施的违反至少六种最佳做法的缓存解决方案(惊喜!)在您脸上爆炸。你已经设法阻止它通过一个壮观的(不是很好的方式)黑客再次吹响,但你想要知道如何以一种不会需要如此壮观的黑客的方式来做到这一点。

你遇到的问题

您需要缓存一些数据,因为每次请求都要花费太多资金来访问数据库。


提供的答案

使用外键而不是导航属性

这是一个非常有效的答案,而且是一个最佳实践。导航属性可以在您重新生成实体数据模型中的代码时随时更改,并且通常不明确。通过一些努力,您可以使用它,并且永远不必担心EF再次处理对象关系。

缓存模型而不是实体对象

另一个有效的答案,以及需要最少量实际工作的答案。 MVC应用程序通常需要在视图模型和实体对象之间存在一些冗余,如果您编写了正确的多层应用程序,您实际上会淹没在冗余对象中。并且没有人会不小心将这些对象再次添加到DbContext中 - 因为他们不能。

批评

您提供的信息非常少。从我可以告诉你从一开始你的方法是错误的。

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

其次,在处理其原始DbContext之后,任何Entity对象都不应该保留在任何位置。实体对象在其DbContext在范围内时以一种方便的方式运行,并且当它超出范围时变成麻烦的POCO。我说麻烦,因为魔法' DbContext与变更跟踪有关,往往会欺骗不熟悉EF内部工作的人认为Entity对象直接连接到数据库中的表行。你遇到的问题完全证明了这一点。

第三,看起来您需要删除整个表并将其重新转储到内存中,即使您只更新单行中的单个列也是如此。这对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应用程序的生产中,因为我们说话和工作得很好。

答案 1 :(得分: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优先,模型优先或代码优先)。

注意:您也可以使用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语句来获取所有IIsReadOnly个实体,其中IsReadOnly设置为true,并将其状态设置为Unchanged

您可以在另一个IIsReadOnly中使用DbContext个参与者,并以通常的方式操纵它们。例如,如果您获得其中一个,请更新它,然后致电SaveChanges,我们会保存更改,因为IsReadOnly将具有默认的false值。但是,您可以轻松避免意外更改缓存数据,只需在缓存时将IsReadOnly属性设置为true即可。