作者:温灏,后端研发,专注于Python和Go,对分布式系统感兴趣,本文系作者投稿,有兴趣投稿的同学,请后台回复【投稿】

由于微服务大行其道,服务之间的协调工作变得越来越重要。今天来简单说一下如何搭建一个基于redis的锁服务。需要说明的一点是,这里的锁是指互斥锁。

RedLock

在redis的官网上,可以很方便的查到一个分布式锁的实现:RedLock。那就先简单说下,redis作者antirez对于分布式锁是如何设计的吧。

基于单redis节点的锁

首先,获取锁采用类似下面的命令:

SET resource_name my_random_value NX PX 30000

这条命令尝试去获取一个资源的锁,这里的锁过期时间是30000毫秒。
在完成操作之后,通过以下Lua脚本来释放锁:

if redis.call("get",KEYS[1]) == ARGV[1] then    return redis.call("del",KEYS[1])else    return 0end

这里是先确认资源对应的value与客户端持有的value是否一致,如果一致的话就释放锁。

这里需要说明几点:

1.锁需要有一个超时时间,这个时长需要略长于操作所需的时间,以备在客户端获取锁之后宕机的情况下,锁也可以安全释放。这种带有效期的锁,通常也被叫做租约;

2.在解锁的时候,实际上进行的是一个get and del的操作,需要确认是释放由客户端自己申请的锁,且getdel操作之间不要有其他操作插入,所以采用lua脚本来做原子保证。

值得注意的是,当锁服务所在的redis节点宕机时,会导致锁服务不可用,数据恢复之后可能会丢失部分锁数据。当然,关于redis数据持久化和恢复,就是另一个话题了。

为了解决明显的单点问题,antirez设计提出了RedLock算法。RedLock基于完全独立的N个节点(通常推荐N = 5,一般可以把N看做是一个大于1的奇数)。

RedLock的实现步骤可以看成下面几步:

  1. 获取当前时间t1,精确到毫秒;

  2. 依次向锁服务所依赖的N个节点发送获取锁的请求,加锁的操作和上面单节点的加锁操作请求相同;

  3. 如果获取了超过半数节点的资源锁(>=N/2+1),则计算获取锁所花费的时间,计算方法是用当前时间t2减去t1,如果花费时间小于锁的过期时间,则成功的获取了锁;

  4. 这时锁的实际有效时间是设置的有效时间t0减去获取锁花费的时间(t2-t1);

  5. 如果在第3步没有成功的获取锁,需要向所有的N个节点发送释放锁的请求,释放锁的操作和上面单节点释放锁操作一致;

由于引入了多节点的redis集群,RedLock的可用性明显是大于单节点的锁服务的。这里需要说明一个节点故障重启的例子:

  1. client1向5个节点请求锁,获取了a,b,c上的锁;

  2. b节点故障重启,丢失了client1申请的锁;

  3. client2向5个节点请求锁,获取了b,d,e上的锁;

这里例子中,从客户端角度来看,有两个客户端合法的在同一时间都持有同一资源的锁,关于这个问题,antirez提出了延迟重启(delayed restarts)的概念:在节点宕机之后,不要立即重启恢复服务,而是至少经过一个完整锁有效周期之后再启动恢复服务,这样可以保证节点因为宕机而丢失的锁数据一定因为过期而失效。

Martin的分析


Martin首先说明,在没有fencing token的保证之下,锁服务可能出现的问题,他给出了下面的图:

客户端停顿导致锁失效

上图说明的问题可以描述成下面的步骤:

  1. client1成功获取了锁,之后陷入了长时间GC中,直到锁过期;

  2. client2在client1的锁过期之后成功的获取了锁,并去完成数据操作;

  3. client1从GC中恢复,从它本身的角度来看,并不会意识到自己持有的锁已经过期,去操作数据;

从上面的例子看出,这里的锁服务提供了完整的互斥锁语义保证,从资源的角度来看,两次操作都是合法的。上面提到,RedLock根据随机字符串来作为单次锁服务的token,这就意味着对于资源而言,无法根据锁token来区分client持有的锁所获取的先后顺序。

为此,Martin引入了fencing token机制,fencing token可以理解成采用全局递增的序列替代随机字符串,作为锁token来使用。这样就可以从资源侧确定client所携带锁的获取先后顺序了。

fencing token机制


除了没有fencing机制保证之外,Martin还指出,RedLock依赖时间同步不同节点之间的状态这种做法有问题。具体可以看个例子:

  1. client1获取节点a,b,c上的锁;

  2. 节点c由于时间同步,发生了时钟漂移,时钟跳跃导致client1获取的锁失效;

  3. client2或缺节点c,d,e上的锁;

本质上来看,RedLock通过不同节点的时钟来进行锁状态的同步。而在分布式系统中,物理时钟本身就有可能出现问题,也就是说,RedLock的安全性保证建立在物理时钟没问题的假设上。分布式系统中不同节点的协调一般不使用物理时钟作为度量,相应的,Lamport提出逻辑时钟作为分布式事件先后顺序的度量。

Martin还指出,引入锁的主要目的无非以下两个:

  • 为了资源效率,避免不必要的重复昂贵计算;

  • 为了正确性,保证数据正确;

对于第一点而言,采用单redis节点的锁就可以满足需求;对于第二点而言,则需要借助更严肃的分布式协调系统(如zookeeper,etcd,consul等等)。

antirez的反驳


在Martin发表自己对RedLock的分析之后,antirez也发表了自己的反驳。
针对Martin提出的两点质疑,antirez分别提出反驳:

  • 首先,antirez认为在RedLock中,虽然没有用到fencing保证机制,但是随机字符串token也可以提供client到具体锁的匹配映射(这点个人不敢苟同,具体原因后面分析);

  • 其次,antirez认为分布式系统中的物理时钟可以通过良好的运维来保证;

事情到这里,关于RedLock的分析和辩论基本可以大概看清楚了。原作者antirez和分布式系统专家Martin分别提出了算法,质疑以及反驳。

我的设计

在整个论证的过程中,个人比较倾向于Martin的看法,antirez的反驳在我看来是没有真正说明RedLock安全性问题的。这篇文章题目叫做“设计一个Redis锁服务”,所以我想提出自己的设计(文末有代码相关链接)。

首先,我设计的Redis锁服务是基于单个redis节点的,在锁服务上,基本和antirez描述的单redis节点的锁服务操作相同,唯一的不同在于采用Martin提出的fencing token替换antirez的随机字符串,整个过程如下:

  • 加锁操作

    • client先获取一个fencing token,携带fencing token去获取资源相关的锁,这时出现两种情况:

    • 1.锁已被占用,且锁的fencing token大于此时client的fencing token,这种情况的主要原因是client在获取fencing token之后出现了长时间GC;

    • 2. 锁已被占用,且锁的fencing token小于此时的client的fencing token,这种情况就是之前有其他客户端成功持有了锁且还没有释放(这里的释放包括client主动释放和锁超时之后的被动释放);

    • 3. 锁未被占用,成功加锁;

  • 解锁操作

    • 这里解锁操作和antirez提出的单节点情况一样,是一个借助lua脚本实现的原子get compare and del的操作;

  • 校验token情况

    • 这里引入了对于fencing token的校验操作,有条件的情况下,可以在资源侧被操作前进行fencing token的校验,保证能够操作资源的client持有的锁依次递增;

缺点

这里缺点和antirez提出的缺点类似,主要出现在可用性和安全性上,包括两点:

  1. redis节点宕机造成锁服务完全不可用;

  2. redis节点故障恢复导致锁数据丢失的问题;

其中第一点在单点服务中无可避免,只能够通过良好的运维和故障转移来提高redis节点的可用性,对于第二点,可以通过必要的校验token操作来保证。

优点

相比RedLock,拥有更好的性能,简单比较一下,一次RedLock加锁操作需要至少N次redis请求,如果加锁时间过程,则需要2*N次redis请求(N次加锁,N次解锁),而我的设计中,只需要2次redis请求(一次生成fencing token,一次加锁)。

相比antirez提出的单redis节点锁的方案,引入fencing token可以更有利于开发者判断系统究竟发生了什么。简单的比较fencing token就可以在加锁阶段判断出问题所在(是正常的锁未释放还是出现了长时间的GC),校验token的操作可以在资源侧保证访问资源的client顺序严格按照获取锁的顺序。

使用场景

如果按照Martin提出的锁的两种使用场景来区分的话,我设计的锁本身由于可用性的制约,不适于严肃的场景,仅仅适用于效率原因或者对数据准确性要求不高的场景。

代码仓库

https://github.com/FelixSeptem/mortise,有兴趣的可以看看

长按订阅更多↓

陛下,请赐我一个好看↓↓↓

从0设计一个基于Redis的锁服务相关推荐

  1. 设计一个基于用户的API限流策略 Rate Limit

    设计一个基于用户的API限流策略 Rate Limit 应用场景 API接口的流量控制策略:缓存.降级.限流.限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的.限流策略 ...

  2. 编程设计一个基于条件风险最小的Bayes分类器

    编程设计一个基于条件风险最小的Bayes分类器: 要求: 混淆矩阵维度可任意设定 先验概率基于训练样本集自动求得 样本属性数量可任意输入设定 朴素贝叶斯求条件风险最小公式: 训练数据集: 代码: im ...

  3. 设计一个基于GUI的扑克程序

    2019独角兽企业重金招聘Python工程师标准>>> 在本课程教材扑克牌代码的基础上,设计一个基于GUI的扑克程序 a) 可以显示 52 张扑克牌,包括洗牌,发牌在内(2) b) ...

  4. 步进电机控制器设计 利用Quartus ii9.0设计一个具有四相单四拍

    步进电机控制器设计 利用Quartus ii9.0设计一个具有四相单四拍,四相双四拍和四相八拍的脉冲分配器.设计一个三选一数据选择器来控制pause信号选择工作方式,以及用两个74160与两个7447 ...

  5. 基于PLC的升降横移立体停车库的设计,设计一个基于西门子S7-200 PLC控制核心的

    基于PLC的升降横移立体停车库的设计,设计一个基于西门子S7-200 PLC控制核心的,三层三列,九个车位的立体停车控制系统. 目录3 1 绪 论4 2 设计要求5 3 硬件设计8 3.1 PLC型号 ...

  6. 步进电机控制器设计 利用Quartus ii9.0设计一个具有四相单四拍,四相双四拍和四相八拍的脉冲分配器

    步进电机控制器设计 利用Quartus ii9.0设计一个具有四相单四拍,四相双四拍和四相八拍的脉冲分配器. 设计一个三选一数据选择器来控制pause信号选择工作方式,以及用两个74160与两个744 ...

  7. 大神级教程!300分钟撸一个基于Redis 6.0 版本的高并发架构

    刚好原先公司搞职位调整,我不太满意,赶上这波金三银四的面试浪潮,干了也有5年的后端开发了,不是大神也是有实战经验的,我就自信满满地去面了几家大厂,结果就遇到... 面试官这夺命连环12问,谁顶得住? ...

  8. 面试官让我设计一个基于分布式锁的库存超卖方案,并发量很高的那种

    今天给大家聊一个有意思的话题:每秒上千订单场景下,如何对分布式锁的并发能力进行优化? 背景引入 首先,我们一起来看看这个问题的背景? 前段时间有个朋友在外面面试,然后有一天找我聊说:有一个国内不错的电 ...

  9. 太强了,300分钟撸一个基于redis的亿级用户高并发系统

    对于双十一这种高并发.大流量的场景一般都会用到缓存抗住大并发,市面上缓存框架用的最多的无疑就是Redis了,Redis作为稳居世界排名第一的KV内存数据库,同时也是最受欢迎的分布式缓存中间件,是应对高 ...

最新文章

  1. 推荐系统笔记(其它应用算法)
  2. 树莓派 VNC Viewer 远程桌面配置教程
  3. cacti安装FAQ
  4. python中matplotlib库实例_Python Matplotlib库入门指南
  5. php 条码打印控件,jQuery插件jquery-barcode实现条码打印的方法
  6. ORA-00257归档日志写满的解决方法 - xwdreamer - 博客园
  7. html页面中文乱码处理
  8. maven设置从本地读_如何在Eclipse中更改Maven本地存储库
  9. linux 运行ctl文件_[命令] Linux 命令 systemctl(程序单元启动和管理)
  10. quartus频率计 时钟设置_基于QuartusII的两种数字频率计的设计与比较
  11. 分子动力学理论部分总结(未整理完)
  12. HexoNext添加网易云音乐
  13. 软件测试的风险分析与解决办法
  14. 在微型计算机中ega,在微机系统中,常有VGA、EGA等说法,它们的含义是什么
  15. UVA, 563 Crimewave
  16. Linux设备模型剖析系列一(基本概念、kobject、kset、kobj_type)
  17. Linux虚拟地址空间
  18. RPC(远程过程调用)详解
  19. EDA-功能仿真和时序仿真有什么区别?
  20. 全面支持ROS,思岚科技发布SLAMWARE ROS SDK !

热门文章

  1. php鼠标已入移除,angularjs鼠标移入移出实现显示隐藏
  2. mysql+在服务中无法启动_MySQL服务初始化后无法启动
  3. 9.67最不下降子序列
  4. 153和154.寻找旋转排序数组中的最小值
  5. 使用PlantText画时序图分析业务流程
  6. 玻利维亚java_BlogJava
  7. 如何只在IE上加载CSS样式表
  8. Android 3.0 r1 API中文文档(107) —— AsyncPlayer
  9. centos7 安装jdk7
  10. es6学习笔记2-—symbol、变量与作用域