В своем веб-приложении я пытаюсь защитить платежную транзакцию, а именно:
- не позволять пользователям тратить больше, чем они имеют, особенно в случае одновременного выполнения
- и предотвратить частичное исполнение.
Дополнительные детали:
- Заказы могут быть оплачены частично, уже оплаченная сумма хранится в оплаченный столбец
- Я использую базу данных MySQL и InnoDB в качестве механизма хранения
- Я предпочитаю не использовать ORM
Я был бы рад, если бы некоторые из вас, мастера SQL, пролили свет на это!
Мои реализации верны?
Как бы вы с этим справились?
ТАБЛИЦЫ БЛОКИРОВКИ
Вот первая попытка использования LOCK TABLES.
Он работает так, как ожидалось, но я думаю, что он не предотвращает частичное выполнение и не является оптимальным с точки зрения конкуренции (блокирует две полные таблицы).
import mysql from "mysql2/promise";
const config = {
host: "localhost",
port: 3306,
database: "testdb",
user: "louis",
password: "almost_posted_it_online!",
};
const pool = mysql.createPool(config);
pay_v2().then(() => console.log("done"));
async function pay_v2() {
const orderId = 1;
const userId = 1;
const aConnection = await pool.getConnection();
try {
await aConnection.query("LOCK TABLES users2 WRITE, orders2 WRITE");
const [rows1] = await aConnection.execute(
"SELECT total FROM orders2 WHERE id = ?", [orderId]);
const orderTotal = (rows1 as any[])[0]["total"] as number;
const [rows2] = await aConnection.execute(
"SELECT coins FROM users2 WHERE id = ?", [userId]);
const coinsAvailable = (rows2 as any[])[0]["coins"] as number;
const coinsUsed = Math.min(coinsAvailable, orderTotal);
console.log(`Using ${coinsUsed} coins`);
await aConnection.execute(
"UPDATE orders2 SET paid = ? WHERE id = ?", [coinsUsed, orderId]);
await aConnection.execute(
"UPDATE users2 SET coins = coins - ? WHERE id = ?", [coinsUsed, userId]);
await aConnection.query("UNLOCK TABLES");
} finally {
aConnection.release();
}
}
Операция и блокировка чтения
Вот вторая попытка использования транзакции и блокировка чтения. Он предотвращает частичные обновления, он сводит к минимуму конкуренцию (заблокированы только две соответствующие строки), но я не совсем уверен, что он предотвращает два одновременных выполнения, чтобы потратить одни и те же монеты дважды.
async function pay_v3() {
const orderId = 1;
const userId = 1;
let aConnection = await pool.getConnection();
try {
await aConnection.beginTransaction()
const [rows1] = await aConnection.execute(
"SELECT total FROM orders2 WHERE id = ? FOR UPDATE", [orderId]);
const orderTotal = (rows1 as any[])[0]["total"] as number;
const [rows2] = await aConnection.execute(
"SELECT coins FROM users2 WHERE id = ? FOR UPDATE", [userId]);
const coinsAvailable = (rows2 as any[])[0]["coins"] as number;
const coinsUsed = Math.min(coinsAvailable, orderTotal);
console.log(`Using ${coinsUsed} coins`);
await aConnection.execute(
"UPDATE orders2 SET paid = ? WHERE id = ?", [coinsUsed, orderId]);
await aConnection.execute(
"UPDATE users2 SET coins = coins - ? WHERE id = ?", [coinsUsed, userId]);
await aConnection.commit();
} catch (error) {
await aConnection.rollback();
throw error;
} finally {
aConnection.release();
}
}