事務(wù)擁有原子性、隔離性、一致性、持久性(acid)。
- 原子性:要么全成功,要么全失敗。
- 隔離性:事務(wù)之間必須保持隔離性,互補干擾,不然會出現(xiàn)臟讀,不可重復(fù)讀和幻讀。
- 一致性:事務(wù)的一致性指的是在一個事務(wù)執(zhí)行之前和執(zhí)行之后數(shù)據(jù)庫都必須處于一致性狀態(tài)。如果事務(wù)成功地完成,那么系統(tǒng)中所有變化將正確地應(yīng)用,系統(tǒng)處于有效狀態(tài)。如果在事務(wù)中出現(xiàn)錯誤,那么系統(tǒng)中的所有變化將自動地回滾,系統(tǒng)返回到原始狀態(tài)。分布式中難點就是事務(wù)的一致性。
- 持久性:事務(wù)提交的結(jié)果,將永遠保持在數(shù)據(jù)庫中。
注意:
- mysql中的innodb引擎支持事務(wù),myisam不支持。
- 代碼中不要使用大事務(wù)。因為事務(wù)都是mysql操作,大多數(shù)mysql操作會數(shù)據(jù)行加共享鎖或者排他鎖的,這些鎖都是阻塞操作,只有在事務(wù)結(jié)束后這些鎖才會釋放。如果事務(wù)執(zhí)行時間較長,鎖將一直不會釋放,如果其他請求也正好獲得鎖住的數(shù)據(jù)行,則會一直等待,等待那個大事務(wù)結(jié)束后才能獲得。
mysql中的事務(wù)自動提交AUTOCOMMIT
在Innodb存儲引擎中事務(wù)默認采取自動提交的模式,也就是說,如果不是顯式的開始一個事務(wù),則每個sql都當作是一個事務(wù)執(zhí)行操作,同樣遵循mvcc,直到顯式的執(zhí)行commit或者rollback表示該事務(wù)結(jié)束。
在當前連接中可以設(shè)置autocommit變量來啟用或者關(guān)閉自動提交,0為關(guān)閉,1為開啟。
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 行于數(shù)據(jù)集 (0.04 秒)
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 秒)
測試:客戶端A和客戶端B設(shè)置autocommit為關(guān)閉狀態(tài),對行加排他鎖
1.在客戶端A加鎖
mysql> update users set name='ds' where id=12;
Query OK, 1 rows affected (0.02 秒)
- 在客戶端B上也update
mysql> update users set name='ds' where id=12;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
按照之前的理解,在客戶端B上肯定是不會加鎖的,且是可以執(zhí)行成功的。事實證明顯id為12的數(shù)據(jù)已經(jīng)加來排他鎖。
- 在客戶端A上執(zhí)行commit后,客戶端B上id為12的行獲得排他鎖,執(zhí)行成功
mysql> commit;
Query OK, 0 rows affected (0.01 秒)
注意:修改autocommit變量對非事務(wù)類型的表,比如myisam或者內(nèi)存表,不會有任何的影響,對這類表沒有commit或者rollback的概念,也可以說一只處于autocommit啟用的模式
誤區(qū):之前以為只有start transaction來算是事務(wù)執(zhí)行,其實每個sql都是一個事務(wù)的提交,只不過在innodb中是采用的自動提交的方式。
并發(fā)造成事務(wù)隔離性破壞
第一類丟失更新
在沒有事務(wù)隔離的情況下,倆個事務(wù)都同時更新一條數(shù)據(jù),但是第二個事務(wù)卻中途退出,導(dǎo)致對第一個事務(wù)的更新失敗。
例:張三讀取火車票,查詢剩余500張,同時李四也查詢并且購買了一張,剩余499張,并且修改了數(shù)據(jù)庫。隨后,張三購買過程中發(fā)生異常,導(dǎo)致事務(wù)回滾,票數(shù)恢復(fù)為500,這樣就導(dǎo)致了李四的修改失敗。第二類丟失更新,不可重復(fù)讀的特例
倆個并發(fā)事務(wù)同時讀取一行數(shù)據(jù),然后其中一個對其進行了修改,另一個也進行了修改,然后造成了第一個寫操作的失敗。
例:現(xiàn)在火車票還剩500張, 張三讀取到票還有500張,操作還未完成,事務(wù)還未提交。于此同時,李四購買了一張票,剩余票為499.最后提交了事務(wù)。隨后,張三也購買了一張,剩余票又為499,但是應(yīng)該是498。覆蓋了張三的事務(wù)。臟讀
是指事務(wù)a修改了數(shù)據(jù)庫的某行數(shù)據(jù),但并未提交事務(wù)時,事務(wù)b這時要取這行數(shù)據(jù)進行相關(guān)操作,讀到的數(shù)據(jù)是a已經(jīng)修改過的了,接著a事務(wù)由于某些原因回退。導(dǎo)致b事務(wù)讀到的這行數(shù)據(jù)和數(shù)據(jù)庫不一致,這就是臟讀。
例:火車票剩余500張,張三購買了一張火車票,數(shù)據(jù)庫修改但是事務(wù)沒提交
隨后李四也要購買,查到剩余火車票為499張,接著張三發(fā)生突發(fā)事情,事務(wù)回退,火車票剩余500張,這時張三讀到的數(shù)據(jù)和數(shù)據(jù)庫不一致,導(dǎo)致臟讀。-
不可重復(fù)讀
不可重復(fù)讀是指在對于數(shù)據(jù)庫中的某條數(shù)據(jù),一個事務(wù)范圍內(nèi)多次查詢返回不同的數(shù)據(jù)值(這里不同是指某一條或多條數(shù)據(jù)的內(nèi)容前后不一致,但數(shù)據(jù)條數(shù)相同),這是由于在查詢間隔,該事務(wù)需要用到的數(shù)據(jù)被另一個事務(wù)修改并提交了。需要注意的是在某些情況下不可重復(fù)讀并不是問題。不可重復(fù)讀可能發(fā)生在update,delete操作中例:現(xiàn)在火車票有500張, 張三購買火車票,讀到還有500張,操作還沒有完成,事務(wù)還沒完成。于此同時李四也要買車票,并且購買了一張,并提交了事務(wù)。這是張三又查了一遍火車票,變成了499張,在一個事務(wù)中前后讀取到的倆次數(shù)據(jù)不一致,導(dǎo)致了不可重復(fù)讀。
幻讀
幻讀:幻讀是事務(wù)非獨立執(zhí)行時發(fā)生的一種現(xiàn)象。例如事務(wù)T1對一個表中所有的行的某個數(shù)據(jù)項做了從“1”修改為“2”的操作,這時事務(wù)T2又對這個表中插入了一行數(shù)據(jù)項,而這個數(shù)據(jù)項的數(shù)值還是為“1”并且提交給數(shù)據(jù)庫。而操作事務(wù)T1的用戶如果再查看剛剛修改的數(shù)據(jù),會發(fā)現(xiàn)還有一行沒有修改,其實這行是從事務(wù)T2中添加的,就好像產(chǎn)生幻覺一樣,這就是發(fā)生了幻讀?;米x和不可重復(fù)讀都是讀取了另一條已經(jīng)提交的事務(wù)(這點就臟讀不同),所不同的是不可重復(fù)讀可能發(fā)生在update,delete操作中,而幻讀發(fā)生在insert操作中
事務(wù)的隔離級別
mysql的innodb引擎的相應(yīng)的隔離級別可以解決相應(yīng)的上述問題。innodb默認的隔離級別是repeatable_read,這個級別可以解決第一類更新失敗、第二類更新失敗、臟讀、不可重復(fù)讀的問題,沒解決幻讀問題
read-uncommited(讀取未提交的)
在該隔離級別,所有事務(wù)都可以看到其他事務(wù)未提交的執(zhí)行結(jié)果。這是事務(wù)最低的隔離級別。解決第一類丟失更新和第二類丟失的問題,但是會出現(xiàn)臟讀、不可重復(fù)讀、幻讀 。本隔離級別很少用于實際應(yīng)用,因為它的性能也不比其他級別好多少read-commited(讀取已提交的)
保證一個事務(wù)修改的數(shù)據(jù)提交后才能被另外一個事務(wù)讀取,即另外一個事務(wù)不能讀取其他事務(wù)未提交的數(shù)據(jù)。解決第一類丟失更新、第二類丟失更新和臟讀的問題,但會出現(xiàn)不可重復(fù)讀和幻讀問題repeatable-read(可重復(fù)讀)
保證一個事務(wù)相同條件下前后兩次獲取的數(shù)據(jù)是一致的,為什么能保證前后倆次獲取的數(shù)據(jù)是一致的呢?原因就是mvcc,查詢時只能查到創(chuàng)建版本小于等于數(shù)據(jù)的當前版本且刪除版本大于當前版本的數(shù)據(jù)。解決第一類丟失更新,第二類丟失更新,臟讀、不可重復(fù)讀的問題,但會出現(xiàn)部分幻讀的問題,為什么是部分呢?請看下面的測試serializable(串行化)
在這個級別,可能導(dǎo)致大量的超時現(xiàn)象和鎖競爭。只要操作產(chǎn)生了共享鎖,就不允許其他事務(wù)修改!簡而言之也是解決幻讀的關(guān)鍵--也就是事務(wù)會將讀取的數(shù)據(jù)集(數(shù)據(jù)行)加上共享鎖和間隙鎖,其他事務(wù)不能寫,可以讀,直到事務(wù)結(jié)束,共享鎖釋放。它解決了所有隔離性相關(guān)問題,但是生產(chǎn)環(huán)境不會使用該隔離級別,效率太低。
注意:隔離級別越高,出現(xiàn)的并發(fā)問題就越少,但是這樣消耗的性能更大,而且并發(fā)能力會很低。所以適合自己的隔離級別是最重要的。
事務(wù)相關(guān)命令
-
查看當前的事務(wù)隔離級別
select @@global.tx_isolation; //查看全局的隔離級別,生產(chǎn)環(huán)境使用 select @@session.tx_isolation;//查看當前會話級別,下面測試使用它查詢 select @@tx_isolation; -
設(shè)置事務(wù)隔離級別
set [global | session ] transaction isolation level [read uncommitted | read committed | repeatable read | serializable]1.默認的行為(不帶session和global)是為下一個(未開始)事務(wù)設(shè)置隔離級別,得重新開啟事務(wù),不能使用本次事務(wù)。
2.如果你使用GLOBAL關(guān)鍵字,是在修改完之后創(chuàng)建的所有新連接(除了不存在的連接)設(shè)置默認事務(wù)級別,你需要SUPER權(quán)限來做這個,生產(chǎn)環(huán)境使用global。
3.使用SESSION 關(guān)鍵字將在當前連接上執(zhí)行的事務(wù)設(shè)置默認事務(wù)級別,不需要重新開啟事務(wù)。 任何客戶端都能自由改變會話隔離級別(甚至在事務(wù)的中間),或者為下一個事務(wù)設(shè)置隔離級別。 -
mysql命令
start transaction;//開始事務(wù) ...... sql語句 ...... commit;//提交事務(wù) rollback;//事務(wù)回滾
測試
在A客戶和B客戶端同時修改事務(wù)隔離級別。使用以下命令來設(shè)置隔離級別,使用session。特別注意:更改隔離級別之后測試的事務(wù)不能使用之前的事務(wù),之前的事務(wù)應(yīng)該commit或者rollback。在更改完隔離級別之后重新start事務(wù)測試。
set session transaction isolation level [read uncommitted | read committed | repeatable read | serializable]
-
客戶端A和B隔離級別為read uncommitted
1.客戶端A開啟事務(wù):mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from y_login; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 20 | 896 | 2 | NULL | | 21 | 5623656 | NULL | NULL | | 22 | 666666 | NULL | NULL | +----+---------+---------+-----------+ 3 rows in set (0.00 sec)- 客戶端b也同時開啟事務(wù):
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from y_login; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 20 | 896 | 2 | NULL | | 21 | 5623656 | NULL | NULL | | 22 | 666666 | NULL | NULL | +----+---------+---------+-----------+ 3 rows in set (0.00 sec)3.客戶端A上修改id為22的數(shù)據(jù)
mysql> update y_login set phone=888 where id=22; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> select * from y_login; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 20 | 896 | 2 | NULL | | 21 | 5623656 | NULL | NULL | | 22 | 888 | NULL | NULL | +----+---------+---------+-----------+-
在客戶端b上查詢id為22的數(shù)據(jù)
mysql> select * from y_login; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 20 | 896 | 2 | NULL | | 21 | 5623656 | NULL | NULL | | 22 | 888 | NULL | NULL | +----+---------+---------+-----------+
可以看到讀到了客戶端A未提交的事務(wù),這時就肯定造成了臟讀。用戶可以拿到這個未提交的phone數(shù)據(jù)做一些其他的相關(guān)操作,最后客戶端A如果rollback的話,關(guān)于phone的一系列操作都是錯誤的。
5.在客戶端B上對id為22的數(shù)據(jù)進行寫操作
mysql> update y_login set phone=999 where id=22; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction可以看到由于客戶端A對這行數(shù)據(jù)進行了寫操作,所以這行數(shù)據(jù)加鎖了,這行數(shù)據(jù)可以讀,但是不能寫,所以事務(wù)中進行寫操作的行會加排他鎖(innodb中是行鎖,排他鎖簡稱寫鎖),就避免了第二類更新丟失的問題。將客戶端A的事務(wù)commit或者rollback后,客戶端A釋放排他鎖,客戶端B搶到排他鎖后可進行讀寫操作,客戶端A釋放時間為6.94s。
mysql> update y_login set phone=999 where id=22; Query OK, 1 row affected (6.94 sec) Rows matched: 1 Changed: 1 Warnings: 0在客戶端 C上,沒有開啟事務(wù)正常執(zhí)行id為22的寫操作時,同樣被加鎖,只有客戶端A釋放排他鎖時,才能執(zhí)行id為22的寫操作。
-
客戶端A和B隔離級別為read committed
1.客戶端A開啟事務(wù):mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from y_login; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 20 | 896 | 2 | NULL | | 21 | 5623656 | NULL | NULL | | 22 | 8823 | NULL | NULL | +----+---------+---------+-----------+ 3 rows in set (0.00 sec)- 客戶端b也同時開啟事務(wù):
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from y_login; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 20 | 896 | 2 | NULL | | 21 | 5623656 | NULL | NULL | | 22 | 8823 | NULL | NULL | +----+---------+---------+-----------+ 3 rows in set (0.00 sec)3.客戶端A上修改id為22的數(shù)據(jù)
mysql> update y_login set phone=888 where id=22; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> select * from y_login; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 20 | 896 | 2 | NULL | | 21 | 5623656 | NULL | NULL | | 22 | 888 | NULL | NULL | +----+---------+---------+-----------+-
在客戶端B上查看數(shù)據(jù),id為22的phone依然是88823,不會讀取未提交的事務(wù)的數(shù)據(jù)。
mysql> select * from y_login; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 20 | 896 | 2 | NULL | | 21 | 5623656 | NULL | NULL | | 22 | 88823 | NULL | NULL | +----+---------+---------+-----------+ -
在做個測試,在客戶端B上修改id為22的數(shù)據(jù)
mysql> update y_login set phone=888 where id=22; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction可以看到由于客戶端A對這行數(shù)據(jù)進行了寫操作,所以這行數(shù)據(jù)加鎖了,這行數(shù)據(jù)可以讀,但是不能寫,所以事務(wù)中進行寫操作的行會加排他鎖,就避免了第二類更新丟失的問題。將客戶端A的事務(wù)commit或者rollback后,客戶端A釋放排他鎖,客戶端B搶到排他鎖后可進行讀寫操作,客戶端A釋放時間為18s。
mysql> update y_login set phone=999 where id=22; Query OK, 1 row affected (18 sec) Rows matched: 1 Changed: 1 Warnings: 0
在客戶端 C上,沒有開啟事務(wù)正常執(zhí)行id為22的寫操作時,同樣被加鎖,只有客戶端A釋放排他鎖時,才能執(zhí)行id為22的寫操作。
-
當客戶端A提交事務(wù),后查看客戶端B的數(shù)據(jù)
mysql> select * from y_login; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 20 | 896 | 2 | NULL | | 21 | 5623656 | NULL | NULL | | 22 | 888 | NULL | NULL | +----+---------+---------+-----------+
可以看到id為22的數(shù)據(jù)已經(jīng)更新,讀取的是事務(wù)提交后的數(shù)據(jù)。這樣就避免了臟讀,但是出現(xiàn)了不可重復(fù)讀的問題。
-
客戶端A和B隔離級別設(shè)置為 repeatable read
1.客戶端A開啟事務(wù):mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from y_login; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 20 | 896 | 2 | NULL | | 21 | 5623656 | NULL | NULL | | 22 | 888 | NULL | NULL | +----+---------+---------+-----------+ 3 rows in set (0.00 sec)- 客戶端b也同時開啟事務(wù):
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from y_login; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 20 | 896 | 2 | NULL | | 21 | 5623656 | NULL | NULL | | 22 | 888 | NULL | NULL | +----+---------+---------+-----------+ 3 rows in set (0.00 sec)3.客戶端A上修改id為22的數(shù)據(jù)
mysql> update y_login set phone=333 where id=22; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 mysql> select * from y_login; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 20 | 896 | 2 | NULL | | 21 | 5623656 | NULL | NULL | | 22 | 333 | NULL | NULL | +----+---------+---------+-----------+-
在客戶端B上查看數(shù)據(jù),id為22的phone依然是888,不會讀取未提交的事務(wù)的數(shù)據(jù)。
mysql> select * from y_login; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 20 | 896 | 2 | NULL | | 21 | 5623656 | NULL | NULL | | 22 | 888 | NULL | NULL | +----+---------+---------+-----------+ -
在做個測試,在客戶端B上修改id為22的數(shù)據(jù)
mysql> update y_login set phone=545 where id=22; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
可以看到由于客戶端A對這行數(shù)據(jù)進行了寫操作,所以這行數(shù)據(jù)加鎖了,這行數(shù)據(jù)可以讀,但是不能寫,所以事務(wù)中進行寫操作的行會加排他鎖,就避免了第二類更新丟失的問題。將客戶端A的事務(wù)commit或者rollback后,客戶端A釋放排他鎖,客戶端B搶到排他鎖后可進行讀寫操作,客戶端A釋放時間為18s。
mysql> update y_login set phone=545 where id=22; Query OK, 1 row affected (18 sec) Rows matched: 1 Changed: 1 Warnings: 0在客戶端 C上,沒有開啟事務(wù)正常執(zhí)行id為22的寫操作時,同樣被加鎖,只有客戶端A釋放排他鎖時,才能執(zhí)行id為22的寫操作。
-
當客戶端A提交事務(wù),后查看客戶端B的數(shù)據(jù)
mysql> select * from y_login; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 20 | 896 | 2 | NULL | | 21 | 5623656 | NULL | NULL | | 22 | 888 | NULL | NULL | +----+---------+---------+-----------+
可以看到id為22的依然是888,并沒有因為客戶端A的提交而改變數(shù)據(jù)。所以不可重復(fù)讀問題解決。
有的人說repeatable read級別可以解決幻讀,有的人說解決不了,其實是解決了一部分,解決讀幻讀,并沒有解決寫幻讀,請看下面的測試,也可以看這篇文章,對這一現(xiàn)在進行很好的測試。
有一張表如下
執(zhí)行流程如下
發(fā)現(xiàn)事務(wù)1中的倆次select結(jié)果相同,且只有初始化的一條,后勤部,并沒有事務(wù)2中insert的數(shù)據(jù),這是因為MVCC的原因,mvcc查詢時只查詢創(chuàng)建版本小于或等于當前版本的原因,所以事務(wù)1中并沒有查到事務(wù)2中的數(shù)據(jù),那既然這樣串行化serializable級別讀貌似就沒啥意義了,帶著疑問繼續(xù)測試。
測試前數(shù)據(jù)如下

在事務(wù)1中select查詢到1條數(shù)據(jù),但是在update時卻更新了2條數(shù)據(jù),出現(xiàn)了幻讀的現(xiàn)象。
這種結(jié)果告訴我們其實在MySQL可重復(fù)讀的隔離級別中并不是完全解決了幻讀的問題,而是解決了讀數(shù)據(jù)情況下的幻讀問題。而對于修改的操作依舊存在幻讀問題,就是說MVCC對于幻讀的解決時不徹底的。
為了解決寫幻的情況,serializable隔離級別就派上用場了,在serializable級別中的類似select * from dept這樣的語句也會加上共享鎖,且會加上間隙鎖(在其他隔離級別中此語句屬于快照讀,不加鎖),這樣就導(dǎo)致在事務(wù)2中的插入操作為阻塞狀態(tài),直到事務(wù)1中commit之后才會進行事務(wù)2中的update操作,進而解決了寫幻讀,可以看出serializable級別解決寫幻讀的關(guān)鍵是間隙鎖。
- 客戶端A和B設(shè)置隔離級別為serializable
事務(wù)會將獲取的數(shù)據(jù)集加上一個共享鎖,且每條數(shù)據(jù)間都會加上間隙鎖,相關(guān)數(shù)據(jù)集的寫操作都不能進行,直到共享鎖釋放也就是事務(wù)提交。-
在客戶端A開始事務(wù)并查詢
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from y_login; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 21 | 5623656 | NULL | NULL | | 23 | 54115 | NULL | NULL | | 24 | 541115 | NULL | NULL | +----+---------+---------+-----------+
-
-
在客戶端B上,更新id為24數(shù)據(jù),由于共享鎖不能加排他鎖,所以報加鎖問題
mysql> update y_login set phone=23 where id=24; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction -
在客戶端上,新增一條記錄,由于每條記錄間加了間隙鎖,所以報加鎖問題。
mysql> insert into y_login(phone) values(2442); ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
現(xiàn)在解決了寫幻讀和讀幻讀的的問題,但是效率很差,很多超時的現(xiàn)象出現(xiàn)。repeatable read隔離級別只能解決寫幻讀的問題。
-
在做個測試,在客戶端A查詢id小于24的數(shù)據(jù)集
mysql> start transaction; Query OK, 0 rows affected (0.00 sec) mysql> select * from y_login where id<24; +----+---------+---------+-----------+ | id | phone | islogin | logintime | +----+---------+---------+-----------+ | 21 | 5623656 | NULL | NULL | | 23 | 54115 | NULL | NULL | +----+---------+---------+-----------+
這里是將id小于24的數(shù)據(jù)集都加共享鎖,猜想如果插入一條數(shù)據(jù)id小于24會報加鎖問題,如果id大于24的話應(yīng)該可以插入。在客戶端B插入數(shù)據(jù)
insert into y_login(id,phone) values(25,2442);
Query OK, 1 row affected (0.01 sec)
insert into y_login(id,phone) values(1,2442);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
證明猜想正確。
思考
前提是innodb存儲引擎下,在myisam存儲引擎下是不支持事務(wù)的。在各個隔離級別下,事務(wù)如果沒提交,但是sql已經(jīng)執(zhí)行完后,會直接修改寫入數(shù)據(jù)庫文件嗎?還是必須在事務(wù)結(jié)束后才會寫到數(shù)據(jù)庫文件呢?
之前認為是事務(wù)開始后,執(zhí)行了sql語句后,數(shù)據(jù)庫就會立馬改變,最后數(shù)據(jù)庫在根據(jù)事務(wù)的結(jié)果進行rollback還是commit。一個不經(jīng)意的測試,結(jié)果并不是這樣,數(shù)據(jù)庫而是在事務(wù)結(jié)束后根據(jù)事務(wù)的狀態(tài)來判斷是否更新狀態(tài)。
-
事務(wù)隔離級別 read uncommitted
-
在客戶端A開始事務(wù),此時數(shù)據(jù)庫中id為12的password為42222
mysql> start transaction; Query OK, 0 rows affected (0.01 秒) mysql> select password from users where id=12; +----------+ | password | +----------+ | 42222 | +----------+ -
繼續(xù)在客戶端操作,更改id為12的password
mysql> update users set password=333 where id=12; Query OK, 1 rows affected (0.01 秒) -
在客戶端B上查詢,已經(jīng)變?yōu)?3
mysql> select password from users where id=12; +----------+ | password | +----------+ | 333 | +----------+ -
查看數(shù)據(jù)庫,此時數(shù)據(jù)未改變333,仍然未事務(wù)開始時數(shù)據(jù)42222
WX20190620-175004@2x.png 在客戶端A上事務(wù)commit后,數(shù)據(jù)改變?yōu)?33。
測試剩余的隔離級別也是同樣情況,磁盤中的mysql文件只有在事務(wù)提交后才會相應(yīng)的改變,也就是在事務(wù)結(jié)束后才會io操作。至于sql為什么查詢出來的改變了?
引用《高性能mysql》一說中所說,是因為事務(wù)日志,事務(wù)日志可以提高事務(wù)的效率,使用事務(wù)日志,存儲引擎在修改表數(shù)據(jù)的時候只需要修改并往內(nèi)存拷貝,事務(wù)提交后在將內(nèi)存的行記錄記錄到硬盤中的文件中,而不用每次將修改的數(shù)據(jù)本身持久化到磁盤中。事務(wù)日志持久之后,內(nèi)存中修改的數(shù)據(jù)可以慢慢刷新到磁盤中。如果數(shù)據(jù)的修改已經(jīng)記錄到事務(wù)日志并持久化,但數(shù)據(jù)還沒有同步到磁盤,此時系統(tǒng)崩潰的話,存儲引擎會在重啟后自動回復(fù)這部分修改的內(nèi)容。由此知道了,事務(wù)的執(zhí)行順序是start transaction,隨后寫入事務(wù)日志,如果rollback的話根據(jù)事務(wù)日志中的事務(wù)id進行回退,如果commit的話將事務(wù)日志中的內(nèi)容寫入到磁盤中進行持久化。
-
redo undo日志
說到事務(wù)日志就必須說一下redo undo日志,首先介紹一個這倆分別是什么
- undo日志
用于存放數(shù)據(jù)修改被修改前的值,假設(shè)修改 tba 表中 id=2的行數(shù)據(jù),把Name=’B’ 修改為Name = ‘B2’ ,那么undo日志就會用來存放Name=’B’的記錄,如果這個修改出現(xiàn)異常,可以使用undo日志來實現(xiàn)回滾操作,保證事務(wù)的一致性,事務(wù)能夠回滾也正是因為undo日志的存在。
- redo日志
當數(shù)據(jù)庫對數(shù)據(jù)做修改的時候,需要把數(shù)據(jù)頁從磁盤讀到buffer pool中,然后在buffer pool中進行修改,那么這個時候buffer pool中的數(shù)據(jù)頁就與磁盤上的數(shù)據(jù)頁內(nèi)容不一致,稱buffer pool的數(shù)據(jù)頁為dirty page 臟數(shù)據(jù),如果這個時候發(fā)生非正常的DB服務(wù)重啟,那么這些數(shù)據(jù)還沒在內(nèi)存,并沒有同步到磁盤文件中(注意,同步到磁盤文件是個隨機IO),也就是會發(fā)生數(shù)據(jù)丟失,如果這個時候,能夠在有一個文件,當buffer pool 中的data page變更結(jié)束后,把相應(yīng)修改記錄記錄到這個文件(注意,記錄日志是順序IO),那么當DB服務(wù)發(fā)生crash的情況,恢復(fù)DB的時候,也可以根據(jù)這個文件的記錄內(nèi)容,重新應(yīng)用到磁盤文件,數(shù)據(jù)保持一致。
這個文件就是redo log ,用于記數(shù)據(jù)修改后的記錄,順序記錄。它可以帶來這些好處:
- 當buffer pool中的dirty page 還沒有刷新到磁盤的時候,發(fā)生crash,啟動服務(wù)后,可通過redo log 找到需要重新刷新到磁盤文件的記錄;
- buffer pool中的數(shù)據(jù)直接flush到disk file,是一個隨機IO,效率較差,而把buffer pool中的數(shù)據(jù)記錄到redo log,是一個順序IO,可以提高事務(wù)提交的速度;
倆者區(qū)別可以看這邊文章,我總結(jié)下吧,undo日志是記錄數(shù)據(jù)修改前的數(shù)據(jù),使事務(wù)可以回滾,保證事務(wù)的一致性,而redo日志保存的數(shù)據(jù)修改后的數(shù)據(jù),保證了數(shù)據(jù)不丟失。在事務(wù)中redo 和undo日志都會寫入。
總結(jié)
在隔離性為read committed,repeatable read事務(wù)中會對寫操作的行加排他鎖,讀不加鎖(mvcc適用于這倆種隔離級別)。在read uncommitted隔離級別中寫操作不加排他鎖,數(shù)據(jù)之間會共享。在隔離性為serializable的事務(wù)中會對讀到的數(shù)據(jù)集加共享鎖也會對寫操作的行加排他鎖。


