1. 程式人生 > >MySQL深入研究--學習總結(5)

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

# 前言 接上文,繼續學習後續章節。細心的同學已經發現,我整理的並不一定是作者講的內容,更多是結合自己的理解,加以闡述,所以建議結合原文一起理解。 # 第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。這樣做的風險是,主機掉電的時候會丟