在微服务架构大行其道的今天,一个订单创建操作可能涉及库存服务、账户服务、优惠券服务等多个独立系统的数据变更。当某个服务出现异常时,如何保证所有相关服务要么全部成功,要么全部回滚?这就是分布式事务要解决的核心问题。
2019年阿里开源的Seata(Simple Extensible Autonomous Transaction Architecture)以其"一站式"的解决方案迅速成为行业标杆。我在金融支付系统架构升级时,曾对比过TCC、SAGA、消息队列等多种方案,最终选择Seata正是看中它三大核心优势:
关键认知:Seata本质上是通过"全局事务ID+分支事务协调"的方式,在分布式环境下模拟出单机事务的ACID特性。这与传统2PC协议最大的区别在于,Seata的AT模式通过解析SQL生成前后镜像实现自动回滚。
Seata的架构设计遵循"控制中心+执行单元"的经典分布式系统模式:
code复制[TM] [RM] [TC]
│ │ │
│──1.begin─▶│ │
│ │──2.reg───▶ │
│ │──3.lock───▶│
│──4.commit▶│ │
│ │──5.ack────▶│
在TC服务中,核心数据存储在三个表中:
sql复制-- 全局事务表
CREATE TABLE global_table (
xid VARCHAR(128) PRIMARY KEY,
status TINYINT,
application_id VARCHAR(32),
transaction_service_group VARCHAR(32),
transaction_name VARCHAR(128),
timeout INT,
begin_time BIGINT,
application_data VARCHAR(2000)
);
-- 分支事务表
CREATE TABLE branch_table (
branch_id BIGINT PRIMARY KEY,
xid VARCHAR(128),
resource_group_id VARCHAR(32),
resource_id VARCHAR(256),
lock_key VARCHAR(1000),
branch_type VARCHAR(8),
status TINYINT,
client_id VARCHAR(64),
application_data VARCHAR(2000)
);
-- 全局锁表
CREATE TABLE lock_table (
row_key VARCHAR(128) PRIMARY KEY,
xid VARCHAR(128),
transaction_id LONG,
branch_id LONG,
resource_id VARCHAR(256),
table_name VARCHAR(32),
pk VARCHAR(36)
);
Seata AT模式最精妙之处在于通过SQL解析生成前后镜像实现自动回滚。以更新库存为例:
java复制@GlobalTransactional
public void purchase(String commodityCode, int count) {
stockService.reduce(commodityCode, count);
// 其他服务调用...
}
当执行update stock_tbl set count=count-10 where code='C1001'时,Seata会:
sql复制SELECT id, code, count FROM stock_tbl WHERE code='C1001' FOR UPDATE
json复制// undo_log记录示例
{
"branchId": 641789253,
"xid": "192.168.1.1:8091:641789253",
"context": "serializer=jackson",
"rollbackInfo": {
"beforeImage": {
"rows": [{
"fields": [{
"name": "id", "type": 4, "value": 1
},{
"name": "count", "type": 4, "value": 100
}]
}]
},
"afterImage": {
"rows": [{
"fields": [{
"name": "id", "type": 4, "value": 1
},{
"name": "count", "type": 4, "value": 90
}]
}]
}
}
}
Seata采用两层锁保证隔离性:
SELECT FOR UPDATE获取数据库本地锁重要限制:AT模式必须使用支持本地事务的关系型数据库,且业务表必须有主键
TC服务作为核心协调者必须保证高可用,推荐部署方案:
code复制 [SLB]
│
┌────────────┼────────────┐
[TC1] [TC2] [TC3]
MySQL MySQL MySQL
│││ │││ │││
Redis Redis Redis
关键配置项:
properties复制# registry.conf
registry {
type = "nacos"
nacos {
serverAddr = "nacos-cluster:8848"
namespace = "seata"
cluster = "default"
}
}
# 存储模式建议使用db+redis
store {
mode = "db"
db {
datasource = "druid"
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://mysql-ha:3306/seata"
user = "seata"
password = "加密密码"
}
redis {
host = "redis-sentinel"
port = 26379
password = ""
database = 0
minConn = 1
maxConn = 10
}
}
yaml复制seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
config:
type: nacos
nacos:
server-addr: nacos-cluster:8848
namespace: seata
group: SEATA_GROUP
registry:
type: nacos
nacos:
server-addr: nacos-cluster:8848
namespace: seata
group: SEATA_GROUP
properties复制# TC服务端配置
server.undo.log.save.days=7
server.undo.log.delete.period=86400000
server.max.commit.retry.timeout=-1
server.max.rollback.retry.timeout=-1
server.recovery.committing-retry-period=1000
server.recovery.asyn-committing-retry-period=1000
server.recovery.rollbacking-retry-period=1000
server.recovery.timeout-retry-period=1000
# 客户端配置
client.rm.report.success.enable=false
client.rm.table.meta.check.enable=false
client.tm.commit.retry.count=5
client.tm.rollback.retry.count=5
client.undo.data.validation=true
client.undo.log.serialization=jackson
client.undo.log.table=undo_log
client.log.exceptionRate=100
通过TC控制台和Prometheus需要重点监控:
| 指标类别 | 关键指标 | 报警阈值 |
|---|---|---|
| 事务统计 | 全局事务总数/秒 | > 5000 (视机器配置) |
| 平均处理耗时(ms) | > 500ms | |
| 异常情况 | 二阶段提交失败率 | > 0.5% |
| 全局锁冲突次数/秒 | > 100 | |
| 资源使用 | TC服务CPU使用率 | > 70%持续5分钟 |
| 数据库连接池活跃连接数 | > 80%最大连接数 |
现象:业务日志出现Could not get global lock异常
排查步骤:
sql复制SELECT * FROM lock_table WHERE row_key = '要查询的行键';
sql复制SELECT * FROM global_table WHERE xid = '上一步查到的xid';
现象:PhaseTwo_Rollbacked状态事务增多
解决方案:
properties复制client.tm.commit.retry.count=10
client.tm.rollback.retry.count=10
根据业务特征选择合适的事务模式:
| 维度 | AT模式 | TCC模式 | SAGA模式 |
|---|---|---|---|
| 侵入性 | 无 | 需编码Try/Confirm/Cancel | 需定义状态机 |
| 隔离性 | 读未提交 | 可自定义隔离级别 | 无隔离 |
| 适用场景 | 常规CRUD | 跨系统集成 | 长流程业务 |
| 性能影响 | 中等(解析SQL) | 低 | 低 |
| 复杂度 | 低 | 中 | 高 |
| 数据一致性 | 最终一致 | 最终一致 | 最终一致 |
在电商系统中,我通常采用混合模式:
在网关层添加全局事务ID传递:
java复制public class SeataFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String xid = RootContext.getXID();
if (StringUtils.isEmpty(xid)) {
xid = UUID.randomUUID().toString();
}
return chain.filter(exchange)
.contextWrite(ctx -> ctx.put(Constants.TRANSACTION_ID_KEY, xid));
}
}
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 必须放在分页插件之前
interceptor.addInnerInterceptor(new SeataInnerInterceptor());
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return interceptor;
}
}
虽然Seata已经非常成熟,但在云原生环境下仍面临挑战。我们正在尝试:
在金融级场景中,我们还扩展实现了: