Кэширование в C++: Redis, Memcached и встроенные решения — продвинутый уровень

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

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

Начать курс

Введение: почему кэширование критично для производительности C++

В современной высоконагруженной разработке на C++ кэширование перестало быть опцией — это обязательный элемент архитектуры. Без него любая CRUD-система или микросервис быстро упирается в лимиты базы данных, а latency запросов растёт экспоненциально. На продвинутом уровне мы сталкиваемся с дилеммой: использовать внешние системы (Redis, Memcached) или развернуть собственное in-memory решение на C++? В этой статье мы разберём плюсы, минусы и конкретные реализации каждого подхода, опираясь на практику работы с сетевыми очередями, блокировками и сериализацией.

1. Redis: гибкость и структуры данных

Redis — это не просто key-value store. Его главное преимущество — богатый набор типов данных: строки, хеши, списки, множества, sorted sets. Для C++ существует несколько клиентов: redis-plus-plus (рекомендуется) и hiredis (низкоуровневый).

Пример подключения и базовых операций с redis-plus-plus

#include <sw/redis++/redis++.h>
#include <iostream>

int main() {
    // Подключение к Redis (по умолчанию localhost:6379)
    sw::redis::Redis redis("tcp://127.0.0.1:6379");

    // Установка значения с TTL 60 секунд
    redis.set("user:1001", "John Doe", std::chrono::seconds(60));

    // Чтение
    auto val = redis.get("user:1001");
    if (val) {
        std::cout << "User: " << *val << std::endl;
    }

    // Работа с хешами
    redis.hset("session:abc", "token", "xyz123");
    redis.hset("session:abc", "expires", "3600");
    auto token = redis.hget("session:abc", "token");
    std::cout << "Token: " << *token << std::endl;

    return 0;
}

Продвинутые фишки: использование транзакций через redis.multi() и redis.exec(), а также Pub/Sub для уведомлений об инвалидации кэша. Redis отлично подходит, когда нужно хранить сложные структуры с атомарными операциями (например, инкремент счётчика).

2. Memcached: скорость и простота

Memcached — это лёгкий распределённый кэш, оптимизированный под хранение плоских данных (строки и бинарные блобы). Он не поддерживает транзакции или сложные структуры, но выигрывает в скорости и минимальном потреблении памяти. Для C++ используем библиотеку libmemcached.

Пример работы с Memcached через libmemcached

#include <libmemcached/memcached.h>
#include <iostream>
#include <cstring>

int main() {
    memcached_st *memc = memcached_create(nullptr);
    memcached_server_st *servers = nullptr;
    memcached_return_t rc;

    // Добавляем сервер
    servers = memcached_server_list_append(servers, "127.0.0.1", 11211, &rc);
    rc = memcached_server_push(memc, servers);
    if (rc != MEMCACHED_SUCCESS) {
        std::cerr << "Error: " << memcached_strerror(memc, rc) << std::endl;
        return 1;
    }

    // Запись с TTL 30 секунд
    const char *key = "config:db_host";
    const char *value = "10.0.0.1";
    rc = memcached_set(memc, key, strlen(key), value, strlen(value), 
                       std::time(0) + 30, (uint32_t)0);
    if (rc != MEMCACHED_SUCCESS) {
        std::cerr << "Set failed: " << memcached_strerror(memc, rc) << std::endl;
    }

    // Чтение
    size_t val_len;
    uint32_t flags;
    char *retrieved = memcached_get(memc, key, strlen(key), &val_len, &flags, &rc);
    if (rc == MEMCACHED_SUCCESS) {
        std::cout << "DB Host: " << retrieved << std::endl;
        free(retrieved);
    }

    memcached_free(memc);
    return 0;
}

Когда выбирать Memcached? Если вам нужен простой кэш для сериализованных объектов, без необходимости хранить сложные структуры. Memcached идеален для кэширования результатов SQL-запросов или HTML-фрагментов.

3. Встроенные решения: custom in-memory кэш на C++

В некоторых сценариях внешние сервисы избыточны — например, в однопоточных или low-latency приложениях (HFT, игры). Тогда разумно написать свой кэш, используя std::unordered_map или std::map с таймерами. Рассмотрим реализацию с LRU-политикой вытеснения.

Реализация простого LRU-кэша с ограничением по времени

#include <unordered_map>
#include <list>
#include <chrono>
#include <mutex>

template<typename Key, typename Value>
class TTLMemoryCache {
private:
    struct CacheEntry {
        Value data;
        std::chrono::steady_clock::time_point expiry;
    };

    std::unordered_map<Key, CacheEntry> cache_;
    std::list<Key> lru_list_;  // для LRU
    std::mutex mtx_;
    size_t max_size_;
    std::chrono::seconds default_ttl_;

public:
    TTLMemoryCache(size_t max_size = 10000, std::chrono::seconds ttl = std::chrono::seconds(60))
        : max_size_(max_size), default_ttl_(ttl) {}

    void put(const Key& key, const Value& value, std::chrono::seconds ttl = std::chrono::seconds(0)) {
        std::lock_guard<std::mutex> lock(mtx_);
        auto now = std::chrono::steady_clock::now();
        auto expiry = now + (ttl.count() > 0 ? ttl : default_ttl_);

        // Если ключ уже существует, обновляем
        auto it = cache_.find(key);
        if (it != cache_.end()) {
            it->second.data = value;
            it->second.expiry = expiry;
            lru_list_.remove(key);
            lru_list_.push_front(key);
            return;
        }

        // Проверка на превышение размера
        if                

Блоги

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