我们通常说的负载均衡是指将一个请求均匀地分摊到不同的节点单元上执行,负载均和分为硬件负载均衡和软件负载均衡:
硬件负载均衡或是软件负载均衡,他们都会维护一个可用的服务端清单,通过心 跳检测来剔除故障的服务端节点以保证清单中都是可以正常访问的服务端节点。 当客户端发送请求到负载均衡设备的时候,该设备按某种算法(比如轮询、权重、最小连接数等)从维护的可用服务端清单中取出一台服务端的地址,然后进行转发
Ribbon 是 Netflix 发布的开源项目,主要功能是提供客户端的软件负载均衡算 法,是一个基于HTTP和TCP的客户端负载均衡工具。 Spring Cloud 对 Ribbon 做了二次封装,可以让我们使用 RestTemplate 的服 务请求,自动转换成客户端负载均衡的服务调用。 Ribbon支持多种负载均衡算法,还支持自定义的负载均衡算法。 Ribbon 只是一个工具类框架,比较小巧,Spring Cloud 对它封装后使用也非 常方便,它不像服务注册中心、配置中心、API网关那样需要独立部署,Ribbon 只需要在代码直接使用即可;
Ribbon是客户端的负载均衡工具,而客户端负载均衡和服务端负载均衡最大的
区别在于服务清单所存储的位置不同,在客户端负载均衡中,所有客户端节点下
的服务端清单,需要自己从服务注册中心上获取,比如Eureka服务注册中心。
同服务端负载均衡的架构类似,在客户端负载均衡中也需要心跳去维护服务端清
单的健康性,只是这个步骤需要与服务注册中心配合完成。在Spring Cloud中,
由于Spring Cloud对Ribbon做了二次封装,所以默认会创建针对Ribbon的
自动化整合配置
在Spring Cloud中,Ribbon主要与RestTemplate对象配合起来使用,Ribbon 会自动化配置 RestTemplate 对象,通过@LoadBalanced 开启 RestTemplate 对象调用时的负载均衡。
由于Spring Cloud Ribbon的封装, 我们在微服务架构中使用客户端负载均衡 调用非常简单, 只需要如下两步:
1、启动多个服务提供者实例并注册到一个服务注册中心或是服务注册中心集群。
2、服务消费者通过被@LoadBalanced
注解修饰过的 RestTemplate
来调用服 务提供者。 这样,我们就可以实现服务提供者的高可用以及服务消费者的负载均衡调用。
Ribbon的负载均衡策略是由IRule接口定义, 该接口由如下实现:
RandomRule | 随机 |
RoundRobinRule | 轮询 |
AvailabilityFilteringRule | 先过滤掉由于多次访问故障的服务,以及并 发连接数超过阈值的服务,然后对剩下的服 务按照轮询策略进行访问 |
WeightedResponseTimeRule | 根据平均响应时间计算所有服务的权重,响应时间越快服务权重就越大被选中的概率即 越高,如果服务刚启动时统计信息不足,则使用 RoundRobinRule 策略,待统计信息足够会切换到该 WeightedResponseTimeRule 策略 |
RetryRule | 先按照 RoundRobinRule策略分发,如果分发到的服务不能访问,则在指定时间内进行重 试,分发其他可用的服务; |
BestAvailableRule | 先过滤掉由于多次访问故障的服务,然后选 择一个并发量最小的服务 |
ZoneAvoidanceRule | 综合判断服务节点所在区域的性能和服务节 点的可用性,来决定选择哪个服务; |
我们在新建一个服务提供者01-springcloud-service-provider -02
,这里我直接copy服务提供着1的项目,修改一下配置文件即可
这两个服务提供者除了配置文件,代码都是相同的!
为了测试负载均衡,我把服务提供者2的服务名称改成 01-springcloud-service-provider
和服务一保持一致
两个服务提供者的服务名称相同!
为了方便区分,我们在Controller里面输出不同的语句,
启动服务提供者1,服务提供者2,Eureka1,Eureka2,
观察Eureka
启动服务消费者,去消费服务(配置如图)
Controller层如下
启动以后,访问 http://localhost:8081/web/hello
观察浏览器输出的内容
再刷新一下
负载均衡调用测试正常
Ribbon默认是轮询策略,如果想使用其他策略,修改配置文件即可
随机策略
重启服务消费者,访问接口,观察是否为随机策略
重试策略
ps:先按照 RoundRobinRule 策略分发,如果分发到的服务不能访问,则在指定时间内进行重试,分发其他可用的服务; 可以断掉其中一个服务,来测试
当我们从服务消费端去调用服务提供者的服务的时候,使用了一个极其方便的对 象叫 RestTemplate,当时我们只使用了 RestTemplate 中最简单的一个功能 getForEntity 发起了一个 get 请求去调用服务端的数据,同时,我们还通过配 置@LoadBalanced
注解开启客户端负载均衡,RestTemplate的功能非常强大, 那么接下来我们就来详细的看一下RestTemplate中几种常见请求方法的使用
在日常操作中,基于Rest的方式通常是四种情况,它们分表是:
第一种:getForEntity 该方法返回一个 ResponseEntity<T>对象,ResponseEntity<T>是 Spring 对 HTTP请求响应的封装,包括了几个重要的元素,比如响应码、contentType、 contentLength、响应消息体等;
@RequestMapping("/web/hello")
public String hello(){
// 逻辑省略
// 调用SpringCloud服务提供者提供的服务
// 加入了 ribbon 的支持,那么在调用时,即可改为使用服务名称来访问:
// getForEntity() 发送一个HTTP GET请求,返回的ResponseEntity包含了响应体所映射成的对象
ResponseEntity<String> responseEntity = restTemplate.getForEntity("http://01-springcloud-service-provider/service/hello",String.class);
String body = responseEntity.getBody();
int statusCodeValue = responseEntity.getStatusCodeValue();
HttpStatus statusCode = responseEntity.getStatusCode();
HttpHeaders headers = responseEntity.getHeaders();
System.out.println(body);
System.out.println(statusCodeValue);
System.out.println(statusCode);
System.out.println(headers);
return restTemplate.getForEntity("http://01-springcloud-service-provider/service/hello",String.class).getBody();
}
以上代码:
getForEntity 方法第一个参数为要调用的服务的地址,即服务提供者提供的http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/hello
接口地址,注意这里是通过服务名调用而不是服务地址,如果改为服务地址就无法使用Ribbon 实现客户端负载均衡了。
getForEntity 方法第二个参数 String.class 表示希望返回的 bod类型,如果希望返回一个对象,也是可以的,比如 User 对象;
服务提供者1的Controller层新增getUser方法
,返回类型为User,model包里新增一个Users类,如图
因为我们是负载均衡调用,所以服务提供者2也要加入相同的方法
服务消费者新增访问的方法,getForEntity 方法返回Users类型
如果需要携带参数,getForEntity还有个重载的方法
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables) throws RestClientException
比如:
restTemplate.getForEntity("http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/hello?id= {1}&name={2}", String.class, "{1, '张无忌'}").getBody();
demo:
服务提供者一:新增一个getUser方法
服务提供者二:同样新增一个getUser方法
服务消费者:
@RequestMapping("/web/getUser")
public Users getUser () {
//逻辑判断(省略)
String[] strArray = {"10001", "申来来", "21"};
return restTemplate.getForEntity("http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/getUser?id={0}&name={1}&phone={2}", Users.class, strArray).getBody();
}
public <T> ResponseEntity<T> getForEntity(String url, Class<T> responseType,
Map<String, ?> uriVariables) throws RestClientException
比如:
Map<String, Object> paramMap = new ConcurrentHashMap<>();
paramMap.put("id", 1); paramMap.put("name", "张无忌");
restTemplate.getForEntity("http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/hello?id=
{id}&name={name}", String.class, paramMap).getBody();
demo:
参数可以传Map
@RequestMapping("/web/getUser")
public Users getUser () {
// 因为多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap
Map<String, Object> paramMap = new ConcurrentHashMap<>();
paramMap.put("id", 10002);
paramMap.put("userName", "张翠山");
paramMap.put("age", "23");
//调用SpringCloud服务提供者提供的服务 注意:因为是Map 所以url后面拼接的参数要等于key,不能写0,1,2啦
return restTemplate.getForEntity("http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/getUser?id={id}&userName={userName}&age={age}", Users.class, paramMap).getBody();
}
getForObject()
与 getForEntity
使用类似,只不过 getForObject 是在 getForEntity 基础上进行了再次封装,可以将 http 的响应体 body 信息转化成指定的对象,方便我们的代码开发; 当你不需要返回响应中的其他信息,只需要 body 体信息的时候,可以 使用这个更方便
<T> T getForObject(URI url, Class<T> responseType) throws RestClientException;
<T> T getForObject(String url, Class<T> responseType, Object... uriVariables) throws RestClientException;
<T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException;
demo:
@RequestMapping("/web/getUser")
public Users getUser () {
//逻辑判断(省略)
String[] strArray = {"10001", "申来来", "21"};
// 因为多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap
Map<String, Object> paramMap = new ConcurrentHashMap<>();
paramMap.put("id", 10002);
paramMap.put("userName", "张翠山");
paramMap.put("age", "23");
//调用SpringCloud服务提供者提供的服务
Users body1 = restTemplate.getForObject("http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/getUser?id={0}&userName={1}&age={2}", Users.class, strArray);
Users body2 = restTemplate.getForObject("http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/getUser?id={id}&userName={userName}&age={age}", Users.class, paramMap);
return restTemplate.getForEntity("http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/getUser?id={id}&userName={userName}&age={age}", Users.class, paramMap).getBody();
}
Post与Get请求非常类似:
restTemplate.postForObject()
restTemplate.postForEntity()
restTemplate.postForLocation()
demo:
服务提供者一:
// Post方式 新增
@PostMapping("/service/addUser")
public Users addUser(@RequestParam("id") Integer id,
@RequestParam("userName") String userName,
@RequestParam("age") Integer age) {
//进行业务处理(省略)
System.out.println("服务提供者1。。。。。。。");
Users user = new Users();
user.setId(id);
user.setUserName(userName);
user.setAge(age);
// 插入数据库逻辑 省略
return user;
}
服务提供者二:
服务消费者:
@RequestMapping("/web/addUser")
public Users addUser () {
//逻辑判断(省略)
// 要传的参数数据(很坑人的)
MultiValueMap<String,Object> dataMap = new LinkedMultiValueMap<String,Object>();
dataMap.add("id", "10002");
dataMap.add("userName", "张翠山");
dataMap.add("age", "23");
//调用SpringCloud服务提供者提供的服务
ResponseEntity<Users> responseEntity = restTemplate.postForEntity("http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/addUser",dataMap,Users.class);
Users body2 = restTemplate.postForObject("http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/addUser",dataMap,Users.class);
int statusCodeValue = responseEntity.getStatusCodeValue();
HttpStatus httpStatus = responseEntity.getStatusCode();
HttpHeaders httpHeaders = responseEntity.getHeaders();
Users body1 = responseEntity.getBody();
System.out.println(statusCodeValue);
System.out.println(httpStatus);
System.out.println(httpHeaders);
System.out.println(body1.getId() + "--" + body1.getUserName() + "--" + body1.getAge());
System.out.println(body2.getId() + "--" + body2.getUserName() + "--" + body2.getAge());
return restTemplate.postForEntity("http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/addUser", dataMap,Users.class).getBody();
}
restTemplate.put();
服务提供者一:
// put方式 修改
@PutMapping("/service/updateUser")
public Users updateUser(@RequestParam("id") Integer id,
@RequestParam("userName") String userName,
@RequestParam("age") Integer age) {
//进行业务处理(省略)
System.out.println("服务提供者1。。。。。。。");
System.out.println("id="+id+"--"+"userName="+userName+"-----"+"age="+age);
Users user = new Users();
user.setId(id);
user.setUserName(userName);
user.setAge(age);
// 修改操作 数据库逻辑 省略
return user;
}
服务提供者二:
服务消费者:
@RequestMapping("/web/updateUser")
public String updateUser () {
//逻辑判断(省略)
// 要传的参数数据(很坑人的)
MultiValueMap<String,Object> dataMap = new LinkedMultiValueMap<String,Object>();
dataMap.add("id", "10002");
dataMap.add("userName", "张翠山");
dataMap.add("age", "23");
//调用SpringCloud服务提供者提供的服务
restTemplate.put("http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/updateUser",dataMap);
return "success";
}
restTemplate.delete();
服务提供者一:
@DeleteMapping("/service/deleteUser")
public Users deleteUser(@RequestParam("id") Integer id,
@RequestParam("userName") String userName,
@RequestParam("age") Integer age) {
//进行业务处理(省略)
System.out.println("服务提供者1。。。。。。。");
System.out.println("id="+id+"--"+"userName="+userName+"-----"+"age="+age);
Users user = new Users();
user.setId(id);
user.setUserName(userName);
user.setAge(age);
// 删除操作 逻辑 省略
return user;
}
服务提供者二:
服务消费者:
@RequestMapping("/web/deleteUser")
public String deleteUser () {
//逻辑判断(省略)
String[] strArray = {"10001", "申来来", "21"};
// 因为多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap
Map<String, Object> paramMap = new ConcurrentHashMap<>();
paramMap.put("id", 10002);
paramMap.put("userName", "张翠山");
paramMap.put("age", "23");
//调用SpringCloud服务提供者提供的服务
restTemplate.delete("http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/deleteUser?id={0}&userName={1}&age={2}",strArray);
restTemplate.delete("http://01-SPRINGCLOUD-SERVICE-PROVIDER/service/deleteUser?id={id}&userName={userName}&age={age}",paramMap);
return "success";
}
get和delete传参一样
put和post传参一样