Skip to content

08: Redis (Caching, Distributed Locks & Pub/Sub)

8.1 Introduction

Redis is an in-memory data store designed for:

• ultra-fast read/write operations
• caching
• rate limiting
• Pub/Sub messaging
• distributed locks
• ephemeral data storage
• background task queues

Within LocalCloudLab, Redis plays a critical role in improving performance, reducing database load, enabling real-time updates inside the cluster, and providing safe distributed coordination between microservices.

Why Redis?

Redis is perfect for:

✔ High-speed caching of frequently-used queries
✔ Reducing PostgreSQL load
✔ Distributed locks for sensitive operations
✔ Pub/Sub messaging between your APIs
✔ Short-lived state (tokens, temporary data, throttling)

Why Redis in Kubernetes?

Running Redis inside your k3s cluster provides:

• Fast networking (in-cluster service)
• No external dependencies
• Easy integration with .NET APIs
• Simple deployment using Helm
• Optional durability if needed

In LocalCloudLab we begin with single-node Redis but the architecture supports upgrading to master/replica or Redis Sentinel later.

8.2 Installing Redis (Bitnami Helm Chart)

Bitnami provides a stable and actively maintained Redis chart. We will install Redis in single-node mode (simple and perfect for a local cluster).

8.2.1 Add Bitnami repository

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

8.2.2 Create a namespace for caching

kubectl create namespace caching

8.2.3 Install Redis single-node cluster

helm install redis bitnami/redis       -n caching       --set architecture=standalone       --set auth.password=YourRedisPassword       --set master.persistence.size=5Gi       --set master.persistence.storageClass="local-path"

Key parameters:

• architecture=standalone
    We use a single Redis instance (simple & enough for LocalCloudLab).

• auth.password
    Redis requires a password by default (good practice).

• persistence.size
    While Redis is in-memory, the persistence layer stores snapshots (RDB) if enabled.

• storageClass
    Uses k3s "local-path" storage by default.

Check installation:

kubectl get pods -n caching
kubectl get svc -n caching

Expected service:

redis-master.caching.svc.cluster.local

To get the password (if not set manually):

kubectl get secret redis -n caching -o jsonpath="{.data.redis-password}" | base64 -d

8.2.4 Test Redis inside the cluster

kubectl run -it redis-client --rm --image=bitnami/redis:latest -n caching -- bash

Inside the pod:

redis-cli -h redis-master -a YourRedisPassword ping

Expected output:

PONG

Redis is now fully operational and reachable from within your cluster.

8.3 Integrating Redis with .NET APIs

Your Search API and Checkin API can leverage Redis for:

• caching
• temporary tokens
• throttling
• reducing load on PostgreSQL

There are two .NET approaches:

1. Using ConnectionMultiplexer (StackExchange.Redis) directly
2. Using Microsoft IDistributedCache

Both will be shown here.

8.3.1 Install Redis client packages

For low-level access (recommended):

dotnet add package StackExchange.Redis

For IDistributedCache integration:

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis

8.3.2 Connection string for Redis

In k8s:

redis-master.caching.svc.cluster.local:6379

Example in appsettings.json:

"Redis": {
  "ConnectionString": "redis-master.caching.svc.cluster.local:6379,password=YourRedisPassword,abortConnect=false"
}

Important: Redis connections should be reused. Do NOT create a new connection per request.

8.3.3 Register Redis in Program.cs

Preferred approach using ConnectionMultiplexer:

builder.Services.AddSingleton<ConnectionMultiplexer>(sp =>
    ConnectionMultiplexer.Connect(builder.Configuration["Redis:ConnectionString"]));

Retrieve it inside services:

private readonly IDatabase _cache;

public SearchCacheService(ConnectionMultiplexer redis)
{
    _cache = redis.GetDatabase();
}

8.3.4 Using Redis as a cache (manual control)

Cache data with TTL:

await _cache.StringSetAsync(
    key: $"search:{query}",
    value: jsonResult,
    expiry: TimeSpan.FromMinutes(5)
);

Retrieve:

var cached = await _cache.StringGetAsync($"search:{query}");
if (!cached.IsNullOrEmpty)
    return JsonSerializer.Deserialize<SearchResult>(cached);

This drastically reduces load on PostgreSQL and increases performance.

8.3.5 Using IDistributedCache (higher-level API)

Register:

builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration["Redis:ConnectionString"];
    options.InstanceName = "LocalCloudLab";
});

Store:

await _cache.SetStringAsync(
    "mykey",
    JsonSerializer.Serialize(obj),
    new DistributedCacheEntryOptions
    {
        AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
    });

Retrieve:

var value = await _cache.GetStringAsync("mykey");

(End of Part 1 — Part 2 will continue with distributed locking, Pub/Sub, monitoring, and durability.)

8.4 Distributed Locking with Redis

When multiple instances of a service run concurrently (or multiple services modify shared state), you may need distributed locks to prevent race conditions.

Common use cases:

• Preventing double-processing of the same order/booking
• Ensuring only one instance performs a scheduled job
• Safely updating counters or stock levels
• Guarding a critical section shared between replicas

Redis is widely used for distributed locks because:

• It is fast
• SETNX operation provides atomic "lock if not exists"
• Expiration (TTL) can release locks automatically if a process crashes

8.4.1 Basic locking pattern with Redis

Simplified lock algorithm:

1. Try to set a key with NX (only if not exists) and EX (expiry)
2. If successful → you hold the lock
3. Perform critical work
4. Delete the key to release the lock
5. If unsuccessful → someone else holds the lock

In raw Redis CLI:

SET my-lock some-random-value NX EX 30

In .NET with StackExchange.Redis:

var lockKey = "lock:booking:" + bookingId;
var lockValue = Guid.NewGuid().ToString();
var acquired = await _cache.StringSetAsync(
    lockKey,
    lockValue,
    expiry: TimeSpan.FromSeconds(30),
    when: When.NotExists);

if (!acquired)
{
    // Another process owns the lock
    return;
}

try
{
    // Critical section
}
finally
{
    // Release lock only if we still own it
    var current = await _cache.StringGetAsync(lockKey);
    if (current == lockValue)
    {
        await _cache.KeyDeleteAsync(lockKey);
    }
}

8.4.2 RedLock (advanced pattern)

RedLock is an algorithm for distributed locks across multiple Redis instances. In LocalCloudLab (single-node Redis), RedLock is not strictly necessary but it’s good to be aware of it.

There is a .NET library:

dotnet add package RedLock.net

Usage pattern:

var redlockFactory = RedLockFactory.Create(new[] { new RedLockEndPoint { EndPoint = "redis-master.caching.svc.cluster.local:6379" } });

using (var redLock = await redlockFactory.CreateLockAsync("lock-key", TimeSpan.FromSeconds(30)))
{
    if (redLock.IsAcquired)
    {
        // Critical section
    }
}

8.4.3 Anti-patterns to avoid

✗ No expiration on locks (can lead to permanent deadlocks)
✗ Using non-unique lock values (harder to know who owns the lock)
✗ Long-running critical sections (lock timeouts too short or too long)
✗ Using Redis locks for everything (locks should be reserved for special cases only)

8.5 Redis Pub/Sub

Redis includes a built-in publish/subscribe mechanism.

It can be used for:

• Notifying workers about new tasks
• Broadcasting cache invalidation messages
• Real-time notifications between services
• Internal messaging for LocalCloudLab components

Pub/Sub is fire-and-forget:

• Messages are not stored long-term
• If a subscriber is offline, it misses messages
• Great for "live" signals, not long-term queues

8.5.1 Basic Pub/Sub flow

Channels are simple string names, e.g.:

"search.events"
"checkin.notifications"

Publisher example in .NET:

var sub = redis.GetSubscriber();
await sub.PublishAsync("search.events", "refresh-cache");

Subscriber example:

var sub = redis.GetSubscriber();
await sub.SubscribeAsync("search.events", (channel, message) =>
{
    Console.WriteLine($"Received on {channel}: {message}");
    // React: invalidate cache, trigger background fetch, etc.
});

8.5.2 Use cases in LocalCloudLab

Examples:

• Search API publishes "search.cache.invalidated" when new data is written
• Checkin API publishes "checkin.created" when a user checks in
• A background worker subscribes and performs extra actions (emails, logs, analytics)

You can model this as:

- Search API → Redis (Pub)
- Worker service → Redis (Sub) → PostgreSQL / other APIs

8.5.3 Pub/Sub vs queues

Redis Pub/Sub is NOT a durable queue. If you need reliable queues with persistence, use:

• Redis streams, or
• RabbitMQ (covered in a later section)

For simple internal signals, Pub/Sub is enough.

8.6 Monitoring Redis

Redis is high-performance, but it can:

• Run out of memory
• Have blocked operations
• Show latency spikes
• Be misconfigured for persistence

Monitoring is key to preventing hidden issues.

8.6.1 Install Redis Exporter

Use the official Helm chart:

helm install redis-exporter prometheus-community/prometheus-redis-exporter       -n caching       --set redisAddress=redis-master.caching.svc.cluster.local:6379       --set redisPassword=YourRedisPassword

This exposes metrics like:

• redis_connected_clients
• redis_memory_used_bytes
• redis_commands_processed_total
• redis_keyspace_hits_total
• redis_keyspace_misses_total
• redis_evicted_keys_total

8.6.2 Add Redis dashboards in Grafana

Import community dashboards such as:

• Redis Overview
• Redis Performance

Common metrics to watch:

• Memory usage vs max memory
• Evictions
• Command latency
• Hit/miss ratio
• Connected clients
• Key count

8.7 Durability & High Availability Options

By default, Redis is in-memory.

Persistence is optional and controlled via:

• RDB (snapshotting)
• AOF (append-only file)

In LocalCloudLab, you can start with minimal persistence and gradually add more.

8.7.1 RDB snapshots

Redis periodically takes snapshots of data and writes them to disk.

Configuration options (redis.conf style):

save 900 1   # after 1 change in 900 seconds
save 300 10
save 60  10000

In the Bitnami chart, these are typically set via values overrides. RDB is:

✔ Fast
✔ Compact

But:

✗ You may lose recent changes between snapshots.

8.7.2 AOF (Append Only File)

AOF writes every operation to a log file:

• Very durable
• Can replay all operations to restore state

Trade-off:

• More disk I/O
• Larger files

For LocalCloudLab, RDB alone is usually enough.

8.7.3 Master/Replica (for future scaling)

Bitnami chart can enable replication:

helm install redis bitnami/redis       -n caching       --set architecture=replication       --set auth.password=YourRedisPassword

This creates:

• 1 master
• 1+ replicas

Benefits:

• Read scaling
• Basic resilience

Still, failover needs Sentinel or an external orchestrator for full HA.

8.8 Summary of Section 8

In this section, you:

✔ Installed Redis using Bitnami’s Helm chart on k3s
✔ Integrated Redis with your .NET APIs using StackExchange.Redis and IDistributedCache
✔ Implemented caching to reduce load on PostgreSQL
✔ Learned how to build distributed locks with Redis
✔ Used Redis Pub/Sub for internal messaging and notifications
✔ Monitored Redis with Prometheus exporter and Grafana dashboards
✔ Reviewed durability options (RDB, AOF) and replication patterns

Redis now acts as a high-speed, flexible auxiliary data layer for LocalCloudLab.

In the next section (Section 09), you will:

• Deploy RabbitMQ for durable queues
• Implement background workers and asynchronous messaging
• Integrate .NET services with RabbitMQ (publisher/consumer)
• Build resilient patterns for long-running operations

(End of Section 08 — Complete)