Batched Atomic Transactions with Redis-py in Python
Executing batched atomic pipeline transactions in Python allows you
to group multiple Redis commands into a single round-trip operation that
executes without interruption from other clients. This article details
how to use the redis-py library's pipeline()
method to buffer commands and execute them atomically using Redis
MULTI and EXEC primitives, along with best
practices for error handling and optimistic locking.
The Pipeline Method
In redis-py, pipelines provide both command batching
(reducing network latency) and atomicity. When the
transaction=True argument is passed—which is the default
behavior—redis-py wraps all buffered commands within a
Redis MULTI and EXEC block. This ensures that
all queued commands are executed sequentially and exclusively,
preventing other clients from executing commands in between.
Basic Atomic Pipeline Example
To execute a basic atomic transaction, instantiate a pipeline from
your Redis client, queue your commands, and call
.execute():
import redis
# Initialize the Redis connection
client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
# Create an atomic pipeline (transaction=True by default)
pipe = client.pipeline(transaction=True)
# Queue commands in the pipeline buffer
pipe.set('user:100:name', 'Alice')
pipe.set('user:100:status', 'active')
pipe.incr('stats:total_users')
# Execute all commands atomically
results = pipe.execute()
# 'results' contains the response of each command in order: [True, True, 1]
print(results)Using Context Managers
The cleanest and most Pythonic approach is using a context manager
with the with statement. This ensures proper resource
cleanup and resets the pipeline state if an exception occurs:
import redis
client = redis.Redis(host='localhost', port=6379, db=0)
with client.pipeline(transaction=True) as pipe:
pipe.hset('account:A', 'balance', 400)
pipe.hset('account:B', 'balance', 600)
results = pipe.execute()Optimistic Locking with WATCH
If your atomic transaction depends on reading keys before writing to
them, wrap the transaction in a WATCH block to handle race
conditions (Check-and-Set / CAS pattern). If any watched key is modified
by another client before .execute() is called, a
redis.exceptions.WatchError is raised:
import redis
client = redis.Redis(host='localhost', port=6379, db=0)
def transfer_funds(sender, receiver, amount):
with client.pipeline() as pipe:
while True:
try:
# Watch the sender key for changes
pipe.watch(sender)
# Check current balance
balance = int(pipe.get(sender) or 0)
if balance < amount:
pipe.unwatch()
raise ValueError("Insufficient funds")
# Start transaction block
pipe.multi()
pipe.decrby(sender, amount)
pipe.incrby(receiver, amount)
# Execute transaction
return pipe.execute()
except redis.WatchError:
# Another client modified the key; retry the transaction
continueUsing client.pipeline(transaction=True) ensures that
your commands are delivered in a single payload and executed as an
isolated, atomic unit on the Redis server.