微服务架构中的分布式事务挑战
微服务拆分后,原本在单体应用中一个数据库事务就能保证的数据一致性,变成了跨服务、跨数据库的分布式事务问题。比如电商下单流程涉及订单服务、库存服务、账户服务三个独立数据库,任何一个环节失败都需要全部回滚。Spring Boot框架本身不提供分布式事务解决方案,需要引入专门的事务协调器。
Seata是微服务架构中使用最广泛的开源分布式事务框架。它提供四种事务模式:AT、TCC、SAGA、XA。AT模式对业务代码侵入最小,通过自动生成补偿SQL实现回滚,适合大多数CRUD场景。TCC模式需要手动编写Try/Confirm/Cancel三个接口,灵活度高但代码量大。本文聚焦AT模式的生产实践。
Seata Server部署与配置
Seata架构中,TC(Transaction Coordinator)是独立的事务协调器,管理全局事务的提交和回滚。TM(Transaction Manager)定义全局事务的边界,RM(Resource Manager)管理分支事务。
部署TC Server,使用Nacos作为注册中心,MySQL存储事务日志:
# file.conf - 存储模式配置
store {
mode = "db"
db {
datasource = "druid"
dbType = "mysql"
driverClassName = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://192.168.1.100:3306/seata?useUnicode=true&characterEncoding=utf8"
user = "seata"
password = "seata_pwd"
minConn = 5
maxConn = 30
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
}
}
# registry.conf - 注册中心配置
registry {
type = "nacos"
nacos {
application = "seata-server"
serverAddr = "192.168.1.100:8848"
group = "SEATA_GROUP"
namespace = "public"
cluster = "default"
}
}
config {
type = "nacos"
nacos {
serverAddr = "192.168.1.100:8848"
group = "SEATA_GROUP"
namespace = "public"
}
}
TC Server的数据库需要先执行建表脚本:
-- Seata Server存储表
CREATE TABLE IF NOT EXISTS `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_create` datetime,
`gmt_modified` datetime,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status`, `gmt_modified`)
);
CREATE TABLE IF NOT EXISTS `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`)
);
CREATE TABLE IF NOT EXISTS `lock_table` (
`row_key` varchar(128) NOT NULL,
`xid` varchar(96),
`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`)
);
业务服务集成Seata AT模式
以订单服务为入口,在pom.xml中引入Seata依赖:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2022.0.0.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
registry:
type: nacos
nacos:
server-addr: 192.168.1.100:8848
group: SEATA_GROUP
config:
type: nacos
nacos:
server-addr: 192.168.1.100:8848
group: SEATA_GROUP
data-source-proxy-mode: AT
每个参与分布式事务的数据库必须创建undo_log表,Seata用此表记录数据变更前快照用于回滚:
-- 在订单库、库存库、账户库各自执行
CREATE TABLE IF NOT EXISTS `undo_log` (
`branch_id` bigint NOT NULL COMMENT 'branch transaction id',
`xid` varchar(128) NOT NULL COMMENT 'global transaction id',
`context` varchar(128) NOT NULL COMMENT 'undo_log context',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` datetime(6) NOT NULL COMMENT 'create datetime',
`log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
);
业务代码中,只需在入口方法加@GlobalTransactional注解:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private InventoryFeignClient inventoryClient;
@Autowired
private AccountFeignClient accountClient;
@GlobalTransactional(name = "createOrder", timeoutMills = 60000, rollbackFor = Exception.class)
public OrderResult createOrder(OrderDTO orderDTO) {
// 1. 创建订单(操作订单库)
Order order = new Order();
order.setUserId(orderDTO.getUserId());
order.setProductId(orderDTO.getProductId());
order.setQuantity(orderDTO.getQuantity());
order.setAmount(orderDTO.getAmount());
order.setStatus("CREATED");
orderMapper.insert(order);
// 2. 扣减库存(远程调用库存服务,操作库存库)
Result<Void> inventoryResult = inventoryClient.deduct(orderDTO.getProductId(), orderDTO.getQuantity());
if (!inventoryResult.isSuccess()) {
throw new BusinessException("库存扣减失败: " + inventoryResult.getMessage());
}
// 3. 扣减余额(远程调用账户服务,操作账户库)
Result<Void> accountResult = accountClient.deduct(orderDTO.getUserId(), orderDTO.getAmount());
if (!accountResult.isSuccess()) {
throw new BusinessException("余额扣减失败: " + accountResult.getMessage());
}
// 4. 更新订单状态
order.setStatus("PAID");
orderMapper.updateById(order);
return new OrderResult(order.getId(), "SUCCESS");
}
}
库存服务和账户服务的本地方法仍用@Transactional注解,Seata的DataSourceProxy会自动拦截SQL执行,生成undo_log记录。
AT模式工作原理与回滚机制
AT模式的全局事务执行流程:
阶段一(执行业务SQL):拦截SQL解析出操作类型和影响行,查询变更前数据生成before image,执行SQL,查询变更后数据生成after image。将before/after image序列化存入undo_log表。本地事务在阶段一提交——业务SQL和undo_log在同一个本地事务中,保证原子性。
阶段二(提交或回滚):全局事务提交时,异步删除undo_log记录。全局事务回滚时,根据undo_log中的before image生成反向SQL(UPDATE变UPDATE、INSERT变DELETE、DELETE变INSERT),执行反向SQL恢复数据,然后删除undo_log。
这里的关键设计是阶段一就提交本地事务释放数据库锁,通过全局锁(lock_table)保证隔离性。如果全局事务回滚时数据已被其他事务修改,Seata会检测到after image不一致并重试或报错。
高并发场景下的锁冲突与调优
AT模式在高并发场景下的主要瓶颈是全局锁竞争。当多个全局事务同时修改同一行数据时,后到的请求需要等待前一个事务释放全局锁。消息中间件可以在一定程度上削峰,但核心还是要减少锁持有时间。
// 调优方向1: 缩小事务范围
// 坏的实践 — 把不需要事务的操作放在@GlobalTransactional中
@GlobalTransactional
public void processOrder(OrderDTO dto) {
// 查询操作不需要在分布式事务中
ProductInfo product = productClient.getInfo(dto.getProductId()); // 不必要
UserInfo user = userClient.getInfo(dto.getUserId()); // 不必要
// 只有写操作才需要在事务中
orderMapper.insert(order);
inventoryClient.deduct(...);
}
// 调优方向2: 合理设置超时和重试
@GlobalTransactional(
name = "createOrder",
timeoutMills = 30000, // 超时时间不宜过长
rollbackFor = Exception.class
)
// Seata配置中调整锁重试参数
seata:
client:
rm:
lock:
retry-interval: 10 // 锁重试间隔10ms
retry-times: 30 // 最多重试30次
retry-policy-branch-rollback-on-conflict: true
服务治理层面,需要监控Seata的事务成功率、平均耗时、全局锁等待时间等指标。将Seata metrics接入Prometheus:
# seata配置开启metrics
metrics:
enabled: true
registry-type: compact
exporter-list: prometheus
exporter-prometheus-port: 9898
# Prometheus scrape配置
scrape_configs:
- job_name: 'seata'
static_configs:
- targets: ['seata-server:9898']
API接口规范方面,远程调用的Feign接口需要正确传播全局事务ID(XID)。Seata默认通过HTTP Header传播XID,但自定义Feign拦截器时不小心覆盖Header会导致XID丢失。排查这类问题需要在Feign拦截器中检查XID是否存在:
@Component
public class SeataFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String xid = RootContext.getXID();
if (xid != null) {
template.header(RootContext.KEY_XID, xid);
}
}
}
业务中台建设中消息中间件与分布式事务的配合也需要注意。如果使用RocketMQ事务消息替代Seata的TCC模式,可以减少服务间同步调用,但消息保证的是最终一致性而非强一致性。选择Seata AT还是事务消息,取决于业务对一致性实时性的要求。订单创建场景用Seata AT,库存异步扣减场景用事务消息,两者配合使用是常见架构。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/springboot-wei-fu-wu-fen-bu-shi-shi-wu-chu-li-seataat-mo/