基于Zookeeper开源客户端Curator实现分布式锁

 分布式锁主流实现方案和选型

a:基于Redis的分布式锁。使用并发量很大、性能要求很高而可靠性问题可以通过其他方案弥补的场景
b:基于ZooKeeper的分布式锁。适用于高可靠(高可用),而并发量不是太高的场景

 

在实际生产中,尤其是分布式环境下,因为我们逻辑真正处理的业务数据是只有一份的,接口并发时势必会出现并发问题,使得业务数据不正确,这个时候就需要一种类似于锁的东西来保证数据的幂等性,比如秒杀业务。实现分布式锁的方式非常多,zookeeper、redis、数据库等均可,如果使用zookeeper原生方式来实现的话还是比较复杂的,基于这种场景,我们利用Apache的开源客户端Curator来实现分布式锁。

Apache Curator是一个比较完善的ZooKeeper客户端框架,通过封装的一套高级API 简化了ZooKeeper的操作。通过查看官方文档,可以发现Curator主要解决了三类问题:

  • 封装ZooKeeper client与ZooKeeper server之间的连接处理
  • 提供了一套Fluent风格的操作API
  • 提供ZooKeeper各种应用场景(recipe, 比如:分布式锁服务、集群领导选举、共享计数器、缓存机制、分布式队列等)的抽象封装

首先来回顾一下zookeeper的相关知识:

zookeeper的四种节点类型

1、持久化节点 :所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。

2、持久化顺序节点:这类节点的基本特性和上面的节点类型是一致的。额外的特性是,在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。基于持久顺序节点原理的经典应用-分布式唯一ID生成器

3、临时节点:和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点,集群zk环境下,同一个路径的临时节点只能成功创建一个,利用这个特性可以用来实现master-slave选举

4、临时顺序节点:相对于临时节点而言,临时顺序节点比临时节点多了个有序,也就是说每创建一个节点都会加上节点对应的序号,先创建成功,序号越小。其经典应用场景为实现分布式锁

监视器(watcher)

当zookeeper创建一个节点时,会注册一个该节点的监视器,当节点状态发生改变时,watch会被触发,zooKeeper将会向客户端发送一条通知(就一条,因为watch只能被触发一次)。

原理

Curator内部是通过InterProcessMutex(可重入锁)来在zookeeper中创建临时有序节点实现的,之前说过,如果通过临时节点及watch机制实现锁的话,这种方式存在一个比较大的问题:所有取锁失败的进程都在等待、监听创建的节点释放,很容易发生"羊群效应",zookeeper的压力是比较大的,而临时有序节点就很好的避免了这个问题,Curator内部就是创建的临时有序节点。

基本原理:

创建临时有序节点,每个线程均能创建节点成功,但是其序号不同,只有序号最小的可以拥有锁,其它线程只需要监听比自己序号小的节点状态即可

基本思路如下:

1、在你指定的节点下创建一个锁目录lock;

2、线程X进来获取锁在lock目录下,并创建临时有序节点;

3、线程X获取lock目录下所有子节点,并获取比自己小的兄弟节点,如果不存在比自己小的节点,说明当前线程序号最小,顺利获取锁;

4、此时线程Y进来创建临时节点并获取兄弟节点 ,判断自己是否为最小序号节点,发现不是,于是设置监听(watch)比自己小的节点(这里是为了发生上面说的羊群效应);

5、线程X执行完逻辑,删除自己的节点,线程Y监听到节点有变化,进一步判断自己是已经是最小节点,顺利获取锁。

 

接下来,我们将基于Zookeeper开源客户端Curator实现分布式锁,具体过程如下(多图演示),

一. 启动zookeeper Server

cd /Users/sunww/Documents/JAVA/Dubbo/zookeeper-3.4.12/bin/

./zkServer.sh start

本demo使用的Curator与zookeeper版本问题,分布式锁见这里 

二. 下单主要逻辑【这里主要使用了Curator来创建了一个ZK互斥锁】

底层主要逻辑如下:

package com.robinboot.service.facade.impl;

import com.robinboot.facade.CuratorFacadeService;
import com.robinboot.result.Result;
import com.robinboot.service.domain.Stock;
import com.robinboot.service.domain.StockOrder;
import com.robinboot.service.service.StockOrderService;
import com.robinboot.service.service.StockService;
import com.robinboot.service.utils.CuratorFrameworkUtils;
import com.robinboot.utils.ServiceException;
//import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.TimeUnit;

/**
 * @auther: TF12778
 * @date: 2020/7/16 18:47
 * @description:
 */
@Service("curatorFacadeService")
public class CuratorFacadeServiceImpl implements CuratorFacadeService {

    @Autowired
    StockService stockService;

    @Autowired
    StockOrderService stockOrderService;

    // 利用Curator创建ZK锁
    InterProcessMutex lock = new InterProcessMutex(CuratorFrameworkUtils.getCuratorClient(), "/zktest");

    /**
     * 下单步骤:校验库存,扣库存,创建订单,支付
     */
    //    @Transactional  此处不需要加事物,否则订单数量超表
    @Override
    public Result<String> saveOrder(int sid) {
        try {
            if (lock.acquire(10 * 1000, TimeUnit.SECONDS)) {
                /**
                 * 1.查库存
                 */
                Stock stock = new Stock();
                stock.setId(sid);
                Stock stockResult = stockService.selectDetail(stock);
                if (stockResult == null || stockResult.getCount() <= 0 || stockResult.getSale() == stockResult.getCount()) {
                    throw new ServiceException("count is less", "500");
                }

                /**
                 * 2.根据查询出来的库存,更新已卖库存数量
                 */
                int count = stockService.updateStock(stockResult);
                if (count == 0){
                    throw new ServiceException("saveOrder fail count is 0", "500");
                }

                /**
                 * 3.创建订单
                 */
                StockOrder order = new StockOrder();
                order.setSid(stockResult.getId());
                order.setName(stockResult.getName());
                int id = stockOrderService.saveStockOrder(order);
                if (id > 0) {
                    return  new Result<String>("success", "saveOrder success", "0", null, "200" );
                }
                return  new Result<String>("error", "saveOrder error", "0", null, "500" );
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                lock.release(); // 释放锁
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        return  new Result<String>("error", "saveOrder error", "0", null, "500" );
    }

API层逻辑:

/**
 * @auther: TF12778
 * @date: 2020/7/16 18:37
 * @description:
 */
@RestController
@RequestMapping("/curator")
public class CuratorController {

    @Autowired
    CuratorFacadeService curatorFacadeService;

    /**
     * http://localhost:8090/robinBootApi/curator/saveOrder
     * @param
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/saveOrder", method = RequestMethod.GET)
    public Result<String> saveOrder() {

        Result<String> result =  curatorFacadeService.saveOrder(1);
        return result;
    }
}

三. 启动jmeter测试工具

通过命令jmeter启动成功

四. 初始化数据库信息

CREATE TABLE `stock` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL DEFAULT '' COMMENT '名称',
  `count` int(11) NOT NULL COMMENT '库存',
  `sale` int(11) NOT NULL COMMENT '已售',
  `version` int(11) NOT NULL COMMENT '乐观锁,版本号',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8

CREATE TABLE `stock_order` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `sid` int(11) NOT NULL COMMENT '库存ID',
  `name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名称',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4868 DEFAULT CHARSET=utf8

五. 高并发,发起测试

这里我们模拟200个用户来抢iphone手机这个操作,如下:

请求地址如下配置:

下面是发起200个请求的返回结果

1. 当库存充足时,可以看到下单成功了,如下界面

2. 当库存不足时,可以看到下单失败了,如下界面

查看数据库信息,可以看到我们的100个iphone手机全部都卖完了(sale=100)

查看订单表,可以看到下了100个iphone订单,没有出现超卖的情况,说明加锁是成功的。

 

Apache Curator框架的ZooKeeper使用详解可以参考这篇文章

参考:https://blog.csdn.net/fanrenxiang/article/details/81704691?utm_medium=distribute.pc_relevant.none-task-blog-OPENSEARCH-7.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-OPENSEARCH-7.nonecase

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值