【大白话说Java面试题 第91题】【Mysql篇】第21题:分布式锁的使用场景和原理?

📌 PDF :大白话说Java面试题 --- 03-Mysql篇

第21题:分布式锁的使用场景和原理

📚 回答:

  • 核心考点
    大厂面试要求深入理解分布式锁的适用场景实现原理常见问题与解决方案,并能根据不同场景进行技术选型。面试官常追问:"Redis锁的过期时间怎么设置?"、"Redlock算法是什么?"、"ZooKeeper锁的羊群效应怎么解决?"

1. 分布式锁的核心概念

定义:分布式锁是控制分布式系统中多个进程/线程对共享资源互斥访问的协调机制,保证同一时刻只有一个客户端持有锁。

为什么需要分布式锁?

场景 单机 分布式
锁机制 JVM锁(synchronized、ReentrantLock) 跨进程/跨节点的分布式锁
问题 多线程竞争共享资源 多实例竞争共享资源(数据库、缓存、文件)

三大核心特性

特性 说明 重要性
互斥性 同一时刻只有一个客户端能持有锁 必须满足
可重入性 同一客户端可重复获取已持有的锁 按需
高可用 锁服务本身不能成为单点故障 必须满足
防死锁 锁持有者宕机时,锁能自动释放 必须满足
高性能 加锁解锁延迟低、吞吐量高 必须满足
2. 核心使用场景

2.1 库存扣减(防止超卖)

java 复制代码
// 电商秒杀场景
public void reduceStock(Long productId, Integer quantity) {
    String lockKey = "lock:product:stock:" + productId;
    // 获取分布式锁
    boolean locked = redisLock.lock(lockKey, 3000);
    if (!locked) {
        throw new BusinessException("系统繁忙,请稍后重试");
    }
    try {
        // 查询库存
        int stock = productMapper.selectStock(productId);
        if (stock < quantity) {
            throw new BusinessException("库存不足");
        }
        // 扣减库存
        productMapper.updateStock(productId, stock - quantity);
    } finally {
        redisLock.unlock(lockKey);
    }
}

2.2 分布式定时任务(防止重复执行)

java 复制代码
@Scheduled(cron = "0 0 2 * * ?")  // 凌晨2点执行
public void doDailyReport() {
    String lockKey = "lock:job:dailyReport";
    // 尝试获取锁,获取成功才执行
    if (redisLock.tryLock(lockKey, 0, TimeUnit.SECONDS)) {
        try {
            generateReport();  // 生成日报
        } finally {
            redisLock.unlock(lockKey);
        }
    } else {
        log.info("另一实例正在执行,跳过");
    }
}

2.3 防止缓存击穿(缓存重建互斥)

java 复制代码
public String getData(String key) {
    String value = redis.get(key);
    if (value != null) {
        return value;
    }
    // 缓存失效,尝试获取锁
    String lockKey = "lock:cache:rebuild:" + key;
    if (redisLock.tryLock(lockKey, 1000)) {
        try {
            // 双重检查
            value = redis.get(key);
            if (value != null) return value;
            
            // 从数据库加载
            value = loadFromDB(key);
            redis.setex(key, 3600, value);
            return value;
        } finally {
            redisLock.unlock(lockKey);
        }
    } else {
        // 等待片刻后重试
        Thread.sleep(100);
        return getData(key);
    }
}

2.4 其他场景

场景 示例 说明
唯一性校验 订单号生成、防重复提交 防止分布式下ID重复
分布式ID生成 雪花算法workerID分配 保证workerID全局唯一
配置动态更新 Apollo/Nacos配置发布 同一时刻只一个节点发布
3. Redis分布式锁的实现原理

3.1 基础版本(SETNX + EXPIRE)

java 复制代码
// 问题:非原子操作,可能SETNX后崩溃导致锁永不释放
Boolean success = redis.setnx(lockKey, clientId);
if (success) {
    redis.expire(lockKey, 30);  // 如果这步崩溃,锁永不释放
}

3.2 原子版本(SET NX EX)

java 复制代码
// Redis 2.6.12+ 支持原子操作
String result = redis.set(lockKey, clientId, "NX", "EX", 30);
// NX:不存在才设置
// EX:过期时间30秒

3.3 完整实现要点

java 复制代码
public class RedisDistributedLock {
    
    // 获取锁
    public boolean lock(String key, String value, int expireSec) {
        String result = jedis.set(key, value, "NX", "EX", expireSec);
        return "OK".equals(result);
    }
    
    // 释放锁(需要Lua脚本保证原子性,防止误删其他线程的锁)
    public boolean unlock(String key, String value) {
        String luaScript = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "    return redis.call('del', KEYS[1]) " +
            "else " +
            "    return 0 " +
            "end";
        Object result = jedis.eval(luaScript, Collections.singletonList(key), 
                                   Collections.singletonList(value));
        return Long.valueOf(1).equals(result);
    }
}

3.4 Redis锁的常见问题与解决方案

问题 原因 解决方案
锁误释放 线程A的锁过期,线程B获取锁,线程A释放时删了B的锁 释放时校验value(客户端标识)
锁过期业务未完成 业务执行时间超过锁过期时间 看门狗(WatchDog)自动续期
主从切换锁丢失 Redis主从异步复制,主宕机锁未同步到从 Redlock算法(多节点)
不可重入 同一线程重复获取同一锁失败 ThreadLocal存储重入次数
阻塞获取 获取锁失败立即返回 自旋重试(需退避算法)

看门狗实现

java 复制代码
// 获取锁后启动定时任务,在锁过期前1/3时间续期
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
    if (isLockHeld()) {
        jedis.expire(lockKey, expireSec);  // 续期
    }
}, expireSec / 3, expireSec / 3, TimeUnit.SECONDS);
4. Redlock算法(Redis作者推荐)

4.1 核心原理

Redlock是Redis作者提出的分布式锁算法,解决Redis主从切换导致锁丢失问题。

工作流程

  1. 获取当前时间戳(毫秒)
  2. 依次向N个(通常5个)独立的Redis节点尝试获取锁
  3. 当成功获取锁的节点数 > N/2(多数派)且总耗时 < 锁有效期时,认为获取锁成功
  4. 锁的有效期 = 初始有效期 - 获取锁耗时
  5. 释放锁时,向所有节点发送释放请求

4.2 Redlock优缺点

优点 缺点
高可用:少数节点宕机不影响 性能低:需要多节点网络通信
强一致性:多数派决策 时钟漂移问题:依赖节点时间同步
自动失效:自带TTL 实现复杂:需要维护多个连接

生产建议 :绝大多数场景不需要Redlock,单节点Redis + 主从 + 看门狗已足够。Redlock只在金融级强一致性场景考虑。

5. ZooKeeper分布式锁实现原理

5.1 核心机制

ZooKeeper的**临时顺序节点(Ephemeral Sequential Node)**特性天然适合分布式锁。

工作流程

  1. 客户端在锁路径下创建临时顺序节点 (如/lock/seq-000001
  2. 获取该路径下所有子节点,判断自己是否是序号最小的节点
  3. 是 → 获得锁;否 → 监听前一个节点的删除事件
  4. 前一个节点删除后,再次判断自己是否最小(重复步骤2)

代码示例

java 复制代码
public class ZooKeeperDistributedLock {
    
    public void lock(String lockPath) throws Exception {
        String currentPath = zk.create(lockPath + "/seq-", 
                                        null, 
                                        ZooDefs.Ids.OPEN_ACL_UNSAFE,
                                        CreateMode.EPHEMERAL_SEQUENTIAL);
        
        List<String> children = zk.getChildren(lockPath, false);
        Collections.sort(children);
        
        String minNode = children.get(0);
        if (currentPath.endsWith(minNode)) {
            // 获得锁
            return;
        } else {
            // 监听前一个节点
            String prevNode = getPrevNode(currentPath, children);
            CountDownLatch latch = new CountDownLatch(1);
            zk.exists(lockPath + "/" + prevNode, event -> latch.countDown());
            latch.await();  // 阻塞等待
            // 重新尝试获取锁(递归)
            lock(lockPath);
        }
    }
}

5.2 Redis vs ZooKeeper对比

对比维度 Redis ZooKeeper
性能 极高(内存,单机10万+ QPS) 一般(1万+ QPS)
一致性 最终一致(主从异步) 强一致(ZAB协议)
可靠性 主从切换可能丢锁 (多数派写入)
实现复杂度
依赖 Redis集群 ZooKeeper集群
自动续期 需自己实现看门狗 原生支持(临时节点)
适用场景 高并发、高性能场景 强一致性场景
6. 分布式锁选型对比
方案 性能 一致性 可用性 复杂度 典型场景
Redis单机 极高 开发/测试
Redis主从 极高 中(可能丢锁) 高并发业务(99%场景)
Redlock 金融级强一致性
ZooKeeper 极高 强一致性要求(配置中心)
数据库唯一索引 简单场景、无额外依赖

选型决策树

复制代码
是否需要极高性能(10万+ QPS)?
├── 是 → Redis主从 + 看门狗
└── 否 → 是否需要强一致性?
    ├── 是 → ZooKeeper / Redlock
    └── 否 → Redis主从
7. 常见问题与解决方案

Q1:锁过期时间怎么设置?

A :设置为业务执行时间的2-3倍,且配合看门狗自动续期。经验值:秒杀场景100-300ms,缓存重建1-3秒。

Q2:获取锁失败怎么处理?

A:根据业务决定:

  • 快速失败:立即返回"系统繁忙"(秒杀场景)
  • 阻塞等待:自旋重试,使用退避算法(指数退避)
  • 排队等待:使用消息队列

Q3:Redis分布式锁怎么实现可重入?

A:使用ThreadLocal存储锁持有信息:

java 复制代码
ThreadLocal<Map<String, Integer>> lockCount = ...;
public boolean lock(String key) {
    if (lockCount.get().containsKey(key)) {
        lockCount.get().put(key, count + 1);
        return true;
    }
    // 尝试获取Redis锁...
}

Q4:ZooKeeper锁的羊群效应如何解决?

A:不监听所有子节点,只监听前一个节点,避免所有客户端同时被唤醒。


💡 面试官想要的满分总结

"分布式锁是分布式系统中协调共享资源访问的核心机制。
核心场景

  • 库存扣减(防超卖)

  • 分布式定时任务(防重复执行)

  • 缓存击穿防护(单实例重建)
    主流实现

  • RedisSET NX EX原子操作,高性能(10万+ QPS),需处理锁过期、误释放、不可重入等问题,通过看门狗自动续期防业务超时

  • ZooKeeper:临时顺序节点,强一致性,自动释放,性能较低(1万+ QPS),适合强一致性场景
    关键技术

  • 防死锁:设置TTL/临时节点

  • 防误释放:释放时校验客户端标识(Lua脚本)

  • 锁续期:看门狗(WatchDog)

  • 多数派算法:Redlock解决主从切换丢锁
    选型建议

  • 高并发业务(99%场景)→ Redis主从 + 看门狗

  • 金融级强一致性 → ZooKeeper / Redlock
    一句话:分布式锁的核心是互斥、防死锁、高可用;Redis高性能适合大多数场景,ZooKeeper强一致适合核心金融系统。"


觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯

相关推荐
流星白龙1 小时前
【MySQL高阶】18.缓冲池页管理
数据库·windows·mysql
JAVA社区1 小时前
Java高级全套教程(十三)—— 分布式锁超详细实战详解(原理+三种方案企业级落地)
java·开发语言·分布式·spring cloud·面试·java-zookeeper
Mahir081 小时前
MyBatis 延迟加载深度解密:从使用方式到底层动态代理原理全解
java·后端·面试·mybatis
超梦dasgg1 小时前
Java 生产环境 Maven 实战指南
java·开发语言·maven
XZ-0700011 小时前
MySQL-前缀索引
数据库·mysql
贺国亚1 小时前
Agent 工程实践 · 生产落地 Playbook
java·人工智能·aigc
专注VB编程开发20年1 小时前
淘宝上架销售技巧:Excel管理系统开发 / VBA / ERP / OA办公管理
java·数据库·excel
Leo1871 小时前
分布式事务
java·分布式·分布式事务
Leon-Ning Liu1 小时前
【真实经验分享】Grid管理仓库 (GIMR/MGMTDB) 迁移重建实战指南
数据库