利用SpringBoot的AOP自定义注解实现kafka分布式事务

背景

在springboot中,只要引入了kafka的springboot依赖,并且在配置文件中加入了事务id前缀,spring就能够在事务管理器中自动注入kakfa的事务,在代码中只要使用spring提供的Transaction注解就能使用kafka的事务。

但是在代码中,用到Transaction这个注解的不只是kafka,多数情况下,这个注解的使用范围是用在传统数据库中的事务的,比如说mysql等,如果一个方法中既要实现mysql的事务,又要实现基于kafka的分布式事务,那么只使用一个注解,将很容易混淆,且增加对代码排错的复杂度,而且在实际的测试过程中,如果同时引入了kafka的事务和mybatisPlus的事务,那么在代码逻辑异常的时候,mybatis的事务将会失效。

所以为了避免在方法上使用同一个Transaction注解造成的事务失效和代码复杂度,就有必要重新写一个专门用于kafka事务的注解。

目标

此次的目标:

  • 实现kafka事务的自动启用
  • 和代码逻辑解耦,做到一个注解使用事务

实现

因为在项目中使用的springboot框架,而springboot框架天生就有一项很重要的能力能够实现上面的目标,那就是:AOP。

在spring的Aop中,有一个很重要的东西,那就是环绕通知,它可以实现在方法执行的前后,执行自定义的代码逻辑。

那么,对应到kafka的事务,就是在调用方法开始前开启kafka的事务,原方法执行通过无异常,就提交事务;原方法有异常,就回滚kafka的事务,并对外抛出异常。

这样,就能实现我们的第一个目标,kafka事务的自动启用。

在AOP中,还有一个重要的功能,那就是切点,也就是在哪个地方使用代理方法,在切点中,可以指定对一个方法、一个类进行代理,也可以对某个注解标识的方法进行代理。

既然可以对注解标识的方法进行代理,那就实现了我们的第二个目标,和代码逻辑解耦,做到一个注解使用事务。

下面,就来实操。

定义kafka生产者,并交给spring容器管理

定义kafka生产者

先引入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;
    }

生产者的定义,需要注意以下几点:

  • 在集群部署有多个实例的情况下设置transactional.id属性的时候,需要为每一个实例分配不同的事务id,因为事务id和事务协调器是一一对应,每个事务id只能存在于一个实例,如果有多个相同事务id的实例启动,事务协调器只会记录最后实例的IP
  • client.id是实例的唯一标识,这个建议最好每一个实例标识都不要一样
  • enable.idempotence这个是幂等性的开关,一定要开启,才能保证分布式事务的成功
  • 创建完生产者后,一定要先初始化事务,因为事务只需要初始化一次就够了,放在代码中初始化将会报错

生产者交给Spring容器管理

这个其实就很容易了,只要使用过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;
    }
}

定义kafka事务注解

这个注解的名称,大家可以随意定义,我这里为了尽可能和S的事务使用习惯一样,就命名为了KafkaTransactional,多加了Kafka。
具体的定义如下:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface KafkaTransactional {
    // 注解标识值
    String value() default "";
    // 描述信息
    String description() default "";
}

使用AOP环绕编写事务代理方法

这一步,需要你拥有一些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功能之后,实现起来还是较为轻松的。

你可能感兴趣的:(分布式,spring,boot,kafka)