Spring Cloud 核心组件——注册中心
1. 什么是微服务的注册中心
注册中心:服务管理,核心是有个服务注册表,心跳机制动态维护。
为什么要用?
微服务应用和机器越来越多,调用方需要知道接口的网络地址,如果靠配置文件的方式去控制网络地址,对于动态新增机器,维护带来很大问题。
主流的注册中心:Zookeeper、Eureka、Consul、ETCD 等。
服务提供者 Provider:启动的时候向注册中心上报自己的网络信息。
服务消费者 Consumer:启动的时候向注册中心上报自己的网络信息,拉取 Provider 的相关网络信息。
2. 分布式应用知识CAP理论知识
CAP定理:指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition Tolerance(分区容错性),三者不可同时获得。
一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(所有节点在同一时间的数据完全一致,越多节点,数据同步越耗时)
可用性(A):负载过大后,集群整体是否还能响应客户端的读写请求。(服务一直可用,而且是正常响应时间)
分区容错性(P):分区容忍性,就是高可用性,一个节点崩了,并不影响其它的节点。(100个节点,挂了几个,不影响服务,越多机器越好)
CAP理论就是说在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的。所以我们只能在一致性和可用性之间进行权衡。
原因:
CA 满足的情况下,P 不能满足的原因:数据同步(C)需要时间,也要正常的时间内响应(A),那么机器数量就要少,所以P就不满足。
CP 满足的情况下,A 不能满足的原因:数据同步(C)需要时间,,机器数量也多(P),但是同步数据需要时间,所以不能再正常时间内响应,所以A就不满足。
AP 满足的情况下,C不能满足的原因:机器数量也多(P),正常的时间内响应(A),那么数据就不能及时同步到其他节点,所以C不满足。
注册中心选择:
Zookeeper:CP 设计,保证了一致性,集群搭建的时候,某个节点失效,则会进行选举行的 leader,或者半数以上节点不可用,则无法提供服务,因此可用性没法满足。
Eureka:AP 原则,无主从节点,一个节点挂了,自动切换其他节点可以使用,去中心化。
结论:
分布式系统中P,肯定要满足,所以只能在 C 和 A 中二选一。没有最好的选择,最好的选择是根据业务场景来进行架构设计,如果要求一致性,则选择 Zookeeper,如金融行业;如果要去可用性,则 Eureka,如电商系统。
Eureka 原理图:
Spring Cloud 体系官方地址:http://projects.spring.io/spring-cloud/
参考:
https://www.jianshu.com/p/d32ae141f680
https://blog.csdn.net/zjcjava/article/details/78608892
3. 使用 IDEA 搭建 Eureka 服务中心 Server 端并启动
第一步:创建项目
和创建普通的 Spring Boot 项目是一样的,只是需要选择下图所示依赖
第二步:添加注解 @EnableEurekaServer
@SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
第三步:增加配置 application.yml(其实可以使用 application.properties,但官网上使用 yml,这里我们照抄官网)
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
第四步:访问注册中心页面
使用 http://localhost:8761/ 访问注册中心页面,这个地址按照配置文件中的来,这里注意下,按照配置文件说的,访问地址为:http://localhost:8761/eureka/
但不同版本,可能不一样,我所使用的版本是 Greenwich,它就不能添加 /eureka/ 否则会报 404 错误
Eureka 管理后台出现一串红色字体:EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.
这个是警告,说明有服务上线率低。
关闭检查方法:Eureka 服务端配置文件加入
server: enable-self-preservation: false
注意:自我保护模式禁止关闭,默认是开启状态 true
4. 创建商品服务,并将服务注册到注册中心
第一步:创建一个 Spring Boot 应用,增加服务注册和发现依赖
第二步:模拟商品信息,存储在内存中
第三步:开发商品列表接口,商品详情接口
第四步:配置文件加入注册中心地址
server: port: 8771 #指定注册中心地址 eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/ #服务的名称 spring: application: name: product-service
我们可以用当前项目启动多个实例,参考教程:https://blog.csdn.net/zhou520yue520/article/details/81167841
启动后,我们可以登录下访问注册中心页面,看到如下效果:
为什么只加一个注册中心地址,就可以注册?
官网解释:By having spring-cloud-starter-netflix-eureka-client on the classpath, your application automatically registers with the Eureka Server.(也就是说,这样 Jar 包在类路径上,就可以自动识别)
5.常用的服务间的调用方式
RPC:远程过程调用,像调用本地服务(方法)一样调用服务器的服务。支持同步、异步调用。客户端和服务器之间建立 TCP 连接,可以一次建立一个,也可以多个调用复用一次链接。PRC 数据包小。
Rest:Http 请求,支持多种协议和功能。开发方便成本低。Http 数据包大。类似 HttpClient,URLConnection。
6.订单服务调用商品服务获取商品信息
第一步:创建 order_service 项目
注意:调用方需要引入 Ribbon 依赖
第二步:使用 Ribbon(类似 HTTPClient,URLConnection)
启动类增加注解
@Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }
第三步:开发伪下单接口
第四步:根据名称进行调用商品,获取商品详情
注意:红字标识的就是上面商品服务的 spring.application.name
@Service public class ProductOrderServiceImpl implements ProductOrderService { @Autowired private RestTemplate restTemplate; @Override public ProductOrder save(int userId, int productId) { Object obj = restTemplate.getForObject("http://product-service/api/v1/product/find?id="+productId, Object.class); System.out.println(obj); ProductOrder productOrder = new ProductOrder(); productOrder.setCreateTime(new Date()); productOrder.setUserId(userId); productOrder.setTradeNo(UUID.randomUUID().toString()); return productOrder; } }
当我们用请求多次访问时,可以从控制台看到端口号的不同,说明这是从不同端口的应用返回的数据
商品服务的 Controller 如下:
@RestController @RequestMapping("/api/v1/product") public class ProductController { @Value("${server.port}") private String port; @Autowired private ProductService productService; /** * 获取所有商品列表 * @return */ @RequestMapping("list") public Object list(){ return productService.listProduct(); } /** * 根据id查找商品详情 * @param id * @return */ @RequestMapping("find") public Object findById(int id){ Product product = productService.findById(id); Product result = new Product(); BeanUtils.copyProperties(product,result); result.setName( result.getName() + " data from port="+port ); return result; } }
我们还可以通过另一种调用方式进行调用
//调用方式二 ServiceInstance instance = loadBalancer.choose("product-service"); String url = String.format("http://%s:%s/api/v1/product/find?id="+productId, instance.getHost(),instance.getPort()); RestTemplate restTemplate = new RestTemplate(); Object obj = restTemplate.getForObject(url, Object.class); //Map<String,Object> productMap = restTemplate.getForObject(url, Map.class);
调用原理:
1)首先从注册中心获取 Provider 的列表
2)通过一定的策略选择其中一个节点
3)再返回给 restTemplate 调用
我们也可以自定义负载均衡策略,官网说明:https://cloud.spring.io/spring-cloud-static/spring-cloud-netflix/2.2.0.M3/reference/html/#customizing-the-ribbon-client-by-setting-properties
server:
port: 8781
#指定注册中心地址
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
#服务的名称
spring:
application:
name: order-service
#自定义负载均衡策略
product-service:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
策略选择:
1)如果每个机器配置一样,则建议不修改策略 (推荐)
2)如果部分机器配置强,则可以改为 WeightedResponseTimeRule
7.使用 Feign 改造订单服务
Feign: 伪 RPC 客户端(本质还是用http)
官方文档: https://cloud.spring.io/spring-cloud-openfeign/
第一步:加入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
第二步:启动类增加 @EnableFeignClients
@SpringBootApplication @EnableFeignClients public class OrderServiceApplication { public static void main(String[] args) { SpringApplication.run(OrderServiceApplication.class, args); } }
第三步:增加一个接口并使用注解 @FeignClient(name="product-service")
package com.jwen.order_service.service; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; /** * 商品服务客户端 */ @FeignClient(name = "product-service") public interface ProductClient { @GetMapping("/api/v1/product/find") String findById(@RequestParam(value = "id") int id); }
注意点:
1)服务名和 Http 方法必须对应
2)使用 RequestBody,应该使用 @PostMapping
3)多个参数的时候,通过 @RequestParam(value = "id") int id 方式调用
第四步:更改调用方式编码
package com.jwen.order_service.service.impl; import com.fasterxml.jackson.databind.JsonNode; import com.jwen.order_service.domain.ProductOrder; import com.jwen.order_service.service.ProductClient; import com.jwen.order_service.service.ProductOrderService; import com.jwen.order_service.utils.JsonUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; import java.util.UUID; @Service public class ProductOrderServiceImpl implements ProductOrderService { @Autowired private ProductClient productClient; @Override public ProductOrder save(int userId, int productId) { String response = productClient.findById(productId); JsonNode jsonNode = JsonUtils.str2JsonNode(response); ProductOrder productOrder = new ProductOrder(); productOrder.setCreateTime(new Date()); productOrder.setUserId(userId); productOrder.setTradeNo(UUID.randomUUID().toString()); productOrder.setProductName(jsonNode.get("name").toString()); productOrder.setPrice(Integer.parseInt(jsonNode.get("price").toString())); return productOrder; } }
JsonUtils 工具类的代码
package com.jwen.order_service.utils; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; /** * json工具类 */ public class JsonUtils { private static final ObjectMapper objectMappper = new ObjectMapper(); /** * json字符串转JsonNode对象的方法 */ public static JsonNode str2JsonNode(String str){ try { return objectMappper.readTree(str); } catch (IOException e) { return null; } } }
Ribbon 和 Feign 两个之间,应该选择 Feign。Feign 默认集成了 Ribbon。写起来更加思路清晰和方便。采用注解方式进行配置,配置熔断等方式方便
超时配置
#修改调用超时时间
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
模拟接口响应慢,线程睡眠新的方式
try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }