Ответ
В 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) вместо блокировок