分布式锁解决的是:在多实例部署下,多个服务进程同时操作同一份共享资源,怎么保证同一时间只有一个线程能执行关键逻辑。
比如本地锁:
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 | 当前线程的唯一标识 |
NX | key 不存在才设置成功 |
EX 30 | 30 秒后自动过期 |
成功设置 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,因为它支持可重入锁和看门狗自动续期。