分布式知识梳理(4)

分布式知识梳理(4)

作者:没有四次元口袋的蓝胖

日期:2026-08-09

标签:Java, 分布式锁, Redis, Zookeeper, 数据库


分布式锁是分布式系统的核心组件。核心掌握:数据库锁、Redis 锁、Zookeeper 锁的实现与对比。


一、为什么需要分布式锁?

1.1 场景

多台服务器访问共享资源,需要互斥访问。

复制代码
服务器 A → 扣减库存(同时访问)→ 数据库
服务器 B → 扣减库存(同时访问)→ 数据库

需要保证:同一时刻只有一个服务器能扣减库存

1.2 分布式锁要求

特性 说明
互斥性 同一时刻只有一个线程能获取锁
可重入 同一线程可以多次获取同一把锁
超时释放 防止死锁(锁超时自动释放)
高可用 锁服务不能单点故障
高性能 获取和释放锁要快

二、基于数据库实现

2.1 唯一索引锁

原理:利用数据库唯一索引的冲突机制。

java 复制代码
// 创建锁表
CREATE TABLE distributed_lock (
    lock_name VARCHAR(100) PRIMARY KEY,
    expire_time DATETIME
);

// 获取锁
public boolean tryLock(String lockName) {
    try {
        // 插入锁记录
        lockMapper.insert(lockName, new Date());
        return true;
    } catch (DuplicateKeyException e) {
        // 唯一索引冲突,获取锁失败
        return false;
    }
}

// 释放锁
public void unlock(String lockName) {
    lockMapper.delete(lockName);
}

问题:

  • 锁没有过期时间,可能死锁
  • 非阻塞,获取失败直接返回

2.2 悲观锁(for update)

原理:利用数据库的排他锁。

java 复制代码
// 获取锁(开启事务)
@Transactional
public boolean tryLock(String lockName) {
    // SELECT ... FOR UPDATE 会阻塞等待
    Lock lock = lockMapper.selectForUpdate(lockName);
    if (lock == null) {
        // 创建锁记录
        lockMapper.insert(lockName, new Date());
    }
    return true;
}

// 释放锁(事务结束自动释放)
// 不需要显式 unlock,事务提交/回滚后锁自动释放

问题:

  • 依赖数据库,性能差
  • 单点故障风险

2.3 优缺点

优点 缺点
实现简单 性能差(依赖数据库)
无需额外组件 单点故障
支持可重入 可能死锁(需要设置过期时间)

三、基于 Redis 实现

3.1 基础版本

原理:利用 Redis 的 SETNX(SET if Not eXists)。

java 复制代码
// 获取锁
public boolean tryLock(String lockName, long expireTime) {
    // SETNX + 过期时间(原子操作)
    Long result = jedis.setnx(lockName, String.valueOf(System.currentTimeMillis() + expireTime));
    return result == 1;
}

// 释放锁
public void unlock(String lockName) {
    jedis.del(lockName);
}

问题:

  • 释放锁时可能误删别人的锁
  • 非原子操作(判断+删除)

3.2 改进版本(解决误删问题)

java 复制代码
// 获取锁(带唯一标识)
public boolean tryLock(String lockName, String requestId, long expireTime) {
    // value 存储 requestId
    Long result = jedis.setnx(lockName, requestId);
    if (result == 1) {
        jedis.pexpire(lockName, expireTime);
        return true;
    }
    return false;
}

// 释放锁(Lua 脚本,原子操作)
public void unlock(String lockName, String requestId) {
    String luaScript = 
        "if redis.call('get', KEYS[1]) == ARGV[1] then " +
        "    return redis.call('del', KEYS[1]) " +
        "else " +
        "    return 0 " +
        "end";
    jedis.eval(luaScript, Arrays.asList(lockName), Arrays.asList(requestId));
}

3.3 完善版本(解决超时问题)

java 复制代码
// 看门狗机制(自动续期)
public class RedisLock {
    private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
    
    public boolean tryLock(String lockName, String requestId, long expireTime) {
        Long result = jedis.setnx(lockName, requestId);
        if (result == 1) {
            jedis.pexpire(lockName, expireTime);
            
            // 启动看门狗,自动续期
            startWatchDog(lockName, requestId, expireTime);
            return true;
        }
        return false;
    }
    
    private void startWatchDog(String lockName, String requestId, long expireTime) {
        executor.scheduleAtFixedRate(() -> {
            // 检查锁是否还属于自己
            if (requestId.equals(jedis.get(lockName))) {
                jedis.pexpire(lockName, expireTime);
            }
        }, expireTime / 3, expireTime / 3, TimeUnit.MILLISECONDS);
    }
    
    public void unlock(String lockName, String requestId) {
        // 停止看门狗
        executor.shutdown();
        
        // Lua 脚本释放锁
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        jedis.eval(luaScript, Arrays.asList(lockName), Arrays.asList(requestId));
    }
}

3.4 Redisson 实现(推荐)

java 复制代码
// 引入 Redisson
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);

// 获取锁
RLock lock = redisson.getLock("myLock");
try {
    // 尝试获取锁,等待 10 秒,锁定 30 秒后自动释放
    if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
        // 业务逻辑
    }
} finally {
    lock.unlock();
}

3.5 优缺点

优点 缺点
性能好(内存操作) Redis 宕机可能丢失锁
支持集群(Redis Cluster) 主从切换可能丢锁
实现相对简单 需要处理超时、续期

四、基于 Zookeeper 实现

4.1 原理

利用 ZK 的临时顺序节点 和Watcher 机制。

复制代码
1. 在 /locks 下创建临时顺序节点
2. 判断自己是否是序号最小的节点
   - 是:获取锁成功
   - 否:监听前一个节点
3. 前一个节点删除(释放锁),触发 Watcher
4. 再次判断自己是否是最小节点

4.2 代码示例

java 复制代码
public class ZkLock {
    private ZooKeeper zk;
    private String lockPath = "/locks/";
    private String currentNode;
    
    public boolean tryLock() throws Exception {
        // 创建临时顺序节点
        currentNode = zk.create(lockPath + "lock-", null, 
            ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
        
        // 获取所有子节点
        List<String> children = zk.getChildren(lockPath, false);
        Collections.sort(children);
        
        // 判断自己是否是最小节点
        int index = children.indexOf(currentNode.substring(lockPath.length()));
        if (index == 0) {
            return true; // 获取锁成功
        }
        
        // 监听前一个节点
        CountDownLatch latch = new CountDownLatch(1);
        String prevNode = children.get(index - 1);
        zk.exists(lockPath + prevNode, event -> {
            if (event.getType() == Watcher.Event.EventType.NodeDeleted) {
                latch.countDown();
            }
        });
        
        // 等待前一个节点删除
        latch.await();
        return true;
    }
    
    public void unlock() throws Exception {
        zk.delete(currentNode, -1);
    }
}

4.3 Curator 实现(推荐)

java 复制代码
// 引入 Curator
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy);
client.start();

// 创建分布式锁
InterProcessMutex lock = new InterProcessMutex(client, "/locks/myLock");

try {
    // 获取锁,等待 10 秒
    if (lock.acquire(10, TimeUnit.SECONDS)) {
        try {
            // 业务逻辑
        } finally {
            lock.release(); // 释放锁
        }
    }
} catch (Exception e) {
    e.printStackTrace();
}

4.4 优缺点

优点 缺点
强一致性(ZAB 协议) 性能较差(磁盘操作)
自动释放(临时节点) 实现复杂
支持可重入 依赖 Zookeeper
高可用(集群) 集群性能下降

五、三种实现对比

维度 数据库锁 Redis 锁 Zookeeper 锁
性能 差(磁盘) 好(内存) 中(磁盘)
一致性 强 最终(主从异步) 强(ZAB)
可靠性 中(单点) 中(主从切换丢锁) 高(集群)
实现复杂度 低 中 高
超时释放 需要实现 需要实现 自动(临时节点)
可重入 需要实现 需要实现 支持
适用场景 低并发 高并发 强一致要求

5.1 选型建议

场景 推荐方案
低并发、简单场景 数据库锁
高并发、性能优先 Redis 锁(Redisson)
强一致、可靠性优先 Zookeeper 锁(Curator)

六、Redis 锁的问题与解决

6.1 主从切换丢锁

问题:

复制代码
1. 客户端 A 在 Master 上获取锁
2. Master 宕机,锁数据未同步到 Slave
3. Slave 晋升为 Master
4. 客户端 B 在新 Master 上获取锁成功
→ 两个客户端同时持有锁

解决:RedLock 算法

java 复制代码
// 部署 N 个独立的 Redis 节点
List<RedissonClient> nodes = Arrays.asList(
    Redisson.create(config1),
    Redisson.create(config2),
    Redisson.create(config3)
);

// 获取锁:在 N/2+1 个节点上成功才算获取锁
RLock lock = new RedissonRedLock(nodes.stream()
    .map(r -> r.getLock("myLock"))
    .toArray(RLock[]::new));

lock.lock();

6.2 RedLock 争议

  • 支持:Martin Fowler 认为解决了主从切换丢锁问题
  • 反对:Antirez(Redis 作者)提出 RedLock,但存在时钟跳跃等问题
  • 结论:对一致性要求极高,用 Zookeeper;一般场景用 Redis 单节点 + 看门狗即可

🗺️ 思维导图速览

复制代码
分布式锁
├── 数据库锁
│   ├── 唯一索引锁(SETNX 思想)
│   └── 悲观锁(FOR UPDATE)
├── Redis 锁
│   ├── 基础版(SETNX + 过期时间)
│   ├── 改进版(Lua 脚本,防误删)
│   ├── 完善版(看门狗,自动续期)
│   └── Redisson(推荐)
├── Zookeeper 锁
│   ├── 临时顺序节点
│   ├── Watcher 机制
│   └── Curator(推荐)
└── 对比
    ├── 性能:Redis > ZK > 数据库
    ├── 一致性:ZK > 数据库 > Redis
    └── 可靠性:ZK > Redis > 数据库

📝 写在最后

学习建议

  1. 数据库锁要知道两种实现:唯一索引、FOR UPDATE
  2. Redis 锁要知道演进过程:SETNX → Lua 脚本 → 看门狗 → Redisson
  3. Zookeeper 锁要知道原理:临时顺序节点 + Watcher
  4. 选型要知道差异:性能优先用 Redis,一致性优先用 ZK

面试回答模板

Q:说说分布式锁的实现?

三种主流实现:数据库锁,利用唯一索引或 FOR UPDATE,实现简单但性能差;Redis 锁,用 SETNX + 过期时间,性能高但主从切换可能丢锁,推荐用 Redisson 的看门狗机制;Zookeeper 锁,用临时顺序节点 + Watcher,强一致但性能较差,推荐用 Curator。

Q:Redis 锁有什么问题?怎么解决?

问题:SETNX 和设置过期时间非原子操作,可能死锁;释放锁时可能误删别人的锁;主从切换可能丢锁。解决:用 Lua 脚本保证原子性;value 存储 requestId,释放时判断;用 RedLock 算法在多个节点上加锁,但存在争议。一般场景用 Redisson 的看门狗机制即可。

Q:Redis 锁和 Zookeeper 锁怎么选?

性能优先选 Redis(内存操作,QPS 高),一致性优先选 Zookeeper(ZAB 协议,强一致)。Redis 锁在主从切换时可能丢锁,ZK 锁不会丢但性能较差。电商秒杀等高并发场景用 Redis,金融转账等强一致场景用 ZK。

相关推荐
Pniubi8 小时前
分布式锁分类讲解
分布式
nvd1110 小时前
深入剖析:GCP Pub/Sub 与 Apache Kafka 对消息处理延迟与消费者卡死的处理机制异同
分布式·kafka·apache
我不是阵雨11 小时前
分布式事务难题与实战解法全解析
分布式·微服务·架构
song50111 小时前
React Native for OpenHarmony 实战:三方库 react-native-parallax-scroll-view 的鸿蒙化适配指南
分布式·react native·react.js·transformer·harmonyos
做个文艺程序员13 小时前
MQ第03篇:RabbitMQ进阶实战:Topic路由+死信队列+延迟消息,SaaS订单超时取消完整方案
分布式·消息队列·rabbitmq·死信队列·延迟队列
风花一世月13 小时前
[特殊字符] 我把一套分布式消防方案做成了能跑的 3D 沙盘(三架无人机共用一根水带,在线直接玩)
分布式·3d·ai·无人机
我不是阵雨16 小时前
如何拿捏分布式四大模式?
分布式
吴声子夜歌18 小时前
Docker入门与实战——实战案例(分布式处理与大数据平台)
大数据·分布式·docker
bksczm18 小时前
Redis基础篇之初识分布式
数据库·redis·分布式
此时不提桶,更待何时1 天前
06-18-A-RabbitMQ消息特性与消费机制详解
分布式·rabbitmq