C++ Caching Strategy Complete Guide | Redis· Memcached
이 글의 핵심
Master C++ caching: Redis, Memcached, in-memory LRU/TTL, cache invalidation, cache-aside/write-through/write-behind patterns, and production patterns.
Why Caching?
Caching reduces latency and load on backend systems by storing frequently accessed data in fast storage. Common scenarios:
- Database query results
- API responses
- Computed values
- Session data
- Static content Benefits:
- Reduced latency (10-100x faster)
- Lower database load
- Higher throughput
- Better user experience
Caching Strategies
1. Cache-Aside (Lazy Loading)
// Application checks cache first, loads from DB on miss
string getData(const string& key) {
// 1. Check cache
string value = cache.get(key);
if (!value.empty()) {
return value; // Cache hit
}
// 2. Cache miss: load from DB
value = database.query(key);
// 3. Store in cache
cache.set(key, value, 3600); // 1 hour TTL
return value;
}
Pros:
- Simple
- Cache only what’s needed
- Cache failure doesn’t break app Cons:
- Cache miss penalty
- Potential cache stampede
2. Write-Through
// Write to cache and DB simultaneously
void setData(const string& key, const string& value) {
// 1. Write to cache
cache.set(key, value);
// 2. Write to DB
database.update(key, value);
}
Pros:
- Cache always consistent with DB
- No stale data Cons:
- Write latency (both cache and DB)
- Wasted cache space for rarely-read data
3. Write-Behind (Write-Back)
// Write to cache first, async write to DB
void setData(const string& key, const string& value) {
// 1. Write to cache
cache.set(key, value);
// 2. Queue for async DB write
writeQueue.push({key, value});
}
// Background thread
void flushWorker() {
while (true) {
auto item = writeQueue.pop();
database.update(item.key, item.value);
}
}
Pros:
- Low write latency
- Batched DB writes Cons:
- Risk of data loss on cache failure
- Complexity
Redis Integration (hiredis)
Installation
# Ubuntu/Debian
sudo apt install libhiredis-dev
# macOS
brew install hiredis
# vcpkg
vcpkg install hiredis
Basic Usage
#include <hiredis/hiredis.h>
#include <iostream>
#include <string>
class RedisClient {
private:
redisContext* context;
public:
RedisClient(const string& host, int port) {
context = redisConnect(host.c_str(), port);
if (context == nullptr || context->err) {
if (context) {
cerr << "Redis error: " << context->errstr << endl;
redisFree(context);
}
throw runtime_error("Redis connection failed");
}
cout << "Redis connected" << endl;
}
~RedisClient() {
if (context) {
redisFree(context);
cout << "Redis disconnected" << endl;
}
}
bool set(const string& key, const string& value, int ttl = 0) {
redisReply* reply;
if (ttl > 0) {
reply = (redisReply*)redisCommand(context, "SETEX %s %d %s",
key.c_str(), ttl, value.c_str());
} else {
reply = (redisReply*)redisCommand(context, "SET %s %s",
key.c_str(), value.c_str());
}
bool success = reply && reply->type == REDIS_REPLY_STATUS;
freeReplyObject(reply);
return success;
}
string get(const string& key) {
redisReply* reply = (redisReply*)redisCommand(context, "GET %s", key.c_str());
string value;
if (reply && reply->type == REDIS_REPLY_STRING) {
value = string(reply->str, reply->len);
}
freeReplyObject(reply);
return value;
}
bool del(const string& key) {
redisReply* reply = (redisReply*)redisCommand(context, "DEL %s", key.c_str());
bool success = reply && reply->integer > 0;
freeReplyObject(reply);
return success;
}
};
int main() {
try {
RedisClient redis("127.0.0.1", 6379);
// Set with TTL
redis.set("user:1000", "John Doe", 3600);
// Get
string value = redis.get("user:1000");
cout << "Value: " << value << endl;
// Delete
redis.del("user:1000");
} catch (exception& e) {
cerr << e.what() << endl;
}
}
Output:
Redis connected
Value: John Doe
Redis disconnected
Memcached Integration (libmemcached)
Installation
# Ubuntu/Debian
sudo apt install libmemcached-dev
# macOS
brew install libmemcached
# vcpkg
vcpkg install libmemcached
Basic Usage
#include <libmemcached/memcached.h>
#include <iostream>
#include <string>
class MemcachedClient {
private:
memcached_st* memc;
public:
MemcachedClient(const string& host, int port) {
memc = memcached_create(nullptr);
memcached_server_add(memc, host.c_str(), port);
cout << "Memcached connected" << endl;
}
~MemcachedClient() {
if (memc) {
memcached_free(memc);
cout << "Memcached disconnected" << endl;
}
}
bool set(const string& key, const string& value, time_t expiration = 0) {
memcached_return_t rc = memcached_set(
memc,
key.c_str(), key.length(),
value.c_str(), value.length(),
expiration,
);
return rc == MEMCACHED_SUCCESS;
}
string get(const string& key) {
size_t valueLen;
uint32_t flags;
memcached_return_t rc;
char* value = memcached_get(
memc,
key.c_str(), key.length(),
&valueLen,
&flags,
&rc
);
string result;
if (rc == MEMCACHED_SUCCESS && value) {
result = string(value, valueLen);
free(value);
}
return result;
}
bool del(const string& key) {
memcached_return_t rc = memcached_delete(
memc,
key.c_str(), key.length(),
);
return rc == MEMCACHED_SUCCESS;
}
};
int main() {
MemcachedClient cache("127.0.0.1", 11211);
// Set with 1 hour expiration
cache.set("session:abc123", "user_data", 3600);
// Get
string value = cache.get("session:abc123");
cout << "Value: " << value << endl;
// Delete
cache.del("session:abc123");
}
In-Memory Cache (LRU)
LRU Cache Implementation
#include <unordered_map>
#include <list>
#include <mutex>
template<typename K, typename V>
class LRUCache {
private:
size_t capacity;
list<pair<K, V>> items; // Most recent at front
unordered_map<K, typename list<pair<K, V>>::iterator> cache;
mutable mutex mtx;
public:
LRUCache(size_t cap) : capacity(cap) {}
void put(const K& key, const V& value) {
lock_guard<mutex> lock(mtx);
auto it = cache.find(key);
if (it != cache.end()) {
// Key exists: move to front
items.erase(it->second);
}
// Add to front
items.push_front({key, value});
cache[key] = items.begin();
// Evict if over capacity
if (cache.size() > capacity) {
auto last = items.back();
cache.erase(last.first);
items.pop_back();
}
}
optional<V> get(const K& key) {
lock_guard<mutex> lock(mtx);
auto it = cache.find(key);
if (it == cache.end()) {
return nullopt; // Cache miss
}
// Move to front (most recently used)
auto value = it->second->second;
items.erase(it->second);
items.push_front({key, value});
cache[key] = items.begin();
return value;
}
void remove(const K& key) {
lock_guard<mutex> lock(mtx);
auto it = cache.find(key);
if (it != cache.end()) {
items.erase(it->second);
cache.erase(it);
}
}
size_t size() const {
lock_guard<mutex> lock(mtx);
return cache.size();
}
};
int main() {
LRUCache<string, string> cache(3);
cache.put("a", "value_a");
cache.put("b", "value_b");
cache.put("c", "value_c");
cout << cache.get("a").value_or("not found") << endl; // value_a
cache.put("d", "value_d"); // Evicts "b" (least recently used)
cout << cache.get("b").value_or("not found") << endl; // not found
}
Output:
value_a
not found
TTL Cache Implementation
#include <chrono>
template<typename K, typename V>
class TTLCache {
private:
struct CacheEntry {
V value;
chrono::steady_clock::time_point expiry;
};
unordered_map<K, CacheEntry> cache;
mutable mutex mtx;
public:
void put(const K& key, const V& value, int ttlSeconds) {
lock_guard<mutex> lock(mtx);
auto expiry = chrono::steady_clock::now() + chrono::seconds(ttlSeconds);
cache[key] = {value, expiry};
}
optional<V> get(const K& key) {
lock_guard<mutex> lock(mtx);
auto it = cache.find(key);
if (it == cache.end()) {
return nullopt;
}
// Check expiry
if (chrono::steady_clock::now() > it->second.expiry) {
cache.erase(it);
return nullopt;
}
return it->second.value;
}
void cleanup() {
lock_guard<mutex> lock(mtx);
auto now = chrono::steady_clock::now();
for (auto it = cache.begin(); it != cache.end();) {
if (now > it->second.expiry) {
it = cache.erase(it);
} else {
++it;
}
}
}
};
int main() {
TTLCache<string, string> cache;
cache.put("key1", "value1", 2); // 2 seconds TTL
cout << cache.get("key1").value_or("expired") << endl; // value1
this_thread::sleep_for(chrono::seconds(3));
cout << cache.get("key1").value_or("expired") << endl; // expired
}
Output:
value1
expired
Cache Invalidation
1. Time-Based (TTL)
redis.set("user:1000", userData, 3600); // 1 hour TTL
Pros: Simple, automatic expiry
Cons: Stale data until expiry
2. Event-Based
void updateUser(int userId, const string& data) {
// Update DB
database.update(userId, data);
// Invalidate cache
cache.del("user:" + to_string(userId));
}
Pros: Always fresh data
Cons: Requires careful event tracking
3. Version-Based
string getCacheKey(const string& key, int version) {
return key + ":v" + to_string(version);
}
void updateData(const string& key, const string& value) {
int newVersion = getNextVersion(key);
cache.set(getCacheKey(key, newVersion), value);
}
Pros: No explicit invalidation needed
Cons: Old versions may linger
Production Patterns
Pattern 1: Cache-Aside with Fallback
class CachedRepository {
private:
RedisClient redis;
Database db;
public:
string getUser(int userId) {
string key = "user:" + to_string(userId);
// Try cache
try {
string cached = redis.get(key);
if (!cached.empty()) {
return cached;
}
} catch (exception& e) {
cerr << "Redis error: " << e.what() << endl;
// Fall through to DB
}
// Load from DB
string userData = db.query("SELECT * FROM users WHERE id = ?", userId);
// Update cache (best effort)
try {
redis.set(key, userData, 3600);
} catch (...) {
// Ignore cache write errors
}
return userData;
}
};
Key: Cache failures don’t break app—always fall back to DB.
Pattern 2: Cache Stampede Prevention
#include <shared_mutex>
class CacheWithLock {
private:
RedisClient redis;
Database db;
unordered_map<string, shared_mutex> locks;
mutex lockMapMutex;
shared_mutex& getLock(const string& key) {
lock_guard<mutex> lock(lockMapMutex);
return locks[key];
}
public:
string get(const string& key) {
// Try cache first (shared lock)
{
shared_lock<shared_mutex> lock(getLock(key));
string cached = redis.get(key);
if (!cached.empty()) {
return cached;
}
}
// Cache miss: exclusive lock to prevent stampede
unique_lock<shared_mutex> lock(getLock(key));
// Double-check cache (another thread may have loaded it)
string cached = redis.get(key);
if (!cached.empty()) {
return cached;
}
// Load from DB
string value = db.query(key);
redis.set(key, value, 3600);
return value;
}
};
Key: Only one thread loads from DB on cache miss, preventing thundering herd.
Pattern 3: Batch Cache Loading
map<string, string> batchGet(const vector<string>& keys) {
map<string, string> results;
vector<string> missingKeys;
// 1. Batch get from cache
for (const auto& key : keys) {
string value = redis.get(key);
if (!value.empty()) {
results[key] = value;
} else {
missingKeys.push_back(key);
}
}
// 2. Batch load missing keys from DB
if (!missingKeys.empty()) {
auto dbResults = database.batchQuery(missingKeys);
// 3. Batch set to cache
for (const auto& [key, value] : dbResults) {
redis.set(key, value, 3600);
results[key] = value;
}
}
return results;
}
Key: Batch operations reduce network round trips.
Pattern 4: Two-Level Cache (L1 + L2)
class TwoLevelCache {
private:
LRUCache<string, string> l1Cache; // In-memory (fast, small)
RedisClient l2Cache; // Redis (slower, large)
Database db;
public:
TwoLevelCache(size_t l1Size) : l1Cache(l1Size), l2Cache("127.0.0.1", 6379) {}
string get(const string& key) {
// L1 cache
auto l1Value = l1Cache.get(key);
if (l1Value) {
return *l1Value;
}
// L2 cache
string l2Value = l2Cache.get(key);
if (!l2Value.empty()) {
l1Cache.put(key, l2Value); // Promote to L1
return l2Value;
}
// DB
string dbValue = db.query(key);
l2Cache.set(key, dbValue, 3600);
l1Cache.put(key, dbValue);
return dbValue;
}
};
Key: Hot data in L1 (in-memory), warm data in L2 (Redis), cold data in DB.
Performance Benchmarks
Latency Comparison
| Storage | Latency | Use Case |
|---|---|---|
| In-memory cache | 0.01ms | Hot data, single process |
| Redis (local) | 0.5-1ms | Distributed cache, rich data types |
| Memcached (local) | 0.3-0.8ms | Distributed cache, simple key-value |
| Database (indexed) | 5-50ms | Cold data, complex queries |
| Database (full scan) | 100-1000ms | Large tables, no index |
Throughput Comparison
// Benchmark: 100,000 reads
// In-memory: 500,000 ops/sec
// Redis: 100,000 ops/sec
// Memcached: 150,000 ops/sec
// PostgreSQL: 10,000 ops/sec
Key: In-memory cache is 10-50x faster than Redis, Redis is 10x faster than DB.
Common Issues
Issue 1: Cache Stampede
Problem: Many threads simultaneously request missing key, all hit DB. Solution: Use locks or probabilistic early expiry.
// Probabilistic early expiry
string get(const string& key, int ttl) {
auto entry = cache.get(key);
if (entry) {
int timeLeft = entry.expiry - now();
// 10% chance to refresh when 10% time left
if (timeLeft < ttl * 0.1 && rand() % 10 == 0) {
return refreshFromDB(key);
}
return entry.value;
}
return refreshFromDB(key);
}
Issue 2: Stale Data
Problem: Cache and DB out of sync. Solution: Invalidate cache on writes.
void updateUser(int userId, const string& data) {
db.update(userId, data);
cache.del("user:" + to_string(userId));
}
Issue 3: Memory Bloat
Problem: Cache grows unbounded. Solution: Use LRU eviction or TTL.
// LRU with max size
LRUCache<string, string> cache(10000); // Max 10k entries
// TTL
redis.set(key, value, 3600); // 1 hour TTL
Best Practices
1. Always Set TTL
// ✅ Always set expiration
redis.set("key", "value", 3600); // 1 hour
// ❌ No expiration (memory leak risk)
redis.set("key", "value");
2. Handle Cache Failures Gracefully
string getData(const string& key) {
try {
auto cached = cache.get(key);
if (cached) return *cached;
} catch (...) {
// Log but don't fail
}
// Always fall back to DB
return db.query(key);
}
3. Use Namespaced Keys
// ✅ Namespaced
string userKey = "user:" + to_string(userId);
string sessionKey = "session:" + sessionId;
// ❌ Flat (collision risk)
string key = to_string(userId);
4. Monitor Cache Hit Rate
class CacheWithMetrics {
atomic<long> hits{0};
atomic<long> misses{0};
public:
optional<string> get(const string& key) {
auto value = cache.get(key);
if (value) {
hits++;
} else {
misses++;
}
return value;
}
double hitRate() const {
long total = hits + misses;
return total > 0 ? (double)hits / total : 0.0;
}
};
Target: 80%+ hit rate for effective caching.
5. Serialize Complex Objects
#include <nlohmann/json.hpp>
struct User {
int id;
string name;
string email;
};
void cacheUser(const User& user) {
nlohmann::json j;
j[id] = user.id;
j[name] = user.name;
j[email] = user.email;
redis.set("user:" + to_string(user.id), j.dump(), 3600);
}
User getUser(int userId) {
string cached = redis.get("user:" + to_string(userId));
if (!cached.empty()) {
auto j = nlohmann::json::parse(cached);
return User{j[id], j[name], j[email]};
}
// Load from DB...
}
Summary
Key Points
- Caching strategies: Cache-aside, write-through, write-behind
- Redis: Rich data types, persistence, pub/sub
- Memcached: Simple, fast key-value cache
- In-memory: LRU, TTL for single-process apps
- Invalidation: TTL, event-based, version-based
- Best practices: Always set TTL, handle failures, namespace keys, monitor hit rate
Strategy Comparison
| Strategy | Consistency | Write Latency | Complexity |
|---|---|---|---|
| Cache-Aside | Eventual | Low | Low |
| Write-Through | Strong | High | Medium |
| Write-Behind | Eventual | Very Low | High |
Caching Checklist
- Cache frequently accessed data?
- Set appropriate TTL?
- Handle cache failures gracefully?
- Invalidate on writes?
- Monitor hit rate?
- Prevent cache stampede?
- Use namespaced keys?
Related Articles
- C++ Redis Integration Guide
- C++ Performance Optimization Guide
- C++ Query Optimization Complete Guide
Keywords
C++ caching, Redis, Memcached, LRU cache, TTL cache, cache invalidation, cache-aside, write-through, write-behind, distributed cache One-line summary: Master C++ caching with Redis, Memcached, and in-memory strategies to reduce latency and database load in high-traffic systems.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Master C++ caching strategies: Redis (hiredis), Memcached (libmemcached), in-memory cache (LRU, TTL), cache invalidation… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
이 글에서 다루는 키워드 (관련 검색어)
C++, caching, Redis, Memcached, LRU, performance, distributed systems 등으로 검색하시면 이 글이 도움이 됩니다.