Введение: почему кэширование критично для производительности 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