Java底層架構之RPC框架Dubbo核心原理之源碼分析

Dubbo是Alibaba開源的分佈式服務框架,它最大的特色是按照分層的方式來架構,使用這種方式可使各個層之間解耦合(或者最大限度地鬆耦合)。從服務模型的角度來看,Dubbo採用的是一種很是簡單的模型,要麼是提供方提供服務,要麼是消費方消費服務,因此基於這一點能夠抽象出服務提供方(Provider)和服務消費方(Consumer)兩個角色。java

Dubbo是什麼程序員

簡單說呢,Dubbo用起來就和EJB、WebService差很少,調用一個遠程的服務(或者JavaBean)的時候在本地有一個接口,就像調用本地的方法同樣去調用,它底層幫你實現好你的方法參數傳輸和遠程服務運行結果傳回以後的返回,就是RPC的一種封裝啦~web

固然,這個只是Dubbo的最基本的功能,它的特色是:算法

1.它主要是使用高效的網絡框架和序列化框架,讓分佈式服務之間調用效率更高。spring

2.採用編程

註冊中心服務器

管理衆多的服務接口地址,當你想調用服務的時候只須要跟註冊中心詢問便可,不用像使用WebService同樣每一個服務都得記錄好接口調用方式。網絡

3.多線程

監控中心架構

:實現對服務方和調用方之間運行狀態的監控,還能控制服務的優先級、權限、權重、上下線等,讓整個龐大的分佈式服務系統的維護和治理比較方便。

4.高可用:有個服務宕機了?註冊中心就會從服務列表去掉該節點。仍是調用到了?客戶端會向註冊中心請求另外一臺可用的服務節點從新調用。註冊中心宕機?註冊中心也能實現高可用(ZooKeeper)。

5.負載均衡:採用軟負載均衡算法實現對多個相同服務的節點的請求負載均衡。

Dubbo註冊中心

上面已經安裝完成了zookeeper的註冊中心了,這個註冊中心主要就是負責dubbo的全部服務地址列表維護,而且能夠經過在ZooKeeper節點中設置相應的值來實現對這個服務的權重、優先級、是否可用、路由、權限等的控制。

你能夠先記住,以後在Dubbo的管理控制檯對服務的一堆治理策略設置和調整,實際上就是修改了註冊中心中的服務對應的配置數據(即修改了zookeeper中服務對應的節點的配置數據)。

以後

Consumer

從註冊中心請求到服務的數據時就能根據這些配置數據進行相應的治理配置參數的代碼執行生效。

Dubbo樣例服務開發

這裏我用maven構建項目,在Spring環境中配置Provider和Consumer。

先說明使用的依賴:

org.springframework

spring-core

4.2.3.RELEASE

org.springframework

spring-beans

4.2.3.RELEASE

org.springframework

spring-context

4.2.3.RELEASE

org.springframework

spring-test

4.2.3.RELEASE

org.springframework

spring-tx

4.2.3.RELEASE

org.springframework

spring-web

4.2.3.RELEASE

org.springframework

spring-webmvc

4.2.3.RELEASE

com.alibaba

dubbo

2.8.4

javassist

javassist

3.12.0.GA

org.jboss.netty

netty

LATEST

com.101tec

zkclient

0.10

org.slf4j

slf4j-log4j12

1.7.12

Provider

聲明服務的接口:

public interface IMyDemo { String sayHello(String name);

}

對接口進行實現(這裏是Provider,須要真的實現,以後在Consumer端調用接口以後實際就是在這裏的實現代碼執行所需邏輯的):

public class MyDemo implements IMyDemo { @Override

public String sayHello(String name) {

String hello = "hello " + name;

System.out.println(hello); return hello;

}

}

本地測試一下這個服務是否可用,這裏還沒用到Dubbo,只是先測試一下Spring容器是否有問題:

@org.junit.Testpublic void testDubbo() throws InterruptedException {

ApplicationContext providerContext = new ClassPathXmlApplicationContext("provider.xml");

IMyDemo demo = providerContext.getBean(IMyDemo.class);

System.out.println(demo.sayHello("world"));

Thread.sleep(60000);

}

RPC之 Dubbo 實現

主要爲三點,動態代理、反射、socket網絡編程

看過不少講dubbo原理的文章,總感受太抽象,偶然間看到一個直播課堂講dubbo原理。結合了一個訂單的例子現場畫筆工具畫圖,直觀不少。截屏記錄下來。(提供技術直播的爲老馬的北京尚學堂,感謝)

客戶端使用動態代理的方式,「僞裝」實現了createOrder方法。

方法相關的數據經過序列化,進入到socket服務器。dubbo的socket實現爲Netty。

服務端從socket服務器取出數據,經過反射的方式找到「真實」的服務實現。

服務端的方法在服務啓動時已注入。

IMG_4519.PNG

服務發現層,可用zookeeper。zookeeper保證了CP(一致性,分區容錯性)。缺點:master節點掛掉時,須要時間從新選擇master,這段時間內註冊中心將不可用。

注意:服務端可消費端註冊成功後,通信只走socket服務器,不會通過註冊中心。

Dubbo

1、dubbo核心技術簡介

遠程服務的調用流程,dubbo本質上就是解決了此問題。

遠程服務器調用流程

1.客戶端發起接口調用

2.服務中間件進行路由選址:找到具體接口實現的服務地址

3.客戶端將請求信息進行編碼(序列化: 方法名,接口名,參數,版本號等)

4.創建與服務端的通信(不是調度中心,而是客戶端與服務端直連)

5.服務端將接收到的信息進行反編碼(反序列化)

6.根據信息找到服務端的接口實現類

7.將執行結果反饋給客戶端

針對上面的調用流程,結合dubbo的服務架構,是否是對dubbo的瞭解又深刻了一些?同窗們有空也能夠根據上述的流程本身實現一遍簡單的遠程調用,下面爲dubbo核心模塊用到的一些技術,能夠提早作一些知識儲備。

核心技術

1.java多線程

2.JVM

3.網絡通信(NIO)

4.動態代理

5.反射

6.序列化

7.路由節點管理(zookeeper)

2、dubbo實際中經常使用配置

常見的一些業務場景和dubbo配置。

分包

建議將服務接口,服務模型,服務異常等均放在API包中,由於服務模型及異常也是API的一部分,同時,這樣作也符合分包原則:重用發佈等價原則(REP),共同重用原則(CRP)

若是須要,也能夠考慮在API包中放置一份spring的引用配置,這樣使用方,只需在Spring加載過程當中引用此配置便可,配置建議放在模塊的包目錄下,以避免衝突,如:com/alibaba/china/xxx/dubbo-reference.xml

粒度

服務接口儘量大粒度,每一個服務方法應表明一個功能,而不是某功能的一個步驟,不然將面臨分佈式事務問題,Dubbo暫未提供分佈式事務支持。

服務接口建議以業務場景爲單位劃分,並對相近業務作抽象,防止接口數量爆炸

不建議使用過於抽象的通用接口,如:Map query(Map),這樣的接口沒有明確語義,會給後期維護帶來不便。

版本

每一個接口都應定義版本號,爲後續不兼容升級提供可能,如:

建議使用兩位版本號,由於第三位版本號一般表示兼容升級,只有不兼容時才須要變動服務版本。

當不兼容時,先升級一半提供者爲新版本,再將消費者所有升爲新版本,而後將剩下的一半提供者升爲新版本。

兼容性

服務接口增長方法,或服務模型增長字段,可向後兼容,刪除方法或刪除字段,將不兼容,枚舉類型新增字段也不兼容,需經過變動版本號升級。

各協議的兼容性不一樣,參見: 服務協議

枚舉值

若是是完備集,能夠用Enum,好比:ENABLE, DISABLE。

若是是業務種類,之後明顯會有類型增長,不建議用Enum,能夠用String代替。

若是是在返回值中用了Enum,並新增了Enum值,建議先升級服務消費方,這樣服務提供方不會返回新值。

若是是在傳入參數中用了Enum,並新增了Enum值,建議先升級服務提供方,這樣服務消費方不會傳入新值。

序列化

服務參數及返回值建議使用POJO對象,即經過set,get方法表示屬性的對象。

服務參數及返回值不建議使用接口,由於數據模型抽象的意義不大,而且序列化須要接口實現類的元信息,並不能起到隱藏實現的意圖。

服務參數及返回值都必需是byValue的,而不能是byRef的,消費方和提供方的參數或返回值引用並非同一個,只是值相同,Dubbo不支持引用遠程對象。

異常

建議使用異常彙報錯誤,而不是返回錯誤碼,異常信息能攜帶更多信息,以及語義更友好,

若是擔憂性能問題,在必要時,能夠經過override掉異常類的fillInStackTrace()方法爲空方法,使其不拷貝棧信息,

查詢方法不建議拋出checked異常,不然調用方在查詢時將過多的try...catch,而且不能進行有效處理,

服務提供方不該將DAO或SQL等異常拋給消費方,應在服務實現中對消費方不關心的異常進行包裝,不然可能出現消費方沒法反序列化相應異常。

調用

不要只是由於是Dubbo調用,而把調用Try-Catch起來。Try-Catch應該加上合適的回滾邊界上。

對於輸入參數的校驗邏輯在Provider端要有。若有性能上的考慮,服務實現者能夠考慮在API包上加上服務Stub類來完成檢驗。

版本控制

在dubbo最佳實踐中有提到,全部接口都應定義版本,在這裏有幾點須要注意下,接口服務若是更新頻繁,而且兼容老版本的,不建議更改版本號,由於dubbo這邊對除 * 之外的版本號,都是採用徹底匹配的方式進行匹配。即服務端的版本號若是從1.0升級爲1.1,而且未保留原有的1.0的服務,那麼客戶端必須同時也將服務版本號升級爲1.1,不然將沒法匹配到遠處服務。

博主在本身項目中的版本使用規則以下,僅供參考:

•版本號採用兩位,x.x 第一位表示須要非兼容升級,第二位表示兼容升級

•bug fix程度的升級不改版本號

•版本升級的時候,保證老版本服務的繼續使用,同時部署新老版本,等客戶端所有升級完成後,再考慮下架老版本服務

•版本能夠細化配置到具體的接口 ,可是咱們建議以通用配置來控制版本號

對服務進行調優

dubbo的服務調用有不少默認配置,這些配置可能會引發服務調用業務上的錯誤,須要特別注意的有如下幾點:

otimeout,調用超時時間,默認爲1000毫秒,即超過1000毫秒沒有返回數據,就會執行重試機制

oretries,失敗重試次數,默認爲2,即失敗(超時)以後的重試次數

oconnections,對每一個提供者的最大連接數,默認爲100,建議根據服務器配置進行調整

oloadbalance,負載均衡策略,默認爲random

oasync, 是否異步執行,默認爲false

odelay, 延遲註冊服務時間,默認爲0,建議不一樣的接口把暴露服務時間錯開,避免dubbo爆端口被佔用錯誤(博主曾深受其害)

以上的幾點,若是服務端與客戶端都同時進行了配置,則客戶端優先級更高。

如下是根據咱們服務器性能與業務需求的部分通用配置.

當某接口執行時間很是長的時候,常見的有三種方式去處理:

•忽略返回值,配置return爲true

•配置爲異步

•配置爲回調的方式

服務端配置:

接口實現:

package com.lijian.dubbo.service.impl;

import java.text.SimpleDateFormat;

import java.util.Date;

import java.util.Map;

import java.util.concurrent.ConcurrentHashMap;

import com.lijian.dubbo.listener.MyListener;

import com.lijian.dubbo.service.CallbackService;

public class CallbackServiceImpl implements CallbackService {

private final Maplisteners = new ConcurrentHashMap();

public CallbackServiceImpl() {

Thread t = new Thread(new Runnable() {

public void run() {

while (true) {

try {

for (Map.Entryentry : listeners

.entrySet()) {

try {

entry.getValue().changed(

getChanged(entry.getKey()));

} catch (Throwable t) {

listeners.remove(entry.getKey());

}

}

Thread.sleep(5000); // 定時觸發變動通知

} catch (Throwable t) { // 防護容錯

t.printStackTrace();

}

}

}

});

t.setDaemon(true);

t.start();

}

public void addListener(String key, MyListener listener) {

listeners.put(key, listener);

listener.changed(getChanged(key)); // 發送變動通知

}

private String getChanged(String key) {

return "Changed: "

+ new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")

.format(new Date());

}

}

對參數進行校驗

使用spring的同窗對@Validated 註解確定不會感到陌生,能夠對請求參數進行格式校驗:

controller代碼:

@RequestMapping(value = {""},method = RequestMethod.POST)

@ResponseBody

public GeneralResult addAdvertising(@Validated @RequestBody AdvertisingForm form){

return GeneralResult.newBuilder().setResult(advertisingService.addAdvertising(form));

}

AdvertisingForm部分代碼:

public class AdvertisingForm {

@NotEmpty(message = "標題不能爲空")

private String title;

@NotEmpty(message = "照片不能爲空")

private String photo;

...

在dubbo中,一樣可使用validate功能進行格式校驗.

須要被校驗的User類:

package com.lijian.dubbo.beans;

import java.io.Serializable;

import javax.validation.constraints.Min;

import org.hibernate.validator.constraints.NotEmpty;

public class User implements Serializable{

private static final long serialVersionUID = 8332069385305414629L;

@NotEmpty(message="姓名不可爲空")

private String name;

@Min(value=18,message="年齡必須大於18歲")

private Integer age;

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Integer getAge() {

return age;

}

public void setAge(Integer age) {

this.age = age;

}

}

dubbo 的validate配置:

接口服務的調用以下:

package com.lijian.dubbo.consumer.main;

import org.springframework.context.support.ClassPathXmlApplicationContext;

import com.lijian.dubbo.beans.User;

import com.lijian.dubbo.consumer.action.UserAction;

public class ValidateMainClass {

@SuppressWarnings("resource")

public static void main(String[] args){

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

context.start();

UserAction userAction = context.getBean(UserAction.class);

User user = new User();

// 若是年齡小於18,會報出 Caused by: javax.validation.ConstraintViolationException: Failed to validate service: com.lijian.dubbo.service.ValidateService, method: insert, cause: [ConstraintViolationImpl{interpolatedMessage='年齡必須大於18歲', propertyPath=age, rootBeanClass=class com.lijian.dubbo.beans.User, messageTemplate='年齡必須大於18歲'}]

// user.setAge(19);

user.setAge(17);

// 若是name爲空,會報出 Caused by: javax.validation.ConstraintViolationException: Failed to validate service: com.lijian.dubbo.service.ValidateService, method: insert, cause: [ConstraintViolationImpl{interpolatedMessage='姓名不可爲空', propertyPath=name, rootBeanClass=class com.lijian.dubbo.beans.User, messageTemplate='姓名不可爲空'}]

user.setName("我是大帥哥");

System.out.println(userAction.addUser(user));

}

}

但願你們能成爲一名更優秀的Java程序員,走向架構師的人生巔峯!

相關文章
相關標籤/搜索