导读
前面文章【一、深入理解redis之需要掌握的知识点 】中,我们对redis需要学习的内容框架进行了一个梳理。
【二、redis中String和List两种数据类型和应用场景 】、【二、redis中Hash、Set、SortedSet应用场景 】两篇文章我们对redis中String、List、Hash、Set、SortedSet五种数据类型做了一下讲解,并且对他们各自的应用场景进行了介绍。
【三、redis数据存储之跳跃表(SKIP LIST) 】深入学习了支撑SortedSet排序背后的数据结构,跳跃表;
【四、redis持久化之RDB与AOF 】学习了redis中的两种持久化策略:RDB(快照)和AOF(追加日志);
【五、redis集群进化过程 】文章中我们学习了redis集群的进化过程,包括解决单点故障问题和性能瓶颈问题等。
【六、redis中AKF问题解决方案 】讲解了Redis使用及集群进化过程中AKF问题的解决方案。
【七、redis中CAP问题解决方案-Paxos理论过半通过 】我们讲解了Redis使用过程中出现CAP问题的解决方案,以及初步认识Paxos分布式一致性协议。
本章我们将要讲解redis中布式锁的理论、原理及解决方案。
如果大家在工作、学习、面试中针对redis还有什么疑问或者其他问题,可以评论区告诉我。
为了保证可以连续不间断地获取最新的技术分析及讲解,建议关注本博客【不吃_花椒】。
声明:redis不是实现分布式锁的最佳工具。要想使用分布式锁,最好使用zookeeper。虽然Redisson (Java 版)已经为我们封装了redis分布式锁,在懂得CAP原理及Paxos原理后,依然认为redis不是实现分布式锁的最佳工具.因为redis最初的设计思想是用来做缓存的,用他去解决分布式问题有点麻烦且棘手,所以何不用直接用天生就是用来解决分布式协调问题的zookeeper.
Redisson (Java 版).实现了分布式锁
理论
理论:
1.多线程分为单机多线程和多服务器多线程;同样锁也分为单机锁和多服务器锁
。
2.单机多线程环境下需要解决的问题有三个:可见性,原子性,顺序性
。
在JAVA语言中使用锁(synchronized,RentrentLock等)来保证单机多线程环境下的原子性,使用vvolatile关键字来保证单机多线程环境下的可见性与顺序性。
3.在单机中借助某个关键字(synchronized)或者某种理论(CAS)来实现锁,同样在多服务器多线程环境中也需要借助一个所有服务器都可以看到的工具来实现锁理论,从而达到分布式锁的目的。
需要注意的是,多服务器多线程的锁比单机多线程锁要复杂的多,因为他不仅仅要解决单机的问题,还要解决多服务器之间的网络错误、宕机问题,也就是CAP(一致性、可用性、分区容错性)
理论的问题。
4.本文要讲解的是使用redis实现分布式锁的理论。
5…这个页面试图提供一个使用Redis实现分布式锁的规范算法。我们提出一种算法,叫Redlock(红锁)
,我们认为这种实现比普通的单实例实现更安全。
6…使用redis实现的分布式锁要实现满足以下几个理论条件才可以满足真实的使用:
安全性(锁互斥)、
活性A(无死锁)、
活性B(单机故障不影响整体)
分布式锁特性
解决方案
Redis实现锁的几种方案优缺点:
1.基于故障转移(主从复制)的实现:
实现redis分布式锁的最简单的方法就是在redis中创建一个key,这个key有一个失效时间(TTL),以保证锁最终会被自动释放(对应无死锁特性)。当客户端释放资源(解锁)的时候,会删除这个key。
从表面上看,似乎效果还不错,但是存在这样一个问题:单点故障。如果redis挂了怎么办?你可能会说可以通过增加一个slave节点来解决问题。但是这个是行不通的。因为redis的主从复制默认是异步的
。在以下场景中锁就失效了:
1.客户端A从master中获取到了锁;
2.在master把锁信息同步到slave之前,master挂掉了;
3.slave被晋升为master节点;
4.客户端B在新的master节点上获得了同一个资源被客户端A已经获取到的另外一个锁。
这个时候锁互斥的安全失效。
如果你可以接受这种小概率事件,那么这种基于主从复制的方案是可以接受的。
2.单一redis实例实现分布式锁:
当你可以接受上一种“基于故障转移(主从复制)实现”方案的缺点时,我们讨论这一种方案的细节问题。
单实例加锁:
获取锁使用命令 SET resource_name my_random_value NX PX 30000
这个命令仅在key不存在的时候才能被执行成功(NX选项)
,并且这个key具有30秒的过期时间(PX属性)
这个key的值是“my_random_value”,他是一个随机值
。必须保证这个值在所有客户端中是唯一的
。所有同一key的获取者(竞争者)这个值都不能一样。
Value的值必须是随机数主要是为了更安全的释放锁,释放锁的时候,使用脚本告诉redis,只有key存在,且值与存储的值是一样的才能删除成功。
可以通过lua脚本实现:
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
使用这种方式释放锁,可以避免误删别的客户端获取到的锁。这个VALUE相当于一个签名。
这个value随机值该怎么设置呢?在linux系统中它应该是从/dev/urandom产生一个20自己的随机数。但是我们可以找到一个比这个代价更小的方法,因为只要保证这个数在所有客户端中是唯一的即可。
例如:把以毫秒为单位的unix时间和客户端ID拼接起来。虽然这种方案理论上不是完全完全的,但是在多数情况下可以满足需求。
Key的失效时间,被称作“锁有效期”。他不仅仅是为了key在已经获取锁客户端出现问题的时候保证其他客户端可以在一段时间后继续使用锁,而且还是为了保证key可以自动失效,更一种情况是定义了客户端必须在有效期内执行完自己的操作,否则视为失败。因此key的过期时间的设定是需要仔细斟酌的,可以在多次压测后获取一个可行的时间
。
3.ReadLock-Redis分布式环境-CAP理论与Paxos理论(过半通过)
在redis分布式环境中,我们假设有N个相互独立的RedisMaster,这N个节点相互独立,不存在主从复制或者其他协调机制。
在“单redis实例实现分布式锁”方案中,我们已经可以安全的获取和释放锁了,这就保证了安全性(锁互斥)和活性A(无死锁)。为了保证活性B(单机故障不影响整体),我们引入N个相互独立的redis实例,在这N个实例上使用安全的获取和释放锁。
这里我们假设有5个RedisMaster节点,这5个节点在5台服务器上或5台虚拟机上运行,这样就保证了他们不会同时宕机。
为了安全的获取到锁,我们可以执行以下操作:
①.获取当前unix时间,以毫秒为单位。
②.一次从5个实例上使用相同的key和随机value获取锁。
在步骤2,当redis设置锁时候,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁失效时间。
例如:你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免在某个redis服务实例已经挂掉的情况下,客户端还在死死的等待响应结果。如果某个redis服务端没有再规定的时间内响应,则客户端应该尽快去尝试另外一个redis实例。
③.客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到了获取锁使用的时间。当且仅当从大多数(3个节点)redis节点都获取到锁,并且使用的时间小于锁失效时间,锁才算获取成功。
④.如果获取到了锁,key的真正有效时间等于有效时间减去获取锁锁使用的时间(步骤3计算的结果)。如果因为某些原因获取锁失败,客户端应该在所有的redis实例上进行解锁(即便某些redis实例根本就没有加锁成功)
Redis实现分布式锁的其他理论详见:http://redis.cn/topics/distlock.html
如需了解更多更详细内容也可关注本人CSDN博客:不吃_花椒
后续redis中将要讲解的内容梳理
往期文章
Redis
二、redis中String和List两种数据类型和应用场景
二、redis中基础数据类型Hash、Set、SortedSet及其应用场景
Java集合
三、JDK1.7和1.8HashMap数据结构及源码分析-续
Java-IO体系
七、IDEA的maven项目的netty包的导入(其他jar同)
十一、JAVA中ServerSocket调用Linux系统内核
十四、使用Selector(多路复用器)实现Netty中Reactor单线程模型
十五、使用Selector(多路复用器)实现Netty中Reactor主从模型
十七、IO进化过程之EVENT(EPOLL-事件驱动异步模型)
如需了解更多更详细内容也可关注本人CSDN博客:不吃_花椒