以每小时 20 美元的价格聘请具备人工智能专业知识的 Laravel 开发人员。48 小时内即可开始工作。

在 Laravel 中构建事务安全的多文档操作

最后更新于 经过

在 Laravel 镜像中构建事务安全的多文档操作

学习如何在 Laravel 中使用 MongoDB 的多文档 ACID 事务,以确保在原子操作不足以保证集合间数据一致性时,能够处理回滚和故障的实际示例。

您将学到什么

  • 当原子操作不足以保证数据一致性时
  • 如何使用 Laravel 的 DB::transaction() 使用 MongoDB
  • 了解事务回滚、提交和失败场景
  • 保持交易快速可靠的最佳实践

介绍

在之前的文章中,我们使用 MongoDB 的原子操作符解决了竞态条件,例如 $inc 我们的钱包扣款和库存更新不再受竞态条件影响。这算胜利吧?

不完全是这样。在测试故障场景时,我发现了一个新问题:如果我的应用程序崩溃了会发生什么? 从钱包扣款,但 创建订单?顾客损失了 80 美元,却什么也没买到。

原子操作保证了单文档一致性,但我们的结账流程会涉及…… 收藏品: wallets , products , 和 orders 我们需要一种方法来确保以下两种情况之一发生: 这三个 更新同时成功,或者 没有任何 这些事确实发生了。

这时,MongoDB 的多文档 ACID 事务就派上了用场——而 Laravel 让它们的使用变得异常简单。

部分失效问题

让我来具体演示一下可能出现的问题。以下是上一篇文章中的结账流程:

民众 功能 查看 要求 $请求)
{
// ... 验证 ...
$产品 = 产品 :: 查找或失败 ($productId)
金额 = $产品 -> 价格 * 数量;
// 步骤 1:原子性地从钱包扣款
$walletResult = 钱包 :: 生的 功能 (收藏) 使用 ($userId, $amount) {
返回 $集合 -> updateOne
[ '用户身份' => $userId, '平衡' => [ '$gte' => $amount]],
[ '$inc' => [ '平衡' => - $amount]]
(英文):
});
如果 ($walletResult -> 获取修改次数 () === 0 ){
返回 回复 () -> json ([ '错误' => 资金不足 ], 400 (英文):
}
// 💥 如果应用在这里崩溃了怎么办?💥
// 步骤 2:原子性地减少库存
库存结果 = 产品 :: 生的 功能 (收藏) 使用 ($productId, $quantity) {
返回 $集合 -> updateOne
[ '_ID' => 新的 对象ID ($productId) '库存' => [ '$gte' => $quantity]],
[ '$inc' => [ '库存' => - $quantity]]
(英文):
});
// 步骤 3:创建订单
$订单 = 命令 :: 创造 ([
'用户身份' => $userId,
'product_id' => $productId,
'数量' => 数量,
'数量' => 金额
]);
返回 回复 () -> json ([ '命令' => $order]);
}

关于按 ID 搜索的说明:Product::findOrFail($productId) Eloquent 方法会自动处理字符串到 ObjectId 的转换。在原始查询中,我们必须显式地创建一个新的 ObjectId($productId),因为我们直接操作的是 MongoDB 驱动程序。Laravel MongoDB 包为其更高级别的 API 提供了这种无缝转换。

让我们通过测试来模拟一次崩溃场景:

民众 功能 test_partial_failure_leaves_inconsistent_state ()
{
$产品 = 产品 :: 创造 ([
'姓名' => '笔记本电脑' ,
'价格' => 1000.00 ,
'库存' => 5
]);
钱包 = 钱包 :: 创造 ([
'用户身份' => 'user-1' ,
'平衡' => 1500.00
]);
// 修改控制器,使其在钱包扣款后崩溃
尝试 {
// 借记钱包
钱包 :: 生的 功能 ($collection) {
返回 $集合 -> updateOne
[ '用户身份' => 'user-1' , '平衡' => [ '$gte' => 1000 ]],
[ '$inc' => [ '平衡' => - 1000 ]]
(英文):
});
// 模拟崩溃
新的 \例外 “模拟服务器崩溃” (英文):
// 这些永远不会执行
产品 :: 生的 功能 ($collection) { ... });
命令 :: 创造 ([ ... ]);
} 抓住 \例外 $e) {
// 应用崩溃了
}
// 检查数据库状态
钱包 -> 刷新 ();
$产品 -> 刷新 ();
$订单 = 命令 :: 在哪里 '用户身份' , 'user-1' -> 第一的 ();
倾倒 '=== 崩溃后 ===' (英文):
倾倒 钱包余额: 钱包 -> 平衡); // 已扣款 500 美元!
倾倒 '产品库存:' $产品 -> 库存); // 5(未更改!)
倾倒 订单存在: 订单 '是的' : '不' )); // 不!
$this -> 断言等于 1500 ,$wallet -> 平衡,
“如果订单尚未创建,则不应从钱包扣款” (英文):
}

结果:

=== 事故后 ===
钱包余额:500
产品库存:5
订单存在:否

客户损失了1000美元,但没有下单。库存没有被动过。我们的数据不一致。

每个操作都是原子性的(没有竞态条件),但它们一起执行时就不是原子性的——不能保证它们会同时成功或同时失败。

理解数据库不变量

一个 不变的 这是一个必须始终适用于所有数据的条件。在我们的电子商务系统中,我们有几个关键的不变条件:

不变式 1:货币守恒

钱包所有扣款总额 = 所有订单金额总额

如果我们从钱包中扣除 1000 美元,但只创建了价值 800 美元的订单,那么系统中的资金就消失了。

不变式 2:库存准确性

实物库存 = 数据库库存 - 订单数量总和

如果我们不减少库存就下订单,就会超卖;如果我们不下订单就减少库存,就会失去对库存的掌控。

不变式 3:钱包顺序一致性

如果钱包被扣款,则必须存在相应的订单。
如果订单存在,则必须从钱包中扣款。

每笔借记都需要收据,每张收据也需要借记。

当操作跨越多个文档或集合时,仅靠原子操作无法维护这些不变性。我们需要事务。

MongoDB 多文档事务简介

MongoDB 的多文档事务与 MySQL 或 PostgreSQL 等传统关系数据库中的事务工作方式完全相同。它们提供 ACID保证 :

  • 原子性 要么所有操作同时成功,要么全部失败。
  • 一致性 数据库从一个有效状态转移到另一个有效状态。
  • 隔离 并发事务之间互不干扰
  • 耐久性 已提交的更改即使在崩溃后仍然存在

作为 MongoDB 文档指出 :

“多文档事务使 MongoDB 成为唯一一个将传统关系数据库的 ACID 保证与文档模型的速度、灵活性和强大功能相结合的数据库,并采用智能分布式系统设计,可横向扩展并将数据放置在所需位置。通过快照隔离,事务提供一致的数据视图,并强制执行‘要么全有要么全无’的原则来维护数据完整性。”

通过 快照隔离 事务提供一致的数据视图,并强制执行要么全部执行要么全部不执行的操作,以维护数据完整性。

Laravel 中的事务是如何运作的?

拉拉维尔的 DB::transaction() 该方法通过 Laravel MongoDB 包与 MongoDB 无缝协作。以下是基本语法:

使用 照明\支持\立面\数据库 ;
数据库 :: 联系 'mongodb' -> 交易 功能 (){
// 此处的所有操作都是交易的一部分
// 如果任何操作失败,所有操作都将回滚
}, 5 (英文): // 死锁时最多重试 5 次

重要提示: Laravel 内置的重试机制仅处理死锁异常。正如您将在本文后面的“事务失败(这没关系)”部分中看到的那样,MongoDB 事务失败的原因有很多,除了死锁之外,还包括网络中断、副本集选举和瞬态事务错误。要全面处理所有 MongoDB 特有的事务错误,您需要使用带有指数退避的手动重试逻辑,我们将在后面介绍。

Laravel事务API的优点在于,无论你使用MongoDB、MySQL还是PostgreSQL,它的工作方式都完全相同。语法也一样熟悉。

实施我们的第一笔交易

让我们重构结账流程,使其使用事务处理:

命名空间 App\Http\Controllers ;
使用 应用\模型\钱包 ;
使用 应用程序\模型\产品 ;
使用 应用程序\模型\订单 ;
使用 照亮\Http\请求 ;
使用 照明\支持\立面\数据库 ;
使用 MongoDB\BSON\ObjectId ;
班级 结账控制器 延伸 控制器
{
民众 功能 查看 要求 $请求)
{
$已验证 = $请求 -> 证实 ([
'用户身份' => '必填|字符串' ,
'product_id' => '必填|字符串' ,
'数量' => '必填|整数|最小值:1' ,
]);
用户 ID = $已验证[ '用户身份' ];
$productId = $已验证[ 'product_id' ];
数量 = $已验证[ '数量' ];
尝试 {
$订单 = 数据库 :: 联系 'mongodb' -> 交易 功能 () 使用 ($userId, $productId, $quantity) {
// 获取产品价格
$产品 = 产品 :: 查找或失败 ($productId)
金额 = $产品 -> 价格 * 数量;
// 步骤 1:原子性地从钱包扣款
$walletResult = 钱包 :: 生的 功能 (收藏) 使用 ($userId, $amount) {
返回 $集合 -> updateOne
[
'用户身份' => $userId,
'平衡' => [ '$gte' => 金额]
],
[
'$inc' => [ '平衡' => - 金额]
]
(英文):
});
如果 ($walletResult -> 获取修改次数 () === 0 ){
新的 \例外 资金不足 (英文):
}
// 步骤 2:原子性地减少库存
库存结果 = 产品 :: 生的 功能 (收藏) 使用 ($productId, $quantity) {
返回 $集合 -> updateOne
[
'_ID' => 新的 对象ID ($productId)
'库存' => [ '$gte' => 数量]
],
[
'$inc' => [ '库存' => - 数量]
]
(英文):
});
如果 库存结果 -> 获取修改次数 () === 0 ){
新的 \例外 库存不足 (英文):
}
// 步骤 3:创建订单
$订单 = 命令 :: 创造 ([
'用户身份' => $userId,
'product_id' => $productId,
'数量' => 数量,
'数量' => 金额,
'地位' => '完全的' ,
'created_at' => 现在 ()
]);
返回 订单;
});
返回 回复 () -> json ([
'成功' => 真的 ,
'命令' => $订单
]);
} 抓住 \例外 $e) {
返回 回复 () -> json ([
'错误' => $e -> 获取消息 ()
], 400 (英文):
}
}
}

主要变化:

  1. 将所有操作包装在 DB::connection('mongodb')->transaction()
  2. 从收货处退回订单 这样我们就可以在外面访问它了。
  3. 业务逻辑失败时抛出异常 (资金不足,缺货)
  4. 捕获异常 返回正确的 HTTP 响应

现在这三个操作是原子性的。如果其中任何一个步骤失败——资金不足、库存不足,甚至是服务器崩溃——MongoDB 都会自动回滚。 全部 变化。

测试事务回滚

让我们验证交易是否真的有效。创建全面的测试:

命名空间 测试\功能 ;
使用 测试\测试用例 ;
使用 应用\模型\钱包 ;
使用 应用程序\模型\产品 ;
使用 应用程序\模型\订单 ;
班级 交易测试 延伸 测试用例
{
受保护 功能 设置 () : 空白
{
父级:: 设置 ();
钱包 :: 截短 ();
产品 :: 截短 ();
命令 :: 截短 ();
}
民众 功能 test_successful_checkout_commits_all_changes ()
{
$产品 = 产品 :: 创造 ([
'姓名' => '笔记本电脑' ,
'价格' => 1000.00 ,
'库存' => 5
]);
钱包 = 钱包 :: 创造 ([
'用户身份' => 'user-1' ,
'平衡' => 1500.00
]);
$响应 = $this -> 帖子Json '/api/checkout' ,[
'用户身份' => 'user-1' ,
'product_id' => $产品 -> ID,
'数量' => 1
]);
$响应 -> 断言状态 200 (英文):
// 所有更改都应提交
钱包 -> 刷新 ();
$产品 -> 刷新 ();
$this -> 断言等于 500.00 ,$wallet -> 平衡, “钱包已扣款” (英文):
$this -> 断言等于 4 ,$产品 -> 库存, 库存减少 (英文):
$this -> 断言等于 1 , 命令 :: 数数 (), 订单已创建 (英文):
$订单 = 命令 :: 第一的 ();
$this -> 断言等于 'user-1' ,$order -> 用户身份);
$this -> 断言等于 1000.00 ,$order -> 数量);
}
民众 功能 test_insufficient_funds_rolls_back_entire_transaction ()
{
$产品 = 产品 :: 创造 ([
'姓名' => '笔记本电脑' ,
'价格' => 1000.00 ,
'库存' => 5
]);
钱包 = 钱包 :: 创造 ([
'用户身份' => 'user-1' ,
'平衡' => 500.00 // 还不够!
]);
$响应 = $this -> 帖子Json '/api/checkout' ,[
'用户身份' => 'user-1' ,
'product_id' => $产品 -> ID,
'数量' => 1
]);
$响应 -> 断言状态 400 (英文):
$响应 -> 断言 ([ '错误' => 资金不足 ]);
// 一切照旧
钱包 -> 刷新 ();
$产品 -> 刷新 ();
$this -> 断言等于 500.00 ,$wallet -> 平衡, 钱包余额未变 (英文):
$this -> 断言等于 5 ,$产品 -> 库存, 库存未变 (英文):
$this -> 断言等于 0 , 命令 :: 数数 (), “未创建订单” (英文):
}
民众 功能 test_out_of_stock_rolls_back_wallet_debit ()
{
$产品 = 产品 :: 创造 ([
'姓名' => '笔记本电脑' ,
'价格' => 1000.00 ,
'库存' => 0 // 缺货!
]);
钱包 = 钱包 :: 创造 ([
'用户身份' => 'user-1' ,
'平衡' => 1500.00
]);
$响应 = $this -> 帖子Json '/api/checkout' ,[
'用户身份' => 'user-1' ,
'product_id' => $产品 -> ID,
'数量' => 1
]);
$响应 -> 断言状态 400 (英文):
$响应 -> 断言 ([ '错误' => 库存不足 ]);
钱包不应被扣款
钱包 -> 刷新 ();
$this -> 断言等于 1500.00 ,$wallet -> 平衡,
“缺货时不应从钱包扣款” (英文):
$this -> 断言等于 0 , 命令 :: 数数 (), “未创建订单” (英文):
}
民众 功能 测试模拟崩溃回滚事务 ()
{
$产品 = 产品 :: 创造 ([
'姓名' => '笔记本电脑' ,
'价格' => 1000.00 ,
'库存' => 5
]);
钱包 = 钱包 :: 创造 ([
'用户身份' => 'user-1' ,
'平衡' => 1500.00
]);
// 通过在事务处理过程中抛出异常来模拟崩溃
尝试 {
数据库 :: 联系 'mongodb' -> 交易 功能 () 使用 ($钱包,$产品) {
// 借记钱包
钱包 :: 生的 功能 ($collection) {
返回 $集合 -> updateOne
[ '用户身份' => 'user-1' , '平衡' => [ '$gte' => 1000 ]],
[ '$inc' => [ '平衡' => - 1000 ]]
(英文):
});
// 模拟崩溃
新的 \例外 “模拟碰撞” (英文):
});
} 抓住 \例外 $e) {
// 交易已中止
}
// 检查所有操作是否已回滚
钱包 -> 刷新 ();
$产品 -> 刷新 ();
$this -> 断言等于 1500.00 ,$wallet -> 平衡,
“事务崩溃时应回滚” (英文):
$this -> 断言等于 5 ,$产品 -> 库存, “股票价格不变” (英文):
$this -> 断言等于 0 , 命令 :: 数数 (), “未创建订单” (英文):
}
}

运行以下测试:

php 工匠 测试 --filter=TransactionTest

所有测试均应通过,确认:

  • ✅ 成功结账后,所有更改都将提交。
  • ✅ 资金不足导致一切倒退
  • ✅ 缺货将回滚钱包借记卡
  • ✅ 崩溃时回滚部分更改

交易失败时(这没关系)

交易并不能消除故障——它们只是改变了故障。 如何 失败是有原因的。根据 MongoDB 文档,事务可能由于多种原因而失败:

  • 网络中断 应用程序和数据库之间的连接断开
  • 副本集选举 - MongoDB 集群在事务期间选举新的主节点
  • 记录冲突 另一笔交易修改了相同的文件。
  • 暂停 - 交易超时(默认值:60 秒)

当事务失败时,MongoDB 保证:

  1. 自动中止 交易已终止。
  2. 完全回滚 - 交易内的所有操作均被撤销
  3. 一致状态 数据库恢复到事务处理前的状态
  4. 返回错误 您的申请收到例外通知

让我们妥善处理这些情况:

民众 功能 查看 要求 $请求)
{
// ... 验证 ...
最大尝试次数 = 3 ;
$尝试 = 0 ;
尽管 尝试 < $maxAttempts) {
$尝试 ++ ;
尝试 {
$订单 = 数据库 :: 联系 'mongodb' -> 交易 功能 () 使用 ($userId, $productId, $quantity) {
// ... 事务逻辑 ...
});
返回 回复 () -> json ([
'成功' => 真的 ,
'命令' => $订单
]);
} 抓住 MongoDB 驱动程序异常运行时异常 $e) {
// 暂时性事务错误 - 重试后可能成功
如果 str_contains ($e -> 获取消息 (), '瞬态交易错误' )){
如果 尝试 < $maxAttempts) {
\日志 :: 警告 “暂时性交易错误,正在重试” ,[
'试图' => 尝试,
'错误' => $e -> 获取消息 ()
]);
// 等待一段时间后再重试(指数退避)
睡眠 100000 * $attempt); // 100毫秒、200毫秒、300毫秒
继续 ;
}
}
// 非瞬态错误或已达到最大尝试次数
\日志 :: 错误 交易失败 ,[
'错误' => $e -> 获取消息 (),
“尝试” => $尝试
]);
返回 回复 () -> json ([
'错误' => 交易失败,请重试。
], 500 (英文):
} 抓住 \例外 $e) {
// 业务逻辑错误(资金不足、缺货)
返回 回复 () -> json ([
'错误' => $e -> 获取消息 ()
], 400 (英文):
}
}
}

此实现方式:

  • 区分暂时性错误和业务性错误 - 重试瞬态错误,立即失败业务错误
  • 使用指数退避 - 重试间隔时间逐渐延长
  • 限制重试次数 - 尝试3次后放弃,以避免无限循环
  • 记录故障 - 有助于调试和监控

MongoDB 的驱动程序具有内置的重试逻辑,但了解如何处理不同的错误类型可以使您的应用程序更加健壮。

Laravel 的自动事务重试

Laravel 实际上提供了一种更简单的重试处理方式。您可以直接指定重试次数:

数据库 :: 联系 'mongodb' -> 交易 功能 (){
// 事务逻辑
}, 5 (英文): // 死锁时最多重试 5 次

然而,这种方法仅在发生死锁时才会重试。要进行更全面的错误处理,包括瞬态事务错误,请使用上面所示的手动重试循环。

性能:保持交易快速

实现交易功能后,我运行了性能基准测试,发现结账平均耗时 450 毫秒——比以前慢得多。查看代码后,我意识到我做了太多事情。 里面 交易。

错误在于:

// ❌ 错误:交易执行过多操作
数据库 :: 联系 'mongodb' -> 交易 功能 () 使用 ($订单,$用户) {
// 数据库操作
钱包 :: 生的 ... (英文):
产品 :: 生的 ... (英文):
命令 :: 创造 ... (英文):
// 外部服务 - 请勿在交易中执行此操作!
邮件 :: ($用户) -> 电子邮件) -> 发送 新的 订单确认 (订单)
Http :: 邮政 'https://shipping-api.com/create-label' ,[
'order_id' => $订单 -> ID
]);
// 分析
分析 :: 追踪 '购买' ,$order -> 数组 ());
});

事务在等待期间持有数据库锁:

  • 电子邮件服务器响应
  • 发货 API 响应
  • 分析服务响应

这会严重影响性能,并会阻塞其他操作。

解决方法如下:

// ✅ 良好:事务仅用于数据库操作
民众 功能 查看 要求 $请求)
{
// ... 验证 ...
// 事务:仅限原子数据库更改
$订单 = 数据库 :: 联系 'mongodb' -> 交易 功能 () 使用 ($userId, $productId, $quantity) {
$产品 = 产品 :: 查找或失败 ($productId)
金额 = $产品 -> 价格 * 数量;
// 原子钱包借记
$walletResult = 钱包 :: 生的 功能 (收藏) 使用 ($userId, $amount) {
返回 $集合 -> updateOne
[ '用户身份' => $userId, '平衡' => [ '$gte' => $amount]],
[ '$inc' => [ '平衡' => - $amount]]
(英文):
});
如果 ($walletResult -> 获取修改次数 () === 0 ){
新的 \例外 资金不足 (英文):
}
// 原子库存减少
库存结果 = 产品 :: 生的 功能 (收藏) 使用 ($productId, $quantity) {
返回 $集合 -> updateOne
[ '_ID' => 新的 对象ID ($productId) '库存' => [ '$gte' => $quantity]],
[ '$inc' => [ '库存' => - $quantity]]
(英文):
});
如果 库存结果 -> 获取修改次数 () === 0 ){
新的 \例外 库存不足 (英文):
}
// 创建订单
返回 命令 :: 创造 ([
'用户身份' => $userId,
'product_id' => $productId,
'数量' => 数量,
'数量' => 金额,
'地位' => '完全的'
]);
});
// 事务提交后,执行耗时操作
// 这些操作失败不会影响数据一致性
尝试 {
$用户 = 用户 :: 寻找 ($userId)
邮件 :: ($用户) -> 电子邮件) -> 发送 新的 订单确认 (订单)
} 抓住 \例外 $e) {
\日志 :: 错误 邮件发送失败 ,[
'order_id' => $订单 -> ID,
'错误' => $e -> 获取消息 ()
]);
// 已加入重试队列,但订单已提交
}
尝试 {
Http :: 邮政 'https://shipping-api.com/create-label' ,[
'order_id' => $订单 -> ID,
'地址' => $用户 -> 地址
]);
} 抓住 \例外 $e) {
\日志 :: 错误 “发货标签失败” ,[
'order_id' => $订单 -> ID,
'错误' => $e -> 获取消息 ()
]);
// 重试队列
}
// 分析可能会静默失败
尝试 {
分析 :: 追踪 '购买' ,$order -> 数组 ());
} 抓住 \例外 $e) {
\日志 :: 警告 “分析跟踪失败” ,[ '错误' => $e -> 获取消息 ()]);
}
返回 回复 () -> json ([
'成功' => 真的 ,
'命令' => $订单
]);
}

重构后,结账时间平均降至 150 毫秒,提高了 67%。

黄金法则: 事务应仅关注原子数据库操作。其他所有操作都在提交之后进行。

利用数据库索引进行优化

为了进一步提升事务性能,请在频繁查询的字段上创建索引。正如 MongoDB Laravel 文档中所建议的,请合理组织数据和索引结构,以优化事务性能:

命名空间 应用程序\控制台\命令 ;
使用 照亮\控制台\命令 ;
使用 照明\支持\立面\数据库 ;
班级 创建 MongoIndex 延伸 命令
{
受保护 $签名 = 'mongo:create-indexes' ;
受保护 $描述 = “创建 MongoDB 索引以优化事务” ;
民众 功能 处理 ()
{
$db = 数据库 :: 联系 'mongodb' -> 获取 MongoDB ();
// 为钱包用户 ID 创建索引,以便在结账时快速查找
$db -> 钱包 -> 创建索引 ([ '用户身份' => 1 ], [ '独特的' => 真的 ]);
$this -> 信息 ✓ 已在 wallets.user_id 上创建索引 (英文):
//钱包上的复合索引用于条件更新
$db -> 钱包 -> 创建索引 ([ '用户身份' => 1 , '平衡' => 1 ]);
$this -> 信息 ✓ 已创建钱包复合指数 (英文):
// 用于库存检查的产品库存索引
$db -> 产品 -> 创建索引 ([ '库存' => 1 ]);
$this -> 信息 ✓ 已在 products.stock 上创建索引 (英文):
// 用户查询的订单复合索引
$db -> 订单 -> 创建索引 ([ '用户身份' => 1 , 'created_at' => - 1 ]);
$this -> 信息 ✓ 已创建订单复合指数 (英文):
// 用于筛选的订单状态索引
$db -> 订单 -> 创建索引 ([ '地位' => 1 ]);
$this -> 信息 ✓ 已在 orders.status 上创建索引 (英文):
$this -> 信息 \n ✅所有索引创建成功! (英文):
$this -> 信息 “交易性能已优化。” (英文):
}
}

注册并运行此命令:

php 工匠 mongo:创建索引

如文档中所述,在事务查询中使用的字段上创建索引可以显著提高性能和可靠性。

何时不应使用交易

交易可以解决特定问题,但并非总是必要的。以下情况可以省略交易:

1. 单文档操作(请改用原子运算符):

// ❌ 请勿使用交易
数据库 :: 联系 'mongodb' -> 交易 功能 (){
产品 :: 生的 fn ($c) => $c -> updateOne
[ '_ID' => $productId],
[ '$inc' => [ “观点” => 1 ]]
));
});
// ✅ 只需使用原子操作
产品 :: 生的 fn ($c) => $c -> updateOne
[ '_ID' => $productId],
[ '$inc' => [ “观点” => 1 ]]
));

2. 最终一致性是可以接受的:

// ❌ 不要将交易用于分析
数据库 :: 联系 'mongodb' -> 交易 功能 (){
用户活动 :: 创造 ([
'用户身份' => $userId,
'行动' => 'viewed_product' ,
'product_id' => $productId
]);
});
// ✅ 直接创建
用户活动 :: 创造 ([
'用户身份' => $userId,
'行动' => 'viewed_product' ,
'product_id' => $productId
]);

3. 对性能要求极高的批量作业:

// ❌ 不要对日志使用事务
数据库 :: 联系 'mongodb' -> 交易 功能 (){
日志 :: 创造 ([ '信息' => 用户已登录 ]);
});
// ✅ 只需插入
日志 :: 创造 ([ '信息' => 用户已登录 ]);

在以下情况下使用交易:

  • 操作涉及多个文档或集合
  • 部分故障会导致状态不一致(例如,钱包扣款但订单未发出)。
  • 金融运作需要保证一致性。
  • 必须保持业务不变性(例如,库存与订单匹配)。

实际案例:预订系统

我们来看另一个实际例子。类似于 MongoDB Learning Byte 中关于 Laravel 事务的内容,假设有一个酒店预订系统:

命名空间 App\Http\Controllers ;
使用 应用程序\模型\租赁 ;
使用 应用程序\模型\预订 ;
使用 照亮\Http\请求 ;
使用 照明\支持\立面\数据库 ;
使用 MongoDB\BSON\ObjectId ;
班级 预订控制器 延伸 控制器
{
民众 功能 店铺 要求 $请求)
{
$已验证 = $请求 -> 证实 ([
'rental_id' => '必填|字符串' ,
'用户身份' => '必填|字符串' ,
'报到' => '必填|日期' ,
'查看' => 'required|date|after:check_in' ,
]);
尝试 {
预订 = 数据库 :: 联系 'mongodb' -> 交易 功能 () 使用 ($已验证) {
// 更新租赁房源信息
租赁结果 = 租赁 :: 生的 功能 (收藏) 使用 ($已验证) {
返回 $集合 -> updateOne
[
'_ID' => 新的 对象ID ($已验证[ 'rental_id' ]),
'可用的' => 真的
],
[
'$set' => [ '可用的' => 错误的 ]
]
(英文):
});
如果 租金结果 -> 获取修改次数 () === 0 ){
新的 \例外 “暂无出租房源” (英文):
}
// 创建预订记录
预订 = 预订 :: 创造 ([
'rental_id' => $已验证[ 'rental_id' ],
'用户身份' => $已验证[ '用户身份' ],
'报到' => $已验证[ '报到' ],
'查看' => $已验证[ '查看' ],
'地位' => '确认的'
]);
返回 预订;
});
返回 回复 () -> json ([
'成功' => 真的 ,
“预订” => 预订
]);
} 抓住 \例外 $e) {
返回 回复 () -> json ([
'错误' => $e -> 获取消息 ()
], 400 (英文):
}
}
}

如果两个操作都成功,则事务提交。否则,事务回滚,以确保数据一致性。租赁车辆的可用性和预订记录保持同步。

测试交易性能

建立衡量交易开销的基准:

民众 功能 测试交易性能 ()
{
// 设置测试数据
为了 ($i = 0 ;$i < 100 ;$i ++ ){
产品 :: 创造 ([
'姓名' => “产品 { $i }” ,
'价格' => 50.00 ,
'库存' => 100
]);
钱包 :: 创造 ([
'用户身份' => “用户-{ $i }” ,
'平衡' => 1000.00
]);
}
$开始 = 微时间 真的 (英文):
// 运行 100 次交易结账
为了 ($i = 0 ;$i < 100 ;$i ++ ){
$产品 = 产品 :: 跳过 ($i) -> 第一的 ();
$this -> 帖子Json '/api/checkout' ,[
'用户身份' => “用户-{ $i }” ,
'product_id' => $产品 -> ID,
'数量' => 1
]);
}
$duration = 微时间 真的 - $start) * 1000 ;
平均值 = $duration / 100 ;
倾倒 "100 笔交易结账在 { $duration }多发性硬化症” (英文):
倾倒 “平均的: { 平均值 每次结账耗时 }毫秒” (英文):
// 通过优化事务处理(无外部调用),平均耗时应低于 200 毫秒
$this -> assertLessThan 200 ,平均值,
“交易结账平均耗时应低于200毫秒” (英文):
}

目标:在合理控制交易范围的情况下,每次结账耗时低于 200 毫秒。

要点总结

  • 原子操作解决单文档一致性问题,事务解决多文档一致性问题。 - 了解在每种情况下应该使用哪种工具
  • 拉拉维尔的 DB::transaction() 与 MongoDB 和 SQL 数据库的操作方式完全相同。 - 与您已熟悉的语法相同
  • 交易提供全有或全无的执行方式。 要么所有操作都成功,要么所有操作都回滚。
  • 尽量缩小交易范围 - 仅包含关键数据库操作;将外部 API 调用移至外部。
  • 使用重试逻辑处理瞬态错误 网络问题和集群选举可能会导致暂时性故障。
  • 测试成功和失败两种情况。 - 验证回滚操作在操作失败时是否能正常工作
  • 在事务查询中使用的字段上创建索引 - 显著提升性能和锁争能力
  • 监控交易持续时间 - 尽可能将交易时间控制在 100 毫秒以内,避免在交易内部执行缓慢的操作。

常见问题解答

MongoDB 中的原子操作和事务有什么区别?

原子操作 $inc 只需一步即可修改单个文档——快速简便。事务协调跨多个文档或集合的多个操作,确保所有操作同时成功或同时失败。对于单个文档的更新(例如递增计数器),请使用原子操作;而当需要跨多个文档保持一致时(例如在创建订单时从钱包扣款),请使用事务。

在 Laravel 中,MongoDB 事务可以运行多长时间?

MongoDB 的事务默认超时时间为 60 秒。如果事务超过此限制,MongoDB 会自动中止它。实际上,您应该将事务的执行时间控制得更短——理想情况下应低于 100 毫秒。长时间运行的事务会持有锁,阻塞其他操作并降低吞吐量。事务关闭时,仅包含关键的原子数据库操作,而将所有其他操作(例如电子邮件、API 调用)移到事务之外。

如果我的交易超时或服务器崩溃会发生什么?

MongoDB 保证完全回滚。如果事务超时、崩溃或遇到任何错误,MongoDB 会自动中止该事务并撤销其中的所有操作。数据库将恢复到事务发生前的状态——不会进行任何部分更新。您的应用程序会收到一个异常,您可以捕获该异常并进行适当的处​​理(例如重试、向用户返回错误等)。

Laravel MongoDB 中可以嵌套事务吗?

不,MongoDB 不支持嵌套事务。如果您调用 DB::transaction() 在另一个里面 DB::transaction() Laravel 会将它们视为同一个事务。所有操作都属于最外层的事务。这是大多数数据库的标准行为——事务是扁平的,而不是嵌套的。请设计代码,确保相关操作使用同一个事务作用域。

如何处理生产环境中的事务错误?

区分瞬态错误和业务错误。瞬态错误(例如网络问题、集群选举)应使用指数退避算法自动重试。业务错误(例如资金不足、缺货)应立即失败,并向用户显示有意义的消息。始终记录事务失败及其上下文信息(用户 ID、操作详情),以便进行调试。监控重试率,如果重试率异常高,则发出警报,因为这表明基础设施存在问题。

下一步

我们构建了一个强大的交易结算系统,能够确保跨多个集合的数据一致性。但还有一个关键问题需要解决:如果数据库交易成功,但响应却始终无法到达客户端,会发生什么情况?前端会重试,结果导致客户被重复收费。

下一篇文章中, “使 Laravel MongoDB 操作幂等:金融交易的安全重试” 我们将实现幂等键,以确保结账操作可以安全重试,即使网络故障造成不确定性。

参考

阿瑟·里贝罗照片

德尔布里奇咨询公司

归档于:
立方体

Laravel 时事通讯

加入超过 4 万名开发者的行列,不错过任何新的技巧、教程等内容。

图像
廷克威尔

这款编辑器专为快速反馈和快速迭代而设计,让您尽享编码和调试的乐趣。它就像您应用程序的一个外壳——但具备多行编辑、代码自动完成等更多功能。

参观廷克韦尔
Tinkerwell 徽标

廷克威尔

Laravel 开发者必备的代码运行器。可在本地和生产环境中体验 AI、自动补全和即时反馈功能。

廷克威尔
几天内即可获得 Laravel 代码审查徽标的专家指导

几天内即可获得 Laravel 代码审查方面的专家指导

专家级代码审查!两位拥有 10 年以上 Laravel 开发经验的开发者将为您提供清晰、实用的反馈,帮助团队构建更优质的应用程序。

几天内即可获得 Laravel 代码审查方面的专家指导
PhpStorm 标志

PhpStorm

首选的 PHP IDE,对 Laravel 及其生态系统提供广泛的开箱即用支持。

PhpStorm
Laravel Cloud 标志

Laravel 云

轻松创建和管理服务器,并在几秒钟内部署 Laravel 应用程序。

Laravel 云
了解 Softtech 的标志

了解软科技

Acquaint Softtech 提供 AI 就绪的 Laravel 开发人员,48 小时内即可上手,每月费用为 3000 美元,没有冗长的销售流程,并提供 100% 退款保证。

了解软科技
Kirschbaum 标志

樱桃树

提供创新和稳定性,确保您的Web应用程序取得成功。

樱桃树
Shift 标志

转移

还在运行旧版本的 Laravel?立即实现 Laravel 自动升级和代码现代化,让您的应用程序保持最新状态。

转移
鱼叉:新一代时间跟踪和发票标志

Harpoon:新一代时间跟踪和发票系统

新一代时间跟踪和计费软件,帮助您的机构规划和预测盈利的未来。

Harpoon:新一代时间跟踪和发票系统
Lucky Media 标志

幸运传媒

Get Lucky Now——拥有十余年经验的 Laravel 开发理想之选!

幸运传媒
SaaSykit:Laravel SaaS 入门套件徽标

SaaSykit:Laravel SaaS 入门套件

SaaSykit 是一个多租户 Laravel SaaS 入门套件,包含运行现代 SaaS 所需的所有功能,例如支付、美观的结账界面、管理面板、用户仪表盘、身份验证、现成组件、统计数据、博客、文档等等。

SaaSykit:Laravel SaaS 入门套件
Inertia.js v3.0.0 版本发布,包含乐观更新、useHttp 等更多功能(图片)。

Inertia.js v3.0.0 版本发布,新增乐观更新、useHttp 等功能。

阅读文章
Laravel Boost v2.4.0 新增安全审计和 Laravel 最佳实践技能图片

Laravel Boost v2.4.0 新增安全审计和 Laravel 最佳实践技能

阅读文章
在 Laravel 镜像中构建事务安全的多文档操作

在 Laravel 中构建事务安全的多文档操作

阅读文章
使用 Laravel 构建 AI:使用 Laravel 13 的 AI SDK 镜像构建您的第一个代理

使用 Laravel 构建 AI:使用 Laravel 13 的 AI SDK 构建您的第一个代理

阅读文章
OG Kit:使用 HTML 和 CSS 生成动态 Open Graph 图像

OG Kit:使用 HTML 和 CSS 生成动态 Open Graph 图像

阅读文章
Prism Workers AI — 为 Prism PHP 图像提供 Cloudflare Workers AI 服务

Prism Workers AI — 为 Prism PHP 提供 Cloudflare Workers AI 服务

阅读文章