
目录一、简单了解分布式锁一分布式锁应对分布式环境的同步挑战二分布式锁的实现方式三分布式锁的使用场景四分布式锁需满足的特点二、Redis 实现分布式锁的基本思路粗糙实现版本一实现步骤二基本代码展示三上述实现的缺陷三、健壮分布式锁聚焦一误删问题的分析问题说明解决方案具体实现步骤具体代码实现二原子性保证问题场景解决方案使用 Lua 脚本设置锁并设置过期时间原子操作释放锁原子操作Java 调用 Lua 脚本三超时自动解锁问题描述传统解决方案改进方案锁续期机制具体实现步骤Java 实现示例注意事项四、总结随着系统架构逐渐从单机走向分布式如何在分布式环境下保证线程同步执行成为一个不可忽视的问题。分布式锁作为解决这一问题的关键技术为分布式系统中的资源共享和任务协调提供了重要支持。选择合适的分布式锁实现方式可以有效提高系统的可靠性和一致性确保业务逻辑的正确执行。历 史相关文章回顾谈谈Redis分布式锁分布式锁的几种简单实现方式分析RedLock 与 Redisson 实现分布式锁一、简单了解分布式锁在多线程环境下为了保证同一时间只有一个线程能够执行某段代码Java 提供了synchronized关键字和ReentrantLock类作为本地锁的解决方案。这些机制在单个应用或单个 JVM 实例中运行良好确保了同一进程内的线程同步。但是随着分布式架构的广泛应用应用程序通常运行在多个节点上并且每个节点都有多个线程同时处理任务。在这种情况下传统的本地锁机制已经无法满足分布式环境下的同步需求。一分布式锁应对分布式环境的同步挑战在分布式系统中应用程序可能运行在多个物理或虚拟的节点上这意味着相同的资源可能会被不同节点上的多个线程同时访问。为了确保这些线程在不同节点上同步执行防止资源竞争和数据不一致问题我们需要使用一种能够跨节点的同步机制——分布式锁。分布式锁是一种用于控制在分布式环境中某个共享资源在同一时刻只能被一个节点或线程使用的机制。它类似于传统的本地锁但具有跨节点的协调能力。分布式锁通常由外部的分布式系统组件如 Redis、Zookeeper、Tair 等来实现这些组件提供了高可用的锁服务确保即使在节点故障或网络分区的情况下锁的状态依然能够保持一致。二分布式锁的实现方式分布式锁可以通过多种方式实现每种方式都有其适用的场景和优缺点。以下是几种常见的分布式锁实现方式简单直接的实现方式见分布式锁的几种简单实现方式分析基于 Redis 的分布式锁Redis 是一种常用的内存数据库可以通过SETNX命令Set if Not Exists来实现分布式锁。Redis 锁具有高性能、低延迟的优点适用于大部分需要快速锁定的场景。通过设置锁的过期时间可以防止死锁问题。基于 Zookeeper 的分布式锁Zookeeper 是一个分布式协调服务提供了严格的一致性保证。它通过创建临时有序节点实现分布式锁。Zookeeper 锁的优点是可靠性高适用于对数据一致性要求高的场景如分布式事务。基于数据库的分布式锁可以利用数据库的行级锁来实现分布式锁通过在数据库表中插入一条记录或更新记录的状态来表示加锁。虽然这种方式实现简单但性能较低适用于锁争用不激烈的场景。基于 Tair 的分布式锁Tair 是一种高性能分布式缓存系统也支持分布式锁功能适用于需要高并发和高可用的场景。三分布式锁的使用场景分布式锁在分布式系统中有广泛的应用典型的使用场景如分布式任务调度确保某个任务在某个时间点只由一个节点执行防止重复调度。分布式事务控制在多服务参与的分布式事务中确保事务的各个阶段按照预定顺序执行。资源竞争防止多个节点同时修改相同的资源如数据库记录、缓存数据导致的数据不一致问题。四分布式锁需满足的特点特点描述互斥性确保同一时刻只有一个线程能持有锁防止多个节点或线程对共享资源的并发访问保证资源的独占使用。可重入性允许同一节点上的同一个线程在已持有锁的情况下能够再次成功获取该锁避免锁重入时产生死锁。锁超时通过为锁设置过期时间防止因线程异常或故障未释放锁而导致的死锁情况确保系统的稳定性和健壮性。高性能与高可用性锁的加锁与解锁操作需要高效以满足高并发需求并且要确保在节点故障或网络分区等情况下锁服务依然可用保障系统的持续运行。阻塞与非阻塞性支持锁的阻塞和非阻塞模式。在阻塞模式下线程在锁不可用时等待锁的释放并在锁可用时及时被唤醒在非阻塞模式下线程可以立即返回继续执行其他逻辑。可扩展性锁机制能够随着系统规模的增长而扩展支持更多节点和更高并发量保持系统的性能和可靠性。二、Redis 实现分布式锁的基本思路粗糙实现版本Redis 是一个高性能的键值存储系统适合用于实现分布式锁因为它能够在高并发的场景下提供快速的读写操作。借助 Redis 的SET命令及其NX不存在则插入参数我们可以构建一个简单的分布式锁机制。一实现步骤获取锁通过SET key value NX EX seconds命令尝试获取锁。如果key不存在则插入成功并设置过期时间EX参数表示锁定成功如果key已存在则表示锁已经被其他客户端持有获取锁失败。解锁当持有锁的线程完成任务后可以通过DEL key命令删除该key来释放锁从而让其他等待锁的线程有机会获得锁。防止死锁为了防止死锁在获取锁时设置一个合理的过期时间TTL即使由于程序异常未能显式释放锁锁也会在 TTL 到期后自动释放。二基本代码展示// 尝试获取锁 if (set(key, 1, NX, EX, 30)) { try { // 执行需要加锁的业务逻辑 } finally { // 释放锁 del(key); } }三上述实现的缺陷尽管这种方法简单易用但它存在几个严重的问题使得其无法成为一个健壮的分布式锁实现非原子性操作锁的获取与锁的过期时间设置不是原子操作。假设在SETNX成功后但在设置过期时间之前程序崩溃或出现异常那么锁将一直存在导致其他线程无法获取锁从而产生死锁。锁误解除当持有锁的线程被阻塞或出现延迟锁的过期时间到期后自动释放此时如果有其他线程获取了同一个锁原本持有锁的线程执行完毕后仍然会执行DEL操作从而误解锁破坏了其他线程的业务逻辑。业务超时自动解锁导致并发问题由于业务执行时间不确定如果锁的 TTL 到期锁会自动释放可能导致多个线程同时执行临界区代码从而引发并发问题。不可重入性该实现不支持可重入性即同一线程无法多次获得同一把锁。如果线程因递归或重复调用需要再次获取锁会因为锁已经存在而获取失败。三、健壮分布式锁聚焦一误删问题的分析问题说明在分布式锁的实现中存在一种潜在的风险即线程在解锁时误删了其他线程持有的锁。具体情况如下线程1持有锁线程1成功获取了锁并执行了一段业务逻辑。线程1阻塞线程1在执行过程中由于某种原因被阻塞未能及时释放锁导致锁的TTL过期时间到期锁自动释放。线程2获取锁此时线程2尝试获取锁并成功获得了已经释放的锁。线程1解除阻塞线程1解除阻塞继续执行并尝试释放锁。误删锁由于线程1并不知道锁已经由线程2重新获取因此直接执行DEL操作误删了属于线程2的锁。解决方案为了解决上述问题可以在锁中存储一个唯一标识符例如线程ID或UUID并在释放锁时检查该标识符是否匹配从而确保只有持有锁的线程才能成功释放锁。具体实现步骤获取锁时存储标识符在获取锁时使用 Redis 的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 脚本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 调用 Lua 脚本通过 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 即将到期时守护线程可以自动为该锁续期从而保证业务逻辑能够顺利执行完毕。具体实现步骤启动守护线程在获取锁后启动一个守护线程定期检查锁的 TTL 是否即将过期。续期机制守护线程在锁即将过期时自动向 Redis 发送PEXPIRE命令延长锁的有效时间。停止守护线程在业务逻辑执行完毕并释放锁后守护线程应该被及时停止避免不必要的资源消耗。Java 实现示例// 获取锁并启动守护线程 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例如在 TTL 为 30 秒时每 25 秒续期一次。可靠性确保守护线程可靠地执行续期操作避免续期失败导致锁过期。通过为分布式锁增加一个守护线程来实现锁续期机制可以避免由于线程阻塞导致的超时自动解锁问题从而确保业务逻辑能够完整执行。这种方法比简单延长 TTL 更加优雅和灵活。四、总结分布式锁确保在分布式环境中某个共享资源在同一时刻只能被一个节点或线程访问避免了传统本地锁在多节点环境中的同步问题。分布式锁通常由外部组件如 Redis、Zookeeper实现这些组件提供了高可用的锁服务确保锁在节点故障或网络分区情况下的可靠性。常见实现方式:简单实现利用 Redis 的 SET 命令和 NX不存在则插入参数可以快速实现分布式锁。然而这种实现存在误删和超时处理等问题。健壮实现通过使用唯一标识符和 Lua 脚本来确保操作的原子性可以解决误删和超时问题提高分布式锁的可靠性。Lua 脚本将锁的设置和释放操作封装为原子操作避免了非原子性操作带来的并发问题。看门狗机制通过监控锁的有效期并在锁即将过期时自动续期确保业务逻辑在锁的有效期内顺利执行避免了因锁超时导致的数据不一致问题。关键特点:互斥性、可重入性、锁超时、高性能与高可用性、阻塞与非阻塞性、可扩展性是分布式锁需要满足的基本特点。这些特点确保了锁的有效性和系统的稳定性。通过选择合适的分布式锁实现方式可以有效提升系统的可靠性和一致性确保业务逻辑的正确执行。在实际应用中需要根据具体场景选择合适的实现方式并进行适当的优化和调整以应对分布式环境下的复杂挑战。