我们使用Redisson实现分布式锁,之所以称为“分布式”锁,是因为它在分布式环境下协调多个节点(多个JVM或多个服务器)对共享资源的访问。分布式锁的目的是在分布式系统中提供一种互斥机制,确保在同一时间只有一个节点(中的一个线程)可以访问共享资源。
Redisson分布式锁的实现依赖于Redis,因为Redis本身是一个分布式缓存数据库,可以被多个节点访问,从而作为分布式锁的存储层。Redisson通过Redis的原子操作和特性来实现分布式锁。
下面详细解释Redisson分布式锁的实现原理:
- 锁的获取 当多个客户端(即分布式系统中的多个节点)尝试获取同一个锁时,Redisson会使用Redis的SET命令(实际上使用Lua脚本保证原子性)来设置一个键(key),这个键代表锁。如果键不存在,则设置成功,表示获取锁;如果键已经存在,则获取失败。
Redisson在设置键的时候会设置一个过期时间(leaseTime),这是为了防止客户端崩溃导致锁无法释放。如果设置了过期时间,即使客户端崩溃,锁也会在过期后自动释放,避免死锁。
锁的释放 当客户端完成业务逻辑后,需要释放锁。释放锁就是删除Redis中对应的键。但是,为了避免误删其他客户端的锁,Redisson在设置锁的时候会生成一个唯一标识(通常使用UUID和线程ID),作为键的值。在删除键之前,会检查值是否匹配,只有匹配才删除。
可重入性 Redisson的锁支持可重入,即同一个客户端上的同一个线程可以多次获取同一把锁。Redisson使用Redis的Hash结构来存储锁,其中键是锁的名称,字段是客户端唯一标识(UUID和线程ID),值是重入次数。当线程第一次获取锁时,重入次数设置为1;如果同一个线程再次获取锁,重入次数加1;释放锁时,重入次数减1,当重入次数为0时,删除锁。
锁的续期 如果业务逻辑执行时间较长,超过了锁的过期时间,那么锁会自动释放,可能导致其他客户端获取锁,造成问题。Redisson提供了锁续期机制(看门狗机制),在获取锁成功后,会启动一个定时任务,定期(默认每10秒)检查锁是否还存在,如果存在则重置过期时间(默认续期到30秒)。这样,只要客户端还在执行业务逻辑,并且没有主动释放锁,锁就不会因为过期而释放。
等待锁的机制 当锁被其他客户端持有时,当前客户端会尝试等待获取锁。Redisson使用了Redis的发布订阅功能来通知等待的客户端锁已被释放,从而避免频繁的轮询。当锁被释放时,等待的客户端会收到通知,然后再次尝试获取锁。
公平锁 Redisson还提供了公平锁的实现。公平锁按照客户端请求锁的顺序来分配锁,即先到先得。公平锁的实现使用了Redis的列表(List)结构来维护等待队列。
红锁(RedLock) 为了进一步提高分布式锁的可靠性,Redisson支持红锁算法。红锁算法需要在多个独立的Redis节点上获取锁,当在大多数节点上获取锁成功时,才认为获取锁成功。这样可以避免单个Redis节点故障导致的问题。
总结 Redisson分布式锁之所以称为“分布式”,是因为它利用Redis作为分布式协调中间件,在多个独立的客户端(可能运行在不同的JVM或不同的机器上)之间进行锁的协调。通过Redis的原子操作和数据结构,实现了互斥、可重入、锁续期、公平锁等特性,从而在分布式系统中提供可靠的锁服务。
需要注意的是,Redis本身是单线程的,所以它可以保证单个命令的原子性。而Redisson使用Lua脚本将多个命令组合成一个原子操作,从而避免了竞态条件。
然而,分布式锁的实现还需要考虑网络延迟、分区等问题。在某些极端情况下,比如发生网络分区时,可能会出现多个客户端同时持有锁的情况。因此,在使用分布式锁时,需要根据业务场景权衡一致性和可用性。
这个问题触及了分布式锁的核心概念。以下详细解释"分布式"的含义以及Redisson的具体实现机制。
在分布式系统中,同一个应用会部署在多个节点(服务器)上:
[服务实例A] [服务实例B] [服务实例C]
| | |
+-----------+-----------+
|
[共享资源/服务]
传统的Java锁(如synchronized
、ReentrantLock
)只能保证单个JVM进程内的线程安全:
synchronized
:JVM级别锁,不同服务器之间无法协调ReentrantLock
:同样是进程内锁分布式锁是在多个独立的进程/服务器之间实现互斥访问的协调机制。它的核心特征是:
[应用节点A] [应用节点B] [应用节点C]
| | |
+---------------+---------------+
|
[Redis集群/单机]
(共享状态存储)
Redisson使用Redis的Hash结构存储锁信息:
# 锁的Key
"my_distributed_lock"
# Hash字段和值
Field: "b983c153-7421-4a1c-9f3a-7b68a1d5e6a9:1" # 客户端ID:线程ID
Value: 1 # 重入次数
这是实现分布式的核心技术!所有锁操作都是原子性的:
加锁Lua脚本:
-- KEYS[1] = 锁key, ARGV[1] = 过期时间, ARGV[2] = 客户端标识
if (redis.call('exists', KEYS[1]) == 0) then
-- 锁不存在,创建锁
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
-- 重入锁
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 返回锁的剩余生存时间
return redis.call('pttl', KEYS[1]);
每个Redisson客户端都有唯一标识,确保锁的归属明确:
// Redisson内部生成客户端ID
public class RedissonLock {
protected String getLockName(long threadId) {
return id + ":" + threadId; // UUID:线程ID
// 示例: "b983c153-7421-4a1c-9f3a-7b68a1d5e6a9:1"
}
}
时序图:
节点A Redis 节点B
| | |
| 尝试加锁 | |
| ----------> | |
| | 创建锁成功 |
| | <------------ | 尝试加锁
| | 返回锁已存在 |
| | ------------> |
| 执行业务 | | 等待/重试
| ----------> | |
| 释放锁 | |
| ----------> | |
| | 通知节点B |
| | ------------> | 获取锁成功
// 节点A(服务器192.168.1.10)
@Service
public class ServiceNodeA {
@Autowired private RedissonClient redissonClient;
public void businessMethod() {
RLock lock = redissonClient.getLock("distributed_lock");
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
System.out.println("节点A获取锁成功,时间: " + new Date());
Thread.sleep(10000); // 模拟业务处理10秒
}
} finally {
lock.unlock();
}
}
}
// 节点B(服务器192.168.1.11)
@Service
public class ServiceNodeB {
@Autowired private RedissonClient redissonClient;
public void businessMethod() {
RLock lock = redissonClient.getLock("distributed_lock"); // 同一个锁名!
try {
if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
System.out.println("节点B获取锁成功,时间: " + new Date());
Thread.sleep(5000);
}
} finally {
lock.unlock();
}
}
}
运行结果:
节点A获取锁成功,时间: Wed Nov 01 10:00:00 CST 2023
节点B获取锁成功,时间: Wed Nov 01 10:00:10 CST 2023 # 10秒后才获取成功
// 防止业务执行时间超过锁过期时间
private void renewExpiration() {
// 每10秒检查一次,如果锁还存在就续期到30秒
Timeout task = timer.newTimeout(timeout -> {
// 续期逻辑:如果还是当前客户端持有锁,就重置过期时间
if (redis.eval("if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then " +
"return redis.call('pexpire', KEYS[1], ARGV[1]); " +
"else return 0; end;",
lockKey, internalLockLeaseTime, clientId)) {
// 递归调用,继续续期
renewExpiration();
}
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
}
// Redisson支持多种Redis部署模式
Config config = new Config();
// 单机模式
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
// 集群模式
config.useClusterServers()
.addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001");
// 哨兵模式
config.useSentinelServers()
.setMasterName("mymaster")
.addSentinelAddress("redis://127.0.0.1:26379");
对于更高要求的场景,可以使用多主Redis实例:
RedissonClient client1 = Redisson.create(config1);
RedissonClient client2 = Redisson.create(config2);
RedissonClient client3 = Redisson.create(config3);
RLock lock1 = client1.getLock("lock");
RLock lock2 = client2.getLock("lock");
RLock lock3 = client3.getLock("lock");
// 红锁:需要大多数实例加锁成功
RLock redLock = redisson.getRedLock(lock1, lock2, lock3);
特性 | 传统锁(synchronized) | 分布式锁(Redisson) |
---|---|---|
作用范围 | 单个JVM进程内 | 跨JVM、跨服务器 |
状态存储 | JVM内存 | Redis等外部存储 |
网络依赖 | 无 | 需要网络访问Redis |
性能 | 极高(内存操作) | 高(网络+Redis操作) |
适用场景 | 单机应用 | 分布式系统、微服务 |
Redisson实现分布式的核心在于:
正是通过这些机制,Redisson能够在分布式的环境中提供可靠的互斥访问控制,这才是"分布式锁"的真正含义。