繁体   English   中英

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

[英]How should I store Dynamically Changing Data into Server Cache?

编辑:本网站的目的:其名为Utopiapimp.com。 它是名为utopia-game.com的游戏的第三方实用程序。 该网站目前拥有超过12k用户,我运行该网站。 游戏是完全基于文本的,并将始终保持这种状态。 用户从游戏中复制并粘贴整页文本,并将复制的信息粘贴到我的网站中。 我针对粘贴的数据运行一系列正则表达式并将其分解。 然后,我将基于该一个粘贴将5个值到30个以上的值插入到DB中。 然后,我接受这些值并对它们运行查询,以非常简单易懂的方式显示信息。 游戏是基于团队的,每个团队有25个用户。 因此每个团队都是一个组,每行都是一个用户信息。 用户可以一次更新所有25行或仅更新一行。 我需要将内容存储到缓存中,因为该网站几乎每分钟都会执行超过1,000次查询。

所以这里是交易。 想象一下,我有一个excel 编辑 (Excel只是一个如何设想的例子,我实际上并没有使用excel)电子表格有100列和5000行。 每行有两个唯一标识符。 一个用于自行,一个用于组合25行一个。 该行中大约有10列几乎不会改变,其他90列将始终在变化。 我们可以说有些甚至会在几秒钟内改变,具体取决于行的更新速度。 也可以从组中添加和删除行,但不能从数据库中删除。 这些行来自数据库中的大约4个查询,以显示数据库中的最新和更新数据。 因此,每次更新数据库中的某些内容时,我也希望更新该行。 如果行或组在12个小时内未更新,则将从Cache中取出。 一旦用户通过数据库查询再次呼叫该组。 它们将被放入Cache中。

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

在Reality中,我仍然拥有所有行,但是我将它们存储在Cache中的方式目前已被破坏。 我将每一行存储在一个类中,该类通过一个巨大的列表存储在服务器缓存中。 当我去更新/删除/插入列表或行中的项目时,大部分时间它都有效,但有时它会因为缓存已更改而引发错误。 我希望能够锁定缓存,就像数据库或多或少地抛出对某一行的锁定一样。 我有12个小时后删除东西的DateTime标记,但这几乎总是会中断,因为其他用户正在更新组中的相同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;
    }

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

要重新发布这个问题,这是一个更好的方法吗? 也许以及如何把锁放在缓存上? 我可以比这更好吗? 我只是希望它在删除或添加行时停止破坏。

编辑:代码SQLCacheDependency不适用于Remus的评论中发布的LINQ。 它适用于全表选择,但我想从行中选择某些列。 我不想选择整行,所以我不能使用Remus的想法。

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

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();

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

假设您已经分析了应用程序,并且知道数据库是您的瓶颈,那么您可以这样做:

在数据库中,您可以在数据上创建索引,对它们的查找通常会显示O(log(n))。 您应该为包含静态数据的查询创建覆盖索引。 保持频繁更改的数据不被索引,因为这会因为必要的索引更新而减慢插入和更新。 您可以在此处阅读SQL Server索引。 获取SQL Server Profiler并检查哪些是最慢的查询以及原因。 正确的索引可以为您带来巨大的性能提升(例如,假设每组有25个人,GroupId上的索引将减少从全表扫描O(n)到O(n / 25)的索引查找的查找时间。

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

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

一般来说,缓存的原因是你觉得你可以比从数据库中提取数据更快地将数据从内存中拉出来(没有过时)。 您可以从Cache中提取正确数据的情况是Cache Hit。 如果您的架构具有较低的缓存命中率,那么缓存可能比帮助更有害。 如果您的数据快速变化,您将具有较低的缓存命中率,并且比单纯查询数据要慢。

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

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

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

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

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

加成

好。 首先,公平警告这将是一个长期的职位。 其次,这甚至不是整个图书馆,因为它太长了。

在使用返回机器的时候,我在2005年初期和2006年初就编写了这段代码,因为.NET 2.0问世了,我还没有调查过最新的库是否可能做得更好(几乎可以肯定)。 我使用的是2005年1月/ 2005年5月/ 2006年1月的图书馆。 您仍然可以从CodePlex获取2006库。

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

Cache类是EntLib的缓存实现。 BackgroundScheduler用于在单独的线程上清除缓存。 ExpirationPollTimerTimer类的包装器。

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

首先,我将BackgroundScheduler替换为两个类: DatabaseExpirationWorkerDatabaseExpirationManager DatabaseExpirationManager包含查询数据库以进行更改并将更改列表传递给事件的重要方法:

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));
    }
}

DatabaseExpirationEventArgs类看起来像这样:

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

    private List<Guid> _expiredKeys;
    public List<Guid> ExpiredKeys
    {
        get  {  return _expiredKeys;  }
    }
}

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

DatabaseExpirationWorker类与BackgroundScheduler几乎完全相同,只是它的DoExpirationTimeoutExpired会调用DatabaseExpirationManager UpdateExpirations方法。 由于BackgroundScheduler中的所有方法都不是virtual ,因此我不能简单地从BackgroundScheduler派生并覆盖其方法。

我做的最后一件事是编写我自己的EntLib的CacheManager版本,它使用我的DatabaseExpirationWorker而不是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);
        }
    }
}

再说一次,自从我查看这段代码以来,它已经有了很多元素,但这会给你一个它的主旨。 即使查看我的旧代码,我也会看到许多可以清理和清理的地方。 我还没有看过最新版本的EntLib中的缓存块,但我想它已经改变和改进了。 请记住,在我构建它的系统中,每秒有数十个更改而不是数百个。 因此,如果数据陈旧一两分钟,那是可以接受的。 如果在您的解决方案中每秒有数千次更改,那么此解决方案可能不可行。

您将整个数据库作为列表存储在内存中,并作为列表遍历为每个请求重新查询它。 坦率地说,我怀疑这个'缓存'实际上比运行SQL查询更快。 遍历列表永远不会打败数据库......

您应该做的是缓存特定的查询结果。 与rowID和groupID的结果集一样,缓存由两个参数键入。 对于刷新,依赖于查询通知周围的内置缓存失效基础结构,请参阅本文神秘通知以了解其工作原理。 使用ASP.Net项目,您所要做的就是利用SqlCacheDependency

我不太确定这是个好主意,如果你能设法加快与数据库的沟通,你可能会有更好的解决方案。

希望我理解你的要求。
很快就变成了很多代码,在这里你拥有它......

这只是一个示例,但它可能是一些建立的东西。 我没有考虑到你需要在一段时间后删除行。
我将缓存分成了具有组的组,其中组包含行。
我设计的示例只在第一个set属性被调用时锁定一行,当只调用get操作时,你应该是安全的。
处理行对象时将释放锁定。 所以你必须使用using()或调用Dispose()才能使它工作。

这是一个缓存(组)类和一个行类。
在注释后添加数据库// Add code to read from database...

public class GroupCache : SimpleCache<RowObject, int>
{
    private static readonly object GroupCacheObjectLock = new object();

    public GroupCache(int groupId)
    {
        GroupId = groupId;
    }
    public int GroupId { get; private set; }

    public static GroupCache GetGroupCache(int groupId)
    {
        lock (GroupCacheObjectLock)
        {
            if (HttpContext.Current.Cache["Group-" + groupId] == null)
            {
                HttpContext.Current.Cache["Group-" + groupId] 
                    = new GroupCache(groupId);
            }
        }
        return HttpContext.Current.Cache["Group-" + groupId];
    }

    public override RowObject CreateItem(int id, 
            SimpleCache<RowObject, int> cache)
    {
        return new RowObject(id, GroupId, this);
    }

}

public class RowObject : SimpleCacheItem<RowObject, int>
{
    private string _property1;

    public RowObject(int rowId, int groupId, SimpleCache<RowObject, int> cache)
        : base(rowId, cache)
    {
        // Add code to read from database...
    }

    public string Property1
    {
        get { return _property1; }
        set
        {
            if (!AcquireLock(-1)) return;
            _property1 = value;
#if DEBUG
            Trace.WriteLine(string.Format("Thread id: {0}, value = {1}", 
                Thread.CurrentThread.ManagedThreadId, value));
#endif
        }
    }
}

这是一个单元测试,主要是为了展示如何使用这些类。

[TestFixture]
public class GroupCacheTest
{
    private int _threadFinishedCount;
    private void MultiThreadTestWorker(object obj)
    {
        for (int n = 0; n < 10; n++)
        {
            for (int m = 0; m < 25; m++)
            {
                using (RowObject row 
                    = GroupCache.GetGroupCache(n).GetCachedItem(m))
                {
                    row.Property1 = string.Format("{0} {1} {2}", obj, n, m);
                    Thread.Sleep(3);
                }
            }
        }
        Interlocked.Increment(ref _threadFinishedCount);
    }
    [Test]
    public void MultiThreadTest()
    {
        _threadFinishedCount = 1;
        for (int i = 0; i < 20; i++)
        {
            ThreadPool.QueueUserWorkItem(MultiThreadTestWorker, "Test-" + i);
        }
        while (_threadFinishedCount < 10)
            Thread.Sleep(100);
    }
}

这是基类。

public abstract class SimpleCacheItem<T, TKey> : IDisposable where T : class
{
    private readonly SimpleCache<T, TKey> _cache;

    protected SimpleCacheItem(TKey id, SimpleCache<T, TKey> cache)
    {
        Id = id;
        _cache = cache;
    }

    protected TKey Id { get; private set; }

    #region IDisposable Members

    public virtual void Dispose()
    {
        if (_cache == null) return;
        _cache.ReleaseLock(Id);
    }

    #endregion

    protected bool AcquireLock(int timeout)
    {
        return _cache.AcquireLock(Id, -1);
    }
}

public abstract class SimpleCache<T, TKey> where T : class
{
    private static readonly object CacheItemLockSyncLock = new object();
    private static readonly object CacheItemStoreSyncLock = new object();
    private readonly Dictionary<TKey, int> _cacheItemLock;
    private readonly Dictionary<TKey, T> _cacheItemStore;

    public abstract T CreateItem(TKey id, SimpleCache<T, TKey> cache);

    public T GetCachedItem(TKey id)
    {
        T item;
        lock (CacheItemStoreSyncLock)
        {
            if (!_cacheItemStore.TryGetValue(id, out item))
            {
                item = CreateItem(id, this);
                _cacheItemStore.Add(id, item);
            }
        }
        return item;
    }

    public void ReleaseLock(TKey id)
    {
        lock (CacheItemLockSyncLock)
        {
            if (_cacheItemLock.ContainsKey(id))
            {
                _cacheItemLock.Remove(id);
            }
        }
#if DEBUG
        Trace.WriteLine(string.Format("Thread id: {0} lock released", 
        Thread.CurrentThread.ManagedThreadId));
#endif
    }

    public bool AcquireLock(TKey id, int timeOut)
    {
        var timer = new Stopwatch();
        timer.Start();
        while (timeOut < 0 || timeOut < timer.ElapsedMilliseconds)
        {
            lock (CacheItemLockSyncLock)
            {
                int threadId;
                if (!_cacheItemLock.TryGetValue(id, out threadId))
                {
                    _cacheItemLock.Add(id, 
                        Thread.CurrentThread.ManagedThreadId);
#if DEBUG
                    Trace.WriteLine(string.Format(
                        "Thread id: {0}, lock acquired after {1} ms", 
                        Thread.CurrentThread.ManagedThreadId, 
                        timer.ElapsedMilliseconds));
#endif
                    return true;
                }
                if (threadId == Thread.CurrentThread.ManagedThreadId) 
                    return true;
            }
            Thread.Sleep(15);
        }
        return false;
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM