SpringCloudAlibaba之Integrated Example示例工程源码解读三

351 阅读6分钟

上一篇:SpringCloudAlibaba之Integrated Example示例工程源码解读二 - 掘金 (juejin.cn)

综述

本文分析SpringCloudAlibaba源码示例工程Integrated Example中的商品点赞场景。

该示例演示了高并发突发高峰流量场景下,如何进行流量控制,削峰填谷。

架构

image.png

  • HTTP请求由基于Spring Cloud Gateway组件的API网关统一接入。
  • Spring Cloud Gateway集成Nacos实现动态路由配置。
  • Spring Cloud Gateway集成Sentinel实现流量控制(Sentinel流量控制配置没有集成Nacos)。
  • 点赞服务分生产端服务和消费端服务,使用RocketMQ连接实现削峰填谷的作用。

此示例中对于突发流量的处理,有两个层面: 1.在流量入口(API网关)进行限流,防止服务被突发流量打死。 2.对核心业务(点赞数据落库)异步处理,快速返回客户端响应和释放系统资源,提高用户体验和系统可用性。

由于是简单的示例,业务处理过程并不复杂,处理速度很快,所以为了更好的演示RocketMQ削峰填谷的功能,系统开辟了一条单独的通道,在网关处不进行限流,直接将流量打到后端的点赞生产者服务上。

查看Nacos中的网关路由配置(integrated-gateway.yaml),对于点赞服务(integrated-provider)其配置了两个API入口“/praise/sentinel”和“/praise/rocketmq”:

spring:
  cloud:
    gateway:
      routes:
          #为商品点赞(不限流)
        - id: praiseItemRocketMQ
          uri: lb://integrated-provider
          predicates:
            - Path=/praise/rocketmq
          #为商品点赞(限流)
        - id: praiseItemSentinel
          uri: lb://integrated-provider
          predicates:
            - Path=/praise/sentinel
  • “/praise/sentinel” 演示基于Sentinel组件的并发限流
  • “/praise/rocketmq” 演示基于RocketMQ组件的削峰填谷

这两API对应integrated-provider服务的同一个接口。

@RestController
@RequestMapping("/praise")
public class PraiseController {
	...

	@GetMapping({ "/rocketmq", "/sentinel" })
	public boolean praise(@RequestParam Integer itemId) {
            ...
	}
}

它们之间的区别在于从网关进入时,“/praise/sentinel”接口被sentinel限制了流量速度。

基于Sentinel组件的并发限流

在高并发的场景中,系统的流量可能会突然迅速暴增,导致系统过载瘫痪。使用Sentinel可以对流量进行控制,防止系统雪崩。

下面来看看示例工程中网关是如何利用sentinel组件限流的。

首先需要在Pom.xml中引入sentinel依赖组件:

<dependency>
        <groupId>com.alibaba.csp</groupId>
        <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>

然后注入对应的 SentinelGatewayFilter 实例以及 SentinelGatewayBlockExceptionHandler 实例即可。(若使用了 Spring Cloud Alibaba Sentinel,则只需按照文档进行配置,无需自己添加 Configuration).

@Configuration
public class GatewayConfig {
        ...
        // 注册Sentinel网关过虑器
	@Bean
	@Order(Ordered.HIGHEST_PRECEDENCE)
	public GlobalFilter sentinelGatewayFilter() {
		return new SentinelGatewayFilter();
	}
        
        @Bean
	@Order(Ordered.HIGHEST_PRECEDENCE)
	public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
		// Register the block exception handler for Spring Cloud Gateway.
		return new SentinelGatewayBlockExceptionHandler(viewResolvers,
				serverCodecConfigurer);
	}
        ...
}

在java配置类(@Configuration)中设置限流规则(实际应用建议在配置中心保存配置数据),针对id为praiseItemSentinel的路由规则进行限流,每秒5次。

@Configuration
public class GatewayConfig {
        ...
        // 初始化Sentinel限流规则
        // 针对id为praiseItemSentinel的路由规则进行限流,每秒5次。
	@PostConstruct
	public void initGatewayRules() {
		Set<GatewayFlowRule> rules = new HashSet<>();
		rules.add(
				new GatewayFlowRule("praiseItemSentinel").setCount(5).setIntervalSec(1));
		GatewayRuleManager.loadRules(rules);
	}
        ...
}

流量控制规则通过GatewayFlowRule类来配置,主要配置参数说明如下:

public class GatewayFlowRule {
    //资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称。
    private String resource;
    //规则是针对 API Gateway 的 route(`RESOURCE_MODE_ROUTE_ID`)还是用户在 Sentinel 中定义的 API 分组(`RESOURCE_MODE_CUSTOM_API_NAME`),默认是 route
    private int resourceMode = 0;
    //限流指标维度,同限流规则的 `grade` 字段
    private int grade = 1;
    //限流阈值
    private double count;
    //统计时间窗口,单位是秒,默认是 1 秒。
    private long intervalSec = 1L;
    //流量整形的控制效果,同限流规则的 `controlBehavior` 字段,目前支持快速失败和匀速排队两种模式,默认是快速失败。
    private int controlBehavior = 0;
    //应对突发请求时额外允许的请求数目。
    private int burst;
    //匀速排队模式下的最长排队时间,单位是毫秒,仅在匀速排队模式下生效。
    private int maxQueueingTimeoutMs = 500;
    ...
}

最后示例代码中添加了一个接口因限流而阻塞时的公共处理类,返回统一的响应消息。

        // 设置请求被限流阻塞时的处理类
        // 此处代码实现:请求限流时,直接返回“此接口被限流了”的消息给客户端。
	@PostConstruct
	public void initBlockHandlers() {
		BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
			@Override
			public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange,
					Throwable throwable) {
				return ServerResponse.status(HttpStatus.OK)
						.contentType(MediaType.APPLICATION_JSON_UTF8)
						.body(BodyInserters.fromObject("此接口被限流了"));
			}
		};
		GatewayCallbackManager.setBlockHandler(blockRequestHandler);
	}

基于RocketMQ组件的削峰填谷

削峰填谷是一种常见的系统优化策略,主要是通过平滑系统的负载,防止系统过载,保证服务的可用性,提高系统性能和用户体验,以达到优化系统的目的。

前面说到的限流就是一种削峰的手段,它可以让突发的瞬时流量匀速的进入系统。而消息队列则是另一种常见的削峰手段,通过消息队列可以将大量并发请求进行缓冲,然后由消费端按照指定的速率慢慢处理,这样既能保证服务的可用性,又能有效的避免过载流量拖死核心服务。

该示例程序使用RocketMQ做为消息中间件实现削峰填谷。RocketMQ集群能承载数十万甚至上百万的TPS,可以轻松应对各种大促秒杀场景。

点赞服务生产者

Maven引入spring-cloud-starter-stream-rocketmq依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>

Nacos配置中心的配置(integrated-provider.yaml):

spring:
  cloud:
    stream:
      bindings:
        praise-output:
          destination: PRAISE-TOPIC-01
          content-type: application/json
      rocketmq:
        binder:
          name-server: rocketmq:9876
        bindings:
          praise-output:
            producer:
              group: test

示例配置中采用了Spring Cloud Stream框架。

Spring Cloud Stream是一个用于构建基于消息的微服务应用框架。

Spring Cloud Stream 内部有两个概念:Binder 和 Binding。

  • Binder: 跟外部消息中间件集成的组件,用来创建 Binding,各消息中间件都有自己的 Binder 实现。

比如 Kafka 的实现 KafkaMessageChannelBinderRabbitMQ 的实现 RabbitMessageChannelBinder 以及 RocketMQ 的实现 RocketMQMessageChannelBinder

  • Binding: 包括 Input Binding 和 Output Binding。

Binding 在消息中间件与应用程序提供的 Provider 和 Consumer 之间提供了一个桥梁,实现了开发者只需使用应用程序的 Provider 或 Consumer 生产或消费数据即可,屏蔽了开发者与底层消息中间件的接触。

下图是 Spring Cloud Stream 的架构设计。

image.png

生产者使用StreamBridge发送消息。StreamBridge 是 Spring Cloud Stream 3.0 之后引入的一个新特性,用于动态地创建并发送消息到绑定器。其主要目标是使应用程序能够动态地(即运行时)向任何可能的绑定器发送消息,而无需在启动时预先创建这些绑定器。

@RestController
@RequestMapping("/praise")
public class PraiseController {
	private static final String BINDING_NAME = "praise-output";
	@Autowired
	private StreamBridge streamBridge;

	@GetMapping({ "/rocketmq", "/sentinel" })
	public boolean praise(@RequestParam Integer itemId) {
		PraiseMessage message = new PraiseMessage();
		message.setItemId(itemId);
		Message<PraiseMessage> praiseMessage = MessageBuilder.withPayload(message)
				.build();
		return streamBridge.send(BINDING_NAME, praiseMessage);
	}

}

点赞服务消费者

Maven引入spring-cloud-starter-stream-rocketmq依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>

Nacos配置中心的配置(integrated-consumer.yaml):

spring:
  cloud:
    stream:
      bindings:
        praise-input:
          destination: PRAISE-TOPIC-01
          content-type: application/json
          group: praise-consumer-group-PRAISE-TOPIC-01
      rocketmq:
        binder:
          name-server: rocketmq:9876
        bindings:
          praise-input:
            consumer:
              # 每4秒消费4条消息
              pullInterval: 4000
              pullBatchSize: 4

消费端消息代码:

@Configuration
public class ListenerAutoConfiguration {
	@Bean
	public Consumer<Message<PraiseMessage>> consumer(PraiseService praiseService) {
		return msg -> {
			praiseService.praiseItem(msg.getPayload().getItemId());
		};
	}
}

此代码定义了一个 Consumer<Message<PraiseMessage>> 类型的 Bean,这个 Bean 是一个函数,接收一个 Message<PraiseMessage> 对象,并对其进行处理。

Message接口是Spring提供的一个消息封装接口,其中的getPayload方法用于获取消息的内容,这个内容就是PraiseMessage对象。

在这个函数式模型中,Spring Cloud Stream会自动将这个函数注册为消息处理器,监听指定的输入绑定,并在接收到消息时调用这个函数。

这个模型的优点是它更简洁,不需要使用 @StreamListener@ServiceActivator 注解,也不需要定义接口和使用 @EnableBinding 注解。

在这种情况下,Spring Cloud Stream 会尝试将函数与所有配置的输入绑定进行匹配。如果函数的参数类型与某个绑定的消息类型相匹配,那么这个函数就会被注册为这个绑定的消息处理器。显然Message与praise-input绑定的消息类型匹配。

示例代码所使用的函数式编程来收发消息的技术,详细介绍可以查看官网说明

docs.spring.io/spring-clou…