原文地址:http://bridgeforyou.cn/2018/09/02/Redis-Dsitributed-Lock-2/

单机版实现的局限性

在上一篇文章中,我们讨论了Redis分布式锁的实现,简单回顾下。

获取锁:

set file:9527 ${random_value} NX EX ${timeout}

释放锁,调用lua脚本:

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

这套实现机制,在只有一个Redis实例的情况下,确实很完美。

然而,大多数生产环境,都不可能只部署一个Redis,至少也是主从架构:

更多的是主从+分片的架构:

当然主从架构也可以进化为一主多从架构乃至主从链架构(Master-Salve Chain):

而其实在主从架构下,之前那套分布式锁的机制,就已经失效了,原因正如之前说的:

如果A往Master放入了一把锁,然后再数据同步到Slave之前,Master crash,Slave被提拔为Master,这时候Master上面就没有锁了,这样其他进程也可以拿到锁,违法了锁的互斥性。

那么,要怎么解决这个问题呢?

Redlock算法

针对Redis集群架构,redis的作者antirez提出了Redlock算法,来实现集群架构下的分布式锁。

Redlock算法并不复杂,我们先简单描述一下,假设我们Redis分片下,有三个Master的节点,这三个Master,又各自有一个Slave:

好,现在客户端想获取一把分布式锁:

  • 记下开始获取锁的时间 startTime
  • 按照A->B->C的顺序,依次向这三台Master发送获取锁的命令。客户端在等待每台Master回响应时,都有超时时间timeout。举个例子,客户端向A发送获取锁的命令,在等了timeout时间之后,都没收到响应,就会认为获取锁失败,继续尝试获取下一把锁
  • 如果获取到超过半数的锁,也就是 3/2+1 = 2把锁,这时候还没完,要记下当前时间endTime
  • 计算拿到这些锁花费的时间 costTime = endTime - startTime,如果costTime小于锁的过期时间expireTime,则认为获取锁成功
  • 如果获取不到超过一半的锁,或者拿到超过一半的锁时,计算出costTime>=expireTime,这两种情况下,都视为获取锁失败
  • 如果获取锁失败,需要向全部Master节点,都发生释放锁的命令,也就是那段Lua脚本

看完这个Redlock算法,相信你会有很多疑问,下面就一起来追问Redlock。

追问Redlock

1、为什么要给每个获取锁的请求设置timeout

为了防止在某个出了问题的Master节点上,浪费太多时间。一旦超时了,马上尝试下一个。

2、获取了过半数的锁之后,还要不要继续获取

这个没有约束。

你可以选择适可而止,这样可以提高获取锁的速度,总共三台,A和B都拿到了,就不必去拿C了。

你也可以很贪心,A和B都拿到了,还要去拿C。这有什么好处呢?后面会跟你说。

3、如果costTime只比expireTime小一点点,会不会有问题?

当然有问题,这样你前脚刚拿到锁,走进门,后脚分布式锁就过期了,别人也拿到锁,进门了,互斥性被打破。

解决办法是,每个请求的timeout要比expireTime小很多,比如你的expireTime是10s,那么timeout可以设置为50ms,这样costTime最多也就50*3=150ms,剩下的9850ms,这九秒多钟,你都可以用来执行代码,保证不会有其他进程可以进入。

For example if the auto-release time is 10 seconds, the timeout could be in the ~ 5-50 milliseconds range. This prevents the client from remaining blocked for a long time trying to talk with a Redis node which is down: if an instance is not available, we should try to talk with the next instance ASAP.

当然,如果你的代码执行了9850ms还没执行完,那别的进程还是可以抢到锁。这也是一个暂时无解的问题。

4、释放锁时,为什么不能只向成功获取到锁的Master发送释放命令,而要向所有的Master节点发送

很简单,假设你向Master A发送了获取锁的命令,set命令执行成功了,但是在回响应时发送了故障,响应没发回来,过了超时时间后,你会认为获取锁失败,而实际上,锁已经在redis那边生效了。

所以在释放锁的时候,必须向全部节点都发生命令,不管你到底有没有在那节点上面获取到锁。

5、如果有节点crash,锁不也还是会丢失吗?

的确,单机时候的问题,在集群依然存在。

Redlock算法,在有节点重启或者crash的情况下,也会有可能无法达到互斥的目的。

假设有三个节点ABC:

  • 进程1在B和C上拿到了锁
  • 这时候B crash了
  • 如果B没有Slave节点,那么B会重启,如果数据还没备份,那么重启后B上的锁就丢了
  • 又或者B有Slave节点,但是crash时,Master B的数据还没同步到Slave,Slave被提拔为Master
  • 不管有没有Slave,其他进程都有可能在Bcrash掉之后,在B上拿到锁,再加上在A拿到的锁,就可以拿到超过半数的锁,这样就有两个进程同时拿到了锁,互斥性被打破

对于上面这个问题,Redis的作者,同时也是Redlock的作者antirez,提出了delay的解决方案,就是让B别那么快重启,稍微等一下,等的时间,就是分布式锁的最大过期时间,等到其他节点上的锁都过期了,你再重启,对外提供服务。

对于有Slave的情况,也可以用类似的方案,Slave先别那么快接替Master,稍微等一下下。

6、会不会有锁饥饿的问题?

还是三台Master节点,现在有三个进程同时要加同一把锁,会不会出现每次都是一个进程抢到一把锁的情况?

这是有可能的。

解决办法1:
获取锁失败后,随机休息一段时间

解决办法2:
如果客户端在发现,就算后面全部的锁,都被我抢到,加起来也不能超过半数,这时候就不再继续往下抢。

举个例子,进程1抢到了节点A的锁,进程2抢到节点B的,这时候进程3想过来抢锁,按照ABC的顺序,逐个抢,A和B都抢不过别人,于是掐指一算,就算C让我抢到了,我也抢不到超过半数了,没必要继续抢了,我还是先尝试抢一下A吧。

这样就不会出现三把锁,分别被三个不同的进程抢的情况了。

Redisson(一个Java的redis客户端)在实现redlock时就采用了这个解决方案。

RedissonMultiLock line248:

现在让我们回过头来看第2个问题,获取了过半数的锁之后,还要不要继续获取?

之前说了,不继续获取可以提高速度,但是贪心点继续获取也并非一无是处,比如你已经获取了A和B,如果把C也获取了,那么就算后面A挂掉了,别人也最多只能从恢复过来的A上获取到锁,还是拿不到超过半数的。

Redlock实现:Redisson

上面讲的只是Redlock的算法,具体怎么用代码来实现,可以看redlock各种语言的客户端源码,比如Java的实现,就可以看看Redisson。

我在看的过程中,就发现redisson在释放锁的时候,只是释放了成功获取到的锁。
RedissonMultiLock line248:

当然,或许redisson有其他考虑,这个还不得而知。

针对这个疑惑,给redisson提了个issuse(https://github.com/redisson/redisson/issues/1606),不过还没有答复 ( ̄▽ ̄)/

参考

【转】Redis学习笔记(五)如何用Redis实现分布式锁(2)—— 集群版的更多相关文章

  1. Hadoop入门学习笔记-第一天 (HDFS:分布式存储系统简单集群)

    准备工作: 1.安装VMware Workstation Pro 2.新建三个虚拟机,安装centOS7.0 版本不限 配置工作: 1.准备三台服务器(nameNode10.dataNode20.da ...

  2. Redis学习笔记(2)——Redis的下载安装部署

    一.下载Redis Redis的官网下载页上有各种各样的版本,如图 但是官网下载的Redis项目不正式支持Windows.如果需要再windows系统上部署,要去GitHub上下载.我下载的是Redi ...

  3. Redis学习笔记(3)——Redis的命令大全

    Redis是一种nosql数据库,常被称作数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) 和 有序集合(sorted se ...

  4. Redis学习笔记(1)——Redis简介

    一.Redis是什么? Remote Dictionary Server(Redis) 是一个开源的使用ANSI C语言编写.遵守BSD协议.支持网络.可基于内存亦可持久化的日志型.Key-Value ...

  5. Redis学习笔记(三)Redis支持的5种数据类型的总结

    继续Redis学习笔记(二)来说说剩余的三种数据类型. 三.列表类型(List) 1.介绍 列表类型可以存储一个有序的字符串列表,常用的操作是向列表两端添加元素,或者获得列表的一段片段.列表类型内部是 ...

  6. Redis学习笔记(二) Redis 数据类型

    Redis 支持五种数据类型:string(字符串).list(列表).hash(哈希).set(集合)和 zset(有序集合),接下来我们讲解分别讲解一下这五种类型的的使用. String(字符串) ...

  7. Redis学习笔记(二)Redis支持的5种数据类型的总结之String和Hash

    引言 在Redis学习笔记(一)中我们已经会安装并且简单使用Redis了,接下来我们一起来学习下Redis支持的5大数据类型. 简介 Redis是REmote DIctionary Server(远程 ...

  8. Redis 学习笔记系列文章之 Redis 的安装与配置 (一)

    1. 介绍 Redis is an open source (BSD licensed), in-memory data structure store, used as database, cach ...

  9. Kubernetes 学习笔记(二):本地部署一个 kubernetes 集群

    前言 前面用到过的 minikube 只是一个单节点的 k8s 集群,这对于学习而言是不够的.我们需要有一个多节点集群,才能用到各种调度/监控功能.而且单节点只能是一个加引号的"集群&quo ...

  10. StackExchange.Redis学习笔记(五) 发布和订阅

    Redis命令中的Pub/Sub Redis在 2.0之后的版本中 实现了 事件推送的  发布订阅命令 以下是Redis关于发布和订阅提供的相关命令 SUBSCRIBE channel [channe ...

随机推荐

  1. 【English】五、颜色相关

    一.常见颜色 黑色    black    白色    white    蓝色    blue    橙色    orange    黄色    yellow        灰色    gray   ...

  2. Jar 初步

    前言 jar 是 java 文件中一种文件格式,用于将 .java 文件编译的字节码文件打包成 jar. 给 Java 应用打包 1. 新建一个 java 源文件 package cn.szxy; p ...

  3. python内存回收的问题

    python实际上,对于占用很大内存的对象,并不会马上释放. 举例,a=range(10000*10000),会发现内存飙升一个多G,del a 或者a=[]都不能将内存降下来.. del 可以删除多 ...

  4. 64位Win7下Asp.net项目连接Oracle时报ORA-6413:连线未打开异常

    当时小弟碰到这个问题的时候,也找了挺久的回答,但是回答都是模棱两可的说是因为()的问题,但是没有给出具体的解决方案,这里小弟就用一个比较笨的方法来解决这个问题. 第一种:就是使用本地IISWeb服务器 ...

  5. fastjson SerializerFeature详解

  6. vue源码分析—Vue.js 源码构建

    Vue.js 源码是基于 Rollup 构建的,它的构建相关配置都在 scripts 目录下.(Rollup 中文网和英文网) 构建脚本 通常一个基于 NPM 托管的项目都会有一个 package.j ...

  7. arts打卡第二周

    Algorithm 旋转数组 Given an array, rotate the array to the right by k steps, where k is non-negative. Ex ...

  8. Redis进阶之使用Lua脚本开发

    1.在Redis中使用Lua 在Redis中执行Lua脚本有两种方法:eval和evalsha. (1)eval eval 脚本内容 key个数 key列表 参数列表 下面例子使用了key列表和参数列 ...

  9. Socket网络编程(案例)

    Socket:套接字 java.net包 1.流式套接字:基于TCP协议的Socket网络编程 工作方式: 1.客户端A连接到服务器: 2.服务器建立连接并把客户端A添加到列表: 3.客户端B.C.. ...

  10. Element ui 日期限制范围

    时间限定范围: <el-date-picker type="date" placeholder="选择日期" v-model="addForm. ...