1. 程式人生 > 資料庫 >MySQL進階(四)事務與鎖

MySQL進階(四)事務與鎖

MySQL的事務與鎖

五、MySQL事務

5.1 事務簡介

(1)在 MySQL 中只有使用了 Innodb 資料庫引擎的資料庫或表才支援事務。

(2)事務處理可以用來維護資料庫的完整性,保證成批的 SQL 語句要麼全部執行,要麼全部不執行。

(3)事務用來管理 insert,update,delete 語句。

5.2 事務四大特徵

一般來說,事務是必須滿足4個條件(ACID):

5.2.1 原子性(Atomicity)

一個事務(transaction)中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。

5.2.2 一致性(Consistency)

在事務開始之前和事務結束以後,資料庫的完整性沒有被破壞。這表示寫入的資料必須完全符合所有的預設規則,這包含資料的精確度、串聯性以及後續資料庫可以自發性地完成預定的工作。(比如:A向B轉賬,不可能A扣了錢,B卻沒有收到)

5.2.3 隔離性(Isolation)

資料庫允許多個併發事務同時對其資料進行讀寫和修改的能力,隔離性可以防止多個事務併發執行時由於交叉執行而導致資料的不一致。事務隔離分為不同級別,包括讀未提交(Read uncommitted)、讀提交(read committed)、可重複讀(repeatable read)和序列化(Serializable)。(比u人:A正在從一張銀行卡里面取錢,在A取錢的過程中,B不能向這張銀行卡打錢)

5.2.4 永續性(Durability)

事務處理結束後,對資料的修改就是永久的,即便系統故障也不會丟失。

5.3 事務提交、回滾

-- UNSIGNED代表無符號數,不能是負數
create table user(
id int primary key auto_increment,
name VARCHAR(20),
balance DECIMAL(10,2) UNSIGNED
);
insert into user VALUES (1,'張三',200);
insert into user VALUES (2,'李四',50000);
-- 轉賬業務,必須都成功,或者都失敗,所以不能一句一句執行,萬一執行了一半,斷電了咋辦
-- 所以要程式設計一個整體
-- 都成功
-- begin;
start transaction;
UPDATE user set balance = balance - 200 where id = 1;
UPDATE user set balance = balance + 200 where id = 2;
commit;
-- 都失敗
start transaction;
UPDATE user set balance = balance - 200 where id = 1;
UPDATE user set balance = balance + 200 where id = 2;
rollback;

5.3.1 實現的原理簡單介紹

mysql每執行一條語句記錄一條日誌,

1、start transaction,先記個日誌,真正執行執行。

2、UPDATE user set balance = balance - 200 where id = 1,先記個日誌,真正執行。

​ 2.1如果此時斷電了,當然不能繼續執行了,過了一會來電了,啟動mysql會檢查日誌,發現有個事
務沒有執行完畢,沒有commit,就會安裝反向的操作把他回滾了。

3、UPDATE user set balance = balance + 200 where id = 2,先記個日誌,真正執行。

4、如commit,記個記錄,執行,結束了,日誌就能刪除了。如果rollback,就會按照日誌反向操作,
回滾。

5.4事務特性–隔離性

隔離強調的是兩個或兩個以上同時發生(併發)的業務同時操作一個數據庫,為了讓兩個事務一方面能都看到、得到正確的結果,一方面還要保證一定的效率而產生的不同的隔離級別。

5.4.1 隔離性有隔離級別(4個)

(1)讀未提交:read uncommitted

(2)讀已提交:read committed

(3)可重複讀:repeatable read

(4)序列化:serializable

檢視個設定事務的隔離級別:

髒讀不可重複讀幻讀
Read uncommitted
Read committed×
Repeatable read××
Serializable×××

檢視設定事務的隔離級別:

SELECT @@global.tx_isolation, @@tx_isolation;
set session transaction isolation level repeatable read;
SET transaction isolation level read uncommitted;
SET transaction isolation level read committed;
set transaction isolation level repeatable read;
SET transaction isolation level serializable;
SET GLOBAL transaction isolation level read uncommitted;
SET GLOBAL transaction isolation level read committed;
set GLOBAL transaction isolation level repeatable read;
SET GLOBAL transaction isolation level serializable;
/*其中,SESSION 和 GLOBAL 關鍵字用來指定修改的事務隔離級別的範圍:
SESSION:表示修改的事務隔離級別將應用於當前 session(當前 cmd 視窗)內的所有事務;
GLOBAL:表示修改的事務隔離級別將應用於所有 session(全域性)中的所有事務,且當前已經存在的
session 不受影響;
如果省略 SESSION 和 GLOBAL,表示修改的事務隔離級別將應用於當前 session 內的下一個還未開始的
事務。*/

5.4.2 讀未提交

事物A和事物B,事物A未提交的資料,事物B可以讀取到
這裡讀取到的資料叫做“髒資料”,叫髒讀
這種隔離級別最低,這種級別一般是在理論上存在,資料庫隔離級別一般都高於該級別
簡而言之第一個事務沒提交,別的事物就能讀,這種資料不一定是正確的因為人家可能回滾呀!
案例:
張三發工資了,老婆讓張三把工資打到他老婆的賬號上,但是該事務並未提交,就讓老婆去檢視,老婆
一看真的打了錢了,高高興興關了網頁,此時張三急中生智進行回滾,錢瞬間回來,一次矇混了一個月
工資。所以張三老婆看到的資料我們稱之為“髒資料”。
必須開兩個事務

SET transaction isolation level read uncommitted;

1-張三轉賬

start transaction;
UPDATE user set balance = balance - 10000 where id = 1;
UPDATE user set balance = balance + 10000 where id = 2;

2-老婆查賬

start transaction;
select * from user where id = 2;
commit;

3-張三回滾

rollback;

4-老婆某天查賬

start transaction;
select * from user where id = 2;
commit;

出現上述情況,即我們所說的髒讀 ,兩個併發的事務,“事務A:領導給singo發工資”、“事務B:singo
查詢工資賬戶”,事務B讀取了事務A尚未提交的資料。

5.4.3 讀已提交

能讀到別的事物已經提交的資料。

A事務在本次事務中,對自己操作過的資料,進行了多次讀取發現數據不一致,不可重複讀。

簡單點說就是不能讓我好好的重複讀,一個事務裡讀出來的資料都不一樣,讓不讓人幹活了。

針對的語句update和delete,會導致不可重複讀

張三拿著工資卡去消費,系統讀取到卡里確實有10200元,而此時她的老婆也正好在網上轉賬,把張三工資卡的2000元轉到另一賬戶,並在張三之前提交了事務,當張三扣款時,系統檢查到張三的工資卡和上次讀取的不一樣了,張三十分納悶,明明卡里有錢,為何…

SET transaction isolation level read committed;

1-張三去消費了,顯示有餘額,賊高興

start transaction;
select * from user where id = 1;

2-老婆轉賬

start transaction;
UPDATE user set balance = balance + 10000 where id = 2;
UPDATE user set balance = balance - 10000 where id = 1;
commit;

3-張三查賬,同一個事務裡,發現錢少了。

select * from user where id = 1;

當隔離級別設定為Read committed 時,避免了髒讀,但是可能會造成不可重複讀。
大多數資料庫的預設級別就是Read committed,比如Sql Server , Oracle。如何解決不可重複讀這一問
題,請看下一個隔離級別。

5.4.4 可重複讀

A事務在本次事務中對未操作的資料進行多次查詢,發現第一次沒有,第二次出現了就像幻覺一樣。或者第一次有而第二次沒有。針對delete和insert。

案例

張三的老婆在銀行部門工作,她時常通過銀行內部系統檢視張三的賬戶資訊。有一天,她正在查詢到張三賬戶資訊時發現張三隻有一個賬戶,心想這傢伙應該沒有私房錢。此時張三在另外一家分行右開了一個賬戶,準備存私房錢。一次同時張三老婆點選了列印,結果打印出的張三賬戶居然多了一個,真是奇怪。

set transaction isolation level repeatable read;

1-張三開啟事務

start transaction;

2-老婆查賬戶

start transaction;
select * from user where name = '張三';

3-張三趁機開戶

insert into user values(3,'張三',10000);
commit;

4-老婆再查詢並列印,應該發現張三多了一個賬戶,但是沒有。

select * from user where name = '張三';

MySQL 通過多版本併發控制(MVCC)(快照讀/一致性讀)其實解決了幻讀問題。

原理:事務開啟後,將歷史資料存一份快照,其他事務增加與刪除的資料,對於當前事務來說是不可見的。

當然還能這樣測一下

set transaction isolation level repeatable read;

1-張三開啟事務

start transaction;

2-老婆查賬戶,給張三開了個賬戶

start transaction;
select * from user where name = '張三';
insert into user values(3,'張三',10000);

3-張三不知道老婆給他開了賬戶,自己也開一個,看見自己沒有這個3號賬戶,居然不能插入,很奇幻。

select * from user where name = '張三';
insert into user values(3,'張三',10000);

5.4.5 序列化

  • 事務A和事務B,事務A在操作資料庫時,事務B只能排隊等待
  • 這種隔離級別很少使用,吞吐量太低,使用者體驗差
  • 這種級別可以避免“幻像讀”,每一次讀取的都是資料庫中真實存在資料,事務A與事務B序列,而不併發。
  • 別的地方一用這個資料就不能修改刪除,直到別的地方提交
SET transaction isolation level serializable;

1-張三

begin;
select * from user;

2-老婆

begin;
select * from user;

3-張三操作發現卡住了

delete from user where id = 9;

4-老婆這邊一提交,那邊就能操作了

commit;

六、MySQL鎖的機制

資料庫鎖機制簡單來說,就是資料庫為了保證資料的一致性,使各種共享資源在被訪問時變得有序而設計的一種規則。

MysQL的鎖機制比較簡單最著的特點是不同的儲存引擎支援不同的鎖機制。 InoDB支援行鎖,(有時也會升級為表鎖)MyISAM只支援表鎖。

表鎖的特點就是開銷小、加鎖快,不會出現死鎖。鎖粒度大,發生鎖衝突的概率小,併發度相對低。

行鎖的特點就是開銷大、加鎖慢,會出現死鎖。鎖粒度小,發生鎖衝突的概率高,併發度搞。今天我們講鎖主要從InnoDB引擎來講,因為它既支援行鎖、也支援表鎖。

6.1 InnoDB行鎖的種類

InnoDB預設的事務隔離級別是RR,並且引數innodb_locks_unsafe_for_binling=0的模式下,行鎖有三種。

6.1.1 記錄鎖(Record Lock)

(1)不加索引,兩個事務修改同一行記錄

事務一:

begin;
update teacher set teacher_no = 'T2010005' where name = 'wangsi';

事務二:

begin;
update teacher set teacher_no = 'T2010006' where name = 'wangsi';

發現卡住了:

事務一提交了,事務二才獲取了。

(2)不加索引,兩個事務修改同一表非同行記錄

事務一:

begin;
update teacher set teacher_no = 'T2010005' where name = 'wangsi';

事務二:

begin;
update teacher set teacher_no = 'T2010006' where name = 'wangsi';

發現卡住了:

事務一提交了,事務二才獲取了。

說明鎖的是表!

(3)加索引,修改同一行記錄,不行

事務一:

begin;
update teacher set teacher_no = 'T2010005' where name = 'wangsi';

事務二:

begin;
update teacher set teacher_no = 'T2010006' where name = 'wangsi';

發現卡住了:

事務一提交了,事務二才獲取了。

(4)加索引,修改同表的不同行,可以修改

事務一:

begin;
update teacher set teacher_no = 'T2010008' where name = 'wangsi';

事務二:

begin;
update teacher set teacher_no = 'T2010009' where name = 'jiangsi';

發現都可一順利修改,說明鎖的的確是行。

證明行行鎖是加在索引上的,這是標準的行級鎖。

6.1.2 間隙鎖(GAP Lock)

在RR這個級別下,為了避免幻讀,引入了間隙鎖,他鎖定的是記錄範圍,不包含記錄本身,也就是不允許在範圍內插入資料。

間隙:根據檢索條件向下尋找最靠近檢索條件的記錄值A作為左區間,向上尋找最近索引條件的記錄值B作為右區間,即鎖定的間隙為(A,B)

檢視隔離級別:

show variables like '%iso%';

唯一索引 等值判斷只會產生記錄鎖,範圍查詢會產生間隙鎖

普通索引 等值判斷也會產生記錄鎖(普通欄位不唯一)

第一步把teacher表的id的4改成8

事務一:

begin;
select * from teacher where id < 6 lock in share mode;

事務二:

begin;
insert into teacher values (5,'zhangnan','T888888');

發現卡住了,因為他會把小於6的資料鎖定,並不允許間隙中間的值插入:

事務三:

begin;
insert into teacher values (9,'huijun','T66666666');

發現成功了,因為9不在鎖定的範圍。

6.1.3 記錄鎖和間隙鎖的組合(next-key lock)

臨鍵鎖,是記錄鎖和間隙鎖的組合,當InnoDb掃描索引時會先對索引記錄加上記錄鎖,在對索記錄兩邊加上間隙鎖。

6.2 表鎖

1、對於InnoDB表,在絕大部分情況下都應該使用行級鎖,因為事務和行鎖往往是我們之所以選擇InnoDB表的理由。但在個另特殊事務中,也可以考慮使用表級鎖。

第一種情況是:事務需要更新大部分或全部資料,表又比較大,如果使用預設的行鎖,不僅這個事務執行效率低,而且可能造成其他事務長時間鎖等待和鎖衝突,這種情況下可以考慮使用表鎖來提高該事務的執行速度。

第二種情況是:事務涉及多個表,比較複雜,很可能引起死鎖,造成大量事務回滾。這種情況也可以考慮一次性鎖定事務涉及的表,從而避免死鎖、減少資料庫因事務回滾帶來的開銷。

2、在InnoDB下 ,使用表鎖要注意以下兩點。

(1)使用LOCK TALBES雖然可以給InnoDB加表級鎖,但必須說明的是,表鎖不是由InnoDB儲存引擎層管理的,而是由其上一層MySQL Server負責的,僅當autocommit=0、innodb_table_lock=1(預設設定)時,InnoDB層才能知道MySQL加的表鎖,MySQL Server才能感知InnoDB加的行鎖,這種情況下,InnoDB才能自動識別涉及表級鎖的死鎖;否則,InnoDB將無法自動檢測並處理這種死鎖。

(2)在用LOCAK TABLES對InnoDB鎖時要注意,要將AUTOCOMMIT設為0,否則MySQL不會給表加鎖;事務結束前,不要用UNLOCAK TABLES釋放表鎖,因為UNLOCK TABLES會隱含地提交事務;COMMIT或ROLLBACK不能釋放用LOCAK TABLES加的表級鎖,必須用UNLOCK TABLES釋放表鎖,正確的方式見如下語句。

例如,可以按如下做:

lock tables teacher write,student read;
select * from teacher;
commit;
unlock tables;

表鎖的力度很大,慎用。

6.3 InnoDB的鎖型別

InnoDB的鎖型別主要有讀鎖(共享鎖)、寫鎖(排他鎖)、意向鎖和MDL鎖。

6.3.1 讀鎖

讀鎖(共享鎖,shared lock)簡稱S鎖。一個事務獲取了一個數據行的讀鎖,其他事務能獲得該行對應的讀鎖但不能獲得寫鎖,即一個事務在讀取一個數據行時,其他事務也可以讀,但不能對該數行增刪改的操作。

簡而言之:就是可以多個事務讀,但只能一個事務寫。

讀鎖有兩種select方式的應用:

  1. 第一種是自動提交模式下的select查詢語句,不需加任何鎖,直接返回查詢結果,這就是一致性非鎖定讀。
  2. 第二種就是通過select… lock in share mode被讀取的行記錄或行記錄的範圍上加一個讀鎖,讓其他事務可以讀,但是要想申請加寫鎖,那就會被阻塞。

事務一:

begin;
select * from teacher where id = 1 lock in share mode;

事務二:

begin;
update teacher set name = 'lucy2' where id = 1;

卡住了,說明加了鎖了。

6.3.2 寫鎖

寫鎖,也叫排他鎖,或者叫獨佔所,簡稱x鎖。一個事務獲取了一個數據行的寫鎖,其他事務就不能再獲取該行的其他鎖與鎖優先順序最高。

寫鎖的應用就很簡單了,有以下兩種情況:

簡而言之:就是隻能有一個事務操作這個資料,別的事務都不行。

(1)一些DML語句的操作都會對行記錄加寫鎖。

事務一:

begin;
update teacher set name = 'lucy' where id = 1;

事務二:

begin;
update teacher set name = 'lucy2' where id = 1;

卡住了,說明加了鎖了。

你發現他還能讀,這是應為mysql實現了MVCC模型。

(2)比較特殊的就是select for update,它會對讀取的行記錄上加一個寫鎖,那麼其他任何事務戴不能對被鎖定的行上加任何鎖了,要不然會被阻塞。

事務一:

begin;
select * from teacher where id = 1 for update;

事務二:

begin;
update teacher set name = 'lucy2' where id = 1;

卡住了,說明加了鎖了。

你發現他還能讀,這是應為mysql實現了MVCC模型。

6.3.3 MDL鎖

MySQL 5.5引入了meta data lock,簡稱MDL鎖,用於保證表中元資料的資訊。在會話A中,表開啟了查詢事務後,會自動獲得一個MDL鎖,會話B就不可以執行任何DDL語句,不能執行為表中新增欄位的操作,會用MDL鎖來保證資料之間的一致性。

元資料就是描述資料的資料,也就是你的表結構。意識是在你開啟了事務之後獲得了意向鎖,其他事務就不能更改你的表結構。

6.3.4 意向鎖

在mysql的innodb引擎中,意向鎖是表級鎖,意向鎖有兩種

意向共享鎖(IS) 是指在給一個數據行加共享鎖前必須獲取該表的意向共享鎖

意向排它鎖(IX) 是指在給一個數據行加排他鎖前必須獲取該表的意向排他鎖

意向鎖和MDL鎖都是為了防止在事務進行中,執行DDL語句導致資料不一致。

6.4 從另一個角度區分鎖的分類

6.4.1 樂觀鎖

樂觀鎖大多是基於資料版本記錄機制實現,一般是給資料庫表增加一個"version"欄位。讀取資料時,將此版本號一同讀出,之後更新時,對此版本號加一。此時將提交資料的版本資料與資料庫表對應記錄的當前版本資訊進行比對,如果提交的資料版本號大於資料庫表當前版本號,則予以更新,否則認為是過期資料。

比如下單操作:

查詢出商品資訊。

select name, version from teacher where id = 1;

根據商品資訊生成訂單。

將商品數量減1。

update teacher set name = 'lucy',version = version + 1 where id = 1 and
version = 3

6.4.2 悲觀鎖

總有刁民想害朕

悲觀鎖依靠資料庫提供的鎖機制實現。MySQL中的共享鎖和排它鎖都是悲觀鎖。資料庫的增刪改操作預設都會加排他鎖,而查詢不會加任何鎖。此處不贅述。

6.5 鎖等待和死鎖

鎖等待是指一個事務過程中產生的鎖,其他事務需要等待上一個事務釋放它的鎖,才能佔用該資源。如果該事務一直不釋放,就需要持續等待下去,直到超過了鎖等待時間,會報一個等待超時的錯誤。

MysQL中通過innodb_lock_wait_timeout引數控制,單位是秒。

死鎖的條件

  1. 兩行記錄,至少兩個事務
  2. 事務A 操作 第n行資料,並加鎖 update teacher set name = 'a' where id = 1;
  3. 事務B 操作 第m行資料,並加鎖 update teacher set name = 'b' where id = 2;
  4. 事務A 操作 第m行資料 update teacher set name = 'c' where id = 2;
  5. 事務B 操作 第n行資料 update teacher set name = 'd' where id = 1;
  6. 形成死鎖 Deadlock found when trying to get lock; try restarting transaction

死鎖是指兩個或兩個以上的程序在執行過程中,因爭奪資源而造成的一種互相等待的現象,就是所謂的
鎖資源請求產生了迴路現象,即死迴圈。

InnoDB引擎可以自動檢測死鎖並回滾該事務好不容易執行了一個業務給我回滾了,所以死鎖儘量不要
出現。

6.6 如何避免死鎖

  1. 出現死鎖並不可怕,但我們要儘量避免死鎖
  2. 如果不同的程式會併發處理同一個表,或者涉及多行記錄,儘量約定使用相同順序訪問表,可以大
    大減少死鎖的發生。
  3. 業務中儘量採用小事務,避免使用大事務,要即使提交和回滾事務,可減少死鎖產生的概率。
  4. 同一個事務中儘量做到一次鎖定所需要的所有資源,減少死鎖發生的概率。
  5. 對於非常容易發生死鎖的業務,可以嘗試使用升級鎖的力度,該用表鎖減少死鎖的發生。

6.7 MVCC,多版本併發控制

此章節本文轉載至:https://blog.csdn.net/SnailMann 的部落格

MVCC ,全稱Multi-Version Concurrency Control ,即多版本併發控制。MVCC是一種併發控制的方法,一般在資料庫管理系統中,實現對資料庫的併發訪問,在程式語言中實現事務記憶體。

MVCC在MySQL InnoDB中的實現主要是為了提高資料庫併發效能,用更好的方式去處理讀-寫衝突,做到即使有讀寫衝突時,也能做到不加鎖,非阻塞併發讀

6.7.1 什麼是當前讀和快照讀?

在學習MVCC多版本併發控制之前,我們必須先了解一下,什麼是MySQL InnoDB下的當前讀快照讀?

  • 當前讀

    像select lock in share mode( 共享鎖), select for update ; update, insert ,delete( 排他鎖)這些操作都是一種當前讀,為什麼叫當前讀?就是它讀取的是記錄的最新版本,讀取時還要保證其他併發事務不能修改當前記錄,會對讀取的記錄進行加鎖

  • 快照度

    不加鎖的select操作就是快照讀,即不加鎖的非阻塞讀;快照讀的前提是隔離級別不是序列級別,序列級別下的快照讀會退化成當前讀;之所以出現快照讀的情況,是基於提高併發效能的考慮,快照讀的實現是基於多版本併發控制,即MVCC,可以認為MVCC是行鎖的一個變種,但它在很多情況下,避免了加鎖操作,降低了開銷;既然是基於多版本,即快照讀可能讀到的並不一定是資料的最新版本,而有可能是之前的歷史版本

說白了MVCC就是為了實現讀-寫衝突不加鎖,而這個讀指的就是快照讀, 而非當前讀,當前讀實際上是一種加鎖的操作,是悲觀鎖的實現

6.7.2 當前讀,快照讀和MVCC的關係

  • 準確的說,MVCC多版本併發控制指的是 “維持一個數據的多個版本,使得讀寫操作沒有衝突” 這麼一個概念。僅僅是一個理想概念
  • 而在MySQL中,實現這麼一個MVCC理想概念,我們就需要MySQL提供具體的功能去實現它,而快照讀就是MySQL為我們實現MVCC理想模型的其中一個具體非阻塞讀功能。而相對而言,當前讀就是悲觀鎖的具體功能實現
  • 要說的再細緻一些,快照讀本身也是一個抽象概念,再深入研究。MVCC模型在MySQL中的具體實現則是由 3個隱式欄位undo日誌Read View等去完成的,具體可以看下面的MVCC實現原理

6.7.3 MVCC能解決什麼問題

資料庫併發場景有三種,分別為:

  • 讀-讀:不存在任何問題,也不需要併發控制
  • 讀-寫:有執行緒安全問題,可能會造成事務隔離性問題,可能遇到髒讀,幻讀,不可重複讀
  • 寫-寫:有執行緒安全問題,可能會存在更新丟失問題,比如第一類更新丟失,第二類更新丟失

MVCC帶來的好處是?

多版本併發控制(MVCC)是一種用來解決讀-寫衝突的無鎖併發控制,也就是為事務分配單向增長的時間戳,為每個修改儲存一個版本,版本與事務時間戳關聯,讀操作只讀該事務開始前的資料庫的快照。

所以MVCC可以為資料庫解決以下問題

  • 在併發讀寫資料庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了資料庫併發讀寫的效能

  • 同時還可以解決髒讀,幻讀,不可重複讀等事務隔離問題,但不能解決更新丟失問題

小結一下咯

總之,MVCC就是因為大牛們,不滿意只讓資料庫採用悲觀鎖這樣效能不佳的形式去解決讀-寫衝突問題,而提出的解決方案,所以在資料庫中,因為有了MVCC,所以我們可以形成兩個組合:

  • MVCC + 悲觀鎖

    MVCC解決讀寫衝突,悲觀鎖解決寫寫衝突

  • MVCC + 樂觀鎖

    MVCC解決讀寫衝突,樂觀鎖解決寫寫衝突

這種組合的方式就可以最大程度的提高資料庫併發效能,並解決讀寫衝突,和寫寫衝突導致的問題

6.7.4 MVCC的實現原理

MVCC的目的就是多版本併發控制,在資料庫中的實現,就是為了解決讀寫衝突,它的實現原理主要是依賴記錄中的 3個隱式欄位undo日誌Read View 來實現的。所以我們先來看看這個三個point的概念

  • 隱式欄位

    每行記錄除了我們自定義的欄位外,還有資料庫隱式定義的DB_TRX_ID , DB_ROLL_PTR , DB_ROW_ID 等欄位

  • DB_TRX_ID

    6byte,最近修改(修改/插入)事務ID:記錄建立這條記錄/最後一次修改該記錄的事務ID

  • DB_ROLL_PTR

    7byte,回滾指標,指向這條記錄的上一個版本(儲存於rollback segment裡)

  • DB_ROW_ID

    6byte,隱含的自增ID(隱藏主鍵),如果資料表沒有主鍵,InnoDB會自動以DB_ROW_ID產生一個聚簇索引

  • 實際還有一個刪除flag隱藏欄位, 既記錄被更新或刪除並不代表真的刪除,而是刪除flag變了

在這裡插入圖片描述

如上圖, DB_ROW_ID 是資料庫預設為該行記錄生成的唯一隱式主鍵, DB_TRX_ID 是當前操作該記錄的事務ID,而DB_ROLL_PTR 是一個回滾指標,用於配合undo日誌,指向上一個舊版本

undo日誌

undo log主要分為兩種:

  • insert undo log

    代表事務在insert 新記錄時產生的undo log , 只在事務回滾時需要,並且在事務提交後可以被立即丟棄

  • update undo log

    事務在進行updatedelete 時產生的undo log ; 不僅在事務回滾時需要,在快照讀時也需要;所以不能隨便刪除,只有在快速讀或事務回滾不涉及該日誌時,對應的日誌才會被purge 執行緒統一清除

purge執行緒,想成是一個環衛工人

  • 從前面的分析可以看出,為了實現InnoDB的MVCC機制,更新或者刪除操作都只是設定一下老記錄的deleted_bit,並不真正將過時的記錄刪除。
  • 為了節省磁碟空間,InnoDB有專門的purge執行緒來清理deleted_bit為true的記錄。為了不影響MVCC的正常工作,purge執行緒自己也維護了一個read view(這個read view相當於系統中最老活躍事務的read view);如果某個記錄的deleted_bit為true,並且DB_TRX_ID相對於purge執行緒的read view可見,那麼這條記錄一定是可以被安全清除的。

對MVCC有幫助的實質是update undo log , undo log 實際上就是存在rollback segment 中舊記錄鏈,它的執行流程如下

1、 比如persion表有一條記錄,記錄如下, name 為Jerry, age 為24歲, 隱式主鍵是1, 事務ID回滾指標,我們假設為NULL

在這裡插入圖片描述

2、 現在來了一個事務1 對該記錄的name 做出了修改,改為Tom

  • 在事務1 修改該行(記錄)資料時,資料庫會先對該行加排他鎖
  • 然後把該行資料拷貝到undo log 中,作為舊記錄,既在undo log中有當前行的拷貝副本
  • 拷貝完畢後,修改該行name 為Tom,並且修改隱藏欄位的事務ID為當前事務1的ID, 我們預設從1 開始,之後遞增,回滾指標指向拷貝到undo log 的副本記錄,既表示我的上一個版本就是它
  • 事務提交後,釋放鎖

在這裡插入圖片描述

3、又來了個事務2 修改person表的同一個記錄,將age 修改為30歲

  • 事務2 修改該行資料時,資料庫也先為該行加鎖
  • 然後把該行資料拷貝到undo log 中,作為舊記錄,發現該行記錄已經有undo log 了,那麼最新的舊資料作為連結串列的表頭,插在該行記錄的undo log 最前面
  • 修改該行age 為30歲,並且修改隱藏欄位的事務ID為當前事務2 的ID, 那就是2 ,回滾指標指向剛剛拷貝到undo log 的副本記錄
  • 事務提交,釋放鎖

在這裡插入圖片描述

從上面,我們就可以看出,不同事務或者相同事務的對同一記錄的修改,會導致該記錄的undo log 成為一條記錄版本線性表,既連結串列, undo log 的鏈首就是最新的舊記錄,鏈尾就是最早的舊記錄(當然就像之前說的該undo log的節點可能是會purge執行緒清除掉,向圖中的第一條insert undo log,其實在事務提交之後可能就被刪除丟失了,不過這裡為了演示,所以還放在這裡)

6.7.5 Read View(讀檢視)

什麼是Read View,說白了Read View就是事務進行快照讀操作的時候生產的讀檢視(Read View),在該事務執行的快照讀的那一刻,會生成資料庫系統當前的一個快照,記錄並維護系統當前活躍事務的ID(當每個事務開啟時,都會被分配一個ID, 這個ID是遞增的,所以最新的事務,ID值越大)

所以我們知道 Read View 主要是用來做可見性判斷的, 即當我們某個事務執行快照讀的時候,對該記錄建立一個 Read View 讀檢視,把它比作條件用來判斷當前事務能夠看到哪個版本的資料,既可能是當前最新的資料,也有可能是該行記錄的undo log 裡面的某個版本的資料。

Read View遵循一個可見性演算法,主要是將 要被修改的資料 的最新記錄中的DB_TRX_ID (即當前事務ID)取出來,與系統當前其他活躍事務的ID去對比(由Read View維護),如果 DB_TRX_ID 跟ReadView的屬性做了某些比較,不符合可見性,那就通過 DB_ROLL_PTR 回滾指標去取出 Undo Log 中的DB_TRX_ID 再比較,即遍歷連結串列的 DB_TRX_ID (從鏈首到鏈尾,即從最近的一次修改查起),直到找到滿足特定條件的 DB_TRX_ID , 那麼這個DB_TRX_ID所在的舊記錄就是當前事務能看見的最新老版本

那麼這個判斷條件是什麼呢?

在展示之前,我先簡化一下Read View,我們可以把Read View簡單的理解成有三個全域性屬性

trx_list(名字我隨便取的)
一個數值列表,用來維護Read View生成時刻系統正活躍的事務ID

up_limit_id
記錄trx_list列表中事務ID最小的ID

low_limit_id
ReadView生成時刻系統尚未分配的下一個事務ID,也就是目前已出現過的事務ID的最大值+1

  • 首先比較DB_TRX_ID < up_limit_id , (當前事務id小於最小id),說明你的事務最早發生,如果小於,則當前事務能看到DB_TRX_ID 所在的記錄
  • 接下來判斷 DB_TRX_ID 大於等於 low_limit_id ,(說明這是事務最新) 如果大於等於則代表DB_TRX_ID 所在的記錄在Read View 生成後才出現的,那對當前事務肯定不可見,如果小於則進入下一個判斷
  • 判斷DB_TRX_ID 是否在活躍事務之中, trx_list.contains(DB_TRX_ID) ,如果在,則代表我Read View 生成時刻,你這個事務還在活躍,還沒有Commit,你修改的資料,我當前事務也是看不見的;如果不在,則說明,你這個事務在Read View 生成之前就已經Commit了,你修改的結果,我當前事務是能看見的

6.7.6整體流程

我們在瞭解了隱式欄位,undo log, 以及Read View的概念之後,就可以來看看MVCC實現的整體流程是怎麼樣了

整體的流程是怎麼樣的呢?我們可以模擬一下

  • 當事務2對某行資料執行了快照讀,資料庫為該行資料生成一個Read View 讀檢視,假設當前事務ID為2,此時還有事務1和事務3在活躍中,事務4在事務2快照讀前一刻提交更新了,所以Read View記錄了系統當前活躍事務1,3的ID,維護在一個列表上,假設我們稱為trx_list

    事務1事務2事務3事務4
    事務開始事務開始事務開始事務開始
    ………………修改且已提交
    進行中快照讀進行中
    ………………
  • Read View不僅僅會通過一個列表trx_list 來維護事務2 執行快照讀那刻系統正活躍的事務ID,還會有兩個屬性up_limit_id (記錄trx_list列表中事務ID最小的ID), low_limit_id (**記錄trx_list列表中事務ID最大的ID,也有人說快照讀那刻系統尚未分配的下一個事務ID也就是目前已出現過的事務ID的最大值+1,我更傾向於後者 ;所以在這裡例子中 up_limit_id 就是1,low_limit_id 就是4 + 1 = 5,trx_list集合的值是1,3,Read View如下圖

在這裡插入圖片描述

  • 我們的例子中,只有事務4修改過該行記錄,並在事務2執行快照讀前,就提交了事務,所以當前該行當前資料的undo log 如下圖所示;我們的事務2在快照讀該行記錄的時候,就會拿該行記錄的DB_TRX_ID 去跟up_limit_id , low_limit_id和活躍事務ID列表(trx_list) 進行比較,判斷當前事務2 能看到該記錄的版本是哪個。

在這裡插入圖片描述

  • 所以先拿該記錄DB_TRX_ID 欄位記錄的事務ID 4 去跟Read View 的的up_limit_id 比較,看4是否小於up_limit_id(1),所以不符合條件,繼續判斷 4 是否大於等於low_limit_id (5),也不符合條件,最後判斷4 是否處於trx_list 中的活躍事務, 最後發現事務ID為4 的事務不在當前活躍事務列表中, 符合可見性條件,所以事務4修改後提交的最新結果對事務2 快照讀時是可見的,所以事務2 能讀到的最新資料記錄是事務4 所提交的版本,而事務4提交的版本也是全域性角度上最新的版本

在這裡插入圖片描述

  • 也正是Read View生成時機的不同,從而造成RC,RR級別下快照讀的結果的不同

6.7.7 MVCC相關問題

RR是如何在RC級的基礎上解決不可重複讀的?

當前讀和快照讀在RR級別下的區別:

表1

事務A事務B
開啟事務開啟事務
快照讀(無影響)查詢金額為500快照讀查詢金額為500
更新金額為400
提交事務
select 快照讀金額為500
select lock in share mode 當前讀金額為400

在上表的順序下,事務B的在事務A提交修改後的快照讀是舊版本資料,而當前讀是實時新資料400

表2

事務A事務B
開啟事務開啟事務
快照讀(無影響)查詢金額為500
更新金額為400
提交事務
select 快照讀金額為400
select lock in share mode 當前讀金額為400

而在表2 這裡的順序中,事務B在事務A提交後的快照讀和當前讀都是實時的新資料400,這是為什麼呢?

  • 這裡與上表的唯一區別僅僅是表1 的事務B在事務A修改金額前快照讀過一次金額資料,而表2 的事務B在事務A修改金額前沒有進行過快照讀。

所以我們知道事務中快照讀的結果是非常依賴該事務首次出現快照讀的地方,即某個事務中首次出現快照讀的地方非常關鍵,它有決定該事務後續快照讀結果的能力

我們這裡測試的是更新,同時刪除和更新也是一樣的,如果事務B的快照讀是在事務A操作之後進行的,事務B的快照讀也是能讀取到最新的資料的

RC,RR級別下的InnoDB快照讀有什麼不同?

正是Read View 生成時機的不同,從而造成RC,RR級別下快照讀的結果的不同

  • 在RR級別下的某個事務的對某條記錄的第一次快照讀會建立一個快照及Read View, 將當前系統活躍的其他事務記錄起來,此後在呼叫快照讀的時候,還是使用的是同一個Read View,所以只要當前事務在其他事務提交更新之前使用過快照讀,那麼之後的快照讀使用的都是同一個Read View,所以對之後的修改不可見;
  • 即RR級別下,快照讀生成Read View時,Read View會記錄此時所有其他活動事務的快照,這些事務的修改對於當前事務都是不可見的。而早於Read View建立的事務所做的修改均是可見
  • 而在RC級別下的,事務中,每次快照讀都會新生成一個快照和Read View, 這就是我們在RC級別下的事務中可以看到別的事務提交的更新的原因

總之在RC隔離級別下,是每個快照讀都會生成並獲取最新的Read View;而在RR隔離級別下,則是同一個事務中的第一個快照讀才會建立Read View, 之後的快照讀獲取的都是同一個Read View。

6.8 Redo log

MySQL資料庫作為現在網際網路公司內最流行的關係型資料庫,相信大家都有工作中使用過。InnoDB是MySQL裡最為常用的一種儲存引擎,主要面向線上事務處理(OLTP)的應用。今天就讓我們來探究一下InnoDB是如何一步一步實現事務的,這次我們先講事務實現的redo log。

首先我們先明確一下InnoDB的修改資料的基本流程,當我們想要修改DB上某一行資料的時候,InnoDB是把資料從磁碟讀取到記憶體的緩衝池上進行修改。資料在記憶體中被修改,與磁碟中相比就存在了差異,我們稱這種有差異的資料為髒頁。InnoDB對髒頁的處理不是每次生成髒頁就將髒頁重新整理回磁碟,這樣會產生海量的IO操作,嚴重影響InnoDB的處理效能。對於此,InnoDB有一套完善的處理策略,與我們這次主題關係不大,表過不提。既然髒頁與磁碟中的資料存在差異,那麼如果在這期間DB出現故障就會造成資料的丟失。為了解決這個問題,redo log就應運而生了。

6.8.1 Redo log工作原理

在講Redo log工作原理之前,先來學習一下MySQL的一些基礎:

6.8.1.1 日誌型別

在這裡插入圖片描述

redo log在資料庫重啟恢復的時候被使用,因為其屬於物理日誌的特性,恢復速度遠快於邏輯日誌。而我們經常使用的binlog就屬於典型的邏輯日誌。

6.8.1.2 checkpoint

坦白來講checkpoint本身是比較複雜的,checkpoint所做的事就是把髒頁給重新整理回磁碟。所以,當DB重啟恢復時,只需要恢復checkpoint之後的資料。這樣就能大大縮短恢復時間。當然checkpoint還有其他的作用。

6.8.1.3 LSN(Log Sequence Number)

LSN實際上就是InnoDB使用的一個版本標記的計數,它是一個單調遞增的值。資料頁和redo log都有各自的LSN。我們可以根據資料頁中的LSN值和redo log中LSN的值判斷需要恢復的redo log的位置和大小。

6.8.1.4 工作原理

好的,現在我們來看看redo log的工作原理。說白了,redo log就是儲存了資料被修改後的值。當我們提交一個事務時,InnoDB會先去把要修改的資料寫入日誌,然後再去修改緩衝池裡面的真正資料頁。

我們著重看看redo log是怎麼一步步寫入磁碟的。redo log本身也由兩部分所構成即重做日誌緩衝(redolog buffer)和重做日誌檔案(redo log file)。這樣的設計同樣也是為了調和記憶體與磁碟的速度差異。InnoDB寫入磁碟的策略可以通過innodb_flush_log_at_trx_commit 這個引數來控制。

在這裡插入圖片描述

當該值為1時,當然是最安全的,但是資料庫效能會受一定影響。

為0時效能較好,但是會丟失掉master thread還沒重新整理進磁碟部分的資料。

這裡我想簡單介紹一下master thread,這是InnoDB一個在後臺執行的主執行緒,從名字就能看出這個執行緒相當的重要。它做的主要工作包括但不限於:重新整理日誌緩衝,合併插入緩衝,重新整理髒頁等。masterthread大致分為每秒執行一次的操作和每10秒執行一次的操作。master thread中重新整理資料,屬於checkpoint的一種。所以如果在master thread在重新整理日誌的間隙,DB出現故障那麼將丟失掉這部分資料。

當該值為2時,當DB發生故障能恢復資料。但如果作業系統也出現宕機,那麼就會丟失掉,檔案系統沒有及時寫入磁碟的資料。

這裡說明一下,innodb_flush_log_at_trx_commit 設為非0的值,並不是說不會在master thread中重新整理日誌了。master thread重新整理日誌是在不斷進行的,所以redo log寫入磁碟是在持續的寫入。

6.8.1.5 宕機恢復

DB宕機後重啟,InnoDB會首先去檢視資料頁中的LSN的數值。這個值代表資料頁被重新整理回磁碟的LSN的大小。然後再去檢視redo log的LSN的大小。如果資料頁中的LSN值大說明資料頁領先於redo log重新整理回磁碟,不需要進行恢復。反之需要從redo log中恢復資料。

6.8.2 redo log的結構

其實這一部分內容日常工作中很少涉及到,稍微瞭解一下就足夠了。

6.8.2.1 log block

Redo log的儲存都是以 塊(block) 為單位進行儲存的,每個塊的大小為512位元組。同磁碟扇區大小一致,可以保證塊的寫入是原子操作。

塊由三部分所構成,分別是 日誌塊頭(log block header),日誌塊尾(log block tailer),日誌本身。日誌頭佔用12位元組,日誌尾佔用8位元組。故每個塊實際儲存日誌的大小為492位元組。

6.8.2.2 log group

一個日誌檔案由多個塊所構成,多個日誌檔案形成一個重做日誌檔案組(redo log group)。不過,log group是一個邏輯上的概念,真實的磁碟上不會這樣儲存。