spring-boot Kafka事務(wù)

需求

在使用@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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,415評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,647評(píng)論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,130評(píng)論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,366評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,887評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,737評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,939評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,174評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評(píng)論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,608評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,914評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容