spring cloud 学习
基础知识
本文代码
本文所用代码开源在:
https://gitee.com/kiteff/springcloud_learning_project
欢迎技术交流一起进步
什么是CAP
在一个分布式系统中,C(一致性)、A(可用性)、P(容错性) ,三者最多同时只能满足两个
项目 | 解释 |
---|---|
一致性 | 在分布式系统中的所有的数据备份,在同一个时刻,都拥有同样的值。(所有的节点在同一时刻的数据完全一致,节点越多,数据同步越耗时) |
可用性 | 负载过大的时候,集群整体是否能够响应客户端的读写请求,(服务一直可用,而且是正常的响应时间) |
分区容错性 | 高可用,一个节点奔溃了,并不会影响其他的节点。 |
为什么CAP只能三选一
组合 | 解释 |
---|---|
CA组合 | 数据同步需要时间,也要在正常的时间内响应,那么机器的数量就必须要少,容错性就不能满足了 |
CP组合 | 数据同步需要时间,机器的数量又要多才能保证容错性,就要耗费大量的时间,可用性就不能满足 |
AP组合 | 机器数量多,同时又要保持在正常时间内响应,就不能保证数据同步,以此来节约时间 |
注册中心如何选择
注册中心 | 解释 |
---|---|
Zookeeper | CP 设计,为了保证一致性,集群某一个节点失效以后,就会进行leader选举,如果板半数以上节点不可以用,集群就无法再提供服务 |
Eureka | AP 设计,无主从节点,一个节点挂了以后,其他节点直接上去,去中心化 |
Eureka Server 搭建
打开 IDEA -> Spring Initializer->选Cloud Discovery -> Eureka Server
-
设置yml 文档
server: port: 8761 eureka: instance: hostname: localhost statusPageUrlPath: http://${eureka.instance.hostname}:${server.port}/info healthCheckUrlPath: http://${eureka.instance.hostname}:${server.port}/health client: #表示这是一个服务端,他不会去获取数据 registerWithEureka: false fetchRegistry: false serviceUrl: #注册中心地址 defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
-
在application 上加入@EnableEurekaServer
@SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
启动即可,然后浏览器访问 http://localhost:8761/ (8761就是设置的server.port)
服务提供者
搭建步骤
和之前一样的建设步骤
-
设置yml 文件
eureka: client: serviceUrl: #这里的地址要和eureka server 的地址一致 defaultZone: http://localhost:8761/eureka/ server: port: 8771 spring: application: name: product-service #这个名字很重要,之后服务消费者要根据这个地址来调用要唯一
小技巧如何启动多个端口服务
为了测试,我们可以在idea 的configuretion -> vm options
里输入 -Dserver.port=8771
这个8771 端口就是服务提供的端口
然后再启动
如何关闭保护模式
Eureka-Server 的yml 文件中
server:
enable-self-preservation: false
保护模式的作用是:如果Eureka server 发现客户端好像死了,在保护模式下并不会直接移除这个客户端,一般会采用熔断的机制,定时的再去访问访问,如果关闭了保护模式就会直接移除,生产环境建议不要关闭
服务消费者
两种模式的区别
模式 | 解释 |
---|---|
RPC | 远程过程调用,像调用本地服务一样调用远程的的服务,支持同步异步. 客户端和服务器之间建立tcp连接,可以一次建立多个,也可以复用一个,缺点是编解码,序列化,链接,丢包 protobuf |
http(rest) | http请求支持多种协议和功能,开发方便成本低,但是数据包大 |
Ribbon
看文档 ,文档为主
两种模式下都要加上 web 依赖
-
编写 yml 文件 ,这里ribbon的yml 和下面的feign 一致
eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ server: port: 8781 spring: application: name: client-demo
-
加入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </dependency>
-
注入负载均衡器
在application 里
@Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }
@LoadBalanced 这个是表示他是一个负载均衡的
-
使用
在servcie中
Object forObject = restTemplate.getForObject("http://product-service/api/v1/product/find?id=" + id, Object.class); System.out.println(forObject); return forObject;
这里 getForObject 里的地址格式是:http://服务提供者的名字/对应的路径
Feign
-
引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
在 application 上增加注解
@EnableFeignClients
-
新增一个接口
/* * value 是要消费的服务名称 */ @FeignClient(value = "product-service") public interface FeignClient { //getMapping 说明这是一个get请求,里面的值就是这个服务的具体地址 @GetMapping("/api/v1/product/find") //方法名随意,里面的参数就是对应 服务提供者所需要的参数 String findById(@RequestParam(value = "id") int id); }
-
在service中调用这个接口
String client = feignClient.findById(id); System.out.println(client); return client;
熔断和降级
概念
概念 | 解释 |
---|---|
熔断 | 熔断的作用是保护服务的调用方和被调用方,一个操作 可能要调用a b c 三个服务,但是比如服务B 现在挂了,这个就会导致,整个操作不能进行,这个时候应该及时熔断,服务b ,这样之后就不会去调用服务b 了,保证了整个操作能完成 |
降级 | 有时候数据量比较大,比如双十一期间,我们可以把一些,次要的服务降级,系统吃紧的时候不去调用他们而去采用兜底数据 |
hystrix
-
引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
-
修改yml配置文件
feign: hystrix: enabled: true
-
设置超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 5000
这一句话的意思是设置5秒超时 , 可以不设置 ,但我建议设置,不然必会发现关闭一个服务时,会有比较明显的抖动
使用方法一
第一种使用的方法是在controller 中使用
-
在要熔断的controller上添加注解
@EnableCircuitBreaker
表示开启了断路器
-
编写代码
@RequestMapping("/getFeign") //指定服务挂了以后用defaultStores方法处理 @HystrixCommand(fallbackMethod = "defaultStores") public Object getProductFeign(@RequestParam (value = "id") int id){ Object project = productService.getProjectByFeign(id); HashMap<Object, Object> map = new HashMap<>(); map.put("info",project); return map; } //处理的方法参数要和调用者的一致 public Object defaultStores(int id) { HashMap<Object, Object> map = new HashMap<>(); map.put("code",-1); map.put("info","断路器启用"); return map; }
使用方法二
第二种方法 要配合Feign使用
-
实现一个一个异常处理类继承要熔断的 FeignClient
要加上
@Componet
注解 才能被扫描到@Component public class FeignClientFallBack implements FeignClient { @Override public String findById(int id) { System.out.println("出现异常了"); return null; } }
-
在FeignClient中添加一个fallback 指定异常处理类
//这里的 fallback 就是指定了异常处理类 @FeignClient(value = "product-service", fallback = FeignClientFallBack.class) public interface FeignClient { @GetMapping("/api/v1/product/find") String findById(@RequestParam(value = "id") int id); }
Zuul 网关
-
添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency>
-
在application 中 开启网关
@SpringBootApplication @EnableZuulProxy public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
-
定义拦截器
@Component //继承ZuulFilter public class MyFilter extends ZuulFilter { //拦截器的类型 PRE_TYPE 表示前置拦截 @Override public String filterType() { return PRE_TYPE; } //拦截器的优先级 越小越先执行 @Override public int filterOrder() { return 4; } //拦截以后的处理方法,返回false 进入下面的异常处理 @Override public boolean shouldFilter() { RequestContext currentContext = RequestContext.getCurrentContext(); HttpServletRequest httpServletRequest = currentContext.getRequest(); if("/ps/api/v1/product/list".equals(httpServletRequest.getRequestURI())){ return true; } return false; } //异常处理 @Override public Object run() throws ZuulException { System.out.println("欢迎傻逼"); return null; } }
-
配置yml 文件
#基础的配置 server: port: 9000 spring: application: name: api-gateway eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ # zuul 网关的配置 zuul: routes: product-service: /ps/** ignored-patterns: /*-service/** #ignored-services: product-service
下面解释一下上面的网关部分的配置
product-service: 表示将product-service的请求,别名成ps
ignored-patterns 表示不拦截哪些url的请求(可以用这个 关闭掉原来的服务访问方式)
ignored-services 表示不拦截哪些service
比如 http://localhost:9000/product-service/api/v1/product/list
在加上ignored-patterns: /*-service/**
就不能再访问了
全链路追踪
安装sleuth
-
配置pom文件
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-sleuth</artifactId> </dependency>
-
编写代码
在服务的controller 中加入
private final Logger logger = LoggerFactory.getLogger(getClass());
然后用日志打印,这里可以不加,但是会出现多次访问的时候日志不打印的情况,所以还是要加上的。
之后我们就能在日志中看到输出如
INFO [product-service,ab6728444294f2ab,ab6728444294f2ab,true] 27528 --- [nio-8771-exec-2]
下面对参数进行解释
值 解释 第一个值 spring.application.name的值 第二个值 sleuth生成的一个ID,叫Trace ID,用来标识一条请求链路,一条请求链路中包含一个Trace ID,多个Span ID 第三个值 panid 基本的工作单元,获取元数据,如发送一个http 第四个值 是否要将该信息输出到zipkin服务中来收集和展示
可视化 zipkin
刚才已经可以打印日志了,但是要做链路追踪还是需要一个可视化的界面才好
-
加上依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>
注意 这个依赖中已经包含了sleuth 的依赖,所以可以将sleuth的依赖去掉
-
安装zipkin
docker run -d -p 9411:9411 openzipkin/zipkin
-
配置yml文件
eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ server: port: 8771 spring: application: name: product-service # 建立zipkin的地址 zipkin: base-url: http://47.106.14.61:9411/ #记录的百分比,默认是0.1,也就是10%,也是是10条访问里记录一条 sleuth: sampler: probability: 1
之后访问几次服务,然后打开
http://47.106.14.61:9411/ 就能看了
配置中心
注册中心的作用是,当我们的微服务如果需要改变一些配置,注册中心可以让各个服务从配置中心拉取配置
安装配置中心
Docker 使用
Docker搭建
参考
https://help.aliyun.com/document_detail/51853.html?spm=a2c4g.11186623.6.820.RaToNY
-
添加yum源
# yum install epel-release –y # yum clean all # yum list
-
安装并运行Docker
# yum install docker-io –y # systemctl start docker
-
检查安装结果
# docker info
-
基本用法
# systemctl start docker #运行Docker守护进程 # systemctl stop docker #停止Docker守护进程 # systemctl restart docker #重启Docker守护进程
-
常见命令
docker search mysql #查询一个镜像 docker images #查看本地的镜像 docker rmi -f image的Id #强制删除本地的一个镜像 docker pull rabbitmq:management #拉取一个镜像 docker run -d --name "xdclass_mq" -p 5672:5672 -p 15672:15672 rabbitmq:management #运行一个容器 docker stop xdclass_mq # 停止一个容器 docker start xdclass_mq #启动容器 docker rm xdclass_mq # 移除一个容器 ,必须先停止 docker ps -a #查看所有容器,包括没在运行的 docker exec -it xzhclass_mq /bin/bash # 进入容器内部
配置中心
在实际的工程中,我们会去修改配置,如果有很多的服务我们一个个手工区修改配置太麻烦了,所以会使用配置中心,将所有的配置文件放在配置中心上,当服务启动的时候,会从配置中心去拉去配置。
远程仓库的搭建
springcloud 的配置中心要配合git 使用,这里使用码云来搭建这个git ,在码云上注册一个新的工程,这里用product-service 作为演示,在git 上创建一个product-service.yml
文件,这里的文件名 要和你的yml 文件中,spring.application.name
的一致,后面会再详细讲解
接下去我们在创建的文件中写入配置
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8777
spring:
application:
name: product-service
sb: xzh
没错这里的配置就是我们product-service
的本地配置,等下product-service
启动的时候讲会从远程git 上获取服务信息。
架设配置中心
现在码云上的仓库已经架设好了,我们要在本地架设配置中心,新建一个项目
勾选上 cloud-config->Config server
和 cloud-discovery->eureka-discovery
主要的就是在项目中加上如下依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置applicaiton.yml文件
spring:
application:
#配置中心在注册中心中的服务名称,等下改造服务的时候要用
name: CONFIG-SERVER
cloud:
config:
server:
git:
# 码云项目的地址
uri: https://gitee.com/kiteff/test_configuration_center
username: 账号
password: 密码
server:
port: 9100
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
修改启动类
@EnableConfigServer
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
其实就是加了一个@EnableConfigServer
注解
现在启动这个配置中心,访问 http://localhost:9100/product-service.yml 可以看到从码云上拉取的yml 配置文件
修改服务
加入依赖
在pom 文件中引入config-client
的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
修改application.yml
首先将application.yml
改成 bootstrap.yml
这一步很重要,不然服务启动的时候还是会从本地去拿数据
在这个 bootstrap.yml
文件中写入如下代码
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
#server:
# port: 8771
spring:
application:
name: product-service
#指定从哪个配置中心拿
cloud:
config:
discovery:
#这个id就是上面的配置中心的name
service-id: CONFIG-SERVER
enabled: true
# label 是版本建议用这个
#label:
#profile:
这里还有几个配置
label 和 profile
label 是分支 比如你新建一个git分支dev 然后在再里面写不同环境下的yml文件
profile 是后缀 比如product-service-dev.yml,你要访问这个可以写 dev
建议是用lable
举一个例子,比如现在用profile
仓库里有product-service-dev.yml
product-service-dev2.yml
两个文件
程序去product-service-dev.yml
获取一个参数 port 的数据,如果碰巧product-service-dev.yml
没有这个参数,程序会继续去product-service-dev2.yml
找!!!这样你可能会遇到很多莫名其妙的问题,所以还是用lable吧,干净整洁。
全部配置完毕后启动项目,我可以尝试修改git
上的 port 端口,可以看到服务每次启动的时候都会根据git 上的端口来启动
消息总线
上一节我们实现了配置中心,但是这个还是不完善的,当修改了git仓库的一个参数的时候,必须重启整个服务才能重新 获取配置,那可不可以不重启的来获取这些数据呢,这就是消息总线了
消息总线,其实就是用消息队列,在git仓库的配置发生变化的时候,通知到我们的服务提供者去读取新的配置。
- 在服务提供者里加入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
- 在yml文件中加入配置
management:
endpoints:
web:
exposure:
include: "*"
- 最后在我们要动态更新的类上方加上 @RefreshScope 注解
这里在product-service 的controller上加上注解
@RestController
@RequestMapping("/api/v1/product")
@RefreshScope
public class ProductController {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ProductService productService;
@Value("${sb}")
private String sb;
可以看到我们定义了一个新的变量,sb将会从git仓库读取他的配置,所以我们再在git 的 product-service.yml 文件中加一个配置sb
现在这个配置文件就是
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8777
spring:
application:
name: product-service
sb: xzh
我们将service启动后,改变sb的值,再用Post
形式访问
http://localhost:8777/actuator/bus-refresh
也就是就是这个product-service 下的这个路径 ,注意,一定要是post形式!!!
访问后,sb 的值就会重新从仓库拉取