简体   繁体   中英

optimistic locking user credit management

I have a central database for handling user credit with multiple servers reads and writes to it. The application sits on top of these servers serve user requests by doing the following for each request:

1. check if user has enough credit for the task by reading from db.
2. perform the time consuming request
3. deduct a credit from user account, save the new credit count back to db.

the application uses the database's optimistic locking. So following might happen

1. request a comes in, see that user x has enough credit,
2. request b comes in, see that user x has enough credit,
3. a performs work
4. a saves the new credit count back to db
5. b performs work
6. b tries to save the new credit count back to db, application gets an exception and fails to account for this credit deduction.

With pessimistic locking, the application will need to explicitly get a lock on the user account to guarantee exclusive access, but this KILLs performance since the system have many concurrent requests.
so what would be a good new design for this credit system?

Here are two "locking" mechanisms at avoid using InnoDB's locking mechanism for either of two reasons:

  • A task that takes longer than you should spend in a BEGIN...COMMIT of InnoDB.
  • A task that ends in a different program (or different web page) than it started in.

Plan A. (This assumes the race condition is rare, and the time wasted for Step 2 is acceptable in those rare cases.)

  1. (same) check if user has enough credit for the task by reading from db.
  2. (same) perform the time consuming request
  3. (added) START TRANSACTION;
  4. (added) Again Check if the user has enough credit. ( ROLLABCK and abort if not.)
  5. (same as old #3) deduct a credit from user account, save the new credit count back to db.
  6. (added) COMMIT;

START..COMMIT is InnoDB transaction stuff. If a race condition caused 'x' to not have credit by step 4, you will ROLLBACK and not perform steps 4 and 5.

Plan B. (This is more complex, but you might prefer it.)

  1. Have a table Locks for locking. It contains user_id and a timestamp.
  2. START TRANSACTION;
  3. If user_id is in Locks , abort ( ROLLBACK and exit).
  4. INSERT INTO Locks the user_id and current_timestamp in Locks (thereby "locking" 'x').
  5. COMMIT;
  6. Perform the processing (original Steps 1,2,3)
  7. DELETE FROM Locks WHERE user_id = 'x'; ( autocommit=1 suffices here.)

A potential problem: If the processing dies in step 6, not getting around to releasing the lock, that user will be locked out forever. The 'solution' is to periodically check Locks for any timestamps that are 'very' old. If any are found, assume that the processing died, and DELETE the row(s).

You didn't state explicitly what you want to achieve, so I assume you don't want to perform the work just to realise it has been in vain due to low credit.

No-lock

Implement credit hold on step (1) and associate the work (2) and the deduction (3) with the hold. This way low credit user won't pass step (1).

Optimistic locking

As a collision is detected in optimistic locking post factum, I don't think it fits the assumption.

Pessimistic locking

It isn't possible to tell definitely without knowing the schema, but I think it's an exaggeration about killing performance . You can smartly incorporate MySQL InnoDB transaction isolation levels and locking reads at finer granularity than exclusively locking a user account completely. For instance, using SELECT ... LOCK IN SHARE MODE which sets shared locks and allows reads for other transactions.

Rick's caution about the tasking taking longer then MySQL will wait ( innodb_lock_wait_timeout ) applies here.

You want The Escrow Transactional Method .

You record the credit left after doling out some to each updating process and the credit doled out to (ie held in escrow for) them. A process retries until success a transaction that increases the credit doled out by what it needs and decreases the credit that is left by what it needs; it succeeds only if that would leave the credit left non-negative. Then it does its long calculation. Regardless of the calculation's success it then applies a transaction that decreases the credit doled out. But on success it also increases the assets while on failure it increases the credit left.

Use the timestamp/rowversion approach that you will find in all real database engines except MySQL.

You can emulate them with MySQL in this way. Have a TIMESTAMP column (updated) that gets updated whenever a row is updated. Select that column along with the rest of the data you require. Use the returned timestamp as a condition in your WHERE clause so that the row will only be updated when the timestamp is still the same as when you read the row.

UPDATE table SET col1 = value WHERE id = 1 AND updated = timestamp_value_read

Now when you run the update and the timestamps do not match no update will be performed. You can test for this by using rows affected, if zero rows were updated then you know that the row was modified between read and write. Handle that condition in your code whichever way is best for your application and your users.

Timestamp tutorial

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM