Java秒杀系统(十六):基于ZooKeeper的分布式锁优化秒杀逻辑

作者: 修罗debug
版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。




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

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

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


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

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

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

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

(3)客户端/服务获取到所有的子节点path之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁,即当前线程获取到了分布式锁。

(4)如果发现自己创建的节点并非ZNode所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,然后对其调用exist()方法,同时对其注册事件监听器。

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

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

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

(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表最终的数据记录结果,如下图所示:


对于这样的结果,可谓是皆大欢喜吧!至此,本文关于ZooKeeper的应用实战我们就介绍到这里了! 

补充:

1、目前,这一秒杀系统的整体构建与代码实战已经全部完成了,该秒杀系统对应的视频教程的链接地址为:https://www.fightjava.com/web/index/course/detail/6,可以点击链接进行试看以及学习,实战期间有任何问题都可以留言或者与Debug联系、交流!

2、另外,Debug也开源了该秒杀系统对应的完整的源代码以及数据库,其地址可以来这里下载:https://gitee.com/steadyjack/SpringBoot-SecondKill 记得Fork跟Star啊!!!

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