Попытка защитить платежную транзакцию с помощью mysqljs2

В своем веб-приложении я пытаюсь защитить платежную транзакцию, а именно:

  1. не позволять пользователям тратить больше, чем они имеют, особенно в случае одновременного выполнения
  2. и предотвратить частичное исполнение.

Дополнительные детали:

  • Заказы могут быть оплачены частично, уже оплаченная сумма хранится в оплаченный столбец
  • Я использую базу данных 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();
  }
}

0

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *