zookeeper、zk分布式锁、redis分布式锁

目录

1、zookeeper 常用的使用场景:

2、分布式锁现方式?Redis 和zk 两种分布式锁的实现方式哪种效率比较高?

        ①Redis 分布式锁

        ②zk 分布式锁

3、zk集群选主


         面试官跟你聊完了 dubbo 相关的一些问题之后,已经确认你对分布式服务框架/RPC框架基本都有一些认知。那么他可能开始要跟你聊分布式相关的其它问题了。
分布式锁这个东西,做 Java 系统开发,分布式系统,可能会有一些场景会用到。最常用的分布式锁就是基于 zookeeper 来实现的

        我们学习分布式锁之前需要了解它的使用场景,分布式锁要解决的场景和使用方法 - 知乎

1、zookeeper 常用的使用场景:

分布式协调
分布式锁
元数据/配置信息管理

HA高可用性

①分布式协调
        这个其实是 zookeeper 很经典的一个用法,简单来说,就好比,你 A 系统发送个请求到 mq,然后 B 系统消息消费之后处理了。那 A 系统如何知道 B 系统的处理结果?用 zookeeper 就可以实现分布式系统之间的协调工作。A 系统发送请求之后可以在 zookeeper 上对某个节点的值注册个监听器,一旦 B 系统处理完了就修改 zookeeper 那个节点的值,A 系统立马就可以收到通
知。

②分布式锁

        首先讲讲为什么要有分布式锁:如果是单体应用,对资源的控制可以用多线程中的sychronized和lock等,但如果是分布式系统,机器有很多台,sychronized这些无法控制其他机器获取资源,因此需要引进分布式锁,大家一起去竞争同一个把锁。
        分布式锁:举个栗子。对某一个数据连续发出两个修改操作,两台机器同时收到了请求,但是只能一台机器先执行完另外一个机器再执行。那么此时就可以使用 zookeeper 分布式锁,一个机器接收到了请求之后先获取 zookeeper 上的一把分布式锁,就是可以去创建一个 znode节点(或者说创建一个目录,如下图,比如创建一个/lock节点下的新节点),接着执行操作;然后另外一个机器也尝试去创建那个 znode节点,结果发现自己创建不了,因为被别人创建了,那只能等着,等第一个机器执行完了自己再执行。

核心思想:当客户端要获取分布式锁时,则创建临时顺序节点,使用完锁就删除这个节点。

具体过程:

1、客户端获取锁时,在lock节点下创建临时顺序节点(在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号);

为什么是临时顺序节点:防止假如宕机,锁被长期占用无法释放。临时的那么会话结束就自动释放。

2、然后客户端获取/lock下面的所有子节点,如果发现自己创建的子节点在所有子节点中最小,那么就判定这个客户端获取到了锁,使用完以后,将该节点删除。

3、如果发现自己创建的子节点并非最小的,客户端会对会比自己小(最近的那个,比如2监听1,3只能监听到2)的自己子节点注册watch监听器,监听删除事件。

③元数据/配置信息管理 
        zookeeper 可以用作很多系统的配置信息的管理,比如 kafka、storm 等等很多分布式系统都会选用 zookeeper 来做一些元数据、配置信息的管理,包括 dubbo 注册中心不也支持 zookeeper
么?

 

分布式锁案例:携程、飞猪、去哪儿等售票平台都市做12306火车票的代理,票务资源实例时掌握在12306这里,因此可以给12306的系统加分布式锁,让代理商取获取锁来购买。

 在Curator(zk的Java客户端)中有五种锁方案:
InterProcessSemaphoreMutex:分布式排它锁(非可重入锁)
InterProcessMutex:分布式可重入排它锁
InterProcessReadWriteLock:分布式读写锁
InterProcessMultiLock:将多个锁作为单个实体管理的容器
InterProcessSemaphoreV2:共享信号量 

模拟代理买票: 

package com.itheima.curator;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;

import java.util.concurrent.TimeUnit;

public class Ticket12306 implements Runnable{

    private int tickets = 10;//数据库的票数

    private InterProcessMutex lock ;

    //构造方法,初始化lock
    public Ticket12306(){
        //重试策略
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
        //2.第二种方式
        //CuratorFrameworkFactory.builder();
        CuratorFramework client = CuratorFrameworkFactory.builder()
                .connectString("192.168.149.135:2181")
                .sessionTimeoutMs(60 * 1000)
                .connectionTimeoutMs(15 * 1000)
                .retryPolicy(retryPolicy)
                .build();

        //开启连接
        client.start();

        lock = new InterProcessMutex(client,"/lock");
    }

    @Override
    public void run() {

        while(true){
            //获取锁
            try {
                lock.acquire(3, TimeUnit.SECONDS);
                if(tickets > 0){

                    System.out.println(Thread.currentThread()+":"+tickets);
                    Thread.sleep(100);
                    tickets--;
                }
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                //释放锁
                try {
                    lock.release();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

    }
}
package com.itheima.curator;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.*;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class LockTest {


    public static void main(String[] args) {
        Ticket12306 ticket12306 = new Ticket12306();

        //创建客户端
        Thread t1 = new Thread(ticket12306,"携程");
        Thread t2 = new Thread(ticket12306,"飞猪");

        t1.start();
        t2.start();
    }

}

④HA高可用性
这个应该是很常见的,比如 hadoop、hdfs、yarn 等很多大数据系统,都选择基于 zookeeper 来开发 HA 高可用机制,就是一个重要进程一般会做主备两个,主进程挂了立马通过 zookeeper感知到切换到备用进程。

2、分布式锁现方式?Redis 和zk 两种分布式锁的实现方式哪种效率比较高?

①Redis 分布式锁

官方叫做 RedLock 算法,是 Redis 官方支持的分布式锁算法。
这个分布式锁有 3 个重要的考量点:

  • 互斥(只能有一个客户端获取锁)
  • 不能死锁
  • 容错(只要大部分 Redis 节点创建了这把锁就可以)

Redis 最普通的分布式锁

第一个最普通的实现方式,就是在 Redis 里使用

SET key value [EX seconds] [PX milliseconds] NX 

创建一个 key,这样就算加锁。其中:

NX :表示只有 key 不存在的时候才会设置成功,如果此时 redis 中存在这个 key ,那么设置失败,返回 nil 。 
EX seconds :设置 key 的过期时间,精确到秒级。意思是 seconds 秒后锁自动释放,别人创建的时候如果发现已经有了就不能加锁了。
PX milliseconds :同样是设置 key 的过期时间,精确到毫秒级。

比如执行以下命令:

SET resource_name my_random_value PX 30000 NX

释放锁就是删除 key ,但是一般可以用 lua 脚本删除,判断 value 一样才删除:

-- 删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删 
if redis.call("get",KEYS[1]) == ARGV[1] then 
    return redis.call("del",KEYS[1]) 
else    return 0 
end

        为啥要用 random_value 随机值呢?因为如果某个客户端获取到了锁,但是阻塞了很长时间才执行完,比如说超过了 30s,此时可能已经自动释放锁了,此时可能别的客户端已经获取到了这个锁,要是你这个时候直接删除 key 的话会有问题,所以得用随机值加上面的 lua 脚本
来释放锁。
        但是这样是肯定不行的。因为如果是普通的 Redis 单实例,那就是单点故障。或者是 Redis 普通主从,那 Redis 主从异步复制,如果主节点挂了(key 就没有了),key 还没同步到从节点,此时从节点切换为主节点,别人就可以 set key,从而拿到锁。

一般redis的分布式锁都可以使用redisson框架来做。 使用:直接lock("key")

原理:当lock的时候,当前客户端会生成一串lua脚本发送到redis服务端。服务端根据这个key是否存在以及key的value状态判断是否已被加锁。
如果加锁成功(有效期30s):生成的value里面带有当前客户端的id,实现可重入效果。然后在这个线程执行过程中,一旦加锁成功还会有一个watch dog每10s去刷新锁的状态。直到释放或者宕机,如果当前线程死锁或阻塞,那么这个锁就一起死锁。如果加锁失败:
自旋到获取锁。

RedLock 算法

这个场景是假设有一个 Redis cluster,有 5 个 Redis master 实例。然后执行如下步骤获取一把锁:
1. 获取当前时间戳,单位是毫秒;
2. 跟上面类似,轮流尝试在每个 master 节点上创建锁,过期时间较短,一般就几十毫秒;
3. 尝试在大多数节点上建立一个锁,比如 5 个节点就要求是 3 个节点 n / 2 + 1 ; 4. 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;
5. 要是锁建立失败了,那么就依次之前建立过的锁删除;
6. 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。

 Redis 官方给出了以上两种基于 Redis 实现分布式锁的方法,详细说明可以查看:https://redis.io/topics/distlock 。

②zk 分布式锁

zk一般使用curator去实现分布式锁。 原理:向zk发起请求,在一个目录(/locks/pd_1_stock)下,创建一个临时节点,也是带有自己客户端的id。
如果目录是空的,自己创建出来的节点就是第一个节点,那么加锁成功。如果成功执行则释放(节点删除)。如果宕机了,基于zk的心跳机制,那个临时节点也会被删除。第二个客户端请求锁时,也创建一个节点,如果不是第一节点,那么向上一节点加一个watcher监听器。如果上一节点被删除立马会感知到,然后在判断自己是不是第一节点,如果不是再监听上一级(公平实现)。完事后陷入等待,直到获取锁。

redis 分布式锁和 zk 分布式锁的对比 

  • redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
  • zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。
  • 另外一点就是,如果是 Redis 获取锁的那个客户端出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。

Redis 分布式锁大家没发现好麻烦吗?遍历上锁,计算时间等等......zk 的分布式锁语义清晰实现简单。所以先不分析太多的东西,就说这两点,我个人实践认为 zk 的分布式锁比 Redis 的分布式锁牢靠、而且模型简单易用。

3、zk集群选主

Leader选举规则:

①Serverid:服务器ID   比如有三台服务器,编号分别是1,2,3。   编号越大在选择算法中的权重越大。

②Zxid:数据ID   服务器中存放的最大数据ID.值越大说明数据越新,在选举算法中数据越新权重越大。

③在Leader选举的过程中,如果某台ZooKeeper     获得了超过半数的选票,     则此ZooKeeper就可以成为Leader了。

 比如在如上12345号zk的集群中,假设按顺序启动。1启动会选举自己,2启动后因为2大于1,所以1会改投2,2也投自己。3启动后同理,1和2都改投3,3投自己,这时候就超过了半数,3作为master。后面4和5启动后3仍然是master。

在ZooKeeper集群服中务中有三个角色:

        Leader 领导者 :    1. 处理事务请求(增删改)

                                        2. 集群内部各服务器的调度者

        Follower 跟随者 :  1. 处理客户端非事务请求(查询),转发事务请求给Leader服务器

                                        2. 参与Leader选举投票

        Observer 观察者: 1. 处理客户端非事务请求,转发事务请求给Leader服务器(这样可以给Follower 分摊压力)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值