14. MongoDB: Транзакции
Multi-document транзакции в MongoDB (начиная с 4.0 для replica sets, 4.2 для sharded clusters) позволяют выполнять ACID операции над несколькими документами и коллекциями.
ACID свойства
Заголовок раздела «ACID свойства»graph LR A[ACID] --> B[Atomicity - всё или ничего] A --> C[Consistency - данные всегда валидны] A --> D[Isolation - транзакции изолированы] A --> E[Durability - данные сохранены]Когда нужны транзакции?
Заголовок раздела «Когда нужны транзакции?»✅ Используйте транзакции:
- Банковские переводы (снятие с одного счёта, пополнение другого)
- Инвентаризация (резервирование товара + создание заказа)
- Сложные бизнес-операции с несколькими изменениями
❌ НЕ нужны транзакции:
- Операции над одним документом (уже atomic!)
- Простые CRUD операции
- Когда eventual consistency приемлема
Базовый пример транзакции
Заголовок раздела «Базовый пример транзакции»const session = client.startSession();
try { await session.withTransaction(async () => { const accounts = db.collection('accounts');
// Списание с первого счёта await accounts.updateOne( { _id: 'account1' }, { $inc: { balance: -100 } }, { session } );
// Пополнение второго счёта await accounts.updateOne( { _id: 'account2' }, { $inc: { balance: 100 } }, { session } ); });
console.log('Transaction committed');} catch (error) { console.error('Transaction aborted:', error);} finally { await session.endSession();}TypeScript примеры
Заголовок раздела «TypeScript примеры»import { MongoClient, ClientSession } from 'mongodb';
const client = new MongoClient('mongodb://localhost:27017/?replicaSet=rs0');await client.connect();const db = client.db('bank');
// Банковский переводasync function transferMoney( fromAccount: string, toAccount: string, amount: number) { const session = client.startSession();
try { const result = await session.withTransaction(async () => { const accounts = db.collection('accounts');
// Проверка баланса const from = await accounts.findOne({ _id: fromAccount }, { session }); if (!from || from.balance < amount) { throw new Error('Insufficient funds'); }
// Списание await accounts.updateOne( { _id: fromAccount }, { $inc: { balance: -amount } }, { session } );
// Пополнение await accounts.updateOne( { _id: toAccount }, { $inc: { balance: amount } }, { session } );
// Запись в журнал await db.collection('transactions').insertOne({ from: fromAccount, to: toAccount, amount, timestamp: new Date() }, { session });
return { success: true }; });
return result; } finally { await session.endSession(); }}
// E-commerce: создание заказа с резервированием товараasync function createOrder(userId: string, items: Array<{productId: string, quantity: number}>) { const session = client.startSession();
try { const result = await session.withTransaction(async () => { const products = db.collection('products'); const orders = db.collection('orders');
// Проверка и резервирование товаров for (const item of items) { const product = await products.findOne({ _id: item.productId }, { session });
if (!product || product.stock < item.quantity) { throw new Error(`Insufficient stock for ${item.productId}`); }
await products.updateOne( { _id: item.productId }, { $inc: { stock: -item.quantity } }, { session } ); }
// Создание заказа const order = await orders.insertOne({ userId, items, status: 'pending', createdAt: new Date() }, { session });
return order.insertedId; });
return result; } catch (error) { console.error('Order creation failed:', error); throw error; } finally { await session.endSession(); }}Ручное управление транзакцией
Заголовок раздела «Ручное управление транзакцией»async function manualTransaction() { const session = client.startSession();
try { // Начало транзакции session.startTransaction({ readConcern: { level: 'snapshot' }, writeConcern: { w: 'majority' }, readPreference: 'primary' });
// Операции await db.collection('accounts').updateOne( { _id: 'acc1' }, { $inc: { balance: -100 } }, { session } );
await db.collection('accounts').updateOne( { _id: 'acc2' }, { $inc: { balance: 100 } }, { session } );
// Commit await session.commitTransaction(); console.log('Transaction committed'); } catch (error) { // Rollback await session.abortTransaction(); console.error('Transaction aborted:', error); } finally { await session.endSession(); }}Mongoose транзакции
Заголовок раздела «Mongoose транзакции»import mongoose from 'mongoose';
await mongoose.connect('mongodb://localhost:27017/bank?replicaSet=rs0');
const Account = mongoose.model('Account', new mongoose.Schema({ owner: String, balance: Number}));
async function transfer(fromId: string, toId: string, amount: number) { const session = await mongoose.startSession();
try { const result = await session.withTransaction(async () => { // Списание const from = await Account.findByIdAndUpdate( fromId, { $inc: { balance: -amount } }, { session, new: true } );
if (!from || from.balance < 0) { throw new Error('Insufficient funds'); }
// Пополнение await Account.findByIdAndUpdate( toId, { $inc: { balance: amount } }, { session } );
return { success: true }; });
return result; } finally { await session.endSession(); }}Настройки транзакций
Заголовок раздела «Настройки транзакций»session.startTransaction({ // Read Concern readConcern: { level: 'snapshot' }, // Консистентный снимок данных
// Write Concern writeConcern: { w: 'majority', wtimeout: 5000 }, // Подтверждение от majority
// Read Preference readPreference: 'primary', // Читать только с Primary
// Max Commit Time maxCommitTimeMS: 30000 // Таймаут 30 сек});Ограничения транзакций
Заголовок раздела «Ограничения транзакций»-
Только в Replica Sets / Sharded Clusters
Окно терминала # Нельзя в standalone MongoDB -
16MB лимит на все операции в транзакции
-
Не более 1000 документов в одной транзакции (рекомендация)
-
DDL операции запрещены:
- CREATE/DROP collection
- CREATE/DROP index
-
60 секунд по умолчанию для транзакции
Производительность
Заголовок раздела «Производительность»// ❌ Плохо: долгая транзакцияasync function badTransaction() { const session = client.startSession(); await session.withTransaction(async () => { // Множество операций for (let i = 0; i < 10000; i++) { await db.collection('items').insertOne({ index: i }, { session }); } // Долгий external API call await fetch('https://external-api.com/verify'); });}
// ✅ Хорошо: короткая транзакцияasync function goodTransaction() { // External calls ВНЕ транзакции const verified = await fetch('https://external-api.com/verify'); if (!verified) return;
const session = client.startSession(); await session.withTransaction(async () => { // Только критичные DB операции await db.collection('accounts').updateOne(..., { session }); await db.collection('logs').insertOne(..., { session }); });}Retry логика
Заголовок раздела «Retry логика»async function transferWithRetry(from: string, to: string, amount: number, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { return await transferMoney(from, to, amount); } catch (error) { if (error.hasErrorLabel('TransientTransactionError') && attempt < maxRetries) { console.log(`Retrying transaction (attempt ${attempt + 1})`); await new Promise(resolve => setTimeout(resolve, 100 * attempt)); continue; } throw error; } }}💡 Best Practices
Заголовок раздела «💡 Best Practices»- Держите транзакции короткими - только критичные операции
- Используйте withTransaction() вместо ручного управления
- Обрабатывайте ошибки и делайте retry для transient errors
- Избегайте external calls внутри транзакций
- Мониторьте длительность и failures
⚠️ Частые ошибки
Заголовок раздела «⚠️ Частые ошибки»- Забывают передать
{ session }в операции - Долгие транзакции (блокируют другие операции)
- Используют транзакции там, где достаточно atomic операций
- Не обрабатывают transient errors
Следующий урок: Schema Design в MongoDB →