微服务分布式事务:轻量级替代方案(抛弃笨重 Seata) #

Seata 重在哪?要单独部署 TC 服务、建多张事务表、客户端代理数据源、配置繁琐、运维复杂、占用资源多,小项目完全没必要。本文给出 最轻量 → 够用 的 4 套方案,都是不用部署中间件、零额外服务、代码极简的方案,Java SpringCloud 直接能用。


📋 目录 #


一、为什么抛弃 Seata? #

Seata 的重量级代价 #

Seata AT 模式架构

全局锁

全局锁

undo_log

undo_log

TC 事务协调器
(独立部署)

RM 资源管理器1
(代理数据源)

RM 资源管理器2
(代理数据源)

undo_log 表

undo_log 表

维度 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 核心原理 #

库存服务定时扫描本地数据库订单服务客户端库存服务定时扫描本地数据库订单服务客户端异步轮询(5秒一次)下次轮询重试alt[调用成功][调用失败]创建订单BEGIN 事务INSERT 订单记录INSERT 本地消息记录事务提交返回订单创建成功(响应快)SELECT 待发送消息HTTP 调用扣减库存成功UPDATE 状态=已完成失败UPDATE retry_count+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乐观锁防重复

多实例部署用分布式锁

可靠性

指数退避重试

最大重试次数兜底

死信队列告警

性能

索引优化查询

批量扫描处理

控制单次扫描量

运维

管理后台查询消息状态

失败消息手动重发

监控积压告警

幂等

下游Redis去重

数据库唯一约束兜底

增强点 实现方式 必要性
CAS 防并发 UPDATE ... WHERE status=0 ⭐⭐⭐ 必须实现
指数退避 2^n 分钟递增 ⭐⭐⭐ 必须实现
死信告警 超过最大重试 → 状态=2 + 告警 ⭐⭐⭐ 必须实现
管理后台 查询/手动重发消息 ⭐⭐ 强烈建议
批量扫描 一次扫描 100~500 条 ⭐⭐ 强烈建议
分布式锁 多实例用 Redis/Zookeeper 锁 ⭐ 视部署规模

2.5 适用场景 #

  • ✅ 电商下单(订单 → 扣库存、积分、优惠券)
  • ✅ 支付流水(支付成功 → 通知业务、记账)
  • ✅ 跨服务数据同步(主库 → 搜索引擎、缓存)
  • ✅ 中小企业90% 的跨服务调用场景

三、方案二:可靠 MQ 最终一致性 #

定位:已有 MQ 环境(RabbitMQ / RocketMQ)时的轻量选择

3.1 核心原理 #

消费者数据库消费者MQ Broker生产者消费者数据库消费者MQ Broker生产者1. 发送成功 → 提交本地事务2. 发送失败 → 不提交事务达到最大重试 → 死信队列alt[成功][失败]发送半消息(或普通消息+本地表)确认接收推送消息执行业务(幂等)ACKNACK(重入队列)

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 核心原理 #

用户下单

支付成功

超时/用户取消

库存扣减成功

库存扣减失败

全部成功 → 发货

某步失败

补偿完成(退回已扣部分)

全部回滚

确认收货

CREATED

PAID

CANCELLED

检查所有子步骤

INVENTORY_FAIL

SHIPPED

COMPENSATING

REFUNDED

COMPLETED

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 核心思想 #

优雅思路(从源头消灭)

不需要

必须拆

必须跨库

微服务拆库

真的需要拆吗?

单库多 Service
(伪分布式)

核心写单库
非核心异步补偿

接受最终一致
异步处理

传统思路(制造问题再解决)

微服务拆库

产生跨库事务

引入 Seata/MQ 解决

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 读写分离 #

Binlog

同步

延迟容忍

强一致写入

最终一致查询

写库(MySQL)

Canal/Debezium

读库(ES/Redis)

报表库(ClickHouse)

客户端

核心思路:写操作保证强一致,读操作接受最终一致。用 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 快速决策树 #

需要跨服务
保证数据一致?

数据量 > 5000万?
QPS > 5000?

✅ 方案四路径一
单库多 Service

有 MQ 环境?

是流程状态类业务?

团队 > 50人?
需要 TC 协调?

✅ 方案三
状态机补偿

✅ 方案二
本地消息表 + MQ

✅ 方案一
本地消息表(纯推荐)

⚠️ 考虑 Seata
(小概率才需要)

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。轻量才是王道。