📌 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主从切换导致锁丢失问题。
工作流程:
- 获取当前时间戳(毫秒)
- 依次向N个(通常5个)独立的Redis节点尝试获取锁
- 当成功获取锁的节点数 > N/2(多数派)且总耗时 < 锁有效期时,认为获取锁成功
- 锁的有效期 = 初始有效期 - 获取锁耗时
- 释放锁时,向所有节点发送释放请求
4.2 Redlock优缺点
| 优点 | 缺点 |
|---|---|
| 高可用:少数节点宕机不影响 | 性能低:需要多节点网络通信 |
| 强一致性:多数派决策 | 时钟漂移问题:依赖节点时间同步 |
| 自动失效:自带TTL | 实现复杂:需要维护多个连接 |
生产建议 :绝大多数场景不需要Redlock,单节点Redis + 主从 + 看门狗已足够。Redlock只在金融级强一致性场景考虑。
5. ZooKeeper分布式锁实现原理
5.1 核心机制
ZooKeeper的**临时顺序节点(Ephemeral Sequential Node)**特性天然适合分布式锁。
工作流程:
- 客户端在锁路径下创建临时顺序节点 (如
/lock/seq-000001) - 获取该路径下所有子节点,判断自己是否是序号最小的节点
- 是 → 获得锁;否 → 监听前一个节点的删除事件
- 前一个节点删除后,再次判断自己是否最小(重复步骤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:不监听所有子节点,只监听前一个节点,避免所有客户端同时被唤醒。
💡 面试官想要的满分总结:
"分布式锁是分布式系统中协调共享资源访问的核心机制。
核心场景:
库存扣减(防超卖)
分布式定时任务(防重复执行)
缓存击穿防护(单实例重建)
主流实现:Redis :
SET NX EX原子操作,高性能(10万+ QPS),需处理锁过期、误释放、不可重入等问题,通过看门狗自动续期防业务超时ZooKeeper:临时顺序节点,强一致性,自动释放,性能较低(1万+ QPS),适合强一致性场景
关键技术:防死锁:设置TTL/临时节点
防误释放:释放时校验客户端标识(Lua脚本)
锁续期:看门狗(WatchDog)
多数派算法:Redlock解决主从切换丢锁
选型建议:高并发业务(99%场景)→ Redis主从 + 看门狗
金融级强一致性 → ZooKeeper / Redlock
一句话:分布式锁的核心是互斥、防死锁、高可用;Redis高性能适合大多数场景,ZooKeeper强一致适合核心金融系统。"
觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯