Seata:分布式事务的“和事佬”,让微服务吵架后还能握手言和!🤝💥
兄弟们,在微服务拆得爽的时候,有没有遇到过这种尴尬:订单服务扣款成功了,库存服务扣减失败了,结果用户钱扣了但没买到货?或者反过来,库存扣了款没扣,公司血亏?这就是分布式事务的“魔鬼细节”——数据一致性问题。😈
Seata 就是阿里开源来收拾这个烂摊子的。它不是魔法,而是一个分布式事务协调框架。简单说:它确保多个微服务的数据库操作,要么一起成功,要么一起滚蛋,绝不允许“一半成功一半失败”的尴尬局面。
一、Seata是啥?—— 微服务世界的“事务协调员” 🎭
先看个典型悲剧场景:
用户下单(100元):
1. 订单服务:创建订单(本地事务提交 ✅)
2. 库存服务:扣减库存(本地事务提交 ✅)
3. 积分服务:增加积分(网络超时,本地事务回滚 ❌)
结果:订单有了,库存少了,积分没加,用户投诉!
本地事务(单库)的 ACID 保证:
- Atomicity(原子性):要么全做,要么全不做
- Consistency(一致性):数据始终一致
- Isolation(隔离性):事务间互不干扰
- Durability(持久性):提交就永久保存
但在微服务(多库)中:每个服务都有自己的数据库,本地事务只能管自己那摊事。这就是 CAP 理论 的现实——分布式系统无法同时保证强一致性(C)和高可用性(A)。
Seata 的解决方案:它不追求“完美”的强一致,而是提供最终一致性的务实方案。它像个“协调员”,指挥各个微服务:“老王你先别真提交,等老张、老李都说准备好了,你们再一起提交!”
二、为什么需要Seata?—— 当“本地事务”管不了“隔壁老王”时 🏘️
传统方案的问题:
- 2PC(两阶段提交) :数据库自带,但同步阻塞严重,性能差,而且需要数据库支持XA协议。
- TCC(Try-Confirm-Cancel) :需要手动写三个阶段的业务代码,侵入性强,开发成本高。
- 本地消息表:要建消息表,实现复杂,一致性靠轮询,延迟高。
- 最大努力通知:可能不一致,不适合金融场景。
Seata 的优势:
- 无侵入(AT模式) :几乎不用改业务代码,加个注解就行
- 高性能:一阶段就提交本地事务,不像2PC那样一直锁着
- 高可用:TC(事务协调器)支持集群部署
- 多模式:AT、TCC、Saga、XA,总有一款适合你
三、Seata的核心架构:三“人”一台戏 🎪
1. TC(Transaction Coordinator) - “总导演”
- 事务协调器,Seata的大脑
- 维护全局事务状态,驱动全局提交或回滚
- 需要独立部署(支持集群)
2. TM(Transaction Manager) - “制片人”
- 事务管理器,在业务侧
- 定义事务边界:
@GlobalTransactional - 告诉TC:“我开始一个全局事务了!”
3. RM(Resource Manager) - “演员”
- 资源管理器,在每个微服务中
- 管理分支事务,向TC注册分支,报告状态
- 执行TC的提交/回滚指令
工作流程:
用户下单
↓
TM(订单服务):@GlobalTransactional 开始全局事务
↓ 生成全局事务ID XID
↓
TC(Seata服务器):记录XID,全局事务开始
↓
订单服务RM:执行本地事务,向TC注册分支1
↓ 一阶段提交本地事务 ✅
库存服务RM:执行本地事务,向TC注册分支2
↓ 一阶段提交本地事务 ✅
积分服务RM:执行本地事务,向TC注册分支3
↓ 一阶段提交本地事务 ✅
↓
TM:通知TC“所有分支一阶段完成”
↓
TC:向所有RM发送“二阶段提交”指令
↓
各RM:删除undo_log(AT模式)或执行Confirm(TCC)
↓
全局事务提交完成!🎉
四、Seata的“王牌模式”详解 🃏
模式1:AT(Auto Transaction)模式 - “无侵入神器” ✨
核心思想:一阶段就提交本地事务,通过全局锁+undo_log保证二阶段可回滚。
一阶段(提交本地事务):
-- 业务SQL:update product set stock = stock - 1 where id = 1
-- Seata代理数据源,自动做三件事:
-- 1. 先查询前镜像(before image)
SELECT stock FROM product WHERE id = 1; -- 假设stock=10
-- 2. 执行业务SQL
UPDATE product SET stock = 9 WHERE id = 1;
-- 3. 查询后镜像(after image),生成undo_log
INSERT INTO undo_log (branch_id, xid, context, rollback_info, log_status, log_created, log_modified)
VALUES (1, '全局事务XID', 'serializer=jackson',
'{"beforeImage":{"rows":[{"fields":[{"name":"id","type":4,"value":1},{"name":"stock","type":4,"value":10}]}],"tableName":"product"},"afterImage":{"rows":[{"fields":[{"name":"id","type":4,"value":1},{"name":"stock","type":4,"value":9}]}],"tableName":"product"}}',
0, NOW(), NOW());
关键点:一阶段就提交了本地事务!锁也释放了!性能接近本地事务。
二阶段提交:
- TC收到所有分支成功 → 异步删除各库的
undo_log✅
二阶段回滚:
- 任何分支失败 → TC通知各分支回滚
- RM查
undo_log,用前镜像恢复数据
-- 回滚时执行
UPDATE product SET stock = 10 WHERE id = 1; -- 恢复为前镜像的值
DELETE FROM undo_log WHERE xid = '全局事务XID';
AT模式的“灵魂” :全局锁(global lock)
- 防止其他全局事务修改同一行
- 在
before image时获取,二阶段后释放 - 解决脏写问题
模式2:TCC模式 - “手动挡,更精准” 🎛️
核心思想:需要业务自己实现三个阶段:
// 1. Try阶段:预留资源
@TwoPhaseBusinessAction(name = "deductStock", commitMethod = "confirm", rollbackMethod = "cancel")
boolean tryDeduct(@BusinessActionContextParameter(paramName = "productId") String productId,
@BusinessActionContextParameter(paramName = "count") int count) {
// 冻结库存,不是真扣
update product_frozen set frozen_count = frozen_count + 1
where product_id = #{productId} and stock - frozen_count >= #{count};
return true;
}
// 2. Confirm阶段:确认执行
boolean confirm(BusinessActionContext context) {
// 真扣:库存减去冻结的
update product set stock = stock - 1 where id = #{productId};
update product_frozen set frozen_count = frozen_count - 1 where product_id = #{productId};
return true;
}
// 3. Cancel阶段:取消回滚
boolean cancel(BusinessActionContext context) {
// 释放冻结
update product_frozen set frozen_count = frozen_count - 1 where product_id = #{productId};
return true;
}
TCC vs AT:
- TCC:需要写三个方法,但无全局锁,性能更好,适合高性能场景
- AT:无侵入,但依赖全局锁,有锁冲突可能
模式3:Saga模式 - “长事务专家” ⏳
核心思想:业务流程驱动,每个服务都要提供正向操作和补偿操作。
// 状态机配置(通常用JSON定义)
{
"name": "orderSaga",
"steps": [
{
"name": "createOrder",
"service": "orderService",
"compensate": "cancelOrder" // 补偿操作
},
{
"name": "deductStock",
"service": "stockService",
"compensate": "restoreStock"
}
]
}
// 补偿时反向执行:先调用restoreStock,再调用cancelOrder
适合场景:业务流程长、不需要强一致性的场景(如旅游订票:订机票→订酒店→租车)。
五、Seata的核心源码设计亮点 💡
1. XID传递:全链路透传
// 通过拦截器自动传递XID
public class SeataHandlerInterceptor implements HandlerInterceptor {
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String xid = request.getHeader(RootContext.KEY_XID);
if (StringUtils.isNotBlank(xid)) {
RootContext.bind(xid); // 绑定到当前线程
}
}
}
// 全局事务上下文存储在ThreadLocal中
public class RootContext {
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
public static void bind(String xid) {
CONTEXT_HOLDER.set(xid);
}
}
2. 数据源代理:无侵入的关键
// 自动代理DataSource
@Configuration
public class SeataAutoConfig {
@Bean
public DataSource dataSource(DataSource originalDataSource) {
return new DataSourceProxy(originalDataSource); // 关键!
}
}
// DataSourceProxy 拦截所有SQL
public class DataSourceProxy extends AbstractDataSourceProxy {
public ConnectionProxy getConnection() throws SQLException {
Connection conn = targetDataSource.getConnection();
return new ConnectionProxy(this, conn); // 返回代理连接
}
}
3. 全局锁设计
// 在global_table中记录锁
public class GlobalLockDAO {
public boolean acquireLock(List<RowLock> locks) {
for (RowLock lock : locks) {
// INSERT INTO global_lock (xid, table_name, pk, row_key, gmt_create)
// VALUES (?, ?, ?, ?, ?)
// 唯一索引冲突表示锁被占用
}
}
}
六、工作中的“防坑”指南 🕳️
坑1:AT模式的“脏回滚”
场景:A服务成功,B服务失败要回滚,但此时C服务修改了A服务刚改的数据。
解决:
- 业务上避免热点数据并发更新
- 用TCC模式替代AT
- 配置
seata.client.lock.retryTimes=30重试获取锁
坑2:undo_log表空间暴涨
-- 定期清理(但确保没有进行中的事务)
DELETE FROM undo_log WHERE log_created < DATE_SUB(NOW(), INTERVAL 7 DAY);
-- 或者用log_status判断
DELETE FROM undo_log WHERE log_status = 1; -- 已完成的事务
坑3:网络分区导致“脑裂”
现象:TC集群网络分裂,部分RM连到老主,部分连到新主。
解决:
- TC用Raft协议(Seata 1.5+支持)
- 配置合理的
session.timeout和选举超时 - 监控告警:
seata.transaction.active.count异常
坑4:AT模式不支持所有SQL
不支持:
- 存储过程、函数
- 某些DDL语句
- 多表关联更新(尽量拆解)
解决:复杂SQL改用TCC或Saga模式。
七、Seata实战:从配置到上线 🚀
1. 快速开始(Spring Cloud + Nacos)
# 1. 每个服务的application.yml
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: my_tx_group # 事务组,需与TC配置一致
config:
type: nacos
nacos:
server-addr: localhost:8848
registry:
type: nacos
nacos:
server-addr: localhost:8848
# 2. 数据库加表
CREATE TABLE undo_log (
id bigint(20) NOT NULL AUTO_INCREMENT,
branch_id bigint(20) NOT NULL,
xid varchar(100) NOT NULL,
context varchar(128) NOT NULL,
rollback_info longblob NOT NULL,
log_status int(11) NOT NULL,
log_created datetime NOT NULL,
log_modified datetime NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id)
);
# 3. 业务代码
@Service
public class OrderService {
@GlobalTransactional // 就这一个注解!
public void createOrder(OrderDTO order) {
// 调用其他服务
inventoryFeignClient.deduct(order.getProductId());
accountFeignClient.debit(order.getUserId(), order.getAmount());
// 本地事务
orderMapper.insert(order);
}
}
2. 监控与运维
# Seata控制台(1.5+自带)
docker run -p 7091:7091 seataio/seata-server:1.5.0
# 关键监控指标
- seata.transaction.active.count # 活跃事务数
- seata.transaction.commit.count # 提交事务数
- seata.transaction.rollback.count # 回滚事务数
- seata.transaction.rt # 事务平均耗时
# 日志排查
# 查看TC日志:grep "全局事务XID" seata-server.log
# 查看RM日志:grep "branch register" application.log
3. 性能调优
# TC server.conf
store.mode=db # 生产用db,不用file
lock.mode=db # 锁存储也用db
service.vgroupMapping.my_tx_group=default # 分组映射
# 客户端优化
seata.client.tm.degradeCheckPeriod=2000
seata.client.tm.degradeCheckAllowTimes=10
seata.client.lock.retryInterval=10 # 锁重试间隔(ms)
八、什么时候不用Seata?🙅♂️
Seata虽好,但不是银弹:
- 简单读多写少场景:用异步消息(RocketMQ事务消息)更轻量
- 对性能极致要求:TCC模式或业务状态机+对账
- 跨语言体系:Seata主要支持Java,多语言用Saga模式+状态机
- 数据量极大:考虑分库分表中间件自带分布式事务(如ShardingSphere)
九、总结:Seata的哲学 🤔
Seata的本质是在分布式环境下,寻找一致性与性能的平衡点。
- AT模式:用空间(undo_log)换时间和简便性
- TCC模式:用代码复杂性换性能和灵活性
- Saga模式:用最终一致性换长事务支持
选择建议:
- 大部分场景:用AT模式,简单省事
- 金融核心:用TCC模式,精准控制
- 业务流程:用Saga模式,容错性强
最后记住:没有完美的分布式事务方案,只有适合场景的方案。 Seata给了你多种选择,但真正理解业务,才能选出最合适的“武器”。
下次当你面对微服务数据不一致时,可以自信地说:“让Seata来协调吧,它是专业的‘和事佬’!” 🤝✨
但别忘了:分布式事务终究是“妥协的艺术”,良好的业务设计(如异步化、幂等、可查询操作)有时比技术方案更重要。技术是手段,业务稳定才是目的!🎯