1

I am working on a Perl / DBI / DBD / MySQL application. I am using InnoDB with transaction level "repeatable read" and "autocommit = 0". To protect data which I am manipulating against concurrent manipulation by other threads or connections, I am using the following pattern (which is quite common):

$h_DB -> do("START TRANSACTION");

$s_SQL = "SELECT data from Users where ID = ? FOR UPDATE";
$h_ST = $h_DB -> prepare($s_SQL);
$h_ST -> execute($s_ID);

# Do some other stuff

@ar_Row = $h_ST -> fetchrow_array();

# Do data manipulation

$h_DB -> do("COMMIT")

For clarity, I have left away the error handling and the variable declarations, but I think the variable names are self-explaining.

My question is: When exactly are the locks (in this case: the exclusive locks) put onto the respective row? Are they already in place after the execute has run, or are they active no sooner than after the row has been fetched? In other words, are the locks already in place in the section designated "# Do some other stuff"?

Normally, it is easy to answer such questions by doing some research using the MySQL command line tools. But in this case, I think this is not possible for the following reasons:

1) I am not sure if the MySQL client behaves exactly like Perl's DBI / DBD.

2) Of course, the example above is extremely simplified. Actually, I'd like to know when the locks are set when using the pattern above, but locking several hundred rows at once, i.e. several hundred rows matching the WHERE clause.

I don't think I could use the command line client to find out because with large data sets there might be buffering which might be handled differently by Perl and the command line client, and I doubt that I could "simulate" a statement which has been executed in Perl, but whose result rows have not been fetched yet, by using something like "LIMIT 0" in the command line client.

Could somebody give a definitive answer?

Binarus
  • 4,005
  • 3
  • 25
  • 41

2 Answers2

3

$s_SQL = "SELECT data from Users where ID = ? FOR UPDATE";

Executing this will try to acquire a lock on the affected rows and then fetches the data (or waits until it gets an exclusive lock for all affected rows). This is done in order to be thread safe and only return data which is already locked: This way it is guaranteed the server returns the latest data which can't be modified by any other client/thread.

The rows will be locked until commit or rollback.

The behavior in MySQL cli client is the same as the locking is done in the server and not in the client library.

This only holds for MySQL InnoDB tables. Other engines like MyISAM behave differently.

HoldOffHunger
  • 18,769
  • 10
  • 104
  • 133
MrTux
  • 32,350
  • 30
  • 109
  • 146
  • Just for my understanding: h_ST -> execute($s_ID) on its own already sets the locks. Did I get this right? You are saying that it also fetches the data, but where does it fetch to? I suppose to some internal buffer on the server side? – Binarus Mar 07 '16 at 16:56
  • The rows are locked before the data is read by the MySQL server and data is sent to the requestor, this way it is guaranteed the the server returns the latest data which can't be modified by any other client/thread – MrTux Mar 07 '16 at 16:57
  • 1
    @Borodin The question was specifically for MySQL with InnoDB – MrTux Mar 07 '16 at 17:55
  • You wrote: "*The rows will be locked until* `commit` *or* `rollback`.". Shouldn't be that "*The rows will be locked until* `commit`, `rollback`, `disconnect`, *or until the application ends, whichever comes first.*"? – Ωmega Oct 22 '19 at 20:28
  • disconnect is a implicit rollback – MrTux Oct 22 '19 at 20:49
1

Rather than

$h_DB->do("START TRANSACTION");
...
$h_DB->do("COMMIT");

you should probably be calling the DBI methods

$h_DB->begin_work;
...
$h_DB->commit;

The only difference is that you are second-guessing the database driver's behaviour in your own code while making it less portable. The DBI calls rely on the driver knowing how to start and stop transactions for this database, and is the proper way

The essence of database transactions is that everything inside the transaction will go ahead unmolested by any parallel database connections. Some databases will simply lock the entire database for the duration of your transaction, but most will be more optimised and allow reads from anywhere and write to unrelated tables while your transaction is ongoing

The implication of a SELECT with the FOR UPDATE qualifier is that you are asking for a transaction to be started when the read begins, and there is no need to explicitly open a transaction yourself. Once you have obtained the values they will not be changed, and nor will anything else in the database that affects them. Connections that need to access the data that you have locked will be suspended unless they have asked for dirty read functionality

Does that cover everything?

Borodin
  • 126,100
  • 9
  • 70
  • 144
  • `there is no need to explicitly open a transaction yourself` this is not true., at least for MySQL. If there is no transaction, this will not lock anything. – MrTux Mar 07 '16 at 17:53
  • "Connections that need to access the data that you have locked will be suspended": only true for "for update" selects and write operations – MrTux Mar 07 '16 at 17:54
  • @MrTux: Any `SELECT` will be denied access to locked data. Otherwise it's not locked! – Borodin Mar 07 '16 at 18:17
  • Not tue and can easy verified on MySQL cli. normal "select" queries are not blocked (with isolation level repeatable read and innodb), for blocking all select queries you need to issue an additional read lock for the table. – MrTux Mar 07 '16 at 18:24
  • @MrTux: You're very confusing. Now you're saying that "normal" select queries aren't locked. Ever. That's just nonsense, and this will be my final comment – Borodin Mar 07 '16 at 18:33
  • I was just saying a "select for update" in one transaction will not block normal "select" queries affecting the same rows as you're implying with "Connections that need to access the data that you have locked will be suspended" - mysql with repeadable read will just return the data was already committed. – MrTux Mar 07 '16 at 18:38
  • 1
    @Binarus Re. "ALL other sorts of SELECT, UPDATE and DELETE will block" Consistent reads (i.e. plain SELECTs) will not block. See http://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html – ThisSuitIsBlackNot Mar 07 '16 at 19:00
  • I also added a hint for the first part 1 was talking about: http://pastebin.com/KKqBp5gB – MrTux Mar 07 '16 at 19:01
  • Yeah, that's true ;) So, "Any SELECT" was a big too exhaustive. – MrTux Mar 07 '16 at 19:04
  • Just add this little detail, so that people can't missunderstand it and we're all happy! ;) - and we can remove this little conversation lateron... – MrTux Mar 07 '16 at 19:07
  • @Borodin "Any SELECT will be denied access to locked data. Otherwise it's not locked!" Not true. According to the [docs](http://dev.mysql.com/doc/refman/5.7/en/innodb-locking-reads.html), for `SELECT ... FOR UPDATE` "Other transactions are blocked from updating those rows, from doing SELECT ... LOCK IN SHARE MODE, or from reading the data in certain transaction isolation levels. *Consistent reads ignore any locks set on the records that exist in the read view.*" In other words, plain SELECTs don't block, they read from a snapshot. – ThisSuitIsBlackNot Mar 07 '16 at 19:09
  • If you do SELECT ... FOR UPDATE, you will get an exclusive lock (write lock), meaning that other SELECT ... LOCK IN SHARE MODE, UPDATE and DELETE will block. If you do SELECT ... LOCK IN SHARE MODE, you get a non-exclusive lock (read lock), meaning that other reading SELECT ... LOCK IN SHARE MODE will not block, but all other UPDATE, DELETE and SELECT ... FOR UPDATE will block. This is true provided all operations are wrapped within transactions with isolation level and autocommit as described in my initial post, and it IMHO has nothing to do with additional read locks for the table. – Binarus Mar 07 '16 at 19:11
  • @MrTux, ThisSuitIsBlackNot: Deleted all my comments and made a new correct one. – Binarus Mar 07 '16 at 19:12