MySQL 事務隔離級別解析和實戰

學霸的一天發表於2019-03-13

1、MySQL 隔離界別檢視

  • 檢視回話隔離級別
SELECT @@session.tx_isolation;
SELECT @@tx_isolation;
複製程式碼
  • 檢視系統隔離級別
SELECT @@global.tx_isolation;
複製程式碼

2、MySQL 隔離級別修改

  • MySQL 預設的隔離級別是可重複讀( REPEATABLE READ)
  • 在 my.inf 檔案中修改隔離級別
transaction-isolation = {READ-UNCOMMITTED | READ-COMMITTED | REPEATABLE-READ | SERIALIZABLE}
複製程式碼

image.png

  • 使用者可以用SET TRANSACTION語句改變單個會話或者所有新進連線的隔離級別。語法如下:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
複製程式碼

image.png

3、MySQL 四種隔離級別

隔離級別 髒讀(Dirty Read) 不可重複讀(NonRepeatable Read) 幻讀(Phantom Read)
未提交讀(Read uncommitted) 可能 可能 可能
已提交讀(Read committed) 不可能 可能 可能
可重複讀(Repeatable read) 不可能 不可能 可能
可序列化(Serializable ) 不可能 不可能 不可能

3.1、未提交讀

  • 允許髒讀,也就是說一個事務有可能讀到另一個事務未提交的資料

3.2、已提交讀

  • 只能讀到已經提交的資料,Oracle等多數資料庫的預設隔離級別

3.3、可重複讀

  • 存在幻讀

3.4、可序列化

  • 完全序列化,每次讀都需要獲得表級共享鎖,讀寫阻塞

4、例項操作

  • 新建一個表用來測試
CREATE TABLE `test` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(50) NOT NULL DEFAULT '' COMMENT '名字',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='測試';
複製程式碼

4.1、髒讀

  • 當一個事務訪問一個資料,並且進行了修改。另一個事務讀到了被修改的資料,並且使用了這個資料。
  • sessoin1 (插入資料但不提交事務)
mysql> SELECT @@session.tx_isolation; // 查詢會話隔離級別可重複讀
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
1 row in set, 1 warning (0.04 sec)

mysql> SELECT @@tx_isolation; //查詢系統隔離級別為可重複讀
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)

mysql> start transaction; //開啟事務
Query OK, 0 rows affected (0.00 sec)

mysql> insert into test(`name`) values("qiu"); //插入資料成功,此時事務還沒有提交
Query OK, 1 row affected (0.01 sec)

mysql> select * from test;
+----+------+
| id | name |
+----+------+
|  1 | qiu  |
+----+------+
1 row in set (0.00 sec)
複製程式碼
  • sessoin2(可重複讀,證明不會出現髒讀)
mysql> SELECT @@session.tx_isolation; //會話隔離級別為可重複讀
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
1 row in set, 1 warning (0.00 sec)

mysql> SELECT @@tx_isolation; //系統隔離級別為可重複讀
+-----------------+
| @@tx_isolation  |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)

mysql> select * from test; //查詢不到 sessoin1 未提交的資料,不會出現髒讀現象
Empty set (0.00 sec)
複製程式碼
  • sessoin3(為提交讀出現髒讀現象)
mysql> SELECT @@session.tx_isolation;//會話隔離級別為未提交讀
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-UNCOMMITTED       |   --------讀到了 session1 未提交的資料,出現髒讀現象
+------------------------+
1 row in set, 1 warning (0.00 sec)

mysql> select * from test;//讀到了 session1 未提交的資料,此為髒讀
+----+------+
| id | name |
+----+------+
|  1 | qiu  |
+----+------+
1 row in set (0.00 sec)
複製程式碼

4.2、不可重複讀

  • 在同一個事務內,多次讀取同一個資料,此時事務還沒有完成。另一個事務在前一個事務兩次讀取之間修改了資料,由於修改了資料,前一個事務讀到的資料不一樣,因此稱為不可重複讀。

  • sessoin1(事務內第一次讀)

mysql> SELECT @@session.tx_isolation; //隔離級別為提交讀
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-COMMITTED         |
+------------------------+
1 row in set, 1 warning (0.00 sec)

mysql> start transaction; //開啟事務
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test; //開啟事務內的第一次查詢
+----+------+
| id | name |
+----+------+
|  2 | qiu  |
+----+------+
1 row in set (0.01 sec)
複製程式碼
  • sessoin2
mysql> SELECT @@session.tx_isolation;//隔離級別為可重複讀
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
1 row in set, 1 warning (0.00 sec)

mysql> start transaction; //開啟事務
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test;
+----+------+
| id | name |
+----+------+
|  2 | qiu  |
+----+------+
1 row in set (0.01 sec)

mysql> insert into test(`name`) values ("hello"); //在sessoin1第一次查詢後修改了資料
Query OK, 1 row affected (0.01 sec)

mysql> select * from test;
+----+-------+
| id | name  |
+----+-------+
|  2 | qiu   |
|  3 | hello |
+----+-------+
2 rows in set (0.00 sec)

mysql> commit; //提交事務
Query OK, 0 rows affected (0.01 sec)
複製程式碼
  • sessoin1(事務內第二次讀)
mysql> select * from test; //在事務內第二次讀,讀到了 sessoin2 提交的資料
+----+-------+
| id | name  |
+----+-------+
|  2 | qiu   |   ---------------READ-COMMITTED級別出現不可重複讀現象
|  3 | hello |
+----+-------+
2 rows in set (0.00 sec)
複製程式碼

4.3、可重複讀

  • 驗證 REPEATABLE-READ 級別下的可重複讀
  • sessoin1(事務內第一次讀)
mysql> SELECT @@session.tx_isolation;//隔離級別為可重複讀
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
1 row in set, 1 warning (0.00 sec)

mysql> start transaction; //開啟事務
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test;
+----+-------+
| id | name  |
+----+-------+
|  2 | qiu   |
|  3 | hello |
+----+-------+
2 rows in set (0.00 sec)
複製程式碼
  • sessoin2
mysql> SELECT @@session.tx_isolation; //隔離級別為可重複讀
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
1 row in set, 1 warning (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test;
+----+-------+
| id | name  |
+----+-------+
|  2 | qiu   |
|  3 | hello |
+----+-------+
2 rows in set (0.00 sec)

mysql> insert into test (`name`) values ("hi"); //sessoin1 第一次讀之後改變資料
Query OK, 1 row affected (0.01 sec)

mysql> select * from test;
+----+-------+
| id | name  |
+----+-------+
|  2 | qiu   |
|  3 | hello |
|  4 | hi    |
+----+-------+
3 rows in set (0.00 sec)

mysql> commit; //提交事務
Query OK, 0 rows affected (0.00 sec)
複製程式碼
  • sessoin1(事務內第二次讀)
mysql> select * from test;
+----+-------+
| id | name  |
+----+-------+
|  2 | qiu   |
|  3 | hello | -------------sessoin1 沒有讀到 sessoin2 提交的資料,出現可重複讀現象
+----+-------+
2 rows in set (0.00 sec)
複製程式碼

4.4、幻讀

  • 第一個事務對錶中的所有資料進行修改,第二個事務往表裡面插入一條資料。此時第一個事務發現表中還有未修改的資料,好像出現了幻覺一樣。

  • 幻讀現象1:

session1:                                                   session2:
mysql> select @@global.tx_isolation, @@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation  |
+-----------------------+-----------------+
| REPEATABLE-READ       | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set, 2 warnings (0.00 sec)
                                                            mysql> start transaction;     
                                                            Query OK, 0 rows affected (0.00 sec)

mysql> start transaction;                             
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test;
Empty set (0.00 sec)

                                                            mysql> insert into test (`id`, `name`) values (1, "hi~~~");
                                                            Query OK, 1 row affected (0.00 sec)

mysql> select * from test;
Empty set (0.00 sec)

                                                            mysql> commit;
                                                            Query OK, 0 rows affected (0.01 sec)

mysql> select * from test;
Empty set (0.00 sec)

mysql> insert into test (`id`, `name`) values (1, "hello");
ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY'
//what fuck ???剛剛查詢,告訴我沒有資料。等我插入的時候就告訴我主鍵衝突了。此乃幻讀現象
複製程式碼
  • 幻讀現象2:
session1:                               session2:
mysql> start transaction;
Query OK, 0 rows affected (0.01 sec)

                                       mysql> start transaction;
                                       Query OK, 0 rows affected (0.01 sec)

mysql> select * from test;
+----+-------+
| id | name  |
+----+-------+
|  1 | hi~~~ |
+----+-------+
1 row in set (0.00 sec)

                                      mysql> insert into test (`id`, `name`) values (2, "hello~~");
                                      Query OK, 1 row affected (0.01 sec)

mysql> select * from test;
+----+-------+
| id | name  |
+----+-------+
|  1 | hi~~~ |
+----+-------+
1 row in set (0.04 sec)

                                      mysql> commit;
                                      Query OK, 0 rows affected (0.00 sec)

mysql> select * from test;
+----+-------+
| id | name  |
+----+-------+
|  1 | hi~~~ |
+----+-------+
1 row in set (0.00 sec)

mysql> update test set name = "up";
Query OK, 2 rows affected (0.01 sec)
Rows matched: 2  Changed: 2  Warnings: 0
//what fuck ???剛出查詢不是隻有一條資料嗎?怎麼更新了兩條。此乃幻讀現象

mysql> select * from test;
+----+------+
| id | name |
+----+------+
|  1 | up   |
|  2 | up   |
+----+------+
2 rows in set (0.00 sec)
複製程式碼
  • innodb_locks_unsafe_for_binlog:設定InnoDB是否在搜尋和索引掃描中使用間隙鎖(gap locking)
  • 當隔離級別是可重複讀,且禁用innodb_locks_unsafe_for_binlog的情況下,在搜尋和掃描index的時候使用的next-key locks可以避免幻讀。

4.5、加鎖

  • 通過加鎖來防止幻讀
session1:                                               session2:

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

                                                        mysql> start transaction;
                                                        Query OK, 0 rows affected (0.00 sec)

//加鎖鎖住了 id <= 1 的範圍
mysql> select * from test where id <= 1 for update;
+----+------+
| id | name |
+----+------+
|  1 | up   |
+----+------+
1 row in set (0.18 sec)
                                                        //id 不在鎖內,允許插入
                                                        mysql> insert into test (`id`, `name`) values (3, "lock");
                                                        Query OK, 1 row affected (0.15 sec)

mysql> select * from test;
+----+------+
| id | name |
+----+------+
|  1 | up   |
|  2 | up   |
+----+------+
2 rows in set (0.01 sec)
                                                        //id = 1 已經加了寫鎖,事務等待鎖釋放
                                                        mysql> insert into test(`id`, `name`) values (1, "lock");
                                                        ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

mysql> select * from test;
+----+------+
| id | name |
+----+------+
|  1 | up   |
|  2 | up   |
+----+------+
2 rows in set (0.00 sec)

                                                        mysql> commit;
                                                        Query OK, 0 rows affected (0.00 sec)

mysql> select * from test;
+----+------+
| id | name |
+----+------+
|  1 | up   |
|  2 | up   |
+----+------+
2 rows in set (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test;
+----+------+
| id | name |
+----+------+
|  1 | up   |
|  2 | up   |
|  3 | lock |  ------------session2 插入的資料
+----+------+
4 rows in set (0.00 sec)
複製程式碼
  • 通過加鎖讀來獲得其他事務提交的結果
session1:                               session2:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

                                        mysql> start transaction;
                                        Query OK, 0 rows affected (0.00 sec)

mysql> select * from test;
+----+------+
| id | name |
+----+------+
|  1 | up   |
+----+------+
4 rows in set (0.01 sec)

                                        mysql> insert into test (`id`, `name`) values (7, "hello");
                                        Query OK, 1 row affected (0.00 sec)

                                        mysql> commit;
                                        Query OK, 0 rows affected (0.08 sec)


mysql> select * from test;
+----+------+
| id | name |
+----+------+
|  1 | up   |
+----+------+
4 rows in set (0.00 sec)

mysql> select * from test;
+----+------+
| id | name |
+----+------+
|  1 | up   |
+----+------+
4 rows in set (0.00 sec)

//讀到了 session2 提交的資料
mysql> select * from test lock in share mode;
+----+-------+
| id | name  |
+----+-------+
|  1 | up    |
|  7 | hello |
+----+-------+
5 rows in set (0.00 sec)

//讀到了 session2 提交的資料
mysql> select * from test for update;
+----+-------+
| id | name  |
+----+-------+
|  1 | up    |
|  7 | hello |
+----+-------+
5 rows in set (0.00 sec)

//讀不到 session2 提交的資料
mysql> select * from test;
+----+------+
| id | name |
+----+------+
|  1 | up   |
+----+------+
4 rows in set (0.00 sec)
複製程式碼
  • 可重複讀和提交讀本身就是互相矛盾的。保證了可重複讀,就讀不到其他事務的提交;保證了提交讀,兩次讀取的資料可能會出現不一致。
  • MySQL 預設的隔離級別是可重複讀,可通過加鎖讀來獲取其他事務的提交。
  • MySQL 的可重複讀並不能避免幻讀,可通過加 Next-Key Lock 來避免幻讀現象。
  • Next-Key Lock:鎖定一個範圍,包括記錄本身。

總結

  • 每種資料庫隔離級別都解決了一個問題。資料庫隔離級別依次增強,效能也依次變差。大部分環境中使用 READ-COMMITTED 是可行的。

參考文獻

關注公眾號

  • 大家可以關注我的公眾號【學霸的一天】,更多有趣、有用的知識等你來發現
    宣傳二維碼.png

相關文章