一、什么是事務
事務:是數據庫操作的最小工作單元,是作為單個邏輯工作單元執行的一系列操作;這些操作作為一個整體一起向系統提交,要么都執行、要么都不執行;事務是一組不可再分割的操作集合(工作邏輯單元);
事務的四大特性:
- 原子性(Atomicity):事務是數據庫的邏輯工作單位,事務中包含的各操作要么都做,要么都不做
- 一致性(Consistency):事務開始前和結束后,數據庫的完整性約束沒有被破壞 。比如A向B轉賬,不可能A扣了錢,B卻沒收到。
- 隔離型(Isolation):一個事務的執行不能被其它事務干擾。即一個事務內部的操作及使用的數據對其它并發事務是隔離的,并發執行的各個事務之間不能互相干擾。
- 持久性(Durability):指一個事務一旦提交,它對數據庫中的數據的改變就應該是永久性的。接下來的其它操作或故障不應該對其執行結果有任何影響。
個人認為這四大特性總結起來就是兩種:
可靠性:原子性、一致性、持久性可以歸納為可靠性。可靠就是要保證數據的一致與不丟失。數據庫要保證數據的一致,就要處理commit與rollBack;顯然處理commit指令的時候需要記錄要提交哪些數據,rollback的時候需要知道回退的原數據。MySQL中commit需要redo log,rollBack 對應undo log
并發控制(隔離性):當多個并發請求過來,并且其中有一個請求是對數據修改操作的時候會有影響,為了避免讀到臟數據,所以需要對事務之間的讀寫進行隔離,至于隔離到啥程度得看業務系統的場景了,實現這個就得用MySQL 的隔離級別。
二、redo log 與undo log
1、redo log
redo log和undo log都屬于InnoDB的事務日志。redo log 主要實現數據的持久化
InnoDB作為MySQL的存儲引擎,數據是存放在磁盤中的,但如果每次讀寫數據都需要磁盤IO,效率會很低。為此,InnoDB提供了緩存(Buffer Pool),Buffer Pool中包含了磁盤中部分數據頁的映射,作為訪問數據庫的緩沖:當從數據庫讀取數據時,會首先從Buffer Pool中讀取,如果Buffer Pool中沒有,則從磁盤讀取后放入Buffer Pool;當向數據庫寫入數據時,會首先寫入Buffer Pool,Buffer Pool中修改的數據會定期刷新到磁盤中(這一過程稱為刷臟)。Buffer Pool的使用大大提高了讀寫數據的效率,但是也帶了新的問題:如果MySQL宕機,而此時Buffer Pool中修改的數據還沒有刷新到磁盤,就會導致數據的丟失,事務的持久性無法保證。
如上圖所示mysql采用redo log來處理該問題:當數據修改時,除了修改Buffer Pool中的數據,還會在redo log Buffer 中記錄這次操作;當事務提交時,會調用fsync接口對redo log進行刷盤。如果MySQL宕機,重啟時可以讀取redo log中的數據,對數據庫進行恢復。redo log采用的是WAL(Write-ahead logging,預寫式日志),所有修改先寫入日志,再更新到Buffer Pool,保證了數據不會因MySQL宕機而丟失,從而滿足了持久性要求。
MySQL支持用戶自定義在commit時如何將log buffer中的日志刷log file中。這種控制通過變量
innodb_flush_log_at_trx_commit 的值來決定。該變量有3種值:0、1、2,默認為1。但注意,這個變量只是控制commit動作是否刷新log buffer到磁盤。
- 當設置為1的時候,事務每次提交都會將log buffer中的日志寫入os buffer并調用fsync()刷到log file on disk中。這種方式即使系統崩潰也不會丟失任何數據,但是因為每次提交都寫入磁盤,IO的性能較差。
- 當設置為0的時候,事務提交時不會將log buffer中日志寫入到os buffer,而是每秒寫入os buffer并調用fsync()寫入到log file on disk中。也就是說設置為0時是(大約)每秒刷新寫入到磁盤中的,當系統崩潰,會丟失1秒鐘的數據。
- 當設置為2的時候,每次提交都僅寫入到os buffer,然后是每秒調用fsync()將os buffer中的日志寫入到log file on disk。
既然redo log也需要在事務提交時將日志寫入磁盤,為什么它比直接將Buffer Pool中修改的數據寫入磁盤(即刷臟)要快呢?主要有以下兩方面的原因:
- 刷臟是隨機IO,因為每次修改的數據位置隨機,但寫redo log是追加操作,屬于順序IO。
- 刷臟是以數據頁(Page)為單位的,MySQL默認頁大小是16KB,一個Page上一個小修改都要整頁寫入;而redo log中只包含真正需要寫入的部分,無效IO大大減少。
2、undo log
undo log 的寫入時機與redo log一致。
InnoDB實現回滾,靠的是undo log:當事務對數據庫進行修改時,InnoDB會生成對應的undo log;如果事務執行失敗或調用了rollback,導致事務需要回滾,便可以利用undo log中的信息將數據回滾到修改之前的樣子。
undo log屬于邏輯日志,它記錄的是sql執行相關的信息。當發生回滾時,InnoDB會根據undo log的內容做與之前相反的工作:對于每個insert,回滾時會執行delete;對于每個delete,回滾時會執行insert;對于每個update,回滾時會執行一個相反的update,把數據改回去。以update操作為例:當事務執行update時,其生成的undo log中會包含被修改行的主鍵(以便知道修改了哪些行)、修改了哪些列、這些列在修改前后的值等信息,回滾時便可以使用這些信息將數據還原到update之前的狀態。
三、Mysql的鎖機制
當數據庫有并發事務的時候,可能會產生數據的不一致,這時候需要一些機制來保證訪問的次序,mysql的鎖機制可以達到該目的
1. 按照鎖的粒度分數據庫鎖有哪些?鎖機制與InnoDB鎖算法
在關系型數據庫中,可以按照鎖的粒度把數據庫鎖分為行級鎖(INNODB引擎)、表級鎖(MYISAM引擎)和頁級鎖(BDB引擎 )。
MyISAM和InnoDB存儲引擎使用的鎖:
- MyISAM采用表級鎖(table-level locking)。
- InnoDB支持行級鎖(row-level locking)和表級鎖,默認為行級鎖
行級鎖:行級鎖是Mysql中鎖定粒度最細的一種鎖,表示只針對當前操作的行進行加鎖。行級鎖能大大減少數據庫操作的沖突。其加鎖粒度最小,但加鎖的開銷也最大。行級鎖分為共享鎖 和 排他鎖。
特點:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低,并發度也最高。
表級鎖:表級鎖是MySQL中鎖定粒度最大的一種鎖,表示對當前操作的整張表加鎖,它實現簡單,資源消耗較少,被大部分MySQL引擎支持。最常使用的MYISAM與INNODB都支持表級鎖定。表級鎖定分為表共享讀鎖(共享鎖)與表獨占寫鎖(排他鎖)。
特點:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發出鎖沖突的概率最高,并發度最低。
頁級鎖 :頁級鎖是MySQL中鎖定粒度介于行級鎖和表級鎖中間的一種鎖。表級鎖速度快,但沖突多,行級沖突少,但速度慢。所以取了折衷的頁級,一次鎖定相鄰的一組記錄。
特點:開銷和加鎖時間界于表鎖和行鎖之間;會出現死鎖;鎖定粒度界于表鎖和行鎖之間,并發度一般
從鎖的類別上來講,有共享鎖和排他鎖。
- 共享鎖(S鎖): 又叫做讀鎖。當用戶要進行數據的讀取時,對數據加上共享鎖。共享鎖可以同時加上多個。事務T對數據對象A加上共享鎖,則事務T可以讀A但不能修改A,其他事務只能再對A加共享鎖,而不能加排他鎖,直到T釋放A上的共享鎖。這保證了其他事務可以讀A,但在T釋放A上的共享鎖之前不能對A做任何修改。
- 排他鎖(X鎖): 又叫做寫鎖。當用戶要進行數據的寫入時,對數據加上排他鎖。排他鎖只可以加一個。若事務T對數據對象A加上排他鎖,事務T可以讀A也可以修改A,其他事務不能再對A加任何鎖,直到T釋放A上的鎖。這保證了其他事務在T釋放A上的排他鎖之前不能再讀取和修改A。
2、InnoDB鎖的特性
由于 MySQL 的Innodb引擎的行鎖是針對索引加的鎖,不是針對記錄加的鎖,所以雖然是訪問不同行的記錄,但是如果是使用相同的索引鍵,是會出現鎖沖突的。
在不通過索引條件查詢的時候,InnoDB使用的是表鎖!
當表有多個索引的時候,不同的事務可以使用不同的索引鎖定不同的行,另外,不論 是使用主鍵索引、唯一索引或普通索引,InnoDB 都會使用行鎖來對數據加鎖。
即便在條件中使用了索引字段,但是否使用索引來檢索數據是由 MySQL 通過判斷不同 執行計劃的代價來決定的,如果 MySQL 認為全表掃 效率更高,比如對一些很小的表,它 就不會使用索引,這種情況下 InnoDB 將使用表鎖,而不是行鎖。因此,在分析鎖沖突時, 別忘了檢查 SQL 的執行計劃(explain查看),以確認是否真正使用了索引。
三、Mysql的隔離機制
Read uncommitted 讀未提交:READ UNCOMMITTED級別忽略其它事務放置的鎖。使用READ UNCOMMITTED級別運行的事務,能夠讀取尚未由其它事務提交的改動后的數據值,這些行為稱為“臟”讀。我們所說的臟讀,兩個并發的事務,事務A可以讀取到事務B未提交的數據。假設事務A回滾,事務B就讀取了一行沒有提交的數據。這種數據我們覺得是不存在的。
Read committed 讀提交:一個事務只能讀取另一個事務已經提交的修改。其避免了臟讀,但仍然存在不可重復讀和幻讀問題。大多數數據庫的默認級別就是Read committed。比方Sql Server , Oracle。
Repeatable read 反復讀:該級別指定了在當前事務提交之前,其它不論什么事務均不能夠改動或刪除當前事務已讀取的數據。并發性低于 READ COMMITTED。由于已讀數據的共享鎖在整個事務期間持有,而不是在每一個語句結束時釋放。這個隔離級別僅僅是說,不可以改動和刪除,可是并沒有強制不能插入新的滿足條件查詢的數據行。所以會產生“幻讀”;Mysql的默認隔離級別就是Repeatable read
Serializable 串行讀:完全串行化的讀,每次讀都需要獲得表級共享鎖,讀寫相互都會阻塞
| 隔離級別 | 讀數據一致性 | 臟讀 | 不可重復讀 | 幻讀 |
| 未提交讀(Read uncommitted) | 最低級別隔離,只能保證不讀取物理上損壞的數據 | 是 | 是 | 是 |
| 已提交讀(Read committed) | 語句級別 | 否 | 是 | 是 |
| 可重復讀(Repeatable read) | 事務級別 | 否 | 否 | 是 |
| 可序列化(Serializable) | 最高級別,事務級 | 否 | 否 | 否 |
- 臟讀(Drity Read):某個事務已更新一份數據,另一個事務在此時讀取了同一份數據,由于某些原因,前一個RollBack了操作,則后一個事務所讀取的數據就會是不正確的。
- 不可重復讀(Non-repeatable read):在一個事務的兩次查詢之中數據不一致,這可能是兩次查詢過程中間插入了一個事務更新了原有的數據。不可重復讀主要針對的是update與delete
- 幻讀(Phantom Read):在一個事務的兩次查詢中數據筆數不一致,例如有一個事務查詢了幾列(Row)數據,而另一個事務卻在此時插入了新的幾列數據,先前的事務在接下來的查詢中,就會發現有幾列數據是它先前所沒有的。幻讀主要是針對insert;
1、mysql解決幻讀的方式:MVCC
在InnoDB中,會在每行數據后添加兩個額外的隱藏的值來實現MVCC,這兩個值一個記錄這行數據何時被創建,另外一個記錄這行數據何時過期(或者被刪除)。 在實際操作中,存儲的并不是時間,而是事務的版本號,每開啟一個新事務,事務的版本號就會遞增。 在可重讀Repeatable reads事務隔離級別下:
- SELECT時,讀取創建版本號<=當前事務版本號,并且會移除版本號為空或>當前事務版本號的數據行。
- INSERT時,保存當前事務版本號為行的創建版本號
- DELETE時,保存當前事務版本號為行的刪除版本號
- UPDATE時,插入一條新紀錄,保存當前事務版本號為行創建版本號,同時保存當前事務版本號到原來刪除的行
舉例說明MVCC如何避免幻讀的:事務A讀取age<20的數據,返回5條,Mysql為其創建的事務版本號是10001;此時事務B插入age=18的一條數據,Mysql為其創建的事務版本號是10001;緊接著事務A再次查詢age<20的數據,返回依然是5條,也就是事務B新插入的數據對于事務A來說是隔離的。
由此我們發現在RR級別中,通過MVCC機制,雖然讓數據變得可重復讀,并且避免的幻讀,但我們讀到的數據可能是歷史數據,是不及時的數據,不是數據庫當前的數據!這在一些對于數據的時效特別敏感的業務中,就很可能出問題。對于這種讀取歷史數據的方式,我們叫它快照讀 (snapshot read),而讀取數據庫當前版本數據的方式,叫當前讀 (current read)。很顯然,在MVCC中是采取的快照讀;如果要實現當前讀就需要使用鎖機制。