views:

502

answers:

7

What's the common way to deal with concurrent updates in an SQL database ?

Consider a simple SQL schema(constraints and defaults not shown..) like

create table credits (
  int id,
  int creds,
  int user_id
);

The intent is to store some kind of credits for a user, e.g. something like stackoverflow's reputation.

How to deal with concurrent updates to that table ? A few options:

  • update credits set creds= 150 where userid = 1;

    In this case the application retreived the current value, calculated the new value(150) and performed an update. Which spells disaster if someone else does the same at the same time. I'm guessing wrapping the retreival of the current value and update in a transaction would solve that , e.g. Begin; select creds from credits where userid=1; do application logic to calculate new value, update credits set credits = 160 where userid = 1; end; In this case you could check if the new credit would be < 0 and just truncate it to 0 if negative credits make no sense.

  • update credits set creds = creds - 150 where userid=1;

    This case wouldn't need to worry about concurrent updates as the DB takes care of the consistency problem, but has the flaw that creds would happily become negative, which might not make sense for some applications.

So simply, what's the accepted method to deal with the (quite simple) problem outlined above, what if the db throws an error ?

+4  A: 

Use transactions:

BEGIN WORK;
SELECT creds FROM credits WHERE userid = 1;
-- do your work
UPDATE credits SET creds = 150 WHERE userid = 1;
COMMIT;

Some important notes:

  • Not all database types support transactions. In particular, mysql's default database type, MyISAM, doesn't. Use InnoDB if you're on mysql.
  • Transactions can abort due to reasons beyond your control. If this happens, your application must be prepared to start all over again, from the BEGIN WORK.
  • You'll need to set the isolation level to SERIALIZABLE, otherwise the first select can read data that other transactions have not committed yet(transactions arn't like mutexes in programming languages). Some databases will throw an error if there's concurrent ongoing SERIALIZABLE transactions, and you'll have to restart the transaction.
  • Some DBMS provide SELECT .. FOR UPDATE , which will lock the rows retreived by select until the transaction ends.

Combining transactions with SQL stored procedures can make the latter part easier to deal with; the application would just call a single stored procedure in a transaction, and re-call it if the transaction aborts.

bdonlan
Wouldn't you need a "select for update" in this case, or atleast a SERIALIZABLE isolation level ?
nos
@nos, it depends on the database. A database with true transaction support should provide a consistent snapshot just with a transaction, although possibly not by default. In the case of innodb, a snapshot of the database state is made as soon as you do a select on any innodb table.
bdonlan
You may indeed need to set the isolation level though: http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html
bdonlan
+1  A: 

For the first scenario you could add another condition in the where-clause to make sure you won't overwrite changes made by a concurrent user. E.g.

update credits set creds= 150 where userid = 1 AND creds = 0;
Ola Herrdahl
A: 

If you store a last update timestamp with the record, when you read the value, read the timestamp as well. When you go to update the record, check to make sure the timestamp matches. If someone came in behind you and updated before you, the timestamps would not match.

Nebakanezer
You get in trouble if they happen to be within the same millisecond (or whichever resolution your db have for timestamps). Which of course never happens, until one day it suddenly does and leaves your poor user with credits (or money.) lost in thin air.
nos
+1  A: 

You could set up a queueing mechanism where additions to or subtractions from a rank type value would get queued up for periodic LIFO processing by some job. If real-time info on a rank's "balance" is required this wouldn't fit because the balance wouldn't compute until the outstanding queue entries are reconciled, but if it's something that doesn't require immediate reconciliation it might serve.

This seems to reflect, at least on the outside looking in, how games like the old Panzer General series handle individual moves. One player's turn comes up, and they declare their moves. Each move in turn is processed in sequence, and there are no conflicts because each move has its place in the queue.

Darth Continent
It is always a good idea to store the individual records. It is also useful if there are bugs in your code and you need to recreate the balance from the source records. It is also useful because you now have a single writer back to your summary table, which reduces contention, blocking and complexity, and makes user experience more seemless.
Doug
A: 

Optimistic locking using a new timestamp column can solve this concurrency issue.

UPDATE credits SET creds = 150 WHERE userid = 1 and modified_data = old_modified_date

Loop
+1  A: 

For MySQL InnoDB tables, this really depends on the isolation level you set.

If you are using the default level 3 (REPEATABLE READ), then you would need to lock any row that affects subsequent writes, even if you are in a transaction. In your example you will need to :

SELECT FOR UPDATE creds FROM credits WHERE userid = 1;
-- calculate --
UPDATE credits SET creds = 150 WHERE userid = 1;

If you are using level 4 (SERIALIZABLE), then a simple SELECT followed by update is sufficient. Level 4 in InnoDB is implemented by read-locking every row that you read.

SELECT creds FROM credits WHERE userid = 1;
-- calculate --
UPDATE credits SET creds = 150 WHERE userid = 1;

However in this specific example, since the computation (adding credits) is simple enough to be done in SQL, a simple:

UPDATE credits set creds = creds - 150 where userid=1;

will be equivalent to a SELECT FOR UPDATE followed by UPDATE.

Savant Degrees
A: 

I'm using Hibernate optimistic locking and Oracle and Spring txn declaration. The isolation level is the db's default which on Oracle is READ_COMMITTED. The following sequence of statements is executed within a txn: - Start txn - Read a Person (Hibernate select). - Change person's statuscode. - Update Person (Hibernate saveOrUpdate) resulting in an sql update statement. - Commit txn

I'm wondering what will happen if this sequence is executed in two separate hibernate applications? Will I get an exception in one application, because the version number was updated in another transaction, or won't I get an exception, and will the updates made by one of the applications be lost?

What should I change to ensure that a concurrent update is impossible?

Thanks, EDH

EdwinDhondt