7. Transactions & Lua Scripts
7.1 MULTI / EXEC Transactions
🚀 Analogy: MULTI is like "adding items to a shopping cart and checking out all at once." All commands are queued up, and EXEC executes them all at once.
# Start transaction
MULTI
# The following commands enter the queue
SET account:A 800
SET account:B 1200
# Execute transaction
EXEC
# View results
GET account:A
GET account:BOK
QUEUED
QUEUED
1) OK
2) OK
"800"
"1200"# Abort transaction
MULTI
SET test_key "value"
DISCARDOK
QUEUED
OK7.2 WATCH — Optimistic Locking
👀 WATCH monitors one or more keys — if those keys are modified by another client before EXEC, the transaction is automatically aborted. This implements CAS (Compare-And-Swap) behavior.
# Scenario: transfer 100 from A to B
# Client 1: watch balance
WATCH account:A
# Read current balance
GET account:A
# Assume it returns "1000"
# Start transaction
MULTI
DECRBY account:A 100
INCRBY account:B 100
EXEC
# If another client modified account:A between WATCH and EXEC
# then EXEC returns nil (transaction aborted), application layer should retryOK
"1000"
OK
QUEUED
QUEUED
1) (integer) 900
2) (integer) 1100⚠️ Note: Redis transactions do NOT support rollback. If one command in the transaction fails, other commands still execute. The design philosophy is "keep it simple and efficient."
7.3 Lua Scripts — True Atomic Operations
⚡ Analogy: If MULTI/EXEC is "batch execution," then Lua scripts are "writing a small program" that runs on the server side — natively atomic and more powerful than transactions.
# EVAL syntax: EVAL script numkeys key [key...] arg [arg...]
# Scenario: deduct stock (atomic operation, prevent overselling)
EVAL "
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock and stock > 0 then
redis.call('DECR', KEYS[1])
return 1
else
return 0
end
" 1 stock:product:1001(integer) 1# Scenario: distributed lock (atomic lock + set expiration)
EVAL "
if redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) then
return 1
else
return 0
end
" 1 lock:order:1001 "owner_001" 10(integer) 1# Scenario: rate limiter (sliding window)
EVAL "
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now .. '-' .. math.random(100000))
redis.call('EXPIRE', key, window)
return 1
else
return 0
end
" 1 rate:api:user1001 100 60 1704067200(integer) 1💡 Tip: Lua scripts are cached in Redis and can be invoked using
EVALSHAwith a SHA1 digest, avoiding repeated transmission of the full script and improving performance.