分布式环境下高可靠性分布式锁实现
本文介绍了分布式锁的概念、应用场景及需满足的特点。重点分析了基于 Redis 的分布式锁实现中的缺陷,如非原子性操作、锁误删除及超时自动解锁问题。通过引入唯一标识符、Lua 脚本保证原子性以及看门狗机制实现锁续期,构建了健壮的分布式锁方案。最后总结了关键实现步骤与注意事项,旨在提升分布式系统的可靠性与一致性。

本文介绍了分布式锁的概念、应用场景及需满足的特点。重点分析了基于 Redis 的分布式锁实现中的缺陷,如非原子性操作、锁误删除及超时自动解锁问题。通过引入唯一标识符、Lua 脚本保证原子性以及看门狗机制实现锁续期,构建了健壮的分布式锁方案。最后总结了关键实现步骤与注意事项,旨在提升分布式系统的可靠性与一致性。

在多线程环境下,为了保证同一时间只有一个线程能够执行某段代码,Java 提供了 synchronized 关键字和 ReentrantLock 类作为本地锁的解决方案。这些机制在单个应用或单个 JVM 实例中运行良好,确保了同一进程内的线程同步。但是,随着分布式架构的广泛应用,应用程序通常运行在多个节点上,并且每个节点都有多个线程同时处理任务。在这种情况下,传统的本地锁机制已经无法满足分布式环境下的同步需求。
在分布式系统中,应用程序可能运行在多个物理或虚拟的节点上,这意味着相同的资源可能会被不同节点上的多个线程同时访问。为了确保这些线程在不同节点上同步执行,防止资源竞争和数据不一致问题,我们需要使用一种能够跨节点的同步机制——分布式锁。
分布式锁是一种用于控制在分布式环境中,某个共享资源在同一时刻只能被一个节点或线程使用的机制。它类似于传统的本地锁,但具有跨节点的协调能力。分布式锁通常由外部的分布式系统组件(如 Redis、Zookeeper、Tair 等)来实现,这些组件提供了高可用的锁服务,确保即使在节点故障或网络分区的情况下,锁的状态依然能够保持一致。
分布式锁可以通过多种方式实现,每种方式都有其适用的场景和优缺点。以下是几种常见的分布式锁实现方式:
SETNX 命令(Set if Not Exists)来实现分布式锁。Redis 锁具有高性能、低延迟的优点,适用于大部分需要快速锁定的场景。通过设置锁的过期时间,可以防止死锁问题。分布式锁在分布式系统中有广泛的应用,典型的使用场景如:
| 特点 | 描述 |
|---|---|
| 互斥性 | 确保同一时刻只有一个线程能持有锁,防止多个节点或线程对共享资源的并发访问,保证资源的独占使用。 |
| 可重入性 | 允许同一节点上的同一个线程在已持有锁的情况下,能够再次成功获取该锁,避免锁重入时产生死锁。 |
| 锁超时 | 通过为锁设置过期时间,防止因线程异常或故障未释放锁而导致的死锁情况,确保系统的稳定性和健壮性。 |
| 高性能与高可用性 | 锁的加锁与解锁操作需要高效,以满足高并发需求,并且要确保在节点故障或网络分区等情况下,锁服务依然可用,保障系统的持续运行。 |
| 阻塞与非阻塞性 | 支持锁的阻塞和非阻塞模式。在阻塞模式下,线程在锁不可用时等待锁的释放,并在锁可用时及时被唤醒;在非阻塞模式下,线程可以立即返回继续执行其他逻辑。 |
| 可扩展性 | 锁机制能够随着系统规模的增长而扩展,支持更多节点和更高并发量,保持系统的性能和可靠性。 |
Redis 是一个高性能的键值存储系统,适合用于实现分布式锁,因为它能够在高并发的场景下提供快速的读写操作。借助 Redis 的 SET 命令及其 NX(不存在则插入)参数,我们可以构建一个简单的分布式锁机制。
SET key value NX EX seconds 命令尝试获取锁。如果 key 不存在,则插入成功,并设置过期时间(EX 参数),表示锁定成功;如果 key 已存在,则表示锁已经被其他客户端持有,获取锁失败。DEL key 命令删除该 key 来释放锁,从而让其他等待锁的线程有机会获得锁。// 尝试获取锁
if (set(key, 1, "NX", "EX", 30)) {
try {
// 执行需要加锁的业务逻辑
} finally {
// 释放锁
del(key);
}
}
尽管这种方法简单易用,但它存在几个严重的问题,使得其无法成为一个健壮的分布式锁实现:
SETNX 成功后,但在设置过期时间之前,程序崩溃或出现异常,那么锁将一直存在,导致其他线程无法获取锁,从而产生死锁。DEL 操作,从而误解锁,破坏了其他线程的业务逻辑。在分布式锁的实现中,存在一种潜在的风险,即线程在解锁时误删了其他线程持有的锁。具体情况如下:
DEL 操作,误删了属于线程 2 的锁。
为了解决上述问题,可以在锁中存储一个唯一标识符(例如线程 ID 或 UUID),并在释放锁时检查该标识符是否匹配,从而确保只有持有锁的线程才能成功释放锁。
SET key value NX PX milliseconds 命令,其中 value 是一个唯一标识符(如线程 ID 或 UUID)。这样可以确保在锁存储时记录锁的所有者信息。DEL 操作以释放锁。String threadId = UUID.randomUUID().toString(); // 生成唯一标识符
if (set(key, threadId, "NX", "EX", 30)) {
try {
// 执行业务逻辑
} finally {
if (threadId.equals(get(key))) {
del(key); // 释放锁
}
}
}
同时,这种方式也能够将分布式锁改造成可重入的分布式锁,在获取锁的时候判断一下是否是当前线程获取的锁,锁标识自增便可。
在分布式锁中,SETNX 和 EXPIRE 操作不是原子性的,可能导致死锁等并发问题。为了解决这个问题,我们可以使用 Lua 脚本来确保这些操作的原子性。
SETNX 成功后,如果 EXPIRE 操作未执行(例如由于服务器故障或网络问题),锁可能没有超时时间,从而导致死锁。Lua 脚本可以将多个 Redis 操作封装为一个原子操作,确保获取锁、设置过期时间、判断标识符和删除锁的操作按预期执行。Lua 脚本示例:
if (redis.call('setnx', KEYS[1], ARGV[1]) < 1) then return 0; -- 获取锁失败 end;
redis.call('expire', KEYS[1], tonumber(ARGV[2])); -- 设置过期时间
return 1; -- 获取锁成功
if (redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]); -- 释放锁 end;
return 0; -- 当前线程不是锁持有者
通过 Java 调用 eval 方法执行上述 Lua 脚本,确保 Redis 操作的原子性:
// 获取锁
Object result = jedis.eval(luaScriptForSet, Collections.singletonList(key), Arrays.asList(threadId, "30"));
// 释放锁
Object result = jedis.eval(luaScriptForDel, Collections.singletonList(key), Collections.singletonList(threadId));
使用 Lua 脚本可以确保分布式锁的关键操作在 Redis 中实现原子性,避免了由于非原子性操作导致的死锁和误删锁等并发问题,从而提升系统的可靠性。
超时自动解锁的问题虽然在某些场景下不可避免,但可以通过一些机制来缓解,比如延长 TTL 或者增加锁续期机制。
在分布式锁的使用中,如果线程的执行时间超过了锁的 TTL(过期时间),锁会自动释放,这时其他线程可能会获取到锁,而原线程还未执行完毕,可能导致数据不一致或业务逻辑错误。
延长 TTL: 可以通过将 TTL 设置得足够长来避免这种情况,但这可能导致其他线程长时间等待锁,特别是在发生意外宕机时,下一个线程将会阻塞很长时间,这并不优雅。
为了更优雅地解决这个问题,可以给获取锁的线程单独开一个守护线程,检测当前线程的运行情况。当发现锁的 TTL 即将到期时,守护线程可以自动为该锁续期,从而保证业务逻辑能够顺利执行完毕。

PEXPIRE 命令,延长锁的有效时间。// 获取锁并启动守护线程
if (set(key, threadId, "NX", "EX", 30)) {
// 启动守护线程
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
if (threadId.equals(get(key))) {
expire(key, 30); // 续期锁
}
}, 25, 25, TimeUnit.SECONDS);
try {
// 执行业务逻辑
} finally {
if (threadId.equals(get(key))) {
del(key); // 释放锁
}
scheduler.shutdown(); // 停止守护线程
}
}
通过为分布式锁增加一个守护线程来实现锁续期机制,可以避免由于线程阻塞导致的超时自动解锁问题,从而确保业务逻辑能够完整执行。这种方法比简单延长 TTL 更加优雅和灵活。
分布式锁确保在分布式环境中,某个共享资源在同一时刻只能被一个节点或线程访问,避免了传统本地锁在多节点环境中的同步问题。分布式锁通常由外部组件(如 Redis、Zookeeper)实现,这些组件提供了高可用的锁服务,确保锁在节点故障或网络分区情况下的可靠性。
常见实现方式:
关键特点:
通过选择合适的分布式锁实现方式,可以有效提升系统的可靠性和一致性,确保业务逻辑的正确执行。在实际应用中,需要根据具体场景选择合适的实现方式,并进行适当的优化和调整,以应对分布式环境下的复杂挑战。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online