需求
在使用@Transactional
注解实现数据库事务时,需要在数据库commit成功后才发送消息,如果事务回滚了,消息就不发送
数据库操作使用的是 mybatis
实现
依赖
zookeeper 版本 3.6
kafka 版本 2.6
使用的 spring-boot 依赖版本是 2.4.6,对应的 spring-kafka 的依赖版本是 2.6.8,这个版本很关键,有些低版本的 spring-kafka 没有isolation-level: read-committed
这个配置
org.springframework.boot
spring-boot-starter-parent
2.4.6
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-actuator
mysql
mysql-connector-java
true
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.2
org.springframework.kafka
spring-kafka
application.yml
- kafka 配置
spring:
application:
name: transaction-test
kafka:
bootstrap-servers: 127.0.0.1:9092
producer:
# 发生错误后,消息重发的次数。
retries: 3
#当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算。
batch-size: 16384
# 设置生产者内存缓冲区的大小。
buffer-memory: 33554432
# 键的序列化方式
key-serializer: org.apache.kafka.common.serialization.StringSerializer
# 值的序列化方式
value-serializer: org.apache.kafka.common.serialization.StringSerializer
# acks=0 : 生产者在成功写入消息之前不会等待任何来自服务器的响应。
# acks=1 : 只要集群的首领节点收到消息,生产者就会收到一个来自服务器成功响应。
# acks=all :只有当所有参与复制的节点全部收到消息时,生产者才会收到一个来自服务器的成功响应。
acks: all
transaction-id-prefix: tx-
properties:
"[enable.idempotence]": true
"[transactional.id]": tran-id-1
consumer:
group-id: default-group
# 自动提交的时间间隔 在spring boot 2.X 版本中这里采用的是值的类型为Duration 需要符合特定的格式,如1S,1M,2H,5D
auto-commit-interval: 1S
# 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:
# latest(默认值)在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录)
# earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录
auto-offset-reset: earliest
# 是否自动提交偏移量,默认值是true,为了避免出现重复数据和数据丢失,可以把它设置为false,然后手动提交偏移量
enable-auto-commit: false
# 如果设置为“read_committed”,
# 那么消费者就会忽略事务未提交的消息,即只能消费到 LSO(LastStableOffset)的位置,
# 默认情况下为 “read_uncommitted”,
# 即可以消费到 HW(High Watermark)处的位置
isolation-level: read-committed
# 键的反序列化方式
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
# 值的反序列化方式
value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
listener:
# 在侦听器容器中运行的线程数。
concurrency: 5
# 当每一条记录被消费者监听器(ListenerConsumer)处理之后提交
# RECORD
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后提交
# BATCH
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,距离上次提交时间大于TIME时提交
# TIME
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后,被处理record数量大于等于COUNT时提交
# COUNT
# TIME | COUNT 有一个条件满足时提交
# COUNT_TIME
# 当每一批poll()的数据被消费者监听器(ListenerConsumer)处理之后, 手动调用Acknowledgment.acknowledge()后提交
# MANUAL
# 手动调用Acknowledgment.acknowledge()后立即提交,一般使用这种
# MANUAL_IMMEDIATE
#listner负责ack,每调用一次,就立即commit
ack-mode: manual_immediate
missing-topics-fatal: false
- 关键配置:
生产者:
# 开启Kafka事务
spring.kafka.producer.transaction-id-prefix
spring.kafka.producer.properties
# 开启Kafka事务后,生产者 acks 必须为 all ,且 retries 必须大于 0
spring.kafka.producer.retries=3
spring.kafka.producer.acks=all
消费者:
# isolation-level必须设置为read-committed,
# 否则默认为read_uncommitted,消费者就会读到未提交的数据,事务就会失效
spring.kafka.consumer.isolation-level=read-committed
配置类
这里有个坑,使用JPA访问数据库可以不配置这个,但是使用mybatis-plus时,@Transactional
注解就会失效。先发送消息,再保存数据,最后手动抛出错误,正常情况应该是数据库事务回滚,kafka事务也回滚,但是实际上数据库事务提交了,并且消费者消息也收到了。
原因是transactionManager没有注册成功,所以需要手动生成一下
详情参考文档:https://blog.csdn.net/feg545/article/details/113742434
和文档不同的是,我这里不创建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) {
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);
// }
}
测试
- 发送消息:
/**
* @author Jenson
*/
@Service
@Slf4j
public class ChickServiceImpl implements ChickService {
@Autowired
@SuppressWarnings("ALL")
private KafkaTemplate kafkaTemplate;
@Override
@Transactional(rollbackFor = Exception.class)
public List batchChick(List chickList) {
// 发送保存的消息
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
ListenableFuture> future = kafkaTemplate.send("jenson-test", "测试消息:"+":" + sdf.format(new Date()));
future.addCallback(new ListenableFutureCallback>() {
@Override
public void onFailure(Throwable throwable) {
log.error("消息发送失败:{}", throwable);
}
@Override
public void onSuccess(SendResult stringStringSendResult) {
log.info("消息发送成功");
}
});
List inChickList = new ArrayList<>();
for (int i = 0; i < chickList.size(); i++) {
Chick chick = chickRepository.insertChick(chickList.get(i));
inChickList.add(chick);
}
// 在这里手动抛出一个错误,测试保存成功时把该代码注释掉
if (chickList.size() > 0) {
int a = 1 / 0;
}
return inChickList;
}
}
- 消费消息
/**
* @author Jenson
*/
@Component
@Slf4j
public class MyConsumer {
@KafkaListener(topics = "jenson-test", groupId = "com.jenson")
public void listenGroup(ConsumerRecord record, Acknowledgment ack) {
String value = record.value();
log.info("receive value : " + value);
log.info("receive record : {} ", record);
// 手动提交offset ,只有在配置文件中配置了手动提交模式,ack才有用
ack.acknowledge();
}
}
参考文档:https://blog.csdn.net/feg545/article/details/113742434
代码地址:https://gitee.com/jenson343/hotchpotch/tree/master/transaction-test