知识模块
☕ Java 知识模块
九、分布式系统
分布式锁

分布式锁

为什么需要分布式锁?

在分布式系统中,多个服务实例同时访问共享资源,需要协调访问顺序。

// 单机锁无法解决分布式问题
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

对比RedisZookeeper
性能
可靠性中(主从切换可能丢锁)
实现简单复杂
续期需要看门狗不需要
排队不支持支持(顺序节点)

常见问题

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 分布式锁如何实现?

  1. 使用 SET NX EX 原子命令加锁
  2. 使用 Lua 脚本保证解锁原子性
  3. 使用 Redisson 看门狗自动续期

Q2: Redis 和 Zookeeper 分布式锁的区别?

对比RedisZookeeper
性能
可靠性
实现简单复杂

Q3: 如何解决锁超时问题?

  1. 设置合理的过期时间
  2. 使用 Redisson 看门狗自动续期
  3. 业务代码添加监控

Q4: Redis 主从切换丢锁怎么办?

使用 RedLock:向多个独立的 Redis 节点申请锁,超过半数成功才算成功。

总结

分布式锁核心要点:
1. Redis:SET NX EX + Lua 脚本
2. Zookeeper:临时顺序节点 + Watch
3. 推荐:Redisson(看门狗、可重入)
4. 注意:锁超时、误删锁、主从切换