StackExchange.Redis для C# (.NET): полное руководство с примерами

Онлайн-тренажер Python для начинающих

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

Начать курс

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,

Рекомендаций