Seata 分库分表整合指南:ShardingSphere + AT 模式,5 步跑通跨库事务
【免费下载链接】incubator-seata:fire: Seata is an easy-to-use, high-performance, open source distributed transaction solution.项目地址: https://gitcode.com/gh_mirrors/in/incubator-seata
当业务数据用 ShardingSphere 按规则拆到多个物理库之后,一次"下单"可能同时要写订单库和库存库。问题边界很清楚:本地事务的提交/回滚只能作用于单个数据库连接,一旦跨库,先成功的写入无法跟随后失败的写入一起撤销。后果就是两个库之间出现中间状态——一边扣了库存,一边没有订单——而且这种不一致在网络抖动、应用宕机时会被放大。Seata 提供的分布式事务机制,正是为把这种跨库写入重新纳回"要么全成、要么全败"的约束而设计的,其中 AT 模式对分库分表场景的适配成本最低。
一、先选型:四种模式各自的适用边界
在动手之前,值得先把主流路线摆开对比。判断维度不是"谁更先进",而是三件事:一致性强度、对业务代码的侵入程度、以及性能代价。
- 经典 2PC:由协调方持有一切参与方的行锁直到最后时刻。一致性最强,但所有参与方在等待期间无法做任何事,长事务下吞吐掉得很快。适合事务极短、参与方很少的内部批处理,不适合面向用户的在线链路。
- TCC:业务方自行实现 Try(预留资源)/ Confirm(确认)/ Cancel(取消)三组接口,锁是业务自己做的"逻辑锁",不占用数据库行锁。性能最好、侵入性最大,三个方法都要处理幂等、悬挂、空回滚。适合交易核心链路且团队有充足人力。
- Saga:把长流程拆成一串本地事务,每一步配一个补偿动作,靠状态机驱动前进与回退。它是"最终一致",中间状态对外可见。适合流程长、环节多(审批、物流流转这类)的场景。
- AT(本文选型):对业务 SQL 无侵入,框架在每条 DML 前后自动记录数据快照,第一阶段随业务本地事务正常提交,第二阶段由协调者(Seata Server)统一裁决提交或回滚。代价是持有全局行锁的周期比本地事务略长,且依赖被操作表有主键。分库分表场景选它的理由很直接:ShardingSphere 负责路由,Seata 负责协调,两边都不需要改动业务 SQL,也不要求业务方重写补偿逻辑。
如果流程本身就很长(几十秒起步)或者步骤之间天然可补偿,优先考虑 Saga——Seata 内置了可视化状态机设计工具,可以图形化编排节点与补偿:
Seata 自带的 Saga 状态机设计器,服务任务的入参与补偿策略均可图形化配置
下文以 AT 模式为主线,结合 ShardingSphere 给出最短可运行的落地路径。
二、最短落地路径:从环境要求到验证
1. 环境要求
- 数据库:MySQL 5.7 及以上(InnoDB,需要 binlog 支撑主从场景)
- Seata Server:2.0.0 及以上版本,存储模式建议 db 模式,建表脚本在 script/server/db/mysql.sql,会创建
global_table、branch_table、lock_table三张表 - ShardingSphere:5.x 稳定版,配置好分库分表规则
- 应用侧:Spring Boot +
seata-spring-boot-starter(对应模块见 spring/seata-spring-boot-starter)
从源码构建 Server 的最短命令:
git clone https://gitcode.com/gh_mirrors/in/incubator-seata cd incubator-seata ./mvnw clean package -DskipTests sh distribution/server/target/seata-server-*/bin/seata-server.sh -p 8091 -h 127.0.0.1 -m db2. 关键配置(最小可运行)
(a)每个物理分库都建 undo_log 表。这是最容易被漏掉的一步。分库分表之后"业务库"是 N 个物理实例,AT 模式回滚依赖每个库里的undo_log表,DDL 直接取仓库现成脚本 script/client/at/db/mysql.sql,对每一个物理分库各执行一次。注意undo_log本身不分片,它在每个分库中都是单表。
(b)数据源装配顺序。顺序必须固定:先用 ShardingSphere 规则构建ShardingSphereDataSource,再用 Seata 的DataSourceProxy包在它外面(见 rm-datasource/src/main/java/org/apache/seata/rm/datasource/DataSourceProxy.java)。用 starter 时这一步由自动装配完成,手动装配时不能反。
(c)应用侧最小 YAML:
seata: enabled: true application-id: order-service tx-service-group: my_tx_group service: vgroup-mapping: my_tx_group: default client: undo: log-table: undo_logvgroup-mapping的含义是把tx-service-group映射到 Seata Server 的注册名,两个 key 必须对得上(绑定关系定义见 ServiceProperties.java)。本地直连测试时可改用grouplist直接写 Server 地址,跳过注册中心。
(d)业务代码加一个注解:
@GlobalTransactional(rollbackFor = Exception.class) public void createOrder(OrderDTO dto) { orderMapper.insert(dto.toOrder()); // 订单库(可能路由到分片 A) inventoryMapper.deduct(dto); // 库存库(可能路由到分片 B) }ShardingSphere 路由照常工作:同一条 DML 被路由到哪个物理库,就在那个库的本地事务里执行并被 Seata 记录为一个分支。
3. 验证方法
- 正向:正常下单,确认订单与库存同时落库,且每个被写到的分库中
undo_log有记录,全局事务结束后记录被清理。 - 反向:在扣减库存之后人为抛异常,重启/等待回滚完成,确认订单记录消失、库存恢复,两个分库状态一致。
- 并发:对同一热点行加压(示例量级:50~100 并发),确认不出现超卖,只出现部分请求因全局锁等待而超时重试。
4. 随路调优参数
不必单独搞调优阶段,下面几项在初版配置里一并给掉即可(key 名以 ConfigurationKeys.java 为准):
client.rm.reportRetryCount(默认 5 次):分支注册/结果上报失败的重试次数,网络不稳时先确认这一项client.tm.commitRetryCount/client.tm.rollbackRetryCount:全局提交/回滚指令的重试client.undo.retryCount:回滚时校验与恢复的重试server.undo.logDeleteDays及 undo 压缩开关:控制undo_log的清理与体积
三、关键机制拆解:两个最容易被误解的点
AT 的回滚靠"存档",不靠"重放 SQL"
现象:全局回滚时,业务代码并没有被执行"反向",库存数字却能精确恢复。
机制:AT 的第一阶段并不是简单地执行业务 SQL。每条 UPDATE/DELETE 在执行前,框架会先读出受影响行的完整原值(before image),执行后再读出新值(after image),把两份快照连同xid、branch_id一起写入undo_log(表结构见上文脚本),然后让本地事务正常提交——注意是提交,不是挂起。回滚阶段由客户端读取undo_log,直接用 before image 拼 UPDATE 语句把行改回去;之后还会校验该行的当前值仍等于 after image,防止回滚期间有外部写入覆盖。
结论:第一阶段的"提交"是为了尽早释放数据库本地锁,回滚正确性完全押注在"before image 完整 + 回滚期间该行没被别人动过"这两个前提上。这也解释了为什么被操作表必须有主键——没有主键就无法精确定位受影响行,快照机制直接失效。
log_status 的"防御位":为什么回滚后还要防一笔晚到的写入
undo_log里有个log_status字段:0 表示正常,1 表示防御态(见 AbstractUndoLogManager.java 中对GlobalFinished状态的定义)。考虑一个时序:全局事务已回滚完毕,但某个慢分支迟到的事务才提交,此时它会往已回滚的数据上"补写"。框架的做法是在全局事务结束时给相关undo_log打上防御标记,迟到分支写快照前先检查该标记,发现全局事务已终结就拒绝插入、快速失败。
结论:AT 的一致性保证覆盖的是"分支在协调者裁决前完成注册"的场景;跨全局事务的并发冲突由lock_table中的全局行锁裁决——持有全局锁期间,其他事务对同一行的写操作会被挡住,读操作则不受全局锁影响。
分支事务如何被"裁决"
一条 SQL 走 Seata 代理连接时:RM 先向 TC 注册分支(拿到全局事务上下文)→ 本地执行 DML 并写快照 → 本地提交 → 向 TC 上报分支结果。TC 在收到所有分支的成功上报后进入全局提交(AT 模式下提交即"什么都不做",因为第一阶段已提交,异步清理即可);只要有一个分支最终失败,TC 向所有分支下发回滚指令,客户端按上文机制用 undo_log 恢复数据,失败分支会按重试策略反复尝试直到成功或放弃并进入人工介入。
四、踩坑与排障
坑 1:代理顺序装反现象:@GlobalTransactional加了但分支从未注册,Seata Server 查不到分支记录,回滚不生效。 排查:确认DataSourceProxy是最外层;若用了 ShardingSphere 编程式 API,检查是否把 Seata 数据源又包进了分片规则里。
坑 2:某个分库漏建 undo_log现象:单分库写入正常,一旦事务路由到那个库,注册分支时报"表不存在"或插入undo_log失败。 排查:对每个物理分库执行SELECT 1 FROM undo_log LIMIT 1逐一确认;多分库时容易只建了主库。
坑 3:表没有主键现象:对无主键表执行 UPDATE/DELETE 时分支注册失败或报错,全局锁无法锁定到行。 排查:lock_table中的pk字段存的就是主键值;要么补主键,要么评估把该表移出 AT 管理范围。
坑 4:vgroup 映射对不上现象:客户端启动正常,发起全局事务时报找不到 TC 地址/事务组。 排查:核对三处一致性——tx-service-group、vgroup-mapping的 key、Server 端注册的 cluster 名;用注册中心里实际注册条目比对。
坑 5:全局事务超时现象:大事务(含远程调用、人肉审批等待)走到一半被 TC 判超时回滚,前面的分支全部撤销。 排查:@GlobalTransactional的timeoutMills与server.session超时配置是否匹配;长流程应改用 Saga 而不是拉长 AT 超时。
坑 6:全局锁等待现象:并发下部分请求报全局锁获取失败/等待超时,QPS 上不去。 排查:看是否多个事务争抢同一行(热点行);能拆分就缩小单事务内的行范围,不能拆分就评估 TCC 或异步化。
五、适用性判断:什么时候不该用
- 单库扛得住,就别拆:分库分表本身是容量问题,容量没到线时引入 ShardingSphere + Seata 只增加故障面。
- 事务跨度以分钟计:AT 的全局锁窗口会很长,阻塞面大。这类流程用 Saga + 补偿,或干脆本地消息表做到达级一致。
- 跨系统而非跨库:对方是独立服务、独立发布节奏时,AT 的"代理数据源"模型套不上,更适合 TCC、Saga 或消息最终一致。
- 对延迟极敏感的读链路:Seata 代理连接对每条 DML 有额外处理开销(快照、注册),纯读流量不要走全局事务。
- 强一致要求 + 超高并发写同一批行:AT 的全局行锁会先于数据库本地锁成为瓶颈,这是 2PC/TCC 都不划算、需要重新设计业务模型(如库存预扣减、合并写)的信号。
一个粗略的参考线(示例量级):日交易十万级、事务内步骤个位数、以库内写为主——AT 是性价比最高的起点;核心资金链路且并发极高——TCC;长流程编排——Saga。
六、结语
ShardingSphere 解决"数据放在哪",Seata 解决"多处写入如何同生共死",两者组合让跨库事务不需要业务重写 SQL,也不需要业务方自备补偿逻辑。落地时抓住三件事:每个分库的undo_log、数据源代理顺序、被操作表必须有主键,其余都是参数微调。AT 模式适合中短事务的跨库写场景,长流程与极端并发应分别交给 Saga 和 TCC,按事务特征选模式,比追新架构更可靠。
【免费下载链接】incubator-seata:fire: Seata is an easy-to-use, high-performance, open source distributed transaction solution.项目地址: https://gitcode.com/gh_mirrors/in/incubator-seata
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考