分布式事务太难?别慌,新手也能搞懂的最佳实践
大家好!我是一个从培训班出来的前端开发,虽然现在主要写 JavaScript 和 Vue,但在找工作那会儿,为了丰富简历、多拿几个 offer,我也硬着头皮啃过 Java 后端的内容。当时看到“分布式事务”这种词,简直像看天书——什么两阶段提交、TCC、Saga……完全懵圈。
更离谱的是,有次面试官问我:“你们项目里怎么处理跨服务的数据一致性?”我支支吾吾说用 try-catch,结果被当场笑出声。那一刻我发誓:一定要把这玩意儿搞明白!
今天,我就用最接地气的方式,带零基础的你一步步搞懂分布式事务。哪怕你连 Java 都没写过几行,只要跟着做,也能理解核心思想,甚至能写进简历里当项目亮点!(对了,文末还会聊聊怎么把它和爬虫结合——别急,先打基础!)
一、分布式事务到底是啥?为什么需要它?
想象一下你用微信转账给朋友:
- 你的账户要扣 100 元
- 你朋友的账户要加 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)——像班长收作业
原理:分“投票”和“执行”两步。
- 准备阶段:协调者问所有参与者:“你们能不能提交?”
→ 每个服务锁住资源,回复“能”或“不能” - 提交阶段:如果都回“能”,协调者说“提交!”;否则说“回滚!”
✅ 优点:强一致性
❌ 缺点:性能差、容易卡死(比如某个服务挂了)
我当初学的时候以为这是银弹,结果发现线上几乎没人用——太重了!
方案 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:消息队列 + 本地消息表 —— 发快递模式
原理:用“可靠消息”保证最终一致。
- 用户服务扣钱时,同时往本地消息表插一条“通知钱包服务加钱”的记录
- 后台任务不断扫描这个表,把消息发到 RabbitMQ/Kafka
- 钱包服务收到消息后加钱,并回执
关键点:扣钱和写消息表必须在一个本地事务里!
@Transactional
public void transferOut(Long userId, double amount) {
// 1. 扣钱
accountDao.decrease(userId, amount);
// 2. 写消息表(同一事务!)
messageDao.insert("WALLET_INCREASE", userId, amount);
}
✅ 优点:简单、可靠、适合大多数场景
✅ 这是我最推荐新手用的方案!
方案 4:Saga 模式 —— 旅游行程取消
原理:把大事务拆成多个小事务,每个小事务都有对应的补偿操作。
比如转账流程:
- 扣用户钱 → 补偿:加回去
- 加钱包钱 → 补偿:减回来
如果第 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),但消息队列更高效、解耦更好。
六、学习建议:下一步怎么走?
- 动手敲代码:照着上面的例子,自己搭两个服务跑起来
- 深入消息队列:学 RabbitMQ 或 Kafka 的基本用法
- 了解 Seata:阿里开源的分布式事务框架,支持 AT/TCC/Saga 模式
- 别死磕理论:先掌握“本地消息表”,够你应付 80% 场景
记住:我当初也是从“简历上写精通分布式”到“实际连事务隔离级别都说不清”。慢慢来,代码写多了,自然就通了。
最后的话
分布式事务听起来高大上,其实本质就是想办法让多个操作“要么全做,要么全不做”。作为培训班出来的开发者,我深知新手最怕“术语轰炸”。所以这篇文章刻意避开了 XA、CAP、BASE 这些词,只讲你能用得上的东西。
下次面试官再问分布式事务,你就可以自信地说:“我们项目用本地消息表+RabbitMQ保证最终一致性,还结合了爬虫做数据同步……”
加油!你的简历,值得更好的 offer!

评论 0