分布式知识梳理(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。

相关推荐
书香门第1 小时前
系统设计练习 - 分布式黑名单服务(design a distributed denylist service)
分布式·系统架构·系统设计
江畔柳前堤8 小时前
大语言模型分布式训练:从并行策略到万卡工程的系统梳理
人工智能·分布式·深度学习·算法·目标检测·机器学习·语言模型
霸道流氓气质13 小时前
RabbitMQ 延迟队列与死信队列:原理、实现与实战
分布式·rabbitmq·ruby
爱吃面的猫14 小时前
大数据Kafka3.x之——Kafka3.3.0安装与使用(详细)
大数据·分布式·zookeeper
小张同学a.1 天前
zabbix企业级监控平台4——分布式监控与grafana数据可视化
linux·运维·数据库·分布式·信息可视化·zabbix·grafana
番茄炒鸡蛋加糖1 天前
分布式 CAP、BASE 理论 & 中间件 CAP 取舍
分布式·中间件
海上小飞龙2 天前
Redis 分布式锁原理:从 SET NX EX 到 Redisson 看门狗
数据库·redis·分布式
明达智控技术2 天前
国产PLC、远程IO模块在食品包装机的应用
分布式·自动化
YOU OU2 天前
RabbitMQ应用问题
分布式·rabbitmq