讀書筆記:Mysql實戰45講 (11-21講)

11、怎麼給字符串加索引

         比如email這個字段,如果email字段沒有索引,那麼這個語句只能做全表掃描

         mysql支持前綴索引,所以可以定義字符串一部分爲索引。

            alter table s add index index1(email);

            alter table s add index index2(email(6))

     存儲圖:

   

   前綴索引這個佔用的空間更小,但是會增加額外的記錄掃描次數

  如果使用index1:

     1.從index1索引樹找到滿足值‘[email protected]’,取得ID2的值

     2.到主鍵上查找主鍵值是ID2的行,判斷email值正確,將這行記錄加入結果集,然後取index索引上查找位置下一條記錄發現不滿足條件,循環結束

     這個過程,只需要回主鍵索引取一次數據,所以系統認爲只掃描了一行

  如果使用index2:

     1.從index2索引樹找到滿足索引值是'zhangs'記錄,然找到ID1,到主鍵查找主鍵值爲ID1的行,判斷email值是不是'zhangsss...com',如果不是丟棄,然後取下一條記錄,接着判斷,如果值對,記錄加入結果集,然後重複,直到取到值不是‘zhanggs’時,循環結束

      使用前綴索引,定義好長度,就可以做到既節省空間,又不用額外增加太多的查詢成本

      在建立索引時關注的是區分度,區分度越高越好。

前綴索引對覆蓋索引的影響

       如果使用email整個字符串的索引結構的話,可以利用覆蓋索引,從index1查到結果後直接返回了,不需要回到ID索引再查一次,而如果使用index2(即email(6)索引結構的話),就不得不回到ID索引再去判斷email字段的值

      使用前綴索引就用不上覆蓋索引對查詢性能的優化了,這也是選擇是否使用前綴索引考慮的一個因素

其他方式:

  比如身份證前6位是地址碼,所以可以創建12以上的前綴索引,但是索引選取越長,佔用的磁盤空間就大,搜索效率越低

即佔用更小的空間,也能達到相同的查詢效率:

第一種:倒序存儲

第二種:使用hash字段。在表上再創建一個整數字段,來保存身份證的校驗碼,同時再這個字段創建索引

    每次插入新紀錄的時候,同時用crc32()這個函數得到校驗碼填到這個新字段。

相同點:都不支持範圍查詢,只支持等值查詢

區別:從查詢效率上看,使用hash字段方式查詢性能更穩定,因爲crc32算出來值雖然有衝突概率但是非常小,而倒序存儲畢竟還是用的前綴索引方式,也就是說還是會增加掃描行數、

小結:

    1、直接創建完整索引,這樣比較佔用空間
    2、創建前綴索引,節省空間,但是會增加查詢掃描次數,並且不能使用覆蓋索引
    3、倒序存儲,再創建前綴索引,用於繞過字符串本身前綴區分度不夠問題
    4、創建hash字段索引,查詢性能穩定,有額外的存儲和計算消耗,跟第三種方式一樣,都不支持範圍掃描

12、爲什麼我的MySQL會抖一下

     在工作中,有種這樣的場景,一條SQL語句,正常執行的時候特別快,但是有時也不知道怎麼回事,它就會變得特別慢,而且這樣的場景很難浮現,它不只隨機,而且持續時間還很短。

     InnoDB在處理更新語句的時候,只做了一個寫日誌這一個磁盤操作,這個日誌叫做redo log(重做日誌),在更新內存寫完redo log後,就返回給客戶端,本次更新成功

     當內存數據也跟磁盤數據也內容不一致的時候,我們稱這個內存頁爲“髒頁”。內存數據寫入(flush)到磁盤後,內存和磁盤上的數據也的內容就一致了,稱爲“乾淨頁”

    平常執行很快的更新操作,其實就在寫內存和日誌,而MySQL偶爾抖一下這個瞬間,可能就是在刷髒頁(flush)

    第一種場景:InnoDB的redo log寫滿了。這個時候系統會停止所有更新操作,把chekpoint往前推進,redo log留出來空間繼續寫

 對應圖,把checkpoint位置從CP推到CP`,淺綠色部分對應的所有髒頁都flush到磁盤上。

第二種場景:系統內存不足。當需要新的內存頁,而內存不夠用的時候,就要淘汰一些數據也,空出內存給別的數據頁使用。如果淘汰的是“髒頁”,就要先flush到磁盤

  在這裏並沒直接把內存淘汰,下次需要請求的時候,從磁盤讀入數據頁,然後拿redo log出來應用,這裏其實從性能考慮的,如果刷髒頁一定會寫盤,就保證了每個數據頁有兩種狀態

     一種是內存裏存在,內存裏就肯定是正確結果,直接返回

     另一種內存裏沒有數據,就可以肯定數據文件上市正確結果,讀入內存後返回,這樣效率最高

第三種場景:屬於MySQL空閒的時候,這時系統沒什麼壓力,就會把髒頁進行flush到磁盤上

第四種場景:MySQL正常關閉的時候,這個時候會把內存的髒頁都flush到磁盤上。這樣下次MySQL啓動的時候,就直接從磁盤上讀數據,啓動速度會很快

主要分析前兩種:

    第一種:是“redo log”寫滿了,要flush髒頁。這種情況是innoDB儘量避免的,因爲這個時候,整個系統不能再接受更新了,所有的更新必須堵住,如果你從監控上看,這個時候更新數會跌爲0

    第二種:內存不夠用,要先將髒數據寫到磁盤。這個情況是常態,InnoDB用緩存池(buffer pool)管理內存,緩衝池中的內存頁有三種狀態:

        a>還沒有使用的       b>使用了並且是乾淨頁      c>使用了並且是髒頁

    當要讀入的數據頁沒有在內存的時候,就必須到緩衝池中申請一個數據頁。這個時候只能把最久不適用的數據頁從內存中淘汰掉,如果淘汰的是乾淨頁直接釋放;如果是髒頁就必須flush到磁盤,變成乾淨頁,然後釋放

    但是出現這樣的情況會明顯影響性能:1>一個查詢要淘汰的髒頁個數太多,會導致查詢時間明顯變長;2>日誌寫滿,更新全被堵住,寫性能跌爲0,這種情況對敏感業務是不能接受的。所以Innodb需要有控制髒頁比例的機制來儘量避免上面兩種情況

InnoDB刷髒頁的控制策略:

     首先需要innodb_io_capacity這個參數,告知InnoDB的磁盤能力,這樣InnoDB才知道主機的IO能力,才能知道需要全力刷髒頁的時候可以多快。一般這個值建議設置成磁盤的IOPS。磁盤的IOPS(磁盤性能指標)可以通過fio這個工具來測試。

    實例:因爲沒有正確設置這個參數導致的問題比比皆是。之前就有公司一個庫的性能問題,說MySQL的寫入速度很慢,TPS很低,但是就看主機的IO壓力並不大。經過一番排查發現罪魁禍首是這個參數設置出問題,他的主機磁盤用的SSD,但是innodb_io_capacity的值設置爲300。於是,InnoDB認爲這個系統的能力差,所以刷髒頁刷的特別慢,甚至比髒頁生成速度還蠻,這就造成髒頁累積,影響了查詢和更新性能

InnoDB怎麼控制引擎按照“全力“的百分比來刷髒頁:

 InnoDB刷盤速度參考兩個因素:1、髒頁比例 2、redo log寫盤速度

 參數innodb_max_dirty_pages_pct是髒頁比例上限,默認值是75%

 InnoDB在後臺刷髒頁,而過程是要將內存頁寫入磁盤。所以無論是你的查詢語句在需要內存的時候淘汰一個髒頁,還是由於刷髒頁的邏輯佔用IO並可能影響到更新於巨,都會造成業務端感知到mysql抖一下的原因,所以要合理設置innodb_io_capacity的值不要讓他接近75%

InnoDB刷新髒頁的策略:

  一旦一個查詢請求需要在執行過程flush掉一個髒頁時,這個查詢就要比平常慢。而在mysql中有這樣的一個機制,在準備刷一個髒頁的時候,如果這個數據頁旁邊的數據頁剛好也是髒頁,就順帶一起刷掉,然後繼續延續。

  其中innodb_flush_neighbors參數就是控制這個行爲的,值爲1的時候出現這種‘連坐’機制,值爲0的話自己刷自己的

  情景對比:這個優化在機器硬盤的時候很有意義,可以減少很多隨機IO。機械硬盤的隨機IOPS一般只有幾百,所以減少很多隨機IO意味着性能大幅度提升

   但是如果SSD這類IOPS比較高的設備,這個時候IOPS往往不是瓶頸,只刷自己的這樣的話可以更快執行完必要的刷髒數據操作,減少SQL語句響應時間

  在MYSQL 8.0  innodb_flush_neighbors默認值爲0

總結:

  在第二章的WAL的概念,這個機制後續需要刷髒頁操作和執行時機。利用WAL技術,數據庫將隨機寫轉換成了順序寫,大大提升了數據庫的性能,但是,由此也帶來了內存髒頁的問題,髒頁會被後臺線程自動flush,也會由數據頁淘汰而出發flush,而刷髒頁的過程由於會佔用資源,可能會讓你的更新和查詢語句響應時間過長

 

表刪掉一半,表文件大小不變?

  當刪除整個表的時候,可以使用drop table命令回收表空間。但是,當刪除數據的場景是刪除某些行,這時就遇到了表中數據被刪掉,但是表空間卻沒有被回收

 數據刪除流程

     InnoDB裏數據都是用B+樹的結構組織的

如果刪除R4這個記錄的話,InnoDB引擎只會把R4這個記錄標記刪除,如果之後要插入一個ID在300和600直接的記錄會複用這個位置。但是磁盤文件的大小並不會縮小

Innodb的數據是按頁存儲的,如果刪掉一個數據頁上的所有記錄怎麼樣呢?

 整個數據頁就可以被服用了,但是數據頁的複用跟記錄的複用是不同的,記錄的複用只限於符合範圍條件的數據,而當整個頁從B+樹裏面摘除以後,可以複用到任何位置。如果相鄰兩個數據頁利用率都很小,系統會把這個頁上的數據合在其中一個頁上,另一個數據頁會被標記爲可複用。

如果用delet 命令把整個表數據刪除呢? 結果就是所有數據頁都會標記爲可複用。但是磁盤上,文件不會變小

所以,delet命令其實只是把記錄的位置或者數據頁標記爲可複用,但磁盤文件大小不變,不能回收表空間,這些可以複用但是沒有被使用的空間,就會造成空洞,其實插入數據也會這樣

插入數據:

  如果數據是按照索引遞增順序插入的,那麼索引是緊湊的。但如果數據是隨機插入的,就可能造成索引的數據頁分裂

 

如圖由於page A滿了再插入一個ID爲550的值,就不得不申請一個新的頁面page B來保存數據了。頁分裂完成後,page A的末尾就留下了空洞(實際上可能不止一個空洞),更新索引上的值

    可以理解爲刪除一箇舊的值,再插入一個新的值,不難理解,這也是會造成空洞

重建表:去除這些空洞,能夠達到收縮表空間的目的

      語句: alter table A engine=InnoDB命令來重建表

原理:在MySQL5.6版本開始引入了online DDL

      首先建立臨時文件,掃描表A主鍵所有數據頁,然後根據表A記錄生成B+樹,存儲在臨時文件中,在生成臨時文件過程中,將所有對A的操作記錄在一個日誌文件(row log)中,臨時文件生成,將日誌文件操作應用到臨時文件,然後用臨時文件代替表A的數據文件

  在重建表過程允許對錶A做DDL操作,也就是這個Online DDL名字來源

 補充:在這裏DDL之前本身要拿MDL寫鎖,但是這個寫鎖在真正拷貝數據之前就退化成讀鎖。退化的原因:就是爲了實現Online,MDL讀鎖不會阻塞增刪改查,但是不直接解鎖的原因是禁止其他線程對這個表同事做DDL

  因爲重建方法都會掃描原表數據和構造臨時文件。對於很大的表來說,這個操作是很消耗IO和CPU資源的,因此,如果是線上服務,要很小心控制操作時間,在GitHub上有gh-ost來做

問題:如果一個表t文件大小爲1TB,對這個表執行重建表,發現執行完成後,空間不僅沒變小,還變成了1.01TB?

           有可能本身沒有空洞,在DDL期間剛好有外部的MDL執行,又引入一些空洞,還有一個重要的機制,在重建表,InnoDB不會把整張表佔滿,每個頁留1/16給後續更新用。也就是,其實重建表之後不是最緊湊的,所以如果有這麼一個過程:

            t表重建,然後插入一部分數據,但是插入這些數據用掉了預留空間,這種情況,重建一次t表,就出現這種現象

 

 

Count爲什麼這麼慢?

    select count(*) from t;

count(*)的實現:

   MyISAM引擎把這個表的總行數存在了磁盤,因此執行count(*)的時候會直接返回這個數,效率很高

  InnoDB引擎它執行count(*)的時候需要把數據一行行從引擎裏面讀出來,然後累計數。

爲什麼不一樣?

  因爲InnoDB支持事物,即使在同一個時刻的多個查詢,由於多版本併發控制(MVCC)的原因,InnoDB表應該返回多少行也是不確定的。比如

   

   因此對於count(*)這個請求來說,InnoDB只好把數據一行行讀出來依次判斷,可見行才能夠用於計算“基於這個查詢”的表總行數

 InnoDB的優化:

  InnoDB是索引組織表,主鍵索引樹的葉子節點是數據,而普通索引樹的葉子節點是主鍵值。所以,普通索引樹比主鍵索引樹小很多。對於count(*)這樣操作,遍歷哪個索引樹得到的結果邏輯都一樣。因此,MySQL優化器會找到最小的那棵樹來遍歷。在保證邏輯正確的前提,儘量減少掃描的數據量,是數據庫系統設計的通用法則之一

     

show table status 獲取表的信息,這裏面也有一個TABLE_ROWS用於顯示這個表當前有多少行,這個命令很快但是不能代替count(*)嗎?

  實際上,TABLE_ROWS這個採樣估算來的,因此它很不準。並且官方文檔說誤差可能達到40%到50%。

如果緩存系統保存計數:

  比如Redis保存表總行數,加一條數據Redis計數加1,刪除減一。

  存在問題:緩存系統可能會丟失更新

  首先,Redis不可能永久留在內存,如果持久化起來,如果剛剛在數據表插入一行,Redis保存值也+1,然後Redis異常重啓,重啓後剛纔的數據就丟失了,解決的辦法異常重啓後,到數據庫裏面單獨執行一次count(*)獲取真實行數,然後寫到Redis。

  但是還是存在邏輯上不精確,比如有這麼一個頁面,要顯示操作記錄的總數,同時還要顯示最近操作的100條記錄。

 1、如果先數據庫,查到100行記錄有最新插入記錄,而Redis沒加1

 2、如果先Redis,計數裏已經+1,查到100行結果裏沒有最新插入記錄

 在這個時序圖中,A插入一個記錄,T3會話B查詢,會顯示出新插入的這個記錄,但是Redis計數還沒加1,出現數據不一致

 因此在併發系統裏面,無法精確控制不同線程的執行時刻,因此存在這個操作時許,也就是Redis緩存和數據不同步的問題

  先寫數據庫,再寫緩存有什麼問題?先寫緩存再寫數據庫有什麼問題?寫庫成功緩存更新失敗怎麼辦?緩存更新成功寫庫失敗怎麼辦? 這個問題可以參考一下

在數據庫保存計數:

    首先,這解決了崩潰丟失的問題,InnoDB是支持崩潰恢復不丟數據的

    再者查看計數精確的問題:

這個時候,會話B讀操作仍然給T3執行的,但是因爲更新事務還沒提交,所以計數+1這個操作會話B還不可見,所以B看到的結果裏,計數數值和記錄結果邏輯上是一致的

小結:

   其實把計數放在Redis裏面,不能夠保證計數和MySQL表裏的數據精確一致的原因是,這兩個不同存儲系統,不支持分佈式事務,無法拿到精確一致的視圖。而把計數值也放在MySQL中,就解決了一致性視圖的問題。InnoDB引擎支持事務,我們利用好事務的原子性和隔離性,可以簡化業務開發時的邏輯。

問題:由於事務可以保證中間結果不被其他事務讀到,因此修改計數值和插入新紀錄的順序是不影響邏輯結果的。但是從併發系統性能的角度考慮,你覺得這個事務序列裏,先插入操作記錄,還是更新技術表呢

    因爲更新計數表涉及到行鎖的競爭,先插入再更新能最大程度減少事務之間的鎖等待,提升了併發度

 

 

16、order by 是怎麼工作的?

 

注:

using index:使用覆蓋索引   

using where :在查找使用索引的情況下,需要回表查詢所需數據  

using index condition:查找使用了索引,但是需要回表查詢數據   

using index & using where:查找使用了索引,但是需要的數據都在索引列中能找到,所以不需要回表查詢數據

   以上四點就能看出它們之前的區別,或許有部分人都存在疑惑 using index & using where 和using index condition那個比較好,從上面的的解釋中就能看出是前者比較好,畢竟不需要回表查詢數據,效率上應該比較快的

當在city上創建索引後,Using filesory表示需要排序,MySQL會給每個線程分配一塊內存用於排序,稱爲sort_buffer

全字段排序:

工作流程如下:

     這個動作在內存中完成還是外部排序,取決於排序所需的內存和參數sort_buffer_size(MySQL開闢的內存sort_buffer大小),如果要排序的數據量大於sort_buffer_size,內存放不下,利用磁盤臨時文件輔助排序,外部排序一般使用歸併排序算法MySQL將要排序的數據分成幾份,每一份獨立排序完再合併成一個有序的大文件,sort_buffer_size越小,分成的份越多

總結:在這個算法過程中,只對原表的數據讀了一遍,剩下的操作都是在sort_buffer和臨時文件執行,存在的問題,如果查詢字段很多,那麼sort_buffer放的字段太多,要分成很多個臨時文件,排序性能很差

rowid排序

  因爲如果查詢返回的字段很多的話,那麼在sort_buffer裏面放的字段數太多,這樣能夠同時放下的行數很少,要分成很多個臨時文件,set max_length_for_sort_data=16 是mysql中專門控制排序行數據長度一個參數,如果單行太大,就換一個算法

整個執行流程:

全字段 VS rowid排序

   體現了MySQL一個設計思想:如果內存多,就多利用內存,儘量減少磁盤訪問

還有一個解決辦法:alter table t add index city_user_age(city,name,age); 創建聯合索引。覆蓋索引是指,索引上的信息足夠滿足查詢請求,不再回到主鍵索引上面取數據

查詢過程變成了

explain變成了:

 

問題:表中有city_name(city,name)這個聯合索引,查杭州和蘇州兩個城市中所有市民的姓名,而且按名字排序,顯示前100條記錄。
  

   1>這個語句執行時候時候有排序過程嘛?爲什麼?

      雖然有(city,name)聯合索引,對於單個city內部,name是遞增的。但是由於這條SQL語句不是單獨查一個city的值,同時查了兩個,因此需要排序


   2>需要實現一個在數據段不需要排序的方案,怎麼實現?

     執行select * from where city="杭州" order by name limit 100;客戶端用100個內存數組A保存,   執行select * from where city="suzhou" order by name limit 100;客戶端用100個內存數組B保存,因爲AB都是有序的,然後用歸併思想就可以得到需要的結果


   3>如果分頁需求,要改成10000,100,你怎麼實現?
     同2>,order by name limie 10100;然後用歸併排序拿到另個結果集裏,按順序取10001~10100的name值

 

 

 

18、爲什麼這些SQL語句性能差異巨大

案例一:條件字段函數操作

    `t_modified` datetime DEFAULT NULL        key `t_modified`(`t_modified`)

      注:對於TIMESTAMP,它把客戶端插入的時間從當前時區轉化爲UTC(世界標準時間)進行存儲。查詢時,將其又轉化爲客戶端當前時區進行返回。而對於DATETIME,不做任何改變,基本上是原樣輸入和輸出。並且存儲範圍也不同:

      timestamp所能存儲的時間範圍爲:'1970-01-01 00:00:01.000000' 到 '2038-01-19 03:14:07.999999'。

      datetime所能存儲的時間範圍爲:'1000-01-01 00:00:00.000000' 到 '9999-12-31 23:59:59.999999'。

總結:TIMESTAMP和DATETIME除了存儲範圍和存儲方式不一樣,沒有太大區別。當然,對於跨時區的業務,TIMESTAMP更爲合適。

    mysql>  select count(*) from t where month(t_modified)=7;

   問題:在生產庫中執行這條語句,卻發現執行了很久才返回結果

   原因:對索引字段做函數操作,可能會破壞索引值的有序性,因此優化器決定放棄走樹索引功能。

   分析:

如果SQL是2018-7-1的話引擎會用B+樹提供的快速定位能力,但是計算month()函數,放棄了樹搜索,優化器可以選擇遍歷主鍵索引,也可以選擇遍歷索引t_modified,優化器對比索引大小後發現,索引t_modified更小,遍歷這個索引比遍歷主鍵索引來的更快。導致了全索引掃描

可以通過這樣修改,用上索引的快速定位能力

例如:select * from t where id +1 =10000這個SQL語句,雖然不會改變有序性,但是也是不能用ID索引快速定位,手動改成 id=10000 -1 纔可以

案例二:隱式類型轉換

tradeid字段類型是varchar(32),而參數是整形,所以需要做類型轉換

怎麼檢查數據類型轉換?

 select "10" >9 的結果

1、如果規則是 將字符串轉換成數字 ,那麼就是做數字比較,結果應該是1;

2、如果規則是 將數字轉換成字符串 ,那麼就是做字符串比較 ,結果是0

對於優化器語句執行相當於:

對索引字段做函數操作,優化器放棄走樹搜索功能

 

案例三、隱式字符串編碼轉換

當查詢id=2的交易操作信息,

並沒有使用索引,其原因是兩個表字符集不同,一個是utf8,一個是utf8mb4,所以做表連接查詢的時候用不上關聯字段的索引

注意:字符集utf8mb4是utf8的超集,所以當這兩個類型的字符串做比較的時候,MySQL內部先把utf8字符串轉成utf8mb4字符集在做比較(按數據長度增加的方向轉換)

CONVERT()把輸入字符串轉成utf8mb4字符集。對索引字段做函數操作,放棄走樹搜索功能

修改:

小結對索引字段做函數操作,可能會破壞索引值的有序性,因此優化器放棄走樹搜索功能,但是MySQL確實有偷懶的嫌疑,即使簡單地把 where id +1=10000改爲where id= 10000-1就能夠上索引快速查找,也不會主動做這個語句重寫

 

 

           

問題:假設現在表裏面有100萬行數據,其中10萬行數據的b的值爲·  1234567890·,現在執行語句這樣寫。

MySQL會怎麼執行呢?

最理想的是MySQL字段b定義的是varchar(10),肯定返回空。但是MySQL並沒有這樣做,

這條SQL語句執行很慢,流程是這樣的:

  1、在傳給引擎執行的時候,做了字符截斷,只截了前10個字節,也就是‘1234567890’這個匹配

  2、這樣滿足條件的數據有10萬行

 3、因爲select* ,所以做10萬次回表,然後每次回表後查出整行,到server層判斷,b值都不是'123456789.abcd',返回結果爲空

 

 

19、爲什麼我查一行語句,也執行這麼慢?

不包括,MySQL數據本身有很大的壓力,導致數據庫服務器CPU詹歐用鋁很高或者IO利用率很高,這種情況下所有語句執行都有可能慢,不屬於討論範圍

第一類:查詢長時間不返回

 

查詢結果長時間不返回

 一般情況下是大概率表t被鎖住了。接下來執行 show processlist命令,看看當前語句處於什麼運行狀態

等MDL寫鎖:

執行show processlist命令查看:

 

state:waiting for table metadata lock 這個狀態標識,現在有一個線程正在表t上請求或者持有MDL寫鎖,把select語句堵住了

如何復現:

通過查詢sys.schema_table_locl_waits這張表,我們可以直接找出造成阻塞的process id,然後把這個連接kill命令斷開

等flush

 在表中執行語句查出來狀態是 Waiting for table flush;

 原因:現在有一個線程正對錶做flush操作,MySQL裏面對錶做flush操作用法,一般有兩個用法:

   flush tables w with read lock;

   flush tables with read lock;

注:FTWRL流程: 1、請求獲取相關類型的MDL lock  2、清空query cache中的內容 3、flush table 將當前所有打開的table的fd關閉 4、請求獲取全局 table-level lock 5、上全局 COMMIT鎖

       當有很大的事務在進行的時候,此時FTWRL的步驟一,步驟二可以完成,但是進行到步驟三的時候,由於表相關的事務正在執行中,相應table的句柄被佔用,無法進行flush table操作。

   指定t表的話,只關閉表t;如果沒有指定具體表明,則表示關閉MySQL所有打開的表。

    但是正常這兩個語句執行起來都很快,觸發它們也被別的線程堵住了。所以出現waiting for table flush狀態可能情況:有一個flush tables命令被別的語句堵住了,然後它又堵住了我們的slect語句

復現步驟:

 在session A中,故意每行都調用一次sleep(1)這樣這個語句默認執行10萬秒。然後sessionB的flush tables t命令再去關閉表t就要等session A的查詢結束,這樣session C就被flush 堵住了

 

行級鎖:

知識補充:

列出表所有字段:SHOW FULL FIELDS FROM tablename;

查看: select @@autocommit;

注:select....lock in share mode 是IS鎖(意向共享鎖),在符合條件的rows上都加了共享鎖,這樣的話,其他session可以讀取這些記錄,也可以繼續添加IS鎖,但是無法修改這些記錄直到你這個加鎖的session執行完成(否者直接鎖等待超時)

       select...for update 走得是IX鎖(意向排它鎖),即在符合條件的rows上都加了排它鎖,其他session無法添加任何s和x鎖。 如果不存在一致性非鎖定讀的話,那麼其他session是無法讀取和修改這些記錄的,但是innodb有非鎖定讀(快照讀並不需要加鎖),for update之後並不會阻塞其他session的快照讀取操作,除了select ...lock in share mode和select ... for update這種顯示加鎖的查詢操作。

通過對比,發現for update的加鎖方式無非是比lock in share mode的方式多阻塞了select...lock in share mode的查詢方式,並不會阻塞快照讀。

實例:

     mysql> select * from t where id =1 lock inshare mode;

由於id=1這個記錄要加讀鎖,如果這個時候已經有一個事務在這行記錄上持有一個寫鎖,我們select語句就會被堵住。

復現步驟:

行鎖復現:

session A啓動了事務,佔有寫鎖,還不提交,是導致session B被堵住的原因

可以通過 sys.innodb_lock_waits;表查到

查看到 blocking_pid 線程時造成阻塞的罪魁禍首。KILL Query 623或者kill 623。

          不過,這裏不應該顯示“KILL QUERY 623”這個命令表示停止623號線當前正在執行的語句,而這個方法是沒有用的。因爲佔有行鎖的是update語句,這個語句已結是之前執行完成了的,現在執行KILL QUERY,無法讓這個事務去掉id=1上的行鎖

           KILL 4 纔有效,也就是直接斷開這個鏈接。這裏隱含的邏輯就是,鏈接被斷開,會自動回滾到這個鏈接裏面正在執行的線程,也就是放id=1上的行鎖

第二類:查詢慢

    一致性讀,又稱爲快照讀。使用的是MVCC機制讀取undo中的已經提交的數據。所以它的讀取是非阻塞的。普通的SELECT就是快照讀。

       當前讀:讀取的是最新版本。UPDATE、DELETE、INSERT、SELECT …  LOCK IN SHARE MODE、SELECT … FOR UPDATE是當前讀。

     mysql> select * from t  where c=50000 limit 1;

 由於字段沒有索引,這個語句只能走id主鍵順序掃描,因此需要掃描5W行。

select * from t where id=1  雖然掃描行數是1,但是執行時間卻長達800毫秒

復現步驟:

session B執行100萬次update語句,生成了100W個回滾日誌(undo log),帶lock in share mode的SQL,是當前讀,因此直接讀到100萬零1次這個結果,所以速度很快;而select * from t where id=1這個語句是一致性讀,因此需要從100萬零一次開始,依次執行undo log,執行100萬次以後纔將1這個結果返回。

輸出結果:

擴展:

      innodb的默認事務隔離級別是rr(可重複讀)。它的實現技術是mvcc。基於版本的控制協議。該技術不僅可以保證innodb的可重複讀,而且可以防止幻讀。但是它防止的是快照讀,也就是讀取的數據雖然是一致的,但是數據是歷史數據。如何做到保證數據是一致的(也就是一個事務,其內部讀取對應某一個數據的時候,數據都是一樣的),同時讀取的數據是最新的數據。innodb提供了一個間隙鎖的技術。也就是結合grap鎖與行鎖,達到最終目的。當使用索引進行插入的時候,innodb會將當前的節點和上一個節點加鎖。這樣當進行select的時候,就不允許加x鎖。那麼在進行該事務的時候,讀取的就是最新的數據。

      在RR級別下,快照讀是通過MVVC(多版本控制)和undo log來實現的,當前讀是通過加record lock(記錄鎖)和gap lock(間隙鎖)來實現的。
所以從上面的顯示來看,如果需要實時顯示數據,還是需要通過加鎖來實現。這個時候會使用next-key技術來實現。

問題:

 

 

 

 

20、幻讀是什麼,幻讀有什麼問題?

  

執行這個語句:

語句命中d=5,對應主鍵id=5,因此select語句執行完後,id=5會加一個寫鎖,因爲兩階段鎖協議,這個寫鎖會在執行commit語句的時候釋放。

假設: 如果id=5這一行加鎖

Q3讀到了id=1這一行現象,被稱爲“幻讀”。也就是說幻讀指的是一個事務在前後兩次查詢同一個範圍的時候,後一次查詢看到了前一次查詢沒有看到的行

這裏,幻讀的說明:

1、在可重複讀下,普通的查詢是快照讀,不會看到別的事務插入的數據,因此,幻讀是當前讀纔出現的

2、上面B修改的結果,被A之後的select語句用當前讀看到,不能稱爲幻讀。幻讀僅專指“新插入的行”

假設:如果把掃描的行,都加上寫鎖,再來看看效果

由於A把所有的行都加了寫鎖,所以B在執行第一個update語句的時候就被鎖住了。等T6的A提交以後,B才能執行,但是。即使把所有的記錄都加上了鎖,也還是阻止不了新插入的記錄。

如果解決幻讀?

   行鎖只能鎖住行。但是新增加記錄這個動作,要更新的是記錄之間的“間隙”。因此,爲了解決幻讀問題,InnoDB只好引入新的鎖,也就是間隙鎖(Gap Lock)

 

 當執行select * from t where d=5 for update的時候,就不只是給數據庫中已有的6個記錄加上行鎖,還同時加了間隙鎖,這樣就無法再插入新的記錄

 

 但是間隙鎖不一樣,跟間隙鎖存在衝突關係的,是“往這個間隙插入一個記錄”這個操作。間隙鎖直接不存在衝突關係

比如:

間隙鎖之間不互鎖:這裏B並不會被堵住。因爲t並沒有c=7這個記錄,因此A加的是(5,10)這個間隙鎖,而B也是在這個間隙加的間隙鎖。它們有共同目標,即:保護這個間隙,不允許插入值,但是他們之間不衝突

   間隙鎖和行鎖合稱 next-key lock,每個next-key lock 是前開後閉區間。也就是類似(-無窮,0]、(0,5]等等

   間隙鎖和next-key lock的引入,幫我們解決了幻讀的問題,但同時也帶來了一些“困擾”,間隙鎖的引入可能導致同一的語句鎖住更大的範圍,這其實是影響了併發度的。當鎖定一個範圍鍵值後,即使某些不存在的鍵值也會被鎖定,無法造成鎖定時候插入任何數據。在某些場景下會對性能造成影響

 間隙鎖是在可重複讀隔離級別下才會生效的、所以,如果把隔離級別設置爲讀提交的話,就沒有間隙鎖了。但同時,你要解決可能出現數據和日誌不一致問題,需要把binlog格式設置爲row,這也是不少公司使用的配置組合\

 

擴展: 

 除了間隙鎖,通過索引實現鎖定的方式還存在其他幾個較大的隱患:
      1.當query無法利用索引的時候,innodb會放棄使用行鎖而改用表鎖,造成併發性能降低
      2.當query使用索引並不包含所有過濾條件時候,數據檢索使用到的索引建指向的數據可能有部分並不屬於該query結果集的行列,但是也會被鎖定,因爲間隙鎖是鎖定範圍
      3.當query在使用索引定位數據時候,如果使用索引建一樣但訪問數據行不同時候(索引只是過濾條件一部分),一樣被鎖

 

問題:

   

判斷一下A,B,C狀態

 

21、爲什麼我只改了一條數據,加了這麼多鎖?

因爲間隙鎖是在可重複讀隔離級別下才有效,總結的加鎖規則:兩個原則,兩個優化和一個bug

分析:根據原則1,加鎖單位是 next-key lock session A加鎖範圍就是(5,10】,同時根據優化2,這個是等值查詢,next-key退化成間隙鎖,因此最終加鎖範圍時(5,10),C可以因爲前開後閉

分析:

但session C要插入就會被session A的間隙鎖(5,10)鎖住,在這個例子中 lock in share mode 值鎖覆蓋索引,但是如果是for update就不一樣。執行 for update時,系統會認爲你接下來更新數據,因此順便給主鍵索引上滿足條件的行加上行鎖

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章