需求
在使用@Transactional
注解實(shí)現(xiàn)數(shù)據(jù)庫事務(wù)時(shí),需要在數(shù)據(jù)庫commit成功后才發(fā)送消息,如果事務(wù)回滾了,消息就不發(fā)送
數(shù)據(jù)庫操作使用的是 mybatis
實(shí)現(xiàn)
依賴
zookeeper 版本 3.6
kafka 版本 2.6
使用的 spring-boot 依賴版本是 2.4.6,對(duì)應(yīng)的 spring-kafka 的依賴版本是 2.6.8,這個(gè)版本很關(guān)鍵,有些低版本的 spring-kafka 沒有isolation-level: read-committed
這個(gè)配置
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.6</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<optional>true</optional>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 集成kafka -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
</dependencies>
application.yml
- kafka 配置
spring:
application:
name: transaction-test
kafka:
bootstrap-servers: 127.0.0.1:9092
producer:
# 發(fā)生錯(cuò)誤后,消息重發(fā)的次數(shù)。
retries: 3
#當(dāng)有多個(gè)消息需要被發(fā)送到同一個(gè)分區(qū)時(shí),生產(chǎn)者會(huì)把它們放在同一個(gè)批次里。該參數(shù)指定了一個(gè)批次可以使用的內(nèi)存大小,按照字節(jié)數(shù)計(jì)算。
batch-size: 16384
# 設(shè)置生產(chǎn)者內(nèi)存緩沖區(qū)的大小。
buffer-memory: 33554432
# 鍵的序列化方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
# 值的序列化方式
value-serializer: org.apache.kafka.common.serialization.StringSerializer
# acks=0 : 生產(chǎn)者在成功寫入消息之前不會(huì)等待任何來自服務(wù)器的響應(yīng)。
# acks=1 : 只要集群的首領(lǐng)節(jié)點(diǎn)收到消息,生產(chǎn)者就會(huì)收到一個(gè)來自服務(wù)器成功響應(yīng)。
# acks=all :只有當(dāng)所有參與復(fù)制的節(jié)點(diǎn)全部收到消息時(shí),生產(chǎn)者才會(huì)收到一個(gè)來自服務(wù)器的成功響應(yīng)。
acks: all
transaction-id-prefix: tx-
properties:
"[enable.idempotence]": true
"[transactional.id]": tran-id-1
consumer:
group-id: default-group
# 自動(dòng)提交的時(shí)間間隔 在spring boot 2.X 版本中這里采用的是值的類型為Duration 需要符合特定的格式,如1S,1M,2H,5D
auto-commit-interval: 1S
# 該屬性指定了消費(fèi)者在讀取一個(gè)沒有偏移量的分區(qū)或者偏移量無效的情況下該作何處理:
# latest(默認(rèn)值)在偏移量無效的情況下,消費(fèi)者將從最新的記錄開始讀取數(shù)據(jù)(在消費(fèi)者啟動(dòng)之后生成的記錄)
# earliest :在偏移量無效的情況下,消費(fèi)者將從起始位置讀取分區(qū)的記錄
auto-offset-reset: earliest
# 是否自動(dòng)提交偏移量,默認(rèn)值是true,為了避免出現(xiàn)重復(fù)數(shù)據(jù)和數(shù)據(jù)丟失,可以把它設(shè)置為false,然后手動(dòng)提交偏移量
enable-auto-commit: false
# 如果設(shè)置為“read_committed”,
# 那么消費(fèi)者就會(huì)忽略事務(wù)未提交的消息,即只能消費(fèi)到 LSO(LastStableOffset)的位置,
# 默認(rèn)情況下為 “read_uncommitted”,
# 即可以消費(fèi)到 HW(High Watermark)處的位置
isolation-level: read-committed
# 鍵的反序列化方式
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
# 值的反序列化方式
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
listener:
# 在偵聽器容器中運(yùn)行的線程數(shù)。
concurrency: 5
# 當(dāng)每一條記錄被消費(fèi)者監(jiān)聽器(ListenerConsumer)處理之后提交
# RECORD
# 當(dāng)每一批poll()的數(shù)據(jù)被消費(fèi)者監(jiān)聽器(ListenerConsumer)處理之后提交
# BATCH
# 當(dāng)每一批poll()的數(shù)據(jù)被消費(fèi)者監(jiān)聽器(ListenerConsumer)處理之后,距離上次提交時(shí)間大于TIME時(shí)提交
# TIME
# 當(dāng)每一批poll()的數(shù)據(jù)被消費(fèi)者監(jiān)聽器(ListenerConsumer)處理之后,被處理record數(shù)量大于等于COUNT時(shí)提交
# COUNT
# TIME | COUNT 有一個(gè)條件滿足時(shí)提交
# COUNT_TIME
# 當(dāng)每一批poll()的數(shù)據(jù)被消費(fèi)者監(jiān)聽器(ListenerConsumer)處理之后, 手動(dòng)調(diào)用Acknowledgment.acknowledge()后提交
# MANUAL
# 手動(dòng)調(diào)用Acknowledgment.acknowledge()后立即提交,一般使用這種
# MANUAL_IMMEDIATE
#listner負(fù)責(zé)ack,每調(diào)用一次,就立即commit
ack-mode: manual_immediate
missing-topics-fatal: false
- 關(guān)鍵配置:
生產(chǎn)者:
# 開啟Kafka事務(wù)
spring.kafka.producer.transaction-id-prefix
spring.kafka.producer.properties
# 開啟Kafka事務(wù)后,生產(chǎn)者 acks 必須為 all ,且 retries 必須大于 0
spring.kafka.producer.retries=3
spring.kafka.producer.acks=all
消費(fèi)者:
# isolation-level必須設(shè)置為read-committed,
# 否則默認(rèn)為read_uncommitted,消費(fèi)者就會(huì)讀到未提交的數(shù)據(jù),事務(wù)就會(huì)失效
spring.kafka.consumer.isolation-level=read-committed
配置類
這里有個(gè)坑,使用JPA訪問數(shù)據(jù)庫可以不配置這個(gè),但是使用mybatis-plus時(shí),@Transactional
注解就會(huì)失效。先發(fā)送消息,再保存數(shù)據(jù),最后手動(dòng)拋出錯(cuò)誤,正常情況應(yīng)該是數(shù)據(jù)庫事務(wù)回滾,kafka事務(wù)也回滾,但是實(shí)際上數(shù)據(jù)庫事務(wù)提交了,并且消費(fèi)者消息也收到了。
原因是transactionManager沒有注冊(cè)成功,所以需要手動(dòng)生成一下
詳情參考文檔:https://blog.csdn.net/feg545/article/details/113742434
和文檔不同的是,我這里不創(chuàng)建chainedKafkaTransactionManager也可以正常使用
配置類如下:
package com.jenson.config;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
//import org.springframework.kafka.transaction.ChainedKafkaTransactionManager;
//import org.springframework.kafka.transaction.KafkaTransactionManager;
import javax.sql.DataSource;
/**
* @author Jenson
*/
@Configuration
public class TransactionConfig {
private final DataSource dataSource;
private final TransactionManagerCustomizers transactionManagerCustomizers;
TransactionConfig(DataSource dataSource,
ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
this.dataSource = dataSource;
this.transactionManagerCustomizers = transactionManagerCustomizers.getIfAvailable();
}
@Bean
@Primary
public DataSourceTransactionManager transactionManager(DataSourceProperties properties) {
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(this.dataSource);
if (this.transactionManagerCustomizers != null) {
this.transactionManagerCustomizers.customize(transactionManager);
}
return transactionManager;
}
// @Bean("chainedKafkaTransactionManager") //解決問題3
// public ChainedKafkaTransactionManager chainedKafkaTransactionManager(DataSourceTransactionManager transactionManager,
// KafkaTransactionManager<?, ?> kafkaTransactionManager){
// return new ChainedKafkaTransactionManager<>(transactionManager, kafkaTransactionManager);
// }
}
測試
- 發(fā)送消息:
/**
* @author Jenson
*/
@Service
@Slf4j
public class ChickServiceImpl implements ChickService {
@Autowired
@SuppressWarnings("ALL")
private KafkaTemplate<String, String> kafkaTemplate;
@Override
@Transactional(rollbackFor = Exception.class)
public List<Chick> batchChick(List<Chick> chickList) {
// 發(fā)送保存的消息
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send("jenson-test", "測試消息:"+":" + sdf.format(new Date()));
future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
@Override
public void onFailure(Throwable throwable) {
log.error("消息發(fā)送失敗:{}", throwable);
}
@Override
public void onSuccess(SendResult<String, String> stringStringSendResult) {
log.info("消息發(fā)送成功");
}
});
List<Chick> inChickList = new ArrayList<>();
for (int i = 0; i < chickList.size(); i++) {
Chick chick = chickRepository.insertChick(chickList.get(i));
inChickList.add(chick);
}
// 在這里手動(dòng)拋出一個(gè)錯(cuò)誤,測試保存成功時(shí)把該代碼注釋掉
if (chickList.size() > 0) {
int a = 1 / 0;
}
return inChickList;
}
}
- 消費(fèi)消息
/**
* @author Jenson
*/
@Component
@Slf4j
public class MyConsumer {
@KafkaListener(topics = "jenson-test", groupId = "com.jenson")
public void listenGroup(ConsumerRecord<String, String> record, Acknowledgment ack) {
String value = record.value();
log.info("receive value : " + value);
log.info("receive record : {} ", record);
// 手動(dòng)提交offset ,只有在配置文件中配置了手動(dòng)提交模式,ack才有用
ack.acknowledge();
}
}
參考文檔:https://blog.csdn.net/feg545/article/details/113742434
代碼地址:https://gitee.com/jenson343/hotchpotch/tree/master/transaction-test