Mysql-innoDB存儲引擎(事物,鎖,MVCC)
innoDB的特性:
從圖中由上至下紅色框中的信息是:基於主鍵的聚集索引 ,數據緩存,外鍵支持(邏輯上建立外鍵),行級別鎖,MVCC多版本控制,事務支持。這些也是InnoDB最重要的特性。
事務:
數據庫操作的最小工作單元,是作為單個邏輯工作單元執行的一系列操作;事務是一組不可再分割的操作集合(工作邏輯單元)。典型事務場景(轉賬):這是兩個事務
update user_account set balance = balance - 1000 where userID = 3;
update user_account set balance = balance +1000 where userID = 1;
mysql中如何開啟事務:
通過navicat使用命令 showvariables like ‘autocommit’; 查看自動提交是否開啟。當開啟後執行update語句會自動提交,當自動提交是關閉的,可以通過以下方式來創建事務提交:
BEGIN;-- 這兩個二選一開啟事務 START TRANSACTION; -- 這是一個事務 UPDATE ...... UPDATE ......
COMMIT;-- 提交或者回滾 ROLLBACK;
begin / start transaction -- 手工開啟事務。
commit / rollback -- 事務提交或回滾。
set session autocommit = on/off; -- 從Session的角度設定事務是否自動開啟。
JDBC 編程:
connection.setAutoCommit(boolean);
Spring 事務AOP編程:
expression=execution(com.gpedu.dao.*.*(..))
事務ACID特性:
- 原子性(Atomicity):最小的工作單元,整個工作單元要麽一起提交成功,要麽全部失敗回滾
- 一致性(Consistency):事務中操作的數據及狀態改變是一致的,即寫入資料的結果必須完全符合預設的規則,不會因為出現系統意外等原因導致狀態的不一致
- 隔離性(Isolation):數據並發的時候,一個事務所操作的數據在提交之前,對其他事務的可見性設定(一般設定為不可見)
- 持久性(Durability):事務所做的修改就會永久保存,不會因為系統意外導致數據的丟失
事務並發帶來什麽問題:
先來看第一張圖:在下圖中,一張表中記錄只有一條,事務B修改該條記錄的 age字段,而此刻 事務A來查詢了,獲得的age是18,接著事務B 回滾了,這樣子就出現了臟讀問題。
再來看第二個圖:事務A先查詢了數據信息,此刻事務B進行了修改並提交,然後事務A又去查詢了一遍,這個時候就會出現不可重復讀的問題。
第三張圖:通過範圍查詢獲得一條數據,此刻事務B 插入了一條數據,事務A又去查詢獲得了兩條數據,此刻就發生了幻讀。
綜上,事務並發給我們帶來了三個主要問題:臟讀,不可重復讀,幻讀。
事務的隔離級別:
- Read Uncommitted(未提交讀) --未解決並發問題,事務未提交對其他事務也是可見的,臟讀(dirty read)。
- Read Committed(提交讀) --解決臟讀問題,一個事務開始之後,只能看到自己提交的事務所做的修改,不可重復讀(nonrepeatableread)。
- Repeatable Read (可重復讀) --解決不可重復讀問題在同一個事務中多次讀取同樣的數據結果是一樣的,這種隔離級別未定義解決幻讀的問題。
- Serializable(串行化) --解決所有問題,最高的隔離級別,通過強制事務的串行執行。
設置read uncommitted級別:set session transaction isolation level read uncommitted;
innoDB對隔離級別的支持程度:
在InnoDB中隔離級別到底如何實現的呢? --通過鎖、MVCC。
InnoDB中的鎖:
鎖是用於管理不同事務對共享資源的並發訪問,InnoDB存儲引擎支持行鎖和表鎖(另類的行鎖,通過行鎖鎖住所有的行)。官方文檔:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html。表鎖與行鎖的區別:
- 鎖定粒度:表鎖 > 行鎖
- 加鎖效率:表鎖 > 行鎖
- 沖突概率:表鎖 > 行鎖
- 並發性能:表鎖 < 行鎖
MYSQL innoDB鎖類型:
- l 共享鎖(行鎖):Shared Locks
- l 排它鎖(行鎖):Exclusive Locks
- l 意向共享鎖(表鎖):Intention Shared Locks
- l 意向排它鎖(表鎖):Intention Exclusive Locks
- l 自增鎖:AUTO-INC Locks
行鎖的算法:
- l 記錄鎖 Record Locks
- l 間隙鎖 Gap Locks
- l 臨鍵鎖 Next-key Locks
共享鎖:
又稱為讀鎖,簡稱S鎖,顧名思義,共享鎖就是多個事務對於同一數據可以共享一把鎖,都能訪問到數據,但是只能讀不能修改,加鎖釋鎖方式:
-- 共享鎖加鎖 BEGIN select * from users WHERE id=1 LOCK IN SHARE MODE; rollback; commit; -- 在以上的SQL枷鎖後未執行提交或者回滾執行其他事務執行 select * from users where id =1; -- 可以執行,共享鎖特性 update users set age=19 where id =1;--會阻塞
排他鎖:
又稱為寫鎖,簡稱X鎖,排他鎖不能與其他鎖並存,如一個事務獲取了一個數據行的排他鎖,其他事務就不能再獲取該行的鎖(共享鎖、排他鎖),只有該獲取了排他鎖的事務是可以對數據行進行讀取和修改,(其他事務要讀取數據可來自於快照),加鎖釋鎖方式:delete / update / insert 默認加上X鎖。
-- 自動獲取排它鎖
set session autocommit = OFF; -- 設置手動提交事務 update users set age = 23 where id =1; --執行該語句後未提交,在其他線程上,執行下列其他事務執行語句會處於阻塞commit; ROLLBACK; -- 手動獲取排它鎖 set session autocommit = ON; begin select * from users where id =1 for update; commit; -- 其他事務執行 select * from users where id =1 lock in share mode; select * from users where id =1 for update; select * from users where id =1;
innoDB--行鎖到底鎖了什麽?
首先先來看一下測試表的結構,其中用的是InnoDB引擎,有一個name的唯一索引,主鍵自增,有3條數據
DROP TABLE IF EXISTS `users`; CREATE TABLE `users` ( `id` int(11) NOT NULL AUTO_INCREMENT, `uname` varchar(32) NOT NULL, `userLevel` int(11) NOT NULL, `age` int(11) NOT NULL, `phoneNum` char(11) NOT NULL, `createTime` datetime NOT NULL, `lastUpdate` datetime NOT NULL, PRIMARY KEY (`id`), KEY `idx_name` (`uname`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=100006 DEFAULT CHARSET=utf8mb4; -- ---------------------------- -- Records of users -- ---------------------------- INSERT INTO `users` VALUES (‘1‘, ‘李二狗‘, ‘2‘, ‘18‘, ‘13666666666‘, ‘2018-12-01 15:39:46‘, ‘2018-12-01 15:39:50‘); INSERT INTO `users` VALUES (‘2‘, ‘張三豐‘, ‘1‘, ‘29‘, ‘13777777777‘, ‘2018-12-01 16:35:41‘, ‘2018-12-01 16:35:44‘); INSERT INTO `users` VALUES (‘3‘, ‘武大郎‘, ‘2‘, ‘44‘, ‘13888888888‘, ‘2018-12-01 16:36:01‘, ‘2018-12-01 16:36:03‘);
案例1:緊接著在一個事務中執行以下語句:可以發現我們把事務設置成手動提交,但是我並未提交或者回滾:
set session autocommit = OFF; update users set lastUpdate=NOW() where phoneNum = ‘13666666666‘;
然後在其他事務中執行如下語句:會發現,上述SQL執行修改會獲得默認的排它鎖,而此刻並未釋放,鎖的列是ID為1,然後我們下列要修改ID為2的數據也是出於阻塞,這是為什麽呢?
update users set lastUpdate=NOW() where id =2; update users set lastUpdate=NOW() where id =1;
案例2,執行以下語句,可以發現我們把事務設置成手動提交,但是我並未提交或者回滾:
set session autocommit = OFF; update users set lastUpdate=NOW() where id = 1;
然後在其他事務上執行:會發現下面2條SQL執行後 第一條會順利執行,而第二條會被阻塞。
update users set lastUpdate=NOW() where id =2; update users set lastUpdate=NOW() where id =1;
案例三:執行一下語句:
set session autocommit = OFF; update users set lastUpdate=NOW() where `name` = ‘李二狗‘;
然後在其他事務上執行:會發現前面兩條會執行成功,而後面兩條執行失敗
-- 其他查詢執行 update users set lastUpdate=NOW() where `name` = ‘李二狗‘; update users set lastUpdate=NOW() where id =1; update users set lastUpdate=NOW() where `name` = ‘張三豐‘; update users set lastUpdate=NOW() where id =2;
InnoDB的行鎖是通過給索引上的索引項加鎖來實現的。對於二級索引,會對一級索引也加鎖。只有通過索引條件進行數據檢索,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖(鎖住索引的所有記錄)表鎖:lock tables xx read/write;
意向共享鎖(IS):表示事務準備給數據行加入共享鎖,即一個數據行加共享鎖前必須先取得該表的IS鎖,意向共享鎖之間是可以相互兼容的。
意向排它鎖(IX):表示事務準備給數據行加入排他鎖,即一個數據行加排他鎖前必須先取得該表的IX鎖,意向排它鎖之間是可以相互兼容的。
意向鎖(IS、IX)是InnoDB數據操作之前自動加的,不需要用戶幹預。
意義:相當於一個標記flgs,當事務想去進行鎖表時,可以先判斷意向鎖是否存在,存在時則可快速返回該表不能啟用表鎖。
自增鎖 AUTO-INC Locks:
針對自增列自增長的一個特殊的表級別鎖,查看自增鎖默認值:show variables like ‘innodb_autoinc_lock_mode‘;默認取值1,代表連續,事務未提交ID永久丟失。當級別為1,執行一下SQL:在插入數據的時候,這個表的ID為自增,連續回滾3次,這3次的ID會永久消失,在下次執行commit的時候ID會在原來的數值上加3.
begin; insert into users(name , age ,phoneNum ,lastUpdate ) values (‘tom2‘,30,‘1344444444‘,now()); ROLLBACK;
針對行鎖的算法:
臨鍵鎖 Next-key Locks:
Next-key locks:InnoDB行鎖的默認算法。鎖住記錄+區間(左開右閉),當sql執行按照索引進行數據的檢索時,查詢條件為範圍查找(between and、<、>等)並有數據命中則此時SQL語句加上的鎖為Next-key locks,鎖住索引的記錄+區間(左開右閉)。先來搞一張表:
DROP TABLE IF EXISTS `test`; CREATE TABLE `test` ( `id` int(11) NOT NULL , `name` varchar(32) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- ---------------------------- -- Records of users -- ---------------------------- INSERT INTO `test` VALUES (‘1‘, ‘1‘); INSERT INTO `test` VALUES (‘4‘, ‘4‘); INSERT INTO `test` VALUES (‘7‘, ‘7‘); INSERT INTO `test` VALUES (‘10‘, ‘10‘);
在InnoDB的默認行級算法中會對數據行進劃分:可以看到是一個左開右閉的這個一個展現。
執行以下sql不提交:由於有數據命中則會鎖住(4,7](7,10] 兩個區間。未提交的情況下執行下列其他事務中前四條全部阻塞而最後一條會成功執行。
begin; select * from test where id>5 and id<9 for update; -- 其他事務 select * from test where id=4 for update; -- 阻塞 select * from test where id=7 for update; -- 阻塞 select * from test where id=10 for update; -- 阻塞 INSERT INTO `test` (`id`, `name`) VALUES (9, ‘9‘); -- 阻塞 INSERT INTO `test` (`id`, `name`) VALUES (11, ‘11‘);-- 成功
為什麽InnoDB要選擇(臨鍵鎖)Next-key locks作為InnoDB行鎖的默認算法?解決幻讀,因為B+Tree是有順序的,從左往右順序遞增,把臨鍵區間也鎖住,其他事務要往裏插入數據是插不進去的。
間隙鎖 Gap Locks:繼臨鍵鎖要是沒有命中數據的情況下:
Gap鎖只在 Repeatable Read (可重復讀) 的隔離級別的情況下才存在。
記錄鎖 Record Locks:繼臨鍵鎖之後,在條件為精準匹配的時候。
那麽鎖是怎麽解決上述產生 臟讀,不可重復讀,以及幻讀的情況呢?
解決臟讀:
解決不可重復讀:
解決幻讀:
死鎖:
- 多個並發事務(2個或者以上);
- 每個事務都持有鎖(或者是已經在等待鎖);
- 每個事務都需要再繼續持有鎖;
- 事務之間產生加鎖的循環等待,形成死鎖
避免死鎖:
- 類似的業務邏輯以固定的順序訪問表和行。
- 大事務拆小。大事務更傾向於死鎖,如果業務允許,將大事務拆小。
- 在同一個事務中,盡可能做到一次鎖定所需要的所有資源,減少死鎖概率。
- 降低隔離級別,如果業務允許,將隔離級別調低也是較好的選擇
- 為表添加合理的索引。可以看到如果不走索引將會為表的每一行記錄添加上鎖(或者說是表鎖)
Mysql 中MVCC版本控制:
MVCC是multiversion concurrency control的縮寫,並發訪問(讀或寫)數據庫時,對正在事務內處理的數據做多版本的管理。以達到用來避免寫操作的堵塞,從而引發讀操作的並發問題 。提供MySQL事物隔離級別下無鎖讀,例如一個事物在執行update等修改數據的sql,並未提交時其他事物進行數據讀取是不影響的,而且讀取內容為數據變更之前的數據。
MVCC多本版快照由innodb的rollback segment構照的,一個sql進行查找數據當查找到某一個數據需要到回滾段中查找數據時,就會根據當前頁上行數據的一個指針到回滾段中查找對應數據,在innodb的表主鍵中都會存在三個隱藏的字段:
- DB_TRX_ID:該字段存儲最後一個修改該行數據的事務ID,占用6byte的空間,MySQL的delete操作是標記刪除,所以對應行數據的該字段就為一個刪除標記。
- DB_ROLL_PTR:該字段就記錄執行roll segment的指針信息,當事務需要rollback時就通過該字段尋找記錄重新構照行數據,該字段占用7byte空間。
- DB_ROW_ID:記錄每個行ID,該ID值為單調遞增型整數,在innodb表指定了主鍵之後DB_ROW_ID存在於主鍵索引上,如果無主鍵該值就不會存在,占用6byte空間。
在一個sql進行查詢時,讀取到一行數據的DB_TRX_ID值和自己事物ID的對比,假如隔離級別為MySQL的默認級別,就只讀取該ID值小於本身事物ID的數據,其余數據就需要通過DB_ROLL_PTR的信息到回滾段中讀取。MVCC是否起到相應的作用需取決於數據庫隔離級別的配置。
在insert和update、delete的操作是有區別的,一個insert語句插入數據再rollback就是直接對undo log的刪除,他並不會影響其他事物的讀取操作,而update、delete操作是在原有數據做更改,可能有其他事物在對該行數據做讀取操作,所以update、delete產生的undo log數據是由內部線程自動清理,在該數據無任何事務在使用時清理掉,所以在undo log中insert和update、delete產生的數據存於不同位置。
下面通過一個案例來熟悉一下MVCC的效果:
-- 數據準備
insert into teacher(name,age) value (‘seven‘,18) ;--假設事務版本為1 insert into teacher(name,age) value (‘qing‘,20) ;--假設事務版本為1 begin; ----------1 select * from users ; ----------2
begin; ----------3 update teacher set age =28 where id =1;----------4
再每一行數據 插入數據表的時候,都會開啟一個事務,每一行數據都會保存執行的時候所獲取的事務版本號,當進行修改的時候會先copy一份待修改的數據到 Undo 緩沖區,在提交後然寫入磁盤,在此過程中會講原先的數據行的刪除版本號置為當前事務ID,然後再在新的數據行把數據行版本號置為當前事務ID。
當我們按照 1,2,3,4,2 的順序去執行的時候,首先執行 1 拿到的事務ID 是2,那麽查詢出來就是原始數據,這個時候事務並沒有提交或者回滾,然後執行3開啟一個事務拿到的事務ID 為3 ,此刻進行 update 操作的時候會 copy 數據到Undo 緩沖區,然後將原始數據的刪除版本號置為3,把新數據的事務版本號置為3,再執行3的時候由於此刻事務ID 還是為2,所以根據查詢規則查找數據行版本號早於當前事務版本的數據行,查找刪除版本號大於當前事務版本的或者刪除版本為nul的數據行,由於修改操作未提交,所以最終得到的結果數據還是原始數據的值,並不會把修改的數據加載回來,解決了不可重復讀的問題。
如果按照這樣的邏輯通過 3,4,1,2的順序去執行,那麽首先修改的操作會拿到事務ID為2,將原來的數據行copy出來,將原來的刪除版本號置為當前事務ID,接著將備份數據的版本號置為當前版本號,然後執行查詢操作再開啟一個新事務,拿到的事務ID為3,根據查詢規則,拿到的是進行了update 操作但並未提交的新數據,造成了臟讀,這是為什麽呢?那麽是由誰去解決這個問題的呢?其實這裏面涉及到了 Undo.log的機制以及當前讀,快照讀的問題,那麽接下來看看他們是怎麽處理這個問題的 。
Undo Log:
Undo Log 是什麽:undo意為取消,以撤銷操作為目的,返回指定某個狀態的操作,undo log指事務開始之前,在操作任何數據之前,首先將需操作的數據備份到一個地方 (Undo Log),UndoLog是為了實現事務的原子性而出現的產物。
Undo Log實現事務原子性:事務處理過程中如果出現了錯誤或者用戶執行了 ROLLBACK語句,Mysql可以利用Undo Log中的備份將數據恢復到事務開始之前的狀態。
UndoLog在Mysql innodb存儲引擎中用來實現多版本並發控制。
Undo log實現多版本並發控制:事務未提交之前,Undo保存了未提交之前的版本數據,Undo 中的數據可作為數據舊版本快照供其他並發事務進行快照讀。
如下圖這樣的處理就避免了臟讀的問題。
當前讀,快照讀:
快照讀:SQL讀取的數據是快照版本,也就是歷史版本,普通的SELECT就是快照讀innodb快照讀,數據的讀取將由 cache(原本數據) + undo(事務修改過的數據) 兩部分組成
當前讀:SQL讀取的數據是最新版本。通過鎖機制來保證讀取的數據無法通過其他事務進行修改UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE都是當前讀。
Redo Log:
Redo Log 是什麽:Redo,顧名思義就是重做。以恢復操作為目的,重現操作;Redo log指事務中操作的任何數據,將最新的數據備份到一個地方 (Redo Log)。
Redo log的持久:不是隨著事務的提交才寫入的,而是在事務的執行過程中,便開始寫入redo 中。具體的落盤策略可以進行配置.RedoLog是為了實現事務的持久性而出現的產物。
Redo Log實現事務持久性:防止在發生故障的時間點,尚有臟頁未寫入磁盤,在重啟mysql服務的時候,根據redolog進行重做,從而達到事務的未入磁盤數據進行持久化這一特性。
流程圖如下:
指定Redo log 記錄在{datadir}/ib_logfile1&ib_logfile2 可通過innodb_log_group_home_dir 配置指定目錄存儲。一旦事務成功提交且數據持久化落盤之後,此時Redo log中的對應事務數據記錄就失去了意義,所以Redo log的寫入是日誌文件循環寫入的。
- 指定Redo log日誌文件組中的數量 innodb_log_files_in_group 默認為2
- 指定Redo log每一個日誌文件最大存儲量innodb_log_file_size 默認48M
- 指定Redo log在cache/buffer中的buffer池大小innodb_log_buffer_size 默認16M
Redo buffer 持久化Redo log的策略, Innodb_flush_log_at_trx_commit:
- 取值 0 每秒提交 Redo buffer --> Redo log OS cache -->flush cache to disk[可能丟失一秒內的事務數據]。
- 取值 1 默認值,每次事務提交執行Redo buffer --> Redo log OS cache -->flush cache to disk[最安全,性能最差的方式]。
- 取值 2 每次事務提交執行Redo buffer --> Redo log OS cache 再每一秒執行 ->flush cache todisk操作。
Mysql-innoDB存儲引擎(事物,鎖,MVCC)