Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Опыт с транзакциями в Node.js
Да, регулярно работаю с транзакциями в Node.js приложениях. Это критично для обеспечения консистентности данных при сложных операциях.
Реальные сценарии использования
1. Переводы денег между счётами
Это классический пример, где нужна ACID гарантия:
await prisma.$transaction(async (tx) => {
// Уменьшаем баланс отправителя
await tx.account.update({
where: { id: senderId },
data: { balance: { decrement: amount } },
});
// Увеличиваем баланс получателя
await tx.account.update({
where: { id: recipientId },
data: { balance: { increment: amount } },
});
// Записываем транзакцию в лог
await tx.transaction.create({
data: {
senderId,
recipientId,
amount,
status: 'completed',
},
});
});
Если произойдёт ошибка в середине — всё откатится (rollback), и деньги не исчезнут.
2. Создание заказа с товарами
Нужно атомарно создать заказ и резервировать товары:
await prisma.$transaction(async (tx) => {
// Создаём заказ
const order = await tx.order.create({
data: {
userId,
totalPrice: totalAmount,
},
});
// Резервируем товары
for (const item of items) {
await tx.inventory.update({
where: { productId: item.productId },
data: { reserved: { increment: item.quantity } },
});
// Добавляем товары в заказ
await tx.orderItem.create({
data: {
orderId: order.id,
productId: item.productId,
quantity: item.quantity,
},
});
}
return order;
});
3. Работа с raw SQL транзакциями
const result = await prisma.$transaction(async (tx) => {
// Используем raw SQL в транзакции
const [user] = await tx.$queryRaw`
SELECT * FROM users WHERE id = ${userId} FOR UPDATE
`;
if (user.balance < amount) {
throw new Error('Insufficient balance');
}
await tx.$executeRaw`
UPDATE users SET balance = balance - ${amount} WHERE id = ${userId}
`;
await tx.$executeRaw`
INSERT INTO transactions (user_id, amount) VALUES (${userId}, ${amount})
`;
return { success: true };
});
Важные концепции, которые узнал
1. ACID свойства
- A (Atomicity) — либо всё, либо ничего. Нет половинок.
- C (Consistency) — данные остаются в корректном состоянии
- I (Isolation) — параллельные транзакции не мешают друг другу
- D (Durability) — после commit данные на диске
2. Уровни изоляции
// Разные БД поддерживают разные уровни
// PostgreSQL:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
// READ UNCOMMITTED — самый слабый
// READ COMMITTED — дефолт в большинстве
// REPEATABLE READ
// SERIALIZABLE — самый сильный, но медленнее
3. SELECT FOR UPDATE (пессимистичная блокировка)
Когда нужна гарантия, что никто не изменит строку, пока мы работаем:
await prisma.$transaction(async (tx) => {
const [account] = await tx.$queryRaw`
SELECT * FROM accounts WHERE id = ${accountId} FOR UPDATE
`;
// Теперь никто не может изменить эту строку
// пока не завершится транзакция
await tx.account.update({
where: { id: accountId },
data: { balance: account.balance - amount },
});
});
4. Обработка race conditions
try {
await prisma.$transaction(async (tx) => {
const product = await tx.product.findUnique({
where: { id: productId },
});
if (product.stock < quantity) {
throw new Error('Out of stock');
}
await tx.product.update({
where: { id: productId },
data: { stock: { decrement: quantity } },
});
});
} catch (error) {
// Транзакция откатилась
console.error('Transaction failed:', error);
}
Ошибки, которые делал и исправил
❌ Ошибка: Слишком долгие транзакции
// Плохо — транзакция ждёт внешнего API
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({ ... });
const response = await fetchExternalAPI(); // 5 сек ожидания!
await tx.user.update({ ... });
});
✅ Исправление: Внешние операции вне транзакции
// Хорошо
const user = await prisma.user.create({ ... });
try {
const response = await fetchExternalAPI();
await prisma.$transaction(async (tx) => {
await tx.user.update({ ... });
});
} catch (error) {
// Откатываем создание юзера при необходимости
await prisma.user.delete({ where: { id: user.id } });
}
❌ Ошибка: Вложенные транзакции (Savepoints)
// Не все БД хорошо поддерживают
await prisma.$transaction(async (tx1) => {
await tx1.user.create({ ... });
await prisma.$transaction(async (tx2) => { // Может не работать!
await tx2.post.create({ ... });
});
});
Когда использовать транзакции
✅ Деньги и платежи
✅ Запасы/инвентарь
✅ Сложные бизнес-операции с несколькими шагами
✅ Когда нужна гарантия консистентности
Когда транзакции не нужны
❌ Простые CREATE/UPDATE операции
❌ Чтение данных (SELECT)
❌ Когда можно использовать eventual consistency
Best Practices
- Держи транзакции короткими — меньше времени, меньше блокировок
- Избегай вложенности — сложность растёт экспоненциально
- Обрабатывай ошибки — транзакция может откатиться неожиданно
- Логируй попытки и откаты — помогает отладке
- Мониторь deadlock'и — когда две транзакции ждут друг друга
- Тестируй сценарии отката — убедись, что откат работает
Инструменты для отладки
-- Вижу текущие транзакции в PostgreSQL
SELECT * FROM pg_stat_activity;
-- Проверяю блокировки
SELECT * FROM pg_locks;
-- Убиваю зависшую транзакцию
SELECT pg_terminate_backend(pid);
Вывод
Транзакции — это не опционально, это основа надёжности приложений, работающих с данными. Я постоянно использую их при разработке платёжных систем, работе с инвентарём и сложными бизнес-операциями. Главное — понимать, когда их использовать и как правильно их структурировать.