你是不是也有这样的经历?
简历上写着“精通 Java,精通 Redis,熟悉高并发场景”,结果一面下来,分布式锁怎么实现?Redisson是怎么加锁的?看门狗机制了解吗?锁丢失你知道怎么解决吗?全程“啊能能”,频频磕巴。
本文不整虚的,带你从0到1,一步步真正搞懂分布式锁的原理与落地实践,面试高频,架构核心,不能不会。
本质很简单,锁的核心是互斥性:
一个线程拿到了锁,其他线程就不能进入,必须等这个锁被释放后才能继续。
分布式锁本质也是为了实现跨进程、跨节点的互斥执行。
使用 Redis 实现分布式锁的基本思路是:
SETNX lock_key thread_id
只要返回 1,说明加锁成功。否则说明其他线程已经获得锁,当前线程就拿不到。
不能光会加锁,还要能安全地解锁。那是不是直接执行 DEL lock_key
就可以了?
⚠️ 错!这样会有误删他人锁的风险。
必须先判断锁是否是自己加的,再删除,需要两步操作,而 Redis 是单线程的,这两步操作如果分开执行就不是原子操作,可能导致严重问题。
使用 Lua 脚本,将判断+删除写成一个 Redis 命令,保证原子性。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
当然,有人会问:
“为什么 SETNX 命令就不用放进 Lua 脚本里?”
答:因为 SETNX
是单条 Redis 命令,Redis 天生是单线程,天然具备原子性。
线程挂了没来得及释放锁,其他线程就永远拿不到锁了?
解决方案:给锁加一个过期时间!
SET lock_key value NX PX 30000
加锁时指定过期时间(如 30 秒),即便线程挂了,锁也会自动释放。
业务还没执行完,锁就过期了。那其他线程就能拿到锁,造成并发执行了!
搞一个看门狗线程,定时续期:
设置看门狗线程为“守护线程”!
守护线程的生命周期依赖主线程,业务线程一挂,看门狗自然终止。要守护的人都挂了,看门狗还有什么意义?
你写递归不?写方法调用不?
如果一个线程在方法 A 中获得了锁,又在方法 B 中再次尝试加锁,会不会失败?
如果你设计的锁不是可重入的,这会导致线程自己锁自己,造成死锁!
对标 Java 的 synchronized
和 ReentrantLock
:
Redisson 使用 Hash 结构来实现可重入:
UUID + threadId
,保证线程唯一;HSET lock_key uuid_threadId 1
为什么是 UUID + threadId?因为在分布式系统中 threadId 可能重复,需要拼接 UUID 保证唯一性。
假设线程没抢到锁,是立即返回失败,还是等等再试?
实际开发中我们常常用阻塞锁:等待一会儿,再次尝试获取锁。
ReentrantLock
:没抢到锁的线程会自旋(循环尝试获取),直到拿到锁。
Redisson 使用了 Redis 的“发布/订阅”机制:
你在主节点执行了 SETNX
,加锁成功,但此时主节点还没来得及同步到从节点就宕机了。
这时候 Redis 选了一个没有锁数据的从节点作为新主,其他线程再次加锁又成功了!
加锁明明成功,却又能被别人加锁 —— 锁丢了!
Redisson 支持“联锁”(MultiLock):
部署多个 Redis 主节点(多主或主从),每次加锁都要对所有主节点加锁成功,才算真正加锁成功。
即便其中某个 Redis 宕机,其它节点仍保有锁数据,仍能保障互斥性。
Redis 官方提出了红锁算法,改进联锁的缺点:
只需要半数以上节点加锁成功就认为锁加成功。
为什么是“半数以上”?这涉及分布式共识协议中的多数派原则:
Redlock 理论上看起来很美,但实际落地仍存在问题:
所以红锁虽然可靠,但在工业界用得非常少。
能力点 | 内容 |
---|---|
锁基本原理 | 互斥、释放、原子性 |
基础命令 | SETNX、EX/PX、Lua 脚本 |
看门狗 | 守护线程、续期机制 |
可重入 | Hash结构+计数器、UUID+ThreadId |
阻塞锁 | 发布/订阅、自旋、超时控制 |
主从问题 | 锁丢失、联锁、多主 |
红锁机制 | 多主、多数派、时间窗口 |
局限性 | 时钟漂移、GC暂停、部署复杂 |