MySQL 'REPEATABLE READ' transaction unexpected behavior - mysql

The default isolation level of MySQL transaction is 'Repeatable Read'.
According to another stackoverflow question(
Difference between read commit and repeatable read)
"Repeatable read is a higher isolation level, that in addition to the guarantees of the read committed level, it also guarantees that any data read cannot change, if the transaction reads the same data again, it will find the previously read data in place, unchanged, and available to read."
Here is my test database;
mysql> select * from people;
+------+---------+
| name | howmany |
+------+---------+
| alex | 100 |
| bob | 100 |
+------+---------+
slow.sql
START TRANSACTION;
SELECT #new_val := howmany FROM people WHERE name = 'alex';
SELECT SLEEP(10);
SET #new_val = #new_val - 5;
UPDATE people SET howmany = #new_val WHERE name = 'alex';
COMMIT;
fast.sql
START TRANSACTION;
SELECT #new_val := howmany FROM people WHERE name = 'alex';
-- SELECT SLEEP(10);
SET #new_val = #new_val - 5;
UPDATE people SET howmany = #new_val WHERE name = 'alex';
COMMIT;
If I run slow.sql, and before it returns I run fast.sql multiple times.
fast.sql will print 95, 90, 85....
I think repeatable read isolation level should make fast.sql fail to run or I misunderstand 'repeatable read'.
I'm running MySQL 5.7 from Ubuntu 16.10.
Thanks very much.

If not wrong then Repeatable Read talks about consistent reads within the same transaction and not with other transaction. From MySQL Documentation
REPEATABLE READ
This is the default isolation level for InnoDB. Consistent reads
within the same transaction read the snapshot established by the first
read. This means that if you issue several plain (nonlocking) SELECT
statements within the same transaction, these SELECT statements are
consistent also with respect to each other.

Repeatable read isolation level guarantees consistency within a single transaction. You are executing multiple transactions. For the behaviour your expecting you would need to look into locking reads. See here for more info. https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html#isolevel_repeatable-read

If some data changes during your transaction by process outside of it, it won't have any effect on data read in said transaction.
I don't see how fast.sql would fail to run because of this isolation level (or any isolation).

Related

MySQL SET user variable locks rows and doesn't obey REPEATABLE READ

I've encountered an undocumented behavior of "SET #my_var = (SELECT ..)" inside a transaction:
The first one is that it locks rows ( depends whether it is a unique index or not ).
Example -
START TRANSACTION;
SET #my_var = (SELECT id from table_name where id = 1);
select trx_rows_locked from information_schema.innodb_trx;
ROLLBACKL;
The output is 1 row locked, which is strange, it shouldn't gain a reading lock.
Also, the equivalent statement SELECT id INTO #my_var won't produce a lock.
It can lead to a deadlock in case of an UPDATED after the SET statement ( for 2 concurrent requests )
In REPEATABLE READ -
The SELECT inside the SET statement gets a new snapshot of the data, instead of using the original SNAPSHOT.
SESSION 1:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START transaction;
SELECT data FROM my_table where id = 2; # Output : 2
SESSION 2:
UPDATE my_table set data = 3 where id = 2 ;
SESSION 1:
SET #data = (SELECT data FROM my_table where id = 2);
SELECT #data; # Output : 3, instead of 2
ROLLBACK;
However, I would expect that #data will contain the original value from the first snapshot ( 2 ).
If I use SELECT data into #data from my_table where id = 2 then I will get the expected value - 2;
Do you have an idea what is the source of the different behavior of SET = (SELECT ..) compared to SELECT data INTO #var FROM .. ?
Thanks.
Correct — when you SELECT in a context where you're copying the results into a variable or a table, it implicitly works as if you had used a locking read SELECT ... FOR SHARE.
This means it places a shared lock on the rows examined, and it also means that the statement reads only the most recently committed version of rows, as if your transaction were in READ-COMMITTED isolation level.
I'm not sure why SELECT ... INTO #var does not do the same kind of implicit locking in MySQL 8.0. My memory is that in older versions of MySQL it did do locking in that query form. I've searched the manual for an explanation but I can't find one yet.
Other cases that implicitly lock the rows examined by SELECT, and therefore reads data as if you transaction is READ-COMMITTED:
INSERT INTO <table> SELECT ...
UPDATE or DELETE multi-table, even if you don't update or delete a given table, the rows joined become locked.
SELECT inside a trigger

Understanding InnoDB Repeatable Read isolation level snapshots

I have the following table:
CREATE TABLE `accounts` (
`name` varchar(50) NOT NULL,
`balance` int NOT NULL,
PRIMARY KEY (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
And it has two accounts in it. "Bob" has a balance of 100. "Jim" has a balance of 200.
I run this query to transfer 50 from Jim to Bob:
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SELECT * FROM accounts;
SELECT SLEEP(10);
SET #bobBalance = (SELECT balance FROM accounts WHERE name = 'bob' FOR UPDATE);
SET #jimBalance = (SELECT balance FROM accounts WHERE name = 'jim' FOR UPDATE);
UPDATE accounts SET balance = #bobBalance + 50 WHERE name = 'bob';
UPDATE accounts SET balance = #jimBalance - 50 WHERE name = 'jim';
COMMIT;
While that query is sleeping, I run the following query in a different session to set Jim's balance to 500:
UPDATE accounts SET balance = 500 WHERE name = 'jim';
What I thought would happen is that this would cause a bug. The transaction would set Jim's balance to 150, because the first read in the transaction (before the SLEEP) would establish a snapshot in which Jim's balance is 200, and that snapshot would be used in the later query to get Jim's balance. So we would subtract 50 from 200 even though Jim's balance has actually been changed to 500 by the other query.
But that's not what happens. Actually, the end result is correct. Bob has 150 and Jim has 450. But I don't understand why this is.
The MySQL documentation says about Repeatable Read:
This is the default isolation level for InnoDB. Consistent reads within the same transaction read the snapshot established by the first read. This means that if you issue several plain (nonlocking) SELECT statements within the same transaction, these SELECT statements are consistent also with respect to each other. See Section 15.7.2.3, “Consistent Nonlocking Reads”.
So what am I missing here? Why does it seem like the SELECT statements in the transaction are not all using a snapshot established by the first SELECT statement?
The repeatable-read behavior only works for non-locking SELECT queries. It reads from the snapshot established by the first query in the transaction.
But any locking SELECT query reads the latest committed version of the row, as if you had started your transaction in READ-COMMITTED isolation level.
A SELECT is implicitly a locking read if it's involved in any kind of SQL statement that modifies data.
For example:
INSERT INTO table2 SELECT * FROM table1 WHERE ...;
The above locks examined rows in table1, even though the statement is just copying them to table2.
SET #myvar = (SELECT ... FROM table1 WHERE ...);
This is also copying a value from table1, into a variable. It locks the examined row in table1.
Likewise SELECT statements that are invoked in a trigger, or as part of a multi-table UPDATE or DELETE, and so on. Anytime the SELECT is part of a larger statement that modifies any data (in a table or in a variable), it locks the rows examined by the SELECT.
And therefore it's a locking read, and behaves like an UPDATE with respect to which row version it reads.

Deadlock in transaction with isolation level serializable

I was trying to understand how locking works with isolation levels. I have gone through this question but can not understand flow given blow
Here i am starting two transactions in different terminals and reading same row in them. As i try to update them both the terminal keeps waiting for the update. No other query is running apart from this
Here are the series of steps i did
conn1: START TRANSACTION;
conn1: SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
conn2: START TRANSACTION;
conn2: SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
conn1: SELECT * from users WHERE id = 1;
conn2: SELECT * from users WHERE id = 1;
conn1: UPDATE users set name = 'name' WHERE id = 1; waiting...
conn2: UPDATE users set name = 'name' WHERE id = 1; waiting...
Here is my first question
Here i want to understand why both the connections are waiting and if they are who has the lock to update the row ?
If i change above steps to
conn1: START TRANSACTION;
conn1: SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
conn2: START TRANSACTION;
conn2: SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
conn1: UPDATE users set name = 'name' WHERE id = 1;
conn2: SELECT * from users WHERE id = 1; waiting...
conn1: commit
conn2: updated results
In this case the difference is i can see conn1 has the lock and until it either commits or rollback the changes all other request will be waiting and will get updated results if conn1 committed
Here is my second question
Is this the correct way if i want to lock a row and if locked i want other connections to wait(even for read) till this lock releases(commit or rollback) or i should use for update clause
DB - Mysql 5.7
As mysql documentation on SERIALIZABLE isolation level says:
This level is like REPEATABLE READ, but InnoDB implicitly converts all plain SELECT statements to SELECT ... LOCK IN SHARE MODE
The clause on autocommit does not apply here, since you explicitly start a transaction.
This means that in the first scenario both transactions obtain a shared lock on the same record. Then the first transaction (T1) tries to execute an update, which needs an exclusive lock. That cannot be granted, since T2 holds a shared lock. Then T2 tries to update, but cannot due to T1 holding a shared lock.
Whether you use an atomic update or a select ... for update statement to lock records, depends on the application logic you need to apply. If you need to fetch the record's data an do some complex calculations with those before updating the record, the use the select ... for update approach. Otherwise, go for the atomic update.

MySql: correct transaction isolation level to use for incrementing a number

Suppose there is a table in DB like this:
id code
a8e09395-771c-4c6b-bb49-4921eeaf3927 2018-1
726b1390-b502-11e8-96f8-529269fb1459 2018-2
7a7ac7a6-b502-11e8-96f8-529269fb1459 2018-3
81758ea6-b502-11e8-96f8-529269fb1459 2019-1
Suppose there are multiple clients writing to this table.
For the "code" column, we want to ensure that it follows a strict "year-nth of this year" pattern.
What is the correct transaction isolation level that all the clients should be using?
----update----2018-09-11 11:31:24---------
START TRANSACTION;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET #code = (SELECT CODE
FROM hey
WHERE id = 123);
UPDATE hey
SET code = #code + 1
WHERE id = 123;
COMMIT;
Did a quick test with the above transaction.
I started 2 consoles, then ran the above code, I ran them both to the line that reads the code column.
Then make one of them update the code column, it'll wait for lock.
Then I make the other one update the code column, it'll deadlock and rollback.
Now the first one's lock is resolved and it can commit.
So looks like this transaction isolation can prevent them from stepping on each other's toes, correct?
You need to solve this with locking.
It doesn't matter what transaction isolation level.
In one session:
mysql1> begin;
mysql1> select max(code) from mytable where code like '2018-%' for update;
Output:
+-----------+
| max(code) |
+-----------+
| 2017-3 |
+-----------+
In a second session, try the same select for update. It pauses, waiting on the lock held by the first session's transaction.
mysql2> begin;
mysql2> select max(code) from mytable where code like '2018-%' for update;
(waits for lock)
In the first session, use the value the select returned to calculate the next value. Then insert the next row and commit.
mysql1> insert into mytable values (uuid(), '2018-4');
mysql1> commit;
The second session returns immediately after the commit in the first session. It correctly returns the new max code:
+-----------+
| max(code) |
+-----------+
| 2017-4 |
+-----------+
Now the second session has the lock, and it can insert the next row, without worrying that any other session sneaks in between the select and the insert.
Any transaction isolation will work if you use FOR UPDATE to lock the rows and ensure the transactions work serially.

set transaction isolation level repeatable read is giving dead locks

I am updating some records in session 1 with open transaction -
begin transaction
update aa
set name = 'harry1'
where name = 'harry'
As you can see that commit/rollback transaction is not issued. Now i try to read the records from another session session 2.
set transaction isolation level repeatable read
select * from aa
Now Isolation level - repeatable read should give me the same old value that was there before update statement in session 1 that should be harry and not harry1.. please correct me if i am wrong.
But when I try to read record in session 2 while transaction is still open in session 1 I get deadlock..can someone tell me why repeatable read is not working and is behaving like read committed .
REPEATABLE READ is same as READ COMMITTED but in addition share locks are retained on rows read for the duration of the transaction. In other words any row that is read cannot be modified by another connection until the transaction commits or rolls back.
So your query on the session 2 is waiting for either commit or rollback on the session 1.
while data is being read in the begin tran block, no other transactions are allow update, preventing a dirty read. Therefore a non-repeatable read does not occur. Once the commit has occurred that an update to the customer table is allow by a concurrent process.
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRAN
--first read
SELECT first_name, last_name from customer WHERE customer_id=5
---second read
SELECT first_name, last_name from customer WHERE customer_id=5
COMMIT TRAN