微服务架构中的依赖通常通过远程调用实现,而远程调用中最常见的问题就是通信消耗与连接数占用。在高并发的情况之下,因通信次数的增加,总的通信时间消耗将会变得不那么理想。同时,因为依赖服务的线程池资源有限,将会出现排队等待与响应延迟的情况。
为了优化上述的两个问题,Hystrix提供了HystrixCollapser来实现请求的合并,以减少通信消耗和占用的线程数。
在HystrixCommand之前放置一个合并处理器,将处于一个很短的时间窗(默认10毫秒)内对同一依赖服务的多个请求进行整合并以批量方式发起请求(服务提供方也需要提供相应的批量实现接口)。
- 修改服务提供者,添加批量处理方法
1.为了方便查看效果,添加一个User实体类
@Data
public class User {
private String id;
private String name;
private String address;
}
2.新增一个controller,实现根据用户ID批量获取用户接口
@RestController
@RequestMapping(value = "collapser")
public class CollapserController {
@RequestMapping(value = "/getUsersByIds")
public List<User> getUsersByIds(String ids) {
System.out.println("请求参数为:" + ids);
List<User> users = new ArrayList<>();
String[] idss = ids.split(",");
for (String id : idss) {
users.add(new User()
.setId(id)
.setName("张无忌")
.setAddress("光明顶"));
}
return users;
}
}
- 新建服务消费者实例
1.新建一个Spring Boot实例,命名为hystrix-collapser(随意)
2.编辑pom.xml,主要依赖内容如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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-hystrix</artifactId>
</dependency>
3.修改application.yml
spring:
application:
name: hystrix-collapser
server:
port: 8000
eureka:
client:
service-url:
defaultZone: http://peer1:1111/eureka/
4.修改应用主类
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class HystrixApplication {
@Bean
//开启负载均衡
@LoadBalanced
RestTemplate restTemplate() {
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(HystrixApplication.class, args);
}
}
5.新建User实体类
@Data
public class User {
private String id;
private String name;
private String address;
}
6.新建service类
@Service
public class CollapserService {
@Autowired
private RestTemplate restTemplate;
//@HystrixProperty(name = "timerDelayInMilliseconds",value = "1000"的作用是将合并时间窗时间设置为1000毫秒
@HystrixCollapser(batchMethod = "findAll", collapserProperties = {
@HystrixProperty(name = "timerDelayInMilliseconds",value = "1000")
})
public Future<User> getUserById(String id) {
return null;
}
@HystrixCommand
public List<User> findAll(List<String> ids) {
User[] users = restTemplate.getForObject("http://hello-client/collapser/getUsersByIds?ids={1}", User[].class, StringUtils.join(ids, ","));
return Arrays.asList(users);
}
}
7.新建controller类
@RestController
@RequestMapping(value = "/collapser")
public class CollapserController {
@Autowired
private CollapserService collapserService;
@RequestMapping(value = "/testCollapser",method = RequestMethod.GET)
public String testCollapser() throws ExecutionException, InterruptedException {
HystrixRequestContext context = HystrixRequestContext.initializeContext();
//先连续三次调用
Future<User> f1 = collapserService.getUserById("1");
Future<User> f2 = collapserService.getUserById("2");
Future<User> f3 = collapserService.getUserById("3");
//等待3000毫秒,因为我们设置了合并时间窗时间设置为1000毫秒
Thread.sleep(3000);
//在调用一次请求
Future<User> f4 = collapserService.getUserById("4");
User user1 = f1.get();
User user2 = f2.get();
User user3 = f3.get();
User user4 = f4.get();
System.out.println("user1>>>>>>>>>>"+user1.getId());
System.out.println("user1>>>>>>>>>>"+user2.getId());
System.out.println("user1>>>>>>>>>>"+user3.getId());
System.out.println("user1>>>>>>>>>>"+user4.getId());
context.close();
return user1.toString();
}
}
测试
启动注册中心、启动服务提供实例、启动刚刚创建的工程
根据controller代码分析一下预期的效果
- f1、f2、f3应该会合并成一个请求到服务提供方获取数据
- f4不会合并请求,会单独调用服务提供方数去数据
下面我们来验证一下,访问http://localhost:8000/collapser/testCollapser
服务提供者打印的日志内容如下:
请求合并存在额外开销,虽然通过请求合并可以减少请求的数量以缓解依赖服务线程池的资源,但是用于请求合并的延迟时间窗会使得依赖服务的请求延迟增高,所以我们是否使用请求合并器需要根据依赖服务调用的实际情况选择。
主要从以下两个方面考虑:
1、请求命令本身的延迟:如果依赖服务的请求命令本身是一个高延迟的命令,那么可以使用请求合并,因为延迟时间窗的时间消耗显得微不足道了。
2、延迟时间窗内的并发量:如果一个时间窗内只有1~2个请求,那么这样的依赖服务不适合使用请求合并器。这种情况不但不能提升系统性能,反而会成为系统瓶颈,因为每个请求都需要多消耗一个时间窗才反应。相反,如果一个时间窗内具有很高的并发量,那么使用请求合并器可以有效的减少网络连接数量并极大提升系统吞吐量,此时延迟时间窗所增加的消耗就可以忽略不计了。