I have a application that unfortunately uses legacy mysql_* functions with MyISAM tables (bleagh...), so I cannot use transactions. I have code that gets a current balance, checks whether this balance is okay, if yes, it will subtract a quantity and save the new balance.
The problem is, I have recently seen an instance where two queries grab the same starting balance, subtract a quantity, then record a new balance. Since they both grabbed the same starting balance, the ending balance after both UPDATES is wrong.
100 - 10 = 90
100 - 15 = 85
When it should be...
100 - 10 = 90
90 - 15 = 75
These requests executed a several minutes apart, so I do not believe the discrepancy is due to race conditions. My initial though is that MySQL cache is storing the result of the identical initial query that gets the balance. I read however that this type of cache is deleted if any relevant tables are modified.
I will most likely fix by putting everything into one query, but I still would like to figure this out. It mystifies me. If cache is deleted when a table is modified, then what happened shouldn't have happened. Has anyone heard of something like this, or have any ideas as to why it may have happened?
Locking the tables is the solution to your problem, I think :)
It's highly unlikely to be a query cache - MySQL is smart enough to invalidate a cache entry if the underlying data set has been modified by another query. If the query cache kept around old stale values long past their expiration, MySQL would be utterly useless.
Do you perhaps have outstanding uncommitted transactions causing this? Without the appropriate locks on the relevant records, your second query could be grabbing stale data quite easily.
Most likely your application has stale data. This is fine, it's how many database applications work, but when you perform your update, instead of doing something like this:
UPDATE account
SET balance = :current_balance - 10
WHERE account_id = 1
You need to do something more like this:
UPDATE account
SET balance = balance - 10
WHERE account_id = 1
That way, you use the current balance from the database, even if somebody changed it in the mean time, instead of relying on stale application data.
If you want to only change the value if no one else has modified it, then you do something like this:
UPDATE account
SET balance = balance - 10
WHERE account_id = 1
AND balance = :current_balance
If the number of affected rows is 1, then you succeeded, the record hadn't changed by someone else. However, if the number of affected rows is 0, then somebody else changed the record. You can then decide what you want to do from there.