Java秒杀系统实战系列~基于ZooKeeper的分布式锁优化秒杀逻辑

摘要:

本篇博文是“Java秒杀系统实战系列文章”的第十七篇,本文咱们将继续秒杀系统的优化之路,采用统一协调调度中心中间件ZooKeeper控制秒杀系统中高并发多线程对于共享资源~代码块的并发访问所出现的并发安全问题,即用ZooKeeper实现一种分布式锁!

内容:

ZooKeeper,看到其名字,不禁得联想至 Zoo + Keeper,即动物园的看管所!这个寓意用以表达的是一种统一协调管理思想,动物园有不少动物,这些动物就相似于分布式系统架构时代所部署的不一样系统服务节点,而这些动物~服务节点偶尔可能须要打交道,相互之间可能须要进行相应的问候,这个时候得须要有一个“看管者”,其职责除了须要管理动物园里的这些动物的行为以外(即这些系统服务的行为),还须要统一协调管理这些动物之间的“问候”、“打交道”(系统服务之间的调用)!

ZooKeeper对外会提供一个多层级的节点命名空间(节点称为ZNode),每一个节点都用一个以斜杠(/)分隔的路径表示,并且每一个节点都有父节点(根节点除外)。ZooKeeper的相关功能特性在实际使用过程当中,其底层可能须要动态的添加、删减相应的节点,此时zk会提供一个Watcher监听器,用以监听那些动态新增、删减的节点,即ZooKeeper会在某些业务场景对一些节点使用上Watcher机制,监听相应的节点的动态。git


咱们即将要在下面介绍的“分布式锁”功能组件即为ZooKeeper提供给开发者的一大利器,其底层的实现原理正是基于Watcher机制 + 动态建立、删减临时顺序节点 所实现的,值得一提的是,一个ZNode节点将表明一个路径。数据库

如下为ZooKeeper实现(获取)分布式锁的原理:apache

(1)当前线程在获取分布式锁的时候,会在ZNode节点(ZNode节点是Zookeeper的指定节点)下建立临时顺序节点,释放锁的时候将删除该临时节点。安全

(2)客户端/服务 调用createNode方法在 ZNode节点 下建立临时顺序节点,而后调用getChildren(“ZNode”)来获取ZNode下面的全部子节点,注意此时不用设置任何Watcher。bash

(3)客户端/服务获取到全部的子节点path以后,若是发现本身建立的子节点序号最小,那么就认为该客户端获取到了锁,即当前线程获取到了分布式锁。微信

(4)若是发现本身建立的节点并不是ZNode全部子节点中最小的,说明本身尚未获取到锁,此时客户端须要找到比本身小的那个节点,而后对其调用exist()方法,同时对其注册事件监听器。多线程

以后,让这个被关注的节点删除(核心业务逻辑执行完,释放锁的时候,就是删除该节点),则客户端的Watcher会收到相应的通知,此时再次判断本身建立的节点是不是ZNode子节点中序号最小的,若是是则获取到了锁,若是不是则重复以上步骤继续获取到比本身小的一个节点并注册监听。架构

以上为ZooKeeper的基本介绍以及关于其底层实现分布式锁的原理的介绍,可是,Debug想说的是“理论再好,若是不会转化为实际的代码或者输出,那只能称之为泛泛而谈、吹牛逼” !并发

下面,咱们将基于Spring Boot搭建的秒杀系统整合ZooKeeper,并基于ZooKeeper实现一种分布式锁,以此解决秒杀系统中高并发多线程并发产生的诸多问题。app

(1)首先,固然是引入ZooKeeper的依赖啦,其中zk的版本在这里咱们采用3.4.10,zk客户端操做实例curator的版本为2.12.0

<!-- zookeeper start -->
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.10</version>
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
        <exclusion>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>2.12.0</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>2.12.0</version>
</dependency>复制代码

紧接着,是在配置文件application.properties中加入ZooKeeper的配置,包括其服务所在的Host、端口Port、命名空间等等:

#zookeeper
zk.host=127.0.0.1:2181
zk.namespace=kill复制代码

(2)而后,跟Redis、Redisson同样,咱们须要基于Spring Boot自定义注入ZooKeeper的相关操做Bean组件,即CuratorFramework实例的自定义配置,以下所示:

//ZooKeeper组件自定义配置
@Configuration
public class ZooKeeperConfig {
 
    @Autowired
    private Environment env;
 
    //自定义注入ZooKeeper客户端操做实例
    @Bean
    public CuratorFramework curatorFramework(){
        CuratorFramework curatorFramework=CuratorFrameworkFactory.builder()
                .connectString(env.getProperty("zk.host"))
                .namespace(env.getProperty("zk.namespace"))
                //重试策略
                .retryPolicy(new RetryNTimes(5,1000))
                .build();
        curatorFramework.start();
        return curatorFramework;
    }
}复制代码

(3)接着,咱们就能够拿来使用了,在KillService秒杀服务类中,咱们建立了一个新的秒杀处理方法killItemV5,表示借助ZooKeeper中间件解决高并发多线程并发访问共享资源~共享代码块出现的并发安全问题!

@Autowired
private CuratorFramework curatorFramework;
//TODO:路径就至关于一个ZNode
private static final String pathPrefix="/kill/zkLock/";
 
//商品秒杀核心业务逻辑的处理-基于ZooKeeper的分布式锁
@Override
public Boolean killItemV5(Integer killId, Integer userId) throws Exception {
    Boolean result=false;
    //定义获取分布式锁的操做组件实例
    InterProcessMutex mutex=new InterProcessMutex(curatorFramework,pathPrefix+killId+userId+"-lock");
    try {
        //尝试获取分布式锁
        if (mutex.acquire(10L,TimeUnit.SECONDS)){
 
            //TODO:核心业务逻辑
            if (itemKillSuccessMapper.countByKillUserId(killId,userId) <= 0){
                ItemKill itemKill=itemKillMapper.selectByIdV2(killId);
                if (itemKill!=null && 1==itemKill.getCanKill() && itemKill.getTotal()>0){
                    int res=itemKillMapper.updateKillItemV2(killId);
                    if (res>0){
                        commonRecordKillSuccessInfo(itemKill,userId);
                        result=true;
                    }
                }
            }else{
                throw new Exception("zookeeper-您已经抢购过该商品了!");
            }
        }
    }catch (Exception e){
        throw new Exception("还没到抢购日期、已过了抢购时间或已被抢购完毕!");
    }finally {
        //释放分布式锁
        if (mutex!=null){
            mutex.release();
        }
    }
    return result;
}复制代码

从上述该源代码中能够看出其核心的处理逻辑在于“定义操做组件实例”、“获取锁”以及“释放锁”的实现上,以下所示:

//定义获取分布式锁的操做组件实例
InterProcessMutex mutex=new InterProcessMutex(curatorFramework,pathPrefix+killId+userId+"-lock");
 
//尝试获取分布式锁
mutex.acquire(10L,TimeUnit.SECONDS)
 
//释放锁
mutex.release();复制代码

(4)至此,基于统一协调调度中心中间件ZooKeeper实现的分布式锁的代码咱们已经实战完毕了,下面咱们按照惯例,进入压测环节,数据用例以及压测的线程组的线程数咱们仍旧跟之前同样,total=6本书,用户Id为10040~10049即10个用户,线程数为1w。

点击JMeter的启动按钮,便可发起秒级并发1w个线程的请求,稍等片刻(由于ZooKeeper须要不断的在当前设定的节点建立、删除临时节点,故而耗时仍是比较长的),观察控制台的输出以及数据库表item_kill、item_kill_success表最终的数据记录结果,以下图所示:


补充:

一、目前,这一秒杀系统的总体构建与代码实战已经所有完成了,完整的源代码数据库地址能够来这里下载:gitee.com/steadyjack/… 记得Fork跟Star啊!!

二、最后,不要忘记了关注一下Debug的技术微信公众号:

相关文章
相关标签/搜索