通过lua实现redis 分布式锁

在 Redis 中使用 Lua 脚本实现分布式锁 是一种高效、原子、安全的方式。由于 Lua 脚本在 Redis 中是单线程、原子执行的,可以避免多个客户端在加锁/解锁时出现竞态条件(Race Condition)。

下面将为你提供一个 完整、生产可用的 Redis 分布式锁 Lua 实现方案,包含:

  • 加锁(lock)
  • 解锁(unlock)
  • 自动续期(可选)
  • 防止误删他人锁
  • 支持可重入(可选)

✅ 一、核心设计原则

  1. 锁的唯一标识 :每个客户端使用唯一的 value(如 UUID)作为锁标识,防止 A 客户端释放 B 客户端的锁。
  2. 原子性操作:加锁和解锁必须通过 Lua 脚本保证原子性。
  3. 自动过期:设置 TTL(如 30 秒),防止死锁。
  4. 可重入支持(可选) :同一个客户端可多次加锁(需记录重入次数)。

本文先实现 基础版(不可重入) ,再给出 可重入增强版


✅ 二、基础版:不可重入分布式锁

🔐 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 版本的客户端实现,也可以告诉我!

相关推荐
optimistic_chen3 小时前
【Redis 系列】常用数据结构---Hash类型
linux·数据结构·redis·分布式·哈希算法
Live&&learn3 小时前
Redis语法入门
数据库·redis
忧郁蓝调263 小时前
Redis不停机数据迁移:基于 redis-shake 的跨实例 / 跨集群同步方案
运维·数据库·redis·阿里云·缓存·云原生·paas
五阿哥永琪3 小时前
Redis的常用数据结构
数据结构·数据库·redis
企鹅侠客5 小时前
第06章—实战应用篇:List命令详解与实战(上)
数据结构·windows·redis·list
奋斗べ青年.5 小时前
【redis】了解redis的主从和集群搭建
数据库·redis·缓存
李昊哲小课6 小时前
Ubuntu 24.04 在线安装 Redis 8.x 完整教程
linux·redis·ubuntu
optimistic_chen9 小时前
【Redis 系列】常用数据结构---String类型
数据结构·数据库·redis·缓存·string
程序员良辰9 小时前
【面试读心术】一场Redis项目面试的AB面
redis·面试·职场和发展