上一篇讲解了一下他们之间的关系以及概念,这篇来具体实操一下
一、Nacos服务注册中的配置
1.概述:
在微服务中,首先需要面对的问题就是如何查找服务(软件即服务),其次,就是如何在不同的服务之间进行通信?如何更好更方便的管理应用中的每一个服务,如何建立各个服务之间联系的纽带,由此注册中心诞生(例如淘宝网卖家提供服务,买家调用服务)。
市面上常用注册中心有Zookeeper(雅虎Apache),Eureka(Netfix),Nacos(Alibaba),Consul(Google)等,我们使用的Nacos
2.特点:
Nacos(DynamicNaming and Configuration Service)是一个应用于服务注册与发现、配置管理的平台。它孵化于阿里巴巴,成长于十年双十一的洪峰考验,沉淀了简单易用、稳定可靠、性能卓越的核心竞争力。
3.安装使用:
3.1.下载解压
下载地址https://github.com/alibaba/nacos/releases3.2数据库建立
在conf文件下有nacos-mysql.sql脚本文件,在数据库里执行,这个sql脚本文件不完全,需要自己手动建立库在库下执行
数据库版本:要求mysql的版本大于5.7版本(MariaDB最好10.5.11)
3.3配置文件配置
打开/conf/application.properties里打开默认配置,并基于你当前环境配置要连接的数据库,连接数据库时使用的用户名和密码(假如前面有"#"要将其去掉)
3.4启动测试
启动:standalone代表单机模式运行,非集群
Linux/Unix/Mac启动命令
./startup.sh -m standalone
Windows启动命令
startup.cmd -m standalone
前提:
- 执行执行令时要么配置环境变量,要么直接在nacos/bin目录下去执行.
- nacos启动时需要本地环境变量中配置了JAVA_HOME(对应jdk的安装目录),
- 一定要确保你连接的数据库(nacos_config)是存在的.
访问服务:
默认地址
http://localhost:8848/nacos
默认密码:nacso/nacos
在IDEA里添加脚本按钮启动:
二、项目的创建
1.创建聚合父工程
结构:
pom文件依赖的添加:
<!--maven支持单继承,多层继承,所以需要导入的方式,只有pom才能导入-->
<groupId>com.cy</groupId>
<artifactId>01-sca</artifactId>
<!--创建父工程的子工程时,父工程的打包方式会自动变为pom方式-->
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<!--创建子工程时辉自动将子工程的名字放到modules下-->
<modules>
<module>sca-provider</module>
<module>sca-consumer</module>
<module>sca-gateway</module>
</modules>
<!--properties中定义一些属性元素,动态的定义版本号,如下
<version>${spring-boot-version}</version>-->
<properties>
<spring-boot-version>2.3.2.RELEASE</spring-boot-version>
</properties>
<!--maven的父工程为pom工程
此工程主要负责依赖版本及部分基础依赖的管理-->
<dependencyManagement>
<!--spring boot 依赖-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot-version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<!--Spring Cloud依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR9</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<!--Spring Cloud Alibaba 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<!--下面这个dependencies元素中定义子工程都需要的依赖-->
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!--编译阶段有效,运行阶段无效-->
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.创建子工程
概念:生产者与消费者都是相对来说的,没有绝对的消费者与生产者
2.1生产者服务创建注册
在父工程下创建子工程生产者服务sca-provider
2.1.1.pom依赖的添加
<!--工程模块坐标-->
<artifactId>sca-provider</artifactId>
<!--创建maven模块自带的,用于指定maven中对java类的编译和运行版本-->
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!--web 服务依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--nacos服务的注册和发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--nacos配置中心的依赖(nacos.io)-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
</dependencies>
2.1.2.创建并配置bootstrap.yml文件
server: #创建生产者服务注册
port: 8081 #服务端口
spring:
application:
name: sca-provider #服务名
cloud:
nacos: #服务启动时会向nacos发送心跳包(5s一次)
discovery: #服务的注册和发现
server-addr: localhost:8848 #nacos server
2.1.3.创建启动类定义代码
@SpringBootApplication
public class ProviderApplication {
public static void main(String[] args) {
SpringApplication.run(ProviderApplication.class, args);
}
/**定义Controller对象(这个对象在spring mvc中给他的定义是handler),
* 基于此对象处理客户端的请求*/
@RestController
public class ProviderController{
//@Value默认读取项目配置文件中配置的内容
//8080为没有读到server.port的值时,给定的默认值
@Value("${server.port:8080}")
private String server;
//Echo 回显
//Rest 一种软件架构编码风格,可以基于这种风格定义url
//http://localhost:8081/provider/echo/tedu
@GetMapping("/provider/echo/{msg}")
public String doRestEcho1(@PathVariable String msg){
return server+":"+msg;
}
}
}
2.1.4.查看服务
启动服务进入Naocs的服务列表进行查看,显示服务名则注册成功
2.2消费者服务创建注册
前两步同消费者创建,注意端口号不要重复即可
2.2.1创建启动类定义代码
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class,args);
}
/*启动类启动时将RestTemplate IOC交给spring管理
* 后续通过此对象进行远程调用*/
//restTemplate 需要调用服务提供方的服务时添加,交由spring去管理
@Bean//别忘了启动类注解@SpringBootApplication包含了配置类注解,它本身就时一个大的配置类
public RestTemplate restTemplate(){
return new RestTemplate();
}
@RestController
public class ConsumerController{
@Value("${spring.application.name}")
private String appName;
@Autowired
private RestTemplate restTemplate;
@GetMapping("/consumer/doRestEcho1")
public String doRestEcho01(){
//调用服务方提供的API url也是API
//1.定义要调用的API
String url = "http://localhost:8081/provider/echo/"+appName;
System.out.println("request url:"+url);
//2.谁去访问这个API
return restTemplate.getForObject(url, String.class);
}
}
}
2.2.2查看服务
启动服务进入Naocs的服务列表进行查看,显示服务名则注册成功
问题:现在是服务调用的形式,一个服务实例可以处理请求是有限的,,那在某个高峰期其他服务全部都来调用一个服务服务器会有宕机的现象,我们又该如何处理呢?
3.负载均衡调用服务
3.1直接负载均衡调用服务
注入LoadBalancerClient类
@Autowired
private LoadBalancerClient loadBalancerClient;
通过注册中心获取服务实例来调用服务
//负载均衡方式调用
@GetMapping("consumer/doRestEcho2")
public String doRestEcho2(){
//1.层注册中心获取服务实例
ServiceInstance instance = loadBalancerClient.choose("sca-provider");
//2.基于RestTemplate进行服务实例对象
String ip = instance.getHost();
int port = instance.getPort();
String url=String.format("http://%s:%s/provider/echo/%s", ip,port,appName);
return restTemplate.getForObject(url, String.class);
}
3.2测试
开启两个服务生产者服务,通过消费者对它进行访问并输出端口号
如图所示,真的实现了负载均衡
思考1:为什么调用注册中心获取的服务实例就可以实现负载均衡的效果?
答:这里的负载均衡实现默认是因为Nacos集成了Ribbon来实现的,Ribbon配合RestTemplate,可以非常容易的实现服务之间的访问。Ribbon是Spring Cloud核心组件之一,它提供的最重要的功能就是客户端的负载均衡(客户端可以采用一定算法,例如轮询访问,访问服务端实例信息),这个功能可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡方式的服务调用。
思考2:经过了长时间的使用,我们发现当前的调用的前几步都是类似的,我们可不可以将它们进行封装直接调用就好了呢?
3.3简洁负载均衡调用服务
在启动类里再次添加RestTemplate的Bean,并添加注解@LoadBalanced
@Bean
@LoadBalanced
public RestTemplate loadBalancerRestTemplate(){
return new RestTemplate();
}
进行注入
@Autowired
private RestTemplate loadBalancerRestTemplate;
直接通过服务名来调用服务
@GetMapping("consumer/doRestEcho3")
public String doRestEcho3(){
//定义url
//节省了注册中心获取服务实例和获取服务的ip和端口号这三步,直接写服务名称即可,记得要用加@LoadBalanced的注解的Bean调用
String url=String.format("http://sca-provider/provider/echo/%s", appName);
//服务调用
return loadBalancerRestTemplate.getForObject(url, String.class );
}
底层实现:
RestTemplate在发送请求的时候会被LoadBalancerInterceptor拦截,它的作用就是用于RestTemplate的负载均衡,LoadBalancerInterceptor将负载均衡的核心逻辑交给了loadBalancer,@LoadBalanced注解是属于Spring,而不是Ribbon的,Spring在初始化容器的时候,如果检测到Bean被@LoadBalanced注解,Spring会为其设置LoadBalancerInterceptor的拦截器。
@LoadBalanced的作用:描述RestTemplate对象,用于告诉Spring框架,在使用RestTempalte进行服务调用时,这个调用过程会被一个拦截器进行拦截,然后在拦截器内部,启动负载均衡策略。
Ribbon:Netflix公司提供的负载均衡客户端,一般应用于服务的消费方法
Ribbon负载均衡策略:默认是轮询策略,当系统提供的负载均衡策略不能满足我们需求时,我们还可以基于IRule接口自己定义策略.
3.4 Feign的远程服务调用--重点
概述:
服务消费方基于rest方式请求服务提供方的服务时,一种直接的方式就是自己拼接url,拼接参数然后实现服务调用,但每次服务调用都需要这样拼接,代码量复杂且不易维护,此时Feign诞生。Feign 是一种声明式Web服务客户端,底层封装了对Rest技术的应用,通过Feign可以简化服务消费方对远程服务提供方法的调用实现
1. 在服务消费方添加依赖
<!--open feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2.在启动类上添加@EnableFeignClients注解
@EnableFeignClients
@SpringBootApplication
public class ConsumerApplication {…}
3.创建service包,定义Http请求API,基于此API借助OpenFeign访问远端服务
@FeignClient(name="sca-provider")//远程调用的服务名
public interface RemoteProviderService {
@GetMapping("provider/echo/{msg}")//服务路径
public abstract String echoMessage(@PathVariable("msg") String msg);
}
注意:注解@PathVariable必须加,因为jdk8后可以省略,但是Feign为了向前兼容是jdk5的版本
4.创建Controller包进行测试
@RestController
@RequestMapping("/consumer")
public class FeignConsumerController {
@Autowired
private RemoteProviderService remoteProviderService;
/*执行远程服务调用*/
/*browser->consumer->provider*/
/*自带负载均衡*/
@GetMapping("/echo/{msg}")
public String doRestEcho(@PathVariable String msg) {
return remoteProviderService.echoMessage(msg);
}
}
5.测试
测试成功
4.Feign配置进阶实践
问题1:一个服务提供方通常会提供很多资源服务,服务消费方基于同一个服务提供方写了很多服务调用接口,此时假如没有指定contextId,服务启动就会失败,例如假如在服务消费方再添加一个如下接口,消费方启动时就会启动失败,
解决:此时我们需要为远程调用服务接口指定一个contextId,作为远程调用服务的唯一标识即可
@FeignClient(name="sca-provider",contextId = "remoteProviderService",)
public interface RemoteProviderService {
@GetMapping("provider/echo/{msg}")
public abstract String echoMessage(@PathVariable("msg") String msg);
}
问题2:当我们在进行远程服务调用时,假如调用的服务突然不可用了或者调用过程超时了,怎么办呢,作为开发者而言,我们不能把错误暴漏在用户面前,所以我们一般服务消费端会给出具体的容错方案
解决:
1.创建出现问题时解决方案的类,实现FallbackFactory接口并重写里面的create方法,在方法里给出解决方案,注意后面加泛型为Feign接口的名字
@Component
public class ProviderFallbackFactory implements FallbackFactory<RemoteProviderService> {
/*org.slf4j.Logger是java的日志规范,定义了一组接口,接口的实现又log4f,logback(快)*/
// private static Logger log= LoggerFactory.getLogger(ProviderFallbackFactory.class);//门面设计模式,有Slf4j注解就不用写这句了
@Override
public RemoteProviderService create(Throwable throwable) {
//方式一:
// return new RemoteProviderService() {
// @Override
// public String echoMessage(String msg) {
// /*服务调节*/
// return "服务忙,稍等";
// }
// };
//方式二:
//借助了jdk8中的lambda表达式
// return (msg)->{//lambda表达式(jdk中一个表达式)
// return "服务忙,稍等";
// };
//方式三:
return msg -> "服务忙,稍等";
}
}
注意:方式一是原型,后两者是lambda的简写表达式
2.在Feign访问接口中应用FallbackFactory=出现问题时解决方案的类名
@FeignClient(name="sca-provider",contextId = "remoteProviderService",fallbackFactory = ProviderFallbackFactory.class)
public interface RemoteProviderService {
@GetMapping("provider/echo/{msg}")
public abstract String echoMessage(@PathVariable("msg") String msg);
3.在配置文件application.yml中添加如下配置,启动feign方式调用时的服务中断处理机制.
feign:
hystrix:
enabled: true #默认值为false
4.测试
在服务生产方添加延时代码模拟访问出错并访问地址查看现象
测试成功
总结
流程:
- 通过 @EnableFeignCleints 注解告诉springcloud,启动 Feign Starter 组件。
- Feign Starter 在项目启动过程中注册全局配置,扫描包下所由@FeignClient注解描述的接口,然后由系统底层创建接口实现类(JDK代理类),并构建类的对象,然后交给spring管理(注册 IOC 容器)。
- 接口被调用时被动态代理类逻辑拦截,将 @FeignClient 请求信息通过编码器生成 Request对象,基于此对象进行远程过程调用。
- 请求对象经Ribbon进行负载均衡,挑选出一个健康的 Server 实例(instance)。
- 通过 Client 携带 Request 调用远端服务返回请求响应。
- 通过解码器生成 Response 返回客户端,将信息流解析成为接口返回数据。
重点知识:
- 为什么使用feign?(基于Feign可以更加友好的实现服务调用,简化服务消费方对服务提供方方法的调用)。
- @FeignClient注解的作用是什么?(告诉Feign Starter,在项目启动时,为此注解描述的接口创建实现类-代理类)
- Feign方式的调用,底层负载均衡是如何实现的?(Ribbon)
- @EnableFeignCleints 注解的作用是什么?(描述配置类,例如启动类)