一線互聯網公司是怎麼處理mysql事務以及隔離級別的?

1. 簡介

MySQL 事務主要用於處理操作量大,複雜度高的數據。比如說,在人員管理系統中,你刪除一個人員,你即需要刪除人員的基本資料,也要刪除和該人員相關的信息,如信箱,文章等等,這樣,這些數據庫操作語句就構成一個事務!

在 MySQL 中只有使用了 Innodb 數據庫引擎的數據庫或表才支持事務。

事務處理可以用來維護數據庫的完整性,保證成批的 SQL 語句要麼全部執行,要麼全部不執行。

事務用來管理 insert,update,delete 語句

2. 事務的基本要素ACID

一般來說,事務是必須滿足4個條件(ACID)::原子性(Atomicity,或稱不可分割性)、一致性(Consistency)、隔離性(Isolation,又稱獨立性)、持久性(Durability)。

原子性:一個事務(transaction)中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。

一致性:在事務開始之前和事務結束以後,數據庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及後續數據庫可以自發性地完成預定的工作。比如A向B轉賬,不可能A扣了錢,B卻沒收到。

隔離性:數據庫允許多個併發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務併發執行時由於交叉執行而導致數據的不一致。事務隔離分爲不同級別,包括讀未提交(Read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和串行化(Serializable)。

持久性:事務完成後,事務對數據庫的所有更新將被保存到數據庫,不能回滾。

3. 事務的併發問題

髒讀:允許讀取未提交的髒數據,比如:事務A讀取了事務B更新的數據,然後B回滾操作,那麼A讀取到的數據是髒數據;

不可重複讀:如果你在時間點t1讀取了一些記錄,在t2時間點也想重新讀取一樣的數據時,這些記錄可能已經被改變,或者消失,比如:事務 A 多次讀取同一數據,事務 B 在事務A多次讀取的過程中,對數據作了更新並提交,導致事務A多次讀取同一數據時,結果不一致。

幻讀:系統管理員A將數據庫中所有學生的成績從具體分數改爲ABCDE等級,但是系統管理員B就在這個時候插入了一條具體分數的記錄,當系統管理員A改結束後發現還有一條記錄沒有改過來,就好像發生了幻覺一樣,這就叫幻讀。

不可重複讀的和幻讀很容易混淆,不可重複讀側重於修改,幻讀側重於新增或刪除。解決不可重複讀的問題只需鎖住滿足條件的行,解決幻讀需要鎖表。

4. 事務的4種隔離級別

爲了解決上面事務的併發問題,sql標準提出了4種隔離級別,下面是每種隔離級別能夠解決的問題對應關係:

事務隔離級別髒讀不可重複讀幻讀

read-uncommittedNNN

read-committedYNN

repeatable-read(default)YYN

serializableYYY

mysql的默認隔離級別是Repeatable。

查看系統級和會話級的隔離級別:

mysql> select @@global.tx_isolation,@@tx_isolation;

+-----------------------+-----------------+| @@global.tx_isolation | @@tx_isolation |

+-----------------------+-----------------+| REPEATABLE-READ | REPEATABLE-READ |

+-----------------------+-----------------+1 row in set, 2 warnings (0.01 sec)

下面用例子說明一下這四種隔離級別:

1. read-uncommitted

更改隔離級別爲read-uncommitted:

mysql> set session tx_isolation='read-uncommitted';

Query OK, 0 rows affected, 1 warning (0.01 sec)

mysql> select @@tx_isolation;

+------------------+| @@tx_isolation |

+------------------+| READ-UNCOMMITTED |

+------------------+1 row in set, 1 warning (0.00 sec)

首先,準備一些測試數據:

mysql> select * from user;

+----+----------+------+| id | name | age |

+----+----------+------+| 1 | zhangsan | 25 |

| 2 | lisi | 26 |

| 3 | wangwu | 27 |

| 4 | nike | 28 |

| 5 | lucy | 29 |

+----+----------+------+5 rows in set (0.00 sec)

客戶端A:

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from user;

+----+----------+------+| id | name | age |

+----+----------+------+| 1 | zhangsan | 25 |

| 2 | lisi | 26 |

| 3 | wangwu | 27 |

| 4 | nike | 28 |

| 5 | lucy | 29 |

+----+----------+------+5 rows in set (0.00 sec)

客戶端B:

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

mysql> update user set age=52 where name='zhangsan';

Query OK, 1 row affected (0.00 sec)

Rows matched: 1 Changed: 1 Warnings: 0

客戶端A:

mysql> select * from user;

+----+----------+------+| id | name | age |

+----+----------+------+| 1 | zhangsan | 52 |

| 2 | lisi | 26 |

| 3 | wangwu | 27 |

| 4 | nike | 28 |

| 5 | lucy | 29 |

+----+----------+------+5 rows in set (0.00 sec)

可以看到,客戶端B的事務還沒有提交,在客戶端A的事務內就看到了更新的數據。

客戶端B:

mysql> rollback;

Query OK, 0 rows affected (0.02 sec)

客戶端A:

mysql> select * from user;

+----+----------+------+| id | name | age |

+----+----------+------+| 1 | zhangsan | 25 |

| 2 | lisi | 26 |

| 3 | wangwu | 27 |

| 4 | nike | 28 |

| 5 | lucy | 29 |

+----+----------+------+5 rows in set (0.00 sec)

由於客戶端B的事務回滾,客戶端A讀取的數據又變成了原始數據,因此上一次客戶端A讀取的數據變成了髒數據。在併發事務中,這種讀取數據的問題就叫做髒讀。

2. read-commited

要解決上面的問題,可以把數據庫的隔離級別改成read-commited。

客戶端A:

mysql> set session tx_isolation='read-committed';

Query OK, 0 rows affected, 1 warning (0.00 sec)

再按照上述步驟測試一下,發現髒讀問題已經解決,在事務B沒有commit之前,事務A不會讀取到髒數據。

下面演示一下不可重複讀的問題。

客戶端A:

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from user;

+----+----------+------+| id | name | age |

+----+----------+------+| 1 | zhangsan | 25 |

| 2 | lisi | 26 |

| 3 | wangwu | 27 |

| 4 | nike | 28 |

| 5 | lucy | 29 |

+----+----------+------+5 rows in set (0.00 sec)

客戶端B:

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

mysql> update user set age=52 where name='zhangsan';

Query OK, 1 row affected (0.01 sec)

Rows matched: 1 Changed: 1 Warnings: 0

mysql> commit;

Query OK, 0 rows affected (0.01 sec)

客戶端A:

mysql> select * from user;

+----+----------+------+| id | name | age |

+----+----------+------+| 1 | zhangsan | 52 |

| 2 | lisi | 26 |

| 3 | wangwu | 27 |

| 4 | nike | 28 |

| 5 | lucy | 29 |

+----+----------+------+5 rows in set (0.00 sec)

mysql> rollback;

Query OK, 0 rows affected (0.00 sec)

可以看到在客戶端B的事務提交前後,客戶端A讀取到的數據不一樣了。也就是重複讀取相同的數據有不同的結果。

個人理解,髒讀也屬於不可重複讀的一個範疇,只是髒讀在事務B未提交之前就導致兩次讀取數據不一樣,不可重複讀在事務B提交之後導致兩次讀取結果不一樣。還有就是髒讀之所以叫髒數據,是因爲這條數據沒有真正的在數據庫中保存過,這是事務的一箇中間狀態。而不可重複讀兩次讀取不同的數據實際都已經存在於數據庫中了。

3. repeatable-read

要解決不可重複讀的問題,可以將數據庫的隔離級別改爲repeatable-read。

客戶端A:

mysql> set session tx_isolation='repeatable-read';

Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> select @@tx_isolation;

+-----------------+| @@tx_isolation |

+-----------------+| REPEATABLE-READ |

+-----------------+1 row in set, 1 warning (0.00 sec)

再按照上述步驟測試一下,發現不可重複讀的問題已經解決,在事務B沒有commit之後,事務A讀取的數據沒有變化,關閉這個事務重新打開一個事務纔會讀到更新後的數據。

下面演示一下幻讀的問題。

客戶端A:

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

mysql> select * from user;

+----+----------+------+| id | name | age |

+----+----------+------+| 1 | zhangsan | 25 |

| 2 | lisi | 26 |

| 3 | wangwu | 27 |

| 4 | nike | 28 |

| 5 | lucy | 29 |

+----+----------+------+5 rows in set (0.00 sec)

客戶端B:

mysql> start transaction;

Query OK, 0 rows affected (0.00 sec)

mysql> insert into user values(6,'shell',30);

Query OK, 1 row affected (0.01 sec)

mysql> commit;

Query OK, 0 rows affected (0.00 sec)

客戶端A:

mysql> insert into user values(6,'shell',30);

ERROR 1062 (23000): Duplicate entry '6' for key 'PRIMARY'

mysql> select * from user;

+----+----------+------+| id | name | age |

+----+----------+------+| 1 | zhangsan | 25 |

| 2 | lisi | 26 |

| 3 | wangwu | 27 |

| 4 | nike | 28 |

| 5 | lucy | 29 |

+----+----------+------+5 rows in set (0.00 sec)

可以看到,對於客戶端A來說,命名沒有id爲6的數據,但是還是插入失敗,再查詢一下還是沒有啊,感覺產生了幻覺,這就是幻讀問題。幻讀和不可重複讀的區別在於,不可重複讀重點是更新後的讀取,幻讀重點是插入刪除這些操作,解決不可重複讀,只需要對對應的數據行加鎖就行了。解決幻讀則需要對整張表加鎖。

如果兩個事務B沒有提交之前事務A執行插入會如何呢?我們來看一下:

客戶端A:

mysql> insert into user values(6,'shell',30);

可以看到如果插入的id和事務B一樣,那麼事務A的操作會被阻塞,直到事務B提交commit後,纔會報錯:

客戶端A:

mysql> insert into user values(8,'svn',32);

ERROR 1062 (23000): Duplicate entry '8' for key 'PRIMARY'

如果客戶端A插入到的數據事務B不衝突,那麼會立即返回成功:

客戶端A:

mysql> insert into user values(9,'svn',32);

Query OK, 1 row affected (0.00 sec)

4. serializable

要解決幻讀的問題,可以將數據庫的隔離級別改爲serializable。

客戶端A:

mysql> set session tx_isolation='serializable';

Query OK, 0 rows affected, 1 warning (0.00 sec)

再按照上述步驟測試一下,發現幻讀的問題已經解決,當事務B嘗試insert的事務,被阻塞,也就是事務A將整張表鎖住了。直到事務A提交commit以後,事務B的操作纔會返回結果。

在這種情況下,只允許一個事務在執行,其它事務必須等待這個事務執行完後才能執行。沒有併發,只是單純的串行。

5. 總結

mysql中默認事務隔離級別是可重複讀時並不會鎖住讀取到的行;

事務隔離級別爲讀提交時,寫數據只會鎖住相應的行;

事務隔離級別爲可重複讀時,如果有索引(包括主鍵索引)的時候,以索引列爲條件更新數據,會存在間隙鎖間隙鎖、行鎖、下一鍵鎖的問題,從而鎖住一些行;如果沒有索引,更新數據時會鎖住整張表;

事務隔離級別爲串行化時,讀寫數據都會鎖住整張表;

隔離級別越高,越能保證數據的完整性和一致性,但是對併發性能的影響也越大,魚和熊掌不可兼得啊。對於多數應用程序,可以優先考慮把數據庫系統的隔離級別設爲Read Committed,它能夠避免髒讀取,而且具有較好的併發性能。儘管它會導致不可重複讀、幻讀這些併發問題,在可能出現這類問題的個別場合,可以由應用程序採用悲觀鎖或樂觀鎖來控制。

推薦一個交流學習羣:697579751 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多:

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章