MySQL深入研究--學習總結(5)

有夢想的老王發表於2021-03-16

前言

接上文,繼續學習後續章節。細心的同學已經發現,我整理的並不一定是作者講的內容,更多是結合自己的理解,加以闡述,所以建議結合原文一起理解。

第20章《幻讀是什麼,幻讀有什麼問題?》

先看下幻讀的定義:

在一個事務中,兩次執行同一個查詢SQL,後一次執行結果比前一次執行結果數量變多了,稱之為幻讀。

在隔離級別中的定義,我們知道RR級別是無法避免幻讀的?但是在innoDB中是如果做到避免幻讀問題呢?其實innoDB在RR級別下解決幻讀問題也並不完美。

現有一張表t,有三個欄位 id主鍵,普通索引C,不帶索引d, 目前有資料id=1,d=5,c=5;id=4,d=5,c=5;

事務A 事務B 事務C
begin;
select * from t where d =5 for update;Q1
update t set d =5 where id =4;
select * from t where d =5 for update;Q2
insert into t values (5,5,5)
select * from t where d =5 for update;Q3
commit;

首先我們知道innoDB在RR級別下,select語句是快照讀,不存在幻讀問題,快照讀的實現方式我們之前闡述過,通過MCVV多版本併發控制解決的,本質就是通過undo log實現的,如果還需要更詳細的研究,可以參考《一文講透MVCC原理》

Q2讀到事務B的更新預警,屬於當前讀看到的,不屬於幻讀.幻讀指的是新插入的行。

首先我們看下,幻讀不解決可能會出現哪些問題?

1、首先如果只加行鎖,執行Q1時,我們希望的是所有d=5的資料都鎖定,但d=5的是id=1的資料,只會對id=1加上行鎖.當事務B執行時,是對id=4進行更新操作,所以可以操作.同樣的事務C也可以執行,那麼就違背了,Q1的語義.

2、資料一致性問題:如果事務A在Q1時候又執行了一個按d=5條件更新語句,那麼在所有事務都提交後,落入到binlog中的順序是,事務B更新一個語句把id=4的d改成了5,事務C插入了一個id=5,d=5的資料,事務A執行了一個把d=5的資料更新。此時如果按binlog的執行順序,則會把事務B事務C的邏輯全部改掉,造成了資料不一致的問題。

所以innoDB為了解決此類幻讀問題,就引入了間隙鎖(Gap lock);

顧名思義就是在行與行之間也加上鎖.

比如上面例子中,有id=1和id=4兩個資料,那麼我不僅在行上加鎖,我再(- ∞ ,1](1,4],(4, + supremum】上也加上鎖,那麼當你再插入資料的時候,就無縫可入了就會被阻塞住。

但需要注意的是,在行鎖中,讀寫鎖,寫寫鎖互相沖突,當一個事務加間隙鎖,另外一個事務也可以對同樣的範圍加間隙鎖。

注意:

當我們執行更新加間隙鎖時,如果where條件不是索引,那麼就會對所有行加行鎖和行之間數值範圍加間隙鎖。

如果執行的where條件是等值並且是非唯一索引列,比如id=5,那麼會對id=5加行鎖,以及(id=5上一個id,id=5]和(id=5,id=5的下一個id值]加間隙鎖。如果是唯一索引,那麼就只加行鎖。

第21章《為什麼只改一行程式碼,鎖這麼多》

這章節,老師總結的加鎖規則,非常受用:兩個原則,兩個優化,一個BUG

兩個原則:

1、加鎖的基本單位是以next-key lock。就是左開右閉。

2、查詢過程中訪問的到物件才會加鎖。

兩個優化:

1、在索引上等值查詢條件,如果是唯一索引,會優化成行鎖。

2、在索引上等值查詢條件,會向右遍歷找到第一個不符合條件的為止,並會優化成間隙鎖。

一個bug:

在唯一索引上的範圍查詢,會訪問到不滿足條件為止。

帶著這些規則,下面看八個案例,以表T為背景,來實踐下。

CREATE TABLE`t`(
`id`int(11)NOT NULL,
`c`int(11)DEFAULT NULL,
`d`int(11)DEFAULT NULL,
PRIMARY KEY(`id`),
KEY`c`(`c`)
) ENGINE=InnoDB;

insert intot values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);

案例一:等值查詢的情況

update t set d=d+1 where id =7;

根據主鍵id進行更新時,由於id=7不存在。根據規則1加鎖單位next-key lock :(5,10] 根據優化2: 退化成間隙鎖(5,10);

此時分別執行兩個SQL:

insert into t values(8,8,8) --由於間隙鎖,會block
update t set d=d+1 where id =10; --不在間隙鎖範圍內,執行成功

經過實驗,證實如此。

案例二:非唯一索引等值查詢

begin;
select id from t where c=5 lock in share mode;

根據優化2 可知會在索引C間隙鎖 (5,10);

但需要注意的是:根據規則2 只有訪問到的物件才會加鎖,由於使用了覆蓋索引,所以不會再主鍵id上再加鎖。

update t set d=d+1 where id =5; --執行不受阻
insert into t values(7,7,7); --blocked

案例三:主鍵索引範圍查詢

select * from t where id =10 for update;
select * from t where id>=10 and id <11;

首先從結果上看,這兩個SQL執行結果是一樣的,但是按照上面總結的加鎖規則,加鎖過程是不一樣的。

SQL1 唯一索引等值查詢,則加的是id=10的行鎖。

SQL2 首先會查詢到id=10這行,加鎖單位next-key lock,(5,10],根據優化1,退化成行鎖,只加id=10這一行,

再查詢id>10,向右找到第一個不滿足條件的行id=15,加next-key lock (10,15]

案例四:非唯一索引範圍鎖

select * from t where c>= 10 and c<11 for update;

首先找到c=10這一行,加next-key (5,10],再根據優化2,向右查詢到第一個不滿足條件的值,退還成間隙鎖(10,15]

案例五:唯一索引範圍鎖bug

select * from t where id>10 and id<=15 for update;

正常id是唯一索引,找到id=15這時 ,應該就停止掃描了,但是innoDB會依然掃描到第一個不滿足條件的id=20為止。

所以就會加(10,15],(15,20],導致id=20這樣也會鎖上,這是沒有必要的。這個bug已被官方證實,但還未修復。

案例六:未唯一索引存在多個相同值的情況

--先插入一條c=10的值,這樣表中存在2個c=10的行
insert into t values(30,10,30);

當我們在c=10加鎖時,加鎖的範圍是next-key lock (5,10] next-key lock (10,10]和間隙鎖 (10,15)

案例七:limit 語句加鎖

delete from t where c =10 limit2;

與案例7的加鎖範圍不同的時,不會再加間隙鎖(10,15);

因為當掃描到c=10,id=30這一行時,已經滿足limit2的條件,不會再往後掃描,也就不會再加間隙鎖(10,15),從而縮小了鎖的範圍。

所以在刪除資料的時候,我們儘量加limit ,即可以控制刪除數量,又可以縮小鎖的範圍。

案例八:一個說死鎖的例子

A執行第一句SQL時,按加鎖規則,會加next-key lock (5,10]和間隙鎖(10,15)。

B執行時,也會加next-key lock(5,10],會被阻塞.

A再次執行插入資料(8,8,8)時,也會被阻塞,從而導致死鎖。

這是為什麼了?B 不是沒有加鎖成功嗎?但是我們要注意的時,加next-key lock 本質就是加間隙鎖再加行鎖。由於間隙鎖加之間不是互斥的,所以B加(5,10)的間隙鎖時成功了,加c=10的行鎖失敗。

所以當A再次插入資料時,已被間隙鎖阻塞。

第23章《MySQL怎麼保證資料不丟失的?》

binlog寫入機制

事務執行時,先把日誌寫到binlog cache,事務提交後再把binlog cache 寫入binlog檔案。

binlog_cache_size控制了每個執行緒的binlog cache的記憶體的大小。如果超過了這個引數就臨時存到磁碟中。

但注意的是,binlog cache並不一定是直接落入磁碟的,是先寫入binlog files,再刷入磁碟。

這個可以通過引數sync_binlog控制:

設定0,表示每次只寫入binlog files,不刷磁碟

設定1,表示每次事務提交,也會刷磁碟

設定n,表示每次都寫入binlog files,但是等累計了n個事務才刷磁碟

常見設定為"100-1000",但同時也會有丟失的風險.

redo log寫入機制

事務執行時,先把日誌寫入 redo log buffer,事務提交時,寫入檔案系統page cache 或者刷入磁碟.也是通過引數來設定:innoDB_flush_log_at_trx_commit

設定0,表示每次事務提交時,依然保留在redo log buffer

設定1,表示每次事務提交直接刷入磁碟

設定2,表示每次事務提交都只寫到page cache.

那是不是0,2狀態是不是就永不刷到磁碟了,那不是會有丟失的風險嗎?

其實不是的,innoDB有一個後臺執行緒,每隔1秒,會把redo log buffer中的日誌,寫到page cache,然後再呼叫fsync刷到磁碟.

由於這個後臺執行緒的存在,所以也會把還未提交事務的redo log 也持久化到磁碟.

如果你的如果你的MySQLMySQL現在出現了效能瓶頸,而且瓶頸在現在出現了效能瓶頸,而且瓶頸在IO上,可以通過哪些方法來提升效能呢?上,可以通過哪些方法來提升效能呢?
針對這個問題,可以考慮以下三種方法:
1.設定binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count引數,減少binlog的寫盤次數。這個方法是基於“額外的故意等待”來實現的,因此可能會增加語句的響應時間,但沒有丟失資料的風險。
2.將sync_binlog設定為大於1的值(比較常見是100~1000)。這樣做的風險是,主機掉電時會丟binlog日誌。
3.將innodb_flush_log_at_trx_commit設定為2。這樣做的風險是,主機掉電的時候會丟資料。

相關文章