负载均衡
负载均衡是高可用网络基础架构的关键组件,通常用于将工作负载分布到多个服务器来提高网站、应用、数据库或其他服务的性能和可靠性。
一个没有负载均衡的 web 架构类似下面这样:
在这里用户直连 web 服务器,如果这个服务器宕机了,那么用户自然也就没办法访问了。另外,如果同时有很多用户试图访问服务器,超过了其能处理的极限,就会出现加载速度缓慢或根本无法连接的情况。
而通过在后端引入一个负载均衡器和至少一个额外的 web 服务器,可以缓解这个故障。通常情况下,所有的后端服务器会保证提供相同的内容,以便用户无论哪个服务器响应,都能收到一致的内容。
Spring Cloud 入门 ---- Ribbon 负载均衡组件
简介
Spring Cloud Ribbon 是基于 Netflix Ribbon 实现的一套客户端负载均衡工具。简单的说,Ribbon 是 Netflix 发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon 客户端组件提供一系列完善的配置项,如连接超时,重试等。简单的说,就是在配置文件中列出 Load Balancer(简称LB)后面所有的机器,Ribbon 会自动的帮助你基于某种规则(如:简单轮询,随机连接等)去连接这些机器。我们很容易使用 Ribbon 实现自定义的负载均衡算法。
概念:
LB 负载均衡 (Load Balance) 是什么
简单的所就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。常见的负载均衡有软件Nginx,LVS,硬件 F5等。
Ribbon本地负载均衡客户端 VS Nginx服务端负载均衡的区别
Nginx 是服务端负载均衡,客户端的所有请求都会交给 nginx,然后由 nginx 实现转发请求。即负载是由服务端实现的。
Ribbon 本地负载均衡,在调用微服务接口时,会在注册中心上获取服务的注册信息列表之后缓存到 JVM 本地【即将服务别名 以及对应的提供服务的机器信息缓存到本地,当调用服务时根据负载均衡策略选择一个提供服务的机器并调用它】,从而在本地实现 RPC 远程服务调用技术。
LB分类:
- 集中式LB
即在服务的消费方和提供方之间使用独立的 LB 设施(可以是硬件,如F5,也可以是软件,如 nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方;
- 进程内LB
将 LB 逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器。
Ribbon就属于进程内LB
,他只是一个类库,集成于消费方进程
,消费方通过它来获取到服务提供方的地址。
总结:
Ribbon其实就是一个软负载均衡的客户端组件,它可以和其他所需请求的客户端结合使用,和Eureka结合只是其中的一个实例。
Eureka + Ribbon 架构图:
以 EurekaServer 作为注册中心,当服务提供者启动时,会将自身服务信息【IP,port等】以别名的方式注册到 EurekaServer【同一种服务别名一样】,服务消费者会从 EurekaServer 查询服务列表【服务别名与其下的节点信息】并缓存到本地,当消费者需要调用服务提供者的服务时,会根据负载均衡策略选择一个节点并调用它;
Ribbon 在工作时分为两步:
第一步:选择EurekaServer,它优先选择在同一个区域内负载较少的server。
第二步:在根据用户指定的策略,在从server取到的服务注册列表中选择一个地址。其中Ribbon提供了多种策略:比如轮询、随机和根据响应时间加权等。
创建演示项目
为了方便我们介绍和演示 Ribbon 负载均衡的效果,我们需要创建 服务消费者 与 服务提供者模块【集群-两个节点】,至于注册中心就用之前的 Eureka 即可。
创建服务提供者
引入 pom 依赖,由于这次使用了数据库,所以需要引入 db 相关依赖
<!-- 引入 eureka client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
创建 yaml 配置文件,由于要演示负载均衡效果,为了启动两个服务,我们这里需要配置两个配置文件,由于
application-one.yml
与application-two.yml
配置差不多一样,我们给出application-one.yml
完整的配置,而application-two.yml
只给出不一样的配置。
application-one.yml
server:
port: 8010
spring:
application:
name: ribbon-payment-provider
security:
# 配置spring security登录用户名和密码
user:
name: akieay
password: 1qaz2wsx
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3600/cloud?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
# 数据源其他配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
filters: stat,wall
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
eureka:
client:
#表示是否将自己注册进 Eureka Server服务 默认为true
register-with-eureka: true
#f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url: # 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
# defaultZone: http://localhost:7001/eureka
defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka7001.com:7001/eureka,http://${spring.security.user.name}:${spring.security.user.password}@eureka7002.com:7002/eureka
instance:
instance-id: payment8010
## 当调用getHostname获取实例的hostname时,返回ip而不是host名
prefer-ip-address: true
# Eureka客户端向服务端发送心跳的时间间隔,单位秒(默认30秒)
lease-renewal-interval-in-seconds: 10
# Eureka服务端在收到最后一次心跳后的等待时间上限,单位秒(默认90秒)
lease-expiration-duration-in-seconds: 30
#mybatis-plus
mybatis-plus:
mapper-locations: classpath*:mapper/*.xml
#实体扫描,多个package用逗号或者分号分隔
typeAliasesPackage: com.akieay.cloud.rbpayment.entity;
configuration:
#是否开启驼峰命名自动映射
map-underscore-to-camel-case: true
#全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。
cache-enabled: false
#指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法
call-setters-on-nulls: true
#指定 MyBatis 所用日志的具体实现,未指定时将自动查找。
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
application-two.yml
server:
port: 8011
eureka:
instance:
instance-id: payment8011
主启动
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@MapperScan(value = "com.akieay.cloud.rbpayment.mapper")
public class RibbonPaymentApplication {
public static void main(String[] args) {
SpringApplication.run(RibbonPaymentApplication.class, args);
}
}
业务类,这里只介绍主要相关的业务类,其它细节请自行补充,或直接去 gitee 上拉取代码查阅。
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements UserService {
@Override
public int create(UserEntity userEntity) {
return baseMapper.insert(userEntity);
}
@Override
public UserEntity getById(int id) {
return baseMapper.selectById(id);
}
}
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("user")
public class UserEntity implements Serializable {
private static final long serialVersionUID=1L;
/**
* 用户ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 年龄
*/
private Integer age;
/**
* 创建时间
*/
private Date createTime;
}
@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Value("${server.port}")
private String serverPort;
@PostMapping(value = "/create")
public CommonResult create(@RequestBody UserEntity userEntity) {
int result = userService.create(userEntity);
log.info("*****插入结果:"+result);
if (result > 0) {
return new CommonResult(200, "插入数据库成功, serverPort:"+serverPort, result);
} else {
return new CommonResult(444, "插入数据库失败");
}
}
@GetMapping(value = "/get/{id}")
public CommonResult<UserEntity> getUserById(@PathVariable("id") Long id) {
UserEntity userEntity = userService.getById(id);
log.info("*****查询结果:"+userEntity);
if (null != userEntity) {
return new CommonResult(200, "查询数据成功, serverPort:"+serverPort, userEntity);
} else {
return new CommonResult(400, "没有对应的记录,查询ID:"+id);
}
}
}
创建两个启动服务,具体细节已经在前面的注册中心模块做了介绍,这里就不介绍了
创建成功后启动服务,打开 Eureka 注册中心,输入账号密码登录上去,即可看到 提供者模块已经注册进入了 Eureka 注册中心,如下图:
至此 服务提供者创建完成
创建服务消费者
引入 pom 依赖,由于该版本的 eureka 自带了 ribbon 依赖,所以我们不需要单独引入 ribbon 依赖。
<!-- 引入 eureka client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
若是需要引入的,可以添加如下依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
创建 application.yml 配置文件
server:
port: 80
spring:
application:
name: ribbon-order-consumer
security:
# 配置spring security登录用户名和密码
user:
name: akieay
password: 1qaz2wsx
eureka:
client:
#表示是否将自己注册进 Eureka Server服务 默认为true
register-with-eureka: true
#f是否从Eureka Server抓取已有的注册信息,默认是true。单点无所谓,集群必需设置为true才能配合ribbon使用负载均衡
fetch-registry: true
service-url:
# 设置与 Eureka Server 交互的地址 查询服务与注册服务都需要这个地址
defaultZone: http://${spring.security.user.name}:${spring.security.user.password}@eureka7001.com:7001/eureka,http://${spring.security.user.name}:${spring.security.user.password}@eureka7002.com:7002/eureka
instance:
instance-id: order80
prefer-ip-address: true
主启动
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
public class RibbonOrderApplication {
public static void main(String[] args) {
SpringApplication.run(RibbonOrderApplication.class, args);
}
}
配置类
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced //赋予默认的负载均衡规则
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
业务类
@RestController
@Slf4j
public class UserConcumerController {
/**
* payment-provider:服务别名
*/
public static final String PAYMENT_URL = "http://RIBBON-PAYMENT-PROVIDER";
@Resource
private RestTemplate restTemplate;
/**
* getForObject
* @param id
* @return
*/
@GetMapping("/consumer/user/getObject/{id}")
public CommonResult getObject(@PathVariable("id") Long id) {
return restTemplate.getForObject(PAYMENT_URL + "/user/get/" + id, CommonResult.class);
}
}
至此,服务消费者基础模块完成,启动服务,打开 Eureka 注册中心,即可看到服务已经注册到了其中,并且访问:http://localhost//consumer/user/getObject/1 可以发现使用默认的
轮询
负载均衡规则轮流调用服务提供者 8010 与 8011。
至此,演示模块的基础创建完成。
Ribbon 负载均衡与 RestTemplate 调用
由于我们该模块是使用的 Ribbon + RestTemplate 来实现的负载均衡与服务调用,所以这里先介绍一下 RestTemplate ;RestTemplate 是一个 HTTP 客户端工具,主要为我们调用 Http 接口提供方便的,支持 GET、POST、PUT、DELEFT …等方法,我们这里主要介绍 GET 与 POST。
GET 请求
主要是 getForObject 与 getForEntity 方法,两者的区别为:
- getForObject 方法:返回对象为响应体中数据转换成的对象,基本可以理解为Json【在前面的案例中我们已经使用过了,具体的返回类型根据我们给定的 class 类型确定】。
- getForEntity 方法:返回对象为 ResponseEntity 对象,包含了响应中的一些重要信息,比如:响应头、响应状态码、响应体等。
演示:
修改服务消费者的业务类:UserConcumerController,由于之前已经存在了 getForObject 的调用,我们这只需要添加 getForEntity 方法的调用即可。
/**
* getForEntity
* @param id
* @return
*/
@GetMapping("/consumer/user/getEntity/{id}")
public CommonResult getEntity(@PathVariable("id") Long id) {
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL + "/user/get/" + id, CommonResult.class);
// 打印响应code 与 响应头信息
log.info(entity.getStatusCode() + " ---- " + entity.getHeaders());
if (entity.getStatusCode().is2xxSuccessful()) {
return entity.getBody();
} else {
return new CommonResult(444, "操作失败");
}
}
重启服务,并分别调用 http://localhost//consumer/user/getObject/1 与 http://localhost//consumer/user/getEntity/1 可以发现两个返回给客户端的响应结果一致,也就是说 getForEntity 的响应实体内容 entity.getBody() 与 getForObject 返回的内容一致。这就验证了我们之前所说的 getForEntity 与 getForObject 的不同之处在于,getForEntity 不仅会返回相应的实体数据,还会返回响应状态码,响应头信息等,如下面我们控制台打印的这条信息。
POST 请求
主要是 postForObject 与 postForEntity 方法,两者区别为: postForEntity 方法可以设置 请求的 header 属性,而 postForObject 不能指定请求的 header,当需要指定 header 的属性值的时候,使用postForEntity方法。
演示:
新增
insertObject
与insertEntity
方法,我们在insertEntity
中设置了请求头信息
@PostMapping("/consumer/user/insertObject")
public CommonResult insertObject(@RequestBody Map<String, Object> user) {
return restTemplate.postForObject(PAYMENT_URL + "/user/create", user, CommonResult.class);
}
@PostMapping("/consumer/user/insertEntity")
public CommonResult insertEntity(@RequestBody Map<String, Object> user) {
HttpHeaders headers = new HttpHeaders();
//添加请求头参数 认证token
headers.add("Authorization", "Bearer 05ce151b-8574-4411-a9c2-baaa4cd17e59");
headers.add("Content-Type", "application/json");
HttpEntity<Map<String, Object>> httpEntity = new HttpEntity<>(user,headers);
ResponseEntity<CommonResult> responseEntity = restTemplate.postForEntity(PAYMENT_URL + "/user/create", httpEntity, CommonResult.class);
log.info(responseEntity.getStatusCode() + " ---- " + responseEntity.getStatusCodeValue() + " ---- " + responseEntity.getHeaders());
return responseEntity.getBody();
}
重启服务,并测试这两个接口;可以看到返回结果差不多;
postForEntity
与postForObject
的不同之处除了可以设置headers
外,返回的结果也不一样,postForObject
只会返回响应实体部分,而postForEntity
会返回 响应码、响应头、响应体等信息;
postForObject
postForEntity
至此关于RestTemplate 的介绍结束,至于其它的请求方法的介绍请自行查阅。
Ribbon自带的负载均衡规则
Ribbon的核心组件IRule
根据特定算法从服务器列表中选取一个要访问的服务
自带的负载均衡算法
- RoundRobinRule: 轮询获取服务实例。
- RandomRule: 随机获取服务实例。
- RetryRule: 先按照 RoundRobinRule 的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务。
- WeightedResponseTimeRule: 对于 RoundRobinRule 的扩展,响应速度越快的实例选择权重越大,越容易被选择。
- BestAvailableRule: 会先过滤由于多次访问故障而处于熔断跳闸状态的服务,然后选择一个并发量最小的服务。
- AvailabilityFilteringRule: 先过滤掉故障实例,再选择并发较小的实例。
- ZoneAvoidanceRule: 采用双重过滤,同时过滤不是同一区域的实例和故障实例,选择并发较小的实例。
替换负载均衡规则
替换负载均衡规则需要我们自定义一个负载均衡规则,这个规则的放置的位置是有限制的;官方文档明确给出警告:这个自定义配置类不能放在 @ComponentScan 所扫描的当前包以及其子包下,否则我们自定义的这个配置类就会被所有的 Ribbon 客户端所共享,达不到特殊化定制的目的了。
即替换的负载均衡规则如果放置在启动类包或其子包下时,这个规则对所有服务生效。
正确的放法如下:
自定义负载均衡规则:
@Configuration
public class MySelfRule {
/**
* 随机
* @return
*/
@Bean
public IRule myRule(){
return new RandomRule();
}
}
修改主启动,添加注解 @RibbonClient
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient
@RibbonClient(name = "RIBBON-PAYMENT-PROVIDER", configuration = MySelfRule.class)
public class RibbonOrderApplication {
public static void main(String[] args) {
SpringApplication.run(RibbonOrderApplication.class, args);
}
}
name:服务别名 configuration:替换的负载均衡规则;重启服务,访问:http://localhost/consumer/user/getObject/1 多次访问,通过返回消息中的端口,我们可用看到访问规则变成了
随机
,有的时候 8010 经常出现,有的时候 8011 经常出现访问完全随机。至于其它的负载均衡策略这里不做介绍,可自行修改测试。另一种修改负载均衡规则的方式【yml 配置】:注释掉启动类上的负载均衡规则配置【避免影响】,yml 新增配置如下:
// 注意服务名与 Eureka 保持一致
RIBBON-PAYMENT-PROVIDER:
ribbon:
ConnectTimeout: 1000 #服务请求连接超时时间(毫秒)
ReadTimeout: 3000 #服务请求处理超时时间(毫秒)
OkToRetryOnAllOperations: true #对超时请求启用重试机制
MaxAutoRetriesNextServer: 1 #切换重试实例的最大个数
MaxAutoRetries: 1 # 切换实例后重试最大次数
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #修改负载均衡算法
若是需要配置全局的负载均衡规则,将之前我们自定义的负载均衡策略
MySelfRule
添加到 主启动所在的包 或 其子包下即可;
Ribbon 默认负载均衡策略【RoundRobinRule】原理
rest 接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次重启服务后,rest 接口计数重置为1。
List<ServiceInstance> instances = discoveryClient.getInstances("RIBBON-PAYMENT-PROVIDER")
如:List[0] instanceA = 127.0.0.1:8011 List[1] instanceB = 127.0.0.1:8010
8010 + 8011 组合成为集群,它们共计 2 台机器,集群总数为 2,按轮询算法原理:
当请求总数为 1 时: 1 % 2 = 1 对应下标位置为 1,则获得服务地址为 127.0.0.1:8010
当请求总数为 2 时: 2 % 2 = 0 对应下标位置为 1,则获得服务地址为 127.0.0.1:8011
当请求总数为 3 时: 3 % 2 = 1 对应下标位置为 1,则获得服务地址为 127.0.0.1:8010
当请求总数为 4 时: 4 % 2 = 0 对应下标位置为 1,则获得服务地址为 127.0.0.1:8011
如此类推… 服务重启后重置为 1。
RoundRobinRule 源码分析
public class RoundRobinRule extends AbstractLoadBalancerRule {
private AtomicInteger nextServerCyclicCounter;
private static final boolean AVAILABLE_ONLY_SERVERS = true;
private static final boolean ALL_SERVERS = false;
private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);
public RoundRobinRule() {
nextServerCyclicCounter = new AtomicInteger(0);
}
public RoundRobinRule(ILoadBalancer lb) {
this();
setLoadBalancer(lb);
}
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
}
Server server = null;
int count = 0;
while (server == null && count++ < 10) {
//获取可达的机器
List<Server> reachableServers = lb.getReachableServers();
//获取所有机器
List<Server> allServers = lb.getAllServers();
//可达的机器数量
int upCount = reachableServers.size();
//总机器数量
int serverCount = allServers.size();
if ((upCount == 0) || (serverCount == 0)) {
log.warn("No up servers available from load balancer: " + lb);
return null;
}
//获取下一次调用服务器的下标[CAS + 自旋锁]
int nextServerIndex = incrementAndGetModulo(serverCount);
//获取下一次调用的服务机器
server = allServers.get(nextServerIndex);
if (server == null) {
/* Transient. */
Thread.yield();
continue;
}
if (server.isAlive() && (server.isReadyToServe())) {
return (server);
}
// Next.
server = null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: "
+ lb);
}
return server;
}
/**
* Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
*
* @param modulo The modulo to bound the value of the counter.
* @return The next value.
*/
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextServerCyclicCounter.get();
int next = (current + 1) % modulo;
if (nextServerCyclicCounter.compareAndSet(current, next))
return next;
}
}
@Override
public Server choose(Object key) {
return choose(getLoadBalancer(), key);
}
@Override
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
nextServerCyclicCounter:记录接口第几次请求。
以上为
RoundRobinRule
负载均衡的源码,其核心是choose
方法用来选择提供服务的机器;该方法会首先判断lb
【负载均衡器】是否为空,若为空抛出异常,然后 while 循环 10 次获取 server 若获取到不为空的 server 则跳出循环,返回 server,否则循环 10 次后,抛出没有可用服务器的异常;获取 server 的流程:获取可达【正常可用】的服务机器列表,获取总服务机器列表;若两者有一方为空,则抛出 没有可用服务 的异常;通过 CAS + 自旋锁 获取下一个可用的服务节点的下标【轮询算法是通过 接口请求次数 % 服务节点数量 = 访问的节点下标】,通过下标获取下一个可用的服务节点,若服务节点能够正常使用则返回,否则将其置空 进入下一次循环判断。
Ribbon—手写轮询算法
服务提供者 新增业务,然后重启服务
/**
* 自定义轮询负载均衡规则测试
* @return
*/
@GetMapping("/lb")
public String getUserLb(){
return "self RoundRobinRule 访问端口:" + serverPort;
}
服务消费者 手写轮询算法
首先为了避免原有的负载均衡规则的影响;需要注释掉原有的 负载均衡配置
自定义负载均衡规则
public interface LoadBalancer {
/**
* 从服务主机列表根据 负载均衡规则 获取主机
* @param serviceInstances
* @return
*/
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
@Component
public class SelfRoundRobinRule implements LoadBalancer {
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement() {
int current;
int next;
do {
current = this.atomicInteger.get();
next = current >= Integer.MAX_VALUE ? 0 : current + 1;
} while (!this.atomicInteger.compareAndSet(current, next));
System.out.println("第几次访问,访问次数为:" + next);
return next;
}
/**
* 轮询负载均衡算法:rest接口第几次请求 % 服务器集群总数量 = 实际调用服务器位置下标, 每次服务重启动后rest接口计数从1开始
* @param serviceInstances
* @return
*/
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}
@Resource
private SelfRoundRobinRule selfRoundRobinRule;
@Resource
private DiscoveryClient discoveryClient;
@GetMapping("/consumer/user/lb")
public String getUserLb() {
//获取指定服务的节点列表
List<ServiceInstance> instances = discoveryClient.getInstances("RIBBON-PAYMENT-PROVIDER");
if (null == instances || instances.size() <= 0) {
return null;
}
ServiceInstance instance = selfRoundRobinRule.instances(instances);
return restTemplate.getForObject(instance.getUri() + "/user/lb", String.class);
}
重启服务,测试访问:http://localhost/consumer/user/lb ,可以看到 8010 与 8011 轮流调用;