阿里云服务器上部署zookeeper 3.6.3(5)- 基于Zookeeper实现分布式锁及Leader选举篇

资料来自MIC老师,仅学习参考。

理解分布式锁

我们先来看一个问题,如图11-1所示,两个用户同时去抢购秒杀商品,当秒杀服务同时收到秒杀请求时,都去进行库存扣减,此时在没有做任何处理的情况下,就会导致库存数量变成负数从而导致超卖现象。

这种情况下我们一般会选择加锁的方式来避免并发的问题。但是在分布式场景中,采用传统的锁并不能解决跨进程并发的问题,所以需要引入一个分布式锁,来解决多个节点之间的访问控制。

Zookeeper如何解决分布式锁

我们可以基于Zookeeper的两种特性来实现分布式锁,先来说第一种,

1、使用唯一节点特性实现分布式锁

就是基于唯一节点特性,如图11-2所示。多个应用程序去抢占锁资源时,只需要在指定节点上创建一个 /Lock 节点,由于Zookeeper中节点的唯一性特性,使得只会有一个用户成功创建 /Lock 节点,剩下没有创建成功的用户表示竞争锁失败。

这种方法能达到目的,但是会有一个问题,如图11-3所示,假设有非常多的节点需要等待获得锁,那么等待的方式自然是使用Watcher机制来监听/lock节点的删除事件,

一旦发现该节点被删除说明之前获得锁的节点已经释放了锁,此时剩下的B、C、D。。节点同时会收到删除事件从而去竞争锁,这个过程会产生惊群效应。

但是会产生“惊群效应”,简单来说就是如果存在许多的客户端在等待获取锁,当成功获取到锁的进程释放该节点后,所有处于等待状态的客户端都会被唤醒,这个时候zookeeper在短时间内发送大量子节点变更事件给所有待获取锁的客户端,然后实际情况是只会有一个客户端获得锁。如果在集群规模比较大的情况下,会对zookeeper服务器的性能产生比较的影响。

2、使用有序节点实现分布式锁

因此为了解决这个问题,我们可以采用Zookeeper的有序节点特性来实现分布式锁。

如图11-4所示,每个客户端都往指定的节点下注册一个临时有序节点,越早创建的节点,节点的顺序编号就越小,那么我们可以判断子节点中最小的节点设置为获得锁。如果自己的节点不是所有子节点中最小的,意味着还没有获得锁。这个的实现和前面单节点实现的差异性在于,每个节点只需要监听比自己小的节点,当比自己小的节点删除以后,客户端会收到watcher事件,此时再次判断自己的节点是不是所有子节点中最小的,如果是则获得锁,否则就不断重复这个过程,这样就不会导致羊群效应,因为每个客户端只需要监控一个节点。

如图11-5所示,表示有序节点实现分布式锁的流程。

Curator实现分布式锁

在本节中我们使用Curator来实现分布式锁。为了实现分布式锁,我们先演示一个存在并发异常的场景。

搭建一个商品抢购场景

sql脚本如下。

DROP TABLE IF EXISTS `goods_stock`; 
CREATE TABLE `goods_stock` ( 
`id` int unsigned NOT NULL AUTO_INCREMENT, 
`goods_no` int NOT NULL COMMENT '商品编号', 
`stock` int DEFAULT NULL COMMENT '库存', 
`isActive` smallint DEFAULT NULL COMMENT '是否上架(1上,0不是)', 
PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;

Controller代码

@Scope(scopeName = "prototype")
@RestController
@RequestMapping("/goods-stock")
public class GoodsStockController {
​
​
    @Autowired
    IGoodsStockService goodsStockService;
​
    @Autowired
    CuratorFramework curatorFramework;
​
    @GetMapping("{goodsNo}")
    public String  purchase(@PathVariable("goodsNo")Integer goodsNo) throws Exception {
        QueryWrapper<GoodsStock> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("goods_no",goodsNo);
        //基于临时有序节点来实现的分布式锁.
        // InterProcessMutex lock=new InterProcessMutex(curatorFramework,"/Locks");
        try {
            // lock.acquire(); //抢占分布式锁资源(阻塞的)
            GoodsStock goodsStock = goodsStockService.getOne(queryWrapper);
            Thread.sleep(new Random().nextInt(1000));
            if (goodsStock == null) {
                return "指定商品不存在";
            }
            if (goodsStock.getStock().intValue() < 1) {
                return "库存不够";
            }
            goodsStock.setStock(goodsStock.getStock() - 1);
            boolean res = goodsStockService.updateById(goodsStock);
            if (res) {
                return "抢购书籍:" + goodsNo + "成功";
            }
        }finally {
            // lock.release(); //释放锁
        }
        return "抢购失败";
    }
}

上述代码使用jmeter进行压测,用1500个线程,库存数量设置成100,监视数据库中库存的变化发现,整个库存变化过程是非常混乱的。

引入Zookeeper实现分布式锁

curator对于锁这块做了一些封装,curator提供了InterProcessMutex 这样一个api。除了分布式锁之外,还提供了leader选举、分布式队列等常用的功能。

  • InterProcessMutex:分布式可重入排它锁

  • InterProcessSemaphoreMutex:分布式排它锁

  • InterProcessReadWriteLock:分布式读写锁

具体的使用方法如下。

  • 引入pom

        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>5.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>5.2.0</version>
        </dependency>
  • 修改GoodsController,增加锁机制

@Scope(scopeName = "prototype")
@RestController
@RequestMapping("/goods-stock")
public class GoodsStockController {
​
​
    @Autowired
    IGoodsStockService goodsStockService;
​
    @Autowired
    CuratorFramework curatorFramework;
​
    @GetMapping("{goodsNo}")
    public String  purchase(@PathVariable("goodsNo")Integer goodsNo) throws Exception {
        QueryWrapper<GoodsStock> queryWrapper=new QueryWrapper<>();
        queryWrapper.eq("goods_no",goodsNo);
        //基于临时有序节点来实现的分布式锁.
        InterProcessMutex lock=new InterProcessMutex(curatorFramework,"/Locks");
        try {
            lock.acquire(); //抢占分布式锁资源(阻塞的)
            GoodsStock goodsStock = goodsStockService.getOne(queryWrapper);
            Thread.sleep(new Random().nextInt(1000));
            if (goodsStock == null) {
                return "指定商品不存在";
            }
            if (goodsStock.getStock().intValue() < 1) {
                return "库存不够";
            }
            goodsStock.setStock(goodsStock.getStock() - 1);
            boolean res = goodsStockService.updateById(goodsStock);
            if (res) {
                return "抢购书籍:" + goodsNo + "成功";
            }
        }finally {
            lock.release(); //释放锁
        }
        return "抢购失败";
    }
}
  • CuratorConfifig

@Configuration
public class CuratorConfig {
​
    @Bean
    public CuratorFramework curatorFramework(){
        CuratorFramework curatorFramework=CuratorFrameworkFactory
                .builder()
                .connectString("127.0.0.1:2181")
                .sessionTimeoutMs(15000)
                .connectionTimeoutMs(20000)
                .retryPolicy(new ExponentialBackoffRetry(1000,10))
                .build();
        curatorFramework.start();
        return curatorFramework;
    }
}
  • 继续通过jmeter压测,可以看到就不存在库存超卖问题了。

Curator实现分布式锁的源码分析

抢占锁的逻辑

Curator构造函数 

acquire方法

调用acquire方法,该方法有两个重载方法,另外一个是带超时时间,当等待超时没有获得锁则放弃锁的占用。

internalLock

 attemptLock

尝试获得锁,实际上是向 zookeeper 注册一个临时有序节点,并且判断当前创建的节点的顺序是否是最 小节点。如果是则表示获得锁成功

 

 createsTheLock

 // Zookeeper中创建临时顺序节点

internalLockLoop

driver.getsTheLock

释放锁的逻辑
release

releaseLock

锁撤销

InterProcessMutex支持一种协商撤销互斥锁的机制, 可以用于死锁的情况想要撤销一个互斥锁可以调用下面这个方法:

public void makeRevocable(RevocationListener<T> listener)

这个方法可以让锁持有者来处理撤销动作。 当其他进程/线程想要你释放锁时,就会回调参数中的监听器方法。 但是,此方法不是强制撤销的,是一种协商机制

当想要去撤销/释放一个锁时,可以通过 Revoker 中的静态方法来发出请求,

Revoker.attemptRevoke();

public static void attemptRevoke(CuratorFramework client,String path) throws 
​
Exception
  • path :加锁的zk节点path,通常可以通过 InterProcessMutex.getParticipantNodes() 获得这个方法会发出撤销某个锁的请求。如果锁的持有者注册了上述的 RevocationListener 监听器,那么就会调用监听器方法协商撤销锁。

使用Zookeeper实现多个节点的leader选举

在分布式计算中,leader election是很重要的一个功能,这个选举过程是这样子的:指派一个进程作为组织者,将任务分发给各节点。在任务开始前,哪个节点都不知道谁是leader或者coordinator。当选举算法开始执行后,每个节点最终会得到一个唯一的节点作为任务leader。除此之外,选举还经常会发生在leader意外宕机的情况下,新的leader要被选举出来,如图11-7所示,这个就是所谓的leader选举,而zookeeper作为leader选举的功能,在很多中间件中都有使用,比如kafka基于zookeeper实现leader选举,Hadoop、Spark等。

Curator实现leader选举

除了作为集群节点的leader选举之外,leader选举还可以用在其他的场景,比如在分布式调度任务系统中,从可靠性角度出发,集群也是必不可少的。但往往,为了保证任务不会重复分配,分配任务的节点只能有一个,这种情况就需要从集群中选出一个Leader(老大)去任务池里取任务,如图11-8所示。

本文就会介绍Curator基于Zookeeper封装的Leader选举工具类LeaderLatch与LeaderSelector的使用及原理分析,Curator有两种选举recipe(Leader Latch和Leader Election),两种实现机制上有一定的差异,后续会逐步说明。

LeaderLatch使用实战

Quartz中最重要的三个对象:Job、Trigger、Scheduler。

模拟定时任务调度,两台服务器只能有一台跑定时任务,多台服务器运行同样的定时任务,只有一台跑任务,当这台挂了,回暖选举出另外一台跑

  • Job,表示任务

  • Trigger,配置调度参数

  • Scheduler,代表一个调度容器,一个调度容器中可以注册多个JobDetail和Trigger

public class GpLeaderLatchListener implements LeaderLatchListener {
    //控制定时任务启动和停止的方法
    private SchedulerFactoryBean schedulerFactoryBean;
​
    public GpLeaderLatchListener(SchedulerFactoryBean schedulerFactoryBean) {
        this.schedulerFactoryBean = schedulerFactoryBean;
    }
​
    @Override
    public void isLeader() {
        System.out.println(Thread.currentThread().getName()+"成为了leader");
        schedulerFactoryBean.setAutoStartup(true);
        schedulerFactoryBean.start();
    }
​
    @Override
    public void notLeader() {
        System.out.println(Thread.currentThread().getName()+"抢占leader失败,不执行任务");
        schedulerFactoryBean.setAutoStartup(false);
        schedulerFactoryBean.stop();
    }
}



@Configuration
public class QuartzConfiguration {
​
​
    @Bean
    public ZkSchedulerFactoryBean schedulerFactoryBean(JobDetail jobDetail,Trigger trigger) throws Exception {
        ZkSchedulerFactoryBean zkSchedulerFactoryBean=new ZkSchedulerFactoryBean();
        zkSchedulerFactoryBean.setJobDetails(jobDetail);
        zkSchedulerFactoryBean.setTriggers(trigger);
        return zkSchedulerFactoryBean;
    }
​
    @Bean
    public JobDetail jobDetail(){
        return JobBuilder.newJob(QuartzJob.class).storeDurably().build();
    }
​
    @Bean
    public Trigger trigger(JobDetail jobDetail){
        SimpleScheduleBuilder simpleScheduleBuilder=
                SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1).repeatForever();
        return TriggerBuilder.newTrigger().forJob(jobDetail).withSchedule(simpleScheduleBuilder).build();
    }
}


public class QuartzJob extends QuartzJobBean {
​
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("开始执行定时任务");
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        System.out.println("当前执行的系统时间:"+sdf.format(new Date()));
    }
}



public class ZkSchedulerFactoryBean extends SchedulerFactoryBean {
​
    private LeaderLatch leaderLatch;
​
    private final String LEADER_PATH="/leader"; //namespace
​
    public ZkSchedulerFactoryBean() throws Exception {
        this.setAutoStartup(false); //应用启动的时候不自动开启定时任务
​
        leaderLatch=new LeaderLatch(getClient(),LEADER_PATH);
        leaderLatch.addListener(new GpLeaderLatchListener(this)); //当leader发生变化的时候,需要触发监听
        leaderLatch.start();
    }
​
    private CuratorFramework getClient(){
        CuratorFramework curatorFramework= CuratorFrameworkFactory
                .builder()
                .connectString("192.168.221.128:2181")
                .sessionTimeoutMs(15000)
                .connectionTimeoutMs(20000)
                .retryPolicy(new ExponentialBackoffRetry(1000,10))
                .build();
        curatorFramework.start();
        return curatorFramework;
    }
​
    @Override
    protected void startScheduler(Scheduler scheduler, int startupDelay) throws SchedulerException {
​
        if(this.isAutoStartup()) {
            super.startScheduler(scheduler, startupDelay);
        }
    }
​
    @Override
    public void destroy() throws SchedulerException {
        CloseableUtils.closeQuietly(leaderLatch);
        super.destroy();
    }
}

LeaderSelector案例演示

LeaderSelector和Leader Latch最的差别在于,LeaderSelector可以释放领导权以后,还可以继续参与竞争。

public class SelectorClientExample extends LeaderSelectorListenerAdapter implements Closeable {
​
​
    private final String name;
    private final LeaderSelector leaderSelector;
​
    public SelectorClientExample(String path,String name) {
        // 利用一个给定的路径创建一个leader selector
        // 执行leader选举的所有参与者对应的路径必须一样
        // 本例中SelectorClient也是一个LeaderSelectorListener,但这不是必须的。
        leaderSelector=new LeaderSelector(getClient(),path,this);
        // 在大多数情况下,我们会希望一个selector放弃leader后还要重新参与leader选举
        leaderSelector.autoRequeue();
        this.name=name;
    }
​
    @Override
    public void close() throws IOException {
        leaderSelector.close();
    }
​
    public void start(){
        leaderSelector.start();
    }
​
    @Override
    public void takeLeadership(CuratorFramework client) throws Exception {
        System.out.println(name+" 成为Leader");
        Thread.sleep(1000);
    }
​
    private CuratorFramework getClient(){
        CuratorFramework curatorFramework= CuratorFrameworkFactory
                .builder()
                .connectString("192.168.221.128:2181")
                .sessionTimeoutMs(15000)
                .connectionTimeoutMs(20000)
                .retryPolicy(new ExponentialBackoffRetry(1000,10))
                .build();
        curatorFramework.start();
        return curatorFramework;
    }
​
    public static void main(String[] args) throws IOException {
        String path="/leader";
        for (int i = 0; i < 10; i++) {
            SelectorClientExample selectorClientExample=
                    new SelectorClientExample(path,"Client:"+i);
            selectorClientExample.start();
        }
        //阻塞,让当前获得leader权限的节点一直持有,直到该进程关闭
        System.in.read();
    }
}

每天努力一点,每天都在进步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

powerfuler

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值