Ответ
В Node.js приложениях я реализую блокировки на уровне БД в зависимости от конкретной СУБД и сценария использования. Вот мои практические подходы:
1. PostgreSQL - пессимистичные блокировки:
const { Pool } = require('pg');
const pool = new Pool();
async function transferFundsWithLock(fromAccountId, toAccountId, amount) {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Блокировка обеих записей для обновления
const fromAccount = await client.query(
'SELECT * FROM accounts WHERE id = $1 FOR UPDATE',
[fromAccountId]
);
const toAccount = await client.query(
'SELECT * FROM accounts WHERE id = $1 FOR UPDATE',
[toAccountId]
);
if (fromAccount.rows[0].balance < amount) {
throw new Error('Insufficient funds');
}
// Выполнение операций
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[amount, fromAccountId]
);
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, toAccountId]
);
// Логирование транзакции
await client.query(
`INSERT INTO transactions (from_account, to_account, amount, status)
VALUES ($1, $2, $3, 'completed')`,
[fromAccountId, toAccountId, amount]
);
await client.query('COMMIT');
return { success: true };
} catch (error) {
await client.query('ROLLBACK');
console.error('Transfer failed:', error);
throw error;
} finally {
client.release();
}
}
2. MongoDB - оптимистичные блокировки:
const { MongoClient } = require('mongodb');
async function updateProductStock(productId, quantityChange) {
const client = await MongoClient.connect(process.env.MONGODB_URI);
const db = client.db('ecommerce');
let retries = 3;
while (retries > 0) {
const product = await db.collection('products').findOne({ _id: productId });
if (!product) {
throw new Error('Product not found');
}
const newStock = product.stock + quantityChange;
if (newStock < 0) {
throw new Error('Insufficient stock');
}
// Оптимистичная блокировка через version
const result = await db.collection('products').updateOne(
{
_id: productId,
version: product.version // Проверяем, что версия не изменилась
},
{
$set: {
stock: newStock,
updatedAt: new Date()
},
$inc: { version: 1 }
}
);
if (result.modifiedCount === 1) {
await client.close();
return { success: true, newStock };
}
retries--;
await new Promise(resolve => setTimeout(resolve, 100)); // Задержка перед повторной попыткой
}
await client.close();
throw new Error('Failed to update product after retries');
}
3. Redis - распределённые блокировки:
const Redis = require('ioredis');
const redis = new Redis();
async function acquireDistributedLock(lockKey, ttl = 10000) {
const lockValue = `${Date.now()}-${Math.random()}`;
// Пытаемся получить блокировку
const result = await redis.set(
lockKey,
lockValue,
'NX', // Только если ключ не существует
'PX', // Время жизни в миллисекундах
ttl
);
if (result === 'OK') {
return {
unlock: async () => {
// Безопасное освобождение с проверкой значения
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`;
await redis.eval(script, 1, lockKey, lockValue);
},
value: lockValue
};
}
return null; // Блокировка не получена
}
// Использование распределённой блокировки
async function processOrderWithLock(orderId) {
const lockKey = `lock:order:${orderId}`;
const lock = await acquireDistributedLock(lockKey);
if (!lock) {
throw new Error('Could not acquire lock for order processing');
}
try {
// Критическая секция
const order = await getOrderFromDB(orderId);
// Обработка заказа...
await updateOrderStatus(orderId, 'processed');
} finally {
await lock.unlock();
}
}
4. Практические рекомендации:
- Выбор стратегии: Пессимистичные блокировки для финансовых операций, оптимистичные для высоконагруженных систем
- Таймауты: Всегда устанавливаю таймауты для блокировок
- Уровни изоляции: Настраиваю подходящий уровень изоляции транзакций
- Мониторинг: Отслеживаю deadlock-и и длительные блокировки
- Альтернативы: Для некоторых сценариев использую очереди сообщений (RabbitMQ, Kafka) вместо блокировок
Ответ 18+ 🔞
Ну ты представляешь, какие танцы с бубном приходится откалывать, чтобы в Node.js с блокировками разобраться? Это ж ёперный театр, а не разработка! Слушай сюда, как я это обычно делаю, чтобы не получить овердохуища проблем в продакшене.
1. PostgreSQL — когда деньги переводятся, тут уже не до шуток Чувак, если речь про бабло, тут нужна пессимистичная блокировка, чтоб наверняка. Иначе какой-нибудь хитрожопый пользователь в двух вкладках кнопку "снять" нажмёт, и всё, пиши пропало — баланс ушёл в минус, а ты потом объясняйся.
const { Pool } = require('pg');
const pool = new Pool();
async function transferFundsWithLock(fromAccountId, toAccountId, amount) {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Берём оба счёта в замок, как в тиски. Никаких тебе гонок!
const fromAccount = await client.query(
'SELECT * FROM accounts WHERE id = $1 FOR UPDATE',
[fromAccountId]
);
const toAccount = await client.query(
'SELECT * FROM accounts WHERE id = $1 FOR UPDATE',
[toAccountId]
);
if (fromAccount.rows[0].balance < amount) {
throw new Error('Insufficient funds');
}
// Теперь спокойно делаем дела
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[amount, fromAccountId]
);
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[amount, toAccountId]
);
// И логируем, чтоб потом не было "а я ничего не отправлял"
await client.query(
`INSERT INTO transactions (from_account, to_account, amount, status)
VALUES ($1, $2, $3, 'completed')`,
[fromAccountId, toAccountId, amount]
);
await client.query('COMMIT');
return { success: true };
} catch (error) {
await client.query('ROLLBACK');
console.error('Transfer failed:', error);
throw error;
} finally {
client.release(); // Не забудь отпустить клиента, а то он так и будет висеть!
}
}
Суть в том, что FOR UPDATE — это как сказать базе: "Слушай, эти строки мои, пока я с ними не разберусь, остальные пусть подождут". Мёртвые захваты (deadlock) могут быть, конечно, но с правильным порядком блокировок — жить можно.
2. MongoDB — когда трафик дикий, а блокировки нужны легковесные Тут уже пессимистичные блокировки — это самоубийство, производительность накроется медным тазом. Поэтому юзаем оптимистичные — типа "а вдруг пронесёт". Если не пронесло — пробуем ещё раз.
const { MongoClient } = require('mongodb');
async function updateProductStock(productId, quantityChange) {
const client = await MongoClient.connect(process.env.MONGODB_URI);
const db = client.db('ecommerce');
let retries = 3; // Даём три попытки, как в хорошем боксе
while (retries > 0) {
const product = await db.collection('products').findOne({ _id: productId });
if (!product) {
throw new Error('Product not found');
}
const newStock = product.stock + quantityChange;
if (newStock < 0) {
throw new Error('Insufficient stock');
}
// Хитрость в version. Считаем, что за время между чтением и записью никто не тронул документ.
const result = await db.collection('products').updateOne(
{
_id: productId,
version: product.version // Если version изменился — значит, кто-то уже обновил, и наш апдейт проёбется
},
{
$set: {
stock: newStock,
updatedAt: new Date()
},
$inc: { version: 1 }
}
);
if (result.modifiedCount === 1) {
await client.close();
return { success: true, newStock };
}
retries--;
await new Promise(resolve => setTimeout(resolve, 100)); // Чуть-чуть ждём, чтобы конкуренты успокоились
}
await client.close();
throw new Error('Failed to update product after retries'); // Всё, сдаёмся
}
Это как в очереди за последним айфоном: ты думаешь, что он твой, но если кто-то перед тобой его схватил — приходится идти в конец очереди и пробовать снова.
3. Redis — когда блокировка нужна между кучей сервисов А вот это уже магия распределённых систем. Тут одна СУБД не справится, нужен Redis, чтобы все инстансы приложения договорились, кто главный.
const Redis = require('ioredis');
const redis = new Redis();
async function acquireDistributedLock(lockKey, ttl = 10000) {
const lockValue = `${Date.now()}-${Math.random()}`; // Уникальное значение, чтобы свои же не сбили блокировку
// Пытаемся захватить замок
const result = await redis.set(
lockKey,
lockValue,
'NX', // Поставить только если ключа нет (Not eXists)
'PX', // Время жизни в миллисекундах
ttl
);
if (result === 'OK') {
return {
unlock: async () => {
// Важно удалять только свой замок, а не чужой! Lua-скрипт в Redis атомарный.
const script = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`;
await redis.eval(script, 1, lockKey, lockValue);
},
value: lockValue
};
}
return null; // Не повезло, кто-то уже держит
}
// Пример использования
async function processOrderWithLock(orderId) {
const lockKey = `lock:order:${orderId}`;
const lock = await acquireDistributedLock(lockKey);
if (!lock) {
throw new Error('Could not acquire lock for order processing'); // Иди жди своей очереди
}
try {
// Тут делаем что-то важное с заказом, зная, что другие сервисы не лезут
const order = await getOrderFromDB(orderId);
await updateOrderStatus(orderId, 'processed');
} finally {
await lock.unlock(); // Обязательно отпускаем, даже если ошибка, иначе все повиснут
}
}
Это как ключ от туалета на заправке: взял — сделал дело — вернул. Если не вернёшь, все будут страдать.
4. Ну и общие советы, чтобы не было мучительно больно:
- Что выбирать: Пессимистичные блокировки — для операций, где ошибка стоит дорого (деньги). Оптимистичные — где скорость и масштаб важнее, а конфликты редки.
- Таймауты — твои лучшие друзья. Вечная блокировка — это гарантированный дедлок в продакшене и пиздец в полночь.
- Уровни изоляции в PostgreSQL — изучай, как таблицу умножения.
READ COMMITTEDчасто хватает, но иногда нуженREPEATABLE READили дажеSERIALIZABLE, если совсем паранойя. - Следи за дедлоками! Они будут, это нормально. Главное — знать, где и почему.
- Иногда блокировки — это костыль. Для фоновых задач или обработки событий лучше очередь (RabbitMQ, Kafka) — там конфликты сами собой решаются.
Короче, чувак, блокировки — это как управление хаосом. Главное — понимать, какой инструмент для какого бардака подходит, и не пытаться молотком забить микроскоп.