Redis 分布式锁

面试官问:"Redis 怎么做分布式锁?"

小陈说:"用 setnx。"

面试官追问:"setnx 过期了怎么办?锁自动释放了,但业务还没执行完?"

小陈说:"...延长过期时间?"

面试官继续追问:"怎么保证锁的安全释放(只能释放自己的锁)?"

小陈答不上来。

分布式锁是 Redis 面试中的高频题,也是最容易出错的实现。这道题能说清楚 setnx + Lua + 续期机制的候选人,对分布式锁的复杂性有实战理解。

一、分布式锁基础 🔴

1.1 为什么需要分布式锁

# 场景:库存扣减
# 两台服务器同时处理订单,都需要扣减库存
# 如果不加锁:

# Server1: GET stock = 100
# Server2: GET stock = 100
# Server1: stock = stock - 1 = 99
# Server2: stock = stock - 1 = 99  # 错误!应该是 98

1.2 分布式锁的要求

要求说明
互斥性同一时刻只有一个客户端能持有锁
不会死锁即使持有锁的客户端崩溃,锁也要能自动释放
独占性只能持有锁的客户端才能释放锁
可重入性(可选)同一个客户端可以多次获取锁

二、setnx + EXPIRE 的坑 🔴

2.1 非原子性问题

# ❌ 错误实现:setnx 和 expire 分开
acquired = redis.setnx(lock_key, lock_value)
if acquired:
    redis.expire(lock_key, 10)  # 如果这行失败了呢?
    # Redis 挂了、重启、或者命令执行失败
    # lock_key 永不过期,其他客户端永远获取不到锁

2.2 正确实现:set nx ex

# ✅ 正确实现:一条命令原子设置
acquired = redis.set(lock_key, lock_value, nx=True, ex=10)
# set nx ex 保证了:
# 1. setnx 的原子性(只有不存在时才设置)
# 2. 过期时间的原子设置(不会锁永不过期)

2.3 ❌ 错误示范

候选人原话:"用 setnx 加锁,expire 设置过期时间。"

问题诊断:setnx 和 expire 是两条命令,不原子。如果在两条命令之间 Redis 挂了,锁永不过期。

【面试官心理】 这道题我会从"原子性"追问。能说出"set + nx + ex 是一条命令保证原子性"的候选人,说明他理解了分布式锁的正确实现。

三、安全释放锁 🟡

3.1 问题

# ❌ 错误释放:直接删除
redis.delete(lock_key)
# 问题:
# T1: 客户端 A 获取锁,业务执行超时
# T2: 锁自动过期,客户端 B 获取锁
# T3: 客户端 A 业务执行完毕,删除锁
# T4: 删除的是客户端 B 的锁!

3.2 解决:Lua 脚本原子释放

-- 安全释放锁的 Lua 脚本
-- 只有锁的值等于当前客户端的值时才删除

if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
# Python 调用
def release_lock(lock_key, lock_value):
    lua = """
    if redis.call('get', KEYS[1]) == ARGV[1] then
        return redis.call('del', KEYS[1])
    else
        return 0
    end
    """
    redis.eval(lua, 1, lock_key, lock_value)

四、锁续期(Watchdog)🟡

4.1 过期时间的问题

# 问题:
# 锁设置了 10 秒过期
# 但业务执行需要 30 秒
# T1: 获取锁
# T11: 锁过期(其他客户端可以获取锁)
# T1~T30: 业务仍在执行(但锁已经没了!)

4.2 Watchdog 续期机制

# Redisson 框架的 watchdog 实现:
# 1. 锁默认过期时间 30 秒
# 2. 后台线程每 10 秒检查:如果仍持有锁,续期 30 秒
# 3. 业务执行完毕,手动释放锁后停止续期

# 伪代码:
def acquire_lock_with_watchdog(lock_key):
    lock_value = uuid.uuid4()
    redis.set(lock_key, lock_value, nx=True, ex=30)

    if acquired:
        # 启动 watchdog 线程
        watchdog_thread = start_daemon_thread(
            interval=10,
            func=lambda: redis.expire(lock_key, 30)  # 续期
        )
        return (lock_value, watchdog_thread)
    return None

五、完整实现 🟡

5.1 简单实现

import uuid
import time

class RedisLock:
    def __init__(self, redis_client, lock_key, timeout=10):
        self.redis = redis_client
        self.lock_key = lock_key
        self.timeout = timeout
        self.lock_value = str(uuid.uuid4())

    def acquire(self):
        # 原子设置:nx + ex
        return self.redis.set(
            self.lock_key,
            self.lock_value,
            nx=True,
            ex=self.timeout
        )

    def release(self):
        # Lua 脚本安全释放
        lua = """
        if redis.call('get', KEYS[1]) == ARGV[1] then
            return redis.call('del', KEYS[1])
        else
            return 0
        end
        """
        self.redis.eval(lua, 1, self.lock_key, self.lock_value)

    def __enter__(self):
        if not self.acquire():
            raise RuntimeError('Failed to acquire lock')
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.release()
        return False

# 使用
with RedisLock(redis, 'order:lock:10086') as lock:
    # 业务逻辑
    order = decrement_stock(10086)

六、生产注意事项 🟢

6.1 Redis 分布式锁的局限性

-- Redis 分布式锁不是完美的:
-- 1. 如果 Redis 是单机:Redis 挂了,锁就没了
-- 2. 如果 Redis 是主从:主从切换时,锁可能丢失
--    (主库加锁成功,但锁还没同步到从库就挂了)

-- 解决方案:
-- 1. RedLock:多台 Redis 实例加锁(需要奇数台,至少 3 台)
-- 2. Zookeeper:强一致性的分布式锁
-- 3. 数据库悲观锁:性能差但可靠

【面试官心理】 分布式锁是 Redis 面试中的高级话题。能说清楚 setnx + Lua + 续期机制、以及 Redis 分布式锁局限性的候选人,说明他对分布式系统的复杂性问题有深入理解。


级别考察重点期望回答
P5基础实现setnx 加锁、过期时间
P6安全释放Lua 脚本、锁值匹配
P7深度问题watchdog 续期、RedLock 争议