分布式事务详解:从理论到实践(RocketMQ + Seata)
前言
在微服务架构日益普及的今天,分布式事务成为了每个后端开发者必须面对的挑战。本文将从理论到实践,深入探讨分布式事务的解决方案,并结合 RocketMQ 和 Seata 框架给出具体的代码示例。
一、为什么需要分布式事务?
1.1 单体架构 vs 微服务架构
lua
+------------------+ +------------------+
| 单体应用 | | 微服务架构 |
+------------------+ +------------------+
| | | |
| +------------+ | | +-----------+ |
| | 订单 | | | | 订单服务 | |
| +------------+ | | +-----------+ |
| | | | | |
| +------------+ | | +-----------+ |
| | 库存 | | | | 库存服务 | |
| +------------+ | | +-----------+ |
| | | | | |
| +------------+ | | +-----------+ |
| | 账户 | | | | 账户服务 | |
| +------------+ | | +-----------+ |
| | | | | |
| +------------+ | | +-----------+ |
| | 单一数据库 | | | | 各自数据库 | |
| +------------+ | | +-----------+ |
+------------------+ +------------------+
本地事务 分布式事务
1.2 典型业务场景
以电商下单为例,一个完整的下单流程涉及:
diff
用户下单流程:
+--------+ +--------+ +--------+ +--------+
| 创建 | -> | 扣减 | -> | 扣减 | -> | 发送 |
| 订单 | | 库存 | | 余额 | | 通知 |
+--------+ +--------+ +--------+ +--------+
| | | |
v v v v
+--------+ +--------+ +--------+ +--------+
| 订单DB | | 库存DB | | 账户DB | | 消息队列|
+--------+ +--------+ +--------+ +--------+
问题来了: 如果扣减余额失败,但订单已创建、库存已扣减,如何保证数据一致性?
1.3 分布式事务的核心挑战
- 网络不可靠:服务间调用可能超时、失败
- 服务不可用:某个服务可能宕机
- 数据不一致:部分操作成功,部分失败
- 并发问题:多个事务同时操作同一资源
二、分布式事务理论基础
2.1 CAP 理论
markdown
C (Consistency)
一致性
/\
/ \
/ \
/ \
/ CAP \
/ 定理 \
/____________\
/ \
A (Availability) P (Partition Tolerance)
可用性 分区容错性
CAP定理:三者只能同时满足两个
- CA:放弃分区容错(单机系统)
- CP:放弃可用性(强一致性系统)
- AP:放弃一致性(高可用系统)
2.2 BASE 理论
- Basically Available(基本可用):允许损失部分可用性
- Soft State(软状态):允许存在中间状态
- Eventually Consistent(最终一致性):经过一段时间后达到一致
2.3 常见一致性模型对比
| 模型 | 描述 | 适用场景 |
|---|---|---|
| 强一致性 | 写入后立即可读 | 金融交易 |
| 弱一致性 | 不保证何时能读到 | 日志记录 |
| 最终一致性 | 一段时间后一致 | 电商订单 |
三、分布式事务解决方案
3.1 方案对比
diff
+------------------+----------+----------+----------+----------+
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
+------------------+----------+----------+----------+----------+
| 2PC/3PC | 强 | 低 | 中 | 数据库 |
| TCC | 强 | 中 | 高 | 资金类 |
| 本地消息表 | 最终 | 高 | 中 | 通用 |
| 事务消息 | 最终 | 高 | 低 | 通用 |
| Saga | 最终 | 高 | 中 | 长事务 |
| Seata AT | 强 | 中 | 低 | 通用 |
+------------------+----------+----------+----------+----------+
3.2 两阶段提交(2PC)
sql
协调者 参与者A 参与者B
| | |
| 1. Prepare请求 | |
|------------------------>| |
| | |
| 1. Prepare请求 | |
|------------------------------------------------->|
| | |
| 2. 准备就绪/失败 | |
|<------------------------| |
| | |
| 2. 准备就绪/失败 | |
|<-------------------------------------------------|
| | |
| 3. Commit/Rollback | |
|------------------------>| |
| | |
| 3. Commit/Rollback | |
|------------------------------------------------->|
| | |
缺点:同步阻塞、单点故障、数据不一致风险
3.3 TCC(Try-Confirm-Cancel)
lua
+----------------------------------------------------------+
| TCC 模式 |
+----------------------------------------------------------+
| |
| Try阶段 Confirm阶段 Cancel阶段 |
| (资源预留) (确认提交) (回滚释放) |
| |
| +----------+ +----------+ +----------+ |
| | 冻结库存 | | 扣减库存 | | 解冻库存 | |
| +----------+ +----------+ +----------+ |
| | | | |
| +----------+ +----------+ +----------+ |
| | 冻结余额 | | 扣减余额 | | 解冻余额 | |
| +----------+ +----------+ +----------+ |
| |
+----------------------------------------------------------+
四、Seata 分布式事务框架
4.1 Seata 架构
lua
+------------------------------------------------------------------+
| Seata 架构 |
+------------------------------------------------------------------+
| |
| +---------------+ |
| | TC (Server) | |
| | 事务协调者 | |
| +---------------+ |
| / \ |
| / \ |
| +--------+ +--------+ |
| | TM | | RM | |
| | 事务 | | 资源 | |
| | 管理器 | | 管理器 | |
| +--------+ +--------+ |
| | | |
| +---------+ +---------+ |
| | 业务服务 | | 数据库 | |
| +---------+ +---------+ |
| |
+------------------------------------------------------------------+
TC (Transaction Coordinator): 事务协调者,维护全局和分支事务状态
TM (Transaction Manager): 事务管理器,定义全局事务范围
RM (Resource Manager): 资源管理器,管理分支事务资源
4.2 Seata 四种模式
diff
+--------+--------+--------+--------+
| AT | TCC | Saga | XA |
+--------+--------+--------+--------+
| 自动 | 手动 | 长事务 | 强一致 |
| 补偿 | 补偿 | 补偿 | 两阶段 |
| 简单 | 复杂 | 中等 | 简单 |
| 最常用 | 资金类 | 复杂流程| 数据库 |
+--------+--------+--------+--------+
4.3 Seata AT 模式实战
4.3.1 项目结构
bash
seata-demo/
├── order-service/ # 订单服务
├── storage-service/ # 库存服务
├── account-service/ # 账户服务
└── business-service/ # 业务入口服务
4.3.2 Maven 依赖
xml
<!-- pom.xml -->
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Seata -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.7.0</version>
</dependency>
<!-- Seata 与 Spring Cloud Alibaba 集成 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2022.0.0.0</version>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
</dependencies>
4.3.3 配置文件
yaml
# application.yml
server:
port: 8080
spring:
application:
name: business-service
datasource:
url: jdbc:mysql://localhost:3306/seata_order?useUnicode=true&characterEncoding=utf8
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
# Seata 配置
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: my_tx_group
service:
vgroup-mapping:
my_tx_group: default
registry:
type: nacos
nacos:
server-addr: localhost:8848
namespace: ""
group: SEATA_GROUP
config:
type: nacos
nacos:
server-addr: localhost:8848
namespace: ""
group: SEATA_GROUP
4.3.4 数据库表结构
sql
-- 订单数据库
CREATE DATABASE seata_order;
USE seata_order;
CREATE TABLE `t_order` (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT,
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户ID',
`product_id` BIGINT(11) DEFAULT NULL COMMENT '商品ID',
`count` INT(11) DEFAULT NULL COMMENT '数量',
`money` DECIMAL(11,0) DEFAULT NULL COMMENT '金额',
`status` INT(1) DEFAULT NULL COMMENT '订单状态:0创建中,1已完成',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- Seata AT 模式需要的 undo_log 表
CREATE TABLE `undo_log` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(100) NOT NULL,
`context` VARCHAR(128) NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
`log_status` INT(11) NOT NULL,
`log_created` DATETIME NOT NULL,
`log_modified` DATETIME NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 库存数据库
CREATE DATABASE seata_storage;
USE seata_storage;
CREATE TABLE `t_storage` (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '商品ID',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO t_storage VALUES(1, 1, 100, 0, 100);
-- 账户数据库
CREATE DATABASE seata_account;
USE seata_account;
CREATE TABLE `t_account` (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT,
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户ID',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用额度',
`residue` DECIMAL(10,0) DEFAULT NULL COMMENT '剩余额度',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO t_account VALUES(1, 1, 1000, 0, 1000);
4.3.5 核心代码实现
订单实体类
java
@Data
@TableName("t_order")
public class Order {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status;
}
订单服务接口
java
public interface OrderService {
/**
* 创建订单
*/
void createOrder(Long userId, Long productId, Integer count, BigDecimal money);
}
订单服务实现
java
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private StorageClient storageClient;
@Autowired
private AccountClient accountClient;
@Override
public void createOrder(Long userId, Long productId, Integer count, BigDecimal money) {
log.info("========== 开始创建订单 ==========");
// 1. 创建订单
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setCount(count);
order.setMoney(money);
order.setStatus(0);
orderMapper.insert(order);
// 2. 扣减库存
log.info("========== 订单服务调用库存服务,扣减库存 ==========");
storageClient.decrease(productId, count);
// 3. 扣减账户余额
log.info("========== 订单服务调用账户服务,扣减余额 ==========");
accountClient.decrease(userId, money);
// 4. 修改订单状态
log.info("========== 修改订单状态 ==========");
order.setStatus(1);
orderMapper.updateById(order);
log.info("========== 订单创建完成 ==========");
}
}
库存服务 Feign 客户端
java
@FeignClient(value = "storage-service")
public interface StorageClient {
@PostMapping("/storage/decrease")
void decrease(@RequestParam("productId") Long productId,
@RequestParam("count") Integer count);
}
账户服务 Feign 客户端
java
@FeignClient(value = "account-service")
public interface AccountClient {
@PostMapping("/account/decrease")
void decrease(@RequestParam("userId") Long userId,
@RequestParam("money") BigDecimal money);
}
库存服务实现
java
@Service
@Slf4j
public class StorageServiceImpl implements StorageService {
@Autowired
private StorageMapper storageMapper;
@Override
public void decrease(Long productId, Integer count) {
log.info("========== 扣减库存开始 ==========");
storageMapper.decrease(productId, count);
log.info("========== 扣减库存结束 ==========");
}
}
账户服务实现
java
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
public void decrease(Long userId, BigDecimal money) {
log.info("========== 扣减账户余额开始 ==========");
// 模拟超时异常,测试分布式事务回滚
// try {
// TimeUnit.SECONDS.sleep(20);
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
accountMapper.decrease(userId, money);
log.info("========== 扣减账户余额结束 ==========");
}
}
业务入口 - 使用 @GlobalTransactional 开启全局事务
java
@RestController
@RequestMapping("/business")
@Slf4j
public class BusinessController {
@Autowired
private OrderService orderService;
/**
* 下单接口
* @GlobalTransactional 开启 Seata 全局事务
*/
@PostMapping("/purchase")
@GlobalTransactional(name = "purchase-order", rollbackFor = Exception.class)
public String purchase(@RequestParam Long userId,
@RequestParam Long productId,
@RequestParam Integer count,
@RequestParam BigDecimal money) {
log.info("========== 开始全局事务,XID: {} ==========", RootContext.getXID());
orderService.createOrder(userId, productId, count, money);
return "下单成功";
}
}
4.4 Seata TCC 模式实战
java
/**
* TCC 接口定义
*/
@LocalTCC
public interface AccountTccService {
/**
* Try: 冻结金额
*/
@TwoPhaseBusinessAction(name = "accountTcc", commitMethod = "confirm", rollbackMethod = "cancel")
boolean tryFreeze(@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);
/**
* Confirm: 确认扣款
*/
boolean confirm(BusinessActionContext context);
/**
* Cancel: 解冻金额
*/
boolean cancel(BusinessActionContext context);
}
/**
* TCC 实现
*/
@Service
@Slf4j
public class AccountTccServiceImpl implements AccountTccService {
@Autowired
private AccountMapper accountMapper;
@Override
@Transactional
public boolean tryFreeze(Long userId, BigDecimal money) {
log.info("========== Try: 冻结金额 userId={}, money={} ==========", userId, money);
// 检查余额是否充足
Account account = accountMapper.selectByUserId(userId);
if (account.getResidue().compareTo(money) < 0) {
throw new RuntimeException("余额不足");
}
// 冻结金额:residue -= money, frozen += money
accountMapper.freeze(userId, money);
return true;
}
@Override
@Transactional
public boolean confirm(BusinessActionContext context) {
Long userId = Long.valueOf(context.getActionContext("userId").toString());
BigDecimal money = new BigDecimal(context.getActionContext("money").toString());
log.info("========== Confirm: 确认扣款 userId={}, money={} ==========", userId, money);
// 确认扣款:frozen -= money, used += money
accountMapper.confirmDecrease(userId, money);
return true;
}
@Override
@Transactional
public boolean cancel(BusinessActionContext context) {
Long userId = Long.valueOf(context.getActionContext("userId").toString());
BigDecimal money = new BigDecimal(context.getActionContext("money").toString());
log.info("========== Cancel: 解冻金额 userId={}, money={} ==========", userId, money);
// 解冻金额:frozen -= money, residue += money
accountMapper.cancelFreeze(userId, money);
return true;
}
}
五、RocketMQ 事务消息
5.1 事务消息原理
sql
+-----------------------------------------------------------------------+
| RocketMQ 事务消息流程 |
+-----------------------------------------------------------------------+
| |
| 生产者 Broker 消费者 |
| | | | |
| | 1. 发送Half消息 | | |
| |----------------------------->| | |
| | | | |
| | 2. 返回发送结果 | | |
| |<-----------------------------| | |
| | | | |
| | 3. 执行本地事务 | | |
| |----+ | | |
| | | | | |
| |<---+ | | |
| | | | |
| | 4. 提交/回滚事务状态 | | |
| |----------------------------->| | |
| | | | |
| | | 5. 投递消息(Commit) | |
| | |------------------------->| |
| | | | |
| | 回查本地事务状态(超时未确认) | | |
| |<-----------------------------| | |
| | | | |
+-----------------------------------------------------------------------+
5.2 事务消息状态
sql
+------------------+------------------+------------------+
| COMMIT | ROLLBACK | UNKNOW |
+------------------+------------------+------------------+
| 提交消息 | 回滚消息 | 未知状态 |
| 消费者可见 | 消息被删除 | 触发回查 |
+------------------+------------------+------------------+
5.3 Maven 依赖
xml
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
</dependency>
5.4 配置文件
yaml
rocketmq:
name-server: localhost:9876
producer:
group: tx-producer-group
send-message-timeout: 3000
5.5 事务消息生产者
java
@Service
@Slf4j
public class TransactionProducerService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private OrderMapper orderMapper;
/**
* 发送事务消息
*/
public void sendTransactionMessage(OrderDTO orderDTO) {
String transactionId = UUID.randomUUID().toString();
Message<OrderDTO> message = MessageBuilder
.withPayload(orderDTO)
.setHeader("transactionId", transactionId)
.build();
// 发送事务消息
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
"order-topic:create", // topic:tag
message,
orderDTO // arg 参数,传递给本地事务执行器
);
log.info("事务消息发送结果: {}, transactionId: {}",
result.getLocalTransactionState(), transactionId);
}
}
5.6 事务监听器
java
@RocketMQTransactionListener
@Slf4j
public class OrderTransactionListener implements RocketMQLocalTransactionListener {
@Autowired
private OrderMapper orderMapper;
@Autowired
private TransactionLogMapper transactionLogMapper;
/**
* 执行本地事务
*/
@Override
@Transactional
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
String transactionId = (String) msg.getHeaders().get("transactionId");
OrderDTO orderDTO = (OrderDTO) arg;
log.info("========== 执行本地事务,transactionId: {} ==========", transactionId);
try {
// 1. 创建订单
Order order = new Order();
order.setUserId(orderDTO.getUserId());
order.setProductId(orderDTO.getProductId());
order.setCount(orderDTO.getCount());
order.setMoney(orderDTO.getMoney());
order.setStatus(0);
orderMapper.insert(order);
// 2. 记录事务日志(用于回查)
TransactionLog txLog = new TransactionLog();
txLog.setTransactionId(transactionId);
txLog.setOrderId(order.getId());
txLog.setStatus(1); // 1: 已提交
transactionLogMapper.insert(txLog);
log.info("本地事务执行成功,提交消息");
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
log.error("本地事务执行失败,回滚消息", e);
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
* 回查本地事务状态
*/
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String transactionId = (String) msg.getHeaders().get("transactionId");
log.info("========== 回查本地事务状态,transactionId: {} ==========", transactionId);
// 查询事务日志
TransactionLog txLog = transactionLogMapper.selectByTransactionId(transactionId);
if (txLog != null && txLog.getStatus() == 1) {
log.info("本地事务已提交");
return RocketMQLocalTransactionState.COMMIT;
}
log.info("本地事务未提交,回滚消息");
return RocketMQLocalTransactionState.ROLLBACK;
}
}
5.7 事务消息消费者
java
@Service
@RocketMQMessageListener(
topic = "order-topic",
selectorExpression = "create",
consumerGroup = "order-consumer-group"
)
@Slf4j
public class OrderTransactionConsumer implements RocketMQListener<OrderDTO> {
@Autowired
private StorageService storageService;
@Autowired
private AccountService accountService;
@Override
public void onMessage(OrderDTO orderDTO) {
log.info("========== 收到订单消息: {} ==========", orderDTO);
try {
// 扣减库存
storageService.decrease(orderDTO.getProductId(), orderDTO.getCount());
// 扣减账户余额
accountService.decrease(orderDTO.getUserId(), orderDTO.getMoney());
log.info("订单处理完成");
} catch (Exception e) {
log.error("订单处理失败", e);
// 消费失败会自动重试
throw new RuntimeException("订单处理失败", e);
}
}
}
六、本地消息表方案
6.1 架构图
lua
+------------------------------------------------------------------+
| 本地消息表方案 |
+------------------------------------------------------------------+
| |
| 服务A 服务B |
| +------------------+ +------------------+ |
| | 业务操作 | | 业务操作 | |
| +--------+---------+ +--------+---------+ |
| | ^ |
| v | |
| +------------------+ +------------------+ |
| | 本地消息表 | | 消息消费 | |
| +--------+---------+ +--------+---------+ |
| | ^ |
| | 消息队列 | |
| | +-----------------+ | |
| +--->| MQ |------------------+ |
| +-----------------+ |
| ^ |
| | |
| +-----------------+ |
| | 定时任务 | |
| | (扫描未发送) | |
| +-----------------+ |
| |
+------------------------------------------------------------------+
6.2 消息表结构
sql
CREATE TABLE `local_message` (
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`message_id` VARCHAR(64) NOT NULL COMMENT '消息ID',
`message_body` TEXT NOT NULL COMMENT '消息内容',
`message_topic` VARCHAR(128) NOT NULL COMMENT '消息主题',
`message_tag` VARCHAR(64) DEFAULT NULL COMMENT '消息标签',
`status` TINYINT(4) NOT NULL DEFAULT '0' COMMENT '状态: 0-待发送, 1-已发送, 2-已确认',
`retry_count` INT(11) NOT NULL DEFAULT '0' COMMENT '重试次数',
`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_message_id` (`message_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
6.3 核心代码实现
java
@Service
@Slf4j
public class LocalMessageService {
@Autowired
private LocalMessageMapper messageMapper;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 保存本地消息(与业务操作在同一事务中)
*/
@Transactional
public void saveMessage(String topic, String tag, Object payload) {
LocalMessage message = new LocalMessage();
message.setMessageId(UUID.randomUUID().toString());
message.setMessageTopic(topic);
message.setMessageTag(tag);
message.setMessageBody(JSON.toJSONString(payload));
message.setStatus(0);
message.setRetryCount(0);
messageMapper.insert(message);
}
/**
* 发送消息
*/
public void sendMessage(LocalMessage message) {
try {
String destination = message.getMessageTag() != null
? message.getMessageTopic() + ":" + message.getMessageTag()
: message.getMessageTopic();
rocketMQTemplate.syncSend(destination, message.getMessageBody());
// 更新状态为已发送
message.setStatus(1);
messageMapper.updateById(message);
log.info("消息发送成功: {}", message.getMessageId());
} catch (Exception e) {
log.error("消息发送失败: {}", message.getMessageId(), e);
// 增加重试次数
message.setRetryCount(message.getRetryCount() + 1);
messageMapper.updateById(message);
}
}
/**
* 确认消息(消费成功后调用)
*/
public void confirmMessage(String messageId) {
LocalMessage message = messageMapper.selectByMessageId(messageId);
if (message != null) {
message.setStatus(2);
messageMapper.updateById(message);
}
}
}
6.4 定时任务扫描发送
java
@Component
@Slf4j
public class MessageSendTask {
@Autowired
private LocalMessageMapper messageMapper;
@Autowired
private LocalMessageService messageService;
/**
* 每隔10秒扫描一次待发送消息
*/
@Scheduled(fixedRate = 10000)
public void scanAndSendMessage() {
// 查询待发送的消息(status=0 且重试次数小于5)
List<LocalMessage> messages = messageMapper.selectPendingMessages(5);
for (LocalMessage message : messages) {
log.info("扫描到待发送消息: {}", message.getMessageId());
messageService.sendMessage(message);
}
}
}
七、最佳实践与选型建议
7.1 方案选型决策树
lua
开始选型
|
v
+-------------+
| 是否需要 |
| 强一致性? |
+------+------+
|
+------------+------------+
| |
v v
是 否
| |
v v
+-------+-------+ +-------+-------+
| 业务能否 | | 考虑最终 |
| 接受性能损耗? | | 一致性方案 |
+-------+-------+ +-------+-------+
| |
+-------+-------+ +-------+-------+
| | | |
v v v v
是 否 事务消息 本地消息表
| | | |
v v v v
Seata AT Seata TCC RocketMQ 可靠性要求高
7.2 各方案适用场景
| 方案 | 适用场景 | 典型案例 |
|---|---|---|
| Seata AT | 常规业务、对一致性要求高 | 订单创建、库存扣减 |
| Seata TCC | 资金类业务、需要精确控制 | 转账、支付 |
| RocketMQ事务消息 | 异步处理、最终一致性 | 订单通知、积分发放 |
| 本地消息表 | 可靠性要求高、不依赖MQ | 跨系统数据同步 |
| Saga | 长事务、复杂业务流程 | 机票+酒店+租车预订 |
7.3 生产环境注意事项
css
+------------------------------------------------------------------+
| 生产环境 Checklist |
+------------------------------------------------------------------+
| |
| [ ] TC Server 高可用部署(至少3节点) |
| [ ] 配置合理的事务超时时间 |
| [ ] 监控全局事务状态 |
| [ ] 处理悬挂事务 |
| [ ] 幂等性设计 |
| [ ] 异常重试机制 |
| [ ] 分布式锁防并发 |
| [ ] 日志记录完整 |
| |
+------------------------------------------------------------------+
7.4 幂等性设计
java
@Service
@Slf4j
public class IdempotentOrderService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private OrderMapper orderMapper;
private static final String IDEMPOTENT_KEY_PREFIX = "order:idempotent:";
/**
* 幂等性创建订单
*/
@Transactional
public Order createOrderIdempotent(String requestId, OrderDTO orderDTO) {
String key = IDEMPOTENT_KEY_PREFIX + requestId;
// 1. 检查是否已处理
Boolean setResult = redisTemplate.opsForValue()
.setIfAbsent(key, "processing", Duration.ofMinutes(30));
if (Boolean.FALSE.equals(setResult)) {
// 已存在,查询已有订单返回
log.info("重复请求,requestId: {}", requestId);
Order existOrder = orderMapper.selectByRequestId(requestId);
if (existOrder != null) {
return existOrder;
}
throw new RuntimeException("订单处理中,请稍后查询");
}
try {
// 2. 创建订单
Order order = new Order();
order.setRequestId(requestId);
order.setUserId(orderDTO.getUserId());
order.setProductId(orderDTO.getProductId());
order.setCount(orderDTO.getCount());
order.setMoney(orderDTO.getMoney());
order.setStatus(0);
orderMapper.insert(order);
// 3. 更新 Redis 状态
redisTemplate.opsForValue().set(key, "completed", Duration.ofHours(24));
return order;
} catch (Exception e) {
// 失败则删除 key,允许重试
redisTemplate.delete(key);
throw e;
}
}
}
八、总结
分布式事务是微服务架构中的核心难题,没有银弹方案。选择合适的方案需要权衡:
- 一致性要求:强一致性选 Seata,最终一致性选事务消息
- 性能要求:高性能选异步方案,低延迟选 AT 模式
- 业务复杂度:简单业务用 AT,复杂业务用 TCC 或 Saga
- 运维成本:本地消息表最简单,Seata 需要维护 TC
核心原则:
- 能不用分布式事务就不用:合理拆分服务边界
- 优先考虑最终一致性:大多数业务可以接受
- 做好幂等设计:分布式环境必备
- 完善监控告警:及时发现和处理异常事务
希望本文能帮助你在实际项目中更好地应对分布式事务挑战!