微服务架构下分布式事务的必要性
单体应用中,数据库本地事务(ACID)就能保证数据一致性。拆分成微服务后,一个业务操作可能跨越多个服务,每个服务有独立的数据库,本地事务无法覆盖跨服务的数据一致性。以电商下单为例:订单服务创建订单、库存服务扣减库存、账户服务扣减余额,三步必须全部成功或全部回滚。
Spring Boot框架中处理分布式事务的主流方案:
| 方案 | 一致性 | 性能 | 侵入性 | 适用场景 |
|——|——–|——|——–|———-|
| 2PC(XA) | 强一致 | 低 | 低 | 传统企业应用 |
| TCC | 强一致 | 中 | 高 | 资金交易 |
| Saga | 最终一致 | 高 | 中 | 长流程业务 |
| Seata AT | 强一致 | 中高 | 低 | 通用业务 |
Seata AT模式因侵入性最低、对业务代码几乎零改造,成为Spring Boot微服务架构中分布式事务的首选方案。
Seata AT模式的工作原理
AT模式的核心是”自动拦截SQL,自动补偿”:
1. 一阶段:拦截业务SQL,执行前保存before-image,执行后保存after-image,生成undo_log,同时提交本地事务
2. 二阶段提交:异步删除undo_log
3. 二阶段回滚:根据undo_log的before-image反向补偿,恢复数据
对比TCC需要业务方实现Try/Confirm/Cancel三个接口,AT模式只需要加一个@GlobalTransactional注解。
但这种自动化的代价是:Seata需要代理数据源,拦截所有SQL来构建undo_log。对SQL的兼容性是AT模式最大的坑。
Seata Server部署与配置
Seata 2.x版本的Server端支持多种存储模式,生产环境推荐使用数据库存储(避免Server重启丢失事务日志):
# application.yml - Seata Server配置
server:
port: 8091
seata:
store:
mode: db
db:
datasource: druid
db-type: mysql
url: jdbc:mysql://mysql-host:3306/seata?rewriteBatchedStatements=true
user: seata
password: ${SEATA_DB_PASS}
min-conn: 5
max-conn: 100
global-table: global_table
branch-table: branch_table
lock-table: lock_table
distributed-lock-table: distributed_lock
# 注册中心
registry:
type: nacos
nacos:
server-addr: nacos-host:8848
namespace: seata
group: DEFAULT_GROUP
application: seata-server
# 配置中心
config:
type: nacos
nacos:
server-addr: nacos-host:8848
namespace: seata
group: DEFAULT_GROUP
Seata Server的数据库建表脚本在官方GitHub的script/server/db目录下,需要提前在MySQL中创建。
Spring Boot客户端集成步骤
1. 添加依赖:
<!-- pom.xml -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!-- 如果使用Spring Cloud,用这个 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2023.0.1.2</version>
</dependency>
2. 客户端配置:
# 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: nacos-host:8848
namespace: seata
group: DEFAULT_GROUP
3. 业务数据库建undo_log表:
每个参与分布式事务的微服务的数据库都需要这张表:
CREATE TABLE IF NOT EXISTS undo_log (
branch_id BIGINT NOT NULL,
xid VARCHAR(128) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT(11) NOT NULL,
log_created DATETIME(6) NOT NULL,
log_modified DATETIME(6) NOT NULL,
PRIMARY KEY (branch_id),
KEY idx_xid (xid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
4. 在业务方法上加注解:
@Service
public class OrderService {
@GlobalTransactional(timeoutMills = 60000, name = "create-order")
public Order createOrder(OrderDTO orderDTO) {
// 1. 创建订单(本地事务)
Order order = orderMapper.insert(orderDTO);
// 2. 扣减库存(远程调用)
inventoryClient.deduct(orderDTO.getProductId(), orderDTO.getQuantity());
// 3. 扣减账户余额(远程调用)
accountClient.debit(orderDTO.getUserId(), orderDTO.getAmount());
return order;
}
}
高并发场景下的锁冲突与优化
AT模式在一阶段就会获取全局锁(记录在Seata Server的lock_table中),防止脏写。但这也带来了锁冲突问题:
当两个分布式事务同时修改同一行数据时,后到的事务会因获取全局锁超时而回滚。在高并发库存扣减场景下,这个问题尤为严重。
优化方案:
1. 缩短全局事务时间:全局事务持有锁的时间越长,冲突概率越高。把非核心操作(如发通知、记日志)移出全局事务。
@GlobalTransactional
public void createOrder(OrderDTO dto) {
// 只放核心数据操作
Order order = orderMapper.insert(dto);
inventoryClient.deduct(dto.getProductId(), dto.getQuantity());
accountClient.debit(dto.getUserId(), dto.getAmount());
}
// 非核心操作在事务外执行
@Async
public void sendNotification(Long orderId) {
notificationClient.send(orderId);
}
2. 降低隔离级别到读未提交:默认全局锁的隔离级别是读已提交,如果业务允许短暂读到未提交数据,可以降低隔离级别减少锁持有时间:
@GlobalTransactional(isolation = Isolation.READ_UNCOMMITTED)
3. 热点数据用Redis预扣减:库存等热点数据先用Redis原子操作预扣减,再异步同步到数据库,绕过分布式事务的锁竞争。
消息中间件与最终一致性方案
不是所有业务都需要强一致。对于长流程业务(如退款、物流),Saga模式或基于消息中间件的最终一致性更合适:
// 退款Saga编排
@Saga
public class RefundSaga {
@SagaStep(compensateMethod = "cancelRefund")
public void initiateRefund(RefundRequest req) {
accountClient.refund(req.getUserId(), req.getAmount());
}
@SagaStep(compensateMethod = "restoreInventory")
public void restoreStock(RefundRequest req) {
inventoryClient.restore(req.getProductId(), req.getQuantity());
}
@SagaStep
public void notifyUser(RefundRequest req) {
notificationClient.sendRefundNotice(req.getUserId());
}
}
结合消息中间件实现可靠消息最终一致性:业务服务发消息前先写本地消息表,定时任务扫描消息表确保消息投递成功。RocketMQ的事务消息机制本质相同,但实现更优雅,不需要本地消息表。
Seata与RocketMQ事务消息的选型标准:跨3个以上服务的强一致业务用Seata AT,2个服务间的一致性用事务消息,长流程异步业务用Saga。
生产环境踩坑清单
1. 数据源代理失效:Seata AT必须代理DataSource才能拦截SQL。如果项目中配置了多个DataSource(如读写分离),确保@GlobalTransactional使用的是Seata代理的那个。检查方法:打断点看DataSource的实现类是否为DataSourceProxy。
2. undo_log序列化失败:MySQL的JSON/TEXT列在undo_log的rollback_info中可能超过LONGBLOB限制。避免在分布式事务中操作超过1MB的单行数据。
3. 回滚时before-image为空:发生在事务提交后、undo_log被清理前,数据被其他事务直接修改(绕过Seata全局锁)。AT模式能检测到这种脏写并抛出异常,但需要人工介入。
4. 全局事务超时:默认60秒。如果业务确实需要更长执行时间,调整timeoutMills参数,但不要设太大——超时的事务会被Seata Server自动回滚,如果此时业务方还在执行,可能导致部分回滚的不一致状态。
5. 服务治理层面的雪崩:Seata Server是单点(即便是集群部署)。Server不可用时不影响已提交的事务,但新事务无法开启。在服务治理策略中,Seata Server降级后应自动切换到本地事务模式,而非让整个服务不可用。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/springboot-wei-fu-wu-fen-bu-shi-shi-wu-shi-zhan-seataat-mo/