序言
众所周知,互联网系统具有“三高”的特性(高可用 H-Availability、高扩展 H-Expansion、高性能 H-Performance),而消息队列在提升系统“三高”特性方面,是必不可少的利器。
首先,消息队列以其多副本设置、宕机选主策略,实现自身的高可用;
其次,消息队列以其分区策略,实现消费端的高扩展;
最后,消息队列天然适合异步处理、负载均衡的场景,异步处理实现调用方与被调用方的解耦,缩短了调用方的等待时间;负载均衡指的是消费者组的概念,单个生产者的消息可以向多个消费者分摊消息,可极大提升整个系统的分发与处理性能。例如博主亲身经历的某智能对话系统,在单笔交易60秒的前提下,单机压测,QPS破百、并发通路数破千,是可以达到的目标。
关联文章
作为springBoot框架使用的第四篇,本章详解消息队列的使用,现在读者已经是一个成熟的读者了,这章我们将使用consul作为配置中心,稍微贴近企业级应用的开发。
本文目录
- 消息队列的作用
- Kafka 的基本概念
- Kafka 消息设计(源码解析)
- Kafka 生产者、消费者设计(源码解析)
- Kafka 消息积压,定位及解决方案
- Kafka 事务不生效,定位与解决方案
- 经验总结
1、消息队列的作用
概念 | 解释 |
---|---|
异步处理 | 降低服务端响应时延,反之,若使用Http等同步请求,客户端需阻塞等待服务端处理完成 |
负载均衡 | 实现多实例对消息分发处理,通过主题、分区、消费者组设置,实现实例间均衡消费的“发布-订阅”模式 |
可能读者不太理解负载均衡能带来好处,作者曾有过深刻体会:在多实例场景下,采用分布式锁可做简单的消息分发,即竞争到锁的实例处理当前消息(例如某个批次任务的处理),但是在消息量一旦加大后,系统在分发效率方面、公平性两方面大打折扣,使用消息队列做分发可解决这个难题,在这里推荐Kafka的“发布-订阅”模式。
值得一提的是,消息队列除了Kafka,还有很多优秀的组件,例如RocketMQ,这些中间件之间的差别对比,暂时不在本次讨论范围。
作者选择Kafka呢?其实是考虑到系统要求极高的吞吐性能(上百QPS),但对于SLA、事务方面没有太高的要求(SLA两个9,事务可选项),读者可按需求选择符合自身业务需求的消息中间件。
2、Kafka 的基本概念
概念 | 定义 | 应用场景 |
---|---|---|
Topic | 消息主题,同类消息的约定名称 | 消息的分发与消费都是以主题为记号 |
Broker数 | 部署kafka服务的实例数,由于单个实例可以部署多个Kafka服务,准确定义是Kafka服务的端口数 | 是集群实现的物理保证 |
副本数 | 每份消息的副本数量 | 用于灾备,一般设置小于等于broker数,因为宕机是以broker为单位,副本数间接影响ISR数量 |
Partition数 | Topic对应的消息被切分的份数 | 提升消息消费能力,设置为大于等于消费者数量,该值与broker数无关 |
来一个直观的感受,topic名称叫做HC_People_Info,3个分区,1个副本
3、Kafka 消息设计
名称 | 设计原则 |
---|---|
Topic | 以服务模块名称作为主题名,不要滥用,如果单个服务模块确实主题很多,可以下设二级目录 |
Key | 一般不设置,若消息间需要保序可设置,功能是避免消息的先发后到。例如,客户端先点击任务启动,再暂停,经过消息队列传导后,服务端不能接受到信息为先暂停、后启动;此时设置taskId为key,相同任务的消息就被Hash到同一个partition,在Kafka层面,可保证被顺序消费 |
Value | 被封装一个类,例如kafkaMsg,注意在传输前,需序列化成字符串 |
Value包含以下关键信息
- 事件ID:方便消息的定位、同一主题可容纳多个事件
- 事件名:方便消息定位
- 唯一序号:方便消费端做幂等
- 时间戳:定位消息事件发生时间(到微妙)
- 具体的业务数据
下面,我们以传递“人的信息”为例子,实现上面的消息设计,假定人的信息只包含年龄、名称
- 配置信息
- 配置类,承接配置信息
- Value类
配置信息
spring:
kafka:
# kafka服务所在机器,需要填写broker的IP+port
bootstrap-servers: xxx.xxx.xxx.xxx:xxxx
producer:
# 保证leader和follower都收到后,给生产者返回ack,持久性最好,时间延迟最大
acks: -1
# 发送不成功,重试三次
retries: 3
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer
max.in.flight.requests.per.connection: 1
consumer:
# 消费者群默认名称;消息只会被消费者组下的一个实例消费;可以在consumerListener上单独定制消费者组
group-id: localGroup
enable-auto-commit: false
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
# auto-commit-interval: 100 # ms
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
# kafka的主题汇总,关键信息!!!
kafkaTopic:
# 模块名
demo:
# 主题名
topic: HC_People_Info
# 事件id及事件名称
ageRequest:
id: 3451662860023003
name: Evt_people_age_Request
nameRequest:
id: 3451662860023004
name: Evt_people_name_Request
配置类
@Service
@Data
public class DemoConfig {
@Value("${kafkaTopic.demo.topic}")
private String topic;
// 事件ID
@Value("${kafkaTopic.demo.ageRequest.id}")
private Long ageRequestId;
// 事件名称
@Value("${kafkaTopic.demo.ageRequest.name}")
private String ageRequestName;
// 事件ID
@Value("${kafkaTopic.demo.nameRequest.id}")
private Long nameRequestId;
// 事件名称
@Value("${kafkaTopic.demo.nameRequest.name}")
private String nameRequestName;
}
Value类
@Data
@Slf4j
public class Msg {
// 事件ID
private Long eventId;
// 事件名称
private String eventName;
// 时间戳
private Long timestamp;
// 序列号
private String sequenceNum;
// 业务数据
private Object object;
public Msg(){}
public Msg(Long eventId, String eventName, String sequenceNum, Object object) {
this.eventId = eventId;
this.eventName = eventName;
this.timestamp = new Date().getTime();
this.sequenceNum = sequenceNum;
this.object = object;
}
}
4、Kafka 生产者、消费者设计(源码解析)
我们在上一节已设计好了主题,那如何写一个demo去调用Kafka呢?分为以下步骤,为方便演示,我们把生产者和消费者设置为同一个应用模块,通过浏览器调用写好的controller,演示数据流从浏览器、controller、producer、Kafka、consumer的流转过程。
- 引入相关POM
- 消费者、消费者服务
- 生产者 、生产者服务
- 序列化服务
- controller编写
- 验证数据流程
引入相关POM
<properties>
<java.version>1.8</java.version>
<lombok.version>1.18.12</lombok.version>
<junit.version>4.12</junit.version>
<spring-boot.version>2.0.7.RELEASE</spring-boot.version>
<consul.version>2.1.2.RELEASE</consul.version>
<elasticsearch.version>7.6.1</elasticsearch.version>
<elasticsearch-data.version>4.0.0.RELEASE</elasticsearch-data.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--kafka-->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
// 如果想使用Kafka的事务功能,需要引入mysql相关包,因为在注入开启事务的注解时,需要指定数据源,数据源的实例化依赖于mysql的相关jar
<!--mysql-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--配置中心相关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
<version>${consul.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
<version>${consul.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-collections</artifactId>
<groupId>commons-collections</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
消费者、消费者服务
@Component
@Slf4j
public class KafkaCustomer {
@Autowired
private NameServiceImpl nameService;
@Autowired
private DemoConfig demoConfig;
/**
* 处理kafka消息并分发
*
* @param msg
*/
@KafkaListener(topics = "${kafkaTopic.demo.topic}")
public void processDemoInfo(String msg) {
try {
log.info("demo msg is [{}]", msg);
Msg kafkaMsg = JsonUtils.fromJson(Msg.class, msg);
log.info("kafkaMsg is [{}]", kafkaMsg);
if (kafkaMsg.getEventName().equals(demoConfig.getNameRequestName())) {
nameService.handle(kafkaMsg.getSequenceNum(), kafkaMsg.getObject());
}
} catch (Exception e) {
log.error("process info error, msg:[{}]", msg, e);
}
}
}
// vo设置
@Data
public class NameRequest {
private String name;
}
// nameServiceImp就是接受信息后打印
@Service
@Slf4j
public class NameServiceImpl implements ReceiverService {
@Override
public void handle(String seqId, Object data) {
try {
NameRequest request = JsonUtils.fromJson(NameRequest.class, JsonUtils.toJson(data));
log.info("received:{}", request);
} catch (Exception e) { }
}
}
生产者、生产者服务
@Slf4j
public class KafkaProducer {
public static KafkaTemplate<String, String> kafkaTemplate;
static {
kafkaTemplate = (KafkaTemplate<String, String>) SpringContext.getAppObject("kafkaTemplate");
}
/**
* 事务提交后发送信息(不包含key值)
*/
public static void sendMsgAfterTrans(String topic, Object msg) {
if (TransactionSynchronizationManager.isActualTransactionActive()) { // 在事务中
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
// 事务commit之后执行该方法
log.info("MsgSender sendMsgAfterTrans params, msg={}", msg);
try {
ListenableFuture<SendResult<String, String>> send =
kafkaTemplate.send(topic, JsonUtils.toJson(msg));
send.addCallback(new KafkaCallBack(topic, msg));
} catch (Exception e) {
log.error("MsgSender sendMsgAfterTrans error, msg={}", msg, e);
}
super.afterCommit();
}
});
}
}
/**
* 事务提交后发送信息(包含key值)
*/
public static void sendMsgAfterTrans(String topic, String key, Object msg) {
if (TransactionSynchronizationManager.isActualTransactionActive()) { // 在事务中
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
// 事务commit之后执行该方法
log.info("MsgSender sendMsgAfterTrans params, msg={}", msg);
try {
ListenableFuture<SendResult<String, String>> send =
kafkaTemplate.send(topic, key, JsonUtils.toJson(msg));
// 发送成功或者失败会回调相应的方法(已被重写),打出日志
send.addCallback(new KafkaCallBack(topic, msg));
} catch (Exception e) {
log.error("MsgSender sendMsgAfterTrans error, msg={}", msg, e);
}
super.afterCommit();
}
});
}
}
/**
* 不开启事务的情况下,发送消息
*
* @param topic
* @param key
* @param msg
*/
public static void sendMsgWithNoTrans(String topic, String key, Object msg) {
log.info("MsgSender sendMsgWithNoTrans params, msg={}", msg);
try {
ListenableFuture<SendResult<String, String>> send =
kafkaTemplate.send(topic, key, JsonUtils.toJson(msg));
send.addCallback(new KafkaCallBack(topic, msg));
} catch (Exception e) {
log.error("MsgSender sendMsgWithNoTrans error, msg={}", msg, e);
}
}
}
// 回调函数,成功或者失败通知
@Slf4j
public class KafkaCallBack implements ListenableFutureCallback<SendResult<String, String>> {
private String topic;
private String key;
private Object msg;
public KafkaCallBack(String topic, Object msg) {
this.topic = topic;
this.msg = msg;
}
public KafkaCallBack(String topic, String key, Object msg) {
this.topic = topic;
this.key = key;
this.msg = msg;
}
@Override
public void onFailure(Throwable throwable) {
log.error("KafkaCallBack fail, Topic={}, Key={}, Msg={}", topic, key, msg);
}
@Override
public void onSuccess(SendResult<String, String> stringStringSendResult) {
log.info("KafkaCallBack success, Topic={}, Key={}, Msg={}", topic, key, msg);
}
}
// 生产者服务
public interface SenderService {
public void requestName(NameRequest request);
public void requestNameTrans(NameRequest request);
}
@Service
public class SenderServiceImpl implements SenderService {
@Autowired
private DemoConfig demoConfig;
public void requestName(NameRequest request) {
Msg value = new Msg(demoConfig.getNameRequestId(), demoConfig.getNameRequestName(),
UUID.randomUUID().toString(), request);
KafkaProducer.sendMsgWithNoTrans(demoConfig.getTopic(),null, value);
}
// 支持事务,业务逻辑都完成后提交;否则回滚
@Transactional
public void requestNameTrans(NameRequest request) {
Msg value = new Msg(demoConfig.getNameRequestId(), demoConfig.getNameRequestName(),
UUID.randomUUID().toString(), request);
KafkaProducer.sendMsgAfterTrans(demoConfig.getTopic(), value);
}
}
序列化服务
// 序列化util
public class JsonUtils {
/**
* 使用ThreadLocal创建对象,防止出现线程安全问题
*/
private static final ThreadLocal<ObjectMapper> OBJECT_MAPPER = new ThreadLocal<ObjectMapper>() {
@Override
protected ObjectMapper initialValue() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); // 忽略不存在的字段
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return objectMapper;
}
};
/**
* 禁止调用无参构造
*
* @throws IllegalAccessException
*/
private JsonUtils() throws IllegalAccessException {
throw new IllegalAccessException("Can't create an instance!");
}
/**
* 将对象转化成json
*
* @param t
*
* @return
*
* @throws JsonProcessingException
*/
public static <T> String toJson(T t) throws JsonProcessingException {
return OBJECT_MAPPER.get().writeValueAsString(t);
}
/**
* 将json转化成bean
*
* @param json
* @param valueType
*
* @return
*
* @throws JsonParseException
* @throws JsonMappingException
* @throws IOException
*/
public static <T> T fromJson(Class<T> valueType, String json)
throws JsonParseException, JsonMappingException, IOException {
return OBJECT_MAPPER.get().readValue(json, valueType);
}
/**
* 将json转化成List
*
* @param json
* @param collectionClass
* @param elementClass
*
* @return
*
* @throws JsonParseException
* @throws JsonMappingException
* @throws IOException
*/
public static <T> List<T> toList(String json, Class<? extends List> collectionClass, Class<T> elementClass)
throws JsonParseException, JsonMappingException, IOException {
JavaType javaType = OBJECT_MAPPER.get().getTypeFactory().constructCollectionType(collectionClass, elementClass);
return OBJECT_MAPPER.get().readValue(json, javaType);
}
/**
* 将json转化成Map
*
* @param json
* @param mapClass
* @param keyClass
* @param valueClass
*
* @return
*
* @throws JsonParseException
* @throws JsonMappingException
* @throws IOException
*/
public static <K, V> Map<K, V> toMap(String json, Class<? extends Map> mapClass, Class<K> keyClass,
Class<V> valueClass)
throws JsonParseException, JsonMappingException, IOException {
JavaType javaType = OBJECT_MAPPER.get().getTypeFactory().constructMapType(mapClass, keyClass, valueClass);
return OBJECT_MAPPER.get().readValue(json, javaType);
}
/**
* 将POJO 对象转json
* @param <K>
* @param <V>
* @param object
* @return
*/
public static <K, V> Map<? extends String, ?> toMap(Object object)
throws JsonParseException, JsonMappingException, IOException {
return OBJECT_MAPPER.get().convertValue(object, new TypeReference<Map<String, Object>>() { });
}
}
// spring容器直接获取对象封装
@Component
public class SpringContext implements ApplicationContextAware {
private static ApplicationContext CONTEXT;
public SpringContext() {
}
public void setApplicationContext(ApplicationContext context) throws BeansException {
CONTEXT = context;
}
public static ApplicationContext getContext() {
return CONTEXT;
}
public static boolean containsBean(String name) {
return CONTEXT.containsBean(name);
}
public static Object getAppObject(String name) {
return CONTEXT.getBean(name);
}
public static <T> T getAppObject(String name, Class<T> requiredType) {
return CONTEXT.getBean(name, requiredType);
}
public static String getActiveProfile() {
return CONTEXT.getEnvironment().getActiveProfiles()[0];
}
}
controller
@Controller
public class AgentController {
@Autowired
private SenderServiceImpl senderService;
@RequestMapping("/sendmsg")
public String sendMsg() {
NameRequest request=new NameRequest();
request.setName("zhangsan");
senderService.requestNameTrans(request);
return "hello";
}
}
// springBoot启动类
// 注意启动类上加上@EnableAsync,具体方法上加上@Async
@SpringBootApplication
@ServletComponentScan
@EnableAsync
// 开启事务
@EnableTransactionManagement
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
验证数据流程
完成上述配置后,终于等到了验证过程,我们来尝试run一下项目吧
成功启动springboot项目,并显示消息主题已创建,消息被当前服务订阅
观察topic的情况,分区情况、消费者组情况,LAG代表消费的延迟情况,若消息有积压,则LAG值会有体现。
调用controller后,看日志打印
生产者日志如下:
消费者日志如下
Kafka的Topic显示有一条消息,已经被消费
5、Kafka 消息积压,定位及解决方案
经过上述四步,我们已经完成了简单的demo演示,但是距离实战还有一定的距离。下面5和6小节,我将分享两个使用过程中极容易遇到的问题与解决办法。
当压测期间,消息量达到一定数量级时候,我们会看到当生产者生产消息速度大于消费者消费速度时候,消息在Kafka中积压
作者将分以下步骤进行解析
- 模拟消息积压
- 定位消息积压
- 解决消费积压
模拟消息积压
controller改动
consumer服务改动,每次消费一条数据需要10秒
定位消息积压
可以看到三个分区的消息积压一共9条
解决消费积压
那我们如何让消息尽快被处理呢?第一,扩容消费者机器的实例;第二,增加Kafka单主题的partition数量;第三,增加单实例的消费能力
第一点和第二点都是运维的同事帮研发同学干了,但是对系统影响范围大,影响所有的主题;第三点是改动最小的,研发同学撸起袖子就直接上了,那我们来看看怎么增加单实例的消费能力——线程池及阻塞队列的使用
如果还不明白线程池使用的同学,可以移步至博主的多线程专题:
话不多说,使用线程池的方式,直接上代码
#消费者线程池配置
kafkaconsumer.executor:
# 核心线程数等于最大线程数,保证不会线程数过多导致OOM
corePoolSize: 10
maximumPoolSize: 10
keepAliveTime: 5
# 阻塞队列的最大长度
capacity: 10
waitTime: 500
// 消费者线程池代码
@Service
@Slf4j
public class KafkaConsumerThreadPool {
@Value("${kafkaconsumer.executor.corePoolSize}")
private Integer corePoolSize;
@Value("${kafkaconsumer.executor.maximumPoolSize}")
private Integer maximumPoolSize;
@Value("${kafkaconsumer.executor.keepAliveTime}")
private Long keepAliveTime;
@Value("${kafkaconsumer.executor.capacity}")
private Integer capacity;
@Value("${kafkaconsumer.executor.waitTime}")
private Long waitTime;
private volatile ThreadPoolExecutor executor = null;
public KafkaConsumerThreadPool() {
}
// 采取DCL方式进行线程池初始化
public KafkaConsumerThreadPool getInstance() {
if (this.executor == null) {
Class var1 = KafkaConsumerThreadPool.class;
synchronized(KafkaConsumerThreadPool.class) {
if (this.executor == null) {
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue(this.capacity);
RejectedExecutionHandler handler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
log.error("task " + r.toString() + " reject from " + executor.toString());
}
};
this.executor = new ThreadPoolExecutor(this.corePoolSize, this.maximumPoolSize, this.keepAliveTime,
TimeUnit.SECONDS, workQueue, handler);
}
}
}
return this;
}
public void executor(Runnable runnable) {
while(true) {
try {
log.info("consumer try to get thread");
this.executor.execute(runnable);
return;
} catch (RejectedExecutionException var5) {
log.info("consumer info get thread failed, wait for ");
try {
Thread.sleep(500L);
} catch (InterruptedException var4) {
var4.printStackTrace();
}
}
}
}
}
// 消费者服务代码
@Component
@Slf4j
public class KafkaCustomer {
@Autowired
private NameServiceImpl nameService;
@Autowired
private DemoConfig demoConfig;
@Autowired
private KafkaConsumerThreadPool kafkaConsumerThreadPool;
/**
* 处理kafka消息并分发
*
* @param msg
*/
@KafkaListener(topics = "${kafkaTopic.demo.topic}")
public void processDemoInfo(String msg) {
try {
log.info("demo msg is [{}]", msg);
Msg kafkaMsg = JsonUtils.fromJson(Msg.class, msg);
log.info("kafkaMsg is [{}]", kafkaMsg);
if (kafkaMsg.getEventName().equals(demoConfig.getNameRequestName())) {
kafkaConsumerThreadPool.getInstance().executor(()->{
nameService.handle(kafkaMsg.getSequenceNum(), kafkaMsg.getObject());
});
}
} catch (Exception e) {
log.error("process info error, msg:[{}]", msg, e);
}
}
}
让我们来再次调用controller,可以看到LAG为0,且消息被迅速消费
6、Kafka 事务不生效,定位与解决方案
原因分析
最明显的问题表象是,生产者服务中开启了事务,但是消息一直没有投递到Kafka,可以从两方面来分析原因:
- 注解位置不对,若方法A调用方法B,只有方法B上有@transaction,则B上的事务无法生效
- 配置问题,springboot的启动类需要相关配置@EnableTransactionManagement,其次也是最隐蔽的问题,事务实现需要有相关数据源、jar的引用
解决方案
第一种问题的解决方案,对于注解位置问题,则从方法A开始,就开启事务(可以理解为从service层就开启注解服务)
第二种问题,需要配置数据源
spring:
# 数据库
datasource:
url: jdbc:mysql://xxx.xxx.xxx.xxx:8888/idsaas_new_press?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=CTT
username: root
password: qatest
driver-class-name: com.mysql.jdbc.Driver
# 连接池
type: com.zaxxer.hikari.HikariDataSource
hikari:
minimum-idle: 5
maximum-pool-size: 15
auto-commit: true
idle-timeout: 30000
pool-name: DatebookHikariCP
max-lifetime: 540000
connection-timeout: 30000
connection-test-query: SELECT 1
// 注意启动类上加上@EnableAsync,具体方法上加上@Async
@SpringBootApplication
@ServletComponentScan
@EnableAsync
// 启动类开启事务
@EnableTransactionManagement
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
// service
@Service
public class SenderServiceImpl implements SenderService {
@Autowired
private DemoConfig demoConfig;
public void requestName(NameRequest request) {
Msg value = new Msg(demoConfig.getNameRequestId(), demoConfig.getNameRequestName(),
UUID.randomUUID().toString(), request);
KafkaProducer.sendMsgWithNoTrans(demoConfig.getTopic(),null, value);
}
// service层就开启事务
@Transactional
public void requestNameTrans(NameRequest request) {
Msg value = new Msg(demoConfig.getNameRequestId(), demoConfig.getNameRequestName(),
UUID.randomUUID().toString(), request);
KafkaProducer.sendMsgAfterTrans(demoConfig.getTopic(), value);
}
}
可以看到数据源开启成功,Kafka事务可以正常使用
7、经验总结
文章末尾,博主想谈谈对以下话题的看法:
- 学习的目的
- 谁是最好的老师
首先,学习是为了得到奖励。不仅是物质上的奖励,真正的奖励是你学到东西的愉悦感,你掌握了从无到有的过程,是对新事物的支配带来的愉悦感。
其次,谁是最好的老师?是你自身的自驱力和怀疑力。
第一,怀疑是智慧的开始,带着问题去阅读;
第二,为什么自驱力重要?这是Idea触发的重要因素
有人问比尔盖茨,你认为微软公司面临的最大竞争对手来自于哪里?是Google、亚马逊、FaceBook么?比尔盖茨回答都不是,他说,这些伟大的公司我都很敬畏,但我最大的竞争对手是那些,每天在自己车库里,不分白天黑夜捣腾新事务、实践新想法的年轻人。
Last but not least,推荐《如何阅读一本书》,相信你一定会受益匪浅。