redis分布锁 操作redis 超时设置了 还释放锁吗

&& 在高并发的使用场景下,如何让redis里的数据尽量保持一致,可以采用分布式锁。以分布式锁的方式来保证对临界资源的互斥读写。
&& redis使用缓存作为分布式锁,性能非常强劲,在一些不错的硬件上,redis可以每秒执行10w次,内网延迟不超过1ms,足够满足绝大部分应用的锁定需求。
&& redis常用的分布式锁的实现方式:
一、setbit / getbit
&& 用索引号为0的第一个比特位来表示锁定状态,其中:0表示未获得锁,1表示已获得锁。
&& 优势:简单;
&& 劣势:竞态条件(race condition),死锁。
&& 获得锁的过程至少需要两步:先getbit判断,后setbit上锁。由于不是原子操作,因此可能存在竞态条件;如果一个客户端使用setbit获取到锁,然后没来得及释放crash掉了,那么其他在等待的客户端将永远无法获得该锁,进而形成了死锁。所以这种形式不太适合实现分布式锁。
二、setnx / del / getset
& redis官网有一篇文章专门谈论了实现分布式锁的话题。基本的原则是:采用setnx尝试获取锁并判断是否获得了锁,setnx设置的值是它想占用锁的时间(预估):
如返回1,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定,该客户端最后可以通过DEL lock.foo来释放该锁。
如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。
& 通过del删除key来释放锁。某个想获得锁的客户端,先采用setnx尝试获取锁,如果获取失败了,那么会通过get命令来获得锁的过期时间以判断该锁的占用是否过期。如果跟当前时间对比,发现过期,那么先执行del,然后执行setnx获取锁。如果整个流程就这样,可能会产生死锁,请参考下面的执行序列:
&& 所以,在高并发的场景下,如果检测到锁过期,不能简单地进行del并尝试通过setnx获得锁。我们可以通过getset命令来避免这个问题。来看看,如果存在一个用户user4,它通过调用getset命令如何避免这种情况的发生:
&getset设置的过期时间跟上面的setnx设置的相同:
&& 如果该命令返回的结果跟上一步通过get获得的过期时间一致,则说明这两步之间,没有新的客户端抢占了锁,则该客户端即获得锁。如果该命令返回的结果跟上一步通过get获得的过期时间不一致,则该锁可能已被其他客户端抢先获得,则本次获取锁失败。
& &这种实现方式得益于getset命令的原子性,从而有效得避免了竞态条件。并且,通过将比对锁的过期时间作为获取锁逻辑的一部分,从而避免了死锁。
三、setnx / del / expire
&&&这是使用最多的实现方式:setnx的目的同上,用来实现尝试获取锁以及判断是否获取到锁的原子性,del删除key来释放锁,与上面不同的是,使用redis自带的expire命令来防止死锁(可能出现某个客户端获得了锁,但是crash了,永不释放导致死锁)。这算是一种比较简单但粗暴的实现方式:因为,不管实际的情况如何,当你设置expire之后,它一定会在那个时间点删除key。如何当时某个客户端已获得了锁,正在执行临界区内的代码,但执行时间超过了expire的时间,将会导致另一个正在竞争该锁的客户端也获得了该锁,这个问题下面还会谈到。
&&我们来看一下宿舍锁的简单实现很简单:
通过一个while(true),在当前线程上进行阻塞等待,并通过一个计数器进行自减操作,防止永久等待。&
阅读(...) 评论()分布锁——redis实现 - 简书
分布锁——redis实现
分布式锁的场景
首先在读文章之前,我们要考虑一个问题,为什么要用分布式锁,也就是什么场景下要用分布式锁?
假如我们有一个抢购业务,之前是单机的时候我们可以用程序锁,扩展到了多个服务节点的时候,那我无法再继续使用lock sync等程序的锁来控制并发中可能会造成的超卖。
这时候我们就该引入一个分布式锁来解决这个问题,当然上面的例子有更好的解决办法,这里仅仅提供一个分布式锁的场景引入。
设计一个分布式锁的要素
OK,我们知道什么情况下用分布式锁了之后,我们要考虑下,如果让我们设计一个分布式锁,要考虑哪些问题?
第一点,既然是锁,那么我要确保这个锁在整个集群中唯一性第二点,我要确保我某个获取到锁的节点挂掉之后不会因为无法释放而产生死锁的问题第三点,我要确保我的锁不会被其他的节点误操作而错误的解锁第四点,我们要考虑我们的锁其他的竞争线程,如何在持有锁的节点释放之后快速的受到通知重新竞争锁第五点,就是锁的性能效率第六点,就是锁的可重入性(这里提一下,对于大多数程序和业务来讲是没必要实现这个功能的)
ok,上面就是我们做一个分布式锁应该注意的地方,当然,这里说的情况并不是很全面,但是基本上已经足够大多数的业务使用了,那么我们带着上面的这些注意的点,一起去看一下怎么实现一个分布式锁。
构建分布式锁
锁的唯一性问题
大家应该都知道redis里有个过期时间的概念,也就是expire这个api可以设置一个key的过期时间,那么利用这个功能我们可以设置一个最大值,避免死锁的问题。
那么说道这里,大家可能跟我之前一样,会考虑到一个问题?这个过期时间设置为多少好呢,如果设置太小了,会造成业务没操作完,锁就提前被其他线程获取了,如果设置太大了,又可能比死锁没好多少。
redisson是怎么解决这个问题的?
redisson默认是设置一个key的过期时间为30秒,那么大家可能想,这也没区别啊!如果看过redisson源码的应该注意到他用了netty,那么他用netty干嘛了?他用netty做了这样的一个事,他给每个上锁的操作都加了一个事件。
什么样的事件?如果我一个上锁操作,上锁失败了,就订阅锁,直到收到通知,否则就暂时等待,这里他是利用java用的信号量来实现的,如果有兴趣的可以看一下他的具体代码。那么如果上锁成功了呢?他会开启一个异步线程,等待通知,这个通知可以是这样的:如果我收到的通知是,我工作完了,要释放锁了。那么这时候他就把这个异步线程从工作者队列中干掉。那么,如果我没有收到通知呢?这一步其实就是redisson的关键实现
redisson锁的代码
如果我没收到通知,我每隔离10s会调用一次这个事件,判断一下过期时间,然后给这个持有锁的线程的key,也就是当前锁,重新设置上为30秒的过期时间,也就说,即使我这台机器挂掉了,那我这个机器持有的锁最多会保留30s的“死锁”时间。
如果我有一堆远程调用,30s根本不够用呢?
没关系,每隔10s你的过期时间都会更新为30s。也就是一直到你释放锁。当然,如果你害怕你的业务会发送阻塞而造成了锁的一直持有的"假死锁"情况,那怎么办?redisson提供了lockInterruptibly(long leaseTime, TimeUnit unit)的时间限制哈。也就是你在多少秒之内如果没完成任务也会自动释放这个锁。
我怎么要确保我的锁不会被其他的节点误操作而错误的解锁呢?
这个其实很好解决,一般对于一个锁来讲,都是需要一个onwer的标示,对于大多数的做法:都是使用uuid+ThreadId,然后操作线程保留这个onwer标示,在set的时候吧这个owner的标示放到value中,解锁的时候判断这个owner标示。
note:一般来讲这个owner标示还起着做重入的时候的作用.
解锁后的快速通知
这里其实是有两种做法:
第一种,类似本地锁的不断重试(自旋)。
第二种方式,也就是redlock的实现RedisSon的做法,pub/sub的方式
自旋如何实现
我原来做这一块的时候利用locksupport的park来做了短暂时间的暂停,再暂停之后不断的重试获取锁。
但是这样就会有这样的几个问题:
锁的通知被释放的时候我无法及时的收到通知,并且这个能获取锁的机会有可能就看运气了,也就是说谁暂停完之后重试的时间正好是我释放的时间,也就是无法实现公平锁(按申请锁的顺序来获取锁)
redis毕竟是网络的,无论是网络抖动的影响还是自身这种不断发请求来讲,都是很大的开销,性能上烂到爆
但是上面的锁翩翩适用一种常见,业务操作比较简单短暂,不会出太多问题,耗时比较短,需要简单的锁模型
pub/sub如何实现
相信了解过redis的都知道它有个发布订阅的功能RedisSon是这么实现快速通知的:
获取锁的线程会去redis中发布一个key,然后所有没有取到锁的就去订阅相应的channel,来接收锁释放的通知,获取锁的释放了之后就会去这里发布释放的通知。收到消息的就会继续重试获取锁的过程
首先,大家都知道,一个分布式锁可以基于zk和redis来实现。但是rediss做分布式锁的效率要比zk高上很多很多倍,因为zk是基于文件系统的实现,而redis是基于内存的操作实现。而且zk做分布式锁的时候还会有可能因为网络抖动的问题发生锁被误释放的问题(这里我们暂时不讨论)
可冲入特性
我这里简单的说一下redisson是怎么实现的:
redisson是吧获取锁的行为变成了一次hashset的操作
redisson锁的代码
这里就是核心的实现:
上面lua代码中第一个if就是先尝试获取锁,如果获取成功就返回,如果不成功就判断要获取锁的线程,和持有锁的线程是否是同一个线程,如果是,那就在value上加个1,代表重入了一次,最后那个return的pttl其实是一个else逻辑,也就是说我既没有获取锁,也不是持有锁的那个线程,也就意味我获取锁失败,那我就返回一个过期时间的值
Redisson的流程简述
redisson的整个过程简介:利用lua在redis中的原子性,获取锁保证唯一性,在value中加上标示防止误解锁,不断的叠加expire来保证持有锁的时候不会被误拿到,利用redis的pub/sub来即使的通知锁的释放,利用Semaphore来实现没有获取锁的线程的等待。
要思考的问题
自旋锁 然后 自旋锁会导致饥饿 就开始使用阻塞,然后 阻塞会导致CPU等资源空置 就开始使用异步解耦(最常见的就是做完了,通知的方式),其实整个过程跟IO的几种模式很像 从BIO到NIO到AIO的整个过程,然后大家看到发布订阅这种通知的模式了。。但如果发布订阅模式突然挂了,你的线程可能永远不会醒来了?这里我还没有完全的关注到这个点,日后有机会吧这个点看一下然后补充上来。
乐于分享,喜欢技术让天下没有难学的技术
《Redis官方文档》用Redis构建分布式锁
《Redis官方文档》用Redis构建分布式锁
校对:方腾飞(红体标记重点)
用Redis构建分布式锁
在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段。 有很多三方库和文章描述如何用Redis实现一个分布式锁管理器,但是这些库实现的方式差别很大,而且很多简单的实现其实只需采用稍微增加一点复杂的设计就可以获得更好的可靠性。 这篇文章的目的就是尝试提出一种官方权威的用Redis实现分布式锁管理器的算法,我们把这个算法称为RedLock,我们相信这个算法会比一般的普通方法更加安全可靠。我们也希望社区能一起分析这个算法,提供一些反馈,然后我们以此为基础,来设计出更加复杂可靠的算法,或者更好的新算法。
在描述具体的算法之前,下面是已经实现了的项目可以作为参考:
(Ruby实现)。还有一个Redlock-rb的分支,添加了一些特性使得实现分布式锁更简单
(Python 实现).
(PHP 实现).
(PHP 更完整的实现)
(Go 实现).
(Java 实现).
(Perl 实现).
(C++ 实现).
(C#/.NET 实现).
(NodeJS 实现). Includes support for lock extension.
安全和可靠性保证
在描述我们的设计之前,我们想先提出三个属性,这三个属性在我们看来,是实现高效分布式锁的基础。
安全属性:互斥,不管任何时候,只有一个客户端能持有同一个锁。
效率属性A:不会死锁,最终一定会得到锁,就算一个持有锁的客户端宕掉或者发生网络分区。
效率属性B:容错,只要大多数Redis节点正常工作,客户端应该都能获取和释放锁。
为什么基于故障切换的方案不够好
为了理解我们想要提高的到底是什么,我们先看下当前大多数基于Redis的分布式锁三方库的现状。 用Redis来实现分布式锁最简单的方式就是在实例里创建一个键值,创建出来的键值一般都是有一个超时时间的(这个是Redis自带的超时特性),所以每个锁最终都会释放(参见前文属性2)。而当一个客户端想要释放锁时,它只需要删除这个键值即可。 表面来看,这个方法似乎很管用,但是这里存在一个问题:在我们的系统架构里存在一个单点故障,如果Redis的master节点宕机了怎么办呢?有人可能会说:加一个slave节点!在master宕机时用slave就行了!但是其实这个方案明显是不可行的,因为这种方案无法保证第1个安全互斥属性,因为Redis的复制是异步的。 总的来说,这个方案里有一个明显的竞争条件(race condition),举例来说:
客户端A在master节点拿到了锁。
master节点在把A创建的key写入slave之前宕机了。
slave变成了master节点 4.B也得到了和A还持有的相同的锁(因为原来的slave里还没有A持有锁的信息)
当然,在某些特殊场景下,前面提到的这个方案则完全没有问题,比如在宕机期间,多个客户端允许同时都持有锁,如果你可以容忍这个问题的话,那用这个基于复制的方案就完全没有问题,否则的话我们还是建议你采用这篇文章里接下来要描述的方案。
采用单实例的正确实现
在讲述如何用其他方案突破单实例方案的限制之前,让我们先看下是否有什么办法可以修复这个简单场景的问题,因为这个方案其实如果可以忍受竞争条件的话是有望可行的,而且单实例来实现分布式锁是我们后面要讲的算法的基础。 要获得锁,要用下面这个命令: SET resource_name my_random_value NX PX 30000 这个命令的作用是在只有这个key不存在的时候才会设置这个key的值(NX选项的作用),超时时间设为30000毫秒(PX选项的作用) 这个key的值设为“my_random_value”。这个值必须在所有获取锁请求的客户端里保持唯一。 基本上这个随机值就是用来保证能安全地释放锁,我们可以用下面这个Lua脚本来告诉Redis:删除这个key当且仅当这个key存在而且值是我期望的那个值。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
这个很重要,因为这可以避免误删其他客户端得到的锁,举个例子,一个客户端拿到了锁,被某个操作阻塞了很长时间,过了超时时间后自动释放了这个锁,然后这个客户端之后又尝试删除这个其实已经被其他客户端拿到的锁。所以单纯的用DEL指令有可能造成一个客户端删除了其他客户端的锁,用上面这个脚本可以保证每个客户单都用一个随机字符串’签名’了,这样每个锁就只能被获得锁的客户端删除了。
这个随机字符串应该用什么生成呢?我假设这是从/dev/urandom生成的20字节大小的字符串,但是其实你可以有效率更高的方案来保证这个字符串足够唯一。比如你可以用RC4加密算法来从/dev/urandom生成一个伪随机流。还有更简单的方案,比如用毫秒的unix时间戳加上客户端id,这个也许不够安全,但是也许在大多数环境下已经够用了。
key值的超时时间,也叫做”锁有效时间”。这个是锁的自动释放时间,也是一个客户端在其他客户端能抢占锁之前可以执行任务的时间,这个时间从获取锁的时间点开始计算。 所以现在我们有很好的获取和释放锁的方式,在一个非分布式的、单点的、保证永不宕机的环境下这个方式没有任何问题,接下来我们看看无法保证这些条件的分布式环境下我们该怎么做。
Redlock算法
在分布式版本的算法里我们假设我们有N个Redis master节点,这些节点都是完全独立的,我们不用任何复制或者其他隐含的分布式协调算法。我们已经描述了如何在单节点环境下安全地获取和释放锁。因此我们理所当然地应当用这个方法在每个单节点里来获取和释放锁。在我们的例子里面我们把N设成5,这个数字是一个相对比较合理的数值,因此我们需要在不同的计算机或者虚拟机上运行5个master节点来保证他们大多数情况下都不会同时宕机。一个客户端需要做如下操作来获取锁:
1.获取当前时间(单位是毫秒)。
2.轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。
3.客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
4.如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。
5.如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁。
这个算法是否是异步的?
这个算法是基于一个假设:虽然不存在可以跨进程的同步时钟,但是不同进程时间都是以差不多相同的速度前进,这个假设不一定完全准确,但是和自动释放锁的时间长度相比不同进程时间前进速度差异基本是可以忽略不计的。这个假设就好比真实世界里的计算机:每个计算机都有本地时钟,但是我们可以说大部分情况下不同计算机之间的时间差是很小的。 现在我们需要更细化我们的锁互斥规则,只有当客户端能在T时间内完成所做的工作才能保证锁是有效的(详见算法的第3步),T的计算规则是锁失效时间T1减去一个用来补偿不同进程间时钟差异的delta值(一般只有几毫秒而已) 如果想了解更多基于有限时钟差异的类似系统,可以参考这篇有趣的文章:《》
失败的重试
当一个客户端获取锁失败时,这个客户端应该在一个随机延时后进行重试,之所以采用随机延时是为了避免不同客户端同时重试导致谁都无法拿到锁的情况出现。同样的道理客户端越快尝试在大多数Redis节点获取锁,出现多个客户端同时竞争锁和重试的时间窗口越小,可能性就越低,所以最完美的情况下,客户端应该用多路传输的方式同时向所有Redis节点发送SET命令。 这里非常有必要强调一下客户端如果没有在多数节点获取到锁,一定要尽快在获取锁成功的节点上释放锁,这样就没必要等到key超时后才能重新获取这个锁(但是如果网络分区的情况发生而且客户端无法连接到Redis节点时,会损失等待key超时这段时间的系统可用性)
释放锁比较简单,因为只需要在所有节点都释放锁就行,不管之前有没有在该节点获取锁成功。
安全性的论证
这个算法到底是不是安全的呢?我们可以观察不同场景下的情况来理解这个算法为什么是安全的。 开始之前,让我们假设客户端可以在大多数节点都获取到锁,这样所有的节点都会包含一个有相同存活时间的key。但是需要注意的是,这个key是在不同时间点设置的,所以这些key也会在不同的时间超时,但是我们假设最坏情况下第一个key是在T1时间设置的(客户端连接到第一个服务器时的时间),最后一个key是在T2时间设置的(客户端收到最后一个服务器返回结果的时间),从T2时间开始,我们可以确认最早超时的key至少也会存在的时间为MIN_VALIDITY=TTL-(T2-T1)-CLOCK_DRIFT,TTL是锁超时时间、(T2-T1)是最晚获取到的锁的耗时,CLOCK_DRIFT是不同进程间时钟差异,这个是用来补偿前面的(T2-T1)。其他的key都会在这个时间点之后才会超时,所以我们可以确定这些key在这个时间点之前至少都是同时存在的。
在大多数节点的key都set了的时间段内,其他客户端无法抢占这个锁,因为在N/2+1个客户端的key已经存在的情况下不可能再在N/2+1个客户端上获取锁成功,所以如果一个锁获取成功了,就不可能同时重新获取这个锁成功(不然就违反了分布式锁互斥原则),然后我们也要确保多个客户端同时尝试获取锁时不会都同时成功。 如果一个客户端获取大多数节点锁的耗时接近甚至超过锁的最大有效时间时(就是我们为SET操作设置的TTL值),那么系统会认为这个锁是无效的同时会释放这些节点上的锁,所以我们仅仅需要考虑获取大多数节点锁的耗时小于有效时间的情况。在这种情况下,根据我们前面的证明,在MIN_VALIDITY时间内,没有客户端能重新获取锁成功,所以多个客户端都能同时成功获取锁的结果,只会发生在多数节点获取锁的时间都大大超过TTL时间的情况下,实际上这种情况下这些锁都会失效 。 我们非常期待和欢迎有人能提供这个算法安全性的公式化证明,或者发现任何bug。
这个系统的性能主要基于以下三个主要特征:
1.锁自动释放的特征(超时后会自动释放),一定时间后某个锁都能被再次获取。
2.客户端通常会在不再需要锁或者任务执行完成之后主动释放锁,这样我们就不用等到超时时间会再去获取这个锁。
3.当一个客户端需要重试获取锁时,这个客户端会等待一段时间,等待的时间相对来说会比我们重新获取大多数锁的时间要长一些,这样可以降低不同客户端竞争锁资源时发生死锁的概率。
然而,我们在网络分区时要损失TTL的可用性时间,所以如果网络分区持续发生,这个不可用会一直持续。这种情况在每次一个客户端获取到了锁并在释放锁之前被网络分区了时都会出现。
基本来说,如果持续的网络分区发生的话,系统也会在持续不可用。
性能、故障恢复和fsync
很多使用Redis做锁服务器的用户在获取锁和释放锁时不止要求低延时,同时要求高吞吐量,也即单位时间内可以获取和释放的锁数量。为了达到这个要求,一定会使用多路传输来和N个服务器进行通信以降低延时(或者也可以用假多路传输,也就是把socket设置成非阻塞模式,发送所有命令,然后再去读取返回的命令,假设说客户端和不同Redis服务节点的网络往返延时相差不大的话)。
然后如果我们想让系统可以自动故障恢复的话,我们还需要考虑一下信息持久化的问题。
为了更好的描述问题,我们先假设我们Redis都是配置成非持久化的,某个客户端拿到了总共5个节点中的3个锁,这三个已经获取到锁的节点中随后重启了,这样一来我们又有3个节点可以获取锁了(重启的那个加上另外两个),这样一来其他客户端又可以获得这个锁了,这样就违反了我们之前说的锁互斥原则了。
如果我们启用AOF持久化功能,情况会好很多。举例来说,我们可以发送SHUTDOWN命令来升级一个Redis服务器然后重启之,因为Redis超时时效是语义层面实现的,所以在服务器关掉期间时超时时间还是算在内的,我们所有要求还是满足了的。然后这个是基于我们做的是一次正常的shutdown,但是如果是断电这种意外停机呢?如果Redis是默认地配置成每秒在磁盘上执行一次fsync同步文件到磁盘操作,那就可能在一次重启后我们锁的key就丢失了。理论上如果我们想要在所有服务重启的情况下都确保锁的安全性,我们需要在持久化设置里设置成永远执行fsync操作,但是这个反过来又会造成性能远不如其他同级别的传统用来实现分布式锁的系统。 然后问题其实并不像我们第一眼看起来那么糟糕,基本上只要一个服务节点在宕机重启后不去参与现在所有仍在使用的锁,这样正在使用的锁集合在这个服务节点重启时,算法的安全性就可以维持,因为这样就可以保证正在使用的锁都被所有没重启的节点持有。 为了满足这个条件,我们只要让一个宕机重启后的实例,至少在我们使用的最大TTL时间内处于不可用状态,超过这个时间之后,所有在这期间活跃的锁都会自动释放掉。 使用延时重启的策略基本上可以在不适用任何Redis持久化特性情况下保证安全性,然后要注意这个也必然会影响到系统的可用性。举个例子,如果系统里大多数节点都宕机了,那在TTL时间内整个系统都处于全局不可用状态(全局不可用的意思就是在获取不到任何锁)。
扩展锁来使得算法更可靠
如果客户端做的工作都是由一些小的步骤组成,那么就有可能使用更小的默认锁有效时间,而且扩展这个算法来实现一个锁扩展机制。基本上,客户端如果在执行计算期间发现锁快要超时了,客户端可以给所有服务实例发送一个Lua脚本让服务端延长锁的时间,只要这个锁的key还存在而且值还等于客户端获取时的那个值。 客户端应当只有在失效时间内无法延长锁时再去重新获取锁(基本上这个和获取锁的算法是差不多的) 然而这个并不会对从本质上改变这个算法,所以最大的重新获取锁数量应该被设置成合理的大小,不然性能必然会受到影响。
想提供帮助?
如果你很了解分布式系统的话,我们非常欢迎你提供一些意见和分析。当然如果能引用其他语言的实现话就更棒了。 谢谢!
原创文章,转载请注明: 转载自本文链接地址:
阿里程序员,熟悉Java、python,对并发编程、分布式系统、领域建模设计、OOD、FP感兴趣
Latest posts by yy-leo ()
Related posts:
(10 votes, average: 4.70 out of 5)
Loading...  Redis中的事务(transaction)是一组命令的集合。事务同命令一样都是Redis最小的执行单位,一个事务中的命令要么都执行,要么都不执行。Redis事务的实现需要用到&MULTI&和&EXEC&两个命令,事务开始的时候先向Redis服务器发送&MULTI&命令,然后依次发送需要在本次事务中处理的命令,最后再发送&EXEC&命令表示事务命令结束。
  举个例子,使用redis-cli连接redis,然后在命令行工具中输入如下命令:  
  从输出中可以看到,当输入MULTI命令后,服务器返回OK表示事务开始成功,然后依次输入需要在本次事务中执行的所有命令,每次输入一个命令服务器并不会马上执行,而是返回&QUEUED&,这表示命令已经被服务器接受并且暂时保存起来,最后输入EXEC命令后,本次事务中的所有命令才会被依次执行,可以看到最后服务器一次性返回了三个OK,这里返回的结果与发送的命令是按顺序一一对应的,这说明这次事务中的命令全都执行成功了。
  再举个例子,在命令行工具中输入如下命令:  
  和前面的例子一样,先输入MULTI最后输入EXEC表示中间的命令属于一个事务,不同的是中间输入的命令有一个错误(set写成了sett),这样因为有一个错误的命令导致事务中的其他命令都不执行了(通过后续的get命令可以验证),可见事务中的所有命令是同呼吸共命运的。
  如果客户端在发送EXEC命令之前断线了,则服务器会清空事务队列,事务中的所有命令都不会被执行。而一旦客户端发送了EXEC命令之后,事务中的所有命令都会被执行,即使此后客户端断线也没关系,因为服务器已经保存了事务中的所有命令。
  除了保证事务中的所有命令要么全执行要么全不执行外,Redis的事务还能保证一个事务中的命令依次执行而不会被其他命令插入。试想一个客户端A需要执行几条命令,同时客户端B发送了几条命令,如果不使用事务,则客户端B的命令有可能会插入到客户端A的几条命令中,如果想避免这种情况发生,也可以使用事务。 
Redis事务错误处理
  如果一个事务中的某个命令执行出错,Redis会怎样处理呢?要回答这个问题,首先要搞清楚是什么原因导致命令执行出错:
  1.语法错误:就像上面的例子一样,语法错误表示命令不存在或者参数错误,这种情况需要区分Redis的版本,Redis 2.6.5之前的版本会忽略错误的命令,执行其他正确的命令,2.6.5之后的版本会忽略这个事务中的所有命令,都不执行,就比如上面的例子(使用的Redis版本是2.8的)
  2.运行错误&运行错误表示命令在执行过程中出现错误,比如用GET命令获取一个散列表类型的键值。这种错误在命令执行之前Redis是无法发现的,所以在事务里这样的命令会被Redis接受并执行。如果食物里有一条命令执行错误,其他命令依旧会执行(包括出错之后的命令)。比如下例:  
  Redis中的事务并没有关系型数据库中的事务回滚(rollback)功能,因此使用者必须自己收拾剩下的烂摊子。不过由于Redis不支持事务回滚功能,这也使得Redis的事务简洁快速。
  回顾上面两种类型的错误,语法错误完全可以在开发的时候发现并作出处理,另外如果能很好地规划Redis数据的键的使用,也是不会出现命令和键不匹配的问题的。 
WATCH、UNWATCH、DISCARD命令
  从上面的例子我们可以看到,事务中的命令要全部执行完之后才能获取每个命令的结果,但是如果一个事务中的命令B依赖于他上一个命令A的结果的话该怎么办呢?就比如说实现类似Java中的i++的功能,先要获取当前值,才能在当前值的基础上做加一操作。这种场合仅仅使用上面介绍的MULTI和EXEC是不能实现的,因为MULTI和EXEC中的命令是一起执行的,并不能将其中一条命令的执行结果作为另一条命令的执行参数,所以这个时候就需要引进Redis事务家族中的另一成员:WATCH命令
  换个角度思考上面说到的实现i++的方法,可以这样实现:
监控i的值,保证i的值不被修改
获取i的原值
如果过程中i的值没有被修改,则将当前的i值+1,否则不执行
  这样就能够避免竞态条件,保证i++能够正确执行。
  WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,EXEC命令执行完之后被监控的键会自动被UNWATCH)。举个例子:  
  上面的例子中,首先设置mykey的键值为1,然后使用WATCH命令监控mykey,随后更改mykey的值为2,然后进入事务,事务中设置mykey的值为3,然后执行EXEC运行事务中的命令,最后使用get命令查看mykey的值,发现mykey的值还是2,也就是说事务中的命令根本没有执行(因为WATCH监控mykey的过程中,mykey被修改了,所以随后的事务便会被取消)。
  UNWATCH命令可以在WATCH命令执行之后、MULTI命令执行之前取消对某个键的监控。举个例子:
  上面的例子中,首先设置mykey的键值为1,然后使用WATCH命令监控mykey,随后更改mykey的值为2,然后取消对mykey的监控,再进入事务,事务中设置mykey的值为3,然后执行EXEC运行事务中的命令,最后使用get命令查看mykey的值,发现mykey的值还是3,也就是说事务中的命令运行成功。
  DISCARD命令则可以在MULTI命令执行之后,EXEC命令执行之前取消WATCH命令并清空事务队列,然后从事务状态中退出。举个例子:
  上面的例子中,首先设置mykey的键值为1,然后使用WATCH命令监控mykey,随后更改mykey的值为2,然后进入事务,事务中设置mykey的值为3,然后执行DISCARD命令,再执行EXEC运行事务中的命令,发现报错&ERR EXEC without MULTI&,说明DISCARD命令成功执行&&取消WATCH命令并清空事务队列,然后从事务状态中退出。
Redis分布式锁
  上面介绍的Redis的WATCH、MULTI和EXEC命令,只会在数据被其他客户端抢先修改的情况下,通知执行这些命令的客户端,让它撤销对数据的修改操作,并不能阻止其他客户端对数据进行修改,所以只能称之为乐观锁(optimistic locking)。
  而这种乐观锁并不具备可扩展性&&当客户端尝试完成一个事务的时候,可能会因为事务执行失败而进行反复的重试。保证数据准确性非常重要,但是当负载变大的时候,使用乐观锁的做法并不完美。这时就需要使用Redis实现分布式锁。
  分布式锁:是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。
  Redis命令介绍:
  Redis实现分布式锁主要用到命令是SETNX命令(SET if Not eXists)。  语法:SETNX key value  功能:当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。
  使用Redis构建锁:
  思路:将&lock:&+参数名设置为锁的键,使用SETNX命令尝试将一个随机的uuid设置为锁的值,并为锁设置过期时间,使用SETNX设置锁的值可以防止锁被其他进程获取。如果尝试获取锁的时候失败,那么程序将不断重试,直到成功获取锁或者超过给定是时限为止。
  代码:  
public String acquireLockWithTimeout(
Jedis conn, String lockName, long acquireTimeout, long lockTimeout)
String identifier = UUID.randomUUID().toString();
String lockKey = "lock:" + lockN
int lockExpire = (int)(lockTimeout / 1000);
//锁的过期时间
long end = System.currentTimeMillis() + acquireT
//尝试获取锁的时限
while (System.currentTimeMillis() & end) {
//判断是否超过获取锁的时限
if (conn.setnx(lockKey, identifier) == 1){
//判断设置锁的值是否成功
conn.expire(lockKey, lockExpire);
//设置锁的过期时间
//返回锁的值
if (conn.ttl(lockKey) == -1) {
//判断锁是否超时
conn.expire(lockKey, lockExpire);
Thread.sleep(1000);
//等待1秒后重新尝试设置锁的值
}catch(InterruptedException ie){
Thread.currentThread().interrupt();
// 获取锁失败时返回null
return null;
  锁的释放:
  思路:使用WATCH命令监视代表锁的键,然后检查键的值是否和加锁时设置的值相同,并在确认值没有变化后删除该键。
  代码:  
public boolean releaseLock(Jedis conn, String lockName, String identifier) {
String lockKey = "lock:" + lockN
while (true){
conn.watch(lockKey);
//监视锁的键
if (identifier.equals(conn.get(lockKey))){
//判断锁的值是否和加锁时设置的一致,即检查进程是否仍然持有锁
Transaction trans = conn.multi();
trans.del(lockKey);
//在Redis事务中释放锁
List&Object& results = trans.exec();
if (results == null){
//事务执行失败后重试(监视的键被修改导致事务失败,重新监视并释放锁)
return true;
conn.unwatch();
//解除监视
return false;
  通过在客户端上面实现一个真正的锁(非乐观锁),将会为程序带来更好的性能,更简单易用的API,但是与此同时,请记住Redis并不会主动使用这个自制的分布式锁,我们必须自己使用这个锁来代替WATCH命令,或者协同WATCH命令一起工作,从而保证数据的准确性与一致性。
  参考:
  http://qifuguang.me//Redis%E4%BA%8B%E5%8A%A1%E4%BB%8B%E7%BB%8D/
  http://blog.csdn.net/ugg/article/details/
  转载请注明出处。
阅读(...) 评论()

我要回帖

更多关于 redis 分布锁 的文章

 

随机推荐