Mysql隔離級別與鎖
--一、隔離級別 4種模式
1、序列化讀(SERIALIZABLE)
保證可序列化的排程,讀資料加表的共享鎖,寫資料加表的排它鎖,降低併發,影響效率
2、未提交讀(READ-UNCOMMITTED)
允許讀未提交的資料
可能會造成:幻讀、不可重複讀、髒讀
3、已提交讀(READ-COMMITTED) (Oracle預設方式)
只允許讀已提交的事務,但不可要求重複讀(在一個事務兩次讀取同一個資料項期間 讀取的資料可以發生變化,當另一個事務更新了該資料並提交時)
可能會造成:幻讀、不可重複讀
4、可重複讀(REPEATABLE-READ) (Mysql預設方式)
只允許讀已提交的事務,在一個事務讀取同一個資料項期間 讀取的都是事務剛開始看到的資料,不會發生變化
1)髒讀:一個事務讀到了其他事務還沒有提交的資料
2)幻讀:一個事務讀到了其他事務新增的資料(insert)
3)不可重複讀:一個事務讀到了其他事務針對舊資料的修改記錄(update、delete)
--當前設定為 手動提交 mysql> show variables like 'autocommit'; +---------------+-------+ | Variable_name | Value | +---------------+-------+ | autocommit | OFF | +---------------+-------+ 1 row in set (0.00 sec) 1)髒讀: 隔離級別為 未提交讀(READ-UNCOMMITTED) --設定隔離級別為 未提交讀(READ-UNCOMMITTED) mysql> set session tx_isolation='READ-UNCOMMITTED'; Query OK, 0 rows affected (0.01 sec) mysql> show variables like 'tx_isolation'; +---------------+------------------+ | Variable_name | Value | +---------------+------------------+ | tx_isolation | READ-UNCOMMITTED | +---------------+------------------+ 1 row in set (0.00 sec) --session1 建立測試表 mysql> create table test_dirty_read_table (id int ,name varchar(5),score int); Query OK, 0 rows affected (0.03 sec) mysql> insert into test_dirty_read_table values (1,'aaa',75),(2,'bbb',80),(3,'ccc',88); Query OK, 3 rows affected (0.00 sec) Records: 3 Duplicates: 0 Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.00 sec) --session2 讀取test_dirty_read_table測試表 有資料 mysql> select * from test_dirty_read_table; +------+------+-------+ | id | name | score | +------+------+-------+ | 1 | aaa | 75 | | 2 | bbb | 80 | | 3 | ccc | 88 | +------+------+-------+ 3 rows in set (0.00 sec) --session1 插入一條id為4的記錄,但不提交 mysql> insert into test_dirty_read_table values (4,'ddd',60); Query OK, 1 row affected (0.00 sec) --session2 出現髒讀,讀到了session1還未提交的資料 mysql> select * from test_dirty_read_table; +------+------+-------+ | id | name | score | +------+------+-------+ | 1 | aaa | 75 | | 2 | bbb | 80 | | 3 | ccc | 88 | | 4 | ddd | 60 | +------+------+-------+ 4 rows in set (0.00 sec) 2)幻讀: 隔離級別為 未提交讀(READ-UNCOMMITTED) 或者 已提交讀(READ-COMMITTED) --這裡設定隔離級別為 已提交讀(READ-COMMITTED) mysql> set session tx_isolation='READ-COMMITTED'; Query OK, 0 rows affected (0.00 sec) mysql> show variables like 'tx_isolation'; +---------------+----------------+ | Variable_name | Value | +---------------+----------------+ | tx_isolation | READ-COMMITTED | +---------------+----------------+ 1 row in set (0.00 sec) --session1 建立測試表 mysql> create table test_phantom_problem_table (id int ,name varchar(5),score int); Query OK, 0 rows affected (0.01 sec) mysql> insert into test_phantom_problem_table values (1,'aaa',75),(2,'bbb',80),(3,'ccc',88); Query OK, 3 rows affected (0.00 sec) Records: 3 Duplicates: 0 Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.00 sec) --session2 開始一個事務 ,讀取 test_phantom_problem_table 測試表 有資料 mysql> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) mysql> select * from test_phantom_problem_table; +------+------+-------+ | id | name | score | +------+------+-------+ | 1 | aaa | 75 | | 2 | bbb | 80 | | 3 | ccc | 88 | +------+------+-------+ 3 rows in set (0.00 sec) --session1 開始一個事務,插入一條id為4的記錄,提交 mysql> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) mysql> insert into test_phantom_problem_table values (4,'ddd',60); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) --session2 還是在當前事務中,但讀取 test_phantom_problem_table 測試表 出現了id為4的資料, 出現幻讀,讀到了session1新增的資料 mysql> select * from test_phantom_problem_table; +------+------+-------+ | id | name | score | +------+------+-------+ | 1 | aaa | 75 | | 2 | bbb | 80 | | 3 | ccc | 88 | | 4 | ddd | 60 | +------+------+-------+ 4 rows in set (0.00 sec) 3)不可重複讀:與幻讀類似,不過是update和delete操作 4)可重複讀: 隔離級別為 可重複讀(REPEATABLE-READ) --設定隔離級別為 可重複讀(REPEATABLE-READ) mysql> set session tx_isolation='REPEATABLE-READ'; Query OK, 0 rows affected (0.00 sec) mysql> show variables like 'tx_isolation'; +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | tx_isolation | REPEATABLE-READ | +---------------+-----------------+ 1 row in set (0.00 sec) --session1 建立測試表 mysql> create table test_repeatable_read_table (id int ,name varchar(5),score int); Query OK, 0 rows affected (0.02 sec) mysql> insert into test_repeatable_read_table values (1,'aaa',75),(2,'bbb',80),(3,'ccc',88); Query OK, 3 rows affected (0.00 sec) Records: 3 Duplicates: 0 Warnings: 0 mysql> commit; Query OK, 0 rows affected (0.00 sec) --session2 開始一個事務 ,讀取 test_repeatable_read_table測試表 有資料 mysql> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) mysql> select * from test_repeatable_read_table; +------+------+-------+ | id | name | score | +------+------+-------+ | 1 | aaa | 75 | | 2 | bbb | 80 | | 3 | ccc | 88 | +------+------+-------+ 3 rows in set (0.00 sec) --session1 開始一個事務,插入一條id為4的記錄,提交 mysql> START TRANSACTION; Query OK, 0 rows affected (0.00 sec) mysql> insert into test_repeatable_read_table values (4,'ddd',60); Query OK, 1 row affected (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec) --session2 還是在當前事務中,但讀取 test_repeatable_read_table 測試表時, 每次都是事務剛開始看到的資料,不會發生變化 mysql> select * from test_repeatable_read_table; +------+------+-------+ | id | name | score | +------+------+-------+ | 1 | aaa | 75 | | 2 | bbb | 80 | | 3 | ccc | 88 | +------+------+-------+ 3 rows in set (0.00 sec) --但有兩種方式可以檢視到session1修改的資料 --(1)可以for update讀取最新版本號, 可以檢視到session1修改的資料 mysql> select * from test_repeatable_read_table for update; +------+------+-------+ | id | name | score | +------+------+-------+ | 1 | aaa | 75 | | 2 | bbb | 80 | | 3 | ccc | 88 | | 4 | ddd | 60 | +------+------+-------+ 4 rows in set (0.00 sec) --(2)退出當前事務後(commit),可以檢視到session1修改的資料 mysql> commit; Query OK, 0 rows affected (0.00 sec) mysql> select * from test_repeatable_read_table; +------+------+-------+ | id | name | score | +------+------+-------+ | 1 | aaa | 75 | | 2 | bbb | 80 | | 3 | ccc | 88 | | 4 | ddd | 60 | +------+------+-------+ 4 rows in set (0.00 sec)
--二、鎖 3種模式
1、Record Lock 索引上加鎖,鎖住單個記錄行key (隔離級別為 READ-COMMITTED的情況下 只有Record Lock鎖;隔離級別為REPEATABLE-READ的情況下,主鍵和唯一索引都降級為此Record Lock鎖)
2、Gap Lock 間隙鎖,鎖定一個範圍 但不包括記錄本身
3、Next-Key Lock Gap Lock + Record Lock 鎖定一個範圍 且包括記錄本身 (隔離級別為 REPEATABLE-READ的情況下 預設為Next-Key Lock 鎖,同樣也是Mysql預設方式)
鎖型別:共享鎖,排它鎖 (這裡測試用以下作為共享鎖,排它鎖的測試)
select ......lock in share mode 產生共享鎖
select ......for update 產生排它鎖
1、Record Lock 鎖
1)隔離級別為 READ-COMMITTED的情況下
--設定隔離級別為 已提交讀(READ-COMMITTED)
mysql> set session tx_isolation='READ-COMMITTED';
Query OK, 0 rows affected (0.00 sec)
--當前的隔離級別
mysql> show variables like 'tx_isolation';
+---------------+----------------+
| Variable_name | Value |
+---------------+----------------+
| tx_isolation | READ-COMMITTED |
+---------------+----------------+
1 row in set (0.00 sec)
--session1 建立測試表
mysql> create table test_record_lock_noindex_table (id int ,name varchar(5),score int);
Query OK, 0 rows affected (0.07 sec)
mysql> insert into test_record_lock_noindex_table values (1,'aaa',75),(2,'bbb',80),(3,'ccc',88);
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
--檢視當前資料
mysql> select * from test_record_lock_noindex_table;
+------+------+-------+
| id | name | score |
+------+------+-------+
| 1 | aaa | 75 |
| 2 | bbb | 80 |
| 3 | ccc | 88 |
+------+------+-------+
3 rows in set (0.00 sec)
--更新無索引的一行資料
mysql> update test_record_lock_noindex_table set score=100 where name='aaa';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--執行計劃為全表
mysql> explain update test_record_lock_noindex_table set score=100 where name='aaa';
+----+-------------+--------------------------------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------------------------------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | UPDATE | test_record_lock_noindex_table | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 100.00 | Using where |
+----+-------------+--------------------------------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set (0.03 sec)
--session2更新相同表 不同行 ,成功執行,不發生表鎖
mysql> update test_record_lock_noindex_table set score=100 where name='bbb';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--session1提交
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
--session2提交
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
--session2檢視修改後的資料 正常
mysql> select * from test_lock_noindex_table_commit;
+------+------+-------+
| id | name | score |
+------+------+-------+
| 1 | aaa | 100 |
| 2 | bbb | 100 |
| 3 | ccc | 88 |
+------+------+-------+
3 rows in set (0.03 sec)
2)隔離級別為REPEATABLE-READ的情況下,主鍵和唯一索引都降級為此Record Lock鎖
--設定隔離級別為 可重複讀(REPEATABLE-READ)
mysql> set session tx_isolation='REPEATABLE-READ';
Query OK, 0 rows affected (0.00 sec)
--當前的隔離級別
mysql> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec)
--session1 建立測試表,name欄位為唯一索引
mysql> create table test_record_lock_ukindex_table (id int ,name varchar(5),score int, unique key uk_name(name));
Query OK, 0 rows affected (0.08 sec)
mysql> insert into test_record_lock_ukindex_table values (1,'aaa',75),(2,'bbb',80),(3,'ccc',88);
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
--檢視當前資料
mysql> select * from test_record_lock_ukindex_table;
+------+------+-------+
| id | name | score |
+------+------+-------+
| 1 | aaa | 75 |
| 2 | bbb | 80 |
| 3 | ccc | 88 |
+------+------+-------+
3 rows in set (0.00 sec)
--更新有唯一索引的一行資料
mysql> update test_record_lock_ukindex_table set score=100 where name='aaa';
Query OK, 1 row affected (0.04 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--執行計劃為索引範圍掃描
mysql> explain update test_record_lock_ukindex_table set score=100 where name='aaa';
+----+-------------+--------------------------------+------------+-------+---------------+---------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------------------------------+------------+-------+---------------+---------+---------+-------+------+----------+-------------+
| 1 | UPDATE | test_record_lock_ukindex_table | NULL | range | uk_name | uk_name | 8 | const | 1 | 100.00 | Using where |
+----+-------------+--------------------------------+------------+-------+---------------+---------+---------+-------+------+----------+-------------+
1 row in set (0.00 sec)
--session2更新相同表 不同行 ,成功執行,不發生表鎖
mysql> update test_record_lock_ukindex_table set score=100 where name='bbb';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--session1提交
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
--session2提交
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
--session2檢視修改後的資料 正常
mysql> select * from test_record_lock_ukindex_table;
+------+------+-------+
| id | name | score |
+------+------+-------+
| 1 | aaa | 100 |
| 2 | bbb | 100 |
| 3 | ccc | 88 |
+------+------+-------+
3 rows in set (0.00 sec)
3、Next-Key Lock 隔離級別為 REPEATABLE-READ的情況下 預設為Next-Key Lock 鎖
1)普通索引的情況下,會產生範圍鎖
--設定隔離級別為 可重複讀(REPEATABLE-READ)
mysql> set session tx_isolation='REPEATABLE-READ';
Query OK, 0 rows affected (0.00 sec)
--當前的隔離級別
mysql> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec)
--session1 建立測試表,name為普通索引
mysql> create table test_nextkey_lock_index_table (id int ,name varchar(5),score int,index idx_name(name) );
Query OK, 0 rows affected (0.25 sec)
mysql> insert into test_nextkey_lock_index_table values (1,'aaa',75),(2,'bbb',80),(3,'ccc',88);
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
--檢視當前資料
mysql> select * from test_nextkey_lock_index_table;
+------+------+-------+
| id | name | score |
+------+------+-------+
| 1 | aaa | 75 |
| 2 | bbb | 80 |
| 3 | ccc | 88 |
+------+------+-------+
3 rows in set (0.00 sec)
--更新有普通索引的一行資料 name為aaa, 此時發生Next-Key Lock鎖,其中gap鎖定了 (- ∞,aaa) 和(aaa,bbb) 的範圍,record lock 鎖定本身的aaa
mysql> update test_nextkey_lock_index_table set score=100 where name='aaa';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--執行計劃為全表掃
mysql> explain update test_nextkey_lock_index_table set score=100 where name='aaa';
+----+-------------+-------------------------------+------------+-------+---------------+----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------------------------+------------+-------+---------------+----------+---------+-------+------+----------+-------------+
| 1 | UPDATE | test_nextkey_lock_index_table | NULL | range | key_name | key_name | 8 | const | 1 | 100.00 | Using where |
+----+-------------+-------------------------------+------------+-------+---------------+----------+---------+-------+------+----------+-------------+
1 row in set (0.02 sec)
--session2更新相同表 不同行 ,沒有產生鎖,因為name欄位為bbb和ccc 不在 (- ∞,aaa) 和(aaa,bbb) 的範圍 也不是aaa本身
mysql> update test_nextkey_lock_index_table set score=100 where name='bbb';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> update test_nextkey_lock_index_table set score=100 where name='ccc';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--session2 已共享鎖的方式讀session1修改的資料失敗,因為name欄位為aaa的 被record lock鎖定了本身
mysql> select * from test_nextkey_lock_index_table where name='aaa' lock in share mode ;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
--session2插入一條name欄位位於aaa和bbb之間的資料abb 失敗,因為abb位於(aaa,bbb) 的範圍
mysql> insert into test_nextkey_lock_index_table values (2,'abb',80);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
-- session2插入一條name欄位大於bbb的資料bcc 成功,因為bcc不在 (- ∞,aaa) 和(aaa,bbb) 的範圍 也不是aaa本身
mysql> insert into test_nextkey_lock_index_table values (2,'bcc',80);
Query OK, 1 row affected (0.00 sec)
2)無索引的情況下,會發生表鎖
--設定隔離級別為 可重複讀(REPEATABLE-READ)
mysql> set session tx_isolation='REPEATABLE-READ';
Query OK, 0 rows affected (0.00 sec)
--當前的隔離級別
mysql> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec)
--session1 建立測試表
mysql> create table test_nextkey_lock_noindex_table (id int ,name varchar(5),score int);
Query OK, 0 rows affected (0.06 sec)
mysql> insert into test_nextkey_lock_noindex_table values (1,'aaa',75),(2,'bbb',80),(3,'ccc',88);
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
--檢視當前資料
mysql> select * from test_nextkey_lock_noindex_table;
+------+------+-------+
| id | name | score |
+------+------+-------+
| 1 | aaa | 75 |
| 2 | bbb | 80 |
| 3 | ccc | 88 |
+------+------+-------+
3 rows in set (0.00 sec)
--更新無索引的一行資料
mysql> update test_nextkey_lock_noindex_table set score=100 where name='aaa';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--執行計劃為全表掃
mysql> explain update test_nextkey_lock_noindex_table set score=100 where name='aaa';
+----+-------------+---------------------------------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+---------------------------------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | UPDATE | test_nextkey_lock_noindex_table | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 100.00 | Using where |
+----+-------------+---------------------------------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set (0.00 sec)
--session2更新相同表 不同行 ,發生表鎖
mysql> update test_nextkey_lock_noindex_table set score=100 where name='bbb';
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> update test_nextkey_lock_noindex_table set score=100 where name='ccc';
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
4、MDL鎖(meta data lock):保證表中的元資料不被修改
--session1 建立測試表
mysql> create table test_mdl_table (id int ,name varchar(5),score int);
Query OK, 0 rows affected (0.09 sec)
mysql> insert into test_mdl_table values (1,'aaa',75),(2,'bbb',80),(3,'ccc',88);
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
--session1 開始一個事務,並查詢資料
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from test_mdl_table;
+------+------+-------+
| id | name | score |
+------+------+-------+
| 1 | aaa | 75 |
| 2 | bbb | 80 |
| 3 | ccc | 88 |
+------+------+-------+
3 rows in set (0.00 sec)
--session2 開始一個事務 ,DDL操作發現被鎖住
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> alter table test_mdl_table add class varchar(5);
鎖等待
--session3 檢視當前的程序,有MDL鎖
mysql> show full processlist;
+----+-----------------+-----------+-------+---------+------+---------------------------------+--------------------------------------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-----------------+-----------+-------+---------+------+---------------------------------+--------------------------------------------------+
| 1 | event_scheduler | localhost | NULL | Daemon | 1530 | Waiting on empty queue | NULL |
| 4 | root | localhost | flydb | Sleep | 146 | | NULL |
| 5 | root | localhost | flydb | Query | 76 | Waiting for table metadata lock | alter table test_mdl_table add class varchar(5) |
| 6 | root | localhost | flydb | Query | 0 | starting | show full processlist |
+----+-----------------+-----------+-------+---------+------+---------------------------------+--------------------------------------------------+
4 rows in set (0.02 sec)