MySQL 這個東西注定是可能會被多個用戶/客戶端來同時訪問的,這是肯定的,MySQL 中存放的都是數據,數據可能有一個上層線程在用,也有可能另一個線程也要用...數據是被所有人共享的,所以就注定了 MySQL?這樣的服務在一個時刻有多個請求過來,然后我們進行數據的 CURD 操作!
MySQL 內部是采用多線程的方式來是西安數據的存儲相關的功能的,所以就注定會有對數據并發訪問的場景,所以為了更好的去解決這樣的問題,我們可以理解為:關系型數據庫為我們提供了事務這樣的概念!
像 Redis 這種K-V型的內存級數據庫也是支持事務的!
MySQL 比其他的數據庫支持的事務設計得更加巧妙!
接下來,我們先談論場景,后面引出事物的概念!
CURD/增刪查改不加控制,會有什么問題?
這和我們日常生活當中轉賬是一樣的,我想給張三的中銀銀行卡轉賬,做法就是將我的賬號的200塊錢,減去100塊錢,然后給張三的銀行卡加上100,這就完成了轉賬,但是可能在減去100過候,這個操作出現異常,銀行卡沒加上來。這個的整個操作就會出現中間過程,這種情況下,我們允許一場產生,但是一旦產生問題了,操作沒有完成,就需要讓減掉的100,加回來,就好像什么都沒錯,等待下一次在合適的時候進行轉賬,這就相當于轉賬的時候,沒有中間過程,而是在轉的時候出現了異常,直接進行回滾!不回滾就有問題!!!必須要回滾 --- 保證我們的狀態和初始狀態一樣!
CURD滿足什么屬性,能解決上述問題?
-
買票的過程是原子的吧
-
買票后相應數據不能被影響吧(雙方之間要隔離,我買我的,你買你的)
-
買完票應該要永久有效吧(購買了,你直接刪掉了???)
-
買前,和買后都要確定的狀態吧
什么是事務?
在談事務之前,我們要明白一點:
實際上,我們在編寫 SQL 的時候,不一定一條 SQL 語句就能解決所有問題,我們有時候要做對應的操作,需要一批 SQL 來共同組合,才有意義!
舉個例子,我今天要給大家進行轉賬,一定是 update 將我賬號的錢減去100,然后一定是 update 將你賬號上的錢 add 上100 --- 這注定是兩條 SQL,在我么你任何一個場景下,單獨抓出來,只能在計數層面上告訴我們在表中的特定位置進行+/-100,單獨的一條 SQL 語句是沒有意義的,就是兩條語句,但是將兩條 SQL 合在一起,就是看成一個整體,那么這個整體就是轉賬邏輯!所以我們將這兩條 SQL,共同構成了 DML 數據管理語言!這就是事務。
所以事務就是要站在 MySQL 的上層,去看待 SQL 語句!
說大白話就是 -- 事務就是由一條或者多條 SQL 語句構成的一個 SQL 的集合體,要完成某種業務!!!
事務就是一組DML語句組成,這些語句在邏輯上存在相關性(減100,自然是要加100,這就是相關性),這一組DML語句要么全部成功,要么全部失敗,是一個整體。是 MySQL提供一種機制,保證我們達到這樣的效果。事務還規定不同的客戶端看到的數據是不相同的。
事務就是要做的或所做的事情,主要用于處理操作量大、復雜度高的數據。假設一種場景:你畢業了,學校的教務系統后臺MySQL中,不再需要你的數據,要刪除你的所有信息(一般不會!),那么要刪除你的基本信息(姓名、電話、籍貫等)的同時,也刪除和你有關的其他信息,比如:你的各科成績,你在校表現,甚至你在論壇發過的文章等。這樣,就需要多條MySQL語句構成,那么所有這些操作合起來,就構成了一個事務。
正如我們上面所說,一個MySQL數據庫,可不止你一個事務運行,同一時刻,甚至有大量的請求被包裝成事務,在向MySQL服務器發起事務處理請求。而每條事務至少一條SQL,最多很多SQL這樣如果大家都訪問同樣的表數據,在不加保護的情況下,就絕對會出現問題。甚至,因為事務由多條SQL構成,那么,也會存在執行到一半出錯或者不想再執行的情況,那么已經執行的怎么辦呢?
所以,一個完整的事務,絕對不是簡單的sql集合,還需要滿足如下四個屬性:(原子-一致-隔離-持久)
-
原子性:一個事務(transaction)中的所有操作,要么全部完成,要么全部不完成,不會結束在中間環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
-
一致性:在事務開始之前和事務結束以后,數據庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及后續數據庫可以自發性地完成預定的工作。(結果是可預期的!轉賬成功與失敗的結果體現!)(這是沒有 MySQL 的技術設計的,但是只要做到了原子性,隔離性,持久性,我們就可以在技術上做到一致性,三因一果)
-
隔離性:數據庫允許多個并發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務并發執行時由于交叉執行而導致數據的不一致。事務隔離分為不同級別,包括讀未提交(Read uncommitted)、讀提交(read committed)、可重復讀(repeatable read)和串行化(Serializable)。
-
持久性:事務處理結束后,對數據的修改就是永久的,即便系統故障也不會丟失。
上面四個屬性,可以簡稱為ACID。
原子性(Atomicity,或稱不可分割性)
一致性(Consistency)
隔離性(Isolation,又稱獨立性)
持久性(Durability)。
事務在 MySQL 里肯定是存在的,存在的形式具體是什么樣的呢?我們可以這么理解:
MySQL 要同時為我們不同的客戶端處理各種各樣的事務請求,這就直接就定了 MySQL 在運行期間,一定會在自身內部存在大量的事務,MySQL 就需要把所有的事務管理起來 --- 先描述,再組織!所以,在我們看來,MySQL 所謂的事務,在我看來就是來了一批對應的 SQL,我把這批 SQL 打包成一個事務對象,然后將這個事務對象最后放入到事務的執行列表中,然后讓 MySQL 去幫我們執行!
所以事務本質上就是 MySQL 內部的一個對象!--- 抽象概念具體化,我們就可以想象了!
為什么會出現事務
事務被MySQL編寫者設計出來,本質是為了當應用程序訪問數據庫的時候,事務能夠簡化我們上層的編程模型,不需要我們去考慮各種各樣的潛在錯誤和并發問題。可以想一下當我們使用事務時,要么提交,要么回滾,我們不會去考慮網絡異常了,服務器宕機了,同時更改一個數據怎么辦對吧?因此事務本質上是為了應用層服務的。而不是伴隨著數據庫系統天生就有的。
事物存在的意義是為了讓上層更好的去使用 MySQL!
就想去柜臺存錢,只需要告訴工作人員我們的需求,具體的過程交給工作人員,不需要問工作人員出錯了怎么辦,只給個需求,中間操作失敗會告訴你,成功會告訴你!
備注:我們后面把MySQL中表中的一行信息,稱為一行記錄。未來就是對表中的記錄做增刪查改操作!
事務的版本支持
在MySQL中只有使用了Innodb數據庫引擎的數據庫或表才支持事務,MyISAM不支持。
查看數據庫引擎
mysql> show engines; -- 表格顯示
mysql> show engines \G -- 行顯示
*************************** 1. row ***************************Engine: InnoDB -- 引擎名稱Support: DEFAULT -- 默認引擎Comment: Supports transactions, row-level locking, and foreign keys -- 描述
Transactions: YES -- 支持事務XA: YESSavepoints: YES -- 支持事務保存點
*************************** 2. row ***************************Engine: MRG_MYISAMSupport: YESComment: Collection of identical MyISAM tables
Transactions: NOXA: NOSavepoints: NO
*************************** 3. row ***************************Engine: MEMORY --內存引擎Support: YESComment: Hash based, stored in memory, useful for temporary tables
Transactions: NOXA: NOSavepoints: NO
*************************** 4. row ***************************Engine: BLACKHOLESupport: YESComment: /dev/null storage engine (anything you write to it disappears)
Transactions: NOXA: NOSavepoints: NO
*************************** 5. row ***************************Engine: MyISAMSupport: YESComment: MyISAM storage engine
Transactions: NO -- MyISAM不支持事務XA: NOSavepoints: NO
*************************** 6. row ***************************Engine: CSVSupport: YESComment: CSV storage engine
Transactions: NOXA: NOSavepoints: NO
*************************** 7. row ***************************Engine: ARCHIVESupport: YESComment: Archive storage engine
Transactions: NOXA: NOSavepoints: NO
*************************** 8. row ***************************Engine: PERFORMANCE_SCHEMASupport: YESComment: Performance Schema
Transactions: NOXA: NOSavepoints: NO
*************************** 9. row ***************************Engine: FEDERATEDSupport: NOComment: Federated MySQL storage engine
Transactions: NULLXA: NULLSavepoints: NULL
9 rows in set (0.00 sec)
事務提交方式
事務的提交方式常見的有兩種:
-
自動提交
-
手動提交
查看事務提交方式
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.41 sec)
是 “ON” 表示的是自動提交!
用 SET 來改變 MySQL 的自動提交模式
mysql> SET AUTOCOMMIT=0; #SET AUTOCOMMIT=0 禁止自動提交
Query OK, 0 rows affected (0.00 sec)mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0.00 sec)mysql> SET AUTOCOMMIT=1; #SET AUTOCOMMIT=1 開啟自動提交
Query OK, 0 rows affected (0.00 sec)mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.01 sec)
事務常見操作方式
簡單銀行用戶表
提前準備
我的 Ubuntu 云服務器,默認開啟3306 mysqld服務
root@instance-hojuqq09:~# netstat -nltp | grep mysql*
tcp6 0 0 :::33060 :::* LISTEN 274596/mysqld
tcp6 0 0 :::3306 :::* LISTEN 274596/mysqld
我們一直使用的是客戶端,也就是這個:
root@instance-hojuqq09:~# which mysql
/usr/bin/mysql
root@instance-hojuqq09:~# ls /usr/bin/mysql
/usr/bin/mysql
root@instance-hojuqq09:~# which mysqld
/usr/sbin/mysqld
我們一直使用的是命令行式的客戶端,當然還有其他的圖形化界面版的 MySQL 客戶端,其他語言版的(可以使用 C/C++ 來連接 MySQL)...而且 MySQL 也是一套網絡服務,所以我們可以使用遠端連接的方式,來連接 MySQL 的,不一定要本地連接!
使用win cmd遠程訪問Ubuntu?22.04云服務器,mysqld服務(需要win上也安裝了MySQL,這里看到結果即可)
注意,使用本地mysql客戶端,可能看不到鏈接效果,本地可能使用域間套接字,查不到鏈接
有時候連不上 --- MySQL 的服務,一般不要暴露在公網上,暴露在公網上了,別人是可以連接的,但是可能不允許登入!MySQL 內部有自己的賬號管理體系,不僅僅是用戶名和密碼就可以登上的,還是需要設置允許哪些用戶在哪里登入,這是后面的 MySQL 的用戶管理話題!需要在 MySQL 的用戶表中進行配置,讓某一個用戶,比如說 root ,允許從遠端的Windows的機器登入!
## 使用win cmd遠程訪問Centos 7云服務器,mysqld服務(需要win上也安裝了MySQL,這里看到結果即可)
## 注意,使用本地mysql客戶端,可能看不到鏈接效果,本地可能使用域間套接字,查不到鏈接
C:\Users\whb>mysql -uroot -p -h42.192.83.143
Enter password: **********
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 3484
Server version: 5.7.33 MySQL Community Server (GPL)Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.## 使用netstat查看鏈接情況,可知:mysql本質是一個客戶端進程
[whb@VM-0-3-centos ~]$ sudo netstat -ntp
Active Internet connections (w/o servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp6 0 0 172.17.0.3:3306 113.132.141.236:19354 ESTABLISHED 30415/mysqld
所以我們要清楚,一個 MySQL 的服務端,是有可能會被多個客戶端使用的!!!
為了便于演示,我們將mysql的默認隔離級別設置成讀未提交。(具體操作我們后面專門會講,現在已使用為主)
設置全局事務隔離級別:
mysql> set global transaction isolation level READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
1 row in set (0.00 sec)mysql> quit
Bye
需要重啟終端,進行查看
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED |
+-------------------------+
1 row in set (0.00 sec)
我們將隔離級別設置為最低,在兩個客戶端之間就可以立馬見效。是為了達到實驗的目的!讓我們能夠清楚的看到雙方事務在交叉時,帶來的問題!
我們當前讓客戶端連接多個,我們可以通過如下方式看看:
mysql> show processlist;
+----+-----------------+-----------+---------+---------+--------+------------------------+------------------+
| Id | User | Host | db | Command | Time | State | Info |
+----+-----------------+-----------+---------+---------+--------+------------------------+------------------+
| 5 | event_scheduler | localhost | NULL | Daemon | 956019 | Waiting on empty queue | NULL |
| 41 | root | localhost | test_db | Query | 0 | init | show processlist |
| 42 | root | localhost | test_db | Sleep | 194 | | NULL |
+----+-----------------+-----------+---------+---------+--------+------------------------+------------------+
3 rows in set, 1 warning (0.00 sec)
有兩個root用戶都在test_db數據庫下,其中一個用戶的命令狀態是sleep!
創建測試表 --- 員工工資表
create table if not exists account(id int primary key,name varchar(50) not null default '',blance decimal(10,2) not null default 0.0
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;
正常演示-證明事務的開始與回滾
我們先來查看當前事物的提交方式:默認是 ON 的!
我們開啟事務的方式有兩種:
- start transaction;
- begin;
一旦啟動了事務,從開始到后面,所有的 SQL 都是一個事務的內部!
為了能看出交叉的效果,我們在另一個機器上也是begin一個事務:
我們以一個為主,我們在開始的時候設置一個保存點;
接下來我們就嘗試向一個表中插入數據;
然后我們再設置一個保存點 --- 就像我們出去玩的時候,迷路了,會在走到的位置標記上,防止下次再走回來,走回來也就意識到了 --- 方便未來我們定向回滾!
接下來,我們再插入一條數據;
然后我們再設置一個保存點 --- 就可以看出,每做一次操作,我們都會設置保存點!
那么以上的所有的 SQL,都屬于同一個事務!也就是上面的所有 SQL 最終都會被打包成一個事務,讓我們的 MySQL 原子的,持久的,隔離的,一致的將我們插入操作做好!
可是今天事務正在運行中,突然后悔了,不想插入李四了,我們就可以進行定向回滾 --- rollback to save2.
我們可以進行 commit 進行事務的提交 --- 一旦事務提交,rollback 就沒有用了!數據就被持久化保存了!
mysql> show variables like 'autocommit'; -- 查看事務是否自動提交。我們故意設置成自動提交,看看該選項是否影響begin
+---------------+-------+
| Variable_name | value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)mysql> start transaction; -- 開始一個事務begin也可以,推薦begin
Query OK, 0 rows affected (0.00 sec)mysql> savepoint save1; -- 創建一個保存點save1
Query OK, 0 rows affected (0.00 sec)mysql> insert into account values (1, '張三', 100); -- 插入一條記錄
Query OK, 1 row affected (0.05 sec)mysql> savepoint save2; -- 創建一個保存點save2
Query OK, 0 rows affected (0.01 sec)mysql> insert into account values (2, '李四', 10000); -- 在插入一條記錄
Query OK, 1 row affected (0.00 sec)mysql> select * from account; -- 兩條記錄都在了
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> rollback to save2; -- 回滾到保存點save2
Query OK, 0 rows affected (0.03 sec)mysql> select * from account; -- 一條記錄沒有了
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 張三 | 100.00 |
+----+--------+---------+
1 row in set (0.00 sec)mysql> rollback; -- 直接rollback,回滾在最開始
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; -- 所有剛剛的記錄沒有了
Empty set (0.00 sec)mysql> commit; -- 取消/提交事務 --- 數據持久化保存了,沒有辦法 rollback 了
Query OK, 0 rows affected (0.00 sec)
以上就是事務的啟動-提交-回滾操作了!
非正常演示1 - 證明未commit,客戶端崩潰,MySQL自動會回滾(隔離級別設置為讀未提交)
-- 終端A
mysql> select * from account; -- 當前表內無數據
Empty set (0.00 sec)mysql> show variables like 'autocommit'; -- 依舊自動提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)mysql> begin; -- 開啟事務
Query OK, 0 rows affected (0.00 sec)mysql> insert into account values (1, '張三', 100); -- 插入記錄
Query OK, 1 row affected (0.00 sec)mysql> select * from account; -- 數據已經存在,但沒有commit,此時同時查看終端B
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 張三 | 100.00 |
+----+--------+---------+
1 row in set (0.00 sec)mysql> Aborted -- ctrl + \ 異常終止MySQL--終端B
mysql> select * from account; --終端A崩潰前
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 張三 | 100.00 |
+----+--------+---------+
1 row in set (0.00 sec)mysql> select * from account; --數據自動回滾
Empty set (0.00 sec)
非正常演示2 - 證明commit了,客戶端崩潰,MySQL數據不會在受影響,已經持久化
--終端 A
mysql> show variables like 'autocommit'; -- 依舊自動提交
+---------------+-------+
| Variable_name | value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)mysql> select * from account; -- 當前表內無數據
Empty set (0.00 sec)mysql> begin; -- 開啟事務
Query OK, 0 rows affected (0.00 sec)mysql> insert into account values (1, '張三', 100); -- 插入記錄
Query OK, 1 row affected (0.00 sec)mysql> commit; -- 提交事務
Query OK, 0 rows affected (0.04 sec)mysql> Aborted -- ctrl + \ 異常終止MySQL--終端 B
mysql> select * from account; --數據存在了,所以commit的作用是將數據持久化到MySQL中
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 張三 | 100.00 |
+----+--------+---------+
1 row in set (0.00 sec)
非正常演示3 - 對比試驗。證明begin操作會自動更改提交方式,不會受MySQL是否自動提交影響
-- 終端 A
mysql> select * from account; --查看歷史數據
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 張三 | 100.00 |
+----+--------+---------+
1 row in set (0.00 sec)mysql> show variables like 'autocommit'; --查看事務提交方式
+---------------+-------+
| Variable_name | value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)mysql> set autocommit=0; --關閉自動提交
Query OK, 0 rows affected (0.00 sec)mysql> show variables like 'autocommit'; --查看關閉之后結果
+---------------+-------+
| Variable_name | value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0.00 sec)mysql> begin; --開啟事務
Query OK, 0 rows affected (0.00 sec)mysql> insert into account values (2, '李四', 10000); --插入記錄
Query OK, 1 row affected (0.00 sec)mysql> select * from account; --查看插入記錄,同時查看終端B
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> Aborted --再次異常終止-- 終端B
mysql> select * from account; --終端A崩潰前
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> select * from account; --終端A崩潰后,自動回滾
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 張三 | 100.00 |
+----+--------+---------+
1 row in set (0.00 sec)
其表現和自動提交是沒有任何關系的!自動提交影響的是什么,我們后面會說!
所以自動提交不影響我們命令行的begin --- commit,我們在命令行當中輸入begin --- commit 的時候,代表我們啟動事務,對應的就是相當于手動開啟事務,只要是手動開啟的事務,就必須要手動 commit!也就是和是否自動沒有任何關系!
非正常演示4 - 證明單條sql與事務的關系
--實驗一
-- 終端A
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 張三 | 100.00 |
+----+--------+---------+
1 row in set (0.00 sec)mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)mysql> set autocommit=0; --關閉自動提交
Query OK, 0 rows affected (0.00 sec)mysql> insert into account values (2, '李四', 10000); --插入記錄
Query OK, 1 row affected (0.00 sec)mysql> select * from account; --查看結果,已經插入。此時可以在查看終端B
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> Aborted --ctrl + \ or ctrl + d,終止終端--終端B
mysql> select * from account; --終端A崩潰前
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> select * from account; --終端A崩潰后
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 張三 | 100.00 |
+----+--------+---------+
1 row in set (0.00 sec)-- 實驗二
--終端A
mysql> show variables like 'autocommit'; --開啟默認提交
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 張三 | 100.00 |
+----+--------+---------+
1 row in set (0.00 sec)mysql> insert into account values (2, '李四', 10000);
Query OK, 1 row affected (0.01 sec)mysql> select * from account; --數據已經插入
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> Aborted --異常終止--終端B
mysql> select * from account; --終端A崩潰前
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> select * from account; --終端A崩潰后,并不影響,已經持久化。autocommit起作用
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
對于InnoDB每一條SQL語言都默認封裝成事務,自動提交。(select有特殊情況,因為MySQL有MVCC)
結論:
只要輸入begin或者start transaction,事務便必須要通過commit提交,才會持久化,與是否設置set autocommit無關。
事務可以手動回滾,同時,當操作異常,MySQL會自動回滾
對于InnoDB每一條SQL語言都默認封裝成事務,自動提交。(select有特殊情況,因為MySQL有MVCC)
從上面的例子,我們能看到事務本身的原子性(回滾),持久性(commit)
那么隔離性?一致性?
事務操作注意事項
如果沒有設置保存點,也可以回滾,只能回滾到事務的開始。直接使用rollback(前提是事務還沒有提交)
如果一個事務被提交了(commit),則不可以回退(rollback)
可以選擇回退到哪個保存點
InnoDB支持事務,MyISAM不支持事務
開始事務可以使start transaction或者begin
事務隔離級別(重點)
如何理解隔離性?
-
我們清楚:MySQL服務可能會同時被多個客戶端進程(線程)訪問,訪問的方式以事務方式進行(上面的測試)
-
一個事務可能由多條SQL構成,也就意味著,任何一個事務,都有執行前、執行中、執行后的階段。而所謂的原子性,其實就是讓用戶層,要么看到執行前,要么看到執行后。執行中出現問題,可以隨時回滾。所以單個事務,對用戶表現出來的特性,就是原子性。
-
但,畢竟所有事務都要有個執行過程,那么在多個事務各自執行多個SQL的時候,就還是有可能會出現互相影響的情況。比如:多個事務同時訪問同一張表,甚至同一行數據。
-
就如同你媽媽給你說:你要么別學,要學就學到最好。至于你怎么學,中間有什么困難,你媽媽不關心。那么你的學習,對你媽媽來講,就是原子的。那么你學習過程中,很容易受別人干擾,此時,就需要將你的學習隔離開,保證你的學習環境是健康的。
-
數據庫中,為了保證事務執行過程中盡量不受干擾,就有了一個重要特征:隔離性
-
數據庫中,允許事務受不同程度的干擾,就有了一種重要特征:隔離級別
我對一個數據表中的第二行記錄做 update ,他要做 select ,這就有一個問題:我要進行 update 進行更新,他要查找,那么是我先跑,還是他先跑?如果是我先跑,那么他查出來的數據一定是最新的,反之,就是老數據。
update 先跑,這是我們一貫的正常認知,但是這個觀點是有問題的!這是時間線的問題的!取決于誰先來的!因為事務要保證原子性,不僅僅是在回滾上體現,原子性也要體現在時間范圍內,誰先來就是誰先操作!雖然事務有執行前/中/后,而執行中我們彼此交叉,但是事務是一個原子整體!誰先來就應該是誰先執行,可是先到來的不一定先/后退出,可能在你執行期間,先到來的事務已經將數據更新了,這次他的更新,在你的 select 中就是不應該看到的!!!因為要保證隔離性!!!
隔離性確保了即使事務并發執行,每個事務也都像是在獨立的環境中運行。
不同的隔離級別提供了不同級別的保護!要隔離到什么層度,因此我們就有了隔離級別的概念!
隔離級別(為了滿足不同的應用場景)
讀未提交【Read Uncommitted】:在該隔離級別,所有的事務都可以看到其他事務沒有提交的執行結果。(實際生產中不可能使用這種隔離級別的),但是相當于沒有任何隔離性,也會有很多并發問題,如臟讀、幻讀、不可重復讀等。我們上面為了做實驗方便,用的就是這個隔離性。
MySQL 事務隔離其實是依靠鎖來實現的,加鎖自然會帶來性能的損失。而讀未提交隔離級別是不加鎖的,所以它的性能是最好的,沒有加鎖、解鎖帶來的性能開銷。但有利就有弊,這基本上就相當于裸奔啊,所以它連臟讀的問題都沒辦法解決。
任何事務對數據的修改都會第一時間暴露給其他事務,即使事務還沒有提交。
下面來做個簡單實驗驗證一下,首先設置全局隔離級別為讀未提交。
set global transaction isolation level read uncommitted;
設置完成后,只對之后新起的 session 才起作用,對已經啟動 session 無效。如果用 shell 客戶端那就要重新連接 MySQL,如果用 Navicat 那就要創建新的查詢窗口。
啟動兩個事務,分別為事務A和事務B,在事務A中使用 update 語句,修改 age 的值為10,初始是1 ,在執行完 update 語句之后,在事務B中查詢 user 表,會看到 age 的值已經是 10 了,這時候事務A還沒有提交,而此時事務B有可能拿著已經修改過的 age=10 去進行其他操作了。在事務B進行操作的過程中,很有可能事務A由于某些原因,進行了事務回滾操作,那其實事務B得到的就是臟數據了,拿著臟數據去進行其他的計算,那結果肯定也是有問題的。
順著時間軸往表示兩事務中操作的執行順序,重點看圖中 age 字段的值。
讀未提交,其實就是可以讀到其他事務未提交的數據,但沒有辦法保證你讀到的數據最終一定是提交后的數據,如果中間發生回滾,那就會出現臟數據問題,讀未提交沒辦法解決臟數據問題。更別提可重復讀和幻讀了,想都不要想。
讀提交【Read Committed】:該隔離級別是大多數數據庫的默認的隔離級別(不是MySQL默認的)。它滿足了隔離的簡單定義:一個事務只能看到其他的已經提交的事務所做的改變。這種隔離級別會引起不可重復讀,即一個事務執行時,如果多次select,可能得到不同的結果。
既然讀未提交沒辦法解決臟數據問題,那么就有了讀提交。讀提交就是一個事務只能讀到其他事務已經提交過的數據,也就是其他事務調用 commit 命令之后的數據。那臟數據問題迎刃而解了。
讀提交事務隔離級別是大多數流行數據庫的默認事務隔離界別,比如 Oracle,但是不是 MySQL 的默認隔離界別。
我們繼續來做一下驗證,首先把事務隔離級別改為讀提交級別。
set global transaction isolation level read committed;
之后需要重新打開新的 session 窗口,也就是新的 shell 窗口才可以。
同樣開啟事務A和事務B兩個事務,在事務A中使用 update 語句將 id=1 的記錄行 age 字段改為 10。此時,在事務B中使用 select 語句進行查詢,我們發現在事務A提交之前,事務B中查詢到的記錄 age 一直是1,直到事務A提交,此時在事務B中 select 查詢,發現 age 的值已經是 10 了。
這就出現了一個問題,在同一事務中(本例中的事務B),事務的不同時刻同樣的查詢條件,查詢出來的記錄內容是不一樣的,事務A的提交影響了事務B的查詢結果,這就是不可重復讀,也就是讀提交隔離級別。
每個 select 語句都有自己的一份快照,而不是一個事務一份,所以在不同的時刻,查詢出來的數據可能是不一致的。
讀提交解決了臟讀的問題,但是無法做到可重復讀,也沒辦法解決幻讀。
可重復讀【Repeatable Read】:這是MySQL默認的隔離級別,它確保同一個事務,在執行中,多次讀取操作數據時,會看到同樣的數據行。但是會有幻讀問題。(雙方都不清楚情況,是一種比較好的隔離級別)(MySQL 默認的隔離級別!)
可重復是對比不可重復而言的,上面說不可重復讀是指同一事物不同時刻讀到的數據值可能不一致。而可重復讀是指,事務不會讀到其他事務對已有數據的修改,及時其他事務已提交,也就是說,事務開始時讀到的已有數據是什么,在事務提交前的任意時刻,這些數據的值都是一樣的。但是,對于其他事務新插入的數據是可以讀到的,這也就引發了幻讀問題。
同樣的,需改全局隔離級別為可重復讀級別。
set global transaction isolation level repeatable read;
在這個隔離級別下,啟動兩個事務,兩個事務同時開啟。
首先看一下可重復讀的效果,事務A啟動后修改了數據,并且在事務B之前提交,事務B在事務開始和事務A提交之后兩個時間節點都讀取的數據相同,已經可以看出可重復讀的效果。
可重復讀做到了,這只是針對已有行的更改操作有效,但是對于新插入的行記錄,就沒這么幸運了,幻讀就這么產生了。我們看一下這個過程:
事務A開始后,執行 update 操作,將 age = 1 的記錄的 name 改為“風箏2號”;
事務B開始后,在事務執行完 update 后,執行 insert 操作,插入記錄 age =1,name = 古時的風箏,這和事務A修改的那條記錄值相同,然后提交。
事務B提交后,事務A中執行 select,查詢 age=1 的數據,這時,會發現多了一行,并且發現還有一條 name = 古時的風箏,age = 1 的記錄,這其實就是事務B剛剛插入的,這就是幻讀。
要說明的是,當你在 MySQL 中測試幻讀的時候,并不會出現上圖的結果,幻讀并沒有發生,MySQL 的可重復讀隔離級別其實解決了幻讀問題!
串行化【Serializable】:這是事務的最高隔離級別,它通過強制事務排序,使之不可能相互沖突,從而解決了幻讀的問題。它在每個讀的數據行上加上共享鎖,但是可能會導致超時和鎖競爭(這種隔離級別太極端,實際生產基本不使用)(對聽一條記錄的操作肯定就是需要進行串行化了!)
隔離級別如何實現:隔離,基本都是通過鎖實現的,不同的隔離級別,鎖的使用是不同的。常見有,表鎖、行鎖、讀鎖、寫鎖、間隙鎖(GAP),Next-Key鎖(GAP+行鎖)等。不過,我們目前現有這個認識就行,先關注上層使用。
查看與設置隔離性
下面的第二個和第三個是一樣的!global 和 session 的差別是:
global 是 session 的默認配置,當我們登入 MySQL 的時候,默認 MySQL 會讀取配置好的隔離級別,用來初始化本次登入的會話隔離級別,這就有點像 global 設置好了,然后我們就相當于一個全局的配置,我們的 session 的隔離級別就默認是將 global 的隔離級別拷貝過來,當然,我們可以更改我們當前的會話級別!
當然了,更改 session 的話,只會更改本次會話!更改全局的,會影響后續的所有客戶端的登入!
-- 查看
mysql> SELECT @@global.transaction_isolation; --查看全局隔級別
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ |
+-----------------------+
1 row in set, 1 warning (0.00 sec)mysql> SELECT @@session.transaction_isolation; --查看會話(當前)全局隔級別
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ |
+------------------------+
1 row in set, 1 warning (0.00 sec)mysql> SELECT @@transaction_isolation; --默認同上
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)--設置
-- 設置當前會話 or 全局隔離級別語法
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}--設置當前會話隔離性,另起一個會話,看不到,只影響當前會話
mysql> set session transaction isolation level serializable; -- 串行化
Query OK, 0 rows affected (0.00 sec)mysql> SELECT @@global.transaction_isolation; --全局隔離性還是RR
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ |
+-----------------------+
1 row in set, 1 warning (0.00 sec)mysql> SELECT @@session.transaction_isolation; --會話隔離性成為串行化
+------------------------+
| @@session.tx_isolation |
+------------------------+
| SERIALIZABLE |
+------------------------+
1 row in set, 1 warning (0.00 sec)mysql> SELECT @@transaction_isolation; --同上
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE |
+----------------+
1 row in set, 1 warning (0.00 sec)--設置全局隔離性,另起一個會話,會被影響
mysql> set global transaction isolation level READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)mysql> SELECT @@global.transaction_isolation;
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| READ-UNCOMMITTED |
+-----------------------+
1 row in set, 1 warning (0.00 sec)mysql> SELECT @@session.transaction_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-UNCOMMITTED |
+------------------------+
1 row in set, 1 warning (0.00 sec)mysql> SELECT @@transaction_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)-- 注意,如果沒有現象,關閉mysql客戶端,重新連接。
讀未提交【Read Uncommitted】
--幾乎沒有加鎖,雖然效率高,但是問題太多,嚴重不建議采用
--終端A
-- 設置隔離級別為 讀未提交
mysql> set global transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)--重啟客戶端mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> begin; --開啟事務
Query OK, 0 rows affected (0.00 sec)mysql> update account set blance=123.0 where id=1; --更新指定行
Query OK, 1 row affected (0.05 sec)
Rows matched: 1 Changed: 1 Warnings: 0--沒有commit哦!!--終端B
mysql> begin;
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 123.00 | <-- 臟讀!
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)--一個事務在執行中,讀到另一個執行中事務的更新(或其他操作)但是未commit的數據,這種現象叫做臟讀(dirty read)
--讀到終端A更新但是未commit的數據[insert, delete同樣!]
讀提交【Read Committed】
不可重復讀就是在讀提交的場景!
--終端A
mysql> set global transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
--重啟客戶端
mysql> select * from account; --查看當前數據
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 123.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> begin; --手動開啟事務,同步的開始終端B事務
Query OK, 0 rows affected (0.00 sec)mysql> update account set blance=321.0 where id=1; --更新張三數據
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0--切換終端到終端B,查看數據。mysql> commit; --commit提交!
Query OK, 0 rows affected (0.01 sec)--切換終端到終端B,再次查看數據。--終端B
mysql> begin; --手動開啟事務,和終端A一前一后
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; --終端A commit之前,查看不到更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 123.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)--終端A commit之后,看到了!
--but,此時還在當前事務中,并未commit,那么就造成了,同一個事務內,同樣的讀取,在不同的時間段(依舊還在事務操作中!),讀取到了不同的值,這種現象叫做不可重復讀(non reapeatable read)!!(這個是問題嗎??)
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
導致 Tom 的工資在3000到4000中出現了一次,也在4000到5000中出現了一次!
不可重復讀(Non-repeatable Read)是數據庫事務并發控制中的一個問題,它發生在一個事務在讀取某個數據后,另一個并發事務修改了該數據并提交,導致第一個事務再次讀取同一數據時得到不同的結果。這種情況破壞了事務的隔離性,因為事務的兩次讀取操作沒有得到一致的數據視圖。
在圖中的例子中,小張的事務在“讀已提交”(Read Committed)隔離級別下執行,這意味著它只能看到其他事務已經提交的更改。當小王的事務更新了Tom的薪資并提交后,小張的事務在之后的查詢中看到了Tom薪資的更新,這就是不可重復讀的一個實例。為了解決這個問題,數據庫通常提供更高級別的隔離,如“可重復讀”(Repeatable Read),它確保在一個事務中多次讀取同一數據時,結果是一致的,即使其他事務對數據進行了修改。
可重復讀【Repeatable Read】
--終端A
mysql> set global transaction isolation level repeatable read; --設置全局隔離級別RR
Query OK, 0 rows affected (0.01 sec)--關閉終端重啟mysql> select @@transaction_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)mysql> select * from account; --查看當前數據
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> begin; --開啟事務,同步的,終端B也開始事務
Query OK, 0 rows affected (0.00 sec)mysql> update account set blance=4321.0 where id=1; --更新數據
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0--切換到終端B,查看另一個事務是否能看到mysql> commit; --提交事務--切換終端到終端B,查看數據。--終端B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; --終端A中事務 commit之前,查看當前表中數據,數據未更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> select * from account; --終端A中事務 commit 之后,查看當前表中數據,數據未更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)--可以看到,在終端B中,事務無論什么時候進行查找,看到的結果都是一致的,這叫做可重復讀!mysql> commit; --結束事務
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; --再次查看,看到最新的更新數據
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)--如果將上面的終端A中的update操作,改成insert操作,會有什么問題??
--終端A
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> begin; --開啟事務,終端B同步開啟
Query OK, 0 rows affected (0.00 sec)mysql> insert into account (id,name,blance) values(3, '王五', 5432.0);
Query OK, 1 row affected (0.00 sec)--切換到終端B,查看另一個事務是否能看到mysql> commit; --提交事務
Query OK, 0 rows affected (0.00 sec)--切換終端到終端B,查看數據。mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)--終端B
mysql> begin; --開啟事務
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; --終端A commit前 查看
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> select * from account; --終端A commit后 查看
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> select * from account; --多次查看,發現終端A在對應事務中insert的數據,在終端B的事務周期中,也沒有什么影響,也符合可重復的特點。但是,一般的數據庫在可重復讀情況的時候,無法屏蔽其他事務insert的數據(為什么?因為隔離性實現是對數據加鎖完成的,而insert待插入的數據因為并不存在,那么一般加鎖無法屏蔽這類問題),會造成雖然大部分內容是可重復讀的,但是insert的數據在可重復讀情況被讀取出來,導致多次查找時,會多查找出來的記錄,就如同產生了幻覺。這種現象,叫做幻讀(phantom read)。很明顯,MySQL在RR級別的時候,是解決了幻讀問題的(解決的方式是用Next-Key鎖(GAP+行鎖)解決的。這塊比較難,有興趣同志了解一下)。
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)mysql> commit; --結束事務
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; --看到更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
多次查看,發現終端A在對應事務中insert的數據,在終端B的事務周期中,也沒有什么影響,也符合可重復的特點。但是,一般的數據庫在可重復讀情況的時候,無法屏蔽其他事務insert的數據(為什么?因為隔離性實現是對數據加鎖完成的,而insert待插入的數據因為并不存在,那么一般加鎖無法屏蔽這類問題),會造成雖然大部分內容是可重復讀的,但是insert的數據在可重復讀情況被讀取出來,導致多次查找時,會多查找出來的記錄,就如同產生了幻覺。這種現象,叫做幻讀(phantom read)。很明顯,MySQL在RR級別的時候,是解決了幻讀問題的(解決的方式是用Next-Key鎖(GAP+行鎖)解決的。這塊比較難,有興趣同志可以了解一下)。
串行化【Serializable】
是事務串行化 --- 導致到來的順序串行化,保證我在執行的時候,沒有人在跑,這就會帶來“慢”的效率問題!
是安全,但是不高效!
--對所有操作全部加鎖,進行串行化,不會有問題,但是只要串行化,效率很低,幾乎完全不會被采用
--終端A
mysql> set global transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE |
+----------------+
1 row in set, 1 warning (0.00 sec)
mysql> begin; --開啟事務,終端B同步開啟
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; --兩個讀取不會串行化,共享鎖
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)mysql> update account set blance=1.00 where id=1; --終端A中有更新或者其他操作,會阻塞。直到終端B事務提交。
Query OK, 1 row affected (18.19 sec)
Rows matched: 1 Changed: 1 Warnings: 0--終端B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)mysql> select * from account; --兩個讀取不會串行化
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 張三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)mysql> commit; --提交之后,終端A中的update才會提交。
Query OK, 0 rows affected (0.00 sec)
總結:
-
其中隔離級別越嚴格,安全性越高,但數據庫的并發性能也就越低,往往需要在兩者之間找一個平衡點。
-
不可重復讀的重點是修改和刪除:同樣的條件,你讀取過的數據,再次讀取出來發現值不一樣了
-
幻讀的重點在于新增:同樣的條件,第1次和第2次讀出來的記錄數不一樣
-
說明:mysql默認的隔離級別是可重復讀,一般情況下不要修改
-
上面的例子可以看出,事務也有長短事務這樣的概念。事務間互相影響,指的是事務在并行執行的時候,即都沒有commit的時候,影響會比較大。
隔離級別 | 臟讀 | 不可重復讀 | 幻讀 | 加鎖讀 |
---|---|---|---|---|
讀未提交 (read uncommitted) | ? | ? | ? | 不加鎖 |
讀已提交 (read committed) | ? | ? | ? | 不加鎖 |
可重復讀 (repeatable read) | ? | ? | ?(MySQL中) | 不加鎖 |
可串行化 (serializable) | ? | ? | ? | 加鎖 |
?:會發生該問題
?:不會發生該問題
一致性(Consistency)
-
事務執行的結果,必須使數據庫從一個一致性狀態,變到另一個一致性狀態。當數據庫只包含事務成功提交的結果時,數據庫處于一致性狀態。如果系統運行發生中斷,某個事務尚未完成而被迫中斷,而改未完成的事務對數據庫所做的修改已被寫入數據庫,此時數據庫就處于一種不正確(不一致)的狀態。因此一致性是通過原子性來保證的。
-
其實一致性和用戶的業務邏輯相關,一般MySQL提供技術支持,但是一致性還是要用戶業務邏輯做支撐,也就是,一致性,是由用戶決定的。
-
而技術上,通過AID保證C:通過原子性(A)、隔離性(I)和持久性(D)來保證一致性(C)。也就是說,A、I 和 D 是實現 C 的手段
推薦閱讀
https://www.jianshu.com/p/3984788e1083
Innodb中的事務隔離級別和鎖的關系 - 美團技術團隊
Mysql 間隙鎖原理,以及Repeatable Read隔離級別下可以防止幻讀原理(百度) - aspirant - 博客園mysql相關基礎知識篇(五)_mysql串行化實現原理-CSDN博客
備注:
基本上,了解了上面的知識,在MySQL事務使用上,肯定沒有問題。不過,這塊設計很優秀,也是面試中可能被問到的,一般來說,如果能說出上面的內容,就已經不錯了。但是如果我們能更詳細,更深入的談論這個問題,那么對我們的面試與學習肯定是有很大的幫助。
不過接下來的內容,會比較難一些,聽的不明白,也沒有太大問題。
如果有時間,我們演示一下,在RR級別的時候,多個事務的update,多個事務的insert,多個事務的delete,是否會有加鎖現象。
現象結果是,update,insert,delete之間是會有加鎖現象的,但是select和這些操作是不沖突的。這就是通過讀寫鎖(鎖有行鎖或者表鎖)+MVCC完成隔離性。
試學內容-如何理解隔離性2
數據庫并發的場景有三種:
-
讀-讀:不存在任何問題,也不需要并發控制
-
讀-寫:有線程安全問題,可能會造成事務隔離性問題,可能遇到臟讀、幻讀、不可重復讀
-
寫-寫:有線程安全問題,可能會存在更新丟失問題,比如第一類更新丟失,第二類更新丟失(后面補充)
讀-寫
多版本并發控制(MVCC)是一種用來解決讀-寫沖突的無鎖并發控制
MySQL 會為每一個事務分配單向增長的事務ID,為每個修改保存一個版本,版本與事務ID關聯,讀操作只讀該事務開始前的數據庫的快照。
一般來說ID越小,來得越早!可以通過 ID 來判斷事務的先后順序;
MySQLD 可能會面臨處理多個事物的情況,事務也有自己的生命周期;
這就意味著MySQLD 要對多個事務進行管理,要先描述,再組織!換句話說,MySQLD 中一定是對應的一個或一套結構體/類對象!事務也要有自己的結構體(原子性申請事務ID,初始化事務對象)!
MVCC可以為數據庫解決以下問題:
-
在并發讀寫數據庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了數據庫并發讀寫的性能
-
同時還可以解決臟讀、幻讀、不可重復讀等事務隔離問題,但不能解決更新丟失問題
但是理解MVCC我們需要知道三個前提知識:
-
3/4個記錄隱藏字段
-
undo日志
-
Read View
3個記錄隱藏列字段
-
DB_TRX_ID(列名稱):6 byte,最近修改(修改/插入)事務ID,記錄創建這條記錄/最后一次修改該記錄的事務ID(代表未來插入的任何數據,插入這一記錄的事務是誰插入的?事務ID是什么?需要將這個事務ID放在表當中,無論是單SQL,系統默認封裝的事務,還是我們手動begin開啟的事務)
-
DB_ROLL_PTR(回滾指針):7 byte,回滾指針,指向這條記錄的上一個版本(簡單理解成,指向歷史版本就行,這些數據一般在undo log中)(如果我們對 MySQL 當中的某一行記錄作修改,MySQL 在特定的隔離級別下,并不會直接去改表中的數據,可能會將要改的記錄,在改之前先把存一份,然后讓我們改動最新的表中的數據,這樣改完之后也可以知道歷史的數據是誰 --- 這種策略就像之前的“ "寫" 時拷貝 ”,所以就要保證改完之后要能找到歷史的數據)(和回滾操作肯定是相關的)
-
DB_ROW_ID:6 byte,隱含的自增ID(隱藏主鍵),如果數據表沒有主鍵,InnoDB會自動以DB_ROW_ID產生一個聚簇索引,默認構建 B+ 樹,可以認為是缺省的,因為主鍵只能有一個!
-
補充:實際還有一個刪除flag隱藏字段,既記錄被更新或刪除并不代表真的刪除,而是刪除flag變了
假設測試表結構是:
mysql> create table if not exists student(name varchar(11) not null,age int not null
);
mysql> insert into student (name, age) values ('張三', 28);
Query OK, 1 row affected (0.05 sec)mysql> select * from student;
+--------+-----+
| name | age |
+--------+-----+
| 張三 | 28 |
+--------+-----+
1 row in set (0.00 sec)
上面描述的意思是:
name | age | DB_TRX_ID(創建該記錄的事務ID) | DB_ROW_ID(隱式主鍵) | DB_ROLL_PTR(回滾指針) |
---|---|---|---|---|
張三 | 28 | null | 1 | null |
我們目前并不知道創建該記錄的事務ID,隱式主鍵,我們就默認設置成null,1。第一條記錄也沒有其他版本,我們設置回滾指針為null。
undo 日志(MySQL 當中比較重要的日志模塊)
這里不想細講,但是有一件事情得說清楚,MySQL 將來是以服務/守護進程的方式,在內存中運行。我們之前所講的所有機制:索引、事務、隔離性、日志等,都是在內存中完成的,即在 MySQL 內部的相關緩沖區中,保存相關數據,完成各種判斷操作。然后在合適的時候,將相關數據刷新到磁盤當中的,進行持久化。
所以,我們這里理解undo log,簡單理解成,就是 MySQL 中的一段內存緩沖區,用來保存日志數據的就行。(buffer pool 中的一部分)
模擬 MVCC
現在有一個事務10(僅僅為了好區分),對student表中記錄進行修改(update):將name(張三)改成name(李四)。
-
事務10,因為要修改,所以要先給該記錄加行鎖保護。
-
修改前,先將改行記錄拷貝到undo log中,所以,undo log中就有了一行副本數據。(原理就類似是寫時拷貝)
-
所以現在 MySQL 中有兩行同樣的記錄。現在修改原始記錄中的name,改成 '李四'。并且修改原始記錄的隱藏字段 DB_TRX_ID 為當前事務10 的 ID,我們默認從 10 開始,之后遞增。而原始記錄的回滾指針 DB_ROLL_PTR 列,里面寫入undo log中副本數據的地址,從而指向副本記錄,既表示我的上一個版本就是它。
-
事務10提交,釋放鎖。
備注:此時,最新的記錄是"李四"那條記錄。
現在又有一個事務11,對student表中記錄進行修改(update):將age(28)改成age(38)。
-
事務11,因為也要修改,所以要先給該記錄加行鎖。(該記錄是那條?)
-
修改前,先將改行記錄拷貝到undo log中,所以,undo log中就又有了一行副本數據。此時,新的副本,我們采用頭插方式,插入undo log。
-
現在修改原始記錄中的age,改成38。并且修改原始記錄的隱藏字段DB_TRX_ID為當前事務11的ID。而原始記錄的回滾指針DB_ROLL_PTR列,里面寫入undo log中副本數據的地址,從而指向副本記錄,既表示我的上一個版本就是它。
-
事務11提交,釋放鎖。
這樣,我們就有了一個基于鏈表記錄的歷史版本鏈。所謂的回滾,無非就是用歷史數據,覆蓋當前數據。
實際上 MySQL 的回滾操作是有很多實現細節的,而且關于形成的版本鏈,我們是覆蓋的方式理解,版本鏈主要是為了進行事務隔離,用來實現隔離性的,而回滾操作是 --- 比如說我們作insert操作,MySQL 就會記錄一條相反的 SQL --- delete 未來在事務的內部作了delete,就會保存成 insert ,所以我們在進行定向回滾的時候,就會逆向的將 SQL 跑一邊,數據就恢復出來了
上面的過程我們就成為 MVCC!
上面的一個一個版本,我們可以稱之為一個一個的快照。
一些思考
一個事務已經提交了,是回滾不了的!因為 undo log 被清理掉了!
上面是以更新(update)主講的,如果是delete呢?一樣的,別忘了,刪數據不是清空,而是設置flag為刪除即可。也可以形成版本。
如果是insert呢?因為insert是插入,也就是之前沒有數據,那么insert也就沒有歷史版本。但是一般為了回滾操作,insert的數據也是要被放入undo log中,為了隔離級別,如果當前事務commit了,那么這個undo log的歷史insert記錄就可以被清空了。
總結一下,也就是我們可以理解成,update和delete可以形成版本鏈,insert暫時不考慮。
那么select呢?
首先,select不會對數據做任何修改,所以,為select維護多版本,沒有意義。不過,此時有個問題,就是:
select讀取,是讀取最新的版本呢?還是讀取歷史版本?
當前讀:讀取最新的記錄,就是當前讀。增刪改,都叫做當前讀,select也有可能當前讀,比如:select lock in share mode(共享鎖),select for update (這個好理解,我們后面不討論)
快照讀:讀取歷史版本(一般而言),就叫做快照讀。(這個我們后面重點討論)
這也就是我們讀寫可以并發!!!我們可以看到,在多個事務同時刪改查的時候,都是當前讀,是要加鎖的。那同時有select過來,如果也要讀取最新版(當前讀),那么也就需要加鎖,這就是串行化。
但如果是快照讀,讀取歷史版本的話,是不受加鎖限制的。也就是可以并行執行!換言之,提高了效率,即MVCC的意義所在。
那么,是什么決定了,select是當前讀,還是快照讀呢?隔離級別!
那為什么要有隔離級別呢?
因為事務都是原子的。所以,無論如何,事務總有先有后。
但是經過上面的操作我們發現,事務從begin->CURD->commit,是有一個階段的。也就是事務有執行前,執行中,執行后的階段。但,不管怎么啟動多個事務,總是有先有后的。
那么多個事務在執行中,CURD操作是會交織在一起的。那么,為了保證事務的"有先有后",是不是應該讓不同的事務看到它該看到的內容,這就是所謂的隔離性與隔離級別要解決的問題。
先來的事務,應不應該看到后來的事務所做的修改呢?
那么,如何保證,不同的事務,看到不同的內容呢?也就是如何如何實現隔離級別?
Read View
Read View就是事務進行快照讀操作的時候生產的讀視圖(Read View),在該事務執行的快照讀的那一刻,會生成數據庫系統當前的一個快照,記錄并維護系統當前活躍事務的ID(當每個事務開啟時,都會被分配一個ID, 這個ID是遞增的,所以最新的事務,ID值越大)
Read View 在 MySQL 源碼中,就是一個類,本質是用來進行可見性判斷的。即當我們某個事務執行快照讀的時候,才會對該記錄創建一個 Read View 讀視圖,把它比作條件,用來判斷當前事務能夠看到哪個版本的數據,既可能是當前最新的數據,也有可能是該行記錄的 undo log 里面的某個版本的數據。
具體讀那些,是由隔離級別確定的!
下面是ReadView結構,但為了減少大家的負擔,我們簡化一下
class Readview {// 省略...
private:/** 高水位,大于等于這個ID的事務均不可見*/trx_id_t m_low_limit_id;/* 低水位:小于這個ID的事務均可見 */trx_id_t m_up_limit_id;/* 創建該 Read View 的事務ID*/trx_id_t m_creator_trx_id;/* 創建視圖時的活躍事務id列表*/ids_t m_ids;/* 配合purge,標識該視圖不需要小于m_low_limit_no的UNDO LOG。* 如果其他視圖也不需要,則可以刪除小于m_low_limit_no的UNDO LOG*/trx_id_t m_low_limit_no;/* 標記視圖是否被關閉*/bool m_closed;// 省略...
};
m_ids; //一張列表,用來維護Read View生成時刻,系統正活躍的事務ID
up_limit_id; //記錄m_ids列表中事務ID最小的ID(沒有寫錯)
low_limit_id; //ReadView生成時刻系統尚未分配的下一個事務ID,也就是目前已出現過的事務ID的最大值+1(也沒有寫錯)
creator_trx_id //創建該ReadView的事務ID
我們在實際讀取數據版本鏈的時候,是能讀取到每一個版本對應的事務ID的,即:當前記錄的DB_TRX_ID。
那么,我們現在手里面有的東西就有,當前快照讀的ReadView和版本鏈中的某一個記錄的DB_TRX_ID。
所以現在的問題就是,當前快照讀,應不應該讀到當前版本記錄。一張圖,解決所有問題!
所以當前進行快照讀,應不應該讀到版本鏈當中的某一個版本,未來進行可見性判斷的時候,一定是拿著版本鏈當中存儲的事務ID,和我的 Read View 中記錄得事務ID,根據大小來決定先后,來判定應不應該看到!
注意:我們要時刻記住:
在大多數現代數據庫系統中(如?MySQL InnoDB、PostgreSQL、Oracle),事務 ID(Transaction ID)通常是在?BEGIN
?開始時就分配的,而不是在第一次執行寫操作(INSERT/UPDATE/DELETE)時才分配。
事務ID不斷在遞增,所以,事務ID大小,就決定了先后順序!但是:
物理存儲的最新數據?=?最后成功提交?的版本(與事務ID大小無必然關系)。
begin期間對同一條記錄的增刪改操作是加鎖的,不需要擔心這種類似的問題!
事務能否看到"最新數據"?取決于:
-
事務的?隔離級別
-
事務開始的?快照時間點
-
其他事務的?提交狀態
版本鏈(MVCC 多版本鏈)的核心作用正是為了實現不同隔離級別下的"快照讀",確保事務能看到符合其隔離級別要求的數據版本。
read view 是事務可見的一個類,不是事務創建出來,就會有 read view,而是當這個事務(已經存在),首次進行快照讀的時候,MySQL 才會 new 出來 read view!
對應源碼策略:
/* Check whether the changes by id are visible.
@param[in] id transaction id to check against the view
@param[in] name table name
@return whether the view sees the modifications of id. */
bool changes_visible(trx_id_t id,const table_name_t& name) constMY_ATTRIBUTE((warn_unused_result))
{ut_ad(id > 0);if (id < m_up_limit_id || id == m_creator_trx_id) {return(true);}check_trx_id_sanity(id, name);if (id >= m_low_limit_id) {return(false);} else if (m_ids.empty()) {return(true);}const ids_t::value_type* p = m_ids.data();return(!std::binary_search(p, p + m_ids.size(), id));
}
如果查到不應該看到當前版本,接下來就是遍歷下一個版本,直到符合條件,即可以看到。上面的readview是當你進行select的時候,會自動形成。
read view 整體流程
假設當前有條記錄:
name | age | DB_TRX_ID(創建該記錄的事務ID) | DB_ROW_ID(隱式主鍵) | DB_ROLL_PTR(回滾指針) |
---|---|---|---|---|
張三 | 28 | null | 1 | null |
事務操作:
事務1 [id=1] | 事務2 [id=2] | 事務3 [id=3] | 事務4 [id=4] |
---|---|---|---|
事務開始 | 事務開始 | 事務開始 | 事務開始 |
... | ... | ... | 修改且已提交 |
進行中 | 快照讀 | 進行中 | |
... | ... | ... |
-
事務4:修改name(張三)變成name(李四)
-
當事務2對某行數據執行了快照讀,數據庫為該行數據生成一個Read View讀視圖
//事務2的 Read view
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:Readview生成時刻,系統尚未分配的下一個事務ID
creator_trx_id // 2
此時版本鏈是:
-
只有事務4修改過該行記錄,并在事務2執行快照讀前,就提交了事務。
-
我們的事務2在快照讀該行記錄的時候,就會拿該行記錄的DB_TRX_ID去跟up_limit_id, low_limit_id和活躍事務ID列表(trx_list)進行比較,判斷當前事務2能看到該記錄的版本。
//事務2的 Read view
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:Readview生成時刻,系統尚未分配的下一個事務ID
creator_trx_id // 2//事務4提交的記錄對應的事務ID
DB_TRX_ID=4//比較步驟
DB_TRX_ID (4) < up_limit_id (1) ? 不小于,下一步
DB_TRX_ID (4) >= low_limit_id(5) ? 不大于,下一步
m_ids.contains(DB_TRX_ID) ? 不包含,說明,事務4不在當前的活躍事務中。// 結論
故,事務4的更改,應該看到。
所以事務2能讀到的最新數據記錄是事務4所提交的版本,而事務4提交的版本也是全局角度上最新的版本
RR與RC的本質區別
當前讀取和快照讀在RR級別下的區別
下面的代碼經過測試,是完全沒有問題的。
select * from user lock in share mode, 以加共享鎖方式進行讀取,對應的就是當前讀。此處只作為測試使用,不重講。
測試表:
--設置RR模式下測試
mysql> set global transaction isolation level REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)--重啟終端mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set, 1 warning (0.00 sec)--用表
create table if not exists user(id int primary key,age int not null,name varchar(50) not null default ''
)ENGINE=InnoDB DEFAULT CHARSET=UTF8;--插入一條記錄,用來測試
mysql> insert into user values (1, 15, '黃蓉');
Query OK, 1 row affected (0.00 sec)
測試用例1-表1:
事務A操作 | 事務A描述 | 事務B描述 | 事務B操作 |
---|---|---|---|
begin | 開啟事務 | 開啟事務 | begin |
select * from user | 快照讀(無影響)查詢 | 快照讀查詢 | select * from user |
update user set age=18 where id=1; | 更新age=18 | - | - |
commit | 提交事務 | - | - |
select 快照讀,沒有讀到age=18 | select * from user | ||
select lock in share mode當前讀,讀到age=18 | select * from user lock in share mode |
測試用例2-表2:
事務A操作 | 事務A描述 | 事務B描述 | 事務B操作 |
---|---|---|---|
begin | 開啟事務 | 開啟事務 | begin |
select * from user | 快照讀,查到age=18 | - | - |
update user set age=28 where id=1; | 更新age=28 | - | - |
commit | 提交事務 | - | - |
select 快照讀 age=28 | select * from user | ||
select lock in share mode當前讀 age=28 | select * from user lock in share mode |
-
用例1與用例2:唯一區別僅僅是表1的事務B在事務A修改age前快照讀過一次age數據
-
而表2的事務B在事務A修改age前沒有進行過快照讀。
結論:
-
事務中快照讀的結果是非常依賴該事務首次出現快照讀的地方,即某個事務中首次出現快照讀,決定該事務后續快照讀結果的能力
-
delete同樣如此
RR與RC的本質區別
-
正是Read View生成時機的不同,從而造成RC,RR級別下快照讀的結果的不同
-
在RR級別下的某個事務的某條記錄的第一次快照讀會創建一個快照及Read View,將當前系統活躍的其他事務記錄起來
-
此后在調用快照讀的時候,還是使用的是同一個Read View,所以只要當前事務在其他事務提交更新之前使用過快照讀,那么之后的快照讀使用的都是同一個Read View,所以對之后的修改不可見;
-
即RR級別下,快照讀生成Read View時,Read View會記錄此時所有其他活動事務的快照,這些事務的修改對于當前事務都是不可見的。而早于Read View創建的事務所做的修改均是可見
-
而在RC級別下的,事務中,每次快照讀都會新生成一個快照和Read View,這就是我們在RC級別下的事務中可以看到別的事務提交的更新的原因
-
總之在RC隔離級別下,是每個快照讀都會生成并獲取最新的Read View;而在RR隔離級別下,則是同一個事務中的第一個快照讀才會創建Read View,之后的快照讀獲取的都是同一個Read View。
-
正是RC每次快照讀,都會形成Read View,所以,RC才會有不可重復讀問題。
讀-讀
-
不討論
寫-寫
-
現階段,直接理解成都是當前讀,當前不做深究
推薦閱讀
關于這塊,有很好的文章,推薦大家閱讀
https://blog.csdn.net/Snailwann/article/details/94724197
詳細分析MySQL事務日志(redo log和undo log) - 駿馬金龍 - 博客園
【MySQL】InnoDB 如何避免臟讀和不可重復讀_innodb怎么解決臟讀-CSDN博客
MySQL 中是如何實現事務隔離的
首先說讀未提交,它是性能最好,也可以說它是最野蠻的方式,因為它壓根兒就不加鎖,所以根本談不上什么隔離效果,可以理解為沒有隔離。
再來說串行化。讀的時候加共享鎖,也就是其他事務可以并發讀,但是不能寫。寫的時候加排它鎖,其他事務不能并發寫也不能并發讀。
最后說讀提交和可重復讀。這兩種隔離級別是比較復雜的,既要允許一定的并發,又想要兼顧的解決問題。
實現可重復讀
為了解決不可重復讀,或者為了實現可重復讀,MySQL 采用了 MVVC (多版本并發控制) 的方式。
我們在數據庫表中看到的一行記錄可能實際上有多個版本,每個版本的記錄除了有數據本身外,還要有一個表示版本的字段,記為 row trx_id,而這個字段就是使其產生的事務的 id,事務 ID 記為 transaction id,它在事務開始的時候向事務系統申請,按時間先后順序遞增。?
按照上面這張圖理解,一行記錄現在有 3 個版本,每一個版本都記錄這使其產生的事務 ID,比如事務A的transaction id 是100,那么版本1的row trx_id 就是 100,同理版本2和版本3。
在上面介紹讀提交和可重復讀的時候都提到了一個詞,叫做快照,學名叫做一致性視圖,這也是可重復讀和不可重復讀的關鍵,可重復讀是在事務開始的時候生成一個當前事務全局性的快照,而讀提交則是每次執行語句的時候都重新生成一次快照。
對于一個快照來說,它能夠讀到那些版本數據,要遵循以下規則:
- 當前事務內的更新,可以讀到;
- 版本未提交,不能讀到;
- 版本已提交,但是卻在快照創建后提交的,不能讀到;
- 版本已提交,且是在快照創建前提交的,可以讀到;
利用上面的規則,再返回去套用到讀提交和可重復讀的那兩張圖上就很清晰了。還是要強調,兩者主要的區別就是在快照的創建上,可重復讀僅在事務開始是創建一次,而讀提交每次執行語句的時候都要重新創建一次。
并發寫問題
存在這的情況,兩個事務,對同一條數據做修改。最后結果應該是哪個事務的結果呢,肯定要是時間靠后的那個對不對。并且更新之前要先讀數據,這里所說的讀和上面說到的讀不一樣,更新之前的讀叫做“當前讀”,總是當前版本的數據,也就是多版本中最新一次提交的那版。
假設事務A執行 update 操作, update 的時候要對所修改的行加行鎖,這個行鎖會在提交之后才釋放。而在事務A提交之前,事務B也想 update 這行數據,于是申請行鎖,但是由于已經被事務A占有,事務B是申請不到的,此時,事務B就會一直處于等待狀態,直到事務A提交,事務B才能繼續執行,如果事務A的時間太長,那么事務B很有可能出現超時異常。如下圖所示。?
加鎖的過程要分有索引和無索引兩種情況,比如下面這條語句
update user set age=11 where id = 1
id 是這張表的主鍵,是有索引的情況,那么 MySQL 直接就在索引數中找到了這行數據,然后干凈利落的加上行鎖就可以了。
而下面這條語句
update user set age=11 where age=10
表中并沒有為 age 字段設置索引,所以, MySQL 無法直接定位到這行數據。那怎么辦呢,當然也不是加表鎖了。MySQL 會為這張表中所有行加行鎖,沒錯,是所有行。但是呢,在加上行鎖后,MySQL 會進行一遍過濾,發現不滿足的行就釋放鎖,最終只留下符合條件的行。雖然最終只為符合條件的行加了鎖,但是這一鎖一釋放的過程對性能也是影響極大的。所以,如果是大表的話,建議合理設計索引,如果真的出現這種情況,那很難保證并發度。
解決幻讀
上面介紹可重復讀的時候,那張圖里標示著出現幻讀的地方實際上在 MySQL 中并不會出現,MySQL 已經在可重復讀隔離級別下解決了幻讀的問題。
前面剛說了并發寫問題的解決方式就是行鎖,而解決幻讀用的也是鎖,叫做間隙鎖,MySQL 把行鎖和間隙鎖合并在一起,解決了并發寫和幻讀的問題,這個鎖叫做 Next-Key鎖。
假設現在表中有兩條記錄,并且 age 字段已經添加了索引,兩條記錄 age 的值分別為 10 和 30。
?
此時,在數據庫中會為索引維護一套B+樹,用來快速定位行記錄。B+索引樹是有序的,所以會把這張表的索引分割成幾個區間。?
如圖所示,分成了3 個區間,(負無窮,10]、(10,30]、(30,正無窮],在這3個區間是可以加間隙鎖的。
之后,我用下面的兩個事務演示一下加鎖過程。?
在事務A提交之前,事務B的插入操作只能等待,這就是間隙鎖起得作用。當事務A執行update user set name='風箏2號’ where age = 10;
?的時候,由于條件 where age = 10 ,數據庫不僅在 age =10 的行上添加了行鎖,而且在這條記錄的兩邊,也就是(負無窮,10]、(10,30]這兩個區間加了間隙鎖,從而導致事務B插入操作無法完成,只能等待事務A提交。不僅插入 age = 10 的記錄需要等待事務A提交,age<10、10<age<30 的記錄頁無法完成,而大于等于30的記錄則不受影響,這足以解決幻讀問題了。
這是有索引的情況,如果 age 不是索引列,那么數據庫會為整個表加上間隙鎖。所以,如果是沒有索引的話,不管 age 是否大于等于30,都要等待事務A提交才可以成功插入。
總結
MySQL 的 InnoDB 引擎才支持事務,其中可重復讀是默認的隔離級別。
讀未提交和串行化基本上是不需要考慮的隔離級別,前者不加鎖限制,后者相當于單線程執行,效率太差。
讀提交解決了臟讀問題,行鎖解決了并發更新的問題。并且 MySQL 在可重復讀級別解決了幻讀問題,是通過行鎖和間隙鎖的組合 Next-Key 鎖實現的。