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)