目录
- 3.1、Zookeeper安装和相关概念
- 3.1.1 安装启动
- 3.1.2 相关概念
- 3.1.3 Java客户端
- 3.2 Zookeeper实现分布式锁的思路分析
- 3.3 ZooKeeper分布式锁的基本实现
3.1、Zookeeper安装和相关概念
3.1.1 安装启动
# 解压到/mysoft文件夹下
tar -zxvf zookeeper-3.7.0-bin.tar.gz
# 重命名
mv apache-zookeeper-3.7.0-bin/ zookeeper
# 打开zookeeper根目录
cd /mysoft/zookeeper
# 创建一个数据目录,备用
mkdir data
# 打开zk的配置目录
cd /mysoft/zookeeper/conf
# copy配置文件,zk启动时会加载zoo.cfg文件
cp zoo_sample.cfg zoo.cfg
# 编辑配置文件
vim zoo.cfg
# 修改dataDir参数为之前创建的数据目录:/mysoft/zookeeper/data
# 切换到bin目录
cd /mysoft/zookeeper/bin
# 启动
./zkServer.sh start
./zkServer.sh status # 查看启动状态
./zkServer.sh stop # 停止
./zkServer.sh restart # 重启
./zkCli.sh # 查看zk客户端
如下,说明启动成功:
3.1.2 相关概念
Zookeeper提供一个多层级的节点命名空间(节点称为znode),每个节点都用一个以斜杠(/)分隔的路径表示,而且每个节点都有父节点(根节点除外),非常类似于文件系统。并且每个节点都是唯一的。
1、znode节点有四种类型:
- PERSISTENT:永久节点。客户端与zookeeper断开连接后,该节点依旧存在
- EPHEMERAL:临时节点。客户端与zookeeper断开连接后,该节点被删除
- PERSISTENT_SEQUENTIAL:永久节点、序列化。客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号
- EPHEMERAL_SEQUENTIAL:临时节点、序列化。客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号
create /aa test # 创建持久化节点
create -e /cc test # 创建临时节点
create -s /bb test # 创建持久序列化节点
create -e -s /dd test # 创建临时序列化节点
2、事件监听
在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper针对节点的监听有如下四种事件:
-
节点创建:stat -w /xx
当/xx节点创建时:NodeCreated
-
节点删除:stat -w /xx
当/xx节点删除时:NodeDeleted
-
节点数据修改:get -w /xx
当/xx节点数据发生变化时:NodeDataChanged
-
子节点变更:ls -w /xx
当/xx节点的子节点创建或者删除时:NodeChildChanged
【注意】zookeeper监听都是一次性的
3.1.3 Java客户端
ZooKeeper的java客户端有:
- 原生客户端
- ZkClient
- Curator框架(类似于redisson,有很多功能性封装)
Java命令演示:
public class ZkTest {
public static void main(String[] args) throws InterruptedException {
ZooKeeper zooKeeper = null;
CountDownLatch countDownLatch = new CountDownLatch(1);
try {
zooKeeper = new ZooKeeper("192.168.239.11:2181", 30000, new Watcher() {
@Override
public void process(WatchedEvent event) {
Event.KeeperState state = event.getState();
if (Event.KeeperState.SyncConnected.equals(state)) {
System.out.println("获取zookeeper连接了.....");
countDownLatch.countDown();
} else if (Event.KeeperState.Closed.equals(state)) {
System.out.println("关闭连接......");
}
}
});
countDownLatch.await();
System.out.println("zookeeper一顿操作...");
// 一、创建一个节点
// 创建一个节点,1-节点路径 2-节点内容 3-节点的访问权限 4-节点类型
// OPEN_ACL_UNSAFE:任何人可以操作该节点
// CREATOR_ALL_ACL:创建者拥有所有访问权限
// READ_ACL_UNSAFE: 任何人都可以读取该节点
// 创建持久化节点
// zooKeeper.create("/atguigu", "haha~~".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
// 创建临时节点
// zooKeeper.create("/test", "haha~~".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
// 创建永久序列化
// zooKeeper.create("/atguigu/cc", "haha~~".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL);
// 创建临时序列化节点
// zooKeeper.create("/atguigu/dd", "haha~~".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// zooKeeper.create("/atguigu/dd", "haha~~".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// zooKeeper.create("/atguigu/dd", "haha~~".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 二、判断节点是否存在
Stat stat = zooKeeper.exists("/test", true);
if (stat != null){
System.out.println("当前节点存在!" + stat.getVersion());
} else {
System.out.println("当前节点不存在!");
}
// 三、获取一个节点的数据
byte[] data = zooKeeper.getData("/aa/bb0000000000", false, null);
System.out.println("节点/aa/bb0000000000下的数据为:" + new String(data));
// 查询一个节点的所有子节点
List<String> children = zooKeeper.getChildren("/aa", new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("的子节点发生变化");
}
});
System.out.println("/aa节点下的子节点:" + children);
// 更新
// zooKeeper.setData("/atguigu", "wawa...".getBytes(), stat.getVersion());
// 删除一个节点
//zooKeeper.delete("/atguigu", -1);
System.in.read();
} catch (IOException e) {
throw new RuntimeException(e);
} catch (KeeperException e) {
throw new RuntimeException(e);
} finally {
if (zooKeeper != null) {
zooKeeper.close();
}
}
}
}
3.2 Zookeeper实现分布式锁的思路分析
获取锁:create一个节点(因为一个节点只能创建一次,当一个线程创建完一个节点后,其他线程都不能创建该节点了)
删除锁:delete一个节点
参照redis分布式锁的特点:
- 互斥 排他
- 防死锁:
- 可自动释放锁(临时节点) :获得锁之后客户端所在机器宕机了,客户端没有主动删除子节点;如果创建的是永久的节点,那么这个锁永远不会释放,导致死锁;由于创建的是临时节点,客户端宕机后,过了一定时间zookeeper没有收到客户端的心跳包判断会话失效,将临时节点删除从而释放锁。
- 可重入锁:借助于ThreadLocal
- 防误删:宕机自动释放临时节点,不需要设置过期时间,也就不存在误删问题。
- 加锁/解锁要具备原子性
- 单点问题:使用Zookeeper可以有效的解决单点问题,ZK一般是集群部署的。
- 集群问题:zookeeper集群是强一致性的,只要集群中有半数以上的机器存活,就可以对外提供服务。
3.3 ZooKeeper分布式锁的基本实现
实现思路:
- 多个请求同时添加一个相同的临时节点,只有一个可以添加成功。添加成功的获取到锁
- 执行业务逻辑
- 完成业务流程后,删除节点释放锁。
基本实现
由于zookeeper获取链接是一个耗时过程,这里可以在项目启动时,初始化链接,并且只初始化一次。借助于spring特性,代码实现如下:
public class ZkClient {
private ZooKeeper zooKeeper;
@PostConstruct
public void init() {
// 项目启动时,创建zk连接
CountDownLatch countDownLatch = new CountDownLatch(1);
try {
zooKeeper = new ZooKeeper("192.168.239.11:2181", 30000, new Watcher() {
@Override
public void process(WatchedEvent event) {
Event.KeeperState state = event.getState();
if (Event.KeeperState.SyncConnected.equals(state)) {
System.out.println("获取zookeeper连接了.....");
countDownLatch.countDown();
} else if (Event.KeeperState.Closed.equals(state)) {
System.out.println("关闭连接......");
}
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
@PreDestroy
public void destory() {
// 项目销毁前,释放zk连接
if(zooKeeper!=null) {
try {
zooKeeper.close();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 获取分布式锁
public ZkDistributedLock getLock(String name) {
return new ZkDistributedLock(name,zooKeeper);
}
}
zk分布式锁具体实现:
public class ZkDistributedLock {
private static final String ROOT_PATH = "/distributed";
private String path;
private ZooKeeper zooKeeper;
public ZkDistributedLock(ZooKeeper zooKeeper, String lockName){
this.zooKeeper = zooKeeper;
this.path = ROOT_PATH + "/" + lockName;
}
public void lock(){
try {
zooKeeper.create(path, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
} catch (Exception e) {
// 重试
try {
Thread.sleep(200);
lock();
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
public void unlock(){
try {
this.zooKeeper.delete(path, 0);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
}
改造StockService的checkAndLock方法:
@Autowired
private ZkClient client;
public void checkAndLock() {
// 加锁,获取锁失败重试
ZkDistributedLock lock = this.client.getZkDistributedLock("lock");
lock.lock();
// 先查询库存是否充足
Stock stock = this.stockMapper.selectById(1L);
// 再减库存
if (stock != null && stock.getCount() > 0){
stock.setCount(stock.getCount() - 1);
this.stockMapper.updateById(stock);
}
// 释放锁
lock.unlock();
}
测试
存在的问题
- 性能一般(比mysql分布式锁略好)
- 不可重入