Redission分布式锁知识讲解大全
目录
在分布式系统中,如何协调多个节点对共享资源的访问是一个关键问题。分布式锁作为解决这一问题的有效工具,能够确保同一时刻只有一个节点可以访问某个共享资源,避免了并发访问导致的数据不一致或冲突问题。本文将深入讲解 Redisson 分布式锁的基本概念、底层实现原理、常见使用场景及其应用。
一、什么是 Redisson 分布式锁?
Redisson 是基于 Redis 的高性能 Java 客户端,它提供了丰富的工具来简化分布式系统中的常见问题。分布式锁是其中一个重要的功能。Redisson 分布式锁允许在分布式环境中保证对共享资源的独占访问。通过 Redisson 提供的 RLock 类,用户可以很容易地实现分布式锁机制。
二、分布式锁的基本概念
在分布式系统中,多个节点(通常是多台机器)可能需要访问共享资源,而如果多个节点在同一时刻访问同一资源,就可能导致数据冲突、重复执行等问题。为了避免这种情况,可以通过分布式锁来确保每次只有一个节点能够访问共享资源,其他节点必须等待锁的释放才能继续访问。
分布式锁的特点:
- 互斥性:在同一时刻,只有一个节点能够获得锁,其他节点必须等待。
- 可重入性:同一线程可以多次获取同一把锁。
- 防死锁:通过设置锁的过期时间,避免死锁的发生。
- 可扩展性:分布式锁支持分布式环境,能够在多台机器间同步锁的状态。
三、Redisson 分布式锁的底层实现
Redisson 分布式锁的底层实现主要基于 Redis 的原子操作,使用 SETNX 命令(SET if Not eXists)和lua脚本实现。当客户端尝试获取锁时,它会尝试在 Redis 中创建一个唯一的 key(作为锁标识符)。如果该 key 不存在,表示锁尚未被占用,客户端可以成功获得锁;如果该 key 已经存在,表示锁已经被占用,客户端必须等待。
1. 锁的获取:
- 客户端通过
SETNX命令和Lua脚本将一个唯一的 key 插入到 Redis 中,表示它已经成功获得锁。 - 一旦获取到锁,客户端可以继续执行需要同步的操作。
- 锁的 key 通常会设置一个过期时间(例如 30 秒),防止客户端因为异常崩溃而持有锁无法释放,导致其他客户端无法获取锁。
2. 锁的释放:
- 释放锁的过程中,Redisson 会检查锁的标识符(key)是否当前客户端,确保没有误删除其他客户端的锁。新建锁时,value是客户端生成的唯一标识,通常是 UUID + 线程ID
- Redission使用Lua脚本确保原子性,避免在释放锁时发生竞态条件
说明:竞态条件指的是,在高并发环境下,多个线程(或多个进程)同时访问和操作同一份贡献资源,最终的执行结果依赖于操作的执行时序,而这个时序是不可预测的,可能造成数据安全问题。
3. 锁的过期与死锁防止:
- 为了防止死锁问题,Redisson 会为每个锁设置过期时间。如果客户端没有在规定时间内释放锁,Redis 会自动删除该锁,确保其他客户端能够获取到锁。
- 锁的过期时间可以由客户端设置,也可以由 Redisson 自动管理。
4. 重入锁(Reentrant Lock):
Redisson 支持重入锁机制,即同一个线程在获取锁后可以多次获取锁而不会被阻塞。重入锁通过对锁的计数来实现,每次获取锁时,锁的计数加一;每次释放锁时,计数减一,只有当计数为 0 时,锁才会被释放。
5. Lua 脚本的重要性:
Redission 的获取锁、释放锁和锁的续期都使用 Lua 脚本来确保原子操作。如果不用 Lua 脚本可能会遇到什么竞态问题?
5.1 获取锁
常见写法:
Boolean success = redisTemplate.opsForValue()
.setIfAbsent("lock:order:123", "uuid:thread1", 30, TimeUnit.SECONDS);
setIfAbsent 方法是原子操作,但Redission为了支持重入锁和一些复杂逻辑,设置键值要复杂很多,如果不使用 Lua 脚本 手写逻辑大概如下
if (!redisTemplate.hasKey("lock:order:123")) {
redisTemplate.opsForValue().set("lock:order:123", "uuid:thread1", 30, TimeUnit.SECONDS);
}
对应 Redis 命令:
EXISTS lock:order:123
SET lock:order:123 uuid:thread1 PX 30000
(1)客户端A执行 EXISTS key,判定 key 不存在;
(2)客户端B在 A 执行SET 之前,也判定key 不存在,抢先 SET key valueB 成功;
(3)A继续执行 SET key valueA,将B的锁覆盖了
导致多个客户端以为自己拿到了锁,执行后续的逻辑操作
5.2 释放锁
常见写法(非原子):
String value = redisTemplate.opsForValue().get("lock:order:123");
if ("uuid:thread1".equals(value)) {
redisTemplate.delete("lock:order:123");
}
对应 Redis 命令:
GET lock:order:123
DEL lock:order:123
(1)客户端A执行 GET key, 确认锁是自己的;
(2)在A 执行 DEL 之前,刚好锁过期了, Redis自动删除key;
(3)客户端 B 立即 SET key valueB 成功,拿到了锁;
(4)A继续执行 DEL key, 将B的锁删掉了;
导致 客户端B的锁被删掉,B的执行操作暴露出来。
5.3 锁的续期
常见写法(非原子):
String value = redisTemplate.opsForValue().get("lock:order:123");
if ("uuid:thread1".equals(value)) {
redisTemplate.expire("lock:order:123", 30, TimeUnit.SECONDS);
}
对应 Redis 命令:
GET lock:order:123
PEXPIRE lock:order:123 30000
(1)客户端A 执行 GET key, 确认锁是自己的;
(2)在 PEXPIRE 之前,锁刚好过期删除,客户端 B 抢到了锁;
(3)A继续执行PEXPIRE key 30000, 把B的锁续期;
导致给别人的锁续命了。
四、Redisson 分布式锁的使用方法
Redisson 提供了简单易用的 API 来实现分布式锁。以下是一些常见的使用方法。
1. 获取锁
通过 Redisson 客户端的 getLock 方法可以获取一个锁对象:
// 获取 Redisson 客户端实例
RedissonClient redisson = Redisson.create();
// 获取锁对象
RLock lock = redisson.getLock("myLock");
// 获取锁,阻塞当前线程,直到锁被获取
lock.lock();
// 执行临界区代码
try {
// 你的业务代码
} finally {
// 释放锁
lock.unlock();
}
2. 设置锁的过期时间
在获取锁时,可以为锁设置一个过期时间,防止死锁:
RLock lock = redisson.getLock("myLock");
lock.lock(10, TimeUnit.SECONDS); // 设置锁过期时间为10秒
3. 尝试获取锁(带超时)
如果不想一直等待锁,可以使用 tryLock 方法,设置等待的最大时间和锁的最大持有时间。
RLock lock = redisson.getLock("myLock");
boolean isLocked = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (isLocked) {
try {
// 执行临界区代码
} finally {
lock.unlock();
}
} else {
// 锁获取失败,执行其他操作
}
4. 重入锁
Redisson 支持重入锁,允许同一线程多次获取同一个锁:
RLock lock = redisson.getLock("myLock");
lock.lock();
try {
// 执行临界区代码
lock.lock(); // 线程再次获取锁,成功
try {
// 其他业务代码
} finally {
lock.unlock(); // 解锁
}
} finally {
lock.unlock(); // 解锁
}
5. 条件释放锁(延迟释放)
如果在执行某些任务时,只有在满足某些条件时才释放锁,可以使用条件判断来控制锁的释放。
RLock lock = redisson.getLock("myLock");
lock.lock(30, TimeUnit.SECONDS); // 设置锁的超时时间
if (condition) {
lock.unlock(); // 提前释放锁
}
五、看门狗机制
1. 看门狗机制的作用
自动为未显式指定存活时间的分布式锁续期,防止业务逻辑执行时间超过锁的默认租约时间(Lease Time)而导致锁被意外释放,从而引发数据不一致。
具体而言,Redisson 会为每个锁维护一个独立的 看门狗定时任务,该任务会定期检查锁的过期时间,并在锁即将过期时自动延长它的有效期。这样一来,即使客户端的操作时间超过了锁的初始过期时间,Redisson 仍然会确保锁不会过期,从而避免了死锁的发生。
2. 如何工作
- 如果没有指定Redis中锁的有效时间,当客户端获取到锁时,Redisson 会在 Redis 中为该锁设置一个过期时间(默认30秒)。
- 同时,Redisson 会启动一个看门狗定时任务,当时间过了1/3时,就会检查当前线程是否还继续持有锁,如果是就重新刷新时间为30秒,直到锁的释放。
3. 看门狗机制的优势
- 防止死锁:如果客户端在执行任务时发生了异常,锁的过期时间仍会被自动延长,从而避免其他客户端因为无法获得锁而产生死锁。
- 高可靠性:看门狗机制确保了锁在整个持有过程中一直有效,减少了因时间管理不当导致的并发问题。
- 自动管理:开发者无需手动延长锁的过期时间,Redisson 会自动管理这一过程,减少了开发者的操作负担。
4. 如何关闭看门狗机制
Redisson 也提供了关闭看门狗机制的选项。如果某些场景下不需要自动延长锁的过期时间,可以通过配置关闭看门狗机制。关闭后,如果客户端没有及时释放锁,锁会在过期时间到达后自动被 Redis 删除。
关闭看门狗机制的方式:
(1)配置类中显式关闭看门狗机制
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379")
.setMasterName("myMaster").setTimeout(3000)
.setWatchdogTimeout(0); // 关闭看门狗机制
(2) 在尝试获取锁时指定leaseTime参数
lock.tryLock(5L, 10L, TimeUnit.SECONDS);
5. 锁自动延长机制的实现
Redisson 中的锁自动延长机制使用了 Redis 的 Lua 脚本 和 Redis 的 SET 命令,通过原子操作来实现锁过期时间的延长。这些操作的优势在于,它们在 Redis 服务器端执行,避免了客户端之间的时间差和网络延迟,从而保证了锁机制的准确性和可靠性。
六、Redisson 分布式锁的应用场景
Redisson 分布式锁作为一种常用的同步工具,能够有效解决多个节点或服务对共享资源的并发访问问题。它在分布式系统中的应用非常广泛,尤其在以下几种常见场景中,能够确保操作的原子性、避免冲突、提高系统稳定性。
1. 防止重复执行任务
在分布式系统中,多个节点可能会并发执行相同的任务,尤其是一些定时任务或异步任务。没有协调机制的情况下,多个节点可能会重复处理同一批数据,导致资源浪费、数据冗余或其他不可预期的错误。
场景举例:
-
任务调度:比如多个服务节点定时执行某项任务(如清理过期数据、同步数据等)。如果没有分布式锁,每个节点可能都会重复执行相同的任务,导致冗余计算和资源浪费。使用 Redisson 分布式锁,可以确保同一时刻只有一个节点能够执行该任务。
解决方案:通过 Redisson 提供的分布式锁,在任务执行前锁定任务资源,确保只有一个节点能获取锁并执行任务。任务完成后,锁释放,其他节点才有机会执行。
2. 分布式计数器/限流
当多个节点同时访问并更新同一个计数器(如库存、访问次数等)时,可能会导致计数器值的竞争问题,甚至出现数据不一致的情况。例如,多个用户并发请求购买同一商品时,库存数量需要实时更新,若没有适当的锁机制,可能会发生超卖问题。
场景举例:
-
库存管理:电商平台在促销活动时,可能有多个用户同时下单购买某个商品。使用分布式锁,确保库存数量的更新操作是串行的,从而避免了多个请求并发更新导致的超卖问题。
解决方案:通过 Redisson 分布式锁保证同一时刻只有一个节点能更新库存值。当库存被更新时,锁被释放,其他请求才能继续执行。
3. 资源访问控制
对于某些共享资源(例如数据库表、文件、缓存等),通常我们需要确保同一时刻只有一个线程或节点能够访问和修改这些资源。否则,多个节点并发访问时可能会引发数据冲突或破坏资源的完整性。
场景举例:
-
数据库操作:当多个节点需要执行某个数据库更新操作时,如果没有协调机制,可能会出现数据不一致。例如,多个节点同时对同一条记录进行更新,可能会丢失数据或导致更新冲突。
解决方案:使用分布式锁确保每次数据库操作时,只有一个节点能够获取锁并执行数据库操作,避免了并发操作导致的冲突。
-
文件上传/处理:在多服务协作的场景下,某个文件可能会被多个节点处理或上传。如果多个节点同时操作同一文件,可能会导致文件内容被覆盖或损坏。
解决方案:通过分布式锁,确保只有一个节点能够在任何时刻处理该文件,保证文件操作的安全性。
4. 缓存一致性控制
分布式系统中,多个节点通常会共享缓存数据。例如,Redis 常作为各节点间的共享缓存。在缓存更新时,如果没有协调机制,可能会发生缓存不一致的问题。多个节点同时修改缓存的同一项数据,可能会导致数据竞争或部分节点看到过时的数据。
场景举例:
-
缓存更新:假设一个电商网站中,多个服务器节点缓存了商品的库存数量。当用户下单购买商品时,需要更新缓存中的库存数据。如果没有分布式锁,不同节点可能会同时更新库存缓存,导致缓存数据的不一致。
解决方案:使用 Redisson 分布式锁,确保只有一个节点可以在同一时刻更新缓存,其他节点需要等待,直到该节点完成更新并释放锁,从而避免缓存更新时的并发问题。
5. 分布式任务处理
在分布式系统中,某些任务可能需要在多个节点之间共享并协调执行。没有锁机制时,可能会出现多个节点并发处理同一任务的问题。通过分布式锁,多个节点可以按照一定的顺序和规则执行任务,避免重复执行。
场景举例:
-
批处理任务:例如,在多个节点处理批量数据时,如果每个节点都独立处理自己的数据集合,可能会导致数据重复处理或漏处理。通过分布式锁,确保每次只有一个节点执行任务,从而避免了并发执行带来的不一致问题。
解决方案:通过 Redisson 分布式锁控制每个节点的执行顺序。只有获得锁的节点才能执行任务,其他节点必须等待锁释放。
6. 确保全局唯一性
在一些场景下,需要保证某个操作或资源是全局唯一的,避免多个节点同时生成重复的资源(如唯一ID、订单号等)。通过分布式锁,可以确保生成全局唯一值的操作不被并发执行。
场景举例:
-
生成唯一ID:比如在一个电商系统中,为每个订单生成一个全局唯一的订单号。如果没有分布式锁,多个节点可能同时生成相同的订单号,导致订单号重复。
解决方案:通过 Redisson 分布式锁,保证只有一个节点可以生成唯一的订单号,其他节点只能等待,直到锁被释放。
七、分布式锁的优势与挑战
优势:
- 高可靠性:利用 Redis 的原子操作保证分布式锁的可靠性。
- 防止死锁:通过设置过期时间和锁标识符,避免死锁的发生。
- 简单易用:Redisson 提供了简洁的 API,开发者可以方便地使用分布式锁。
- 性能高效:Redis 本身具有极高的性能,保证分布式锁操作的低延迟。
挑战:
- 网络分区问题:分布式系统中的网络分区可能导致锁的丢失或无法正确释放。
- 锁粒度问题:锁粒度过大会导致性能瓶颈,过小则可能增加锁操作的频率。
- 误释放锁:如果锁的释放机制没有处理好,可能会导致其他客户端误释放锁。
八、总结
Redisson 分布式锁为分布式系统提供了一个高效、可靠的锁机制,确保了在多个节点并发访问共享资源时的同步与互斥。通过简单易用的 API,Redisson 提供了强大的锁机制,帮助开发者避免了重复任务、死锁、资源冲突等问题。然而,在实际应用中,需要注意合理设置锁的过期时间、精确控制锁的粒度,并防止可能的网络分区带来的锁丢失问题。
总之,Redisson 分布式锁是一种非常实用的工具,能够极大地简化分布式系统中的同步问题,是每个分布式系统开发者必备的技术利器。
更多推荐



所有评论(0)