본문으로 건너뛰기
Previous
Next
C++ Caching Strategy Complete Guide | Redis· Memcached

C++ Caching Strategy Complete Guide | Redis· Memcached

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

StorageLatencyUse Case
In-memory cache0.01msHot data, single process
Redis (local)0.5-1msDistributed cache, rich data types
Memcached (local)0.3-0.8msDistributed cache, simple key-value
Database (indexed)5-50msCold data, complex queries
Database (full scan)100-1000msLarge 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

  1. Caching strategies: Cache-aside, write-through, write-behind
  2. Redis: Rich data types, persistence, pub/sub
  3. Memcached: Simple, fast key-value cache
  4. In-memory: LRU, TTL for single-process apps
  5. Invalidation: TTL, event-based, version-based
  6. Best practices: Always set TTL, handle failures, namespace keys, monitor hit rate

Strategy Comparison

StrategyConsistencyWrite LatencyComplexity
Cache-AsideEventualLowLow
Write-ThroughStrongHighMedium
Write-BehindEventualVery LowHigh

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?

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 등으로 검색하시면 이 글이 도움이 됩니다.