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

一、MySQL 隔離界別查看

  • 查看回話隔離級別
SELECT @@session.tx_isolation;
SELECT @@tx_isolation;
複製代碼
  • 查看系統隔離級別
SELECT @@global.tx_isolation;
複製代碼

二、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

三、MySQL 四種隔離級別

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

3.一、未提交讀

  • 容許髒讀,也就是說一個事務有可能讀到另外一個事務未提交的數據

3.二、已提交讀

  • 只能讀到已經提交的數據,Oracle等多數數據庫的默認隔離級別

3.三、可重複讀

  • 存在幻讀

3.四、可串行化

  • 徹底串行化,每次讀都須要得到表級共享鎖,讀寫阻塞

四、實例操做

  • 新建一個表用來測試
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.一、髒讀

  • 當一個事務訪問一個數據,而且進行了修改。另外一個事務讀到了被修改的數據,而且使用了這個數據。
  • 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.二、不可重複讀

  • 在同一個事務內,屢次讀取同一個數據,此時事務尚未完成。另外一個事務在前一個事務兩次讀取之間修改了數據,因爲修改了數據,前一個事務讀到的數據不同,所以稱爲不可重複讀。html

  • sessoin1(事務內第一次讀)mysql

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.三、可重複讀

  • 驗證 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.四、幻讀

  • 第一個事務對錶中的全部數據進行修改,第二個事務往表裏面插入一條數據。此時第一個事務發現表中還有未修改的數據,好像出現了幻覺同樣。sql

  • 幻讀現象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.五、加鎖

  • 經過加鎖來防止幻讀
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
相關文章
相關標籤/搜索