Я обновляю финансовое приложение, построенное на 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"
Прежде всего, я рекомендую установить ограничение скорости на вашей конечной точке, например 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 }
@jQueeny, да, я прочитал твое, вспомнил $inc, а затем добавил $inc в этот пост. Предыдущий код «await ...One({ a: 'A' }, { { b: 0 } })» был создан намеренно. Я думал так: первая запись, вызывающая конфликт второй записи, может быть «чем угодно». Дело в том, что оно должно уменьшить значение таким образом, чтобы последняя запись конфликтовала с первой. Это причина установки этого { b : 0 }. Я рассмотрел его, потому что большинство читателей свяжут первую запись со второй и помогут им читать лучше, если они похожи, хотя в этом нет необходимости.
продолжая предыдущий комментарий: вторая упомянутая здесь запись - это строка «оператор-2». По сути, он не использует $inc, но строка «вычитание значения» делает то же самое. Изменение в посте сделало эти два сообщения похожими для лучшего чтения. @jQueeny, мы будем рады обнаружить любые проблемы, связанные с этим, поскольку мы все знаем, что вся цель этих инициатив — создать «полезный контент для всех», и для этого необходимы совместные усилия.
Схемы 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
}
Изменили ли вы свой неправильный ответ на
$inc
после прочтения моего ответа?