文章目錄
- 一、Zookeeper
-
- 1. zookeeper概述
- 1.1 工作機制
- 1.2 特點
- 1.3 資料結構
- 1.4 應用場景
-
- 1.4.1 統一命名服務
- 1.4.2 統一配置管理
- 1.4.3 伺服器節點動态上下線
- 1.4.4 軟負載均衡
- 2. Zookeeper本地模式安裝
- 2.1 本地模式安裝
-
- 2.1.1 安裝前準備
- 2.1.2 配置修改
- 2.1.3 操作Zookeeper
- 2.2 配置參數解讀
- 3. Zookeeper内部原理
- 3.1 選舉機制(面試重點)
- 3.2 節點類型
- 3.3 監聽器原理(面試重點)
- 3.4 寫資料流程
- 4. Zookeeper實戰(開發重點)
- 4.1 分布式安裝部署
-
- 4.1.1 安裝zookeeper
-
- 4.1.2 配置伺服器編号
- 4.1.3 配置zoo.cfg檔案
- 4.1.4 配置其餘兩台伺服器
- 4.1.5 叢集操作
- 4.2 用戶端指令行操作
- 4.3 API應用
-
- 4.3.1 IDEA環境搭建
- 4.3.2 建立ZooKeeper用戶端
- 4.3.3 建立節點
- 4.3.4 查詢節點的值
- 4.3.5 修改節點的值
- 4.3.6 删除節點
- 4.3.7 擷取子節點
- 4.3.8 監聽子節點的變化
- 4.3.9 判斷Znode是否存在
- 4.4 案例-模拟美團商家上下線
-
- 4.4.1 需求
- 4.4.2 商家服務類
- 4.4.3 客戶類
- 4.5 案例-分布式鎖-商品秒殺
- 二、Dubbo
-
- 1. dubbo概述
- 1.1 什麼是分布式系統?
-
- 1.1.1 單一應用架構
- 1.1.2 垂直應用架構
- 1.1.3 分布式服務架構
- 1.1.4 流動計算架構
- 1.2 Dubbo簡介
-
- 1.2.1 RPC
- 1.2.2 節點角色
- 1.2.3 調用關系
- 2.1 注冊中心
- 2.1.1 Zookeeper
-
- 2.1.2 安裝
- 2.2 服務提供方
-
- 2.2.1 服務方的pom.xml
- 2.2.2 服務方接口
- 2.2.3 服務方實作
- 2.2.4 服務方的配置檔案spring.xml
- 2.2.5 服務方的web.xml
- 2.3 服務消費方
-
- 2.3.1 消費方的pom.xml
- 2.3.2 消費方的Controller
- 2.3.3 消費方的接口
- 2.3.4 消費方的web.xml
- 2.3.5 消費方的springmvc.xml
- 2.4 啟動服務測試
- 3. 監控中心
- 3.1 服務管理端
-
- 3.1.1 安裝管理端
- 3.2.1 管理端使用
- 3.2 監控統計中心
- 4. 綜合實戰
- 4.1 配置說明
-
- 4.1.1 啟動時檢查
- 4.1.2 逾時時間
- 4.1.3 重試次數
- 4.1.4 多版本
- 4.1.5 本地存根
- 4.2 負載均衡政策
- 4.3 高可用
-
- 4.3.1 zookeeper當機
- 4.4 服務降級
-
- 4.4.1 為什麼要服務降級
- 4.4.2 服務降級實作方式
- 4.5 整合MyBatis實作使用者注冊
-
- 4.5.1 初始化資料庫
- 4.5.2 建立聚合項目-項目子產品化
- 4.5.3 啟動測試
一、Zookeeper
1. zookeeper概述
美團,餓了麼,淘寶,58同城等等應用都是zookeeper的現實生活版
Zookeeper是一個開源的分布式(多台伺服器幹一件事)的,為分布式應用提供協調服務的Apache項目。
在大資料技術生态圈中,zookeeper(動物管理者),Hadoop(大象),Hive(蜜蜂),Pig(豬)等技術
1.1 工作機制
Zookeeper從設計模式角度來了解:是一個基于觀察者模式(一個人幹活,有人盯着他)設計的分布式服務管理架構
它負責 存儲 和 管理 大家都關心的資料
然後接受觀察者的注冊,一旦這些資料的發生變化
Zookeeper就将負責通知已經注冊的那些觀察者做出相應的反應
進而實作叢集中類似Master/Slave管理模式
Zookeeper = 檔案系統 + 通知機制
\1. 商家營業并入駐
\2. 擷取到目前營業的飯店清單
\3. 伺服器節點下線
\4. 伺服器節點上下線事件通知
\5. 重新再去擷取伺服器清單,并注冊監聽
1.2 特點
分布式和叢集的差別?
無論分布式和叢集,都是很多人在做事情。具體差別如下:
例如:我有一個飯店,越來越火爆,我得多招聘一些從業人員
分布式:招聘1個廚師,1個服務員,1個前台,三個人負責的工作不一樣,但是最終目的都是為飯店工作
叢集:招聘3個服務員,3個人的工作一樣
\1. 是一個leader和多個follower來組成的叢集
\2. 叢集中隻要有半數以上的節點存活,Zookeeper就能正常工作(5台伺服器挂2台,沒問題;4台服
務器挂2台,就停止)
\3. 全局資料一緻性,每台伺服器都儲存一份相同的資料副本,無論client連接配接哪台server,資料都是
一緻的
\4. 資料更新原子性,一次資料要麼成功,要麼失敗
\5. 實時性,在一定時間範圍内,client能讀取到最新資料
\6. 更新的請求按照順序執行,會按照發送過來的順序,逐一執行(發來123,執行123,而不是321
或者别的)
1.3 資料結構
ZooKeeper資料模型的結構與linux檔案系統很類似,整體上可以看作是一棵樹,每個節點稱做一個ZNode(ZookeeperNode)。
每一個ZNode預設能夠存儲1MB的資料(中繼資料),每個ZNode的路徑都是唯一的中繼資料(Metadata),又稱中介資料、中繼資料,為描述資料的資料(data aboutdata),主要是描述資料屬性(property)的資訊,用來支援如訓示存儲位置、曆史資料、資源查找、檔案記錄等功能
1.4 應用場景
提供的服務包括:統一命名服務、統一配置管理、統一叢集管理、伺服器節點動态上下線、軟負載均衡等
1.4.1 統一命名服務
在分布式環境下,通常需要對應用或服務進行統一的命名,便于識别
例如:伺服器的IP位址不容易記,但域名相比之下卻是很容易記住
1.4.2 統一配置管理
分布式環境下,配置檔案做同步是必經之路
1000台伺服器,如果配置檔案作出修改,那一台一台的修改,運維人員肯定會瘋,如何做到修改一處就快速同步到每台伺服器上
将配置管理交給Zookeeper
1、将配置資訊寫入到Zookeeper的某個節點上
2、每個用戶端都監聽這個節點
3、一旦節點中的資料檔案被修改,Zookeeper這個話匣子就會通知每台用戶端伺服器
1.4.3 伺服器節點動态上下線
用戶端能實時擷取伺服器上下線的變化
1.4.4 軟負載均衡
Zookeeper會記錄每台伺服器的通路數,讓通路數最少的伺服器去處理最新的客戶請求(雨露均沾)
2. Zookeeper本地模式安裝
2.1 本地模式安裝
2.1.1 安裝前準備
\1. 安裝jdk
\2. 拷貝apache-zookeeper-3.6.0-bin.tar.gz到opt目錄
\3. 解壓安裝包
[r[email protected] opt]# tar -zxvf apache-zookeeper-3.6.0-bin.tar.gz
\4. 重命名
[[email protected] opt]# mv apache-zookeeper-3.6.0-bin zookeeper
2.1.2 配置修改
\1. 在/opt/zookeeper/這個目錄上建立zkData和zkLog目錄
[[email protected] zookeeper]# mkdir zkData
[[email protected] zookeeper]# mkdir zkLog
\2. 進入/opt/zookeeper/conf這個路徑,複制一份 zoo_sample.cfg 檔案并命名為 zoo.cfg
[[email protected] 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
[[email protected] bin]# ./zkServer.sh start
\2. 檢視程序是否啟動
[[email protected] bin]# jps
QuorumPeerMain:是zookeeper叢集的啟動入口類,是用來加載配置啟動QuorumPeer線程的
\3. 檢視狀态:
[[email protected] bin]# ./zkServer.sh status
\4. 啟動用戶端
[[email protected] 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是通過内部的選舉機制臨時産生的
\1. Server1先投票,投給自己,自己為1票,沒有超過半數,根本無法成為leader,順水推舟将票數投給了id比自己大的Server2
\2. Server2也把自己的票數投給了自己,再加上Server1給的票數,總票數為2票,沒有超過半數,也無法成為leader,也學習Server1,順水推舟,将自己所有的票數給了id比自己大的Server3
\3. Server3得到了Server1和Server2的兩票,再加上自己投給自己的一票。3票超過半數,順利成為leader
\4. 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-VI8FoFWG-1617709302919)(https://i.loli.net/2021/04/06/9VohIm3XirnqAWs.png)]
\1. 在main方法中建立Zookeeper用戶端的同時就會建立兩個線程,一個負責網絡連接配接通信,一個負責監聽
\2. 監聽事件就會通過網絡通信發送給zookeeper
\3. zookeeper獲得注冊的監聽事件後,立刻将監聽事件添加到監聽清單裡
\4. zookeeper監聽到 資料變化 或 路徑變化,就會将這個消息發送給監聽線程
常見的監聽
\1. 監聽節點資料的變化:get path [watch]
\2. 監聽子節點增減的變化:ls path [watch]
\5. 監聽線程就會在内部調用process方法(需要我們實作process方法内容)
3.4 寫資料流程
\1. Client 想向 ZooKeeper 的 Server1 上寫資料,必須的先發送一個寫的請求
\2. 如果Server1不是Leader,那麼Server1 會把接收到的請求進一步轉發給Leader。
\3. 這個Leader 會将寫請求廣播給各個Server,各個Server寫成功後就會通知Leader。
\4. 當Leader收到半數以上的 Server 資料寫成功了,那麼就說明資料寫成功了。
\5. 随後,Leader會告訴Server1資料寫成功了。
\6. Server1會回報通知 Client 資料寫成功了,整個流程結束
4. Zookeeper實戰(開發重點)
4.1 分布式安裝部署
叢集思路:先搞定一台伺服器,再克隆出兩台,形成叢集!
4.1.1 安裝zookeeper
4.1.2 配置伺服器編号
在/opt/zookeeper/zkData建立myid檔案
[[email protected] zkData]# vim myid
在檔案中添加與server對應的編号:1
其餘兩台伺服器分别對應2和3
4.1.3 配置zoo.cfg檔案
打開zoo.cfg檔案,增加如下配置
#######################cluster##########################
server.1=192.168.204.141:2888:3888
server.2=192.168.204.142:2888:3888
server.3=192.168.204.143:2888:3888
配置參數解讀 server.A=B:C:D
A:一個數字,表示第幾号伺服器
叢集模式下配置的/opt/zookeeper/zkData/myid檔案裡面的資料就是A的值
B:伺服器的ip位址
C:與叢集中Leader伺服器交換資訊的端口
D:選舉時專用端口,萬一叢集中的Leader伺服器挂了,需要一個端口來重新進行選舉,選出一個新的Leader,而這個端口就是用來執行選舉時伺服器互相通信的端口。
4.1.4 配置其餘兩台伺服器
\1. 在虛拟機資料目錄vms下,建立zk02
\2. 将本台伺服器資料目錄下的.vmx檔案和所有的.vmdk檔案分别拷貝zk02下
\3. 虛拟機->檔案->打開 (選擇zk02下的.vmx檔案)
\4. 開啟此虛拟機,彈出對話框,選擇“我已複制該虛拟機”
\5. 進入系統後,修改linux中的ip,修改/opt/zookeeper/zkData/myid中的數值為2
第三台伺服器zk03,重複上面的步驟
4.1.5 叢集操作
\1. 每台伺服器的防火牆必須關閉
[[email protected] bin]# systemctl stop firewalld.service
\2. 啟動第1台
[[email protected] bin]# ./zkServer.sh start
\3. 檢視狀态
[[email protected] bin]# ./zkServer.sh status
注意:因為沒有超過半數以上的伺服器,是以叢集失敗 (防火牆沒有關閉也會導緻失敗)
\4. 當啟動第2台伺服器時
檢視第1台的狀态:Mode: follower
檢視第2台的狀态:Mode: leader
4.2 用戶端指令行操作
啟動用戶端
[[email protected] 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"
多級建立節點
在日本下,建立Tokyo “hot”
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"
- 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 {
// 叢集ip
private String connStr
="192.168.249.81:2181,192.168.249.82:2181,192.168.249.83:2181";
/*
session逾時 60秒:一定不能太少,因為連接配接zookeeper和加載叢集環境會因為性能原因延遲略高
如果時間太少,還沒有建立好用戶端,就開始操作節點,會報錯的
*/
private int sessionTimeout = 60000;
@Test
public void init() throws IOException {
// 建立監聽器
Watcher watcher = new Watcher() {
public void process(WatchedEvent watchedEvent) {
}
};
// 建立zookeeper用戶端
ZooKeeper zk = new ZooKeeper(connStr, sessionTimeout, watcher);
}
}
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 IOException{
// 省略...
}
@Test
public void createNode() throws Exception {
String nodeCreated = zKcli.create("/lagou", "laosun".getBytes(),
Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
// 參數1:要建立的節點的路徑
// 參數2:節點資料
// 參數3:節點權限
// 參數4:節點的類型
System.out.println("nodeCreated = " + nodeCreated);
}
4.3.4 查詢節點的值
@Test
public void find() throws Exception{
byte[] bs = zKcli.getData("/lagou", false, new Stat()); // 路徑不存在時會報錯
String data = new String(bs);
System.out.println("查詢到資料:"+data);
}
4.3.5 修改節點的值
@Test
public void update()throws Exception{
Stat stat = zKcli.setData("/lagou", "laosunA".getBytes(), 0);
//先檢視節點詳情,獲得dataVersion = 0
System.out.println(stat);
}
4.3.6 删除節點
@Test
public void delete() throws Exception {
zKcli.delete("/lagou", 1); // 先檢視節點詳情,獲得dataVersion = 1
System.out.println("删除成功!");
}
4.3.7 擷取子節點
@Test
public void getChildren() throws Exception {
List<String> children = zKcli.getChildren("/",false); // false:不監聽
for (String child : children) {
System.out.println(child);
}
}
4.3.8 監聽子節點的變化
@Test
public void getChildren() throws Exception {
List<String> children = zKcli.getChildren("/", true); // true:注冊監聽
for (String child : children) {
System.out.println(child);
} /
/ 讓線程不停止,等待監聽的響應
System.in.read();
}
程式在運作的過程中,我們在linux下建立一個節點
IDEA的控制台就會做出響應:NodeChildrenChanged–/
4.3.9 判斷Znode是否存在
@Test
public void exist() throws Exception {
Stat stat = zKcli.exists("/lagou", false);
System.out.println(stat == null ? "不存在" : "存在");
}
4.4 案例-模拟美團商家上下線
4.4.1 需求
模拟美團服務平台,商家營業通知,商家打烊通知
提前在根節點下,建立好 /meituan 節點
4.4.2 商家服務類
public class ShopServer {
private static String connectString =
"192.168.204.141:2181,192.168.204.142:2181,192.168.204.143:2181";
private static int sessionTimeout = 60000;
private ZooKeeper zk = null;
// 建立到zk的用戶端連接配接
public void getConnect() throws IOException {
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
public void process(WatchedEvent event) {
}
});
} /
/ 注冊到叢集
public void register(String ShopName) throws Exception {
// 一定是"EPHEMERAL_SEQUENTIAL短暫有序型"的節點,才能給shop編号,shop1,
shop2...”
String create = zk.create("/meituan/Shop", ShopName.getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
System.out.println("【"+ShopName+"】 開始營業! " + create);
} /
/ 業務功能
public void business(String ShopName) throws Exception {
System.out.println("【"+ShopName+"】 正在營業中 ...");
System.in.read();
}
public static void main(String[] args) throws Exception {
ShopServer shop = new ShopServer();
// 1.連接配接zookeeper叢集(和美團取得聯系)
shop.getConnect();
// 2.将伺服器節點注冊(入住美團)
shop.register(args[0]);
// 3.業務邏輯處理(做生意)
shop.business(args[0]);
}
}
4.4.3 客戶類
public class Customers {
private static String connectString =
"192.168.204.141:2181,192.168.204.142:2181,192.168.204.143:2181";
private static int sessionTimeout = 60000;
private ZooKeeper zk = null;
// 建立到zk的用戶端連接配接
public void getConnect() throws IOException {
zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
public void process(WatchedEvent event) {
// 再次擷取所有商家
try {
getShopList();
} catch (Exception e) {
e.printStackTrace();
}
}
});
} /
/ 擷取伺服器清單資訊
public void getShopList() throws Exception {
// 1擷取伺服器子節點資訊,并且對父節點進行監聽
List<String> shops = zk.getChildren("/meituan", true);
// 2存儲伺服器資訊清單
ArrayList<String> shoplist = new ArrayList();
// 3周遊所有節點,擷取節點中的主機名稱資訊
for (String shop : shops) {
byte[] data = zk.getData("/meituan/" + shop, false, new Stat());
shoplist.add(new String(data));
} /
/ 4列印伺服器清單資訊
System.out.println(shoplist);
} /
/ 業務功能
public void business() throws Exception {
System.out.println("客戶正在浏覽商家 ...");
System.in.read();
}
public static void main(String[] args) throws Exception {
// 1.擷取zk連接配接 (客戶打開美團)
Customers client = new Customers();
client.getConnect();
// 2.擷取/meituan的子節點資訊,從中擷取伺服器資訊清單(從美團中擷取商家清單)
client.getShopList();
// 3.業務程序啟動 (對比商家,點餐)
client.business();
}
}
\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-Jk3RlyHb-1617709302922)(https://i.loli.net/2021/04/06/25gsynjNeWCdDcT.png)]
避免“羊群效應”,zookeeper采用分布式鎖
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-66Bc4bNy-1617709302922)(https://i.loli.net/2021/04/06/GRmW642l8vN1BTz.png)]
\1. 所有請求進來,在/lock下建立 臨時順序節點 ,放心,zookeeper會幫你編号排序
\2. 判斷自己是不是/lock下最小的節點
\1. 是,獲得鎖(建立節點)
\2. 否,對前面小我一級的節點進行監聽
\3. 獲得鎖請求,處理完業務邏輯,釋放鎖(删除節點),後一個節點得到通知(比你年輕的死了,你成為最嫩的了)
\4. 重複步驟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
<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>
<?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>
<?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:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsD: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="controller,service,mapper"/>
<!-- 2.建立資料連接配接池對象 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="jdbc:mysql://192.168.204.131:3306/zkproduct?
serverTimezone=GMT" />
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="username" value="root" />
<property name="password" value="123123" />
<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>
<property name="configLocation" value="classpath:mybatis/mybatisconfig.xml"></property>
</bean>
<!-- 4.告訴spring容器,資料庫語句代碼在哪個檔案中-->
<!-- mapper.xDao接口對應resources/mapper/xDao.xml-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="mapper"></property>
</bean>
<!-- 5.将資料源關聯到事務 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 6.開啟事務 -->
<tx:annotation-driven/>
</beans>
<?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"
xsD: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>
<servletclass>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>
@Mapper
@Component
public interface OrderMapper {
// 生成訂單
@Insert("insert into `order` (id,pid,userid) values (#{id},#{pid},#
{userid})")
int insert(Order order);
}
@Mapper
@Component
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);
}
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
ProductMapper productMapper;
@Autowired
OrderMapper orderMapper;
@Override
public void reduceStock(int id) throws Exception {
// 1.擷取庫存
Product product = productMapper.getProduct(id);
// 模拟網絡延遲
Thread.sleep(1000);
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());
order.setPid(id);
order.setUserid(101);
orderMapper.insert(order);
}else
throw new RuntimeException("減庫存失敗,請重試!");
}
}
@Controller
public class ProductAction {
@Autowired
private OrderService orderService;
@GetMapping("/product/reduce")
@ResponseBody
public Object reduceStock(int id) throws Exception{
orderService.reduceStock(id);
return "ok";
}
}
\3. 啟動測試
\1. 啟動兩次工程,端口号分别8001和8002
\2. 使用nginx做負載均衡
upstream sga{
server 192.168.204.1:8001;
server 192.168.204.1:8002;
}
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://sga;
root html;
index index.html index.htm;
}
\3. 使用 JMeter 模拟1秒内發出10個http請求
\1. 檢視測試結果,10次請求全部成功
\2. 檢視資料庫,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>
\5. 在控制層中加入分布式鎖的邏輯代碼
@Controller
public class ProductAction {
@Autowired
private ProductService productService;
private static String connectString =
"192.168.204.141:2181,192.168.204.142:2181,192.168.204.143:2181";
@GetMapping("/product/reduce")
@ResponseBody
public Object reduce( int id) throws Exception {
// 重試政策 (1000毫秒試1次,最多試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);
try {
//3.加鎖
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)是關鍵;
例如:某個超市的收銀系統,某個公司的員工管理系統
ORM:對象關系映射(Object Relational Mapping)
優點
小項目開發快 成本低
架構簡單
易于測試
易于部署
缺點
大項目子產品耦合嚴重 不易開發 維護 溝通成本高
新增業務困難
核心業務與邊緣業務混合在一塊,出現問題互相影響
1.1.2 垂直應用架構
當通路量逐漸增大,單一應用增加機器帶來的加速度越來越小,将應用拆成幾個互不相幹的幾個應用,以提高效率;
大子產品按照mvc分層模式,進行拆分成多個互不相關的小子產品,并且每個小子產品都有獨立的伺服器
此時,用于加速前端頁面開發的web架構(MVC)是關鍵;因為每個小應用都有獨立的頁面
MVC:模型視圖控制器 (Model View Controller)
缺點:
子產品之間不可能完全沒有交集,公用子產品無法重複利用,開發性的浪費
1.1.3 分布式服務架構
當垂直應用越來越多,應用之間互動不可避免,将核心業務抽取出來,作為獨立的業務,逐漸形成
穩健的服務中心,使前端應用能更快速的響應多變的市場需求;
此時,使用者提高業務複用及整合的分布式服務架構(RPC)遠端調用是關鍵;
RPC:獨立的應用伺服器之間,要依靠
物流服務不忙,有100台伺服器;
如何做到資源優化調配?↓
1.1.4 流動計算架構
當服務越來越多,容量的評估,小服務資源的浪費等問題逐漸呈現,此時需增加一個排程中心基于通路壓力實時管理叢集容量,提高叢集使用率;
此時,用于提高機器使用率的資源排程和治理中心(SOA)是關鍵;
SOA:面向服務架構(Service-Oriented Architecture),簡單了解就是“服務治理”,例如:公共汽車站的“排程員”
1.2 Dubbo簡介
Dubbo是分布式服務架構,是阿裡巴巴的開源項目,現交給apache進行維護
Dubbo緻力于提高性能和透明化的RPC遠端服務調用方案,以及SOA服務治理方案
簡單來說,dubbo是個服務架構,如果沒有分布式的需求,是不需要用的
1.2.1 RPC
RPC【Remote Procedure Call】是指遠端過程調用,是一種程序間通信方式
RPC基本的通信原理
\1. 在用戶端将對象進行序列化
\2. 底層通信架構使用netty(基于tcp協定的socket),将序列化的對象發給服務方提供方
\3. 服務提供方通過socket得到資料檔案之後,進行反序列化,獲得要操作的對象
\4. 對象資料操作完畢,将新的對象序列化,再通過服務提供方的socket傳回給用戶端
\5. 用戶端獲得序列化資料,再反序列化,得到最新的資料對象,至此,完成一次請求
RPC兩個核心子產品:通訊(socket),序列化。
1.2.2 節點角色
節點 | 角色說明 |
---|---|
Provider | 服務的提供方(洗浴中心) |
Consumer | 服務的消費方(客人) |
Registry | 服務注冊與發現的注冊中心(便民服務中心,所有的飯店娛樂場所都在已在本中心 注冊) |
Monitor | 監控服務的統計中心(統計服務被調用的次數) |
Container | 服務運作容器(燒烤一條街,洗浴一條街) |
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-UWxCTdrc-1617709302927)(https://i.loli.net/2021/04/06/S2xn8pbroBJARXs.png)]
1.2.3 調用關系
1.服務容器負責啟動,加載,運作服務提供者;
2.服務提供者在啟動時,向注冊中心注冊自己提供的服務;
3.服務消費者在啟動時,向注冊中心訂閱自己所需的服務;
4.在注冊中心傳回服務提供者位址清單給消費者,如果有變更,注冊中心将基于長連接配接推送變更資料給消費者;
5.服務消費者,從提供者位址清單中,基于軟負載均衡算法,選一台提供者進行調用,如果調用失敗,再選另一台調用;
6.服務消費者和提供者,在記憶體中累計調用次數和調用時間,定時每分鐘發送一次統計資料到監控中心;
2.1 注冊中心
2.1.1 Zookeeper
官方推薦使用zookeeper注冊中心;
注冊中心負責服務位址的注冊與查找,相當于目錄服務;
服務提供者和消費者隻在啟動時與注冊中心互動,注冊中不轉發請求,壓力較小;
Zookeeper是apache hadoop的子項目,是一個樹形的目錄服務,支援變更推送,适合作為
dubbo的服務注冊中心,工業強度較高,可用于生産環境;
dubbo即是求職的人,也是招聘機關,而zookeeper就是人才市場/招聘網站;
2.1.2 安裝
安裝zookeeper
2.2 服務提供方
1、一個空的maven項目
2、提供一個服務接口即可
2.2.1 服務方的pom.xml
各種依賴請嚴格按照下面的版本
<packaging>war</packaging>
<properties>
<spring.version>5.0.6.RELEASE</spring.version>
</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 服務方接口
public interface HelloService {
String sayHello(String name);
}
2.2.3 服務方實作
@com.alibaba.dubbo.config.annotation.Service
public class HelloServiceImpl implements HelloService {
@Override
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"
xsD: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.204.141: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"
xsD: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>
<listenerclass>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
@RestController
public class HelloAction {
@com.alibaba.dubbo.config.annotation.Reference
private HelloService hs;
@RequestMapping("hello")
@ResponseBody
public String hello( String name){
return hs.sayHello(name);
}
}
2.3.3 消費方的接口
注意:
controller中要依賴HelloService,是以我們建立一個接口;
這裡是消費方,不需要實作,因為實作會讓服務方為我們搞定!
public interface HelloService {
String sayHello(String name);
}
2.3.4 消費方的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"
xsD: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>
<servletclass>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.3.5 消費方的springmvc.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"
xsD: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">
<!--Dubbo的應用名稱,通常使用項目名 -->
<dubbo:application name="dubbo-consumer" />
<!--配置Dubbo的注冊中心位址 -->
<dubbo:registry address="zookeeper://192.168.204.141:2181" />
<!--配置Dubbo掃描類,将這個類作為服務進行釋出 -->
<dubbo:annotation package="controller" />
</beans>
2.4 啟動服務測試
首先啟動服務方,再啟動消費方。
通路:http://localhost:8002/hello?name=james
3. 監控中心
我們在開發時,需要知道注冊中心都注冊了哪些服務,以便我們開發和測試。
圖形化顯示注冊中心的中 服務清單
我們可以通過部署一個web應用版的管理中心來實作。
3.1 服務管理端
3.1.1 安裝管理端
\1. 解壓 dubbo-admin-master.zip
\2. 修改配置檔案
\3. 傳回到項目根目錄,使用maven打包:mvn clean package
\4. 在dos下運作target目錄中的jar檔案:java -jar dubbo-admin-0.0.1-
SNAPSHOT.jar
\5. 此時打開浏覽器輸入:http://localhost:7001/ ;
第一次通路時,需要登入,帳号密碼都是root
3.2.1 管理端使用
\1. 啟動服務方,将服務注冊到zookeeper
\2. 啟動dubbo-server服務方後,重新整理管理端,服務注冊成功,隻是沒有消費者
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-0kCMCwtW-1617709302930)(https://i.loli.net/2021/04/06/ILFxoGBOkK4lymc.png)]
\3. 點選服務名,進入服務提供者頁面
\4. 把消費者也運作起來,重新整理服務,顯示正常
\5. 檢視消費者
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-jxD5fHiK-1617709302930)(https://i.loli.net/2021/04/06/Zq7XBAswgQ9Vp8N.png)]
3.2 監控統計中心
Monitor:統計中心 ,記錄服務被調用多少次等
\1. 解壓dubbo-monitor-simple-2.5.3.zip
\2. 修改dubbo-monitor-simple-2.5.3\conf\dubbo.properties
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-bRyqkMEt-1617709302931)(https://i.loli.net/2021/04/06/TcYQEDdfzIPUhao.png)]
\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();
}
}
<!--預設是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+"!!!!!";
}
}
并不是所有的方法都适合設定重試次數
幂等方法:适合(當參數一樣,無論執行多少次,結果是一樣的,例如:查詢,修改)
非幂等方法:不适合(當參數一樣,執行結果不一樣,例如:删除,添加)
單獨設定某個方法
\1. 提供方接口添加sayNo()方法并實作
public interface HelloService {
String sayHello( String name );
String sayNo();
}
@Service
public class HelloServiceImpl implements HelloService {
public String sayHello(String name) {
System.out.println("=============hello被調用一次
===============");
try {
Thread.sleep(3000);
}catch (Exception e){
e.printStackTrace();
}
return "Hello,"+name+"!!!!!";
}
public String sayNo() {
System.out.println("-------no被調用一次-------");
return "no!";
}
}
\2. 消費方接口添加sayNo()方法聲明
public interface HelloService {
String sayHello( String name );
String sayNo();
}
\3. 消費方controller
@Controller
public class HelloAction {
//@Reference 此注解已經在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();
}
}
\4. 消費方配置方法重試次數
<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="*",那麼就會随機調用服務提供者的版本
4.1.5 本地存根
目前我們的分布式架構搭建起來有一個嚴重的問題,就是所有的操作全都是 消費者發起,由服務提供者執行
消費者動動嘴皮子卻什麼活都不幹,這樣會讓提供者很累,例如簡單的參數驗證,消費者完全能夠勝任,把合法的參數再發送給提供者執行,效率高了,提供者也沒那麼累了
先在消費者處理一些業務邏輯,再調用提供者的過程,就是“本地存根”
代碼實作肯定在 消費者,建立一個HelloServiceStub類并且實作HelloService接口
注意:必須使用構造方法的方式注入
public class HelloServiceStub implements HelloService {
private HelloService helloService;
// 注入HelloService
public HelloServiceStub(HelloService helloService) {
this.helloService = helloService;
}
public String sayHello(String name) {
System.out.println("本地存根資料驗證。。。");
if(!StringUtils.isEmpty(name)){
return helloService.sayHello(name);
}
return "i am sorry!";
}
public String sayNo() {
return helloService.sayNo();
}
}
修改消費者配置:
<dubbo:reference interface="service.HelloService" id="helloService"
version="1.0.0" stub="service.impl.HelloServiceStub">
<dubbo:method name="sayHello" retries="3"/>
<dubbo:method name="sayNo" retries="0"/>
</dubbo:reference>
4.2 負載均衡政策
負載均衡(Load Balance), 其實就是将請求分攤到多個操作單元上進行執行,進而共同完成工作任務。
簡單的說,好多台伺服器,不能總是讓一台伺服器幹活,應該“雨露均沾”
dubbo一共提供4種政策,預設為 random 随機配置設定調用
修改提供者配置并啟動3個提供者,讓消費者對其進行通路
tomcat端口8001,8002,8003
provider端口20881,20882,20883
HelloServiceImpl01類,伺服器1,伺服器2,伺服器3
public String sayNo() {
System.out.println("----伺服器1---1.0被調用一次-------");
return "no!";
}
啟動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.sunguoan</groupId>
<artifactId>sun-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<spring.version>5.0.6.RELEASE</spring.version>
</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服務提供者工程,jar項目)
<parent>
<groupId>com.sunguoan</groupId>
<artifactId>sun-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sun-service</artifactId>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>com.sunguoan</groupId>
<artifactId>sun-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.sunguoan</groupId>
<artifactId>sun-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>
lagou-dubbo-web(consumer服務消費者工程,war項目)
<!-- 解決post亂碼 -->
<filter>
<filter-name>charset</filter-name>
<filterclass>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>charset</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>springMVC</servlet-name>
<servletclass>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 啟動測試
\1. 先選擇父項目(聚合工程)進行全員安裝成jar
\2. 啟動服務service
\3. 啟動調用者web