Форум программистов, компьютерный форум, киберфорум
UnmanagedCoder
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск  

Кэш REDIS и C#

Запись от UnmanagedCoder размещена 01.06.2025 в 18:26
Показов 5054 Комментарии 0
Метки .net, c#, lua, redis

Нажмите на изображение для увеличения
Название: 737f8743-9db7-4cdb-8118-6b9710181630.jpg
Просмотров: 281
Размер:	104.0 Кб
ID:	10866
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. Установка предельно проста - запускаем командную строку с правами администратора и вводим:

PowerShell
1
choco install redis-64
После установки можно запустить сервер 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 клиента пользуюсь такой вот штукой: &lt;script language=javascript&gt;...

Прослушивание mp3 в потоке, а не полная загрузка в кэш
Есть такой код: &lt;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()?

Метки .net, c#, lua, redis
Размещено в Без категории
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
мат медиц модель 30. презентация проекта
anaschu 27.08.2026
хоп хоп хоп хидахоп, а я кладую))
Как у меня протекала болезнь
zorxor 27.08.2026
Здравствуйте, друзья! Эта запись блога предназначена именно для вас - для моих дорогих друзей, которые знали меня лично. Чтобы ответить на вопрос - а что же со мной произошло на самом деле? Я учился. . .
Нашел вот забавное видео о измерениях. Лучшее что я видел на эту тему
kumehtar 26.08.2026
ILETXiw9bMQ Основная суть и тезисы по измерениям: 0D (Нулевое измерение): точка, не имеющая длины, ширины, высоты или объема. Объект не может перемещаться в 0D. 1D (Первое измерение):. . .
[EasyBuilder Pro] Памятка по разработке для панелей Weintek
ФедосеевПавел 26.08.2026
Памятка по разработке для панелей Weintek ВВЕДЕНИЕ Ранее, при реализации проектов основное внимание уделял разработке управляющей программы для контроллера, а панели оператора доставалось время. . .
Модель по догадкам
anaschu 25.08.2026
Прошло две недели. Я уже рассказывал, как разговаривал с сотрудниками у сортировки и как понял, что главная ветка — не про приёмку, а про отбор. Но тогда я думал, что понял механику. На этой неделе я. . .
Запись в регистр сведений независимо от заполненности табличной части
Maks 25.08.2026
Реализация из решения ниже выполнена на нетиповом документе с несколькими табличными частями, разработанного в КА2. Задача: Обеспечить запись документа в регистр сведений независимо от. . .
Ноутбук Альфария
kumehtar 24.08.2026
Встретился тут в сети ноутбук Альфария, примарха Альфа-Легиона. Хотя возможно, это ноутбук Омегона, разумеется. Ну как вам?
Мастера простых решений
DevAlt 23.08.2026
В сишарп стэках winforms, да и wpf существует сложная система связывания источниках данных и элементов формы(текстовые поля и метки), опирается все это на технологию событий и мета. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru