dubbo+rabbitmq+hystrix實現服務的高可用
實際業務中,我們總希望我們的系統能夠儘可能做到高可用,容錯性強,系統內部在服務的呼叫鏈路上,可控制性更好,這樣一旦系統一旦出現問題,容易追蹤問題的來源,尤其是在分散式開發,微服務化越來越流行的今天,如何達到上述的幾項標準,是考量我們的系統的有效試金石,下面,我們使用dubbo模擬分散式系統環境下的呼叫,使用rabbitmq模擬分散式下訊息的確認,以及在分散式環境下,服務呼叫失敗的時候,採用hystrix實現服務的優雅容錯;
環境準備:springboot2.x,windows版rabbitmqs【演示採用本地安裝,實際生產建議在linux下】,dubbo2.6,windos版的dubbo管理控制檯,windows版zookeeper;
1、dubbo在2018年2月出了新版本,相比老版本,效能提升不少,這個大概跟dubbo轉讓給appache有關係吧,dubbo的具體使用不做過多介紹了,大家可以官網查查,很詳細,首先,dubbo的使用,以及監控中心依賴zookeper,所以這裡我們首先啟動zookeeper,
zookeeper的啟動非常簡單,網上下載一個zookeeper,放在本地的某個路徑下,進入到bin目錄下,直接雙擊啟動檔案,就可以啟動了,
雙擊後,如果沒有報錯,可以看到啟動資訊,說明zookeeper啟動成功,
接下來啟動dubbo的管控臺,其實,單獨從使用角度來說,沒有控制檯一樣可以使用dubbo,但是如果不使用dubbo控制檯,很多效果不好直觀的展示,而且管控臺介面上可以做很多的運維操作,比如針對服務的容錯、降級、負載均衡等,細粒度的控制都可以在管控臺上,配合程式一起,可以在使用dubbo的時候更能體會它的妙處;
2.6x之前的管控臺需要在linux上的tomcat中才能跑起來安裝比較麻煩,而且坑不少,我之前搭建的時候就踩了不少坑,新的dubbo管控臺使用很簡單,從dubbo官網上把dubbo的admin包下載下來,本地使用maven命令編譯一下,然後啟動就可以使用了,這是一個springboot的jar包,直接通過命令:java -jar
編譯完成後,就是一個普通的jar包,然後啟動,
然後,瀏覽器輸入:http://localhost:7001/,看到如下介面,標示dubbo控制檯安裝成功,
看到這裡,是不是覺得新版的dubbo控制檯安裝很方便;接下來,我們啟動rabbitmq,這裡說明一下,由於我在模擬的場景中,需要使用訊息確認,也就是服務呼叫方呼叫成功後,傳送訊息給服務提供方,當然,使用http回撥也可以,但個人認為,使用訊息佇列的高擴充套件性更強,
rabbitmq的搭建過程不再贅述了,我這裡安裝是windows版的,安裝完畢rabbitmq,進入到rabbitmq-server目錄下的sbin的資料夾下,直接雙擊啟動檔案,或者cmd黑視窗在這個目錄下,輸入,rabbitmq-service start,就可以啟動rabbitmq的伺服器了,初次在瀏覽器輸入:http://localhost:15672會讓你輸入使用者名稱和密碼,直接輸入guest 和 guest就可以進入到rabbitmq的控制檯了,
rabbitmq的管控臺的使用,同學們可自行百度,資料很多,這裡主要說兩個概念,一個是exchange,另一個是queue,其中在實際業務場景中,exchange也是我們接觸和使用比較多的,程式碼中模擬的也是使用exchange下的topic模式,到這裡,前置的環境準備已經結束,下面上程式碼;
這裡模擬的場景是:有兩個服務,一個是使用者服務,一個是訂單服務,下訂單的時候需要通過dubbo去呼叫使用者服務的介面,獲取使用者資訊,這個是典型的分散式應用環境下的一種介面呼叫形式,整個專案分為3個模組,使用者服務,訂單服務,公共服務,如圖所示:
這裡使用了dubbo官方推薦的一種形式,將需要暴露出去的服務介面通過一個單獨的工程,這個包被其他專案依賴即可,也就是所謂的“分包”原則,
使用者服務,訂單服務的pom依賴檔案相同,直接貼出來,
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>com.atguigu.gmall</groupId>
<artifactId>gmall-interface</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.atguigu.gmall</groupId>
<artifactId>gmall-interface</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- rabbitmq的依賴包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>
spring-cloud-starter-netflix-hystrix
</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
因為是springboot專案,所以使用的依賴包一目瞭然,先看公共服務,因為這個裡面的是需要暴露出去的介面,因此這個工程裡面只有介面,沒有實現,
所有的介面,都放在service包裡面,再看使用者服務,也就是生產者,生產者要將使用者的介面實現的服務暴露出去,
因為要整合dubbo,配置檔案中需要對dubbo的資訊做相應的配置,生產者的配置跟消費端不一樣,大家可以從配置資訊看出來,因為使用到了rabbitmq,這裡連同rabbitmq的配置資訊也一起加上了,具體的每一項的含義,大家可以官網看看,這裡不再解釋了,spring.rabbitmq.virtual-host=/test這個配置需要在rabbitmq的控制檯上提前設定好一個交換機也就是exchange的名字,
server.port=8090
dubbo.application.name=dubbo-provider
dubbo.registry.address=127.0.0.1:2181
dubbo.registry.protocol=zookeeper
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880
#dubbo.scan.base-packages=com.atguigu.gmall
#rabbitmq的相關配置 //作為消費者
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/test
spring.rabbitmq.connection-timeout=2000ms
#設定手動確認訊息
spring.rabbitmq.listener.simple.acknowledge-mode=manual
#設定消費者每次最大最小的併發消費數量
spring.rabbitmq.listener.simple.concurrency=1
spring.rabbitmq.listener.simple.max-concurrency=5
#定義消費者最多同時消費10個訊息
spring.rabbitmq.listener.simple.prefetch=10
下面就來看程式碼的實現,生產者這邊使用者模擬把資料庫的使用者資訊取出來,通過dubbo註冊到dubbo的註冊中心中去,程式碼實現部分如下,
@Service(version="1.0.0")
@Component
public class UserServiceImpl implements UserService {
public List<UserAddress> getUserAddressList(String userId) {
System.out.println("UserServiceImpl.....old...");
UserAddress address1 = new UserAddress(1, "北京市昌平區巨集福科技園綜合樓3樓", "1", "李襄垣", "010-56253825", "Y");
UserAddress address2 = new UserAddress(2, "深圳市寶安區工業大廈B5層", "1", "王小二師", "010-56253825", "N");
/*try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
return Arrays.asList(address1,address2);
}
}
然後,啟動main函式,注意main函式上面的這個@EnableDubbo註解不能少,標識掃描的介面包自動會被dubbo掃描到,然後啟動,
@EnableDubbo
@SpringBootApplication
public class AppProducer {
public static void main(String[] args) {
SpringApplication.run(AppProducer.class, args);
}
}
啟動成功後,我們看dubbo的控制檯,可以看到多了一個服務的提供者,標識我們的服務註冊上去了,
下面來看使用者服務,也就是消費者,pom檔案一致,先看配置檔案,
server.port=8093
dubbo.application.name=service-consumer
dubbo.registry.address=zookeeper://127.0.0.1:2181
#dubbo.scan.base-packages=com.atguigu.gmall
#rabbitmq的相關配置
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
spring.rabbitmq.virtual-host=/test
spring.rabbitmq.connection-timeout=2000ms
#生產者確認訊息
spring.rabbitmq.publisher-confirms=true
#訊息未被消費則原封不動返回,不被處理
spring.rabbitmq.publisher-returns=true
spring.rabbitmq.publisher-mandatory=true
訂單服務模擬web專案,這裡的場景是,訂單服務通過dubbo呼叫使用者服務介面,呼叫成功後,通過rabbitmq將成功呼叫後的訊息回傳給使用者服務,
先看呼叫部分程式碼,
@Component
public class OrderServiceImpl implements OrderService {
@Reference(version = "1.0.0",timeout=3000)
UserService userService;
@Autowired
private RabbitTemplate rabbitTemplate;
//設定消費端的超時服務降級
public List<UserAddress> defaultAddress(String userId) {
return Arrays.asList(new UserAddress(10, "測試地址", "1", "測試", "測試", "Y"));
}
@HystrixCommand(fallbackMethod="defaultAddress")
public List<UserAddress> initOrder(String userId) {
System.out.println("使用者id�?" + userId);
// 1、查詢使用者的收貨地址
List<UserAddress> addressList = userService.getUserAddressList(userId);
for (UserAddress userAddress : addressList) {
System.out.println(userAddress.getUserAddress());
}
if (addressList.size() > 0) {
// 通知使用者端,該訊息已經收到了
sendMessage();
}
return addressList;
}
// 訊息確認機制,如果訊息已經發出,但是rabbitmq並沒有迴應或者是拒絕接收訊息了呢?就可以通過回撥函式的方式將原因打印出來
RabbitTemplate.ConfirmCallback confirmCallback = new RabbitTemplate.ConfirmCallback() {
public void confirm(CorrelationData correlationData, boolean isack, String cause) {
System.out.println("本次訊息的唯一標識是:" + correlationData);
System.out.println("是否是否被成功接收" + isack);
if (isack == false) {
System.out.println("訊息拒絕接收的原因是:" + cause);
} else {
System.out.println("訊息傳送成功");
}
}
};
// 有關訊息被退回來的具體描述訊息
RabbitTemplate.ReturnCallback returnCallback = new ReturnCallback() {
public void returnedMessage(Message message, int replyCode, String desc, String exchangeName, String routeKey) {
System.out.println("err code :" + replyCode);
System.out.println("錯誤訊息的描述 :" + desc);
System.out.println("錯誤的交換機是 :" + exchangeName);
System.out.println("錯誤的路右鍵是 :" + routeKey);
}
};
public void sendMessage() {
CorrelationData orderData = new CorrelationData("已經成功呼叫使用者的資訊" + "-" + new Date().getTime());
rabbitTemplate.setConfirmCallback(confirmCallback);
rabbitTemplate.setReturnCallback(returnCallback);
rabbitTemplate.convertAndSend("springboot-exchange", "order.user", "已經成功呼叫使用者的資訊", orderData);
}
}
注意,在消費方,有一個很重要的註解就是,@Reference(version = “1.0.0”,timeout=3000),即需要在你呼叫的服務介面上面新增這個依賴,後面的timeout是其他的配置,
裡面的關於rabbitmq 的傳送訊息的程式碼比較簡單,只是在呼叫訊息成功後傳送了一個字串的文字訊息到制定的exchange中,使用者服務那邊會用一個監聽的程式,一旦接受到訊息,監聽就會觸發,
監聽部分的程式碼如下,
@Component
public class MessageConsumer {
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value="springboot-queue",durable="true"),
exchange = @Exchange(value="springboot-exchange",durable="true",type="topic"),
key = "#"
)
)
public void handleMessage(@Payload String returnMessage, Channel channel, @Headers Map<String, Object> headers) {
System.out.println("訂單服務響應回來的訊息是 :" + returnMessage);
Long tag = (Long) headers.get(AmqpHeaders.DELIVERY_TAG);
// 消費完畢,手動確認訊息簽收
try {
channel.basicAck(tag, false);
} catch (Exception e) {
e.printStackTrace();
}
}
}
接下來,我們通過一個controller模擬呼叫訂單介面,orderController的程式碼如下,
@Controller
public class OrderController {
@Autowired
OrderService orderService;
@GetMapping(value="/getUserAddress")
@ResponseBody
public List<UserAddress> getUserAddress(String userId){
return orderService.initOrder(userId);
}
}
然後,啟動訂單服務,
看到這裡,說明消費者也已經成功註冊並能夠獲取到註冊中心的服務列表了,
下面在瀏覽器輸入:http://localhost:8093/getUserAddress
說明,我們通過dubbo已經成功呼叫到了使用者的資訊啦;
但是實際業務中,如果存在服務的呼叫超時了,在dubbo中也給瞭解決機制,就是服務超時處理,例如生產者程式碼這樣,
我們人為的模擬生產者需要處理5秒,而在消費方,我們看到設定的呼叫超時時間是3秒,
在這樣的時候,呼叫會失敗,
這樣的情形我們是不能容忍的,但實際中確實可能因為服務呼叫響應超時,為了使體驗更好,我們通常採用優雅的方式,也就是在服務不可用的時候給前臺返回一些託底資料,通過熔斷器的方式返回資料,這裡就使用到了hystrix,通過它,我們不再直接將錯誤的資訊丟擲去而是返回一些託底資料提醒前臺,hystrix的使用也比較簡單,只需要在呼叫的方法上新增一個註解,合格註解指向熔斷的時候的業務方法即可,
新增完這個註解後,我們再次啟動訂單服務,繼續在瀏覽器呼叫可以看到,
這是我們看到,就算呼叫超時,也不會報錯了,而是返回的是我們自定義的資訊,一定程度上起到了熔斷保護的作用,同時,我們的rabbimq也成功接收到了來自訂單服務成功相應的訊息,
到此,我們基本完成了dubbo+rabbitmq+hystrix實現分散式服務呼叫,以及服務的優雅降級和容錯,不足之處,敬請多多諒解!