分布式事务的CAP权衡与方案选型
微服务架构中,一次业务操作可能跨多个服务和数据库实例,本地事务无法保证跨库的数据一致性。分布式事务方案需要在一致性(C)、可用性(A)和分区容错性(P)之间权衡。强一致方案(XA两阶段提交)性能开销大、锁占用时间长,不适合高并发互联网场景;柔性事务方案(TCC、Saga、本地消息表)牺牲强一致性换取性能,通过最终一致性保证数据正确。
Seata是阿里开源的分布式事务框架,支持AT、TCC、SAGA和XA四种模式。AT模式通过SQL解析自动生成补偿SQL,业务代码零侵入;TCC模式需要业务实现Try-Confirm-Cancel三个接口,灵活性高但开发量大。实际项目中,AT模式适合对性能要求不极端且SQL操作规范的场景,TCC模式适合对一致性要求高且业务逻辑明确的场景。
Seata Server部署与配置
Seata Server(TC,Transaction Coordinator)是事务协调器,负责管理全局事务的分支注册和提交/回滚决策。使用Docker部署:
docker run -d --name seata-server \
-p 8091:8091 \
-p 7091:7091 \
-e SEATA_IP=127.0.0.1 \
-e STORE_MODE=db \
-e STORE_DB_DRIVER_CLASS_NAME=com.mysql.cj.jdbc.Driver \
-e STORE_DB_URL="jdbc:mysql://mysql:3306/seata" \
-e STORE_DB_USER=root \
-e STORE_DB_PASSWORD=password \
apache/seata-server:2.1.0
DB存储模式下,Seata Server将事务日志持久化到MySQL,支持多节点集群部署。创建元数据表:
-- seata数据库建表SQL
CREATE DATABASE seata;
USE seata;
-- 全局事务表
CREATE TABLE global_table (
xid VARCHAR(128) NOT NULL,
transaction_id BIGINT,
status TINYINT NOT NULL,
application_id VARCHAR(32),
transaction_service_group VARCHAR(32),
transaction_name VARCHAR(128),
timeout INT,
begin_time BIGINT,
application_data VARCHAR(2000),
gmt_modified DATETIME NOT NULL,
PRIMARY KEY (xid),
KEY idx_gmt_modified (gmt_modified)
);
-- 分支事务表
CREATE TABLE branch_table (
branch_id BIGINT NOT NULL,
xid VARCHAR(128) NOT NULL,
transaction_id BIGINT,
resource_group_id VARCHAR(32),
resource_id VARCHAR(256),
branch_type VARCHAR(8),
status TINYINT,
client_id VARCHAR(64),
application_data VARCHAR(2000),
gmt_create DATETIME,
gmt_modified DATETIME,
PRIMARY KEY (branch_id),
KEY idx_xid (xid)
);
-- 锁表(AT模式行锁)
CREATE TABLE lock_table (
row_key VARCHAR(128) NOT NULL,
xid VARCHAR(128),
transaction_id BIGINT,
branch_id BIGINT,
resource_id VARCHAR(256),
table_name VARCHAR(32),
pk VARCHAR(36),
gmt_create DATETIME,
gmt_modified DATETIME,
PRIMARY KEY (row_key)
);
AT模式原理与Spring Boot集成
AT模式分为两个阶段。一阶段:拦截业务SQL,解析SQL获取操作前的前镜像(before image)和操作后的后镜像(after image),将前后镜像存入undo_log表,执行业务SQL并提交本地事务,同时向TC注册分支事务并申请全局锁。二阶段:TC通知提交时,异步删除undo_log即可;TC通知回滚时,根据undo_log的前镜像生成反向SQL补偿数据。
<!-- Maven依赖 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
# application.yml
seata:
enabled: true
application-id: order-service
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
grouplist:
default: 127.0.0.1:8091
registry:
type: file
config:
type: file
file:
name: seata-server.conf
data-source:
auto-commit: false
proxy-mode: AT
每个参与全局事务的数据库需要创建undo_log表:
-- 在业务数据库中创建undo_log表
CREATE TABLE undo_log (
branch_id BIGINT NOT NULL COMMENT '分支事务ID',
xid VARCHAR(128) NOT NULL COMMENT '全局事务ID',
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL,
ext_field VARCHAR(4000),
PRIMARY KEY (branch_id),
KEY idx_xid (xid)
);
业务代码中使用@GlobalTransactional注解开启全局事务:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private AccountFeignClient accountFeignClient;
@Autowired
private StorageFeignClient storageFeignClient;
@GlobalTransactional(name = "createOrder", timeoutMills = 60000, rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
// 1. 创建订单(操作order_db)
Order order = new Order();
order.setUserId(dto.getUserId());
order.setAmount(dto.getAmount());
order.setStatus("CREATED");
orderMapper.insert(order);
// 2. 扣减账户余额(远程调用account-service,操作account_db)
accountFeignClient.deduct(dto.getUserId(), dto.getAmount());
// 3. 扣减库存(远程调用storage-service,操作storage_db)
storageFeignClient.deduct(dto.getProductId(), dto.getQuantity());
// 如果步骤2或3抛出异常,Seata TC会通知所有分支回滚
// order_db: 根据undo_log回滚insert
// account_db: 远程服务收到回滚通知,根据undo_log回滚扣减
// storage_db: 同理回滚
}
}
TCC模式实现与补偿接口设计
TCC模式不依赖SQL解析,由业务自行实现Try、Confirm、Cancel三个接口。Try阶段做资源预留(如冻结金额、预占库存),Confirm阶段做真正的业务操作(扣减冻结金额、确认库存扣减),Cancel阶段释放预留资源(解冻金额、释放库存)。
// TCC接口定义
@LocalTCC
public interface AccountTccAction {
/**
* Try: 冻结金额
*/
@TwoPhaseBusinessAction(
name = "deductAccount",
commitMethod = "confirmDeduct",
rollbackMethod = "cancelDeduct"
)
boolean prepareDeduct(
@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "amount") BigDecimal amount
);
/**
* Confirm: 确认扣减(从冻结金额中扣减)
*/
boolean confirmDeduct(BusinessActionContext context);
/**
* Cancel: 取消冻结(返还冻结金额)
*/
boolean cancelDeduct(BusinessActionContext context);
}
// TCC实现
@Service
public class AccountTccActionImpl implements AccountTccAction {
@Autowired
private AccountMapper accountMapper;
@Autowired
private FreezeMapper freezeMapper;
@Override
@Transactional
public boolean prepareDeduct(Long userId, BigDecimal amount) {
// 幂等检查:防止重复Try
FreezeRecord exist = freezeMapper.findByXidAndBranch(
RootContext.getXID(), RootContext.getBranchId()
);
if (exist != null) return true;
Account account = accountMapper.selectById(userId);
if (account.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("余额不足");
}
// 冻结金额
account.setBalance(account.getBalance().subtract(amount));
account.setFrozen(account.getFrozen().add(amount));
accountMapper.updateById(account);
// 记录冻结(用于幂等和Cancel补偿)
FreezeRecord record = new FreezeRecord();
record.setXid(RootContext.getXID());
record.setBranchId(RootContext.getBranchId());
record.setUserId(userId);
record.setAmount(amount);
record.setStatus("FROZEN");
freezeMapper.insert(record);
return true;
}
@Override
@Transactional
public boolean confirmDeduct(BusinessActionContext context) {
Long userId = context.getActionContext("userId", Long.class);
BigDecimal amount = context.getActionContext("amount", BigDecimal.class);
// 幂等检查
FreezeRecord record = freezeMapper.findByXidAndBranch(
context.getXid(), context.getBranchId()
);
if (record == null || record.getStatus().equals("CONFIRMED")) return true;
// 从冻结金额中扣减
Account account = accountMapper.selectById(userId);
account.setFrozen(account.getFrozen().subtract(amount));
accountMapper.updateById(account);
// 更新冻结记录状态
record.setStatus("CONFIRMED");
freezeMapper.updateById(record);
return true;
}
@Override
@Transactional
public boolean cancelDeduct(BusinessActionContext context) {
Long userId = context.getActionContext("userId", Long.class);
BigDecimal amount = context.getActionContext("amount", BigDecimal.class);
// 幂等检查
FreezeRecord record = freezeMapper.findByXidAndBranch(
context.getXid(), context.getBranchId()
);
if (record == null) return true;
if (record.getStatus().equals("CANCELLED")) return true;
// 释放冻结金额,返还可用余额
Account account = accountMapper.selectById(userId);
account.setBalance(account.getBalance().add(amount));
account.setFrozen(account.getFrozen().subtract(amount));
accountMapper.updateById(account);
record.setStatus("CANCELLED");
freezeMapper.updateById(record);
return true;
}
}
幂等性与空回滚处理
TCC模式的三大问题:幂等性、空回滚和悬挂。幂等性要求Confirm和Cancel接口可重复执行不产生副作用;空回滚指Try未执行但收到了Cancel请求(如Try超时后TC发送Cancel);悬挂指Cancel先于Try执行,之后Try才到达导致资源被永久锁定。
// 幂等、空回滚、悬挂统一处理
public abstract class TccHandler {
@Autowired
private TccLogMapper tccLogMapper;
protected TccLog checkAndRecord(String xid, Long branchId, String phase) {
TccLog log = tccLogMapper.findByXidAndBranch(xid, branchId);
// 空回滚检查:Cancel时Try未执行
if ("cancel".equals(phase) && log == null) {
// Try未执行,插入标记记录防止后续Try执行(防悬挂)
TccLog mark = new TccLog();
mark.setXid(xid);
mark.setBranchId(branchId);
mark.setStatus("CANCELLED_WITHOUT_TRY");
tccLogMapper.insert(mark);
return null; // 返回null表示空回滚,跳过业务处理
}
// 幂等检查
if (log != null && log.getStatus().equals(phase + "ED")) {
return null; // 已执行过,跳过
}
// 悬挂检查:Try时发现已有Cancel记录
if ("try".equals(phase) && log != null
&& "CANCELLED_WITHOUT_TRY".equals(log.getStatus())) {
return null; // Cancel已执行,Try不再执行
}
return log;
}
}
通过事务日志表(tcc_log)记录每个分支事务的执行状态,在三个阶段入口处做检查,解决幂等、空回滚和悬挂问题。这是TCC模式生产环境的必备防护机制。
AT与TCC混合使用场景
实际项目中,同一全局事务内可以混合使用AT和TCC模式。对简单CRUD操作的服务使用AT模式(零侵入),对涉及复杂业务逻辑或非数据库资源(如调用第三方API扣款)的服务使用TCC模式。Seata通过@GlobalTransactional统一管理全局事务,各分支自行选择AT或TCC模式注册。
@GlobalTransactional
public void mixedOrder(OrderDTO dto) {
// AT模式分支:订单服务自动生成undo_log
orderMapper.insert(buildOrder(dto));
// TCC模式分支:账户服务使用TCC补偿
accountTccAction.prepareDeduct(dto.getUserId(), dto.getAmount());
// AT模式分支:库存服务自动生成undo_log
storageMapper.deduct(dto.getProductId(), dto.getQuantity());
}
回滚时,AT分支通过undo_log自动补偿,TCC分支通过Cancel接口补偿。两种模式的事务日志独立存储,TC统一调度。混合模式兼顾开发效率和业务灵活性,是微服务架构中推荐的分布式事务实践方案。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/fen-bu-shi-shi-wu-shi-zhan-seataat-mo-shi-yu-tcc-bu-chang/