分布式锁

5 阅读3分钟

分布式锁解决的是:在多实例部署下,多个服务进程同时操作同一份共享资源,怎么保证同一时间只有一个线程能执行关键逻辑。

比如本地锁:

synchronized
ReentrantLock

只能锁住当前 JVM
如果项目部署了 3 台后端服务:

服务 A
服务 B
服务 C

每个 JVM 里都有自己的锁,A 加锁不会影响 B、C,所以本地锁不够,需要一个所有实例都能访问的公共锁,比如 Redis。

Redis 分布式锁核心命令

常见写法是:

SET lock:order:1 uniqueValue NX EX 30

含义:

参数含义
lock:order:1锁的 key
uniqueValue当前线程的唯一标识
NXkey 不存在才设置成功
EX 3030 秒后自动过期

成功设置 key,说明抢锁成功。
设置失败,说明别人已经拿到锁。

为什么要加过期时间?

防止业务执行中服务宕机,锁永远不释放,造成死锁。

线程 A 拿到锁
线程 A 宕机
如果锁没有过期时间
其他线程永远拿不到锁

为什么 value 要放唯一标识?

防止误删别人的锁。

错误场景:

线程 A 拿锁,锁 30 秒过期
线程 A 执行太慢,锁过期
线程 B 拿到新锁
线程 A 执行完,直接 DEL lock
结果把线程 B 的锁删了

所以释放锁时不能直接 DEL,要先判断 value 是不是自己的。

释放锁要用 Lua 脚本

因为判断和删除必须是原子操作:

if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

不能这样写:

先 get 判断
再 del 删除

因为两步之间锁可能已经过期并被别人拿走。

分布式锁常见用途

在点评项目这类场景里,分布式锁常用于:

场景作用
一人一单防止同一用户并发下多次下单
缓存重建防止热点 key 失效后大量线程同时查库
定时任务防止多实例重复执行同一个任务
资源修改防止多个实例同时改同一份业务数据

Redis 分布式锁常见问题

问题解决
死锁设置过期时间
误删锁value 使用唯一标识,释放时校验
非原子释放用 Lua 脚本
业务时间超过锁时间设置合理过期时间,或用 Redisson 看门狗续期
Redis 主从切换导致锁丢失高一致要求可考虑 RedLock、Zookeeper 或数据库约束兜底

Redisson 做了什么?

Redisson 是 Redis 分布式锁的成熟实现,常见能力包括:

能力说明
自动续期watchdog 看门狗机制
可重入锁同一线程可以重复加锁
阻塞等待支持等待一定时间获取锁
Lua 保证原子性加锁、释放更安全

比如:

RLock lock = redissonClient.getLock("lock:order:" + userId);
boolean success = lock.tryLock(1, 10, TimeUnit.SECONDS);

if (!success) {
    return Result.fail("不能重复下单");
}

try {
    // 执行业务逻辑
} finally {
    lock.unlock();
}

面试回答版

分布式锁是为了解决多实例环境下的并发互斥问题。本地锁只能锁住单个 JVM,如果服务部署多个实例,就需要用 Redis、Zookeeper 或数据库实现跨进程的锁。Redis 分布式锁一般使用 SET key value NX EX,保证加锁和设置过期时间是原子的。value 要使用线程唯一标识,释放锁时用 Lua 脚本先判断 value 是否一致,再删除 key,避免误删别人的锁。

在项目里,分布式锁可以用于一人一单、缓存重建、定时任务防重复执行等场景。如果自己实现,要注意死锁、锁过期、误删锁和业务执行时间超过锁时间等问题;生产中一般会优先使用 Redisson,因为它支持可重入锁和看门狗自动续期。