Sql事務有原子性、一致性、隔離性、持久性四個基本特性,要實現徹底的ACID事務,是以犧牲事務的吞吐性能做爲代價的。在有些應用場景中,經過分析業務數據讀寫,使得能夠下降事務的隔離性,容忍相應出現的數據一致性問題,實現事務的高併發、高吞吐、低時延性,這是sql事務優化的最佳實踐。本文對sql標準中隔離性級別定義和可能會出現的問題進行一一介紹,最後經過Mysql數據庫進行相應的演示。html
目錄mysql
- 1. Sql事務特性
- 2. Sql事務特性:原子性
- 3. Sql事務特性:一致性、併發性和隔離性
- 4. Sql隔離級別和問題
- 4.1 髒讀
- 4.2 不可重複讀
- 4.3 幻讀
- 4.4 不可重複讀和幻讀的區別
- 5. 演示
- 5.1 準備工做
- 5.2 事務的原子性
- 5.3 事務的髒讀
- 5.4 事務的不可重複讀
- 5.5 幻讀
- 6. 小結
- 7. 參考資料
1. Sql事務特性
業界經常使用字母縮寫ACID描述Sql事務的四個基本特性,這四個字母分別表明爲,sql
- Atomicity 原子性
- Consistency 一致性
- Isolation 隔離性
- Durability 持久性
下面對這四個特性進行介紹,數據庫
- 原子性:在一個事務中sql的操做,要不成功提交,要不回滾。當一個事務對數據庫有作多項改動,這些改動要不所有一塊兒提交進入數據庫,要不就所有回滾,數據庫無變化。
- 一致性:在事務執行的過程當中,數據庫一直保持一致性的狀態,不管事務是否成功提交或者回滾,或者事務在執行中。當一個事務改動了多個表的記錄,查詢時要不看到全部變化後的新記錄,要不就看到變化前的老記錄,不會出現查詢出新老記錄混合出現的場景。
- 隔離性:各個事務之間相互隔離,互不影響,隔離性是經過數據庫鎖機制實現。要說明的是,隔離性是相對的,在數據庫使用過程當中,爲了實現高併發,會下降事務之間的隔離性,並犧牲必定的數據一致性。更詳細的討論見下文。
- 持久性:事務的執行結果具備持久性,一旦事務提交後,其結果將被安全持久化到存儲設備上,不管遇到電力中斷、系統崩潰、關機、或者其它潛在威脅。
這四個特性中,原子性是事務最基本的特性,現代數據庫都支持完整的原子性事務,而對於一致性、隔離性、持久性,在面對高可用性、高併發、高吞吐時會進行相應的取捨。安全
2. Sql事務特性:原子性
原子性是事務最基本的特性,根據其定義能夠知道事務的執行分爲三個階段,bash
- uncommitted 未提交,當前事務在執行中。
- commited 已提交,當前事務作的改動被數據庫接受,已安全持久化到數據庫存儲中。
- rollback 已回滾,當前事務所作的操做被撤銷,對數據庫無改動。
一個執行中的事務只能以commited/rollback二者狀態之一做爲結束。session
3. Sql事務特性:一致性、併發性和隔離性
數據庫事務中,保持數據一致性是須要代價的,若要保證絕對一致性,則相關聯的事務只能以串行執行(serializability),這是一種嚴格的隔離方式。在這種隔離方式下,有數據關聯性的幾個事務操做,只能一個一個按順序執行,事務的併發被徹底限制,數據庫的事務吞吐將大爲下降,一個寫入操做甚至會被一個只讀查詢操做阻塞,等待讀操做完成以後才能夠進行下一步寫操做。併發
在有些通用場景中,對讀數據的準確性和時效性要求沒有那麼高,但但願有高吞吐量,能快速獲取查詢結果,在數據庫操做高併發的同時,實現低時延性、快速的響應。爲了實現這個目的,數據庫專家提出了不一樣的數據隔離性級別,經過下降事務的隔離性,從而使得數據庫的併發吞吐可以得到最佳的效率。oracle
在sql-1992標準中,對數據庫實現的隔離級別和隔離性提出了相關的規範定義,其中隔離級別包括四種,隔離性按低往高排序分別爲,app
- READ UNCOMMITTED 讀未提交:能夠容許讀未提交的事務數據
- READ COMMITTED 讀提交:只容許讀已提交的事務數據
- REPEATABLE READ 可重複讀:保證讀取的數據不會出現不一致的狀況
- SERIALIZABLE 串行:保證數據讀取和寫入的絕對一致性
現代數據庫基本都實現了上述四個級別的事務隔離配置,供不一樣場景下使用。
4. Sql隔離級別和問題
魚和熊掌不可兼得,面對隔離性和數據一致性,即是這樣的選擇題。追求高併發吞吐,必然低隔離性,數據一致性問題則愈嚴重。瞭解sql不一樣隔離級別定義和相應會出現的一致性問題,是進行隔離性級別優化選擇的前提。
下表對Sql隔離級別和問題進行簡要說明(依據sql-1992標準),
隔離級別 | 髒讀 dirty read | 不可重複讀 non-repeatable read | 幻讀 phantom | 併發吞吐性 |
---|---|---|---|---|
讀未提交 | 可能 | 可能 | 可能 | 高 |
讀提交 | 不會 | 可能 | 可能 | 中等 |
可重複讀 | 不會 | 不會 | 可能 | 低 |
串行 | 不會 | 不會 | 不會 | 串行 |
上表中,有列出三種數據不一致的問題,
- 髒讀
- 不可重複讀
- 幻讀
下面對這三個問題一一進行講解,而後給出mysql數據庫中的三種問題的演示。
4.1 髒讀
在一個事務T1中對某個數據記錄進行了修改。若在事務T1提交以前,T2中此刻讀取這個數據記錄,隨後T1進行了回滾操做,則T2將讀取到一個未提交的無效數據。這個問題就叫作髒讀。
髒讀的問題在於,讀取到錯誤的、無效的數據。
4.2 不可重複讀
在一個事務T1中讀取了某個數據記錄,若此時事務T2對這個數據記錄進行了修改和刪除並提交,隨後T1再嘗試重複讀取同一數據記錄,這個時候T1發現數據有變化(或者發現已經不存在)。這種在一個事務中,重複讀取數據卻獲取到不一致的查詢結果,就叫作不可重複讀的問題。
不可重複讀主要問題在於,在一個事務中同一數據記錄屢次讀取,會有先後不一致的問題(儘管先後讀取的數據都是準確的)。
4.3 幻讀
在一個事務T1中讀取了一系列知足指定查詢條件的數據記錄,若此時事務T2執行一些操做,若T2操做會更新某些數據記錄,而這些數據記錄恰好落入T1事務中的查詢條件,則當T1再次讀取同一查詢條件的數據記錄,發現數據記錄有不同。
幻讀的主要問題在於,在一個事務中數據記錄讀取的準確性依賴查詢條件,其數據集合是當前事務所涉及的數據記錄的超集。
4.4 不可重複讀和幻讀的區別
一個常見的疑問是,不可重複讀和幻讀的區別。從事務的控制角度,不可重複讀針對的是當前事務所操做的數據記錄,幻讀針對的是符合當前事務查詢條件的全部數據記錄,後者是前者的超集。從解決方案來講,對於不可重複讀的問題,只要鎖住當前事務操做的數據記錄便可,或者讀取快照,兩種方法均可以有效地避免先後讀取不一致的問題;而對於幻讀,則須要鎖住全部符合查詢條件的記錄,其範圍是無限擴大的,有時候甚至須要鎖住整張表。
舉個例子來講,下面的sql語句,將狀態爲NEW的記錄進行更新,若表中符合NEW狀態的記錄有5個,
update `order` set `status`='PAID' where `status`='NEW';
則,
- 要解決不可重複讀的問題,只要鎖住當前表中那5條記錄便可,或者留存快照,當前事務不受其它事務影響便可。
- 要解決幻讀的問題,則要鎖住全部可能出現NEW狀態的其它事務操做,包括插入和更新操做。解決幻讀問題,本質問題是須要了解其它事務對數據庫的更新變化,一旦發現對當前事務有影響,則對外部其它事務進行阻塞,保證當前事務的優先執行權。
5. 演示
下面經過Mysql演示Sql的不一樣隔離級別和出現的問題,演示中使用的Mysql版本爲5.7.16。
5.1 準備工做
在數據庫中,執行以下語句,建立測試數據庫和表order。
CREATE SCHEMA IF NOT EXISTS `test` DEFAULT CHARACTER SET utf8mb4; DROP TABLE IF EXISTS `test`.`order` ; CREATE TABLE IF NOT EXISTS `test`.`order` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `name` VARCHAR(64) NOT NULL DEFAULT '未知', `quantity` INT NOT NULL DEFAULT '0', `price` DOUBLE NOT NULL DEFAULT '0.0', `status` VARCHAR(64) NOT NULL DEFAULT 'NEW' COMMENT '訂單狀態:NEW-新訂單,PAID-訂單已付,CLOSE-訂單結束', `date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`)) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='訂單'; INSERT INTO `test`.`order` (`id`, `name`, `quantity`, `price`, `status`) VALUES ('1', 'apple', '1', '5.0', 'NEW'); select * from `test`.`order`;
下面是一些基本的sql事務查詢語句,
- 查詢事務:SELECT * FROM information_schema.INNODB_TRX\G;
- 查詢當前隔離級別:select @@tx_isolation;
- 設置當前隔離級別:set session transaction isolation level read uncommitted;
演示中會啓動兩個sql鏈接,分別爲session1和session2,方便演示兩個session之間的相互影響。
5.2 事務的原子性
事務的提交,
start transaction; update `test`.`order` set `price`='7.0' where `id`='1'; commit;
事務的回滾,回滾後數據的修改被撤銷,
start transaction; update `test`.`order` set `price`='8.0' where `id`='1'; rollback;
5.3 事務的髒讀
請按照下表執行相應的演示步驟,
step | session 1 | session 2 |
---|---|---|
1 | use test; | use test; |
2 | set session transaction isolation level read uncommitted; | |
3 | start transaction; | |
4 | select * from `order`; | |
5 | start transaction; | |
6 | update `order` set `price`='10.0' where `id`='1'; | |
7 | select * from `order`; | |
8 | rollback; | |
9 | select * from `order`; | |
10 | commit; | ; |
其中,
- session-1 中在第2步設置了隔離級別爲:讀未提交。
- session-1 中在第7步讀取到的數據爲session-2中未提交的數據,以後session-2在第8步進行了回滾,使得該數據失效。
請見session-1的輸出,
mysql> set session transaction isolation level read uncommitted; Query OK, 0 rows affected (0.00 sec) mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from `order`; +----+-------+----------+-------+--------+---------------------+ | id | name | quantity | price | status | date | +----+-------+----------+-------+--------+---------------------+ | 1 | apple | 1 | 7 | NEW | 2018-09-13 22:44:29 | +----+-------+----------+-------+--------+---------------------+ 1 row in set (0.00 sec) mysql> select * from `order`; +----+-------+----------+-------+--------+---------------------+ | id | name | quantity | price | status | date | +----+-------+----------+-------+--------+---------------------+ | 1 | apple | 1 | 10 | NEW | 2018-09-13 22:46:49 | +----+-------+----------+-------+--------+---------------------+ 1 row in set (0.00 sec) mysql> select * from `order`; +----+-------+----------+-------+--------+---------------------+ | id | name | quantity | price | status | date | +----+-------+----------+-------+--------+---------------------+ | 1 | apple | 1 | 7 | NEW | 2018-09-13 22:44:29 | +----+-------+----------+-------+--------+---------------------+ 1 row in set (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec)
能夠看到在session-1中第7步第二次查詢時,得到了無效的數據,這就是髒讀。解決髒讀,能夠提升隔離級別到:讀已提交。
set session transaction isolation level read committed;
請見接下來的演示。
5.4 事務的不可重複讀
請按照下表執行相應的演示步驟,
step | session 1 | session 2 |
---|---|---|
1 | use test; | use test; |
2 | set session transaction isolation level read committed; | |
3 | start transaction; | |
4 | select * from `order`; | |
5 | start transaction; | |
6 | update `order` set `price`='11.0' where `id`='1'; | |
7 | select * from `order`; | |
8 | commit; | |
9 | select * from `order`; | ; |
10 | commit; | ; |
其中,
- session-1 中在第2步設置了隔離級別爲:讀已提交。
- session-1 中在第9步讀取到的數據爲session-2中已提交的數據,該數據和前兩次查詢的數據不一致。
請見session-1的輸出,
mysql> set session transaction isolation level read committed; Query OK, 0 rows affected (0.00 sec) mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from `order`; +----+-------+----------+-------+--------+---------------------+ | id | name | quantity | price | status | date | +----+-------+----------+-------+--------+---------------------+ | 1 | apple | 1 | 7 | NEW | 2018-09-13 22:44:29 | +----+-------+----------+-------+--------+---------------------+ 1 row in set (0.00 sec) mysql> select * from `order`; +----+-------+----------+-------+--------+---------------------+ | id | name | quantity | price | status | date | +----+-------+----------+-------+--------+---------------------+ | 1 | apple | 1 | 7 | NEW | 2018-09-13 22:44:29 | +----+-------+----------+-------+--------+---------------------+ 1 row in set (0.00 sec) mysql> select * from `order`; +----+-------+----------+-------+--------+---------------------+ | id | name | quantity | price | status | date | +----+-------+----------+-------+--------+---------------------+ | 1 | apple | 1 | 11 | NEW | 2018-09-13 22:52:45 | +----+-------+----------+-------+--------+---------------------+ 1 row in set (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec)
能夠看到在session-1中第9步第三次查詢時,得到了不一致的數據,這就是不可重複讀的問題。解決不可重複讀,能夠提升隔離級別到:可重複讀。
set session transaction isolation level repeatable read;
請見接下來的演示。
5.5 幻讀
請按照下表執行相應的演示步驟,
step | session 1 | session 2 |
---|---|---|
1 | use test; | use test; |
2 | set session transaction isolation level repeatable read; | |
3 | start transaction; | |
4 | select * from `order` where `status`='new'; | |
5 | insert into `order` (`name`, `status`) VALUES ('apple', 'NEW'); | |
6 | select * from `order` where `status`='new'; | |
7 | update `order` set `status`='PAID' where `status`='NEW'; | |
8 | select * from `order` where `status`='PAID'; | |
9 | commit; | ; |
其中,
- session-1 中在第2步設置了隔離級別爲:可重複讀。
- session-1 中在第6步讀取到NEW狀態的數據記錄爲一條。
- session-1 中在第7步更新了NEW狀態的數據記錄,狀態設置爲PAID。
- session-1 中在第8步查詢更新結果,發現實際上更新操做影響了兩條數據記錄。
請見session-1的輸出,
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from `order` where `status`='new'; +----+-------+----------+-------+--------+---------------------+ | id | name | quantity | price | status | date | +----+-------+----------+-------+--------+---------------------+ | 1 | apple | 1 | 5 | NEW | 2018-09-14 17:20:24 | +----+-------+----------+-------+--------+---------------------+ 1 row in set (0.00 sec) mysql> select * from `order` where `status`='new'; +----+-------+----------+-------+--------+---------------------+ | id | name | quantity | price | status | date | +----+-------+----------+-------+--------+---------------------+ | 1 | apple | 1 | 5 | NEW | 2018-09-14 17:20:24 | +----+-------+----------+-------+--------+---------------------+ 1 row in set (0.00 sec) mysql> update `order` set `status`='PAID' where `status`='NEW'; Query OK, 2 rows affected (0.00 sec) Rows matched: 2 Changed: 2 Warnings: 0 mysql> select * from `order` where `status`='PAID'; +----+-------+----------+-------+--------+---------------------+ | id | name | quantity | price | status | date | +----+-------+----------+-------+--------+---------------------+ | 1 | apple | 1 | 5 | PAID | 2018-09-14 17:22:18 | | 2 | apple | 0 | 0 | PAID | 2018-09-14 17:22:18 | +----+-------+----------+-------+--------+---------------------+ 2 rows in set (0.00 sec) mysql> commit; Query OK, 0 rows affected (0.00 sec)
能夠看到在session-1中第7步進行更新操做時,更新了當前事務並未看見的另一條數據記錄,這就是幻讀所面臨的問題。解決幻讀問題,能夠提升隔離級別到:串行。
set session transaction isolation level serializable;
若在上述演示中,在session-1中的第2步設置隔離級別爲串行,則session-2中的第5步insert操做會被阻塞,直到session-1完成事務。
6. 小結
保證絕對的數據一致性,是以併發吞吐的降低爲代價的。在不少時候,犧牲必定的隔離性,在有些應用場景下能夠容忍必定的數據不一致問題,從而保障高併發的需求。瞭解sql隔離級別定義和相應會出現的問題,是進行隔離性級別優化選擇的前提,根據不一樣的應用場景,選擇合適的隔離級別,是數據庫性能調優的重要手段。
7. 參考資料
- sql1992規範
- sql2011規範
- Mysql 8.0用戶手冊 - ACID
- Mysql 8.0用戶手冊 - 15.5.2.1 Transaction Isolation Levels
- SqlServer 2017用戶手冊 - Transaction Isolation Levels
- Oracle Database - Data Concurrency and Consistency
- wiki 文檔 - sql standard