Node.js — Transaction, xatolarni boshqarish
Muammodan boshlaylik
O'tgan darsdagi API ga buyurtma berish endpointi kerak:
app.post('/api/buyurtmalar', async (req, res) => {
const [b] = await pool.execute('INSERT INTO buyurtmalar (mijoz_id) VALUES (?)', [mijozId]);
for (const q of qatorlar) {
await pool.execute('INSERT INTO buyurtma_qatorlari ...', [...]);
await pool.execute('UPDATE mahsulotlar SET soni = soni - ? WHERE id = ?', [...]);
}
res.json({ id: b.insertId });
});
Uchinchi mahsulotda xato chiqdi (omborda yo'q yoki tarmoq uzildi). Natija: buyurtma yaratildi, ikkita mahsulot ombordan ayirildi, uchinchisi — yo'q. Baza yarim-tayyor holatda (36-dars).
Buni Node.js da to'g'ri tranzaksiya bilan qanday yozish kerak?
Asosiy qoida: bitta ulanish
Tranzaksiya — ulanishga bog'liq. pool.execute() har safar pooldan istalgan bo'sh ulanishni oladi:
// ❌ Har bir so'rov boshqa ulanishda bo'lishi mumkin
await pool.query('START TRANSACTION'); // ulanish #3
await pool.execute('INSERT ...'); // ulanish #7 — tranzaksiyadan tashqarida!
await pool.query('COMMIT'); // ulanish #2 — hech narsani commit qilmaydi
To'g'risi — pooldan bitta ulanishni olib, butun tranzaksiyani shu ulanishda bajarish:
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
// ... barcha so'rovlar conn orqali
await conn.commit();
} catch (err) {
await conn.rollback();
throw err;
} finally {
conn.release();
}
Uchta majburiy element:
try—beginTransaction...commitcatch—rollbackva xatoni yuqoriga uzatishfinally—release(), har qanday holatda (54-dars: leak)
Buyurtma berish — to'liq misol
import { pool } from './db.js';
export class OmbordaYetarliEmas extends Error {
constructor(mahsulotId) {
super(`Omborda yetarli emas: mahsulot #${mahsulotId}`);
this.mahsulotId = mahsulotId;
}
}
export async function buyurtmaBerish(mijozId, qatorlar) {
// Deadlock xavfini kamaytirish: doim bir xil tartibda qulflash (38-dars)
const saralangan = [...qatorlar].sort((a, b) => a.mahsulotId - b.mahsulotId);
const ids = saralangan.map(q => q.mahsulotId);
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
// 1. Mahsulotlarni qulflab o'qish — eng yangi narx va son
const [mahsulotlar] = await conn.query(
'SELECT id, narx, soni FROM mahsulotlar WHERE id IN (?) ORDER BY id FOR UPDATE',
[ids]
);
const xarita = new Map(mahsulotlar.map(m => [m.id, m]));
for (const q of saralangan) {
const m = xarita.get(q.mahsulotId);
if (!m || m.soni < q.soni) {
throw new OmbordaYetarliEmas(q.mahsulotId);
}
}
// 2. Buyurtma
const [b] = await conn.execute(
'INSERT INTO buyurtmalar (mijoz_id) VALUES (?)',
[mijozId]
);
const buyurtmaId = b.insertId;
// 3. Qatorlar — bitta INSERT da
const values = saralangan.map(q => [buyurtmaId, q.mahsulotId, q.soni, xarita.get(q.mahsulotId).narx]);
await conn.query(
'INSERT INTO buyurtma_qatorlari (buyurtma_id, mahsulot_id, soni, narx) VALUES ?',
[values]
);
// 4. Ombordan ayirish
for (const q of saralangan) {
await conn.execute(
'UPDATE mahsulotlar SET soni = soni - ? WHERE id = ?',
[q.soni, q.mahsulotId]
);
}
await conn.commit();
return buyurtmaId;
} catch (err) {
await conn.rollback();
throw err;
} finally {
conn.release();
}
}
Muhim jihatlar:
FOR UPDATE— tekshiruv va ayirish orasida boshqa so'rov oxirgi donani "o'g'irlay olmaydi" (38-dars)- Narx bazadan olinadi, mijoz yuborgan qiymatdan emas — aks holda kimdir
narx: 1deb yuboradi VALUES ?+ ichma-ich massiv —mysql2ko'p qatorliINSERTni o'zi yasaydi (querybilan,executeemas)- O'z xato classi — biznes xatosini texnik xatodan ajratish uchun
Endpoint:
app.post('/api/buyurtmalar', async (req, res, next) => {
try {
const id = await buyurtmaBerish(req.user.id, req.body.qatorlar);
res.status(201).json({ data: { id } });
} catch (err) {
if (err instanceof OmbordaYetarliEmas) {
return res.status(409).json({ xato: err.message, mahsulotId: err.mahsulotId });
}
next(err);
}
});
Qayta ishlatiladigan yordamchi
Har safar try/catch/finally ni yozish — takrorlanish va xato xavfi. Bir marta yozib qo'yamiz:
export async function tranzaksiya(fn) {
const conn = await pool.getConnection();
try {
await conn.beginTransaction();
const natija = await fn(conn);
await conn.commit();
return natija;
} catch (err) {
await conn.rollback();
throw err;
} finally {
conn.release();
}
}
Ishlatish:
const buyurtmaId = await tranzaksiya(async (conn) => {
const [b] = await conn.execute('INSERT INTO buyurtmalar (mijoz_id) VALUES (?)', [mijozId]);
await conn.execute('UPDATE mijozlar SET oxirgi_buyurtma = NOW() WHERE id = ?', [mijozId]);
return b.insertId;
});
commit, rollback, release ni unutish endi imkonsiz.
Deadlock — qayta urinish
Yuqori yuklamada deadlock (38-dars) vaqti-vaqti bilan bo'ladi. Uni xato sifatida qaytarish o'rniga — avtomatik qayta urinish:
const QAYTA_URINILADIGAN = new Set(['ER_LOCK_DEADLOCK', 'ER_LOCK_WAIT_TIMEOUT']);
export async function tranzaksiyaQaytaUrinish(fn, urinishlar = 3) {
for (let i = 1; ; i++) {
try {
return await tranzaksiya(fn);
} catch (err) {
if (!QAYTA_URINILADIGAN.has(err.code) || i >= urinishlar) throw err;
const kutish = 50 * 2 ** i + Math.random() * 50; // 100, 200, 400 ms + tasodifiy
await new Promise(r => setTimeout(r, kutish));
}
}
}
Tasodifiy qo'shimcha (jitter) — to'qnashgan ikki so'rov bir vaqtda qayta urinib, yana to'qnashmasligi uchun.
[!WARNING] Qayta urinish faqat butun tranzaksiya qayta bajarilganda xavfsiz. Agar
fnichida tashqi amal bo'lsa (email yuborish, to'lov API) — u ikki marta bajariladi. Tashqi amallarni tranzaksiyadan keyin bajaring (36-dars).
Savepoint — ichma-ich amallar
await tranzaksiya(async (conn) => {
for (const [i, qator] of importQatorlari.entries()) {
await conn.query(`SAVEPOINT q${i}`);
try {
await conn.execute('INSERT INTO mahsulotlar (nom, narx) VALUES (?, ?)', [qator.nom, qator.narx]);
await conn.query(`RELEASE SAVEPOINT q${i}`);
} catch (err) {
await conn.query(`ROLLBACK TO SAVEPOINT q${i}`);
xatolar.push({ qator: i + 1, xato: err.message });
}
}
});
Xato qatorlar tashlab ketiladi, qolganlari bitta tranzaksiyada saqlanadi (39-dars).
Global xatolar
// Pool darajasidagi xatolar (ulanish uzilishi)
pool.on('connection', conn => {
conn.on('error', err => console.error('MySQL ulanish xatosi:', err.code));
});
// Ushlanmagan Promise xatolari — jarayon jimgina o'lmasin
process.on('unhandledRejection', err => {
console.error('Ushlanmagan xato:', err);
process.exit(1); // jarayon menejeri (pm2, Docker) qayta ishga tushiradi
});
// To'g'ri to'xtash — ochiq ulanishlarni yopish
process.on('SIGTERM', async () => {
await pool.end();
process.exit(0);
});
Nima uchun shunday?
Nima uchun Node.js da tranzaksiya uchun alohida ulanish olish kerak, PHP da esa $pdo->beginTransaction() yetarli?
PHP-FPM da har bir HTTP so'rov — alohida jarayon va bitta ulanish. Bitta so'rov ichidagi barcha amallar avtomatik bitta ulanishda. Node.js esa bitta jarayon ichida minglab so'rovni bir vaqtda (asinxron) bajaradi va ular bitta pool ni bo'lishadi. await qilingan har bir nuqtada boshqa so'rov ishlashi mumkin. Agar tranzaksiya qadamlari turli ulanishlarga tushsa — ular boshqa so'rovlarning tranzaksiyalari bilan aralashib ketadi. Shuning uchun Node.js da ulanishni aniq "egallab olish" kerak.
Xulosa
Eslab qoling:
- Tranzaksiya — bitta ulanishda:
pool.getConnection() try { begin...commit } catch { rollback; throw } finally { release }SELECT ... FOR UPDATE— tekshirish va yozish orasidagi poygani to'xtatish- Narxlarni bazadan oling, mijoz yuborganidan emas
tranzaksiya(fn)yordamchisi — commit/rollback/release ni unutib bo'lmaydi- Deadlock / lock timeout — qayta urinish, exponential backoff + jitter
- Tashqi amallar (email, to'lov) — tranzaksiyadan keyin
- O'z xato classlari — biznes xatolarini HTTP 409/422 ga aylantirish
Keyingi dars
57-dars: Sequelize ORM — model, migration, association