每日Java面试场景题知识点之-如何设计分布式锁

每日Java面试场景题知识点之-如何设计分布式锁

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

在单机环境中,我们可以通过 synchronizedReentrantLock 轻松实现线程间的互斥访问。但在分布式系统中,多个服务实例部署在不同机器上,JVM级别的锁无法跨进程生效,此时就必须引入分布式锁来保证跨节点的资源互斥访问。

典型场景:

  • 电商库存扣减,防止超卖
  • 定时任务防重复执行
  • 分布式环境下的幂等性保证
  • 账户余额变更

二、分布式锁的核心设计原则

设计一个合格的分布式锁,必须满足以下核心要素:

1. 互斥性

任意时刻,只有一个客户端能持有锁,这是分布式锁最基本的要求。

2. 可重入性

同一个线程/进程可以多次获取同一把锁而不会产生死锁,类似 ReentrantLock 的语义。

3. 防死锁

锁必须设置超时时间,即使持有锁的客户端发生崩溃,锁也能在超时后自动释放,避免其他客户端永远无法获取锁。

4. 高可用

锁服务本身应当具备高可用性,不能因为单个节点故障导致锁服务不可用。

5. 加锁和解锁的归属一致性

只有加锁的客户端才能解锁,不能出现A加的锁被B解掉的情况。

6. 高性能

加锁和解锁操作应当高效,不能成为系统的性能瓶颈。

三、主流实现方案深度解析


方案一:基于 Redis 实现分布式锁

核心原理

利用 Redis 的 SETNX(Key不存在时才设置)命令实现互斥,结合 EXPIRE 设置过期时间防止死锁。

基础实现(加锁)
java 复制代码
public boolean tryLock(String lockKey, String requestId, long expireTime) {
    // 使用 SET 命令原子性加锁并设置过期时间
    // NX: 不存在才设置  PX: 毫秒级过期
    String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
    return "OK".equals(result);
}

关键点: 必须使用 SET key value NX PX expireTime 一条命令完成加锁和设置过期时间,保证原子性。如果先用 SETNX 再用 EXPIRE,两步操作之间如果宕机,会导致死锁。

解锁实现(Lua脚本保证原子性)
java 复制代码
public boolean unlock(String lockKey, String requestId) {
    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(lockKey), 
        Collections.singletonList(requestId));
    return Long.valueOf(1).equals(result);
}
``n
**为什么用Lua脚本?** 解锁需要两步操作:先GET判断是否是自己的锁,再DEL删除。这两步必须原子执行,否则可能出现:A的锁刚好过期,B获取了锁,A的DEL请求删除了B的锁。Lua脚本在Redis中单线程执行,保证原子性。

#### Redisson 框架(生产级方案)

手写Redis分布式锁容易踩坑,生产环境推荐使用 **Redisson** 框架:

```java
// 引入依赖后直接使用
RLock lock = redisson.getLock("myLock");
try {
    // 尝试加锁:等待时间5秒,锁自动释放时间30秒
    if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
        // 执行业务逻辑
        doBusiness();
    }
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

Redisson 的核心优势:

  • 看门狗机制(Watchdog): 默认锁超时30秒,Redisson会每10秒(超时时间的1/3)自动续期,只要客户端还活着,锁就不会过期释放,完美解决业务执行时间不确定的问题
  • 可重入: 内部维护加锁次数计数器,支持同一客户端多次加锁
  • 公平锁支持: 提供 RedissonFairLock,按请求顺序获取锁
  • 读写锁: 提供 RedissonReadWriteLock,支持读写分离场景
  • 联锁(MultiLock): 同时对多个Redis实例加锁,提高可靠性
Redis方案优缺点

优点: 性能极高,SET/DEL操作亚毫秒级;实现简单,生态成熟

缺点:

  • 主从切换时可能丢失锁信息(主节点加锁后未同步到从节点就宕机)
  • 锁过期时间难以精确预估,业务未执行完锁可能已释放

方案二:基于 ZooKeeper 实现分布式锁

核心原理

利用 ZooKeeper 的临时顺序节点Watch机制实现分布式锁:

  1. 每个客户端在锁节点下创建一个临时顺序节点
  2. 判断自己是否为序号最小的节点,是则获取锁成功
  3. 否则Watch前一个节点的删除事件,前一个节点删除时被唤醒重新判断
  4. 客户端断开连接后,临时节点自动删除,锁自动释放
Curator 框架实现
java 复制代码
// 使用 Curator 框架(ZooKeeper的生产级客户端)
InterProcessMutex lock = new InterProcessMutex(client, "/locks/myLock");
try {
    if (lock.acquire(5, TimeUnit.SECONDS)) {
        try {
            // 执行业务逻辑
            doBusiness();
        } finally {
            lock.release();
        }
    }
} catch (Exception e) {
    log.error("获取分布式锁异常", e);
}
ZooKeeper方案优缺点

优点:

  • 可靠性极高: 临时节点+Session机制,客户端宕机后锁自动释放,不存在死锁风险
  • 公平锁: 顺序节点天然保证FIFO,先到先得
  • CP特性: ZK保证一致性,不会出现多个客户端同时持有锁

缺点:

  • 性能较低: 每次加锁需要创建节点、网络通信,性能远不如Redis
  • 性能瓶颈: 大量客户端频繁创建删除节点时,ZK可能成为瓶颈
  • 运维复杂: ZK集群维护成本高于Redis

方案三:基于 MySQL 实现分布式锁

方案一:唯一索引法
sql 复制代码
-- 创建锁表
CREATE TABLE distributed_lock (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    lock_key VARCHAR(128) NOT NULL UNIQUE,
    lock_value VARCHAR(128) NOT NULL,
    expire_time BIGINT NOT NULL,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 加锁:插入成功即获取锁
INSERT INTO distributed_lock(lock_key, lock_value, expire_time) 
VALUES('myLock', 'uuid-xxx', UNIX_TIMESTAMP() * 1000 + 30000);

-- 解锁:删除对应记录
DELETE FROM distributed_lock 
WHERE lock_key = 'myLock' AND lock_value = 'uuid-xxx';
方案二:乐观锁法(基于版本号)
sql 复制代码
-- 使用版本号实现乐观锁
UPDATE account SET balance = balance - 100, version = version + 1 
WHERE id = 1 AND version = #{currentVersion};
MySQL方案优缺点

优点: 实现简单,无需引入额外中间件,适合已有MySQL的环境

缺点: 性能最差,数据库成为性能瓶颈;锁无超时自动释放机制(需额外定时清理);不适合高并发场景


四、三种方案对比总结

性能维度: Redis > ZooKeeper > MySQL

可靠性维度: ZooKeeper > Redis > MySQL

实现复杂度: MySQL最简单,Redis中等(Redisson封装后也很简单),ZooKeeper较复杂

适用场景:

  • Redis分布式锁: 适用于高并发、追求极致性能的场景,绝大多数互联网公司首选
  • ZooKeeper分布式锁: 适用于对可靠性要求极高、并发量中等的场景,如金融核心系统
  • MySQL分布式锁: 适用于并发量低、不想引入额外中间件的轻量级场景

五、面试高频追问与最佳实践

追问1:Redis主从切换导致锁丢失怎么办?

使用 RedLock 算法(Redis作者提出):向N个独立的Redis实例同时加锁,超过半数(N/2+1)加锁成功才算获取锁成功。

java 复制代码
// Redisson 实现红锁
RLock lock1 = redisson1.getLock("myLock");
RLock lock2 = redisson2.getLock("myLock");
RLock lock3 = redisson3.getLock("myLock");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
redLock.lock();
// ...业务逻辑...
redLock.unlock();

注意:RedLock算法在学术界存在争议(Martin Kleppmann的批评),实际生产中多数公司仍使用单Redis实例+Redisson看门狗方案。

追问2:锁续期问题怎么解决?

业务执行时间不确定时,锁可能提前过期。解决方案:

  • Redisson看门狗: 自动续期,最简方案
  • 手动续期: 启动后台线程定期延长过期时间
  • 合理设置超时: 根据P99耗时设置超时时间,留有冗余

追问3:分布式锁和分布式事务有什么关系?

分布式锁保证互斥访问 (串行化),分布式事务保证数据一致性。两者是不同层面的问题,但经常配合使用:先通过分布式锁保证同一时刻只有一个客户端操作资源,再通过事务保证操作的一致性。

生产级最佳实践

  1. 优先使用Redisson,不要手写Redis分布式锁
  2. 必须设置合理的超时时间,防死锁
  3. 解锁必须验证归属,用Lua脚本或Redisson
  4. 加锁时设置唯一requestId,避免误删他人锁
  5. 业务代码必须放在try-finally中,确保异常时也能释放锁
  6. 考虑降级方案,当锁服务不可用时系统如何兜底
  7. 监控锁的持有时间和等待时间,及时发现异常

六、结语

分布式锁是分布式系统中的基础设施级组件,选型时需要在性能、可靠性、复杂度之间做权衡。面试中不仅要答出实现方案,更要展示对各种边界情况(死锁、主从切换、续期)的深入理解,这才是高级工程师应有的思考深度。

感谢读者观看

相关推荐
战族狼魂2 小时前
集 “自动飞行、智能识别、实时预警、勤务联动” 于一体的高速公路应急车道无人机检测系统方案
java·人工智能·大模型·无人机
一只鹿鹿鹿2 小时前
信息化项目管理规范(参考Word文件)
java·大数据·运维·开发语言·数据库
Java小白笔记2 小时前
Linux 手动部署 Oracle JDK 17 完全指南
java·linux·oracle
夕除2 小时前
实战--2
java·spring boot·spring
kyriewen2 小时前
面试8家前端岗位后,我发现了一个残酷的事实:AI不是加分项,是门槛
前端·javascript·面试
Chase_______2 小时前
【Java杂项】final 关键字详解:变量、方法、类限制与引用可变性
java·开发语言·python
用户887665426632 小时前
Git 和 GitHub 入门:从版本控制到团队协作,一篇文章讲清楚
面试·github
凤山老林2 小时前
DDD(领域驱动设计)在复杂业务系统中的落地指南
java·开发语言·数据库·ddd·领域驱动
JEECG低代码平台2 小时前
JimuChatBI — 首款免费开源的 Java 智能问数ChatBI平台,零成本接入,AI对话式智能分析
java·人工智能·开源·aigc·人工智能低代码