Master Redis transactions - MULTI/EXEC, WATCH for optimistic locking, Lua scripting, and atomic operation patterns
Implements atomic Redis operations using MULTI/EXEC, WATCH for optimistic locking, and Lua scripts. Claude uses this when you need transactional guarantees for complex operations like fund transfers, distributed locks, or conditional updates that must execute atomically.
/plugin marketplace add pluginagentmarketplace/custom-plugin-redis/plugin install pluginagentmarketplace-developer-roadmap-interactive@pluginagentmarketplace/custom-plugin-redisThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/config.yamlassets/distributed-lock.luareferences/GUIDE.mdreferences/TRANSACTION_PATTERNS.mdscripts/helper.pyscripts/lua-loader.shProduction-grade transaction handling for Redis. Master MULTI/EXEC for atomic command batches, WATCH for optimistic locking, and Lua scripting for complex atomic operations.
| Feature | MULTI/EXEC | WATCH+MULTI | Lua Script |
|---|---|---|---|
| Atomicity | ✅ | ✅ | ✅ |
| Isolation | Partial | Partial | ✅ Full |
| Conditional logic | ❌ | ❌ | ✅ |
| Read-modify-write | ❌ | ✅ | ✅ |
| Cluster support | ⚠️ Same slot | ⚠️ Same slot | ⚠️ Same slot |
| Debugging | Easy | Medium | Harder |
MULTI # Start transaction
SET user:123:balance 100
INCR user:123:login_count
LPUSH user:123:actions "login"
EXEC # Execute atomically
# Returns: [OK, 1, 1]
MULTI
SET key1 "value1"
SET key2 "value2"
DISCARD # Abort, nothing executed
WATCH user:123:balance # Start watching
balance = GET user:123:balance # Read current value
# Client-side logic
if balance >= 50:
MULTI
DECRBY user:123:balance 50
INCRBY merchant:456:balance 50
EXEC # nil if balance changed
else:
UNWATCH # Release watch
WATCH key1 key2 key3 # Watch multiple keys
# ... read values ...
MULTI
# ... modify values ...
EXEC # nil if ANY watched key changed
UNWATCH # Always call after failed EXEC
MAX_RETRIES = 5
for attempt in range(MAX_RETRIES):
try:
pipe = r.pipeline(True) # True = use MULTI
pipe.watch('user:123:balance')
balance = int(pipe.get('user:123:balance') or 0)
if balance < 50:
pipe.unwatch()
raise InsufficientFunds()
pipe.multi()
pipe.decrby('user:123:balance', 50)
pipe.incrby('merchant:456:balance', 50)
pipe.execute()
break # Success
except redis.WatchError:
continue # Retry
finally:
pipe.reset()
-- Atomic increment with cap
local current = tonumber(redis.call('GET', KEYS[1]) or 0)
local max = tonumber(ARGV[1])
if current < max then
return redis.call('INCR', KEYS[1])
else
return current
end
EVAL "return redis.call('GET', KEYS[1])" 1 mykey
# Load and execute by hash (faster for repeated calls)
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# Returns: "a42059b356c875f0717db19a51f6aaa9161e77a2"
EVALSHA a42059b356c875f0717db19a51f6aaa9161e77a2 1 mykey
SCRIPT EXISTS <sha1> [sha1 ...] # Check if loaded
SCRIPT FLUSH [ASYNC|SYNC] # Clear cache
SCRIPT KILL # Kill running (if no writes)
SCRIPT DEBUG YES|SYNC|NO # Enable debugging
-- transfer.lua
local from = KEYS[1]
local to = KEYS[2]
local amount = tonumber(ARGV[1])
local from_balance = tonumber(redis.call('GET', from) or 0)
if from_balance >= amount then
redis.call('DECRBY', from, amount)
redis.call('INCRBY', to, amount)
return 1 -- Success
else
return 0 -- Insufficient funds
end
-- acquire_lock.lua
local lock_key = KEYS[1]
local holder = ARGV[1]
local ttl = tonumber(ARGV[2])
if redis.call('SET', lock_key, holder, 'NX', 'EX', ttl) then
return 1
else
return 0
end
-- release_lock.lua
local lock_key = KEYS[1]
local holder = ARGV[1]
if redis.call('GET', lock_key) == holder then
return redis.call('DEL', lock_key)
else
return 0
end
-- rate_limit.lua
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = tonumber(redis.call('GET', key) or 0)
if current < limit then
redis.call('INCR', key)
if current == 0 then
redis.call('EXPIRE', key, window)
end
return 1 -- Allowed
else
return 0 -- Denied
end
-- ✅ GOOD: Always use KEYS and ARGV
local value = redis.call('GET', KEYS[1])
redis.call('SET', KEYS[2], ARGV[1])
-- ❌ BAD: Dynamic key names break cluster
local key = 'user:' .. ARGV[1] -- Don't!
redis.call('GET', key)
-- ✅ GOOD: Early return for efficiency
if not redis.call('EXISTS', KEYS[1]) then
return nil
end
-- ✅ GOOD: Use pcall for error handling
local ok, result = pcall(redis.call, 'GET', KEYS[1])
if not ok then
return redis.error_reply("Operation failed")
end
distributed-lock.lua - Production-ready lock implementationconfig.yaml - Transaction configurationlua-loader.sh - Load and manage Lua scriptshelper.py - Python transaction utilitiesTRANSACTION_PATTERNS.md - Best practices guideGUIDE.md - Complete referenceEXECABORT Transaction discarded because of previous errors
Cause: Syntax error in queued command
Diagnosis:
MULTI
SET key # Missing value!
EXEC
# (error) EXECABORT...
Fix: Validate commands before queueing
Cause: High contention on watched key
Solutions:
BUSY Redis is busy running a script
Cause: Lua script running too long (>lua-time-limit)
Recovery:
# Kill script (only if no writes performed)
SCRIPT KILL
# If script performed writes
# Must restart Redis or wait
Prevention:
# redis.conf
lua-time-limit 5000 # 5 seconds
NOSCRIPT No matching script
Cause: Script not loaded (cache cleared or different node)
Fix:
# Always handle NOSCRIPT
try:
result = r.evalsha(sha, 1, key)
except redis.NoScriptError:
result = r.eval(script, 1, key)
□ All keys in same hash slot (cluster)?
□ WATCH before read operations?
□ UNWATCH after failed EXEC?
□ Lua script uses KEYS/ARGV properly?
□ Script execution time reasonable?
□ Handling NOSCRIPT error?
□ Retry logic for WatchError?
| Operation | Time | Notes |
|---|---|---|
| MULTI | O(1) | Start transaction |
| Queue command | O(1) | Add to queue |
| EXEC | O(N) | Execute N commands |
| WATCH | O(1) | Per key |
| EVAL | O(N) | Script complexity |
| EVALSHA | O(N) | Same as EVAL (no load overhead) |
| Code | Name | Description | Recovery |
|---|---|---|---|
| T001 | EXECABORT | Syntax error in queue | Fix command syntax |
| T002 | WATCH_FAILED | Key modified | Retry transaction |
| T003 | BUSY | Script timeout | SCRIPT KILL or wait |
| T004 | NOSCRIPT | Script not loaded | Re-EVAL or LOAD |
| T005 | CROSSSLOT | Keys in different slots | Use hash tags |
# test_redis_transactions.py
import redis
import pytest
@pytest.fixture
def r():
return redis.Redis(decode_responses=True)
def test_multi_exec(r):
pipe = r.pipeline()
pipe.set("tx:key1", "value1")
pipe.set("tx:key2", "value2")
results = pipe.execute()
assert results == [True, True]
r.delete("tx:key1", "tx:key2")
def test_watch_success(r):
r.set("tx:balance", "100")
pipe = r.pipeline(True)
pipe.watch("tx:balance")
balance = int(pipe.get("tx:balance"))
pipe.multi()
pipe.set("tx:balance", balance - 50)
results = pipe.execute()
assert results == [True]
assert r.get("tx:balance") == "50"
r.delete("tx:balance")
def test_lua_script(r):
script = "return redis.call('GET', KEYS[1])"
r.set("tx:lua", "test_value")
result = r.eval(script, 1, "tx:lua")
assert result == "test_value"
r.delete("tx:lua")
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.