yudao-cloud 开发指南 yudao-cloud 开发指南
  • 萌新必读
  • 后端手册
  • 微服务手册
  • 工作流手册
  • 大屏手册
  • 支付手册
  • 商城手册
  • ERP 手册
  • CRM 手册
  • MES 手册
  • WMS 手册
  • HRM 手册
  • FMS 手册
  • PMS 手册
  • OA 手册
  • IM 即时通讯手册
  • AI 大模型手册
  • IoT 物联网手册
  • 公众号手册
  • 前端手册 Vue 3.x
  • 前端手册 Vben 5.x
  • 前端手册 Vue 2.x
  • 前端手册 Admin Uniapp
  • 工作流手册
  • 大屏手册
  • 支付手册
  • 商城手册
  • ERP 手册
  • CRM 手册
  • MES 手册
  • WMS 手册
  • HRM 手册
  • FMS 手册
  • PMS 手册
  • OA 手册
  • IM 即时通讯手册
  • AI 大模型手册
  • IoT 物联网手册
  • 公众号手册
  • 系统手册
视频教程
  • Vue3 + element-plus (opens new window)
  • Vue3 + vben5(ant-design-vue、element-plus) (opens new window)
  • Vue2 + element-ui (opens new window)
单体版 (opens new window)
作者博客 (opens new window)
GitHub (opens new window)
  • 萌新必读
  • 后端手册
  • 微服务手册
  • 工作流手册
  • 大屏手册
  • 支付手册
  • 商城手册
  • ERP 手册
  • CRM 手册
  • MES 手册
  • WMS 手册
  • HRM 手册
  • FMS 手册
  • PMS 手册
  • OA 手册
  • IM 即时通讯手册
  • AI 大模型手册
  • IoT 物联网手册
  • 公众号手册
  • 前端手册 Vue 3.x
  • 前端手册 Vben 5.x
  • 前端手册 Vue 2.x
  • 前端手册 Admin Uniapp
  • 工作流手册
  • 大屏手册
  • 支付手册
  • 商城手册
  • ERP 手册
  • CRM 手册
  • MES 手册
  • WMS 手册
  • HRM 手册
  • FMS 手册
  • PMS 手册
  • OA 手册
  • IM 即时通讯手册
  • AI 大模型手册
  • IoT 物联网手册
  • 公众号手册
  • 系统手册
视频教程
  • Vue3 + element-plus (opens new window)
  • Vue3 + vben5(ant-design-vue、element-plus) (opens new window)
  • Vue2 + element-ui (opens new window)
单体版 (opens new window)
作者博客 (opens new window)
GitHub (opens new window)
  • 萌新必读

    • 简介
    • 交流群
    • 视频教程
    • 功能列表
    • 快速启动(后端项目)
    • 快速启动(前端项目)
    • 接口文档
    • 技术选型
    • 项目结构
    • 代码热加载
    • 一键改包
    • 迁移模块(适合新项目)
    • 删除功能(以租户为例)
    • 表结构变更(版本升级)
    • 国产信创数据库(DM 达梦、大金、OpenGauss、瀚高、OceanBase、TiDB、GaussDB、PolarDB、神通、Vastbase、MogDB、TongRDS、CacheDB)
    • 面试题、简历模版、简历优化
    • 项目外包
  • 后端手册

    • 新建服务
    • 代码生成【单表】(新增功能)
    • 代码生成【主子表】
    • 代码生成(树表)
    • 代码生成(移动端)
    • 功能权限
    • 数据权限
    • 用户体系
    • 三方登录
    • OAuth 2.0(SSO 单点登录)
    • SaaS 多租户【字段隔离】
    • SaaS 多租户【数据库隔离】
    • WebSocket 实时通信
    • 异常处理(错误码)
    • 参数校验、时间传参
    • 分页实现
    • VO 对象转换、数据翻译
    • 文件存储(上传下载)
    • Excel 导入导出
    • 操作日志、访问日志、异常日志
    • MyBatis 数据库
    • MyBatis 联表&分页查询
    • 多数据源(读写分离)、事务
    • Redis 缓存
    • 本地缓存
    • 异步任务
    • 分布式锁
    • 幂等性(防重复提交)
    • 请求限流(RateLimiter)
    • HTTP 接口签名(防篡改)
    • HTTP 接口加解密
    • 单元测试
    • 验证码
    • 工具类 Util
    • 数据库文档
  • 微服务手册

    • 微服务调试(必读)
    • 注册中心 Nacos
    • 配置中心 Nacos
    • 服务网关 Spring Cloud Gateway
    • 服务调用 Feign
    • 定时任务 XXL Job
    • 消息队列(内存)
    • 消息队列(Redis)
    • 消息队列(RocketMQ)
    • 消息队列(RabbitMQ)
    • 消息队列(Kafka)
    • 消息队列(Cloud)
    • 分布式事务 Seata
      • 1. 方案说明
        • 1.1 整体结构
        • 1.2 AT 模式的执行过程
        • 1.3 示例
      • 2. 获取集成材料
      • 3. 接入步骤
        • 3.1 启动 Seata Server
        • 3.2 初始化数据库
        • 3.3 添加依赖
        • 3.4 添加配置
        • 3.5 复制示例代码
        • 3.6 启动验证
      • 4. 使用说明
        • 4.1 全局事务的写法
        • 4.2 XID 的传递
        • 4.3 不在全局事务中的范围
        • 4.4 隔离性
        • 4.5 接入自己的业务
      • 5. 常见问题
        • 5.1 报错 NoClassDefFoundError: com/fasterxml/jackson/...
        • 5.2 报错 can not get cluster name in registry config 或 no available service found in cluster
        • 5.3 发起方回滚了,参与方的数据却没有回滚
        • 5.4 参与方报错了,全局事务却提交了
        • 5.5 启动日志出现 Seata auto-agent detected 警告
        • 5.6 回滚失败,日志出现 Has dirty records when undo
        • 5.7 Druid 的版本变成了 1.2.25
        • 5.8 报错 No bean found of type interface feign.codec.Encoder
    • 服务保障 Sentinel
    • 分库分表
  • 工作流手册

    • 工作流演示
    • 功能开启
    • 工作流(达梦适配)
    • 审批接入(流程表单)
    • 审批接入(业务表单)
    • 流程设计器(BPMN)
    • 流程设计器(钉钉、飞书)
    • 选择审批人、发起人自选
    • 会签、或签、依次审批
    • 流程发起、取消、重新发起
    • 审批通过、不通过、驳回
    • 审批加签、减签
    • 审批转办、委派、抄送
    • 执行监听器、任务监听器
    • 流程表达式
    • 流程审批通知
    • 移动端审批
  • 大屏手册

    • 报表设计器
    • 大屏设计器
  • 支付手册

    • 功能开启
    • 支付宝支付接入
    • 微信公众号支付接入
    • 微信小程序支付接入
    • 支付宝、微信退款接入
    • 支付宝转账接入
    • 微信转账接入
    • 钱包充值、支付、退款
    • 模拟支付、退款
    • 移动端支付管理
  • 会员手册

    • 功能开启
    • 微信公众号登录
    • 微信小程序登录
    • 微信小程序订阅消息
    • 微信小程序码
    • 会员用户、标签、分组
    • 会员等级、积分、签到
    • 移动端会员管理
  • 商城手册

    • 商城演示
    • 功能开启
    • 商城装修
    • 在线客服
    • 【商品】商品分类
    • 【商品】商品属性
    • 【商品】商品 SPU 与 SKU
    • 【商品】商品评价
    • 【交易】购物车
    • 【交易】交易订单
    • 【交易】售后退款
    • 【交易】快递发货
    • 【交易】门店自提
    • 【交易】分销返佣
    • 【营销】优惠劵
    • 【营销】积分商城
    • 【营销】拼团活动
    • 【营销】秒杀活动
    • 【营销】砍价活动
    • 【营销】满减送活动
    • 【营销】限时折扣
    • 【营销】内容管理
    • 【统计】会员、商品、交易统计
    • 移动端商城管理
  • ERP手册

    • ERP 演示
    • 功能开启
    • 【产品】产品信息、分类、单位
    • 【库存】产品库存、库存明细
    • 【库存】其它入库、其它出库
    • 【库存】库存调拨、库存盘点
    • 【采购】采购订单、入库、退货
    • 【销售】销售订单、出库、退货
    • 【财务】采购付款、销售收款
    • 移动端 ERP
  • CRM手册

    • CRM 演示
    • 功能开启
    • 【线索】线索管理
    • 【客户】客户管理、公海客户
    • 【商机】商机管理、商机状态
    • 【合同】合同管理、合同提醒
    • 【回款】回款管理、回款计划
    • 【产品】产品管理、产品分类
    • 【通用】数据权限
    • 【通用】跟进记录、待办事项
    • 移动端 CRM
  • AI大模型手册

    • AI 大模型演示
    • 功能开启
    • AI 聊天对话
    • AI 绘画创作
    • AI 知识库(RAG)
    • AI 音乐创作
    • AI 写作助手
    • AI 思维导图
    • AI 工具(function calling)
    • AI 工作流
    • Dify 工作流
    • FastGPT 工作流
    • Coze 智能体
    • 推理模式(thinking)
    • 联网搜索
    • MCP Client 客户端
    • MCP Server 服务端
    • 【模型接入】Claude
    • 【模型接入】OpenAI
    • 【模型接入】通义千问
    • 【模型接入】DeepSeek
    • 【模型接入】字节豆包
    • 【模型接入】腾讯混元
    • 【模型接入】硅基流动
    • 【模型接入】MiniMax
    • 【模型接入】月之暗面
    • 【模型接入】百川智能
    • 【模型接入】文心一言
    • 【模型接入】LLAMA
    • 【模型接入】智谱 GLM
    • 【模型接入】讯飞星火
    • 【模型接入】微软 OpenAI
    • 【模型接入】谷歌 Gemini
    • 【模型接入】Stable Diffusion
    • 【模型接入】Midjourney
    • 【模型接入】Suno
    • 【模型接入】Grok
    • 【模型接入】阶跃星辰
    • 移动端 AI 大模型
  • IoT物联网手册

    • 功能开启
    • 产品管理
    • 设备管理
    • 物模型配置
    • 设备网关与子设备
    • 设备动态注册
    • 设备影子
    • 消息透传与数据解析
    • 设备接入(概述)
    • 设备接入(HTTP 协议)
    • 设备接入(MQTT 协议)
    • 设备接入(EMQX 协议)
    • 设备接入(TCP 协议)
    • 设备接入(UDP 协议)
    • 设备接入(WebSocket 协议)
    • 设备接入(CoAP 协议)
    • 设备接入(Modbus Client 模式)
    • 设备接入(Modbus Server 模式)
    • 设备接入(自定义协议)
    • 场景联动
    • 数据流转
    • 告警配置
    • OTA 固件升级
    • 移动端 IoT 物联网
  • MES手册

    • MES 演示
    • 功能开启
    • 【基础】物料产品、分类、计量单位
    • 【基础】客户管理、供应商管理
    • 【基础】车间设置、工作站设置
    • 【基础】编码规则
    • 【生产】工序设置、工艺流程
    • 【生产】生产工单
    • 【生产】生产排产、工序流转卡
    • 【生产】生产报工
    • 【生产】安灯配置、安灯呼叫
    • 【生产】工作记录
    • 【仓库】仓库与库区库位、条码赋码、SN码
    • 【仓库】批次管理、库存现有量、库存事务
    • 【仓库】到货通知、采购入库、采购退货
    • 【仓库】生产领料、生产退料、物料消耗
    • 【仓库】产品产出、产品入库
    • 【仓库】发货通知、销售出库、销售退货
    • 【仓库】外协发料、外协入库
    • 【仓库】其他入库、其他出库
    • 【仓库】调拨单、装箱管理
    • 【仓库】库存盘点
    • 【质量】检测项设置、常见缺陷
    • 【质量】质检方案
    • 【质量】来料检验(IQC)
    • 【质量】过程检验(IPQC)
    • 【质量】出货检验(OQC)
    • 【质量】退货检验(RQC)
    • 【质量】待检任务、检验结果、缺陷记录
    • 【设备】设备类型、设备台账
    • 【设备】点检保养项目、点检保养方案
    • 【设备】点检记录、保养记录、维修单
    • 【工具】工具类型、工装夹具台账
    • 【排班】班组设置、节假日设置
    • 【排班】排班计划、排班日历
    • 移动端 MES
  • WMS手册

    • WMS 演示
    • 功能开启
    • 【基础】仓库
    • 【基础】商品、SKU、分类、品牌
    • 【基础】往来企业(供应商、客户)
    • 【库存】库存记录、流水、统计
    • 【单据】入库
    • 【单据】出库
    • 【单据】移库
    • 【单据】盘库
    • 【其它】WMS、MES、ERP 对比
    • 移动端 WMS
  • HRM人力资源

    • HRM 演示
    • 功能开启
    • 【组织】工作台、组织架构
    • 【员工】员工管理
    • 【招聘】招聘管理
    • 【考勤】考勤管理
    • 【社保】社保管理
    • 【薪资】计薪设置、薪资档案
    • 【薪资】月度工资、工资条
    • 【绩效】绩效模板、绩效计划
    • 【绩效】绩效考核、绩效档案
    • 移动端 HRM
  • FMS财务管理

    • FMS 演示
    • 功能开启
    • 【设置】账套管理、财务参数、财务指标
    • 【设置】币别、科目、辅助核算、初始余额
    • 【设置】凭证字、常用摘要、凭证模板
    • 【凭证】凭证管理
    • 【账簿】账簿管理
    • 【报表】财务报表
    • 【结账】期末结账
    • 移动端 FMS
  • PMS项目管理

    • PMS 演示
    • 功能开启
    • 【项目中心】工作台与项目管理
    • 【项目中心】项目详情与迭代
    • 【项目中心】工作项与协作
    • 【知识中心】知识库管理
    • 【知识中心】文档与协作
    • 移动端 PMS
  • OA协同办公

    • OA 演示
    • 功能开启
    • 【协作】日程、任务、计划与汇报
    • 【协作】公告、讨论、通讯录与笔记
    • 【办公】企业邮箱
    • 【办公】企业云盘
    • 【行政】办公用品、用印管理
    • 【行政】会议室、车辆管理
    • 【流程】公文管理
    • 【流程】出差、费用报销
    • 【流程】考勤、请假、加班、转正与离职
    • 移动端 OA
  • IM即时通讯手册

    • IM 演示
    • 功能开启
    • 【好友】好友关系、好友申请
    • 【群聊】群组、群成员、入群申请
    • 【消息】私聊、群聊、频道消息
    • 【频道】频道、频道素材
    • 【内容】表情、敏感词
    • 【通话】语音通话、视频通话、共享屏幕
    • 【WebSocket】实时推送与离线消息
    • 移动端 IM 即时通讯
  • 公众号手册

    • 功能开启
    • 公众号接入
    • 公众号粉丝
    • 公众号标签
    • 公众号消息
    • 模版消息
    • 自动回复
    • 公众号菜单
    • 公众号素材
    • 公众号图文
    • 公众号统计
    • 移动端公众号管理
  • 系统手册

    • 短信配置
    • 邮件配置
    • 站内信配置
    • Webhook(钉钉、飞书、企微)
    • 数据脱敏
    • 敏感词
    • 地区 & IP 库
  • 运维手册

    • 开发环境
    • Linux 部署
    • Docker 部署
    • Jenkins 部署
    • 宝塔部署
    • 1Panel 部署
    • Docker Compose 部署
    • Kubernetes 部署
    • HTTPS 证书
    • 服务监控
    • Tomcat WAR 部署
    • 东方通服务器部署(TongWeb、TongHttpServer)
    • 宝兰德服务器部署(BES AppServer)
    • 内网穿透
  • 前端手册 Vue 3.x

    • 开发规范
    • 菜单路由
    • Icon 图标
    • 字典数据
    • 系统组件
    • 通用方法
    • 配置读取
    • CRUD 组件
    • 国际化
    • IDE 调试
    • 代码格式化
  • 前端手册 Vben 5.x

    • 开发规范
    • 菜单路由
    • 图标、主题、国际化
    • 字典数据
    • 系统组件
    • 通用方法
    • 配置读取
    • IDE 调试
    • 代码格式化
  • 前端手册 Vue 2.x

    • 开发规范
    • 菜单路由
    • Icon 图标
    • 字典数据
    • 系统组件
    • 通用方法
    • 配置读取
  • 前端手册 Admin Uniapp

    • 开发规范
    • 菜单路由
    • 图标、主题、国际化
    • 字典数据
    • 系统组件
    • 通用方法
    • IDE 调试
    • 代码格式化
    • 运行发布
  • 更新日志

    • 【v2026-10】
    • 【v2026-09】
    • 【v2026-08】
    • 【v2026-07】
    • 【v2026-06】
    • 【v2026-05】
    • 【v2026-04】
    • 【v2026-03】
    • 【v2026-01】
    • 【v2025-12】
    • 【v2025-11】
    • 【v2025-10】
    • 【v2025-09】
    • 【v2025-08】
  • 开发指南
  • 微服务手册
芋道源码
2022-12-31
目录

分布式事务 Seata

项目默认不开启分布式事务。一个业务操作需要通过 RPC 调用多个服务、修改多个数据库,并且要求它们要么都成功、要么都回滚时,可以基于 Apache Seata (opens new window) 的 AT 模式按需接入,实际使用的依赖版本见《3.3 添加依赖》小节。

Seata 提供了 AT、TCC、SAGA、XA 多种事务模式,本文只接入对业务代码侵入最小的 AT 模式。接入方式是在 《多数据源(读写分离)》 的基础上,由 Dynamic Datasource 为指定的数据源包装 Seata 代理,并通过 Feign 请求头传递全局事务的 XID:

  • 各服务的 master 主库、slave 从库保持不变,不参与全局事务。工作流、商城、支付等已有业务的 SQL 和 RPC 调用,不会因为接入 Seata 而自动变成分布式事务。
  • 只有添加了 @GlobalTransactional 注解的入口方法,以及它直接、通过 Feign 间接调用的、开启了 Seata 代理的数据源(示例是 system-server 的 seata_order、infra-server 的 seata_account),才组成一个全局事务。
  • 每个服务的依赖、配置是独立的,只影响接入的服务。其它服务、gateway 网关无需接入 Seata。

友情提示:

分布式事务会带来额外的 Seata Server 部署、全局锁、回滚日志等成本。优先通过服务拆分让强一致的数据落在同一个服务、同一个库中;跨服务的场景,也可以先评估“本地事务 + 消息队列 + 补偿”的最终一致性方案是否满足需求。

# 1. 方案说明

# 1.1 整体结构

system-server(发起方,TM)
    DemoSeataTradeService#placeOrder  @GlobalTransactional
        ├── DemoSeataOrderService#createOrder    @DS("seata_order") + @Transactional(分支一)
        │     └── yudao_seata_order 库:demo_seata_order 表 + undo_log 表
        └── DemoSeataAccountApi#deductBalance    Feign 调用,请求头 TX_XID = 全局事务的 XID
                  │
infra-server(参与方)     ▼
    Seata 自带的 HTTP 拦截器:将请求头中的 XID 绑定到当前线程
    DemoSeataAccountRpcController → DemoSeataAccountService#deductBalance  @DS("seata_account") + @Transactional(分支二)
        └── yudao_seata_account 库:demo_seata_account 表 + undo_log 表

Seata Server(TC,事务协调者):记录全局事务及其分支,决定二阶段提交或回滚

# 1.2 AT 模式的执行过程

AT 模式对业务代码没有侵入,业务代码仍然是普通的 MyBatis Plus 读写:

① 一阶段:每个分支执行业务 SQL 时,Seata 的 DataSourceProxy 先记录修改前、后的数据镜像,与业务数据在同一个本地事务中写入 undo_log 表,然后立即提交本地事务,并向 Seata Server 注册分支。

② 二阶段提交:全局事务的入口方法正常结束,Seata Server 通知各服务的分支异步删除 undo_log 即可,业务数据在一阶段已经提交。

③ 二阶段回滚:入口方法抛出异常,Seata Server 通知各服务的分支根据 undo_log 生成反向 SQL,将数据恢复到修改前的镜像。

所以,一阶段提交后、二阶段完成前,其它不在全局事务中的查询可以读到“中间状态”的数据,参见《4.4 隔离性》小节。

# 1.3 示例

项目提供了两个独立的演示库,不会修改已有的业务表:

服务 数据源 库 表 说明
system-server seata_order yudao_seata_order demo_seata_order 订单,主键 id 由调用方传入,作为幂等主键
infra-server seata_account yudao_seata_account demo_seata_account 账户,初始化租户 1、用户 1 的账户,余额 100 分

两张表都继承项目的 TenantBaseDO,带有 create_time、update_time(LocalDateTime)和 tenant_id 字段,可以在回滚后核对时间、租户字段是否一起恢复。租户编号通过项目已有的 Feign 拦截器传递给 infra-server。

示例 DemoSeataTradeService#placeOrder 的执行顺序是:先在 system-server 创建订单,再通过 Feign 调用 infra-server 条件扣减余额(balance >= 金额 时才更新):

场景 结果
余额足够 两个分支都提交:订单存在,余额减少
余额不足 infra-server 的分支二抛出异常,system-server 分支一的订单根据 undo_log 删除:订单不存在,余额不变
两个分支都成功,外层的全局事务随后失败 两个服务的分支都根据 undo_log 回滚:订单不存在,余额恢复
重复的订单编号 分支一主键冲突、抛出异常,不会重复扣减余额

为什么不直接使用商城、支付的业务做示例?

商城下单、支付等真实流程,涉及库存、优惠券、积分、钱包等多个服务,以及消息队列、定时任务、第三方回调等不受数据库事务控制的操作,需要按业务逐个评估回滚方式。所以,示例使用独立的演示库,接入 Seata 不等于商城、支付等业务自动启用了分布式事务。

# 2. 获取集成材料

集成材料在项目的 script/seata/ (opens new window) 目录。目前只有 master-jdk17 分支提供这些材料,master、master-jdk25 分支请先从 master-jdk17 分支获取,同步到对应分支后,也可以直接从当前分支获取。材料适用于下面三个分支,接入步骤相同,兼容差异如下:

分支 JDK Spring Boot 差异
master 8 2.7.18 无,Jackson 2 已由 Spring Boot 提供
master-jdk17 17 3.5.15 无,Jackson 2 已由 Spring Boot 提供
master-jdk25 25 4.1.2 Spring Boot 4 默认使用 Jackson 3,必须添加 dependencies.xml 中的 Jackson 2 依赖;使用 Feign 客户端的服务缺少 spring-boot-data-commons 模块时,还需要按条件添加 dependencies-jdk25.xml,见《3.3 添加依赖》小节

目录中的文件及用途如下:

文件 说明 目标位置
sql/mysql/demo_seata.sql 创建 2 个演示库,各一张示例表 + undo_log 表 在 MySQL 中执行
dependencies.xml Maven 依赖片段 yudao-module-system-server/pom.xml、yudao-module-infra-server/pom.xml 都要添加
dependencies-jdk25.xml 仅 master-jdk25 分支按条件补充的 spring-boot-data-commons 依赖片段 满足条件的每个使用 Feign 客户端的服务,示例的 yudao-module-system-server/pom.xml、yudao-module-infra-server/pom.xml 都要检查
seata.yaml 按服务分为两段:数据源、Seata 客户端的配置片段 分别合并到两个服务的 application-local.yaml 等配置文件,或 Nacos 配置
src/main/java/.../system/seatademo/ 订单 DO、Mapper、Service,全局事务入口 DemoSeataTradeService,Feign 客户端 DemoSeataAccountApi,传递 XID 的 DemoSeataRpcConfiguration yudao-module-system-server/src/main/java/ 的同名包
src/main/java/.../infra/seatademo/ 账户 DO、Mapper、Service,RPC 接口 DemoSeataAccountRpcController yudao-module-infra-server/src/main/java/ 的同名包

单体项目 ruoyi-vue-pro 的 script/seata/ 目录,使用同样的 SQL、依赖,以及订单、账户的 DO、Mapper、Service;差异是单体项目在同一个进程中直接调用账户 Service,没有 Feign 相关的类。

# 3. 接入步骤

友情提示:以 `system-server` 发起、`infra-server` 参与为例子。

示例代码的包名分别是 cn.iocoder.yudao.module.system.seatademo、cn.iocoder.yudao.module.infra.seatademo,在对应服务的 yudao.info.base-package 下,才能被 Spring、MyBatis 扫描到。接入其它服务时,需要将包名改到对应服务的 base-package 下。

# 3.1 启动 Seata Server

Seata Server(TC)是独立的进程,与项目使用的 JDK 版本无关。本机演示时,使用官方 Docker 镜像启动即可:

docker run -d --name yudao-seata-server \
  -p 127.0.0.1:18091:18091 \
  -e SEATA_PORT=18091 \
  -e JVM_XMS=256m \
  -e JVM_XMX=512m \
  apache/seata-server:2.6.0
  • -p 127.0.0.1:18091:18091:只在本机的 127.0.0.1 上映射端口,不对局域网、公网开放;system-server、infra-server 与 Docker 运行在同一台机器上,通过 grouplist 直连 127.0.0.1:18091。
  • SEATA_PORT:Seata Server 的服务端口,必须与《3.4 添加配置》小节中 seata.service.grouplist.default 的端口一致。
  • JVM_XMS、JVM_XMX:限制 JVM 内存,本机演示足够。

这里没有设置 SEATA_IP:Seata Server 不接受 127.0.0.1 这类回环地址作为自己的地址,会改用容器内的 IP,日志、XID 中看到 172.17.* 之类的地址属于正常现象,不影响本机演示。服务与 Seata Server 不在同一台机器时,需要使用服务能够访问到的真实地址、端口,参见下面的生产部署说明。

执行 docker logs -f yudao-seata-server 命令,确认日志中没有异常、服务已经启动在 18091 端口。

本机演示 ≠ 生产部署

上面的命令使用镜像默认的 file 存储模式,全局事务的状态保存在容器内的文件中,容器删除或重建后会丢失,也只有单个节点;客户端也是通过 file 直连这个地址。生产环境需要:

  • 将存储模式改为 db(需要初始化 Seata Server 自己的表)或 raft 等可持久化的方式,并部署多个节点保证高可用。
  • 将 Seata Server 注册到 Nacos,客户端的 seata.registry、seata.config 改为 Nacos,而不是 file 直连。
  • Seata Server 注册、声明的地址,必须是各个服务能够访问到的真实地址、端口,不能依赖 127.0.0.1 等回环地址。
  • 为 Seata Server 设置访问控制,只允许业务服务所在的内网访问。

具体参见 Seata 官方的 《新人文档》 (opens new window)、《Docker 部署》 (opens new window)、《高可用部署》 (opens new window)、《事务分组》 (opens new window) 文档。

# 3.2 初始化数据库

在 MySQL 中执行 sql/mysql/demo_seata.sql 脚本,创建 yudao_seata_order、yudao_seata_account 两个库,以及:

  • 业务表:demo_seata_order、demo_seata_account,并初始化租户 1、用户 1 的账户,余额 100 分。
  • undo_log 表:AT 模式的回滚日志,每个参与全局事务的库都要有一张,与业务表在同一个库中。表结构来自 Seata 官方的 script/client/at/db/mysql.sql (opens new window),Seata Server 不需要这张表。

脚本使用「库名.表名」的写法、CREATE ... IF NOT EXISTS 和 INSERT IGNORE,可以重复执行,不会删除已有的库、表、数据,也不会修改各服务的 master 主库。生产环境中,两个库通常属于不同服务、不同的 MySQL 实例,分别执行对应库的语句即可。

# 3.3 添加依赖

将 dependencies.xml 中的依赖,复制到 yudao-module-system-server、yudao-module-infra-server 两个服务的 pom.xml 中:

<dependency>
    <groupId>org.apache.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>2.6.0</version>
    <exclusions>
        <exclusion>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

① Seata 使用 org.apache.seata 的坐标,并显式指定 2.6.0 版本:项目的 Spring Cloud Alibaba BOM 管理的 Seata 版本是 2.5.0(master-jdk17、master-jdk25)或 io.seata 的 1.6.1(master),不是本文验证的版本。它的 seata-all 自带 io.seata 包的兼容类,Dynamic Datasource 4.6.0 包装数据源时使用的 io.seata.rm.datasource.DataSourceProxy 就由它提供。

② 不要引入 spring-cloud-starter-alibaba-seata、io.seata:* 等坐标,否则会与 seata-all 2.6.0 自带的兼容类重复。XID 的传递,由示例的 Feign 拦截器和 Seata 自带的 HTTP 拦截器完成,参见《4.2 XID 的传递》小节。

③ 排除 Seata 传递的 druid 1.2.25,避免它把项目原有的 Druid 降级。排除后,Seata 解析 SQL 时继续使用项目 Druid starter 提供的版本。

④ Seata 默认使用 Jackson 2 序列化 undo_log 中的数据镜像。master、master-jdk17 分支的 Spring Boot 已经提供,声明后版本不变;master-jdk25 分支的 Spring Boot 4 默认使用 Jackson 3,必须添加这两个依赖,版本由 Spring Boot BOM 管理。它们只给 Seata 内部使用,不会替换项目接口、Redis 等使用的 JSON 序列化。

⑤ 仅 master-jdk25 分支(Spring Boot 4.1.2 + Spring Cloud OpenFeign 5.0.2):使用 Feign 客户端的服务,启动时的 classpath 中有 org.springframework.data.domain.Pageable(例如说 Redis 传递引入的 spring-data-commons),却没有 Spring Boot 4 拆分到 spring-boot-data-commons 模块的 org.springframework.boot.data.autoconfigure.web.DataWebProperties 时,需要将 dependencies-jdk25.xml 中的依赖,添加到该服务的 pom.xml 中,版本由 Spring Boot BOM 管理。示例的两个服务都需要检查:

  • 发起方 yudao-module-system-server:示例的 DemoSeataAccountApi 等 Feign 客户端。
  • 参与方 yudao-module-infra-server:虽然示例没有在其中新增 Feign 客户端,但多租户组件的 YudaoTenantRpcAutoConfiguration 会注册 TenantCommonApi 等 Feign 客户端,同样受这个条件影响。

依赖如下:

<!-- 仅 master-jdk25 分支,且 classpath 中缺少 spring-boot-data-commons 时添加 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-data-commons</artifactId>
</dependency>

原因是 OpenFeign 5.0.2 中,普通的 Encoder 要求 classpath 中没有 Pageable,支持分页参数的 Encoder 要求 Pageable、DataWebProperties 都有。只有 Pageable 时,两个都不会创建,启动报错参见《5.8 报错 No bean found of type interface feign.codec.Encoder》小节。classpath 中已经有该模块时,不需要重复添加;单体项目及 master、master-jdk17 分支不使用该文件。

本文对这一条件的验证,是在 Java 25 下只包含示例相关依赖的启动环境中进行的:发起方、参与方都补充了该依赖后通过,参与方使用真实的 YudaoTenantRpcAutoConfiguration、TenantCommonApi,不补充时复现了上述报错;并没有验证完整业务服务的构建、启动。所以,以自己服务实际的 classpath 为准判断是否需要添加。

# 3.4 添加配置

seata.yaml 按服务分为两段,分别合并到对应服务的 application-local.yaml 配置文件中。

① system-server(发起方):

spring:
  datasource:
    dynamic:
      seata: true # 开启 Dynamic Datasource 的 Seata 支持,由它为每个数据源包装 Seata 的 DataSourceProxy
      seata-mode: AT # 使用 AT 模式
      datasource:
        master:
          seata: false # 只追加这一行:master 主库不参与 AT,不被 Seata 代理
        slave:
          seata: false # 只追加这一行:slave 从库不参与 AT,不被 Seata 代理;没有配置 slave 时,删除整个 slave 段
        seata_order: # 分布式事务示例的订单库,只有 @DS("seata_order") 的 Mapper、Service 使用
          url: ${YUDAO_SEATA_ORDER_URL:jdbc:mysql://127.0.0.1:3306/yudao_seata_order?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true}
          username: ${YUDAO_SEATA_DB_USERNAME:root}
          password: ${YUDAO_SEATA_DB_PASSWORD:}
          seata: true # 参与 AT:被 Seata 代理,本地事务提交时注册分支、写入 undo_log

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: yudao_seata_demo_group # 事务分组,与下面的 vgroup-mapping 对应
  enable-auto-data-source-proxy: false # 必须关闭:数据源已经由 Dynamic Datasource 代理,避免双重代理
  client:
    http:
      interceptor-enabled: false # 发起方不需要从 HTTP 请求头 TX_XID 接收 XID;当前服务也要作为参与方时,删除本项
  service:
    vgroup-mapping:
      yudao_seata_demo_group: default # 事务分组 -> Seata Server 集群名
    grouplist:
      default: ${YUDAO_SEATA_SERVER_ADDR:127.0.0.1:18091} # Seata Server 地址,端口与启动 Seata Server 时的 SEATA_PORT 一致
  registry:
    type: file # 直连上面的 grouplist,仅用于本机演示;服务的注册发现仍然使用 Nacos,不受影响
  config:
    type: file

② infra-server(参与方):数据源换成 seata_account(环境变量 YUDAO_SEATA_ACCOUNT_URL),seata 的配置与 system-server 相同,只是不配置 seata.client.http.interceptor-enabled,保持默认的 true,由 Seata 自带的 HTTP 拦截器绑定请求头中的 XID。完整内容见 seata.yaml 的第二段。

注意事项:

  • 合并,而不是替换:master、slave 原有的 url、username、password 等配置保持不变,只追加 seata: false 一行。Dynamic Datasource 的 seata 默认对每个数据源都是开启的,不追加时,主库、从库也会被 Seata 代理、向 Seata Server 注册。seata: false 只追加到该服务当前 profile 已经存在的数据源上:没有配置 slave 时,不要复制 slave 段,否则会新建一个没有 url 的 slave 数据源,导致启动失败。
  • seata.enable-auto-data-source-proxy 必须为 false。否则 Seata 会再代理一次 Dynamic Datasource 的路由数据源,形成双重代理。
  • 两个服务的 tx-service-group、vgroup-mapping、grouplist 必须一致。
  • 如果服务的配置放在 Nacos 中,同样合并到 Nacos 中该服务的配置。同一个配置项在 Nacos 与本地配置文件中同时存在时,通常以 Nacos 为准,注意不要用片段覆盖掉 Nacos 中已有的 master 数据源。

数据库的账号密码通过环境变量传入,不要把真实密码写入配置文件、提交到 Git:

环境变量 说明 默认值 使用的服务
YUDAO_SEATA_ORDER_URL 订单库的 JDBC URL jdbc:mysql://127.0.0.1:3306/yudao_seata_order?... system-server
YUDAO_SEATA_ACCOUNT_URL 账户库的 JDBC URL jdbc:mysql://127.0.0.1:3306/yudao_seata_account?... infra-server
YUDAO_SEATA_DB_USERNAME 演示库的账号 root 两个服务
YUDAO_SEATA_DB_PASSWORD 演示库的密码 空,按实际设置 两个服务
YUDAO_SEATA_SERVER_ADDR Seata Server 的地址 127.0.0.1:18091 两个服务

本地开发时,在 IDEA 启动 SystemServerApplication、InfraServerApplication 的 Run Configuration 中,分别设置 Environment variables 即可。

# 3.5 复制示例代码

① 将 src/main/java 下的 cn.iocoder.yudao.module.system.seatademo 包,复制到 yudao-module-system-server/src/main/java 的同名目录:

  • DemoSeataOrderDO、DemoSeataOrderMapper:订单表,Mapper 上添加 @DS("seata_order") 注解。
  • DemoSeataOrderService:类上添加 @DS("seata_order") 注解,createOrder 方法添加 @Transactional(rollbackFor = Exception.class),是全局事务的分支一。
  • DemoSeataAccountApi:调用 infra-server 账户接口的 Feign 客户端,使用独立的 contextId,避免与已有的 infra-server Feign 客户端冲突。
  • DemoSeataRpcConfiguration:通过 @EnableFeignClients(clients = DemoSeataAccountApi.class) 单独声明 Feign 客户端,不修改已有的 RpcConfiguration;并注册传递 XID 的 Feign 拦截器。
  • DemoSeataTradeService:placeOrder 方法添加 @GlobalTransactional 注解,是全局事务的入口。

② 将 cn.iocoder.yudao.module.infra.seatademo 包,复制到 yudao-module-infra-server/src/main/java 的同名目录:

  • DemoSeataAccountDO、DemoSeataAccountMapper:账户表,Mapper 上添加 @DS("seata_account") 注解,updateBalanceDecr 方法按 balance >= 金额 条件扣减。
  • DemoSeataAccountService:类上添加 @DS("seata_account") 注解,deductBalance 方法添加 @Transactional(rollbackFor = Exception.class),是全局事务的分支二;余额不足时抛出业务异常。
  • DemoSeataAccountRpcController:RPC 接口,路径是 /rpc-api/infra/seata-demo/account/deduct-balance,在 infra-server 已有安全配置放行的 /rpc-api/infra/** 下。

全局事务的入口 placeOrder:

@GlobalTransactional(name = "demo-seata-place-order", rollbackFor = Exception.class)
public void placeOrder(Long orderId, Long userId, Integer amount) {
    // 1. 创建订单:分支一,本地事务提交后,订单已经写入 seata_order 库,同时记录 undo_log
    demoSeataOrderService.createOrder(orderId, userId, amount);
    // 2. 扣减余额:分支二,由 infra-server 在 seata_account 库执行;XID 通过 Feign 请求头传递
    // 必须调用 getCheckedData():账户服务的业务异常会被转换成 CommonResult 错误码返回,不检查就不会抛出异常,全局事务会被提交
    demoSeataAccountApi.deductBalance(userId, amount).getCheckedData();
}

示例代码只使用 Java 8 语法和构造方法注入,不引用 javax、jakarta 包,三个分支均无需修改。

# 3.6 启动验证

① 在 system-server 的 cn.iocoder.yudao.module.system.seatademo 包下,新增两个临时的类。

DemoSeataVerifyService:模拟“下单成功后,外层的后续步骤失败”。它必须是单独的 @Service 类,同一个类内部的 this.xxx() 调用不会经过 @GlobalTransactional 的拦截:

package cn.iocoder.yudao.module.system.seatademo;

import lombok.RequiredArgsConstructor;
import org.apache.seata.spring.annotation.GlobalTransactional;
import org.springframework.stereotype.Service;

/**
 * 分布式事务示例:验证外层全局事务失败时的回滚,验证完成后删除
 *
 * @author 芋道源码
 */
@Service
@RequiredArgsConstructor
public class DemoSeataVerifyService {

    private final DemoSeataTradeService demoSeataTradeService;

    @GlobalTransactional(name = "demo-seata-verify", rollbackFor = Exception.class)
    public void placeOrderThenFail(Long orderId, Long userId, Integer amount) {
        demoSeataTradeService.placeOrder(orderId, userId, amount); // 加入外层的全局事务,两个服务的分支都提交成功
        throw new IllegalStateException("模拟下单之后的步骤失败");
    }

}

DemoSeataVerifyRunner:在 system-server 启动完成后,在租户编号为 1 的上下文中,依次执行三个场景,并核对是否提交、订单是否存在符合预期;余额在 infra-server 的库中,在步骤 ③ 中核对。网络、数据库或参与方服务暂时不可用等普通的回滚失败,Seata Server 会自动重试,所以核对时有限次轮询,等待数据达到预期,不以执行完成后立即读取的结果为准:

package cn.iocoder.yudao.module.system.seatademo;

import cn.hutool.core.thread.ThreadUtil;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

/**
 * 分布式事务示例:启动后验证提交、回滚,验证完成后删除
 *
 * @author 芋道源码
 */
@Component
@Slf4j
@RequiredArgsConstructor
public class DemoSeataVerifyRunner implements ApplicationRunner {

    private static final Long USER_ID = 1L;

    private final DemoSeataTradeService demoSeataTradeService;
    private final DemoSeataVerifyService demoSeataVerifyService;
    private final DemoSeataOrderService demoSeataOrderService;

    @Override
    public void run(ApplicationArguments args) {
        TenantUtils.execute(1L, () -> {
            long orderId = System.currentTimeMillis(); // 每次启动使用新的订单编号
            // 场景一:余额足够,两个分支都提交:订单存在
            verify("成功", orderId, () -> demoSeataTradeService.placeOrder(orderId, USER_ID, 10), true);
            // 场景二:余额不足,infra-server 的分支二失败,分支一根据 undo_log 回滚:订单不存在
            verify("余额不足", orderId + 1, () -> demoSeataTradeService.placeOrder(orderId + 1, USER_ID, 100000000), false);
            // 场景三:两个分支都已提交,外层的全局事务失败,两个服务的分支都根据 undo_log 回滚:订单不存在
            verify("外层失败", orderId + 2, () -> demoSeataVerifyService.placeOrderThenFail(orderId + 2, USER_ID, 10), false);
        });
    }

    private void verify(String scene, Long orderId, Runnable action, boolean expectCommit) {
        boolean committed;
        try {
            action.run();
            committed = true;
            log.info("[{}] 全局事务提交", scene);
        } catch (Exception ex) {
            committed = false;
            log.info("[{}] 全局事务回滚:{}", scene, ex.getMessage());
        }
        // 有限次轮询,等待订单达到预期:最多 10 次,每次间隔 500 毫秒
        boolean orderExists = false;
        for (int i = 0; i < 10; i++) {
            orderExists = demoSeataOrderService.getOrder(orderId) != null;
            if (orderExists == expectCommit) {
                break;
            }
            ThreadUtil.sleep(500);
        }
        if (committed == expectCommit && orderExists == expectCommit) {
            log.info("[{}] 符合预期:订单是否存在:{}", scene, orderExists);
        } else {
            log.error("[{}] 不符合预期:是否提交:{}(预期 {}),订单是否存在:{}(预期 {})",
                    scene, committed, expectCommit, orderExists, expectCommit);
        }
    }

}

② 按 Seata Server → infra-server → system-server 的顺序启动,并设置好《3.4 添加配置》小节的环境变量;gateway 等其它服务不需要启动。三个场景都应该输出「符合预期」,出现「不符合预期」时参见《5. 常见问题》排查。system-server 首次执行时,日志示例如下,仅供参考,时间、线程等前缀已省略:

[成功] 全局事务提交
[成功] 符合预期:订单是否存在:true
[余额不足] 全局事务回滚:用户(1) 的账户不存在或余额不足
[余额不足] 符合预期:订单是否存在:false
[外层失败] 全局事务回滚:模拟下单之后的步骤失败
[外层失败] 符合预期:订单是否存在:false

启动日志中,一般还可以看到两个服务的 Seata TM、RM 连接 Seata Server 的日志,具体内容以实际使用的 Seata 版本为准。

③ 余额在 infra-server 的库中,直接连接 MySQL 核对:首次执行(余额从 100 开始)后,余额应为 100 - 10 = 90,只扣减了场景一的 10 分;场景三扣减的 10 分已经回滚。不是 90 时,稍等片刻后再查询一次,仍不一致时查看 Seata Server 的日志。undo_log 在二阶段完成后删除,删除是异步的,稍等片刻后再核对,应为 0 条;仍有记录时,查看 Seata Server 的日志,确认是否有全局事务在重试提交、回滚,或者出现了《5.6》小节中不可重试的脏数据回滚失败:

SELECT id, user_id, amount, tenant_id, create_time FROM yudao_seata_order.demo_seata_order ORDER BY id DESC LIMIT 3;
SELECT id, user_id, balance, tenant_id, update_time FROM yudao_seata_account.demo_seata_account;
SELECT 'order' AS db, COUNT(*) FROM yudao_seata_order.undo_log
UNION ALL SELECT 'account', COUNT(*) FROM yudao_seata_account.undo_log;

验证完成后,记得删除 DemoSeataVerifyService、DemoSeataVerifyRunner 两个类,否则每次启动都会扣减演示账户的余额。需要重新演示时,只修改演示库的数据即可,例如说 UPDATE yudao_seata_account.demo_seata_account SET balance = 100 WHERE id = 1;。

# 4. 使用说明

# 4.1 全局事务的写法

① @GlobalTransactional 只添加在发起方的入口方法上,包名是 org.apache.seata.spring.annotation;参与方的 RPC 接口、Service 不要添加,只使用本地事务。入口方法所在的类,需要通过 @Service 等组件扫描注册。Seata 2.6 不会增强 @Bean 方法定义的 Bean,此时 @GlobalTransactional 不生效、也不会报错。

② 入口方法不要再添加 @Transactional。本地事务会在开始时固定一个数据源的连接,后续的 @DS 切换不会生效,参见 《多数据源(读写分离)》 的事务说明。

③ 每个分支在单独的 Service 上添加 @DS + @Transactional(rollbackFor = Exception.class),先切换数据源、再开启本地事务。

④ 异常要抛出,不要 try catch 后吞掉,否则 Seata 认为执行成功,会提交全局事务。特别是 Feign 调用:项目的 RPC 接口会将参与方的异常转换成 CommonResult 的错误码返回,发起方必须调用 getCheckedData() 或 checkError(),将错误码重新抛出为异常。

⑤ 和 @Transactional 一样,通过 this.xxx() 自调用、或者在其它线程中执行时,注解不会被拦截。

# 4.2 XID 的传递

XID 是全局事务的编号,参与方需要拿到它,才能把本地事务注册为全局事务的分支:

① 发起方:示例的 DemoSeataRpcConfiguration 注册了一个 Feign 拦截器,在当前线程存在 XID(处于全局事务中)时,添加 TX_XID 请求头;不在全局事务中的 Feign 调用不受影响。它对当前服务的所有 Feign 客户端生效。

② 参与方:seata-spring-boot-starter 自带 HTTP 拦截器,在请求开始时将 TX_XID 请求头中的 XID 绑定到当前线程,请求结束时解绑,不需要额外代码。

③ 链路传递:参与方再通过 Feign 调用其它服务时,也需要注册同样的 Feign 拦截器;同时作为发起方、参与方的服务,不要配置 seata.client.http.interceptor-enabled: false。

④ 不要为参与全局事务的 Feign 调用开启重试,避免同一个操作在全局事务中执行多次。

只在可信的内网 RPC 中传递 XID

参与方会信任请求头中的 TX_XID,将当前请求加入对应的全局事务。所以:

  • 只在内网的服务之间传递 XID。项目的 /rpc-api/** 接口只给内网的服务调用,不要通过网关对外暴露。
  • 在 gateway 网关中,为外部请求移除 TX_XID 请求头(例如说使用 Spring Cloud Gateway 的 RemoveRequestHeader 过滤器),不要让公网的输入加入全局事务。
  • 只作为发起方的服务,按《3.4 添加配置》小节关闭 Seata 的 HTTP 拦截器。

# 4.3 不在全局事务中的范围

  • master、slave 等配置了 seata: false 的数据源:即使在全局事务中执行,也是普通的本地 SQL,全局事务回滚时不会恢复。需要参与时,在该数据源中创建 undo_log 表,并去掉 seata: false。
  • 没有接入 Seata 的服务:通过 Feign 调用它们时,对方的操作不会回滚。
  • 异步线程:XID 保存在当前线程中,@Async、线程池、CompletableFuture 等异步执行的 SQL 或 Feign 调用,不会自动加入全局事务。
  • @DSTransactional:它是 Dynamic Datasource 自己的多数据源本地事务,与 Seata 的全局事务是两套机制,不要混用在同一个调用链路中。
  • 《分库分表》 的 sharding 数据源:不要为 ShardingSphere 的逻辑连接开启 Seata 代理,本方案没有验证两者的组合。
  • 消息队列、Redis、文件、第三方接口等非数据库的操作:不会回滚,需要自行补偿,或者在全局事务提交后再执行。

# 4.4 隔离性

① 写隔离:一阶段提交前,Seata 会获取被修改记录的全局锁。其它全局事务修改同一条记录时,会等待锁释放,等待超时后失败、回滚。所以,热点数据(例如说同一个账户)的并发会下降。

② 读隔离:默认是读未提交。不在全局事务中的查询,可以读到其它全局事务一阶段已提交、但最终会回滚的数据。需要读已提交时,查询语句使用 SELECT ... FOR UPDATE,并在方法上添加 @GlobalTransactional 或 @GlobalLock 注解。

③ 回滚时的数据校验:回滚前,Seata 会比较当前数据与一阶段记录的修改后镜像。回滚失败分为两种:

  • 网络中断、数据库或参与方服务暂时不可用等普通失败:Seata Server 会自动重试回滚。
  • 其它不在全局事务中的 SQL 修改了这条记录(脏数据):该分支的回滚失败且不可重试。Seata Server 停止自动重试,全局事务以回滚失败结束,等待也不会自动恢复;对应业务库的 undo_log 会保留该分支的记录,需要人工核对、修复数据后再清理,参见《5.6 回滚失败,日志出现 Has dirty records when undo》小节。

所以,被全局事务修改的表,其它写入也要通过全局事务或 @GlobalLock。

详细说明,参见 Seata 官方的 《AT 模式》 (opens new window) 文档。

# 4.5 接入自己的业务

在示例的基础上,接入自己的业务时:

① 每个参与全局事务的服务,都添加《3.3 添加依赖》小节的依赖、《3.4 添加配置》小节的 Seata 配置。

② 在每个参与全局事务的库中创建 undo_log 表,业务表必须有主键。在 spring.datasource.dynamic.datasource 中,为这些库配置数据源,seata 保持默认的 true;其它数据源配置 seata: false。

③ 发起方注册传递 XID 的 Feign 拦截器。RPC 接口建议和项目其它接口一样,放在被调用方的 -api 模块中;示例为了不修改 yudao-module-infra-api 模块,才将 DemoSeataAccountApi 放在发起方。

④ 调用方需要重试时,像示例的订单编号一样,使用业务唯一键保证幂等。

⑤ @GlobalTransactional 的超时时间默认是 60 秒,可以通过 timeoutMills 属性调整,并大于 Feign 调用的超时时间。全局事务越长,全局锁持有越久,尽量只包含必须一致的数据库操作。

除 MySQL 外的数据库,需要使用 Seata 官方对应数据库的 undo_log 建表 SQL,并自行验证;本文只验证了 MySQL。

# 5. 常见问题

# 5.1 报错 NoClassDefFoundError: com/fasterxml/jackson/...

例如说 com/fasterxml/jackson/core/JsonProcessingException、com/fasterxml/jackson/datatype/jsr310/JavaTimeModule,通常出现在 master-jdk25 分支第一次提交分支事务时。缺少 Seata 序列化 undo_log 使用的 Jackson 2 依赖,按《3.3 添加依赖》小节,在每个接入的服务中同时添加 jackson-databind、jackson-datatype-jsr310 两个依赖。

# 5.2 报错 can not get cluster name in registry config 或 no available service found in cluster

客户端没有找到、或者连接不上 Seata Server:

  • seata.tx-service-group 与 seata.service.vgroup-mapping 的 key 不一致,或者两个服务的配置不一致。
  • Seata Server 没有启动,或者 grouplist 的端口与启动时的 SEATA_PORT 不一致。
  • Docker 端口只绑定了 127.0.0.1,服务却运行在另一台机器、另一个容器中。

# 5.3 发起方回滚了,参与方的数据却没有回滚

按顺序排查:

  • 发起方是否注册了传递 XID 的 Feign 拦截器,即 DemoSeataRpcConfiguration 是否在 system-server 的扫描范围内。
  • 参与方是否接入了 Seata,并且没有配置 seata.client.http.interceptor-enabled: false。
  • 参与方的数据源是否配置了 seata: false,或者是在异步线程中执行的。
  • 参与方的该数据源是否被 Seata 代理:可以查看参与方启动、执行时 Seata RM 相关的日志,具体内容以实际使用的 Seata 版本为准。

# 5.4 参与方报错了,全局事务却提交了

发起方没有检查 Feign 调用的返回结果。项目的 RPC 接口会将参与方的异常转换成 CommonResult 的错误码返回,需要调用 getCheckedData() 或 checkError() 重新抛出,参见《4.1 全局事务的写法》小节。另外,也排查入口方法是否添加了 @Transactional、异常是否被 try catch 吞掉。

# 5.5 启动日志出现 Seata auto-agent detected 警告

seata.enable-auto-data-source-proxy 没有设置为 false,Dynamic Datasource 的路由数据源又被 Seata 代理了一次,按《3.4 添加配置》小节关闭即可。

注意,Dynamic Datasource 4.6.0 的这个检查依赖 Seata 旧版的类名,在 Seata 2.6 中可能不会打印警告,所以必须显式配置 false,不能依赖该警告判断。

# 5.6 回滚失败,日志出现 Has dirty records when undo

一阶段提交后、二阶段回滚前,这条记录被不在全局事务中的 SQL 修改了,Seata 无法安全地恢复数据,参见《4.4 隔离性》小节。

这是不可重试的回滚失败,与网络、数据库或参与方服务暂时不可用等可以自动重试的失败不同:Seata Server 会停止自动重试,全局事务以回滚失败结束,等待不会自动恢复,Seata Server 中也不一定还保留可以操作的全局事务。处理方式:

  1. 在报错分支所在服务的业务库中,根据日志中的 xid、branchId 查询 undo_log,该分支的记录会保留,其中有一阶段的修改前、修改后镜像。
  2. 对照镜像与当前数据,人工核对、修复业务数据。其它分支可能已经回滚成功,需要按业务整体核对。
  3. 确认修复完成后,再删除这条全局事务对应的 undo_log 记录。

# 5.7 Druid 的版本变成了 1.2.25

seata-spring-boot-starter 没有排除 com.alibaba:druid,参见《3.3 添加依赖》小节。可以通过 mvn dependency:tree -Dincludes=com.alibaba:druid 查看实际使用的版本。

# 5.8 报错 No bean found of type interface feign.codec.Encoder

例如说 IllegalStateException: No bean found of type interface feign.codec.Encoder for demoSeataAccountApi(发起方 system-server)、... for system-server(参与方 infra-server 的 TenantCommonApi),出现在 master-jdk25 分支使用 Feign 客户端的服务启动时。

该服务的 classpath 中有 org.springframework.data.domain.Pageable,却缺少 spring-boot-data-commons 模块的 DataWebProperties,OpenFeign 5.0.2 的两个 Encoder 都不会创建。按《3.3 添加依赖》小节的 ⑤,为报错的服务添加 dependencies-jdk25.xml 中的 spring-boot-data-commons 依赖。可以通过 mvn dependency:tree -Dincludes=org.springframework.boot:spring-boot-data-commons 确认是否已经存在。

消息队列(Cloud)
服务保障 Sentinel

← 消息队列(Cloud) 服务保障 Sentinel→

Theme by Vdoing | Copyright © 2019-2026 芋道源码 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×