Seata:分布式事务的“和事佬”,让微服务吵架后还能握手言和

4 阅读9分钟

Seata:分布式事务的“和事佬”,让微服务吵架后还能握手言和!🤝💥

兄弟们,在微服务拆得爽的时候,有没有遇到过这种尴尬:订单服务扣款成功了,库存服务扣减失败了,结果用户钱扣了但没买到货?或者反过来,库存扣了款没扣,公司血亏?这就是分布式事务的“魔鬼细节”——数据一致性问题。😈

Seata​ 就是阿里开源来收拾这个烂摊子的。它不是魔法,而是一个分布式事务协调框架。简单说:它确保多个微服务的数据库操作,要么一起成功,要么一起滚蛋,绝不允许“一半成功一半失败”的尴尬局面。


一、Seata是啥?—— 微服务世界的“事务协调员” 🎭

先看个典型悲剧场景:

用户下单(100元):
1. 订单服务:创建订单(本地事务提交 ✅)
2. 库存服务:扣减库存(本地事务提交 ✅)
3. 积分服务:增加积分(网络超时,本地事务回滚 ❌)

结果:订单有了,库存少了,积分没加,用户投诉!

本地事务(单库)的 ACID 保证

  • Atomicity(原子性):要么全做,要么全不做
  • Consistency(一致性):数据始终一致
  • Isolation(隔离性):事务间互不干扰
  • Durability(持久性):提交就永久保存

但在微服务(多库)中:每个服务都有自己的数据库,本地事务只能管自己那摊事。这就是 CAP 理论​ 的现实——分布式系统无法同时保证强一致性(C)和高可用性(A)。

Seata 的解决方案:它不追求“完美”的强一致,而是提供最终一致性的务实方案。它像个“协调员”,指挥各个微服务:“老王你先别真提交,等老张、老李都说准备好了,你们再一起提交!”


二、为什么需要Seata?—— 当“本地事务”管不了“隔壁老王”时 🏘️

传统方案的问题:

  1. 2PC(两阶段提交) :数据库自带,但同步阻塞严重,性能差,而且需要数据库支持XA协议。
  2. TCC(Try-Confirm-Cancel) :需要手动写三个阶段的业务代码,侵入性强,开发成本高。
  3. 本地消息表:要建消息表,实现复杂,一致性靠轮询,延迟高。
  4. 最大努力通知:可能不一致,不适合金融场景。

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服务刚改的数据。

解决

  1. 业务上避免热点数据并发更新
  2. TCC模式替代AT
  3. 配置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连到老主,部分连到新主。

解决

  1. TC用Raft协议(Seata 1.5+支持)
  2. 配置合理的session.timeout和选举超时
  3. 监控告警: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虽好,但不是银弹:

  1. 简单读多写少场景:用异步消息(RocketMQ事务消息)更轻量
  2. 对性能极致要求:TCC模式或业务状态机+对账
  3. 跨语言体系:Seata主要支持Java,多语言用Saga模式+状态机
  4. 数据量极大:考虑分库分表中间件自带分布式事务(如ShardingSphere)

九、总结:Seata的哲学 🤔

Seata的本质是在分布式环境下,寻找一致性与性能的平衡点

  • AT模式:用空间(undo_log)换时间和简便性
  • TCC模式:用代码复杂性换性能和灵活性
  • Saga模式:用最终一致性换长事务支持

选择建议

  • 大部分场景:用AT模式,简单省事
  • 金融核心:用TCC模式,精准控制
  • 业务流程:用Saga模式,容错性强

最后记住:没有完美的分布式事务方案,只有适合场景的方案。 ​ Seata给了你多种选择,但真正理解业务,才能选出最合适的“武器”。

下次当你面对微服务数据不一致时,可以自信地说:“让Seata来协调吧,它是专业的‘和事佬’!” 🤝✨

但别忘了:分布式事务终究是“妥协的艺术”,良好的业务设计(如异步化、幂等、可查询操作)有时比技术方案更重要。技术是手段,业务稳定才是目的!🎯