分布式锁
为什么需要分布式锁?
在分布式系统中,多个服务实例同时访问共享资源,需要协调访问顺序。
// 单机锁无法解决分布式问题
synchronized (this) {
// 只能保证单个 JVM 内的互斥
// 多个服务实例仍然会并发执行
}
// 分布式锁保证跨服务互斥
distributedLock.lock();
try {
// 只有一个服务实例能执行
int stock = getStock();
if (stock > 0) {
deductStock();
}
} finally {
distributedLock.unlock();
}实现方案
| 方案 | 优点 | 缺点 |
|---|---|---|
| Redis | 性能高、实现简单 | 需要处理锁续期、主从切换 |
| Zookeeper | 可靠性高、支持排队 | 性能较低、实现复杂 |
| 数据库 | 简单、无需额外组件 | 性能差、有单点问题 |
Redis 分布式锁
基础实现
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
public boolean lock(String key, String value, long expireTime) {
// SET NX EX 原子操作
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, value, expireTime, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
}
public void unlock(String key, String value) {
// Lua 脚本保证原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
value
);
}
}Redisson 实现(推荐)
@Autowired
private RedissonClient redissonClient;
public void deductStock() {
RLock lock = redissonClient.getLock("stock_lock");
try {
// 尝试获取锁,最多等待 10 秒,锁自动释放时间 30 秒
boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (acquired) {
// 业务逻辑
int stock = getStock();
if (stock > 0) {
deductStock();
}
}
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}Redisson 特性
| 特性 | 说明 |
|---|---|
| 可重入锁 | 同一线程可多次获取锁 |
| 看门狗 | 自动续期,防止业务未完成锁过期 |
| 公平锁 | 按请求顺序获取锁 |
| 读写锁 | 支持读写分离 |
| 红锁 | 多 Redis 节点,防止主从切换丢锁 |
看门狗机制
业务执行时间 > 锁过期时间?
┌─────────────────────────────────────────────┐
│ 锁过期时间:30s │
│ 业务执行:50s │
│ │
│ 看门狗每 10s 检查一次: │
│ - 如果锁还被持有,续期到 30s │
│ - 如果业务完成,正常释放 │
└─────────────────────────────────────────────┘Zookeeper 分布式锁
原理
1. 创建临时顺序节点 /lock/lock-000001
2. 判断自己是否是最小节点
├── 是 → 获取锁成功
└── 否 → 监听前一个节点
3. 前一个节点删除时,收到通知
4. 再次判断自己是否是最小节点Curator 实现
@Autowired
private CuratorFramework curatorClient;
public void withLock() throws Exception {
InterProcessMutex lock = new InterProcessMutex(curatorClient, "/lock/stock");
try {
// 获取锁
lock.acquire();
// 业务逻辑
deductStock();
} finally {
// 释放锁
lock.release();
}
}Zookeeper 锁特性
| 特性 | 说明 |
|---|---|
| 临时节点 | 客户端断开连接自动删除 |
| 顺序节点 | 按创建顺序排队 |
| Watch 机制 | 节点变化时通知 |
| 可重入 | 同一客户端可多次获取 |
Redis vs Zookeeper
| 对比 | Redis | Zookeeper |
|---|---|---|
| 性能 | 高 | 中 |
| 可靠性 | 中(主从切换可能丢锁) | 高 |
| 实现 | 简单 | 复杂 |
| 续期 | 需要看门狗 | 不需要 |
| 排队 | 不支持 | 支持(顺序节点) |
常见问题
1. 锁超时问题
// 问题:业务执行时间超过锁过期时间
lock("key", 10); // 10秒过期
executeBusiness(); // 执行15秒
// 锁已自动释放,其他线程获取锁
// 业务执行完后释放的是别人的锁!
// 解决:Redisson 看门狗自动续期
RLock lock = redissonClient.getLock("key");
lock.lock(); // 默认30秒,看门狗自动续期2. 误删锁问题
// 问题:删除了别人的锁
String value = uuid();
lock("key", value);
executeBusiness();
// 此时锁已过期被其他线程获取
unlock("key"); // 删除了别人的锁!
// 解决:Lua 脚本原子操作
// 只删除自己持有的锁
if (redis.get("key") == value) {
redis.del("key");
}3. 主从切换丢锁
主节点写入锁成功 → 还未同步到从节点 → 主节点宕机
→ 从节点升级为主节点 → 锁丢失
解决:RedLock(红锁)
→ 多个 Redis 节点同时获取锁
→ 超过半数成功才算成功面试高频问题
Q1: Redis 分布式锁如何实现?
- 使用 SET NX EX 原子命令加锁
- 使用 Lua 脚本保证解锁原子性
- 使用 Redisson 看门狗自动续期
Q2: Redis 和 Zookeeper 分布式锁的区别?
| 对比 | Redis | Zookeeper |
|---|---|---|
| 性能 | 高 | 中 |
| 可靠性 | 中 | 高 |
| 实现 | 简单 | 复杂 |
Q3: 如何解决锁超时问题?
- 设置合理的过期时间
- 使用 Redisson 看门狗自动续期
- 业务代码添加监控
Q4: Redis 主从切换丢锁怎么办?
使用 RedLock:向多个独立的 Redis 节点申请锁,超过半数成功才算成功。
总结
分布式锁核心要点:
1. Redis:SET NX EX + Lua 脚本
2. Zookeeper:临时顺序节点 + Watch
3. 推荐:Redisson(看门狗、可重入)
4. 注意:锁超时、误删锁、主从切换