• SpringCloud Alibaba实战——服务治理:实现服务调用的负载均衡


    负载均衡

    在正式优化程序代码之前,我们先来看看什么是负载均衡。说得直白点,负载均衡就是将原本由一台服务器处理的请求根据一定的规则分担到多台服务器上进行处理。目前,大部分系统都实现了负载均衡的功能。

    负载均衡根据发生的位置,可以分为服务端负载均衡和客户端负载均衡。

    服务端负载均衡

    服务端负载均衡指的是在服务端处理负载均衡的逻辑,如下图所示。

    负载均衡在服务端进行处理,当客户端访问服务端的服务A时,首先访问到服务端的负载均衡器,由服务端的负载均衡器将客户端的请求均匀的分发到服务端部署的两个服务A上。

    客户端负载均衡

    客户端负载均衡指的是客户端处理负载均衡的逻辑,如下图所示。

    负载均衡逻辑在客户端一侧,客户端应用调用服务端的应用A时,在向服务端发送请求时,就已经经过负载均衡的逻辑处理,并直接向服务端的某个服务发送请求。

    启动多服务

    为了实现服务调用的负载均衡功能,我们在本地的IDEA环境中分别启动两个用户微服务进程和两个商品微服务进程。

    启动多个用户微服务

    这里,我们在IDEA开发环境中启动多个用户微服务,其实也就是在同一台机器(服务器)上启动多个用户微服务。启动用户微服务时,默认监听的端口为8060,主要由如下配置决定。

    1. server:
    2. port: 8060

    在同一台机器(服务器)上启动多个用户微服务时,只需要保证启动的多个用户微服务监听的端口号不同即可。

    IDEA中可以通过配置不同的端口号来启动多个相同的服务,如下所示,再配置一个用户微服务,使其端口号为8061。

    按照上图所示,在Name一栏输入UserStarter2,点击Main class一栏后面的弹出框按钮,选择用户微服务的启动类io.binghe.shop.UserStarter,最重要的就是在VM options一栏后面添加JVM启动参数-Dserver.port=8061,将新添加的用户微服务的监听端口设置为8061。

    配置好之后,在IDEA中分别启动端口为8060和8061的两个用户微服务,启动后打开Nacos的服务列表,如下所示。

    可以看到,在服务列表中出现了两个用户微服务的实例,说明两个用户微服务都启动成功了。

    启动多个商品微服务

    与用户微服务类似,在IDEA中再次配置一个端口号为8071的商品微服务,如下所示。

    接下来,分别启动端口为8070和8071的两个商品微服务,启动后查看Nacos的服务列表,如下所示。

    可以看到,端口为8070和8071的两个商品微服务,也成功启动啦。

    实现自定义负载均衡

    这里,我们通过修改订单微服务的代码来实现自定义负载均衡,由于在整个项目中,订单微服务作为客户端存在,由订单微服务调用用户微服务和商品微服务,所以,这里采用的是客户端负载均衡的模式。

    修改订单微服务代码

    此处实现的逻辑在订单微服务的io.binghe.shop.order.service.impl.OrderServiceV3Impl类中,并且在OrderServiceV3Impl类上会标注@Service("orderServiceV3")注解。订单微服务的代码结构如下所示。

    1. ├─shop-order
    2. │ │ pom.xml
    3. │ │ shop-order.iml
    4. │ │
    5. │ └─src
    6. │ └─main
    7. │ ├─java
    8. │ │ └─io
    9. │ │ └─binghe
    10. │ │ └─shop
    11. │ │ │ OrderStarter.java
    12. │ │ │
    13. │ │ └─order
    14. │ │ ├─config
    15. │ │ │ LoadBalanceConfig.java
    16. │ │ │
    17. │ │ ├─controller
    18. │ │ │ OrderController.java
    19. │ │ │
    20. │ │ ├─mapper
    21. │ │ │ OrderItemMapper.java
    22. │ │ │ OrderMapper.java
    23. │ │ │
    24. │ │ └─service
    25. │ │ │ OrderService.java
    26. │ │ │
    27. │ │ └─impl
    28. │ │ OrderServiceV1Impl.java
    29. │ │ OrderServiceV2Impl.java
    30. │ │ OrderServiceV3Impl.java
    31. │ │
    32. │ └─resources
    33. │ │ application.yml
    34. │ │
    35. │ └─mapper
    36. │ OrderItemMapper.xml
    37. │ OrderMapper.xml

    首先,在OrderServiceV3Impl类中修改getServiceUrl()方法,使其能够在多个服务地址中随机获取一个服务地址,而不总是获取第一个服务地址。

    修改前的代码如下所示。

    1. private String getServiceUrl(String serviceName){
    2. ServiceInstance serviceInstance = discoveryClient.getInstances(serviceName).get(0);
    3. return serviceInstance.getHost() + ":" + serviceInstance.getPort();
    4. }

    修改后的代码如下所示。

    1. private String getServiceUrl(String serviceName){
    2. List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
    3. int index = new Random().nextInt(instances.size());
    4. ServiceInstance serviceInstance = instances.get(index);
    5. String url = serviceInstance.getHost() + ":" + serviceInstance.getPort();
    6. log.info("负载均衡后的服务地址为:{}", url);
    7. return url;
    8. }

    在getServiceUrl()方法中,首先通过服务的名称在Nacos中获取到所有的服务实例列表,然后使用随机函数随机生成一个0到服务列表长度减1的整数,而这个随机生成的整数恰好在服务实例列表的下标范围内,然后以这个整数作为下标获取服务列表中的某个服务实例。从而以随机的方式实现了负载均衡,并从获取到的服务实例中获取到服务实例所在服务器的IP地址和端口号,拼接成IP:PORT的形式返回。

    接下来,将io.binghe.shop.order.controller.OrderController类中的OrderService修改为注入beanName为orderServiceV3的OrderService对象,如下所示。

    1. @Autowired
    2. @Qualifier(value = "orderServiceV3")
    3. private OrderService orderService;

    至此,订单微服务的代码修改完成。

    测试负载均衡效果

    启动订单微信服务,并在浏览器或其他测试工具中多次访问链接http://localhost:8080/order/submit_order?userId=1001&productId=1001&count=1,在订单微服务输出的日志信息中会存在如下所示的日志。

    1. 负载均衡后的服务地址为:192.168.0.27:8061
    2. 负载均衡后的服务地址为:192.168.0.27:8071
    3. 负载均衡后的服务地址为:192.168.0.27:8060
    4. 负载均衡后的服务地址为:192.168.0.27:8070
    5. 负载均衡后的服务地址为:192.168.0.27:8060
    6. 负载均衡后的服务地址为:192.168.0.27:8071
    7. 负载均衡后的服务地址为:192.168.0.27:8061
    8. 负载均衡后的服务地址为:192.168.0.27:8070

    其中,端口为8060和8061的微服务为用户微服务,端口为8070和8071的微服务为商品微服务。初步实现了订单微服务调用用户微服务和商品微服务的负载均衡。

    使用Ribbon实现负载均衡

    Ribbon是SpringCloud提供的一个能够实现负载均衡的组件,使用Ribbon能够轻松实现微服务之间调用的负载均衡。

    修改订单微服务代码

    在订单微服务中的io.binghe.shop.order.config.LoadBalanceConfig 类的 restTemplate()方法上添加@LoadBalanced注解,如下所示。

    1. @Bean
    2. @LoadBalanced
    3. public RestTemplate restTemplate(){
    4. return new RestTemplate();
    5. }

    接下来,实现的逻辑在订单微服务的io.binghe.shop.order.service.impl.OrderServiceV4Impl类中,并且在OrderServiceV4Impl类上会标注@Service("orderServiceV4")注解。订单微服务的代码结构如下所示。

    1. ├─shop-order
    2. │ │ pom.xml
    3. │ │ shop-order.iml
    4. │ │
    5. │ └─src
    6. │ └─main
    7. │ ├─java
    8. │ │ └─io
    9. │ │ └─binghe
    10. │ │ └─shop
    11. │ │ │ OrderStarter.java
    12. │ │ │
    13. │ │ └─order
    14. │ │ ├─config
    15. │ │ │ LoadBalanceConfig.java
    16. │ │ │
    17. │ │ ├─controller
    18. │ │ │ OrderController.java
    19. │ │ │
    20. │ │ ├─mapper
    21. │ │ │ OrderItemMapper.java
    22. │ │ │ OrderMapper.java
    23. │ │ │
    24. │ │ └─service
    25. │ │ │ OrderService.java
    26. │ │ │
    27. │ │ └─impl
    28. │ │ OrderServiceV1Impl.java
    29. │ │ OrderServiceV2Impl.java
    30. │ │ OrderServiceV3Impl.java
    31. │ │ OrderServiceV4Impl.java
    32. │ │
    33. │ └─resources
    34. │ │ application.yml
    35. │ │
    36. │ └─mapper
    37. │ OrderItemMapper.xml
    38. │ OrderMapper.xml

    在OrderServiceV4Impl类中删除如下代码。

    1. @Autowired
    2. private DiscoveryClient discoveryClient;
    3. private String getServiceUrl(String serviceName){
    4. List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
    5. int index = new Random().nextInt(instances.size());
    6. ServiceInstance serviceInstance = instances.get(index);
    7. String url = serviceInstance.getHost() + ":" + serviceInstance.getPort();
    8. log.info("负载均衡后的服务地址为:{}", url);
    9. return url;
    10. }

    在saveOrder()方法中删除如下两行代码。

    1. //从Nacos服务中获取用户服务与商品服务的地址
    2. String userUrl = this.getServiceUrl(userServer);
    3. String productUrl = this.getServiceUrl(productServer);

    修改通过restTemplate调用用户微服务和商品微服务的方法。修改前的代码如下所示。

    1. User user = restTemplate.getForObject("http://" + userUrl + "/user/get/" + orderParams.getUserId(), User.class);
    2. if (user == null){
    3. throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
    4. }
    5. Product product = restTemplate.getForObject("http://" + productUrl + "/product/get/" + orderParams.getProductId(), Product.class);
    6. if (product == null){
    7. throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
    8. }
    9. //#####################此处省略N行代码#########################
    10. Result<Integer> result = restTemplate.getForObject("http://" + productUrl + "/product/update_count/" + orderParams.getProductId() + "/" + orderParams.getCount(), Result.class);
    11. if (result.getCode() != HttpCode.SUCCESS){
    12. throw new RuntimeException("库存扣减失败");
    13. }

    修改后的代码如下所示。

    1. User user = restTemplate.getForObject("http://" + userServer + "/user/get/" + orderParams.getUserId(), User.class);
    2. if (user == null){
    3. throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
    4. }
    5. Product product = restTemplate.getForObject("http://" + productServer + "/product/get/" + orderParams.getProductId(), Product.class);
    6. if (product == null){
    7. throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
    8. }
    9. //#####################此处省略N行代码#########################
    10. Result<Integer> result = restTemplate.getForObject("http://" + productServer + "/product/update_count/" + orderParams.getProductId() + "/" + orderParams.getCount(), Result.class);
    11. if (result.getCode() != HttpCode.SUCCESS){
    12. throw new RuntimeException("库存扣减失败");
    13. }

    接下来,将io.binghe.shop.order.controller.OrderController类中的OrderService修改为注入beanName为orderServiceV4的OrderService对象,如下所示。

    1. @Autowired
    2. @Qualifier(value = "orderServiceV4")
    3. private OrderService orderService;

    至此,订单微服务的代码修改完成。

    测试负载均衡效果

    启动订单微服务,并在浏览器或其他测试工具中多次访问链接http://localhost:8080/order/submit_order?userId=1001&productId=1001&count=1,启动的每个用户微服务和商品微服务都会打印相关的日志,说明使用Ribbon实现了负载均衡功能。

    注意:这里就不粘贴测试时每个启动的微服务打印的日志了,小伙伴们可自行测试并演示效果。

    使用Fegin实现负载均衡

    Fegin是SpringCloud提供的一个HTTP客户端,但只是一个声明式的伪客户端,它能够使远程调用和本地调用一样简单。阿里巴巴开源的Nacos能够兼容Ribbon,而Fegin又集成了Ribbon,所以,使用Fegin也能够实现负载均衡。

    修改订单微服务代码

    (1)在订单微服务的pom.xml文件中添加Fegin相关的依赖,如下所示。

    1. <dependency>
    2. <groupId>org.springframework.cloud</groupId>
    3. <artifactId>spring-cloud-starter-openfeign</artifactId>
    4. </dependency>

    (2)在订单微服务的启动类io.binghe.shop.OrderStarter 上添加 @EnableFeignClients 注解,如下所示。

    1. /**
    2. * @author binghe
    3. * @version 1.0.0
    4. * @description 订单服务启动类
    5. */
    6. @SpringBootApplication
    7. @EnableTransactionManagement(proxyTargetClass = true)
    8. @MapperScan(value = { "io.binghe.shop.order.mapper" })
    9. @EnableDiscoveryClient
    10. @EnableFeignClients
    11. public class OrderStarter {
    12. public static void main(String[] args){
    13. SpringApplication.run(OrderStarter.class, args);
    14. }
    15. }

    (3)在订单微服务工程shop-order下新建io.binghe.shop.order.fegin包,并在io.binghe.shop.order.fegin下新建UserService接口,并在UserService接口上标注@FeignClient("server-user")注解,其中注解的value属性为用户微服务的服务名称。UserService接口用来远程调用用户微服务的接口,源码如下所示。

    1. /**
    2. * @author binghe (公众号:冰河技术)
    3. * @version 1.0.0
    4. * @description 调用用户微服务的接口
    5. */
    6. @FeignClient("server-user")
    7. public interface UserService {
    8. @GetMapping(value = "/user/get/{uid}")
    9. User getUser(@PathVariable("uid") Long uid);
    10. }

    (4)在io.binghe.shop.order.fegin下新建ProductService接口,并在ProductService接口上标注@FeignClient("server-product")注解,其中注解的value属性为商品微服务的服务名称。ProductService接口用来远程调用商品微服务的接口,源码如下所示。

    1. /**
    2. * @author binghe (公众号:冰河技术)
    3. * @version 1.0.0
    4. * @description 调用商品微服务的接口
    5. */
    6. @FeignClient("server-product")
    7. public interface ProductService {
    8. /**
    9. * 获取商品信息
    10. */
    11. @GetMapping(value = "/product/get/{pid}")
    12. Product getProduct(@PathVariable("pid") Long pid);
    13. /**
    14. * 更新库存数量
    15. */
    16. @GetMapping(value = "/product/update_count/{pid}/{count}")
    17. Result updateCount(@PathVariable("pid") Long pid, @PathVariable("count") Integer count);
    18. }

    (5)接下来,实现的逻辑在订单微服务的io.binghe.shop.order.service.impl.OrderServiceV5Impl类中,并且在OrderServiceV5Impl类上会标注@Service("orderServiceV5")注解。订单微服务的代码结构如下所示。

    1. ├─shop-order
    2. │ │ pom.xml
    3. │ │ shop-order.iml
    4. │ │
    5. │ ├─src
    6. │ │ └─main
    7. │ │ ├─java
    8. │ │ │ └─io
    9. │ │ │ └─binghe
    10. │ │ │ └─shop
    11. │ │ │ │ OrderStarter.java
    12. │ │ │ │
    13. │ │ │ └─order
    14. │ │ │ ├─config
    15. │ │ │ │ LoadBalanceConfig.java
    16. │ │ │ │
    17. │ │ │ ├─controller
    18. │ │ │ │ OrderController.java
    19. │ │ │ │
    20. │ │ │ ├─fegin
    21. │ │ │ │ ProductService.java
    22. │ │ │ │ UserService.java
    23. │ │ │ │
    24. │ │ │ ├─mapper
    25. │ │ │ │ OrderItemMapper.java
    26. │ │ │ │ OrderMapper.java
    27. │ │ │ │
    28. │ │ │ └─service
    29. │ │ │ │ OrderService.java
    30. │ │ │ │
    31. │ │ │ └─impl
    32. │ │ │ OrderServiceV1Impl.java
    33. │ │ │ OrderServiceV2Impl.java
    34. │ │ │ OrderServiceV3Impl.java
    35. │ │ │ OrderServiceV4Impl.java
    36. │ │ │ OrderServiceV5Impl.java
    37. │ │ │
    38. │ │ └─resources
    39. │ │ │ application.yml
    40. │ │ │
    41. │ │ └─mapper
    42. │ │ OrderItemMapper.xml
    43. │ │ OrderMapper.xml

    修改OrderServiceV5Impl类的代码,修改前的代码如下所示。

    1. @Autowired
    2. private RestTemplate restTemplate;
    3. private String userServer = "server-user";
    4. private String productServer = "server-product";

    修改后的代码如下所示。

    1. @Autowired
    2. private UserService userService;
    3. @Autowired
    4. private ProductService productService;

    修改OrderServiceV5Impl类中saveOrder()的代码,修改前的代码如下所示。

    1. User user = restTemplate.getForObject("http://" + userServer + "/user/get/" + orderParams.getUserId(), User.class);
    2. if (user == null){
    3. throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
    4. }
    5. Product product = restTemplate.getForObject("http://" + productServer + "/product/get/" + orderParams.getProductId(), Product.class);
    6. if (product == null){
    7. throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
    8. }
    9. //##################此处省略N行代码########################
    10. Result<Integer> result = restTemplate.getForObject("http://" + productServer + "/product/update_count/" + orderParams.getProductId() + "/" + orderParams.getCount(), Result.class);
    11. if (result.getCode() != HttpCode.SUCCESS){
    12. throw new RuntimeException("库存扣减失败");
    13. }

    修改后的代码如下所示。

    1. User user = userService.getUser(orderParams.getUserId());
    2. if (user == null){
    3. throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
    4. }
    5. Product product = productService.getProduct(orderParams.getProductId());
    6. if (product == null){
    7. throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
    8. }
    9. //##################此处省略N行代码########################
    10. Result<Integer> result = productService.updateCount(orderParams.getProductId(), orderParams.getCount());
    11. if (result.getCode() != HttpCode.SUCCESS){
    12. throw new RuntimeException("库存扣减失败");
    13. }

    可以看到,修改后的代码使用Fegin调用远程的用户微服务和商品微服务,已经完全没有了拼接URL路径的痕迹。

    接下来,将io.binghe.shop.order.controller.OrderController类中的OrderService修改为注入beanName为orderServiceV5的OrderService对象,如下所示。

    1. @Autowired
    2. @Qualifier(value = "orderServiceV5")
    3. private OrderService orderService;

    至此,订单微服务的代码修改完成。

    测试负载均衡效果

    启动订单微服务,并在浏览器或其他测试工具中多次访问链接http://localhost:8080/order/submit_order?userId=1001&productId=1001&count=1,启动的每个用户微服务和商品微服务都会打印相关的日志,说明使用Ribbon实现了负载均衡功能。

    注意:这里就不粘贴测试时每个启动的微服务打印的日志了,小伙伴们可自行测试并演示效果。

  • 相关阅读:
    WebServer 解析HTTP 请求报文
    41-数组 _ 数组作为函数参数
    工程师 - HUE(Humans in User Experience)介绍
    win系统环境搭建(二)——Windows安装JDK8
    pdf拆分成多个文件的方法以及注意事项
    解锁代码注释之谜:掌握代码注释的艺术与科学,提升软件开发与团队协作的卓越实践
    用HFSS仿真电路板上螺线管电感量
    flask-socketio实现websocket通信
    安卓Compose(一)
    五金行业智慧采购解决方案:应用集中采购协同管理系统激活企业数字化采购价值
  • 原文地址:https://blog.csdn.net/m0_73311735/article/details/126688902