1. 程式人生 > >dubbo+rabbitmq+hystrix實現服務的高可用

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實現分散式服務呼叫,以及服務的優雅降級和容錯,不足之處,敬請多多諒解!