Redis与分布式锁的问题已经是老生常谈了,本文尝试总结一些Redis、Zookeeper实现分布式锁的常用方案,并提供一些比较好的实践思路(基于Java)。不足之处,欢迎探讨。

Redis分布式锁

单机Redis下实现分布式锁

方案1:使用SET命令。

假如当前客户端需要占有一个user_lock的锁,它首次需要生成一个token(一个随机字符串,例如uiid),并使用该token进行加锁。

加锁命令:

redis> SET user_lock <token> EX 15 NX
OK

EX:该键会在指定时间后指定过期,单位为秒,类似参数还有PX、EXAT、PXAT。

NX:只有该键不存在的时候才会设置key的值。

所以如果user_lock键不存在,上面Redis命令会成功创建该Redis键,并设置该键在15秒后过期。

而其他客户端也使用该命令进行加锁,在这15秒时间内,其他客户端加锁失败(NX参数保证了该Redis键存在时命令执行失败)。

所以,当前客户端中锁定了user_lock,锁的有效时间为15秒。

为什么要使用token、有效期呢?有以下原因:

(1)锁的有效期可以保证不会发生死锁的情况。通常占有锁的客户端操作完成后需要释放锁(删除Redis键),使用锁有效期后,即使占有锁的客户端故障下线,15秒后锁也会自动失效,其他客户端就可以抢占该锁,不会出现死锁的情况。

(2)token的作用是防止客户端释放了不是自己占有的锁。客户端释放锁时需要检测该锁当前是否为自己所占有,即键user_lock的值是否为自己的token,如果是才可以删除该键。

这里涉及两个命令,可以lua脚本保证原子性。如下面命令:

> EVAL "if redis.call('GET',KEYS[1]) == ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 user_lock <token>
(integer) 1

如果不使用token,所以客户端都使用同一个值作为键user_lock的值,假如客户端A占有了锁user_lock,但由于过期时间到了,user_lock键被Redis服务器删除,这时客户端B占有了锁。而客户端A操作后,直接使用DEL命令删除当前user_lock键,这样客户端A就删除了非自己占有的锁。

该方案可参考官方文档:https://redis.io/commands/set

从上面内容可以看到,该方案的分布式锁并不是安全的,占有锁的客户端将在锁有效时间过后自动失去锁,这时其他客户端就可以占有该锁,这样将出现两个客户端同时占有一个锁的情况,分布式锁失效了。

所以,该方案锁的有效时间就非常重要,锁的有效时间设置过短,可能会出现分布式锁失效的情况,而有效时间设置过长,那么占有锁的客户端下线后,其他客户端仍然要无效等待较长时间才可以占有该锁,性能较差。

有没有更好一点的方案?我们看一下方案2。

方案2:自动延迟锁有效时间。

我们可以在一开始给锁设置一个较短的有效时间,并启动一个后台线程,在该锁失效前,主动延迟该锁的有效时间,

例如,在一开始时给锁设置有效时间为10秒,并启动一个后台线程,每隔9秒,就将锁的过期时间修改为当前时间10秒后。

示意代码如下:

new Thread(new Runnable() {
public void run() {
while(lockIsExist) {
redis.call("EXPIRE user_lock 10");
Thread.sleep(1000 * 9);
}
}
}).start();

这样就可以保证当前占用客户端的锁不会因为时间到期而失效,避免了分布式锁失效的问题,并且如果当前客户端故障下线,由于没有后台线程定时延迟锁有效时间,该锁也会很快自动失效。

提示;当前客户端释放锁的时候,需要停止该后台线程或者修改lockIsExist为false。

Java客户端Redisson提供了该方案,使用非常方便。

下面介绍一下如何示意Redisson实现Redis分布式锁。

(1)添加Redisson引用。

<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.4</version>
</dependency>

(2)使用示例如下:

Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient redissonClient = Redisson.create(config); RLock lock = redissonClient.getLock("user_lock");
lock.lock();
try {
// process...
} finally {
lock.unlock();
}

如果没有特殊原因,建议直接使用Redisson提供的分布式锁。

但这种方式就一定安全吗?

大家考虑这样一种场景,假如获得锁的客户端因为CPU负载过高或者GC等原因,负责延迟锁过期时间的线程没法按时获得CPU去执行任务,则同样会出现锁失效的场景。



该场景暂时没有比较好的处理方案,也不展开。

Sentinel、Cluster模式下实现分布式锁

实际生产环境中比较少使用单节点的Redis,通常会部署Sentinel、Cluster模型部署Redis集群,Redis在这两种模式下线实现分布式锁会有一个很麻烦的问题了。

为了保证高性能,Redis主从同步使用的是异步模式,就是说Redis主节点返回SET命令成功响应时,Redis从节点可能还没有同步该命令。

如果这时主节点故障下线了,那么就会出现以下情况:

(1)Sentinel、Cluster模式会选举一个从节点成为新主节点,而这个主节点是没有执行SET命令的。也就是说这时客户端并没有占有锁。

(2)客户端收到(之前主节点返回的)SET命令的成功响应,以为自己占有锁成功。

这时其他客户端也请求这个锁,也能占有这个锁,这时就会出现分布式锁失效的情况。

出现这个情况的本质是Redis使用了异步复制的方式同步主从节点数据,并不严格保证主从节点数据的一致性。

对此,Redis作者提出了RedLock算法,大概方案是部署多个单独的Redis主节点,并将SET命令同时发送到多个节点,当收到半数以上Redis主节点返回成功后,则认为加锁成功。

这种机制感觉与分布式一致性算法(如Raft算法)中利用的“Quorum机制”基本一致吧。

关于该方案是否能真正保证分布式锁安全,Redis作者与另一位大佬Martin爆发了热烈的讨论,本文偏向实战内容,这里不一一展示RedLock算法细节。

即使该算法可以真正保证分布式锁安全,如果你要使用该方案,也很麻烦,需要另外部署多个Redis主节点,还需要支持该算法的可靠的客户端。考虑这些情况,如果在严格要求分布式锁安全的情况,使用ZooKeeper、Etcd等严格保证数据一致性的组件更合适。

Zookeeper分布式锁

Zookeeper由于保证集群数据一致,并自带Watch,客户端过期失效检测等机制,非常适合实现分布式锁。

Zookeeper实现分布式锁的方式很简单,客户端通过创建临时节点来锁定分布式锁,如果创建成功,则加锁成功,否则,说明该锁已经被其他客户端锁定,这时当前客户端监听该临时节点变化,如果该临时节点被删除,则可以再次尝试锁定该分布式锁。

虽然ZooKeeper实现分布锁的不同方案细节不同,但整体基本基于该方案进行扩展。

这里推荐使用Curator框架(Netflix提供的ZooKeeper客户端)实现分布式锁,非常方便。

下面介绍一下Curator的使用。

(1)引入Curator引用

<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>3.3.0</version>
</dependency> <dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>3.3.0</version>
</dependency>

注意Curator版本与ZooKeeper版本对应。

(2)使用InterProcessMutex类实现分布式锁。

CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", 60000, 15000,
new ExponentialBackoffRetry(1000, 3));
client.start();
InterProcessMutex lock = new InterProcessMutex(client, "/user_lock");
lock.acquire();
try {
// process...
} finally {
lock.release();
}

Curator支持多种分布式锁,非常全面:

  • InterProcessMutex:可重入排它锁,例子展示就是这种锁。
  • InterProcessSemaphoreMutex:不可重入排它锁。
  • InterProcessReadWriteLock:分布式读写锁。

    使用方式也非常简单,这里不一一展开。

那么Zookeeper实现分布式锁一定安全吗?

假如客户端Client1在ZooKeeper中加锁成功,即成功创建了临时ZK节点,但Client1由于GC长时间没有响应ZooKeeper的心跳检测请求,ZooKeeper将Client1判断为失效,从而将临时ZK节点,这时客户端Client2请求加锁就可以成功加锁。那么这时就会出现Client1、Client2同时占有一个分布式锁,即分布式锁失效。

该场景与上面说的Redis延迟线程没有按时执行的场景有点类型,该场景展示也没有较好的解决方案。

虽然理论上ZooKeeper存在分布式锁失效的可能,但发生的概率应该比较,也可以通过增加ZooKeeper判断客户端的时间来减少这种场景,所以ZooKeeper分布式锁是可以满足绝大数要求分布式锁的场景的。

总结一下:

(1)

如果不严格要求分布式锁安全,可以考虑在Sentinel、Cluster模式下使用redis实现分布式锁。例如多个客户端同时获取锁并不会导致严重的业务问题,或者只是要求性能优化避免多个客户端同时操作等场景,都可以使用Redisson提供的分布式锁。

(2)如果严格要求分布式锁安全,则可以使用ZooKeeper、Etcd等组件实现分布式锁。

当然,建议使用Redisson、Curator等成熟框架实现分布式锁,避免重复编码,也减少错误风险。

如需系统学习Redis,可参考作者新书《Redis核心原理与实践》。本书通过深入分析Redis 6.0源码,总结了Redis核心功能的设计与实现。通过阅读本书,读者可以深入理解Redis内部机制及最新特性,并学习到Redis相关的数据结构与算法、Unix编程、存储系统设计,分布式系统架构等一系列知识。

经过该书编辑同意,我会继续在个人技术公众号(binecy)发布书中部分章节内容,作为书的预览内容,欢迎大家查阅,谢谢。

语雀平台预览:《Redis核心原理与实践》

Redis、Zookeeper实现分布式锁——原理与实践的更多相关文章

  1. 《从Paxos到Zookeeper:分布式一致性原理与实践》【PDF】下载

    内容简介 Paxos到Zookeeper分布式一致性原理与实践从分布式一致性的理论出发,向读者简要介绍几种典型的分布式一致性协议,以及解决分布式一致性问题的思路,其中重点讲解了Paxos和ZAB协议. ...

  2. 《从Paxos到Zookeeper:分布式一致性原理与实践》第一章读书笔记

    第一章主要介绍了计算机系统从集中式向分布式系统演变过程中面临的挑战,并简要介绍了ACID.CAP和BASE等经典分布式理论,主要包含以下内容: 集中式的特点 分布式的特点 分布式环境的各种问题 ACI ...

  3. 一般实现分布式锁都有哪些方式?使用redis如何设计分布式锁?使用zk来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?

    #(1)redis分布式锁 官方叫做RedLock算法,是redis官方支持的分布式锁算法. 这个分布式锁有3个重要的考量点,互斥(只能有一个客户端获取锁),不能死锁,容错(大部分redis节点创建了 ...

  4. 关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁

    首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在j ...

  5. 利用多写Redis实现分布式锁原理与实现分析(转)

    利用多写Redis实现分布式锁原理与实现分析   一.关于分布式锁 关于分布式锁,可能绝大部分人都会或多或少涉及到. 我举二个例子:场景一:从前端界面发起一笔支付请求,如果前端没有做防重处理,那么可能 ...

  6. 女朋友也能看懂的Zookeeper分布式锁原理

      前言 关于分布式锁,在互联网行业的使用场景还是比较多的,比如电商的库存扣减,秒杀活动,集群定时任务执行等需要进程互斥的场景.而实现分布式锁的手段也很多,大家比较常见的就是redis跟zookeep ...

  7. zookeeper(4)--zookeeper分布式锁原理

    目前几乎很多大型网站及应用都是分布式部署的,分布式场景中的数据一致性问题一直是一个比较重要的话题.分布式的CAP理论告诉我们“任何一个分布式系统都无法同时满足一致性(Consistency).可用性( ...

  8. 【连载】redis库存操作,分布式锁的四种实现方式[一]--基于zookeeper实现分布式锁

    一.背景 在电商系统中,库存的概念一定是有的,例如配一些商品的库存,做商品秒杀活动等,而由于库存操作频繁且要求原子性操作,所以绝大多数电商系统都用Redis来实现库存的加减,最近公司项目做架构升级,以 ...

  9. Redis分布式锁原理

    1. Redis分布式锁原理 1.1. Redisson 现在最流行的redis分布式锁就是Redisson了,来看看它的底层原理就了解redis是如何使用分布式锁的了 1.2. 原理分析 分布式锁要 ...

随机推荐

  1. Azure Bicep(三)变量控制

    一,引言 当我们在使用 Azure Bicep 的时候会出现以下几个问题: 1)文件中有很多地方会重用很多相同的值 2)输入参数可以在统一的地方进行修改 带着这些问题,我们开始今天的内容,学习如何在 ...

  2. 图解Redis6中的9种数据结构,墙裂建议准备去面试的人先看(干货,建议收藏)

    如图所示,Redis中提供了9种不同的数据操作类型,他们分别代表了不同的数据存储结构. 图2-17 数据类型 String类型 String类型是Redis用的较多的一个基本类型,也是最简单的一种类型 ...

  3. 【UE4】GAMES101 图形学作业4:贝塞尔曲线

    总览 Bézier 曲线是一种用于计算机图形学的参数曲线. 在本次作业中,你需要实现de Casteljau 算法来绘制由4 个控制点表示的Bézier 曲线(当你正确实现该算法时,你可以支持绘制由更 ...

  4. 80. 删除有序数组中的重复项 II

    题目 给你一个有序数组 nums ,请你原地删除重复出现的元素(不需要考虑数组中超出新长度后面的元素),使每个元素最多出现两次 ,返回删除后数组的新长度. 不要使用额外的数组空间,你必须在原地修改输入 ...

  5. Java RMI学习与解读(一)

    Java RMI学习与解读(一) 写在前面 本文记录在心情美丽的一个晚上. 嗯.就是心情很美丽. 那为什么晚上还要学习呢? emm... 卷... 卷起来. 全文基本都是根据su18师傅和其他师傅的文 ...

  6. 为什么用于开关电源的开关管一般用MOS管而不是三极管

    区别: 1.MOS管损耗比三极管小,导通后压降理论上为0. 2.MOS管为电压驱动型,只需要给电压即可,意思是即便串入一个100K的电阻,只要电压够,MOS管还是能够导通. 3.MOS管的温度特性要比 ...

  7. Go语言核心36讲(Go语言进阶技术十一)--学习笔记

    17 | go语句及其执行规则(下) 知识扩展 问题 1:怎样才能让主 goroutine 等待其他 goroutine? 我刚才说过,一旦主 goroutine 中的代码执行完毕,当前的 Go 程序 ...

  8. python mysqlclient安装失败 Command "python setup.py egg_info" failed with error code 1

    python2 python3 中代码 pip install mysqlclient 都安装失败的话, 很有可能是你的操作系统中没有安装mysql 如果确定已经安装了,请忽略下面的内容. Ubunt ...

  9. AtCoder Grand Contest 055题解

    我太菜啦!!!md,第一题就把我卡死了...感觉对构造题不会再爱了... A - ABC Identity 先来看这个题吧,题意就是给定你一个字符串,让你将这个字符串最多分成6个子串,使得每个字符都在 ...

  10. cf12D Ball(MAP,排序,贪心思想)

    题意: N位女士一起聚在一个舞厅.每位女士有三个特征值B,I,R.分别代表美貌,智慧,富有. 对于一位女士而言,如果存在一个女士的B,I,R都分别大于她自己的B,I,R.则她自己会自杀. 统计总共有多 ...