欢迎光临
我们一直在努力

【SpringBoot 3.x 第292节】夯爆了,Kafka 幂等生产者与事务生产者的源码级区别,一文带你吃透(超详解)!

🏆 本文收录于 《滚雪球学 Spring Boot 3.x》 专栏,专注 Spring Boot 3.x 系统学习与实战进阶,内容持续更新中。

本专栏致力于打造高质量、体系化的 Spring Boot 3.x 学习路线,从零基础入门到进阶实战,帮助你快速掌握核心知识与企业级开发经验。   📌 想快速定位学习内容,可查看:【Spring Boot 3 教程导航帖】

🎉 当前专栏限时优惠中:一次订阅,终身阅读,后续更新章节全部免费解锁 👉 立即订阅 👈️   🎁 想系统打通 Spring Boot 各主流版本?还可以查看: 👉 《Spring Boot 2.x 实战》 👉 《Spring Boot 3.x 实战》 👉 《Spring Boot 4.x 实战》

🚀 推荐合集:《Spring Boot 全栈实战合集》,一站式覆盖 Spring Boot 2.x、3.x、4.x 核心特性、项目实战与架构进阶内容。

演示环境说明:

  • 开发工具:IDEA 2021.3
  • JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
  • Spring Boot版本:3.5.4(于25年7月24日发布)
  • Maven版本:3.8.2 (或更高)
  • Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
  • 操作系统:Windows 11

全文目录:

  • 1. 为什么这个主题值得放进 Spring Boot 3 专栏
  • 2. Spring Boot 3.x 学习这类问题时要先建立什么技术基线
    • 2.1 Spring Boot 3.x 的技术前提
    • 2.2 本文的知识目标
    • 2.3 文章中的工程约定
  • 3. Kafka Producer 发送链路全景:从业务代码到 Broker 落盘
    • 3.1 从业务调用角度看发送链路
    • 3.2 绘图看整体链路
    • 3.3 几个关键内部类必须先认识
      • (1)KafkaProducer
      • (2)RecordAccumulator
      • (3)Sender
      • (4)TransactionManager
      • (5)ProducerStateManager(Broker 端)
  • 4. 为什么会出现重复消息、乱序、半成功、跨主题不一致
    • 4.1 最典型的重复场景
    • 4.2 乱序从哪里来
    • 4.3 跨多个 Topic/Partition 的“半成功”
    • 4.4 为什么业务层去重不等于 Kafka 幂等
  • 5. 幂等生产者:解决了什么问题,没有解决什么问题
    • 5.1 什么叫幂等生产者
    • 5.2 幂等生产者主要依赖哪些条件
      • (1)为什么必须 `acks=all`
      • (2)为什么必须 `retries>0`
      • (3)为什么 `max.in.flight` 不能太大
    • 5.3 幂等生产者的核心机制:PID + Sequence
    • 5.4 幂等生产者没有解决什么问题
      • (1)没有解决跨多个 Topic/Partition 的原子提交
      • (2)没有解决跨 Producer Session 的完整语义
      • (3)没有解决业务层主动重复发送
      • (4)没有解决消费端看到未完成事务的问题
  • 6. 事务生产者:在幂等之上又补了哪些能力
    • 6.1 事务生产者的本质
    • 6.2 为什么事务一定建立在幂等之上
    • 6.3 事务生产者相比幂等多了什么
      • (1)事务身份:`transactional.id`
      • (2)事务状态机
      • (3)事务协调器与事务日志
      • (4)消费者可见性控制
    • 6.4 事务生产者能解决哪些场景
  • 7. 源码级对比:幂等生产者与事务生产者到底差在哪
    • 7.1 两者共用的底座:KafkaProducer 还是同一个入口
    • 7.2 源码视角下的共同点
    • 7.3 第一层区别:是否显式开启事务 API
    • 7.4 第二层区别:状态机复杂度不同
    • 7.5 第三层区别:事务生产者需要 `transactional.id`
    • 7.6 第四层区别:是否要向事务中注册分区
    • 7.7 第五层区别:是否要生成事务完成标记
    • 7.8 第六层区别:消费端协作要求不同
    • 7.9 用表格总结源码级差异
    • 7.10 从 Spring Boot 3.x 角度再翻译一次
  • 8. Spring Boot 3.x 下的工程化接入思路
    • 8.1 为什么不建议把所有 Producer 都做成事务型
    • 8.2 本文案例的项目结构
    • 8.3 Maven 依赖
      • 代码解析
    • 8.4 基础配置文件
      • 配置解析
  • 9. 可运行案例一:幂等生产者实战
    • 9.1 主题配置
      • 代码解析
    • 9.2 定义订单事件 DTO
      • 代码解析
    • 9.3 幂等 Producer 配置
      • 代码解析
    • 9.4 幂等发送服务
      • 代码解析
    • 9.5 控制器:触发幂等发送
      • 代码解析
    • 9.6 幂等案例小结
  • 10. 可运行案例二:事务生产者实战
    • 10.1 定义事务相关 DTO
    • 10.2 事务 Producer 配置
      • 代码解析
        • (1)事务 ProducerFactory 和幂等 ProducerFactory 是两套
        • (2)`setTransactionIdPrefix()` 是 Spring 风格的重要桥接点
        • (3)为什么要有 `KafkaTransactionManager`
    • 10.3 事务发送服务
      • 代码解析
    • 10.4 从源码角度理解 `executeInTransaction`
    • 10.5 为什么事务型模板不建议在事务外随便发
    • 10.6 用 Mermaid 看事务型发送路径
  • 11. 可运行案例三:数据库与 Kafka 发送协同的设计思路
    • 11.1 先说结论:Kafka 事务 ≠ 数据库 + Kafka 的全局 XA
    • 11.2 为什么业务上还会把它们一起使用
    • 11.3 本文给出的教学方案:本地事务 + Kafka 发送协同
      • 实体类
      • Repository
      • 协同服务
      • 代码解析
    • 11.4 为什么很多公司最终会落到 Outbox Pattern
  • 12. 消费端如何配合事务生产者实现端到端语义
    • 12.1 为什么消费者要关心事务
    • 12.2 `read_committed` 的意义
    • 12.3 简单消费者示例
      • 代码解析
  • 13. 深入源码思维:为什么说事务生产者比幂等生产者“重得多”
    • 13.1 幂等的核心复杂度在哪里
    • 13.2 事务的核心复杂度在哪里
    • 13.3 为什么事务异常常常比幂等异常更难处理
  • 14. 高频误区与线上避坑
    • 14.1 误区一:开启幂等就等于彻底没有重复消息
    • 14.2 误区二:事务生产者就是“更强的幂等”
    • 14.3 误区三:事务型 KafkaTemplate 可以当普通模板随便用
    • 14.4 误区四:Kafka 事务能直接解决数据库与消息的一切一致性问题
    • 14.5 误区五:消费者不配 `read_committed` 也没关系
    • 14.6 误区六:所有业务都应该上事务 Producer
  • 15. 一图总结:幂等与事务的关系
  • 16. 面试题与答题模板
    • 16.1 面试题:Kafka 幂等生产者和事务生产者有什么区别?
      • 参考回答
    • 16.2 面试题:为什么事务一定需要 `transactional.id`?
      • 参考回答
    • 16.3 面试题:幂等能保证 exactly-once 吗?
      • 参考回答
  • 17. 全文总结
    • 17.1 从概念上总结
    • 17.2 从源码思路上总结
    • 17.3 从 Spring Boot 3.x 工程实践上总结
    • 17.4 给零基础读者的学习建议
    • 课后思考题
    • 小结
    • 🧧 学习福利 · 限时开放 🧧
    • 🫵 Who am I?

1. 为什么这个主题值得放进 Spring Boot 3 专栏

很多同学第一次接触 Kafka 时,会把注意力放在三个表面问题上:

  • 生产者怎么发消息;
  • 消费者怎么收消息;
  • Spring Boot 怎么配 application.yml。

但真正到了线上,决定系统稳定性的往往不是“能不能发”,而是:

  • 失败重试时会不会出现重复消息;
  • 多条消息要么一起成功,要么一起失败时怎么办;
  • 跨多个 Topic 的写入是否原子;
  • 业务服务重启、Broker 故障、网络抖动后语义是否还能成立;
  • Spring Boot 中的 KafkaTemplate、ProducerFactory、事务管理器到底把底层封装成了什么样子。

而“Kafka 幂等生产者”与“Kafka 事务生产者”正好就是这几个问题的核心答案。

对初学者来说,这两个概念最容易被混淆:

  • 幂等生产者:很多人以为“开启幂等 = 完整 exactly-once”。
  • 事务生产者:很多人又以为“事务生产者 = 幂等生产者多了一个 commit()”。

实际上,这两者虽然相关,但保证边界完全不同。

如果你只是记住:

  • enable.idempotence=true 可以去重;
  • transactional.id 可以事务;

那么你在面试和项目里都还停留在“会背配置”的阶段。

真正的进阶点是:

  • 你要知道 Kafka 为什么能去重;
  • 你要知道 为什么去重只在单 Producer Session 内成立;
  • 你要知道 为什么事务生产者一定建立在幂等之上;
  • 你要知道 Spring Boot 3.x 里一个 transactional KafkaTemplate 为什么不能随便在事务外使用;
  • 你要知道 源码里负责这些状态管理的关键类是谁。
  • 所以,这篇文章会有意采用“从浅入深”的写法:先把概念说清,再进入源码级差异,最后落到 Spring Boot 3.x 工程实践。

    2. Spring Boot 3.x 学习这类问题时要先建立什么技术基线

    在进入 Kafka 细节之前,我们要先明确本文的技术上下文。

    2.1 Spring Boot 3.x 的技术前提

    Spring Boot 3.x 相比 2.x,有几个你必须知道的基础变化:

  • JDK 17 成为主流起步版本。这意味着我们在示例里可以放心使用更现代的 Java 写法,但本文仍以“容易看懂”为优先,不会为了炫技而牺牲可读性。
  • Spring Framework 6 / Jakarta EE 9+。也就是说,很多包名已经从 javax.* 切换为 jakarta.*。如果你从旧项目迁移过来,这一点必须敏感。
  • Spring Boot 对 Kafka 的整合更加工程化。它不只是把 KafkaTemplate 暴露给你,而是通过自动配置、属性绑定、事务管理器、监听容器工厂等,把底层 Kafka client 的能力包装成更符合 Spring 风格的使用方式。
  • 对于本文主题来说,最重要的一点是:

    在 Spring Boot 3.x 中,你学习 Kafka 事务,不应该只盯着原生 KafkaProducer API;你还要理解 Spring 的 ProducerFactory、KafkaTemplate、KafkaTransactionManager 是如何把底层事务能力桥接到业务代码的。

    2.2 本文的知识目标

    读完本文,你至少应该能回答以下问题:

    • 为什么 Kafka 生产者在重试时可能产生重复消息?
    • 幂等生产者如何借助 PID、sequence number、broker 端状态记录 去重?
    • 事务生产者为什么要有 transactional.id?它和幂等的关系是什么?
    • 为什么“幂等”主要解决单分区、单会话下的重复写入问题,而事务可以把多个分区/多个 Topic 的写入变成一个原子单元?
    • Spring Boot 3.x 下如何分别配置幂等生产者和事务生产者?
    • KafkaTemplate 在事务模式下为什么会要求在事务上下文中使用?
    • 消费端如果不配 read_committed,事务生产者的价值为什么会打折?

    2.3 文章中的工程约定

    为了让案例更贴近真实项目,本文统一采用如下约定:

    • Java 17
    • Spring Boot 3.3.x 风格项目
    • spring-kafka 3.x 体系
    • 消息体使用 JSON
    • 案例领域采用“订单创建 / 审计日志 / 库存预占”这样的电商语义

    这套约定既适合入门,也方便你以后迁移到真实业务。

    3. Kafka Producer 发送链路全景:从业务代码到 Broker 落盘

    在对比幂等生产者与事务生产者之前,先要弄明白:普通 Producer 到底是怎么把一条消息发出去的。

    如果这个链路不清楚,你后面看到 PID、Sequence、TxnCoordinator、事务状态机时会觉得像“凭空多出来的一层魔法”。

    3.1 从业务调用角度看发送链路

    在 Spring Boot 3.x 项目里,我们最常见的发送代码通常长这样:

    kafkaTemplate.send("order-events", orderNo, event);

    表面上就一行,但底下至少经过了以下环节:

  • 业务代码调用 KafkaTemplate;
  • KafkaTemplate 向 ProducerFactory 获取 Producer;
  • ProducerFactory 最终创建或复用底层 KafkaProducer;
  • KafkaProducer 对消息进行序列化、分区计算;
  • 消息进入 RecordAccumulator 做批量缓存;
  • 后台 Sender 线程从缓存中取批次并发起网络请求;
  • Broker 接收 ProduceRequest,写入日志;
  • Broker 返回 ACK;
  • 客户端回调 Future/Callback,业务线程获得结果。
  • 这条链路中,幂等机制与事务机制都不是“额外插一个 if 判断”那么简单,而是嵌入到了生产者状态管理、批次管理、请求协议和 Broker 状态校验里面。

    3.2 绘图看整体链路

    示意图绘制如下,仅供参考:

    这个图先记住,后面我们会在这个图上不断“加层”:

    • 幂等生产者会在 KafkaProducer → Broker 这条链路上增加 Producer 身份与序列校验;
    • 事务生产者会再增加 事务状态机、分区注册、提交/回滚标记、消费者可见性控制。

    3.3 几个关键内部类必须先认识

    后面分析源码时,你会频繁见到下面这些类名:

    (1)KafkaProducer

    客户端入口。发送、flush、事务 API 都在这里暴露。无论是普通发送、幂等发送还是事务发送,最外层都是这个类。

    (2)RecordAccumulator

    消息并不是一条一条立刻发出去,而是先按分区聚合成批次。这个累加器决定了 Producer 的吞吐与批量发送行为。

    (3)Sender

    真正负责网络 I/O 的后台线程。业务线程把消息塞进缓存,真正的发送工作主要由它完成。

    (4)TransactionManager

    名字虽然叫“TransactionManager”,但它在 Kafka Producer 里并不只服务于显式事务。它本质上承担了Producer 侧身份、序列、事务状态等管理工作。后面你会看到,幂等和事务在这里就开始分层了。

    (5)ProducerStateManager(Broker 端)

    这是 Broker 端的重要状态组件。它维护某个分区中 Producer 的写入元数据,比如最后的 sequence、epoch、offset 等。幂等之所以能成立,不是客户端自己说“我去重了”就行,而是 Broker 端也必须记状态并做校验。

    把这五个角色建立起来,你再看源码差异会清晰很多。

    4. 为什么会出现重复消息、乱序、半成功、跨主题不一致

    很多人学 Kafka 时会先背“消息中间件默认至少一次”,但并没有把这个结论拆开理解。

    实际上,重复消息的产生并不神秘,它来自于两个非常朴素的事实:

  • 网络调用可能失败;
  • 失败后客户端通常会重试。
  • 问题就在于:客户端并不总能准确知道上一条请求到底是“没成功”,还是“成功了但 ACK 丢了”。

    4.1 最典型的重复场景

    举个最经典的例子。

  • Producer 向 Broker 发送一条消息;
  • Broker 已经成功写入日志;
  • Broker 返回 ACK 的瞬间网络抖动;
  • Producer 没收到 ACK,以为失败;
  • Producer 触发重试,再发一次同样的消息。
  • 对于 Broker 来说,如果没有额外机制,它根本不知道第二次是不是“第一次的重试”,还是“业务真的要再发一条同样内容的消息”。于是重复消息就出现了。

    4.2 乱序从哪里来

    另一个经常被忽略的问题是乱序。

    比如同一分区内有两个批次:

    • 批次 A 先发;
    • 批次 B 后发;
    • A 因网络问题失败,B 却先成功;
    • A 重试成功。

    那么最终 Broker 看到的顺序就可能是 B 在前,A 在后。这时不仅重复有风险,顺序也会被打乱。

    4.3 跨多个 Topic/Partition 的“半成功”

    普通 Producer 还有一个非常现实的问题:

    假设你的业务逻辑是:

    • 发一条订单主消息到 order-events;
    • 再发一条审计消息到 audit-events;
    • 再发一条库存预占消息到 stock-events。

    如果前两条成功,第三条失败,那么系统就会进入一种尴尬状态:

    • 主流程看起来订单已经创建;
    • 审计日志也记下来了;
    • 但库存预占没通知出去。

    这时候就不是“重复”问题,而是“跨多条消息、多个主题的原子一致性”问题。幂等生产者解决不了这个问题,事务生产者才是为这个场景设计的。

    4.4 为什么业务层去重不等于 Kafka 幂等

    有些同学会说:“那我自己给消息加一个全局唯一 ID,不就能消费端去重了吗?”

    这种做法当然有价值,但它和 Kafka 幂等不是一个层次:

    • 业务去重,是你在业务语义层补偿;
    • Kafka 幂等,是客户端和 Broker 在消息写入协议层做去重。

    两者不是互斥关系,而是上下两层:

    • 协议层尽量不产生重复;
    • 业务层仍然要对关键操作保持幂等意识。

    这也是为什么成熟系统不会只迷信某一个开关,而是把“消息协议语义 + 业务语义”同时设计好。

    5. 幂等生产者:解决了什么问题,没有解决什么问题

    现在我们正式进入幂等生产者。

    5.1 什么叫幂等生产者

    Kafka 幂等生产者的目标非常明确:

    当同一个 Producer 因重试导致同一条记录被重复发送时,Broker 能够识别出“这其实是同一条逻辑消息的重发”,从而避免把它写成多份重复数据。

    注意这里有几个关键词:

    • 同一个 Producer;
    • 重试导致的重复发送;
    • Broker 能识别重发。

    也就是说,幂等生产者并不是“把所有重复消息都消灭”,它的能力边界是有前提的。

    5.2 幂等生产者主要依赖哪些条件

    要让幂等真正成立,Kafka 对 Producer 配置有明确要求:

    • enable.idempotence=true
    • acks=all
    • retries>0
    • max.in.flight.requests.per.connection<=5

    很多初学者把这几项当成死记硬背,其实它们背后都有原因:

    (1)为什么必须 acks=all

    因为幂等不是只追求“没重复”,还要尽量避免在主从切换下造成语义不一致。acks=all 表示 Leader 要等 ISR 副本都确认后再返回,语义最强。

    (2)为什么必须 retries>0

    如果你根本不重试,那幂等去重机制就失去主要价值了。幂等本来就是为“可安全重试”服务的。

    (3)为什么 max.in.flight 不能太大

    因为 Broker 侧只会为每个 Producer 保留有限数量的批次序列窗口。如果同时挂起太多未确认请求,会让顺序控制和重复检测变复杂,甚至破坏保证。

    5.3 幂等生产者的核心机制:PID + Sequence

    幂等并不是靠“消息体内容相同就判重”,那样太不可靠。Kafka 的设计更底层,它依赖的是:

    • Producer ID(PID):给生产者分配一个身份;
    • Sequence Number:针对分区写入递增序号;
    • Broker 端状态记录:Broker 记住这个 Producer 在这个分区最后成功写到了哪个序号。

    于是一个简化后的思路就是:

  • Producer 拿到 PID;

  • Producer 向某个分区发批次时附带 sequence;

  • Broker 收到后检查:

    • 如果 sequence 正好是期望值,说明是正常新写入;
    • 如果 sequence 比期望值小,说明可能是重复重试;
    • 如果 sequence 不连续,说明可能出现乱序或状态异常。
  • 这就是幂等能成立的根本原因:不是客户端单边承诺,而是客户端携带状态,Broker 严格校验状态。

    5.4 幂等生产者没有解决什么问题

    这是最关键的一段。

    幂等生产者主要没有解决下面几类问题:

    (1)没有解决跨多个 Topic/Partition 的原子提交

    你连续发三条消息,其中前两条成功,第三条失败。幂等不会帮你把前两条自动回滚。

    (2)没有解决跨 Producer Session 的完整语义

    如果 Producer 重启了,新的进程虽然可以重新建立状态,但“上一轮会话中的业务重复提交”并不是幂等机制天然全兜底的。

    (3)没有解决业务层主动重复发送

    如果你的应用代码自己调用了两次 send(),即便消息内容一模一样,Kafka 也不会把它们简单当成“重复重试”处理,因为从协议角度看,它们就是两次独立发送。

    (4)没有解决消费端看到未完成事务的问题

    这听起来像事务才有的问题,没错——因为幂等根本不控制“多条消息作为一个事务单元的可见性”。

    所以一句话概括:

    幂等生产者解决的是“单 Producer 在可重试发送时的重复写入问题”,而不是“分布式事务的一致性问题”。

    6. 事务生产者:在幂等之上又补了哪些能力

    理解了幂等之后,再看事务就容易了。

    6.1 事务生产者的本质

    事务生产者本质上是在幂等基础上进一步增强,使得 Producer 可以把:

    • 多条消息;
    • 多个分区;
    • 多个 Topic;
    • 甚至消费位点提交(offset commit)

    组成一个原子事务单元。

    也就是说,事务生产者不是简单地“去重更强一点”,而是让 Kafka 具备了:

    • 要么全部成功可见;
    • 要么全部失败不可见;

    这样的提交语义。

    6.2 为什么事务一定建立在幂等之上

    因为事务的前提就是:

    • 事务内的每次发送不能因为重试而产生副作用;
    • 同一个事务过程中的批次、分区、状态变更必须可识别;
    • Producer 重启后还要能进行事务恢复和旧实例隔离。

    所以 Kafka 的设计是:

    一旦你配置了 transactional.id,幂等会被自动启用。

    这不是“框架善意附赠”,而是协议层设计要求。

    6.3 事务生产者相比幂等多了什么

    至少多了四层能力:

    (1)事务身份:transactional.id

    幂等只需要把“当前 Producer 会话”识别出来;

    事务则需要一个更稳定的逻辑身份,让 Producer 在重启后还能恢复事务语义,并且对旧实例进行 fencing(围栏隔离)。这就是 transactional.id 的意义。

    (2)事务状态机

    事务不是发完就结束,它至少有:

    • 初始化事务;
    • 开启事务;
    • 向事务中加入分区;
    • 发送消息;
    • 提交事务;
    • 回滚事务。

    这意味着客户端要维护一个严格状态机,Broker 也要配合事务协调。

    (3)事务协调器与事务日志

    事务不是 Producer 自己说了算。Producer 要和事务协调器交互,Broker 集群要记录事务状态。这也是为什么事务比单纯幂等更重。

    (4)消费者可见性控制

    事务提交之前,消息并不是对所有消费者都等价可见。只有消费者使用 read_committed,才能真正实现“只读已提交事务消息”的效果。

    6.4 事务生产者能解决哪些场景

    典型适用场景有:

    • 一个订单操作要向多个 Topic 同时发通知;
    • 一边消费消息,一边处理业务,再把结果写回 Kafka,并希望 offset 提交与结果发送保持原子性;
    • 流处理场景中需要更强的一致性语义。

    但也要注意:

    Kafka 事务不是万金油,它并不等于跨数据库、缓存、RPC、消息系统的全局 XA。

    这点在 Spring Boot 项目里尤其要讲清楚,否则很容易把 Kafka 事务误用成“万能分布式事务”。

    7. 源码级对比:幂等生产者与事务生产者到底差在哪

    这一节是全文核心。我们不满足于停留在“配置不同”,而是要看源码层面的能力差异。

    7.1 两者共用的底座:KafkaProducer 还是同一个入口

    不管你是普通 Producer、幂等 Producer 还是事务 Producer,最外层都是 KafkaProducer。

    这说明一个重要事实:

    Kafka 没有为事务单独造一个完全不同的 Producer 类,而是在同一个 Producer 主体里,通过配置、状态管理和协议行为,让不同模式呈现出不同语义。

    也就是说,幂等与事务不是“两个平行产品”,而是同一套 Producer 体系下的不同工作模式。

    7.2 源码视角下的共同点

    两者的共同点包括:

  • 都使用 KafkaProducer 作为入口;
  • 都依赖后台 Sender 线程执行真正的网络发送;
  • 都使用 RecordAccumulator 进行批量聚合;
  • 都依赖 Broker 端保留 Producer 状态;
  • 都会用到 Producer 的序列信息来保证重试时语义可控。
  • 这也是为什么很多人第一次读源码时会疑惑:

    “为什么幂等和事务都和 TransactionManager 有关系?”

    答案是:因为 Kafka 这里的 TransactionManager 并不只是“提交/回滚事务管理器”那么狭义,它承担的是更广义的 Producer 状态协调。

    7.3 第一层区别:是否显式开启事务 API

    幂等生产者对业务代码来说几乎是“无感”的。你还是像普通 Producer 一样调用:

    producer.send(record);

    你通常不会额外写:

    • initTransactions()
    • beginTransaction()
    • commitTransaction()
    • abortTransaction()

    但事务生产者不同,它在原生 API 层明确引入了一整套事务方法。

    也就是说,幂等主要是发送协议增强,事务则是明确暴露给业务使用的事务编排模型。

    7.4 第二层区别:状态机复杂度不同

    幂等生产者虽然也有 Producer 身份和 sequence 管理,但业务线程并不需要显式控制“事务开始 / 提交 / 回滚”的状态。

    事务生产者则必须维护更复杂的状态机。你从源码类 TransactionManager 就能看出来,它要处理的状态切换远比幂等模式复杂得多:

    • 是否已经初始化事务;
    • 当前是否在事务中;
    • 是否正在提交事务;
    • 是否正在回滚事务;
    • 是否已经进入不可恢复错误状态;
    • 是否要把某个分区加入当前事务;
    • 是否要把消费位点也纳入事务。

    换句话说:

    幂等关注的是“单次发送重试的正确性”,事务关注的是“一个事务生命周期内所有动作的正确性”。

    7.5 第三层区别:事务生产者需要 transactional.id

    幂等可以只在当前 Producer 会话内工作,而事务需要一个更稳定的逻辑身份。

    这个身份就是 transactional.id。

    它的意义至少有三个:

  • 让 Kafka 能在 Producer 重启后恢复事务语义;
  • 让系统能识别“新实例”和“旧僵尸实例”;
  • 通过 epoch fencing 阻止旧实例继续写入。
  • 所以你可以把它理解成:

    • 幂等:更像“当前会话的安全重试”;
    • 事务:更像“带身份恢复能力的安全事务写入”。

    7.6 第四层区别:是否要向事务中注册分区

    事务生产者在发送过程中,会把涉及到的分区逐步加入事务上下文。

    这一步很重要,因为事务提交时,Broker 必须知道:

    • 这个事务涉及哪些分区;
    • 这些分区上的数据应该被一起标记为已提交,还是一起标记为已回滚。

    幂等生产者不需要做这种“事务参与分区收集”,因为它不维护跨分区原子单元。

    7.7 第五层区别:是否要生成事务完成标记

    事务提交或回滚后,Kafka 不只是“客户端本地认为结束了”,它还会在日志层面留下事务完成标记。

    这样消费者在 read_committed 模式下才能区分:

    • 哪些记录属于已提交事务,可读;
    • 哪些记录属于已回滚事务,不可读。

    幂等生产者没有这套事务完成标记机制,因为它没有事务边界。

    7.8 第六层区别:消费端协作要求不同

    幂等生产者大多只影响生产端去重行为,对消费端配置没有那么强的耦合要求。

    事务生产者则不同——如果你的消费者仍然用 read_uncommitted,那么未提交或已回滚事务中的记录可能仍然以某种形式被看到,事务语义就不完整了。

    这说明:

    幂等更像 Producer 单边增强;事务则是 Producer、Broker、Consumer 三方协作协议。

    7.9 用表格总结源码级差异

    对比维度幂等生产者事务生产者
    目标 防止重试造成重复写入 保证多条消息/多分区/多 Topic 原子提交
    入口类 KafkaProducer KafkaProducer
    是否自动启用幂等 是自身能力 是,且事务会隐式开启幂等
    是否要求 transactional.id 否 是
    是否需要显式事务 API 否 是:initTransactions/begin/commit/abort
    状态管理复杂度 中等 高
    是否注册事务参与分区 否 是
    是否有提交/回滚标记 否 是
    消费端是否强依赖 read_committed 不强依赖 强依赖
    典型问题边界 单 Producer 重试去重 原子写入与端到端事务语义

    7.10 从 Spring Boot 3.x 角度再翻译一次

    在 Spring Boot 3.x 项目里,上述差异会表现为:

    • 幂等生产者:你主要在 ProducerFactory 或属性中增强底层发送语义,业务代码调用方式几乎不变;
    • 事务生产者:你除了配置 ProducerFactory 外,还会引入 KafkaTransactionManager、事务型 KafkaTemplate、executeInTransaction() 或 @Transactional 等 Spring 事务接入方式。

    所以从工程体验来说:

    幂等更像“升级发送器”;事务更像“建立一套新的发送编排模型”。

    8. Spring Boot 3.x 下的工程化接入思路

    这一节正式进入代码实践前的设计。

    8.1 为什么不建议把所有 Producer 都做成事务型

    很多初学者知道事务更强,就会想:

    “那我直接所有 Producer 都上事务,不就最稳了吗?”

    现实里一般不这么做,原因有三:

  • 事务更重:需要协调器、状态机、提交/回滚流程,吞吐与延迟成本更高;
  • 并不是所有场景都需要原子提交:很多场景只要求重试不重复,幂等就够了;
  • 事务模板对使用方式有要求:事务型 KafkaTemplate 通常要求在事务上下文内使用,误用更容易出问题。
  • 所以比较合理的工程设计是:

    • 普通业务通知:优先考虑幂等生产者;
    • 跨多个 Topic 的原子事件发布:使用事务生产者;
    • 涉及数据库与消息协同:根据一致性要求选择“事务同步 + 补偿 / Outbox / Kafka 事务”等不同方案。

    8.2 本文案例的项目结构

    下面我们准备一套结构清晰的 Spring Boot 3.x 示例工程:

    src/main/java/com/example/kafkademo
    ├── KafkaProducerDemoApplication.java
    ├── config
    │ ├── KafkaTopicsConfig.java
    │ ├── IdempotentProducerConfig.java
    │ ├── TransactionalProducerConfig.java
    │ └── ConsumerConfig.java
    ├── controller
    │ └── OrderEventController.java
    ├── dto
    │ ├── AuditEvent.java
    │ ├── OrderCreatedEvent.java
    │ └── StockReservedEvent.java
    ├── entity
    │ └── OrderLog.java
    ├── repository
    │ └── OrderLogRepository.java
    ├── service
    │ ├── IdempotentOrderProducerService.java
    │ ├── TransactionalOrderProducerService.java
    │ ├── OrderLocalTransactionService.java
    │ └── OrderEventConsumer.java
    └── util
    └── JsonUtil.java

    这个工程的好处是:

    • 幂等案例和事务案例分开;
    • 代码之间可以共用 DTO;
    • 后面做数据库协同示例时也方便扩展;
    • 符合真实项目“配置层、服务层、控制层”分层习惯。

    8.3 Maven 依赖

    下面是一份适合本文案例的 pom.xml。它不是极简依赖,而是有意保留了 Web、Kafka、JPA、H2、测试等模块,便于你后续继续扩展。

    <project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.3.0</version>
    <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>kafka-producer-demo</artifactId>
    <version>1.0.0</version>
    <name>kafka-producer-demo</name>
    <description>Kafka idempotent and transactional producer demo</description>

    <properties>
    <java.version>17</java.version>
    </properties>

    <dependencies>
    <!– Web:用于提供测试接口 –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!– Kafka:Spring Boot 3.x 集成 Kafka 的核心依赖 –>
    <dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
    </dependency>

    <!– JPA:用于演示数据库与 Kafka 协同场景 –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!– H2:本地内存数据库,方便示例运行 –>
    <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
    </dependency>

    <!– 校验依赖 –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

    <!– 监控依赖,便于观察运行情况 –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <!– 测试依赖 –>
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>
    <dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka-test</artifactId>
    <scope>test</scope>
    </dependency>
    </dependencies>

    <build>
    <plugins>
    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    </plugin>
    </plugins>
    </build>
    </project>

    代码解析

    这份依赖设计有几个值得注意的点:

  • 我们没有引入 Lombok。原因很简单:专栏文章优先保证“拿到任何环境都能直接看懂”。
  • JPA + H2 不是为了炫技,而是为了后面演示“数据库本地事务 + Kafka 发布协同”场景。
  • spring-kafka-test 虽然本文不展开完整集成测试,但它是你后续做自动化验证的重要工具。
  • 8.4 基础配置文件

    server:
    port: 8080

    spring:
    application:
    name: kafka–producer–demo
    datasource:
    url: jdbc:h2:mem:testdb;MODE=MYSQL;DB_CLOSE_DELAY=–1;DB_CLOSE_ON_EXIT=FALSE
    driver-class-name: org.h2.Driver
    username: sa
    password:
    h2:
    console:
    enabled: true
    path: /h2–console
    jpa:
    hibernate:
    ddl-auto: update
    show-sql: true
    properties:
    hibernate:
    format_sql: true
    kafka:
    bootstrap-servers: localhost:9092
    producer:
    key-serializer: org.apache.kafka.common.serialization.StringSerializer
    value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
    consumer:
    group-id: demo–consumer–group
    auto-offset-reset: earliest
    key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
    value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
    properties:
    spring.json.trusted.packages: "*"
    isolation.level: read_committed

    management:
    endpoints:
    web:
    exposure:
    include: health,info,metrics

    配置解析

    这里先只保留公共配置,不直接把事务相关参数全塞到 spring.kafka.producer.* 里。原因是:

    • 我们要分别演示幂等 Producer 和事务 Producer;
    • 如果把事务配置全局打开,会让默认模板都变成事务型,反而不利于对比教学。

    所以更好的做法是:公共属性放 application.yml,差异化能力放到各自的 Java Config 中。

    9. 可运行案例一:幂等生产者实战

    先做幂等案例,因为这是理解事务的前置基础。

    9.1 主题配置

    package com.example.kafkademo.config;

    import org.apache.kafka.clients.admin.NewTopic;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.kafka.config.TopicBuilder;

    /**
    * Kafka 主题配置。
    * 这里直接在应用启动时创建主题,便于本地演示。
    */

    @Configuration
    public class KafkaTopicsConfig {

    /**
    * 订单事件主题。
    */

    @Bean
    public NewTopic orderEventsTopic() {
    return TopicBuilder.name("order-events")
    .partitions(3)
    .replicas(1)
    .build();
    }

    /**
    * 审计事件主题。
    */

    @Bean
    public NewTopic auditEventsTopic() {
    return TopicBuilder.name("audit-events")
    .partitions(3)
    .replicas(1)
    .build();
    }

    /**
    * 库存事件主题。
    */

    @Bean
    public NewTopic stockEventsTopic() {
    return TopicBuilder.name("stock-events")
    .partitions(3)
    .replicas(1)
    .build();
    }
    }

    代码解析

    这段代码不是本文重点,但很实用。对于零基础读者来说,本地测试 Kafka 最痛苦的一件事就是:

    “还没进入业务代码,主题都没建。”

    所以在教学文章里把主题创建也纳入配置,是很好的体验优化。

    9.2 定义订单事件 DTO

    package com.example.kafkademo.dto;

    import jakarta.validation.constraints.NotBlank;
    import jakarta.validation.constraints.NotNull;

    import java.math.BigDecimal;
    import java.time.LocalDateTime;

    /**
    * 订单创建事件。
    */

    public class OrderCreatedEvent {

    /**
    * 订单号。
    */

    @NotBlank(message = "订单号不能为空")
    private String orderNo;

    /**
    * 用户编号。
    */

    @NotBlank(message = "用户编号不能为空")
    private String userId;

    /**
    * 订单金额。
    */

    @NotNull(message = "订单金额不能为空")
    private BigDecimal amount;

    /**
    * 创建时间。
    */

    private LocalDateTime createdTime;

    public String getOrderNo() {
    return orderNo;
    }

    public void setOrderNo(String orderNo) {
    this.orderNo = orderNo;
    }

    public String getUserId() {
    return userId;
    }

    public void setUserId(String userId) {
    this.userId = userId;
    }

    public BigDecimal getAmount() {
    return amount;
    }

    public void setAmount(BigDecimal amount) {
    this.amount = amount;
    }

    public LocalDateTime getCreatedTime() {
    return createdTime;
    }

    public void setCreatedTime(LocalDateTime createdTime) {
    this.createdTime = createdTime;
    }
    }

    代码解析

    为什么 DTO 里要有 orderNo 并且后面会把它作为 key?

    因为真实业务里,想保证同一订单相关消息尽可能落到同一分区,你就应该使用业务主键作为 key。这样:

    • 同一订单的消息会按 key 路由到固定分区;
    • 分区内顺序才有意义;
    • 幂等和顺序讨论才更贴近真实场景。

    9.3 幂等 Producer 配置

    package com.example.kafkademo.config;

    import org.apache.kafka.clients.producer.ProducerConfig;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.kafka.core.DefaultKafkaProducerFactory;
    import org.springframework.kafka.core.KafkaTemplate;
    import org.springframework.kafka.core.ProducerFactory;

    import java.util.HashMap;
    import java.util.Map;

    /**
    * 幂等生产者配置。
    */

    @Configuration
    public class IdempotentProducerConfig {

    /**
    * 创建幂等 ProducerFactory。
    *
    * @param kafkaProperties Spring Boot 自动绑定的 Kafka 公共属性
    * @return 幂等 ProducerFactory
    */

    @Bean("idempotentProducerFactory")
    public ProducerFactory<String, Object> idempotentProducerFactory(KafkaProperties kafkaProperties) {
    Map<String, Object> props = new HashMap<>(kafkaProperties.buildProducerProperties());

    // 开启幂等:防止生产者重试造成重复写入
    props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
    // 所有 ISR 副本确认后才返回,保证更强语义
    props.put(ProducerConfig.ACKS_CONFIG, "all");
    // 允许重试,这样幂等能力才有意义
    props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
    // 控制同一连接上未确认请求数,避免破坏幂等与顺序保证
    props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5);
    // 适当设置批量大小与等待时间,提高吞吐
    props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16_384);
    props.put(ProducerConfig.LINGER_MS_CONFIG, 10);

    return new DefaultKafkaProducerFactory<>(props);
    }

    /**
    * 幂等 KafkaTemplate。
    *
    * @param producerFactory 幂等生产者工厂
    * @return KafkaTemplate
    */

    @Bean("idempotentKafkaTemplate")
    public KafkaTemplate<String, Object> idempotentKafkaTemplate(
    @Qualifier("idempotentProducerFactory") ProducerFactory<String, Object> producerFactory) {
    return new KafkaTemplate<>(producerFactory);
    }
    }

    代码解析

    这段配置对应了我们前面讲的幂等前提:

    • 开启 enable.idempotence
    • acks=all
    • 足够的 retries
    • 合理的 max.in.flight

    你会注意到:

    这里没有任何事务 API,也没有 transactional.id。

    这就是幂等 Producer 最典型的工程形态:

    • 你增强的是发送语义;
    • 不是引入事务生命周期管理。

    9.4 幂等发送服务

    package com.example.kafkademo.service;

    import com.example.kafkademo.dto.OrderCreatedEvent;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.kafka.core.KafkaTemplate;
    import org.springframework.kafka.support.SendResult;
    import org.springframework.stereotype.Service;

    import java.time.LocalDateTime;
    import java.util.concurrent.CompletableFuture;

    /**
    * 幂等订单生产服务。
    */

    @Service
    public class IdempotentOrderProducerService {

    private static final Logger log = LoggerFactory.getLogger(IdempotentOrderProducerService.class);

    private final KafkaTemplate<String, Object> idempotentKafkaTemplate;

    public IdempotentOrderProducerService(
    @Qualifier("idempotentKafkaTemplate") KafkaTemplate<String, Object> idempotentKafkaTemplate) {
    this.idempotentKafkaTemplate = idempotentKafkaTemplate;
    }

    /**
    * 发送订单创建事件。
    *
    * @param event 订单创建事件
    */

    public void sendOrderCreatedEvent(OrderCreatedEvent event) {
    // 如果创建时间为空,则由服务端补齐
    if (event.getCreatedTime() == null) {
    event.setCreatedTime(LocalDateTime.now());
    }

    // 以订单号作为消息 key,确保同一订单尽量进入同一分区
    CompletableFuture<SendResult<String, Object>> future =
    idempotentKafkaTemplate.send("order-events", event.getOrderNo(), event);

    future.whenComplete((result, ex) -> {
    if (ex != null) {
    log.error("幂等消息发送失败,orderNo={}", event.getOrderNo(), ex);
    return;
    }

    if (result != null && result.getRecordMetadata() != null) {
    log.info("幂等消息发送成功,topic={}, partition={}, offset={}, orderNo={}",
    result.getRecordMetadata().topic(),
    result.getRecordMetadata().partition(),
    result.getRecordMetadata().offset(),
    event.getOrderNo());
    }
    });
    }
    }

    代码解析

    这段发送代码看起来和普通 Producer 没什么区别,这恰恰就是幂等模式的特点:

    • 业务层调用方式几乎不变;
    • 幂等语义主要体现在底层 Producer 行为上。

    也正因为如此,幂等非常适合“从普通发送平滑升级”的场景。

    9.5 控制器:触发幂等发送

    package com.example.kafkademo.controller;

    import com.example.kafkademo.dto.OrderCreatedEvent;
    import com.example.kafkademo.service.IdempotentOrderProducerService;
    import com.example.kafkademo.service.TransactionalOrderProducerService;
    import jakarta.validation.Valid;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;

    import java.util.HashMap;
    import java.util.Map;

    /**
    * 订单事件控制器。
    */

    @RestController
    @RequestMapping("/api/orders")
    public class OrderEventController {

    private final IdempotentOrderProducerService idempotentOrderProducerService;
    private final TransactionalOrderProducerService transactionalOrderProducerService;

    public OrderEventController(IdempotentOrderProducerService idempotentOrderProducerService,
    TransactionalOrderProducerService transactionalOrderProducerService) {
    this.idempotentOrderProducerService = idempotentOrderProducerService;
    this.transactionalOrderProducerService = transactionalOrderProducerService;
    }

    /**
    * 幂等方式发送订单创建消息。
    */

    @PostMapping("/idempotent")
    public ResponseEntity<Map<String, Object>> sendIdempotent(@Valid @RequestBody OrderCreatedEvent event) {
    idempotentOrderProducerService.sendOrderCreatedEvent(event);

    Map<String, Object> result = new HashMap<>();
    result.put("success", true);
    result.put("mode", "idempotent");
    result.put("orderNo", event.getOrderNo());
    return ResponseEntity.ok(result);
    }

    /**
    * 事务方式发送多条消息。
    */

    @PostMapping("/transactional")
    public ResponseEntity<Map<String, Object>> sendTransactional(@Valid @RequestBody OrderCreatedEvent event) {
    transactionalOrderProducerService.publishOrderEventsInTransaction(event);

    Map<String, Object> result = new HashMap<>();
    result.put("success", true);
    result.put("mode", "transactional");
    result.put("orderNo", event.getOrderNo());
    return ResponseEntity.ok(result);
    }
    }

    代码解析

    控制器里同时暴露两个接口,是为了让你更直观地对比两种模式:

    • /api/orders/idempotent
    • /api/orders/transactional

    它们接收相同的业务入参,但背后的发送编排模型完全不同。

    9.6 幂等案例小结

    到这里你应该已经感受到:

    • 幂等 Producer 使用上并不复杂;
    • 复杂性主要藏在 Kafka client 与 Broker 协议层;
    • 从 Spring Boot 工程角度看,幂等非常适合作为默认增强方案。

    但别忘了,幂等依然只解决“重复重试”问题,不保证“多条消息一起成功”。

    下面我们进入事务 Producer。

    10. 可运行案例二:事务生产者实战

    10.1 定义事务相关 DTO

    先补上两个 DTO,用来模拟订单创建后需要同时发出的两条辅助消息。

    package com.example.kafkademo.dto;

    import java.time.LocalDateTime;

    /**
    * 审计事件。
    */

    public class AuditEvent {

    /**
    * 业务对象编号。
    */

    private String bizNo;

    /**
    * 审计动作。
    */

    private String action;

    /**
    * 事件时间。
    */

    private LocalDateTime eventTime;

    public String getBizNo() {
    return bizNo;
    }

    public void setBizNo(String bizNo) {
    this.bizNo = bizNo;
    }

    public String getAction() {
    return action;
    }

    public void setAction(String action) {
    this.action = action;
    }

    public LocalDateTime getEventTime() {
    return eventTime;
    }

    public void setEventTime(LocalDateTime eventTime) {
    this.eventTime = eventTime;
    }
    }

    package com.example.kafkademo.dto;

    import java.time.LocalDateTime;

    /**
    * 库存预占事件。
    */

    public class StockReservedEvent {

    /**
    * 订单号。
    */

    private String orderNo;

    /**
    * 用户编号。
    */

    private String userId;

    /**
    * 预占时间。
    */

    private LocalDateTime reserveTime;

    public String getOrderNo() {
    return orderNo;
    }

    public void setOrderNo(String orderNo) {
    this.orderNo = orderNo;
    }

    public String getUserId() {
    return userId;
    }

    public void setUserId(String userId) {
    this.userId = userId;
    }

    public LocalDateTime getReserveTime() {
    return reserveTime;
    }

    public void setReserveTime(LocalDateTime reserveTime) {
    this.reserveTime = reserveTime;
    }
    }

    10.2 事务 Producer 配置

    package com.example.kafkademo.config;

    import org.apache.kafka.clients.producer.ProducerConfig;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.kafka.core.DefaultKafkaProducerFactory;
    import org.springframework.kafka.core.KafkaTemplate;
    import org.springframework.kafka.core.ProducerFactory;
    import org.springframework.kafka.transaction.KafkaTransactionManager;

    import java.util.HashMap;
    import java.util.Map;

    /**
    * 事务生产者配置。
    */

    @Configuration
    public class TransactionalProducerConfig {

    /**
    * 创建事务型 ProducerFactory。
    *
    * @param kafkaProperties Spring Boot 自动绑定的 Kafka 公共属性
    * @return 事务型 ProducerFactory
    */

    @Bean("transactionalProducerFactory")
    public ProducerFactory<String, Object> transactionalProducerFactory(KafkaProperties kafkaProperties) {
    Map<String, Object> props = new HashMap<>(kafkaProperties.buildProducerProperties());

    // 事务生产者隐式要求幂等,这里显式写出便于教学理解
    props.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
    props.put(ProducerConfig.ACKS_CONFIG, "all");
    props.put(ProducerConfig.RETRIES_CONFIG, Integer.MAX_VALUE);
    props.put(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION, 5);

    DefaultKafkaProducerFactory<String, Object> producerFactory = new DefaultKafkaProducerFactory<>(props);
    // 设置事务 ID 前缀,Spring 会基于此前缀为底层 Producer 分配事务身份
    producerFactory.setTransactionIdPrefix("demo-tx-");
    return producerFactory;
    }

    /**
    * 事务型 KafkaTemplate。
    */

    @Bean("transactionalKafkaTemplate")
    public KafkaTemplate<String, Object> transactionalKafkaTemplate(
    @Qualifier("transactionalProducerFactory") ProducerFactory<String, Object> producerFactory) {
    return new KafkaTemplate<>(producerFactory);
    }

    /**
    * Kafka 事务管理器。
    * Spring 事务体系通过它把 Kafka Producer 绑定到线程上下文。
    */

    @Bean
    public KafkaTransactionManager<String, Object> kafkaTransactionManager(
    @Qualifier("transactionalProducerFactory") ProducerFactory<String, Object> producerFactory) {
    return new KafkaTransactionManager<>(producerFactory);
    }
    }

    代码解析

    这一段是全文最重要的配置之一。

    你需要特别注意下面几点:

    (1)事务 ProducerFactory 和幂等 ProducerFactory 是两套

    这不是重复劳动,而是为了表达清晰:

    • 幂等 Producer:只负责安全重试;
    • 事务 Producer:负责事务生命周期。
    (2)setTransactionIdPrefix() 是 Spring 风格的重要桥接点

    在原生 Kafka API 里,你会更关注 transactional.id。

    在 Spring Kafka 里,DefaultKafkaProducerFactory 通过 transactionIdPrefix 来管理事务型 Producer 的创建与缓存,这是 Spring 工程化封装的重要体现。

    (3)为什么要有 KafkaTransactionManager

    因为在 Spring 体系中,事务不只是“手动 begin/commit”。

    你可能会通过:

    • @Transactional
    • TransactionTemplate
    • KafkaTemplate.executeInTransaction()

    来驱动事务,而这些能力都离不开事务管理器把资源绑定到线程上下文。

    10.3 事务发送服务

    package com.example.kafkademo.service;

    import com.example.kafkademo.dto.AuditEvent;
    import com.example.kafkademo.dto.OrderCreatedEvent;
    import com.example.kafkademo.dto.StockReservedEvent;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.kafka.core.KafkaTemplate;
    import org.springframework.stereotype.Service;

    import java.time.LocalDateTime;

    /**
    * 事务订单生产服务。
    */

    @Service
    public class TransactionalOrderProducerService {

    private static final Logger log = LoggerFactory.getLogger(TransactionalOrderProducerService.class);

    private final KafkaTemplate<String, Object> transactionalKafkaTemplate;

    public TransactionalOrderProducerService(
    @Qualifier("transactionalKafkaTemplate") KafkaTemplate<String, Object> transactionalKafkaTemplate) {
    this.transactionalKafkaTemplate = transactionalKafkaTemplate;
    }

    /**
    * 在一个 Kafka 事务中发布多条消息。
    *
    * @param event 订单创建事件
    */

    public void publishOrderEventsInTransaction(OrderCreatedEvent event) {
    if (event.getCreatedTime() == null) {
    event.setCreatedTime(LocalDateTime.now());
    }

    AuditEvent auditEvent = new AuditEvent();
    auditEvent.setBizNo(event.getOrderNo());
    auditEvent.setAction("ORDER_CREATED");
    auditEvent.setEventTime(LocalDateTime.now());

    StockReservedEvent stockReservedEvent = new StockReservedEvent();
    stockReservedEvent.setOrderNo(event.getOrderNo());
    stockReservedEvent.setUserId(event.getUserId());
    stockReservedEvent.setReserveTime(LocalDateTime.now());

    transactionalKafkaTemplate.executeInTransaction(operations -> {
    // 第一条:订单创建主事件
    operations.send("order-events", event.getOrderNo(), event);
    // 第二条:审计日志事件
    operations.send("audit-events", event.getOrderNo(), auditEvent);
    // 第三条:库存预占事件
    operations.send("stock-events", event.getOrderNo(), stockReservedEvent);

    log.info("事务消息已全部加入待提交队列,orderNo={}", event.getOrderNo());
    return null;
    });
    }
    }

    代码解析

    这段代码看起来也很简单,但它代表的是发送模式的根本变化:

    • 幂等模式:一条消息就是一次相对独立的发送;
    • 事务模式:多条消息先进入同一个事务上下文,再统一提交。

    也就是说,事务的重点不在于“每条 send 长得不一样”,而在于:

    发送动作不再是彼此独立的,而是被纳入一个统一的提交边界。

    如果中间出现异常,整个事务会失败,而不会留下“主消息成功、辅助消息失败”的半成功状态。

    10.4 从源码角度理解 executeInTransaction

    从设计上看,KafkaTemplate.executeInTransaction() 做了几件关键事情:

  • 从事务型 ProducerFactory 获取一个事务 Producer;
  • 调用 beginTransaction();
  • 执行业务回调里的多次 send();
  • 如果回调正常结束,则 commitTransaction();
  • 如果回调异常,则触发回滚;
  • 最终释放或归还 Producer 资源。
  • 这恰好对应了 Kafka 原生事务 API 的生命周期,只不过 Spring 帮你包装得更符合模板方法模式。

    10.5 为什么事务型模板不建议在事务外随便发

    这是个很重要的实战点。

    如果一个 KafkaTemplate 是基于事务型 ProducerFactory 创建的,那么它通常期望自己运行在事务上下文中。

    原因很简单:

    • 它背后的 Producer 是按事务语义设计的;
    • Spring 也会在发送时检查当前线程是否已有事务资源;
    • 如果你不在事务上下文里随便调用,框架很可能直接抛出异常或表现出与你预期不一致的行为。

    所以工程中比较稳妥的方式是:

    • 非事务发送,使用普通/幂等模板;
    • 事务发送,使用专门的事务模板。

    这比“一把梭哈所有发送都走事务模板”清晰得多。

    10.6 用 Mermaid 看事务型发送路径

    示意图绘制如下,仅供参考:

    这个图里最关键的一步是:将分区加入事务。

    它说明事务不是简单“最后一起 commit”这么口语化,而是 Producer 在发送过程中就不断维护“这个事务影响了哪些分区”的元数据。

    11. 可运行案例三:数据库与 Kafka 发送协同的设计思路

    这一节是很多读者最关心也最容易误用的地方。

    11.1 先说结论:Kafka 事务 ≠ 数据库 + Kafka 的全局 XA

    很多同学一看到 Spring 里既有 @Transactional,又有 KafkaTransactionManager,就容易脑补成:

    “那我数据库插入一行,再发 Kafka,天然就是全局强一致。”

    这并不准确。

    Spring Kafka 官方也明确强调:KafkaTransactionManager 是针对 单个 Kafka ProducerFactory 的事务管理器,它不是 XA 事务管理器。

    所以你一定要记住:

    • 数据库事务管理器:管理数据库连接与本地事务;
    • KafkaTransactionManager:管理 Kafka Producer 与 Kafka 事务;
    • 它们可以被 Spring 协调、串联、同步,但不是传统意义上的两阶段提交 XA。

    11.2 为什么业务上还会把它们一起使用

    因为很多场景里,我们并不追求理论上的全局 XA,而是追求:

    • 足够强的一致性;
    • 清晰的失败策略;
    • 可控的补偿机制;
    • 合理的复杂度。

    比如:

  • 先写数据库订单表;
  • 再发 Kafka 订单创建事件;
  • 如果 Kafka 发送失败,则回滚数据库事务;
  • 或者反过来,先发 Kafka,再提交数据库;
  • 或者采用 Outbox 表进行异步发布。
  • 不同方案适用于不同业务。

    11.3 本文给出的教学方案:本地事务 + Kafka 发送协同

    下面给一个教学用的示例:

    • 数据库保存订单日志;
    • 再通过事务型 KafkaTemplate 发送订单事件;
    • 让你理解 Spring Boot 3.x 中事务边界是如何编排的。

    实体类

    package com.example.kafkademo.entity;

    import jakarta.persistence.*;

    import java.math.BigDecimal;
    import java.time.LocalDateTime;

    /**
    * 订单日志实体。
    */

    @Entity
    @Table(name = "t_order_log")
    public class OrderLog {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
    * 订单号。
    */

    @Column(nullable = false, unique = true, length = 64)
    private String orderNo;

    /**
    * 用户编号。
    */

    @Column(nullable = false, length = 64)
    private String userId;

    /**
    * 订单金额。
    */

    @Column(nullable = false, precision = 18, scale = 2)
    private BigDecimal amount;

    /**
    * 创建时间。
    */

    @Column(nullable = false)
    private LocalDateTime createdTime;

    public Long getId() {
    return id;
    }

    public void setId(Long id) {
    this.id = id;
    }

    public String getOrderNo() {
    return orderNo;
    }

    public void setOrderNo(String orderNo) {
    this.orderNo = orderNo;
    }

    public String getUserId() {
    return userId;
    }

    public void setUserId(String userId) {
    this.userId = userId;
    }

    public BigDecimal getAmount() {
    return amount;
    }

    public void setAmount(BigDecimal amount) {
    this.amount = amount;
    }

    public LocalDateTime getCreatedTime() {
    return createdTime;
    }

    public void setCreatedTime(LocalDateTime createdTime) {
    this.createdTime = createdTime;
    }
    }

    Repository

    package com.example.kafkademo.repository;

    import com.example.kafkademo.entity.OrderLog;
    import org.springframework.data.jpa.repository.JpaRepository;

    import java.util.Optional;

    /**
    * 订单日志仓储。
    */

    public interface OrderLogRepository extends JpaRepository<OrderLog, Long> {

    /**
    * 根据订单号查询。
    */

    Optional<OrderLog> findByOrderNo(String orderNo);
    }

    协同服务

    package com.example.kafkademo.service;

    import com.example.kafkademo.dto.OrderCreatedEvent;
    import com.example.kafkademo.entity.OrderLog;
    import com.example.kafkademo.repository.OrderLogRepository;
    import jakarta.transaction.Transactional;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Service;

    import java.time.LocalDateTime;

    /**
    * 本地数据库事务与 Kafka 发布协同服务。
    *
    * 注意:该示例主要用于教学,帮助理解 Spring 中数据库事务与消息发布的配合方式。
    * 在真正的高一致性生产场景中,仍需根据业务选择更适合的策略,例如 Outbox Pattern。
    */

    @Service
    public class OrderLocalTransactionService {

    private static final Logger log = LoggerFactory.getLogger(OrderLocalTransactionService.class);

    private final OrderLogRepository orderLogRepository;
    private final TransactionalOrderProducerService transactionalOrderProducerService;

    public OrderLocalTransactionService(OrderLogRepository orderLogRepository,
    TransactionalOrderProducerService transactionalOrderProducerService) {
    this.orderLogRepository = orderLogRepository;
    this.transactionalOrderProducerService = transactionalOrderProducerService;
    }

    /**
    * 先写数据库,再发布 Kafka 事务消息。
    *
    * @param event 订单创建事件
    */

    @Transactional
    public void createOrderAndPublish(OrderCreatedEvent event) {
    OrderLog orderLog = new OrderLog();
    orderLog.setOrderNo(event.getOrderNo());
    orderLog.setUserId(event.getUserId());
    orderLog.setAmount(event.getAmount());
    orderLog.setCreatedTime(LocalDateTime.now());

    orderLogRepository.save(orderLog);
    log.info("订单日志已写入数据库,orderNo={}", event.getOrderNo());

    // 发布 Kafka 事务消息
    transactionalOrderProducerService.publishOrderEventsInTransaction(event);
    }
    }

    代码解析

    这段代码的价值不在于“它是不是最终生产最佳方案”,而在于帮你建立正确认知:

  • 数据库本地事务和 Kafka 事务是两套不同资源管理体系;
  • Spring 可以把它们在业务层编排得比较自然;
  • 但这不意味着天然获得全局 XA;
  • 如果你的业务对一致性要求极高,Outbox Pattern 往往是更稳妥的架构方案。
  • 11.4 为什么很多公司最终会落到 Outbox Pattern

    因为 Outbox 的优点是:

    • 业务主数据和待发送事件先写同一个数据库本地事务;
    • 后续由独立任务或 CDC 把 Outbox 表推送到 Kafka;
    • 这样能显著降低“数据库成功但消息没发出去”的窗口。

    所以你可以把本文的三个层次理解为:

    • 幂等:解决发送重试重复;
    • Kafka 事务:解决 Kafka 内部多消息原子提交;
    • Outbox:解决数据库与消息系统协同一致性。

    这三个不是替代关系,而是不同边界的问题。

    12. 消费端如何配合事务生产者实现端到端语义

    只讲 Producer 而不讲 Consumer,这篇文章就不完整。

    12.1 为什么消费者要关心事务

    如果 Producer 已经用了事务,但消费者仍然随便消费,那么你很可能得不到自己想象中的语义。

    原因在于:

    • 事务中的消息可能尚未提交;
    • 已回滚事务中的消息不应对业务可见;
    • 消费者要通过隔离级别决定“我到底读不读这些记录”。

    12.2 read_committed 的意义

    当消费者配置:

    spring:
    kafka:
    consumer:
    properties:
    isolation.level: read_committed

    它的语义就是:

    • 只读取已提交事务中的消息;
    • 跳过未提交或已回滚事务对应的记录。

    这一步非常关键。否则你 Producer 端辛辛苦苦做事务,消费端却没有按事务语义读取,那整体效果就被削弱了。

    12.3 简单消费者示例

    package com.example.kafkademo.service;

    import com.example.kafkademo.dto.OrderCreatedEvent;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.kafka.annotation.KafkaListener;
    import org.springframework.stereotype.Component;

    /**
    * 订单事件消费者。
    */

    @Component
    public class OrderEventConsumer {

    private static final Logger log = LoggerFactory.getLogger(OrderEventConsumer.class);

    /**
    * 监听订单创建主题。
    *
    * @param event 订单创建事件
    */

    @KafkaListener(topics = "order-events", groupId = "demo-consumer-group")
    public void consumeOrderCreatedEvent(OrderCreatedEvent event) {
    log.info("消费到订单事件,orderNo={}, userId={}, amount={}",
    event.getOrderNo(), event.getUserId(), event.getAmount());
    }
    }

    代码解析

    消费者代码本身并不复杂,关键反而在配置:

    • 生产端开启事务;
    • 消费端使用 read_committed。

    这说明 Kafka 事务语义不是“Producer 一边努力就行”,而是消息链路多方共同参与。

    13. 深入源码思维:为什么说事务生产者比幂等生产者“重得多”

    这一节不贴更多源码文件,而是用“源码阅读视角”帮你建立理解框架。

    13.1 幂等的核心复杂度在哪里

    幂等主要复杂在:

    • Producer 端给分区写入维护 sequence;
    • Broker 端维护 ProducerState;
    • 重试时能根据序列判断是不是重复;
    • 需要在吞吐、顺序、重试之间取得平衡。

    它的难点是“安全重试”。

    13.2 事务的核心复杂度在哪里

    事务的复杂度比幂等明显更高,至少多出以下问题:

  • 如何初始化事务身份;
  • 如何保证同一 transactional.id 的旧实例不能继续写;
  • 如何维护事务进行中的状态;
  • 如何动态记录本事务影响了哪些分区;
  • 如何提交事务并让消费者只看见已提交记录;
  • 如何回滚事务并让已写入但未提交的数据对业务不可见;
  • 如何在异常状态下把 Producer 置为不可恢复,避免语义被破坏。
  • 你会发现,事务本质上把问题从“发送一条消息”升级成了“管理一个分布式日志系统中的事务生命周期”。

    13.3 为什么事务异常常常比幂等异常更难处理

    幂等发送失败时,很多场景你只需要判断:

    • 这次 send 是否成功;
    • 是否需要重试;
    • 是否需要人工排查是否重复。

    事务失败时则不同,你还要考虑:

    • 当前事务是否还能继续使用;
    • 是否必须 abort;
    • Producer 是否已经进入 fatal error;
    • 当前事务涉及的消息是否部分写入但未提交;
    • 消费者到底能不能看见这部分数据。

    所以事务型 Producer 的错误处理模型也更复杂,线上排障成本更高。

    14. 高频误区与线上避坑

    这一节很重要,我按“新手最容易犯的错误”来写。

    14.1 误区一:开启幂等就等于彻底没有重复消息

    错。

    幂等主要防的是:

    • 同一 Producer 因重试造成的重复写入。

    它不负责:

    • 应用层重复调用;
    • 业务逻辑重复触发;
    • 跨系统重复投递的所有可能。

    所以关键业务依然要做业务幂等。

    14.2 误区二:事务生产者就是“更强的幂等”

    也不准确。

    更准确的说法是:

    • 事务生产者建立在幂等之上;
    • 但它新增的核心能力是 原子提交与事务可见性控制。

    所以两者不是“强弱版本关系”,而是“包含关系 + 语义升级”。

    14.3 误区三:事务型 KafkaTemplate 可以当普通模板随便用

    这在 Spring Boot 项目里非常常见。

    事务型模板最好在以下方式中使用:

    • executeInTransaction()
    • @Transactional + 正确事务管理器绑定
    • 监听容器启动 Kafka 事务的场景

    否则就可能出现:

    • 没有事务上下文;
    • 模板抛异常;
    • 或者行为和你预期不一致。

    14.4 误区四:Kafka 事务能直接解决数据库与消息的一切一致性问题

    错。

    Kafka 事务解决的是 Kafka 内部消息写入的一致性边界; 数据库与 Kafka 之间仍需要结合:

    • Spring 事务同步;
    • 失败补偿;
    • Outbox Pattern;
    • CDC 等架构方案。

    14.5 误区五:消费者不配 read_committed 也没关系

    如果你用的是事务生产者,这个误区会直接让事务价值缩水。

    因为 Producer 端的事务语义,本来就需要消费端按已提交隔离级别来配合,才能形成完整闭环。

    14.6 误区六:所有业务都应该上事务 Producer

    不建议。

    原则应该是:

    • 能用幂等解决的,不要贸然上事务;
    • 只有确实存在“多消息原子提交”需求时,才引入事务;
    • 事务越强,成本越高,排障越复杂。

    这就是典型的工程权衡。

    15. 一图总结:幂等与事务的关系

    示意图绘制如下,仅供参考:

    这个图我建议你收藏。很多同学学完一堆概念,脑子里还是一团线,原因就在于缺少这种“层级图”。

    请记住:

    • 事务不是替代幂等;
    • 事务是幂等之上的进一步扩展;
    • 幂等关注重试安全;
    • 事务关注提交边界。

    16. 面试题与答题模板

    下面这组问题非常高频,建议你直接掌握。

    16.1 面试题:Kafka 幂等生产者和事务生产者有什么区别?

    参考回答

    Kafka 幂等生产者和事务生产者的共同点是都基于 Kafka Producer 的状态管理机制,事务生产者还会隐式开启幂等。它们的主要区别在于语义边界:

  • 幂等生产者主要解决同一 Producer 重试导致的重复写入问题,核心依赖 PID、sequence number 以及 Broker 端的 ProducerState 校验。
  • 事务生产者在幂等基础上增加了 transactional.id、事务状态机、事务协调、提交/回滚标记以及消费端 read_committed 可见性控制,可以保证多条消息、多个分区甚至多个 Topic 的原子提交。
  • 从 Spring Boot 工程角度看,幂等生产者更像底层发送语义增强,而事务生产者需要配合 KafkaTransactionManager、事务型 KafkaTemplate 和事务上下文来使用。
  • 16.2 面试题:为什么事务一定需要 transactional.id?

    参考回答

    因为事务不仅要解决当前发送过程的可靠性,还要解决 Producer 重启后的事务恢复和旧实例 fencing 问题。transactional.id 提供了稳定的逻辑身份,让 Kafka 能够区分同一个事务型 Producer 的新旧实例,并通过 epoch 机制阻止旧实例继续写入,避免僵尸 Producer 破坏事务语义。

    16.3 面试题:幂等能保证 exactly-once 吗?

    参考回答

    不能简单这么说。幂等生产者能保证的是同一 Producer 在重试时不会把同一条逻辑消息重复写入 Kafka 日志,这是 Producer 侧的重要基础。但如果要实现更完整的端到端 exactly-once,还需要事务生产者、消费者 read_committed、位点提交协同以及业务层幂等等多方面配合。

    17. 全文总结

    现在我们把全文收束一下。

    如果只用一句话总结:

    幂等生产者解决“重试不重复”,事务生产者解决“多条消息原子提交”;事务建立在幂等之上,但两者的语义边界、状态复杂度和工程使用方式都明显不同。

    再拆开一点看:

    17.1 从概念上总结

    • 幂等:避免同一 Producer 重试造成重复写入;
    • 事务:保证一个事务单元内多条消息要么全部提交、要么全部回滚。

    17.2 从源码思路上总结

    • 两者都走 KafkaProducer 主链路;
    • 两者都依赖 Producer 状态与 Broker 状态协作;
    • 事务生产者在此基础上增加事务状态机、分区注册、提交/回滚以及消费者可见性控制。

    17.3 从 Spring Boot 3.x 工程实践上总结

    • 默认业务通知:优先幂等;
    • 多 Topic/多分区原子发布:使用事务;
    • 数据库与 Kafka 协同:根据一致性要求权衡 Spring 事务同步、Outbox、CDC 等方案;
    • 不要迷信单个配置项,真正可靠的系统一定是“协议语义 + 业务幂等 + 架构补偿”三者结合。

    17.4 给零基础读者的学习建议

    如果你是第一次接触这个主题,请按下面顺序继续深入:

  • 先亲手把本文幂等案例跑起来;
  • 再切换到事务案例,观察多 Topic 一起提交的效果;
  • 然后尝试故意在事务回调中抛异常,看回滚行为;
  • 最后再去读 KafkaProducer、TransactionManager、ProducerStateManager 的源码,这时你会发现已经不抽象了。
  • 这就是“从零基础到源码级理解”的正确路线:

    • 先建立业务问题感;
    • 再建立概念模型;
    • 再通过工程代码落地;
    • 最后回到源码反证你的理解。

    课后思考题

  • 如果业务只要求“重试不重复”,为什么事务反而可能是过度设计?
  • 为什么事务生产者必须配合 read_committed 才能体现完整价值?
  • 为什么很多公司在“数据库 + 消息一致性”问题上最终选择 Outbox,而不是直接依赖 Kafka 事务?
  • 如果让你给团队新人解释 transactional.id 的作用,你会怎么用通俗语言描述?
  • 把这四个问题真正想明白,你对 Kafka Producer 可靠性语义的理解,就已经超过很多只会背配置的人了。

    小结

    • 幂等不是万能去重,它是协议层的安全重试机制。
    • 事务不是“更强幂等”,而是“幂等之上的原子提交机制”。
    • Spring Boot 3.x 的价值不只在于自动配置,更在于它把底层 Kafka 事务能力工程化成了 ProducerFactory、KafkaTemplate 和事务管理器。
    • 真正成熟的消息系统设计,从来不是只靠一个开关,而是边界清晰、职责分层。

    …

    ok,同学们,本节课就上到这儿,下课~

    🧧 学习福利 · 限时开放 🧧

    当然,无论你是计算机专业在读学生,还是对编程充满兴趣的入门者,都强烈建议系统学习SpringBoot全体系专栏:👉 「滚雪球学 Spring Boot」;涵盖SpringBoot所有教学内容。

    该专栏以“循序渐进 + 实战驱动”为核心理念,从基础到进阶到就业到架构师逐层展开,帮助你快速建立完整的 Spring Boot 技术体系,带你玩转SpringBoot框架。

    📌 学习承诺: 通过该专栏,你将能够:

    • 快速掌握 Spring Boot 核心开发能力
    • 构建完整的后端项目认知体系
    • 实现从“入门”到“独立开发”的跃迁

    就像“滚雪球”一样,知识不断积累、能力持续放大,实现指数级成长 🚀

    最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

    同时欢迎大家关注技术号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。

    ps:本文涉及所有源代码,均已上传至Gitee开源,供同学们直接对照学习 Gitee传送门,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗

    🫵 Who am I?

    我是 bug菌,一名深耕 Java 后端领域数十年的一线研发老兵,曾担任独角兽企业后端技术经理、研发架构师等职位,长期专注于 Java 后端、分布式架构、微服务治理、高并发系统、工程效能与研发管理等方向。

    目前活跃于多个主流技术社区,包括:

    CSDN|稀土掘金|InfoQ|51CTO|华为云开发者社区|阿里云开发者社区|腾讯云开发者社区|开源中国|博客园|墨天轮 等平台。

    曾获得:

    • CSDN 博客之星 Top30
    • 华为云多年度十佳博主 & 卓越贡献奖
    • 掘金多年度人气作者 Top40
    • CSDN、掘金、InfoQ、51CTO 等平台签约作者 / 优质作者

    截至目前,全网技术内容累计影响读者众多,全网粉丝已超过 30w+。

    如果你也关注 Java 后端、架构设计、技术成长、职场进阶与研发管理,欢迎关注我的技术内容合集入口:👉 点击查看 👈️

    硬核技术号 「猿圈奇妙屋」 期待你的加入。

    这里不仅分享技术干货,也记录一线研发人的成长、踩坑、思考与进阶路径。

    愿我们一起打怪升级,在技术路上持续进阶。

    – End –

    赞(0)
    未经允许不得转载:171主机测评 » 【SpringBoot 3.x 第292节】夯爆了,Kafka 幂等生产者与事务生产者的源码级区别,一文带你吃透(超详解)!
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址