Mysql-innoDB存儲引擎(事物,鎖,MVCC)

 innoDB的特性:

  從圖中由上至下紅色框中的信息是:基於主鍵的彙集索引 ,數據緩存,外鍵支持(邏輯上創建外鍵),行級別鎖,MVCC多版本控制,事務支持。這些也是InnoDB最重要的特性。html

事務:

  數據庫操做的最小工做單元,是做爲單個邏輯工做單元執行的一系列操做;事務是一組不可再分割的操做集合(工做邏輯單元)。典型事務場景(轉帳):這是兩個事務mysql

  update user_account set balance = balance - 1000 where userID = 3;算法

  update user_account set balance = balance +1000 where userID = 1;sql

mysql中如何開啓事務:數據庫

  經過navicat使用命令 showvariables like ‘autocommit’; 查看自動提交是否開啓。當開啓後執行update語句會自動提交,當自動提交是關閉的,能夠經過如下方式來建立事務提交:express

BEGIN;-- 這兩個二選一開啓事務
START TRANSACTION;
-- 這是一個事務
UPDATE ......
UPDATE ......
COMMIT;-- 提交或者回滾 ROLLBACK;

  begin / start transaction -- 手工開啓事務。編程

  commit / rollback -- 事務提交或回滾。緩存

  set session autocommit = on/off; -- 從Session的角度設定事務是否自動開啓。安全

JDBC 編程:session

  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,那麼執行2查詢出來就是原始數據,這個時候事務並無提交或者回滾,而後執行3開啓一個事務拿到的事務ID 爲3 ,此刻執行4(在更新操做的時候,採用的是先標記舊的那行記錄爲已刪除,而且刪除版本號是事務版本號,而後插入一行新的記錄的方式。)進行 update 操做的時候會 copy 數據到Undo 緩衝區,而後將Undo.log的原始數據的刪除版本號置爲3,把新數據的事務版本號置爲3,再執行2的時候因爲此刻事務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操做。
相關文章
相關標籤/搜索