本文需要读者有一定的基础,最后一个分布式锁案例部分需要有AOP、JUC、反射、自定义注解等基础。不适合当面试题,适合实操。
为什么会使用这些技术进行分布式锁的操作,是因为楼主的公司是这样做的,然后非常的方便,只需要在需要的地方(类、方法)添加注解就能实现分布式锁,小伙伴们跟着楼主一起敲一下命令和代码,就能学会Zookeeper的相关知识点。
Lets go!
再等下!不会有人SpringMVC还没学就来看Zookeeper吧,凝视 .jpg,岂不跟我一样是天才少年?冲!
最后等下!本文没有介绍Zookeeper的下载和安装,博主是使用的zookeeper3.5.10,并且是在windows环境下使用的。给你们推荐个安装Zookeeper的网址:ZooKeeper下载和配置
目录
一、定义一个是临界资源的bean(Ticket)表示所有的票都在这里出,这个bean是单例的;
二、在configuration里面进行配置好InterProcessMutex
Zookeeper结合AOP和自定义注解实现分布式锁(重要)
zookeeper是什么
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,主要为了解决分布式架构下数据一致性问题,典型的应用场景有分布式配置中心、分布式注册中心、分布式锁、分布式队列、集群选举、分布式屏障、发布/订阅等场景。
zookeeper的数据结构
Zookeeper是一个类似于文件系统的数据结构,最外层我们可以想象成一个大的文件夹,里面都是一些小的文件夹。可以跟Linux的文件路径进行联想。
zk的节点类型:
-
持久化目录节点: 客户端与zookeeper断开连接后,该节点依旧存在,只要不手动删除该节点,他将永远存在。
-
持久化顺序编号目录节点:-s 客户端与zookeeper断开连接后,该节点依旧存在,只是zookeeper给该节点名称进行顺序编号。
-
临时目录节点:-e 客户端与zookeeper断开连接后,该节点被删除。
-
临时顺序编号目录节点:-se 客户端与zookeeper断开连接后,该节点被删除,只是zookeeper给该节点名称进行顺序编号。
Zookeeper客户端命令(CURD)
-
ls [path]:查看某个节点下的节点
-
ls -s [path]:查看某个节点的详细信息
-
create [path]:在某个节点下创建节点
-
create [path] [value]:在某个节点下创建节点(带数据)
-
get [path]:获取某个节点的数据
-
set [path]:设置/修改某个节点的数据
-
delete [path]:删除某个节点
-
节点不能重复创建
-
deleteall [path]:删除节点及其子节点
-
help:查看zk的命令
以下是演示过程,根节点用反斜杠来代表 /
-
创建临时节点:create -e [path]
使用参数 -e来指定为临时节点
-
创建顺序节点:create -s [path]
使用参数 -s来指定顺序节点
-
创建临时顺序节点:create -es [path]
Zookeeper JavaAPI 操作(Curator)
常见的Zookeeper的API:
-
原生Java API:官方提供,不好用,写代码麻烦
-
ZKClient:在原生API上面做了一些封装,依旧很麻烦
-
Curator:简化了上面的操作
Curator介绍
Curator是Netflix公司开源的一套Zookeeper客户端框架。了解过Zookeeper原生API都会清楚其复杂度。Curator帮助我们在其基础上进行封装、实现一些开发细节,包括接连重连、反复注册Watcher和NodeExistsException等。目前已经作为Apache的顶级项目出现,是最流行的Zookeeper客户端之一。
Curator的使用
-
添加依赖:如果zookeeper是3.5.x以上的,就要用curator的4.0.0以上的;
<dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>4.0.0</version> </dependency>
-
在配置类中返回类型为CuratorFramework的bean,创建CuratorFramework的方式有两种:
-
第一种,通过工厂创建
/** connectString – 连接字符串,zk的地址和端口:localhost:2181 sessionTimeoutMs – 会话超时时间 ms connectionTimeoutMs – 连接超时时间 ms retryPolicy – 重试策略 */ @Bean public CuratorFramework curatorFramework(){ CuratorFramework client = CuratorFrameworkFactory.newClient( "localhost:2181", 60*1000, 15*1000, new ExponentialBackoffRetry(3000, 10) ); return client; }
-
第二种(推荐),使用build创建:可以指定命名空间,指定命名空间实际上就是指定一个根目录,所有的操作都会在这个目录下,达到逻辑隔离的目的。
@Bean public CuratorFramework curatorFramework(){ CuratorFramework client = CuratorFrameworkFactory.builder() .connectString("localhost:2181") .sessionTimeoutMs(60 * 1000) .connectionTimeoutMs(15 * 1000) .retryPolicy(new ExponentialBackoffRetry(3000, 10)) .namespace("wangpeng")//命名空间,所有操作都在这个目录下进行 .build(); return client; }
-
-
在需要的地方自动注入,并且调用start()方法,就能通过curator操作zookeeper啦
Curator创建节点
1.基本创建
2.创建节点带有数据
3.设置节点的类型
4.创建多级节点
@Test
public void createTest1() throws Exception {
client.start();
//如果创建节点,没有指定数据,则将当前客户端的ip指定为数据
String path = client.create().forPath("/app1");
System.out.println(path);
}
@Test
public void createTest2() throws Exception{
client.start();
//创建节点带有数据
String path = client.create().forPath("/app2","hehe".getBytes(StandardCharsets.UTF_8));
System.out.println(path);
}
@Test
public void createTest3() throws Exception{
client.start();
//设置节点类型:使用.withMode的方式,里面传入一个枚举类型
//默认类型:持久化的
String path = client.create().withMode(CreateMode.EPHEMERAL).forPath("/app3");
System.out.println(path);
}
@Test
public void createTest4() throws Exception{
client.start();
//创建多级节点
//creatingParentsIfNeeded()如果需要就创建父节点
String path = client.create().creatingParentsIfNeeded().forPath("/app4/p1");
System.out.println(path);
}
Curator查询节点
1.查询数据:get
2.查询子节点:ls
3.查询节点状态信息 ls -s
@Test
public void testGet1() throws Exception {
client.start();
//查询数据:get
byte[] data = client.getData().forPath("/app1");
System.out.println(new String(data));
}
@Test
public void testGet2() throws Exception {
client.start();
//查询子节点:ls
List<String> strings = client.getChildren().forPath("/");
strings.forEach(s -> System.out.println(s));
}
@Test
public void testGet3() throws Exception {
client.start();
//查询节点状态信息 ls -s
//创建一个状态的对象
Stat status = new Stat();
client.getData().storingStatIn(status).forPath("/app1");
System.out.println(status);
}
Curator修改数据
1.修改数据:setData().forPath()
2.根据版本修改(乐观锁):setData().withVersion(version).forPath(),乐观锁就不展开讲了,有兴趣的同学可以自己去了解
@Test
public void testSet() throws Exception {
client.start();
// 1.修改数据
client.setData().forPath("/app1","ppp".getBytes(StandardCharsets.UTF_8));
}
@Test
public void testSetForVersion() throws Exception {
client.start();
// 2.根据版本修改(乐观锁)
Stat status = new Stat();
client.getData().storingStatIn(status).forPath("/app1");
int version = status.getVersion();
client.setData().withVersion(version).forPath("/app1","test".getBytes(StandardCharsets.UTF_8));
}
Curator删除节点
1.删除单个节点
2.删除带有子节点的节点
3.必须删除成功
4.回调
@Test
public void testDelete1() throws Exception {
client.start();
// 1.删除单个节点
client.delete().forPath("/app1");
}
@Test
public void testDelete2() throws Exception {
client.start();
// 2.删除带有子节点的节点
client.delete().deletingChildrenIfNeeded().forPath("/app4");
}
@Test
public void testDelete3() throws Exception {
client.start();
// 3.必须删除成功,会一直重试
client.delete().guaranteed().forPath("/app2");
}
@Test
public void testDelete4() throws Exception {
client.start();
// 4.回调
client.delete()
.guaranteed()
.inBackground(new BackgroundCallback() {
@Override
public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
System.out.println("我被删除啦~~");
System.out.println(event);
}
})
.forPath("/app1");
}
Watch事件监听
zookeeper允许用户在指定节点上注册一些Watcher,并且在特定事件触发的时候,zookeeper服务端会将事件通知到感兴趣的客户端上面去,该机制是zookeeper实现分布式协调服务的重要特性。
zookeeper中引入了Watcher机制来实现发布/订阅功能,能够让多个订阅者同事监听某一个对象,当一个对象自身状态变化时,会通知所有订阅者。
Curator引入了Cache来实现对zookeeper服务的事件的监听。
Curator提供了三种Watcher:
-
NodeCache:只是监听某一个特定的节点(增删改都会监听到)
@Test public void testNodeCache() throws Exception { // 1.创建NodeCache对象 NodeCache nodeCache = new NodeCache(client,"/app1"); // 2.注册监听 nodeCache.getListenable().addListener(new NodeCacheListener() { @Override public void nodeChanged() throws Exception { System.out.println("节点变化了"); //获取节点更改后的数据 byte[] data = nodeCache.getCurrentData().getData(); System.out.println(new String(data)); } }); // 3.开启监听:如果设置为true则开启监听是加载缓冲数据 nodeCache.start(true); //这个while是为了测试的时候,不让进程终止 while (true) { } }
-
PathChildrenCache:监控某一个ZNode节点的子节点,不包括本身
@Test public void testPathChildrenCache() throws Exception { //创建监听对象 PathChildrenCache pathChildrenCache = new PathChildrenCache(client, "/app2", true); //绑定监听器 pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() { @Override public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent event) throws Exception { System.out.println("子节点变化了"); System.out.println(event); PathChildrenCacheEvent.Type type = event.getType(); if (type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)) { byte[] data = event.getData().getData(); System.out.println("修改后的数据是:"+new String(data)); } } }); //开启监听 pathChildrenCache.start(); while (true) { } }
-
TreeCache:可以监控整个树上的所有节点,类似于上面两种的结合
@Test public void testTreeCache() throws Exception { //1创建监听器 TreeCache treeCache = new TreeCache(client, "/"); //2注册监听 treeCache.getListenable().addListener(new TreeCacheListener() { @Override public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent event) throws Exception { System.out.println("子节点变化了"); System.out.println(event); TreeCacheEvent.Type type = event.getType(); if (type.equals(TreeCacheEvent.Type.NODE_UPDATED)) { byte[] data = event.getData().getData(); System.out.println("修改后的数据是:"+new String(data)); } } }); //3开启 treeCache.start(); while (true) { } }
分布式锁
当我们的应用是分布式集群工作环境下,属于多JVM下的工作环境,跨JVM之间已经无法通过多线程的锁解决同步问题。
分布式锁是:处理跨机器的进程之间的数据同步问题
分布式锁实现:
-
基于缓存的方式:redis、MemCache
-
zookeeper实现分布式锁
-
数据库层面实现分布式锁:悲观锁、乐观锁
ZooKeeper分布式锁原理:
核心思想:当客户端要获取锁,则创建节点,使用完锁,则删除节点
-
客户端获取锁时,在lock节点下创建临时顺序节点
-
然后所有客户端都获取lock下面的所有子节点,客户端获取到子节点之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。使用完锁之后,将该节点删除。
-
如果发现自己创建的节点并非lock所有子节点中最小的,说明自己没有获得到锁,此时客户端需要找到比自己小的哪个节点,同时注册事件监听器,监听删除事件。(只找一个比自己小的节点,例如:2找1,3找2
-
如果发现比自己小的哪个节点被删除,则客户端的Watcher会收到相应通知,此时再判断自己创建的节点是否是lock子节点中序号最小的,如果是则获取到了锁,如果不是则重复3步骤
Curator实现分布式锁的API:
类型 | |
---|---|
分布式排它锁(非可重入锁) | InterProcessSemaphoreMutex |
分布式可重入排它锁 | InterProcessMutex |
分布式读写锁 | InterProcessReadWriteLock |
将多个锁作为单个实体容器管理的容 | InterProcessMultiLock |
共享信号量 | InterProcessSemaphoreV2 |
Curator使用分布式可重入锁的案例:
使用买票的场景来模拟分布式锁的应用,有一个出票的机构,然后有两个不同服务器的买票机构(用两个线程模拟),然后在出票的地方加上分布式锁;
首先Ticket代表出票的机构,然后两个现场分别在他的run方法里面进行卖票;
首先是Ticket类的实现,定义两个变量:tickets代表剩余票的数量、lock是InterProcessMutex(分布式可重入锁)的引用;
然后是锁的初始化,在无参构造函数里面,进行初始化:首先使用Curator连接到Zookeeper,然后使用client传入锁的构造函数;
然后就是在操作临界资源之前使用acquire()方法获得锁,之后使用release()方法释放锁;
public class Ticket implements Runnable {
private int tickets = 10;
private InterProcessMutex lock;
public Ticket() {
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("localhost:2181")
.sessionTimeoutMs(60 * 1000)
.connectionTimeoutMs(15 * 1000)
.retryPolicy(new ExponentialBackoffRetry(3000, 10))
.build();
client.start();
lock = new InterProcessMutex(client,"/lock");
}
@Override
public void run() {
try {
//获取锁,参数是时间,代表等等锁的时间
lock.acquire(3, TimeUnit.SECONDS);
while (tickets>0){
System.out.println(Thread.currentThread().getName()+"买了一张票"+tickets--);
}
} catch (Exception e) {
e.printStackTrace();
}finally {
try {
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
Zookeeper整合自定义注解实现分布式锁(了解)
还是卖票的案例,将在web的环境下实现;主要目的是帮助楼主回想反射和自定义注解的知识点,有点忘了,尴尬.jpg
一、定义一个是临界资源的bean(Ticket)表示所有的票都在这里出,这个bean是单例的;
@Component
public class Ticket {
private int tickets;
//初始化票的数量
@PostConstruct
private void init(){
this.tickets = 10;
}
public String saleTicket() {
return tickets>0?"已经售出票,剩余数量:" + --this.tickets:"没有余票了";
}
public String get(){
return "剩余票数是:"+this.tickets;
}
}
二、在configuration里面进行配置好InterProcessMutex
@Bean
public CuratorFramework curatorFramework(){
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("localhost:2181")
.sessionTimeoutMs(60 * 1000)
.connectionTimeoutMs(15 * 1000)
.retryPolicy(new ExponentialBackoffRetry(3000, 10))
// .namespace("lock")//命名空间,所有操作都在这个目录下进行
.build();
return client;
}
@Bean
public InterProcessMutex interProcessMutex(CuratorFramework curatorFramework){
curatorFramework.start();
InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework, "/lock");
return interProcessMutex;
}
三、自定义一个注解
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}
四、定义一个用来加锁的bean,会在service层调用它
自动注入:临界资源(Ticket)、锁(InterProcessMutex)
@Component
public class TicketLock {
@Autowired
Ticket ticket;
@Autowired
InterProcessMutex lock;
//逻辑是:传进来service层的类名,通过类名获得类模板
//然后判断是否被自定义注解标记,如果标记了,则上锁;如果没标记,则不能修改临界资源
public String sale(String className){
String result = "加锁失败,无法购票";
try {
Class<?> cls = Class.forName(className);
if (cls.isAnnotationPresent(MyAnnotation.class)) {
lock.acquire();
result = ticket.saleTicket();
lock.release();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
五、定义好正常的Controller层和Service层
controller
@RestController
@RequestMapping("/ticket")
public class TicketController {
@Autowired
TicketService service;
@RequestMapping("/sale")
public String sale(){
return service.sale();
}
@RequestMapping("/get")
public String get(){
return service.get();
}
}
service
@Service
public class TicketServiceImpl implements TicketService {
@Autowired
TicketLock lock;
@Override
public String sale() {
String name = this.getClass().getName();
System.out.println(name);
return lock.sale(name);
}
}
六、测试效果
-
service添加了
@MyAnnotation
注解-
访问路径:http://localhost:9090/ticket/sale
-
多次访问结果:
已经售出票,剩余数量:10
已经售出票,剩余数量:9
...
没有余票了
-
-
service没有添加
@MyAnnotation
注解-
访问路径:http://localhost:9090/ticket/sale
-
多次访问结果:
加锁失败,无法购票
加锁失败,无法购票
...
加锁失败,无法购票
-
Zookeeper结合AOP和自定义注解实现分布式锁(重要)
这是实际开发过程中经常会使用的一种通用的方式,上面那个例子只是用来演示反射和注解的一种实现,没有实际意义
一、改变上面例子的service
因为是是使用AOP的方式,所有service直接改成正常的情况就行了,平时咋写就咋写
@Service
@MyAnnotation
public class TicketServiceImpl implements TicketService {
@Autowired
Ticket ticket;
@Override
@MyAnnotation
public String sale() {
return ticket.saleTicket();
}
@Override
public String get() {
return ticket.get();
}
}
二、引入AOP所需要的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
三、定义切片类,切入点,通知
-
在spring容易能扫到的位置定义一个类,加上
@Component
和@Component
注解,使成为一个切片类 -
自动注入Curator的锁:InterProcessMutex(可重入分布式锁)
-
定义好切入点表达式:这里定义的是工程下Service层的所有函数(无论返回值,参数数量等)
-
写通知方法,逻辑是:
-
首先获得类对象,判断类上面是否加了
@MyAnnotation
注解,如果加了则所有的的函数都会上锁 -
如果类上面没有加,获得方法的签名(MethodSignature),实例化方法;判断方法上面是否加了
@MyAnnotation
注解,如果加了则给这个方法上锁。 -
然后是在finally块判断:锁是否加上了,如果加上了则释放。如果没有加上则证明:该类和该方法都没有被
@MyAnnotaion
注解,所以直接放行
-
@Aspect
@Component
public class AspectLock {
@Autowired
InterProcessMutex interProcessMutex;
@Pointcut("execution(* com.uyun.curator.Service..*.*(..))")
public void cutOnClass(){}
@Around("cutOnClass()")
public Object lockOnClass(ProceedingJoinPoint pjp){
//定义一个连接点(被AOP的那个方法)的返回值
Object result = null;
//获得TicketServiceImpl的类对象
Class<?> cls = pjp.getTarget().getClass();
try {
//判断类是否被@MyAnnotation注解
if (cls.isAnnotationPresent(MyAnnotation.class)){
System.out.println("通过注解类上锁了...");
interProcessMutex.acquire();//上锁
result = pjp.proceed();//执行方法,获得返回值
}else {
//类没有被@MyAnnotation注解的时候,拿到方法的签名:里面包括方法的名字,参数类型等
MethodSignature signature = (MethodSignature) pjp.getSignature();
//获得方法的名字,和参数类型
String name = signature.getName();
Class[] parameterTypes = signature.getParameterTypes();
//获得该方法的对象
Method method = cls.getMethod(name,parameterTypes);
//判断类是否被@MyAnnotation注解
if (method.isAnnotationPresent(MyAnnotation.class)) {
interProcessMutex.acquire();//上锁
System.out.println("通过注解方法上锁了...");
result = pjp.proceed();//执行方法,获得返回值
}
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
try {
//判断是否上锁了,如果上锁了则解锁
if (interProcessMutex.isAcquiredInThisProcess()){
interProcessMutex.release();
}else {
//没有上锁,证明:该方法和类都没有被@MyAnnotation标注,直接执行不继续上锁;
System.out.println("没有上锁,放行");
result = pjp.proceed();
}
} catch (Throwable e) {
e.printStackTrace();
}
}
return result;
}
}
四、测试效果
-
service没有添加
@MyAnnotation
注解,sale()方法上加了-
访问路径:http://localhost:9090/ticket/sale
-
多次测试结果:成功返回数据
-
-
-
service没有添加
@MyAnnotation
注解,get()方法也没有添加-
访问路径:http://localhost:9090/ticket/get
-
多次测试结果:成功返回数据
-
-
-
service添加
@MyAnnotation
注解,sale()方法也加了-
访问路径:http://localhost:9090/ticket/sale
-
多次测试结果:成功返回数据
-
-
-
service添加
@MyAnnotation
注解,get()方法没有添加-
访问路径:http://localhost:9090/ticket/get
-
多次测试结果:成功返回数据
-
-