在springboot中,只要引入了kafka的springboot依赖,并且在配置文件中加入了事务id前缀,spring就能够在事务管理器中自动注入kakfa的事务,在代码中只要使用spring提供的Transaction注解就能使用kafka的事务。
但是在代码中,用到Transaction这个注解的不只是kafka,多数情况下,这个注解的使用范围是用在传统数据库中的事务的,比如说mysql等,如果一个方法中既要实现mysql的事务,又要实现基于kafka的分布式事务,那么只使用一个注解,将很容易混淆,且增加对代码排错的复杂度,而且在实际的测试过程中,如果同时引入了kafka的事务和mybatisPlus的事务,那么在代码逻辑异常的时候,mybatis的事务将会失效。
所以为了避免在方法上使用同一个Transaction注解造成的事务失效和代码复杂度,就有必要重新写一个专门用于kafka事务的注解。
此次的目标:
因为在项目中使用的springboot框架,而springboot框架天生就有一项很重要的能力能够实现上面的目标,那就是:AOP。
在spring的Aop中,有一个很重要的东西,那就是环绕通知,它可以实现在方法执行的前后,执行自定义的代码逻辑。
那么,对应到kafka的事务,就是在调用方法开始前开启kafka的事务,原方法执行通过无异常,就提交事务;原方法有异常,就回滚kafka的事务,并对外抛出异常。
这样,就能实现我们的第一个目标,kafka事务的自动启用。
在AOP中,还有一个重要的功能,那就是切点,也就是在哪个地方使用代理方法,在切点中,可以指定对一个方法、一个类进行代理,也可以对某个注解标识的方法进行代理。
既然可以对注解标识的方法进行代理,那就实现了我们的第二个目标,和代码逻辑解耦,做到一个注解使用事务。
下面,就来实操。
先引入kafka的maven依赖包
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>3.0.0</version>
</dependency>
kafka的生产者创建代码如下:
@Bean("myKafkaProducer")
public KafkaProducer<String,String> kafkaProducer(){
Properties props = new Properties();
props.setProperty("bootstrap.servers", "127.0.0.1:9092");
//禁止自动提交
props.setProperty("enable.auto.commit", "false");
props.setProperty("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.setProperty("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//设置消费者从头开始消费
props.setProperty("auto.offset.reset", "earliest");
//开启幂等性
props.setProperty("enable.idempotence", "true");
//设置事务id,事务id必须唯一,因为事务id是用来分配事务协调器的,且事务id相同的话后面的事务id会覆盖前面的事务id,不适用实例化多个生产者的情况
props.setProperty("transactional.id", "product-transactional-id-1");
//设置客户端id
props.setProperty("client.id", "product-client-id-1");
//设置ack确认机制
props.setProperty("acks", "all");
//设置重试次数
props.setProperty("retries", "3");
//设置批量发送的大小为1,保证broken的顺序性
props.setProperty("max.in.flight.requests.per.connection", "1");
//创建生产者
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
//初始化事务
producer.initTransactions();
return producer;
}
生产者的定义,需要注意以下几点:
这个其实就很容易了,只要使用过SpringBoot的观众应该都很清楚,只要在配置类名上面加上Configuration注解,并在方法中加入Bean注解,就交给Spring容器管理了。
这里需要注意的是要给生产者起一个名字,这样更容易区分和注入自定义的生产者到代码中。
完整代码如下
@Configuration
public class KafkaProducerConfig {
@Bean("myKafkaProducer")
public KafkaProducer<String,String> kafkaProducer(){
Properties props = new Properties();
props.setProperty("bootstrap.servers", "127.0.0.1:9092");
//禁止自动提交
props.setProperty("enable.auto.commit", "false");
props.setProperty("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.setProperty("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//设置消费者从头开始消费
props.setProperty("auto.offset.reset", "earliest");
//开启幂等性
props.setProperty("enable.idempotence", "true");
//设置事务id,事务id必须唯一,因为事务id是用来分配事务协调器的,且事务id相同的话后面的事务id会覆盖前面的事务id,不适用实例化多个生产者的情况
props.setProperty("transactional.id", "product-transactional-id-1");
//设置客户端id`在这里插入代码片`
props.setProperty("client.id", "product-client-id-1");
//设置ack确认机制
props.setProperty("acks", "all");
//设置重试次数
props.setProperty("retries", "3");
//设置批量发送的大小为1,保证broken的顺序性
props.setProperty("max.in.flight.requests.per.connection", "1");
//创建生产者
KafkaProducer<String, String> producer = new KafkaProducer<>(props);
//初始化事务
producer.initTransactions();
return producer;
}
}
这个注解的名称,大家可以随意定义,我这里为了尽可能和S的事务使用习惯一样,就命名为了KafkaTransactional,多加了Kafka。
具体的定义如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface KafkaTransactional {
// 注解标识值
String value() default "";
// 描述信息
String description() default "";
}
这一步,需要你拥有一些SpringBoot的AOP的一些前置知识,如果不知道的话可以去搜一下,我就略过讲解,直接上代码了。
@Aspect
@Component
public class KafkaTransactionAopConfig {
@Resource(name = "myKafkaProducer")
private KafkaProducer<String,String> kafkaProducer;
@Pointcut("@annotation(com.tmx.inject.KafkaTransactional)")
public void kafkaTransactionAop() {
}
@Around("kafkaTransactionAop()")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
log.info("kafkaTransaction -- before");
kafkaProducer.beginTransaction();
try {
proceedingJoinPoint.proceed();
kafkaProducer.commitTransaction();
} catch (Throwable t) {
log.info("kafka transaction error,回滚");
kafkaProducer.abortTransaction();
//为了使用mybatis的事务,这里需要抛出异常
throw new RuntimeException("kafka事务异常,请处理");
}
log.info("kafkaTransaction -- after");
}
}
可以看到,我们先定义了一个切面,并且给spring管理,在Pointcut切点配置中,我切的是使用了KafkaTransactional注解的方法,只要使用了这个注解的方法都会被aroundAdvice这个方法织入,在业务代码执行前先开启事务,业务执行无异常提交事务,业务执行有异常就回滚事务。
在上线的工作都完成之后,使用起来就很简单了,就像是使用Transactional注解一样,你只要在需要实现kafka事务的service层方法上加入这个KafkaTransactional事务注解即可,如果还需要使用数据库的事务,则把Transactional一起加上就可以了,如下所示:
@Transactional(rollbackFor = Exception.class)
@KafkaTransactional
public void testTrancation1(){
longListHashMap.put(5L, goods1Days);
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.set(User::getModifyTime, LocalDateTime.now());
wrapper.eq(User::getId, 1);
//数据库更新
UserService.update(null,wrapper);
//kafka消息发送
kafkaService.sendMsg();
}
以上,就是在springboot中自定义注解实现kafka事务的全过程,我们用到了springboot的AOP功能来实现这个业务需求,可以看到,在使用了aop功能之后,实现起来还是较为轻松的。