• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » High-Throughput Caching Strategies: Scaling MySQL for C Application APIs

High-Throughput Caching Strategies: Scaling MySQL for C Application APIs

Leveraging Redis for High-Throughput MySQL Caching in C Applications

When building high-throughput C applications that interact with MySQL, direct database queries can quickly become a bottleneck. This is particularly true for read-heavy workloads where the same data is fetched repeatedly. Implementing an effective caching layer is paramount to achieving low latency and high concurrency. Redis, with its in-memory data structures, low latency, and rich feature set, is an excellent choice for this purpose. This post details strategies for integrating Redis as a caching layer for MySQL data accessed by C applications, focusing on practical implementation and performance considerations.

Cache Invalidation Strategies

The most critical aspect of any caching system is cache invalidation. Stale data is often worse than no data. For MySQL-backed caches, common invalidation strategies include:

  • Time-To-Live (TTL): Data expires after a set duration. Simple but can lead to brief periods of stale data.
  • Write-Through: Update the cache immediately after updating the database. Ensures data consistency but adds latency to writes.
  • Write-Around: Write directly to the database, and only update the cache if the data is subsequently read. Less overhead on writes, but cache misses on initial reads after a write.
  • Write-Back: Write to the cache first, and asynchronously update the database. Offers the lowest write latency but introduces complexity and risk of data loss if the cache fails before persistence.
  • Event-Driven Invalidation: Trigger cache invalidation based on database events (e.g., using triggers or binlog listeners). More complex but offers fine-grained control.

For high-throughput C applications, a combination of TTL and a carefully managed write strategy is often optimal. We’ll focus on a TTL-based approach with explicit invalidation on writes for this discussion.

Redis Client Integration in C

To interact with Redis from C, we’ll use the hiredis library. It’s a lightweight, thread-safe client library for Redis. First, ensure hiredis is installed on your system. On Debian/Ubuntu:

sudo apt-get update
sudo apt-get install libhiredis-dev

Here’s a basic C code snippet demonstrating how to connect to Redis and set/get a key:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <hiredis/hiredis.h>

int main() {
    redisContext *c;
    redisReply *reply;

    // Connect to Redis server
    c = redisConnect("127.0.0.1", 6379);
    if (c == NULL || c->err) {
        if (c) {
            printf("Connection error: %s\n", c->errstr);
            redisFree(c);
        } else {
            printf("Can't allocate redis context\n");
        }
        return 1;
    }

    printf("Connected to Redis server!\n");

    // Set a key with TTL
    const char *key = "user:123:profile";
    const char *value = "{\"name\": \"Alice\", \"email\": \"[email protected]\"}";
    int ttl_seconds = 3600; // 1 hour

    reply = (redisReply *)redisCommand(c, "SET %s %s EX %d", key, value, ttl_seconds);
    if (reply == NULL) {
        printf("SET command failed: %s\n", c->errstr);
        redisFree(c);
        return 1;
    }
    printf("SET reply type: %d, reply: %s\n", reply->type, reply->str);
    freeReplyObject(reply);

    // Get a key
    reply = (redisReply *)redisCommand(c, "GET %s", key);
    if (reply == NULL) {
        printf("GET command failed: %s\n", c->errstr);
        redisFree(c);
        return 1;
    }

    if (reply->type == REDIS_REPLY_STRING) {
        printf("GET reply: %s\n", reply->str);
    } else if (reply->type == REDIS_REPLY_NIL) {
        printf("GET reply: Key not found\n");
    } else {
        printf("GET reply type: %d\n", reply->type);
    }
    freeReplyObject(reply);

    // Disconnect from Redis
    redisFree(c);
    return 0;
}

To compile this, you’ll need to link against the hiredis library:

gcc your_program.c -o your_program -lhiredis

Caching MySQL Query Results

A common pattern is to cache the results of specific, frequently executed SQL queries. The cache key should be designed to be unique for a given set of query parameters. For example, fetching a user’s profile by ID:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <hiredis/hiredis.h>
#include <mysql/mysql.h> // Assuming MySQL C Connector is installed

// Function to fetch user profile from MySQL
char* fetch_user_profile_from_db(long long user_id, MYSQL *conn) {
    // ... (MySQL query execution logic here) ...
    // For simplicity, returning a dummy string
    char *result = malloc(256);
    snprintf(result, 256, "{\"id\": %lld, \"name\": \"User %lld\", \"email\": \"user%[email protected]\"}", user_id, user_id, user_id);
    return result;
}

// Function to get user profile, checking cache first
char* get_user_profile(long long user_id, redisContext *redis_c, MYSQL *mysql_conn) {
    char cache_key[100];
    snprintf(cache_key, sizeof(cache_key), "user:profile:%lld", user_id);
    int ttl_seconds = 600; // Cache for 10 minutes

    redisReply *reply = (redisReply *)redisCommand(redis_c, "GET %s", cache_key);
    if (reply == NULL) {
        fprintf(stderr, "Redis GET command failed: %s\n", redis_c->errstr);
        // Fallback to DB if Redis fails, but log the error
    } else {
        if (reply->type == REDIS_REPLY_STRING) {
            char *cached_data = strdup(reply->str); // Duplicate as freeReplyObject will free reply->str
            freeReplyObject(reply);
            printf("Cache HIT for %s\n", cache_key);
            return cached_data;
        }
        freeReplyObject(reply);
    }

    printf("Cache MISS for %s. Fetching from DB...\n", cache_key);
    // Cache miss, fetch from MySQL
    char *db_data = fetch_user_profile_from_db(user_id, mysql_conn);

    // Store in Redis cache
    if (db_data) {
        redisCommand(redis_c, "SET %s %s EX %d", cache_key, db_data, ttl_seconds);
        // Ignore SET command errors for high throughput, but log them in production
    }

    return db_data;
}

int main() {
    // Initialize Redis connection
    redisContext *redis_c = redisConnect("127.0.0.1", 6379);
    if (redis_c == NULL || redis_c->err) {
        fprintf(stderr, "Redis connection error: %s\n", redis_c ? redis_c->errstr : "unknown");
        return 1;
    }
    printf("Connected to Redis.\n");

    // Initialize MySQL connection (placeholder)
    MYSQL *mysql_conn = mysql_init(NULL);
    if (!mysql_real_connect(mysql_conn, "localhost", "user", "password", "database", 0, NULL, 0)) {
        fprintf(stderr, "MySQL connection error: %s\n", mysql_error(mysql_conn));
        redisFree(redis_c);
        return 1;
    }
    printf("Connected to MySQL.\n");

    long long user_id_to_fetch = 456;
    char *profile_data = get_user_profile(user_id_to_fetch, redis_c, mysql_conn);

    if (profile_data) {
        printf("User Profile: %s\n", profile_data);
        free(profile_data); // Free memory allocated by fetch_user_profile_from_db
    } else {
        printf("Failed to retrieve user profile.\n");
    }

    // Cleanup
    mysql_close(mysql_conn);
    redisFree(redis_c);

    return 0;
}

Implementing Write Invalidation

When data is modified in MySQL, the corresponding cache entries must be invalidated to prevent serving stale data. This is crucial for maintaining data integrity. The simplest approach is to explicitly delete the cache key after a successful database write operation.

// Assuming 'redis_c' is an active redisContext and 'mysql_conn' is an active MYSQL connection

// Function to update user profile in MySQL
int update_user_profile_in_db(long long user_id, const char *new_name, const char *new_email, MYSQL *conn) {
    // ... (MySQL UPDATE query execution logic here) ...
    // For simplicity, assume success and return 1
    printf("Updating user %lld in DB...\n", user_id);
    return 1;
}

// Function to update user profile and invalidate cache
int update_user_profile(long long user_id, const char *new_name, const char *new_email, redisContext *redis_c, MYSQL *mysql_conn) {
    // 1. Update the database
    if (!update_user_profile_in_db(user_id, new_name, new_email, mysql_conn)) {
        fprintf(stderr, "Failed to update user %lld in database.\n", user_id);
        return 0; // Indicate failure
    }

    // 2. Invalidate the cache entry
    char cache_key[100];
    snprintf(cache_key, sizeof(cache_key), "user:profile:%lld", user_id);

    redisReply *reply = (redisReply *)redisCommand(redis_c, "DEL %s", cache_key);
    if (reply == NULL) {
        fprintf(stderr, "Redis DEL command failed for key %s: %s\n", cache_key, redis_c->errstr);
        // Log this error, but the DB update was successful.
        // The cache will eventually expire via TTL, but this is not ideal.
    } else {
        printf("Cache invalidated for key %s (deleted: %lld)\n", cache_key, reply->integer);
        freeReplyObject(reply);
    }

    return 1; // Indicate success
}

// Example usage within main() or another function:
// if (update_user_profile(user_id_to_fetch, "Alice Smith", "[email protected]", redis_c, mysql_conn)) {
//     printf("User profile updated and cache invalidated successfully.\n");
// } else {
//     printf("Failed to update user profile.\n");
// }

Connection Pooling and Thread Safety

For high-throughput applications, establishing a new Redis connection for every request is prohibitively expensive. Implementing connection pooling is essential. hiredis itself doesn’t provide a built-in connection pool, but you can manage one manually or use a third-party library. A common pattern is to create a pool of connections at application startup and distribute them among threads.

hiredis is thread-safe in terms of its API calls, meaning multiple threads can call hiredis functions concurrently on different redisContext objects. However, a single redisContext object is not thread-safe. If you intend to use hiredis from multiple threads, each thread should ideally have its own redisContext, or you must protect shared contexts with mutexes.

A simple thread-safe connection pool can be implemented using a queue and mutexes. When a thread needs a connection, it acquires a lock, takes a connection from the pool, and releases the lock. When done, it returns the connection to the pool under the same lock.

#include <pthread.h>
#include <queue>
#include <hiredis/hiredis.h>

#define MAX_CONNECTIONS 10

typedef struct {
    redisContext *context;
    // Other connection-specific data if needed
} RedisConnection;

typedef struct {
    queue_t *connection_queue; // Using a thread-safe queue implementation
    pthread_mutex_t mutex;
    pthread_cond_t cond;
} RedisConnectionPool;

// Initialize the pool
void init_redis_pool(RedisConnectionPool *pool, const char *host, int port) {
    pthread_mutex_init(&pool->mutex, NULL);
    pthread_cond_init(&pool->cond, NULL);
    pool->connection_queue = create_thread_safe_queue(); // Implement this

    for (int i = 0; i < MAX_CONNECTIONS; ++i) {
        redisContext *c = redisConnect(host, port);
        if (c && !c->err) {
            RedisConnection *rc = malloc(sizeof(RedisConnection));
            rc->context = c;
            enqueue_thread_safe_queue(pool->connection_queue, rc); // Implement this
        } else {
            fprintf(stderr, "Failed to create Redis connection %d\n", i);
            // Handle error: maybe retry or exit
        }
    }
}

// Get a connection from the pool
RedisConnection* get_redis_connection(RedisConnectionPool *pool) {
    pthread_mutex_lock(&pool->mutex);
    RedisConnection *rc = NULL;
    while (is_empty_thread_safe_queue(pool->connection_queue)) { // Implement this
        pthread_cond_wait(&pool->cond, &pool->mutex);
    }
    rc = dequeue_thread_safe_queue(pool->connection_queue); // Implement this
    pthread_mutex_unlock(&pool->mutex);
    return rc;
}

// Return a connection to the pool
void release_redis_connection(RedisConnectionPool *pool, RedisConnection *rc) {
    pthread_mutex_lock(&pool->mutex);
    enqueue_thread_safe_queue(pool->connection_queue, rc); // Implement this
    pthread_cond_signal(&pool->cond);
    pthread_mutex_unlock(&pool->mutex);
}

// Destroy the pool
void destroy_redis_pool(RedisConnectionPool *pool) {
    pthread_mutex_lock(&pool->mutex);
    while (!is_empty_thread_safe_queue(pool->connection_queue)) {
        RedisConnection *rc = dequeue_thread_safe_queue(pool->connection_queue);
        redisFree(rc->context);
        free(rc);
    }
    destroy_thread_safe_queue(pool->connection_queue); // Implement this
    pthread_mutex_unlock(&pool->mutex);
    pthread_mutex_destroy(&pool->mutex);
    pthread_cond_destroy(&pool->cond);
}

// Example usage in a threaded worker function:
/*
void *worker_thread(void *arg) {
    RedisConnectionPool *pool = (RedisConnectionPool *)arg;
    RedisConnection *rc = get_redis_connection(pool);
    if (rc) {
        // Use rc->context for Redis operations
        redisCommand(rc->context, "PING");
        // ... other operations ...
        release_redis_connection(pool, rc);
    }
    return NULL;
}
*/

Note: The queue_t and its associated functions (create_thread_safe_queue, enqueue_thread_safe_queue, dequeue_thread_safe_queue, is_empty_thread_safe_queue, destroy_thread_safe_queue) are placeholders for a robust thread-safe queue implementation. You would typically use libraries like pthreads for mutexes/condition variables and implement the queue logic yourself or use a suitable library.

Serialization Formats

When storing complex data structures (like JSON objects representing user profiles) in Redis, you need a serialization format. JSON is a common choice due to its human-readability and widespread support. However, for maximum performance and minimal overhead in C, consider:

  • Binary Serialization: Libraries like Protocol Buffers or MessagePack offer compact binary formats that are faster to serialize/deserialize and consume less memory/network bandwidth than JSON.
  • Custom Binary Formats: For very specific, fixed data structures, a custom binary format can be highly optimized.

If using JSON, ensure your C application has a robust JSON parsing library (e.g., json-c or cJSON). For this example, we’ve used plain strings which are suitable for simple values or pre-formatted JSON strings.

Monitoring and Performance Tuning

Effective caching requires continuous monitoring. Key metrics to track include:

  • Cache Hit Ratio: (Number of cache hits) / (Total number of cache lookups). Aim for a high hit ratio (e.g., > 90%).
  • Cache Latency: The time taken for Redis GET/SET operations.
  • Redis Memory Usage: Monitor Redis’s memory consumption to avoid OOM errors.
  • Network Throughput: Ensure your network connection to Redis is not saturated.
  • CPU Usage: Both application and Redis CPU usage.

Redis provides commands like INFO and MONITOR (use with caution in production as it can impact performance) to gather statistics. Tools like RedisInsight or Prometheus with the Redis exporter can provide more sophisticated monitoring.

Tuning involves adjusting TTLs, optimizing cache keys, potentially increasing Redis memory limits, and ensuring efficient serialization. For very high-throughput scenarios, consider Redis Cluster for sharding and high availability.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Step-by-Step: Diagnosing indexing lock conflicts and high CPU during bulk stock updates on DigitalOcean Servers
  • How to Debug and Fix memory leaks and socket exhaustion in daemon processes in Modern C++ Applications
  • Infrastructure as Code: Provisioning Secure PHP Clusters on DigitalOcean Using Terraform
  • Fixing Slow Largest Contentful Paint (LCP) caused by unoptimized database queries in Legacy Laravel Codebases Without Breaking API Contracts
  • An Auditor’s Checklist for Securing Laravel Backends on Google Cloud

Copyright © 2026 · Vinay Vengala