I'm trying to understand how next-key locking works with serializable isolation level in MySql 5.6.
There are next explanation in documentation:
"SELECT ... FROM is a consistent read, reading a snapshot of the database and setting no locks unless the transaction isolation level is set to SERIALIZABLE. For SERIALIZABLE level, the search sets shared next-key locks on the index records it encounters. However, only an index record lock is required for statements that lock rows using a unique index to search for a unique row."
So, If I have next table:
table simple2
(
id int primary key,
val int,
index (val)
);
insert into simple2 values (1,1),(2,2),(3,3),(5,5),(7,7),(15,15);
Then:
Transaction 1:
set transaction isolation level serializable;
start transaction;
select * from simple2 where val = 7;
Transaction 2:
set transaction isolation level serializable;
start transaction;
insert into simple2 values (10, 10); //blocked
I don't understand why I can't add (10,10).
Because, according to doc explanation, only row index and gap before index should be blocked by Transaction 1: (5, 7].
Related
We have simple table:
CREATE TABLE t1 (
c1 INT UNSIGNED NOT NULL AUTO_INCREMENT,
c2 INT,
PRIMARY KEY (c1)
)
ENGINE=INNODB
1st transaction:
set transaction isolation level read uncommitted;
start transaction;
insert into t1 set c2 = 1;
For some reason INSERT creates IX lock on t1 table. I expected no locks. Especially i did not expect only IX lock, without X lock on some row.
2nd transaction:
set transaction isolation level read uncommitted;
start transaction;
update t1 set c2 = 2 where c1 = 1;
Here UPDATE statement creates IX lock on table t1 and create X lock on updated row. As i expected in read uncommited isolation level. But this X lock is waiting for X lock that belongs to 1st transaction. Yes, somehow it appears right now.
Why INSERT statement creates X lock on inserted row? What purpose? Where can i get information about such behaviour?
Why it happened in such weird way? Why INSERT created only IX lock and X lock appeared later?
It is normal behaviour for any database to lock in exclusive mode (X) any row or page it modifies. Also, READ UNCOMMITTED referrers to SELECT, nothing to do with INSERT.
To release the lock either COMMIT or ROLLBACK the first transaction.
As Locks Set by Different SQL Statements in InnoDB, INSERT section said,
INSERT sets an exclusive lock on the inserted row.
Prior to inserting the row, a type of gap lock called an insert intention gap lock is set.
If a duplicate-key error occurs, a shared lock on the duplicate index record is set.
I wonder why set a shared lock, as the subsequent doc said, this might lead to a deadlock. Why not simply request an exclusive lock?
This use of a shared lock can result in deadlock should there be multiple sessions trying to insert the same row if another session already has an exclusive lock.
The below example copied from the MySQL document which demonstrate how to trigger the deadlock.
CREATE TABLE t1 (i INT, PRIMARY KEY (i)) ENGINE = InnoDB;
Now suppose that three sessions perform the following operations in order:
Session 1:
START TRANSACTION;
INSERT INTO t1 VALUES(1);
Session 2:
START TRANSACTION;
INSERT INTO t1 VALUES(1);
Session 3:
START TRANSACTION;
INSERT INTO t1 VALUES(1);
Session 1:
ROLLBACK;
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.
CREATE TABLE 'test'.'t1' (
'id' INT NULL);
CREATE TABLE 'test'.'t2' (
'id' INT NULL);
INSERT INTO test.t1 VALUES(1);
INSERT INTO test.t2 VALUES(1);
example1:
sqlConnection1:
SET autocommit = 0;
START TRANSACTION;
UPDATE test.t1 set id = 1 WHERE id = 2;
sqlConnection2:
SET autocommit = 0;
START TRANSACTION;
LOCK TABLES test.t2 WRITE,test.t1 WRITE;
COMMIT;
UNLOCK TABLES;
sqlConnection1:
UPDATE test.t2 set id = 1 where id = 2;
COMMIT;
sqlConnection2:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restart transaction
example2:
sqlConnection1:
SET autocommit = 0;
START TRANSACTION;
UPDATE test.t1 set id = 1 WHERE id = 1;
sqlConnection2:
SET autocommit = 0;
START TRANSACTION;
LOCK TABLES test.t2 WRITE,test.t1 WRITE;
COMMIT;
UNLOCK TABLES;
sqlConnection1:
UPDATE test.t2 set id = 1 where id = 1;
COMMIT;
sqlConnection1:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restart transaction
example3:
deadlock not found after mysql5.6
question:
What causes the results to differ in three examples
There are several points in this problem.
When InnoDB tries to update a record it will put a RECORD LOCK or a GAP LOCK onto the range it is going to update. For records that are existent, InnoDB will put a record lock, otherwise a gap lock. For example, in example 1, InnoDB will add a gap lock, and in this case, it adds a [2,∞) gap lock onto the table.
MySQL's LOCK TABLES command actually implements in MySQL itself but not in InnoDB. Although MySQL's LOCK TABLE has a "deadlock-free design" in other table-lock-only storage engines like MyISAM, it cannot avoid the deadlocks in InnoDB since InnoDB has row-level locks. Fortunately, the InnoDB has a deadlock detection mechanism that can detect when an InnoDB row lock and a MySQL table lock occur together on the same table and do what you see -- rollback a "lighter" transaction to avoid deadlock, which is so-called "victim".
The InnoDB engine will generally decide the transaction with fewer rows affected to be the victim. In example 1, connection 1 puts two GAP LOCKS on rows [2,∞) for two tables, while connection 2 only acquires two WRITE LOCKS and does not get anything done. Thus, connection 2 is "lighter" and being selected as the "victim". In example 2, connection 1 only has two RECORD LOCKS while connection 2 still has two WRITE LOCKS, and connection 1 basically do not do anything. Thus InnoDB decides that connection 1 uses fewer resources (i.e. locks in this case), and chooses connection 1 as the victim.
Since MySQL 5.6, a new lock wait mechanism has been introduced to MySQL. When MySQL cannot acquire a table lock, it will wait as long as the interval lock-wait-timeout specified. quote from the MySQL document:
This variable[lock-wait-timeout] specifies the timeout in seconds for attempts to acquire metadata locks. The permissible values range from 1 to 31536000 (1 year). The default is 31536000.
This timeout applies to all statements that use metadata locks. These include DML and DDL operations on tables, views, stored procedures, and stored functions, as well as LOCK TABLES, FLUSH TABLES WITH READ LOCK, and HANDLER statements.
Thus, MySQL will not acquire a table lock when it sees InnoDB already has a lock on the table it is going to manipulate and wait until InnoDB gets its work done. For further information, you can refer to MySQL document(The following documents are for 8.0 version, but the basic principles are the same):
https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html
https://dev.mysql.com/doc/refman/8.0/en/innodb-deadlock-detection.html
I am wondering if it is necessary to use locking in most likely concurrent environment and how in following case. Using MySQL database server with InnoDB engine
Let's say I have a table
CREATE TABLE `A` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`m_id` INT NOT NULL, -- manual id
`name` VARCHAR(10)
) ENGINE=INNODB;
And the procedure
CREATE PROCEDURE `add_record`(IN _NAME VARCHAR(10))
BEGIN
DECLARE _m_id INT;
DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK;
START TRANSACTION;
SELECT (`m_id` + 1) INTO _m_id FROM `A` WHERE `id` = (SELECT MAX(`id`) FROM `A`);
INSERT INTO `A`(`m_id`, `name`) VALUES(_m_id, _NAME);
COMMIT;
END$$
Like you see the fact is that I am increasing m_id manually and concurrent transactions are most likely happening. I can't make my mind if database might become in inconsistent state. Also using FOR UPDATE and LOCK IN SHARE MODE has no point in this situation as transaction deals with new records and has nothing to do with updates on a specific row. Further LOCK TABLES are not allowed in stored procedures and is quite insufficient.
So, my question is how to avoid inconsistent state in marked scenario if it is possible to happen actually. Any advice will be grateful
transaction deals with new records and has nothing to do with updates on a specific row
Such a new record is known as a phantom:
phantom
A row that appears in the result set of a query, but not in the result set of an earlier query. For example, if a query is run twice within a transaction, and in the meantime, another transaction commits after inserting a new row or updating a row so that it matches the WHERE clause of the query.
This occurrence is known as a phantom read. It is harder to guard against than a non-repeatable read, because locking all the rows from the first query result set does not prevent the changes that cause the phantom to appear.
Among different isolation levels, phantom reads are prevented by the serializable read level, and allowed by the repeatable read, consistent read, and read uncommitted levels.
So to prevent phantoms from occurring on any statement, one can simply set the transaction isolation level to be SERIALIZABLE. InnoDB implements this using next-key locks, which not only locks the records that your queries match but also locks the gaps between those records.
The same can be accomplished on a per-statement basis by using locking reads, such as you describe in your question: LOCK IN SHARE MODE or FOR UPDATE (the former allows concurrent sessions to read the matching records while the lock is in place, whilst the latter does not).
First, a sequence table
CREATE TABLE m_id_sequence (
id integer primary key auto_increment
);
and then alter the procedure to get the next m_id from the sequence table
DELIMITER $$
CREATE PROCEDURE `add_record`(IN _NAME VARCHAR(10))
BEGIN
DECLARE _m_id INT;
DECLARE EXIT HANDLER FOR SQLEXCEPTION ROLLBACK;
START TRANSACTION;
INSERT INTO m_id_sequence VALUES ();
SET _m_id = LAST_INSERT_ID();
INSERT INTO `A`(`m_id`, `name`) VALUES(_m_id, _NAME);
COMMIT;
END$$
DELIMITER ;