当前位置: 首页 > news >正文

前端做项目的网站资源公司做网站的步骤

前端做项目的网站资源,公司做网站的步骤,做二维码推送网站,如皋网站开发公司文章目录 前言一、为什么需要分布式锁#xff1f;二、基于 Redis 分布式锁怎么实现#xff1f;三、Redis 分布锁存在的问题3.1 死锁问题3.2 锁过期时间问题3.3 锁被别人释放问题 四、Redis 分布锁小结五、Redis 主从同步对分布式锁的影响六、Redlock 方案七、Redlock 的争论7… 文章目录 前言一、为什么需要分布式锁二、基于 Redis 分布式锁怎么实现三、Redis 分布锁存在的问题3.1 死锁问题3.2 锁过期时间问题3.3 锁被别人释放问题 四、Redis 分布锁小结五、Redis 主从同步对分布式锁的影响六、Redlock 方案七、Redlock 的争论7.1 分布式专家 Martin 对于 Redlock 的质疑7.2 Redis 作者 Antirez 的反驳 八、基于 ZooKeeper 的分布式锁九、基于 Etcd 的分布式锁十、我对分布式锁的理解十一、总结十二、后记 前言 高并发业务场景下部署在不同机器上的业务进程如果需要同时操作共享资源为了避免「时序性」问题通常会借助 Redis 的分布式锁来做互斥以保证业务的正确性。 基于 Redis 实现的分布式锁虽然足够简单但这把小小的分布锁究竟安全吗有没有可能会失效 基于 Redis 如何实现一个分布式锁它足够安全吗一个严谨的分布式锁模型如何实现应该考虑什么我们经常听到的 Redlock 有什么问题业界争论 Redlock到底在争论什么哪种观点是对的分布式锁到底用 Redis 还是 ZooKeeper、Etcd 这些问题你能清晰回答上来吗 读完这篇文章你不仅可以彻底了解分布式锁还会对「分布式系统」有更加深刻的理解。 文章有点长但干货很多希望你可以耐心读完。 一、为什么需要分布式锁 在开始讲分布式锁之前有必要简单介绍一下为什么需要分布式锁 与分布式锁相对应的是「单机锁」当我们在写多线程程序时为了避免同时操作进程中的全局变量通常会使用一把锁来「互斥」以保证全局变量的正确性。 又或者当我们在同一台机器的不同进程想要同时操作一个共享资源例如修改同一个文件我们可以使用操作系统提供的「文件锁」或「信号量」来做互斥。 你可以看到这些互斥操作都仅限于线程、进程处于同一台机器上如果是分布在「不同机器」上的不同进程要同时操作一个共享资源例如修改数据库的某一行如何互斥呢 此时我们就需要引入「分布式锁」来解决这个问题了。 想要实现分布式锁必须借助一个外部系统所有进程都去这个系统上申请「加锁」。 而这个外部系统必须要实现「互斥」的能力即两个请求同时进来只会给一个进程返回成功另一个返回失败或等待。 这个外部系统可以是 MySQL也可以是 Redis、ZooKeeper、Etcd。但在高并发业务场景下为了追求更好的性能我们通常会选择使用 Redis。 下面我就以 Redis 为主线由浅入深带你深度剖析一下分布式锁的各种「安全性」问题帮你彻底理解分布式锁。 在问题分析的过程中你还会看到分布锁在「分布式系统」下可能会遇到的疑难问题感受分布式系统的复杂性。 二、基于 Redis 分布式锁怎么实现 我们从最简单的开始讲起。 想要实现分布式锁我们依赖 Redis 的「互斥」的能力我们可以使用 SETNX 命令这个命令表示SET if Not eXists即如果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 命令删除这个 key 即可 127.0.0.1:6379 DEL lock// 释放锁(integer) 1这个逻辑非常简单整体的路程就是这样 但是它存在一个很大的问题当客户端 1 拿到锁后如果发生下面的场景就会造成「死锁」 程序处理业务逻辑异常没及时释放锁进程挂了没机会释放锁。 这时这个客户端就会一直占用这个锁而其它客户端就「永远」拿不到这把锁了锁饥饿。 怎么解决这个问题呢 三、Redis 分布锁存在的问题 3.1 死锁问题 对于第 1 种情况程序在处理业务逻辑时发生异常没及时释放锁通常我们需要对这块业务代码加上异常处理保证无论业务逻辑是否异常都可以把锁释放掉例如在 Go 的 defer、Java/Python 的 finally 中及时释放锁 Godefer redis.del(key) Javatry … catch … fianlly: redis.del(key) Pythontry … except … fianlly: redis.del(key) 这个取决于你的业务代码的「健壮性」比较容易解决。 对于第 2 种情况进程挂了没机会释放锁我们很容易想到的方案是在申请锁时给这把锁设置一个「租期」。 在 Redis 中实现时就是给这个 key 设置一个「过期时间」。 这里我们假设操作共享资源的时间不会超过 10s那么在加锁时给这个 key 设置 10s 过期即可 127.0.0.1:6379 SETNX lock 1 // 加锁 (integer) 1 127.0.0.1:6379 EXPIRE lock 10 // 10s后自动过期 (integer) 1这样一来无论客户端是否异常这个锁都可以在 10s 后被「自动释放」其它客户端依旧可以拿到锁。但这样真的没问题吗 还是有问题。现在的操作加锁、设置过期是 2 条命令有没有可能只执行了第一条第二条却「来不及」执行的情况发生呢例如 SETNX 执行成功执行 EXPIRE 时由于网络问题执行失败 SETNX 执行成功Redis 异常宕机EXPIRE 没有机会执行 SETNX 执行成功客户端异常崩溃EXPIRE 也没有机会执行。 总之这两条命令不能保证是原子操作一起成功就有潜在的风险导致过期时间设置失败依旧发生「死锁」问题。 怎么办在 Redis 2.6.12 版本之前我们需要想尽办法保证 SETNX 和 EXPIRE 原子性执行还要考虑各种异常情况如何处理。 但在 Redis 2.6.12 之后Redis 扩展了 SET 命令的参数把 NX/EX 集成到了 SET 命令中用这一条命令就可以了 // 一条命令保证原子性执行 127.0.0.1:6379 SET lock 1 EX 10 NX OK这样就解决了原子性问题。我们再来看分析下它还有什么问题 试想这样一种场景 客户端 1 加锁成功开始操作共享资源 客户端 1 操作共享资源的时间「超过」了锁的过期时间锁被「自动释放」 客户端 2 加锁成功开始操作共享资源 客户端 1 操作共享资源完成释放锁但释放的是客户端 2 的锁。 看到了么这里存在两个严重的问题 锁过期客户端 1 操作共享资源耗时太久导致锁被自动释放之后被客户端 2 持有 释放别人的锁客户端 1 操作共享资源完成后却又释放了客户端 2 的锁。 导致这两个问题的原因是什么我们一个个来看。 3.2 锁过期时间问题 第一个问题可能是我们评估操作共享资源的时间不准确导致的。 例如操作共享资源的时间「最慢」可能需要 15s而我们却只设置了 10s 过期那这就存在锁提前过期的风险。 过期时间太短那增大冗余时间例如设置过期时间为 20s这样总可以了吧 这样确实可以「缓解」这个问题降低出问题的概率但依旧无法「彻底解决」问题。 为什么原因在于客户端在拿到锁之后在操作共享资源时遇到的场景有可能是很复杂的例如程序内部发生异常、网络请求超时等等。 既然是「预估」时间也只能是大致计算除非你能预料并覆盖到所有导致耗时变长的场景但这其实很难很多时候我们都是凭「感觉」设置过期时间不太靠谱。 有什么更好的解决方案吗我们可以考虑设计这样的方案加锁时先设置一个过期时间然后我们开启一个「守护线程」定时去检测这个锁的失效时间如果锁快要过期了操作共享资源还未完成那么就自动对锁进行「续期」重新设置过期时间。 担心锁过期我们主动启动一个线程定时给锁续期避免还未操作完共享资源锁就自动过期被释放。 这确实是一个比较好的思路。如果你是 Java 技术栈幸运的是已经有一个库把这些工作都封装好了Redisson。 Redisson 是一个 Java 语言实现的 Redis SDK 客户端在使用分布式锁时它就采用了「自动续期」的方案来避免锁过期这个守护线程我们一般也把它叫做「看门狗」线程。 除此之外这个 SDK 还封装了很多易用的功能 可重入锁 乐观锁 公平锁 读写锁 Redlock红锁下面会详细讲。 这个 SDK 提供的 API 非常友好它可以像操作本地锁的方式操作分布式锁。如果你是 Java 技术栈可以直接把它用起来。 这里不重点介绍 Redisson 的使用你可以看官方 Github 学习如何使用比较简单。 3.3 锁被别人释放问题 我们再来看上面提到的第二个问题这个问题在于一个客户端释放了其它客户端持有的锁。 想一下导致这个问题的关键点在哪 重点在于每个客户端在释放锁时都是「无脑」操作并没有检查这把锁是否还「归自己持有」所以就会发生释放别人锁的风险这样的解锁流程很不「严谨」 如何解决这个问题呢解决办法是客户端在加锁时设置一个只有自己知道的「唯一标识」进去。 例如可以是自己的线程 ID也可以是一个 UUID随机且唯一这里我们以 UUID 举 例 // 锁的VALUE设置为UUID 127.0.0.1:6379 SET lock $uuid EX 20 NX OK之后在释放锁时要先判断这把锁是否还归自己持有伪代码可以这么写 // 锁是自己的才释放 if redis.get(lock) $uuid:redis.del(lock)这里释放锁使用的是 GET DEL 两条命令这时又会遇到我们前面讲的原子性问题了。 客户端 1 执行 GET判断锁是自己的 客户端 1 执行 GET 结束后这个锁刚好超时自动释放 此时恰好客户端 2 又获取到了这个锁 之后客户端 1 在执行 DEL 时释放的却是客户端2的锁冲突。 由此可见这两个命令还是必须要原子执行才行。怎样原子执行呢 Lua 脚本。我们可以把这个逻辑写成 Lua 脚本让 Redis 来执行。因为 Redis 处理每一个请求是「单线程」执行的在执行一个 Lua 脚本时其它请求必须等待直到这个 Lua 脚本处理完成这样一来GET DEL 之间就不会插入其它命令了。 安全释放锁的 Lua 脚本如下 // 判断锁是自己的才释放 if redis.call(GET,KEYS[1]) ARGV[1] thenreturn redis.call(DEL,KEYS[1]) elsereturn 0 end四、Redis 分布锁小结 好这样一路优化整个的加锁、解锁的流程就更「严谨」了。 这里我们先小结一下基于 Redis 实现的分布式锁一个严谨的的流程如下 加锁SET $lock_key $unique_id EX $expire_time NX 操作共享资源没操作完之前开启守护线程定期给锁续期 释放锁Lua 脚本先 GET 判断锁是否归属自己再 DEL 释放锁。 简化版代码 对于大部分业务场景其实不必考虑上面提到的各种问题那么下面的代码基本就够啦。 package mainimport (contextgithub.com/go-redis/redis/v8time )var redisClient *redis.Clientfunc init() {redisClient redis.NewClient(redis.Options{Addr: localhost:6379,Password: ,DB: 0,}) }func acquireLock(lockKey string, expiration time.Duration) bool {ctx : context.Background()result, err : redisClient.SetNX(ctx, lockKey, locked, expiration).Result()if err ! nil {panic(err)}return result }func releaseLock(lockKey string) {ctx : context.Background()redisClient.Del(ctx, lockKey) }func main() {lockKey : my_lock_keylocked : acquireLock(lockKey, 10*time.Second)if locked {defer releaseLock(lockKey)// 执行业务逻辑} } 每个问题的解决方案 死锁给锁设置租期过期时间 过期时间评估不好锁提前过期守护线程定时续期 锁被别人释放锁写入唯一标识释放锁先检查标识再释放。 还有哪些问题场景会危害 Redis 锁的安全性呢 五、Redis 主从同步对分布式锁的影响 我们之前分析的场景都是锁在「单个」Redis 实例中可能产生的问题并没有涉及到 Redis 的部署架构细节。 而我们在使用 Redis 时一般会采用主从集群 哨兵的模式部署这样做的好处在于当主库异常宕机时哨兵可以实现「故障自动切换」把从库提升为主库继续提供服务以此保证可用性。 那当「主从发生切换」时这个分布锁会依旧安全吗 试想这样的场景 客户端 1 在主库上执行 SET 命令加锁成功 此时主库异常宕机SET 命令还未同步到从库上主从复制是异步的 从库被哨兵提升为新主库这个锁在新的主库上丢失了 可见当引入 Redis 副本后分布锁还是可能会受到影响。怎么解决这个问题 为此Redis 的作者提出一种解决方案就是我们经常听到的 Redlock红锁。它真的可以解决上面这个问题吗 六、Redlock 方案 好终于到了这篇文章的重头戏。啊上面讲的那么多问题难道只是基础 是的那些只是开胃菜真正的硬菜从这里刚刚开始。如果上面讲的内容你还没有理解我建议你重新阅读一遍先理清整个加锁、解锁的基本流程。 如果你已经对 Redlock 有所了解这里可以跟着我再复习一遍如果你不了解 Redlock没关系我会带你重新认识它。 值得注意的是后面我不仅会讲 Redlock 的原理还会引出有关「分布式系统」中的很多问题你最好跟紧我的思路在脑中一起分析问题的答案。 现在我们来看Redis 作者提出的 Redlock 方案是如何解决主从切换后锁失效问题的。 Redlock 的方案基于 2 个前提 不再需要部署从库和哨兵实例只部署主库 但主库要部署多个官方推荐至少 5 个实例 也就是说想用使用 Redlock你至少要部署 5 个 Redis 实例而且都是主库它们之间没有任何关系都是一个个孤立的实例。 注意不是部署 Redis Cluster就是部署 5 个简单的 Redis 实例。 Redlock 具体如何使用呢 整体的流程是这样的一共分为 5 步 客户端先获取「当前时间戳T1」 客户端依次向这 5 个 Redis 实例发起加锁请求用前面讲到的 SET 命令且每个请求会设置超时时间毫秒级要远小于锁的有效时间如果某一个实例加锁失败包括网络超时、锁被其它人持有等各种异常情况就立即向下一个 Redis 实例申请加锁 如果客户端从 3 个大多数以上 Redis 实例加锁成功则再次获取「当前时间戳T2」如果 T2 - T1 锁的过期时间此时认为客户端加锁成功否则认为加锁失败 加锁成功去操作共享资源例如修改 MySQL 某一行或发起一个 API 请求 加锁失败向「全部节点」发起释放锁请求前面讲到的 Lua 脚本释放锁。 我简单帮你总结一下有 4个重点 客户端在多个 Redis 实例上申请加锁 必须保证大多数节点加锁成功 大多数节点加锁的总耗时要小于锁设置的过期时间 释放锁要向全部节点发起释放锁请求。 第一次看可能不太容易理解建议你把上面的文字多看几遍加深记忆。 然后记住这 5 步非常重要下面会根据这个流程剖析各种可能导致锁失效的问题假设。 好明白了 Redlock 的流程我们来看 Redlock 为什么要这么做。 1) 为什么要在多个实例上加锁 本质上是为了「容错」部分实例异常宕机剩余的实例加锁成功整个锁服务依旧可用。 2) 为什么大多数加锁成功才算成功 多个Redis实例一起来用其实就组成了一个「分布式系统」。 在分布式系统中总会出现「异常节点」所以在谈论分布式系统问题时需要考虑异常节点达到多少个也依旧不会影响整个系统的「正确性」。 这是一个分布式系统「容错」问题这个问题的结论是如果只存在「故障」节点只要大多数节点正常那么整个系统依旧是可以提供正确服务的。 这个问题的模型就是我们经常听到的「拜占庭将军」问题感兴趣可以去看算法的推演过程。 3) 为什么步骤 3 加锁成功后还要计算加锁的累计耗时 因为操作的是多个节点所以耗时肯定会比操作单个实例耗时更久而且因为是网络请求网络情况是复杂的有可能存在延迟、丢包、超时等情况发生网络请求越多异常发生的概率就越大。 所以即使大多数节点加锁成功但如果加锁的累计耗时已经「超过」了锁的过期时间那此时有些实例上的锁可能已经失效了这个锁就没有意义了。 4) 为什么释放锁要操作所有节点 在某一个 Redis 节点加锁时可能因为「网络原因」导致加锁失败。 例如客户端在一个 Redis 实例上加锁成功但在读取响应结果时网络问题导致读取失败那这把锁其实已经在 Redis 上加锁成功了。 所以释放锁时不管之前有没有加锁成功需要释放「所有节点」的锁以保证清理节点上「残留」的锁。 好了明白了 Redlock 的流程和相关问题看似 Redlock 确实解决了 Redis 节点异常宕机锁失效的问题保证了锁的「安全性」。但事实真的如此吗 七、Redlock 的争论 Redis 作者把这个方案一经提出就马上受到业界著名的分布式系统专家的质疑 这个专家叫 Martin是英国剑桥大学的一名分布式系统研究员。在此之前他曾是软件工程师和企业家从事大规模数据基础设施相关的工作。他还经常在大会做演讲写博客、写书、也是开源贡献者。 他写的这本分布式系统领域的书《数据密集型应用系统设计》豆瓣评分高达 9.7好评如潮。 他马上写了篇文章质疑这个 Redlock 的算法模型是有问题的并对分布式锁的设计提出了自己的看法。 之后Redis 作者 Antirez 面对质疑不甘示弱也写了一篇文章反驳了对方的观点并详细剖析了 Redlock 算法模型的更多设计细节。 而且关于这个问题的争论在当时互联网上也引起了非常激烈的讨论。 二人思路清晰论据充分这是一场高手过招也是分布式系统领域非常好的一次思想的碰撞双方都是分布式系统领域的专家却对同一个问题提出很多相反的论断究竟是怎么回事 下面我会从他们的争论文章中提取重要的观点和精华整理呈现给你。 提醒后面的信息量极大可能不宜理解最好放慢速度阅读。 7.1 分布式专家 Martin 对于 Redlock 的质疑 在他的文章中主要阐述了 4 个论点 1) 分布式锁的目的是什么 Martin 表示你必须先清楚你在使用分布式锁的目的是什么他认为有两个目的。 第一效率。 使用分布式锁的互斥能力是避免不必要地做同样的两次工作例如一些昂贵的计算任务。如果锁失效并不会带来「恶性」的后果例如发了 2 次邮件等无伤大雅。 第二正确性。 使用锁用来防止并发进程互相干扰。如果锁失效会造成多个进程同时操作同一条数据产生的后果是数据严重错误、永久性不一致、数据丢失等恶性问题就像给患者服用重复剂量的药物一样后果严重。 他认为如果你是为了前者——效率那么使用单机版 Redis 就可以了即使偶尔发生锁失效宕机、主从切换都不会产生严重的后果。而使用 Redlock 太重了没必要。 而如果是为了正确性Martin 认为 Redlock 根本达不到安全性的要求也依旧存在锁失效的问题 2) 锁在分布式系统中会遇到的问题 Martin 表示一个分布式系统更像一个复杂的「野兽」存在着你想不到的各种异常情况。 这些异常场景主要包括三大块这也是分布式系统会遇到的三座大山NPC。 NNetwork Delay网络延迟 PProcess Pause进程暂停比如GC CClock Drift时钟漂移。 Martin 用一个进程暂停GC的例子指出了 Redlock 安全性问题 客户端 1 请求锁定节点 A、B、C、D、E 客户端 1 的拿到锁后进入 GC时间比较久 所有 Redis 节点上的锁都过期了 客户端 2 获取到了 A、B、C、D、E 上的锁 客户端 1 GC 结束认为成功获取锁 客户端 2 也认为获取到了锁发生「冲突」。 Martin 认为GC 可能发生在程序的任意时刻而且执行时间是不可控的。 注当然即使是使用没有 GC 的编程语言在发生网络延迟时也都有可能导致 Redlock 出现问题这里 Martin 只是拿 GC 举例。 3) 假设时钟正确的是不合理的 又或者当多个 Redis 节点「时钟」发生问题时也会导致 Redlock 锁失效。 客户端 1 获取节点 A、B、C 上的锁但由于网络问题无法访问 D 和 E 节点 C 上的时钟「向前跳跃」导致锁到期(从而其他客户端可以获取C上的锁了 客户端 2 获取节点 C、D、E 上的锁由于网络问题无法访问 A 和 B 客户端 1 和 2 现在都相信它们持有了锁冲突 Martin 觉得Redlock 必须「强依赖」多个节点的时钟是保持同步的一旦有节点时钟发生错误那这个算法模型就失效了。 即使 C 不是时钟跳跃而是「崩溃后立即重启」也会发生类似的问题。 Martin 继续阐述机器的时钟发生错误是很有可能发生的 系统管理员「手动修改」了机器时钟 机器时钟在同步 NTP 时间时发生了大的「跳跃」 总之Martin 认为Redlock 的算法是建立在「同步模型」基础上的有大量资料研究表明同步模型的假设在分布式系统中是有问题的。 在混乱的分布式系统的中你不能假设系统时钟就是对的所以你必须非常小心你的假设。 4) 提出 fencing token 的方案保证正确性 相对应的Martin 提出一种被叫作 fencing token 的方案保证分布式锁的正确性。 这个模型流程如下 客户端在获取锁时锁服务可以提供一个「递增」的 token 客户端拿着这个 token 去操作共享资源 共享资源可以根据 token 拒绝「后来者」的请求 这样一来无论 NPC 哪种异常情况发生都可以保证分布式锁的安全性因为它是建立在「异步模型」上的。 而 Redlock 无法提供类似 fencing token 的方案所以它无法保证安全性。 他还表示一个好的分布式锁无论 NPC 怎么发生可以不在规定时间内给出结果但并不会给出一个错误的结果。也就是只会影响到锁的「性能」或称之为活性而不会影响它的「正确性」。 Martin 的结论 1、Redlock 不伦不类它对于效率来讲Redlock 比较重没必要这么做而对于正确性来说Redlock 是不够安全的。 2、时钟假设不合理该算法对系统时钟做出了危险的假设假设多个节点机器时钟都是一致的如果不满足这些假设锁就会失效。 3、无法保证正确性Redlock不能提供类似 fencing token 的方案所以解决不了正确性的问题。为了正确性请使用有「共识系统」的软件例如 ZooKeeper。 好了以上就是 Martin 反对使用 Redlock 的观点看起来有理有据。 下面我们来看 Redis 作者 Antirez 是如何反驳的。 7.2 Redis 作者 Antirez 的反驳 在 Redis 作者的文章中重点有 3 个 1) 解释时钟问题 首先Redis 作者一眼就看穿了对方提出的最为核心的问题时钟问题。 Redis 作者表示Redlock 并不需要完全一致的时钟只需要大体一致就可以了允许有「误差」。 例如要计时 5s但实际可能记了 4.5s之后又记了 5.5s有一定误差但只要不超过「误差范围」锁失效时间即可这种对于时钟的精度的要求并不是很高而且这也符合现实环境。 对于对方提到的「时钟修改」问题Redis 作者反驳到 手动修改时钟不要这么做就好了否则你直接修改 Raft 日志那 Raft 也会无法工作… 时钟跳跃通过「恰当的运维」保证机器时钟不会大幅度跳跃每次通过微小的调整来完成实际上这是可以做到的 为什么 Redis 作者优先解释时钟问题因为在后面的反驳过程中需要依赖这个基础做进一步解释。 2) 解释网络延迟、GC 问题 之后Redis 作者对于对方提出的网络延迟wan、进程 GC 可能导致 Redlock 失效的问题也做了反驳 我们重新回顾一下Martin 提出的问题假设 客户端 1 请求锁定节点 A、B、C、D、E 客户端 1 的拿到锁后进入 GC 所有 Redis 节点上的锁都过期了 客户端 2 获取节点 A、B、C、D、E上的锁 客户端 1 GC 结束认为成功获取锁 客户端 2 也认为获取到锁发生「冲突」。 Redis 作者反驳到这个假设其实是有问题的Redlock 是可以保证锁安全的。这是怎么回事呢 还记得前面介绍 Redlock 流程的那 5 步吗这里我再拿过来让你复习一下。 客户端先获取「当前时间戳T1」 客户端依次向这 5 个 Redis 实例发起加锁请求用前面讲到的 SET 命令且每个请求会设置超时时间毫秒级要远小于锁的有效时间如果某一个实例加锁失败包括网络超时、锁被其它人持有等各种异常情况就立即向下一个 Redis 实例申请加锁 如果客户端从 3 个大多数以上 Redis实例加锁成功则再次获取「当前时间戳T2」如果 T2 - T1 锁的过期时间此时认为客户端加锁成功否则认为加锁失败 加锁成功去操作共享资源例如修改 MySQL 某一行或发起一个 API 请求 加锁失败向「全部节点」发起释放锁请求前面讲到的Lua脚本释放锁。 注意重点是 1-3在步骤 3加锁成功后为什么要重新获取「当前时间戳T2」还用 T2 - T1 的时间与锁的过期时间做比较 Redis 作者强调如果在 1-3 发生了网络延迟、进程 GC 等耗时长的异常情况那在第 3 步 T2 - T1是可以检测出来的如果超出了锁设置的过期时间那这时就认为加锁会失败之后释放所有节点的锁就好了 Redis 作者继续论述如果对方认为发生网络延迟、进程 GC 是在步骤 3 之后也就是客户端确认拿到了锁去操作共享资源的途中发生了问题导致锁失效那这不止是 Redlock 的问题任何其它锁服务例如 ZooKeeper都有类似的问题这不在讨论范畴内。 这里我举个例子解释一下这个问题 客户端通过 Redlock 成功获取到锁通过了大多数节点加锁成功、加锁耗时检查逻辑 客户端开始操作共享资源此时发生网络延迟、进程 GC 等耗时很长的情况 此时锁过期自动释放 客户端开始操作 MySQL此时的锁可能会被别人拿到锁失效。 Redis 作者这里的结论就是 客户端在拿到锁之前无论经历什么耗时长问题Redlock 都能够在第 3 步检测出来 客户端在拿到锁之后发生 NPC那 Redlock、ZooKeeper 都无能为力 所以Redis 作者认为 Redlock 在保证时钟正确的基础上是可以保证正确性的。 3) 质疑 fencing token 机制 Redis 作者对于对方提出的 fencing token 机制也提出了质疑主要分为 2 个问题这里最不宜理解请跟紧我的思路。 第一这个方案必须要求要操作的「共享资源服务器」有拒绝「旧 token」的能力。 例如要操作 MySQL从锁服务拿到一个递增数字的 token然后客户端要带着这个 token 去改 MySQL 的某一行这就需要利用 MySQL 的「事务隔离性」来做。 // 两个客户端必须利用事务和隔离性达到目的 // 注意 token 的判断条件 UPDATE table T SET val $new_val, current_token $token WHERE id $id AND current_token $token但如果操作的不是MySQL呢例如向磁盘上写一个文件或发起一个 HTTP 请求那这个方案就无能为力了这对要操作的资源服务器提出了更高的要求。 也就是说大部分要操作的资源服务器都是没有这种互斥能力的。 再者如果资源服务器都有了「互斥」能力那还要分布式锁干什么 所以Redis 作者认为这个方案是站不住脚的。 第二退一步讲即使 Redlock 没有提供 fencing token 的能力但 Redlock 已经提供了随机值就是前面讲的 UUID利用这个随机值也可以达到与 fencing token 同样的效果。 如何做呢 Redis 作者只是提到了可以完成fencing token类似的功能但却没有展开相关细节根据我查阅的资料大概流程应该如下。 客户端使用 Redlock 拿到锁 客户端在操作共享资源之前先把这个锁的 VALUE在要操作的共享资源上做标记 客户端处理业务逻辑最后在修改共享资源时判断这个标记是否与之前一样一样才修改类似 CAS 的思路。 还是以 MySQL 为例举个例子就是这样的 客户端使用 Redlock 拿到锁 客户端要修改 MySQL 表中的某一行数据之前先把锁的 VALUE 更新到这一行的某个字段中这里假设为 current_token 字段) 客户端处理业务逻辑 客户端修改 MySQL 的这一行数据把 VALUE 当做 WHERE 条件再修改。 UPDATE table T SET val $new_val WHERE id $id AND current_token $redlock_value可见这种方案依赖 MySQL 的事务机制也达到对方提到的 fencing token 一样的效果。 但这里还有个小问题是网友参与问题讨论时提出的两个客户端通过这种方案先「标记」再「检查修改」共享资源那这两个客户端的操作顺序无法保证啊 而用 Martin 提到的 fencing token因为这个 token 是单调递增的数字资源服务器可以拒绝小的 token 请求保证了操作的「顺序性」 Redis 作者对于这个问题做了不同的解释我觉得很有道理他解释道分布式锁的本质是为了「互斥」只要能保证两个客户端在并发时一个成功一个失败就好了不需要关心「顺序性」。 前面 Martin 的质疑中一直很关心这个顺序性问题但 Redis 的作者的看法却不同。 综上Redis 作者的结论 1、作者同意对方关于「时钟跳跃」对 Redlock 的影响但认为时钟跳跃是可以避免的取决于基础设施和运维。 2、Redlock 在设计时充分考虑了NPC 问题在 Redlock 步骤 3 之前出现 NPC可以保证锁的正确性但在步骤 3 之后发生 NPC不止是 Redlock有问题其它分布式锁服务同样也有问题所以不在讨论范畴内。 是不是觉得很有意思 在分布式系统中一个小小的锁居然可能会遇到这么多问题场景影响它的安全性 不知道你看完双方的观点更赞同哪一方的说法呢 别急后面我还会综合以上论点谈谈自己的理解。 好讲完了双方对于Redis分布锁的争论你可能也注意到了Martin 在他的文章中推荐使用 ZooKeeper 实现分布式锁认为它更安全确实如此吗 八、基于 ZooKeeper 的分布式锁 如果你有了解过 ZooKeeper基于它实现的分布式锁是这样的 客户端 1 和 2 都尝试创建「临时节点」例如 /lock 假设客户端 1 先到达则加锁成功客户端2加锁失败 客户端 1 操作共享资源 客户端 1 删除 /lock 节点释放锁 ZooKeeper 不像 Redis 那样需要考虑锁的过期时间问题它是采用了「临时节点」保证客户端拿到锁后只要连接不断就可以一直持有锁。 如果客户端 1 异常崩溃了这个临时节点也会自动删除保证了锁一定会被释放。 不错没有锁过期的烦恼还能在异常时自动释放锁是不是觉得很完美 其实不然。 思考一下客户端1创建临时节点后ZooKeeper 是如何保证让这个客户端一直持有锁呢 原因就在于客户端1此时会与 ZooKeeper 服务器维护一个 Session这个 Session 会依赖客户端「定时心跳」来维持连接。 如果 ZooKeeper 长时间收不到客户端的心跳就认为这个 Session 过期了也会把这个临时节点删除。 同样地基于此问题我们也讨论一下 GC 问题对 ZooKeeper 的锁有何影响 客户端 1 创建临时节点 /lock 成功拿到了锁 客户端 1 发生长时间 GC 客户端 1 无法给 ZooKeeper 发送心跳ZooKeeper 把临时节点「删除」 客户端 2 创建临时节点 /lock 成功拿到了锁 客户端 1 GC 结束它仍然认为自己持有锁冲突。 可见即使是使用 ZooKeeper也无法保证进程 GC、网络延迟异常场景下的安全性。 这就是前面 Redis 作者在反驳的文章中提到的如果客户端已经拿到了锁但客户端与锁服务器发生「失联」例如 GC那不止 Redlock 有问题其它锁服务都有类似的问题ZooKeeper 也是一样 那基于 Etcd 实现的分布锁呢 九、基于 Etcd 的分布式锁 基于 Etcd 实现的分布式锁流程 客户端 1 创建一个 lease 租约设置过期时间 客户端 1 携带这个租约创建/lock节点 客户端 1 发现节点不存在拿锁成功 客户端 2 同样方式创建节点节点已存在拿锁失败 客户端 1 定时给这个租约「续期」保持自己一直持有锁 客户端 1 操作共享资源 客户端 1 删除 /lock 节点释放锁。 示例代码 package mainimport (contextgo.etcd.io/etcd/client/v3go.etcd.io/etcd/client/v3/concurrencylogtime )func main() {cli, err : clientv3.New(clientv3.Config{Endpoints: []string{localhost:2379},DialTimeout: 5 * time.Second,})if err ! nil {log.Fatal(err)}defer cli.Close()session, err : concurrency.NewSession(cli)if err ! nil {log.Fatal(err)}defer session.Close()mutex : concurrency.NewMutex(session, /my-lock/)if err : mutex.Lock(context.Background()); err ! nil {log.Fatal(err)}log.Println(acquired lock)// 执行业务逻辑if err : mutex.Unlock(context.Background()); err ! nil {log.Fatal(err)}log.Println(released lock) }Etcd 虽然没有像ZooKeeper提供临时节点的概念但Etcd提供了一个叫「租约」的概念。 我们先创建一个租约对象并设置一定的过期时间之后在创建节点时把这个租约和节点进行「关联」。 之后我们定时给这个租约进行「续期」保证我们创建的节点一直有效一直持有锁。 你看这里的定时给租约续期的步骤和上面 ZooKeeper 客户端定时给Server发心跳类似其目的都是让服务端保持这个 Session 或 KV 持续有效。 所以它依旧存在和 ZooKeeper 相同的问题 客户端 1 创建节点/lock成功拿到了锁 客户端 1 发生长时间 GC 客户端 1 无法向 Etcd 发请求给租约「续期」 租约到期Etcd 「删除」锁节点 客户端 2 创建临时节点 /lock 成功拿到了锁 客户端 1 GC 结束它仍然认为自己持有锁冲突 可见基于 Etcd 实现的分布锁当拿到锁发生 GC、网络延迟问题依旧可能失效。 至此这里我们可以得出结论一个分布式锁无论是基于 Redis 还是 ZooKeeper、Etcd 实现在极端情况下都无法保证 100% 安全都存在失效的可能。 如果你的业务数据非常敏感在使用分布式锁时一定要注意这个问题不能假设分布式锁 100% 安全。 但为什么我们总是能听到很多人使用 ZooKeeper、Etcd 实现分布式锁呢 因为抛开安全性ZooKeeper 和 Etcd 相比于 Redis 实现分布锁在功能层面有一个非常好用的特性Watch。 这个 API 允许客户端「监听」ZooKeeper、Etcd 某个节点的变化以此实现「公平」的分布式锁篇幅原因这里就不展开了。 十、我对分布式锁的理解 好了前面详细介绍了基于 Redis 的 Redlock 和 ZooKeeper、Etcd 实现的分布锁在各种异常情况下的安全性问题回到 Redlock 上面来我想和你聊一聊我对 Redlock 的看法。 1) 到底要不要用 Redlock 前面也分析了Redlock 只有建立在「时钟正确」的前提下才能正常工作如果你可以保证这个前提那么可以拿来使用。 但保证时钟正确我认为并不是你想的那么简单就能做到的。 第一从硬件角度来说时钟发生偏移是时有发生无法避免的。 例如CPU 温度、机器负载、芯片材料都是有可能导致时钟发生偏移。 第二从我的工作经历来说曾经就遇到过时钟错误、运维暴力修改时钟的情况发生进而影响了系统的正确性所以人为错误也是很难完全避免的。 所以我对 Redlock 的个人看法是尽量不用它而且它的性能不如单机版 Redis部署成本也高我还是会优先考虑使用 Redis「主从哨兵」的模式实现分布式锁。 那正确性如何保证呢第二点给你答案。 2) 如何正确使用分布式锁 在分析Martin 观点时它提到了 fencing token 的方案给我了很大的启发虽然这种方案有很大的局限性但对于保证「正确性」的场景是一个非常好的思路。 所以我们可以把这两者结合起来用 1、使用分布式锁在上层完成「互斥」目的虽然极端情况下锁会失效但它可以最大程度把并发请求阻挡在最上层减轻操作资源层的压力。 2、但对于要求数据绝对正确的业务在资源层一定要做好「兜底」设计思路可以借鉴 fencing token 的方案来做即在 DB 层通过版本号的方式来更新数据避免并发冲突。 两种思路结合我认为对于大多数业务场景已经可以满足要求了。 十一、总结 好了总结一下。这篇文章我们主要探讨了基于 Redis 实现的分布式锁究竟是否安全这个问题。 从最简单分布式锁的实现到处理各种异常场景再到引出 Redlock以及两个分布式专家的辩论引申出分布式系统 NPC 问题。 最后我们还对比了基于 ZooKeeper、Etcd 的分布式锁的安全问题以及与Redis的差异。 对于分布式锁可以总结下 分布式锁并不是 100% 安全无论是基于 Redis、ZooKeeper 还是 Etcd 很多人用分布锁以为拿到锁后就可以安心地去改共享资源认为分布锁 100% 安全其实不然拿到锁后面临各种异常情况都有可能导致锁失效这时候再去改资源可能锁已经被别人拿到去改资源了产生并发冲突 一个严谨的分布式锁模型应该考虑锁租期、锁归属、副本同步、NPC 问题 使用 Redis 分布锁可以最大程度把并发请求阻挡在最上层非常适合高并发场景但对于数据敏感的业务场景资源层要做兜底fencing token 的思路类似乐观锁两者结合起来用 十二、后记 1、在分布式系统环境下看似完美的设计方案可能并不是那么「严丝合缝」如果稍加推敲就会发现各种问题。所以在思考分布式系统问题时一定要谨慎再谨慎。 2、从 Redlock 的争辩中我们不要过多关注对错而是要多学习大神的思考方式以及对一个问题严格审查的严谨精神。 最后用 Martin 在对于 Redlock 争论过后写下的感悟来结尾 “前人已经为我们创造出了许多伟大的成果站在巨人的肩膀上我们可以才得以构建更好的软件。无论如何通过争论和检查它们是否经得起别人的详细审查这是学习过程的一部分。但目标应该是获取知识而不是为了说服别人让别人相信你是对的。有时候那只是意味着停下来好好地想一想。” 原文地址https://mp.weixin.qq.com/s/yZC6VJGxt1ANZkn0SljZBg
http://www.zqtcl.cn/news/795894/

相关文章:

  • 藤虎网络广州网站建设网站域名实名认证官网
  • 佛山专业网站建设公司推荐it行业做网站一个月多少钱
  • 三网合一网站怎么做苏醒主题做的网站
  • wordpress站内统计插件wordpress模板 单栏
  • 龙岩网站定制网站开发 技术路线
  • 广州制作网站开发网站标题怎么设置
  • 海南旅游网站开发背景做网站兼容ie
  • 查找人网站 优帮云本地升级wordpress
  • 安庆什么网站好小事做wordpress主题vue
  • 高端商品网站网络运维工程师面试题及答案
  • 做网站的dw全称是啥适合迷茫年轻人的工作
  • 免费软件库合集软件资料网站wordpress go链接跳转错误
  • 重庆那里做网站外包好和镜像网站做友链
  • 网站栏目关键词装修效果图制作软件
  • 企业网站开发公司-北京公司北京医疗网站建设公司
  • 可以做配音兼职的网站产品网站怎样做外部链接
  • 如何制作网站效果图做外单要上什么网站
  • 网站开发预算编制网站可以制作ios
  • 强化网站建设网页翻译怎么弄出来
  • 长春火车站到龙嘉机场高铁时刻表视频网站建设公司排名
  • 武进网站建设代理商google官网下载
  • 简单网站开发流程图知乎怎么申请关键词推广
  • 成寿寺网站建设公司文登区做网站的公司
  • 建设一个网站用什么软件下载阿里外贸平台网站建设
  • 可信网站myeclipse网站开发
  • 做设计找素材的 网站有哪些网站建设实训个人总结
  • 浙江省建设厅继续教育官方网站网站做vr的收费
  • 建造网站 备案苏州手机网站设计
  • 做外贸卖小商品是哪个网站手机首页设计
  • 大连网站制作公司营销策划公司有哪些职位