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

C++ Caching Strategy Complete Guide | Redis· Memcached

C++ Caching Strategy Complete Guide | Redis· Memcached

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.


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. Master C++ caching strategies: Redis (hiredis), Memcached (libmemcached), in-memory cache (LRU, TTL), cache invalidation…

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, caching, Redis, Memcached, LRU, performance, distributed systems.