Java微服务-第一节 注册中心Eureka、微服务调用Ribbon和Feign
微服务简介
概念
- 简单来说,微服务架构风格[1]是一种将一个单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,服务间通信采用轻量级通信机制(通常用HTTP资源API)。
- 这些服务围绕业务能力构建并且可通过全自动部署机制独立部署。
- 这些服务共用一个最小型的集中式的管理,服务可用不同的语言开发,使用不同的数据存储技术。
资源说明
- 官网地址:https://spring.io/projects/spring-cloud
- 中文地址:https://springcloud.cc/
- 中文社区:http://springcloud.cn/
- SpringBoot和SpringCloud有啥关系?
- SpringBoot专注于快速、方便的开发单个微服务个体,SpringCloud关注全局的服务治理框架。
- 它将SpringBoot开发的一个个单体微服务整合并管理起来,为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务
- SpringBoot可以离开SpringCloud独立使用开发项目,但是SpringCloud离不开SpringBoot,属于依赖的关系.
- 版本说明
- 因为Spring Cloud不同其他独立项目,它拥有很多子项目的大项目。所以它是的版本是 版本名+版本号 (如Angel.SR6)。
- 版本名:是伦敦的地铁名
- 版本号:SR(Service Releases)是固定的,后面会有一个递增的数字。 所以 Brixton.SR5就是Brixton的第5个Release版
- 本次学习使用的版本为: Hoxton SR6,相关API文档: https://cloud.spring.io/spring-cloud-static/Hoxton.SR5/reference/html/
微服务要解决的问题
- 引入网络,网络具有不确定性
- 对于不同服务之间的数据一致性(分布式事务)
- 对系统问题的日志跟踪-日志跟踪方案ELK
- 增加系统的复杂度
微服务的解决方案
SpringCloud、SpringCloud Alibaba、阿里巴巴Dubbo/HSF、京东JSF、新浪微博Motan、当当网Dubbox
注册中心Eureka
- 场景设置:
- 商品服务: 查询商品列表、查询商品详情
- 订单服务: 创建订单
- 案例功能: 前台调用订单服务,订单服务远程调用商品服务获取商品详情信息,基于该商品信息创建订单.
- 作用:提供服务注册与发现功能,对服务的url地址进行统一管理.
- 对于服务提供者Provider的作用:启动的时候向注册中心上报自己的网络信息
- 对于服务消费者Consumer的作用:启动的时候向注册中心上报自己的网络信息,拉取provider的相关网络信息
- 主流的注册中心(这四款注册中心都支持和SpringCloud集成):zookeeper、Eureka、consul、etcd、nacos
- Spring-Cloud Euraka介绍:是SpringCloud集合中一个组件,它是对Euraka的集成,用于服务注册和发现。Eureka是Netflix中的一个开源框架(2.0版本以上已经闭源)。它和 zookeeper、Consul一样,都是用于服务注册管理的.
注册中心eureka-server搭建
- 使用Spring Initializr创建SpringBoot项目,然后Dependencies选择Spring Cloud Discovery->Eureka Server勾选,点击完成即可
-
启动类上添加
@EnableEurekaServer
注解@SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args) { SpringApplication.run(EurekaServerApplication.class, args); } }
-
修改application.properties为application.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
商品服务接口product-api搭建
- 使用Spring Initializr创建SpringBoot项目,然后Dependencies选择Developer Tools->Lombok勾选,点击完成即可
-
替换pom文件如下,并删除启动类ProductApiApplication
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.coderzhong.demo</groupId> <artifactId>product-api</artifactId> <version>0.0.1-SNAPSHOT</version> <name>product-api</name> <description>Demo project for Spring Boot</description> <url/> <licenses> <license/> </licenses> <developers> <developer/> </developers> <scm> <connection/> <developerConnection/> <tag/> <url/> </scm> <properties> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.SR6</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
-
新建Product类
package com.coderzhong.demo.domain; ... @Getter @Setter @NoArgsConstructor @AllArgsConstructor public class Product implements Serializable { private Long id;//商品id private String name;//商品名称 private int price;//商品价格 private int stock;//商品库存 }
商品服务product-server搭建
- 使用Spring Initializr创建SpringBoot项目,然后Dependencies选择Developer Tools->Lombok、Spring Cloud Discovery->Eureka Discovery Client、 Web->Spring Web
-
pom文件导入product-api依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.1.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.coderzhong.demo</groupId> <artifactId>product-server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>product-server</name> <description>Demo project for Spring Boot</description> <url/> <properties> <java.version>1.8</java.version> <spring-cloud.version>Hoxton.SR6</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>com.coderzhong.demo</groupId> <artifactId>product-api</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </path> </annotationProcessorPaths> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build>
-
把application.properties修改成application.yml,并添加配置信息
server: port: 8081 spring: application: name: product-server eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/
- 启动测试,会在Eureka注册中心控制台页面中看到product-server实例
- 启动多个实例,在注册中心管控台页面也可以看到.(在idea启动配置中添加-Dserver.port=8082参数,可以覆盖配置文件中的配置)
- Edit Configurations…->点击左边的工程“ProductServerApplication”->点击顶部“-”右边的复制图标,然后修改 Name为ProductServerApplication:8082
- 新增Environment下的VM options 参数为
-Dserver.port=8082
,然后启动该工程
-
添加mapper,service,controller类.
//ProductController package com.coderzhong.demo.productserver.web; @RestController @Slf4j public class ProductController { @Autowired private IProductService productService; @Value("${server.port}") private String port; @RequestMapping("/api/v1/product/find") public Product find(Long id) { // log.info("查找商品"); Product product = productService.get(id); Product result = new Product(); BeanUtils.copyProperties(product,result); result.setName(result.getName()+",data from "+port); System.out.println("进入....."+port); return result; } } //IProductService package com.coderzhong.demo.productserver.server; import com.coderzhong.demo.domain.Product; import java.util.List; public interface IProductService { List<Product> list(); Product get(Long id); } //ProductServiceImpl package com.coderzhong.demo.productserver.server.impl; @Service public class ProductServiceImpl implements IProductService { @Autowired private ProductMapper productMapper; @Override public List<Product> list() { return productMapper.list(); } @Override public Product get(Long id) { return productMapper.get(id); } } //ProductMapper package com.coderzhong.demo.productserver.mapper; @Component public class ProductMapper { private Map<Long, Product> productMap = new HashMap(); public ProductMapper(){ Product p1 = new Product(1L,"小米手机",2799,10); Product p2 = new Product(2L,"苹果手机",5799,20); Product p3 = new Product(3L,"华为手机",4699,10); Product p4 = new Product(4L,"三星手机",4799,10); Product p5 = new Product(5L,"OPPO手机",3799,10); productMap.put(p1.getId(),p1); productMap.put(p2.getId(),p2); productMap.put(p3.getId(),p3); productMap.put(p4.getId(),p4); productMap.put(p5.getId(),p5); } public List<Product> list(){ return new ArrayList<>(productMap.values()); } public Product get(Long id){ return productMap.get(id); } }
知识点:如何启动暂停多个工程?
- 方式一:控制台切换
- 当3个工程都启动之后,控制台会出现3个ProductServerApplication、ProductServerApplication:8082、EurekaServerApplication
- 控制台窗口左边会出现启动、暂停、关闭等按钮,可以控制每个工程的启停。
- 方式2:dashboard
- 点击idea顶部工具栏->View->Tools Windows->Services
- 点击”+“->Run Configuration Type->筛选Spring boot->点击确定
- 可以看到下面窗口新增一个Spring boot,展开之后可以看到所有的项目
- 可以一次性启动、暂停n个项目。
SpringCloud Eureka 自我保护机制
- Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 会将这些实例保护起来,让这些实例不会过期,但是在保护期内如果服务刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一个无效的服务实例,此时会调用失败,对于这个问题需要服务消费者端要有一些容错机制,如重试,断路器等。即注册中心发现某个节点心跳失败之后,如果15分钟小于85%,注册中心不会剔除该节点。
-
我们在单机测试的时候很容易满足心跳失败比例在 15 分钟之内低于 85%,这个时候就会触发 Eureka 的保护机制,一旦开启了保护机制,则服务注册中心维护的服务实例就不是那么准确了,此时我们可以使用eureka.server.enable-self-preservation=false来关闭保护机制,这样可以确保注册中心中不可用的实例被及时的剔除(不推荐)
application.yml中新增如下: server: enable-self-preservation: false
- Eureka客户端与服务器之间的通信
- Register(注册):Eureka客户端将关于运行实例的信息注册到Eureka服务器。注册发生在第一次心跳
- Renew(更新 / 续借):Eureka客户端需要更新最新注册信息(续借),通过每30秒发送一次心跳。更新通知是为了告诉Eureka服务器实例仍然存活。如果服务器在90秒内没有看到更新,它会将实例从注册表中删除。建议不要更改更新间隔,因为服务器使用该信息来确定客户机与服务器之间的通信是否存在广泛传播的问题
- Fetch Registry(抓取注册信息):Eureka客户端从服务器获取注册表信息并在本地缓存。之后,客户端使用这些信息来查找其他服务。通过在上一个获取周期和当前获取周期之间获取增量更新,这些信息会定期更新(每30秒更新一次)。获取的时候可能返回相同的实例。Eureka客户端自动处理重复信息。
- Cancel(取消):Eureka客户端在关机时向Eureka服务器发送一个取消请求。这将从服务器的实例注册表中删除实例,从而有效地将实例从流量中取出。
微服务调用方式Ribbon
订单服务order-server搭建
- 使用Spring Initializr创建SpringBoot项目,然后Dependencies选择Developer Tools->Lombok、Spring Cloud Discovery->Eureka Discovery Client、Spring Cloud Routing->Robbin(Maintenance)(废弃) 、 Web->Spring Web
- 注意: 从 Spring Cloud 2020.0.x 版本开始,Ribbon 已经被 Spring Cloud Netflix 项目废弃,并被 Spring Cloud LoadBalancer 替代
- pom文件导入product-api依赖
-
把application.properties修改成application.yml,并添加配置信息
server: port: 8090 spring: application: name: order-server eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka/
-
把创建订单的功能实现
//Order类 @Data public class Order implements Serializable { private String orderNo;//订单编号 private Date createTime;//下单时间 private String productName;//产品名称 private int productPrice;//产品价格 private Long userId;//用户id } //OrderService public interface OrderService { public Order save(Long userId, Long productId); } //OrderServiceImpl类 @Service public class OrderServiceImpl implements OrderService { //注入网络请求类 @Autowired private RestTemplate restTemLate; @Override public Order save(Long userId, Long productId) { //商品信息应该通过productId,从商品服务查询 //Product product = null;//远程获取 //使用http请求,方式:httpclient,RestTemplate,URLconnection //该方法无法访问多节点,只能固定写死 //Product product =restTemplate.getFor0bject( url: "http://localhost:8081/api/v1/product/find?id="+productId,Product.class);//远程获取 //使用Ribbon Product product =restTemLate.getForObject("http://product-server/api/v1/product/find?id="+productId,Product.class); Order order = new Order(); order.setOrderNo(UUID.randomUUID().toString().replace("-","")); order.setCreateTime(new Date()); order.setUserId(userId); order.setProductName(product.getName()); order.setProductPrice(product.getPrice()); System.out.println("执行保存订单操作"); return order; } } //OrderController类 @RestController @RequestMapping("/api/v1/orders") public class OrderController { @Autowired private OrderService orderService; @RequestMapping public Order save(Long userId,Long productId){ return orderService.save(userId,productId); } } //启动类 @springBootApplication public class OrderServerApplication { public static void main(string[] args){ SpringApplication.run(0rderserverApplication.class,args);} //容器中获取实例 @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } }
- 启动服务,浏览器输入:http://localhost:8090/api/v1/orders?productId=1&userId=2
使用Ribbon来实现远程调用
- 上面的代码中使用http直连的方式访问商品服务,直接绕过注册中心,但是存在缺点,无法访问商品服务的多个节点,只能固定访问一个节点。
- 使用Ribbon实现步骤
-
获取商品服务全部列表:替换http请求的域名为商品服务在注册中心注册的服务名称
http://product-server/api/v1/product/find
-
配置负载均衡:在RestTemplate的bean上贴上
@LoadBalanced
注解
-
- 负载均衡策略调整:在调用者(order-server)yml配置文件配置
- 注意: 服务的名称需要和代码中的服务名称一致,不然是修改不了负载均衡策略.
product-server: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
微服务调用方式Feign
- 问题:使用RestTemplate+Ribbon实现远程调用存在一个问题,请求的域名地址是裸露的、硬编码存在,容易被误修改。
- 使用Feign实现步骤
-
在product-api项目中添加openfeign依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
在product-api项目中添加ProductFeignApi接口
/* 1. 使用Feignclient标记为feign接口 并且name参数的值是提供服务的service-id 2. 使用 @RequestMapping规范请求的url地址 3. 使用@RequestParam("id") 确定请求的参数 */ @FeignClient(name = "product-server") public interface ProductFeignApi { @RequestMapping("/api/v1/product/find") Product find(@RequestParam("id") Long id); }
-
在product-server项目中添加ProductFeignApi的实现类(本质上就是个Controller),注意要把之前的ProductController删除掉
//ProductFeignClient @RestController public class ProductFeignClient implements ProductFeignApi { @Autowired private IProductService productService; @Value("${server.port}") private String port; @Override public Product find(Long id) { Product product = productService.get(id); Product result = new Product(); BeanUtils.copyProperties(product,result); result.setName(result.getName()+",data from "+port); return result; } }
-
在order-server项目中的启动类上贴上
@EnableFeignClients
注解//启动类 @springBootApplication //扫描@EnableFeignclients注解对应的包名下的所有接口,并且创建代理对象。 @EnableFeignClients(basePackages ="com.coderzhong.demo.feign") public class OrderServerApplication { public static void main(string[] args){ SpringApplication.run(0rderserverApplication.class,args);} //删除RestTemplate实例的注入 }
-
把之前RestTemplate的远程调用替换成Feign方式调用即可 (注意product-api和order-server中包名的问题.)
@Service public class OrderServiceImpl implements OrderService { //由于在启动类的时候已经扫描创建该接口对应的代理对象,所以这里可以直接在容器中找到。 @Autowired private ProductFeignApi productFeignApi; @0verride public Order save(Long userId, Long productId){ Product product = productFeignApi.find(productId);//远程获取 ... } }
-
- 超时时间设置
- 源码中默认options中配置的是6000毫秒,但是Feign默认加入了Hystrix,此时默认是1秒超时.我们可以通过修改配置,修改默认超时时间.
-
服务调用者order-server新增yml配置
feign: client: config: default: connectTimeout: 2000 readTimeout: 2000
-
超时重试次数设置
# 因为feign底层用的是ribbon,因此可以直接设置ribbon ribbon: MaxAutoRetries: 1 # 同一个节点,最大重试次数,不包括第一次 MaxAutoRetriesNextServer: 2 # 重试的时候可以选择其他节点的次数 ConnectTimeout: 2000 ReadTimeout: 2000