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原理》](https://blog.csdn.net/DILIGENT203/article/details/100751755?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&tdsourcetag=s_pctim_aiomsg)。
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為背景,來實踐下。
```sql
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);
```
##案例一:等值查詢的情況
```sql
update t set d=d+1 where id =7;
```
根據主鍵id進行更新時,由於id=7不存在。根據規則1加鎖單位next-key lock :(5,10] 根據優化2: 退化成間隙鎖(5,10);
此時分別執行兩個SQL:
```sql
insert into t values(8,8,8) --由於間隙鎖,會block
update t set d=d+1 where id =10; --不在間隙鎖範圍內,執行成功
```
![](https://img2020.cnblogs.com/blog/874710/202103/874710-20210316094339475-2115568875.png)
![](https://img2020.cnblogs.com/blog/874710/202103/874710-20210316094332516-1987850318.png)
經過實驗,證實如此。
## 案例二:非唯一索引等值查詢
```sql
begin;
select id from t where c=5 lock in share mode;
```
根據優化2 可知會在索引C間隙鎖 (5,10);
但需要注意的是:根據規則2 只有訪問到的物件才會加鎖,由於使用了覆蓋索引,所以不會再主鍵id上再加鎖。
```sql
update t set d=d+1 where id =5; --執行不受阻
insert into t values(7,7,7); --blocked
```
![](https://img2020.cnblogs.com/blog/874710/202103/874710-20210316094323826-453668265.png)
##案例三:主鍵索引範圍查詢
```sql
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]
## 案例四:非唯一索引範圍鎖
```sql
select * from t where c>= 10 and c<11 for update;
```
首先找到c=10這一行,加next-key (5,10],再根據優化2,向右查詢到第一個不滿足條件的值,退還成間隙鎖(10,15]
##案例五:唯一索引範圍鎖bug
```sql
select * from t where id>10 and id<=15 for update;
```
正常id是唯一索引,找到id=15這時 ,應該就停止掃描了,但是innoDB會依然掃描到第一個不滿足條件的id=20為止。
所以就會加(10,15],(15,20],導致id=20這樣也會鎖上,這是沒有必要的。這個bug已被官方證實,但還未修復。
## 案例六:未唯一索引存在多個相同值的情況
```sql
--先插入一條c=10的值,這樣表中存在2個c=10的行
insert into t values(30,10,30);
```
![](https://img2020.cnblogs.com/blog/874710/202103/874710-20210316094314865-423349035.png)
當我們在c=10加鎖時,加鎖的範圍是next-key lock (5,10] next-key lock (10,10]和間隙鎖 (10,15)
## 案例七:limit 語句加鎖
```sql
delete from t where c =10 limit2;
```
與案例7的加鎖範圍不同的時,不會再加間隙鎖(10,15);
因為當掃描到c=10,id=30這一行時,已經滿足limit2的條件,不會再往後掃描,也就不會再加間隙鎖(10,15),從而縮小了鎖的範圍。
所以在刪除資料的時候,我們儘量加limit ,即可以控制刪除數量,又可以縮小鎖的範圍。
## 案例八:一個說死鎖的例子
![](https://img2020.cnblogs.com/blog/874710/202103/874710-20210316094306251-884131717.png)
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。這樣做的風險是,主機掉電的時候會丟