在 Redis 中使用 Lua 脚本实现分布式锁 是一种高效、原子、安全的方式。由于 Lua 脚本在 Redis 中是单线程、原子执行的,可以避免多个客户端在加锁/解锁时出现竞态条件(Race Condition)。
下面将为你提供一个 完整、生产可用的 Redis 分布式锁 Lua 实现方案,包含:
- 加锁(lock)
- 解锁(unlock)
- 自动续期(可选)
- 防止误删他人锁
- 支持可重入(可选)
✅ 一、核心设计原则
- 锁的唯一标识 :每个客户端使用唯一的
value(如 UUID)作为锁标识,防止 A 客户端释放 B 客户端的锁。 - 原子性操作:加锁和解锁必须通过 Lua 脚本保证原子性。
- 自动过期:设置 TTL(如 30 秒),防止死锁。
- 可重入支持(可选) :同一个客户端可多次加锁(需记录重入次数)。
本文先实现 基础版(不可重入) ,再给出 可重入增强版。
✅ 二、基础版:不可重入分布式锁
🔐 1. 加锁脚本(lock.lua)
lua
-- KEYS[1] = 锁的 key(如 "my_lock")
-- ARGV[1] = 锁的 value(唯一标识,如 UUID)
-- ARGV[2] = 过期时间(毫秒,如 "30000")
local key = KEYS[1]
local value = ARGV[1]
local ttl = tonumber(ARGV[2])
-- 尝试加锁:只有当 key 不存在时才设置成功
if redis.call("SET", key, value, "NX", "PX", ttl) then
return 1 -- 加锁成功
else
return 0 -- 加锁失败(已被占用)
end
✅
SET key value NX PX ttl是 Redis 2.6.12+ 的原子命令:
NX:仅当 key 不存在时设置PX:设置过期时间(毫秒)
🔓 2. 解锁脚本(unlock.lua)
sql
-- KEYS[1] = 锁的 key
-- ARGV[1] = 锁的 value(用于验证是否是自己的锁)
local key = KEYS[1]
local value = ARGV[1]
-- 获取当前锁的 value
local current_value = redis.call("GET", key)
-- 如果存在且等于自己的 value,则删除
if current值 == value then
redis.call("DEL", key)
return 1 -- 解锁成功
else
return 0 -- 不是自己的锁,拒绝删除
end
⚠️ 注意:不能直接
DEL,否则可能误删别人的锁!
✅ 三、Python 示例(使用 redis-py)
python
import redis
import uuid
import time
# 初始化 Redis 连接
r = redis.StrictRedis(host='localhost', port=6379, decode_responses=True)
# 加载 Lua 脚本(推荐使用 script_load + evalsha 提高性能)
lock_script = r.register_script("""
if redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) then
return 1
else
return 0
end
""")
unlock_script = r.register_script("""
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
""")
def acquire_lock(lock_name, acquire_timeout=10, lock_timeout=30):
"""获取分布式锁"""
identifier = str(uuid.uuid4())
lock_timeout_ms = int(lock_timeout * 1000)
end_time = time.time() + acquire_timeout
while time.time() < end_time:
# 执行 Lua 脚本尝试加锁
if lock_script(keys=[lock_name], args=[identifier, lock_timeout_ms]):
return identifier # 返回锁标识
time.sleep(0.01) # 短暂等待后重试
return False
def release_lock(lock_name, identifier):
"""释放分布式锁"""
unlock_script(keys=[lock_name], args=[identifier])
# ===== 使用示例 =====
lock_key = "order:123:lock"
# 尝试获取锁
lock_id = acquire_lock(lock_key, acquire_timeout=5, lock_timeout=30)
if lock_id:
try:
print("✅ 获取锁成功,执行业务逻辑...")
time.sleep(5) # 模拟业务处理
finally:
# 释放锁
release_lock(lock_key, lock_id)
print("🔓 锁已释放")
else:
print("❌ 获取锁超时")
✅ 四、增强版:支持可重入锁(Reentrant Lock)
适用于同一线程/协程需要多次进入临界区的场景。
设计思路:
-
使用 Hash 结构 存储锁:
- field = 客户端 ID(UUID)
- value = 重入次数(counter)
-
设置整体 TTL
🔐 可重入加锁脚本(reentrant_lock.lua)
lua
-- KEYS[1] = 锁 key
-- ARGV[1] = 客户端 ID
-- ARGV[2] = 锁过期时间(毫秒)
local key = KEYS[1]
local client_id = ARGV[1]
local ttl = tonumber(ARGV[2])
-- 检查是否已持有锁
local current_client = redis.call("HGET", key, client_id)
if current_client then
-- 已持有:重入次数 +1,刷新 TTL
redis.call("HINCRBY", key, client_id, 1)
redis.call("PEXPIRE", key, ttl)
return 1
else
-- 未持有:尝试获取锁
if redis.call("HLEN", key) == 0 then
-- 锁空闲,创建
redis.call("HMSET", key, client_id, 1)
redis.call("PEXPIRE", key, ttl)
return 1
else
-- 锁被他人持有
return 0
end
end
🔓 可重入解锁脚本(reentrant_unlock.lua)
lua
-- KEYS[1] = 锁 key
-- ARGV[1] = 客户端 ID
local key = KEYS[1]
local client_id = ARGV[1]
-- 检查是否持有锁
local counter = redis.call("HGET", key, client_id)
if not counter then
return 0 -- 未持有锁
end
counter = tonumber(counter)
if counter > 1 then
-- 重入次数 >1,减1
redis.call("HINCRBY", key, client_id, -1)
return 1
else
-- 最后一次,删除整个 key
redis.call("DEL", key)
return 1
end
💡 使用方式与基础版类似,只需替换 Lua 脚本。
✅ 五、注意事项与最佳实践
| 问题 | 解决方案 |
|---|---|
| 锁过期但业务未完成 | 使用"看门狗"(Watchdog)线程自动续期(如 Redisson 的机制) |
| 主从切换导致锁丢失 | 使用 Redlock 算法(多 Redis 实例),但争议较大;更推荐强一致存储(如 etcd) |
| Lua 脚本调试困难 | 在开发环境打印日志(redis.log(redis.LOG_WARNING, "msg")) |
| 性能优化 | 使用 SCRIPT LOAD + EVALSHA 避免每次传输脚本 |
📌 重要提醒 :
Redis 分布式锁 不保证绝对安全 (尤其在主从异步复制场景)。
对一致性要求极高的场景(如金融),建议使用 etcd / ZooKeeper。
✅ 六、总结
| 功能 | 基础版 | 可重入版 |
|---|---|---|
| 原子加锁 | ✅ | ✅ |
| 防止误删 | ✅ | ✅ |
| 自动过期 | ✅ | ✅ |
| 同客户端多次加锁 | ❌ | ✅ |
| 复杂度 | 低 | 中 |
推荐:
- 普通场景 → 用 基础版
- 需要递归调用/嵌套锁 → 用 可重入版
通过以上 Lua 脚本 + 客户端封装,你可以在 Redis 中实现一个高效、安全、可靠 的分布式锁系统。如果需要 Go / Java / Node.js 版本的客户端实现,也可以告诉我!