1、什么是分布式锁
锁,解决的是多线程或多进程情况下的数据一致性问题;分布式锁,解决的是分布式集群下的数据一致性问题。
为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。
在单机环境中,Java中提供了很多并发处理相关的API。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!
举个例子:
机器A , 机器B是一个集群, A, B两台机器上的程序都是一样的, 具备高可用性能.
A, B机器都有一个定时任务, 每天晚上凌晨2点需要执行一个定时任务, 但是这个定时任务只能执行一遍, 否则的话就会报错, 那A,B两台机器在执行的时候, 就需要抢锁, 谁抢到锁, 谁执行, 谁抢不到, 就不用执行了!
1.1 分布式锁的原理是什么
本身这个事情就没有多复杂:假设幼儿园一个班级有30名小朋友,现在有30个苹果,那么如果分苹果,确保每个小孩能每人领到一个呢?如果让小朋友自己取拿,总会有贪心的小朋友会拿2个,那么就会导致其他人拿不到。
办法自然是让老师分配,老师是公平的。对于分布式锁,也是这个原理,即让第三方去提供锁,集群中的应用ABC都去公共的P拿锁,公共的P就可以利用队列,让ABC排队或synchronized实现线程安全。
当然为了可靠性,第三方也可能采用集群方式,使得维护第三方也会很复杂。
分布式锁,有不同的实现,redis和zookeeper均可以提供该功能,那么:
二者有什么区别?
什么场景下应该使用哪种技术呢?
2、单机
单机和集群是指第三方P的部署形式。
单机场景下,首选redis实现分布式锁。
理由如下(同时也是redis的特点):
1.redis完全基于内存
2.redis单线程多路复用,减少了多个线程创建时的开销,避免了不必要的上下文切换以及资源竞争导致的锁开销
3.全程hash结构
总之就一个字,速度贼特么快!而zk的话是目录树结构,性能完全没得比。
2.1 redis可能会丢数据吗
redis持久化方式有两种对吧,rdb和aof。rdb遇到redis宕机,的确是会丢掉上次bgsave到目前的数据,可是你可以选择aof模式,这样及时的备份数据,就可以避免丢失。
aof模式下还需要做件小事情,打开redis.conf文件,找到${appendfsync}这个字段,给它配置成always,搞定,如此简单。
${appendfsync}:
no:完全依赖于操作系统的调度
everysec:每隔一秒调用一次fsync
always:每次写命令都会调用fsync,最安全,性能差
2.2 死锁
redis通过过期时间可以避免死锁。
zk的ephemeral节点在连接端挂掉之后能够自己删除,注意节点类型即可。
2.3 注意事项
唯一要注意的就是redis的内存淘汰,别莫名其妙数据给删了都不知道。内存淘汰策略的话可以选择noeviction,到了阈值就抛异常,获取锁的时候要写数据嘛,这样内存满了就不能写了。
但是如今稍微大一些的系统,都不太可能使用单实例,redis和zk实现的分布式锁主要区别也不是在单实例场景下。
3. 集群
3.1 redis
redis可重入性及锁续期没有实现,通过redisson解决(类似AQS的实现,看门狗监听机制)
说起分布式集群,必然要提起的是CAP原则,CAP即Consistency、Availability、Partition tolerance,就是一致性、可用性和分区容错性,三者只能取其二,我们就是要从CP、AP来选则,为什么不选CA?因为选了CA就是单机版本了。
先说redis集群,redis集群是基于AP实现的,有3种模式:
第一种是master-slaver,简单的主从模式;此时主从节点均提供服务,从节点仅提供读服务。
第二种是sentinel,叫哨兵模式,就是多了个哨兵,假如master宕机了,它帮你做主从切换,不用再人工干涉了;
第三种是cluster,叫集群模式,这种模式下多个节点存储不同的数据。节点上有个叫slot的玩意儿,将范围限制在0-16383,每次数据来了,通过crc16计算出一个结果,然后对16384取余数,通过这个数就知道哈希槽在哪个节点了,最终到对应的节点上存数据。需要知道的是,为了保证高可用,每个节点也引入了主从模式
集群下的主从,严格来说是主备。从节点不参与读写操作
问题就出在这里,说一下redis主从复制过程:
1.从服务器连接主服务器,发送sync命令;
2.主服务器收到sync命令后,开始执行bgsave命令生成rdb文件并使用缓冲区记录此后执行的所有写命令;
3.主服务器bgsave执行完后,向所有从服务器发送rdb文件,并在发送期间继续记录被执行的写命令;
4.从服务器收到rdb文件后丢弃所有旧数据,载入收到的快照;
5.主服务器发送完rdb文件后开始向从服务器发送缓冲区中的写命令;
6.从服务器完成对rdb文件的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
7.主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令
划重点,redis为了保证高可用,主从复制过程是异步的,也就是说,当客户端向master写了一个数据,master提交完就给你飞吻了,而不管slaver是否已经同步完成数据,这样写数据和同步数据各玩各的,同步数据过程中不影响写数据,这样叫做高可用,但也带来了以下两个问题:
- 问题1,主从复制丢数据
- C1向master申请到了锁a,master在同步数据的过程中挂掉了,此时slaver还没有拿到锁a的数据,主备切换,slaver升级成master1,C2向master1申请锁a,申请成功,此时C1和C2都拿到锁,锁失效了。
- 问题2,脑裂
- C1向master申请到了锁a,master网络出问题了,sentinel收不到master的心跳,于是将slaver选举成master1,但此时还有些C端能够访问到master,假如此时C2能够访问到master,但C3访问的却是master1,于是C2在master上能够申请到锁b,C3在master1上也能够申请到锁b
当这两种情况出现的时候,锁肯定就有问题了,即便是RedLock来实现,也只能降低风险,而不能保证100%可靠。不要拿超级计算机上使用成千上万个redis单实例实现RedLock来杠。
下面说zk集群,zk集群是基于CP实现的,具有强一致性。而强一致性,则是通过ZAB(ZooKeeper Atomic Broadcast)协议实现的,即ZooKeeper原子播送协议。
ZAB协议支持自动恢复和原子播送,是专门为zk量身定做的一种一致性协议。
3.2 zookeeper
zk是非可重入锁,借助Apache curator框架,可以实现重入锁功能
zookeeper集群是基于CP实现的,不能保证每次服务请求的可用性。
任何时刻对ZooKeeper的访问请求能得到一致的数据结果,同时系统对网络分割具备容错性;但是它不能保证每次服务请求的可用性(注:也就是在极端环境下,ZooKeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果)。所以说,ZooKeeper不能保证服务可用性。
进一步说明不满足服务请求的可用性:
进行leader选举时集群都是不可用。在使用ZooKeeper获取服务列表时,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30 ~ 120s, 且选举期间整个zk集群都是不可用的,这就导致在选举期间注册服务瘫痪,虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。所以说,ZooKeeper不能保证服务可用性。
3.2.1 zk如何实现一致性的?
原子播送即同步复制的过程,我们从其它的一致性协议2PC说起。
2PC协议,叫两阶段提交协议(Two-phase Commit),简单些来说:
第一阶段:表决(请求阶段)
事务协调者通知每个参与者准备提交或取消事务,然后进入表决过程,参与者要么在本地执行事务,但不提交;参与者将告知协调者自己的决策: 同意或取消
第二阶段:执行(提交阶段)
此时,协调者将基于请求阶段的投票结果进行决策: 提交或取消;当且仅当所有的参与者同意提交事务,协调者才通知所有的参与者提交事务,否则协调者将通知所有的参与者取消事务;参与者在接收到协调者发来的消息后将执行相应的操作
2PC协议隐藏的问题中在这里我们仅仅需要关注的是同步阻塞,即同步过程中不允许有新的数据写入。
其他的一致性协议还有3PC、Paxos等,ZAB协议的过程可以参考2PC协议(但ZAB协议是过半有效),但实际上更像是Paxos协议的改进版本,由于比较复杂,这里放不下,咿,还听押韵的~就不说了。但是,他们都没有能够彻底解决同步阻塞的问题。
4. 结论
redis和zk分布式锁的区别,关键就在于高可用性和强一致性的选择,redis的性能高于zk太多了,可在一致性上又远远不如zk。
原文链接