学习如何在 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
(英文):}}}
主要变化:
- 将所有操作包装在
DB::connection('mongodb')->transaction() - 从收货处退回订单 这样我们就可以在外面访问它了。
- 业务逻辑失败时抛出异常 (资金不足,缺货)
- 捕获异常 返回正确的 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 保证:
- 自动中止 交易已终止。
- 完全回滚 - 交易内的所有操作均被撤销
- 一致状态 数据库恢复到事务处理前的状态
- 返回错误 您的申请收到例外通知
让我们妥善处理这些情况:
民众
功能
查看
(
要求
$请求){
// ... 验证 ...最大尝试次数
=
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 操作幂等:金融交易的安全重试” 我们将实现幂等键,以确保结账操作可以安全重试,即使网络故障造成不确定性。





