Caching data with Redis can lead to stale data if the right operations aren't used carefully β οΈπ°οΈ.
What is Redis? π
For those unfamiliar, Redis is an open-source, in-memory data structure store used as a database, cache, and message broker. It supports data structures such as strings, hashes, lists, sets, and more.
Its most appealing feature is speed β Redis keeps data in memory, making it incredibly fast for reads and writes. It's a popular choice for caching data that needs to be accessed quickly and frequently.
Redis Gotchas β οΈπ
One Redis operation, HSET, doesnβt allow you to set an expiry directly. You have to invoke a second operation, EXPIRE, to set a TTL (time-to-live) for the cache.
Gotcha: Calling HSET then EXPIRE without a transaction could lead to stale data if, for example, the EXPIRE command fails while the HSET succeeds.
HSET depends on the EXPIRE command to clear the cache and sync it with new data by reinvoking the backend.
β‘οΈ If EXPIRE fails, the cached data will stay forever and become stale. If your app depends on fresh data, this could lead to showing outdated info. π¬
import redis
redis_client = redis.Redis(host='localhost', port=6379, db=0)
cities = ['Mississauga', 'Oakville']
for city in cities:
key = f"weather_{city}"
redis_client.hset(key, mapping={
'temp': 22,
'wind': 5,
'condition': 'Cloudy',
'city': city
})
redis_client.expire(key, 10)
Β
Why It Matters π‘
When caching, expiration is just as important as storing data. Stale data in a cache can be worse than no cache at all β it tricks your app into thinking itβs working with fresh info. π«π°οΈ
Β
The Solution: Use Transactions or Lua Scripts ππ
To ensure that both commands succeed or fail together, we should wrap them in a pipeline within a transaction (MULTI/EXEC) or use a Lua script.
A Redis pipeline allows multiple commands to be sent to the server as a single batch, reducing network overhead and minimizing the impact of network
glitches. For example, without pipelining, if two dependent write operations are executed separately, one might reach the Redis server while the other
could be lost due to a network issue. With pipelining, both operations are transmitted together in one network packet, making it more likely
that either both are received or both are dropped β offering a basic form of network-level consistency.
However, it's important to note that pipelining does not guarantee atomicity β commands are still executed one after the other by Redis, and
failures in individual commands do not roll back others.
To ensure true atomic execution, Redis transactions should be used. Using a transaction ensures that all operations within the block are executed
sequentially and without interference from other clients. The MULTI command queues the operations, and EXEC executes them all at once. This provides a
degree of isolation: no other client's commands are interleaved during the execution of the transaction.
transaction = redis_client.pipeline(transaction=True)
for city in cities:
key = f"weather_{city}"
transaction.hset(key, mapping={
'temp': 22,
'wind': 5,
'condition': 'Cloudy',
'city': city
})
transaction.expire(key, 10)
transaction.execute()
Heads up: Redis transactions are not fully atomic. If one command fails, others won't be rolled back β partial changes can still persist.
True Atomicity with Lua Scripts π§ββοΈβ¨
Redis guarantees atomicity when using Lua scripts. All operations run as one indivisible block β no other commands can run until the script finishes.
If the script completes, all changes are applied in order.
If thereβs a runtime error, the entire script aborts and no changes are made.
But be careful! Lua scripts block the entire Redis server while running. If a script takes too long, it can slow down or freeze Redis for other clients.
So keep Lua scripts super fast .
cities = ['Mississauga', 'Oakville']
lua_script = """
for i = 1, #KEYS do
local base = (i - 1) * 5
local key = KEYS[i]
redis.call('HSET', key, 'temp', ARGV[base + 1], 'wind', ARGV[base + 2], 'condition', ARGV[base + 3], 'city', ARGV[base + 4])
redis.call('EXPIRE', key, tonumber(ARGV[base + 5]))
end
return #KEYS
"""
keys = [f"weather_{city}" for city in cities]
# For each city, pack values: temp=22, wind=5, condition='Cloudy', city=city, expire=10
args = []
for city in cities:
args.extend(['22', '5', 'Cloudy', city, '10'])
redis_client.eval(lua_script, len(keys), *keys, *args)
Final Thoughts π
Always consider atomicity when working with Redis for critical caching.
Stale cache is worse than no cache β it can cause hidden bugs and confusing behavior.
Redis is fast and simple, but it can fail silently β wrap multi-step operations when consistency matters!
That's it for today!
Hope this article helped you avoid some Redis gotchas.
Happy coding and creating! β¨π
π You can also read this article on my website.
Top comments (0)