Как хранить динамически изменяемые данные в кеше сервера? - PullRequest
4 голосов
/ 07 апреля 2010

РЕДАКТИРОВАТЬ: Цель этого веб-сайта: его называют Utopiapimp.com.Это сторонняя утилита для игры под названием utopia-game.com.В настоящее время на сайте более 12 тысяч пользователей, и я управляю сайтом.Игра полностью текстовая и всегда будет таковой.Пользователи копируют и вставляют полные страницы текста из игры и вставляют скопированную информацию на мой сайт.Я запускаю серию регулярных выражений для вставленных данных и разбиваю их.Затем я вставляю в базу данных от 5 до 30 значений на основе этой одной вставки.Затем я беру эти значения и выполняю запросы к ним, чтобы отобразить информацию ОЧЕНЬ простым и понятным способом.Игра основана на команде, и каждая команда имеет 25 пользователей.Таким образом, каждая команда - это группа, а в каждой строке - ОДНА информация о пользователях.Пользователи могут обновлять все 25 строк или только одну строку за раз.Мне нужно хранить вещи в кеше, потому что сайт очень медленно выполняет более 1000 запросов почти каждую минуту.

Так что вот в чем дело.Представьте, что у меня есть таблица Excel EDIT (Excel - всего лишь пример того, как это можно себе представить, я на самом деле не использую Excel) с 100 столбцами и 5000 строками.Каждая строка имеет два уникальных идентификатора.Один для ряда сам по себе и один для группировки 25 рядов по одному.В строке около 10 столбцов, которые почти никогда не изменятся, а остальные 90 столбцов всегда будут меняться.Можно сказать, что некоторые из них даже изменятся в течение нескольких секунд в зависимости от того, насколько быстро обновляется строка.Строки также можно добавлять и удалять из группы, но не из базы данных.Строки взяты из примерно 4 запросов из базы данных, чтобы показать самые последние и обновленные данные из базы данных.Поэтому каждый раз, когда что-то в базе данных обновляется, я также хотел бы обновить строку.Если строка или группа не были обновлены в течение примерно 12 часов, они будут удалены из кэша.Как только пользователь снова вызывает группу через запросы БД.Они будут помещены в кэш.

Это то, что я хотел бы.Это желание.

В реальности у меня все еще есть все строки, но способ их хранения в Cache в настоящее время не работает.Я храню каждую строку в классе, а класс хранится в кэше сервера через огромный список.Когда я иду, чтобы обновить / Удалить / Вставить элементы в списке или строках, чаще всего это работает, но иногда выдает ошибки, потому что кэш изменился.Я хочу иметь возможность заблокировать кеш, как база данных более или менее блокирует строку.У меня есть метки DateTime, чтобы удалять вещи через 12 часов, но это почти всегда ломается, потому что другие пользователи обновляют те же 25 строк в группе, или просто меняется кэш.

Это пример того, как я добавляю элементыКэширование показывает, что я извлекаю только 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 НЕ работает для 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();

Ответы [ 4 ]

5 голосов
/ 10 апреля 2010

Я действительно сомневаюсь, что ваше решение для кеширования действительно полезно.List<T> не может быть проиндексирован, поэтому поиск в вашем списке всегда является операцией O (n).

Предполагая, что вы профилировали свое приложение и знаете, что база данных является вашим узким местом, вы можете сделать следующее:1004 *

В базе данных вы можете создавать индексы для ваших данных, при поиске в них будет обычно отображаться O (log (n)).Вы должны создать индексы покрытия для запросов, которые включают ваши статические данные.Оставьте часто изменяемые данные неиндексированными, поскольку это приведет к замедлению операций вставки и обновления из-за необходимых обновлений индекса.Вы можете прочитать об индексировании SQL Server здесь .Возьмите в руки SQL Server Profiler и выясните, какие из них являются самыми медленными и почему.Правильные индексы могут принести вам огромный прирост производительности (например, индекс вашего GroupId сократит время поиска от полного сканирования таблицы O (n) до индекса поиска O (n / 25), при условии, что в группе 25 человек).

Чаще всего люди пишут неоптимальный SQL (возвращая ненужные столбцы, выберите N + 1, декартовы объединения).Вы также должны это проверить.

Перед внедрением кэша я бы удостоверился, что ваша база данных действительно является причиной ваших проблем с производительностью.Преждевременная оптимизация - корень всего зла, кеширование трудно сделать правильно.Кэширование часто меняющихся данных - это не то, для чего предназначено кэширование.

4 голосов
/ 12 апреля 2010

В общем, причина кеширования в том, что вы чувствуете, что можете извлечь данные из памяти (без их устаревания) быстрее, чем вы можете извлечь их из базы данных. Ситуация, когда вы можете извлечь нужные данные из кэша, является попаданием в кэш. Если ваша схема имеет низкий коэффициент попадания в кэш, то, вероятно, кэш наносит больший вред, чем помогает. Если ваши данные быстро изменяются, у вас будет низкий коэффициент попадания в кэш, и он будет медленнее, чем простой запрос данных.

Хитрость в том, чтобы разделить ваши данные между редко меняющимися и часто меняющимися элементами. Кэшируйте редко меняющиеся элементы и не кешируйте часто меняющиеся элементы. Это можно сделать даже на уровне базы данных для одной сущности, используя отношение 1: 1, где одна из таблиц содержит редко меняющиеся данные и другую часто меняющуюся информацию. Вы сказали, что ваши исходные данные будут содержать 10 столбцов, которые почти никогда не будут изменить и 90, которые часто меняются. Постройте свои объекты вокруг этого понятия, чтобы вы могли кэшировать 10, которые редко меняются, и запрашивать 90, которые часто меняются.

Я храню каждую строку в классе и класс хранится в кэше сервера через огромный список

Из вашего исходного поста звучит так, будто вы храните не каждый экземпляр в кэше, а вместо этого список экземпляров в кэше в виде одной записи. Проблема в том, что вы можете получить многопоточность в этом проекте. Когда несколько потоков извлекают один список для правила их всех, все они обращаются к одному и тому же экземпляру в памяти (при условии, что они находятся на одном сервере). Кроме того, как вы обнаружили, CacheDependency не будет работать в этом дизайне, потому что у него истечет весь список, а не один элемент.

Одним из очевидных, но весьма проблемных решений было бы изменить ваш дизайн для хранения каждого экземпляра в памяти с помощью некоторого логического ключа кэша и добавить CacheDependency для каждого экземпляра. Проблема заключается в том, что, если количество экземпляров велико, это создаст много накладных расходов в системе, проверяя валюту каждого из экземпляров и истекая при необходимости. Если элементы кэша опрашивают базу данных, это также создаст много трафика.

Подход, который я использовал для решения проблемы наличия большого числа зависимых от базы данных CacheDependencies, заключается в создании настраиваемого ICacheItemExpiration в CachingBlock из Enterprise Library. Это также означало, что я использовал CachingBlock для кэширования своих объектов, а не кеш ASP.NET напрямую. В этом варианте я создал класс под названием DatabaseExpirationManager, который отслеживал, какие элементы истекают из кэша. Я бы по-прежнему добавлял каждый элемент в кеш индивидуально, но с этой измененной зависимостью, которая просто регистрировала элемент с DatabaseExpirationManager. DatabaseExpirationManager будет уведомлен о ключах, срок действия которых должен истечь, и истечет срок действия элементов из кэша. С самого начала я скажу, что это решение, вероятно, не будет работать с быстро меняющимися данными. DatabaseExpirationManager будет работать постоянно, удерживая блокировку в списке элементов, срок действия которой истекает, и предотвращая добавление новых элементов. Вам нужно будет провести серьезный многопоточный анализ, чтобы убедиться, что вы сократили конкуренцию, не включив условие гонки.

ДОПОЛНЕНИЕ

Ok. Во-первых, справедливое предупреждение о том, что это будет длинный пост. Во-вторых, это даже не вся библиотека, так как это было бы слишком долго.

На обратном пути я написал этот код в начале и конце 2005 / начале 2006 года, когда вышел .NET 2.0, и я не исследовал, могут ли более современные библиотеки справиться с этим лучше (почти наверняка они ). Я использовал библиотеки января 2005 года / мая 2005 года / января 2006 года. Вы все еще можете получить библиотеку 2006 года от CodePlex.

Я пришел к этому решению, посмотрев на источник системы кэширования в Enterprise Library. Короче, все кормили через класс CacheManager. Этот класс имеет три основных компонента (все три находятся в пространстве имен Microsoft.Practices.EnterpriseLibrary.Caching): Cache BackgroundScheduler ExpirationPollTimer

Класс Cache является реализацией кеша EntLib. BackgroundScheduler использовался для очистки кэша в отдельном потоке. ExpirationPollTimer был оберткой вокруг Timer класса.

Итак, во-первых, следует отметить, что Cache очищает себя на основе таймера. Точно так же мое решение будет опрашивать базу данных по таймеру. Кеш EntLib и кеш ASP.NET работают с отдельными элементами, имеющими делегата для проверки истечения срока действия элемента. Мое решение работало при условии, что внешняя сущность проверит, когда истекает срок действия товаров. Второе, на что следует обратить внимание, это то, что когда вы начинаете играть с центральным кешем, вы должны быть внимательны к проблемам многопоточности.

Сначала я заменил BackgroundScheduler на два класса: DatabaseExpirationWorker и DatabaseExpirationManager. 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 и datetime (с PK в обоих столбцах и кластеризованным индексом в datetime IIRC). Таким образом, это может быть запрошено очень быстро. Также обратите внимание, что я использовал Guid в качестве ключа в Cache.

Класс DatabaseExpirationWorker был почти идентичен классу BackgroundScheduler, за исключением того, что его DoExpirationTimeoutExpired будет вызывать метод DatabaseExpirationManager UpdateExpirations. Поскольку ни один из методов в BackgroundScheduler не был virtual, я не мог просто извлечь из BackgroundScheduler и переопределить его методы.

Последнее, что я сделал, это написал свою собственную версию CacheManager EntLib, которая использовала мой 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);
        }
    }
}

Опять же, с тех пор, как я просмотрел этот код, было много лун, но это дает вам представление о нем. Даже просматривая мой старый код, я вижу много мест, которые можно очистить и очистить. Я также не смотрел на блок Caching в самой последней версии EntLib, но думаю, что он изменился и улучшился. Имейте в виду, что в системе, в которой я это построил, в секунду происходили десятки изменений, а не сотни. Итак, если данные устарели в течение минуты или двух, это было приемлемо. Если в вашем решении тысячи изменений в секунду, то это решение может оказаться неосуществимым.

2 голосов
/ 07 апреля 2010

Вы сохраняете всю базу данных в памяти в виде списка и повторно запрашиваете его для каждого запроса в качестве обхода списка. Честно говоря, я сомневаюсь, что этот «кеш» на самом деле быстрее, чем просто выполнение SQL-запроса. Пересекающий список никогда не превзойдет базу данных ...

Вместо этого вам следует кэшировать конкретные результаты запроса. Как и результирующий набор для rowID и groupID, кэшируется ключом двух аргументов. Для обновления используйте инфраструктуру аннулирования встроенного кэша в Query Notification, см. Эту статью Таинственное уведомление , чтобы понять, как это работает. В проекте ASP.Net все, что вам нужно сделать, это использовать SqlCacheDependency .

1 голос
/ 15 апреля 2010

Я не уверен, что это хорошая идея, у вас, вероятно, будет лучшее решение, если вы сможете ускорить связь с вашей базой данных.

Надеюсь, я понял ваши требования.
Это быстро стало много кода, вот оно ...

Это всего лишь пример, но это может быть что-то, на чем можно основываться. Я не учел вашу необходимость удалять строки через определенное время.
Я разделил кеш на сегменты с группами, где группы содержат строки.
Я разработал пример для блокировки строки только при вызове первого свойства 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;
    }
}

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...