什么是事務
事務是一條或多條數據庫操作語句的組合,具備ACID,4個特點。
- 原子性:要不全部成功,要不全部撤銷
- 隔離性:事務之間相互獨立,互不干擾
- 一致性:數據庫正確地改變狀態后,數據庫的一致性約束沒有被破壞
- 持久性:事務的提交結果,將持久保存在數據庫中
事務并發會產生什么問題
臟讀:臟讀就是指當一個事務正在訪問數據,并且對數據進行了修改,而這種修改還沒有提交到數據庫中,這時,另外一個事務也訪問這個數據,然后使用了這個數據,前一個事務回滾了,導致后一個事務的數據是無效的
不可重復讀:是指在一個事務內,多次讀同一數據。在這個事務還沒有結束時,另外一個事務也訪問該同一數據。那么,在第一個事務中的兩次讀數據之間,由于第二個事務的修改,那么第一個事務兩次讀到的的數據可能是不一樣的。這樣就發生了在一個事務內兩次讀到的數據是不一樣的,因此稱為是不可重復讀。
幻讀:是指當事務不是獨立執行時發生的一種現象,例如第一個事務對一個表中的數據進行了修改,這種修改涉及到表中的全部數據行。同時,第二個事務也修改這個表中的數據,這種修改是向表中插入一行新數據。那么,以后就會發生操作第一個事務的用戶發現表中還有沒有修改的數據行,就好象發生了幻覺一樣。
事務隔離級別,解決什么并發問題,以及存在什么并發問題
- READ_UNCOMMITTED
這是事務最低的隔離級別,它充許另外一個事務可以看到這個事務未提交的數據。
解決第一類丟失更新的問題,但是會出現臟讀、不可重復讀、第二類丟失更新的問題,幻讀 。 - READ_COMMITTED
保證一個事務修改的數據提交后才能被另外一個事務讀取,即另外一個事務不能讀取該事務未提交的數據。
解決第一類丟失更新和臟讀的問題,但會出現不可重復讀、第二類丟失更新的問題,幻讀問題 - REPEATABLE_READ
保證一個事務相同條件下前后兩次獲取的數據是一致的
解決第一類丟失更新,臟讀、不可重復讀、第二類丟失更新的問題,但會出幻讀。 - SERIALIZABLE
事務被處理為順序執行。
解決所有問題
提醒:
Mysql默認的事務隔離級別為repeatable_read
MVCC(Multi-Version Concurrency Control) 多版本并發控制
MVCC的實現,是通過保存數據在某個時間點的多版本快照來實現的.
InnoDB的MVCC是通過在每行記錄后面保存2個隱藏的列來實現的,一列保存了行的創建時間,一列保存了行的過期時間(或刪除時間).但它們都存儲的是系統版本號
MVCC最大的作用是: 實現了非阻塞的讀操作,寫操作也只鎖定了必要的行.
MYSQL的MVCC 只在 read committed 和 repeatable read 2個隔離級別下工作.
-
在MVCC的機制下,mysql InnoDB(默認隔離級別)的增刪改查變成了如下模式:
SELECT:
InnoDB只查找版本早于當前事務版本的數據行(行的系統版本號小于等于事務的系統版本號)
行的刪除號要么未定義,要么大于當前事務版本號,這樣可以確保事務讀取到的行,在事務開始之前未被刪除.INSERT:
InnoDB 為新插入的每一行保存當前系統版本號做為行版本號。
DELETE:
INNODB 為刪除的每一行保存當前系統版本號作為行刪除標識UPDATE:
InnoDB 為插入的每一行新記錄,保存當前系統版本號作為行版本號,同時保存當前系統版本號到原來的行作為行刪除標識.
并發控制機制
- 樂觀鎖是一種思想,它其實并不是一種真正的『鎖』,它會先嘗試對資源進行修改,在寫回時判斷資源是否進行了改變,如果沒有發生改變就會寫回,否則就會進行重試,在整個的執行過程中其實都沒有對數據庫進行加鎖;
- 悲觀鎖就是一種真正的鎖了,它會在獲取資源前對資源進行加鎖,確保同一時刻只有有限的線程能夠訪問該資源,其他想要嘗試獲取資源的操作都會進入等待狀態,直到該線程完成了對資源的操作并且釋放了鎖后,其他線程才能重新操作資源;
鎖的種類
對數據的操作其實只有兩種,也就是讀和寫,而數據庫在實現鎖時,也會對這兩種操作使用不同的鎖;InnoDB 實現了標準的行級鎖,也就是共享鎖(Shared Lock)和互斥鎖(Exclusive Lock);共享鎖和互斥鎖的作用其實非常好理解:
- 共享鎖(讀鎖):允許事務對一條行數據進行讀取;
- 互斥鎖(寫鎖):允許事務對一條行數據進行刪除或更新;
鎖的粒度
無論是共享鎖還是互斥鎖其實都只是對某一個數據行進行加鎖,InnoDB 支持多種粒度的鎖,也就是行鎖和表鎖;為了支持多粒度鎖定,InnoDB 存儲引擎引入了意向鎖(Intention Lock),意向鎖就是一種表級鎖。
與上一節中提到的兩種鎖的種類相似的是,意向鎖也分為兩種:
- 意向共享鎖:事務想要在獲得表中某些記錄的共享鎖,需要在表上先加意向共享鎖;
- 意向互斥鎖:事務想要在獲得表中某些記錄的互斥鎖,需要在表上先加意向互斥鎖;
意向鎖其實不會阻塞全表掃描之外的任何請求,它們的主要目的是為了表示是否有人請求鎖定表中的某一行數據。
- 有的人可能會對意向鎖的目的并不是完全的理解,我們在這里可以舉一個例子:如果沒有意向鎖,當已經有人使用行鎖對表中的某一行進行修改時,如果另外一個請求要對全表進行修改,那么就需要對所有的行是否被鎖定進行掃描,在這種情況下,效率是非常低的;不過,在引入意向鎖之后,當有人使用行鎖對表中的某一行進行修改之前,會先為表添加意向互斥鎖(IX),再為行記錄添加互斥鎖(X),在這時如果有人嘗試對全表進行修改就不需要判斷表中的每一行數據是否被加鎖了,只需要通過等待意向互斥鎖被釋放就可以了。
鎖的算法
Record Lock、Gap Lock 和 Next-Key Lock。
Record Lock
記錄鎖(Record Lock)是加到索引記錄上的鎖,假設我們存在下面的一張表 users:
CREATE TABLE users(
id INT NOT NULL AUTO_INCREMENT,
last_name VARCHAR(255) NOT NULL,
first_name VARCHAR(255),
age INT,
PRIMARY KEY(id),
KEY(last_name),
KEY(age)
);
如果我們使用 id 或者 last_name 作為 SQL 中 WHERE 語句的過濾條件,那么 InnoDB 就可以通過索引建立的 B+ 樹找到行記錄并添加索引,但是如果使用 first_name 作為過濾條件時,由于 InnoDB 不知道待修改的記錄具體存放的位置,也無法對將要修改哪條記錄提前做出判斷就會鎖定整個表
Gap Lock
記錄鎖是在存儲引擎中最為常見的鎖,除了記錄鎖之外,InnoDB 中還存在間隙鎖(Gap Lock),間隙鎖是對索引記錄中的一段連續區域的鎖;當使用類似 SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE; 的 SQL 語句時,就會阻止其他事務向表中插入 id = 15 的記錄,因為整個范圍都被間隙鎖鎖定了。
雖然間隙鎖中也分為共享鎖和互斥鎖,不過它們之間并不是互斥的,也就是不同的事務可以同時持有一段相同范圍的共享鎖和互斥鎖,它唯一阻止的就是其他事務向這個范圍中添加新的記錄。
Next-Key Lock
Next-Key 鎖相比前兩者就稍微有一些復雜,它是記錄鎖和記錄前的間隙鎖的結合,在 users 表中有以下記錄:
如果使用 Next-Key 鎖,那么 Next-Key 鎖就可以在需要的時候鎖定以下的范圍:
Next-Key 鎖鎖定的是當前值和前面的范圍。
當我們更新一條記錄,比如 SELECT * FROM users WHERE age = 30 FOR UPDATE;,InnoDB 不僅會在范圍 (21, 30] 上加 Next-Key 鎖,還會在這條記錄后面的范圍 (30, 40] 加間隙鎖,所以插入 (21, 40] 范圍內的記錄都會被鎖定。
Next-Key 鎖的作用其實是為了解決幻讀的問題
死鎖的發生
既然 InnoDB 中實現的鎖是悲觀的,那么不同事務之間就可能會互相等待對方釋放鎖造成死鎖,最終導致事務發生錯誤;想要在 MySQL 中制造死鎖的問題其實非常容易:
InnoDB鎖
行鎖
支持并發高,帶來最大的鎖開銷. 在存儲引擎層實現,服務器感知不到表鎖
服務器會為諸如: ALTER Table 之類的語句使用表鎖,忽略存儲引擎的鎖機制
鎖的類型又分為:
(1). 共享鎖(S Lock) , 允許事務讀取一行數據
(2). 排他鎖(X Lock),允許事務刪除或更新一行數據.意向鎖
InnoDB還實現了一種鎖,叫意向鎖(Intention Lock).意向鎖是將鎖定的對象分為多個層次.
意向鎖的類型分為:
(1). 意向共享鎖(IS Lock)
(2). 意向排他鎖(IX Lock)
比如: 需要對頁上的記錄加X鎖,那么需要分別對 數據庫A,表,頁 上加意向鎖IX,最后對記錄r上加X鎖.
一旦對數據庫A,表,頁上加IX鎖失敗,則阻塞.
一致性非鎖定讀
不加鎖的讀
- 是InnoDB存儲引擎下的讀取數據的方式( read committed 和 repeatable read).
- 讀取mysql數據庫時,如果讀取的行正在執行DELETE,UPDATE等操作,這時,讀取操作不會因此去等待行上的X鎖釋放,相反,InnoDB會讀取行的一個快照數據.
這樣利用MVCC,InnoDB實現了非阻塞讀的實現.極大的提高了數據庫的并發性.
但在不同的事務隔離級別下讀取的數據的方式也不一樣:
(1). 在read committed隔離級別下:
一致性非鎖定讀總是讀取被鎖定行的最新一份快照數據. 產生了不可重復讀的問題.
(2). 在repeatable read 事務隔離級別下:
一致性非鎖定讀總是讀取事務開始時的行數據版本. 解決不可重復讀的問題
一致性鎖定讀
還有一種讀的方式叫: 一致性鎖定讀(加鎖的讀).
1). select .... for update. 加X鎖
2). select .... lock in share mode. 加S鎖
InnoDB 鎖的算法
- Record Lock: 每個行記錄的鎖
- GAP Lock: 間隙鎖,鎖定一個范圍,但不包含記錄本身.
- Next-Key Lock: Gap Lock+Record Lock 鎖定一個范圍并鎖定記錄本身
上面所說的鎖定的對象均為: 索引記錄. 如果InnoDB存儲引擎在建立的時候沒有設置任何一個索引,那么這時,InnoDB存儲引擎會使用隱式的主鍵進行鎖定.
當查詢的索引含有唯一屬性時,InnoDB存儲引擎會對Next-Key Lock進行優化,降級為Record Lock.
下面的2句話是InnoDB在不同隔離級別下產生"不可重復讀" 和 "幻讀" 和解決它 的根本原因:
- InnoDB存儲引擎默認的事務隔離級別(repeatable read)下,采用的是 Next-Key Locking的方式來加鎖.
- read committed隔離級別下采用的是: Record Lock 的方式來加鎖.
事務隔離性由以上鎖機制實現
原子性和持久性的實現
redo log 稱為重做日志(也叫事務日志),用來保證事務的原子性和持久性.
redo恢復提交事務修改的頁操作,redo是物理日志,頁的物理修改操作.
事務的提交過程如下圖:
當提交一個事務時,實際上它干了如下2件事:
一: InnoDB存儲引擎把事務寫入日志緩沖(log buffer),日志緩沖把事務刷新到事務日志.
二: InnoDB存儲引擎把事務寫入緩沖池(Buffer pool).
事務日志也是寫磁盤日志,為什么不需要雙寫技術?
因為事務日志塊的大小和磁盤扇區的大小一樣,都是512字節,因此事務日志的寫入可以保證原子性,不需要doublewrite技術
一致性的實現
undo log 用來保證事務的一致性. undo 回滾行記錄到某個特定版本,undo 是邏輯日志,根據每行記錄進行記錄.
undo 存放在數據庫內部的undo段,undo段位于共享表空間內.
undo 只把數據庫邏輯的恢復到原來的樣子.
undo日志除了回滾作用之外, undo 實現MVCC,讀取一行記錄時,發現事務鎖定,通過undo恢復到之前的版本,實現非鎖定讀取.
binlog redolog undolog 區別
binlog(二進制日志)就是典型的邏輯日志,而事務日志(redo log)則記錄的物理日志
binlog,是mysql服務層產生的日志,常用來進行數據恢復、數據庫復制,常見的mysql主從架構,就是采用slave同步master的binlog實現的, 另外通過解析binlog能夠實現mysql到其他數據源(如ElasticSearch)的數據復制。
redo log就是保存執行的SQL語句到一個指定的Log文件,當Mysql執行recovery時重新執行redo log記錄的SQL操作即可。當客戶端執行每條SQL(更新語句)時,redo log會被首先寫入log buffer;當客戶端執行COMMIT命令時,log buffer中的內容會被視情況刷新到磁盤。redo log在磁盤上作為一個獨立的文件存在,即Innodb的log文件。當數據庫或主機失效重啟時,會根據redo log進行數據的恢復,如果redo log中有事務提交,則進行事務提交修改數據。這樣實現了事務的原子性、一致性和持久性。
Undo Log: 除了記錄redo log外,當進行數據修改時還會記錄undo log,undo log用于數據的撤回操作,它記錄了修改的反向操作,比如,插入對應刪除,修改對應修改為原來的數據,通過undo log可以實現事務回滾,并且可以根據undo log回溯到某個特定的版本的數據,實現MVCC。
- redo log 是在存儲引擎層產生的,binlog是在數據庫上層的一種邏輯日志,任何存儲引擎均會產生binlog.
- binlog記錄的是sql語句, 重做日志則記錄的是對每個頁的修改.
- 寫入的時間點不一樣. binlog 是在事務提交后進行一次寫入,redo log在事務的進行中不斷的被寫入.
- redo log 是等冪操作(執行多次等于執行一次,redo log 記錄<T0,A,950>記錄新值,執行多少次都一樣) , binlog 不一樣;
redo log 是可能是多條記錄, 如:
<T0,start>
<Action1> ..... <ActionN>
<t0,commit>
既有start,又有commit 才是一條完整的redo log。才會被執行,缺失commit在恢復時是不會被執行的.
如遇到并發寫入,則redo log 還有可能是如下的情況:
T1,T2,T1,*T2,T3,T1,*T3,*T1
帶*的是事務提交的時間. (從左到右的時間順序)
redo log ,每個事務對應多個日志條目. 重做日志是并發寫入的. 無順序.
binlog,則如下:
T1,T4,T3,T2,T8,T6,T7,T5
1, redo log(事務日志)保證事務的原子性和持久性(物理日志)
2, undo log保證事務的一致性,InnoDB的MVCC也是用undo log來實現的(邏輯日志).
3, redo log中帶有有checkPoint,用來高效的恢復數據.
4, 物理日志記錄的是修改頁的的詳情,邏輯日志記錄的是操作語句. 物理日志恢復的速度快于邏輯日志.
MVCC實現
- innodb中通過B+樹作為索引的數據結構,并且主鍵所在的索引為ClusterIndex(聚簇索引), ClusterIndex中的葉子節點中保存了對應的數據內容。一個表只能有一個主鍵,所以只能有一個聚簇索引,如果表沒有定義主鍵,則選擇第一個非NULL唯一索引作為聚簇索引,如果還沒有則生成一個隱藏id列作為聚簇索引。
- 除了Cluster Index外的索引是Secondary Index(輔助索引)。輔助索引中的葉子節點保存的是聚簇索引的葉子節點的值。
- InnoDB行記錄中除了剛才提到的rowid外,還有trx_id和db_roll_ptr, trx_id表示最近修改的事務的id,db_roll_ptr指向undo segment中的undo log。
- 新增一個事務時事務id會增加,trx_id能夠表示事務開始的先后順序。
Undo log分為Insert和Update兩種,delete可以看做是一種特殊的update,即在記錄上修改刪除標記。
update undo log記錄了數據之前的數據信息,通過這些信息可以還原到之前版本的狀態。
當進行插入操作時,生成的Insert undo log在事務提交后即可刪除,因為其他事務不需要這個undo log。
進行刪除修改操作時,會生成對應的undo log,并將當前數據記錄中的db_roll_ptr指向新的undo log
行的更新過程
- 初始數據行
F1~F6是某行列的名字,1~6是其對應的數據。后面三個隱含字段分別對應該行的事務號和回滾指針,假如這條數據是剛INSERT的,可以認為ID為行號1,其他兩個字段為空。
- 事務1更改該行的各字段的值
當事務1更改該行的值時,會進行如下操作:
用排他鎖鎖定該行
記錄redo log
把該行修改前的值Copy到undo log,即上圖中下面的行
修改當前行的值,填寫事務編號,使回滾指針指向undo log中的修改前的行
- 事務2修改該行的值
與事務1相同,此時undo log,中有有兩行記錄,并且通過回滾指針連在一起。
如果undo log一直不刪除,則會通過當前記錄的回滾指針回溯到該行創建時的初始內容,所幸的時在Innodb中存在purge線程,它會查詢那些比現在最老的活動事務還早的undo log,并刪除它們,從而保證undo log文件不至于無限增長。
- 事務提交
當事務正常提交時Innbod只需要更改事務狀態為COMMIT即可,不需做其他額外的工作,而Rollback則稍微復雜點,需要根據當前回滾指針從undo log中找出事務修改前的版本,并恢復。如果事務影響的行非常多,回滾則可能會變的效率不高,根據經驗值沒事務行數在1000~10000之間,Innodb效率還是非常高的。很顯然,Innodb是一個COMMIT效率比Rollback高的存儲引擎。據說,Postgress的實現恰好與此相反。
MVCC過程:
- 每行數據都存在一個版本(事務版本號),每次數據更新時都更新該版本
- 修改時Copy出當前版本隨意修改,各事務之間無干擾
- 保存時比較版本號,如果成功(commit),則覆蓋原記錄;失敗則放棄copy(rollback)(根據undo log ID)
就是每行都有版本號,保存時根據版本號決定是否成功,聽起來含有樂觀鎖的味道。。。,而Innodb的實現方式是:
- 事務以排他鎖的形式修改原始數據
- 把修改前的數據存放于undo log,通過回滾指針與主數據關聯
修改成功(commit)啥都不做,失敗則恢復undo log中的數據(rollback)
二者最本質的區別是,當修改數據時是否要排他鎖定,如果鎖定了還算不算是MVCC?
Innodb的實現真算不上MVCC,因為并沒有實現核心的多版本共存,undo log中的內容只是串行化的結果,記錄了多個事務的過程,不屬于多版本共存。但理想的MVCC是難以實現的,當事務僅修改一行記錄使用理想的MVCC模式是沒有問題的,可以通過比較版本號進行回滾;但當事務影響到多行數據時,理想的MVCC據無能為力了。
比如,如果Transaciton1執行理想的MVCC,修改Row1成功,而修改Row2失敗,此時需要回滾Row1,但因為Row1沒有被鎖定,其數據可能又被Transaction2所修改,如果此時回滾Row1的內容,則會破壞Transaction2的修改結果,導致Transaction2違反ACID。
理想MVCC難以實現的根本原因在于企圖通過樂觀鎖代替二段提交。修改兩行數據,但為了保證其一致性,與修改兩個分布式系統中的數據并無區別,而二提交是目前這種場景保證一致性的唯一手段。二段提交的本質是鎖定,樂觀鎖的本質是消除鎖定,二者矛盾,故理想的MVCC難以真正在實際中被應用,Innodb只是借了MVCC這個名字,提供了讀的非阻塞而已。
Ref:
http://blog.csdn.net/tangkund3218/article/details/47904021