下述所有代码都是在订单服务中修改的,商品服务并不需要修改,只需要启动多个实例即可。
引入
在介绍Eureka组件的最后,留下了一个问题就是,无论启动多少个实例,只能调用第一个。原因是因为服务调用时获取的是一个实例数组,但是我们进行了硬编码,只get第一个实例进行了调用。找到原因之后,我们就要找到修改代码的方式,进而合理分配调用的主机,减少单机的压力。
但是,如何修改代码,进行合理分配成了一个难点。我想到一个解决方法就是在项目启动之初,去注册中心拉取被调用服务的数量,这样每次进行调用时使用 计数器 % 实例数 的方法来进行分配每次远程调用访问的主机。但是,由于拉取被调用服务的数量这个操作也是远程调用,因此可能每次拉取的实例顺序是不同的,这个我们可以在项目启动时把数量和主机内容都进行记录,没有啥问题;不过,更严重的一个问题是,在运行过程中,主机是可能宕机的,并且对于一个大促是可能进行动态扩缩容的,这是对被调用方进行一个改变,调用方并不会重启,这就导致调用方启动时获取的实例数量不会改变,从而导致扩缩容添加的主机起不到作用,因此微服务自己出了一个类似于Nginx的组件来进行负载均衡。
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
// 版本三:自定义负载均衡版本
private static final String PRODUCT_SERVICE_NAME = "cloud-provider-payment8001";
@Resource
private RestTemplate restTemplate;
@Resource
private DiscoveryClient discoveryClient;
// 使用原子类来表示调用次数
private AtomicInteger count = new AtomicInteger(1);
// 初始化方法,在项目启动时调取实例
private List<ServiceInstance> instances;
@PostConstruct
private void init() {
instances = discoveryClient.getInstances(PRODUCT_SERVICE_NAME);
log.info("查看被调用方实例的数量:" + instances.size());
}
@Override
public Order getOrderById(Integer id) {
// 获取订单
Order order = this.getById(id);
// 获取应该调用的实例下标
int index = this.count.getAndIncrement() % instances.size();
// 获取该实例的内容
ServiceInstance serviceInstance = this.instances.get(index);
// 拼接URL
String url = serviceInstance.getUri() + "/product/query/" + order.getProductId();
log.info("被调用服务的URI:" + serviceInstance.getUri());
// 远程调用
Product product = this.restTemplate.getForObject(url, Product.class);
order.setProduct(product);
// 返回结果
return order;
}
}
如上图所示,订单服务在启动时,就会去拉取被调用方存在几个实例。
如何启动个实例?
如下图,想要启动哪个服务的多实例,直接右击该服务。
右击之后,点击Copy Configuration选项。
点击之后,会出现如下弹窗,首先将Name改换成自己想要的,我一般会改一下端口号,表示我复制实例之后的端口号,然后点击Modify options。
点击之后,会出现一堆选项,然后点击Add VM options。
点击之后,会在原来的弹框中多出现一行内容,然后配置自己的信息即可。我一般不会修改其他内容,只会把端口号改变,这样不会造成端口冲突。
再次重启商品服务,由于之前我已经启动了三个实例,因此在Eureka服务列表中会出现四个实例,如下图:
如下图,在启动订单服务之后,也表示商品服务现在有四个实例,因此启动成功。
负载均衡介绍
概念
负载均衡 Load Balance,简称LB:负载就是压力、流量,均衡就是公平的、合理的将流量进行分配。负载均衡是高并发、高可用必不可少的关键组件。
当服务流量增大时,通常会通过增加机器数量的方式来进行扩容,负载均衡就是按照一定的算法把客户端的请求发送到这些机器中,完成流量分发。
分类
服务端负载均衡
在服务端进行负载均衡的算法分配,比较有名的服务端负载均衡器是Nginx。客户端发送请求,请求首先到达负载均衡器,然后负载均衡器通过负载均衡算法,在多个服务器之间选择一个进行调用。
客户端负载均衡
在客户端进行负载均衡的算法分配。把负载均衡的功能以库的方式集中到客户端,而不是再由一台指定的负载均衡设备集中提供,比如SpringCloud的LoadBalancer,请求到达客户端,客户端从Eureka中拉取服务列表。然后在发送请求到服务器之前,客户端自己通过负载均衡选择一个服务器,进行访问。
对于客户端和服务器来说,并不是服务器就一直是服务器,客户端一直就是客户端。对于上述来说,客户端请求到达服务端,但是服务端又要调用另一个服务端。因此这个中间的服务端也可以称作客户端,如果针对前端请求来说,他就是服务端;如果对于被调用方来说,他就是客户端。因此,不要太纠结这个东西。
SpringCloudLoadBalancer
在早期,负载均衡是SpringCloud的Ribbon来实现的,但是由于它不进行维护了。因此SpringCloud官方又实现了一个SpringCloudLoadBalancer来进行维护使用。作为客户端负载均衡器来说,使用还是比较简单的。
引入依赖
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
修改业务类
@Configuration
public class RestTemplateConfig {
@Bean
@LoadBalanced // 负载均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@Slf4j
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService {
// 版本一:没有引入微服务组件时
/*@Resource
private RestTemplate restTemplate;
@Override
public Order getOrderById(Integer id) {
Order order = this.getById(id);
String url = "http://127.0.0.1:8001/product/query/" + order.getProductId();
Product product = this.restTemplate.getForObject(url, Product.class);
order.setProduct(product);
return order;
}*/
// 版本二:引入了服务注册与发现
/*private final String PRODUCT_SERVICE_NAME = "cloud-provider-payment8001";
@Resource
private RestTemplate restTemplate;
@Resource
private DiscoveryClient discoveryClient;
@Override
public Order getOrderById(Integer id) {
// 获取订单
Order order = this.getById(id);
// 获取商品服务的实例
List<ServiceInstance> instances = discoveryClient.getInstances(this.PRODUCT_SERVICE_NAME);
// 输出日志查看获取到的实例有哪些内容
log.info(instances.toString());
// 获取第一个商品实例的uri
String uri = instances.get(0).getUri().toString();
log.info(uri);
// 拼接url
String url = uri + "/product/query/" + order.getProductId();
// 远程调用
Product product = this.restTemplate.getForObject(url, Product.class);
order.setProduct(product);
// 返回结果
return order;
}*/
// 版本三:自定义负载均衡版本
/*private static final String PRODUCT_SERVICE_NAME = "cloud-provider-payment8001";
@Resource
private RestTemplate restTemplate;
@Resource
private DiscoveryClient discoveryClient;
private AtomicInteger count = new AtomicInteger(1);
private List<ServiceInstance> instances;
@PostConstruct
private void init() {
instances = discoveryClient.getInstances(PRODUCT_SERVICE_NAME);
log.info("查看被调用方实例的数量:" + instances.size());
}
@Override
public Order getOrderById(Integer id) {
// 获取订单
Order order = this.getById(id);
// 获取应该调用的实例下标
int index = this.count.getAndIncrement() % instances.size();
// 获取该实例的内容
ServiceInstance serviceInstance = this.instances.get(index);
// 拼接URL
String url = serviceInstance.getUri() + "/product/query/" + order.getProductId();
log.info("被调用服务的URI:" + serviceInstance.getUri());
// 远程调用
Product product = this.restTemplate.getForObject(url, Product.class);
order.setProduct(product);
// 返回结果
return order;
}*/
// 版本四:负载均衡版本
// 不需要自己获取实例,只需要调用服务名就可以自动进行负载均衡获取实例
@Resource
private RestTemplate restTemplate;
private final String PRODUCT_SERVICE_NAME = "cloud-provider-payment8001";
@Override
public Order getOrderById(Integer id) {
// 获取订单
Order order = this.getById(id);
// 拼接url
String url = "http://" + this.PRODUCT_SERVICE_NAME + "/product/query/" + order.getProductId();
// 远程调用
Product product = this.restTemplate.getForObject(url, Product.class);
order.setProduct(product);
// 返回结果
return order;
}
}
测试服务
启动服务之后,输入URL:127.0.0.1:80/order/query/80,多点击几次,就会发现订单服务在进行调用时,会调用不同实例的商品服务,从而证明实现了负载均衡器。
负载均衡策略
策略概述
负载均衡是一种思想,无论是哪种负载均衡器,他们的负载均衡策略都是相似的。SpringCloudBalancer支持两种负载均衡策略:轮询策略和随机选择策略。
轮询策略(Round-Robin):表示服务器轮流处理客户端发来的请求,这是一种最简单的、也是最常用的负载均衡策略。例如学生值日轮流打扫卫生。
随机选择(Random):表示随机一个服务器来执行客户端发来的请求。
自定义策略:SpringCloud默认负载均衡策略是轮询策略,实现是RoundRobinLoadBalancer;如果服务的消费者想要采用随机的负载均衡策略,实现是RandomLoadBalancer;当然,也可以自己实现一个负载均衡策略,例如非公平负载均衡策略(权重)。
1. 定义随机算法对象,通过@Bean将其加载到Spring容器中。
// 该类需要满足:不用@Configuration注释、在组件扫描范围内
public class CustomLoadBalancerConfig {
@Bean
ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
return new RandomLoadBalancer(loadBalancerClientFactory
.getLazyProvider(name, ServiceInstanceListSupplier.class),
name);
}
}
2. 添加注解
@Configuration
// 添加@LoadBalancerClient注解
// 可以对不同的服务提供⽅配置不同的客⼾端负载均衡算法策略
// name:该负载均衡策略对哪个服务生效(服务提供方)
// configuration:该负载均衡策略用哪个负载均衡策略实现
@LoadBalancerClient(name = "cloud-consumer-order80", configuration = CustomLoadBalancerConfig.class)
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
实现原理
SpringCloudLoadBalancer的实现,主要是依赖于LoadBalancerInterceptor来实现的。顾名思义,这是一个拦截器,会拦截RestTemplate远程调用的请求,下述内容具体看源码进行解析:
下述图片是LoadBalancerInteceptor拦截器中的一个类,就算用来进行拦截远程调用请求的。可以看到,在这个类中,第一行代码获取了服务的URI,即http://cloud-provider-product8001。第二行代码是获取URI中的主机,即cloud-provider-product8001。稍后判断一下名字是否存在,存在的话就进去进行负载均衡处理。
点击excute方法之后,先进去一个接口,然后再找到接口的实现类,即如下类。该类中会做一系列的处理,然后又进去一个choose方法。
在chosse方法中,是根据服务名和负载均衡策略,选择处理的服务。首先,会根据服务名找到实现的负载均衡策略,然后根据负载均衡算法,在列表中选择一个服务实例。
对于下面这个方法,再点进去对应的choose方法。
在choose方法点进去之后,就是一个接口,接口对应的实现类则是具体的负载均衡策略,对实现的负载均衡策略感兴趣的可以点进去看看。