分布式锁是解决分布式系统中并发控制问题的关键机制。在单体应用中,我们可以使用Java的synchronized或ReentrantLock来实现线程同步,但这些机制只在单个JVM内有效。
当应用扩展为分布式架构后,多个服务实例可能同时访问和修改共享资源.
传统的单机锁无法跨进程协调资源访问,这就需要分布式锁来解决。"
我认为分布式锁主要应用在以下几个典型场景:
一个有效的分布式锁需要满足几个关键特性:
写项目的时候有一些需要注意的问题:
总的来说,分布式锁是分布式系统中不可或缺的组件,它解决了单机锁无法解决的跨进程资源协调问题。选择合适的分布式锁实现,需要根据业务场景、一致性要求和性能需求综合考虑。在实际应用中,我们不仅要关注锁的功能实现,还要考虑异常情况处理、性能优化和可用性保障。"
分布式锁的本质是在分布式环境下实现的互斥协调机制。与单机锁不同,分布式锁需要解决的核心问题是:如何在没有共享内存的情况下,协调多个分布式节点对共享资源的访问。
从技术实现角度看,分布式锁依赖于一个所有节点都能访问的共享协调点(如Redis、Zookeeper或数据库),通过原子操作和一致的协议来确保在任意时刻只有一个客户端能够获取锁。
一个完善的分布式锁应具备四个关键特性:
分布式锁的实现也受CAP理论约束,不同实现方式在一致性、可用性和分区容忍性之间有不同权衡。
例如,基于Redis的分布式锁通常偏向AP,而基于Zookeeper的实现偏向CP。
在我的实践经验中,分布式锁的选择需要根据业务场景、一致性要求和性能需求综合考虑。
例如,在我负责的xx系统中,对于xx类核心数据,我们使用了Redisson实现的Redis分布式锁,它通过’看门狗’机制解决了锁超时问题;而对于一些非核心数据,我们选择了更轻量的实现方案。
分布式锁的本质挑战在于,它需要在分布式系统固有的网络延迟、分区和节点故障等问题存在的情况下,依然能够提供可靠的互斥保证。这本质上是一个分布式共识问题,也是为什么完美的分布式锁实现是非常有挑战性的。"
Redis分布式锁的实现原理是利用Redis的单线程模型和原子操作特性,通过在Redis中创建一个键值对来表示锁,确保在任意时刻只有一个客户端能够成功设置这个键值对,从而实现分布式环境下的互斥控制。
最基本的实现方式是使用SET lock_key unique_value NX PX timeout
命令,它将获取锁和设置过期时间合并为一个原子操作。
unique_value
通常是客户端的唯一标识,用于安全释放锁;NX
表示只在键不存在时设置;PX timeout
设置锁的过期时间,防止客户端崩溃导致的死锁。if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
这种基本实现存在一些挑战:
单节点Redis的分布式锁在Redis节点故障时可能导致锁失效。为了提高可靠性Redis提出了Redlock算法,它使用多个独立的Redis节点来实现更可靠的分布式锁。
Redlock算法在一定程度上提高了分布式锁的可靠性,但仍然存在一些问题:
Redis实现分布式锁有多种方式,每种方式都有其特点和适用场景。
从核心特性来看,一个完善的分布式锁必须具备几个关键特性:
互斥性, 防死锁, 高可用, 高性能.
其次在在实现细节上,需要注意以下几点:
锁的粒度设计,超时时间设置,锁的获取策略,异常处理机制.
从高级特性方面考虑的话:
锁续期机制,监控与告警,降级策略
在实际项目中,我通常使用Redisson框架,它已经很好地解决了这些问题,包括可重入性、自动续期和读写锁等高级特性,简化了分布式锁的实现和维护。"
分布式锁不是完全可靠的,它存在几个经典的可靠性问题:
最开始我们也是简单地用DEL命令删除锁,后来遇到了并发问题才深入研究这块。
首先说为什么要安全释放。假设这样一个场景:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这个脚本会先检查锁是否还是自己的(通过之前设置的唯一标识),是才删除。用Lua脚本是为了保证这个过程的原子性。
但是后来我们发现,即使这样还是不够。因为在业务执行期间,锁过期了就会被其他线程获取,导致并发执行。
所以我们又引入了看门狗机制。
看门狗其实就是一个自动续期的后台线程。
它会每隔一段时间(比如10秒)检查锁是否还是自己的,如果是就续期。
这样只要持有锁的客户端还活着,锁就不会过期。
现在我们的完整方案是:
第一是合理设置过期时间。这个要根据业务的实际执行时间来定,比如我们的业务一般是毫秒级的,我们会设置锁的过期时间为30秒,留出足够的冗余来应对各种异常情况。
第二是使用看门狗机制。
获取锁时,先设置一个相对较短的过期时间,比如30秒
同时启动一个后台线程(看门狗)
看门狗每隔10秒检查一次,如果发现锁还在使用就自动续期
如果客户端崩溃了,看门狗也就停了,锁自然过期,这样不会产生死锁
try {
// 获取锁
lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
// 执行业务逻辑
} catch (Exception e) {
// 异常处理
} finally {
// 释放锁
lock.unlock();
}
即使有了这些机制,在Redis主从架构下还是可能出现问题。因为如果主节点在复制数据到从节点前崩溃了,这时候从节点被提升为主节点,之前加的锁就丢失了。
以对于一些强一致性要求的场景,我们会考虑:
要么使用Redis Cluster多主节点
要么切换到Zookeeper这样的CP系统
首先说下为什么要可重入。
在实际业务中,我们经常会遇到同一个线程多次获取同一把锁的场景。
比如一个方法获取了锁,它调用的子方法也需要这个锁。如果不支持可重入,就会导致死锁。
实现可重入的核心思路是:
当然,这只是基本实现。在生产环境还需要考虑:
public class RedisDistributedLock {
private StringRedisTemplate redisTemplate;
private static final long DEFAULT_EXPIRE = 30; // 默认30秒过期
private static final long DEFAULT_WAIT = 3; // 默认等待3秒
// 获取锁
public boolean tryLock(String key, String value, long timeout) {
try {
// SET key value NX EX 30
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(key, value, timeout, TimeUnit.SECONDS);
return Boolean.TRUE.equals(result);
} catch (Exception e) {
// 记录日志
return false;
}
}
// 释放锁
public boolean releaseLock(String key, String value) {
// 使用Lua脚本保证原子性
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then " +
"return redis.call('del',KEYS[1]) " +
"else " +
"return 0 " +
"end";
try {
Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key), value);
return Long.valueOf(1).equals(result);
} catch (Exception e) {
// 记录日志
return false;
}
}
// 实际使用示例
public void doBusinessWithLock() {
String key = "order:1";
String value = UUID.randomUUID().toString();
try {
if (tryLock(key, value, DEFAULT_EXPIRE)) {
// 获取锁成功,执行业务逻辑
doBusiness();
} else {
// 获取锁失败的处理
throw new RuntimeException("获取锁失败");
}
} finally {
// 释放锁
releaseLock(key, value);
}
}
}