excel做公司的小网站,最好的建设网站,建站工具箱 discuz,常州市做网站为什么我们需要分布式锁#xff1f;在深入探讨分布式锁的细节之前#xff0c;了解为什么需要它至关重要。与分布式锁相对的是“单机锁”。在编写多线程程序时#xff0c;为了防止因同时操作共享变量而导致的数据问题#xff0c;我们通常使用锁来实现互斥#xff0c;从而保…为什么我们需要分布式锁在深入探讨分布式锁的细节之前了解为什么需要它至关重要。与分布式锁相对的是“单机锁”。在编写多线程程序时为了防止因同时操作共享变量而导致的数据问题我们通常使用锁来实现互斥从而保证共享变量的正确性。然而这种锁的范围仅限于同一个进程内。如果多个进程需要同时操作共享资源怎么办如何实现互斥例如现代业务应用通常采用微服务架构这意味着一个应用可能部署在多个进程中。如果这多个进程需要修改 MySQL 中的同一行数据为了避免因操作无序导致的数据错误我们需要引入分布式锁来解决这个问题。要实现分布式锁我们必须依赖一个外部系统所有进程都向该系统请求“获取锁”。这个外部系统必须提供互斥性——也就是说如果两个请求同时到达它必须让一个进程成功而向另一个进程返回失败或者强制等待。这个外部系统可以是 MySQL、Redis 或 ZooKeeper。不过为了追求更好的性能我们通常会选择 Redis 或 ZooKeeper。下面我将以 Redis 为例深度分析分布式锁的各种“安全”问题帮助你从基本概念到进阶考量全方位地理解它们。如何实现分布式锁让我们从最简单的方法开始。要实现分布式锁Redis 必须支持互斥。我们可以使用SETNX命令代表 SET if Not eXists如果不存在则设置——意思是只有当 key 不存在时才设置 key 的值否则它什么也不做。这里我们要模拟两个客户端进程执行此命令以实现互斥从而实现一个基本的分布式锁。客户端 1 请求获取锁并成功127.0.0.1:6379 SETNX lock 1 (integer) 1 // 客户端 1获取锁成功客户端 2 请求获取锁但因为晚了一步而失败127.0.0.1:6379 SETNX lock 1 (integer) 0 // 客户端 2获取锁失败此时成功获取锁的客户端可以继续操作共享资源例如修改 MySQL 中的一行数据或调用 API。一旦操作完成必须及时释放锁以便允许其他客户端访问共享资源。如何释放锁呢非常简单——直接使用DEL命令删除这个 key127.0.0.1:6379 DEL lock (integer) 1这个逻辑非常简单整体流程如下获取锁 → 操作共享资源 → 释放锁然而这存在一个主要问题如果客户端 1 获取了锁随后遇到了以下场景之一就会导致死锁程序在处理业务逻辑时遇到异常没能及时释放锁。进程崩溃完全没有机会释放锁。在这些情况下该客户端将无限期地持有锁导致其他客户端永远无法获取锁。如何解决这个问题如何避免死锁一个直观的解决方案是在获取锁时设置一个“租期”。在 Redis 中这意味着给 key 设置一个过期时间。假设操作共享资源的时间不会超过 10 秒我们可以在获取锁时将 key 设置为 10 秒后过期127.0.0.1:6379 SETNX lock 1 (integer) 1 127.0.0.1:6379 EXPIRE lock 10 // 10秒后自动过期 (integer) 1这样无论客户端是否遇到异常锁都会在10 秒后自动释放允许其他客户端获取它。但这真的是万无一失的吗并不完全是。目前获取锁SETNX和设置过期时间EXPIRE是两条独立的命令。如果第一条命令成功了但第二条命令因为不可预见的问题执行失败了怎么办例如SETNX 成功但由于网络问题EXPIRE 失败。SETNX 成功但在 EXPIRE 执行之前 Redis突然宕机。SETNX 成功但在发出 EXPIRE 之前客户端崩溃。简而言之由于这两条命令不是原子的不能保证一起成功存在过期时间设置失败的风险从而导致同样的死锁问题。我们该如何修复这个问题在 Redis 2.6.12 之前开发者必须实现复杂的变通方案来确保 SETNX EXPIRE 的原子执行同时还要处理边缘情况。然而从 Redis 2.6.12 开始SET命令扩展了新的参数允许在一个操作中原子地完成锁的获取和过期时间的设置// 一条命令保证原子执行 127.0.0.1:6379 SET lock 1 EX 10 NX OK这以一种相对简单的方式解决了死锁问题。让我们进一步分析还可能存在什么其他问题考虑这种场景客户端 1 成功获取锁并开始操作共享资源。客户端 1 操作共享资源的时间“超过”了锁的过期时间导致锁被“自动释放”。客户端 2 随后成功获取了锁并开始操作共享资源。客户端 1 完成了对共享资源的操作并着手释放锁但实际上它释放的是目前由客户端 2 持有的锁。你看到这里面的两个严重问题了吗锁过期客户端 1 操作共享资源耗时太长导致锁自动过期随后被客户端 2 获取。释放了他人的锁客户端 1 操作完成后释放锁但实际上释放的是客户端 2 的锁。这两个问题的根源是什么让我们逐一检查。第一个问题可能是由于对操作共享资源所需时间的预估不准确造成的。例如该操作“最慢”可能需要 15 秒但我们只设置了 10 秒的过期时间这就造成了锁过早过期的风险。如果过期时间太短我们能不能直接增加缓冲时间比如把过期时间设为 20 秒这样够吗这确实可以“缓解”问题并降低发生的概率但仍然无法“彻底解决”问题。为什么原因在于客户端获得锁之后在操作共享资源时遇到的情况可能非常复杂比如内部程序异常、网络请求超时等。既然是“预估”就只能是一个近似计算。除非你能预测并覆盖所有导致延迟增加的场景但这实际上是非常困难的。有什么更好的解决方案吗别担心稍后我会详细讨论针对这个问题的解决方案。让我们继续看第二个问题。第二个问题在于一个客户端释放了另一个客户端持有的锁。想一想导致这个问题的关键点是什么关键点在于每个客户端在释放锁时执行的是“盲目”操作没有检查锁是否仍然“归自己所有”。这导致了释放他人锁的风险。这样的解锁过程太“粗心”了如何解决这个问题如果锁被别人释放了怎么办解决方案是当客户端获取锁时设置一个只有它自己知道的“唯一标识”。例如可以是它自己的线程 ID或者一个 UUID随机且唯一。这里我们用 UUID 举例// 设置锁的 VALUE 为 UUID 127.0.0.1:6379 SET lock $uuid EX 20 NX OK这里我们要假设 20 秒对于操作共享资源来说是完全足够的所以暂时不考虑锁自动过期的问题。随后在释放锁时我们需要先验证锁是否仍由我们自己持有。伪代码可以这样写// 只有是你自己的锁才能解锁 if redis.get(lock) $uuid: redis.del(lock)这里释放锁涉及使用 GET DEL 命令这又引入了我们之前讨论过的原子性问题。客户端 1 执行GET并确认锁属于自己。客户端 2 执行SET强制获取锁虽然不太可能但我们需要严谨地考虑锁的安全性。客户端 1 执行DEL意外释放了客户端 2 的锁。这表明这两条命令必须原子执行。如何实现原子执行使用 Lua 脚本。我们可以将这个逻辑封装在一个 Lua 脚本中让 Redis 来执行它。由于 Redis 是单线程处理请求的在执行 Lua 脚本时其他请求必须等待脚本完成。这确保了在GET和DEL之间不会插入其他命令。这是用于安全释放锁的 Lua 脚本// 只有锁属于你时才释放 if redis.call(GET,KEYS[1]) ARGV[1] then return redis.call(DEL,KEYS[1]) else return 0 end太棒了。经过这一系列的优化整个加锁和解锁的过程变得更加“严谨”了。让我们在这里总结一下。对于基于 Redis 的分布式锁实现一个严谨的流程如下获取锁SET $lock_key $unique_id EX $expire_time NX操作共享资源。释放锁使用 Lua 脚本先GET检查锁是否属于当前持有者然后DEL释放它。很好。现在我们有了这个完整的锁模型让我们回到前面提到的第一个问题。如果很难评估锁的过期时间怎么办锁过期时间很难估算怎么办前面我们提到如果锁的过期时间估算不当存在锁“过早”过期的风险。当时给出的折中方案是尽可能让过期时间“富余”以降低锁过早过期的概率。但这并不能完美解决问题。那么我们要怎么做呢能不能设计这样一个方案在获取锁时先设置一个过期时间然后启动一个“守护线程”定期检查锁的剩余时间。如果锁快要过期了而对共享资源的操作还没完成这个线程就自动给锁“续期”重置过期时间。这确实是一个更好的方案。如果你使用的是 Java 技术栈幸运的是已经有一个库封装了所有这些工作Redisson。Redisson 是一个 Java 实现的 Redis SDK 客户端。在使用分布式锁时它采用“自动续期”的方式来防止锁过期。这个守护线程通常被称为“看门狗Watchdog”线程。此外这个 SDK 封装了许多用户友好的功能可重入锁乐观锁公平锁读写锁Redlock稍后介绍……这个 SDK 提供的 API 非常友好允许你像操作本地锁一样操作分布式锁。这里重点不是介绍 Redisson 的用法。你可以参考官方文档学习如何使用它。现在让我们总结一下前面遇到的 Redis 分布式锁问题及其对应的解决方案死锁设置过期时间。过期时间预估不足导致锁过早过期使用守护线程进行自动续期。锁被他人释放在锁中嵌入唯一标识并在释放前验证所有权。还有其他可能破坏 Redis 锁安全性的场景吗目前分析的问题都假设锁是在单个 Redis 实例上运行的不涉及 Redis 的部署架构细节。实际上Redis 通常采用主从集群 Sentinel哨兵模式部署。这样做的好处是如果主节点Master意外故障Sentinel 可以执行自动故障转移将从节点Slave提升为主节点以确保可用性。但是当发生“主从切换”时这个分布式锁还安全吗考虑这种场景客户端 1 在主节点上执行SET命令并成功获取锁。此时主节点意外崩溃而SET命令尚未同步到从节点主从复制是异步的。从节点被 Sentinel 提升为新的主节点而新主节点上丢失了这把锁显然当引入 Redis 复制时分布式锁仍可能受到影响。如何解决这个问题为了解决这个问题Redis 的作者提出了一个解决方案也就是我们经常听到的Redlock红锁。它真的能解决上述问题吗