浅谈Redis和zookeeper的分布式锁设计
Posted 朱培
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈Redis和zookeeper的分布式锁设计相关的知识,希望对你有一定的参考价值。
本文主要谈一下使用Redis和zookeeper来进行分布式锁的设计过程和原理。一般实现分布式锁都有哪些方式?使用redis如何设计分布式锁?使用zk来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?
对于分布式锁,一般来说有一下的需求:
- 可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行。
- 这把锁要是一把可重入锁(避免死锁)
- 有高可用的获取锁和释放锁功能
- 获取锁和释放锁的性能要好
一、redis的SETNX方式实现分布式锁
SET key:lock 随机值 NX PX 30000,这个命令会返回OK,这个的NX的意思就是只有key不存在的时候才会设置成功,PX 30000的意思是30秒后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。如果已经存在了,redis就会返回nil,这样就没有拿到锁,可以自己每隔1秒去尝试一下是否可以拿到这把锁。
释放锁:删除这个key,可以通过lua脚本进行删除整个key,判断value一样才删除。
为什么要使用随机值?
因为如果某个客户端获取到了锁,但是阻塞了很长时间才执行完。此时可能已经自动释放锁了,然后又有别的客户端获得了这个锁,例如设置30秒过期,当超过30秒之后,又有新的客户端来设置了新的key。此时再去删除的话就会有问题,因为删的是别的客户端的key,所以这里我们使用随机值加lua脚本来释放锁。
使用该方案的缺点
对于redis集群来说,因为Redis的主从复制,如果主节点挂了,key还没有同步到从节点,如果此时从节点变成了主节点,那么这个主节点上面就会被认为没有锁,那么别的客户端就会拿到锁。所以通过set方式来建立分布式锁在某些情况下是会出现一些问题的。
二、redis的RedLock实现分布式锁
在Redis的分布式环境中,我们假设有N个Redis master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制。
实现步骤:
- 获取当前时间戳,单位是毫秒
- 轮流尝试在每个master节点上创建锁,过期时间较短,一般就几十毫秒
- 尝试在大多数节点上建立一个锁,比如5个节点就要求是3个节点(n / 2 +1),客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了
- 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间,如果锁建立失败了,客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。
- 只要有别的客户端建立了一把分布式锁,就需要不断的轮询去尝试获取锁
只有在有界的网络延迟、有界的程序中断、有界的时钟错误范围,Redlock才能正常工作,但是这三种场景的边界又是无法确认的,所以不建议使用Redlock。对于正确性要求高的场景,推荐使用Zookeeper。
三、zookeeper的分布式锁
获取锁的时候,其实就是去创建一个临时节点,如果已经存在了,说明有其他客户端已经占用这把锁了,这时就对这个临时节点注册一个监听器,当释放锁的时候,删除那个临时节点即可。指定的节点下注册一个临时有序节点,越早创建的节点,我们可以通过有序节点来实现分布式锁,每个客户端都往节点的顺序编号就越小,那么我们可以判断子节点中最小的节点设置为获得锁。如果自己的节点不是所有子节点中最小的,意味着还没有获得锁。每个节点只需要监听比自己小的节点,当比自己小的节点删除以后,客户端会收到 watcher 事件,此时再次判断自己的节点是不是所有子节点中最小的,如果是则获得锁,否则就不断重复这个过程,这样就不会导致惊群效应,因为每个客户端只需要监控一个节点。
具体步骤:
- 客户端连接zookeeper,并在/lock下创建临时的且有序的子节点,第一个客户端对应的子节点为/lock/r-lock0000000000,第二个为/lock/r-lock0000000001,以此类推;
- 客户端获取/lock下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听刚好在自己之前一位的子节点删除消息,获得子节点变更通知后重复此步骤直至获得锁;
- 然后获取到比当前节点顺序小的所有节点的一个集合。如果不为空,就获取这个集合节点中的最后一个节点B。然后让B去监听它的上一个A,A的会话超时或者A节点被删除(释放)了,B就获取到了锁,同理,当B被删除或者会话超时时,C就获取到了锁。
- 执行业务代码,完成业务流程后,删除对应的子节点释放锁。
redis分布式锁和zookeeper分布式锁的对比
redis分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。而zookeeper分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。如果是redis获取锁的那个客户端宕机了,那么只能等待超时时间之后才能释放锁;而zk的话,因为创建的是临时znode,只要客户端挂了,znode就没了,此时就自动释放锁。
四、Curator实现分布式锁
curator是Netflix公司开源的一个ZooKeeper客户端封装。Curator内部是通过InterProcessMutex(可重入锁)来在zookeeper中创建临时有序节点实现的,如果通过临时节点及watch机制实现锁的话,这种方式存在一个比较大的问题:所有取锁失败的进程都在等待、监听创建的节点释放,很容易发生"惊群效应",简单来说就是如果存在许多的客户端在等待获取锁,当成功获取到锁的进程释放该节点后,所有处于等待状态的客户端都会被唤醒,这个时候 zookeeper 在短时间内发送大量子节点变更事件给所有待获取锁的客户端,然后实际情况是只会有一个客户端获得锁。如果在集群规模比较大的情况下,会对 zookeeper 服务器的性能产生比较的影响。zookeeper的压力是比较大的,而临时有序节点就很好的避免了这个问题,Curator内部就是创建的临时有序节点。
1、导入maven依赖
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.0</version>
</dependency>
2、代码实现:
public class ZooKeeperDistributedLock
private InterProcessMutex interProcessMutex; //可重入排它锁
private String lockName; //竞争资源标志
private String root = "/distributed_lock/";//根节点
private static CuratorFramework curatorFramework;
private static String ZK_URL = "localhost:2181,localhost:2182,localhost:2183";
static
curatorFramework= CuratorFrameworkFactory.newClient(ZK_URL,new ExponentialBackoffRetry(1000,3));
curatorFramework.start();
/**
* 实例化
* @param lockName
*/
public ZooKeeperDistributedLock(String lockName)
try
this.lockName = lockName;
interProcessMutex = new InterProcessMutex(curatorFramework, root + lockName);
catch (Exception e)
System.out.println("初始化InterProcessMutex异常:"+e);
/**
* 获取锁
*/
public void acquireLock()
int flag = 0;
try
//重试2次,每次最大等待2s,也就是最大等待4s
while (!interProcessMutex.acquire(2, TimeUnit.SECONDS))
flag++;
if(flag>1) //重试两次
break;
catch (Exception e)
System.out.println("获取锁发生异常:"+e);
if(flag>1)
System.out.println("Thread:"+Thread.currentThread().getId()+" 需要的锁处于繁忙状态!");
else
System.out.println("Thread:"+Thread.currentThread().getId()+" 获取锁成功");
/**
* 释放锁
*/
public void releaseLock()
try
if(interProcessMutex != null && interProcessMutex.isAcquiredInThisProcess())
interProcessMutex.release();
curatorFramework.delete().inBackground().forPath(root+lockName);
System.out.println("Thread:"+Thread.currentThread().getId()+" 释放锁成功");
catch (Exception e)
System.out.println("Thread:"+Thread.currentThread().getId()+" 释放锁时发生异常:"+e);
public static void main(String[] args)
final ZooKeeperDistributedLock zooKeeperDistributedLock = new ZooKeeperDistributedLock("aa");
//模拟30个客户端
for(int i = 0; i < 30; i ++)
new Thread(new Runnable()
public void run()
try
System.out.println(Thread.currentThread().getName()+"开始执行------------");
zooKeeperDistributedLock.acquireLock();
System.out.println(Thread.currentThread().getName() + "|获取到锁了,处理业务去了!");
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "|获取到锁了,业务处理完成!");
catch (Exception e)
e.printStackTrace();
finally
zooKeeperDistributedLock.releaseLock();
).start();
3、执行结果
对于Curator实现分布式锁的基本原理可以参考我的这篇文章:https://zhupei.blog.csdn.net/article/details/95942847
以上是关于浅谈Redis和zookeeper的分布式锁设计的主要内容,如果未能解决你的问题,请参考以下文章
SpringBoot电商项目实战 — Redis实现分布式锁