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实现分布式服务调用,以及服务的优雅降级和容错,不足之处,敬请多多谅解!

猜你喜欢

转载自blog.csdn.net/zhangcongyi420/article/details/83480176