📌PDF:大白话说Java面试题 — 03-Mysql篇
第21题:分布式锁的使用场景和原理
📚回答:
- 核心考点:
大厂面试要求深入理解分布式锁的适用场景、实现原理、常见问题与解决方案,并能根据不同场景进行技术选型。面试官常追问:“Redis锁的过期时间怎么设置?”、“Redlock算法是什么?”、“ZooKeeper锁的羊群效应怎么解决?”
1. 分布式锁的核心概念
定义:分布式锁是控制分布式系统中多个进程/线程对共享资源互斥访问的协调机制,保证同一时刻只有一个客户端持有锁。
为什么需要分布式锁?
| 场景 | 单机 | 分布式 |
|---|---|---|
| 锁机制 | JVM锁(synchronized、ReentrantLock) | 跨进程/跨节点的分布式锁 |
| 问题 | 多线程竞争共享资源 | 多实例竞争共享资源(数据库、缓存、文件) |
三大核心特性:
| 特性 | 说明 | 重要性 |
|---|---|---|
| 互斥性 | 同一时刻只有一个客户端能持有锁 | 必须满足 |
| 可重入性 | 同一客户端可重复获取已持有的锁 | 按需 |
| 高可用 | 锁服务本身不能成为单点故障 | 必须满足 |
| 防死锁 | 锁持有者宕机时,锁能自动释放 | 必须满足 |
| 高性能 | 加锁解锁延迟低、吞吐量高 | 必须满足 |
2. 核心使用场景
2.1 库存扣减(防止超卖)
// 电商秒杀场景publicvoidreduceStock(LongproductId,Integerquantity){StringlockKey="lock:product:stock:"+productId;// 获取分布式锁booleanlocked=redisLock.lock(lockKey,3000);if(!locked){thrownewBusinessException("系统繁忙,请稍后重试");}try{// 查询库存intstock=productMapper.selectStock(productId);if(stock<quantity){thrownewBusinessException("库存不足");}// 扣减库存productMapper.updateStock(productId,stock-quantity);}finally{redisLock.unlock(lockKey);}}2.2 分布式定时任务(防止重复执行)
@Scheduled(cron="0 0 2 * * ?")// 凌晨2点执行publicvoiddoDailyReport(){StringlockKey="lock:job:dailyReport";// 尝试获取锁,获取成功才执行if(redisLock.tryLock(lockKey,0,TimeUnit.SECONDS)){try{generateReport();// 生成日报}finally{redisLock.unlock(lockKey);}}else{log.info("另一实例正在执行,跳过");}}2.3 防止缓存击穿(缓存重建互斥)
publicStringgetData(Stringkey){Stringvalue=redis.get(key);if(value!=null){returnvalue;}// 缓存失效,尝试获取锁StringlockKey="lock:cache:rebuild:"+key;if(redisLock.tryLock(lockKey,1000)){try{// 双重检查value=redis.get(key);if(value!=null)returnvalue;// 从数据库加载value=loadFromDB(key);redis.setex(key,3600,value);returnvalue;}finally{redisLock.unlock(lockKey);}}else{// 等待片刻后重试Thread.sleep(100);returngetData(key);}}2.4 其他场景
| 场景 | 示例 | 说明 |
|---|---|---|
| 唯一性校验 | 订单号生成、防重复提交 | 防止分布式下ID重复 |
| 分布式ID生成 | 雪花算法workerID分配 | 保证workerID全局唯一 |
| 配置动态更新 | Apollo/Nacos配置发布 | 同一时刻只一个节点发布 |
3. Redis分布式锁的实现原理
3.1 基础版本(SETNX + EXPIRE)
// 问题:非原子操作,可能SETNX后崩溃导致锁永不释放Booleansuccess=redis.setnx(lockKey,clientId);if(success){redis.expire(lockKey,30);// 如果这步崩溃,锁永不释放}3.2 原子版本(SET NX EX)
// Redis 2.6.12+ 支持原子操作Stringresult=redis.set(lockKey,clientId,"NX","EX",30);// NX:不存在才设置// EX:过期时间30秒3.3 完整实现要点
publicclassRedisDistributedLock{// 获取锁publicbooleanlock(Stringkey,Stringvalue,intexpireSec){Stringresult=jedis.set(key,value,"NX","EX",expireSec);return"OK".equals(result);}// 释放锁(需要Lua脚本保证原子性,防止误删其他线程的锁)publicbooleanunlock(Stringkey,Stringvalue){StringluaScript="if redis.call('get', KEYS[1]) == ARGV[1] then "+" return redis.call('del', KEYS[1]) "+"else "+" return 0 "+"end";Objectresult=jedis.eval(luaScript,Collections.singletonList(key),Collections.singletonList(value));returnLong.valueOf(1).equals(result);}}3.4 Redis锁的常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 锁误释放 | 线程A的锁过期,线程B获取锁,线程A释放时删了B的锁 | 释放时校验value(客户端标识) |
| 锁过期业务未完成 | 业务执行时间超过锁过期时间 | 看门狗(WatchDog)自动续期 |
| 主从切换锁丢失 | Redis主从异步复制,主宕机锁未同步到从 | Redlock算法(多节点) |
| 不可重入 | 同一线程重复获取同一锁失败 | ThreadLocal存储重入次数 |
| 阻塞获取 | 获取锁失败立即返回 | 自旋重试(需退避算法) |
看门狗实现:
// 获取锁后启动定时任务,在锁过期前1/3时间续期ScheduledExecutorServicescheduler=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主从切换导致锁丢失问题。
工作流程:
- 获取当前时间戳(毫秒)
- 依次向N个(通常5个)独立的Redis节点尝试获取锁
- 当成功获取锁的节点数 > N/2(多数派)且总耗时 < 锁有效期时,认为获取锁成功
- 锁的有效期 = 初始有效期 - 获取锁耗时
- 释放锁时,向所有节点发送释放请求
4.2 Redlock优缺点
| 优点 | 缺点 |
|---|---|
| 高可用:少数节点宕机不影响 | 性能低:需要多节点网络通信 |
| 强一致性:多数派决策 | 时钟漂移问题:依赖节点时间同步 |
| 自动失效:自带TTL | 实现复杂:需要维护多个连接 |
生产建议:绝大多数场景不需要Redlock,单节点Redis + 主从 + 看门狗已足够。Redlock只在金融级强一致性场景考虑。
5. ZooKeeper分布式锁实现原理
5.1 核心机制
ZooKeeper的**临时顺序节点(Ephemeral Sequential Node)**特性天然适合分布式锁。
工作流程:
- 客户端在锁路径下创建临时顺序节点(如
/lock/seq-000001) - 获取该路径下所有子节点,判断自己是否是序号最小的节点
- 是 → 获得锁;否 → 监听前一个节点的删除事件
- 前一个节点删除后,再次判断自己是否最小(重复步骤2)
代码示例:
publicclassZooKeeperDistributedLock{publicvoidlock(StringlockPath)throwsException{StringcurrentPath=zk.create(lockPath+"/seq-",null,ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);List<String>children=zk.getChildren(lockPath,false);Collections.sort(children);StringminNode=children.get(0);if(currentPath.endsWith(minNode)){// 获得锁return;}else{// 监听前一个节点StringprevNode=getPrevNode(currentPath,children);CountDownLatchlatch=newCountDownLatch(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存储锁持有信息:
ThreadLocal<Map<String,Integer>>lockCount=...;publicbooleanlock(Stringkey){if(lockCount.get().containsKey(key)){lockCount.get().put(key,count+1);returntrue;}// 尝试获取Redis锁...}Q4:ZooKeeper锁的羊群效应如何解决?
A:不监听所有子节点,只监听前一个节点,避免所有客户端同时被唤醒。
💡面试官想要的满分总结:
"分布式锁是分布式系统中协调共享资源访问的核心机制。
核心场景:
- 库存扣减(防超卖)
- 分布式定时任务(防重复执行)
- 缓存击穿防护(单实例重建)
主流实现:
- Redis:
SET NX EX原子操作,高性能(10万+ QPS),需处理锁过期、误释放、不可重入等问题,通过看门狗自动续期防业务超时- ZooKeeper:临时顺序节点,强一致性,自动释放,性能较低(1万+ QPS),适合强一致性场景
关键技术:
- 防死锁:设置TTL/临时节点
- 防误释放:释放时校验客户端标识(Lua脚本)
- 锁续期:看门狗(WatchDog)
- 多数派算法:Redlock解决主从切换丢锁
选型建议:
- 高并发业务(99%场景)→Redis主从 + 看门狗
- 金融级强一致性 →ZooKeeper / Redlock
一句话:分布式锁的核心是互斥、防死锁、高可用;Redis高性能适合大多数场景,ZooKeeper强一致适合核心金融系统。"
觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯