别再把微服务当银弹了!深度剖析...

4 阅读6分钟

别再把微服务当银弹了!深度剖析分布式场景下的"数据一致性"终极方案

写在前面

微服务不是灵丹妙药。拆完服务才发现,最头疼的不是服务治理,而是数据一致性

我见过太多团队,一上来就 All in 微服务,结果订单创建了,库存没扣;支付成功了,积分没加。最后只能靠定时任务补偿,或者干脆让运营手动改数据。

今天我们聊聊分布式事务的演进路径,从 2PC 到 SAGA,再到 Seata 的 AT/TCC 模式。不讲理论,只讲坑在哪,怎么填。


传统方案的困境

2PC(两阶段提交):理论完美,现实打脸

2PC 的核心思想是协调者统一指挥,参与者分两阶段执行:

Phase 1: Prepare(预提交)
Coordinator -> Participant A: canCommit?
Coordinator -> Participant B: canCommit?

Phase 2: Commit(正式提交)
Coordinator -> All: doCommit!

问题在哪?

问题影响生产环境表现
同步阻塞所有参与者锁资源等待RT 飙升,吞吐量暴跌
单点故障协调者挂了全员等死数据库连接池耗尽
数据不一致Phase 2 网络分区部分提交部分回滚

结论:2PC 只适合低并发、强一致性场景(如银行核心系统),互联网业务别碰。


SAGA 模式:补偿式事务的艺术

SAGA 的核心是正向操作 + 反向补偿,每个子事务都有对应的回滚逻辑。

时序图示例

订单服务 -> 创建订单(Ti)
库存服务 -> 扣减库存(Tj)
支付服务 -> 扣款(Tk)

若 Tk 失败:
  -> 补偿 Tj(恢复库存)
  -> 补偿 Ti(取消订单)

两种编排方式对比

编排方式优点缺点适用场景
Choreography(事件驱动)解耦,无中心节点难以追踪,补偿逻辑分散简单流程
Orchestration(协调器)流程清晰,易监控协调器成为瓶颈复杂业务

SAGA 的致命缺陷:无法保证隔离性。

举个例子:订单创建后、支付前,用户查询到"待支付"状态,但最终可能因库存不足被回滚。这种"中间状态可见"在金融场景是不可接受的。


Seata:阿里开源的分布式事务框架

Seata 提供了 AT、TCC、SAGA、XA 四种模式,我们重点对比 AT 和 TCC

AT 模式:自动补偿的黑魔法

AT 模式通过拦截 SQL,自动生成回滚日志(undo_log),实现无侵入式事务。

核心原理
-- Phase 1: 业务 SQL 执行前
SELECT * FROM account WHERE id = 1 FOR UPDATE; -- 前镜像
UPDATE account SET balance = balance - 100 WHERE id = 1;
SELECT * FROM account WHERE id = 1; -- 后镜像
INSERT INTO undo_log (before_image, after_image);

-- Phase 2: 提交或回滚
COMMIT; -- 成功则删除 undo_log
或
根据 before_image 生成反向 SQL 回滚; -- 失败则补偿
优点
  • 零侵入:业务代码无需改动
  • 性能高:一阶段直接提交,不锁资源
缺点
  • 脏读风险:一阶段提交后,其他事务可能读到未最终确认的数据
  • 依赖数据库:需要解析 SQL,不支持 NoSQL

TCC 模式:手动补偿的硬核方案

TCC 要求业务实现三个接口:

public interface AccountTccAction {
    @TwoPhaseBusinessAction(name = "deduct", commitMethod = "commit", rollbackMethod = "rollback")
    boolean prepare(@BusinessActionContextParameter(paramName = "userId") Long userId,
                    @BusinessActionContextParameter(paramName = "amount") BigDecimal amount);
    
    boolean commit(BusinessActionContext context);
    
    boolean rollback(BusinessActionContext context);
}
实现示例
@Service
public class AccountTccActionImpl implements AccountTccAction {
    
    @Override
    public boolean prepare(Long userId, BigDecimal amount) {
        // Try: 冻结金额(不实际扣款)
        accountMapper.freeze(userId, amount);
        return true;
    }
    
    @Override
    public boolean commit(BusinessActionContext context) {
        // Confirm: 扣减冻结金额
        Long userId = context.getActionContext("userId", Long.class);
        BigDecimal amount = context.getActionContext("amount", BigDecimal.class);
        accountMapper.deduct(userId, amount);
        return true;
    }
    
    @Override
    public boolean rollback(BusinessActionContext context) {
        // Cancel: 解冻金额
        Long userId = context.getActionContext("userId", Long.class);
        BigDecimal amount = context.getActionContext("amount", BigDecimal.class);
        accountMapper.unfreeze(userId, amount);
        return true;
    }
}
数据库设计
CREATE TABLE account (
    id BIGINT PRIMARY KEY,
    balance DECIMAL(10,2),      -- 可用余额
    frozen_amount DECIMAL(10,2) -- 冻结金额
);

AT vs TCC 终极对比

维度AT 模式TCC 模式
侵入性无侵入高侵入(需实现 3 个接口)
性能高(一阶段提交)中(需额外冻结/解冻操作)
隔离性弱(脏读风险)强(资源预留)
适用场景普通业务金融、库存等强一致性场景
开发成本

我的选择标准:

  • 订单、日志类业务 → AT 模式
  • 账户、库存类业务 → TCC 模式

Seata 实战:Spring Boot + MyBatis-Plus 配置

1. 引入依赖

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.7.0</version>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

2. 配置文件(application.yml)

seata:
  enabled: true
  application-id: order-service
  tx-service-group: default_tx_group
  service:
    vgroup-mapping:
      default_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: seata
      group: SEATA_GROUP
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: seata
      group: SEATA_GROUP

3. 数据源代理配置

@Configuration
public class DataSourceConfig {
    
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return new DruidDataSource();
    }
    
    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }
    
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy) throws Exception {
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(dataSourceProxy);
        return factory.getObject();
    }
}

4. 业务代码

@Service
public class OrderServiceImpl {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private StorageService storageService; // Feign 调用
    
    @Autowired
    private AccountService accountService; // Feign 调用
    
    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public void createOrder(OrderDTO orderDTO) {
        // 1. 创建订单
        orderMapper.insert(orderDTO);
        
        // 2. 扣减库存
        storageService.deduct(orderDTO.getProductId(), orderDTO.getCount());
        
        // 3. 扣减账户余额
        accountService.deduct(orderDTO.getUserId(), orderDTO.getMoney());
    }
}

高并发场景下的幂等性设计准则

准则 1:唯一索引 + 插入前置

CREATE TABLE idempotent_record (
    biz_id VARCHAR(64) PRIMARY KEY,  -- 业务唯一 ID(订单号/流水号)
    status TINYINT,
    create_time DATETIME
);
public void processOrder(String orderId) {
    try {
        // 插入幂等记录(唯一索引保证原子性)
        idempotentMapper.insert(orderId, PROCESSING);
    } catch (DuplicateKeyException e) {
        // 重复请求,直接返回
        return;
    }
    
    // 执行业务逻辑
    doBusinessLogic();
    
    // 更新状态
    idempotentMapper.updateStatus(orderId, SUCCESS);
}

准则 2:分布式锁 + Token 机制

@Service
public class PaymentService {
    
    @Autowired
    private RedissonClient redisson;
    
    public void pay(String orderId, String token) {
        RLock lock = redisson.getLock("pay:" + orderId);
        
        try {
            if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
                // 验证 Token(一次性令牌)
                String cachedToken = redis.get("token:" + orderId);
                if (!token.equals(cachedToken)) {
                    throw new BizException("重复支付");
                }
                
                // 执行支付
                doPayment(orderId);
                
                // 删除 Token
                redis.del("token:" + orderId);
            }
        } finally {
            lock.unlock();
        }
    }
}

准则 3:状态机 + 版本号

@Update("UPDATE orders SET status = #{newStatus}, version = version + 1 " +
        "WHERE order_id = #{orderId} AND status = #{oldStatus} AND version = #{version}")
int updateStatus(@Param("orderId") String orderId,
                 @Param("oldStatus") int oldStatus,
                 @Param("newStatus") int newStatus,
                 @Param("version") int version);
public void cancelOrder(String orderId) {
    Order order = orderMapper.selectById(orderId);
    
    // 只有"待支付"状态才能取消
    int rows = orderMapper.updateStatus(orderId, WAIT_PAY, CANCELED, order.getVersion());
    
    if (rows == 0) {
        throw new BizException("订单状态已变更,取消失败");
    }
}

TCC 模式的三大异常处理

1. 空回滚

场景:Try 阶段因网络超时未执行,但 Seata 认为失败触发 Cancel。

解决方案:事务控制表记录状态

@Override
public boolean rollback(BusinessActionContext context) {
    String xid = context.getXid();
    
    // 查询事务记录
    TccTransaction tx = tccMapper.selectByXid(xid);
    if (tx == null) {
        // 空回滚:插入一条 ROLLBACK 记录,防止后续 Try 执行
        tccMapper.insert(xid, ROLLBACK);
        return true;
    }
    
    // 正常回滚
    accountMapper.unfreeze(userId, amount);
    return true;
}

2. 悬挂

场景:Cancel 先于 Try 执行(网络延迟导致)。

解决方案:Try 阶段检查事务状态

@Override
public boolean prepare(Long userId, BigDecimal amount) {
    String xid = RootContext.getXID();
    
    // 检查是否已回滚
    TccTransaction tx = tccMapper.selectByXid(xid);
    if (tx != null && tx.getStatus() == ROLLBACK) {
        // 悬挂:拒绝执行
        return false;
    }
    
    // 正常冻结
    accountMapper.freeze(userId, amount);
    tccMapper.insert(xid, TRY);
    return true;
}

3. 幂等性

解决方案:状态机 + 唯一约束

CREATE TABLE tcc_transaction (
    xid VARCHAR(128) PRIMARY KEY,
    branch_id BIGINT,
    status TINYINT,
    UNIQUE KEY uk_xid_branch (xid, branch_id)
);

性能优化建议

1. 异步化 Commit/Rollback

seata:
  client:
    rm:
      async-commit-buffer-limit: 10000  # 异步提交队列大小

2. 批量删除 undo_log

-- 定时任务清理 3 天前的日志
DELETE FROM undo_log WHERE create_time < DATE_SUB(NOW(), INTERVAL 3 DAY) LIMIT 10000;

3. 合理设置超时时间

@GlobalTransactional(timeoutMills = 60000) // 1 分钟超时

总结

分布式事务没有银弹,只有权衡:

  • AT 模式:适合 80% 的业务场景,快速落地
  • TCC 模式:金融级强一致性,开发成本高
  • SAGA 模式:长流程业务,接受最终一致性

最后一句话:能不拆服务就别拆,拆了就做好数据一致性的准备。