赞
踩
微服务架构越来越流行,这个没有异议。
2009 年,Netflix 重新定义了它的应用程序员的开发模型,这个算是微服务的首次探索。 20014 年,《Microservices》,这篇文章以一个更加通俗易懂的方式,为大家定义了微服务。
互联网应用产品的两大特点:
1. 需求变化快 2. 用户群体庞大
在这样的情况下,我们需要构建一个能够灵活扩展,同时能够快速应对外部环境变化的一个应用,使用 传统的开发方式,显然无法满足需求。这个时候,微服务就登场了。
简单来说,微服务就是一种将一个单一应用程序拆分为一组小型服务的方法,拆分完成后,每一个服务 都运行在独立的进程中,服务于服务之间采用轻量级的通信机制来进行沟通(Spring Cloud 中采用基于 HTTP 的 RESTful API)。
每一个服务,都是围绕具体的业务进行构建,例如一个电商系统,订单服务、支付服务、物流服务、会 员服务等等,这些拆分后的应用都是独立的应用,都可以独立的部署到生产环境中。就是在采用微服务 之后,我们的项目不再拘泥于一种语言,可以 Java、Go、Python、PHP 等等,混合使用,这在传统的 应用开发中,是无法想象的。而使用了微服务之后,我们可以根据业务上下文来选择合适的语言和构建 工具进行构建。
微服务可以理解为是 SOA 的一个传承,一个本质的区别是微服务是一个真正分布式、去中心化的,微服务的拆分比 SOA 更加彻底。
面向服务的架构(SOA)是一个组件模型,它将应用程序的不同功能单元(称为服务)进行拆分,并通过这些服务之间定义良好的接口和协议联系起来。接口是采用中立的方式进行定义的,它应该独立于实现服务的硬件平台、操作系统和编程语言。这使得构件在各种各样的系统中的服务可以以一种统一和通用的方式进行交互。
Spring Cloud 是一系列框架的集合,Spring Cloud 内部包含了许多框架,这些框架互相协作,共同构建分布式系统。利用这些组件,可以非常方便的构建一个分布式系统。
不同于其他的框架,Spring Cloud 版本名称是通过 A(Angel)、B(Brixton)、C(Camden)、 D(Dalston),E(Edgware)、F(Finchley)这些名字使用了伦敦地铁站的名 字,目前最新版是 H (Hoxton)版。
Spring Cloud 中,除了大的版本之外,还有一些小版本,小版本命名方式如下:
Eureka是Spring Cloud中的注册中心,类似于Dubbo中的Zookeeper.
传统的单体应用:
为了解决服务之间的耦合,注册中心闪亮登场。
Spring Cloud Eureka是Spring Cloud Netfix微服务套件中的一部分,它基于NetlixEureka做了二次封装,主要负责完成微服务架构中的服务治理功能。Spring Cloud通过为Eureka 增加了Spring Boot风格的自动化配置,我们只需通过简单引入依赖和注解配置就能勾建的微服务应用轻松地与Eureka服务治理体系进行整合。Eureka基于REST来实现服务的注册与发现。
Spring Cloud Eureka并且服务端与客户端均采用Java编写,所以Eureka主要适用于通过Java实现的分布式系统,或是与JVM兼容语言构建的系统。但是,由于Eureka服务端的服务治理机制提供了完备的RESTful API,所以它也支持将非Java语言构建的微服务应用纳入Eureka的服务治理体系中来。只是在使用其他语言平台的时候,需要自己来实现Eureka的客户端程序。
Spring Cloud中封装了Eureka,在Eureka的基础上,优化了一些配置,然后提供了可视化的页面,可以方便的查看服务的注册情况以及服务注册中心集群的运行情况。
Eureka由两部分:服务端和客户端,
从图中,我们可以看出, Eureka中,有三个角色:
Eureka本身是使用Java来开发的,
Spring Cloud使用Spring Boot技术对Eureka进行了封装,
所以,在Spring Cloud中使用Eureka非常方便,只需要引入spring-cloud-starter-netlix-eurekaserver这个依赖即可。
然后就像启动一个普通的Spring Boot项目一样启动Eureka即可创建一个普通的Spring Boot项目, 在程序的启动类 EurekaClientApplication 加上注解@EnableEurekaServer 开启@EnableEurekaServer功能
- package com.liruilong.eureka;
-
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
-
- // 开启eureka功能
- @EnableEurekaServer
- @SpringBootApplication
- public class EurekaApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(EurekaApplication.class, args);
- }
-
- }
- #给当前服务起一个名字
- spring.application.name=eureka
- #设置端口号
- server.port=1111
- #默认情况下, Eureka Server也是一个普通的微服务,所以当它还是一个注册中心的时候,
- # 他会有两层身份: 1.注册中心: 2,普通服务,
- # 即当前服务会自己把自己注册到自己上面来#register-with-eureka设置为false,
- # 表示当前项目不要注册到注册中心上
- eureka.client.register-with-eureka=false
- #表示是否从eureka Server 上获取注册信息
- eureka.client.fetch-registry=false
创建时,添加Eureka依赖:
项目启动后标记这是一个Eureka Server:
项目启动之后,浏览器输入http://localhost:1111/就可以查看Eureka后台管理页面
使用了注册中心之后,所有的服务都要通过服务注册中心来进行信息交换。
服务注册中心的稳定性就非常重要了,一旦服务注册中心掉线,会影响到整个系统的稳定性。所以,在实际开发中, Eureka一般都是以集群的形式出现的。
Eureka集群,实际上就是启动多个Eureka实例,多个Eureka实例之间,互相注册,互相同步数据,共同组成一个Eureka集群。
首先我们需要一点准备工作,修改电脑的hosts文件(c:\windows\system32\drivers\etc\hosts ) 127.0.0.1 eurekaA eurekaB
我们在resources目录下,再添加两个配置文件,分别为application-a.properties以及application-b.properties:然后通过命令行的方式配置不同的参数启动项目。--spring.profiles.active=a/b
用一个服务,改配置启动两次,模拟Eureka集群。相互注册
服务打包之后java -jar 服务jar 。
application-a.properties
- #给当前服务起一个名字
- spring.application.name=eureka
- #设置端口号
- server.port=1111
- eureka.instance.hostname=eurekaA
- #默认情况下, Eureka Server也是一个普通的微服务,所以当它还是一个注册中心的时候,
- # 他会有两层身份: 1.注册中心: 2,普通服务,
- # 即当前服务会自己把自己注册到自己上面来#register-with-eureka设置为false,
- # 表示当前项目不要注册到注册中心上
- eureka.client.register-with-eureka=true
- #表示是否从eureka Server 上获取注册信息
- eureka.client.fetch-registry=true
- #A服务要注册到B服务上
- eureka.client.service-url.defaultZone=http://eurekaB:1112/eureka
application-b.properties
- #给当前服务起一个名字
- spring.application.name=eureka
- #设置端口号
- server.port=1112
- eureka.instance.hostname=eurekaB
- #默认情况下, Eureka Server也是一个普通的微服务,所以当它还是一个注册中心的时候,
- # 他会有两层身份: 1.注册中心: 2,普通服务,
- # 即当前服务会自己把自己注册到自己上面来#register-with-eureka设置为false,
- # 表示当前项目不要注册到注册中心上
- eureka.client.register-with-eureka=true
- #表示是否从eureka Server 上获取注册信息
- eureka.client.fetch-registry=true
- eureka.client.service-url.defaultZone=http://eurkaA:1111/eurka
上述配置代码中, defaultZone 为默认的 Zone,来源于 AWS (业务流程管理开发平台是一个易于部署和使用的业务流程管理基础平台软件)的概念。区域(Region)和可 用区(Availability Zone, AZ)是 AWS 的另外两个概念。区域是指服务器所在的区域,比如北 美洲、南美洲、欧洲和亚洲等,每个区域一般由多个可用区组成。 在本案例中 defaultZone 是 指 Eureka Server 的注册地址。
项目中的报错可以不用管;
Eureka本身可以分为两部分,Eureka Server和Eureka Client
Eureka Server 主要对外提供了三个功能:
Eureka Client主要用来简化每一个服务和Eureka Server之间的交互,Eureka Client会自动拉取,更新以及缓存Eureka Serve中的信息,这样,即使Eureka Server所有节点都宕机,Eureka Client依然能够获取到想要调用的服务器的地址(但是地址可能不准确)。
Eureka客户端的配置主要分为以下两个方面。
服务注册(Register):服务提供者将自己注册到服务注册中心(Eureka Serve),需要注意,所谓的服务注册,只是一个业务上的划分,本质上他就是一个Eureka Client,当Eureka Client向Eureka Server注册时,他需要提供自身的一些元数据信息,例如IP地址,端口,名称,运行状态。
服务续约(Renew):Eureka Client 注册到 Eureka Server 上之后, 事情没有结束,刚刚开始而已。注册成功之后,默认情况下,Eureka Client每隔30秒就向Eureka Server发送一条心跳消息,来告诉Eureka Server我还在运行,如果Eureka Server连续 90 秒都收到Eureka Client的续约消息(连续三次),会认为Eureka Client 已经掉线了,会将掉线的Eureka Client 从当前的服务注册列表中删除。
服务下线(Cancel):当Eureka Client 下线时,他会主动发送一条消息,
DiscoveryManager. getinstance() .shutdownComponent();
告诉Eureka Server ,我下线啦。
获取注册表信息(Fetch Registries),Eureka Client从Eureka Server上获取服务的注册信息,并将其缓存到本地,本地客户端上。在需要调用远程服务时,会从该信息中查询远程服务所对应的IP地址,端口等信息,Eureka Client上缓存的服务注册信息会定期更新30秒,如果Eureka Server 返回的注册表信息与本地缓存的注册表信息不同的话,Eureka Client会自动处理。
服务踢除(Eviction):在默认情况下,当 Eureka Client 连续 90 秒没有向 Eureka Server 发送服务续约(即心跳〉 时, Eureka Server 会将该服务实例从服务注册列表删除,即服务剔除。
属性信息:
Eureka集群原理:
在集群架构中,Eureka Server之间通过Replicata进行数据同步,不同的Eureka Server之间不区分主从节点,所有的节点之间都是平衡的,节点之间,通过置顶的ServiceURl来相互注册,形成一个集群,进而提高节点的可用性。
在Eureka Server集群中,如果有一个节点宕机,Eureka client 会自动切花到新的Eureka Server上,每一个Eureka Server节点,都会相互同步数据,Eurack的链接方式,可以是单线的,在配置ServierURl时,可以指定多个注册地址,
Eureka的区分:region地理的不同区域,zone具体的机房。
服务注册就是把一个微服务注册到Euraka Server 上,当其他服务需要调用该服务时,只需要从Eureka Server 上查询该服务的信息即可。
创建一个provider服务,作为我们的服务提供者,创建项目时,选择Eureka Client依赖,这样,当服务创建成功后,简单配置一下,就可以被注册到Eureka Servel。
- spring.application.name=provider
- server.port=1113
- eureka.client.service-url.defaultZone = http://localhost:1111/eureka
分别表示当前服务的名称,端口号,以及服务的地址
启动Eureka服务,等到服务注册中心启动成功之后,在启动provider。都启动成功之后,浏览器输入:http://localhost:1111就可以查看注册信息。
首先在provider中提供一个接口,用于生产,然后创建一个新的consumer项目,消费这个接口。在provider中,提供一个hello接口,如下:
方式一:在普通服务中写死服务调用的接口,耦合度太高。
方法二:在Eureka Client中通过DiscoveryClient来获取服务列表,然后选择指定的生产者,返回服务列表。
方法三:开启多个不同端口的provider生产服务实现Eureka Client集群及,做简单的线性负载均衡。
生产者:provider服务:
- package com.liruilong.provider;
-
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RequestParam;
- import org.springframework.web.bind.annotation.RestController;
-
- /**
- * @Description :
- * @Author: Liruilong
- * @Date: 2020/3/12 23:02
- */
- @RestController
- public class HelloController {
-
- @Value("${server.port}")
- Integer port;
- @GetMapping("/hello")
- public String hello(@RequestParam(defaultValue = "小明") String name){
- return "hello Spring Cloud: I am " + name+ "I am from post:" +port ;
- }
- }
- spring.application.name=provider
- server.port=1113
- eureka.client.service-url.defaultZone = http://localhost:1111/eureka
消费者:consumer服务
- package com.liruilong.consumer;
-
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.cloud.client.ServiceInstance;
- import org.springframework.cloud.client.discovery.DiscoveryClient;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- import java.io.BufferedReader;
- import java.io.IOException;
- import java.io.InputStreamReader;
- import java.net.HttpURLConnection;
- import java.net.MalformedURLException;
- import java.net.URL;
- import java.util.List;
-
-
- /**
- * @Description :
- * @Author: Liruilong
- * @Date: 2020/3/12 23:18
- */
-
- @RestController
- public class UserHelloController {
-
-
- @Autowired
- DiscoveryClient discoveryClient;
-
- /**
- * @Author Liruilong
- * @Description http服务请求写死的,
- * @Date 10:32 2020/3/13
- * @Param []
- * @return java.lang.String
- **/
- @GetMapping("/hello1")
- public String hello1(){
- String sendurl = "http://localhost:1113/hello";
- return sendURl(sendurl);
- }
-
- /**
- * @Author Liruilong
- * @Description :借助Eureka Client提供的DiscoveryClient工具,根据服务名重Eureka上查询一个服务的详细信息。
- * @Date 10:18 2020/3/13
- * @Param []
- * @return java.lang.String
- **/
-
- @GetMapping("/hello2")
- public String hello2(){
- //DiscoveryClient查询到的服务列表是一个集合,因为服务中部署的过程中,可能是集群形式。每一项都是一个实例
- List<ServiceInstance> list = discoveryClient.getInstances("provider");
- ServiceInstance instance = list.get(0);
- String host = instance.getHost();
- int port = instance.getPort();
- StringBuffer sendurl = new StringBuffer();
- sendurl.append("http://").append(host).append(":").append(port).append("/hello");
- return sendURl(sendurl.toString());
- }
-
-
- /**
- * @Author Liruilong
- * @Description 实现简单的线性负载均衡+集群
- * @Date 11:26 2020/3/13
- * @Param []
- * @return java.lang.String
- **/
- int count = 0;
- @GetMapping("/hello3")
- public String hello3(){
- //DiscoveryClient查询到的服务列表是一个集合,因为服务中部署的过程中,可能是集群形式。每一项都是一个实例
- List<ServiceInstance> list = discoveryClient.getInstances("provider");
- ServiceInstance instance = list.get((count++) % list.size());
- String host = instance.getHost();
- int port = instance.getPort();
- StringBuffer sendurl = new StringBuffer().append("http://").append(host).append(":").append(port).append("/hello");
- return sendURl(sendurl.toString());
- }
-
- /**
- * @Author Liruilong
- * @Description: 利用HttpUrConnection来发起请求,
- * @Date 10:32 2020/3/13
- * @Param [sendurl]
- * @return java.lang.String
- **/
-
- public String sendURl( String sendurl){
- HttpURLConnection connection = null;
- try {
- URL url = new URL(sendurl);
- connection = (HttpURLConnection) url.openConnection();
- if(connection.getResponseCode() == 200){
- BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream()));
- String s = br.readLine();
- br.close();
- return s;
- }
- } catch (MalformedURLException e) {
- e.printStackTrace();
- } catch (IOException e) {
- e.printStackTrace();
- }
- return "error";
- }
- }
- spring.application.name=consumer
- server.port=1115
- eureka.client.service-url.defaultZone = http://localhost:1111/eureka/
测试:
Http调用:使用Spring提供的RestTemplate来实现,提供一个RestTemplate。http调用时不在使用HttpUrlConnection,直接使用RestTemplate。
- package com.liruilong.fsconsumer;
-
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.cloud.client.loadbalancer.LoadBalanced;
- import org.springframework.context.annotation.Bean;
- import org.springframework.web.client.RestTemplate;
-
- @SpringBootApplication
- public class FsconsumerApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(FsconsumerApplication.class, args);
- }
-
- @Bean
- @LoadBalanced
- public RestTemplate restTemplate(){
- return new RestTemplate();
- }
-
- }
- @Autowired
- DiscoveryClient discoveryClient;
-
- @Autowired
- RestTemplate restTemplate;
-
- /**
- * @Author Liruilong
- * @Description :借助Eureka Client提供的DiscoveryClient工具,根据服务名重Eureka上查询一个服务的详细信息。
- * @Date 10:18 2020/3/13
- * @Param []
- * @return java.lang.String
- **/
-
- @GetMapping("/hello2")
- public String hello2(){
- //DiscoveryClient查询到的服务列表是一个集合,因为服务中部署的过程中,可能是集群形式。每一项都是一个实例
- List<ServiceInstance> list = discoveryClient.getInstances("provider");
- ServiceInstance instance = list.get(0);
- String host = instance.getHost();
- int port = instance.getPort();
- StringBuffer sendurl = new StringBuffer();
- sendurl.append("http://").append(host).append(":").append(port).append("/hello");
- return restTemplate.getForObject(sendurl.toString(),String.class);
- }
负载均衡,这里使用过Ribbon实现, Ribbon是一个为客户端提供负载均衡功能的服务,它内部提供了一个叫做ILoadBalance的接口代表负载均衡器的操作,比如有添加服务器操作、选择服务器操作、获取所有的服务器列表、获取可用的服务器列表等等。
使用RestTemplate进行Eureka Client(包括服务提供者以及服务消费者,在这里其实是服务消费者使用RestTemplate)之间的通信,为RestTemplate配置类添加@LoadBalanced注解即可
-
- @Autowired
- @Qualifier("restTemplate")
- RestTemplate restTemplate;
-
-
- /**
- * @Author Liruilong
- * @Description :利用discoveryClient查provider地址,然后根据本地的负载均衡处理。选一个服务,然后拼接url
- * restTemplate不能复用。
- * @Date 11:26 2020/3/13
- * @Param []
- * @return java.lang.String
- **/
- int count = 0;
-
- @GetMapping("/hello3")
- public String hello3() {
- return restTemplate.getForObject("http://provider/hello",String.class);
- }
RestTemplate是从Spring3.0开始支持的一个Http请求工具,这个请求工具和Spring Boot无关,更和Spring Cloud无关。RestTemplate提供了常见的REST请求方法模板,例如GET, POST,PUTDELETE请求以及一些通用的请求执行方法exchange和execute方法。
RestTemplate本身实现了RestOperations接口,而在RestOperations接口中,定义了常见的RESTful操作,这些操作在RestTemplate中都得到了很好的实现
首先我们在provider中定义一个hello2接口:
这两大类方法实际上是重载的,唯一不同的,就是返回值类型。getForobject返回的是一个对象,这个对象就是服务端返回的具体值. getForEntity返回的是一个ResponseEntity,这个ResponseEntity中除了服务端返回的具体数据外,还保留了Http 响应头的数据
看清楚两者的区别之后,接下来看下两个各自的重载方法, getForObject和getForEntity分别有三个重载方法,两者的三个重载方法基本都是一致的。所以,这里,我们主要看其中一种。三个重载方法,其实代表了三种不同的传参方式。
- @GetMapping("/hello3")
- public String hello3() {
- return restTemplate.getForObject("http://provider/hello?name={1}", String.class,"liruilong ");
- }
-
- @GetMapping("/hello4")
- public String hello4() throws UnsupportedEncodingException {
- String s1 ;
- ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://provider/hello?name={1}", String.class, "liruilomng");
- Map<String,Object> map = new HashMap<>();
- map.put("name","liruilong");
- ResponseEntity<String> responseEntitys = restTemplate.getForEntity("http://provider/hello?name={name}",String.class,map);
- System.out.println(responseEntitys.getBody());
- System.out.println(restTemplate.getForEntity(URI.create("http://provider/hello?name"+ URLEncoder.encode("liruilong","utf-8")),String.class));
- s1 = responseEntity.getBody() + "\n";
- s1 += responseEntity.getStatusCode() + "\n";
- s1 += responseEntity.getStatusCodeValue() + "\n";
- s1 += responseEntity.getHeaders().toString();
- return s1;
- }
首先在provider中提供两个POST接口,同时,因为POST请求可能需要传递JSON,所以,这里我们创建一个普通的Maven项目作为commons模块,然后这个commons模块被provider和consumer共同引用,这样我们就可以方便的传递JSON了
.commons模块创建成功后,首先在commons模块中添加User对象,然后该模块分别被provider和consumer引用。
然后,我们在provider中,提供和两个POST接口:
- /**
- * @Author Liruilong
- * @Description K-v形式
- * @Date 18:07 2020/3/18
- * @Param [user]
- * @return com.liruilong.commons.User
- **/
-
- @PostMapping("/user1")
- public User addUser1(User user){
- return user;
- }
-
- /**
- * @Author Liruilong
- * @Description JSON 形式
- * @Date 2020/3/18
- * @Param [user]
- * @return com.liruilong.commons.User
- **/
-
- @PostMapping("/user2")
- public User addUser2(@RequestBody User user){
- return user;
- }
post参数到底是key/value形式还是json形式,主要看第二个参数,
如果第二个参数是MultivalueMap ,则参数是以key/value形式来传递的,如果是一个普通对象,则参数是以json形式来传递的。
- import org.springframework.util.LinkedMultiValueMap;
-
- import org.springframework.util.MultiValueMap;
-
-
-
-
- @GetMapping("/hello6")
- public User hello6(){
- MultiValueMap<String,Object> map = new LinkedMultiValueMap<>();
- map.add("username","LiRuilong");
- map.add("password","123456");
- User user = restTemplate.postForObject("http://provider/user1",map,User.class);
- System.out.println(restTemplate.postForObject("http://provider/user2",new User("123","123"), User.class).toString());
- return user;
-
- }
最后再看看一下postForLocation.有的时候,当我执行完一个post请求之后,立马要进行重定向.一个非常常见的场景就是注册,注册是一个post请求,注册完成之后,立马重定向到登录页面去登录。对于这种场景,我们就可以使用postForLocation.首先我们在provider上提供一个用户注册接口
- package com.liruilong.provider;
-
- import com.liruilong.commons.User;
- import org.springframework.stereotype.Controller;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestBody;
- import org.springframework.web.bind.annotation.ResponseBody;
-
- /**
- * @Description :
- * @Author: Liruilong
- * @Date: 2020/3/18 19:19
- */
- @Controller
- public class RegisterController {
-
- @GetMapping("/loginPage")
- @ResponseBody
- public String loginPage(String username) {
- return "loginPage" + username;
- }
-
- @PostMapping("regiser")
- public String regiser(User user) {
- return "redirect:http://provider/loginPage?username=" + user.getUsername();
- }
-
-
- }
- @GetMapping("/hello7")
- public String hello7(){
-
- MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
- map.add("username","liruilong");
- map.add("password","123");
- URI uri = restTemplate.postForLocation("http://provider/regiser",map);
- System.out.println(uri);
- String s = restTemplate.getForObject(uri,String.class);
- System.out.println(s);
- return s;
- }
- /**
- * @Author Liruilong
- * @Description K-v形式
- * @Date 18:07 2020/3/18
- * @Param [user]
- * @return com.liruilong.commons.User
- **/
-
- @PutMapping("/user1")
- public void updateUser1(User user){
- System.out.println(user.toString());
- }
-
- /**
- * @Author Liruilong
- * @Description JSON 形式
- * @Date 2020/3/18
- * @Param [user]
- * @return com.liruilong.commons.User
- **/
-
- @PutMapping("/user2")
- public void updateUser2(@RequestBody User user){
- System.out.println(user.toString());
- }
- @GetMapping("/hello8")
- public void hello8(){
- MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
- map.add("username","liruilong");
- map.add("password","123");
- restTemplate.put("http://provider/user1",map);
- restTemplate.put("http://provider/user2",new User("123","123"));
-
- }
- @DeleteMapping("/user1")
- public void DeleteUser1(Integer id){
- System.out.println(id);
- }
-
- @DeleteMapping("/user2/{id}")
- public void DeleteUser2(@PathVariable Integer id){
- System.out.println(id);
- }
-
- @GetMapping("/hello9")
- public void hello9(){
- restTemplate.delete("http://provider/user1?id={1}","99");
- restTemplate.delete("http://provider/user2/{1}","99");
- }
客户端负载均衡就是相对服务端负载均衡而言的。
服务端负载均衡,就是传统的Nginx的方式,用Nginx做负载均衡,我们称之为服务端负载均衡:
这种负载均衡,我们称之为服务端负载均衡,它的一个特点是,就是调用的客户端并不知道具体是哪个Server提供的服务,它也不关心,反正请求发送给Nginx, Nginx再将请求转发给Tomcat,客户端只需要记着Nginx的地址即可。客户端负载均衡则是另外一种情形:
客户端负载均衡,就是调用的客户端本身是知道所有Server的详细信息的,当需要调用Server上的接口的时候,客户端从自身所维护的Server列表中,根据提前配置好的负载均衡策略,自己挑选一个Server来调用,此时,客户端知道它所调用的是哪一个Server.
在RestTemplate中,要想使用负载均衡功能,只需要给RestTemplate实例上添加一个@LoadBalanced注解即可,此时, RestTemplate就会自动具备负载均衡功能,这个负载均衡就是客户端负载均衡。
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Nettlix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模板请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。
负载均衡在系统架构中是一个非常重要,并且是不得不去实施的内容。因为负载均衡是对系统的高可用、网络压力的缓解和处理能力扩容的重要手段之一。我们通常所说的负载均衡都指的是服务端负载均衡,其中分为硬件负载均衡和软件负载均衡。
不论采用硬件负载均衡还是软件负载均衡,只要是服务端负载均衡都能以类似下图的架构方式构建起来:
硬件负载均衡的设备或是软件负载均衡的软件模块都会维护一个下挂可用的服务端清单,通过心跳检测来剔除故障的服务端节点以保证清单中都是可以正常访问的服务端节点。当客户端发送请求到负载均衡设备的时候,该设备按某种算法(比如线性轮询、按权重负战、按流量负载等)从维护的可用服务端清单中取出一台服务端的地址,然后进行转发。
在Spring Cloud中,实现负载均衡非常容易,只需要添加@LoadBalanced注解即可。
只要添加了该注解,一个原本普普通通做Rest请求的工具RestTemplate就会自动具备负载均衡功能,这个是怎么实现的呢?
整体上来说,这个功能的实现就是三个核心点:
除了 Eureka, Spring Cloud 还提供了 Consul 服务注册与发现的支持实现。 Consul 是由 HashiCorp 基于 Go 语言开发的服务软件,支持多数据中心、 分布式和高可 用的服务注册和发现。 它采用 Raft 算法保证服务的一致性,支持健康检查。
consul安装好之后访问:8500
新建一個項目:
- spring.application.name=consul-pro
- server.port=2000
-
- #Consul的配置
- spring.cloud.consul.host=39.97.241.18
- spring.cloud.consul.port=8500
- spring.cloud.consul.discovery.service-name=consul-pro
- package com.liruilong.consulpro;
-
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
-
- @SpringBootApplication
- @EnableDiscoveryClient
- public class ConsulProApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(ConsulProApplication.class, args);
- }
-
- }
构建一个生产服务
- package com.liruilong.consulpro;
-
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- /**
- * @Description :
- * @Author: Liruilong
- * @Date: 2020/3/20 17:00
- */
- @RestController
- public class HelloController {
-
- @Value("${server.port}")
- Integer prot;
-
-
- @GetMapping("/hello")
- public String hello(){
- return "生活加油:"+prot;
- }
- }
构建一个消费服务:
- spring.application.name=consul-con
- server.port=2002
- spring.cloud.consul.port=8500
- spring.cloud.consul.host=39.97.241.18
- spring.cloud.consul.discovery.service-name=consul-con
-
- package com.liruilong.consulcon;
-
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
- import org.springframework.context.annotation.Bean;
- import org.springframework.web.client.RestTemplate;
-
- @SpringBootApplication
- @EnableDiscoveryClient
- public class ConsulConApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(ConsulConApplication.class, args);
- }
-
- @Bean
- RestTemplate restTemplate(){
- return new RestTemplate();
- }
-
- }
消费接口服务:这个报错我也没办法解决。
- package com.liruilong.consulcon;
-
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.cloud.client.ServiceInstance;
- import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RestController;
- import org.springframework.web.client.RestTemplate;
-
- /**
- * @Description :
- * @Author: Liruilong
- * @Date: 2020/3/20 19:52
- */
- @RestController
- public class Hellocontroller {
-
- @Autowired
- RestTemplate template;
-
- @Autowired
- LoadBalancerClient loadBalancerClient;
-
- @GetMapping("/hello")
- public String hello(){
-
- ServiceInstance chooes = loadBalancerClient.choose("consul-provider");
- System.out.println(chooes.getHost());
- System.out.println(chooes.getInstanceId());
- return template.getForObject(chooes.getUri()+"/hello",String.class);
- }
- }
Hystrix叫做断路器熔断器。微服务系统中,整个系统出错的概率非常高,因为在微服务系统中,涉及到的模块太多了,每一个模块出错,都有可能导致整个服务出,当所有模块都稳定运行时,整个服务才算是稳定运行。
我们希望当整个系统中,某一个模块无法正常工作时,能够通过我们提前配置的一些东西,来使得整个系统正常运行,即单个模块出问题,不影响整个系统
首先创建一个新的SpringBoot模块,然后添加依赖:
-
- spring.application.name=hystrix
- server.port=3000
- eureka.client.service-url.defaultZone = http://localhost:1111/eureka
- package com.liruilong.hystrix;
-
- import org.springframework.boot.SpringApplication;
- import org.springframework.boot.autoconfigure.SpringBootApplication;
- import org.springframework.cloud.client.SpringCloudApplication;
- import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
- import org.springframework.cloud.client.loadbalancer.LoadBalanced;
- import org.springframework.context.annotation.Bean;
- import org.springframework.web.client.RestTemplate;
-
- /*@SpringBootApplication
- @EnableCircuitBreaker */
- //开启断路器
- @SpringCloudApplication
- public class HystrixApplication {
-
- public static void main(String[] args) {
- SpringApplication.run(HystrixApplication.class, args);
- }
-
- @Bean
- @LoadBalanced
- public RestTemplate restTemplate(){
- return new RestTemplate();
- }
- }
-
提供接口:
- package com.liruilong.hystrix;
-
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RestController;
-
- /**
- * @Description :
- * @Author: Liruilong
- * @Date: 2020/3/21 11:55
- */
- @RestController
- public class Hellocontroller {
- @Autowired
- HelloService helloService;
-
- @GetMapping("/hello")
- private String hello(){
- return helloService.hello();
- }
-
- }
调用方式可以链式调用,
- package com.liruilong.hystrix;
-
- import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
- import org.springframework.web.client.RestTemplate;
-
- /**
- * @Description :
- * @Author: Liruilong
- * @Date: 2020/3/21 11:56
- */
- @Service
- public class HelloService {
-
- @Autowired
- RestTemplate restTemplate;
-
- /**
- * @Author Liruilong
- * @Description 在这个方法中,发起一个远程调用,去调用provider 中的。/hello 接口
- * 在这个方法上添加@HystrixCommand 注解,配置fallbackMethon
- * @Date 16:40 2020/3/21
- * @Param []
- * @return java.lang.String
- **/
-
- @HystrixCommand(fallbackMethod = "error")
- public String hello(){
- return restTemplate.getForObject("http://provider/hello",String.class);
- }
-
- /**
- * @Author Liruilong
- * @Description 可以依次降级
- * @Date 17:02 2020/3/21
- * @Param []
- * @return java.lang.String
- **/
-
- @HystrixCommand(fallbackMethod = "erroe1")
- public String error(){
- System.out.println("服务降级啦1");
- return restTemplate.getForObject("http://provider/hello",String.class);
- }
- public String erroe1(){
- return "服务降级2";
- }
- }
请求命令就是以继承类的方式来替代前面的注解方式。需要继承Hystrixcommand来实现,run方法里写逻辑,接口中通过构造函数调用,对于异常的处理getFallback方法。
- package com.liruilong.hystrix;
-
- import com.netflix.hystrix.HystrixCommand;
- import org.springframework.web.client.RestTemplate;
- /**
- * @Description :
- * @Author: Liruilong
- * @Date: 2020/3/21 17:28
- */
- public class HelloCommand extends HystrixCommand<String> {
-
- RestTemplate restTemplate;
-
- //setter 请求的详细配置
- public HelloCommand(Setter setter, RestTemplate restTemplate) {
- super(setter);
- this.restTemplate = restTemplate;
- }
-
- @Override
- protected String run() throws Exception {
- return restTemplate.getForObject("http://provider/hello",String.class);
-
- }
-
- @Override
- protected String getFallback() {
- return "继承类的方式-服务降级啦";
- }
- }
-
调用方式分为两种 ,
- package com.liruilong.hystrix;
-
-
- import com.netflix.hystrix.HystrixCommand;
- import com.netflix.hystrix.HystrixCommandGroupKey;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.RestController;
- import org.springframework.web.client.RestTemplate;
-
- import java.util.concurrent.ExecutionException;
- import java.util.concurrent.Future;
-
- /**
- * @Description :
- * @Author: Liruilong
- * @Date: 2020/3/21 11:55
- */
- @RestController
- public class Hellocontroller {
- @Autowired
- HelloService helloService;
- @Autowired
- RestTemplate restTemplate;
-
- @GetMapping("/hello")
- private String hello(){
- return helloService.hello();
- }
- @GetMapping("/hello1")
- private void hello1(){
- HelloCommand helloCommand1 = new HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("helloCommand")), restTemplate);
- System.out.println(helloCommand1.execute());
- // 方法二:入队
-
- //方法一:直接执行
- HelloCommand helloCommand = new HelloCommand(HystrixCommand.Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("helloCommand1")), restTemplate);
-
- Future<String> queue = helloCommand.queue();
- try{
- String s = queue.get();
- System.out.println(s);
- } catch (InterruptedException e) {
- e.printStackTrace();
- } catch (ExecutionException e) {
- e.printStackTrace();
- }
-
- }
-
- }
异步请求调用:
- @HystrixCommand(fallbackMethod = "erroe1")
- public Future<String> hello2(){
- return new AsyncResult<String>() {
- @Override
- public String invoke() {
- return restTemplate.getForObject("http://provider/hello",String.class);
- }
- };
- }
- @GetMapping("/hello2")
- public void hello2() throws ExecutionException, InterruptedException {
- Future<String> hello2 = helloService.hello2();
- System.out.println(hello2.get());
-
- }
当服务器调用的时,如果不是provider原因导致的调用失败,而是consumer中本本身代码出了问题,即consumer中抛出了代码异常。这个时候,也会自动进行服务降级,但是这个时候我们需要知道异常出自哪里。
我们可以在error方法中获取异常。
- @HystrixCommand(fallbackMethod = "erroe1")
- public String error(Throwable t){
- System.out.println("服务降级啦1");
- System.out.println(t.getMessage());
- return restTemplate.getForObject("http://provider/hello",String.class);
- }
- public String erroe1(){
- return "服务降级2";
- }
继承的方式:
- @Override
- protected String getFallback() {
- System.out.println(getExecutionException().getMessage());
- return "继承类的方式-服务降级啦";
- }
设定不要服务降级
- @HystrixCommand(fallbackMethod = "erroe1",ignoreExceptions = ArithmeticException.class)
- public String error(Throwable t){
- System.out.println("服务降级啦1");
- System.out.println(t.getMessage());
- return restTemplate.getForObject("http://provider/hello",String.class);
- }
在consumer中调用同一个接口,如果参数形同,则可以使用之前的缓存,需要加@CacheResult,缓存有一个生命周期的概念,这里也一样,需要初始化HystrixRequestContext.初始化完成之后,缓存开始生效。调用close方法之后,缓存开始失效。
- @HystrixCommand(fallbackMethod = "erroe3")
- @CacheResult//表示该方法的请求结果会被缓存i起来,默认情况下,缓存的key即使方法的参数,
- public String hello3(String name) {
- return restTemplate.getForObject("http://provider/hello?name={1}",String.class,name);
- }
- public String erroe3(String name){
- return "服务降级+缓存";
- }
默认情况下,缓存的key就是所调用的方法的参数,如果参数有多个,就是多个参数组合起来作为缓存key。
如果多个参数,但是又想使用其中一个作为缓存的key,那么可以通过@Cachekey注解来解决。同时当多个参数时,键相同,即使,其他参数不相同,多次请求也可以使用第一次缓存的结果。
- @HystrixCommand(fallbackMethod = "erroe3")
- @CacheResult//表示该方法的请求结果会被缓存i起来,默认情况下,缓存的key即使方法的参数,
- public String hello3(@CacheKey String name, Integer age) {
- return restTemplate.getForObject("http://provider/hello?name={1}",String.class,name);
- }
- public String erroe3(String name){
- return "服务降级+缓存";
- }
@CacheRemove:在做数据缓存时,如果我们删除数据同时希望把缓存的数据也删除,即可以使用这个。
- @HystrixCommand
- @CacheRemove(commandKey = "hello3")
- public String deleteByName(String name){
- return name;
- }
继承的话需要调用:
- @Override
- protected String getCacheKey() {
- return null;
- }
如果consumer中,频繁的调用provider中的同一个接口,在调用时,只是参数不一样,那个这样的情况下,就可以将多个请求合并为一个,提高请求发送的效率。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。