SpringBoot集成Kafka构建消息驱动微服务
下载、安装Kafka
◆下载Kafka : https://kafka.apache.org/quickstart
◆解压、启动ZK和Kafka Server即可(使用默认配置)
SpringCloud Stream消息驱动组件概览
◆负责与中间件交互的抽象绑定器: Binder
◆发送消息与接收消息的应用通信信道: Input、Output
创建相关的微服务e-commerce-stream-client
引入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>e-commerce-springcloud</artifactId>
<groupId>com.taluohui.ecommerce</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>e-commerce-stream-client</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!-- 模块名及描述信息 -->
<name>e-commerce-stream-client</name>
<description>Stream Client</description>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- 创建工程需要的两个依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- web 工程 -->
<dependency>
<groupId>com.imooc.ecommerce</groupId>
<artifactId>e-commerce-mvc-config</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- zipkin = spring-cloud-starter-sleuth + spring-cloud-sleuth-zipkin-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
<version>2.5.0.RELEASE</version>
</dependency>
<!-- SpringCloud Stream-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<!-- SpringCloud Stream + Kafka -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>
<!-- SpringCloud Stream + RocketMQ -->
<!-- <dependency>-->
<!-- <groupId>com.alibaba.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>-->
<!-- </dependency>-->
</dependencies>
<!--
SpringBoot的Maven插件, 能够以Maven的方式为应用提供SpringBoot的支持,可以将
SpringBoot应用打包为可执行的jar或war文件, 然后以通常的方式运行SpringBoot应用
-->
<build>
<finalName>${artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
添加配置文件
server:
port: 8006
servlet:
context-path: /ecommerce-stream-client
spring:
application:
name: e-commerce-stream-client
cloud:
nacos:
# 服务注册发现
discovery:
enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
server-addr: 127.0.0.1:8848
namespace: 1bc13fd5-843b-4ac0-aa55-695c25bc0ac6
metadata:
management:
context-path: ${server.servlet.context-path}/actuator
# 消息驱动的配置
stream:
# SpringCloud Stream + Kafka
kafka:
binder:
brokers: 127.0.0.1:9092
auto-create-topics: true # 如果设置为false, 就不会自动创建Topic, 你在使用之前需要手动创建好
# SpringCloud Stream + RocketMQ
# rocketmq:
# binder:
# name-server: 127.0.0.1:9876
# 开启 stream 分区支持
instanceCount: 1 # 消费者的总数
instanceIndex: 0 # 当前消费者的索引
bindings:
# 默认发送方
output: # 这里用 Stream 给我们提供的默认 output 信道
destination: ecommerce-stream-client-default # 消息发往的目的地, Kafka 中就是 Topic
content-type: text/plain # 消息发送的格式, 接收端不用指定格式, 但是发送端要
# 消息分区
producer:
# partitionKeyExpression: payload.author # 分区关键字, payload 指的是发送的对象, author 是对象中的属性
partitionCount: 1 # 分区大小
# 使用自定义的分区策略, 注释掉 partitionKeyExpression
partitionKeyExtractorName: qinyiPartitionKeyExtractorStrategy
partitionSelectorName: qinyiPartitionSelectorStrategy
# 默认接收方
input: # 这里用 Stream 给我们提供的默认 input 信道
destination: ecommerce-stream-client-default
group: e-commerce-qinyi-default
# 消费者开启分区支持
consumer:
partitioned: true
# Qinyi 发送方
qinyiOutput:
destination: ecommerce-stream-client-qinyi
content-type: text/plain
# Qinyi 接收方
qinyiInput:
destination: ecommerce-stream-client-qinyi
group: e-commerce-qinyi-qinyi
# spring-kafka 的配置
kafka:
bootstrap-servers: 127.0.0.1:9092
producer:
retries: 3
consumer:
auto-offset-reset: latest
sleuth:
sampler:
# ProbabilityBasedSampler 抽样策略
probability: 1.0 # 采样比例, 1.0 表示 100%, 默认是 0.1
# RateLimitingSampler 抽样策略
rate: 100 # 每秒间隔接受的 trace 量
zipkin:
sender:
type: kafka # 默认是 http
base-url: http://localhost:9411/
# 暴露端点
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
启动项
/**
* <h1>基于 SpringCloud Stream 构建消息驱动微服务</h1>
* */
@EnableDiscoveryClient
@SpringBootApplication
public class StreamClientApplication {
public static void main(String[] args) {
SpringApplication.run(StreamClientApplication.class, args);
}
}
创建vo包,新建对象
/**
* <h1>消息传递对象: SpringCloud Stream + Kafka/RocketMQ</h1>
* */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class QinyiMessage {
private Integer id;
private String projectName;
private String org;
private String author;
private String version;
/**
* <h2>返回一个默认的消息, 方便使用</h2>
* */
public static QinyiMessage defaultMessage() {
return new QinyiMessage(
1,
"e-commerce-stream-client",
"imooc.com",
"Qinyi",
"1.0"
);
}
}
新建stream包
使用默认信道发消息
发送方
/**
* <h1>使用默认的通信信道实现消息的发送</h1>
* */
@Slf4j
@EnableBinding(Source.class)
public class DefaultSendService {
private final Source source;
public DefaultSendService(Source source) {
this.source = source;
}
/**
* <h2>使用默认的输出信道发送消息</h2>
* */
public void sendMessage(QinyiMessage message) {
String _message = JSON.toJSONString(message);
log.info("in DefaultSendService send message: [{}]", _message);
// Spring Messaging, 统一消息的编程模型, 是 Stream 组件的重要组成部分之一
source.output().send(MessageBuilder.withPayload(_message).build());
}
}
接收方
/**
* <h1>使用默认的信道实现消息的接收</h1>
* */
@Slf4j
@EnableBinding(Sink.class)
public class DefaultReceiveService {
/**
* <h2>使用默认的输入信道接收消息</h2>
* */
@StreamListener(Sink.INPUT)
public void receiveMessage(Object payload) {
log.info("in DefaultReceiveService consume message start");
QinyiMessage qinyiMessage = JSON.parseObject(
payload.toString(), QinyiMessage.class
);
// 消费消息
log.info("in DefaultReceiveService consume message success: [{}]",
JSON.toJSONString(qinyiMessage));
}
}
使用自定义信道发消息
自定义输出信道
/**
* <h1>自定义输出信道</h1>
* */
public interface QinyiSource {
String OUTPUT = "qinyiOutput";
/** 输出信道的名称是 qinyiOutput, 需要使用 Stream 绑定器在 yml 文件中声明 */
@Output(QinyiSource.OUTPUT)
MessageChannel qinyiOutput();
}
使用自定义的输出信道发送消息
/**
* <h1>使用自定义的通信信道 QinyiSource 实现消息的发送</h1>
* */
@Slf4j
@EnableBinding(QinyiSource.class)
public class QinyiSendService {
private final QinyiSource qinyiSource;
public QinyiSendService(QinyiSource qinyiSource) {
this.qinyiSource = qinyiSource;
}
/**
* <h2>使用自定义的输出信道发送消息</h2>
* */
public void sendMessage(QinyiMessage message) {
String _message = JSON.toJSONString(message);
log.info("in QinyiSendService send message: [{}]", _message);
qinyiSource.qinyiOutput().send(
MessageBuilder.withPayload(_message).build()
);
}
}
自定义输入信道
/**
* <h1>自定义输入信道</h1>
* */
public interface QinyiSink {
String INPUT = "qinyiInput";
/** 输入信道的名称是 qinyiInput, 需要使用 Stream 绑定器在 yml 文件中配置*/
@Input(QinyiSink.INPUT)
SubscribableChannel qinyiInput();
}
使用自定义的输入信道实现消息的接收
/**
* <h1>使用自定义的输入信道实现消息的接收</h1>
* */
@Slf4j
@EnableBinding(QinyiSink.class)
public class QinyiReceiveService {
/** 使用自定义的输入信道接收消息 */
@StreamListener(QinyiSink.INPUT)
public void receiveMessage(@Payload Object payload) {
log.info("in QinyiReceiveService consume message start");
QinyiMessage qinyiMessage = JSON.parseObject(payload.toString(), QinyiMessage.class);
log.info("in QinyiReceiveService consume message success: [{}]",
JSON.toJSONString(qinyiMessage));
}
}
在配置文件中也要做相应的配置
# Qinyi 发送方
qinyiOutput:
destination: ecommerce-stream-client-qinyi
content-type: text/plain
# Qinyi 接收方
qinyiInput:
destination: ecommerce-stream-client-qinyi
group: e-commerce-qinyi-qinyi
SpringCloud Stream消息分组和消费分区的配置与说明
消息分组
◆应用的不同实例放在一个消费者组中 ,每一条消息只会被一个实例消费
◆消费者组的思想是通过多实例扩展服务吞吐量,且不会造成消息的重复消费
只需要在配置文件的接收方配置上group
# Qinyi 接收方
qinyiInput:
destination: ecommerce-stream-client-qinyi
group: e-commerce-qinyi-qinyi
消费分区
◆消费分区的作用就是为了确保具有共同特征标识的数据由同一个消费者实例进行处理
# 消息驱动的配置
stream:
# 开启 stream 分区支持
instanceCount: 1 # 消费者的总数
instanceIndex: 0 # 当前消费者的索引
bindings:
# 默认发送方
output: # 这里用 Stream 给我们提供的默认 output 信道
destination: ecommerce-stream-client-default # 消息发往的目的地, Kafka 中就是 Topic
content-type: text/plain # 消息发送的格式, 接收端不用指定格式, 但是发送端要
# 消息分区
producer:
# partitionKeyExpression: payload.author # 分区关键字, payload 指的是发送的对象, author 是对象中的属性
partitionCount: 1 # 分区大小
# 使用自定义的分区策略, 注释掉 partitionKeyExpression
partitionKeyExtractorName: qinyiPartitionKeyExtractorStrategy
partitionSelectorName: qinyiPartitionSelectorStrategy
# 默认接收方
input: # 这里用 Stream 给我们提供的默认 input 信道
destination: ecommerce-stream-client-default
group: e-commerce-qinyi-default
# 消费者开启分区支持
consumer:
partitioned: true
/**
* <h1>自定义从 Message 中提取 partition key 的策略</h1>
* */
@Slf4j
@Component
public class QinyiPartitionKeyExtractorStrategy implements PartitionKeyExtractorStrategy {
@Override
public Object extractKey(Message<?> message) {
QinyiMessage qinyiMessage = JSON.parseObject(
message.getPayload().toString(), QinyiMessage.class
);
// 自定义提取 key
String key = qinyiMessage.getProjectName();
log.info("SpringCloud Stream Qinyi Partition Key: [{}]", key);
return key;
}
}
/**
* <h1>决定 message 发送到哪个分区的策略</h1>
* */
@Slf4j
@Component
public class QinyiPartitionSelectorStrategy implements PartitionSelectorStrategy {
/**
* <h2>选择分区的策略</h2>
* */
@Override
public int selectPartition(Object key, int partitionCount) {
int partition = key.toString().hashCode() % partitionCount;
log.info("SpringCloud Stream Qinyi Selector info: [{}], [{}], [{}]",
key.toString(), partitionCount, partition);
return partition;
}
}