Implementation of a first example of a Resilient Cache-Aside Layer with Redis and PostgreSQL.
Redis
Redis (Remote Dictionary Server) is a high-performance, in-memory data structure store that functions as a database, cache, and message broker. Unlike traditional relational databases (RDBMS) like PostgreSQL that prioritize disk storage, Redis maintains its entire dataset in RAM. This ensures near-instantaneous read/write speeds, with latencies measured in sub-milliseconds.
Key Characteristics
- Extreme Speed: Capable of processing hundreds of thousands of operations per second.
- Flexible Data Structures: Supports Lists, Sets, Hashes, Bitmaps, and Streams.
- Optional Persistence: Can be configured to save snapshots to disk periodically.
- Simplicity: Follows an intuitive Key-Value model.
Common Use Cases
- Caching System: Reduces load on the primary DB by storing frequent query results.
- Session Management: Ideal for web applications needing fast access to login states or shopping carts.
- Leaderboards: Uses Sorted Sets to rank scores in real-time.
- Message Queues: Facilitates microservice communication via Pub/Sub or Streams.
Infrastructure Strategy: VM vs. Kubernetes
Choosing between Virtual Machines (VM) and Kubernetes (K8s) depends on performance vs. convenience.
| Criterion | Virtual Machine (VM) | Kubernetes (K8s) |
|---|---|---|
| Performance | Maximum (Low Latency). Avoids SDN overhead. | Good, but dependent on the network layer. |
| Memory Control | Full kernel control (Overcommit, THP).+1 | Shared resources via Cgroups. |
| Persistence | Native and simple.+1 | Complex (StatefulSets). |
| Ideal For | Latency-critical production. | Microservices and rapid CI/CD. |
Exportar para Sheets
Implementation Choice: We utilized a dedicated VM (srv-infrastructure-redis-master-01) via Vagrant to ensure maximum hardware performance and direct kernel access.
Provisioning and Kernel Optimization
Infrastructure starts at the OS level. Using Vagrant, we provisioned a node named srv-infrastructure-redis-master-01.
And a DNS: redis.devops-db.internal to IP 172.21.5.165
nslookup redis.devops-db.internal
Server: 100.64.0.1
Address: 100.64.0.1#53
Name: redis.devops-db.internal
Address: 172.21.5.165The “Why” Behind Kernel Tuning
One of the most critical steps in a Redis deployment is the Overcommit Memory setting.
# vi /etc/sysctl.conf
vm.overcommit_memory = 1
sudo sysctl -pTechnical Justification: Redis performs background saves (BGSAVE/AOF rewrite) by forking the process. Without overcommit_memory = 1, the Linux kernel might deny the fork request if it detects that the memory usage could potentially exceed the physical RAM, even if the actual copy-on-write memory usage is small. This ensures stability during data persistence tasks.
Installation
Simple as that:
sudo apt update
sudo apt install redis-server -y
Start the service
sudo systemctl restart redis-server
sudo systemctl enable redis-server
sudo systemctl status redis-serverCheck:
redis-cli -h redis.devops-db.internal -a zaMKYDo8AJ4XnAS2SwqJLTE8GsqrBEA939dQUlI5UK1ikJyA325U9zRtB ping
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
PONGWith this, you can then use redis-cli normally.
export REDISCLI_AUTH="zaMKYDo8AJ4XnAS2SwqJLTE8GsqrBEA939dQUlI5UK1ikJyA325U9zRtB"
redis-cli -h redis.devops-db.internalAdvanced Redis Configuration
Our instance is mapped to redis.devops-db.internal. We configured the server with a focus on memory boundaries and persistence durability.
Memory Management and Policy
# /etc/redis/redis.conf
bind 127.0.0.1 172.21.5.165
maxmemory 3gb
appendonly yes
appendfsync everysec
aof-use-rdb-preamble yes
# Strong temporary password (we can replace it with ACLs later)
requirepass zaMKYDo8AJ4XnAS2SwqJLTE8GsqrBEA939dQUlI5UK1ikJyA325U9zRtB
The “Magic” of Persistence: RDB, AOF, and Hybrid
Redis offers two primary durability methods:
- RDB (Redis Database): Takes “snapshots” at specific intervals (e.g., every 5 minutes). Excellent for backups.+1
- AOF (Append Only File): Logs every write operation. Safer but results in larger files.
How Hybrid Persistence Works
The “magic” occurs during the AOF Rewrite process. With aof-use-rdb-preamble yes:+1
- Binary Snapshot: Redis takes an instantaneous binary snapshot (RDB format) of the entire memory.
- Injection: This binary snapshot is written at the start of the new AOF file.
- Log Append: Subsequent commands are appended in standard text log format at the end of the file.
- The Result: Upon restart, Redis loads the binary preamble at staggering speeds and then replays the few remaining text commands. You get millisecond boot times and zero data loss.+1
Caching Patterns and Strategies
Side-Cache (Cache-Aside)
The application manages the communication between Redis and the DB.
- Workflow: The app checks Redis. On a Cache Miss, it fetches from PostgreSQL, responds to the user, and then saves it in Redis for future use.
- Resilience: High; if Redis fails, the system continues via the DB (at lower speeds).
Inline-Cache (Read-Through / Write-Through)
The application treats Redis as the primary data store, unaware of the DB backend.
- Workflow: The app asks Redis. If missing, a middleware/plugin fetches from the DB and returns it to the app.
- Pros/Cons: Cleaner code but higher infra complexity and total dependency on the cache layer.+2
Comparison Table
| Feature | Side-Cache (Cache-Aside) | Inline-Cache (Read/Write Through) |
|---|---|---|
| Complexity | High (App manages logic). | Low (App only asks Redis). |
| Infrastructure | Low. | High. |
| Resilience | System works if Redis fails. | System fails if the cache fails. |
Exportar para Sheets
Data Modeling: SQL vs. Redis
We simulate tables using Namespacing (prefixes separated by :).
Translation Table
| SQL Operation | Redis Command (Hash) | Description |
|---|---|---|
INSERT INTO users | HSET user:1 field val | Creates the object. |
SELECT * WHERE id=1 | HGETALL user:1 | Returns the full object. |
SELECT email | HGET user:1 email | Extracts one field. |
UPDATE SET dept='X' | HSET user:1 dept 'X' | Partial update. |
DELETE WHERE id=1 | DEL user:1 | Removes object. |
SELECT count(*) | DBSIZE | Counts all keys in the DB. |
Exportar para Sheets
Professional Security: The ACL Revolution
Security was transitioned from a global requirepass to a granular ACL system.
Admin and User Roles
We defined an administrator, faustobranco, with total control, and a restricted devops_api_user for the application.
https://redis.io/docs/latest/commands/acl-cat
User Implementation
The Admin (faustobranco): Full privileges over all keys and commands.
ACL SETUSER faustobranco on >1234qwer ~* +@allAPI User (api_user): Restricted to user:profile:* keys; cannot flush the DB.
ACL SETUSER api_user on >password_forte_123 ~user:profile:* +@hash +@read +@write -flushall -flushdb+1LDAP Sync (ldap_sync): Manage ldap:* keys without administrative access.
ACL SETUSER ldap_sync on >password_ldap_456 ~ldap:* +@all -@adminCheck:
ACL USERS
1) "default"
2) "devops_api_user"
3) "faustobranco"ACL GETUSER faustobranco
1) "flags"
2) 1) "on"
2) "allkeys"
3) "allcommands"
3) "passwords"
4) 1) "ef51306214d9a6361ee1d5b452e6d2bb70dc7ebb85bf9e02c3d4747fb57d6bec"
5) "commands"
6) "+@all"
7) "keys"
8) 1) "*"With a username/password, calling redis-cli is slightly different.
redis-cli -h redis.devops-db.internal --user faustobranco -a 1234qwer| ACL LIST | Displays all users and their rules in text format. |
| ACL USERS | Lists only the names of created users. |
| ACL WHOAMI | It tells you which user you are currently logged in as. |
| ACL GETUSER <nome> | Shows specific details about a user (very useful!). |
| ACL DELUSER <nome> | Removes the user from the system. |
Expert-Level redis.conf Tuning
To achieve production stability on a 4GB VM, we implemented specific resource boundaries.
Multi-Threaded I/O: Offloads network socket operations.
io-threads 3
io-threads-do-reads yesActive Defragmentation: Real-time memory compaction to eliminate fragmentation.
activedefrag yesSlow Client Protection: Prevents RAM exhaustion from slow consumers.
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60Aggressiveness settings (recommended default values)
active-defrag-ignore-bytes 100mb
active-defrag-threshold-initial 10
active-defrag-threshold-upper 100Records events that cause latency greater than 100 milliseconds
latency-monitor-threshold 100Avoid performing fsync while a BGSAVE or BGREWRITEAOF is running. This reduces latency (ms spikes) in exchange for a minimal risk of data loss.
no-appendfsync-on-rewrite yes
Memory Auditing and Monitoring
Distinguishing between used_memory and used_memory_rss is vital for capacity planning.
Memory Footprint (Lua Script)
To calculate the total memory used by a specific key pattern:
EVAL "local keys = redis.call('keys', ARGV[1]) local total = 0 for i, k in ipairs(keys) do total = total + redis.call('memory', 'usage', k) end return total" 0 "user:profile:*"
Integration Code

The user_manager.py logic implements the core strategy: Check the cache; if not found, query the DB and update the cache.
Why Hashes instead of Strings?
While SET/GET (Strings) are easy, Hashes (HSET/HGETALL) are significantly more memory-efficient in Redis. They allow us to store complex objects as a single key while maintaining the ability to retrieve specific fields without deserializing a whole JSON blob.
Data Seeding (faker2.py)
This script populates PostgreSQL with 300-500 realistic profiles.
import random
import unicodedata
import psycopg2
from psycopg2.extras import execute_batch
from faker import Faker
# Existing connection constants
PG_HOST = 'postgresql.devops-db.internal'
PG_USER = 'devops_api'
PG_PASSWORD = '1234qwer'
PG_DATABASE = 'devops_api'
def remove_accents(input_text: str) -> str:
"""
Removes diacritical marks (accents) from a given string.
"""
normalized_text = unicodedata.normalize('NFD', input_text)
filtered_text = ''.join(
character for character in normalized_text
if unicodedata.category(character) != 'Mn'
)
return filtered_text
def recreate_table_and_populate():
"""
Recreates the users table with the new schema and inserts
between 300 and 500 realistic records.
"""
# Initialize Faker with Portuguese locale for realistic local names
data_generator = Faker('pt_PT')
valid_departments = ['Engineering', 'Human Resources', 'DevOps', 'Security', 'Sales', 'Marketing']
# Define a random volume of rows between 300 and 500
total_records = random.randint(300, 500)
try:
db_connection = psycopg2.connect(
host=PG_HOST,
user=PG_USER,
password=PG_PASSWORD,
dbname=PG_DATABASE
)
db_cursor = db_connection.cursor()
# Step 1: Update the schema to include gender and birth_date
print("INFO: Recreating 'users' table with the new schema...")
db_cursor.execute("""
DROP TABLE IF EXISTS devops_api.users;
CREATE TABLE devops_api.users (
id SERIAL PRIMARY KEY,
full_name VARCHAR(100) NOT NULL,
gender CHAR(1) NOT NULL,
birth_date DATE NOT NULL,
department VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL,
username VARCHAR(100) NOT NULL
);
""")
# Step 2: Generate realistic, correlated data
print(f"INFO: Generating {total_records} realistic records...")
records_to_insert = []
for _ in range(total_records):
# Determine gender first to match the generated name appropriately
random_gender = random.choice(['M', 'F'])
if random_gender == 'M':
fake_name = data_generator.name_male()
else:
fake_name = data_generator.name_female()
user_name = remove_accents(fake_name).split()[0].lower() + "." + remove_accents(fake_name).split()[-1].lower()
fake_email = f"{user_name}@fake-devops-db.internal"
# Generate a birth date for working-age adults
fake_birth_date = data_generator.date_of_birth(minimum_age=18, maximum_age=65)
fake_department = random.choice(valid_departments)
records_to_insert.append((fake_name, random_gender, fake_birth_date, fake_department, fake_email, user_name))
# Step 3: Perform the bulk insert
sql_insert_query = """
INSERT INTO devops_api.users (full_name, gender, birth_date, department, email, username)
VALUES (%s, %s, %s, %s, %s, %s);
"""
print("INFO: Executing bulk insert into PostgreSQL...")
execute_batch(db_cursor, sql_insert_query, records_to_insert)
db_connection.commit()
print(f"INFO: Database seeded successfully with {total_records} rows.")
db_cursor.close()
db_connection.close()
except psycopg2.Error as err:
print(f"ERROR: Database operation failed. Details: {err}")
if __name__ == "__main__":
recreate_table_and_populate()Cache Orchestrator (user_manager.py)
Implements the Cache-Aside strategy with ACL-based authentication.
import os
import psycopg2
import redis
from datetime import date
# Infrastructure Configuration
PG_CONFIG = {
'host': 'postgresql.devops-db.internal',
'user': 'devops_api',
'password': '1234qwer',
'dbname': 'devops_api'
}
REDIS_CONFIG = {
'host': '172.21.5.165',
'port': 6379,
'password': 'zaMKYDo8AJ4XnAS2SwqJLTE8GsqrBEA939dQUlI5UK1ikJyA325U9zRtB',
'decode_responses': True
}
def get_redis_client():
"""Returns a functional Redis client."""
return redis.StrictRedis(**REDIS_CONFIG)
def fetch_user_from_db(user_id):
"""
Retrieves the complete user profile from PostgreSQL.
Maps the new schema: full_name, gender, birth_date, department, email, username.
"""
try:
conn = psycopg2.connect(**PG_CONFIG)
cursor = conn.cursor()
# Explicitly selecting columns based on the new schema
query = """
SELECT full_name, gender, birth_date, department, email, username
FROM devops_api.users
WHERE id = %s;
"""
cursor.execute(query, (user_id,))
row = cursor.fetchone()
cursor.close()
conn.close()
if row:
# Map birth_date to string for Redis compatibility
return {
"full_name": row[0],
"gender": row[1],
"birth_date": str(row[2]), # Convert date object to string
"department": row[3],
"email": row[4],
"username": row[5]
}
return None
except Exception as error:
print(f"DATABASE_ERROR: {error}")
return None
def sync_user_to_cache(redis_client, user_id, user_data):
"""
Saves the user profile into a Redis Hash and sets a TTL.
"""
cache_key = f"user:profile:{user_id}"
# Store all fields at once using the mapping argument
redis_client.hset(cache_key, mapping=user_data)
# Set expiration for 2 hours (7200 seconds)
redis_client.expire(cache_key, 7200)
print(f"CACHE_SYNC: User {user_id} cached successfully.")
def get_user_data(user_id):
"""
Main entry point implementing the Cache-Aside pattern.
"""
r_client = get_redis_client()
cache_key = f"user:profile:{user_id}"
# Check Redis first
cached_profile = r_client.hgetall(cache_key)
if cached_profile:
print(f"CACHE_HIT: Serving user {user_id} from Redis.")
return cached_profile
# Fallback to Postgres
print(f"CACHE_MISS: Fetching user {user_id} from PostgreSQL.")
db_profile = fetch_user_from_db(user_id)
if db_profile:
sync_user_to_cache(r_client, user_id, db_profile)
return db_profile
return {"error": "User not found"}
if __name__ == "__main__":
# Test with one of the IDs generated by your faker script
test_user_id = 1
profile = get_user_data(test_user_id)
print(f"FINAL_DATA: {profile}")SCAN 0 should not be performed; it’s like a Select * in RDBMS, but to illustrate:
SCAN 0
1) "0"
2) 1) "user:profile:1"
2) "totp:secret:user_ldap_100"
3) "totp:secret:user_ldap_99"After running the Python script to generate the data and insert it into Redis, we can verify the inserted data and the TTL (Time To Live) behavior using iredis, a command-line client for Redis.
redis.devops-db.internal:6379> HGETALL user:profile:1
1) "full_name"
2) "\xc3\x89rica de Teixeira"
3) "gender"
4) "F"
5) "birth_date"
6) "1987-09-21"
7) "department"
8) "DevOps"
9) "email"
10) "erica.teixeira@fake-devops-db.internal"
11) "username"
12) "erica.teixeira"
redis.devops-db.internal:6379> TTL "user:profile:1"
(integer) 7182After 2 hours:
redis.devops-db.internal:6379> TTL "user:profile:1"
(integer) -2
redis.devops-db.internal:6379> HGETALL user:profile:1
(empty array)