Пользователи могут одновременно снимать средства, превышающие баланс их кошелька, инициируя несколько одновременных выводов средств

Я обновляю финансовое приложение, построенное на node.js, mongodb и «mongoose», и в настоящее время сталкиваюсь с проблемой логики обработки вывода средств из системы. Проблема в том, что пользователь может запросить сумму, превышающую его баланс, если он одновременно достигнет конечной точки вывода средств. Например, при доступном балансе в 10 долларов США пользователь может обрабатывать несколько выводов по 10 долларов США, если они делают это одновременно.

Я знаю, откуда возникла проблема, но я не смог ее решить. Я знаю, что это происходит, потому что действия происходят одновременно, поэтому user.balance всегда будет 10, потому что списание происходит после чтения. Я относительно новичок в бэкэнд-разработке, поэтому извините, если я не использую правильную терминологию. Вот как выглядит мой код.

 try{
   // Perform tasks such as validating the account number, etc.

   const user = await User.findById(req.user._id);

   if (user.balance < req.body.amount)
     return res.status(400).send({message:"Insufficient Balance"});

    // update the user balance in the DB to user.balance - req.body.amount

    return res.status(200).send({message:"Fund has been sent to the account provided"})
 }

Я попробовал обновить запись пользователя в коллекции и использовать этот ответ для проверки баланса пользователя, но я не уверен, решит ли это полностью мою проблему, останутся ли лазейки или есть лучший способ справиться с этим. этот.

Это второй подход, который я опробовал

 try{

   // Perform tasks such as validating the account number, etc.
   const idx = uuid()

   const user = await User.findByIdAndUpdate(req.user._id, {withdrawal_intent:idx} {
        new: true,
   });


   if (user.balance < req.body.amount)
     return res.status(400).send({message:"Insufficient Balance"});

    // update the user balance in the DB to user.balance - req.body.amount

    return res.status(200).send({message:"Fund has been sent to the account provided"})
 }

Я готов переделать код, чтобы он соответствовал наилучшему способу решения этой проблемы.

P.S: Это старая кодовая база, поэтому я могу использовать старый синтаксис mongoose. "mongoose": "^5.9.9"

Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
168
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Прежде всего, я рекомендую установить ограничение скорости на вашей конечной точке, например 1 запрос в секунду.

Во-вторых, если вы еще не определили столбец проверки баланса для предотвращения отрицательных балансов, вам следует это сделать.

(Хотя я не являюсь экспертом по MongoDB), этого можно добиться, используя сериализуемую транзакцию. Это потребует выбора и обновления баланса пользователя, добавления новой записи в таблицу вывода средств и последующего подтверждения транзакции. Если проверка не пройдена, транзакция будет отменена.

Для финансовых приложений хорошим подходом могут быть хранимые процедуры.

Вот пример:

BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;

select "balance", "recordID" into "_balance", "_recordID" 
from "Balance"
where "userID" = "in_userID"
for update;

if ("_balance"< "in_amount") raise exception('INSUFFICENT_BALANCE'); end if;

update "Balance" set "balance" = "balance" - "in_amount" where "recordID" = "_recordID";

insert into "Withdrawal" () values ();

COMMIT;
Ответ принят как подходящий

Короткий ответ:

Состояние гонки, которое вызывает проблему в этом вопросе, можно решить с помощью метода findOneAndUpdate. Самый важный момент — это условие, переданное в этот метод. При этом всегда следует проверять, что выполняемое обновление не должно приводить к отрицательному значению. Ниже приведен пример кода. При его чтении воспользуйтесь комментариями, включенными в него.

Подробный ответ:

Пожалуйста, прочитайте этот пост последовательно.

Условное обновление: Условное обновление — это распространенный оптимистический подход к обеспечению согласованности данных в условиях одновременного доступа, то есть согласованности в условиях параллелизма.

Он работает по принципу, что любой клиент, выполняющий обновление, проверяет значение непосредственно перед обновлением, чтобы увидеть, изменилось ли оно с момента последнего чтения. Если тест не пройден, то произошло состояние гонки — конфликт записи-записи, и текущее обновление следует прервать, проинформировав пользователей о ситуации. Для этого исходного вопроса был реализован тест на состояние гонки, поскольку обновление не приведет к отрицательному балансу.

Следующий код продемонстрирует это. В основном он основан на методе findOneAndUpdate, поскольку он выполняет три операции: поиск, обновление и чтение атомарным способом. Более подробную информацию можно найти в комментариях, приведенных в коде. Два оператора в коде требуют некоторой дополнительной информации, как показано ниже.

Оператор — оператор №1: это оператор, вызывающий состояние гонки в коде. На данный момент оно не прокомментировано. Чтобы запустить код без возникновения состояния гонки, прокомментируйте это утверждение.

Оператор - оператор № 2: это оператор, в котором было реализовано условное обновление, условие - { a: 'A', b: { $gte: orgdoc.b } } выполняет задание.

// MongoDB: 7.0.2
// Mongoose : 8.3.2
// Node.js v21.6.0.
//conditionalUpdate.mjs

import mongoose, { Schema } from 'mongoose';

main().catch((err) => {
  console.info(err);
});

async function main() {
  await mongoose.connect('mongodb://127.0.0.1:27017/myapp');

  const someSchema = new Schema({ a: String, b: Number });
  const SomeModel = mongoose.model('SomeModel', someSchema);
  await SomeModel.deleteMany();

  // creating original document.
  const orgdoc = await SomeModel({
    a: 'A',
    b: 1,
  }).save();

  // deducting value
  const latestvalue = orgdoc.b - 1;

  // let us assume this operation is being executed by a separate client program in another session
  // performing a concurrent update against the same document
  // statement #1
  await SomeModel.updateOne({ a: 'A' }, { $inc: { b: -1 } });

  // performing a conditional update to safeguard
  // the possible write-write conflict
  // statement #2
  const updateQuery = SomeModel.findOneAndUpdate(
    { a: 'A', b: { $gte: orgdoc.b } },
    { b: latestvalue },
    { new: true }
  );
  const newdoc = await updateQuery.exec();

  // informing user for the needful action
  if (newdoc) {
    console.info(`Updation passed`);
  } else {
    console.info(
      `Updation failed - write-write conflict detected, please check the latest document against yours`
    );
  }
  console.info(`Original document: ${orgdoc}`);
  console.info(`Update Query: ${updateQuery}`);
  console.info(`Latest document : ${await SomeModel.findOne({ a: 'A' })}`);
}

// Output 1: When statement #1 is uncommented
// Updation failed - write-write conflict detected, please check the latest document against yours
// Original document: { a: 'A', b: 1, _id: ..., __v: 0 }
// Update Query: SomeModel.findOneAndUpdate({ a: 'A', b: { '$gte': 1 } }, { '$set': { b: 0 } })
// Latest document : { _id: ..., a: 'A', b: 0, __v: 0 }

// Output 2: When statement #1 is commented
// Updation passed
// Original document: { a: 'A', b: 1, _id: ..., __v: 0 }
// Update Query: SomeModel.findOneAndUpdate({ a: 'A', b: { '$gte': 1 } }, { '$set': { b: 0 } })
// Latest document : { _id: ..., a: 'A', b: 0, __v: 0 }

Изменили ли вы свой неправильный ответ на $inc после прочтения моего ответа?

jQueeny 21.04.2024 15:59

@jQueeny, да, я прочитал твое, вспомнил $inc, а затем добавил $inc в этот пост. Предыдущий код «await ...One({ a: 'A' }, { { b: 0 } })» был создан намеренно. Я думал так: первая запись, вызывающая конфликт второй записи, может быть «чем угодно». Дело в том, что оно должно уменьшить значение таким образом, чтобы последняя запись конфликтовала с первой. Это причина установки этого { b : 0 }. Я рассмотрел его, потому что большинство читателей свяжут первую запись со второй и помогут им читать лучше, если они похожи, хотя в этом нет необходимости.

WeDoTheBest4You 22.04.2024 03:27

продолжая предыдущий комментарий: вторая упомянутая здесь запись - это строка «оператор-2». По сути, он не использует $inc, но строка «вычитание значения» делает то же самое. Изменение в посте сделало эти два сообщения похожими для лучшего чтения. @jQueeny, мы будем рады обнаружить любые проблемы, связанные с этим, поскольку мы все знаем, что вся цель этих инициатив — создать «полезный контент для всех», и для этого необходимы совместные усилия.

WeDoTheBest4You 22.04.2024 04:01

Схемы Mongoose v5 поддерживают валидаторы схемы min и max , поэтому вы можете применить min из 0 к balance, чтобы невозможно было опуститься ниже нее следующим образом:

const userSchema = new mongoose.Schema({
    name: String,
    balance: { 
        type: Number, 
        min: 0
    }
})

Вы можете использовать $inc, чтобы увеличить поле balance на отрицательную величину, но я бы сначала проверил, что req.body.amount является положительным целым числом. Затем, когда вы выполняете обновление, убедитесь, что вы используете опцию findByIdAndUpdate или findOneAndUpdate с { runValidators: true }, например:

// convert string to integer
const amount = parseInt(req.body.amount);

// now only do update if positive integer
if (Math.sign(amount) > 0){
   const user = await User.findByIdAndUpdate(req.user._id,
   {
      $inc: {
         balance: -amount
      }
   },{ runValidators: true, new: true });
}else{
  //Return a response that amount must be positive amount
}

Другие вопросы по теме