Синхронизация вставки в Redis

113
22 января 2021, 15:30

Имеется N: N > 1 клиентов расположенных на разных серверах. В таком сценарии возможна ситуация когда множество клиентов пытаются записать данные в Redis из-за их истекшего TTL, а это влечет за собой дополнительные запросы к DB. Количество запросов к DB При N: N = 8 равно 8. Требуется минимизировать количество обращений к DB.

Схема: User -> API -> DB || Redis -> User.

Один из вариантов - использование встроенного в Redis механизма блокировок. Пример ниже иллюстрирует добавление записи с учетом блокировки ключа. Ожидается, что при указанных условиях будет выполнен только один запрос к DB.

public override async Task<T> SetItemAsync<T>(string k, Func<Task<T>> action, TimeSpan TTL)
{
    // Вероятно клиент, что один
    // из клиент завершил запись.
    T v = await GetFromCache<T>(k);
    // Если данные в Redis есть, 
    // возвращаем, а иначе 
    // пытаемся добавить.
    if (!IsDefault(v))
        return v;
    // Время на которое блокируется ключ.
    // Может зависить от типа данных.
    // Сделать конфигурируемым.
    TimeSpan _lockTimeout = TimeSpan.FromSeconds(3);
    // Время окончания блокировки.
    DateTime _lockTimeoutEnd = DateTime.Now.AddSeconds(_lockTimeout.TotalSeconds);
    // Пытаемся получить блокировку
    // с учетом таймаута.
    while (DateTime.Now < _lockTimeoutEnd)
    {
        // _lockToken - Environment.MachineName.
        if (_db.LockTake(k, _lockToken, _lockTimeout))
        {
            try
            {
                // Получаем данные из DB. Возможны случаи
                // когда выполнения данного участка кода
                // не уложится в таймаут блокировки.
                v = await action();
                // Записываем данные в Redis.
                await SetToCache(k, v, TTL);
                // Публикуем сообщение в соответствующий канал, чтобы
                // остальные клиенты перезапросили из Redis данные с
                // заданным ключем и перезаписали локальный кеш. Это
                // для случая когда будет использоваться Redis + LocalMemory.
                await _db.PublishAsync(PubSubMessageType.CACHE_RESETED.ToString(), k);
            }
            finally
            {
                _db.LockRelease(k, _lockToken);
            }
        }
        else
        {
            await Task.Delay(50);
            // Вероятно клиент, что один
            // из клиент завершил запись.
            v = await GetFromCache<T>(k);
            if (!IsDefault(v))
                return v;
        }
    }
    return default;
}
  1. Корректен ли Retry механизм? Если получить блокировку не получится, вызывающий код не получит данных, правильно ли это? Подразумевается, что он сам решит, что в таком случае делать.

  2. Из-за установленного таймаута вечной блокировки быть не может, например из-за неудачно завершившегося потока, который ранее получил блокировку. Так или иначе один из клиентов или потоков в рамках того же клиента установит значение.

  3. В случае когда получить блокировку не получилось, попыток снять ее не производится. Подразумевается, что другой клиент или поток в рамках того же клиента получит блокировку в результате ее таймаута.

  4. Является ли overhead'ом попытки считать информацию в рамках описанного процесса?

  5. Подразумевается, что доступ к методу SetItemAsync синхронизирован в рамках клиента.

П.3. Если выставлять таймаут блокировки с учетом данных, которые будут извлекаться из DB, может возникнуть ситуация когда задан долгий таймаут, тогда следует снять блокировку принудительно. Как тут грамотно поступить?

Вопросов оставленно больше одного, но тем не менее. Правильна ли реализация с учетом поставленной цели? Ответы на вопросы из пунктов 1-5 тоже привествуются. Может что-то не учтено или в корне неправильно.

  • Данные изменяются довольно часто, т.е могут считатся неактуальными через 60 секунд или раньше.
  • Обращения за данными происходят постоянно.

Дополнительная информация:

Redis - 4.0.11. 3 VM. Clustered | .NET Client - StackExchange.Redis

Answer 1

Сейчас у вас данные выбрасываются исключительно по TTL. Значит, вас устраивает неактуальность данных в кэше течении TTL секунд. Т.е. реальные ограничения у вас:

  • Отставание кэша от данных в базе допустимо, данные в кэше должны отставать от реальных данных в базе не больше чем на N секунд (N = ваш текущий TTL)
  • Обращения в кэш идут постоянно, гораздо чаще чем раз в N секунд
  • Выборка тяжелая, одновременные запросы с нескольких серверов убивают базу.

Этим ограничениям полностью соответствует Upfront population:

  • Делаете один отдельный сервис, который регулярно, чаще, чем раз в N секунд читает данные из базы, и кладет их в кэш.
  • Все сервера просто читают из кэша.

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

READ ALSO
Консоль выводит NaN

Консоль выводит NaN

При вводе некоторых чисел например 4,5 выводит Nan все перепробовал и на -1 домножал и по модулю брал все-ровно NaN

123
WPF | Разный DataContext для разных элементов

WPF | Разный DataContext для разных элементов

В примере с использованием MVVM контекст данных вводили прямо вxaml

131
Mssql и Visual Studio не могу обновить процедуру

Mssql и Visual Studio не могу обновить процедуру

Имею процедуру, она выполняется и работает в студии mssql Я её связал ранее через источник данных с vs проектомПотом я изменил её, поменял в селекте...

117
Экспорт таблицы listView в Excel [дубликат]

Экспорт таблицы listView в Excel [дубликат]

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

98