When multiple clients concurrently modify shared data in Redis—such as decrementing product inventory during flash sales—race conditions can lead to incorrect results. Without proper synchronization, two clients might read the same initial value, both decrement it locally, and write back identical results, causing lost updates.
While locking mechanisms like distributed locks can enforce mutual exclusion, they introduce latency and complexity. Redis offers a more efficient alternative: atomic operations. These operations execute as indivisible units, eliminating the need for explicit locks while preserving data consistency under concurrent access.
Understanding the Read-Modify-Write Problem
The core issue arises from the classic Read-Modify-Write (RMW) pattern:
current = GET(product_id)
current = current - 1
SET(product_id, current)
If two clients execute this sequence simultaneously, both may read the same inventory count (e.g., 10), decrement it to 9, and write back 9—resulting in only one decrement being applied, not two. This is a classic race condition.
The problem stems from the fact that these three steps are not atomic. Even though Redis processes commands sequentially, each individual command is treated separately unless grouped into a single atomic unit.
Redis Atomic Operations: Two Approaches
1. Native Atomic Commands
For simple arithmetic modifications, Redis provides built-in atomic commands:
INCR key— Increment integer value by 1DECR key— Decrement integer value by 1INCRBY key increment— Add arbitrary integerDECRBY key decrement— Subtract arbitrary integer
Using DECR product_stock replaces the entire RMW sequence. Since Redis executes each command in a single-threaded manner, these operations are inherently atomic. Multiple clients calling DECR concurrently will each see a unique, sequentially decremented value without interference.
2. Lua Scripting for Complex Atomic Logic
When logic involves conditional checks, multiple steps, or non-arithmetic operations, native commands are insufficient. Redis supports atomic execution of Lua scripts via the EVAL command.
Consider a rate-limiting scenario: allow no more than 20 requests per minute per client IP. The logic requires:
- Increment request counter
- Check if this is the first request (counter == 1)
- If so, set a 60-second expiration
- Reject if counter exceeds 20
A naive implementation using separate commands fails:
current = GET(client_ip)
IF current > 20 THEN ERROR
ELSE
new_val = INCR(client_ip)
IF new_val == 1 THEN EXPIRE(client_ip, 60)
END
Between GET and INCR, another client may have incremented the counter, causing the expiration to be skipped for the second client—even if it was the first to trigger the limit.
Wrapping all logic into a single Lua script ensures atomicity:
-- rate_limit.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, 60)
end
if current > limit then
return 0 -- reject
else
return 1 -- allow
end
Execute with:
redis-cli --eval rate_limit.lua client_ip , 20
Redis guarantees that the entire script runs without interruption. Even if 100 clients invoke it simultaneously, each script executes one at a time, maintaining consistency.
Performance Considerations
While Lua scripts provide flexibility, they also block the Redis main thread during execution. Long-runing or computationally heavy scripts degrade overall throughput. Therefore:
- Prefer native atomic commands (e.g.,
INCR,LPUSH) when posisble - Use Lua only when multiple steps must be grouped atomically
- Avoid I/O, loops, or heavy computation within scripts
- Keep scripts short and focused
By leveraging Redis’s atomic primitives, applications achieve high concurrency without the overhead of distributed locking, making them ideal for real-time systems requiring low-latency, consistent state updates.