分布式事务太难?别慌,新手也能搞懂的最佳实践

林间写码人
2025-12-22 16:06
阅读 498

大家好!我是一个从培训班出来的前端开发,虽然现在主要写 JavaScript 和 Vue,但在找工作那会儿,为了丰富简历、多拿几个 offer,我也硬着头皮啃过 Java 后端的内容。当时看到“分布式事务”这种词,简直像看天书——什么两阶段提交、TCC、Saga……完全懵圈。

更离谱的是,有次面试官问我:“你们项目里怎么处理跨服务的数据一致性?”我支支吾吾说用 try-catch,结果被当场笑出声。那一刻我发誓:一定要把这玩意儿搞明白!

今天,我就用最接地气的方式,带零基础的你一步步搞懂分布式事务。哪怕你连 Java 都没写过几行,只要跟着做,也能理解核心思想,甚至能写进简历里当项目亮点!(对了,文末还会聊聊怎么把它和爬虫结合——别急,先打基础!)


一、分布式事务到底是啥?为什么需要它?

想象一下你用微信转账给朋友:

  1. 你的账户要扣 100 元
  2. 你朋友的账户要加 100 元

这两个操作必须同时成功,或者同时失败。如果只扣了你的钱,但没加到朋友账上,那可就亏大了!

在单体应用里(比如一个 Spring Boot 项目),这事很简单——用数据库事务就行:

@Transactional
public void transfer(String from, String to, double amount) {
    accountDao.decrease(from, amount); // 扣钱
    accountDao.increase(to, amount);   // 加钱
}

但如果你的系统是微服务架构呢?比如“用户服务”管扣钱,“钱包服务”管加钱。两个服务各自有自己的数据库,这时候上面的 @Transactional 就不管用了!

分布式事务,就是解决“跨多个数据库/服务”的数据一致性问题。


二、环境准备:手把手搭建开发环境

别怕!我们用最简单的工具:

  • JDK 8 或 11(去 Oracle 官网下载)
  • Maven(项目依赖管理)
  • IntelliJ IDEA(免费社区版就行)
  • MySQL(装两个实例模拟两个服务)

步骤 1:创建两个 Spring Boot 项目

start.spring.io 生成两个项目:

  • user-service(用户服务)
  • wallet-service(钱包服务)

都勾选:

  • Spring Web
  • Spring Data JPA
  • MySQL Driver

步骤 2:配置两个数据库

分别启动两个 MySQL 实例(或用不同库名):

服务 数据库名 表结构
user-service user_db account(id, name, balance)
wallet-service wallet_db account(id, user_id, balance)

建表 SQL 示例:

-- user_db
CREATE TABLE account (
  id BIGINT PRIMARY KEY,
  name VARCHAR(50),
  balance DECIMAL(10,2)
);

-- wallet_db
CREATE TABLE account (
  id BIGINT PRIMARY KEY,
  user_id BIGINT,
  balance DECIMAL(10,2)
);

三、核心概念:四种主流方案通俗讲

方案 1:两阶段提交(2PC)——像班长收作业

原理:分“投票”和“执行”两步。

  1. 准备阶段:协调者问所有参与者:“你们能不能提交?”
    → 每个服务锁住资源,回复“能”或“不能”
  2. 提交阶段:如果都回“能”,协调者说“提交!”;否则说“回滚!”

✅ 优点:强一致性
❌ 缺点:性能差、容易卡死(比如某个服务挂了)

我当初学的时候以为这是银弹,结果发现线上几乎没人用——太重了!


方案 2:TCC(Try-Confirm-Cancel)——预订酒店模式

原理:把一个操作拆成三步:

  • Try:预占资源(比如冻结 100 元)
  • Confirm:真正扣款(确认冻结的钱)
  • Cancel:释放资源(取消冻结)
// 用户服务接口
public interface AccountService {
    boolean tryDecrease(Long userId, double amount);   // 冻结
    boolean confirmDecrease(Long userId, double amount); // 确认
    boolean cancelDecrease(Long userId, double amount);  // 取消
}

✅ 优点:灵活、性能好
❌ 缺点:代码复杂,每个业务都要写三套逻辑


方案 3:消息队列 + 本地消息表 —— 发快递模式

原理:用“可靠消息”保证最终一致。

  1. 用户服务扣钱时,同时往本地消息表插一条“通知钱包服务加钱”的记录
  2. 后台任务不断扫描这个表,把消息发到 RabbitMQ/Kafka
  3. 钱包服务收到消息后加钱,并回执

关键点:扣钱和写消息表必须在一个本地事务里

@Transactional
public void transferOut(Long userId, double amount) {
    // 1. 扣钱
    accountDao.decrease(userId, amount);
    // 2. 写消息表(同一事务!)
    messageDao.insert("WALLET_INCREASE", userId, amount);
}

✅ 优点:简单、可靠、适合大多数场景
✅ 这是我最推荐新手用的方案!


方案 4:Saga 模式 —— 旅游行程取消

原理:把大事务拆成多个小事务,每个小事务都有对应的补偿操作

比如转账流程:

  1. 扣用户钱 → 补偿:加回去
  2. 加钱包钱 → 补偿:减回来

如果第 2 步失败,就执行第 1 步的补偿。

✅ 优点:无锁、高并发
❌ 缺点:补偿逻辑复杂,可能无法完全回退(比如已发邮件)


四、实战:用“本地消息表”实现转账

我们来做一个简化版转账功能!

步骤 1:在 user-service 创建消息表

CREATE TABLE outbox (
  id BIGINT AUTO_INCREMENT,
  event_type VARCHAR(50),
  payload JSON,
  status ENUM('PENDING', 'SENT') DEFAULT 'PENDING',
  PRIMARY KEY (id)
);

步骤 2:写转账逻辑(user-service)

@Service
public class TransferService {

    @Autowired
    private AccountRepository accountRepo;
    
    @Autowired
    private OutboxRepository outboxRepo;

    @Transactional
    public void transfer(Long fromUserId, Long toUserId, double amount) {
        // 1. 扣款
        Account from = accountRepo.findById(fromUserId).orElseThrow();
        if (from.getBalance() < amount) throw new RuntimeException("余额不足");
        from.setBalance(from.getBalance() - amount);
        accountRepo.save(from);

        // 2. 写消息(同一事务!)
        OutboxMessage msg = new OutboxMessage();
        msg.setEventType("WALLET_DEPOSIT");
        msg.setPayload(Map.of("userId", toUserId, "amount", amount));
        outboxRepo.save(msg);
    }
}

步骤 3:后台任务发送消息

@Component
public class MessagePublisher {
    
    @Scheduled(fixedDelay = 5000) // 每5秒扫一次
    public void publishPendingMessages() {
        List<OutboxMessage> pending = outboxRepo.findByStatus("PENDING");
        for (OutboxMessage msg : pending) {
            // 发到 RabbitMQ(伪代码)
            rabbitTemplate.convertAndSend("transfer-exchange", msg.getPayload());
            msg.setStatus("SENT");
            outboxRepo.save(msg);
        }
    }
}

步骤 4:wallet-service 消费消息

@RabbitListener(queues = "wallet-queue")
public void handleDeposit(Map<String, Object> payload) {
    Long userId = ((Number) payload.get("userId")).longValue();
    Double amount = (Double) payload.get("amount");
    
    Account account = accountRepo.findByUserId(userId);
    account.setBalance(account.getBalance() + amount);
    accountRepo.save(account);
}

搞定!这样即使 wallet-service 暂时挂了,消息也会重试,最终数据一致。


五、新手常见问题解答

Q1:这些方案能写进简历吗?

当然能! 尤其是“本地消息表”方案,很多中小公司都在用。你可以写:

“基于 Spring Boot + RabbitMQ 实现分布式事务,采用本地消息表保证跨服务数据最终一致性”

Q2:Java 不熟怎么办?

先掌握基本语法和 Spring Boot CRUD。分布式事务的核心是思想,不是语言。你甚至可以用 Python 写类似逻辑!

Q3:和爬虫有关系吗?

有!假设你用爬虫抓取商品价格,然后更新到订单服务和库存服务——这也涉及分布式事务!你可以用同样方案保证价格、库存同步。

Q4:一定要用消息队列吗?

不一定。也可以用定时任务轮询数据库(如上面的 @Scheduled),但消息队列更高效、解耦更好。


六、学习建议:下一步怎么走?

  1. 动手敲代码:照着上面的例子,自己搭两个服务跑起来
  2. 深入消息队列:学 RabbitMQ 或 Kafka 的基本用法
  3. 了解 Seata:阿里开源的分布式事务框架,支持 AT/TCC/Saga 模式
  4. 别死磕理论:先掌握“本地消息表”,够你应付 80% 场景

记住:我当初也是从“简历上写精通分布式”到“实际连事务隔离级别都说不清”。慢慢来,代码写多了,自然就通了。


最后的话

分布式事务听起来高大上,其实本质就是想办法让多个操作“要么全做,要么全不做”。作为培训班出来的开发者,我深知新手最怕“术语轰炸”。所以这篇文章刻意避开了 XA、CAP、BASE 这些词,只讲你能用得上的东西。

下次面试官再问分布式事务,你就可以自信地说:“我们项目用本地消息表+RabbitMQ保证最终一致性,还结合了爬虫做数据同步……”

加油!你的简历,值得更好的 offer!

评论 0

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝