Redis (Remote Dictionary Server) - это ультраскоростное хранилище данных в оперативной памяти, работающее по принципу "ключ-значение". Суть проста: данные хранятся не на диске, а прямо в RAM, что обеспечивает феноменальную скорость доступа - микросекунды вместо милисекунд. Созданный итальянским разработчиком Сальваторе Санфилиппо в 2009 году, Redis изначально задумывался как решение очень конкретной проблемы - кеширование метрик в реальном времени. Но, как это часто бывает с по-настоящему удачными технологиями, он вырос далеко за пределы первоначальной идеи.
Фундаментальное отличие Redis от других NoSQL решений в том, что он не просто хранит пары "ключ-значение", а поддерживает разнообразные структуры данных. Строки, хеши, списки, множества, сортированные множества, битовые карты, гиперлоглоги - впечатляющий арсенал, который превращает Redis из простого кеша в мощный инструмент для решения сложных задач.
| C# | 1
2
3
| SET user:1000 "Информация о пользователе"
HSET user:1001 name "Иван" age "30" country "Россия"
LPUSH notifications:1000 "Новое сообщение" "Обновление системы" |
|
Эти команды демонстрируют, насколько просто можно работать с разными типами данных. Первая сохраняет строку, вторая - хеш (фактически, словарь ключей и значений), а третья добавляет элементы в начало списка.
Но Redis это не только скорость и разнообразие типов данных. Это целая система возможностей:
1. Атомарные операции - все команды выполняются без прерываний, что гарантирует целосность данных даже при одновременном доступе.
2. Транзакции - группирование команд в атомарные блоки выполнения.
3. Pub/Sub - механизм публикации/подписки для реализации паттерна "наблюдатель".
4. Geospatial - работа с геоданными, поиск по радиусу или прямоугольной области.
5. Lua скриптинг - выполнение сложных операций атомарно через встроеный интерпретатор.
А еще Redis предлагает гибкие настройки перзистентности данных - от чисто in-memory работы до периодического сохранения на диск. Можно настроить как запись лога операций (AOF - Append Only File), так и создание снапшотов в определенные моменты времени.
Один из самых приятных аспектов Redis - это его производительность. Официальные бенчмарки показывают, что Redis способен обрабатывать сотни тысяч операций в секунду на обычном железе. Сравнивая с Memcached (другим популярным решением для кеширования), Redis обычно показывает схожую производительность в базовых операциях, но значительно опережает его, когда дело касается сложных структур данных и встроеных функций. При этом Redis крайне экономен в потреблении ресурсов - достаточно нескольких мегабайт RAM для самого процесса, а все остальное доступно для ваших данных.
Важное преимущество Redis над конкурентами - его однопоточная архитектура. Звучит как недостаток? Совсем нет! Благодаря этому решению Redis избегает множества проблем с конкуренцией и блокировками, обычно присущих многопоточным системам. Все операции выполняются последовательно, что исключает необходимость сложных механизмов синхронизации. Однако с версии 6.0 Redis получил многопоточную поддержку для операций ввода-вывода, сохраняя при этом однопоточную обработку команд. Это дает лучшее из обоих миров - предсказуемость выполнения и масштабируемость.
Если сравнивать Redis со встроенными механизмами кеширования в .NET, разница становится очевидной. Встроеный MemoryCache хорош для небольших приложений, но имеет существенные ограничения:
| C# | 1
2
3
| // Стандартный MemoryCache в .NET
MemoryCache cache = new MemoryCache("MyCache");
cache.Add("key", "value", DateTimeOffset.Now.AddMinutes(30)); |
|
Этот код прост, но такое кеширование ограничено одним сервером. Попробуйте масштабировать приложение на несколько экземпляров - и вы столкнетесь с проблемой синхронизации кешей. Redis же изначально спроектирован как распределенное решение:
| C# | 1
2
3
| // Redis через StackExchange.Redis
IDatabase db = connection.GetDatabase();
db.StringSet("key", "value", TimeSpan.FromMinutes(30)); |
|
Теперь поговорим конкретнее о типах данных Redis и их применении в C# проектах:
1. Строки (Strings) - самый простой тип данных. Идеально подходят для кеширования HTML-фрагментов, JSON-ответов API, результатов запросов. В C# часто используются для кеширования сериализованных объектов.
2. Хеши (Hashes) - структуры типа словаря, где каждый ключ содержит набор пар "поле-значение". Прекрасно подходят для хранения объектов с множеством свойств:
| C# | 1
2
3
4
5
6
| // Сохранение пользователя как хеша
db.HashSet("user:1001", new HashEntry[] {
new HashEntry("name", "Иван"),
new HashEntry("email", "ivan@example.com"),
new HashEntry("visits", 42)
}); |
|
3. Списки (Lists) - коллекции строк, упорядоченные по порядку вставки. Идеальны для реализации лент новостей, очередей сообщений, последних действий пользователя:
| C# | 1
2
3
4
5
| // Добавление элементов в список последних действий
db.ListRightPush("user:1001:actions", "logged_in");
db.ListRightPush("user:1001:actions", "updated_profile");
// Получение последних 5 действий
var recentActions = db.ListRange("user:1001:actions", -5, -1); |
|
4. Множества (Sets) - неупорядоченные коллекции уникальных строк. Отлично подходят для реализации тегов, категорий, уникальных посетителей:
| C# | 1
2
3
4
5
| // Добавление тегов к статье
db.SetAdd("article:1000:tags", "csharp", "redis", "performance");
// Нахождение общих тегов для двух статей
var commonTags = db.SetCombine(SetOperation.Intersect,
"article:1000:tags", "article:1001:tags"); |
|
5. Сортированные множества (Sorted Sets) - как обычные множества, но каждый элемент ассоциирован с рейтингом (score). Незаменимы для рейтингов, лидербордов, приоритезированных очередей:
| C# | 1
2
3
4
5
| // Добавление игроков с их счетом
db.SortedSetAdd("leaderboard", "player1", 1000);
db.SortedSetAdd("leaderboard", "player2", 2500);
// Получение топ-10 игроков
var topPlayers = db.SortedSetRangeByRankWithScores("leaderboard", 0, 9, Order.Descending); |
|
Redis не останавливается на базовой функциональности. Система модулей (Redis Modules) позволяет расширять возможности Redis для специфических задач. Некоторые из наиболее популярных:
RediSearch - полнотекстовый поисковый движок с поддержкой сложных запросов,
RedisJSON - нативная поддержка JSON с возможностью запросов в стиле PATCH,
RedisTimeSeries - для работы с временными рядами, идеально для метрик и мониторинга,
RedisAI - интеграция с моделями машинного обучения.
Эти модули превращают Redis из просто кеша в универсальное хранилище данных для широкого спектра задач.
Особо стоит отметить механизм Pub/Sub в Redis, который превращает его в легкую систему обмена сообщениями. В C# это выглядит примерно так:
| C# | 1
2
3
4
5
6
7
| // Подписка на канал
subscriber.Subscribe("notifications", (channel, message) => {
Console.WriteLine($"Получено сообщение: {message}");
});
// Публикация сообщения
db.Publish("notifications", "Привет, мир!"); |
|
Интеграция Redis с C#
Хотя Redis изначально разрабатывался для UNIX-подобных систем, существует форк от MSOpenTech, позволяющий запустить его на Windows. Для установки Redis на Windows понадобится 64-битная операционная система и пакетный менеджер Chocolatey. Установка предельно проста - запускаем командную строку с правами администратора и вводим:
После установки можно запустить сервер Redis командой redis-server.exe. Если команда не распознается, возможно, Chocolatey не смог добавить путь к Redis в переменную PATH. Исправить это можно вручную:
| PowerShell | 1
| SET PATH=%PATH%;"c:\Program Files\Redis" |
|
Теперь, когда у нас есть работающий экземпляр Redis, пора подключить его к нашему C#-приложению. Самой популярной и производительной библиотекой для этого является StackExchange.Redis, разработанная командой Stack Overflow. Установим ее через NuGet в наш проект:
| C# | 1
| Install-Package StackExchange.Redis |
|
Для тех, кто предпочитает графический интерфейс, можно просто найти и установить пакет через менеджер пакетов NuGet в Visual Studio. Важный момент при работе с Redis - правильное управление подключениями. StackExchange.Redis использует класс ConnectionMultiplexer для организации пула соединений. Его ключевая особеность - он создан для совместного использования во всем приложении, а не для создания нового экземпляра при каждом обращении к кешу.
Создание нового подключения при каждой операции не только негативно влияет на производительность, но и может быстро исчерпать лимит подключений, особенно если вы используете Redis от сторонних провайдеров типа Azure Cache.
Идеальный подход - реализовать синглтон для управления подключением. Вот пример класса-помощника:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| public class RedisConnectionHelper
{
static RedisConnectionHelper()
{
RedisConnectionHelper.lazyConnection = new Lazy<ConnectionMultiplexer>(() =>
{
return ConnectionMultiplexer.Connect("localhost");
});
}
private static Lazy<ConnectionMultiplexer> lazyConnection;
public static ConnectionMultiplexer Connection
{
get
{
return lazyConnection.Value;
}
}
} |
|
Этот класс использует механизм Lazy<T> для отложеной инициализации подключения, что позволяет создать соединение только при первом обращении к свойству Connection. Параметр конструктора ConnectionMultiplexer.Connect - строка подключения, которая может содержать множество настроек:
| C# | 1
2
| var options = "localhost,abortConnect=false,connectTimeout=3000,password=secret";
var connection = ConnectionMultiplexer.Connect(options); |
|
Здесь мы указываем хост, отключаем прерывание при ошибках подключения, устанавливаем таймаут в 3 секунды и пароль для аутентикации. В современных проектах на ASP.NET Core принято использовать механизм внедрения зависимостей (dependency injection). Добавить Redis в контейнер зависимостей можно следующим образом:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
| public void ConfigureServices(IServiceCollection services)
{
// Регистрируем ConnectionMultiplexer как singleton
services.AddSingleton<IConnectionMultiplexer>(sp =>
{
var configuration = ConfigurationOptions.Parse(Configuration.GetConnectionString("Redis"));
return ConnectionMultiplexer.Connect(configuration);
});
// Регистрируем сервис для работы с Redis
services.AddSingleton<IRedisCacheService, RedisCacheService>();
} |
|
Теперь мы можем внедрять IConnectionMultiplexer или наш сервис IRedisCacheService в любой компонент приложения:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| public class ProductController : Controller
{
private readonly IRedisCacheService _cacheService;
public ProductController(IRedisCacheService cacheService)
{
_cacheService = cacheService;
}
public async Task<IActionResult> GetProduct(int id)
{
// Пытаемся получить из кеша
var product = await _cacheService.GetAsync<Product>($"product:{id}");
if (product != null)
return Ok(product);
// Если нет в кеше, получаем из базы данных
product = await _dbService.GetProductByIdAsync(id);
// Кешируем результат на 1 час
await _cacheService.SetAsync($"product:{id}", product, TimeSpan.FromHours(1));
return Ok(product);
}
} |
|
Помимо StackExchange.Redis, существуют и другие библиотеки для работы с Redis в C#:
1. ServiceStack.Redis - более высокоуровневая библиотека с объектно-ориентированым API, включает ORM-подобные функции, но не бесплатна для комерческих проектов.
2. StackExchange.Redis.Extensions - расширение для StackExchange.Redis, добавляющее поддержку сериализации объектов и упрощающее работу с коллекциями.
3. Microsoft.Extensions.Caching.Redis - официальная библиотека от Microsoft, интегрирующая Redis с стандартным интерфейсом кеширования ASP.NET Core.
Базовая работа с данными в Redis через C# предельно проста. Получив экземпляр базы данных из ConnectionMultiplexer, мы можем выполнять все стандартные операции:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // Получаем базу данных
IDatabase db = connection.GetDatabase();
// Сохраняем строку
db.StringSet("mykey", "Hello, Redis!");
// Получаем строку
string value = db.StringGet("mykey");
// Устанавливаем время жизни
db.StringSet("tempkey", "I will expire", TimeSpan.FromMinutes(5));
// Проверяем существование ключа
bool exists = db.KeyExists("mykey");
// Удаляем ключ
db.KeyDelete("mykey"); |
|
Важной особеностью StackExchange.Redis является поддержка асинхронных операций, что критично для высоконагруженных приложений:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
| // Асинхронное сохранение
await db.StringSetAsync("asynckey", "Async value");
// Асинхронное получение
string asyncValue = await db.StringGetAsync("asynckey");
// Пакетные операции для повышения производительности
var batch = db.CreateBatch();
var task1 = batch.StringSetAsync("batch1", "value1");
var task2 = batch.StringSetAsync("batch2", "value2");
var task3 = batch.StringSetAsync("batch3", "value3");
batch.Execute();
await Task.WhenAll(task1, task2, task3); |
|
Библиотека также предоставляет удобные методы для работы со всеми типами данных Redis, которые мы рассмотрели ранее. Например, для хешей:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Сохраняем отдельные поля
db.HashSet("user:1001", "name", "Анна");
db.HashSet("user:1001", "email", "anna@example.com");
// Сохраняем все поля одновременно
db.HashSet("user:1002", new HashEntry[] {
new HashEntry("name", "Борис"),
new HashEntry("email", "boris@example.com"),
new HashEntry("age", 32)
});
// Получаем одно поле
string name = db.HashGet("user:1001", "name");
// Получаем все поля
HashEntry[] allFields = db.HashGetAll("user:1001");
// Удаляем поле
db.HashDelete("user:1001", "age");
// Проверяем существование поля
bool hasEmail = db.HashExists("user:1001", "email"); |
|
При работе с Redis через C# есть несколько важных нюансов, которые стоит учитывать. Во-первых, все ключи и значения в Redis - это бинарные данные, поэтому StackExchange.Redis использует типы RedisKey и RedisValue, которые могут неявно приводиться к строкам и обратно.
Во-вторых, для работы с более сложными объектами потребуется сериализация. Распространенные варианты - JSON (через Newtonsoft.Json или System.Text.Json) или бинарная сериализация (например, через протокол MessagePack или ProtoBuf).
Вот пример сервиса для работы с объектами через JSON-сериализацию:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| public class RedisCacheService : IRedisCacheService
{
private readonly IDatabase _db;
private readonly JsonSerializerOptions _options;
public RedisCacheService(IConnectionMultiplexer redis)
{
_db = redis.GetDatabase();
_options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
}
public async Task<T> GetAsync<T>(string key) where T : class
{
var value = await _db.StringGetAsync(key);
if (value.IsNull)
return null;
return JsonSerializer.Deserialize<T>(value, _options);
}
public async Task SetAsync<T>(string key, T value, TimeSpan? expiry = null) where T : class
{
var serialized = JsonSerializer.Serialize(value, _options);
await _db.StringSetAsync(key, serialized, expiry);
}
} |
|
Еще одна особенность Redis - механизм транзакций, который позволяет выполнить группу команд атомарно. В StackExchange.Redis это реализовано через интерфейс ITransaction:
| C# | 1
2
3
4
| ITransaction transaction = db.CreateTransaction();
Task<bool> t1 = transaction.StringSetAsync("counter", "1");
Task<bool> t2 = transaction.SetAddAsync("users:active", "user:1001");
bool committed = transaction.Execute(); |
|
Обратите внимание, что транзакции в Redis отличаются от классических ACID-транзакций в реляционных БД. Они гарантируют лиш атомарность, но не изоляцию в полном смысле - при ошибке в одной команде остальные всё равно выполнятся.
Отдельного внимания заслуживает оптимизация производительности при работе с Redis. Вот несколько советов:
1. Используйте пайплайны для последовательного выполнения несвязанных команд:
| C# | 1
2
3
4
5
6
7
8
| var batch = db.CreateBatch();
var tasks = new List<Task>();
for (int i = 0; i < 1000; i++)
{
tasks.Add(batch.StringSetAsync($"key:{i}", $"value:{i}"));
}
batch.Execute();
await Task.WhenAll(tasks); |
|
2. Применяйте мультиплексинг для параллельного выполнения команд на разных сокетах:
| C# | 1
2
3
4
5
6
7
| var config = new ConfigurationOptions
{
EndPoints = { "localhost:6379" },
CommandMap = CommandMap.Create(new HashSet<string>
{ // Включаем мультиплексинг
}, false),
}; |
|
3. Оптимизируйте сериализацию, используя бинарные форматы:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| // MessagePack часто быстрее JSON
using MessagePack;
[MessagePackObject]
public class User
{
[Key(0)]
public string Name { get; set; }
[Key(1)]
public int Age { get; set; }
}
// Сериализация
byte[] bytes = MessagePackSerializer.Serialize(user);
db.StringSet("user:1001", bytes);
// Десериализация
byte[] data = db.StringGet("user:1001");
User user = MessagePackSerializer.Deserialize<User>(data); |
|
При работе с Redis в продакшн-окружении критически важно настроить таймауты и механизмы повторных попыток. StackExchange.Redis предоставляет гибкие возможности для этого:
| C# | 1
2
3
4
5
| var options = ConfigurationOptions.Parse("localhost:6379");
options.ConnectTimeout = 5000; // 5 секунд
options.SyncTimeout = 10000; // 10 секунд
options.ConnectRetry = 3; // количество повторных попыток
options.ReconnectRetryPolicy = new ExponentialRetry(5000); // Экспоненциальная задержка |
|
Для защиты от падения производительности при временных сбоях сети стоит реализовать паттерн Circuit Breaker:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| private readonly CircuitBreakerPolicy _circuitBreaker = Policy
.Handle<RedisConnectionException>()
.CircuitBreaker(
exceptionsAllowedBeforeBreaking: 3,
durationOfBreak: TimeSpan.FromSeconds(30)
);
public async Task<T> GetWithResilienceAsync<T>(string key) where T : class
{
return await _circuitBreaker.ExecuteAsync(async () =>
{
var value = await _db.StringGetAsync(key);
if (value.IsNull)
return null;
return JsonSerializer.Deserialize<T>(value);
});
} |
|
Это требует подключения библиотеки Polly, но значительно повышает надежность вашего приложения.
Для многих проектов также важна способность мониторить статус Redis и диагностировать проблемы. StackExchange.Redis предлагает несколько инструментов для этого:
| C# | 1
2
3
4
5
6
7
8
9
10
11
| // Получение статистики подключения
ConnectionMultiplexerStats stats = connection.GetCounters();
// Профилирование команд
connection.RegisterProfiler(new RedisProfiler());
// Обработка событий
connection.ConnectionFailed += (sender, e) =>
{
Logger.Error($"Redis connection failed: {e.Exception.Message}");
}; |
|
И наконец, когда мы работаем с Redis в масштабных распределенных приложениях, стоит подумать об оркестрации множественных экземпляров Redis через Redis Sentinel или Redis Cluster:
| C# | 1
2
3
4
5
6
7
8
| var sentinelOptions = ConfigurationOptions.Parse("sentinel-1:26379,sentinel-2:26379");
sentinelOptions.ServiceName = "mymaster"; // Имя группы мастер-реплика
sentinelOptions.CommandMap = CommandMap.Sentinel;
sentinelOptions.TieBreaker = ""; // Отключаем tie-breaker для Sentinel
var sentinelConnection = ConnectionMultiplexer.Connect(sentinelOptions);
var masterConnection = sentinelConnection.GetServer(sentinelOptions.EndPoints[0])
.SentinelGetMasterAddressByName("mymaster"); |
|
Интеграция Redis с C# через современные библиотеки открывает огромные возможности для оптимизации производительности и масштабирования приложений, но требует внимания к деталям и понимания нюансов как самого Redis, так и используемых клиентских библиотек.
Внесение и считывание данных из БД Redis Здравствуйте!
Есть задание - создать консольное приложение, подключающееся к БД Redis и... Свой прокси сервер с поддержкой кэш Здравствуйте друзья есть ли у кого из вас исходник на C# прокси сервера выложите или дайте ссылку. Как программно отключить\очистить DNS-кэш Задача в том чтобы выполнять DNS resolve по указанному хосту и засекать время резолва.
Проблемма -... Где хранится кэш и как с ним работать Программа периодически грузит несколько указанных изображений из инета с помощью класса...
Практические сценарии использования
Разберем наиболее распространенные и полезные способы использования Redis в реальных C# проектах.
Кеширование результатов запросов к базе данных
Самый очевидный и распространенный сценарий - кеширование результатов запросов к основной базе данных. Представьте интернет-магазин, где информация о популярных товарах запрашивается тысячи раз в минуту. Каждый такой запрос к SQL базе данных создает нагрузку и потребляет ресурсы.
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| public async Task<Product> GetProductByIdAsync(int id)
{
string cacheKey = $"product:{id}";
// Пробуем получить из кеша
var cachedProduct = await _redis.StringGetAsync(cacheKey);
if (!cachedProduct.IsNull)
{
return JsonSerializer.Deserialize<Product>(cachedProduct);
}
// Если в кеше нет, получаем из базы данных
var product = await _dbContext.Products
.Include(p => p.Category)
.Include(p => p.Reviews)
.FirstOrDefaultAsync(p => p.Id == id);
if (product != null)
{
// Кешируем результат на 30 минут
await _redis.StringSetAsync(
cacheKey,
JsonSerializer.Serialize(product),
TimeSpan.FromMinutes(30)
);
}
return product;
} |
|
Этот паттерн (Cache-Aside или Lazy Loading) позволяет существенно снизить нагрузку на базу данных и улучшить отзывчивость приложения. Однако есть важный нюанс - согласованость данных. Что делать, если товар изменился? Нужно инвалидировать кеш:
| C# | 1
2
3
4
5
6
7
8
9
| public async Task UpdateProductAsync(Product product)
{
// Обновляем в БД
_dbContext.Products.Update(product);
await _dbContext.SaveChangesAsync();
// Инвалидируем кеш
await _redis.KeyDeleteAsync($"product:{product.Id}");
} |
|
Распределенные блокировки
В распределенных системах часто возникает необходимость синхронизировать доступ к ресурсам между разными экземплярами приложения. Redis предоставляет механизм распределенных блокировок, который намного надежнее, чем самописные решения:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
| public async Task ProcessPayment(string orderId)
{
string lockKey = $"lock:order:{orderId}";
string lockValue = Guid.NewGuid().ToString();
// Пытаемся получить блокировку
bool acquired = await _redis.StringSetAsync(
lockKey,
lockValue,
TimeSpan.FromSeconds(30),
When.NotExists
);
if (!acquired)
{
throw new ConcurrencyException("Заказ уже обрабатывается");
}
try
{
// Выполняем критическую операцию
await _paymentService.ProcessAsync(orderId);
}
finally
{
// Атомарно освобождаем блокировку, только если это наша блокировка
string script = @"
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
";
await _redis.ScriptEvaluateAsync(
script,
new RedisKey[] { lockKey },
new RedisValue[] { lockValue }
);
}
} |
|
Обратите внимание на использование Lua-скрипта для атомарного освобождения блокировки - это предотвращает ситуацию, когда наша блокировка истекла и была занята другим процессом, а мы случайно освободили чужую блокировку.
Сессии пользователей
Хранение сессий пользователей - еще один классический сценарий использования Redis. В отличие от стандартного ASP.NET сессионного состояния, Redis-сессии доступны из любого экземпляра приложения, что критично при горизонтальном масштабировании:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // Настройка в Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379";
options.InstanceName = "MyApp:";
});
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
}
// Использование в контроллере
public IActionResult AddToCart(int productId)
{
// Получаем текущую корзину из сессии
var cart = HttpContext.Session.Get<ShoppingCart>("Cart") ?? new ShoppingCart();
// Модифицируем корзину
cart.AddItem(productId);
// Сохраняем обратно в сессию
HttpContext.Session.Set("Cart", cart);
return RedirectToAction("Index");
} |
|
Счетчики и рейтинги в реальном времени
Redis отлично подходит для ведения различных счетчиков и рейтингов, особенно когда требуется высокая производительность записи и атомарные операции:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| // Увеличиваем счетчик просмотров статьи
await _redis.StringIncrementAsync($"article:{articleId}:views");
// Добавляем оценку пользователя и пересчитываем рейтинг
await _redis.SortedSetAddAsync(
$"product:{productId}:ratings",
userId,
rating
);
// Получаем средний рейтинг
double avgRating = await _redis.SortedSetScoreAsync(
$"product:{productId}:ratings",
"avg"
) ?? 0;
// Получаем топ-5 лучших продуктов
var topProducts = await _redis.SortedSetRangeByScoreAsync(
"products:ratings",
order: Order.Descending,
take: 5
); |
|
Кеширование API ответов
Для RESTful API с высокой нагрузкой кеширование ответов критично. Можно реализовать специальный middleware для ASP.NET Core:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| public class RedisCacheMiddleware
{
private readonly RequestDelegate _next;
private readonly IDatabase _redis;
public RedisCacheMiddleware(RequestDelegate next, IConnectionMultiplexer redis)
{
_next = next;
_redis = redis.GetDatabase();
}
public async Task InvokeAsync(HttpContext context)
{
// Только для GET-запросов
if (context.Request.Method != "GET")
{
await _next(context);
return;
}
string cacheKey = $"api:{context.Request.Path}{context.Request.QueryString}";
// Проверяем кеш
var cachedResponse = await _redis.StringGetAsync(cacheKey);
if (!cachedResponse.IsNull)
{
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(cachedResponse);
return;
}
// Запоминаем оригинальный поток ответа
var originalBody = context.Response.Body;
try
{
// Создаем новый поток для чтения ответа
using var memStream = new MemoryStream();
context.Response.Body = memStream;
// Продолжаем выполнение запроса
await _next(context);
// Если статус успешный, кешируем ответ
if (context.Response.StatusCode == 200)
{
memStream.Position = 0;
using var reader = new StreamReader(memStream);
var responseBody = await reader.ReadToEndAsync();
await _redis.StringSetAsync(
cacheKey,
responseBody,
TimeSpan.FromMinutes(5)
);
memStream.Position = 0;
await memStream.CopyToAsync(originalBody);
}
}
finally
{
context.Response.Body = originalBody;
}
}
} |
|
Rate limiting и защита от DDoS
Redis позволяет реализовать эффективное ограничение частоты запросов (rate limiting), критически важное для защиты API от злоупотреблений и DDoS-атак:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
| public async Task<bool> CheckRateLimitAsync(string clientIp, int maxRequests, TimeSpan period)
{
string key = $"ratelimit:{clientIp}";
var tran = _redis.CreateTransaction();
var countTask = tran.StringIncrementAsync(key);
var expireTask = tran.KeyExpireAsync(key, period);
await tran.ExecuteAsync();
long requestCount = await countTask;
return requestCount <= maxRequests;
} |
|
Этот простой метод можно интегрировать в middleware для автоматической проверки всех входящих запросов:
| C# | 1
2
3
4
5
6
7
8
9
| // В контроллере или middleware
if (!await _rateLimit.CheckRateLimitAsync(
HttpContext.Connection.RemoteIpAddress.ToString(),
100, // Максимум 100 запросов
TimeSpan.FromMinutes(1) // За 1 минуту
))
{
return StatusCode(429, "Too Many Requests");
} |
|
Для более полного понимания возможностей Redis, разберем и другие востребованные сценарии его применения с C#.
Очереди сообщений и отложенные задачи
Redis превосходно справляется с ролью легковесной очереди сообщений. Особенно это полезно для фоновых и отложенных задач, когда нужно распределить нагрузку:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| // Добавление задачи в очередь
await _redis.ListRightPushAsync("tasks:email", JsonSerializer.Serialize(new EmailTask
{
To = "user@example.com",
Subject = "Ваш заказ отправлен",
Body = "Детали заказа...",
Priority = 2
}));
// Обработчик задач (в отдельном процессе или сервисе)
while (true)
{
// Извлекаем задачу с блокировкой до 5 секунд
var taskJson = await _redis.ListLeftPopAsync("tasks:email", TimeSpan.FromSeconds(5));
if (taskJson.IsNull)
continue;
var task = JsonSerializer.Deserialize<EmailTask>(taskJson);
try
{
await _emailService.SendAsync(task.To, task.Subject, task.Body);
}
catch (Exception ex)
{
// При ошибке возвращаем задачу в очередь или в отдельную очередь для ошибок
await _redis.ListRightPushAsync("tasks:email:failed", taskJson);
_logger.LogError(ex, "Ошибка при отправке email");
}
} |
|
Если вам нужны отложенные задачи, можно использовать сортированное множество с временными метками в качестве score:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| // Планирование задачи на конкретное время
var executeAt = DateTimeOffset.Now.AddHours(1).ToUnixTimeSeconds();
await _redis.SortedSetAddAsync("scheduled:tasks", taskId, executeAt);
// В сервисе-обработчике
while (true)
{
var now = DateTimeOffset.Now.ToUnixTimeSeconds();
// Извлекаем все задачи, время выполнения которых наступило
var dueTasks = await _redis.SortedSetRangeByScoreAsync(
"scheduled:tasks",
0,
now,
Exclude.None,
Order.Ascending,
0,
10
);
if (dueTasks.Length == 0)
{
await Task.Delay(1000);
continue;
}
foreach (var taskId in dueTasks)
{
// Атомарно удаляем задачу из очереди отложенных
bool removed = await _redis.SortedSetRemoveAsync("scheduled:tasks", taskId);
if (removed)
{
// Задача успешно удалена, можно выполнять
await ProcessTask(taskId.ToString());
}
}
} |
|
Геоданные и поиск по местоположению
Redis имеет встроеную поддержку геопространственных данных, что делает его идеальным для приложений с геолокацией:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // Добавляем местоположения магазинов
await _redis.GeoAddAsync("stores",
new GeoEntry(37.7749, -122.4194, "store:1"), // Сан-Франциско
new GeoEntry(40.7128, -74.0060, "store:2"), // Нью-Йорк
new GeoEntry(34.0522, -118.2437, "store:3") // Лос-Анджелес
);
// Находим ближайшие магазины к пользователю
var nearbyStores = await _redis.GeoRadiusAsync(
"stores",
37.7833, -122.4167, // Координаты пользователя
50, GeoUnit.Kilometers,
order: Order.Ascending,
options: GeoRadiusOptions.WithDistance,
count: 5
);
foreach (var store in nearbyStores)
{
Console.WriteLine($"Магазин {store.Member}: {store.Distance} км");
} |
|
Pub/Sub для обновлений в реальном времени
Redis Pub/Sub - отличный механизм для уведомлений и обновлений в реальном времени:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // Настройка подписки в фоновом сервисе
public async Task StartListeningAsync(CancellationToken cancellationToken)
{
var subscriber = _redis.GetSubscriber();
await subscriber.SubscribeAsync("notifications:system", (channel, message) => {
_notificationService.BroadcastToClients(message.ToString());
});
await subscriber.SubscribeAsync($"notifications:user:{userId}", (channel, message) => {
_notificationService.SendToUser(userId, message.ToString());
});
// Предотвращаем завершение задачи
await Task.Delay(Timeout.Infinite, cancellationToken);
}
// Отправка уведомления
public async Task NotifySystemEvent(string message)
{
var subscriber = _redis.GetSubscriber();
await subscriber.PublishAsync("notifications:system", message);
} |
|
Битовые операции для компактного хранения состояний
Redis позволяет эффективно работать с битовыми картами, что полезно для хранения статусов, флагов и компактного представления данных:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Отмечаем дни активности пользователя (1 бит на день)
await _redis.StringSetBitAsync($"user:{userId}:active:2023-11", dayOfMonth - 1, true);
// Проверяем, был ли пользователь активен в конкретный день
bool wasActive = await _redis.StringGetBitAsync($"user:{userId}:active:2023-11", dayOfMonth - 1);
// Подсчитываем количество дней активности
long activeDays = await _redis.StringBitCountAsync($"user:{userId}:active:2023-11");
// Находим пересечение активных дней для группы пользователей
await _redis.StringBitOperationAsync(
Bitwise.And,
"team:active:2023-11",
new RedisKey[] { "user:1:active:2023-11", "user:2:active:2023-11", "user:3:active:2023-11" }
); |
|
Такое эффективное представление данных особенно ценно при работе с большими объемами информации и необходимости экономии памяти.
Продвинутые техники
Паттерны кеширования
Мы уже касались паттерна Cache-Aside (Lazy Loading), но стоит рассмотреть и другие подходы:
Write-Through — данные сначала записываются в кеш, а затем автоматически и в основное хранилище:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
| public async Task SaveProduct(Product product)
{
// Сначала сохраняем в Redis
await _redis.StringSetAsync(
$"product:{product.Id}",
JsonSerializer.Serialize(product)
);
// Затем в основную БД
_dbContext.Products.Update(product);
await _dbContext.SaveChangesAsync();
} |
|
Write-Behind (Write-Back) — запись сначала в кеш, а в БД — асинхронно, с задержкой:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| public async Task SaveProductWithDelay(Product product)
{
// Сохраняем в Redis
await _redis.StringSetAsync($"product:{product.Id}", JsonSerializer.Serialize(product));
// Добавляем в очередь на запись в БД
await _redis.ListRightPushAsync("db:write:queue", JsonSerializer.Serialize(new
{
Type = "Product",
Id = product.Id,
Action = "Update",
Timestamp = DateTime.UtcNow
}));
}
// В фоновом сервисе
private async Task ProcessWriteQueue()
{
while (!_cancellationToken.IsCancellationRequested)
{
var item = await _redis.ListLeftPopAsync("db:write:queue");
if (!item.IsNull)
{
var writeOperation = JsonSerializer.Deserialize<dynamic>(item);
await ProcessDatabaseWrite(writeOperation);
}
else
{
await Task.Delay(100);
}
}
} |
|
Стратегии инвалидации кеша
Время жизни (TTL) — не единственный способ управления актуальностью данных:
1. Инвалидация по событиям — самая точная, но требует тщательного отслеживания зависимостей:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| // При обновлении продукта
public async Task UpdateProduct(Product product)
{
// Обновляем в БД
_dbContext.Products.Update(product);
await _dbContext.SaveChangesAsync();
// Инвалидируем прямые ключи
await _redis.KeyDeleteAsync($"product:{product.Id}");
// Инвалидируем зависимые ключи
await _redis.KeyDeleteAsync($"category:{product.CategoryId}:products");
await _redis.KeyDeleteAsync("products:featured");
// Оповещаем другие экземпляры приложения
await _redis.PublishAsync("cache:invalidate", $"product:{product.Id}");
} |
|
2. Версионирование кеша — добавляем версию к ключу:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| public async Task<List<Product>> GetCategoryProducts(int categoryId)
{
// Получаем текущую версию для категории
long version = await _redis.StringGetAsync($"category:{categoryId}:version") ?? 1;
// Формируем ключ с версией
string cacheKey = $"category:{categoryId}:products:v{version}";
var cachedData = await _redis.StringGetAsync(cacheKey);
if (!cachedData.IsNull)
return JsonSerializer.Deserialize<List<Product>>(cachedData);
// Получаем из БД и кешируем
var products = await _dbContext.Products
.Where(p => p.CategoryId == categoryId)
.ToListAsync();
await _redis.StringSetAsync(cacheKey, JsonSerializer.Serialize(products), TimeSpan.FromHours(1));
return products;
}
// При изменении категории увеличиваем версию
public async Task UpdateCategory(Category category)
{
// Обновление в БД
// ...
// Инкрементируем версию
await _redis.StringIncrementAsync($"category:{category.Id}:version");
} |
|
Оптимизация памяти и eviction политики
Redis предоставляет несколько политик вытеснения данных при достижении лимита памяти:
| C# | 1
2
3
4
5
6
7
| // Настройка в redis.conf
// maxmemory 1gb
// maxmemory-policy allkeys-lru
// Мониторинг использования памяти из C#
var info = await _redis.GetServer("localhost:6379").InfoAsync();
string usedMemory = info.First(i => i.Key == "used_memory_human").Value; |
|
Для экономии памяти при работе с сериализоваными объектами в C# можно:
1. Использовать компактную сериализацию (MessagePack, ProtoBuf).
2. Применять сжатие для больших данных:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| public async Task<T> GetCompressedAsync<T>(string key) where T : class
{
var compressedData = await _redis.StringGetAsync(key);
if (compressedData.IsNull)
return null;
// Распаковываем
using var ms = new MemoryStream(compressedData);
using var zip = new GZipStream(ms, CompressionMode.Decompress);
using var resultStream = new MemoryStream();
await zip.CopyToAsync(resultStream);
resultStream.Position = 0;
return JsonSerializer.Deserialize<T>(resultStream);
}
public async Task SetCompressedAsync<T>(string key, T value, TimeSpan? expiry = null) where T : class
{
using var ms = new MemoryStream();
using (var zip = new GZipStream(ms, CompressionLevel.Optimal, true))
{
JsonSerializer.Serialize(zip, value);
}
await _redis.StringSetAsync(key, ms.ToArray(), expiry);
} |
|
Lua-скрипты для атомарных операций
Lua-скрипты — мощный инструмент для выполнения сложной логики на стороне Redis:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| public async Task<bool> UpdateIfNotChanged(string key, string newValue, string expectedValue)
{
string script = @"
if redis.call('get', KEYS[1]) == ARGV[1] then
redis.call('set', KEYS[1], ARGV[2])
return 1
else
return 0
end
";
var result = await _redis.ScriptEvaluateAsync(
script,
new RedisKey[] { key },
new RedisValue[] { expectedValue, newValue }
);
return (long)result == 1;
} |
|
Lua особено полезна для реализации счетчиков с ограничением, атомарных инкрементов с проверками условий и других сложных операций, которые должны выполняться как единое целое.
Кластеризация и репликация
Для высоконагруженных систем одного сервера Redis недостаточно. Redis Cluster обеспечивает горизонтальное масштабирование:
| C# | 1
2
3
4
5
6
7
8
| var options = ConfigurationOptions.Parse("redis1:7000,redis2:7001,redis3:7002");
options.CommandMap = CommandMap.Create(new HashSet<string>() { "CLUSTER" }, available: false);
var cluster = ConnectionMultiplexer.Connect(options);
var db = cluster.GetDatabase();
// Остальной код работает точно так же, библиотека сама
// определяет нужную ноду кластера для ключа |
|
Кластер автоматически распределяет данные между нодами, используя алгоритм консистентного хеширования, что обеспечивает предсказуемое расположение ключей и оптимальную производительность.
Помимо Redis Cluster, стоит рассмотреть и другую модель масштабирования - репликацию "мастер-реплика". В отличие от кластера, она не распределяет данные, а дублирует их на всех серверах, обеспечивая высокую доступность чтения:
| C# | 1
2
3
4
5
6
7
8
9
10
| var options = ConfigurationOptions.Parse("master:6379,replica1:6379,replica2:6379");
options.AllowAdmin = true;
var connection = ConnectionMultiplexer.Connect(options);
// Для записи используем только мастер
var master = connection.GetServer("master:6379");
var db = connection.GetDatabase();
// Для чтения можно использовать любой сервер, включая реплики
// Библиотека автоматически распределяет запросы чтения |
|
Для надежной работы репликации в промышленном окружении обычно используется Redis Sentinel - система мониторинга и автоматического переключения при отказе мастера:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| var sentinelOptions = ConfigurationOptions.Parse("sentinel1:26379,sentinel2:26379,sentinel3:26379");
sentinelOptions.ServiceName = "mymaster"; // имя группы серверов в конфигурации Sentinel
sentinelOptions.TieBreaker = "";
sentinelOptions.CommandMap = CommandMap.Sentinel;
using var sentinelConnection = ConnectionMultiplexer.Connect(sentinelOptions);
var sentinels = sentinelConnection.GetEndPoints().Select(e => sentinelConnection.GetServer(e));
// Получаем адрес текущего мастера
RedisServer currentMaster = null;
foreach (var sentinel in sentinels)
{
if (!sentinel.IsConnected)
continue;
try
{
var masterEndpoint = sentinel.SentinelGetMasterAddressByName("mymaster");
currentMaster = connection.GetServer(masterEndpoint);
break;
}
catch
{
continue;
}
}
// Настраиваем обработчик событий переключения мастера
connection.ConfigurationChanged += (s, e) => {
_logger.LogInformation("Redis configuration changed!");
// Перенастройка подключений или обновление кеша
}; |
|
Redis Streams для обработки событий
Redis Streams, появившиеся в версии 5.0, представляют собой аппенд-онли структуру данных, идеально подходящую для реализации шаблона "публикация-подписка" с сохранением истории сообщений:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| // Публикация события в поток
string messageId = await _redis.StreamAddAsync(
"orders:events",
new NameValueEntry[] {
new NameValueEntry("type", "created"),
new NameValueEntry("orderId", "12345"),
new NameValueEntry("amount", "299.99"),
new NameValueEntry("timestamp", DateTime.UtcNow.ToString("o"))
}
);
// Чтение событий из потока
StreamEntry[] entries = await _redis.StreamReadAsync("orders:events", "0-0");
foreach (var entry in entries)
{
string type = entry.Values.FirstOrDefault(v => v.Name == "type").Value;
string orderId = entry.Values.FirstOrDefault(v => v.Name == "orderId").Value;
// Обработка события
ProcessOrderEvent(type, orderId);
} |
|
Но настоящая сила Streams проявляется при использовании групп потребителей, позволяющих распределить обработку между несколькими процессами:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| // Создание группы потребителей
await _redis.StreamCreateConsumerGroupAsync("orders:events", "order-processors", "0-0");
// Чтение необработанных сообщений группой
StreamEntry[] pendingMessages = await _redis.StreamReadGroupAsync(
"orders:events",
"order-processors",
"consumer-1",
">", // Специальный ID для новых сообщений
10 // Максимальное количество сообщений
);
// Подтверждение обработки
foreach (var message in pendingMessages)
{
try
{
await ProcessMessage(message);
await _redis.StreamAcknowledgeAsync("orders:events", "order-processors", message.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Ошибка обработки сообщения {message.Id}");
}
}
// Получение и повторная обработка зависших сообщений
StreamPendingInfo pending = await _redis.StreamPendingAsync("orders:events", "order-processors");
if (pending.Count > 0)
{
StreamPendingMessageInfo[] pendingDetails = await _redis.StreamPendingMessagesAsync(
"orders:events",
"order-processors",
10, // Количество сообщений
"consumer-1", // Можно указать конкретного потребителя или null для всех
10000 // Минимальное время ожидания в миллисекундах
);
foreach (var msg in pendingDetails)
{
// Повторное получение и обработка
var messageData = await _redis.StreamRangeAsync("orders:events", msg.MessageId, msg.MessageId);
await ProcessMessage(messageData.First());
await _redis.StreamAcknowledgeAsync("orders:events", "order-processors", msg.MessageId);
}
} |
|
Безопасность Redis
Безопасность Redis долгое время была его ахиллесовой пятой, но современные версии предлагают несколько уровней защиты:
| C# | 1
2
3
4
5
6
| // Подключение с аутентификацией
var secureOptions = ConfigurationOptions.Parse("redis-server:6379,password=секретный_пароль");
secureOptions.Ssl = true; // Включаем шифрование TLS
secureOptions.SslHost = "redis.example.com"; // Проверка сертификата по имени хоста
var secureConnection = ConnectionMultiplexer.Connect(secureOptions); |
|
Для гранулярного контроля доступа в Redis 6.0+ можно использовать управление доступом на основе ролей (ACL):
| C# | 1
2
| // Выполнение ACL команд через C#
await _redis.GetServer("redis-server:6379").ExecuteAsync("ACL", "SETUSER", "readonly", "on", ">password", "~*", "+@read"); |
|
Эта команда создает пользователя "readonly" с паролем "password", дает ему доступ ко всем ключам ("~*") и разрешает только операции чтения ("+@read").
Транзакции и пакетные операции
Для повышения производительности при выполнении множества операций используются транзакции и пакеты:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| // Транзакция (MULTI/EXEC)
var transaction = _redis.CreateTransaction();
var task1 = transaction.StringSetAsync("key1", "value1");
var task2 = transaction.HashSetAsync("hash1", new HashEntry[] { new HashEntry("field1", "value1") });
var task3 = transaction.SetAddAsync("set1", "member1");
bool committed = await transaction.ExecuteAsync();
if (committed)
{
// Все команды выполнены успешно
bool task1Result = await task1;
bool task2Result = await task2;
bool task3Result = await task3;
}
else
{
// Транзакция не выполнена
}
// Пакет (без гарантии атомарности, но с одним сетевым запросом)
var batch = _redis.CreateBatch();
var batchTask1 = batch.StringSetAsync("key2", "value2");
var batchTask2 = batch.HashSetAsync("hash2", new HashEntry[] { new HashEntry("field2", "value2") });
batch.Execute();
// Ожидаем завершения операций
await Task.WhenAll(batchTask1, batchTask2); |
|
Главное отличие между транзакцией и пакетом - транзакция гарантирует атомарность (либо все команды выполняются, либо ни одна), тогда как пакет просто объединяет команды в один сетевой запрос без гарантии атомарности.
Шардирование данных для больших систем
В масштабных проектах даже возможностей Redis Cluster может быть недостаточно. Когда объем данных превышает возможности кластера или требуется изоляция между разными типами данных, применяется ручное шардирование:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| public class ShardedRedisService
{
private readonly List<IConnectionMultiplexer> _shardConnections;
private readonly int _shardCount;
public ShardedRedisService(IConfiguration config)
{
_shardCount = config.GetValue<int>("Redis:ShardCount");
_shardConnections = new List<IConnectionMultiplexer>(_shardCount);
for (int i = 0; i < _shardCount; i++)
{
var connection = ConnectionMultiplexer.Connect(
config.GetConnectionString($"Redis:Shard{i}")
);
_shardConnections.Add(connection);
}
}
public IDatabase GetShardForKey(string key)
{
// Используем консистентное хеширование для определения шарда
int shardIndex = Math.Abs(key.GetHashCode()) % _shardCount;
return _shardConnections[shardIndex].GetDatabase();
}
public async Task<T> GetAsync<T>(string key) where T : class
{
var db = GetShardForKey(key);
var value = await db.StringGetAsync(key);
return value.IsNull ? null : JsonSerializer.Deserialize<T>(value);
}
public async Task SetAsync<T>(string key, T value, TimeSpan? expiry = null) where T : class
{
var db = GetShardForKey(key);
await db.StringSetAsync(
key,
JsonSerializer.Serialize(value),
expiry
);
}
} |
|
Для более предсказуемого распределения ключей можно использовать различные алгоритмы шардирования:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // Шардирование по префиксу ключа
public IDatabase GetShardByKeyPrefix(string key)
{
if (key.StartsWith("user:"))
return _shardConnections[0].GetDatabase();
else if (key.StartsWith("product:"))
return _shardConnections[1].GetDatabase();
else if (key.StartsWith("order:"))
return _shardConnections[2].GetDatabase();
else
return _shardConnections[3].GetDatabase(); // Все остальное
}
// Шардирование по диапазону значений
public IDatabase GetShardByRange(int userId)
{
if (userId < 10000)
return _shardConnections[0].GetDatabase();
else if (userId < 20000)
return _shardConnections[1].GetDatabase();
else
return _shardConnections[2].GetDatabase();
} |
|
Мониторинг и управление Redis
Эффективная работа с Redis в продакшне невозможна без надежного мониторинга. В C# это можно реализовать так:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| public async Task<RedisHealthStatus> CheckRedisHealthAsync()
{
var status = new RedisHealthStatus();
try
{
var server = _connection.GetServer(_connection.GetEndPoints().First());
var info = await server.InfoAsync();
status.MemoryUsed = info.First(x => x.Key == "used_memory_human").Value;
status.TotalConnections = int.Parse(info.First(x => x.Key == "connected_clients").Value);
status.UptimeInSeconds = int.Parse(info.First(x => x.Key == "uptime_in_seconds").Value);
status.CommandsPerSecond = int.Parse(info.First(x => x.Key == "instantaneous_ops_per_sec").Value);
status.KeyspaceHits = int.Parse(info.First(x => x.Key == "keyspace_hits").Value);
status.KeyspaceMisses = int.Parse(info.First(x => x.Key == "keyspace_misses").Value);
status.IsHealthy = true;
// Расчет hit ratio
long totalLookups = status.KeyspaceHits + status.KeyspaceMisses;
status.HitRatio = totalLookups > 0
? (double)status.KeyspaceHits / totalLookups
: 0;
// Проверка критических показателей
if (status.HitRatio < 0.8)
{
_logger.LogWarning($"Redis cache hit ratio is low: {status.HitRatio:P}");
}
}
catch (Exception ex)
{
status.IsHealthy = false;
status.ErrorMessage = ex.Message;
_logger.LogError(ex, "Failed to check Redis health");
}
return status;
} |
|
Для экспорта метрик Redis в системы мониторинга вроде Prometheus можно использовать:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| [ApiController]
[Route("metrics")]
public class MetricsController : ControllerBase
{
private readonly IConnectionMultiplexer _redis;
public MetricsController(IConnectionMultiplexer redis)
{
_redis = redis;
}
[HttpGet("redis")]
public async Task<IActionResult> GetRedisMetrics()
{
var sb = new StringBuilder();
var server = _redis.GetServer(_redis.GetEndPoints().First());
var info = await server.InfoAsync();
sb.AppendLine("# HELP redis_used_memory Redis used memory in bytes");
sb.AppendLine("# TYPE redis_used_memory gauge");
sb.AppendLine($"redis_used_memory {info.First(x => x.Key == "used_memory").Value}");
sb.AppendLine("# HELP redis_connected_clients Redis connected clients");
sb.AppendLine("# TYPE redis_connected_clients gauge");
sb.AppendLine($"redis_connected_clients {info.First(x => x.Key == "connected_clients").Value}");
// Другие метрики...
return Content(sb.ToString(), "text/plain");
}
} |
|
Продвинутые сценарии с Lua-скриптами
Одно из самых мощных применений Lua-скриптов в Redis — создание собственных высокопроизводительных алгоритмов прямо на сервере:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| // Реализация механизма скользящего окна для rate limiting
public async Task<bool> CheckRateLimitSlidingWindow(string key, int maxRequests, int windowSeconds)
{
string script = @"
local key = KEYS[1]
local now = tonumber(ARGV[1])
local windowSize = tonumber(ARGV[2])
local maxCount = tonumber(ARGV[3])
-- Удаляем устаревшие записи
redis.call('ZREMRANGEBYSCORE', key, 0, now - windowSize)
-- Получаем текущее количество запросов
local count = redis.call('ZCARD', key)
-- Проверяем, не превышен ли лимит
if count < maxCount then
-- Добавляем новый запрос
redis.call('ZADD', key, now, now .. '-' .. math.random())
-- Обновляем TTL ключа
redis.call('EXPIRE', key, windowSize)
return 1
else
return 0
end
";
int now = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var result = await _redis.ScriptEvaluateAsync(
script,
new RedisKey[] { key },
new RedisValue[] { now, windowSeconds, maxRequests }
);
return (long)result == 1;
} |
|
Этот скрипт реализует более точный механизм ограничения частоты запросов с использованием "скользящего окна", а не простого счетчика с TTL.
Подводные камни и решения
Работа с Redis, как и с любой технологией, не обходится без сюрпризов и неочевидных проблем. Опыт показывает, что многие разработчики наступают на одни и те же грабли, особенно когда используют Redis с C# в промышленных масштабах.
Одна из самых распространеных проблем - утечки соединений. StackExchange.Redis управляет пулом соединений внутри объекта ConnectionMultiplexer, но многие разработчики по привычке создают его заново для каждой операции:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Неправильно: утечка соединений
public string GetValue(string key)
{
var redis = ConnectionMultiplexer.Connect("localhost");
var db = redis.GetDatabase();
return db.StringGet(key);
// Не вызывается Dispose для ConnectionMultiplexer!
}
// Правильно: использование синглтона
private static Lazy<ConnectionMultiplexer> _connection =
new Lazy<ConnectionMultiplexer>(() => ConnectionMultiplexer.Connect("localhost"));
public string GetValue(string key)
{
var db = _connection.Value.GetDatabase();
return db.StringGet(key);
} |
|
Еще одна головная боль - непредвиденные исключения при работе с Redis. Сетевые сбои случаются всегда в самый неподходящий момент, и необработанное исключение RedisConnectionException может положить все приложение:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| // Пример с обработкой исключений и повторными попытками
public async Task<string> GetValueWithRetry(string key)
{
int maxRetries = 3;
int retryCount = 0;
TimeSpan delay = TimeSpan.FromMilliseconds(200);
while (true)
{
try
{
var db = _connection.Value.GetDatabase();
return await db.StringGetAsync(key);
}
catch (RedisConnectionException ex)
{
retryCount++;
if (retryCount > maxRetries)
throw new CacheException("Не удалось подключиться к Redis после нескольких попыток", ex);
_logger.LogWarning($"Ошибка подключения к Redis: {ex.Message}. Повторная попытка {retryCount}/{maxRetries}");
await Task.Delay(delay);
delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2); // Экспоненциальная задержка
}
}
} |
|
Большой проблемой становится неправильная стратегия сериализации. Многие сохраняют в Redis сложные объекты с глубокой вложенностью, что приводит к раздуванию объема данных и падению производительности:
| C# | 1
2
3
4
5
6
7
8
9
10
11
| // Плохо: сохраняем весь объект с вложенными коллекциями
await _redis.StringSetAsync($"product:{id}", JsonSerializer.Serialize(hugeProductWithCollections));
// Лучше: сохраняем только необходимые данные
var productDto = new {
Id = product.Id,
Name = product.Name,
Price = product.Price,
CategoryId = product.Category?.Id
};
await _redis.StringSetAsync($"product:{id}", JsonSerializer.Serialize(productDto)); |
|
Интересный подводный камень - горячие ключи в Redis. Если один ключ обрабатывается слишком часто, он может стать узким местом в однопоточной модели Redis:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
| // Проблема: счетчик с высокой конкуренцией
await _redis.StringIncrementAsync("global:hitcounter");
// Решение: разделить на сегменты
int segment = new Random().Next(0, 100);
await _redis.StringIncrementAsync($"global:hitcounter:{segment}");
// При необходимости получения общего значения
long totalHits = 0;
for (int i = 0; i < 100; i++)
{
totalHits += (long)await _redis.StringGetAsync($"global:hitcounter:{i}") ?? 0;
} |
|
Многие разработчики сталкиваются с проблемой фрагментации памяти в Redis. При частом добавлении и удалении данных память не всегда освобождается оптимально. Решение - настройка политики maxmemory и переодическая перезагрузка инстанса в периоды низкой нагрузки.
Нельзя забывать и про согласованость данных при использовании Cache-Aside паттерна. Если один экземпляр приложения обновляет запись в БД, а другой работает с устаревшими данными из кеша, возникают проблемы:
| C# | 1
2
3
4
5
6
7
8
9
| // Распространенное решение: инвалидация через Pub/Sub
// В сервисе, обновляющем данные
await _db.Products.UpdateAsync(product);
await _redis.PublishAsync("cache:invalidate", $"product:{product.Id}");
// Подписка в каждом экземпляре приложения
_redis.GetSubscriber().Subscribe("cache:invalidate", (channel, message) => {
_redis.KeyDeleteAsync(message.ToString());
}); |
|
Отдельная категория проблем - неоптимальные шаблоны использования Redis. Например, новички часто используют KEYS для поиска по паттерну, не подозревая, что эта команда блокирует весь Redis на время выполнения:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Плохо для продакшна - блокирует сервер при большом количестве ключей
var keys = _server.Keys(pattern: "user:*");
// Лучше - инкрементальное сканирование
var allKeys = new List<RedisKey>();
long cursor = 0;
do
{
var result = await _server.ExecuteAsync("SCAN", cursor.ToString(), "MATCH", "user:*", "COUNT", "100");
var innerResult = (RedisResult[])result;
cursor = long.Parse((string)innerResult[0]);
var items = (RedisResult[])innerResult[1];
allKeys.AddRange(items.Select(x => (RedisKey)(string)x));
}
while (cursor != 0); |
|
Мониторинг - еще одна частая слабость Redis-инсталляций. Без надлежащего наблюдения за метриками легко пропустить моменты, когда Redis перестает справляться с нагрузкой:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Простая проверка здоровья Redis
public async Task<bool> IsRedisHealthy()
{
try
{
var latency = await MeasureLatency();
var memoryStats = await GetMemoryStats();
// Проверяем критические показатели
return latency.TotalMilliseconds < 100 &&
memoryStats.UsedPercentage < 80 &&
_connection.Value.IsConnected;
}
catch
{
return false;
}
} |
|
При работе с Redis Cluster многие забывают о мультиплекснинге - когда несколько логических соединений разделяют одно физическое. По умолчанию StackExchange.Redis использует его, но иногда это может привести к блокировкам при отправке больших команд.
Демо-приложение
Теперь, когда мы разобрались со всеми теоретическими аспектами и потенциальными проблемами, самое время собрать все знания в единый пример. Создадим многоуровневую систему кеширования для типичного интернет-магазина с каталогом товаров и корзиной покупателя.
Архитектура нашего приложения будет включать несколько слоев кеширования:
1. L1 кеш (ближайший к приложению) - это MemoryCache внутри самого приложения.
2. L2 кеш (распределенный) - Redis для хранения общих данных между инстансами.
3. Основное хранилище - SQL база данных (для простоты примера - Entity Framework Core).
Основная идея многоуровневого кеширования состоит в минимизации обращений даже к Redis, используя локальный кеш для самых "горячих" данных:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| public class MultiLevelCacheService<T> where T : class
{
private readonly IMemoryCache _memoryCache;
private readonly IRedisCacheService _redisCache;
private readonly IRepository<T> _repository;
private readonly TimeSpan _memoryCacheDuration;
private readonly TimeSpan _redisCacheDuration;
public MultiLevelCacheService(
IMemoryCache memoryCache,
IRedisCacheService redisCache,
IRepository<T> repository,
TimeSpan? memoryCacheDuration = null,
TimeSpan? redisCacheDuration = null)
{
_memoryCache = memoryCache;
_redisCache = redisCache;
_repository = repository;
_memoryCacheDuration = memoryCacheDuration ?? TimeSpan.FromSeconds(30);
_redisCacheDuration = redisCacheDuration ?? TimeSpan.FromMinutes(10);
}
public async Task<T> GetByIdAsync(int id)
{
string cacheKey = $"{typeof(T).Name.ToLower()}:{id}";
// Проверяем L1 кеш (память)
if (_memoryCache.TryGetValue(cacheKey, out T cachedItem))
return cachedItem;
// Проверяем L2 кеш (Redis)
var redisItem = await _redisCache.GetAsync<T>(cacheKey);
if (redisItem != null)
{
// Помещаем в L1 кеш
_memoryCache.Set(cacheKey, redisItem, _memoryCacheDuration);
return redisItem;
}
// Получаем из основного хранилища
var item = await _repository.GetByIdAsync(id);
if (item != null)
{
// Сохраняем в обоих уровнях кеша
_memoryCache.Set(cacheKey, item, _memoryCacheDuration);
await _redisCache.SetAsync(cacheKey, item, _redisCacheDuration);
}
return item;
}
// Метод для инвалидации кеша при обновлении
public async Task InvalidateCacheAsync(int id)
{
string cacheKey = $"{typeof(T).Name.ToLower()}:{id}";
_memoryCache.Remove(cacheKey);
await _redisCache.RemoveAsync(cacheKey);
}
} |
|
Для корзины покупателя, которая требует большей согласованности между сессиями пользователя, используем немного другой подход:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| public class ShoppingCartService
{
private readonly IRedisCacheService _redisCache;
private readonly IPublishService _publishService;
public ShoppingCartService(IRedisCacheService redisCache, IPublishService publishService)
{
_redisCache = redisCache;
_publishService = publishService;
}
public async Task<ShoppingCart> GetCartAsync(string userId)
{
string cartKey = $"cart:{userId}";
return await _redisCache.GetAsync<ShoppingCart>(cartKey) ?? new ShoppingCart { UserId = userId };
}
public async Task AddItemToCartAsync(string userId, int productId, int quantity)
{
string cartKey = $"cart:{userId}";
// Используем Lua-скрипт для атомарного обновления корзины
string script = @"
local cartKey = KEYS[1]
local productId = ARGV[1]
local quantity = tonumber(ARGV[2])
local cart = redis.call('GET', cartKey)
if not cart then
cart = '{""items"":[]}'
end
local cartObj = cjson.decode(cart)
local found = false
for i, item in ipairs(cartObj.items) do
if item.productId == productId then
item.quantity = item.quantity + quantity
found = true
break
end
end
if not found then
table.insert(cartObj.items, {productId=productId, quantity=quantity})
end
local newCart = cjson.encode(cartObj)
redis.call('SET', cartKey, newCart, 'EX', 86400)
return newCart
";
var result = await _redisCache.EvaluateScriptAsync<string>(
script,
new[] { cartKey },
new[] { productId.ToString(), quantity.ToString() }
);
// Оповещаем другие экземпляры приложения об изменении
await _publishService.PublishAsync("cart:updated", userId);
}
} |
|
Полная архитектура включает инфраструктуру для автоматической инвалидации кешей через Redis Pub/Sub, оповещающую все экземпляры приложения об изменениях:
| C# | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| public class CacheInvalidationService : IHostedService
{
private readonly IConnectionMultiplexer _redis;
private readonly IMemoryCache _memoryCache;
public CacheInvalidationService(IConnectionMultiplexer redis, IMemoryCache memoryCache)
{
_redis = redis;
_memoryCache = memoryCache;
}
public Task StartAsync(CancellationToken cancellationToken)
{
var subscriber = _redis.GetSubscriber();
subscriber.Subscribe("cache:invalidate", (channel, message) => {
_memoryCache.Remove(message.ToString());
});
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
} |
|
В результате мы получаем высокопроизводительную систему кеширования, которая:- Использует локальный кеш для минимизации сетевых обращений,
- Синхронизирует данные между экземплярами через Redis,
- Обеспечивает атомарные операции даже для сложных структур,
- Автоматически инвалидирует устаревшие данные,
- Защищена от рейс-кондишнов благодаря Lua-скриптам.
Такая архитектура может обрабатывать тысячи запросов в секунду с минимальной нагрузкой на основную базу данных и стабильным временем отклика даже при пиковых нагрузках.
Запуск asp-скрипта с клиента и кэш... Hi, all!
Для запуска asp c клиента пользуюсь такой вот штукой:
<script language=javascript>... Прослушивание mp3 в потоке, а не полная загрузка в кэш Есть такой код:
<object classid='CLSID:22D6f312-B0F6-11D0-94AB-0080C74C7E95' id='Player'... Не могу убрать кэш в Win2000 Server, IIS 5 Попробовал вчера настройки кэширования в IIS, посмотрел, как работает это на сайте и после этого... Есть ли кэш у контекста объекта EDM Привет!
Возник такой вопрос. Как известно, при взятии данных из контекста объекта Entity Data... Кэш и память Есть программа. Генерирует большое количество вариаций текста.
Работает по 3 вариантам:
1) Если... HTTP-сервер: Как использовать кэш Доброго времени суток, уважаемые.
Что бы дальше было понятней, немного обо мне: занимаюсь... Исходник КЭШ прокси + сокс Друзья, столкнулась с проблемой разработки связки кэширующий прокси + socks. Обошла кучу формув, но... Вывести информацию о ЦП, а именно L1 кэш Ребят нужно вывести в шарпе информацию о ЦП,а именно L1 кэш и еще пару вещей, при чем тех,которые... Очистить кэш childaction mvc 3 Сначала я хотел использовать SqlDependency, но оказалось на дочерние действия это свойство не... Кэш документация по Unity Подскажите, а кэш документация при на видении на методы в редакторе (использую висуал студио 2015)... Кэш в WebBrowser Здравствуйте. Подскажите как реализовать. Есть 2 формы с WebBrowser в каждом. с первого авторизуюсь... HttpClient() - убить кэш Как полностью избавиться от кэша для каждого вызова HttpClient()?
|