The labs on this site need JavaScript to run. The same content is written out below.
Fundamentals › Database
What happens when two transactions try to change the same row?
Many connections can work with a database at the same time. When one connection changes a row with UPDATE, the database puts a lock on that row. A lock is a mark that says "I am changing this row, so other connections must not change it." The SQL we write has no statement for this; the database takes the lock by itself when it receives the UPDATE. The lock is not released until the transaction of that connection ends with COMMIT or ROLLBACK. If another connection tries to change the same row, the database makes that UPDATE wait until the lock is released, and if it waits too long, the database ends it with an error. In this lab you change the row that connection B updates and remove the COMMIT of connection A, run the same SQL, and see when the UPDATE of B runs and what the final balances are.
Checked against real execution · Ran all 4 combinations on H2 2.4.240 with two connections and checked against the results; the rules were checked against the MySQL documentation
CREATE TABLE account (id INT PRIMARY KEY, balance INT); INSERT INTO account VALUES (1, 1000), (2, 1000); -- Connection A (runs first) BEGIN; -- The row being changed gets a lock, and it is kept until this transaction ends UPDATE account SET balance = balance - 100 WHERE id = 1; -- End: make the transaction final and release the lock COMMIT; -- Connection B (runs after the UPDATE of A) BEGIN; -- The database first checks whether another connection holds a lock on the row to be changed UPDATE account SET balance = balance + 50 WHERE id = 1; COMMIT;
It was run and checked against H2 2.4.240 (in-memory database, JDBC), and it was not checked against execution on MySQL. For all 4 combinations, it built the whole SQL on the screen (including the comment lines), split it into the "Connection A" and "Connection B" parts, and ran each part exactly as written, with connection A (the main thread) and connection B (a thread). The order of execution is the BEGIN and UPDATE of A, the BEGIN of B, the UPDATE attempt of B, the COMMIT of A (if there is one), and the COMMIT of B. It created a new in-memory database with a name (DB_CLOSE_DELAY=-1) for each combination so that the two connections look at the same database, and it left the isolation level at the H2 default, READ COMMITTED. What it checked is the three items of the result table. They are whether the UPDATE of B was still not finished after 0.4 seconds and whether it finished later than the time of the COMMIT of A (the times were recorded and compared), the kind of error if there was one (the SQLSTATE of H2 is HYT00, the error code is 50200, and the first line of the message is Timeout trying to lock table "ACCOUNT"; H2 writes the table name in the message even for a row lock), and the balances read on a new connection after all the statements ended. For the combinations that did not end, the read was also made while A held the lock, and the value read after closing the connection of A was the same. The time to wait for a lock was shortened to 1 second on connection B with SET LOCK_TIMEOUT 1000 (the H2 default is 2 seconds, and this setting is not in the code on the screen). The screen shows no time numbers, and the default of MySQL InnoDB is innodb_lock_wait_timeout of 50 seconds. As the facts the step sentences rely on, it also checked by execution the value that another connection read right after A's UPDATE (1000, because it is not final yet), that while B was waiting, a SELECT from another connection did not wait and read 1000 right away (that reads do not wait in H2 was measured separately, and the screen mentions it in only one sentence), that after B waited and ended with an error, auto-commit of B is still off so the transaction is still open, and that the UPDATE of B that ran afterward added 50 to the 900 that A made final and got 950. From the MySQL 8.4 documentation, we opened and checked section 17.7.1 InnoDB Locking (an exclusive lock lets a transaction change or delete the row, and when one transaction holds an exclusive lock on a row, a request by another transaction for a lock on the same row cannot be granted at once and must wait), section 17.7.3 Locks Set by Different SQL Statements in InnoDB (UPDATE ... WHERE sets an exclusive lock on the records it meets in the search, and if it finds one row with a unique index, an index record lock is enough; InnoDB locks are released when the transaction commits or aborts), and innodb_lock_wait_timeout on the page for InnoDB startup options and system variables (innodb-parameters) (default 50 seconds, error 1205 after waiting, and only the statement is rolled back, not the whole transaction). From the H2 documentation, we checked the Advanced page, Multi-Version Concurrency Control (a connection sees only the final data and its own changes, and if connections try to lock or change the same row at the same time, it waits until the lock timeout) and Lock Timeout (an error if the lock cannot be got, and it can be set for each connection). Section 17.7.3 does not directly state in a sentence when the lock of an UPDATE is released; we read it from the general sentence in the same section (InnoDB locks are released when the transaction commits or aborts). We did not check by execution whether MySQL gives the same result for the same SQL. This lab covers only the exclusive lock and the waiting on one row, and does not cover read locks (SELECT ... FOR UPDATE), the differences by isolation level (iso), its use in stock decrement (stock), deadlocks where connections wait for each other (dbdeadlock), or locks on the empty gaps between rows (gaplock).
Previous lab: Transactions · Next lab: Storage and durability
Struck-through text was removed from the first version of the code; highlighted text was added.