1.Zookeeper的四种节点
节点概念
1.永久节点
通过create /path content 来创建,
2.临时节点
通过 create -e /path content 创建,客户端链接断开后消失,
3.永久序列化节点
create -s /path content ,可以在同一个节点中创建多个内容,序列号会递增。
4.临时序列化节点
create -s -e /path content
2.Zookeeper中节点的事件监听:使用都是一次性的
1.节点创建 nodeCreated
stat -w /xx
2.节点删除 nodeDeleted
stat -w /xx
3.节点数据变化 nodeDataChanged
get -w /xx
4.子节点删除 nodeChildrenChanged
ls -w /xx
3.实现基本的锁功能:独占排他
我们使用zkClient来连接zookeeper,为了使能一运行项目就连接上zookeeper,我们可以使用spring提供的@Component注解将zkClient注入到容器中,在项目启动后便会调用zkClient的无参构造方法,我们再创建init函数连接zookeeper、使用@PostConstruct注解,让他在无参构造方法执行结束后自动执行。同时记得创建destroy函数释放zookeeper连接,使用@PreDestroy注解使他在spring容器销毁之前执行。
@PostConstruct
public void init(){
//获取链接 项目启动时
CountDownLatch countDownLatch = new CountDownLatch(1);
try {
zooKeeper = new ZooKeeper("192.168.146.130:2181", 30000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
Event.KeeperState state = watchedEvent.getState();
if (Event.KeeperState.SyncConnected.equals(state) &&Event.EventType.None.equals(watchedEvent.getType() )) {
System.out.println("获取连接");
countDownLatch.countDown();
} else if(Event.KeeperState.Closed.equals(state)){
System.out.println("关闭连接");
}
}
});
countDownLatch.await();
}catch (Exception e){
e.printStackTrace();}
}
@PreDestroy
public void destroy(){
//释放zk链接
try {
if (zooKeeper!=null){
zooKeeper.close();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
类似于之前文章中Redis创建distributeLock实现java中的lock接口,我们也创建一个zkDistributeLock类实现Lock接口
public class ZkDistributedLock implements Lock {
private ZooKeeper zooKeeper;
private String lockName;
private String currentNodePath;
private static final ThreadLocal<Integer> THREAD_LOCAL=new ThreadLocal<>();
/**
根节点要定义死 防止走错路
*/
private static final String ROOT_PATH = "/locks";
public ZkDistributedLock(ZooKeeper zooKeeper, String lockName) {
this.zooKeeper = zooKeeper;
this.lockName = lockName;
try {
if(zooKeeper.exists(ROOT_PATH,false)==null){
zooKeeper.create(ROOT_PATH,null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void lock() {
this.tryLock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
try {
//判断threadLocal中是否有锁,有锁直接重入(+1)
Integer flag=THREAD_LOCAL.get();
if (flag!=null&&flag>0){
THREAD_LOCAL.set(flag++);
return true;
}
//创建znode节点过程:为了防止zk客户端程序获取锁之后,服务器宕机带来死锁问题,创建临时节点
//所有请求要求获取锁时,给每一个请求创建临时序列化节点
currentNodePath = this.zooKeeper.create(ROOT_PATH + "/" + lockName+ "-",
null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
//获取前置节点 如果前置节点 则获取锁成功 否则监听前置节点
String preNode = this.getPreNode();
if (preNode!=null){
//利用闭锁思想实现阻塞功能
//获取前置节点是否为空不具有原子性 所以需要再次判断zk中前置节点是否存在
CountDownLatch countDownLatch=new CountDownLatch(1);
if (this.zooKeeper.exists(ROOT_PATH + "/" + preNode, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
countDownLatch.countDown();
}
})==null){
THREAD_LOCAL.set(1);
return true;
}
countDownLatch.await();
}
THREAD_LOCAL.set(1);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
private String getPreNode() {
try {
List<String> children = this.zooKeeper.getChildren(ROOT_PATH, false);
//如果没有子节点则抛出异常
if (CollectionUtils.isEmpty(children)){
throw new IllegalMonitorStateException("非法操作!");
}
//获取和当前节点同一资源的锁
List<String> nodes = children.stream().filter(node -> StringUtils.startsWith(node, lockName + "-")).
collect(Collectors.toList());
if (CollectionUtils.isEmpty(nodes)){
throw new IllegalMonitorStateException("非法操作!");
}
//排好队
Collections.sort(nodes);
//获取当前节点下标
String currentNode = StringUtils.substringAfterLast(currentNodePath, "/");
int index = Collections.binarySearch(nodes, currentNode);
if (index < 0){
throw new IllegalMonitorStateException("非法操作!");
}
else if (index > 0){
//返回前置
return nodes.get(index-1);
}
return null;
} catch (Exception e) {
e.printStackTrace();
throw new IllegalMonitorStateException("非法操作!");
}
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
try {
THREAD_LOCAL.set(THREAD_LOCAL.get()-1);
if (THREAD_LOCAL.get()==0){
//删除节点
this.zooKeeper.delete(currentNodePath,-1);
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
@Override
public Condition newCondition() {
return null;
}
}
我们使用java.lang包下的threadLocal来记录可重入锁
使用zookeeper我们创建的是临时锁,所以我们就不用考虑过期时间,也不用自动续期了。
场景方法实践:
public void deduct(){
ZkDistributedLock lock = this.zkClient.getLock("lock");
lock.lock();
//查询库存
String stock = redisTemplate.opsForValue().get("stock").toString();
try {
//判断库存充足
if (stock != null && stock.length() != 0) {
Integer st = Integer.valueOf(stock);
if (st > 0) {
// 扣减库存
redisTemplate.opsForValue().set("stock", String.valueOf(--st));
}
}
} finally {
lock.unlock();
}
}
二、Curator
配置:
@Configuration
public class CuratorConfig {
@Bean
public CuratorFramework curatorFramework() {
CuratorFramework curatorFramework = CuratorFrameworkFactory
.builder()
.connectString("localhost:2181")
.sessionTimeoutMs(15000)
.connectionTimeoutMs(20000)
.retryPolicy(new ExponentialBackoffRetry(1000, 10))
.build();
curatorFramework.start();
return curatorFramework;
}
}
模拟场景:
使用InterProcessMutex来申请锁
acquire加锁 release解锁
@Scope(scopeName = "prototype")
@RestController
@RequestMapping("/goods-stock")
public class GoodsStockController {
@Autowired
private IGoodsStockService goodsStockService;
@Autowired
private 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 "抢购失败";
}
}
但是实践起来没有自己写的zookeeper锁性能高。