第三篇:服务消费者 Ribbon 和 Feign 实战

第三篇主要以下内容:

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
  • 写起来思路清晰方便
  • 采用注解方式配置,配置熔断等方式方便
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值