PHP: полный мануал с примерами, функциями и ООП
Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Дерево руководства
Глава 148 · После атомарной записи одного документа MongoDB
MongoDB: транзакции — заказ, остаток, отмена и повтор запроса
Сохранить несколько связанных документов вместе, отменить отказ и увидеть реальный результат каждого шага.
Когда заказ и остаток должны сохраниться вместе
Покупатель оформил две кружки. Нужно уменьшить остаток с 5 до 3 и создать один заказ. Если записать эти изменения независимо, отказ второй операции может оставить товар списанным без заказа. Транзакция объединяет несколько операций: подтвердить изменения вместе либо завершить работу без их сохранения.
| Момент | Остаток кружек | Заказов | Что видит обычный внешний запрос |
|---|---|---|---|
| До оформления | 5 | 0 | Исходные данные |
| Изменения внутри незавершённой транзакции | 3 | 1 | Пока по-прежнему 5 и 0 |
| После успешного commit | 3 | 1 | Сохранённые остаток и заказ |
| При отмене вместо commit | 5 | 0 | Исходные данные сохранены |
Это продолжение атомарного изменения одного документа. Одна условная запись уже атомарна сама по себе. Транзакция нужна, когда одно правило должно охватывать несколько документов или коллекций. Она не исправляет неудачную модель данных и не делает любую последовательность запросов автоматически правильной.
- Manager соединяет PHP с MongoDB
- startSession создаёт контекст
- startTransaction объявляет группу
- Каждая операция получает ту же Session
- Проверки выполнены → commit
- Условие не выполнено → abort
Session можно представить как номер группы, который прикладывается к каждому действию. Но это не обычное поле документа: объект драйвера связывает запросы протоколом MongoDB. Без опции session запрос не попадёт в эту группу. MongoDB Session также не имеет отношения к PHP session_start и хранению входа пользователя в браузере.
Минимальная форма: начать и отменить пустую группу
$session = $manager->startSession();
$session->startTransaction();
$session->abortTransaction();
$session->endSession();$manager — уже подключённый Manager; $session — переменная с созданной логической сессией. -> вызывает метод объекта, скобки содержат его аргументы. startTransaction, commitTransaction и abortTransaction возвращают void: результатом служит изменение состояния, а не число сохранённых заказов. Пустая группа в этом фрагменте не меняет документы; в первом полном примере ниже вместо неё выполняются реальные записи.
Полезные основы: документ и коллекция, Manager, Query и BulkWrite, параметры и возвращаемое значение, исключения и finally. try запускает работу, catch обрабатывает конкретный ожидаемый отказ, finally освобождает свои ресурсы при нормальном завершении или исключении. Неожиданная ошибка в программах ниже не превращается в сообщение об успехе.
Отдельный replica set и самостоятельный запуск
Для этих архивов нужен отдельный локальный replica set: MongoDB 8.0, PHP 8.4+ CLI x64 и расширение mongodb 2.5+. Контрольная среда: MongoDB 8.0.34, PHP 8.5.11, расширение 2.5.5. Composer не требуется. Replica set — набор серверов, работающих с реплицируемыми данными; здесь намеренно один участник для обучения. Он даёт нужный режим транзакций, но не отказоустойчивость.
Обычный standalone MongoDB не выполняет эти многодокументные транзакции. README каждого архива содержит установку бинарников, запуск нового mongod с отдельным dbpath, replSet phpmanRs и bind_ip 127.0.0.1, однократный rs.initiate для этого нового экземпляра и проверку PRIMARY. Рабочую службу, чужие данные и её конфигурацию менять не нужно.
Один запуск после подготовки сервера
# PowerShell в каталоге распакованного архива:
$env:PHPMAN_MONGODB_URI='mongodb://127.0.0.1:27076/?replicaSet=phpmanRs&retryWrites=false'
$env:PHPMAN_MONGODB_DATABASE='php_manual_txn_lab'
php example.phpОткройте result.html: это таблица фактических значений. result.json содержит те же строки; metrics.json — версии, проверки и SHA256 файлов. Helper tx118-helper.php принимает только явно заданный loopback seed без учётных данных и проверяет один PRIMARY набора phpmanRs. Это ограничение учебного стенда, а не рекомендация отключить защиту рабочей базы.
Каждая программа создаёт коллекции mongodb118_txn_… со случайным суффиксом. Владение фиксируется только после успешного create. Индексы и исходные документы готовятся до транзакции; finally закрывает свою Session и удаляет только точные созданные имена. Затем listCollections подтверждает отсутствие. Нужны права создания, чтения, записи, индекса и удаления своей учебной коллекции, listCollections, hello и buildInfo. При принудительном завершении процесса finally не гарантируется; неизвестные объекты по префиксу удалять нельзя.
Session, настройки и разные причины отказа
Каждое чтение и запись внутри группы получают опцию session с тем же объектом. У Session только одна открытая транзакция; операции здесь выполняются последовательно. Для следующих отдельных транзакций можно повторно использовать ещё не закрытую Session.
$manager->executeQuery($namespace, $query, ['session' => $session]);
$manager->executeBulkWrite($namespace, $bulk, ['session' => $session]);Настройки транзакции выбирают через defaultTransactionOptions в startSession или options в startTransaction. readConcern определяет правила видимости чтения; snapshot выбирает согласованный снимок. readPreference выбирает сервер, и чтения транзакции используют primary. writeConcern определяет подтверждение commit; majority вместе со snapshot обеспечивает соответствующие гарантии большинства. На одном учебном участнике majority не доказывает отказоустойчивость нескольких копий. Подробное сравнение: подтверждение записи и согласованность чтения.
maxCommitTimeMS=1000 ограничивает одну команду commit на сервере. Это не таймер всей работы PHP. Внутренним операциям транзакции не задают отдельный writeConcern. Длительное ожидание пользователя, письмо или вызов платёжного API не нужно помещать в это тело: транзакция MongoDB не умеет откатывать чужой сервис.
| Ситуация | Что она означает | Действие приложения |
|---|---|---|
| Условие available >= quantity не совпало | Запрос корректен, но товара недостаточно | Проверить matchedCount, отменить группу и объяснить отказ |
| Duplicate key, код 11000 | Уникальное правило отклонило запись; транзакция могла быть прекращена сервером | Не продолжать как будто запись удалась; разрешить отказ и проверить свой результат |
| TransientTransactionError | Попытка транзакции может быть повторена | Ограниченный повтор всего тела по метке, без повторения внешних эффектов |
| UnknownTransactionCommitResult | Подтверждение потеряно или исход commit неизвестен | Разрешать исход и повторять commit той же транзакции; не начинать заново списание |
В этих шести программах нет автоматического цикла повторов и нет принудительного обрыва сети. Ожидаемые бизнес-отказы проверяются отдельно, остальные ошибки выходят наружу. Драйвер может сам повторять commit по своим правилам даже при retryWrites=false; это не равно повтору всего тела приложения. Общая инструкция PHP.net о повторах уточнена по правилам MongoDB Core API. Неизвестный commit нельзя объявлять доказанным rollback.
Сохранить заказ и списать остаток одним решением
Заказ должен появиться вместе со списанием двух кружек со склада. Если сохранить только заказ или только остаток, две части системы разойдутся. Нужна общая граница подтверждения для двух коллекций.
Входные данные
В stock одна Кружка с available=5. В orders записей нет. Покупатель заказывает 2 штуки; ожидаем остаток 3 и один заказ.
- Session объединяет операции
- startTransaction открывает работу
- Остаток 5 → 3 внутри Session
- Заказ order-118 добавлен в той же Session
- Снаружи ещё остаток 5 и нет заказа
- commit → снаружи 3 и один заказ
Короткое действие
$session = $manager->startSession();
$session->startTransaction();
// Каждой операции передайте ['session' => $session].
$manager->executeBulkWrite($stockNamespace, $stockChange, ['session' => $session]);
$manager->executeBulkWrite($orderNamespace, $newOrder, ['session' => $session]);
$session->commitTransaction();
// Полная программа ниже проверяет остаток, ловит отказ и закрывает Session.Фрагмент выделяет действие внутри сценария. Объекты, namespace, данные и helper полностью подготовлены в самостоятельной программе ниже.
Что получится
| Наблюдение | Остаток, шт. | Заказов | Состояние |
|---|---|---|---|
| До начала | 5 | 0 | none |
| Внутри транзакции | 3 | 1 | in_progress |
| Снаружи до commit | 5 | 0 | отдельное чтение |
| После commit | 3 | 1 | committed |
Почему получились эти значения
Session — логическая сессия драйвера, не PHP-сессия авторизации и не новая база. Её создаёт Manager. startTransaction начинает многодокументную транзакцию: группу изменений, которые должны сохраниться целиком либо не сохраниться.
Важно не только вызвать startTransaction, но и передать ['session' => $session] каждой операции Manager. Helper передаёт её третьим аргументом executeQuery/executeBulkWrite. Чтение без этой опции в таблице специально наблюдает состояние вне транзакции.
Условие available >= 2 проверяется на сервере вместе с уменьшением. matchedCount=1 означает найденный подходящий документ; если нет, приложение должно отказаться от заказа. Транзакция не заменяет проверку бизнес-условия.
Внутренние чтения видят внесённые изменения, внешние до commit — старый остаток и отсутствие заказа. После успешного commit видна согласованная пара. State меняется от none через starting и in_progress к committed; вызов startTransaction сам по себе ещё не отправляет все будущие операции.
Параметры snapshot/primary/majority задаются для транзакции через defaultTransactionOptions Session; startTransaction может переопределить конкретный параметр. getTransactionOptions показывает maxCommitTimeMS=1000. Внутренним записям отдельный writeConcern не задаётся.
Границы и типичные ошибки
Сессия принадлежит создавшему Manager. В одну Session нельзя параллельно поместить несколько открытых транзакций. Не держите её открытой, пока пользователь думает или отвечает внешний HTTP-сервис. Изменения одного документа уже атомарны; транзакция нужна при действительно общей границе нескольких документов.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/tx118-helper.php';
[$manager,$database]=tx118Environment();
$runtime=['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),'server'=>tx118Command($manager,'admin',['buildInfo'=>1])[0]['version']];
$prefix='mongodb118_txn_'.bin2hex(random_bytes(8));
$collections=[];$owned=[];$session=null;$sessionEnded=false;$details=[];
try {
foreach(['stock', 'orders'] as $role){
$name=$prefix.'_'.$role;
if(tx118Exists($manager,$database,$name)){throw new RuntimeException('Owned fixture collision');}
tx118Command($manager,$database,['create'=>$name],true);
$owned[]=$name;$collections[$role]=$name;
}
$stock=$database.'.'.$collections['stock'];$orders=$database.'.'.$collections['orders'];
tx118Insert($manager,$stock,[['_id'=>'mug','title'=>'Кружка','available'=>5]]);
$stockOptions=['projection'=>['_id'=>0,'available'=>1],'limit'=>1];
$orderOptions=['projection'=>['_id'=>1,'quantity'=>1],'limit'=>10];
$session=tx118Session($manager);
$before=tx118Read($manager,$stock,['_id'=>'mug'],$stockOptions)[0]['available'];
$none=$session->getTransactionState();
$session->startTransaction();
$starting=$session->getTransactionState();$options=$session->getTransactionOptions();
$write=tx118Update($manager,$stock,['_id'=>'mug','available'=>['$gte'=>2]],['$inc'=>['available'=>-2]],$session);
if($write->getMatchedCount()!==1){throw new DomainException('Insufficient stock');}
$insert=tx118Insert($manager,$orders,[['_id'=>'order-118','sku'=>'mug','quantity'=>2]],$session);
$inside=tx118Read($manager,$stock,['_id'=>'mug'],$stockOptions,$session)[0]['available'];
$outside=tx118Read($manager,$stock,['_id'=>'mug'],$stockOptions)[0]['available'];
$insideOrders=count(tx118Read($manager,$orders,[],$orderOptions,$session));
$outsideOrders=count(tx118Read($manager,$orders,[],$orderOptions));
$inProgress=$session->getTransactionState();$active=$session->isInTransaction();
$session->commitTransaction();
$committed=$session->getTransactionState();
$after=tx118Read($manager,$stock,['_id'=>'mug'],$stockOptions)[0]['available'];
$afterOrders=count(tx118Read($manager,$orders,[],$orderOptions));
$rows=[['До начала',$before,0,$none],['Внутри транзакции',$inside,$insideOrders,$inProgress],
['Снаружи до commit',$outside,$outsideOrders,'отдельное чтение'],['После commit',$after,$afterOrders,$committed]];
$checks=['conditional_decrement'=>$write->getMatchedCount()===1 && $write->getModifiedCount()===1,
'order_inserted'=>$insert->getInsertedCount()===1,'inside_changes_visible'=>$inside===3 && $insideOrders===1,
'outside_uncommitted_hidden'=>$outside===5 && $outsideOrders===0,
'committed_pair'=>$after===3 && $afterOrders===1,
'session_lifecycle'=>$none==='none' && $starting==='starting' && $inProgress==='in_progress' && $committed==='committed',
'active_only_before_commit'=>$active && !$session->isInTransaction(),
'transaction_option_inherited'=>is_array($options) && $options['maxCommitTimeMS']===1000];
$details=['starting_state'=>$starting,'maxCommitTimeMS'=>$options['maxCommitTimeMS']];
} finally {
try {
if($session!==null && !$sessionEnded){
try{if($session->isInTransaction()){$session->abortTransaction();}}
finally{$session->endSession();$sessionEnded=true;}
}
} finally {
foreach(array_reverse($owned) as $name){tx118Command($manager,$database,['drop'=>$name],true);}
}
}
$removed=true;foreach($owned as $name){$removed=$removed && !tx118Exists($manager,$database,$name);}
$checks['owned_collections_removed']=$removed;
$checks['session_closed']=$sessionEnded;
tx118Report('mongodb118-transactions-order-stock', ['Наблюдение', 'Остаток, шт.', 'Заказов', 'Состояние'], $rows, $checks, $runtime, $details);
echo 'mongodb118-transactions-order-stock: saved result.html and result.json
';
Ожидаемый результат
mongodb118-transactions-order-stock: saved result.html and result.json
Запустите php example.php на отдельном локальном replica set и откройте result.html. В архиве есть tx118-helper.php и README с подготовкой сервера, расширения и переменных окружения. Программа создаёт и убирает только собственные учебные коллекции.
Открыть фактический результат HTML · Скачать результат JSON
Упражнение с ориентиром результата. Измените количество на 4: после commit ожидаются остаток 1 и один заказ на 4 штуки. Добавьте quantity в видимую таблицу и проверьте, что обе операции получили одну Session.
Практическая проблема: Example of a transaction in MongoDB 4.0 using PHP. Разработчик не видит, как связать PHP-запросы одной транзакцией. Разбор показывает replica set, отдельный объект Session и обязательную передачу session каждой операции. Вопрос задаёт ситуацию; правила сверены с первичными источниками, программа и данные урока оригинальные.
Отменить уже добавленный заказ при нехватке товара
Черновик заказа уже добавлен внутри транзакции, но товара не хватает. Нужно убрать и черновик, а не оставить заказ с несуществующим резервом.
Входные данные
Доступна 1 кружка, заказ запрашивает 3. Пример намеренно создаёт черновик раньше условного списания, чтобы результат abort был виден.
- Создан черновик внутри Session
- Попытка available >= 3 при остатке 1
- matchedCount = 0, сервер не бросает ошибку
- Приложение сообщает нехватку
- abort отменяет всю группу
- Снаружи остаток 1 и заказов 0
Короткое действие
if ($writeResult->getMatchedCount() !== 1) {
$session->abortTransaction();
// Созданный в этой же Session черновик заказа тоже не сохраняется.
}
// Нулевой matchedCount не является исключением сервера: решение принимает приложение.Фрагмент выделяет действие внутри сценария. Объекты, namespace, данные и helper полностью подготовлены в самостоятельной программе ниже.
Что получится
| Шаг | Остаток | Количество | Результат |
|---|---|---|---|
| Черновик внутри | 1 | 1 | ещё не сохранён |
| Условное списание | 1 | 0 | совпавших товаров |
| После abort | 1 | 0 | Недостаточно товара |
| Session | NULL | NULL | aborted |
Почему получились эти значения
Нулевое число совпадений — допустимый ответ update. Оно не вызывает исключение MongoDB. PHP проверяет результат и бросает собственный DomainException, то есть сообщает нарушение правила заказа.
catch перехватывает только этот ожидаемый бизнес-отказ и вызывает abortTransaction. Все записи данной транзакции отменяются вместе; вручную удалять черновик отдельным запросом не нужно.
Исключение сети или неправильная команда не подменяются сообщением «товара нет». Такие ошибки здесь остаются ошибками запуска; finally завершает Session и убирает только учебные коллекции.
В реальном оформлении разумно проверять/резервировать остаток до вставки заказа, чтобы раньше отказаться. Обратный порядок в упражнении намеренный: он показывает, почему abort отменяет уже выполненную запись.
Границы и типичные ошибки
Проверка наличия до транзакции отдельно от списания оставляет окно гонки. Условный update в транзакции и проверка matchedCount всё равно нужны. Отмена базы не отправляет и не отменяет уведомления автоматически.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/tx118-helper.php';
[$manager,$database]=tx118Environment();
$runtime=['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),'server'=>tx118Command($manager,'admin',['buildInfo'=>1])[0]['version']];
$prefix='mongodb118_txn_'.bin2hex(random_bytes(8));
$collections=[];$owned=[];$session=null;$sessionEnded=false;$details=[];
try {
foreach(['stock', 'orders'] as $role){
$name=$prefix.'_'.$role;
if(tx118Exists($manager,$database,$name)){throw new RuntimeException('Owned fixture collision');}
tx118Command($manager,$database,['create'=>$name],true);
$owned[]=$name;$collections[$role]=$name;
}
$stock=$database.'.'.$collections['stock'];$orders=$database.'.'.$collections['orders'];
tx118Insert($manager,$stock,[['_id'=>'mug','available'=>1]]);
$session=tx118Session($manager);$reason='';$insideCount=0;$matched=-1;
$session->startTransaction();
try{
tx118Insert($manager,$orders,[['_id'=>'draft-118','sku'=>'mug','quantity'=>3]],$session);
$insideCount=count(tx118Read($manager,$orders,[],['projection'=>['_id'=>1],'limit'=>10],$session));
$result=tx118Update($manager,$stock,['_id'=>'mug','available'=>['$gte'=>3]],['$inc'=>['available'=>-3]],$session);
$matched=$result->getMatchedCount();
if($matched!==1){throw new DomainException('Недостаточно товара');}
$session->commitTransaction();
}catch(DomainException $failure){
$reason=$failure->getMessage();$session->abortTransaction();
}
$state=$session->getTransactionState();
$left=tx118Read($manager,$stock,['_id'=>'mug'],['projection'=>['available'=>1],'limit'=>1])[0]['available'];
$count=count(tx118Read($manager,$orders,[],['projection'=>['_id'=>1],'limit'=>10]));
$rows=[['Черновик внутри',1,$insideCount,'ещё не сохранён'],['Условное списание',1,$matched,'совпавших товаров'],
['После abort',$left,$count,$reason],['Session',null,null,$state]];
$checks=['draft_existed_inside'=>$insideCount===1,'condition_matched_zero'=>$matched===0,
'reason_is_business'=>$reason==='Недостаточно товара','stock_unchanged'=>$left===1,
'draft_rolled_back'=>$count===0,'transaction_aborted'=>$state==='aborted' && !$session->isInTransaction()];
} finally {
try {
if($session!==null && !$sessionEnded){
try{if($session->isInTransaction()){$session->abortTransaction();}}
finally{$session->endSession();$sessionEnded=true;}
}
} finally {
foreach(array_reverse($owned) as $name){tx118Command($manager,$database,['drop'=>$name],true);}
}
}
$removed=true;foreach($owned as $name){$removed=$removed && !tx118Exists($manager,$database,$name);}
$checks['owned_collections_removed']=$removed;
$checks['session_closed']=$sessionEnded;
tx118Report('mongodb118-transactions-business-abort', ['Шаг', 'Остаток', 'Количество', 'Результат'], $rows, $checks, $runtime, $details);
echo 'mongodb118-transactions-business-abort: saved result.html and result.json
';
Ожидаемый результат
mongodb118-transactions-business-abort: saved result.html and result.json
Запустите php example.php на отдельном локальном replica set и откройте result.html. В архиве есть tx118-helper.php и README с подготовкой сервера, расширения и переменных окружения. Программа создаёт и убирает только собственные учебные коллекции.
Открыть фактический результат HTML · Скачать результат JSON
Упражнение с ориентиром результата. Поставьте запрос 1 штука: бизнес-отказ исчезнет, сработает commit и останется 0 кружек с одним заказом. Обновите ожидаемый результат и объясните, почему прежняя таблица abort теперь не подходит.
Откатить списание после ошибки уникального ключа
Списание прошло, но ключ запроса уже занят существующим заказом. Нужно сохранить исходный остаток и исходный заказ, не пытаясь продолжать повреждённую транзакцию.
Входные данные
У orders есть уникальный индекс request_key; запись 900 уже содержит used-request. Остаток 5. Новая попытка уменьшает его до 4 и пытается записать заказ 901 с тем же ключом.
- Индекс и исходные данные созданы заранее
- В транзакции остаток 5 → 4
- Вставка повторного request_key
- Сервер сообщает код 11000
- Ошибка транзакции обработана, commit не вызывается
- Снаружи снова остаток 5 и только заказ 900
Короткое действие
try {
$manager->executeBulkWrite($orders, $insertWithDuplicateKey, ['session' => $session]);
} catch (MongoDB\Driver\Exception\BulkWriteException $failure) {
if ($session->isInTransaction()) { $session->abortTransaction(); }
throw $failure; // Нельзя игнорировать ошибку и затем считать commit успешным.
}Фрагмент выделяет действие внутри сценария. Объекты, namespace, данные и helper полностью подготовлены в самостоятельной программе ниже.
Что получится
| Этап | Остаток | Заказ/код |
|---|---|---|
| До транзакции | 5 | 900 |
| После списания внутри | 4 | новый заказ ещё не записан |
| Сервер отклонил дубликат | NULL | 11000 |
| После отказа | 5 | 900 |
Почему получились эти значения
Уникальный индекс — правило сервера, запрещающее одинаковые значения ключа в разных документах. Он создан до транзакции. Проверка «сначала поиск, потом вставка» сама по себе не заменяет индекс при двух одновременных запросах.
Ошибка BulkWriteException содержит WriteResult и ошибки записи. Программа принимает ожидаемым только единственный код 11000; любой другой отказ пробрасывается. Текст ошибки не сравнивается целиком, поскольку он зависит от версии и случайного имени коллекции.
Ошибка уникальности может завершить транзакцию на сервере даже до явного abort. Состояние Session приводится к завершённому через abort, когда драйвер ещё считает её активной. После этого запросы подтверждают, что предыдущее уменьшение тоже не осталось.
Нельзя подавить ошибку дубликата и затем продолжить эту транзакцию как успешную. Когда дубликат означает повтор запроса, результат следует разрешать отдельной понятной процедурой; шестой сценарий показывает один последовательный вариант.
Границы и типичные ошибки
ordered=false не превращает транзакцию в пакет с частичным успехом. Ошибка сервера отличается от ожидаемого matchedCount=0. Эта программа не моделирует WriteConflict или разрыв сети и не доказывает алгоритм повторов при конкурентной нагрузке.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/tx118-helper.php';
[$manager,$database]=tx118Environment();
$runtime=['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),'server'=>tx118Command($manager,'admin',['buildInfo'=>1])[0]['version']];
$prefix='mongodb118_txn_'.bin2hex(random_bytes(8));
$collections=[];$owned=[];$session=null;$sessionEnded=false;$details=[];
try {
foreach(['stock', 'orders'] as $role){
$name=$prefix.'_'.$role;
if(tx118Exists($manager,$database,$name)){throw new RuntimeException('Owned fixture collision');}
tx118Command($manager,$database,['create'=>$name],true);
$owned[]=$name;$collections[$role]=$name;
}
$stock=$database.'.'.$collections['stock'];$orders=$database.'.'.$collections['orders'];
tx118Command($manager,$database,['createIndexes'=>$collections['orders'],
'indexes'=>[['key'=>['request_key'=>1],'name'=>'request_key_unique','unique'=>true]]],true);
tx118Insert($manager,$stock,[['_id'=>'mug','available'=>5]]);
tx118Insert($manager,$orders,[['_id'=>900,'request_key'=>'used-request','quantity'=>1]]);
$session=tx118Session($manager);$codes=[];$changedInside=-1;
$session->startTransaction();
try{
$change=tx118Update($manager,$stock,['_id'=>'mug','available'=>['$gte'=>1]],['$inc'=>['available'=>-1]],$session);
if($change->getMatchedCount()!==1){throw new RuntimeException('Fixture stock mismatch');}
$changedInside=tx118Read($manager,$stock,['_id'=>'mug'],['projection'=>['available'=>1],'limit'=>1],$session)[0]['available'];
tx118Insert($manager,$orders,[['_id'=>901,'request_key'=>'used-request','quantity'=>1]],$session);
throw new RuntimeException('Expected duplicate key did not occur');
}catch(MongoDB\Driver\Exception\BulkWriteException $failure){
$codes=array_map(fn($error)=>$error->getCode(),$failure->getWriteResult()->getWriteErrors());
if($codes!==[11000]){throw $failure;}
if($session->isInTransaction()){$session->abortTransaction();}
}
$left=tx118Read($manager,$stock,['_id'=>'mug'],['projection'=>['available'=>1],'limit'=>1])[0]['available'];
$saved=tx118Read($manager,$orders,[],['projection'=>['_id'=>1],'sort'=>['_id'=>1],'limit'=>10]);
$rows=[['До транзакции',5,'900'],['После списания внутри',$changedInside,'новый заказ ещё не записан'],
['Сервер отклонил дубликат',null,(string)$codes[0]],['После отказа',$left,implode(', ',array_column($saved,'_id'))]];
$checks=['decrement_was_inside'=>$changedInside===4,'duplicate_code'=>$codes===[11000],
'decrement_rolled_back'=>$left===5,'only_original_order'=>array_column($saved,'_id')===[900],
'no_active_transaction'=>!$session->isInTransaction()];
} finally {
try {
if($session!==null && !$sessionEnded){
try{if($session->isInTransaction()){$session->abortTransaction();}}
finally{$session->endSession();$sessionEnded=true;}
}
} finally {
foreach(array_reverse($owned) as $name){tx118Command($manager,$database,['drop'=>$name],true);}
}
}
$removed=true;foreach($owned as $name){$removed=$removed && !tx118Exists($manager,$database,$name);}
$checks['owned_collections_removed']=$removed;
$checks['session_closed']=$sessionEnded;
tx118Report('mongodb118-transactions-duplicate-rollback', ['Этап', 'Остаток', 'Заказ/код'], $rows, $checks, $runtime, $details);
echo 'mongodb118-transactions-duplicate-rollback: saved result.html and result.json
';
Ожидаемый результат
mongodb118-transactions-duplicate-rollback: saved result.html and result.json
Запустите php example.php на отдельном локальном replica set и откройте result.html. В архиве есть tx118-helper.php и README с подготовкой сервера, расширения и переменных окружения. Программа создаёт и убирает только собственные учебные коллекции.
Открыть фактический результат HTML · Скачать результат JSON
Упражнение с ориентиром результата. Замените второй request_key на new-request и завершите ветку успешным commit: получите остаток 4 и два разных заказа. Сравните этот результат с отменой всей группы при сохранённом дубликате.
Практическая проблема: insert_many mongoDB and how to ignore duplicate inserts in a session transaction. Попытка проигнорировать duplicate key и продолжить прежнюю транзакцию не гарантирует успех. В уроке код 11000 ожидается явно, ранее выполненное списание не остаётся сохранённым. Вопрос задаёт ситуацию; правила сверены с первичными источниками, программа и данные урока оригинальные.
Перенести целые копейки между двумя документами
Внутренние учебные кошельки должны согласованно перенести 2500 копеек: уменьшить A, увеличить B и сохранить квитанцию. Отсутствующий получатель не должен оставлять одно списание.
Входные данные
A=10000 копеек, B=3000. Первая попытка переводит 2500 в B, вторая — 1000 в несуществующий missing. Все суммы небольшие целые, внешней оплаты нет.
- Условное уменьшение A
- Проверка существования B и увеличение
- Квитанция в той же Session
- commit → 7500 + 5500 = 13000
- Вторая попытка не находит получателя
- abort возвращает её списание
Короткое действие
// Оба изменения и квитанция получают одну и ту же Session.
$debit = ['$inc' => ['balance_kopecks' => -2500]];
$credit = ['$inc' => ['balance_kopecks' => 2500]];
// Только если оба matchedCount === 1:
$session->commitTransaction();
// При отсутствии получателя откатываем и уже сделанное списание.Фрагмент выделяет действие внутри сценария. Объекты, namespace, данные и helper полностью подготовлены в самостоятельной программе ниже.
Что получится
| Операция | Кошелёк A, коп. | Кошелёк B, коп. | Всего, коп. | Квитанций |
|---|---|---|---|---|
| До перевода | 10000 | 3000 | 13000 | 0 |
| Переведено 2500 | 7500 | 5500 | 13000 | 1 |
| Отказ: получатель отсутствует | 7500 | 5500 | 13000 | 1 |
Почему получились эти значения
Функция transfer принимает получателя, положительное количество копеек и ID квитанции. Замыкание получает Manager, namespace и Session через use. Для каждой попытки startTransaction открывает новую транзакцию в той же незакрытой Session.
Первое изменение включает условие достаточного баланса; второе проверяет matchedCount получателя. Только после двух совпадений пишется квитанция и вызывается commit. Сумма 13000 служит наглядной проверкой сохранения общего количества копеек.
Во второй попытке списание уже произошло внутри транзакции, но получателя нет. DomainException приводит к abort: состояние обоих кошельков и число квитанций остаются как после первой попытки.
commitStarted отделяет ошибку тела от ошибки подтверждения. Если commit начат и выброшено исключение, программа не сообщает «деньги возвращены» и не начинает новый перевод автоматически. Ошибка идёт вызывающему коду, который должен разрешить неизвестный исход по правилам приложения.
Границы и типичные ошибки
Это модель внутреннего переноса синтетических целых величин, не готовый платёжный сервис. Для реального финансового учёта нужны авторизация, валюты, журнал, допустимые диапазоны, идемпотентность и политика восстановления. Вызов банка не станет частью транзакции MongoDB.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/tx118-helper.php';
[$manager,$database]=tx118Environment();
$runtime=['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),'server'=>tx118Command($manager,'admin',['buildInfo'=>1])[0]['version']];
$prefix='mongodb118_txn_'.bin2hex(random_bytes(8));
$collections=[];$owned=[];$session=null;$sessionEnded=false;$details=[];
try {
foreach(['wallets', 'receipts'] as $role){
$name=$prefix.'_'.$role;
if(tx118Exists($manager,$database,$name)){throw new RuntimeException('Owned fixture collision');}
tx118Command($manager,$database,['create'=>$name],true);
$owned[]=$name;$collections[$role]=$name;
}
$wallets=$database.'.'.$collections['wallets'];$receipts=$database.'.'.$collections['receipts'];
tx118Insert($manager,$wallets,[['_id'=>'A','balance_kopecks'=>10000],['_id'=>'B','balance_kopecks'=>3000]]);
$session=tx118Session($manager);
$transfer=function(string $to,int $amount,string $receiptId) use($manager,$wallets,$receipts,$session): void {
if($amount<=0 || $amount>10000 || $to==='A'){throw new DomainException('Недопустимый перевод');}
$session->startTransaction();
$commitStarted=false;
try{
$debit=tx118Update($manager,$wallets,['_id'=>'A','balance_kopecks'=>['$gte'=>$amount]],['$inc'=>['balance_kopecks'=>-$amount]],$session);
if($debit->getMatchedCount()!==1){throw new DomainException('Недостаточно средств');}
$credit=tx118Update($manager,$wallets,['_id'=>$to],['$inc'=>['balance_kopecks'=>$amount]],$session);
if($credit->getMatchedCount()!==1){throw new DomainException('Получатель отсутствует');}
tx118Insert($manager,$receipts,[['_id'=>$receiptId,'from'=>'A','to'=>$to,'kopecks'=>$amount]],$session);
$commitStarted=true;$session->commitTransaction();
}catch(Throwable $failure){
if(!$commitStarted && $session->isInTransaction()){$session->abortTransaction();}
throw $failure;
}
};
$transfer('B',2500,'transfer-1');
$committed=$session->getTransactionState();
$afterSuccess=tx118Read($manager,$wallets,[],['projection'=>['_id'=>1,'balance_kopecks'=>1],'sort'=>['_id'=>1],'limit'=>10]);
$refused='';
try{$transfer('missing',1000,'transfer-2');}
catch(DomainException $failure){$refused=$failure->getMessage();}
$afterFailure=tx118Read($manager,$wallets,[],['projection'=>['_id'=>1,'balance_kopecks'=>1],'sort'=>['_id'=>1],'limit'=>10]);
$records=tx118Read($manager,$receipts,[],['projection'=>['_id'=>1],'limit'=>10]);
$rows=[['До перевода',10000,3000,13000,0],
['Переведено 2500',$afterSuccess[0]['balance_kopecks'],$afterSuccess[1]['balance_kopecks'],array_sum(array_column($afterSuccess,'balance_kopecks')),1],
['Отказ: получатель отсутствует',$afterFailure[0]['balance_kopecks'],$afterFailure[1]['balance_kopecks'],array_sum(array_column($afterFailure,'balance_kopecks')),count($records)]];
$checks=['success_balances'=>array_column($afterSuccess,'balance_kopecks')===[7500,5500],
'sum_preserved'=>array_sum(array_column($afterSuccess,'balance_kopecks'))===13000,
'success_committed'=>$committed==='committed','failed_recipient'=>$refused==='Получатель отсутствует',
'failed_debit_rolled_back'=>$afterSuccess===$afterFailure,
'only_one_receipt'=>array_column($records,'_id')===['transfer-1'],
'second_transaction_aborted'=>$session->getTransactionState()==='aborted'];
} finally {
try {
if($session!==null && !$sessionEnded){
try{if($session->isInTransaction()){$session->abortTransaction();}}
finally{$session->endSession();$sessionEnded=true;}
}
} finally {
foreach(array_reverse($owned) as $name){tx118Command($manager,$database,['drop'=>$name],true);}
}
}
$removed=true;foreach($owned as $name){$removed=$removed && !tx118Exists($manager,$database,$name);}
$checks['owned_collections_removed']=$removed;
$checks['session_closed']=$sessionEnded;
tx118Report('mongodb118-transactions-wallet-transfer', ['Операция', 'Кошелёк A, коп.', 'Кошелёк B, коп.', 'Всего, коп.', 'Квитанций'], $rows, $checks, $runtime, $details);
echo 'mongodb118-transactions-wallet-transfer: saved result.html and result.json
';
Ожидаемый результат
mongodb118-transactions-wallet-transfer: saved result.html and result.json
Запустите php example.php на отдельном локальном replica set и откройте result.html. В архиве есть tx118-helper.php и README с подготовкой сервера, расширения и переменных окружения. Программа создаёт и убирает только собственные учебные коллекции.
Открыть фактический результат HTML · Скачать результат JSON
Упражнение с ориентиром результата. Измените первую сумму на 1000: ожидаются A=9000, B=4000, общий итог 13000. Затем проверьте отказ суммы больше доступного баланса: обе стороны и квитанции должны остаться прежними.
Посмотреть предварительное изменение и закрыть Session без сохранения
Нужно разобраться, что происходит с незавершённой работой при закрытии Session. Предварительное изменение покажет состояние внутри транзакции и подтвердит отсутствие сохранения после endSession.
Входные данные
Остаток 8, предварительное уменьшение на 3. Session создаётся без defaultTransactionOptions; явные настройки передаются в startTransaction. Это короткая серверная демонстрация, не удержание корзины на время чтения страницы.
- none: транзакции нет, options=null
- startTransaction → starting
- Запись с Session → in_progress
- Внутри видно 5 вместо 8
- endSession отменяет незавершённую работу
- Новое внешнее чтение снова показывает 8
Короткое действие
$session->startTransaction($options);
$settings = $session->getTransactionOptions();
$state = $session->getTransactionState();
// Сделали предварительную запись в этой Session, посмотрели результат.
$session->endSession(); // Активная транзакция отменяется.
// Больше не обращаемся к закрытой Session; читаем результат отдельным запросом.Фрагмент выделяет действие внутри сценария. Объекты, namespace, данные и helper полностью подготовлены в самостоятельной программе ниже.
Что получится
| Шаг | getTransactionState | isInTransaction | maxCommitTimeMS | Остаток |
|---|---|---|---|---|
| До startTransaction | none | false | NULL | 8 |
| После startTransaction | starting | true | 1000 | 8 |
| Предварительное списание | in_progress | true | 1000 | 5 |
| После endSession | Session закрыта | не вызываем | не вызываем | 8 |
Почему получились эти значения
isInTransaction отвечает логическим true/false, getTransactionState — более подробной строкой состояния. До начала getTransactionOptions даёт null; после startTransaction — массив параметров, включающий объекты ReadConcern/ReadPreference/WriteConcern и число maxCommitTimeMS.
starting означает, что транзакция объявлена в Session, но ещё не выполнялась серверная операция. После записи состояние in_progress. Это полезная диагностика жизненного цикла, а не подсчёт запросов или пользовательских заказов.
endSession закрывает сессию и отменяет связанную незавершённую транзакцию. После него нельзя обращаться к другим методам этого Session. Поэтому последняя строка таблицы подписана «не вызываем», а итог читается без закрытого объекта.
Для обычной отмены бизнес-операции понятнее явный abortTransaction. endSession здесь нужен, чтобы изучить закрытие ресурса; сборка мусора тоже закрывает Session, но явный finally делает границу учебного кода заметной.
Границы и типичные ошибки
Не используйте длинную транзакцию как интерактивный предварительный просмотр для пользователя: она удерживает ресурсы. Для реального интерфейса сначала рассчитайте предложение чтением, а подтверждение оформляйте короткой условной записью/транзакцией. Строка локального состояния не доказывает серверный исход после неизвестного commit.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/tx118-helper.php';
[$manager,$database]=tx118Environment();
$runtime=['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),'server'=>tx118Command($manager,'admin',['buildInfo'=>1])[0]['version']];
$prefix='mongodb118_txn_'.bin2hex(random_bytes(8));
$collections=[];$owned=[];$session=null;$sessionEnded=false;$details=[];
try {
foreach(['stock'] as $role){
$name=$prefix.'_'.$role;
if(tx118Exists($manager,$database,$name)){throw new RuntimeException('Owned fixture collision');}
tx118Command($manager,$database,['create'=>$name],true);
$owned[]=$name;$collections[$role]=$name;
}
$stock=$database.'.'.$collections['stock'];
tx118Insert($manager,$stock,[['_id'=>'mug','available'=>8]]);
$session=$manager->startSession(['causalConsistency'=>false]);
$none=$session->getTransactionState();$noTransaction=!$session->isInTransaction();
$beforeOptions=$session->getTransactionOptions();
$session->startTransaction(tx118Options());
$starting=$session->getTransactionState();$currentOptions=$session->getTransactionOptions();
$active=$session->isInTransaction();
tx118Update($manager,$stock,['_id'=>'mug','available'=>['$gte'=>3]],['$inc'=>['available'=>-3]],$session);
$preview=tx118Read($manager,$stock,['_id'=>'mug'],['projection'=>['available'=>1],'limit'=>1],$session)[0]['available'];
$inProgress=$session->getTransactionState();
$session->endSession();$sessionEnded=true;
// После endSession методы этой Session больше не вызываем.
$outside=tx118Read($manager,$stock,['_id'=>'mug'],['projection'=>['available'=>1],'limit'=>1])[0]['available'];
$rows=[['До startTransaction',$none,'false','NULL',8],
['После startTransaction',$starting,$active?'true':'false',(string)$currentOptions['maxCommitTimeMS'],8],
['Предварительное списание',$inProgress,'true',(string)$currentOptions['maxCommitTimeMS'],$preview],
['После endSession','Session закрыта','не вызываем','не вызываем',$outside]];
$checks=['initial_state'=>$none==='none' && $noTransaction,'initial_options_null'=>$beforeOptions===null,
'starting_and_active'=>$starting==='starting' && $active,'in_progress_after_write'=>$inProgress==='in_progress',
'option_contract'=>$currentOptions['maxCommitTimeMS']===1000
&& $currentOptions['readConcern'] instanceof MongoDB\Driver\ReadConcern
&& $currentOptions['writeConcern'] instanceof MongoDB\Driver\WriteConcern
&& $currentOptions['readPreference'] instanceof MongoDB\Driver\ReadPreference,
'preview_changed'=>$preview===5,'end_session_aborted'=>$outside===8];
} finally {
try {
if($session!==null && !$sessionEnded){
try{if($session->isInTransaction()){$session->abortTransaction();}}
finally{$session->endSession();$sessionEnded=true;}
}
} finally {
foreach(array_reverse($owned) as $name){tx118Command($manager,$database,['drop'=>$name],true);}
}
}
$removed=true;foreach($owned as $name){$removed=$removed && !tx118Exists($manager,$database,$name);}
$checks['owned_collections_removed']=$removed;
$checks['session_closed']=$sessionEnded;
tx118Report('mongodb118-transactions-checkout-preview', ['Шаг', 'getTransactionState', 'isInTransaction', 'maxCommitTimeMS', 'Остаток'], $rows, $checks, $runtime, $details);
echo 'mongodb118-transactions-checkout-preview: saved result.html and result.json
';
Ожидаемый результат
mongodb118-transactions-checkout-preview: saved result.html and result.json
Запустите php example.php на отдельном локальном replica set и откройте result.html. В архиве есть tx118-helper.php и README с подготовкой сервера, расширения и переменных окружения. Программа создаёт и убирает только собственные учебные коллекции.
Открыть фактический результат HTML · Скачать результат JSON
Упражнение с ориентиром результата. Завершите предварительное изменение явным commit перед endSession: последнее внешнее чтение должно показать 5. При abort перед закрытием останется 8. Не вызывайте getTransactionState после endSession.
Практическая проблема: Is Possible Mongodb Transaction progress without Replica Set?. Один сервер ошибочно приравнивают к standalone без возможности транзакций. Инструкция различает одиночный процесс в replica set и standalone; первая учебная конфигурация не обещает отказоустойчивость. Вопрос задаёт ситуацию; правила сверены с первичными источниками, программа и данные урока оригинальные.
Повторить ключ запроса без второго списания товара
Клиент повторил запрос оформления, потому что не увидел ответ. Один и тот же ключ должен вернуть прежний заказ, а не списать ещё две кружки. Изменённое содержимое под старым ключом должно быть отклонено.
Входные данные
request-118-001 означает заказ двух кружек. Начальный остаток 5. Последовательно идут первый запрос, точный повтор и конфликтный повтор на три штуки.
- Уникальный _id = ключ запроса
- В транзакции ищем сохранённый результат
- Нет → заказ и списание, commit
- Повтор → сравниваем состав и возвращаем заказ
- Другой состав → отказ
- Остаток 3, один заказ
Короткое действие
// Ключ _id запроса уникален. В транзакции сначала ищем готовый результат.
if ($existingOrder !== []) {
// Сравнить sku/quantity с исходным запросом.
$session->abortTransaction(); // Ничего нового не записывали.
return $existingOrder[0];
}
// Иначе вставить заказ и списать остаток с той же Session, затем commit.
// Не вызывайте здесь реальный платёж: откат базы не отменит внешний сервис.Фрагмент выделяет действие внутри сценария. Объекты, namespace, данные и helper полностью подготовлены в самостоятельной программе ниже.
Что получится
| Попытка | Ответ | Заказ | Остаток |
|---|---|---|---|
| Первый запрос, 2 шт. | создан | order-4001 | 3 |
| Тот же ключ, 2 шт. | повтор: существующий заказ | order-4001 | 3 |
| Тот же ключ, 3 шт. | Ключ уже связан с другим составом | не создан | 3 |
Почему получились эти значения
Идемпотентность в этой задаче означает: повтор одной логической команды не повторяет её эффект. Ключ назначает клиент или ваш сервер по договору; он должен быть стабилен при повторе и связан с теми же разрешёнными параметрами.
Поле _id уже уникально. Оно используется как ключ запроса в orders. Сначала читается возможный результат, затем либо возвращается существующий order_id, либо в той же транзакции создаётся заказ и уменьшается остаток.
На точном повторе записей нет: транзакция чтения завершается abort, а ответ берётся из сохранённого документа. При quantity=3 сравнение обнаруживает, что старый ключ связан с другим составом, и не разрешает новую операцию.
Проверка наличия помогает последовательному повтору, но при двух одновременных первых запросах возможны конфликт записи/уникальности. Нельзя обойтись только поиском. Уникальный ключ остаётся последним правилом базы; после конфликта приложение должно разрешить исход отдельным чтением и ограниченной политикой повторов.
order-4001 — фиксированный учебный номер одного заказа, не генератор номеров для рабочего сервиса. Здесь нет потерянного сетевого ответа: повтор задаётся явно после успешного commit. Неизвестный commit не следует называть ни подтверждённым успехом, ни доказанным откатом.
Границы и типичные ошибки
Сохранённый ключ должен учитывать пользователя/арендатора и неизменный состав операции. Для внешней оплаты нужна согласованная идемпотентность провайдера и процесс доставки, например отдельная очередь/outbox; rollback базы не отменяет уже отправленный платёж или письмо. В этом примере внешних эффектов нет.
Полная самостоятельная программа и архив
<?php
declare(strict_types=1);
require __DIR__.'/tx118-helper.php';
[$manager,$database]=tx118Environment();
$runtime=['php'=>PHP_VERSION,'extension'=>phpversion('mongodb'),'server'=>tx118Command($manager,'admin',['buildInfo'=>1])[0]['version']];
$prefix='mongodb118_txn_'.bin2hex(random_bytes(8));
$collections=[];$owned=[];$session=null;$sessionEnded=false;$details=[];
try {
foreach(['stock', 'orders'] as $role){
$name=$prefix.'_'.$role;
if(tx118Exists($manager,$database,$name)){throw new RuntimeException('Owned fixture collision');}
tx118Command($manager,$database,['create'=>$name],true);
$owned[]=$name;$collections[$role]=$name;
}
$stock=$database.'.'.$collections['stock'];$orders=$database.'.'.$collections['orders'];
tx118Insert($manager,$stock,[['_id'=>'mug','available'=>5]]);
$session=tx118Session($manager);
$place=function(string $key,int $quantity) use($manager,$stock,$orders,$session): array {
if(!preg_match('/\Arequest-[A-Za-z0-9-]{1,40}\z/D',$key) || $quantity<1 || $quantity>10){throw new DomainException('Неверный запрос');}
$session->startTransaction();$commitStarted=false;
try{
$existing=tx118Read($manager,$orders,['_id'=>$key],['projection'=>['_id'=>1,'order_id'=>1,'sku'=>1,'quantity'=>1],'limit'=>1],$session);
if($existing!==[]){
if($existing[0]['sku']!=='mug' || $existing[0]['quantity']!==$quantity){throw new DomainException('Ключ уже связан с другим составом');}
$session->abortTransaction(); // В этой попытке записей нет.
return ['status'=>'повтор: существующий заказ','order_id'=>$existing[0]['order_id']];
}
// _id уникален: request key не может стать двумя заказами.
tx118Insert($manager,$orders,[['_id'=>$key,'order_id'=>'order-4001','sku'=>'mug','quantity'=>$quantity]],$session);
$debit=tx118Update($manager,$stock,['_id'=>'mug','available'=>['$gte'=>$quantity]],['$inc'=>['available'=>-$quantity]],$session);
if($debit->getMatchedCount()!==1){throw new DomainException('Недостаточно товара');}
$commitStarted=true;$session->commitTransaction();
return ['status'=>'создан','order_id'=>'order-4001'];
}catch(Throwable $failure){
if(!$commitStarted && $session->isInTransaction()){$session->abortTransaction();}
throw $failure;
}
};
$first=$place('request-118-001',2);
$firstStock=tx118Read($manager,$stock,['_id'=>'mug'],['projection'=>['available'=>1],'limit'=>1])[0]['available'];
$second=$place('request-118-001',2);
$secondStock=tx118Read($manager,$stock,['_id'=>'mug'],['projection'=>['available'=>1],'limit'=>1])[0]['available'];
$refused='';
try{$place('request-118-001',3);}catch(DomainException $failure){$refused=$failure->getMessage();}
$finalStock=tx118Read($manager,$stock,['_id'=>'mug'],['projection'=>['available'=>1],'limit'=>1])[0]['available'];
$saved=tx118Read($manager,$orders,[],['projection'=>['_id'=>1,'quantity'=>1],'limit'=>10]);
$rows=[['Первый запрос, 2 шт.',$first['status'],$first['order_id'],$firstStock],
['Тот же ключ, 2 шт.',$second['status'],$second['order_id'],$secondStock],
['Тот же ключ, 3 шт.',$refused,'не создан',$finalStock]];
$checks=['first_committed'=>$first['status']==='создан' && $firstStock===3,
'repeated_result'=>$second['status']==='повтор: существующий заказ' && $second['order_id']===$first['order_id'],
'no_second_debit'=>$secondStock===3 && $finalStock===3,
'payload_conflict'=>$refused==='Ключ уже связан с другим составом',
'single_order'=>count($saved)===1 && $saved[0]['_id']==='request-118-001' && $saved[0]['quantity']===2,
'no_active_after_resolution'=>!$session->isInTransaction()];
} finally {
try {
if($session!==null && !$sessionEnded){
try{if($session->isInTransaction()){$session->abortTransaction();}}
finally{$session->endSession();$sessionEnded=true;}
}
} finally {
foreach(array_reverse($owned) as $name){tx118Command($manager,$database,['drop'=>$name],true);}
}
}
$removed=true;foreach($owned as $name){$removed=$removed && !tx118Exists($manager,$database,$name);}
$checks['owned_collections_removed']=$removed;
$checks['session_closed']=$sessionEnded;
tx118Report('mongodb118-transactions-idempotent-order', ['Попытка', 'Ответ', 'Заказ', 'Остаток'], $rows, $checks, $runtime, $details);
echo 'mongodb118-transactions-idempotent-order: saved result.html and result.json
';
Ожидаемый результат
mongodb118-transactions-idempotent-order: saved result.html and result.json
Запустите php example.php на отдельном локальном replica set и откройте result.html. В архиве есть tx118-helper.php и README с подготовкой сервера, расширения и переменных окружения. Программа создаёт и убирает только собственные учебные коллекции.
Открыть фактический результат HTML · Скачать результат JSON
Упражнение с ориентиром результата. Повторите ключ ещё раз с quantity=2: получите тот же order-4001 и остаток 3. Затем используйте новый ключ и другой учебный order_id: это уже новый заказ, а не повтор прежнего.
Восемь методов Session: начало, результат и завершение
Карточки сохраняют параметры, возвращаемое значение, ошибки и версионные условия соответствующих страниц PHP.net. Каждый метод связан с двумя различными прикладными ситуациями из выполненных программ.
| Метод | Назначение |
|---|---|
| MongoDB\Driver\Manager::startSession | Создать логическую Session, связывающую последовательные операции одного Manager. |
| MongoDB\Driver\Session::startTransaction | Начать многодокументную транзакцию в имеющейся Session. |
| MongoDB\Driver\Session::commitTransaction | Подтвердить группу изменений и завершить транзакцию. |
| MongoDB\Driver\Session::abortTransaction | Завершить транзакцию без сохранения её изменений. |
| MongoDB\Driver\Session::isInTransaction | Проверить, считает ли Session транзакцию начатой и ещё не завершённой. |
| MongoDB\Driver\Session::endSession | Закрыть Session; незавершённая связанная транзакция отменяется. |
| MongoDB\Driver\Session::getTransactionState | Получить строковую фазу транзакции для диагностики жизненного цикла. |
| MongoDB\Driver\Session::getTransactionOptions | Прочитать настройки текущей транзакции, чтобы видеть применённый договор. |
Выбрать границу одной бизнес-операции
Выберите одно проверяемое бизнес-правило: заказ не существует без списания, перенос не меняет общую сумму, повтор ключа не повторяет эффект. Определите документы, выполняйте нужные условные операции с одной Session и проверяйте их результат до commit. Получайте из базы конкретный документ или нужные поля, а не весь каталог для последующего поиска в PHP.
Продолжение: согласованность чтения и подтверждение записи, условная атомарная запись одного документа, индексы под условия и уникальность и корректные типы идентификатора, даты и суммы.
Эта глава раскрывает восемь перечисленных методов и шесть ограниченных сценариев. Полные стратегии повторов с deadline, восстановление неизвестного исхода, конкурентные первые запросы, outbox, sharded transactions, переключение primary, серверные лимиты большой нагрузки и остальные методы Session требуют отдельных разделов. Один работающий checkout не означает, что все эти темы уже объяснены или проверены.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
MongoDB\Driver\Manager::startSessionСоздать логическую Session, связывающую последовательные операции одного Manager.
Метод с mongodb 1.4.0; defaultTransactionOptions добавлен в 1.5.0, его maxCommitTimeMS — в 1.6.0, snapshot — в 1.11.0. · mongodb
final public MongoDB\Driver\Manager::startSession(?array $options = null): MongoDB\Driver\SessionСоздать логическую Session, связывающую последовательные операции одного Manager.
final public MongoDB\Driver\Manager::startSession(?array $options = null): MongoDB\Driver\SessionПараметры
| Аргумент или опция | Смысл |
|---|---|
| causalConsistency: bool | Причинный порядок операций в Session; default true. Сам по себе не превращает операции в транзакцию. |
| defaultTransactionOptions: array | Параметры новых транзакций по умолчанию; default []. Конкретные значения переопределяются в startTransaction. Доступно с сервером MongoDB 4.0+. |
| defaultTransactionOptions.maxCommitTimeMS: int | Максимальное время одной команды commit в миллисекундах. По документации — неотрицательное знаковое 32-битное целое; 0 не задаёт конечного серверного предела. Это не общий бюджет всей транзакции. |
| defaultTransactionOptions.readConcern: MongoDB\Driver\ReadConcern | Уровень чтения транзакции. Сам readConcern доступен с MongoDB 3.2; неподдерживаемая версия вызывает ошибку при выполнении. Для рассматриваемых транзакций нужен replica set MongoDB 4.0+; в архиве 8.0. |
| defaultTransactionOptions.readPreference: MongoDB\Driver\ReadPreference | Выбор сервера. Транзакция с чтениями использует primary; не отправляйте её отдельные чтения на secondary. |
| defaultTransactionOptions.writeConcern: MongoDB\Driver\WriteConcern | Требование подтверждения commit. Задаётся на транзакции, не на каждой её внутренней записи. |
| snapshot: bool | Default false. С MongoDB 5.0+ первый поддерживаемый find/aggregate/unsharded distinct получает timestamp, последующие чтения используют snapshot этого момента. Нельзя сочетать с транзакциями, записью или causalConsistency=true. При snapshot=true causalConsistency по умолчанию становится false. |
Результат
MongoDB\Driver\Session. Её можно использовать только с тем Manager, который её создал; объект передаётся опцией session при командах, чтениях и записях.
Ошибки и границы
InvalidArgumentException: разбор аргументов либо causalConsistency=true вместе со snapshot=true. RuntimeException: Session не удалось создать, например нет требуемой поддержки crypto в libmongoc. Session отличается от PHP session_start; создание Session не сохраняет заказ.
Версии
Метод с mongodb 1.4.0; defaultTransactionOptions добавлен в 1.5.0, его maxCommitTimeMS — в 1.6.0, snapshot — в 1.11.0.
Применение 1: Настроить общую работу заказа и склада
$session = $manager->startSession([
'causalConsistency' => false,
'defaultTransactionOptions' => tx118Options(),
]);Два namespace получают один объект Session. У startTransaction наследуется maxCommitTimeMS=1000; заказ и остаток становятся видимыми после общего commit.
Пояснение и полная программа · Фактическая таблица результата
Применение 2: Открыть Session для короткого предварительного изменения
$session = $manager->startSession(['causalConsistency' => false]);
$session->startTransaction(tx118Options());Сначала транзакции нет, затем явные настройки начинают предварительную работу. Закрытие Session отменяет изменение остатка 8→5.
Пояснение и полная программа · Фактическая таблица результата
Короткие фрагменты показывают вызов в контексте связанных программ. Manager, Session, данные и helper подготовлены в полном архиве. Каждый архив запускается самостоятельно; фрагменты не выданы за отдельные программы.
MongoDB\Driver\Session::startTransactionНачать многодокументную транзакцию в имеющейся Session.
С mongodb 1.5.0; maxCommitTimeMS добавлен в 1.6.0. Многодокументные транзакции появились в MongoDB 4.0 для replica set, в 4.2 для sharded cluster; standalone их не поддерживает. · mongodb
final public MongoDB\Driver\Session::startTransaction(?array $options = null): voidНачать многодокументную транзакцию в имеющейся Session.
final public MongoDB\Driver\Session::startTransaction(?array $options = null): voidПараметры
| Аргумент или опция | Смысл |
|---|---|
| options: ?array | Каждый переданный параметр заменяет одноимённый defaultTransactionOptions этой Session. В одной Session допускается только одна открытая транзакция. |
| maxCommitTimeMS: int | Максимальное время одной команды commit в миллисекундах. По документации — неотрицательное знаковое 32-битное целое; 0 не задаёт конечного серверного предела. Это не общий бюджет всей транзакции. |
| readConcern: MongoDB\Driver\ReadConcern | Уровень чтения транзакции. Сам readConcern доступен с MongoDB 3.2; неподдерживаемая версия вызывает ошибку при выполнении. Для рассматриваемых транзакций нужен replica set MongoDB 4.0+; в архиве 8.0. |
| readPreference: MongoDB\Driver\ReadPreference | Выбор сервера. Транзакция с чтениями использует primary; не отправляйте её отдельные чтения на secondary. |
| writeConcern: MongoDB\Driver\WriteConcern | Требование подтверждения commit. Задаётся на транзакции, не на каждой её внутренней записи. |
Результат
Возвращаемого значения нет. Операции входят в транзакцию только при явной передаче session. Завершение — commitTransaction или abortTransaction; закрытие Session также отменяет незавершённую работу.
Ошибки и границы
InvalidArgumentException при разборе аргументов; CommandException при серверном отказе начать работу, например проблеме получения блокировки; RuntimeException при невозможном запуске, например транзакция уже начата. Ошибка может проявиться на первой серверной операции: starting не доказывает выполненную запись.
Версии
С mongodb 1.5.0; maxCommitTimeMS добавлен в 1.6.0. Многодокументные транзакции появились в MongoDB 4.0 для replica set, в 4.2 для sharded cluster; standalone их не поддерживает.
Применение 1: Объединить две коллекции
$session->startTransaction();
// Далее обе операции получают ['session' => $session].После объявленного starting запись переводит Session в in_progress. Снаружи до commit остаются 5 кружек и 0 заказов.
Пояснение и полная программа · Фактическая таблица результата
Применение 2: Открыть новую попытку переноса копеек
$session->startTransaction();
// Условное списание, зачисление и квитанция — одна группа.Первая транзакция сохраняет 7500/5500 и квитанцию. Новая попытка в той же Session с отсутствующим получателем отменяется.
Пояснение и полная программа · Фактическая таблица результата
Короткие фрагменты показывают вызов в контексте связанных программ. Manager, Session, данные и helper подготовлены в полном архиве. Каждый архив запускается самостоятельно; фрагменты не выданы за отдельные программы.
MongoDB\Driver\Session::commitTransactionПодтвердить группу изменений и завершить транзакцию.
С mongodb 1.5.0. При повторе commit драйвер применяет свои правила подтверждения; retryWrites=false не означает отключение внутренних повторов commit. Сетевой отказ и failover в пакете не имитируются. · mongodb
final public MongoDB\Driver\Session::commitTransaction(): voidПодтвердить группу изменений и завершить транзакцию.
final public MongoDB\Driver\Session::commitTransaction(): voidПараметры
Параметров нет. Метод вызывается у Session, созданной соответствующим Manager.
Результат
Возвращаемого значения нет. Успешное завершение сохраняет изменения группы; до commit её записи не видны вне транзакции.
Ошибки и границы
InvalidArgumentException при разборе аргументов; CommandException при серверном отказе commit, конфликте/проблеме сети; RuntimeException при невозможном состоянии, например транзакция не начата. Метки ошибок проверяют через hasErrorLabel, не по тексту. Важное уточнение по MongoDB Core API: TransientTransactionError допускает ограниченный повтор всего тела, UnknownTransactionCommitResult требует разрешать исход и повторять commit той же транзакции, а не безусловно снова переводить деньги. Неизвестный результат не доказывает rollback. В этих программах автоматического повтора нет.
Версии
С mongodb 1.5.0. При повторе commit драйвер применяет свои правила подтверждения; retryWrites=false не означает отключение внутренних повторов commit. Сетевой отказ и failover в пакете не имитируются.
Применение 1: Сохранить заказ вместе с остатком
$session->commitTransaction();Внешнее чтение теперь показывает один order-118 и остаток 3. До подтверждения внешний запрос видел 0 и 5.
Пояснение и полная программа · Фактическая таблица результата
Применение 2: Закончить перенос между двумя документами
$commitStarted = true;
$session->commitTransaction();Только после выполненных списания, зачисления и квитанции подтверждается перенос. Сумма двух учебных балансов осталась 13000 копеек.
Пояснение и полная программа · Фактическая таблица результата
Короткие фрагменты показывают вызов в контексте связанных программ. Manager, Session, данные и helper подготовлены в полном архиве. Каждый архив запускается самостоятельно; фрагменты не выданы за отдельные программы.
MongoDB\Driver\Session::abortTransactionЗавершить транзакцию без сохранения её изменений.
С mongodb 1.5.0. · mongodb
final public MongoDB\Driver\Session::abortTransaction(): voidЗавершить транзакцию без сохранения её изменений.
final public MongoDB\Driver\Session::abortTransaction(): voidПараметры
Параметров нет. Метод вызывается у Session, созданной соответствующим Manager.
Результат
Возвращаемого значения нет; изменения операций этой транзакции отменяются. Отдельные операции без её Session и внешние HTTP-эффекты не входят в эту группу.
Ошибки и границы
InvalidArgumentException при разборе аргументов; RuntimeException, если отмена невозможна, например транзакция не была начата. Нельзя вызывать abort после подтверждённого commit как «отмену заказа»: для этого нужна новая бизнес-операция. Ошибка сервера может уже завершить транзакцию; ошибка commit не всегда означает доказанную отмену.
Версии
С mongodb 1.5.0.
Применение 1: Убрать черновик при нехватке товара
$session->abortTransaction();Черновик был виден внутри Session, но после отказа остались 1 кружка и 0 заказов. Нулевой matchedCount обработан приложением.
Пояснение и полная программа · Фактическая таблица результата
Применение 2: Завершить чтение при точном повторе запроса
if ($existing !== []) {
$session->abortTransaction();
return ['order_id' => $existing[0]['order_id']];
}Повторная попытка ничего не записывала: возвращён order-4001, остаток остался 3. Этот abort завершает новую читающую транзакцию, а не отменяет прежний заказ.
Пояснение и полная программа · Фактическая таблица результата
Короткие фрагменты показывают вызов в контексте связанных программ. Manager, Session, данные и helper подготовлены в полном архиве. Каждый архив запускается самостоятельно; фрагменты не выданы за отдельные программы.
MongoDB\Driver\Session::isInTransactionПроверить, считает ли Session транзакцию начатой и ещё не завершённой.
С mongodb 1.6.0. · mongodb
final public MongoDB\Driver\Session::isInTransaction(): boolПроверить, считает ли Session транзакцию начатой и ещё не завершённой.
final public MongoDB\Driver\Session::isInTransaction(): boolПараметры
Параметров нет. Метод вызывается у Session, созданной соответствующим Manager.
Результат
true после начала до commit/abort, false в остальных случаях. Более подробную фазу даёт getTransactionState.
Ошибки и границы
Документация указывает InvalidArgumentException при разборе аргументов. Это состояние объекта драйвера, не запрос аудита к серверу и не доказательство конечного исхода потерянного commit. После endSession объект больше не используют.
Версии
С mongodb 1.6.0.
Применение 1: Отметить границу подтверждения
$active = $session->isInTransaction();
$session->commitTransaction();
$after = $session->isInTransaction();Во время работы active=true, после успешного commit — false. Видимость заказа проверяется отдельным чтением.
Пояснение и полная программа · Фактическая таблица результата
Применение 2: Завершить состояние после отказа уникальности
if ($session->isInTransaction()) {
$session->abortTransaction();
}Ожидаемый код 11000 не проглатывается как успех. После обработки исходный заказ 900 остаётся единственным, остаток возвращён к 5.
Пояснение и полная программа · Фактическая таблица результата
Короткие фрагменты показывают вызов в контексте связанных программ. Manager, Session, данные и helper подготовлены в полном архиве. Каждый архив запускается самостоятельно; фрагменты не выданы за отдельные программы.
MongoDB\Driver\Session::endSessionЗакрыть Session; незавершённая связанная транзакция отменяется.
С mongodb 1.5.0. · mongodb
final public MongoDB\Driver\Session::endSession(): voidЗакрыть Session; незавершённая связанная транзакция отменяется.
final public MongoDB\Driver\Session::endSession(): voidПараметры
Параметров нет. Метод вызывается у Session, созданной соответствующим Manager.
Результат
Возвращаемого значения нет. Сессия закрыта, после этого другие методы данного Session вызывать нельзя.
Ошибки и границы
Документация указывает InvalidArgumentException при разборе аргументов. Session закрывается и при сборке мусора, поэтому в обычной ситуации явный вызов не обязателен. Здесь он служит понятной границей finally. Закрытие не отменяет уже подтверждённый заказ и не отменяет внешнюю оплату.
Версии
С mongodb 1.5.0.
Применение 1: Освободить Session после завершения работы
try {
// Операции и успешный commit.
} finally {
$session->endSession();
}После commit сохраняются заказ и остаток; закрытие Session завершает ресурс. В полном архиве наконец удаляются только учебные коллекции.
Пояснение и полная программа · Фактическая таблица результата
Применение 2: Отменить незавершённое предварительное изменение
$session->endSession();
$sessionEnded = true;Внутри было 5 кружек, после закрытия внешнее чтение видит исходные 8. Методы закрытой Session не вызываются.
Пояснение и полная программа · Фактическая таблица результата
Короткие фрагменты показывают вызов в контексте связанных программ. Manager, Session, данные и helper подготовлены в полном архиве. Каждый архив запускается самостоятельно; фрагменты не выданы за отдельные программы.
MongoDB\Driver\Session::getTransactionStateПолучить строковую фазу транзакции для диагностики жизненного цикла.
С mongodb 1.7.0. Написание состояний дополнительно сверено с исходником расширения 2.5.5; не путайте их с кодами ошибок сервера. · mongodb
final public MongoDB\Driver\Session::getTransactionState(): stringПолучить строковую фазу транзакции для диагностики жизненного цикла.
final public MongoDB\Driver\Session::getTransactionState(): stringПараметры
Параметров нет. Метод вызывается у Session, созданной соответствующим Manager.
Результат
Строка текущего состояния. В используемом драйвере 2.5.5: none, starting, in_progress, committed, aborted. Это диагностическая фаза Session; исход при потерянном подтверждении нужно разрешать отдельно.
Ошибки и границы
Документация указывает InvalidArgumentException при разборе аргументов. Не подменяйте этим методом проверку бизнес-результата в документах. starting предшествует первой связанной серверной операции; getTransactionOptions раскрывает настройки, isInTransaction — логическую активность.
Версии
С mongodb 1.7.0. Написание состояний дополнительно сверено с исходником расширения 2.5.5; не путайте их с кодами ошибок сервера.
Применение 1: Увидеть продвижение подтверждённого заказа
$phase = $session->getTransactionState();В программе последовательно получены none, starting, in_progress и committed. Таблица связывает фазы с видимостью остатка и заказа.
Пояснение и полная программа · Фактическая таблица результата
Применение 2: Отследить предварительную запись
$phase = $session->getTransactionState();До операции starting, после записи in_progress. После endSession чтение состояния намеренно не выполняется, вместо этого проверяется фактический остаток.
Пояснение и полная программа · Фактическая таблица результата
Короткие фрагменты показывают вызов в контексте связанных программ. Manager, Session, данные и helper подготовлены в полном архиве. Каждый архив запускается самостоятельно; фрагменты не выданы за отдельные программы.
MongoDB\Driver\Session::getTransactionOptionsПрочитать настройки текущей транзакции, чтобы видеть применённый договор.
С mongodb 1.7.0. · mongodb
final public MongoDB\Driver\Session::getTransactionOptions(): ?arrayПрочитать настройки текущей транзакции, чтобы видеть применённый договор.
final public MongoDB\Driver\Session::getTransactionOptions(): ?arrayПараметры
Параметров нет. Метод вызывается у Session, созданной соответствующим Manager.
Результат
Массив настроек текущей транзакции; если транзакции нет, возвращается null. Значения readConcern/readPreference/writeConcern представлены соответствующими объектами, maxCommitTimeMS — числом. Набор включает применённые параметры, поэтому не обращайтесь к произвольному ключу без проверки.
Ошибки и границы
Документация указывает InvalidArgumentException при разборе аргументов. Возвращённый массив предназначен для чтения: изменение его PHP-элемента не меняет уже работающую транзакцию. Настройки выбирают при startTransaction/defaultTransactionOptions; диагностика не создаёт новую транзакцию.
Версии
С mongodb 1.7.0.
Применение 1: Проверить унаследованный предел commit
$options = $session->getTransactionOptions();
$maxCommitMs = $options['maxCommitTimeMS'];После startTransaction без аргумента получено 1000 из defaultTransactionOptions. Проверка не выдаёт его за лимит всей операции заказа.
Пояснение и полная программа · Фактическая таблица результата
Применение 2: Сравнить отсутствие транзакции и явные настройки
$before = $session->getTransactionOptions(); // null
$session->startTransaction(tx118Options());
$current = $session->getTransactionOptions();Перед началом null; после начала есть 1000 и объекты трёх политик. Таблица показывает изменение вместе с состоянием Session.
Пояснение и полная программа · Фактическая таблица результата
Короткие фрагменты показывают вызов в контексте связанных программ. Manager, Session, данные и helper подготовлены в полном архиве. Каждый архив запускается самостоятельно; фрагменты не выданы за отдельные программы.
Источники и обсуждения
- MongoDB\Driver\Manager::startSession
- MongoDB\Driver\Session::startTransaction
- MongoDB\Driver\Session::commitTransaction
- MongoDB\Driver\Session::abortTransaction
- MongoDB\Driver\Session::isInTransaction
- MongoDB\Driver\Session::endSession
- MongoDB\Driver\Session::getTransactionState
- MongoDB\Driver\Session::getTransactionOptions
- MongoDB 8.0: транзакции и политика повторов
- MongoDB: многодокументные транзакции
- Учебный replica set
- PHP extension 2.5.5: состояния Session
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


