微服务分布式事务:轻量级替代方案(抛弃笨重 Seata)
微服务分布式事务:轻量级替代方案(抛弃笨重 Seata) #
Seata 重在哪?要单独部署 TC 服务、建多张事务表、客户端代理数据源、配置繁琐、运维复杂、占用资源多,小项目完全没必要。本文给出 最轻量 → 够用 的 4 套方案,都是不用部署中间件、零额外服务、代码极简的方案,Java SpringCloud 直接能用。
📋 目录 #
- 一、为什么抛弃 Seata?
- 二、方案一:本地消息表(最推荐)
- 三、方案二:可靠 MQ 最终一致性
- 四、方案三:状态机补偿
- 五、方案四:完全不用分布式事务(最优解)
- 六、选型决策指南
- 七、面试高频问答
一、为什么抛弃 Seata? #
Seata 的重量级代价 #
| 维度 | Seata AT/TCC | 本地消息表 | 差距 |
|---|---|---|---|
| 额外服务 | 必须部署 TC Server | 无 | Seata 多一个服务 |
| 数据库开销 | 每个库都要 undo_log + lock_table | 仅一张消息表 | Seata 每库 2+ 张表 |
| 代码侵入 | 低(AT)/ 高(TCC) | 低 | TCC 侵入性更大 |
| 性能影响 | 全局锁竞争,RT 增加 30%+ | 无锁,本地事务 | 消息表更快 |
| 运维复杂度 | TC 集群 + 注册中心 + 配置中心 | 无 | Seata 运维成本极高 |
| 适用规模 | 大厂复杂场景 | 中小项目 90% 场景 | 小项目 Seata 过度设计 |
核心结论 #
90% 的分布式事务场景,根本不需要 Seata。 本地消息表 + 异步补偿就能搞定,运维成本降一个数量级。
二、方案一:本地消息表(最推荐) #
定位:零组件依赖、生产最稳、运维成本为零的首选方案
2.1 核心原理 #
2.2 数据库表设计 #
-- 本地消息表(每个需要发送消息的业务库都建一张)
CREATE TABLE local_message (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
biz_no VARCHAR(64) NOT NULL COMMENT '业务单号(如订单号)',
biz_type VARCHAR(32) NOT NULL COMMENT '业务类型(如 ORDER_CREATE)',
target_service VARCHAR(64) NOT NULL COMMENT '目标服务(如 inventory-service)',
content VARCHAR(2048) NOT NULL COMMENT '消息体 JSON(业务参数)',
status TINYINT NOT NULL DEFAULT 0 COMMENT '0待发送 1已发送 2失败 3已确认',
retry_count INT NOT NULL DEFAULT 0 COMMENT '已重试次数',
max_retry INT NOT NULL DEFAULT 5 COMMENT '最大重试次数',
next_retry_time DATETIME NOT NULL COMMENT '下次重试时间',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_biz (biz_no, biz_type),
KEY idx_status_retry (status, next_retry_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本地消息表';
设计要点:
uk_biz保证幂等——同一笔业务不会重复插入消息;idx_status_retry让扫描查询走索引,不扫全表。
2.3 完整代码实现 #
2.3.1 实体类 #
@Data
@TableName("local_message")
public class LocalMessage {
@TableId(type = IdType.AUTO)
private Long id;
private String bizNo;
private String bizType;
private String targetService;
private String content;
/** 0待发送 1已发送 2失败 3已确认 */
private Integer status;
private Integer retryCount;
private Integer maxRetry;
private LocalDateTime nextRetryTime;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
2.3.2 Mapper 层 #
@Mapper
public interface LocalMessageMapper extends BaseMapper<LocalMessage> {
/**
* 扫描待发送的消息(走索引,避免全表扫描)
*/
@Select("SELECT * FROM local_message " +
"WHERE status = 0 AND next_retry_time <= NOW() " +
"LIMIT #{limit}")
List<LocalMessage> scanPendingMessages(@Param("limit") int limit);
/**
* 更新状态为已发送(乐观锁,防止并发重复处理)
*/
@Update("UPDATE local_message SET status = 1, update_time = NOW() " +
"WHERE id = #{id} AND status = 0")
int markSent(@Param("id") Long id);
}
2.3.3 业务层 —— 核心事务方法 #
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderMapper orderMapper;
private final LocalMessageMapper messageMapper;
private final ObjectMapper objectMapper;
/**
* 创建订单 —— 核心点:订单 + 消息 同一个本地事务
*/
@Transactional(rollbackFor = Exception.class)
public OrderDTO createOrder(OrderCreateRequest request) {
// 1. 创建订单
Order order = new Order();
order.setOrderNo(request.getOrderNo());
order.setUserId(request.getUserId());
order.setAmount(request.getAmount());
order.setStatus(OrderStatus.CREATED);
orderMapper.insert(order);
// 2. 写入本地消息(同一个事务,保证一致性)
LocalMessage message = new LocalMessage();
message.setBizNo(order.getOrderNo());
message.setBizType("ORDER_CREATE");
message.setTargetService("inventory-service");
message.setContent(objectMapper.writeValueAsString(
Map.of("orderNo", order.getOrderNo(),
"skuId", request.getSkuId(),
"quantity", request.getQuantity())
));
message.setStatus(0);
message.setRetryCount(0);
message.setMaxRetry(5);
message.setNextRetryTime(LocalDateTime.now());
messageMapper.insert(message);
// ✅ 事务提交 = 订单 + 消息同时落库,要么一起成功,要么一起失败
return OrderDTO.fromEntity(order);
}
}
2.3.4 定时任务 —— 异步消息投递 #
@Component
@RequiredArgsConstructor
@Slf4j
public class MessageScanner {
private final LocalMessageMapper messageMapper;
private final RestTemplate restTemplate;
/**
* 每 5 秒扫描一次待发送消息
*/
@Scheduled(fixedRate = 5000)
public void scanAndSend() {
List<LocalMessage> messages = messageMapper.scanPendingMessages(100);
for (LocalMessage msg : messages) {
try {
// CAS 更新状态,防止并发重复处理
int updated = messageMapper.markSent(msg.getId());
if (updated == 0) continue; // 已被其他实例处理
// 调用下游服务
String url = "http://" + msg.getTargetService() + "/api/internal/handle";
ResponseEntity<Void> resp = restTemplate.postForEntity(
url, JSON.parseObject(msg.getContent()), Void.class);
if (resp.getStatusCode().is2xxSuccessful()) {
// 调用成功 → 标记完成
msg.setStatus(3); // 已确认
messageMapper.updateById(msg);
} else {
handleFailure(msg);
}
} catch (Exception e) {
log.warn("消息投递失败, id={}, bizNo={}, err={}",
msg.getId(), msg.getBizNo(), e.getMessage());
handleFailure(msg);
}
}
}
private void handleFailure(LocalMessage msg) {
msg.setRetryCount(msg.getRetryCount() + 1);
if (msg.getRetryCount() >= msg.getMaxRetry()) {
msg.setStatus(2); // 失败(需人工介入)
log.error("消息超过最大重试次数, id={}, bizNo={}", msg.getId(), msg.getBizNo());
} else {
msg.setStatus(0); // 退回待发送,等待下次重试
// 指数退避:1min, 2min, 4min, 8min, 16min
int delayMinutes = (int) Math.pow(2, msg.getRetryCount());
msg.setNextRetryTime(LocalDateTime.now().plusMinutes(delayMinutes));
}
messageMapper.updateById(msg);
}
}
2.3.5 下游消费端 —— 幂等处理 #
@RestController
@RequestMapping("/api/internal")
@RequiredArgsConstructor
public class InternalApiController {
private final InventoryService inventoryService;
private final RedisTemplate<String, String> redisTemplate;
/**
* 下游接口必须幂等!同一笔订单可能被重复调用
*/
@PostMapping("/handle")
public Result<Void> handleOrderMessage(@RequestBody OrderMessage msg) {
String idempotentKey = "idempotent:order:" + msg.getOrderNo();
Boolean firstTime = redisTemplate.opsForValue()
.setIfAbsent(idempotentKey, "1", 24, TimeUnit.HOURS);
if (Boolean.FALSE.equals(firstTime)) {
log.info("重复消息,已跳过, orderNo={}", msg.getOrderNo());
return Result.success(); // 幂等返回成功,不重复扣减
}
inventoryService.deduct(msg.getSkuId(), msg.getQuantity());
return Result.success();
}
}
2.4 生产级增强要点 #
| 增强点 | 实现方式 | 必要性 |
|---|---|---|
| CAS 防并发 | UPDATE ... WHERE status=0 |
⭐⭐⭐ 必须实现 |
| 指数退避 | 2^n 分钟递增 |
⭐⭐⭐ 必须实现 |
| 死信告警 | 超过最大重试 → 状态=2 + 告警 | ⭐⭐⭐ 必须实现 |
| 管理后台 | 查询/手动重发消息 | ⭐⭐ 强烈建议 |
| 批量扫描 | 一次扫描 100~500 条 | ⭐⭐ 强烈建议 |
| 分布式锁 | 多实例用 Redis/Zookeeper 锁 | ⭐ 视部署规模 |
2.5 适用场景 #
- ✅ 电商下单(订单 → 扣库存、积分、优惠券)
- ✅ 支付流水(支付成功 → 通知业务、记账)
- ✅ 跨服务数据同步(主库 → 搜索引擎、缓存)
- ✅ 中小企业90% 的跨服务调用场景
三、方案二:可靠 MQ 最终一致性 #
定位:已有 MQ 环境(RabbitMQ / RocketMQ)时的轻量选择
3.1 核心原理 #
3.2 三种 MQ 可靠投递模式对比 #
| 模式 | 原理 | MQ 要求 | 复杂度 | 推荐度 |
|---|---|---|---|---|
| 本地消息表 + MQ | 本地事务写消息表 → 定时扫表投递 MQ | 任意 MQ | 低 | ⭐⭐⭐⭐⭐ |
| RocketMQ 事务消息 | 半消息 → 执行本地事务 → 提交/回滚 | RocketMQ | 中 | ⭐⭐⭐⭐ |
| RabbitMQ Confirm + 消费者幂等 | 确认投递 + 消费端幂等 | RabbitMQ | 中 | ⭐⭐⭐ |
3.3 推荐实现:本地消息表 + MQ(最稳妥) #
@Service
@RequiredArgsConstructor
@Slf4j
public class OrderServiceV2 {
private final OrderMapper orderMapper;
private final LocalMessageMapper messageMapper;
private final RocketMQTemplate rocketMQTemplate;
private final ObjectMapper objectMapper;
/**
* 创建订单 —— 本地消息表保证可靠投递
*/
@Transactional(rollbackFor = Exception.class)
public void createOrder(OrderCreateRequest request) {
// 1. 创建订单
orderMapper.insert(buildOrder(request));
// 2. 写本地消息表
LocalMessage msg = new LocalMessage();
msg.setBizNo(request.getOrderNo());
msg.setBizType("ORDER_CREATE");
msg.setContent(objectMapper.writeValueAsString(request));
msg.setStatus(0);
messageMapper.insert(msg);
}
/**
* 定时扫描 → 发送到 MQ(比直接调 HTTP 更解耦)
*/
@Scheduled(fixedRate = 3000)
public void scanAndSendToMQ() {
List<LocalMessage> messages = messageMapper.scanPendingMessages(200);
for (LocalMessage msg : messages) {
try {
rocketMQTemplate.convertAndSend(
"order-topic",
msg.getContent(),
message -> {
message.setKeys(msg.getBizNo());
return message;
}
);
// MQ 发送成功 → 更新状态
msg.setStatus(1); // 已发送到 MQ
messageMapper.updateById(msg);
} catch (Exception e) {
log.warn("MQ 发送失败, bizNo={}", msg.getBizNo());
handleFailure(msg); // 同方案一的重试逻辑
}
}
}
}
3.4 消费端可靠消费 #
@Component
@RocketMQMessageListener(
topic = "order-topic",
consumerGroup = "inventory-consumer-group"
)
@Slf4j
public class OrderMessageConsumer implements RocketMQListener<String> {
private final InventoryService inventoryService;
private final RedisTemplate<String, String> redisTemplate;
@Override
public void onMessage(String body) {
OrderMessage msg = JSON.parseObject(body, OrderMessage.class);
// 幂等检查(Redis 去重)
String key = "consume:order:" + msg.getOrderNo();
Boolean first = redisTemplate.opsForValue()
.setIfAbsent(key, "1", 48, TimeUnit.HOURS);
if (Boolean.FALSE.equals(first)) {
return; // 重复消费,跳过
}
try {
inventoryService.deduct(msg.getSkuId(), msg.getQuantity());
} catch (Exception e) {
// 抛出异常 → MQ 自动重试
// 达到最大重试次数 → 进入死信队列 → 人工处理
throw new RuntimeException("消费失败,等待 MQ 重试", e);
}
}
}
3.5 MQ 方案 vs 本地消息表方案 #
| 维度 | 纯本地消息表 | 本地消息表 + MQ |
|---|---|---|
| 部署依赖 | 无 | 需要 MQ |
| 实时性 | 5~30 秒延迟 | 1~5 秒延迟 |
| 吞吐量 | 依赖 HTTP 调用 | MQ 天然高吞吐 |
| 解耦程度 | 直接 HTTP 调用下游 | 完全解耦 |
| 适用规模 | QPS < 1000 | QPS > 1000 |
| 运维成本 | 零 | 需维护 MQ |
建议:如果项目中已有 MQ,优先用「本地消息表 + MQ」模式;如果没有 MQ 且 QPS 不高,纯本地消息表方案(方案一)就足够了。
四、方案三:状态机补偿 #
定位:流程状态类业务的最佳选择,零中间件、零额外表
4.1 核心原理 #
4.2 实现方式 #
4.2.1 状态定义与流转 #
public enum OrderStatus {
CREATED, // 已创建
PAID, // 已支付
INVENTORY_DEDUCTED, // 库存已扣
SHIPPED, // 已发货
COMPLETED, // 已完成
INVENTORY_FAIL, // 库存扣减失败
COMPENSATING, // 补偿中
REFUNDED, // 已退款
CANCELLED; // 已取消
/**
* 合法状态流转(状态机核心)
*/
public boolean canTransitionTo(OrderStatus target) {
return switch (this) {
case CREATED -> Set.of(PAID, CANCELLED).contains(target);
case PAID -> Set.of(INVENTORY_DEDUCTED, INVENTORY_FAIL, COMPENSATING).contains(target);
case INVENTORY_DEDUCTED -> Set.of(SHIPPED, COMPENSATING).contains(target);
case INVENTORY_FAIL -> Set.of(COMPENSATING, CANCELLED).contains(target);
case COMPENSATING -> Set.of(REFUNDED, PAID).contains(target);
case SHIPPED -> COMPLETED.equals(target);
default -> false;
};
}
}
4.2.2 安全状态变更 #
@Service
@RequiredArgsConstructor
public class OrderStateMachine {
private final OrderMapper orderMapper;
/**
* 安全的状态变更(CAS + 状态机校验)
*/
@Transactional(rollbackFor = Exception.class)
public boolean transition(String orderNo, OrderStatus expected, OrderStatus target) {
// 1. 校验状态机规则
if (!expected.canTransitionTo(target)) {
throw new IllegalStateException(
String.format("非法状态流转: %s → %s, orderNo=%s", expected, target, orderNo));
}
// 2. CAS 更新(乐观锁,防止并发问题)
int rows = orderMapper.updateStatus(
orderNo, expected.ordinal(), target.ordinal());
if (rows == 0) {
throw new ConcurrencyException("状态已变更,请重试");
}
return true;
}
}
4.2.3 定时校对(补偿核心) #
@Component
@RequiredArgsConstructor
@Slf4j
public class OrderConsistencyChecker {
private final OrderMapper orderMapper;
private final OrderStateMachine stateMachine;
private final InventoryClient inventoryClient;
private final PaymentClient paymentClient;
/**
* 每 30 秒校对一次订单状态一致性
* 核心:上游状态 = PAID,但库存状态不确定 → 主动去库存服务确认
*/
@Scheduled(fixedRate = 30000)
public void checkConsistency() {
// 查找 PAID 超过 1 分钟但未进入下一步的订单
List<Order> stuckOrders = orderMapper.selectStuckOrders(
OrderStatus.PAID, Duration.ofMinutes(1));
for (Order order : stuckOrders) {
try {
// 向库存服务查询:这笔订单是否已经扣过库存?
boolean deducted = inventoryClient.checkDeduction(order.getOrderNo());
if (deducted) {
// 已扣但本地状态没更新 → 补偿状态
stateMachine.transition(
order.getOrderNo(), OrderStatus.PAID, OrderStatus.INVENTORY_DEDUCTED);
} else {
// 没扣 → 触发补偿(退款)
stateMachine.transition(
order.getOrderNo(), OrderStatus.PAID, OrderStatus.COMPENSATING);
paymentClient.refund(order.getOrderNo());
}
} catch (Exception e) {
log.warn("订单校对失败, orderNo={}", order.getOrderNo(), e);
}
}
}
}
4.3 状态机补偿 vs 本地消息表 #
| 维度 | 本地消息表 | 状态机补偿 |
|---|---|---|
| 额外组件 | 一张消息表 | 无(复用业务表) |
| 实现复杂度 | 中(消息表 + 扫描器) | 中(状态枚举 + 校对器) |
| 适用业务 | 通用(任何跨服务调用) | 流程型(订单、支付、审批) |
| 补偿方式 | 重试消息 | 反向操作(退款、回库) |
| 一致性保证 | 最终一致 | 最终一致 |
| 数据可视化 | 消息表直接查 | 业务状态直接查 |
4.4 适用场景 #
- ✅ 订单流转(创建 → 支付 → 发货 → 完成)
- ✅ 支付流程(预扣 → 确认 → 到账)
- ✅ 审批流程(提交 → 审核 → 通过/拒绝)
- ✅ 任何有明确状态流转的业务
五、方案四:完全不用分布式事务(最优解) #
定位:从架构层面消灭分布式事务问题,最轻、最优
5.1 核心思想 #
5.2 四条路径 #
路径一:拆库改单库(最直接) #
❌ 错误拆分:订单库 + 库存库 + 用户库 → 跨库事务
✅ 正确做法:共用一个业务库,用 Service 层拆分模块
-- 一个库,多个 Schema 或前缀
CREATE TABLE biz_order (...); -- 订单
CREATE TABLE biz_inventory (...); -- 库存
CREATE TABLE biz_account (...); -- 账户
-- 一个本地事务搞定一切
BEGIN;
INSERT INTO biz_order VALUES (...);
UPDATE biz_inventory SET stock = stock - 1 WHERE sku_id = ?;
UPDATE biz_account SET balance = balance - ? WHERE user_id = ?;
COMMIT;
适用条件:数据量 < 5000 万、QPS < 5000、团队 < 20 人。很多小团队根本不需要拆库。
路径二:核心写单库 + 非核心异步 #
@Service
@RequiredArgsConstructor
public class OrderService {
/**
* 核心链路同步 + 非核心异步
*/
@Transactional
public void createOrder(OrderRequest req) {
// ✅ 核心业务:同步强一致(同库事务)
orderMapper.insert(buildOrder(req));
inventoryMapper.deduct(req.getSkuId(), req.getQuantity());
accountMapper.deduct(req.getUserId(), req.getAmount());
// ✅ 非核心业务:异步最终一致
asyncTaskExecutor.execute(() -> {
try { scoreService.addScore(req.getUserId(), req.getAmount()); } catch (Exception e) { log.warn("积分失败"); }
try { couponService.useCoupon(req.getCouponId()); } catch (Exception e) { log.warn("优惠券失败"); }
try { searchService.updateIndex(req.getOrderNo()); } catch (Exception e) { log.warn("索引更新失败"); }
});
}
}
路径三:CQRS 读写分离 #
核心思路:写操作保证强一致,读操作接受最终一致。用 Canal/Debezium 监听 Binlog 异步同步数据,不存在分布式事务。
路径四:SAGA 编排(手动版) #
/**
* 手动 SAGA:比 Seata TCC 简单得多,不用任何框架
* 核心思想:正向执行 + 失败反向补偿
*/
@Service
@RequiredArgsConstructor
public class OrderSaga {
private final PaymentClient paymentClient;
private final InventoryClient inventoryClient;
public void execute(OrderRequest req) {
// 正向操作(记录每一步结果)
boolean paymentDone = false;
boolean inventoryDone = false;
try {
// Step 1: 扣款
paymentClient.deduct(req.getUserId(), req.getAmount());
paymentDone = true;
// Step 2: 扣库存
inventoryClient.deduct(req.getSkuId(), req.getQuantity());
inventoryDone = true;
} catch (Exception e) {
// 反向补偿(按已完成的步骤逆序回滚)
if (inventoryDone) {
try { inventoryClient.rollback(req.getSkuId(), req.getQuantity()); } catch (Exception ignored) {}
}
if (paymentDone) {
try { paymentClient.refund(req.getUserId(), req.getAmount()); } catch (Exception ignored) {}
}
throw new BusinessException("订单创建失败,已自动回滚");
}
}
}
5.3 什么时候真的需要分布式事务? #
| 场景 | 是否需要 | 建议 |
|---|---|---|
| 单体改微服务过渡期 | ❌ | 先单库多 Service |
| 核心链路(支付、下单) | ⚠️ | 本地消息表 / 同库事务 |
| 非核心链路(积分、通知) | ❌ | 异步 + 最终一致 |
| 跨公司服务对接 | ⚠️ | 本地消息表 + 对账 |
| 超大流量电商(双十一) | ✅ | Seata / TCC |
| 金融核心系统(银行) | ✅ | TCC / SAGA 框架 |
原则:能用同步不用异步,能用单库不用跨库,能用本地消息表不用 Seata。
六、选型决策指南 #
6.1 快速决策树 #
6.2 四大方案对比总览 #
| 维度 | 本地消息表 | MQ 最终一致 | 状态机补偿 | 不用分布式事务 |
|---|---|---|---|---|
| 额外部署 | 无 | MQ(已有则无) | 无 | 无 |
| 额外表 | 1 张消息表 | 1 张消息表 | 无 | 无 |
| 代码侵入 | 中 | 中 | 中 | 低 |
| 实时性 | 秒级 | 毫秒~秒级 | 秒级 | 毫秒级 |
| 一致性 | 最终一致 | 最终一致 | 最终一致 | 强一致/最终一致 |
| 适用规模 | 小~中 | 中~大 | 小~中 | 任何规模 |
| 运维成本 | ⭐ | ⭐⭐ | ⭐ | ⭐ |
| 推荐指数 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
6.3 一句话选型 #
| 你的情况 | 选这个 |
|---|---|
| 小项目、不想部署任何新服务 | 本地消息表(方案一) |
| 已有 MQ 环境 | 本地消息表 + MQ(方案二) |
| 流程状态类业务(订单、审批) | 状态机补偿(方案三) |
| 还没到拆库的规模 | 单库多 Service(方案四路径一) |
| 核心同步 + 非核心异步 | 同库事务 + 异步补偿(方案四路径二) |
| 大厂双十一级别 | Seata TCC(不在本文范围) |
七、面试高频问答 #
Q1:本地消息表和事务消息有什么区别? #
本地消息表:
- 消息存在业务库,和业务数据在同一个本地事务里
- 定时任务扫表发送
- 不依赖任何 MQ 特性,用任意 MQ 都行
事务消息(RocketMQ):
- 利用 MQ 的半消息机制
- 发半消息 → 执行本地事务 → 提交/回滚消息
- 依赖 RocketMQ 特有功能
面试回答要点:
"本地消息表更通用,不依赖特定 MQ;事务消息实时性更好但强依赖 RocketMQ。
生产环境推荐本地消息表 + 任意 MQ 的组合,兼容性和可靠性都更好。"
Q2:消息重复消费怎么保证幂等? #
三层幂等保障:
1. Redis SETNX 去重(第一道防线,性能高)
2. 数据库唯一索引兜底(如 biz_no + biz_type)
3. 业务逻辑幂等(如 UPDATE stock = stock - 1 天然幂等)
面试回答要点:
"先 Redis 去重挡掉大部分重复请求,数据库唯一约束做最终兜底。
业务操作尽量设计成幂等的,比如用 UPDATE 而不是 INSERT。"
Q3:本地消息表消息堆积怎么办? #
应对方案:
1. 监控告警:消息表待发送数量 > 阈值 → 告警
2. 增大扫描频率:fixedRate 从 5s 降到 1s
3. 批量处理:单次扫描从 100 提升到 500
4. 排查根因:通常是下游服务挂了 → 优先恢复下游
5. 紧急扩容:横向扩展消费者实例
面试回答要点:
"消息堆积通常是下游服务问题。短期加大扫描力度,长期排查下游。
同时做好监控,在堆积发生前就能发现趋势。"
Q4:为什么不直接用 Seata? #
核心反对理由:
1. 架构重量:TC Server + 注册中心 + 配置中心,运维成本高
2. 性能损耗:全局锁竞争,RT 增加 30%+
3. 数据库负担:每个参与方库都要 undo_log 表
4. 过度设计:小项目 90% 场景用不上强一致,最终一致就够了
5. 故障风险:TC 单点是最大风险点
面试回答要点:
"Seata 适合对强一致性有要求且流量大的场景。
但大部分业务接受最终一致,用本地消息表或 MQ 就够了,
架构更简单、性能更好、运维成本更低。"
Q5:分布式事务的三大理论知道吗? #
CAP 定理:
C(一致性)A(可用性)P(分区容错)三选二
分布式系统必须选 P,所以在 C 和 A 之间权衡
BASE 理论:
Basically Available(基本可用)
Soft State(软状态)
Eventually Consistent(最终一致)
是 CAP 中 AP 的实践指导
XA 规范:
两阶段提交协议(2PC)
Seata AT 模式的理论基础
强一致但性能差,实践中很少用
面试回答要点:
"我们用的是 BASE 理论指导下的最终一致方案。
不追求强一致(XA/2PC),而是通过本地消息表保证最终一致,
用异步补偿兜底,在一致性和性能之间取得平衡。"
最后总结:能用单库就不拆库,能用异步就不用同步,能用本地消息表就不用 Seata。轻量才是王道。