a:基于Redis的分布式锁。使用并发量很大、性能要求很高而可靠性问题可以通过其他方案弥补的场景
b:基于ZooKeeper的分布式锁。适用于高可靠(高可用),而并发量不是太高的场景
在实际生产中,尤其是分布式环境下,因为我们逻辑真正处理的业务数据是只有一份的,接口并发时势必会出现并发问题,使得业务数据不正确,这个时候就需要一种类似于锁的东西来保证数据的幂等性,比如秒杀业务。实现分布式锁的方式非常多,zookeeper、redis、数据库等均可,如果使用zookeeper原生方式来实现的话还是比较复杂的,基于这种场景,我们利用Apache的开源客户端Curator来实现分布式锁。
Apache Curator是一个比较完善的ZooKeeper客户端框架,通过封装的一套高级API 简化了ZooKeeper的操作。通过查看官方文档,可以发现Curator主要解决了三类问题:
首先来回顾一下zookeeper的相关知识:
1、持久化节点 :所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。
2、持久化顺序节点:这类节点的基本特性和上面的节点类型是一致的。额外的特性是,在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。基于持久顺序节点原理的经典应用-分布式唯一ID生成器。
3、临时节点:和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点,集群zk环境下,同一个路径的临时节点只能成功创建一个,利用这个特性可以用来实现master-slave选举。
4、临时顺序节点:相对于临时节点而言,临时顺序节点比临时节点多了个有序,也就是说每创建一个节点都会加上节点对应的序号,先创建成功,序号越小。其经典应用场景为实现分布式锁。
当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实现分布式锁,具体过程如下(多图演示),
cd /Users/sunww/Documents/JAVA/Dubbo/zookeeper-3.4.12/bin/
./zkServer.sh start
本demo使用的Curator与zookeeper版本问题,分布式锁见这里
底层主要逻辑如下:
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 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("success", "saveOrder success", "0", null, "200" );
}
return new Result("error", "saveOrder error", "0", null, "500" );
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
lock.release(); // 释放锁
} catch (Exception e) {
e.printStackTrace();
}
}
return new Result("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 saveOrder() {
Result result = curatorFacadeService.saveOrder(1);
return result;
}
}
通过命令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