第三篇主要以下内容:
1.常见的服务间调用方式
2.创建 order_service 订单服务,ribbon 实战订单服务调用商品服务
3.ribbon 源码解析
4.自定义负载均衡策略
5.微服务调用方式 feign 实战,订单服务调用商品服务
6.服务的自我保护机制
7.服务调用方式 ribbon 和 Feign 的选择
1. 常见的服务间调用方式
RPC(Remote Procedure Call Protocol):
远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
简言之,RPC使得程序能够像访问本地系统资源一样,去访问远端系统资源。
REST:
可以看着是http协议的一种直接应用,默认基于json作为传输格式,使用简单,学习成本低效率高,但是安全性较低。
准备前提:
由于添加 Ribbon 负载均衡,需要知道 order_server 具体调用的是哪个端口的 product_server,所以一下改造 product_service 项目。
把 ProductController 修改如下:
@Value("${server.port}")
private String port;
/**
* 获取全部商品列表,并且读取配置文件商品服务的端口
* @author 药岩
* @date 2020/3/22
*/
@RequestMapping(value = "find")
public Product findById(@RequestParam("id") int id){
Product product = productService.findById(id);
Product result = new Product();
//将product的属性拷贝到result
BeanUtils.copyProperties(product, result);
result.setName(result.getName() + "data from port " + port);
return result;
}
2. 创建 order_service 订单服务,ribbon 实战订单服务调用商品服务
2.1 填写包名、项目名
2.2 添加 Spring Web、服务发现 Eureka Client、Ribbon 依赖
2.3 在 application.yml 配置
server:
port: 8781
spring:
application:
name: order-service # 服务名
eureka:
client:
service-url: # 指明注册中心地址,往注册中心注册
defaultZone: http://localhost:8761/eureka/
2.4 新建 controller、service、serviceImpl、domain、config 包
2.5 在 config 包下,编写 Ribbon 配置类
/**
* ribbon 配置类
* @author 药岩
* @date 2020/3/22
*/
@Configuration
public class RibbonConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
2.6 在 domain 包下,编写 ProductOrder 订单实体类
/**
* 订单实体类
* @author 药岩
* @date 2020/3/22
*/
public class ProductOrder implements Serializable {
private int id;
/**
* 商品名称
*/
private String productName;
/**
* 订单号
*/
private String tradeNo;
/**
* 价格
*/
private int price;
private Date createTime;
/**
* 用户Id
*/
private int userId;
/**
* 用户名称
*/
private String userName;
此处 set、get 方法...
}
2.7 在 service 包下,编写业务层 ProductOrderService 接口
public interface ProductOrderService {
ProductOrder save(int userId, int productId);
}
2.8 在 Impl 包下,编写业务层 ProductOrderServiceImpl 实现类
@Service
public class ProductOrderServiceImpl implements ProductOrderService {
@Autowired
private RestTemplate restTemplate;
/**
* 远程调用商品服务 product-service 接口,获取数据
* @author 药岩
* @date 2020/3/22
*/
@Override
public ProductOrder save(int userId, int productId) {
//获取商品详情
Map<String, Object> productMap = restTemplate.getForObject("http://product-service:8771/api/v1/product/find?id=" + productId, Map.class);
System.out.println("LOGGER info:" + productMap);
ProductOrder productOrder = new ProductOrder();
productOrder.setUserId(userId);
productOrder.setId(productId);
productOrder.setTradeNo(UUID.randomUUID().toString());
productOrder.setProductName(productMap.get("name").toString());
productOrder.setPrice(Integer.parseInt(productMap.get("price").toString()));
return productOrder;
}
}
2.9 在 Controller 包下,编写控制层 OrderController
@RestController
@RequestMapping(value = "/api/v2/order")
public class OrderController {
@Autowired
private ProductOrderService productOrderService;
/**
* 根据商品id,远程调用商品服务 product-service 获取数据
* @author 药岩
* @date 2020/3/22
*/
@RequestMapping(value = "save")
public ProductOrder save(@RequestParam("user_id") int userId, @RequestParam("product_id") int productId){
return productOrderService.save(userId, productId);
}
}
现在启动 eureka_server、三个 product_service、order_service 服务。
浏览器访问 eureka_server 注册中心: http://localhost:8761/
可以看到有4个服务已经注册到注册中心了。
我们现在访问订单服务,通过订单服务调用商品服务
浏览器多次访问:http://localhost:8781/api/v2/order/save?user_id=1&product_id=3
控制台打印如下:可以发现 Ribbon 默认帮我们做了负载均衡策略
LOGGER info:{id=3, name=MacBook Prodata from port 8772, price=17999, store=14}
LOGGER info:{id=3, name=MacBook Prodata from port 8771, price=17999, store=14}
LOGGER info:{id=3, name=MacBook Prodata from port 8773, price=17999, store=14}
LOGGER info:{id=3, name=MacBook Prodata from port 8772, price=17999, store=14}
LOGGER info:{id=3, name=MacBook Prodata from port 8772, price=17999, store=14}
浏览器多次访问查看:
第二种调用方法:不编写 RibbonConfig 配置类
@Service
public class ProductOrderServiceImpl implements ProductOrderService {
@Autowired
private LoadBalancerClient loadBalancerClient;
/**
* 这种方法是采用注入 LoadBalancerClient 来实现的
* @author 药岩
* @date 2020/3/22
*/
@Override
public ProductOrder save(int userId, int productId) {
//指定调用的商品服务名
ServiceInstance serviceInstance =loadBalancerClient.choose("product-service");
//编写 URL,%s 是格式化
String url = String.format("http://%s:%s/api/v1/product/find?id=" + productId, serviceInstance.getHost(), serviceInstance.getPort());
RestTemplate restTemplate = new RestTemplate();
//获取商品详情
Map<String, Object> productMap = restTemplate.getForObject(url, Map.class);
System.out.println("LOGGER info:" + productMap);
ProductOrder productOrder = new ProductOrder();
productOrder.setUserId(userId);
productOrder.setId(productId);
productOrder.setTradeNo(UUID.randomUUID().toString());
productOrder.setProductName(productMap.get("name").toString());
productOrder.setPrice(Integer.parseInt(productMap.get("price").toString()));
return productOrder;
}
}
3. ribbon 源码解析:
3.1 在 @LoadBalanced 注解中,可以知道 RestTemplate 中其实是配置了 LoadBalancerClient 均衡器。
3.2 进入 LoadBalancerClient 发现 它有一个实现类
关系图
3.3 在 RibbonLoadBalancerClient 类中有一个 choose 方法,上面我们调用过
3.4 这里把 product 传入了进来,我们进入 chooseServer 方法
3.5 查看 chooseServer 的实现 BaseLoadBalancer类的 chooseServer 方法
3.6 进入 rule
3.7 可以发现默认为轮询策略
给 rule 赋值
3.8 获取注册中心的 product 服务列表
3.9 通过 RoundRobinRule 轮询策略,选择了端口为 8773 的服务
最后,返回给 restTemplate 调用。
4. 自定义负载均衡策略
主要的负载均衡算法:
RoundRobinRule:轮询策略。Ribbon默认采用的策略。若经过一轮轮询没有找到可用的provider,其最多轮询 10 轮。若最终还没有找到,则返回 null。
RandomRule: 随机策略,从所有可用的 provider 中随机选择一个。
RetryRule: 重试策略。先按照 RoundRobinRule 策略获取 provider,若获取失败,则在指定的时限内重试。默认的时限为 500 毫秒。
4.1 在配置文件 application.yml 里配置
# 自定义负载均衡策略
product-service: # 被调方的服务名
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 随机
4.2 debug 调试可以看出 rule 为随机策略。
4.3 启动项目,打开浏览器访问:
查看控制台如下:
LOGGER info:{id=3, name=MacBook Prodata from port 8772, price=17999, store=14}
LOGGER info:{id=3, name=MacBook Prodata from port 8771, price=17999, store=14}
LOGGER info:{id=3, name=MacBook Prodata from port 8773, price=17999, store=14}
LOGGER info:{id=3, name=MacBook Prodata from port 8771, price=17999, store=14}
LOGGER info:{id=3, name=MacBook Prodata from port 8772, price=17999, store=14}
5. 微服务调用方式 feign 实战,订单服务调用商品服务
5.1 添加 feign 依赖
<!-- feign 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
5.2 添加 @EnableFeignClients 开启 feign 客户端
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
5.3 编写 feign 接口 ProductClient
// feign 客户端,指定商品服务名
@FeignClient(name = "product-service")
public interface ProductClient {
/**
* 这里指定调用product-service商品服务的URL接口
* @author 药岩
* @date 2020/3/22
*/
@GetMapping(value = "/api/v1/product/find")
String findById(@RequestParam(value = "id") int id);
}
5.4 编写一个 Json 工具类 JsonUtils
/**
* Json转换工具类
* @author 药岩
* @date 2020/3/22
*/
public class JsonUtils {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public static JsonNode strJsonNode(String str){
try {
return OBJECT_MAPPER.readTree(str);
} catch (JsonProcessingException e) {
return null;
}
}
}
5.5 改造 ProductOrderServiceImpl 实现类
@Service
public class ProductOrderServiceImpl implements ProductOrderService {
@Autowired
private ProductClient productClient;
/**
* 采用 feign 的方式调用商品服务
* @author 药岩
* @date 2020/3/22
*/
@Override
public ProductOrder save(int userId, int productId) {
//直接调用 product-service 商品服务的接口
String json = productClient.findById(productId);
JsonNode jsonNode = JsonUtils.strJsonNode(json);
ProductOrder productOrder = new ProductOrder();
productOrder.setUserId(userId);
productOrder.setId(productId);
productOrder.setTradeNo(UUID.randomUUID().toString());
productOrder.setProductName(jsonNode.get("name").toString());
productOrder.setPrice(Integer.parseInt(jsonNode.get("price").toString()));
return productOrder;
}
}
5.6 启动 order-service 订单服务,访问浏览器:http://localhost:8781/api/v2/order/save?user_id=1&product_id=3
控制台打印日志:
LOGGER info:{"id":3,"name":"MacBook Prodata from port 8772","price":17999,"store":14}
LOGGER info:{"id":3,"name":"MacBook Prodata from port 8771","price":17999,"store":14}
LOGGER info:{"id":3,"name":"MacBook Prodata from port 8771","price":17999,"store":14}
LOGGER info:{"id":3,"name":"MacBook Prodata from port 8771","price":17999,"store":14}
LOGGER info:{"id":3,"name":"MacBook Prodata from port 8772","price":17999,"store":14}
可以看出 feign 里面包含了 ribbon 的负载均衡策略,源码中 feign 就是调用用 ribbon 来做的负载均衡。
6. 服务的自我保护机制
如果 order-service 订单服务调用 product-service 商品服务超过默认响应时间时,product-service 会抛一个超时的异常。
模拟接口响应慢:
在 product-service 的 controller 服务中,改造一下 findById 接口。
@RequestMapping(value = "find")
public Product findById(@RequestParam("id") int id){
try {
//睡眠1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
Product product = productService.findById(id);
Product result = new Product();
//将product的属性拷贝到result
BeanUtils.copyProperties(product, result);
result.setName(result.getName() + "data from port " + port);
return result;
}
再次访问浏览器:http://localhost:8781/api/v2/order/save?user_id=1&product_id=3
控制台:Read timed out
application.yml 自定义超时时间
#自定义超时时间
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 2000 #设置2秒
访问:http://localhost:8781/api/v2/order/save?user_id=1&product_id=3
正常返回了数据。
但是有些小伙伴会发现源码里面的默认时间就是60秒,可是为什么睡眠一秒却会报错呢?
源码:
原因:由于hystrix默认时间是1秒超时。
7. 服务调用方式 ribbon 和 Feign 的选择
建议选择 feign
- 默认集成 ribbon
- 写起来思路清晰方便
- 采用注解方式配置,配置熔断等方式方便