一、场景
单机应用架构中,秒杀案例使用ReentrantLcok或者synchronized来达到秒杀商品互斥的目的。然而在分布式系统中,会存在多台机器并行去实现同一个功能。也就是说,在多进程中,如果还使用以上JDK提供的进程锁,来并发访问数据库资源就可能会出现商品超卖的情况。因此,需要我们来实现自己的分布式锁。
二、特性
实现一个分布式锁应该具备的特性:
A、高可用、高性能的获取锁与释放锁
B、在分布式系统环境下,一个方法或者变量同一时间只能被一个线程操作
C、具备锁失效机制,网络中断或宕机无法释放锁时,锁必须被删除,防止死锁
D、具备阻塞锁特性,即没有获取到锁,则继续等待获取锁
E、具备非阻塞锁特性,即没有获取到锁,则直接返回获取锁失败
F、具备可重入特性,一个线程中可以多次获取同一把锁,比如一个线程在执行一个带锁的方法,该方法中又调用了另一个需要相同锁的方法,则该线程可以直接执行调用的方法,而无需重新获得锁
分布式锁几种实现方式:
1)、基于数据库实现分布式锁
2)、基于 Redis 实现分布式锁
3)、基于 Zookeeper 实现分布式锁
前两种对于分布式生产环境来说并不是特别推荐,高并发下数据库锁性能太差,Redis在锁时间限制和缓存一致性存在一定问题。
三、Zookeeper 实现分布式锁
Zookeeper的数据存储结构就像一棵树,这棵树由节点组成,这种节点叫做Znode。如下图:
Znode分为四种类型:
1).持久节点 (PERSISTENT):默认的节点类型。创建节点的客户端与zookeeper断开连接后,该节点依旧存在 。
2).持久节点顺序节点(PERSISTENT_SEQUENTIAL):所谓顺序节点,就是在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号:
3).临时节点(EPHEMERAL):和持久节点相反,当创建节点的客户端与zookeeper断开连接后,临时节点会被删除:
4).临时顺序节点(EPHEMERAL_SEQUENTIAL)顾名思义,临时顺序节点结合和临时节点和顺序节点的特点:在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号;当创建节点的客户端与zookeeper断开连接后,临时节点会被删除。
Zookeeper分布式锁的原理
Zookeeper分布式锁恰恰应用了临时顺序节点。具体如何实现呢?让我们来看一看详细步骤:
1)、获取锁
首先,在Zookeeper当中创建一个持久节点ParentLock。当第一个客户端想要获得锁时,需要在ParentLock这个节点下面创建一个临时顺序节点 Lock1。
之后,Client1查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock1是不是顺序最靠前的一个。如果是第一个节点,则成功获得锁。
这时候,如果再有一个客户端 Client2 前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock2。
Client2查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock2是不是顺序最靠前的一个,结果发现节点Lock2并不是最小的。
于是,Client2向排序仅比它靠前的节点Lock1注册Watcher,用于监听Lock1节点是否存在。这意味着Client2抢锁失败,进入了等待状态。
这时候,如果又有一个客户端Client3前来获取锁,则在ParentLock下载再创建一个临时顺序节点Lock3。
Client3查找ParentLock下面所有的临时顺序节点并排序,判断自己所创建的节点Lock3是不是顺序最靠前的一个,结果同样发现节点Lock3并不是最小的。
于是,Client3向排序仅比它靠前的节点Lock2注册Watcher,用于监听Lock2节点是否存在。这意味着Client3同样抢锁失败,进入了等待状态。
这样一来,Client1得到了锁,Client2监听了Lock1,Client3监听了Lock2。这恰恰形成了一个等待队列,很像是Java当中ReentrantLock所依赖的
2)释放锁
释放锁分为两种情况:A
A、任务完成,客户端显示释放
当任务完成时,Client1会显示调用删除节点Lock1的指令。
B、任务执行过程中,客户端崩溃
获得锁的Client1在任务执行过程中,如果Duang的一声崩溃,则会断开与Zookeeper服务端的链接。根据临时节点的特性,相关联的节点Lock1会随之自动删除。
由于Client2一直监听着Lock1的存在状态,当Lock1节点被删除,Client2会立刻收到通知。这时候Client2会再次查询ParentLock下面的所有节点,确认自己创建的节点Lock2是不是目前最小的节点。如果是最小,则Client2顺理成章获得了锁。
同理,如果Client2也因为任务完成或者节点崩溃而删除了节点Lock2,那么Client3就会接到通知。
最终,Client3成功得到了锁。
Springboot实现zookeeper分布式锁,这里使用第三方库Curator来实现对zookeeper的操作
Curator是Netflix公司开源的一套zookeeper客户端框架,解决了很多Zookeeper客户端非常底层的细节开发工作,包括连接重连、反复注册Watcher和NodeExistsException异常等等。
四、代码示例
1、maven引入
<!-- ZooKeeper 之 Curator-->
<!-- ZooKeeper版本号为4的话,机器安装zookeeper的版本要求是3.5及其以上的版本-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.1</version>
</dependency>
2、配置文件 application.properties
server.port= 10181
##ZooKeeper 集成 Curator
zk.url = 127.0.0.1:2187
3、代码类
配置类:
package com.allen.lock.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author xuguocai 2020/9/3 11:00
*/
@Configuration
@Slf4j
public class ZooKeeperConf {
@Value("${zk.url}")
private String zkUrl;
@Bean
public CuratorFramework getCuratorFramework() {
// 用于重连策略,1000毫秒是初始化的间隔时间,3代表尝试重连次数
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient(zkUrl, retryPolicy);
//必须调用start开始连接ZooKeeper
client.start();
return client;
}
}
controller类:
package com.allen.lock.controller;
import com.allen.lock.service.ZooKeeperService;
import org.apache.curator.framework.CuratorFramework;
import org.apache.zookeeper.data.Stat;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author xuguocai 2020/9/3 11:12
*/
@RestController
@RequestMapping("/zookeeper")
public class ZooKeeperController {
@Autowired
private ZooKeeperService zooKeeperService;
@Autowired
private CuratorFramework zkClient;
/**
* zookeeper 获取节点下的数据
*
* post请求: http://localhost:10181/zookeeper/getData?path=/task
*
* @param path
* @return
*/
@PostMapping("/getData")
public String getData(@RequestParam String path) {
byte[] bytes = null;
try {
bytes = zkClient.getData().forPath(path);
} catch (Exception e) {
e.printStackTrace();
}
String str = new String(bytes);
return str;
}
/**
* 创建节点路径
* @param path
* @return
*/
@PostMapping("/create")
public String create(@RequestParam String path) {
try {
zkClient.create().forPath(path);
} catch (Exception e) {
e.printStackTrace();
}
return "success";
}
@PostMapping("/delete")
public String delete(@RequestParam String path) {
try {
zkClient.delete().forPath(path);
} catch (Exception e) {
e.printStackTrace();
}
return "success";
}
@PostMapping("/setData")
public String setData(@RequestParam(value = "path") String path, @RequestParam(value = "data") String data) {
try {
zkClient.setData().forPath(path, data.getBytes());
} catch (Exception e) {
e.printStackTrace();
}
return "success";
}
@PostMapping("/check")
public String check(@RequestParam(value = "path") String path) {
Stat stat = null;
try {
stat = zkClient.checkExists().forPath(path);
} catch (Exception e) {
e.printStackTrace();
}
return "stat" + stat;
}
@PostMapping("/children")
public String children(@RequestParam(value = "path") String path) {
List<String> children = null;
try {
children = zkClient.getChildren().forPath(path);
} catch (Exception e) {
e.printStackTrace();
}
return "children" + children;
}
@PostMapping("/watch")
public String watch(@RequestParam(value = "path") String path) {
Stat stat = null;
try {
stat = zkClient.checkExists().watched().forPath(path);
} catch (Exception e) {
e.printStackTrace();
}
return "watch " + stat;
}
/**
* zookeeper分布式锁
*
* @param product
* @return
*/
@PostMapping("/makeOrder")
public String makeOrder(@RequestParam(value = "product") String product) {
zooKeeperService.makeOrder(product);
return "success";
}
}
实现类:
package com.allen.lock.service;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @author xuguocai 2020/9/3 11:12
*/
@Service
@Slf4j
public class ZooKeeperService {
private static final String lockPath = "/lock/order";
@Autowired
private CuratorFramework zkClient;
public void makeOrder(String product) {
log.info("try do job for " + product);
String path = lockPath + "/" + product;
try {
// InterProcessMutex 构建一个分布式锁
InterProcessMutex lock = new InterProcessMutex(zkClient, path);
try {
if (lock.acquire(5, TimeUnit.HOURS)) { // 获得锁
// 模拟业务处理耗时5秒
Thread.sleep(5*1000);
log.info("do job " + product + " done");
}
} finally {
// 释放该锁
lock.release();
}
} catch (Exception e) {
log.error("失败:{}",e.getMessage());
// zk异常
e.printStackTrace();
}
}
}
学习地址:
springboot基于zookeeper实现分布式锁_holmes369的博客-CSDN博客
Spring Boot 集成 ZooKeeper,及其简单应用和分布式锁_kula丶diamond的博客-CSDN博客
项目地址:
https://github.com/krycai/gc-framework/tree/master/gc-zookeeper