先介绍两个概念

Safety Properties, 在程序运行中不会进入非预期的状态(如非法调用参数, 数组下标越界等运行错误) Liveness Properties, 在程序运行中预期状态一定会到达(如停机, 获取资源请求一定有返回结果等等)

保证分布式锁有效的三个属性

  1. Safety Properties:安全性,此处也就是互斥性,任意时刻只能有一个客户端可以持有锁
  2. Liveness Property A:无死锁,即使持有锁的客户端崩溃或被分区,也可以获得锁
  3. Liveness Property B:容错性,只要大多数 Redis 节点正常,客户端就能获取和释放锁

为什么基于故障转移(failover-based)的实现还不够

我们先来看看现有大多数 Redis 分布式锁的实现
最简单的方案是在一个实例中创建一个 key,并给这个 key 设置过期时间,保证这个锁最终一定能够释放,当客户端释放锁的时候,删除这个 key
看上去可能不错,但是有个问题:当我们的 Redis 主节点挂掉时会发生什么?好,那我们增加一个从节点,当主节点不可用时自动切换到从节点。但不幸地是这不行,因为 Redis 复制是异步的,所以不能保证互斥性

在这个方案下有一个明显的竞态条件:

  1. 客户端 A 在 master 节点获取锁
  2. 在写 key 操作被传输到 slave 节点前 master 节点挂了
  3. slave 晋升为 master
  4. 客户端 B 获取 A 其实刚刚已经获取到的锁 SAFETY VIOLATION 违反了文章开头提到的安全属性

虽然有上述缺陷,但在一些特殊场景下,这种方案还是可以使用的。比如故障发生的时候,多个客户端同时持有锁对于系统运行或者业务逻辑没有太大影响,那么就可以使用这种基于复制的解决方案。否则最好还是使用本文后续将会提到的 Redlock 算法

单实例情况下正确的实现

在解决单实例单点故障的限制前,我们先来看看如何正确地执行它
获取一个锁的方式: set resource_name my_random_value NX PX 30000
解释:

  • 在 resource_name 不存在(NX 选项)的时候创建它,并设置过期时间为 30000 毫秒
  • 值是一个随机值,而且这个值在每个客户端和每个锁请求中都是唯一的,这样做的目的是为了能够安全地释放锁,不会出现 A 客户端获取的锁被 B 客户端删除的情况。使用一段简单的 lua 代码告诉 Redis,只有 key 存在而且值与当前客户端持有的值相等时才删除这个 key
if redis.call("get", KEYS[1]) == ARGV[1] thenreturn redis.call("del", KEYS[1])
elsereturn 0
end
复制代码

防止由其他客户端创建的锁被错误删除非常重要
举个例子,当一个客户端获取了锁,并因一些长时间的操作,阻塞时间超过了锁的可用时间(key 过期时间)导致 key 被删除,然后该 key 又被其它客户端创建(也就是其它客户端获得锁)
如果前一个客户端在后一个客户端用完锁前进行了释放锁的操作,就导致了实际上现在属于后一个客户端的锁被删除
所以必须使用上面的脚本保证客户端释放的一定是自己持有的锁,而且随机值的生成很重要,必须是全局唯一

接下来我们把上述的算法扩展到分布式的情况

Redlock 算法

在算法的分布式版本中,假设有 N 个 Redis 节点。而且这些节点全都是相互独立的,都是 master 节点,且不使用分布式协调方案。假设 N=5 ,即部署 5 个 Redis master 节点在不同的机器(或虚拟机)上

客户端需要进行如下的操作来获取锁:

  1. 获取当前时间(毫秒)
  2. 尝试按顺序在 N 个节点获取锁(set 相同的 key value)。客户端在每个节点请求锁时,使用一个相对总的锁过期时间而言非常小的请求超时时间。例如锁过期时间为 10s,那么请求超时时间应该设置在大约 5~50ms 之间。这可以防止客户端在一个挂掉的节点上长时间阻塞:如果实例不可用,我们应该尝试尽快与下一个实例
  3. 客户端计算获取锁所花的时间(当前时间减去第一步中的时间)。当且仅当客户端在大多数节点上(至少三个)都成功获得了锁,而且总时间消耗小于锁有效时间,锁被认为获取成功
  4. 如果锁获取成功了,那么它的有效时间就是最初的锁有效时间减去之前获取锁所消耗的时间
  5. 如果因为某些原因,锁获取失败了(无论是不能在大部分节点成功获取锁,还是锁有效时间小于 0),将会尝试释放所有节点的锁(即使是那些没有获取成功的节点)

这个算法是异步的吗

这个算法依赖于一个假设:即使在没有同步时钟机制的两个进程中,每个进程的本地时间仍然以相同的速率前进,即使有误差,这个误差时间相对于锁自动释放时间也是极小到可以忽略的。这个假设非常像现实世界中的计算机:每台计算机都有一个本地时钟,我们经常相信不同的计算机的时间差是很小的 在这一点上需要再细化一下互斥锁的规则:必须确保客户端在 锁过期时间-跨进程的时间差(clock drift) 时间内做完自己所有的工作 更多相关信息可以阅读这篇有趣的文章:Leases: an efficient fault-tolerant mechanism for distributed file cache consistency

错误重试

当一个客户端不能获得锁时,它应该在随机延迟后再次尝试,避免大量客户端同时获取锁的情况出现,这种情况下可能发生脑裂(split brain condition),导致大家都获取不了锁。另外,客户端越快尝试在大多数节点中获取锁,出现脑裂情况的时间窗口就越小。所以理想的情况下,客户端应该并行同时向全部节点发起获取锁请求 这里有必要强调一下,客户端在没有成功获取锁时,一定要尽快并行在全部节点上释放锁,这样就没有必要等到 key 超时后才能重新获取这个锁(但是如果网络分区的情况发生,客户端无法连接到 Redis 节点时,会损失锁自动过期释放这段时间内的系统可用性)

释放锁

释放锁比较简单,因为只需要所有节点都释放锁都行,不管之前有没有在该节点获取成功锁

安全性论证

这个算法到底是不是安全的,我们可以观察一些不同情况下的表现
我们假设客户端可以在全部节点上获取成功锁,所有的节点将会有一个相同存活时间的 key。但要注意,这个 key 是在不同时间设置的,所以 key 也会在不同时间超时。如果在最坏情况下,第一个 key 在 T1 时间设置(在发起请求前采样),最后一个 key 在 T2 时间设置(在服务器响应后采样),我们可以确认最早超时的 key 至少也会至少存在 MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT 时间。所有其他的 key 超时都会大于这个时间,所以我们可以确定至少在这个时间点前这些 key 都是同时存在的
在大部分节点都设置了 key 的时候,其他客户端无法抢占这个锁,因为 N/2+1 SET NX 操作在 N/2+1 个 key 存在的情况下无法成功。所以如果一个锁被获取成功了,就不可能重新在同一时间获取它(违反了安全属性)

此外我们还需要确保多个客户端同时获取锁时不会同时成功
如果一个客户端获取大多数节点的锁的耗时接近甚至超过锁的最大有效时间,那么系统就会认为这个锁是无效的,并全部解锁。所以我们只需要考虑在大多数节点获取锁的耗时小于锁有效时间的情况。在前面讨论的案例中可知,在 MIN_VALIDITY 时间内,没有客户端能成功重新获取锁。所以多个客户端只可能在在大多数节点上获取锁的时间大于 TTL 时才可以,这会导致锁失效

可用性(liveness)论证

系统可用性基于三个特性:

  1. 自动释放锁(基于 key 过期):最终锁一定能够再次被获取
  2. 现实情况下客户端一般都会主动释放锁,所以我们不需要等到 key 过期才能再去获取锁
  3. 当客户端发起重试获取锁的请求时,它会等待一段比去大多数节点获取锁的时间更长的时间,这会降低多个客户端同时请求锁而发生脑裂状态的概率

然而,我们在网络分区发生的时候会损失 TTL 时间的系统可用性,所以如果分区连续发生,不可用也会持续。这种情况在每次客户端获得锁并在释放锁前遇到了网络分区的情况时都会发生
基本上,如果持续的网络分区的话,系统也会持续不可用

性能、故障后恢复和 fsync

很多用户使用 Redis 做分布式锁服务时,不但要求加解锁要低延迟,还要求高吞吐量(每秒能够执行加/解锁操作的次数)。为了达到这个需求,可以通过多路复用并行和 N 个服务器通信,或者也可以将 socket 设置为非阻塞模式,一次性发送全部的命令,之后再一次性处理全部返回的命令,假设客户端和不同 Redis 服务节点的网络延迟不大的话

为了能够实现故障恢复,我们需要考虑关于持久化的问题 假设有一个客户端成功得获取了锁(至少 3/5 个节点成功),而已经成功获得锁的其中一个节点重启了,那么我们就又有了 3 个可以分配锁的节点,这样其它客户端就又可以成功获得锁了,违反了互斥锁的安全性原则

如果启用了 AOF 持久化,情况会好很多。例如我们可以发起 SHUTDOWN 请求并重启服务器,因为 Redis 超时时间是语义层面的,所以在服务器关掉期间超时还是存在的,所以过期策略仍然存在 但如果是意外停机呢?如果 Redis 被配置为每秒同步数据到磁盘一次(默认),可能在重启的时候丢失一些 key。理论上,如果我们要确保锁在任何重启的情况下都安全,就必须设置 fsync=always。但这样会完全牺牲性能,使其和传统的 CP 系统的分布式锁方案没有区别
但事情往往不像第一眼看上去这么糟糕,基本上,只要一个服务节点挂了重启后不去管系统中现有活跃的锁,这样当节点重启时,整个系统中活跃的锁必然是由正在已获得锁的客户端使用的,而不是新加入系统的
为了确保这一点,只需让崩溃重启的实例,在最大锁有效时间内不可用,令该节点的旧锁信息全部过期释放
使用延迟重启基本上可以解决安全性问题,但要注意,这可能会造成可用性的下降:当系统内的大多数节点都挂了,那么在 TTL 时间内整个系统都处于不可用状态(无法获得锁)

使算法更可靠:扩展锁

如果客户端执行的工作由小的步骤组成,那么可以使用比较小的 TTL 时间来设置锁,并在锁快过期时刷新锁有效时间(续约)。但在技术上不会改变算法本质,因此应该限制重新获取锁尝试的最大次数,不然会违反可用性

转载于:https://juejin.im/post/5ce2648c51882525c75bfb05

Redlock 算法:Redis 实现分布式锁(译)相关推荐

  1. 基于Redis的分布式锁和Redlock算法

    来自:后端技术指南针 1 前言 今天开始来和大家一起学习一下Redis实际应用篇,会写几个Redis的常见应用. 在我看来Redis最为典型的应用就是作为分布式缓存系统,其他的一些应用本质上并不是杀手 ...

  2. redis实现轮询算法_基于zookeeper或redis实现分布式锁

    前言 在分布式系统中,分布式锁是为了解决多实例之间的同步问题.例如master选举,能够获取分布式锁的就是master,获取失败的就是slave.又或者能够获取锁的实例能够完成特定的操作. 目前比较常 ...

  3. Redis构建分布式锁——Redlock

    本文来自:http://ifeve.com/redis-lock/ 简介 在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段. 有很多三方库和文章描述如何用Redis实现一个分布式锁 ...

  4. Redis实现分布式锁的深入探究

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 一.分布式锁简介 锁 是一种用来解决多个执行线程 访问共享资源 错 ...

  5. nx set 怎么实现的原子性_基于Redis的分布式锁实现

    前言 本篇文章主要介绍基于Redis的分布式锁实现到底是怎么一回事,其中参考了许多大佬写的文章,算是对分布式锁做一个总结 分布式锁概览 在多线程的环境下,为了保证一个代码块在同一时间只能由一个线程访问 ...

  6. Zookeeper和Redis实现分布式锁,附我的可靠性分析

    作者:今天你敲代码了吗 链接:https://www.jianshu.com/p/b6953745e341 在分布式系统中,为保证同一时间只有一个客户端可以对共享资源进行操作,需要对共享资源加锁来实现 ...

  7. Redis——由分布式锁造成的重大事故

    作者:浪漫先生 原文:juejin.im/post/6854573212831842311 前言 基于Redis使用分布式锁在当今已经不是什么新鲜事了.本篇文章主要是基于我们实际项目中因为redis分 ...

  8. 《Redis官方文档》用Redis构建分布式锁

    <Redis官方文档>用Redis构建分布式锁 用Redis构建分布式锁 在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段. 有很多三方库和文章描述如何用Redis实现 ...

  9. 《Redis官方文档》用Redis构建分布式锁(悲观锁)

    2019独角兽企业重金招聘Python工程师标准>>> **用Redis构建分布式锁 ** 在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段. 有很多三方库和文章 ...

最新文章

  1. Java动态代理与Cglib代理
  2. Android中ActivityManagerService与应用程序(客户端)通信模型分析
  3. MATLAB实战系列(三十六)-MATLAB 离散Hopfield神经网络的分类——高校科研能力评价
  4. ASP.NET生成缩略图类C#代码
  5. Koa / Co / Bluebird or Q / Generators / Promises / Thunks 的相互关系
  6. java crud_Java 8流中的数据库CRUD操作
  7. 今日英语:out of the box
  8. Swoole 2019 :化繁为简、破茧成蝶
  9. 快手小剧场推出独立APP“追鸭”
  10. 关键词文章自动生成工具-关键词组合工具-关键词文章采集工具
  11. HTML静态网页设计
  12. 人工智能导论测试题——第1章绪论
  13. 控制器Ryu+Mininet完成集线器、自学习交换机、流量监控实例开发
  14. 开源的高性能Java集合:GNU Trove介绍
  15. 如何一键删除PPT的动画效果?
  16. git与gerrit基础概念
  17. ​FH5202原厂2A开关式同步降压型锂电池充电电路IC
  18. PVT的spatial reduction attention(SRA)
  19. 战术对抗仿真训练平台系统
  20. 伪静态与纯静态的区别是什么?

热门文章

  1. 蓝桥杯 基础练习 特殊回文数
  2. cookie知识总结
  3. 自动化测试学习之路--HTML常见元素、属性的简单学习
  4. c 传string 给java_JNI基础 将字符串传递给c,在c中拼接后返回给java
  5. matlab调和均值滤波_matlab均值滤波(原创).doc
  6. android 键盘遮盖输入框_Android软键盘遮住输入框的解决方法终极适配
  7. 倾斜模型精细化处理_基于倾斜摄影和近景摄影技术的实景三维模型结合(CC与 DPModeler结合)...
  8. 第12章[12.4] 鼠标移入移除时弹出和关闭窗口
  9. 说透Applet的数字签名之1——Applet及其运行
  10. Java Portlets 介绍