如何将动态更改的数据存储到服务器缓存中?

Spo*_*com 4 c# asp.net caching

编辑:本网站的目的:名为Utopiapimp.com。它是一款名为utopia-game.com的第三方实用程序。该网站目前有超过12,000名用户,我运行该网站。游戏是完全基于文本的,并且将始终保持不变。用户从游戏中复制并粘贴整页文字,然后将复制的信息粘贴到我的网站中。我针对粘贴的数据运行一系列正则表达式并将其分解。然后,基于该粘贴,将5个值到30个以上的值插入数据库。然后,我采用这些值并对它们运行查询,以非常简单易懂的方式显示信息。该游戏是基于团队的,每个团队都有25个用户。因此,每个团队都是一个小组,每一行都是一个用户信息。用户可以一次更新全部25行或仅更新一行。

所以这里是交易。想象一下我有一个excel 编辑(Excel只是如何想象它的一个示例,我实际上并不使用excel)100列和5000行的电子表格。每行都有两个唯一的标识符。一排自己排成一排,一排25排。该行中大约有10列几乎永远不会更改,而其他90列则总是会更改。我们可以说,根据行的更新速度,有些甚至可能在几秒钟内发生变化。也可以从组中添加和删除行,但不能从数据库中添加和删除行。这些行是从数据库中的大约4个查询中获取的,以显示数据库中的最新数据和更新数据。因此,每次数据库中的某些内容更新时,我也希望更新该行。如果行或组在12个小时左右的时间内仍未更新,则将从缓存中删除该行或组。一旦用户通过数据库查询再次调用该组。它们将被放入缓存。

以上是我想要的。那是愿望。

实际上,我仍然拥有所有行,但是当前将它们存储在Cache中的方式已中断。我将每一行存储在一个类中,并且该类通过HUGE列表存储在服务器缓存中。当我去更新/删除/插入列表或行中的项目时,大多数情况下它都起作用,但是有时由于缓存已更改,它会引发错误。我希望能够像数据库或多或少地在行上锁定一样来锁定缓存。我有DateTime戳记可在12小时后删除内容,但这几乎总是会中断,因为其他用户正在更新组中相同的25行,或者只是缓存已更改。

这是我如何向Cache添加项目的一个示例,该示例显示了我仅拉出10个左右很少更改的列。此示例全部删除12小时后未更新的行:

DateTime dt = DateTime.UtcNow;
    if (HttpContext.Current.Cache["GetRows"] != null)
    {
        List<RowIdentifiers> pis = (List<RowIdentifiers>)HttpContext.Current.Cache["GetRows"];
        var ch = (from xx in pis
                  where xx.groupID == groupID 
                  where xx.rowID== rowID
                  select xx).ToList();
        if (ch.Count() == 0)
        {
            var ck = GetInGroupNotCached(rowID, groupID, dt); //Pulling the group from the DB
            for (int i = 0; i < ck.Count(); i++)
                pis.Add(ck[i]);
            pis.RemoveAll((x) => x.updateDateTime < dt.AddHours(-12));
            HttpContext.Current.Cache["GetRows"] = pis;
            return ck;
        }
        else
            return ch;
    }
    else
    {
        var pis = GetInGroupNotCached(rowID, groupID, dt);//Pulling the group from the DB
        HttpContext.Current.Cache["GetRows"] = pis;
        return pis;
    }
Run Code Online (Sandbox Code Playgroud)

最后一点,我从缓存中删除了项目,因此缓存实际上并没有变得很大。

重新发布问题,有什么更好的方法?也许以及如何在缓存上加锁?我能比这更好吗?我只希望它在删除或添加行时停止中断。

编辑:代码SQLCacheDependency不适用于Remus注释中发布的LINQ。它适用于全表选择,但是我只想从行中选择某些列。我不想选择“整个行”,因此无法使用Remus的Idea。

以下两个代码示例都不起作用。

var ck = (from xx in db.GetInGroupNotCached
              where xx.rowID== rowID
              select new {                 
                  xx.Item,
                  xx.AnotherItem,
                  xx.AnotherItem,
                  }).CacheSql(db, "Item:" + rowID.ToString()).ToList();


var ck = (from xx in db.GetInGroupNotCached
              where xx.rowID== rowID
              select new ClassExample {              
                Item=  xx.Item,
                 AnotherItem= xx.AnotherItem,
                 AnotherItemm = xx.AnotherItemm,
                  }).CacheSql(db, "Item:" + rowID.ToString()).ToList();
Run Code Online (Sandbox Code Playgroud)

Joh*_*lph 5

我真的怀疑您的缓存解决方案是否真的有用。List<T>无法索引,因此列表中的查找始终是O(n)操作。

假设您已对应用程序进行了概要分析,并且知道数据库是您的瓶颈,那么您可以执行以下操作:

在数据库中,您可以在数据上创建索引,对它们的查找通常将显示O(log(n))。您应该为包含静态数据的查询创建覆盖率索引。将经常变化的数据保留为未编制索引,因为由于必要的索引更新,这将减慢插入和更新的速度。您可以在此处阅读有关SQL Server索引的信息。上手SQL Server Profiler,检查哪些是最慢的查询及其原因。适当的索引可以为您带来巨大的性能提升(例如,假设每个组有25个人,则GroupId上的索引会将查找时间从全表扫描O(n)减少到O(n / 25)的索引查找) 。

人们通常会写出次优的SQL(返回不必要的列,选择N + 1,笛卡尔联接)。您也应该检查一下。

在实施缓存之前,我将确保您的数据库确实是导致性能问题的元凶。过早的优化是所有罪恶的根源,缓存很难做到的权利。缓存频繁更改的数据不是缓存的目的。


Tho*_*mas 5

通常,缓存的原因是您觉得从内存中提取数据(而不会过时)的速度比从数据库中提取数据的速度快。您可以从缓存中提取正确数据的一种情况是缓存命中。如果您的架构具有较低的缓存命中率,那么缓存可能会带来更多的伤害。如果你的数据变化很快,你的缓存命中率就会很低,而且比简单地查询数据要慢。

诀窍是在不经常更改和经常更改的元素之间拆分您的数据。缓存不经常变化的元素,不缓存经常变化的元素。这甚至可以通过使用 1:1 关系在单个实体的数据库级别完成,其中一个表包含不经常更改的数据和其他频繁更改的信息。您说您的源数据将包含 10 列几乎从不变化和经常变化的90。围绕该概念构建您的对象,以便您可以缓存很少更改的 10 个并查询频繁更改的 90 个。

我将每一行存储在一个类中,并且该类通过一个巨大的列表存储在服务器缓存中

从您的原始帖子中,听起来您没有将每个实例存储在缓存中,而是将缓存中的实例列表作为单个条目存储。问题是您可能会在此设计中遇到多线程问题。当多个线程将 one-list-to-rule-them-all 拉取时,它们都在访问内存中的同一个实例(假设它们在同一台服务器上)。此外,正如您所发现的,CacheDependency将在此设计中不起作用,因为它将使整个列表而不是单个项目失效。

一个显而易见但存在很大问题的解决方案是更改您的设计,将每个实例存储在内存中,并使用某种逻辑缓存键,并CacheDependency为每个实例添加一个。问题是,如果实例数量很大,这将在系统中产生大量开销,用于验证每个实例的货币并在必要时到期。如果缓存项正在轮询数据库,那也会产生大量流量。

我用来解决具有大量依赖于数据库的 CacheDependencies 问题的方法是在企业库的 CachingBlock 中创建自定义 ICacheItemExpiration。这也意味着我使用 CachingBlock 来缓存我的对象,而不是直接缓存 ASP.NET 缓存。在这个变体中,我创建了一个名为 a 的类DatabaseExpirationManager,它跟踪哪些项目将从缓存中过期。我仍然会单独将每个项目添加到缓存中,但是有了这个修改后的依赖项,它只是将项目注册到DatabaseExpirationManager. 该DatabaseExpirationManager会被通知需要是过期的,并会从到期缓存中的项目的关键。我会说,从一开始,这个解决方案可能不适用于快速变化的数据。DatabaseExpirationManager将不断运行,锁定其要过期的项目列表并防止添加新项目。您必须进行一些认真的多线程分析,以确保在不启用竞争条件的同时减少争用。

添加

好的。首先,公平警告,这将是一篇很长的文章。其次,这甚至不是整个图书馆,因为那太长了。

拿回车来说,我在 2005 年初和 2005 年末/2006 年初写了这段代码,因为 .NET 2.0 出来了,我还没有调查最近的库是否会做得更好(几乎可以肯定)。我使用的是 2005 年 1 月/2005 年 5 月/2006 年 1 月的库。您仍然可以从 CodePlex 中获取 2006 库。

我想出这个解决方案的方式是查看企业库中缓存系统的来源。简而言之,一切都通过CacheManager课堂进行。该类具有三个主要组件(所有三个都在Microsoft.Practices.EnterpriseLibrary.Caching命名空间中): Cache BackgroundScheduler ExpirationPollTimer

Cache班是EntLib的实现缓存。将BackgroundScheduler被用来清除在单独的线程缓存。这ExpirationPollTimer是一个Timer类的包装器。

因此,首先应该注意的是,Cache清除本身是基于计时器的。同样,我的解决方案将在计时器上轮询数据库。EntLib 缓存和 ASP.NET 缓存都处理具有委托的单个项目,以检查项目何时到期。我的解决方案在外部实体检查项目何时到期的前提下起作用。要注意的第二件事是,无论何时开始使用中央缓存,都必须注意多线程问题。

首先,我BackgroundScheduler用两个类替换了:DatabaseExpirationWorkerDatabaseExpirationManagerDatabaseExpirationManager包含查询数据库更改并将更改列表传递给事件的重要方法:

private object _syncRoot = new object();
private List<Guid>  _objectChanges = new List<Guid>();
public event EventHandler<DatabaseExpirationEventArgs> ExpirationFired;
...
public void UpdateExpirations()
{
    lock ( _syncRoot )
    {
        DataTable dt = GetExpirationsFromDb();
        List<Guid> keys = new List<Guid>();
        foreach ( DataRow dr in dt.Rows )
        {
            Guid key = (Guid)dr[0];
            keys.Add(key);
            _objectChanges.Add(key);
        }

        if ( ExpirationFired != null )
            ExpirationFired(this, new DatabaseExpirationEventArgs(keys));
    }
}
Run Code Online (Sandbox Code Playgroud)

这个DatabaseExpirationEventArgs类看起来像这样:

public class DatabaseExpirationEventArgs : System.EventArgs
{
    public DatabaseExpirationEventArgs( List<Guid> expiredKeys )
    {
        _expiredKeys = expiredKeys;
    }

    private List<Guid> _expiredKeys;
    public List<Guid> ExpiredKeys
    {
        get  {  return _expiredKeys;  }
    }
}
Run Code Online (Sandbox Code Playgroud)

在这个数据库中,所有的主键都是 Guid。这使得跟踪更改变得更加简单。中间层中的每个保存方法都会将它们的 PK 和当前日期时间写入一个表中。每次系统轮询数据库时,它都会存储启动轮询的日期时间(来自数据库,而不是来自中间层),GetExpirationsFromDb并将返回自那时以来发生更改的所有项目。另一种方法将定期删除早已被轮询的行。这个更改表非常狭窄:一个 guid 和一个日期时间(两列都有一个 PK,日期时间 IIRC 上的聚集索引)。因此,它可以被非常快速地查询。另请注意,我使用 Guid 作为缓存中的键。

DatabaseExpirationWorker班是几乎相同的BackgroundScheduler,除了它DoExpirationTimeoutExpired会调用该DatabaseExpirationManager UpdateExpirations方法。由于 中的任何方法都不BackgroundSchedulervirtual,我不能简单地从BackgroundScheduler它的方法派生和覆盖它。

我做的最后一件事是编写我自己的 EntLib CacheManager 版本,它使用 myDatabaseExpirationWorker而不是 ,BackgroundScheduler它的索引器将检查对象到期列表:

private List<Guid> _objectExpirations;
private void OnExpirationFired( object sender, DatabaseExpirationEventArgs e )
{
    _objectExpirations = e.ExpiredKeys;
    lock(_objectExpirations)
    {
        foreach( Guid key in _objectExpirations)
            this.RealCache.Remove(key);
    }
}

private Microsoft.Practices.EnterpriseLibrary.Caching.CacheManager _realCache;
private Microsoft.Practices.EnterpriseLibrary.Caching.CacheManager RealCache
{
    get
    {
        lock(_syncRoot)    
        {       
            if ( _realCache == null )
                _realCache = Microsoft.Practices.EnterpriseLibrary.Caching.CacheManager.CacheFactory.GetCacheManager();

            return _realCache;
        }
    }
}


public object this[string key]
{
    get
    {
        lock(_objectExpirations)
        {
            if (_objectExpirations.Contains(key))
                return null;
            return this.RealCache.GetData(key);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

再一次,自从我查看这段代码以来,已经有很多个月了,但这为您提供了它的要点。即使翻看我的旧代码,我也看到了许多可以清理和清理的地方。我也没有看过最新版本的 EntLib 中的缓存块,但我想它已经改变和改进了。请记住,在我构建它的系统中,每秒发生数十次更改,而不是数百次。因此,如果数据在一两分钟内过时,这是可以接受的。如果您的解决方案每秒有数千次更改,则此解决方案可能不可行。