以前對於數據庫事務概念的理解有不少不到位的地方,今天用簡單的實例再來闡述一下數據庫事務和隔離級別的概念,也方便之後溫故而知新。mysql
事務(Transaction)是併發控制的基本單位。所謂的事務,它是一個操做序列,這些操做要麼都執行,要麼都不執行,它是一個不可分割的工做單位。例如,銀行轉帳工做:從一個帳號扣款並使另外一個帳號增款,這兩個操做要麼都執行,要麼都不執行。因此,應該把它們當作一個事務。事務是數據庫維護數據一致性的單位,在每一個事務結束時,都能保 持數據一致性。
咱們以Msql數據庫的操做爲例,再進一步解釋一下數據庫事務: 首先咱們用如下命令查看該Mysql會話的事務隔離級別,關於事務隔離級別及其做用,咱們在後面的章節中會進行詳細介紹,這裏只要簡單知道數據庫能夠設置不一樣的事務隔離級別,不一樣的隔離級別會對事務的操做產生不一樣的效果便可。使用如下命令能夠查詢當前Mysql會話的事務隔離級別,能夠看到,Mysql默認的事務隔離級別是REPEATABLE-READ。sql
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
複製代碼
爲了用實例來解釋事務,咱們建立了以下的bank數據表,並插入一條數據,數據庫
mysql> describe bank;
+---------+---------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+---------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| name | varchar(40) | NO | | NULL | |
| balance | decimal(10,2) | YES | | NULL | |
+---------+---------------+------+-----+---------+----------------+
mysql> select * from bank;
+----+------+---------+
| id | name | balance |
+----+------+---------+
| 3 | fufu | 2000.00 |
+----+------+---------+
複製代碼
使用start transaction命令開啓數據庫事務,安全
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
複製代碼
更新id爲3的行的balance值爲3000.00,bash
mysql> update bank set balance = 3000 where id = 3; Query OK, 1 row affected (0.09 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> select * from bank; +----+------+---------+ | id | name | balance | +----+------+---------+ | 3 | fufu | 3000.00 | +----+------+---------+ 1 row in set (0.00 sec) 複製代碼
此時咱們能夠看到,select語句查詢到的id爲3的行的balance值已經修改成3000.00,接下來咱們再嘗試插入一條新數據,markdown
mysql> insert into bank (name, balance) values ('melo', 1000); Query OK, 1 row affected (0.06 sec) mysql> select * from bank; +----+------+---------+ | id | name | balance | +----+------+---------+ | 3 | fufu | 3000.00 | | 4 | melo | 1000.00 | +----+------+---------+ 2 rows in set (0.00 sec) 複製代碼
因爲以上的update和insert操做都是在start transaction命令開啓事務以後,因此直到事務結束,這些操做都屬於同一事務,假設咱們在insert操做時產生了錯誤,能夠根據事務的定義得知,這些屬於同一事務的全部操做要麼都執行要麼都不執行,咱們能夠驗證一下,使用rollback命令,模擬事務失敗回滾,併發
mysql> rollback;
Query OK, 0 rows affected (0.01 sec)
複製代碼
此時咱們在查詢數據庫中的全部數據,發現數據恢復到了update命令執行前的狀態,id爲3的行的balance值等於2000沒有變化。性能
mysql> select * from bank; +----+------+---------+ | id | name | balance | +----+------+---------+ | 3 | fufu | 2000.00 | +----+------+---------+ 1 row in set (0.00 sec) 複製代碼
到此,咱們闡述了數據庫事務的定義並用簡單的Mysql操做說明了事務的操做方式,咱們能夠總結出數據庫事務的生命週期以下:
spa
如今咱們回過頭來思考一下上述示例,示例中的全部操做都是在一個Mysql會話中進行的,也就是沒有其餘用戶在同時鏈接數據庫進行操做,在這種沒有併發會話的使用場景中,不管事務是正常結束仍是異常結束,對於該單獨用戶讀取數據不會形成任何影響,由於他的全部操做都是串行的。可是在實際應用場景中,數據庫每時每刻都服務於不少會話,假設用戶A的事務A開始後更新了數據庫數據,此時用戶B開始讀取該數據,用戶B將會讀取到了新的值。可是若是緊接着事務A在下一條SQL語句操做時產生了錯誤,將事務A回滾了,那麼用戶B讀取到的數據就是錯誤的無效數據了。這只是數據庫事務在併發環境下會產生的一個簡單的問題,因此接下來詳細闡述併發事務會產生的問題。日誌
這節咱們主要說明併發事務時可能會出現的問題,咱們用時間點和事務操做表格的方式來舉例。
定義:A事務撤銷時,把已經提交的B事務的更新數據覆蓋了。
時間點 | 事務A | 事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢帳戶餘額爲1000元 | |
T4 | 查詢帳戶餘額爲1000元 | |
T5 | 存入100元把餘額改成1100元 | |
T6 | 提交事務 | |
T7 | 取出100元把餘額改成900元 | |
T8 | 撤銷事務 | |
T9 | 餘額恢復爲1000元(丟失更新) |
以上的示例演示了第一類丟失更新問題,事務B雖然成功了,可是它所作的更新沒有被永久存儲,這種併發問題是因爲徹底沒有隔離事務形成的。當兩個事務更新相同的數據時,若是一個事務被提交,另外一個事務卻撤銷,那麼會連同第一個事務所作的更新也被撤銷了。(這是絕對避免出現的事情) 事務A的開始時間和結束時間包含事務B的開始和結束時間,事務A回滾事務的同時,把B的已經提交的事務也回滾的,這是避免的,這就是第一類丟失更新.
定義:A事務提交時,把已經提交的B事務的更新數據覆蓋了。
時間點 | 事務A | 事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢帳戶餘額爲1000元 | |
T4 | 查詢帳戶餘額爲1000元 | |
T5 | 取出100元把餘額改成900元 | |
T6 | 提交事務 | |
T7 | 存入100元把餘額改成1100 | |
T8 | 提交事務 | |
T9 | 餘額恢復爲1100元(丟失更新) |
第二類丟失更新和第一類的區別其實是對數據的影響是由A事務的撤銷仍是提交形成的,它和不可重複讀(下面介紹)本質上是同一類併發問題,一般把它看作是不可重複讀的一個特例。兩個或多個事務查詢同一數據。而後都基於本身的查詢結果更新數據,這時會形成最後一個提交的更新事務,將覆蓋其它已經提交的更新事務。
定義:讀到未提交更新的數據
時間點 | 事務A | 事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢帳戶餘額爲1000元 | |
T4 | 取出500元把餘額改成500元 | |
T5 | 查詢帳戶餘額爲500元(髒讀) | |
T6 | 撤銷事務,餘額恢復爲1000元 | |
T7 | 存入100元把餘額改成600元 | |
T8 | 提交事務 |
A事務查詢到了B事務未提交的更新數據,A事務依據這個查詢結果繼續執行相關操做。可是接着B事務撤銷了所作的更新,這會致使A事務操做的是髒數據,以上的示例中T5時刻產生了髒讀,最終致使A事務提交時帳戶餘額的不正確,可能有人會有疑問,B事務尚未提交或撤銷,T5時刻A事務爲何能讀到已經改變的數據,這裏要說的是,數據表中的數據是實時改變的,事務只是控制數據的最終狀態,也就是說若是沒有正確的隔離級別,在更新操做語句結束後,即便事務未完成,其餘事務就已經能夠讀取到改變的數據值了。
如今爲止:全部的數據庫都避免髒讀操,能夠用兩個Mysql會話試驗一下以上的操做,在默認的隔離級別下(REPEATABLE-READ),A事務在T5時刻讀取到的餘額爲1000元,不會是500元。
定義:讀到已經提交更新的數據,但一個事務範圍內兩個相同的查詢卻返回了不一樣數據。
時間點 | 事務A | 事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 查詢帳戶餘額爲1000元 | |
T4 | 查詢帳戶餘額爲1000元 | |
T5 | 取出100元把餘額改成900元 | |
T6 | 提交事務 | |
T7 | 查詢帳戶餘額爲900元(與T4讀取的一不一致,不可重複讀) |
定義:讀到已提交插入數據,幻讀與不可重複讀相似,幻讀是查詢到了另外一個事務已提交的新插入數據,而不可重複讀是查詢到了另外一個事務已提交的更新數據。
時間點 | 事務A | 事務B |
---|---|---|
T1 | 開始事務 | |
T2 | 開始事務 | |
T3 | 統計用戶Z總存款數爲1000元 | |
T4 | 新增Z的一個存款帳號,存款100元 | |
T5 | 提交事務 | |
T6 | ||
T7 | 再次統計用戶Z總存款數爲1100元(與T4讀取的一不一致,幻讀) |
A事務第一次查詢時,沒有問題,第二次查詢時查到了B事務已提交的新插入數據,這致使兩次查詢結果不一樣。
不可重複讀和幻讀的區別: 簡單來講,不可重複讀是因爲數據修改引發的,幻讀是由數據插入或者刪除引發的。
不可重複讀,是指在數據庫訪問中,一個事務範圍內兩個相同的查詢卻返回了不一樣數據。這是因爲查詢時系統中其餘事務修改的提交而引發的。好比事務T1讀取某一數據,事務T2讀取並修改了該數據,T1爲了對讀取值進行檢驗而再次讀取該數據,便獲得了不一樣的結果。
一種更易理解的說法是:在一個事務內,屢次讀同一個數據。在這個事務尚未結束時,另外一個事務也訪問該同一數據。那麼,在第一個事務的兩次讀數據之間。因爲第二個事務的修改,那麼第一個事務讀到的數據可能不同,這樣就發生了在一個事務內兩次讀到的數據是不同的,所以稱爲不可重複讀,即原始讀取不可重複。
所謂幻讀,是指事務A讀取與搜索條件相匹配的若干行。事務B以插入或刪除行等方式來修改事務A的結果集,而後再提交。
幻讀是指當事務不是獨立執行時發生的一種現象,例如第一個事務對一個表中的數據進行了修改,好比這種修改涉及到表中的「所有數據行」。同時,第二個事務也修改這個表中的數據,這種修改是向表中插入「一行新數據」。那麼,之後就會發生操做第一個事務的用戶發現表中還有沒有修改的數據行,就好象發生了幻覺同樣.通常解決幻讀的方法是增長範圍鎖RangeS,鎖定檢鎖範圍爲只讀,這樣就避免了幻讀。
以上就是數據庫併發事務致使的五大問題,總結來講其中兩類是更新問題,三類是讀問題,數據庫是如何避免這種併發事務問題的呢?答案就是經過不一樣的事務隔離級別,在不一樣的隔離級別下,併發事務讀取數據的結果是不同的,好比在髒讀小節裏介紹的,若是是在REPEATABLE-READ隔離級別下,A事務在T5時刻讀取是讀取不到B事務未提交的數據的。咱們須要根據業務的要求,設置不一樣的隔離級別,在效率和數據安全性中找到平衡點。
SQL標準定義了4類隔離級別,包括了一些具體規則,用來限定事務內外的哪些改變是可見的,哪些是不可見的。低級別的隔離級通常支持更高的併發處理,並擁有更低的系統開銷。
當數據庫系統使用SERIALIZABLE隔離級別時,一個事務在執行過程當中徹底看不到其餘事務對數據庫所作的更新。當兩個事務同時操做數據庫中相同數據時,若是第一個事務已經在訪問該數據,第二個事務只能停下來等待,必須等到第一個事務結束後才能恢復運行。所以這兩個事務其實是串行化方式運行。
當數據庫系統使用REPEATABLE READ隔離級別時,一個事務在執行過程當中能夠看到其餘事務已經提交的新插入的記錄,可是不能看到其餘事務對已有記錄的更新。
當數據庫系統使用READ COMMITTED隔離級別時,一個事務在執行過程當中能夠看到其餘事務已經提交的新插入的記錄,並且還能看到其餘事務已經提交的對已有記錄的更新。
當數據庫系統使用READ UNCOMMITTED隔離級別時,一個事務在執行過程當中能夠看到其餘事務沒有提交的新插入的記錄,並且還能看到其餘事務沒有提交的對已有記錄的更新。
以上的四種隔離級別按從高到底排序,你可能會說,選擇SERIALIZABLE,由於它最安全!沒錯,它是最安全,但它也是最慢的!四種隔離級別的安全性與性能成反比!最安全的性能最差,最不安全的性能最好!
經過以上的四種隔離級別的定義,咱們已經能夠分析出,每一個隔離級別能夠避免哪些併發問題了,總結一下以下表:
隔離級別 | 第一類丟失更新 | 第二類丟失更新 | 髒讀 | 不可重複讀 | 幻讀 |
---|---|---|---|---|---|
SERIALIZABLE (串行化) | 避免 | 避免 | 避免 | 避免 | 避免 |
REPEATABLE READ(可重複讀) | 避免 | 避免 | 避免 | 避免 | 容許 |
READ COMMITTED (讀已提交) | 避免 | 容許 | 避免 | 容許 | 容許 |
READ UNCOMMITTED(讀未提交) | 避免 | 容許 | 容許 | 容許 | 容許 |
咱們經過隔離級別的定義很容易本身分析出這張表,好比可重複讀隔離級別的定義是一個事務在執行過程當中能夠看到其餘事務已經提交的新插入的記錄,可是不能看到其餘事務對已有記錄的更新。因此,在這種隔離級別下,在髒讀示例的T5時刻和不可重複讀的T7時刻,事務A都是不管事務B是否提交,事務A都是沒法讀取到事務B對已有記錄的更新的,因此不會產生髒讀和不可重複讀,而又因爲這種隔離級別下能夠看到其餘事務已經提交的新插入記錄,天然是沒法避免幻讀的產生。另外,值得注意的是全部隔離級別均可以免第一類丟失更新的問題。 大多數關係數據庫默認使用Read committed的隔離級別,Mysql InnoDB默認使用Read repeatable的隔離級別,這和Mysql replication 機制使用Statement日誌格式有關。各數據庫隔離級別的實現也是有差異的,例如Oracle支持Read committed 和Serializable兩種隔離級別,另外能夠經過使用讀快照在Read committed級別上禁止不可重複讀問題;MySQL默認採用RR隔離級別,SQL標準是要求RR解決不可重複讀的問題,可是由於MySQL採用了gap lock,因此實際上MySQL的RR隔離級別也解決了幻讀的問題,也就是Mysql InnoDB在Read repeatable級別上使用next-key locking 策略來避免幻讀現象的產生。