别再把微服务当银弹了!深度剖析分布式场景下的"数据一致性"终极方案
写在前面
微服务不是灵丹妙药。拆完服务才发现,最头疼的不是服务治理,而是数据一致性。
我见过太多团队,一上来就 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 模式:长流程业务,接受最终一致性
最后一句话:能不拆服务就别拆,拆了就做好数据一致性的准备。