转载请注明出处:https://blog.csdn.net/weixin_41133233/article/details/85070323
本文出自 程熙cjp 的博客
继上一篇讲述完ribbon的概念和搭建服务和消费之后,本篇小熙将会讲述负载均衡以及源码追踪。
一. 准备环境
-
SQL表数据
注意小熙的mysql数据库是5.5的,版本不一致的可以提取表结构即可。
/* Navicat Premium Data Transfer Source Server : chengnuo Source Server Type : MySQL Source Server Version : 50559 Source Host : localhost:3306 Source Schema : vue01 Target Server Type : MySQL Target Server Version : 50559 File Encoding : 65001 Date: 19/12/2018 15:45:19 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for t_user -- ---------------------------- DROP TABLE IF EXISTS `t_user`; CREATE TABLE `t_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `age` int(11) NULL DEFAULT NULL, `username` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `password` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `sex` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; -- ---------------------------- -- Records of t_user -- ---------------------------- INSERT INTO `t_user` VALUES (1, 20, '小歌', '789', 'xiaoge@163com', '女'); INSERT INTO `t_user` VALUES (2, 21, 'lucy', '123', '[email protected]', '女'); INSERT INTO `t_user` VALUES (3, 21, '小熙', '456', '[email protected]', '男'); INSERT INTO `t_user` VALUES (4, 22, '小白', '123', '[email protected]', '男'); SET FOREIGN_KEY_CHECKS = 1;
-
服务提供端代码结构:
(1). pojo类代码@Entity @Table(name = "t_user") @Data public class TUser { @Id @Column(name = "id") private Integer id; @Column(name = "age") private Integer age; @Column(name = "username") private String username; @Column(name = "password") private String password; @Column(name = "email") private String email; @Column(name = "sex") private String sex; }
(2). dao层代码
package com.chengxi.dao; import tk.mybatis.mapper.common.Mapper; import com.chengxi.pojo.TUser; @org.apache.ibatis.annotations.Mapper public interface TUserMapper extends Mapper<TUser> {}
(3). service层代码
接口类代码
package com.chengxi.service; import com.chengxi.pojo.TUser; /** * @author chengxi * @date 2018/12/3 09:29 */ public interface UserService { /** * 通过id查找用户信息 * @param id * @return */ public TUser selectUserById(Integer id); }
实现类代码
package com.chengxi.service.Impl; import com.chengxi.dao.TUserMapper; import com.chengxi.pojo.TUser; import com.chengxi.service.UserService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; /** * @author chengxi * @date 2018/12/3 09:31 */ @Service @Transactional public class UserServiceImpl implements UserService { @Resource private TUserMapper tUserMapper; @Override public TUser selectUserById(Integer id) { return tUserMapper.selectByPrimaryKey(id); } }
(4). controller层代码
package com.chengxi.controller; import com.chengxi.pojo.TUser; import com.chengxi.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Random; /** * @author chengxi * @date 2018/12/3 09:28 */ @RestController @RequestMapping(value = "/userService") public class UserController { @Autowired private UserService userServiceImpl; @GetMapping(value = "/{id}") public TUser selectUserById(@PathVariable Integer id) throws InterruptedException { return userServiceImpl.selectUserById(id); } }
-
消费者端代码结构
目前只关注controller层和pojo层就好,其他的后面会介绍
(1)controller层代码
/** * @author chengxi * @date 2018/12/3 15:09 */ @RestController @RequestMapping(value = "/userConsumer") public class UserController { @Autowired private DiscoveryClient discoveryClient; @Resource private RestTemplate restTemplate; @GetMapping(value = "/{id}") public TUser selectUserById(@PathVariable Integer id){ // 从注册服务中根据提供类的名称,获取对应的集群集合 List<ServiceInstance> instances = discoveryClient.getInstances("user-service"); // 由于只测试了一个提供者,所以直接取第一个(从零开始),多个的时候涉及到负载均衡以及轮询等算法 ServiceInstance serviceInstance = instances.get(0); // 根据获取的提供者信息,根据其中host获取对应的ip地址 String ip = serviceInstance.getHost(); // 根据获取的提供者信息,获取对应的port信息 int port = serviceInstance.getPort(); // 拼接url地址 String url = "http://"+ip+":"+port+"/userService/"+id; ResponseEntity<TUser> forEntity = restTemplate.getForEntity(url, TUser.class); return forEntity.getBody(); } }
(2)pojo层代码
这里的用户po类代码和服务提供端一样,以后可以提出一个po类项目,之后可以项目间依赖调用而不用重写,这里只是简单demo案例
至此项目准备环境都完成了。
二.搭建服务端集群
由于要展示ribbon的负载均衡的方式去访问服务端,所以要搭建服务端集群
这里介绍一个简单的方法:
步骤如图:
按照如上步骤,可以创建多个服务端集群节点,小熙由于电脑性能问题,就只创建两个节点了。
将项目全部启动,结果如图:
三. 测试负载均衡
-
第一种不使用@LoadBalanced注解找服务名称,最麻烦但最接近源码的调用方法。
(注意将springboot启动配置类中restTemplate方法上的@LoadBalanced注解去掉,否则由于该注解拦截httpclient发出的请求,而找寻找服务名称而报找不到实例的错误,下面会在源码中详细解说)
在这里小熙使用的是postman测试的,也可以使用浏览器测试。这里主要是理解流程。
-
使用@LoadBalanced注解找服务名称,简单但是源码被封装了
首先在该类中添加调用的方法:
/** * 使用Ribbon的负载均衡抒写的查询 * @param ids * @return */ @GetMapping(value = "/selectUsers") public List<TUser> selectUsersById(@RequestParam ArrayList<Integer> ids){ // 创建一个用户集合类,用于接收用户集合 List<TUser> tUsers = new ArrayList<>(); // 直接拼写服务端地址 String serviceUrl = "http://user-service/userService/"; // 使用java8新特性lambda表达式 ids.forEach(id -> { // 执行多次查询,这里使用提供者的服务器名称进行路径访问, // 因为在restTemplate的配制方法上加了@LoadBalanced注解,所以在每次发送url请求时都会进行拦截,由Ribbon在服务中进行服务名对应的ip,port查找、替换然后再查询 tUsers.add(restTemplate.getForEntity(serviceUrl + id, TUser.class).getBody()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } }); return tUsers; }
调用过程如图:
四. 负载均衡源码解析
-
首先找到LoadBalancerInterceptor类,因为这个是Ribbon拦截请求的实现类
-
点击execute进入RibbonLoadBalancerClient类查看具体实现过程
-
点击ILoadBalancer进入BaseLoadBalancer类中(如果想深入理解可以深入看看)
-
还是主要实现类RibbonLoadBalancerClient中的两个重要方法
至此,大体的流程的简要概括清晰了吧。
可能还会有同学问为什么我在restTemplate方法上添加@LoadBalanced注解就会被拦截呢?
别急下面给你解答:
全局搜索ctr+shift+f @LoadBalanced有哪些类用到了LoadBalanced有哪些类用到了, 发现LoadBalancerAutoConfiguration类,即LoadBalancer自动配置类。
在该类中,首先维护了一个被@LoadBalanced修饰的RestTemplate对象的List,在初始化的过程中,通过调用customizer.customize(restTemplate)方法来给RestTemplate增加拦截器LoadBalancerInterceptor。
而LoadBalancerInterceptor,用于实时拦截,在LoadBalancerInterceptor这里实现来负载均衡。LoadBalancerInterceptor的拦截方法又回到了上面刚讲解完的,进入源码的第一个方法intercept,如图:
至此,小熙将自己对于Ribbon源码的理解简单讲述了一下。如果理解有什么出入,还望大神们指点下哈。
嗯,本篇小熙简述完了负载均衡的实现和源码追踪,下一篇小熙将讲解hystrix熔断技术。