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

Кэш REDIS и C#

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

Нажмите на изображение для увеличения
Название: 737f8743-9db7-4cdb-8118-6b9710181630.jpg
Просмотров: 283
Размер:	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
Комментарии
 
Новые блоги и статьи
Hyper-V: Компьютер должен поддерживать доверенный платформенный модуль 2.0.
Maks 31.08.2026
При установке Windows 11 на виртуальную машину Hyper-V 2-го поколения вылезла такая ошибка: Решение: в параметрах виртуальной машины, в разделе "Безопасность" (Security) активировать флаг. . .
Архитектура биовида Стива в Майнкрафте: Зачем бонобо кубический каннибализм
anaschu 30.08.2026
Кубический Вагинокапитализм в Minecraft: Математический инвариант ОДУ и рок Стивов-бонобо Главная задача разработанной «Модели Всего» — наглядно продемонстрировать наличие системной «судьбы». . .
Оттачиваю умение писать js программы.
russiannick 30.08.2026
Проектом выходного дня стало написание Книги шифров Виженера. Итогом стала версия 200, синий туман. Синий туман назван так, потому что замораживает текст под собой. Нажатие синих кнопок управляют. . .
мат медиц модель 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
Прошло две недели. Я уже рассказывал, как разговаривал с сотрудниками у сортировки и как понял, что главная ветка — не про приёмку, а про отбор. Но тогда я думал, что понял механику. На этой неделе я. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru