Answer the question
In order to leave comments, you need to log in
How to protect against double debiting in a multi-threaded application?
The spending consists of three stages. Reading the user's balance, checking whether there are enough funds and actually recording a new balance. Recording and reading the balance goes to the database. In my case postgres.
If in a multi-threaded application it happens that two threads read the balance, check whether there are enough funds for the purchase and this stage is successful for both, then two entries will follow, which will drive the user into a minus. What are the options for double spending protection?
I'm throwing it myself as I think:
1 Make a constraint on the user's balance> 0.
2 Set a read lock on the user for the duration of the first successful process, let the second one wait. There is a problem that I did not find a line lock blocking on reading. If it is, tell me pliz
What other options are there?
In general, everything is much more complicated. the user's balance is calculated on the fly and is not stored in the user. It is calculated based on the history of its replenishment / expenses.
Additional question. What is the best way to design an application that controls user balances? Write balance to user? Then how to ensure consistency at the base level? Write triggers when a transaction appears / changes to recalculate the balance?
Or is it better to recalculate every time from history? Who had experience, please share
And the last question is how to write a test for such cases? Application on ruby-on-rails
Answer the question
In order to leave comments, you need to log in
Blocking during spending has already been said, but it happens that the process can last long enough so that the user in the next window cannot make a payment in parallel (he will have everything hanging), so blocking is implemented programmatically
Add the lockedBalance field to the user account, add it at the beginning of the purchase transaction to this value, the amount you need to spend (and at the end, subtract the same amount both from here and from the total balance), respectively, the final balance when checking, consider from the difference between the main balance and this blocked one. If the transaction fails, it will have to be tracked, the blocked balance should also be reduced by the amount of the transaction, but do not touch the total.
more beautiful, instead of one field, start a special table - current transactions, where, in accordance with the status, calculate this blocked balance (the status of the transaction is started, the transaction is completed or the transaction has broken off), this is relevant just in case when the transaction is completed long enough not to carry it out within the database transaction (otherwise the user will get at the time of maintenance, for example, restarting the database for an update, and his transaction will be lost), besides, you already have this table, just add more statuses
if the check passed, then both will be executed
Or is it better to recalculate every time from history?
The spending consists of three stages. Reading the user's balance, checking whether there are enough funds and actually recording a new balance.
the user's balance is calculated on the fly and is not stored in the user. It is calculated based on the history of its replenishment / expenses.
If in a multi-threaded application it happens that two threads read the balance, check whether there are enough funds for the purchase and this stage is successful for both, then two entries will follow, which will drive the user into a minus. What are the options for double spending protection?
Locks in the database are always not very good, and with serializable the performance of the database will drop to the floor. Constraint as an option, but it will only help for this situation and it turns out that the logic was dragged into the base.
The best option is to simply divide the work between separate threads, i.e. a specific user is served by a specific thread and no other, then there will be no race in principle. But this is if your architecture allows.
Add a version field to the table.
Do everything in a transaction (level not lower than Read committed):
1. When selecting a row from the database, save the version value in your program.
2. When updating, do this:
Update ... set balance = balance - purchase amount, version=version + 1 where version = saved version value.
3. If the request is successful, then the number of rows changed = 1, if nothing has changed = 0.
All this is called optimistic locking
1) Use transaction isolation level: serializable
but Update performance will drop, so it's better to take measurements and see if it fits or not. Usually there are not as many Updates as there are reads.
Advantage : you won't have to dance with a tambourine.
2) To make this operation through a class which controls. Those. if we make a request a second time, it's the class that looks to see if the first one has completed.
And here there are already 2 options:
- answer the user with a refusal (like: "wait for the previous operation to complete")
- wait for the first one to complete and proceed with the second one. But if after the first, the balance is <0, then tell the user about it.
Didn't find what you were looking for?
Ask your questionAsk a Question
731 491 924 answers to any question