views:

88

answers:

3

For a browser game, I have a database table fights:

  • fightID
  • fromID
  • toID
  • simulated (1=true, 0=false)

Whenever a user requests a page, I select all fights which still have to be simulated:

SELECT fightID, fromID, toID FROM fights WHERE simulated = 0

These outstanding fights are then simulated in the PHP script and, finally, the fight is marked as simulated and the winner gets his points:

UPDATE fights SET simulated = 1 WHERE fightID = X
UPDATE users SET points = points+1 WHERE userID = WINNER

The problem:

Imagine two users coming to the page one after another in only a few milliseconds. On both users's page load the same outstanding fights are selected. Then they are simulated and - since both requested the page almost at the same time - the winner gets his points twice. The fights are then marked as simulated. But this is too late.

How can I avoid this problem? Thank you very much!

+3  A: 

If you are sure that once you read the fights from the DB, they will be simulated (i.e. it is not possible that the user quits and that some fights are left unsimulated), you an use a transaction to read and update the fights in one, atomic operation.

START TRANSACTION;
SELECT fightID, fromID, toID FROM fights WHERE simulated = 0
UPDATE fights SET simulated = 1 WHERE fightID = X
COMMIT;

and then later update the scores.

Wookai
+1 for transactions, this is what they are for. But I don't understand your caveat about the user quitting; that is what rollbacks are for.
Ben James
I know what a rollback is ;). But let's say that the outcome of the fight is computed on the server side, and then is rendered on the client side using a flash animation. It is possible that the user does not watch all fights, which may mean that the unseen fight are canceled. I don't see how you could rollback part of the transaction in a different request (neither can I see how you could detect that the user is leaving and send that last request, for that matter).
Wookai
Thank you very much! You could avoid the problem with a user quitting simply by using ignore_user_abort(true), couldn't you?
A: 

Instead of keepeng the points in the users table you can create a 'winners' table

userId
fightId
...

and (userId,fightId) would be the primary key. That would prevent any user geting more than 1 (or whatever) points per fight. The points would be calculated by select count(*) from winners where userId=?

pablochan
Strictly speaking, this would denormalize his DB, wouldn't it ;) ?
Wookai
I'm 99,99% sure that it wouldn't but I don't remember all the definitions for the normalization levels (only the first three, but this definately doesn't go against them), so if you could point out the flaw I'd appreciate it.
pablochan
I wasn't sure so I figured I'd tease you ;)... I don't know if this is against any of the rules, but it feels suboptimal, as you have a 1:1 relationship between the winners table and the fights table. Thus, you will need one more join, store two more IDs, etc... Again, this is just a feeling and not based on a specific rule.
Wookai
Then why not say that in the first place? :P. I don't know the size of the app, but if it isn't REALLY big (in terms of users) the additional table and the joins are a non-issue.
pablochan
Actually wait, there are no additional joins :P
pablochan
I don't think what you are talking about (normalization levels???) but I can't use this approach for my purpose anyway. But no down vote, of course, since in other cases it may be useful. Thanks for the answer!
http://en.wikipedia.org/wiki/Database_normalizationMy mistake, it should be 'normal forms'.
pablochan
A: 

Hmm, what if you use 3 states of simulation? 0 = not simulated, 1= simulating NOW, 2= simulated. Then you can flag simulation immediately after user loads the fight.

Andrey Vlasov
This wouldn't solve the problem. It is exactly what I'm doing at the moment. If two users access a page almost at the same time, the script selects the data from MySQL in both cases. But if the script updates the status to 1 (simulating NOW) for the first user, the second user has already selected the data ...