I have a medium-traffic website with a MySQL database, and I'm seeing occasional Duplicate entry errors when 2+ concurrent requests try to update the same row.
We use Perl/DBI to access the database.
Perl'ish pseudo-code:
$dbh->begin_work;
my $row = $dbh->selectrow_hashref( "SELECT * FROM mytable WHERE id=$some_id" );
if ( defined($row) ) {
# ... do stuff; uses $row ...
$dbh->do( "UPDATE mytable SET ... WHERE id=$some_id" );
}
else {
# ... do other stuff, different from above ...
$dbh->do( "INSERT INTO mytable SET id=$some_id, ... " );
sleep 30; # added for emphasis
}
$dbh->commit;
The id
column is unique
, obviously.
To repeat/rephrase the issue, suppose request #1 comes along. Row is inserted. While SLEEPing, request #2 comes along; $row
is undef because we still haven't committed request #1, so we try to INSERT again, and get the Duplicate entry error.
I understand why this is happening -- because we're not locking. This is the background. The question is how to implement a lock given this background.
Unfortunately, INSERT INTO ... ON DUPLICATE KEY UPDATE...
doesn't work since we're doing slightly different things depending on existence of $row
.
I looked into SELECT ... FOR UPDATE
and SELECT ... LOCK IN SHARE MODE
as described here:
MySQL InnoDB: Difference Between `FOR UPDATE` and `LOCK IN SHARE MODE`
but because we're inserting a new row during request #1, there is no row to lock prior to the insert, that would lock out request #2.
After reading the above link and other resources on the web, I don't really know what to try next that would work reliably, without deadlocks and other scary things like that.
Ideas? Help? Thanks!
As specified here: https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-record-locks
select for update
should solve this problem as it claims to prevent inserting as well, the first request with select for update
should block the second request select for update
with the same id.
Another requirement is that the isolation level has to be at least read committed