第七阶段模块一
分布式技术-Zookeeper
1. Zookeeper概述
1.1 概述
美团,饿了么,淘宝,58同城等等应用都是zookeeper的现实生活版。
老孙我开了个饭店,如何才能让大家都能吃到我们的饭菜?需要入驻美团,这样大家就可以在美团 app中看到我的饭店,下订单,从而完成一次交易。
Zookeeper是一个开源的分布式(多台服务器干一件事)的,为分布式应用提供协调服务的 Apache项目。
在大数据技术生态圈中,zookeeper(动物管理员),Hadoop(大象),Hive(蜜蜂), Pig(猪)等技术
1.2 工作机制
Zookeeper从设计模式角度来理解:是一个基于观察者模式(一个人干活,有人盯着他)设计的分布式服务管理框架
它负责 存储 和 管理 大家都关心的数据
然后接受观察者的注册,一旦这些数据的发生变化
Zookeeper就将负责通知已经注册的那些观察者做出相应的反应
从而实现集群中类似Master/Slave管理模式
Zookeeper = 文件系统 + 通知机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4WmaxNtL-1631713315118)(E:\MarkDown\拉勾笔记\zookeeper-工作机制)]
- 商家营业并入驻
- 获取到当前营业的饭店列表
- 服务器节点下线
- 服务器节点上下线事件通知
- 重新再去获取服务器列表,并注册监听
1.3 特点
分布式和集群的区别?
无论分布式和集群,都是很多人在做事情。具体区别如下:
例如:我有一个饭店,越来越火爆,我得多招聘一些工作人员
分布式:招聘1个厨师,1个服务员,1个前台,三个人负责的工作不一样,但是最终目的都是为饭店工作
集群:招聘3个服务员,3个人的工作一样
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mdyjt9S6-1631713315122)(E:\MarkDown\拉勾笔记\zookeeper-特点)]
- 是一个leader和多个follower来组成的集群(狮群中,一头雄狮,N头母狮)
- 集群中只要有半数以上的节点存活,Zookeeper就能正常工作(5台服务器挂2台,没问题;4台服务器挂2台,就停止)
- 全局数据一致性,每台服务器都保存一份相同的数据副本,无论client连接哪台server,数据都是一致的
- 数据更新原子性,一次数据要么成功,要么失败(不成功便成仁)
- 实时性,在一定时间范围内,client能读取到最新数据
- 更新的请求按照顺序执行,会按照发送过来的顺序,逐一执行(发来123,执行123,而不是321 或者别的)
1.4 数据结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ohH8PpHv-1631713315125)(E:\MarkDown\拉勾笔记\zookeeper-数据结构)]
ZooKeeper数据模型的结构与linux文件系统很类似,整体上可以看作是一棵树,每个节点称做一 个ZNode(ZookeeperNode)。
每一个ZNode默认能够存储1MB的数据(元数据),每个ZNode的路径都是唯一的
元数据(Metadata),又称中介数据、中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息,用来支持如指示存储位置、历史数据、 资源查找、文件记录等功能
1.5 应用场景
提供的服务包括:统一命名服务、统一配置管理、统一集群管理、服务器节点动态上下线、软负载均衡等
1.5.1 统一命名服务
在分布式环境下,通常需要对应用或服务进行统一的命名,便于识别
例如:服务器的IP地址不容易记,但域名相比之下却是很容易记住
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2Q9yTVhg-1631713315129)(E:\MarkDown\拉勾笔记\zookeeper-应用场景-统一命名服务)]
1.5.2 统一配置管理
分布式环境下,配置文件做同步是必经之路
1000台服务器,如果配置文件作出修改,那一台一台的修改,运维人员肯定会疯,如何做到修改一处就快速同步到每台服务器上
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mgczs2UX-1631713315132)(E:\MarkDown\拉勾笔记\zookeeper-应用场景-统一配置管理)]
将配置管理交给Zookeeper
1、将配置信息写入到Zookeeper的某个节点上
2、每个客户端都监听这个节点
3、一旦节点中的数据文件被修改,Zookeeper这个话匣子就会通知每台客户端服务器
1.5.3 服务器节点动态上下线
客户端能实时获取服务器上下线的变化
在美团APP上实时可以看到商家是否正在营业或打样
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oTvGgnXI-1631713315134)(E:\MarkDown\拉勾笔记\zookeeper-应用场景-服务器节点动态上下线)]
1.5.4 软负载均衡
Zookeeper会记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户请求(雨露均沾)
都是自己的孩子,得一碗水端平
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h73CZFJZ-1631713315136)(E:\MarkDown\拉勾笔记\zookeeper-应用场景-软负载均衡)]
1.6 下载地址
镜像库地址:http://archive.apache.org/dist/zookeeper/
apache-zookeeper-3.6.0.tar.gz需要安装maven,然后再运行mvn clean install 和mvn javadoc:aggregate,前一个命令会下载安装好多jar包,不知道要花多长时间
apache-zookeeper-3.6.0-bin.tar.gz已经自带所需要的各种jar包
2. Zookeeper本地模式安装
2.1 本地模式安装
2.1.1 安装前准备
1.安装jdk
2.拷贝apache-zookeeper-3.6.0-bin.tar.gz到opt目录
3.解压安装包
[root@localhost opt]# tar -zxvf apache-zookeeper-3.6.0-bin.tar.gz
4.重命名
[root@localhost opt]# mv apache-zookeeper-3.6.0-bin zookeeper
2.1.2 配置修改
1.在/opt/zookeeper/这个目录上创建zkData和zkLog目录
[root@localhost zookeeper]# mkdir zkData
[root@localhost zookeeper]# mkdir zkLog
2.进入/opt/zookeeper/conf这个路径,复制一份 zoo_sample.cfg 文件并命名为 zoo.cfg
[root@localhost conf]# cp zoo_sample.cfg zoo.cfg
3.编辑zoo.cfg文件,修改dataDir路径:
dataDir=/opt/zookeeper/zkData
dataLogDir=/opt/zookeeper/zkLog
2.1.3 操作Zookeeper
1.启动Zookeeper
[root@localhost bin]# ./zkServer.sh start
2.查看进程是否启动
[root@localhost bin]# jps
QuorumPeerMain:是zookeeper集群的启动入口类,是用来加载配置启动QuorumPeer线程的
3.查看状态:
[root@localhost bin]# ./zkServer.sh status
4.启动客户端
[root@localhost bin]# ./zkCli.sh
5.退出客户端
[zk: localhost:2181(CONNECTED) 0] quit
2.2 配置参数解读
Zookeeper中的配置文件zoo.cfg中参数含义解读如下:
tickTime =2000:通信心跳数,Zookeeper服务器与客户端心跳时间,单位毫秒
Zookeeper使用的基本时间,服务器之间或客户端与服务器之间维持心跳的时间间隔,也就是每个tickTime时间就会发送一个心跳,时间单位为毫秒。
initLimit =10:LF初始通信时限
集群中的Follower跟随者服务器与Leader领导者服务器之间,启动时能容忍的最多心跳数
10*2000(10个心跳时间)如果领导和跟随者未发出心跳通信,就视为失效的连接,领导和跟随者彻底断开
syncLimit =5:LF同步通信时限
集群启动后,Leader与Follower之间的最大响应时间单位,假如响应超过syncLimit * tickTime >10秒,Leader就认为Follwer已经死掉,会将Follwer从服务器列表中删除
dataDir:数据文件目录+数据持久化路径
主要用于保存Zookeeper中的数据。
dataLogDir:日志文件目录
clientPort =2181:客户端连接端口
监听客户端连接的端口。
3. Zookeeper内部原理
3.1 选举机制(面试重点)
半数机制:集群中半数以上机器存活,集群可用。所以Zookeeper适合安装奇数台服务器
虽然在配置文件中并没有指定Master和Slave。但是,Zookeeper工作时,是有一个节点为Leader,其他则为Follower,Leader是通过内部的选举机制临时产生的
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5weNCL6g-1631713315138)(E:\MarkDown\拉勾笔记\zookeeper-内部原理- 选举机制(面试重点))]
- Server1先投票,投给自己,自己为1票,没有超过半数,根本无法成为leader,顺水推舟将票数投给了id比自己大的Server2
- Server2也把自己的票数投给了自己,再加上Server1给的票数,总票数为2票,没有超过半数,也无法成为leader,也学习Server1,顺水推舟,将自己所有的票数给了id比自己大的Server3
- Server3得到了Server1和Server2的两票,再加上自己投给自己的一票。3票超过半数,顺利成为 leader
- Server4和Server5都投给自己,但是无法改变Server3的票数,只好听天由命,承认Server3是 leader
3.2 节点类型
持久型(persistent):
**持久化目录节点(persistent)**客户端与zookeeper断开连接后,该节点依旧存在
**持久化顺序编号目录节点(persistent_sequential)**客户端与zookeeper断开连接后,该节点依旧存在,创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护,例如:Znode001,Znode002…
短暂型(ephemeral):
**临时目录节点(ephemeral)**客户端和服务器端断开连接后,创建的节点自动删除
**临时顺序编号目录节点(ephemeral_sequential)**客户端与zookeeper断开连接后,该节点被删除,创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护,例如:Znode001,Znode002…
注意:序号是相当于i++,和数据库中的自增长类似
3.3 监听器原理(面试重点)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nS4awrWs-1631713315140)(E:\MarkDown\拉勾笔记\zookeeper-内部原理- 监听器原理(面试重点))]
-
在main方法中创建Zookeeper客户端的同时就会创建两个线程,一个负责网络连接通信,一个负 责监听
-
监听事件就会通过网络通信发送给zookeeper
-
zookeeper获得注册的监听事件后,立刻将监听事件添加到监听列表里
-
zookeeper监听到 数据变化 或 路径变化,就会将这个消息发送给监听线程
常见的监听
1.监听节点数据的变化:get path [watch]
2.监听子节点增减的变化:ls path [watch]
-
监听线程就会在内部调用process方法(需要我们实现process方法内容)
3.4 写数据流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a3pqFQrV-1631713315142)(E:\MarkDown\拉勾笔记\zookeeper-内部原理-写数据流程)]
- Client 想向 ZooKeeper 的 Server1 上写数据,必须的先发送一个写的请求
- 如果Server1不是Leader,那么Server1 会把接收到的请求进一步转发给Leader。
- 这个Leader 会将写请求广播给各个Server,各个Server写成功后就会通知Leader。
- 当Leader收到半数以上的 Server 数据写成功了,那么就说明数据写成功了。
- 随后,Leader会告诉Server1数据写成功了。
- Server1会反馈通知 Client 数据写成功了,整个流程结束
4. Zookeeper实战(开发重点)
集群思路:先搞定一台服务器,再克隆出两台,形成集群!
4.1 分布式安装部署
4.1.1 安装zookeeper
请参考本文 2.1
4.1.2 配置服务器编号
在/opt/zookeeper/zkData创建myid文件
[root@localhost zkData]# vim myid
在文件中添加与server对应的编号:1
其余两台服务器分别对应2和3
4.1.3 配置zoo.cfg文件
打开zoo.cfg文件,增加如下配置
#######################cluster##########################
server.1=192.168.44.129:2888:3888
server.2=192.168.44.130:2888:3888
server.3=192.168.44.131:2888:3888
配置参数解读 server.A=B:C:D
A:一个数字,表示第几号服务器
集群模式下配置的/opt/zookeeper/zkData/myid文件里面的数据就是A的值
B:服务器的ip地址
C:与集群中Leader服务器交换信息的端口
D:选举时专用端口,万一集群中的Leader服务器挂了,需要一个端口来重新进行选举,选出一个新的Leader,而这个端口就是用来执行选举时服务器相互通信的端口。
4.1.4 配置其余两台服务器
- 在虚拟机数据目录下,创建zk02
- 将本台服务器数据目录下的.vmx文件和所有的.vmdk文件分别拷贝zk02下
- 虚拟机->文件->打开 (选择zk02下的.vmx文件)
- 开启此虚拟机,弹出对话框,选择“我已复制该虚拟机”
- 进入系统后,修改linux中的ip,修改/opt/zookeeper/zkData/myid中的数值为2
第三台服务器zk03,重复上面的步骤
4.1.5 集群操作
1.每台服务器的防火墙必须关闭
[root@localhost bin]# systemctl stop firewalld.service
2.启动第1台
[root@localhost bin]# ./zkServer.sh start
3.查看状态
[root@localhost bin]# ./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /opt/zookeeper/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Error contacting service. It is probably not running.
注意:因为没有超过半数以上的服务器,所以集群失败 (防火墙没有关闭也会导致失败)
4.当启动第2台服务器时
查看第1台的状态:Mode: follower
查看第2台的状态:Mode: leader
4.2 客户端命令行操作
启动客户端
[root@localhost bin]# ./zkCli.sh
显示所有操作命令
help
查看当前znode中所包含的内容
ls /
查看当前节点详细数据
zookeeper老版本使用 ls2 / ,现在已经被新命令替代
ls -s /
cZxid:创建节点的事务
每次修改ZooKeeper状态都会收到一个zxid形式的时间戳,也就是ZooKeeper事务ID。
事务ID是ZooKeeper中所有修改总的次序。
每个修改都有唯一的zxid,如果zxid1小于zxid2,那么zxid1在zxid2之前发生
ctime:被创建的毫秒数(从1970年开始)
mZxid:最后更新的事务zxid
mtime:最后修改的毫秒数(从1970年开始)
pZxid:最后更新的子节点zxid
cversion:创建版本号,子节点修改次数
dataVersion:数据变化版本号
aclVersion:权限版本号
ephemeralOwner:如果是临时节点,这个是znode拥有者的session id。如果不是临时节点则是0。
dataLength:数据长度
numChildren:子节点数
分别创建2个普通节点
在根目录下,创建中国和美国两个节点
create /china
create /usa
在根目录下,创建俄罗斯节点,并保存“普京”数据到节点上
create /ru "pujing"
多级创建节点
在日本下,创建东京 “热”
japan必须提前创建好,否则报错 “节点不存在”
create /japan/Tokyo "hot"
获得节点的值
get /japan/Tokyo
创建短暂节点:创建成功之后,quit退出客户端,重新连接,短暂的节点消失
create -e /uk
ls /
quit
ls /
创建带序号的节点
在俄罗斯ru下,创建3个city
create -s /ru/city # 执行三次
ls /ru
[city0000000000, city0000000001, city0000000002]
如果原来没有序号节点,序号从0开始递增。
如果原节点下已有2个节点,则再排序时从2开始,以此类推
修改节点数据值
set /japan/Tokyo "too hot"
监听 节点的值变化 或 子节点变化(路径变化)
1.在server3主机上注册监听/usa节点的数据变化
addWatch /usa
2.在Server1主机上修改/usa的数据
set /usa "telangpu"
3.Server3会立刻响应
WatchedEvent state:SyncConnected type:NodeDataChanged path:/usa
4.如果在Server1的/usa下面创建子节点NewYork
create /usa/NewYork
5.Server3会立刻响应
WatchedEvent state:SyncConnected type:NodeCreatedpath:/usa/NewYork
删除节点
delete /usa/NewYork
递归删除节点 (非空节点,节点下有子节点)
deleteall /ru
不仅删除/ru,而且/ru下的所有子节点也随之删除
4.3 API应用
4.3.1 IDEA环境搭建
1.创建一个Maven工程
2.添加pom文件
<dependencies>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.8.2</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
3.在resources下创建log4j.properties
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/zk.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n
4.3.2 创建ZooKeeper客户端
public class TestZK {
// zookeeper集群的ip和端口
private String connectString = "192.168.44.129:2181,192.168.44.130:2181,192.168.44.131:2181";
/**
* session超时的时间: 时间不宜设置太小。因为zookeeper和加载集群环境会因为性能等原因二延迟略高,
* 如果时间太少,还没有创建好客户端,就开始操作。会报错的。
*/
private int sessionTimeout = 60*1000;
// zookeeper客户端对象
private ZooKeeper zkClient;
@Test
public void init() throws Exception {
zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
public void process(WatchedEvent watchedEvent) {
System.out.println("得到监听反馈,在进行业务处理的代码!");
}
});
}
}
4.3.3 创建节点
一个ACL对象就是一个Id和permission对
表示哪个/哪些范围的Id(Who)在通过了怎样的鉴权(How)之后,就允许进行那些操作 (What):Who How What;
permission(What)就是一个int表示的位码,每一位代表一个对应操作的允许状态。
类似linux的文件权限,不同的是共有5种操作:CREATE、READ、WRITE、DELETE、 ADMIN(对应更改ACL的权限)
OPEN_ACL_UNSAFE:创建开放节点,允许任意操作 (用的最多,其余的权限用的很少)
READ_ACL_UNSAFE:创建只读节点
CREATOR_ALL_ACL:创建者才有全部权限
@Before
public void init() throws Exception {
zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
public void process(WatchedEvent watchedEvent) {
System.out.println("得到监听反馈,在进行业务处理的代码!");
}
});
}
@Test
public void createNode() throws Exception {
String str = zkClient.create("/lagou","weiwei".getBytes(),ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println("已创建节点:" + str);
}
4.3.4 查询节点的值
// 获取节点上的值
@Test
public void getNodeData() throws Exception {
byte[] bytes = zkClient.getData("/lagou", false, new Stat());
String str = new String(bytes);
System.out.println("/lagou节点的数据:" + str);
}
4.3.5 修改节点的值
// 修改节点上的数据
@Test
public void updateData() throws Exception{
Stat stat = zkClient.setData("/lagou", "weiwei02".getBytes(), 0);
System.out.println(stat);
}
4.3.6 删除节点
// 删除节点
@Test
public void deleteData() throws Exception{
zkClient.delete("/lagou",1);
System.out.println("删除成功!");
}
4.3.7 获取子节点
// 获取子节点
@Test
public void getChildren() throws Exception{
List<String> list = zkClient.getChildren("/china", false);
for (String child : list){
System.out.println(child);
}
}
4.3.8 监听子节点的变化
// 监听根节点下面的变化
@Test
public void watchNode() throws Exception{
List<String> list = zkClient.getChildren("/", true);
for (String s : list){
System.out.println(s);
}
// 让线程无限的等待下去
System.in.read();
}
程序在运行的过程中,我们在linux下创建一个节点
IDEA的控制台就会做出响应:NodeChildrenChanged–/
4.3.9 判断Znode是否存在
// 判断子节点是否存在
@Test
public void exists() throws Exception{
Stat stat = zkClient.exists("/laogu", false);
if (stat == null){
System.out.println("不存在");
}else {
System.out.println("存在");
}
}
4.4 案例-模拟美团商家上下线
4.4.1 需求
模拟美团服务平台,商家营业通知,商家打烊通知
提前在根节点下,创建好 /meituan 节点
4.4.2 商家服务类
/**
* @auther wei
* @date 2021/9/10 16:10
* @description: 商家服务类
*/
public class ShopServer {
// zookeeper集群的ip和端口
private String connectString = "192.168.44.129:2181,192.168.44.130:2181,192.168.44.131:2181";
private int sessionTimeout = 60*1000;
// zookeeper客户端对象
private ZooKeeper zkClient;
// 创建客户端,连接zookeeper
public void connect() throws Exception{
zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
}
});
}
// 注册到zookeeper
public void register(String shopName) throws Exception{
// 一定要创建EPHEMERAL_SEQUENTIAL 临时有序的节点(营业)
// 以来可以自动编号,二来断开时,节点自动删除(打烊)
String s = zkClient.create("/meituan/shop", shopName.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("【"+shopName+"】开始营业了!" + s);
}
// 做买卖
private void business(String shopName) throws Exception {
System.out.println("【"+shopName+"】正在火爆营业中!");
System.in.read();
}
public static void main(String[] args) throws Exception {
// 1.我要开一个饭店
ShopServer shop = new ShopServer();
// 2.连接zookeeper集群(和美团取得联系)
shop.connect();
// 3.将服务节点注册到zookeeper(入住美团)
shop.register(args[0]);
// 4.业务逻辑处理(做生意)
shop.business(args[0]);
}
}
4.4.3 客户类
/**
* @auther wei
* @date 2021/9/10 16:10
* @description: 商家服务类
*/
public class ShopServer {
// zookeeper集群的ip和端口
private String connectString = "192.168.44.129:2181,192.168.44.130:2181,192.168.44.131:2181";
private int sessionTimeout = 60*1000;
// zookeeper客户端对象
private ZooKeeper zkClient;
// 创建客户端,连接zookeeper
public void connect() throws Exception{
zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
// 再次获取商家列表
try {
getShopList();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
// 注册到zookeeper
public void register(String shopName) throws Exception{
// 一定要创建EPHEMERAL_SEQUENTIAL 临时有序的节点(营业)
// 以来可以自动编号,二来断开时,节点自动删除(打烊)
String s = zkClient.create("/meituan/shop", shopName.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("【"+shopName+"】开始营业了!" + s);
}
// 做买卖
private void business(String shopName) throws Exception {
System.out.println("【"+shopName+"】正在火爆营业中!");
System.in.read();
}
public static void main(String[] args) throws Exception {
// 1.我要开一个饭店
ShopServer shop = new ShopServer();
// 2.连接zookeeper集群(和美团取得联系)
shop.connect();
// 3.将服务节点注册到zookeeper(入住美团)
shop.register(args[0]);
// 4.业务逻辑处理(做生意)
shop.business(args[0]);
}
}
1.运行客户类,就会得到商家列表
2.首先在linux中添加一个商家,然后观察客户端的控制台输出(商家列表会立刻更新出最新商 家),多添加几个,也会实时输出商家列表
create /meituan/KFC "KFC"
create /meituan/BKC "BurgerKing"
create /meituan/baozi "baozi"
3.在linux中删除商家,在客户端的控制台也会实时看到商家移除后的最新商家列表
delete /meituan/baozi
4… 运行商家服务类(以main方法带参数的形式运行)
4.5 案例-分布式锁-商品秒杀
锁:我们在多线程中接触过,作用就是让当前的资源不会被其他线程访问!
我的日记本,不可以被别人看到。所以要锁在保险柜中
当我打开锁,将日记本拿走了,别人才能使用这个保险柜
在zookeeper中使用传统的锁引发的 “羊群效应” :1000个人创建节点,只有一个人能成功,999 人需要等待!
羊群是一种很散乱的组织,平时在一起也是盲目地左冲右撞,但一旦有一只头羊动起来,其他的羊也会不假思索
地一哄而上,全然不顾旁边可能有的狼和不远处更好的草。羊群效应就是比喻人都有 一种从众心理,从众心理很
容易导致盲从,而盲从往往会陷入骗局或遭到失败。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ty7Lfdef-1631713315144)(E:\MarkDown\拉勾笔记\分布式锁1)]
避免“羊群效应”,zookeeper采用分布式锁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KNJzRiAJ-1631713315145)(E:\MarkDown\拉勾笔记\分布式锁2)]
-
所有请求进来,在/lock下创建 临时顺序节点 ,放心,zookeeper会帮你编号排序
-
判断自己是不是/lock下最小的节点
- 是,获得锁(创建节点)
- 否,对前面小我一级的节点进行监听
-
获得锁请求,处理完业务逻辑,释放锁(删除节点),后一个节点得到通知(比你年轻的死了,你成为最嫩的了)
-
重复步骤2
实现步骤
1. 初始化数据库
创建数据库zkproduct,使用默认的字符集utf8
-- 商品表
create table product(
id int primary key auto_increment, -- 商品编号
product_name varchar(20) not null, -- 商品名称
stock int not null, -- 库存
version int not null -- 版本
)
insert into product (product_name,stock,version) values('锦鲤-清空购物车-大奖',5,0)
-- 订单表
create table `order`(
id varchar(100) primary key, -- 订单编号
pid int not null, -- 商品编号
userid int not null -- 用户编号
)
2. 搭建工程
搭建ssm框架,对库存表-1,对订单表+1
pom.xml
<packaging>war</packaging>
<properties>
<spring.version>5.2.7.RELEASE</spring.version>
</properties>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.5</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.20</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- maven内嵌的tomcat插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<!-- 目前apache只提供了tomcat6和tomcat7两个插件 -->
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>8001</port>
<path>/</path>
</configuration>
<executions>
<execution>
<!-- 打包完成后,运行服务 -->
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 后台的日志输出:针对开发者-->
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
</configuration>
spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--1.扫描包注解-->
<context:component-scan base-package="mapper,service,controller"/>
<!--2.配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="jdbc:mysql://192.168.44.128:3306/zkproduct?serverTimeZone=GMT"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="username" value="root"/>
<property name="password" value="Weiwei@666"/>
<property name="maxActive" value="10"/>
<property name="minIdle" value="5"/>
</bean>
<!--3.创建sqlSessionFactory-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
</bean>
<!--4.事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--5.开启事务-->
<tx:annotation-driven/>
</beans>
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
dao层
/**
* @auther wei
* @date 2021/9/10 19:34
* @description 订单操作类
*/
@Mapper
public interface OrderMapper {
// 生成订单
@Insert("insert into `order` (id,pid,userid) values (#{id},#{pid},#{userid})")
int insert(Order order);
}
/**
* @auther wei
* @date 2021/9/10 19:34
* @description 商品操作类
*/
@Mapper
public interface ProductMapper {
// 查询商品(目的查询库存)
@Select("select * from product where id = #{id}")
Product getProduct(@Param("id") int id);
// 减库存
@Update("update product set stock = stock -1 where id = #{id}")
int reduceStock(@Param("id") int id);
}
实体类
/**
* @auther wei
* @date 2021/9/10 19:35
* @description 订单表
*/
public class Order implements Serializable {
private String id;
private int pid;
private int userid;
public Order() {
}
public Order(String id, int pid, int userid) {
this.id = id;
this.pid = pid;
this.userid = userid;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public int getPid() {
return pid;
}
public void setPid(int pid) {
this.pid = pid;
}
public int getUserid() {
return userid;
}
public void setUserid(int userid) {
this.userid = userid;
}
}
/**
* @auther wei
* @date 2021/9/10 19:35
* @description 商品表
*/
public class Product implements Serializable {
private int id;
private String product_name;
private int stock;
private int version;
public Product() {
}
public Product(int id, String product_name, int stock, int version) {
this.id = id;
this.product_name = product_name;
this.stock = stock;
this.version = version;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getProduct_name() {
return product_name;
}
public void setProduct_name(String product_name) {
this.product_name = product_name;
}
public int getStock() {
return stock;
}
public void setStock(int stock) {
this.stock = stock;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
}
service层
/**
* @auther wei
* @date 2021/9/10 19:35
* @description 商品服务
*/
public interface ProductService {
// 减库存
void reduceStock(int id) throws Exception;
}
/**
* @auther wei
* @date 2021/9/10 19:37
* @description 商品服务实现类
*/
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductMapper productMapper;
@Autowired
private OrderMapper orderMapper;
public void reduceStock(int id) throws Exception {
// 1.获取库存(根据商品id查询商品)
Product product = productMapper.getProduct(id);
if (product.getStock() <= 0){
throw new RuntimeException("已抢光!");
}
// 2.减库存
int i = productMapper.reduceStock(id);
if (i == 1){
//生成订单
Order order = new Order();
order.setId(UUID.randomUUID().toString()); // 使用UUID工具帮我们生成一个订单号
order.setPid(id);
order.setUserid(101);
orderMapper.insert(order);
}else {
throw new RuntimeException("减库存失败!");
}
}
}
controller层
/**
* @auther: wei
* @date 2021/9/10 19:33
* @description: 控制层
*/
@Controller
public class ProductAction {
@Autowired
private ProductService productService;
@GetMapping("/product/reduce")
@ResponseBody
public Object reduce(int id) throws Exception {
productService.reduceStock(id);
return "ok";
}
}
3. 启动测试
1.启动两次工程,端口号分别8001和8002
2.使用nginx做负载均衡
upstream wei{
server localhost:8001;
server localhost:8002;
}
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://wei;
root html;
index index.html index.htm;
}
3.使用 JMeter 模拟1秒内发出10个http请求
下载地址:http://jmeter.apache.org/download_jmeter.cgi
- 查看测试结果,10次请求全部成功
- 查看数据库,stock库存变成 -5 (并发导致的数据结果错误)
4. apahce提供的zookeeper客户端
基于zookeeper原生态的客户端类实现分布式是非常麻烦的,我们使用apahce提供了一个zookeeper客户端来实现
Curator:http://curator.apache.org/
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version> <!-- 网友投票最牛逼版本 -->
</dependency>
recipes是curator族谱大全,里面包含zookeeper和framework
5. 在控制层中加入分布式锁的逻辑代码
/**
* @auther: wei
* @date 2021/9/10 19:33
* @description: 控制层
*/
@Controller
public class ProductAction {
@Autowired
private ProductService productService;
private String connectString = "192.168.44.129:2181,192.168.44.130:2181,192.168.44.131:2181";
@GetMapping("/product/reduce")
@ResponseBody
public Object reduce(int id) throws Exception {
// 重试策略(1000毫秒试一次,最多试3次)
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
// 1.创建curator工具对象
CuratorFramework client = CuratorFrameworkFactory.newClient(connectString, retryPolicy);
client.start();
// 2.根据工具对象创建“内部互斥锁”
InterProcessMutex lock = new InterProcessMutex(client, "/product_"+id);
// 3.加锁
try {
lock.acquire();
productService.reduceStock(id);
} catch (Exception e){
if (e instanceof RuntimeException){
throw e;
}
}finally {
// 4.释放锁
lock.release();
}
return "ok";
}
}
6. 再次测试,并发问题解决!
分布式系统架构解决方案—Dubbo
1. dubbo概述
1.1 什么是分布式系统?
《分布式系统原理与范型》定义:
“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”
分布式系统(distributed system)是建立在网络之上的软件系统。
简单来说:多个(不同职责)人共同来完成一件事!
任何一台服务器都无法满足淘宝的双十一的数据吞吐量,一定是很多台服务器公共来完成的。
歇后语:“三个臭皮匠赛过诸葛亮”,就是分布式系统的真实写照
1.1.1 单一应用架构
当网站流量很小时,只需要一个应用,将所有的功能部署到一起(所有业务都放在一个tomcat 里),从而减少部署节点和成本;
此时,用于简化 增删改查 工作量的数据访问框架 (ORM)是关键;
例如:某个超市的收银系统,某个公司的员工管理系统
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uh5c9dtq-1631713315147)(E:\MarkDown\拉勾笔记\Dubbo-单一应用架构)]
ORM:对象关系映射(Object Relational Mapping)
优点
小项目开发快
成本低
架构简单
易于测试
易于部署
缺点
大项目模块耦合严重不易开发 维护 沟通成本高
新增业务困难
核心业务与边缘业务混合在一块,出现问题互相影响
1.1.2 垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成几个互不相干的几个应用,以提高效率;
大模块按照mvc分层模式,进行拆分成多个互不相关的小模块,并且每个小模块都有独立的服务器
此时,用于加速前端页面开发的web框架(MVC)是关键;因为每个小应用都有独立的页面
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BNauStZt-1631713315148)(E:\MarkDown\拉勾笔记\Dubbo-垂直应用架构)]
MVC:模型视图控制器 (Model View Controller)
缺点:
模块之间不可能完全没有交集,公用模块无法重复利用,开发性的浪费
1.1.3 分布式服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的业务,逐渐形成稳健的服务中心,使前端应用能更快速的响应多变的市场需求;
此时,用户提高业务复用及整合的分布式服务框架(RPC)远程调用是关键;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IVgOE6m8-1631713315150)(E:\MarkDown\拉勾笔记\Dubbo-分布式服务架构)]
RPC:独立的应用服务器之间,要依靠RPC(Romote Procedure Call)才能调用
物流服务不忙,有100台服务器; 商品服务特别忙,也是100台服务器;
如何做到资源优化调配?↓
1.1.4 流动计算架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐呈现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率;
此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MGcrQPYO-1631713315151)(E:\MarkDown\拉勾笔记\Dubbo-流动计算架构)]
SOA:面向服务架构(Service-Oriented Architecture),简单理解就是“服务治理”,例如:公交车站的“调度员“
1.2 Dubbo简介
Dubbo是分布式服务框架,是阿里巴巴的开源项目,现交给apache进行维护
Dubbo致力于提高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案
简单来说,dubbo是个服务框架,如果没有分布式的需求,是不需要用的
1.2.1 RPC
RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式
RPC基本的通信原理
- 在客户端将对象进行序列化
- 底层通信框架使用netty(基于tcp协议的socket),将序列化的对象发给服务提供方
- 服务提供方通过socket得到数据文件之后,进行反序列化,获得要操作的对象
- 对象数据操作完毕,将新的对象序列化,再通过服务提供方的socket返回给客户端
- 客户端获得序列化数据,再反序列化,得到最新的数据对象,至此,完成一次请求
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KMGE9VW8-1631713315153)(E:\MarkDown\拉勾笔记\Dubbo-RPC基本的通信原理)]
RPC两个核心模块:通讯(socket),序列化。
1.2.2 节点角色
节点 | 角色说明 |
---|---|
Provider | 服务的提供方(洗浴中心) |
Consumer | 服务的消费方(客人) |
Registry | 服务注册与发现的注册中心(便民服务中心,所有的饭店娱乐场所都在已在本中心注册) |
Monitor | 监控服务的统计中心(统计服务被调用的次数) |
Container | 服务运行容器(烧烤一条街,洗浴一条街) |
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FRMiQIks-1631713315154)(E:\MarkDown\拉勾笔记\Dubbo- 节点角色)]
1.2.3 调用关系
1.服务容器负责启动,加载,运行服务提供者;
2.服务提供者在启动时,向注册中心注册自己提供的服务;
3.服务消费者在启动时,向注册中心订阅自己所需的服务;
4.在注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者;
5.服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败, 再选另一台调用;
6.服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心;
2. 快速入门
http://dubbo.apache.org/
2.1 注册中心
2.1.1 Zookeeper
官方推荐使用zookeeper注册中心;
注册中心负责服务地址的注册与查找,相当于目录服务;
服务提供者和消费者只在启动时与注册中心交互,注册中不转发请求,压力较小;
Zookeeper是apache hadoop的子项目,是一个树形的目录服务,支持变更推送,适合作为dubbo的服务注册中心,工业强度较高,可用于生产环境;
dubbo即是求职的人,也是招聘单位,而zookeeper就是人才市场/招聘网站
2.1.2 安装
1、安装jdk
2、拷贝apache-zookeeper-3.6.0-bin.tar.gz到opt目录
3、解压安装包
[root@localhost opt]# tar -zxvf apache-zookeeper-3.6.0-bin.tar.gz
4、重命名
[root@localhost opt]# mv apache-zookeeper-3.6.0-bin zookeeper
5、在/opt/zookeeper/这个目录上创建zkData和zkLog目录
[root@localhost zookeeper]# mkdir zkData
[root@localhost zookeeper]# mkdir zkLog
6、进入/opt/zookeeper/conf这个路径,复制一份 zoo_sample.cfg 文件并命名为 zoo.cfg
[root@localhost conf]# cp zoo_sample.cfg zoo.cfg
7、编辑zoo.cfg文件,修改dataDir路径:
dataDir=/opt/zookeeper/zkData
dataLogDir=/opt/zookeeper/zkLog
8、启动Zookeeper
[root@localhost bin]# ./zkServer.sh start
9、查看状态:
[root@localhost bin]# ./zkServer.sh status
2.2 服务提供方
1、一个空的maven项目
2、提供一个服务接口即可
2.2.1 服务方的pom.xml
各种依赖请严格按照下面的版本
<packaging>war</packaging>
<properties>
<spring.version>5.0.6.RELEASE</spring.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${spring.version}</version>
</dependency>
<!--dubbo -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.5.7</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.11.0.GA</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven </groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>8001</port>
<path>/</path>
</configuration>
<executions>
<execution>
<!-- 打包完成后,运行服务 -->
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
2.2.2 服务方接口
/**
* @auther wei
* @date 2021/9/13 10:55
* @description 服务接口
*/
public interface HelloService {
String sayHello(String name);
}
2.2.3 服务方实现
import com.alibaba.dubbo.config.annotation.Service;
import service.HelloService;
/**
* @auther wei
* @date 2021/9/13 10:56
* @description 服务实现类
*/
@Service
public class HelloServiceImpl implements HelloService {
public String sayHello(String name) {
return "Hello,"+name+"!!!";
}
}
2.2.4 服务方的配置文件spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!--1.服务提供方在zookeeper中的"别名"-->
<dubbo:application name="dubbo-server"/>
<!--2.注册中心的地址-->
<dubbo:registry address="zookeeper://192.168.44.129:2181"/>
<!--3.扫描类(将什么包下的类作为服务提供类)-->
<dubbo:annotation package="service.impl"/>
</beans>
2.2.5 服务方的web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="
http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring.xml</param-value>
</context-param>
</web-app>
2.3 服务消费方
2.3.1 消费方的pom.xml
与服务方一致,只需要修改tomcat的端口为8002
2.3.2 消费方的Controller
/**
* @auther wei
* @date 2021/9/13 11:44
* @description 控制层
*/
@Controller
public class HelloAction {
@Reference // 远程去服务方将service的实现类注入进来
private HelloService helloService;
@GetMapping("/hello")
@ResponseBody
public String sayHi(String name){
return helloService.sayHello(name);
}
}
2.3.3 消费方的接口
注意:
controller中要依赖HelloService,所以我们创建一个接口;
这里是消费方,不需要实现,因为实现会让服务方为我们搞定!
/**
* @auther wei
* @date 2021/9/13 11:46
* @description 服务方接口(声明而已,具体实现会远程调用dubbo-server的service实现类)
*/
public interface HelloService {
String sayHello(String name);
}
2.2.4 消费方的spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://code.alibabatech.com/schema/dubbo
http://code.alibabatech.com/schema/dubbo/dubbo.xsd">
<!--1.服务提供方在zookeeper中的"别名"-->
<dubbo:application name="dubbo-consumer"/>
<!--2.注册中心的地址-->
<dubbo:registry address="zookeeper://192.168.44.129:2181"/>
<!--3.扫描类(将什么包下的类作为服务提供类)-->
<dubbo:annotation package="controller"/>
</beans>
2.3.5 消费方的web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="
http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" id="WebApp_ID" version="3.1">
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
2.4 启动服务测试
首先启动服务方,再启动消费方。
访问:http://localhost:8002/hello?name=james
3. 监控中心
我们在开发时,需要知道注册中心都注册了哪些服务,以便我们开发和测试。
图形化显示注册中心的中 服务列表
我们可以通过部署一个web应用版的管理中心来实现。
3.1 服务管理端
3.1.1 安装管理端
1.解压 dubbo-admin-master.zip
2.修改配置文件(服务器端口7001,注册中心zookeeper地址:zookeeper://192.168.44.129:2181)
3.返回到项目根目录,使用maven打包:mvn clean package
4.在dos下运行target目录中的jar文件:java -jar dubbo-admin-0.0.1- SNAPSHOT.jar
5.此时打开浏览器输入:http://localhost:7001/ ;
第一次访问时,需要登录,帐号密码都是root
3.1.2 管理端使用
1.启动服务方,将服务注册到zookeeper
2.启动dubbo-server服务方后,刷新管理端,服务注册成功,只是没有消费者
3.点击服务名,进入服务提供者页面
4.把消费者也运行起来,刷新服务,显示正常
5.查看消费者
3.2 监控统计中心
Monitor:统计中心 ,记录服务被调用多少次等
1.解压dubbo-monitor-simple-2.5.3.zip
2.修改dubbo-monitor-simple-2.5.3\conf\dubbo.properties
3.双击运行dubbo-monitor-simple-2.5.3\bin\start.bat
4.分别修改dubbo-server和dubbo-consumer的spring.xml,加入下面标签
<!-- 让监控 去注册中心 自动找服务 -->
<dubbo:monitor protocol="registry"/>
4. 综合实战
4.1 配置说明
4.1.1 启动时检查
启动时会在注册中心检查依赖的服务是否可用,不可用时会抛出异常
在消费方编写初始化容器的main方法启动(tomcat启动方式,必须访问一次action才能初始化 spring)
public class Test {
public static void main(String[] args) throws IOException {
ClassPathXmlApplicationContext context =
new
ClassPathXmlApplicationContext("classpath:spring/spring.xml");
System.in.read();
}
}
<!--spring.xml设置 默认是true:抛异常;false:不抛异常-->
<dubbo:consumer check="false" />
系统级别日志,需要配合log4j才输出,在resources下添加log4j.properties,内容如下:
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %m%n
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=dubbo.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %l %m%n
log4j.rootLogger=error, stdout,file
4.1.2 超时时间
由于网络或服务端不可靠,会导致调用过程中出现不确定的阻塞状态(超时)
为了避免超时导致客户端资源(线程)挂起耗尽,必须设置超时时间
在服务提供者添加如下配置:
<!--设置超时时间为2秒,默认为1秒-->
<dubbo:provider timeout="2000"/>
可以将服务实现HelloServiceImpl.java中加入模拟的网络延迟进行测试:
@Service
public class HelloServiceImpl implements HelloService {
public String sayHello(String name) {
try {
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
return "Hello,"+name+"!!!!!";
}
}
超时设置2秒,而模拟的网络延迟有3秒,超出时限,报错!
配置原则:
dubbo推荐在Provider上尽量多配置Consumer端属性:
1.作服务的提供者,比服务使用方更清楚服务性能参数,如调用的超时时间,合理的重试次数等
2.在Provider配置后,Consumer不配置则会使用Provider的配置值,即Provider配置可以作消费者的缺省值
4.1.3 重试次数
当出现失败,自动切换并重试其它服务器,dubbo重试的缺省值是2次,我们可以自行设置
在provider提供方配置:
<!-- 消费方连接第1次不算,再来重试3次,总共重试4次 -->
<dubbo:provider timeout="2000" retries="3"/>
@Service
public class HelloServiceImpl implements HelloService {
public String sayHello(String name) {
System.out.println("----被调用1次----");
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
return "Hello,"+name+"!!!";
}
}
并不是所有的方法都适合设置重试次数
幂等方法:适合(当参数一样,无论执行多少次,结果是一样的,例如:查询,修改)
非幂等方法:不适合(当参数一样,执行结果不一样,例如:删除,添加)
单独设置某个方法
- 提供方接口添加sayNo()方法并实现
public interface HelloService {
public String sayHello(String name);
public String sayNo();
}
@Service
public class HelloServiceImpl implements HelloService {
public String sayHello(String name) {
System.out.println("----被调用1次----");
try {
Thread.sleep(3000);
} catch (Exception e) {
e.printStackTrace();
}
return "Hello,"+name+"!!!";
}
public String sayNo() {
System.out.println("no被调用了1次");
return "no";
}
}
- 消费方接口添加sayNo()方法声明
public interface HelloService {
public String sayHello(String name);
public String sayNo();
}
- 消费方controller
@Controller
public class HelloAction {
// @Reference 远程去服务方将service的实现类注入进来,
// 此注解已经在xml文件中被<dubbo:reference>顶替,所以自动注入即可
@Autowired
private HelloService helloService;
@GetMapping("hello")
@ResponseBody
public String sayHi(String name){
return helloService.sayHello(name);
}
@GetMapping("no")
@ResponseBody
public String no(){
return helloService.sayNo();
}
}
- 消费方配置方法重试次数
<dubbo:reference interface="service.HelloService" id="helloService">
<dubbo:method name="sayHello" retries="3"/>
<dubbo:method name="sayNo" retries="0"/> <!--不重试-->
</dubbo:reference>
4.1.4 多版本
一个接口,多个(版本的)实现类,可以使用定义版本的方式引入
为HelloService接口定义两个实现类,提供者修改配置:
<dubbo:service interface="service.HelloService" class="service.impl.HelloServiceImpl01" version="1.0.0"/>
<dubbo:service interface="service.HelloService" class="service.impl.HelloServiceImpl02" version="2.0.0"/>
消费者就可以根据version的版本,选择具体的服务版本
<dubbo:reference interface="service.HelloService" id="helloService" version="2.0.0">
<dubbo:method name="sayHello" retries="3"/>
<dubbo:method name="sayNo" retries="0"/>
</dubbo:reference>
注意:消费者的控制层要改为自动注入,因为@Reference注解和< dubbo:reference>在这里冲突
@Controller
public class HelloAction {
@Autowired
private HelloService helloService;
}
当消费者的版本修改为 version="*",那么就会随机调用服务提供者的版本
-------1.0被调用一次-------
-------2.0被调用一次-------
-------1.0被调用一次-------
-------1.0被调用一次-------
-------1.0被调用一次-------
-------2.0被调用一次-------
4.1.5 本地存根
目前我们的分布式架构搭建起来有一个严重的问题,就是所有的操作全都是消费者发起,由服务提供者执行
消费者动动嘴皮子却什么活都不干,这样会让提供者很累,例如简单的参数验证,消费者完全能够胜任,把合法的参数再发送给提供者执行,效率高了,提供者也没那么累了
例如:去房产局办理房屋过户,请带好自己的证件和资料,如果什么都不带,那么办理过户手续会很麻烦,得先调查你有什么贷款,有没有抵押,不动产证是不是你本人,复印资料等操作。一天肯定办不完。明天还要来。如果你能提前将这些东西准备好,办理过户,1个小时足矣,这就是“房产 中介办事效率高的原因”
话不多说,先在消费者处理一些业务逻辑,再调用提供者的过程,就是“本地存根”
代码实现肯定在消费者,创建一个HelloServiceStub类并且实现HelloService接口
注意:必须使用构造方法的方式注入
/**
* @auther wei
* @date 2021/9/13 17:01
* @description 本地存根
*/
public class HelloServiceStub implements HelloService {
private HelloService helloService; // helloService的代理对象
// 本地存根必须以构造方法的形式注入
public HelloServiceStub(HelloService helloService){
this.helloService = helloService;
}
@Override
public String sayHello(String name) {
if (!StringUtils.isEmpty(name)) {
return helloService.sayHello(name);
}
return "i am sorry!";
}
@Override
public String sayNo() {
return helloService.sayNo();
}
}
修改消费者配置:
<dubbo:reference interface="service.HelloService" id="helloService" version="1.0.0" stub="stub.HelloServiceStub">
<dubbo:method name="sayHello" retries="3"/>
<dubbo:method name="sayNo" retries="0"/>
</dubbo:reference>
4.2 负载均衡策略
负载均衡(Load Balance), 其实就是将请求分摊到多个操作单元上进行执行,从而共同完成工作任务。
简单的说,好多台服务器,不能总是让一台服务器干活,应该“雨露均沾”
dubbo一共提供4种策略,缺省为 random 随机分配调用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hVhwzOrM-1631713315157)(E:\MarkDown\拉勾笔记\Dubbo- 负载均衡)]
修改提供者配置并启动3个提供者,让消费者对其进行访问
tomcat端口8001,8002,8003
provider端口20881,20882,20883
<dubbo:provider timeout="2000" retries="3" port="20881"/>
HelloServiceImpl02类,服务器1,服务器2,服务器3
public String sayHello(String name) {
System.out.println("--服务器3--2.0-被调用1次----");
/*try {
Thread.sleep(3000); // 模拟网络延迟
} catch (Exception e) {
e.printStackTrace();
}*/
return "Hello,"+name+"!!!";
}
启动consumer进行测试
消费方修改权重
<dubbo:reference loadbalance="roundrobin" interface="service.HelloService" id="helloService" version="2.0.0" stub="stub.HelloServiceStub">
<dubbo:method name="sayHello" retries="3"/>
<dubbo:method name="sayNo" retries="0"/>
</dubbo:reference>
最好使用管理端修改权重
4.3 高可用
4.3.1 zookeeper宕机
zookeeper注册中心宕机,还可以消费dubbo暴露的服务
监控中心宕掉不影响使用,只是丢失部分采样数据
数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务
注册中心对等集群,任意一台宕掉后,将自动切换到另一台
注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯
服务提供者无状态,任意一台宕掉后,不影响使用
服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复
测试:
正常发出请求
关闭zookeeper:./zkServer.sh stop
消费者仍然可以正常消费
4.4 服务降级
壁虎遇到危险会自动脱落尾巴,目的是损失不重要的东西,保住重要的
服务降级,就是根据实际的情况和流量,对一些服务有策略的停止或换种简单的方式处理,从而释放服务器的资源来保证核心业务的正常运行
4.4.1 为什么要服务降级
而为什么要使用服务降级,这是防止分布式服务发生雪崩效应
什么是雪崩?就是蝴蝶效应,当一个请求发生超时,一直等待着服务响应,那么在高并发情况下, 很多请求都是因为这样一直等着响应,直到服务资源耗尽产生宕机,而宕机之后会导致分布式其他服务调用该宕机的服务也会出现资源耗尽宕机,这样下去将导致整个分布式服务都瘫痪,这就是雪崩。
4.4.2 服务降级实现方式
在管理控制台配置服务降级:屏蔽和容错
屏蔽:mock=force:return+null 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。
容错:mock=fail:return+null 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。
4.5 整合MyBatis实现用户注册
4.5.1 初始化数据库
CREATE DATABASE smd
USE smd
CREATE TABLE users(
uid INT(11) AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
PASSWORD VARCHAR(50) NOT NULL,
phone VARCHAR(50) NOT NULL,
createtime VARCHAR(50) NOT NULL
)
4.5.2 创建聚合项目-项目模块化
lagou-dubbo(项目目录)
lagou-dubbo-parent(父工程,聚合项目:定义所有模块用的依赖版本)
<modelVersion>4.0.0</modelVersion>
<groupId>com.wei</groupId>
<artifactId>lagou-dubbo-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<spring.version>5.0.6.RELEASE</spring.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!-- JSP相关 -->
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<scope>provided</scope>
<version>2.5</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jsp-api</artifactId>
<scope>provided</scope>
<version>2.0</version>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.2.8</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.2</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.9</version>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.32</version>
</dependency>
<!--dubbo -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.5.7</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.6</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.11.0.GA</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
lagou-dubbo-entity(实体工程,jar项目)
lagou-dubbo-dao(数据访问层工程,jar项目)
lagou-dubbo-interface(服务接口定义工程,jar项目)
lagou-dubbo-service(privoder服务提供者工程,war项目)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>lagou-dubbo-parent</artifactId>
<groupId>com.wei</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>war</packaging>
<artifactId>lagou-dubbo-service</artifactId>
<dependencies>
<dependency>
<groupId>com.wei</groupId>
<artifactId>lagou-dubbo-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.wei</groupId>
<artifactId>lagou-dubbo-dao</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>8001</port>
<path>/</path>
</configuration>
<executions>
<execution>
<!-- 打包完成后,运行服务 -->
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
lagou-dubbo-web(consumer服务消费者工程,war项目)
<!-- 解决post乱码 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
4.5.3 启动测试
- 先选择父项目(聚合工程)进行全员安装成jar
- 启动服务service
- 启动调用者web