StackExchange.Redis для C# (.NET): полное руководство с примерами
1. Что это и зачем нужна
StackExchange.Redis — это высокопроизводительный, асинхронный клиент для Redis, разработанный командой StackOverflow. Это де-факто стандартная библиотека для работы с Redis в экосистеме .NET. Она предназначена для .NET Framework 4.6+ и .NET Core/.NET 5+.
Зачем она нужна? Redis — это in-memory хранилище данных, которое часто используется как кэш, брокер сообщений (Pub/Sub), очередь задач или высокопроизводительная NoSQL база данных. StackExchange.Redis предоставляет удобный API для всех этих сценариев: управление строками, хешами, списками, множествами, транзакциями, Lua-скриптами, потоковой передачей данных и т.д. Без этой библиотеки вам пришлось бы писать низкоуровневый код для работы с сокетами и протоколом Redis (RESP).
Ключевая особенность библиотеки — её асинхронность и многопоточность. Она использует ConnectionMultiplexer — единый, разделяемый объект для всех соединений к серверу Redis. Это позволяет эффективно использовать сокеты и избегать создания множества подключений. Библиотека полностью поддерживает Redis Cluster, Sentinel и репликацию.
2. Установка
Установка производится через NuGet Package Manager. Выполните одну из следующих команд в консоли диспетчера пакетов (Package Manager Console) или в терминале .NET CLI:
// Через .NET CLI
dotnet add package StackExchange.Redis
// Через Package Manager Console
Install-Package StackExchange.Redis
После установки добавьте using-директиву в ваш код:
using StackExchange.Redis;
3. Быстрый старт — минимальный рабочий пример
Ниже приведен минимальный пример, который подключается к локальному серверу Redis, записывает строку и читает её обратно:
using StackExchange.Redis;
// Создаем мультиплексор (точка входа для всех операций)
// Замените "localhost:6379" на ваш адрес Redis сервера
ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("localhost:6379");
// Получаем базу данных (по умолчанию db 0)
IDatabase db = redis.GetDatabase();
// Записываем значение
bool setResult = db.StringSet("mykey", "Hello, Redis!");
Console.WriteLine($"Set result: {setResult}");
// Читаем значение
RedisValue value = db.StringGet("mykey");
Console.WriteLine($"Get result: {value}");
// Закрываем соединение (обычно не требуется, если используете using)
redis.Close();
Важно: ConnectionMultiplexer — это долгоживущий объект. Его следует создавать один раз на всё время жизни приложения (например, как синглтон в DI-контейнере). Не создавайте новый мультиплексор для каждого запроса!
4. Основные методы/функции/классы
4.1. ConnectionMultiplexer.Connect()
Сигнатура: public static ConnectionMultiplexer Connect(string configuration)
Что делает: Создает и возвращает новый экземпляр ConnectionMultiplexer, который управляет всеми соединениями к Redis. Принимает строку конфигурации (адрес, порт, пароль, таймауты и т.д.).
Пример:
var redis = ConnectionMultiplexer.Connect("myredisserver.com:6379,password=secret,connectTimeout=5000");
4.2. IDatabase.StringSet()
Сигнатура: bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always)
Что делает: Устанавливает значение для указанного ключа. Может принимать опциональное время жизни (expiry) и условие установки (только если ключ существует, не существует и т.д.).
Пример:
IDatabase db = redis.GetDatabase();
db.StringSet("user:1:name", "Alice");
db.StringSet("session:token", "abc123", TimeSpan.FromMinutes(30)); // с TTL
4.3. IDatabase.StringGet()
Сигнатура: RedisValue StringGet(RedisKey key, CommandFlags flags = CommandFlags.None)
Что делает: Получает значение по ключу. Возвращает RedisValue, которое может быть пустым (null), если ключ не существует.
Пример:
RedisValue name = db.StringGet("user:1:name");
if (name.HasValue) {
Console.WriteLine($"Name: {name}");
}
4.4. IDatabase.KeyDelete()
Сигнатура: bool KeyDelete(RedisKey key, CommandFlags flags = CommandFlags.None)
Что делает: Удаляет указанный ключ из Redis. Возвращает true, если ключ был удалён, и false, если ключ не существовал.
Пример:
bool deleted = db.KeyDelete("user:1:name");
Console.WriteLine($"Key deleted: {deleted}");
4.5. IDatabase.HashSet()
Сигнатура: void HashSet(RedisKey key, HashEntry[] hashFields, CommandFlags flags = CommandFlags.None)
Что делает: Устанавливает одно или несколько полей в хеше. Если хеш не существует, он будет создан.
Пример:
db.HashSet("user:1", new HashEntry[] {
new HashEntry("name", "Bob"),
new HashEntry("age", 30),
new HashEntry("email", "bob@example.com")
});
4.6. IDatabase.HashGetAll()
Сигнатура: HashEntry[] HashGetAll(RedisKey key, CommandFlags flags = CommandFlags.None)
Что делает: Возвращает все поля и значения указанного хеша в виде массива HashEntry.
Пример:
var allFields = db.HashGetAll("user:1");
foreach (var field in allFields) {
Console.WriteLine($"{field.Name}: {field.Value}");
}
4.7. IDatabase.ListLeftPush() и IDatabase.ListRightPop()
Сигнатура: long ListLeftPush(RedisKey key, RedisValue value, When when = When.Always, CommandFlags flags = CommandFlags.None)
Что делает: ListLeftPush добавляет элемент в начало списка (слева). ListRightPop удаляет и возвращает элемент из конца списка (справа). Используется для реализации очередей (FIFO) или стеков (LIFO).
Пример (очередь):
// Добавляем задачи в очередь
db.ListLeftPush("task:queue", "task1");
db.ListLeftPush("task:queue", "task2");
// Извлекаем задачи (FIFO)
RedisValue task = db.ListRightPop("task:queue");
Console.WriteLine($"Processing: {task}"); // Выведет "task1"
4.8. IDatabase.Publish() и ISubscriber.Subscribe()
Сигнатура (Publish): long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None)
Сигнатура (Subscribe): void Subscribe(RedisChannel channel, Action<RedisChannel, RedisValue> handler, CommandFlags flags = CommandFlags.None)
Что делает: Реализует паттерн Pub/Sub (издатель-подписчик). Publish отправляет сообщение на канал. Subscribe подписывается на канал и вызывает обработчик при получении сообщения.
Пример (Pub/Sub):
// Подписчик (в отдельном потоке или задаче)
ISubscriber sub = redis.GetSubscriber();
sub.Subscribe("notifications", (channel, message) => {
Console.WriteLine($"Received: {message} on channel {channel}");
});
// Издатель
ISubscriber pub = redis.GetSubscriber();
pub.Publish("notifications", "Hello, subscribers!");
4.9. IDatabase.ScriptEvaluate()
Сигнатура: RedisResult ScriptEvaluate(string script, RedisKey[]? keys = null, RedisValue[]? values = null, CommandFlags flags = CommandFlags.None)
Что делает: Выполняет Lua-скрипт на стороне сервера Redis. Позволяет выполнять атомарные операции, которые невозможно реализовать с помощью стандартных команд.
Пример (атомарный инкремент с проверкой):
string luaScript = @"
local current = redis.call('GET', KEYS[1])
if not current or tonumber(current) < tonumber(ARGV[1]) then
redis.call('SET', KEYS[1], ARGV[1])
return 1
end
return 0
";
var result = db.ScriptEvaluate(luaScript, new RedisKey[] { "counter" }, new RedisValue[] { 100 });
Console.WriteLine($"Script executed, result: {result}");
5. Реальный практический пример (100-200 строк кода)
Рассмотрим пример реализации простого сервиса кэширования с инвалидацией для веб-приложения. Сервис будет кэшировать результаты запросов к базе данных, используя Redis.
using StackExchange.Redis;
using System;
using System.Threading.Tasks;
public class CacheService
{
private readonly IDatabase _db;
private readonly IServer _server;
public CacheService(string connectionString)
{
var redis = ConnectionMultiplexer.Connect(connectionString);
_db = redis.GetDatabase();
_server = redis.GetServer(connectionString.Split(',')[0]); // Получаем сервер для администрирования
}
// Получить значение из кэша
public async Task<T> GetAsync<T>(string key)
{
RedisValue value = await _db.StringGetAsync(key);
if (value.HasValue)
{
return Deserialize<T>(value);
}
return default;
}
// Установить значение в кэш с временем жизни
public async Task SetAsync<T>(string key, T value, TimeSpan? expiry = null)
{
string serialized = Serialize(value);
await _db.StringSetAsync(key, serialized, expiry ?? TimeSpan.FromMinutes(10));
}
// Удалить ключ из кэша
public async Task RemoveAsync(string key)
{
await _db.KeyDeleteAsync(key);
}
// Инвалидировать кэш по паттерну (например, "user:*")
public async Task InvalidateByPatternAsync(string pattern)
{
// Используем SCAN для поиска ключей (не блокирующая операция)
var keys = _server.Keys(pattern: pattern);
foreach (var key in keys)
{
await _db.KeyDeleteAsync(key);
}
}
// Пример использования: кэширование данных пользователя
public async Task<UserData> GetUserDataAsync(int userId)
{
string cacheKey = $"user:{userId}:data";
// Пытаемся получить из кэша
var cachedData = await GetAsync<UserData>(cacheKey);
if (cachedData != null)
{
Console.WriteLine("Cache HIT");
return cachedData;
}
// Если нет в кэше, загружаем из БД (имитация)
Console.WriteLine("Cache MISS, loading from DB...");
var userData = await LoadUserFromDatabaseAsync(userId);
// Сохраняем в кэш на 5 минут
await SetAsync(cacheKey, userData, TimeSpan.FromMinutes(5));
return userData;
}
// Вспомогательные методы
private string Serialize<T>(T obj) => System.Text.Json.JsonSerializer.Serialize(obj);
private T Deserialize<T>(RedisValue value) => System.Text.Json.JsonSerializer.Deserialize<T>(value);
// Имитация загрузки из базы данных
private async Task<UserData> LoadUserFromDatabaseAsync(int userId)
{
await Task.Delay(500); // Симуляция задержки БД
return new UserData
{
Id = userId,
Name = $"User_{userId}",
Email = $"user{userId}@example.com",
CreatedAt = DateTime.UtcNow
};
}
}
public class UserData
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public DateTime CreatedAt { get; set; }
}
// Пример использования
public class Program
{
public static async Task Main()
{
var cache = new CacheService("localhost:6379");
// Первый запрос — кэш промахнется
var user1 = await cache.GetUserDataAsync(1);
Console.WriteLine($"User: {user1.Name}");
// Второй запрос — кэш попадет
var user2 = await cache.GetUserDataAsync(1);
Console.WriteLine($"User: {user2.Name}");
// Инвалидируем кэш для этого пользователя
await cache.RemoveAsync("user:1:data");
Console.WriteLine("Cache invalidated");
}
}
6. Советы по производительности и типичные ошибки
Советы по производительности
- Используйте один ConnectionMultiplexer на всё приложение. Создание нового мультиплексора для каждого запроса — это дорого и может привести к исчерпанию сокетов. Регистрируйте его как синглтон в DI-контейнере.
- Используйте асинхронные методы (Async-версии). Всегда предпочитайте
StringSetAsync,