Имеется 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;
}
Корректен ли Retry механизм? Если получить блокировку не получится, вызывающий код не получит данных, правильно ли это? Подразумевается, что он сам решит, что в таком случае делать.
Из-за установленного таймаута вечной блокировки быть не может, например из-за неудачно завершившегося потока, который ранее получил блокировку. Так или иначе один из клиентов или потоков в рамках того же клиента установит значение.
В случае когда получить блокировку не получилось, попыток снять ее не производится. Подразумевается, что другой клиент или поток в рамках того же клиента получит блокировку в результате ее таймаута.
Является ли overhead'ом попытки считать информацию в рамках описанного процесса?
Подразумевается, что доступ к методу SetItemAsync синхронизирован в рамках клиента.
П.3. Если выставлять таймаут блокировки с учетом данных, которые будут извлекаться из DB, может возникнуть ситуация когда задан долгий таймаут, тогда следует снять блокировку принудительно. Как тут грамотно поступить?
Вопросов оставленно больше одного, но тем не менее. Правильна ли реализация с учетом поставленной цели? Ответы на вопросы из пунктов 1-5 тоже привествуются. Может что-то не учтено или в корне неправильно.
Дополнительная информация:
Redis - 4.0.11. 3 VM. Clustered | .NET Client - StackExchange.Redis
Сейчас у вас данные выбрасываются исключительно по TTL. Значит, вас устраивает неактуальность данных в кэше течении TTL секунд. Т.е. реальные ограничения у вас:
Этим ограничениям полностью соответствует Upfront population:
При необходимости - триггерите заселение кэша не по таймеру, а по событиям изменения данных - и получаете минимальное отставание кэша от реальных данных, сравнимое с временем выполнения прямой выборки.
Сборка персонального компьютера от Artline: умный выбор для современных пользователей