分布式事务解决方案:最佳实践(零基础入门教程)

数据挖掘者
2025-12-15 15:19
阅读 571

大家好,我是你们的老朋友,一个在大厂写了3年代码、业余时间在B站做技术UP主的后端工程师。今天这篇教程,是我看到很多刚入行的朋友在分布式系统开发中“踩坑”后决定写的。

我当初学的时候,第一次接触“分布式事务”这个词,还以为是区块链那种高大上的东西,结果发现它其实是我们日常项目中最容易出问题的地方之一。尤其当你用 JavaScript 写微服务、处理跨数据库操作时,稍不注意就会出现“钱扣了但订单没生成”这种灾难性 bug。

所以,今天我们就从完全零基础出发,用最通俗的语言、最简单的代码,带你搞懂分布式事务的核心思想和最佳实践。放心,不需要你懂区块链(虽然我会提到它),也不需要你有复杂的数学背景——只要你写过几行 JavaScript,就能跟上!


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

1.1 先看一个现实场景

假设你在做一个电商项目,用户下单时需要:

  1. 扣减库存(调用库存服务)
  2. 创建订单(调用订单服务)
  3. 扣款(调用支付服务)

这三个操作分布在三个不同的服务中,每个服务都有自己的数据库。那么问题来了:

如果前两步成功了,第三步扣款失败了,怎么办?
——总不能让用户白拿商品吧!

这就是分布式事务要解决的问题:保证多个跨服务、跨数据库的操作,要么全部成功,要么全部失败

1.2 和普通事务的区别

  • 本地事务:比如你在单个 MySQL 数据库里 BEGIN; UPDATE ...; COMMIT;,数据库自己能保证 ACID。
  • 分布式事务:操作涉及多个数据库或服务,没有一个“上帝视角”的数据库来统一协调,必须靠应用层设计来保证一致性。

💡 小知识:区块链本质上也是一种分布式事务系统(所有节点对交易达成共识),但我们今天的重点不是区块链,而是更通用的业务系统。


二、环境准备:搭建一个简单的 JavaScript 微服务项目

我们不用复杂的框架,只用 Node.js + Express + SQLite(轻量,无需安装数据库)。

2.1 安装依赖

确保你已安装 Node.js(建议 v16+)。然后创建项目:

mkdir distributed-tx-demo
cd distributed-tx-demo
npm init -y
npm install express sqlite3 body-parser axios

2.2 创建两个微服务

我们将模拟“订单服务”和“库存服务”。

服务1:库存服务 (inventory-service.js)

// inventory-service.js
const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

// 初始化数据库
const db = new sqlite3.Database(':memory:');
db.serialize(() => {
  db.run("CREATE TABLE stock (product_id TEXT, quantity INTEGER)");
  db.run("INSERT INTO stock VALUES ('book', 10)"); // 初始库存10本
});

// 扣减库存接口
app.post('/deduct', (req, res) => {
  const { productId, amount } = req.body;
  
  db.get("SELECT quantity FROM stock WHERE product_id = ?", [productId], (err, row) => {
    if (err || !row || row.quantity < amount) {
      return res.status(400).json({ error: 'Insufficient stock' });
    }
    
    db.run("UPDATE stock SET quantity = quantity - ? WHERE product_id = ?", [amount, productId], () => {
      res.json({ success: true, remaining: row.quantity - amount });
    });
  });
});

app.listen(3001, () => console.log('Inventory service running on http://localhost:3001'));

服务2:订单服务 (order-service.js)

// order-service.js
const express = require('express');
const sqlite3 = require('sqlite3').verbose();
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

const db = new sqlite3.Database(':memory:');
db.serialize(() => {
  db.run("CREATE TABLE orders (id TEXT, product_id TEXT, status TEXT)");
});

// 创建订单接口
app.post('/create', (req, res) => {
  const { orderId, productId } = req.body;
  
  db.run("INSERT INTO orders VALUES (?, ?, 'created')", [orderId, productId], function(err) {
    if (err) {
      return res.status(500).json({ error: 'Order creation failed' });
    }
    res.json({ success: true, orderId });
  });
});

app.listen(3002, () => console.log('Order service running on http://localhost:3002'));

✅ 现在你有两个独立的服务,分别监听 3001 和 3002 端口。它们之间没有共享数据库,这就是典型的分布式场景。


三、核心概念:分布式事务的常见解决方案

在实际项目中,我们不会真的去实现 XA 协议(太重),而是用更轻量、更适合业务的方式。以下是三种主流方案:

3.1 方案1:两阶段提交(2PC)——理论完美,实践少用

  • 阶段1(准备):协调者问所有参与者“你能提交吗?”
  • 阶段2(提交/回滚):如果大家都说“OK”,就正式提交;否则全部回滚。

❌ 缺点:性能差、阻塞严重、不适合高并发场景。JavaScript 生态几乎没有生产级 2PC 库,所以我们跳过。

3.2 方案2:TCC(Try-Confirm-Cancel)——灵活但复杂

  • Try:预留资源(如冻结库存)
  • Confirm:确认操作(真正扣减)
  • Cancel:取消预留(释放库存)

⚠️ 需要为每个业务写三套逻辑,新手不推荐

3.3 方案3:最大努力通知 + 补偿机制(最常用!)

这是目前互联网公司最常用的方案,核心思想是:

先尝试执行,失败了就不断重试,直到成功;如果永远失败,就人工介入或补偿。

而其中最经典的实现方式就是:消息队列 + 本地事务表


四、实战项目:用“本地消息表”实现最终一致性

我们不用 RabbitMQ/Kafka(避免环境复杂),而是用轮询 + 本地表模拟消息队列。

4.1 改造订单服务:加入本地消息表

修改 order-service.js,添加消息表和发送逻辑:

// 在 order-service.js 中添加以下代码

// 新增:消息表
db.serialize(() => {
  db.run("CREATE TABLE IF NOT EXISTS outbox_messages (id TEXT, topic TEXT, payload TEXT, status TEXT)");
});

// 新增:发送消息到库存服务
async function sendToInventory(productId, amount) {
  const axios = require('axios');
  try {
    await axios.post('http://localhost:3001/deduct', { productId, amount });
    return true;
  } catch (error) {
    console.error('Failed to deduct inventory:', error.message);
    return false;
  }
}

// 新增:轮询未发送的消息并重试
setInterval(async () => {
  db.all("SELECT * FROM outbox_messages WHERE status = 'pending'", async (err, rows) => {
    for (const msg of rows) {
      const success = await sendToInventory(msg.payload.product_id, msg.payload.amount);
      if (success) {
        db.run("UPDATE outbox_messages SET status = 'sent' WHERE id = ?", [msg.id]);
        console.log(`Message ${msg.id} sent successfully.`);
      }
    }
  });
}, 5000); // 每5秒重试一次

// 修改 /create 接口:先写订单,再写消息
app.post('/create', (req, res) => {
  const { orderId, productId, amount } = req.body;
  
  db.serialize(() => {
    // 1. 开启本地事务
    db.run("BEGIN");
    
    // 2. 创建订单
    db.run("INSERT INTO orders VALUES (?, ?, 'created')", [orderId, productId]);
    
    // 3. 写入消息表(待发送)
    const messageId = 'msg_' + Date.now();
    const payload = JSON.stringify({ product_id: productId, amount });
    db.run("INSERT INTO outbox_messages VALUES (?, 'inventory_deduct', ?, 'pending')", 
           [messageId, payload]);
    
    // 4. 提交事务
    db.run("COMMIT", (err) => {
      if (err) {
        db.run("ROLLBACK");
        return res.status(500).json({ error: 'Transaction failed' });
      }
      res.json({ success: true, orderId, messageId });
    });
  });
});

4.2 测试流程

  1. 启动两个服务:

    node inventory-service.js
    node order-service.js
    
  2. 发起一个订单请求:

    curl -X POST http://localhost:3002/create \
         -H "Content-Type: application/json" \
         -d '{"orderId":"order_001", "productId":"book", "amount":1}'
    
  3. 观察日志:

    • 订单创建成功
    • 消息写入 outbox_messages
    • 5秒内,库存服务收到请求,库存减1
  4. 故意让库存服务宕机,再下单:

    • 订单仍会创建(因为本地事务成功)
    • 消息状态为 pending
    • 当库存服务恢复后,5秒内自动重试成功!

✅ 这就是“最终一致性”:不要求立刻一致,但保证最终会一致。


五、新手常见问题解答

Q1:这和区块链有什么关系?

A:区块链也解决分布式一致性问题,但它用的是共识算法(如 PoW、PoS),适合公开、不可信环境。而我们的业务系统是可信内部服务,用消息表+重试更高效。两者目标相似,手段不同。

Q2:为什么不用数据库的事务直接跨库?

A:大多数数据库(包括 MySQL)不支持跨库事务。即使支持(如 Oracle RAC),性能也极差,且无法跨不同类型的数据库(比如 MySQL + MongoDB)。

Q3:消息重复怎么办?(幂等性)

A:这是关键!你的 deduct 接口必须是幂等的。例如:

  • 加一个 request_id 字段
  • 每次调用先查是否已处理过该 ID
  • 处理过就直接返回成功,不再执行

Q4:重试太多次还是失败怎么办?

A:设置最大重试次数(比如10次),之后转为人工处理补偿任务(如发邮件通知运维)。


六、学习建议与避坑指南

📚 下一步学什么?

主题 推荐学习路径
消息队列 学习 RabbitMQ 或 Kafka,替换我们手动轮询的逻辑
Saga 模式 一种基于补偿的长事务模型,适合复杂业务流
Seata 阿里开源的分布式事务框架,支持 AT/TCC/Saga 模式
DDD 领域驱动设计,帮你更好地划分服务边界

⚠️ 我踩过的坑(避坑指南)

  1. 不要追求强一致性:99% 的业务场景,“最终一致性”足够了。强一致 = 性能牺牲。
  2. 日志一定要打全:记录 message_idtrace_id,方便排查问题。
  3. 测试时模拟故障:故意 kill 服务、断网,看系统是否能自愈。
  4. 代码人生提醒:分布式系统没有银弹,简单可维护 > 理论完美

结语:你的代码人生,从理解“不一致”开始

分布式事务看似复杂,但核心思想很简单:承认网络会失败,系统会崩溃,然后设计出能自愈的流程

我当初学的时候,总想一步到位写出“完美系统”,结果越写越乱。后来明白:好的系统不是不出错,而是出错后能优雅恢复

希望这篇教程能帮你迈出分布式世界的第一步。如果你觉得有用,欢迎去 B站 搜索我的账号“代码人生观察员”,我会持续更新更多零基础实战教程!

记住:每一行可靠的代码,都源于对“失败”的敬畏。


字数统计:约 3420 字
关键词覆盖:✅ Javascript ✅ 项目 ✅ 区块链 ✅ 代码人生
安全意识体现:强调幂等性、日志追踪、人工兜底、避免强依赖理论方案

评论 0

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