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

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

写在前面

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

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

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


传统方案的困境

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

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

yaml 复制代码
Phase 1: Prepare(预提交)
Coordinator -> Participant A: canCommit?
Coordinator -> Participant B: canCommit?

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

问题在哪?

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

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


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

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

时序图示例

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

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

两种编排方式对比

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

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

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


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

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

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

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

核心原理
sql 复制代码
-- 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 要求业务实现三个接口:

java 复制代码
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);
}
实现示例
java 复制代码
@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;
    }
}
数据库设计
sql 复制代码
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. 引入依赖

xml 复制代码
<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)

yaml 复制代码
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. 数据源代理配置

java 复制代码
@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. 业务代码

java 复制代码
@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:唯一索引 + 插入前置

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

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

java 复制代码
@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:状态机 + 版本号

java 复制代码
@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);
java 复制代码
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。

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

java 复制代码
@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 阶段检查事务状态

java 复制代码
@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. 幂等性

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

sql 复制代码
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

yaml 复制代码
seata:
  client:
    rm:
      async-commit-buffer-limit: 10000  # 异步提交队列大小

2. 批量删除 undo_log

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

3. 合理设置超时时间

java 复制代码
@GlobalTransactional(timeoutMills = 60000) // 1 分钟超时

总结

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

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

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

相关推荐
Predestination王瀞潞2 小时前
Maven项目的架构(Spring Boot 实战版)
spring boot·架构·maven
好学且牛逼的马2 小时前
Spring Boot 核心注解完全手册
java·spring boot·后端
彭于晏Yan2 小时前
Spring Boot监听Redis Key过期事件
java·spring boot·redis
翘着二郎腿的程序猿2 小时前
SpringBoot集成Knife4j/Swagger:接口文档自动生成,告别手写API文档
java·spring boot·后端
小鸡脚来咯2 小时前
Spring Boot 常见面试题汇总
java·spring boot·后端
李白的粉2 小时前
基于springboot的阿博图书馆管理系统
java·spring boot·后端·毕业设计·课程设计·源代码·图书馆管理系统
ren049182 小时前
Spring Framework、SpringBoot、Mybatis、Freemarker
spring boot·spring·mybatis
absunique2 小时前
Spring boot 3.3.1 官方文档 中文
java·数据库·spring boot
召田最帅boy2 小时前
Spring Boot博客系统集成AI智能摘要功能实战
人工智能·spring boot·后端