Hi everybody,
I'm running a website where users can adopt virtual pets. There is a per-user adoption limit on each pet. So, for example, you can adopt one of our pets a maximum of 10 times. At the moment we do something like this:
CREATE TABLE `num_adopted` (
`petid` int(11) NOT NULL,
`userid` int(11) NOT NULL,
`total` int(11) unsigned NOT NULL,
PRIMARY KEY (`petid`,`userid`),
) ENGINE=InnoDB
Then when somebody adopts a pet we:
START TRANSACTION;
SELECT total
FROM num_adopted
WHERE petid=? AND userid=?
FOR UPDATE
We check the total against our limit
. If they've already adopted up to the limit
, we ROLLBACK
and tell the user. Otherwise we:
INSERT INTO num_adopted (petid, userid, total)
VALUES (?, ?, 1)
ON DUPLICATE KEY UPDATE total=total+1
Then we add a row in a different table to record the new pet for them, and finally:
COMMIT
This has to work flawlessly under a very high level of concurrency.
Now, if the limit is 10 and the user has already adopted limit-1 pets, I can see that the FOR UPDATE
annotation in the first SELECT
guarantees that the total will be locked so that multiple concurrent adoptions won't end up seeing the total as limit-1
(which would allow the user to surpass their limit
). The first adoption will see total=limit-1 and complete successfully, the rest will block. Eventually, the rest will see total=limit and refuse to adopt another pet.
But what if the limit
=1 and the total
=0? Could multiple adoption transactions see no row in the num_adopted table (so total=0) at the same time, allowing the user to adopt more than one pet? It's not clear that FOR UPDATE
can lock a row that doesn't exist. If so, would this altered scheme solve the problem?
START TRANSACTION;
INSERT INTO num_adopted (petid, userid, total)
VALUES (?, ?, 1)
ON DUPLICATE KEY UPDATE total=total+1;
SELECT total
FROM num_adopted
WHERE petid=? AND userid=?
Check if total>limit, if so, ROLLBACK
. Otherwise record the adopted pet and COMMIT
.