IDEA导入项目
1 新建空工程
2 解压 01 到 06 到工程目录
3 IDEA 中,按两下shift,搜索 add maven projects
4 选择6个项目的pom.xml进行导入(按住ctrl多选)
四. Spring Cloud Ribbon
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。
Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。因为微服务间的调用,API网关的请求转发等内容,实际上都是通过Ribbon来实现的,包括后续将要介绍的Feign,它也是基于Ribbon实现的工具。所以,对Spring Cloud Ribbon的理解和使用,对于我们使用Spring Cloud来构建微服务非常重要。
Ribbon 提供了负载均衡
和重试
功能, 它底层是使用 RestTemplate
进行 Rest API 调用。
1 RestTemplate
RestTemplate 是StringBoot提供的,Spring Cloud使用的一个Rest远程调用
工具。
常用方法:
getForObject("url",转换类型,提交的参数)
– 执行get请求postForObject("url",协议体数据,转换类型)
--执行post请求
单独使用RestTemplate来执行远程调用
之前的系统结构是浏览器直接访问后台服务,现在我们通过一个demo演示项目来演示Spring Cloud远程调用。
- 1) 新建Springboot项目–sp06-ribbon
- 2) 编辑pom.xml文件,添加 sp01-commons 依赖
<dependency>
<groupId>cn.tedu</groupId>
<artifactId>sp01-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 3) 编辑application.yml
spring:
application:
name: ribbon # 指定向eureka注册时的名字
server:
port: 3001
eureka:
client:
service-url:
defaultZone: http://eureka1:2001/eureka,http://eureka2:2002/eureka
- 4) 编辑主程序Sp06RibbonApplication
创建 RestTemplate
实例,添加@EnableDiscoveryClient注解
package cn.tedu.sp06;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableDiscoveryClient
@SpringBootApplication
public class Sp06RibbonApplication {
public static void main(String[] args) {
SpringApplication.run(Sp06RibbonApplication.class, args);
}
//创建 RestTemplate 实例,并交给 spring 容器管理
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
- 5)编辑RibbonController
package cn.tedu.sp06.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import cn.tedu.sp01.pojo.Item;
import cn.tedu.sp01.pojo.Order;
import cn.tedu.sp01.pojo.User;
import cn.tedu.web.util.JsonResult;
@RestController
public class RibbonController {
@Autowired
private RestTemplate rt;
@GetMapping("/item-service/{orderId}")
public JsonResult<List<Item>> getItems(@PathVariable String orderId) {
//向指定微服务地址发送 get 请求,并获得该服务的返回结果
//{1} resttemplate工具定义的占位符格式,用 orderId 填充
return rt.getForObject("http://localhost:8001/{1}", JsonResult.class, orderId);
}
@PostMapping("/item-service/decreaseNumber")
public JsonResult decreaseNumber(@RequestBody List<Item> items) {
//发送 post 请求
return rt.postForObject("http://localhost:8001/decreaseNumber", items, JsonResult.class);
}
/
@GetMapping("/user-service/{userId}")
public JsonResult<User> getUser(@PathVariable Integer userId) {
return rt.getForObject("http://localhost:8101/{1}", JsonResult.class, userId);
}
@GetMapping("/user-service/{userId}/score")
public JsonResult addScore(
@PathVariable Integer userId, Integer score) {
return rt.getForObject("http://localhost:8101/{1}/score?score={2}", JsonResult.class, userId, score);
}
/
@GetMapping("/order-service/{orderId}")
public JsonResult<Order> getOrder(@PathVariable String orderId) {
return rt.getForObject("http://localhost:8201/{1}", JsonResult.class, orderId);
}
@GetMapping("/order-service")
public JsonResult addOrder() {
return rt.getForObject("http://localhost:8201/", JsonResult.class);
}
}
- 6) 启动sp06服务,访问测试
注册表信息:
http://localhost:3001/item-service/35
以上响应结果是xml格式,这是因为eureka-client的jackson-dataformat-xml
依赖项将响应的数据自动转成了xml格式,需要排除掉这个依赖项。
分别在sp02,sp03,sp04,sp06项目的 spring-cloud-starter-netflix-eureka-client 依赖中排除 jackson.dataformat-xml:
<exclusions>
<exclusion>
<artifactId>jackson-dataformat-xml</artifactId>
<groupId>com.fasterxml.jackson.dataformat</groupId>
</exclusion>
</exclusions>
其它测试:
http://localhost:3001/item-service/decreaseNumber
使用postman,POST发送以下格式数据:
[{"id":1, "name":"abc", "number":23},{"id":2, "name":"def", "number":11}]
http://localhost:3001/user-service/7
http://localhost:3001/user-service/7/score?score=100
http://localhost:3001/order-service/123abc
http://localhost:3001/order-service/
2 Ribbon 负载均衡和重试
Ribbon 的作用,解决的问题:
负载均衡
(微服务系统必须功能)
访问压力可以分散到多台服务器。
重试
(不是必须功能)
当第一次请求失败或等待超时,可以自动发起重试请求。
2.1 Ribbon负载均衡
1) 添加ribbon依赖
在 eureka-client 中已经包含。
2)RestTemplate 设置 @LoadBalanced
@LoadBalanced
负载均衡注解,会对 RestTemplate 实例进行封装,创建动态代理对象,并切入(AOP)负载均衡代码,把请求分发到集群中的服务器。对 RestTemplate功能进行增强。
package cn.tedu.sp06;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableDiscoveryClient
@SpringBootApplication
public class Sp06RibbonApplication {
public static void main(String[] args) {
SpringApplication.run(Sp06RibbonApplication.class, args);
}
//创建 RestTemplate 实例,并交给 spring 容器管理
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
3)访问路径设置为服务id
在 RibbonController 中,请求的后台服务路径,改成 service-id
,根据eureka地址表,可以获取这个服务的集群主机地址列表。
package cn.tedu.sp06.controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import cn.tedu.sp01.pojo.Item;
import cn.tedu.sp01.pojo.Order;
import cn.tedu.sp01.pojo.User;
import cn.tedu.web.util.JsonResult;
@RestController
public class RibbonController {
@Autowired
private RestTemplate rt;
@GetMapping("/item-service/{orderId}")
public JsonResult<List<Item>> getItems(@PathVariable String orderId) {
//这里服务器路径用 service-id 代替,ribbon 会向服务的多台集群服务器分发请求
//从eureka可以得到"item-service"对应的主机地址列表:localhost:8001,localhost:8002
return rt.getForObject("http://item-service/{1}", JsonResult.class, orderId);
}
@PostMapping("/item-service/decreaseNumber")
public JsonResult decreaseNumber(@RequestBody List<Item> items) {
return rt.postForObject("http://item-service/decreaseNumber", items, JsonResult.class);
}
/
@GetMapping("/user-service/{userId}")
public JsonResult<User> getUser(@PathVariable Integer userId) {
return rt.getForObject("http://user-service/{1}", JsonResult.class, userId);
}
@GetMapping("/user-service/{userId}/score")
public JsonResult addScore(
@PathVariable Integer userId, Integer score) {
return rt.getForObject("http://user-service/{1}/score?score={2}", JsonResult.class, userId, score);
}
/
@GetMapping("/order-service/{orderId}")
public JsonResult<Order> getOrder(@PathVariable String orderId) {
return rt.getForObject("http://order-service/{1}", JsonResult.class, orderId);
}
@GetMapping("/order-service")
public JsonResult addOrder() {
return rt.getForObject("http://order-service/", JsonResult.class);
}
}
4)访问测试负载均衡
访问测试,ribbon 会把请求分发到 8001 和 8002 两个服务端口上.
http://localhost:3001/item-service/34添加链接描述
2.2 Ribbon重试
重试是一种容错
机制。
当调用后台服务失败,可以自动重试,如果重试成功,可以向客户端正常返回结果。
1)添加 spring-retry 依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
2)设置重试参数:
MaxAutoRetries
- 单台服务器的重试次数MaxAutoRetriesNextServer
- 更换服务器的次数OkToRetryOnAllOperations
- 是否对所有类型请求都重试,默认只对 get 重试;如果要对Post等所有请求重试,设置成 true。
注意:对post请求重试,需要做好 幂等性 控制,因为重复操作可能引起不一致问题。
下面两个超时设置不能在 yml 中配置,需要在 java 代码中设置:
ConnectTimeout
- 建立连接超时时间ReadTimeout
- 已建立连接并发送了请求,等待响应的超时时间
application.yml 配置 ribbon 重试
spring:
application:
name: ribbon # 指定向eureka注册时的名字
server:
port: 3001
eureka:
client:
service-url:
defaultZone: http://eureka1:2001/eureka,http://eureka2:2002/eureka
# ribbon 重试配置
ribbon:
# 没有提示,而且有黄色警告
MaxAutoRetriesNextServer: 2
MaxAutoRetries: 1
OkToRetryOnAllOperations: true
主程序设置 RestTemplate 的请求工厂的超时属性
package cn.tedu.sp06;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
@EnableDiscoveryClient
@SpringBootApplication
public class Sp06RibbonApplication {
public static void main(String[] args) {
SpringApplication.run(Sp06RibbonApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
//RestTemplate 中默认的 Factory 实例中,两个超时属性默认是 -1,
//未启用超时,也不会触发重试
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(1000);
factory.setReadTimeout(1000);
return new RestTemplate(factory);
}
}
3)访问测试
将item-service 的 ItemController 添加延迟代码,以便测试 ribbon 的重试机制
package cn.tedu.sp02.item.controller;
import java.util.List;
import java.util.Random;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import cn.tedu.sp01.pojo.Item;
import cn.tedu.sp01.service.ItemService;
import cn.tedu.web.util.JsonResult;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestController
public class ItemController {
@Autowired
private ItemService itemService;
@Value("${server.port}")
private int port;
@GetMapping("/{orderId}")
public JsonResult<List<Item>> getItems(@PathVariable String orderId) throws InterruptedException {
log.info("server.port="+port+", orderId="+orderId);
if(Math.random()<0.6){
// 60%概率会执行延迟代码,设置随机延迟
int t = new Random().nextInt(5000);
log.info("item-service-"+port+"-暂停"+t);
Thread.sleep(t);
}
List<Item> items = itemService.getItems(orderId);
return JsonResult.ok(items).msg("port="+port);
}
@PostMapping("/decreaseNumber")
public JsonResult decreaseNumber(@RequestBody List<Item> items) {
itemService.decreaseNumber(items);
return JsonResult.ok().msg("减少商品库存成功");
}
}
访问测试 ribbon 重试机制
通过 ribbon 访问 item-service,当超时,ribbon 会重试请求集群中其他服务器
http://localhost:3001/item-service/35