面试官:项目中如何实现分布式锁?
分布式锁(Distributed Lock)是一种用于分布式系统中的同步机制,主要是为了防止分布式系统中,多个服务实例同时操作一个共享资源所带来的并发安全问题。
分布式锁确保在同一时间只有一个实例操作共享资源,从而保证了数据的安全性。
1.分布式锁实现方案
分布式锁的实现方案有多种,例如以下这几种:
基于数据库实现分布式锁:可以通过数据库的乐观锁或悲观锁实现分布式锁,但是由于数据库的 IO 操作比较慢,不适合高并发场景。
基于 Zookeeper 实现分布式锁:Zookeeper 是一个高可用性的分布式协调服务,可以通过它来实现分布式锁。但是使用 Zookeeper 需要部署额外的服务,增加了系统复杂度。
基于 Redis 实现分布式锁:Redis 是一个高性能的内存数据库,支持分布式部署,可以通过 Redis 的原子操作实现分布式锁,而且具有高性能和高可用性。
综合以上方案来看,基于数据库实现的分布式锁不适用于高并发场景,而基于 Zookeeper 实现的分布式锁又需要额外部署 Zookeeper 服务,增加了运营成本,所以使用 Redis 实现分布式锁是目前主流的实现方案。
2.为什么 Redis 可以实现分布式锁?
因为 Redis 作为一个独立的第三方系统(数据中间件),其本身就支持分布式应用。也就是针对于 Redis 的所有操作,所有的分布式系统都是全局可见的,如下图所示:
3.Redis 如何实现分布式锁?
使用 Redis 实现分布式锁的方案有以下 4 种:
setnx(set if Not eXists):尝试设置键 key 的值为 value,但如果 key 已经存在,则不会执行任何操作并返回 0,如果 key 不存在则加锁成功。
缺陷:存在死锁问题、锁误删问题、不可重入问题、锁无法自动续期问题。
set nx ex/px:setnx 升级版本,Redis 2.6 版本后才能支持此语法。尝试加锁和设置锁超时时间,使用案例 set key value nx px 3000。
缺陷:存在锁误删问题、不可重入问题、锁无法自动续期问题。
Lua 脚本:解决锁重入的问题
缺陷:实现复杂、且存在锁无法自动续期问题。
Redisson 框架:基于 Redis 实现分布式锁的开源框架。其实现简单、不存在锁重入和锁续期等问题。
问题解释
死锁问题:SETNX 如未设置过期时间,锁忘记删了或加锁线程宕机都会导致死锁,也就是分布式锁一直被占用的情况。
锁误删问题:SETNX 设置了超时时间,但因为执行时间太长,所以在超时时间之内锁已经被自动释放了,但线程不知道,因此在线程执行结束之后,会把其他线程的锁误删的问题。
不可重入问题:也就是说同一线程在已经获取了某个锁的情况下,如果再次请求获取该锁,则请求会失败(因为只有在第一次能加锁成功)。也就是说,一个线程不能对自己已持有的锁进行重复锁定。
无法自动续期:线程在持有锁期间,任务未能执行完成,锁可能会因为超时而自动释放。SETNX 无法自动根据任务的执行情况,设置新的超时实现,以延长锁的时间。
综合以上实现方案来看,生产级别使用 Redis 实现分布式锁的方案,应该选用 Redisson 框架。
4.Redisson 介绍
Redisson 是一个开源的用于操作 Redis 的 Java 框架。与 Jedis 和 Lettuce 等轻量级的 Redis 框架不同,它提供了更高级且功能丰富的 Redis 客户端。它提供了许多简化 Redis 操作的高级 API,并支持分布式对象、分布式锁、分布式集合等特性。
Redisson 官网:https://redisson.org/
Redisson 特性说明
Redisson 可以设置分布式锁的过期时间,从而避免锁一直被占用而导致的死锁问题。
Redisson 在为每个锁关联一个线程 ID 和重入次数(递增计数器)作为分布锁 value 的一部分存储在 Redis 中,这样就避免了锁误删和不可重入的问题。
Redisson 还提供了自动续期的功能,通过定时任务(看门狗)定期延长锁的有效期,确保在业务未完成前,锁不会被其他线程获取。
5.Redisson 实现分布式锁
1. 添加 Redisson 框架
添加 Redisson 依赖:
2. 配置 Redis 连接信息
将 RedissonClient 对象保存到 Spring Ioc 容器,并为其设置 Redis 服务连接信息,具体实现代码如下:
3. 创建分布式锁
Redisson 分布式锁的操作和 Java 中的 ReentrantLock(可重入锁)的操作很像,都是先使用 tryLock 尝试获取(非公平)锁,再通过 unlock 释放锁,具体实现如下:
a.实现公平锁
Redisson 默认创建的分布式锁是非公平锁(出于性能的考虑),想要把它变成公平锁可使用以下代码实现:
b.实现读写锁
Redisson 还可以创建读写锁,如下代码所示:
读写锁的特点就是并发性能高,它是允许多个线程同时获取读锁进行读操作的,也就是说在没有写锁的情况下,读取操作可以并发执行,提高了系统的并行度。但写锁则是独占式的,同一时间只有一个线程可以获得写锁,无论是读还是写都无法与写锁并存,这样就确保了数据修改时的数据一致性。
c.实现联锁
Redisson 也支持联锁,也叫分布式多锁 MultiLock,它允许客户端一次性获取多个独立资源(RLock)上的锁,这些资源可能是不同的键或同一键的不同锁。当所有指定的锁都被成功获取后,才会认为整个操作成功锁定。这样能够确保在分布式环境下进行跨资源的并发控制。
联锁的实现示例如下:
课后思考
说说 Redisson 分布式锁的底层实现原理?Redisson 如何解决分布式锁的单点故障问题?
本文已收录到我的面试小站 www.javacn.site,其中包含的内容有:Redis、JVM、并发、并发、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、设计模式、消息队列等模块。
评论