Skip to content

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.

bash
# 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:B
bash
OK
QUEUED
QUEUED
1) OK
2) OK
"800"
"1200"
bash
# Abort transaction
MULTI
SET test_key "value"
DISCARD
bash
OK
QUEUED
OK

7.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.

bash
# 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 retry
bash
OK
"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.

bash
# 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
bash
(integer) 1
bash
# 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
bash
(integer) 1
bash
# 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
bash
(integer) 1

💡 Tip: Lua scripts are cached in Redis and can be invoked using EVALSHA with a SHA1 digest, avoiding repeated transmission of the full script and improving performance.