Caching with Redis
Imagine you are managing an e-commerce website. On the 11/11 super sale day, traffic spikes 100-fold. Millions of users...
Caching with Redis
Unit 5: Performance Patterns (Redis Caching) Topic Code: PERF-501 Reading Time: ~40 minutes
Learning Objectives
- Explain the benefits of caching for system performance and scalability.
- Introduce Redis and its core features as a key-value store.
- Describe and differentiate caching strategies: Cache-Aside, Read-Through, and Write-Through.
- Analyze common cache invalidation strategies (TTL, write-through, explicit invalidation).
- Implement the Cache-Aside pattern in Python with Redis to optimize an API endpoint.
Section 1: Concept/Overview
1.1 Introduction
Imagine you are managing an e-commerce website. On the 11/11 super sale day, traffic spikes 100-fold. Millions of users simultaneously access the hottest product pages. For every access, your system has to run a complex SQL query to retrieve product information, check inventory, and fetch reviews from the database. The database quickly becomes overloaded, response times skyrocket, and finally, the website crashes. Customers are disappointed, and you lose revenue.
This is the problem that caching was born to solve. Instead of "bothering" the database for every identical request, we can store the result of the first access in a place with extremely fast retrieval speeds (the cache). Subsequent accesses will retrieve data directly from the cache without touching the database. This significantly reduces load on the database, increases application response speed, and improves the overall system's load-bearing capacity. In the modern IT industry, caching is no longer a "nice-to-have" option but has become an essential component in the architecture of most high-performance systems, from giant social networks like Facebook and Twitter to streaming platforms like Netflix.
1.2 Formal Definition
Caching is a performance optimization technique that involves storing copies of data in a temporary location (called a cache) so that future requests for that same data can be served faster. A cache is a high-speed data storage layer, typically located between the application layer and the original data storage layer (data source), such as a database.
The main goal of caching is to reduce latency and increase throughput.
- Latency: The time required to retrieve data. Accessing data from RAM (where cache usually resides) is thousands of times faster than accessing it from a disk (where databases usually store data).
- Throughput: The number of requests the system can handle per unit of time. By reducing the load on the database, the system can serve more users simultaneously.
Redis (REmote DIctionary Server) is an open-source, in-memory data structure store, used as a database, cache, and message broker. With the ability to read/write data with sub-millisecond latency, Redis is a top choice for implementing caching.
1.3 Analogy
Imagine your Database is a huge library containing millions of books. The Cache is the librarian's desk.
- Without Cache: Every time a student (user) needs a popular book (hot data), they have to go deep into the bookshelves (database) to search. This process is very slow and laborious, especially when hundreds of students are looking for the same book. The library becomes congested.
- With Cache: The librarian (application) notices that the book "Python Programming for Beginners" is very popular. After the first time going to find it, she decides to place a few copies right on her desk (cache). The next time another student asks to borrow this book, she just needs to grab it from the desk and give it to them immediately. This process is much faster, and the bookshelves inside the library are "quieter" to serve requests for less popular books.
In this example:
- Library: Database (slow but contains everything).
- Desk: Redis Cache (smaller, faster, only contains popular items).
- Librarian: Your Application (decides what should be put on the desk).
- Student: User/Client.
1.4 History
Redis was created by Salvatore Sanfilippo (also known by the nickname "antirez"), an Italian developer. The project started in early 2009. Antirez originally developed Redis to improve the scalability of his real-time web analytics startup. He was dissatisfied with the performance of traditional database systems (like MySQL) for this type of workload.
He needed a system that could handle a large number of write operations and serve data with extremely low latency. Instead of just being a simple key-value store, he designed Redis to support complex data structures like Lists, Sets, and Hashes from the start. This made Redis extremely flexible and powerful. Redis quickly became popular in the open-source community and is trusted by major companies like Twitter, GitHub, Pinterest, and Snapchat.
Section 2: Core Components
2.1 Architecture Overview
A typical caching architecture with Redis acts as an intermediary layer between the application and the main database.
+-----------+ 1. Request data +-----------------+ 2. Cache Miss +---------------+
| | ------------------------> | | ----------------------> | |
| Client/ | | Application | | Database |
| Service | 5. Return data | (with Caching | 3. Get data | (PostgreSQL,|
| | <------------------------ | Logic) | <---------------------- | MySQL) |
+-----------+ | | | |
+-------+---------+ +---------------+
| ^
| | 4. Store in Cache
2b. Cache Hit | | (for next time)
| v
+-------+---------+
| |
| Redis Cache |
| |
+-----------------+
Workflow (Cache-Aside Pattern):
- Client sends a request to retrieve data (e.g., product info
product:123). - Application first checks if data
product:123exists in Redis Cache.
- 2b. Cache Hit (Found): If yes, Redis returns data immediately to the Application. The Application returns it to the Client. The process ends here.
- 2. Cache Miss (Not Found): If not, the Application proceeds to step 3.
- Application queries data from the main Database.
- Application receives data from the Database, then saves a copy of it to Redis Cache with a specific key (e.g.,
product:123) and usually an expiration time (TTL). - Application returns data to the Client.
2.2 Key Components
In the context of caching, Redis provides the following core components:
Component 1: Key-Value Store
- Definition: The foundation of Redis is a key-value store. Every piece of data in Redis is stored as a pair
(key, value).keyis a unique string used to identify data, andvaluecan be a string, a number, or a more complex data structure. - Role: In caching,
keyis usually a unique identifier for a resource (e.g.,user:100,product:_id_), andvalueis that resource's data (often in JSON format or a serialized string). - Syntax (using
redis-cli):
# Set a value for a key
> SET user:101 '{"name": "Alice", "email": "alice@example.com"}'
OK
# Get a value by its key
> GET user:101
"{\"name\": \"Alice\", \"email\": \"alice@example.com\"}"
Component 2: Time-To-Live (TTL)
- Definition: TTL is a feature that allows you to set a lifetime for a key. After this time expires, Redis will automatically delete that key.
- Role: Extremely important in caching to ensure data does not become "stale". It also helps free up memory by automatically removing data that is rarely accessed or no longer valid. This is the simplest and most effective cache invalidation strategy.
- Syntax:
# Set a key that will expire in 60 seconds
> SET session:xyz "some_session_data" EX 60
OK
# Check the remaining time to live for a key
> TTL session:xyz
(integer) 58
# After 60 seconds...
> GET session:xyz
(nil) # nil means the key does not exist
Component 3: Redis Data Structures
-
Definition: Unlike simple key-value stores that only allow string values, Redis supports many complex data structures.
-
Role: Allows for implementation of advanced caching patterns.
-
Strings: Most common for caching, storing HTML pages, JSON objects, etc.
-
Hashes: Ideal for caching objects with multiple fields. Instead of storing the whole JSON object, you can store it as a hash and only update/retrieve individual fields.
-
Lists: Used to cache ordered sequences of data, e.g., the 10 latest posts.
-
Sets: Used to cache collections of unique values, e.g., a list of tags for a post.
-
Syntax (Example with Hash):
# Store user 102's data as a hash
> HSET user:102 name "Bob" email "bob@example.com" age 30
(integer) 3
# Get just the name of user 102
> HGET user:102 name
"Bob"
# Get all fields of the user
> HGETALL user:102
1) "name"
2) "Bob"
3) "email"
4) "bob@example.com"
5) "age"
6) "30"
2.3 Comparison of Approaches (Caching Strategies)
| Approach | Pros | Cons | When to use |
|---|---|---|---|
| Cache-Aside (Lazy Loading) | - Cache logic is in the application, flexible. |
- Resilient to Redis failure: If Redis goes down, the application can
still work (just slower).
- Only caches data when it is actually requested, avoiding caching data
that is never used. | - Cache Miss Penalty: The first request is always
slower because of the 3 steps: query cache, query DB, write cache.
- Data in cache can become stale compared to DB until cache expires or is
invalidated.
- Code is slightly more complex as the app manages both cache and DB. |
Most common. Suitable for read-heavy systems where slight data latency is
acceptable. Most general web applications. | | Read-Through | - Application
logic is simpler: just talks to the cache.
- Cache manages retrieving data from DB upon cache miss. | - Requires a
Redis provider or library that supports Read-Through.
- Initial access still has "cache miss penalty" similar to Cache-Aside.
- Harder to separate app logic and cache logic. | When you want data
retrieval logic encapsulated and reusable. Often seen in systems using
frameworks or platforms providing this mechanism (e.g., Hazelcast, Ehcache). | |
Write-Through | - Data in cache and DB is always consistent.
- Application only needs to write to cache, cache handles writing to DB. |
- Increased write latency because writing to 2 places.
- If Redis goes down, write operations might fail.
- Might cache data that is never read again, wasting memory. |
Applications requiring very high data consistency between cache and DB, e.g.,
banking systems, session management. Not suitable for write-heavy systems. |
Section 3: Implementation
To practice, we will use Python with the redis-py library and Flask framework to build a simple API.
Setup:
# Create a virtual environment
python -m venv venv
source venv/bin/activate
# Install required libraries
pip install Flask redis
Level 1 - Basic (Beginner)
"Hello World" example to connect to Redis, perform SET and GET.
# basic_redis.py
import redis
import time
# Code Example #1: Connecting to Redis
try:
# Connect to a local Redis instance
# decode_responses=True will automatically decode responses from bytes to utf-8 strings
r = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
# Check if the connection is successful
r.ping()
print("Successfully connected to Redis!")
except redis.exceptions.ConnectionError as e:
print(f"Could not connect to Redis: {e}")
exit(1)
# Code Example #2: Basic SET, GET, and TTL
# A simple key-value pair
key = "greeting"
value = "Hello, Redis Fresher!"
print(f"\nSetting key '{key}' to '{value}'")
r.set(key, value)
# Retrieving the value
retrieved_value = r.get(key)
print(f"Retrieved value: {retrieved_value}")
# Setting a key with a Time-To-Live (TTL) of 5 seconds
ttl_key = "transient_key"
print(f"\nSetting key '{ttl_key}' with a 5-second TTL")
r.set(ttl_key, "This will disappear soon...", ex=5)
# Check TTL
print(f"TTL for '{ttl_key}': {r.ttl(ttl_key)} seconds")
# Wait for 6 seconds to see it expire
print("Waiting for 6 seconds...")
time.sleep(6)
# Try to get the expired key
expired_value = r.get(ttl_key)
print(f"Value of '{ttl_key}' after expiration: {expired_value}")
Running the script:
python basic_redis.py
Expected Output:
Successfully connected to Redis!
Setting key 'greeting' to 'Hello, Redis Fresher!'
Retrieved value: Hello, Redis Fresher!
Setting key 'transient_key' with a 5-second TTL
TTL for 'transient_key': 5 seconds
Waiting for 6 seconds...
Value of 'transient_key' after expiration: None
Common Errors:
- Error 1:
redis.exceptions.ConnectionError: Error 61 connecting to localhost:6379. Connection refused. - Description: This error occurs when the Python application cannot connect to the Redis server.
- Fix: Ensure you have installed and started the Redis server. If using Docker, run
docker run -d -p 6379:6379 redis. If installed directly, run theredis-servercommand.
Level 2 - Intermediate
Build an API endpoint to retrieve product information. Initially, it will be very slow. Then, we will apply the Cache-Aside pattern to optimize it.
# intermediate_api.py
from flask import Flask, jsonify
import redis
import time
import json
app = Flask(__name__)
# Connect to Redis
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
# Mock database of products
mock_db = {
"product:101": {"name": "Laptop Pro", "price": 1200, "in_stock": True},
"product:102": {"name": "Gaming Mouse", "price": 75, "in_stock": True},
"product:103": {"name": "Mechanical Keyboard", "price": 150, "in_stock": False},
}
def get_product_from_db(product_id: str) -> dict:
"""A slow function to simulate a database call."""
print(f"--- Database query for {product_id} ---")
time.sleep(2) # Simulate a 2-second delay for DB query
return mock_db.get(product_id)
# Code Example #3: The slow, un-cached endpoint
@app.route('/products/slow/<product_id>')
def get_slow_product(product_id):
"""This endpoint fetches data directly from the 'database' every time."""
start_time = time.time()
product_data = get_product_from_db(f"product:{product_id}")
end_time = time.time()
duration = end_time - start_time
if not product_data:
return jsonify({"error": "Product not found"}), 404
return jsonify({
"data": product_data,
"source": "database",
"duration_seconds": duration
})
# Code Example #4: Implementing Cache-Aside pattern
@app.route('/products/fast/<product_id>')
def get_fast_product(product_id):
"""This endpoint uses Redis caching to speed up responses."""
start_time = time.time()
cache_key = f"product:{product_id}"
# 1. Check cache first
cached_product = redis_client.get(cache_key)
if cached_product:
# Cache Hit!
print(f"--- Cache HIT for {cache_key} ---")
product_data = json.loads(cached_product) # Deserialize from JSON string
source = "cache"
else:
# Cache Miss!
print(f"--- Cache MISS for {cache_key} ---")
# 2. If not in cache, get from DB
product_data = get_product_from_db(cache_key)
if product_data:
# 3. Store in cache for next time with a 60-second TTL
redis_client.set(cache_key, json.dumps(product_data), ex=60)
source = "database"
end_time = time.time()
duration = end_time - start_time
if not product_data:
return jsonify({"error": "Product not found"}), 404
return jsonify({
"data": product_data,
"source": source,
"duration_seconds": duration
})
if __name__ == '__main__':
app.run(debug=True)
How to run and check:
- Run server:
flask --app intermediate_api run - Open browser or
curlto call API:
- Time 1 (Slow):
curl http://127.0.0.1:5000/products/slow/101-> Takes ~2 seconds. - Time 2 (Slow):
curl http://127.0.0.1:5000/products/slow/101-> Still takes ~2 seconds. - Time 1 (Fast - Cache Miss):
curl http://127.0.0.1:5000/products/fast/101-> Takes ~2 seconds, terminal prints--- Cache MISS ---. - Time 2 (Fast - Cache Hit):
curl http://127.0.0.1:5000/products/fast/101-> Response almost immediate (<0.01 seconds), terminal prints--- Cache HIT ---.
Level 3 - Advanced
The caching logic in get_fast_product can be reused but makes code hard to read. We will refactor it into a Python Decorator, a very common and professional pattern. We will also add cache invalidation logic.
# advanced_api.py
from flask import Flask, jsonify, request
import redis
import time
import json
from functools import wraps
app = Flask(__name__)
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
# Mock DB (same as before)
mock_db = {
"product:101": {"name": "Laptop Pro", "price": 1200, "in_stock": True},
"product:102": {"name": "Gaming Mouse", "price": 75, "in_stock": True},
}
def get_product_from_db(product_id: str) -> dict:
print(f"--- Database query for {product_id} ---")
time.sleep(2)
return mock_db.get(product_id)
# Code Example #5: The Caching Decorator
def cache(ttl: int):
"""
A decorator to cache the result of a function with a given TTL.
It assumes the first argument of the decorated function is the cache key identifier.
"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# Generate a cache key from function name and its arguments
# This is more robust than assuming the first arg is the key
key_parts = [func.__name__] + list(map(str, args)) + [f"{k}={v}" for k, v in sorted(kwargs.items())]
cache_key = ":".join(key_parts)
cached_result = redis_client.get(cache_key)
if cached_result:
print(f"--- Cache HIT for {cache_key} ---")
return json.loads(cached_result)
print(f"--- Cache MISS for {cache_key} ---")
result = func(*args, **kwargs)
if result is not None:
redis_client.set(cache_key, json.dumps(result), ex=ttl)
return result
return wrapper
return decorator
# Code Example #6: Applying the decorator to a clean function
@app.route('/products/v3/<product_id>')
@cache(ttl=60)
def get_product_v3(product_id):
"""
The business logic is now clean. The caching is handled by the decorator.
The decorator generates a key like: 'get_product_v3:101'
"""
product_data = get_product_from_db(f"product:{product_id}")
return product_data # We just return the data, decorator handles caching.
# Code Example #7: Implementing explicit cache invalidation
@app.route('/products/v3/<product_id>', methods=['PUT'])
def update_product(product_id):
"""
When a product is updated, we MUST invalidate its cache to avoid serving stale data.
"""
# Simulate updating the product in the database
update_data = request.get_json()
db_key = f"product:{product_id}"
if db_key not in mock_db:
return jsonify({"error": "Product not found"}), 404
mock_db[db_key].update(update_data)
print(f"--- Updated product {db_key} in DB: {mock_db[db_key]} ---")
# Explicitly delete the cache key associated with the decorated function
# The key name must match the one generated by the decorator
cache_key_to_invalidate = f"get_product_v3:{product_id}"
deleted_count = redis_client.delete(cache_key_to_invalidate)
if deleted_count > 0:
print(f"--- Invalidated cache for key: {cache_key_to_invalidate} ---")
return jsonify({"status": "updated", "invalidated_cache": deleted_count > 0})
if __name__ == '__main__':
app.run(port=5001, debug=True) # Run on a different port to avoid conflict
How to run and check:
- Run server:
flask --app advanced_api run --port 5001 - GET Time 1 (Miss):
curl http://127.0.0.1:5001/products/v3/101-> Takes ~2 seconds. - GET Time 2 (Hit):
curl http://127.0.0.1:5001/products/v3/101-> Extremely fast. - Update price (Invalidation):
curl -X PUT -H "Content-Type: application/json" -d '{"price": 1250}' http://127.0.0.1:5001/products/v3/101-> Server terminal will print--- Invalidated cache... ---. - GET Time 3 (Miss again):
curl http://127.0.0.1:5001/products/v3/101-> Takes ~2 seconds again, because cache was deleted and must retrieve latest data from DB (with price 1250).
Section 4: Best Practices
✅ DO's - Recommended
| Practice | Why | Example |
|---|---|---|
| Always Set TTL | Avoid wasting memory and prevent stale data from persisting forever in cache. This is the most important defense mechanism. | redis_client.set("mykey", "value", ex=3600) (expires after 1 hour) |
| Use Key Naming Convention | Helps manage, debug, and avoid key collisions. A good convention is object-type:id:field, e.g., user:101:profile. | user:101, product:_id_, session:xyz |
| Serialize Data | Redis stores values as byte strings. Always serialize complex objects (like Python dictionaries) to a standard format like JSON before storing. | redis_client.set("user:101", json.dumps(user_dict)) |
| Handle Connection Errors | Network is unstable or Redis server might crash. Your application should have a fallback mechanism (e.g., read directly from DB) instead of crashing completely. | try...except redis.exceptions.ConnectionError: |
| Invalidate Cache on Data Change | When data in DB is updated (UPDATE, DELETE), delete (invalidate) the corresponding key in cache to ensure consistency. | redis_client.delete("product:101") |
❌ DON'Ts - Avoid
| Anti-pattern | Consequence | How to avoid |
|---|---|---|
| Caching Large, Rarely Accessed Data | Wastes valuable Redis RAM. Cache should only be for "hot data" - frequently accessed data. | Analyze and only cache endpoints/data with high traffic. |
| Using Redis as Primary DB without Persistence | Redis is in-memory by default. If server restarts, all data is lost. | If durability is needed, enable AOF (Append Only File) or RDB (Redis Database Backup). But as a cache, data loss is usually acceptable. |
| Using Expensive Commands in Production | Commands like KEYS *, FLUSHALL, FLUSHDB can block Redis server for a long time when there are many keys, affecting all other clients. | Use SCAN instead of KEYS. Minimize use of FLUSHALL in production. |
| No Strategy for Cache Stampede | When a hot key expires, thousands of requests will simultaneously "miss" and attack the DB to retrieve data, causing DB crash. | Use techniques like "locking" (only one request allowed to populate cache) or "probabilistic early expiration". |
🔒 Security Considerations
- Set Password: Always configure
requirepassinredis.confto require client authentication.
# redis.conf
requirepass your_strong_password
- Bind IP Address: Only allow connections from trusted IP addresses (e.g., application server) by configuring
bind 127.0.0.1 your_app_server_ip. - Rename or Disable Dangerous Commands: You can rename commands like
FLUSHALLto a hard-to-guess string to avoid accidental execution or attacks.rename-command FLUSHALL "". - Run Redis with Low Privilege User: Do not run Redis as
root.
⚡ Performance Tips
- Pipelining: Reduce round-trip time (RTT) overhead by sending multiple commands at once and receiving all responses in one go.
# Code Example #8: Using pipeline for batch operations
pipe = redis_client.pipeline()
pipe.set('user:101:visits', 10)
pipe.incr('user_logins')
pipe.get('user:101:visits')
# The commands are sent to Redis all at once
results = pipe.execute()
# results will be [True, 1, '10']
- Use
MGET/MSET: When retrieving or setting multiple keys, useMGET(multi-get) orMSET(multi-set) instead of callingGET/SETin a loop. This significantly reduces round-trips. - Choose Right Data Structure: Use Hashes to store objects instead of JSON strings if you frequently only need to access a few fields of the object. This saves bandwidth and deserialization costs.
Section 5: Case Study
5.1 Scenario
Company/Project: "TicketNow", an online concert ticket platform.
Requirements: The system must handle extremely high traffic in the first few minutes when tickets go on sale for a famous artist's show. The endpoint /events/{event_id} displaying event details (name, date, venue) must have a response time under 50ms, even with 10,000 requests/second.
Constraints: Database is PostgreSQL, overloading every time tickets go on sale. Event details rarely change.
5.2 Problem Analysis
Log analysis shows that 99% of requests to endpoint /events/{event_id} are read operations (GET). Each request executes a complex SQL JOIN query to aggregate information from multiple tables (events, venues, artists). This creates a massive read load on the database, causing locks, increased latency, and eventually timeouts, preventing users from viewing event info to buy tickets.
5.3 Solution Design
We will deploy the Cache-Aside strategy using Redis.
- Architecture: Place a Redis instance as a cache layer between the web server and PostgreSQL database.
- Data Flow:
- When there is a request to
/events/{event_id}, the application creates a cache key, e.g.,event_details:{event_id}. - Application checks this key in Redis.
- Cache Hit: If present, return JSON data from Redis immediately.
- Cache Miss: If not, application queries database, serializes result to JSON, saves to Redis with long TTL (e.g., 1 hour, since event info changes rarely), and then returns to user.
- Invalidation: When admin updates event info (e.g., changes time), system sends a
DELETEcommand to Redis to remove correspondingevent_details:{event_id}key, ensuring next access gets latest data.
5.4 Implementation
# ticketnow_solution.py
from flask import Flask, jsonify
import redis
import json
import time
# --- Setup (similar to previous examples) ---
app = Flask(__name__)
redis_client = redis.Redis(host='localhost', port=6379, db=1, decode_responses=True)
# Mock PostgreSQL Database
def get_event_details_from_db(event_id: int) -> dict:
"""Simulates a slow, complex SQL JOIN query."""
print(f"!!! POSTGRESQL QUERY for event {event_id} !!!")
time.sleep(1.5) # Simulate 1.5s query time
# In reality, this data comes from a complex JOIN
events = {
888: {"event_name": "The Grand Finale Tour", "artist": "The Rockers", "venue": "City Stadium"},
999: {"event_name": "Acoustic Evening", "artist": "Solo Singer", "venue": "Royal Theatre"}
}
return events.get(event_id)
# --- Solution Implementation ---
@app.route('/events/<int:event_id>')
def get_event_details(event_id):
"""
Endpoint to get event details, optimized with Cache-Aside pattern.
"""
cache_key = f"event_details:{event_id}"
# 1. Check cache
try:
cached_event = redis_client.get(cache_key)
if cached_event:
print(f"--- CACHE HIT for {cache_key} ---")
return jsonify(json.loads(cached_event))
except redis.exceptions.RedisError as e:
# If Redis is down, we can log the error and proceed to the database.
# This makes the system more resilient.
print(f"Redis error: {e}. Falling back to database.")
# 2. Cache Miss: Get from DB
print(f"--- CACHE MISS for {cache_key} ---")
event_data = get_event_details_from_db(event_id)
if not event_data:
return jsonify({"error": "Event not found"}), 404
# 3. Store in cache with a 1-hour TTL
try:
# We add the source to the data for demonstration purposes
event_data_with_source = {**event_data, "source": "database"}
redis_client.set(cache_key, json.dumps(event_data_with_source), ex=3600)
except redis.exceptions.RedisError as e:
print(f"Could not write to Redis cache: {e}")
return jsonify(event_data_with_source)
5.5 Results & Lessons Learned
-
Improved Metrics:
-
Latency: Average response time for repeat requests dropped from
~1500msto< 10ms(99% reduction). -
Database Load: Number of read queries on
eventstable decreased by over 95% during peak hours. -
Throughput: System can handle from 500 requests/second up to over 10,000 requests/second without upgrading database.
-
Availability: System became more stable, no longer crashing due to database overload.
-
Lessons Learned:
- Identify the Bottleneck: Caching is most effective when applied in the right place. Identifying slow, read-heavy queries is the first important step.
- TTL is Your Friend: Setting reasonable TTL is the simplest and most effective way to balance performance and data freshness.
- Resilience Matters: Design the system so that cache failure doesn't crash the entire application. Always have a fallback mechanism to the main data source.
- Simple is Effective: For read-heavy workloads, the Cache-Aside strategy is a powerful solution, relatively easy to implement, and yields enormous benefits.