MySQL複習(二):MySQL鎖、MySQL事務、SQL優化、數據庫分庫分表

五、MySQL鎖

根據加鎖的範圍,MySQL裏面的鎖大致可以分成全局鎖、表級鎖和行鎖三類

1、全局鎖

全局鎖就是對整個數據庫實例加鎖。MySQL提供了一個加全局讀鎖的方法,命令是Flush tables with read lock。當需要讓整個庫處於只讀狀態的時候,可以使用這個命令,之後其他線程的以下語句會被阻塞:數據更新語句(數據的增刪改)、數據定義語句(包括建表、修改表結構等)和更新類事務的提交語句

全局鎖的典型使用場景是,做全庫邏輯備份。也就是把整庫每個表都select出來存成文本

但是讓整個庫都只讀,可能出現以下問題:

  • 如果在主庫上備份,那麼在備份期間都不能執行更新,業務基本上就得停擺
  • 如果在從庫上備份,那麼在備份期間從庫不能執行主庫同步過來的binlog,會導致主從延遲
    在可重複讀隔離級別下開啓一個事務能夠拿到一致性視圖

官方自帶的邏輯備份工具是mysqldump。當mysqldump使用參數–single-transaction的時候,導數據之前就會啓動一個事務,來確保拿到一致性視圖。而由於MVCC的支持,這個過程中數據是可以正常更新的

2、表級鎖

MySQL裏面表級別的鎖有兩種:一種是表鎖,一種是元數據鎖(meta data lock,MDL)

表鎖的語法是lock tables … read/write。可以用unlock tables主動釋放鎖,也可以在客戶端斷開的時候自動釋放。lock tables語法除了會限制別的線程的讀寫外,也限定了本線程接下來的操作對象

如果在某個線程A中執行lock tables t1 read,t2 wirte;這個語句,則其他線程寫t1、讀寫t2的語句都會被阻塞。同時,線程A在執行unlock tables之前,也只能執行讀t1、讀寫t2的操作。連寫t1都不允許

另一類表級的鎖是MDL。MDL不需要顯式使用,在訪問一個表的時候會被自動加上。MDL的作用是,保證讀寫的正確性。如果一個查詢正在遍歷一個表中的數據,而執行期間另一個線程對這個表結構做了變更,刪了一列,那麼查詢線程拿到的結果跟表結構對不上,肯定不行

在MySQL5.5版本引入了MDL,當對一個表做增刪改查操作的時候,加MDL讀鎖;當要對錶做結構變更操作的時候,加MDL寫鎖

  • 讀鎖之間不互斥,因此可以有多個線程同時對一張表增刪改查
  • 讀寫鎖之間、寫鎖之間是互斥的,用來保證變更表結構操作的安全性。因此,如果有兩個線程要同時給一個表加字段,其中一個要等另一個執行完才能開始執行

給一個表加字段,或者修改字段,或者加索引,需要掃描全表的數據。在對大表操作的時候,需要特別小心,以免對線上服務造成影響

在這裏插入圖片描述

session A先啓動,這時候會對錶t加一個MDL讀鎖。由於session B需要的也是MDL讀鎖,因此可以正常執行。之後sesession C會被blocked,是因爲session A的MDL讀鎖還沒有釋放,而session C需要MDL寫鎖,因此只能被阻塞。如果只有session C自己被阻塞還沒什麼關係,但是之後所有要在表t上新申請MDL讀鎖的請求也會被session C阻塞。所有對錶的增刪改查操作都需要先申請MDL讀鎖,就都被鎖住,等於這個表現在完全不可讀寫了

事務中的MDL鎖,在語句執行開始時申請,但是語句結束後並不會馬上釋放,而會等到整個事務提交後再釋放

1.如果安全地給小表加字段?

首先要解決長事務,事務不提交,就會一直佔着DML鎖。在MySQL的information_schema庫的innodb_trx表中,可以查到當前執行的事務。如果要做DDL變更的表剛好有長事務在執行,要考慮先暫停DDL,或者kill掉這個長事務

2.如果要變更的表是一個熱點表,雖然數據量不大,但是上面的請求很頻繁,而又不得不加個字段,該怎麼做?

在alter table語句裏面設定等待時間,如果在這個指定的等待時間裏面能夠拿到MDL寫鎖最好,拿不到也不要阻塞後面的業務語句,先放棄。之後再通過重試命令重複這個過程

3、行鎖

行鎖就是針對數據表中行記錄的鎖。比如事務A更新了一行,而這時候事務B也要更新同一行,則必須等事務A的操作完成後才能進行更新

1)、兩階段鎖協議

在這裏插入圖片描述

事務A持有的兩個記錄的行鎖都是在commit的時候才釋放的,事務B的update語句會被阻塞,直到事務A執行commit之後,事務B才能繼續執行

在InnoDB事務中,行鎖是在需要的時候才加上的,但並不是不需要了就立刻釋放,而是要等到事務結束時才釋放。這個就是兩階段鎖協議

如果事務中需要鎖多個行,要把最可能造成鎖衝突、最可能影響併發度的鎖儘量往後放

假設要實現一個電影票在線交易業務,顧客A要在影院B購買電影票。業務需要涉及到以下操作:

1.從顧客A賬戶餘額中扣除電影票價

2.給影院B的賬戶餘額增加這張電影票價

3.記錄一條交易日誌

爲了保證交易的原子性,要把這三個操作放在一個事務中。如何安排這三個語句在事務中的順序呢?

如果同時有另外一個顧客C要在影院B買票,那麼這兩個事務衝突的部分就是語句2了。因爲它們要更新同一個影院賬戶的餘額,需要修改同一行數據。根據兩階段鎖協議,所有的操作需要的行鎖都是在事務提交的時候才釋放的。所以,如果把語句2安排在最後,比如按照3、1、2這樣的順序,那麼影院賬戶餘額這一行的鎖時間就最少。這就最大程度地減少了事務之間的鎖等待,提升了併發度

2)、一致性非鎖定讀

一致性非鎖定讀是指InnoDB通過行多版本控制的方式來讀取當前執行時間數據庫中行的數據。如果讀取的行正在執行delete或update操作,這時讀取操作不會因此去等待行上排它鎖的釋放。相反地,InnoDB會去讀取行的一個快照數據

在這裏插入圖片描述

非鎖定讀機制極大地提高了數據庫的併發性。在InnoDB的默認設置下,這是默認的讀取方式,即讀取不會佔用和等待表上的鎖

快照數據其實就是當前行數據之前的歷史版本,每行記錄可能有多個版本。一個行記錄有不止一個快照數據,行多版本的併發控制稱爲多版本併發控制(MVCC)

在事務隔離級別爲讀提交和可重複讀下,InnoDB使用非鎖定的一致性讀。對於快照數據的定義卻不相同。在讀提交隔離級別下,對於快照數據,非一致性讀總是讀取被鎖定行的最新一份快照數據。而在可重複讀隔離級別下,對於快照數據,非一致性讀總是讀取事務開始時的行數據版本

3)、一致性鎖定讀

InnoDB支持兩種一致性的鎖定讀操作:

  • select … for update
  • select … lock in share mode

select … for update對讀取的行記錄加一個排它鎖,其他事務不能對已鎖定的行加上任何鎖。select … lock in share mode對讀取的行記錄加一個共享鎖,其他事務可以向北鎖定的行加共享鎖,但是如果加排它鎖,則會被阻塞

4)、行鎖的3種算法

InnoDB有3種行鎖的算法,分別是:

  • Record Lock:單個行記錄上的鎖
  • Gap Lock:間隙鎖,鎖定一個範圍,但不包含記錄本身
  • Next-Key Lock:Gap Lock+Record Lock,鎖定一個範圍,並且鎖定記錄本身

1)幻讀問題

幻讀指的是一個事務在前後兩次查詢同一個範圍的時候,後一次查詢看到了前一次查詢沒有看到的行

InnoDB默認的事務隔離級別是可重複讀,在該隔離級別下采用Next-Key Lock的方式來加鎖。而在事務隔離級別爲讀提交下,僅採用Record Lock

訂單表中有id爲1、2、5的三條數據,當隔離級別爲讀提交的時候會出現幻讀的問題,過程如下:

操作 會話A 會話B
1 set session transaction isolation level read committed;
2 begin;
3 select * from order where id>2 for update;(查詢結果:5)
4 begin;
5 insert into order(id,order_no,order_name) values(4,4,‘訂單4’);
6 commit;
7 select * from order where id>2 for update;(查詢結果:4,5)

在可重複隔離級別下,select * fromorderwhere id>2 for update鎖住的不是id爲5的這條記錄,而是對(2, +supremum]這個範圍加了排它鎖。因此任何對於這個範圍的插入操作都是不被允許的,操作5將會被阻塞,從而避免了幻讀的問題

2)間隙鎖

建表和初始化語句如下:

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 into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);

這個表除了主鍵id外,還有一個索引c

爲了解決幻讀問題(在讀提交隔離級別下,不存在間隙鎖),InnoDB引入了間隙鎖,鎖的就是兩個值之間的空隙

在這裏插入圖片描述

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

行鎖分成讀鎖和寫鎖

在這裏插入圖片描述

跟間隙鎖存在衝突關係的是往這個間隙中插入一個記錄這個操作。間隙鎖之間不存在衝突關係

在這裏插入圖片描述

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

間隙鎖和行鎖合稱Next-Key Lock,每個Next-Key Lock是前開後閉區間。表t初始化以後,如果用select * from t for update要把整個表所有記錄鎖起來,就形成了7個Next-Key Lock,分別是(-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。因爲+∞是開區間,在實現上,InnoDB給每個索引加了一個不存在的最大值supremum,這樣才符合都是前開後閉區間

間隙鎖和Next-Key Lock的引入,解決了幻讀的問題,但同時也帶來了一些困擾

間隙鎖導致的死鎖

在這裏插入圖片描述

1.sessionA執行select … for update語句,由於id=9這一行並不存在,因此會加上間隙鎖(5,10)

2.sessionB執行select … for update語句,同樣會加上間隙鎖(5,10),間隙鎖之間不會衝突

3.sessionB試圖插入一行(9,9,9),被sessionA的間隙鎖擋住了,只好進入等待

4.sessionA試圖插入一行(9,9,9),被sessionB的間隙鎖擋住了

兩個session進入互相等待狀態,形成了死鎖

間隙鎖的引入可能會導致同樣的語句鎖住更大的範圍,這其實是影響併發度的

3)Next-Key Lock加鎖規則

  • 原則1:加鎖的基本單位是Next-Key Lock,Next-Key Lock是前開後閉區間
  • 原則2:查找過程中訪問到的對象纔會加鎖
  • 優化1:索引上的等值查詢,給唯一索引加鎖的時候,Next-Key Lock退化爲行鎖
  • 優化2:索引上的等值查詢,向右遍歷時且最後一個值不滿足等值條件的時候,Next-Key Lock退化爲間隙鎖
  • 一個bug:唯一索引上的範圍查詢會訪問到不滿足條件的第一個值爲止

這個規則只限於MySQL5.x系列<=5.7.24,8.0系列<=8.0.13

5)、死鎖和死鎖檢測

在併發系統中不同線程出現循環資源依賴,涉及的線程都在等待別的線程釋放資源時,就會導致這幾個線程都進入無限等待的狀態,稱爲死鎖

在這裏插入圖片描述

事務A在等待事務B釋放id=2的行鎖,而事務B在等待事務A釋放id=1的行鎖。事務A和事務B在互相等待對方的資源釋放,就是進入了死鎖狀態。當出現死鎖以後,有兩種策略:

  • 一種策略是,直接進入等待,直到超時。這個超時時間可以通過參數innodb_lock_wait_timeout來設置
  • 另一種策略是,發起死鎖檢測,發現死鎖後,主動回滾死鎖鏈條中的某一個事務,讓其他事務得以繼續執行。將參數innodb_deadlock_detect設置爲on,表示開啓這個邏輯

在InnoDB中,innodb_lock_wait_timeout的默認值是50s,意味着如果採用第一個策略,當出現死鎖以後,第一個被鎖住的線程要過50s纔會超時退出,然後其他線程纔有可能繼續執行。對於在線服務來說,這個等待時間往往是無法接受的

正常情況下還是要採用主動死鎖檢查策略,而且innodb_deadlock_detect的默認值本身就是on。主動死鎖監測在發生死鎖的時候,是能夠快速發現並進行處理的,但是它有額外負擔的。每當一個事務被鎖的時候,就要看看它所依賴的線程有沒有被別人鎖住,如此循環,最後判斷是否出現了循環等待,也就是死鎖

如果所有事務都要更新同一行的場景,每個新來的被堵住的線程都要判斷會不會由於自己的加入導致死鎖,這是一個時間複雜度是O(n)的操作

怎麼解決由這種熱點行更新導致的性能問題?

1.如果確保這個業務一定不會出現死鎖,可以臨時把死鎖檢測關掉

2.控制併發度

3.將一行改成邏輯上的多行來減少鎖衝突。以影院賬戶爲例,可以考慮放在多條記錄上,比如10個記錄,影院的賬戶總額等於這10個記錄的值的總和。這樣每次要給影院賬戶加金額的時候,隨機選其中一條記錄來加。這樣每次衝突概率變成員原來的1/10,可以減少鎖等待個數,也就減少了死鎖檢測的CPU消耗

六、MySQL事務

1、事務的特性

  • 原子性:一個事務中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被回滾到事務開始前的狀態,就像這個事務從來沒有執行過一樣
  • 一致性:在事務開始之前和事務結束以後,數據庫的完整性沒有被破壞
  • 隔離性:數據庫允許多個併發事務同時對數據進行讀寫和修改的能力,隔離性可以防止多個事務併發執行時由於交叉執行而導致數據的不一致
  • 持久性:事務處理結束後,對數據的修改就是永久的,即便系統故障也不會丟失

2、隔離級別

1)、當數據庫上有多個事務同時執行的時候,就可能出現髒讀、不可重複讀、幻讀的問題

  • 髒讀:B事務讀取到了A事務尚未提交的數據
  • 不可重複讀:一個事務讀取到了另一個事務中提交的update的數據
  • 幻讀/虛讀:一個事務讀取到了另一個事務中提交的insert的數據

2)、事務的隔離級別包括:讀未提交、讀提交、可重複讀和串行化

  • 讀未提交:一個事務還沒提交時,它做的變更就能被別的事務看到
  • 讀提交:一個事務提交之後,它做的變更纔會被其他事務看到(解決髒讀,Oracle默認的隔離級別)
  • 可重複讀:一個事務執行過程中看到的數據,總是跟這個事務在啓動時看到的數據是一致的,而且未提交變更對其他事務也是不可見的(解決髒讀和不可重複讀,MySQL默認的隔離級別)
  • 串行化:對於同一行記錄,寫會加寫鎖,讀會加讀鎖,當出現讀寫鎖衝突的時候,後訪問的事務必須等前一個事務執行完成,才能繼續執行(解決髒讀、不可重複讀和幻讀)

安全性依次提交,性能依次降低

3)、案例

假設數據表T中只有一列,其中一行的值爲1

create table T(c int) engine=InnoDB;
insert into T(c) values(1);

下面是按照時間順序執行兩個事務的行爲:

在這裏插入圖片描述

  • 若隔離級別是讀未提交,則V1是2。這時候事務B雖然還沒提交,但是結果已經被A看到了。V2、V3都是2
  • 若隔離級別是讀提交,則V1是1,V2是2。事務B的更新在提交後才能被A看到。V3也是2
  • 若隔離級別是可重複讀,則V1、V2是1,V3是2。之所以V2是1,遵循的是事務在執行期間看到的數據前後必須是一致的
  • 若隔離級別是串行化,V1、V2值是1,V3是2

在實現上,數據庫裏面會創建一個視圖,訪問的時候以視圖的邏輯結果爲準。讀未提交隔離級別下直接返回記錄上的最新值,沒有視圖概念;在讀提交隔離級別下,這個視圖是在每個SQL語句開始執行的時候創建的;在可重複讀隔離級別下,這個視圖是在事務啓動時創建的,整個事務存在期間都用這個視圖;而串行化隔離級別下直接用加鎖的方式來避免並行訪問(InnoDB會對每個select語句後自動加上lock in share mode)

3、事務啓動的方式

MySQL的事務啓動方式有以下幾種:

  • 顯示啓動事務語句,begin或start transaction。提交語句是commit,回滾語句是rollback
  • set autocommit=0,這個命令將這個線程的自動提交關掉。意味着如果只執行一個select語句,這個事務就啓動了,而且不會自動提交事務。這個事務持續存在直到主動執行commit或rollback語句,或者斷開連接

建議使用set autocommit=1,通過顯示語句的方式來啓動事務

可以在information_schema庫中的innodb_trx這個表中查詢長事務,如下語句查詢持續時間超過60s的事務

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

4、事務隔離的實現(以可重複讀爲例)

redo log恢復提交事務修改的頁操作,而undo log回滾行記錄到某個特定版本。redo log是物理日誌,記錄的是頁的屋裏修改操作;undo log是邏輯日誌,根據每行記錄進行記錄

在MySQL中,每條記錄在更新的時候都會同時記錄一條回滾操作(undo log)。記錄上的最新值,通過回滾操作,都可以得到前一個狀態的值

假設一個值從1被按順序改成了2、3、4,在undo log裏面就會有類似下面的記錄:

在這裏插入圖片描述

當前值是4,但是在查詢這條記錄的時候,不同時刻啓動的事務會有不同的read-view。如圖中看到的,在視圖A、B、C裏面,這一個記錄的值分別是1、2、4,同一條記錄在系統中可以存在多個版本,就是數據庫的多版本併發控制(MVCC)。對於read-viewA,要得到1,就必須將當前值一次執行圖中所有的回滾操作得到

即使現在有另外一個事務正在將4改成5,這個事務跟read-view A、B、C對應的事務是不會衝突的

系統會判斷,當沒有事務再需要用到這些undo log時,回收已經使用並分配的undo頁

下面是一個只有兩行的表的初始化語句:

CREATE TABLE `t` (
  `id` int(11) NOT NULL,
  `k` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);

事務A、B、C的執行流程如下,採用可重複讀隔離級別

在這裏插入圖片描述

begin/start transaction命令:不是一個事務的起點,在執行到它們之後的第一個操作InnoDB表的語句,事務才真正啓動,一致性視圖是在執行第一個快照讀語句時創建的

start transaction with consistent snapshot命令:馬上啓動一個事務,一致性視圖是在執行這條命令時創建的

按照上圖的流程執行,事務B查到的k的值是3,而事務A查到的k的值是1

1)、快照在MVCC裏是怎麼工作的?

在可重複讀隔離級別下,事務啓動的時候拍了個快照。這個快照是基於整個庫的,那麼這個快照是如何實現的?

InnoDB裏面每個事務有一個唯一的事務ID,叫做transaction id。它在事務開始的時候向InnoDB的事務系統申請,是按申請順序嚴格遞增的

每行數據也都是有多個版本的。每次事務更新數據的時候,都會生成一個新的數據版本,並且把transaction id賦值給這個數據版本的事務ID,記作row trx_id。同時,舊的數據版本要保留,並且在新的數據版本中,能夠有信息可以直接拿到它。也就是說,數據表中的一行記錄,其實可能有多個版本,每個版本有自己的row trx_id

下圖是一個記錄被多個事務連續更新後的狀態:

在這裏插入圖片描述

語句更新生成的undo log(回滾日誌)就是上圖中的是哪個虛線箭頭,而V1、V2、V3並不是物理上真實存在的,而是每次需要的時候根據當前版本和undo log計算出來的。比如,需要V2的時候,就是通過V4依次執行U3、U2算出來的

按照可重複讀的定義,一個事務啓動的時候,能夠看到所以已經提交的事務結果。但是之後,這個事務執行期間,其他事務的更新對它不可見。在實現上,InnoDB爲每個事務構造了一個數組,用來保存這個事務啓動瞬間,當前在啓動了但還沒提交的所有事務ID。數組裏面事務ID的最小值記爲低水位,當前系統裏面已經創建過的事務ID的最大值加1記爲高水位。這個視圖數組和高水位就組成了當前事務的一致性視圖。而數據的可見性規則,就是基於數據的row trx_id和這個一致性視圖的對比結果得到的

這個視圖數組把所有的row trx_id分成了幾種不同的情況

在這裏插入圖片描述

對於當前事務的啓動瞬間來說,一個數據版本的row trx_id,有以下幾種可能:

1)如果落在綠色部分,表示這個版本是已提交的事務或者是當前事務自己生成的,這個數據是可見的

2)如果落在紅色部分,表示這個版本是由將來啓動的事務生成的,肯定不可見

3)如果落在黃色部分,那就包括兩種情況

  • 若row trx_id在數組中,表示這個版本是由還沒提交的事務生成的,不可見
  • 若row trx_id不在數組中,表示這個版本是已經提交了的事務生成的,可見

InnoDB利用了所有數據都有多個版本的這個特性,實現了秒級創建快照的能力

2)、爲什麼事務A的查詢語句返回的結果是k=1?

假設:

1.事務A開始時,系統裏面只有一個活躍事務ID是99

2.事務A、B、C的版本號分別是100、101、102

3.三個事務開始前,(1,1)這一行數據的row trx_id是90

這樣,事務A的是數組就是[99,100],事務B的視圖數組是[99,100,101],事務C的視圖數組是[99,100,101,102]

在這裏插入圖片描述

從上圖中可以看到,第一個有效更新是事務C,從數據從(1,1)改成了(1,2)。這時候,這個數據的最新版本的row trx_id是102,而90這個版本已經成爲了歷史版本

第二個有效更新是事務B,把數據從(1,2)改成了(1,3)。這時候,這個數據的最新版本是101,而102又成爲了歷史版本

在事務A查詢的時候,其實事務B還沒提交,但是它生成的(1,3)這個版本已經變成當前版本了。但這個版本對事務A必須是不可見的,否則就變成髒讀了

現在事務A要讀數據了,它的視圖數組是[99,100]。讀數據都是從當前版本讀起的。所以,事務A查詢語句的讀數據流程是這樣的:

  • 找到(1,3)的時候,判斷出row trx_id=101,比高水位大,處於紅色區域,不可見
  • 接着,找到上一個歷史版本,一看row trx_id=102,比高水位大,處於紅色區域,不可見
  • 再往前找,終於找到了(1,1),它的row trx_id=90,比低水位小,處於綠色區域,可見

雖然期間這一行數據被修改過,但是事務A不論在什麼時候查詢,看到這行數據的結果都是一致的,我們稱之爲一致性讀

一個數據版本,對於一個事務視圖來說,除了自己的更新總是可見以外,有三種情況:

  • 版本未提交,不可見
  • 版本已提交,但是是在視圖創建後提交的,不可見
  • 版本已提交,而且是在視圖創建前提交的,可見

事務A的查詢語句的視圖數組是在事務A啓動的時候生成的,這時候:

  • (1,3)還沒提交,屬於情況1,不可見
  • (1,2)雖然提交了,但是是在視圖數組創建之後提交的,屬於情況2,不可見
  • (1,1)是在視圖數組創建之前提交的,可見

3)、爲什麼事務B的查詢語句返回的結果是k=3?

在這裏插入圖片描述

事務B要去更新數據的時候,就不能再在歷史版本上更新了,否則事務C的更新就丟失了。因此,事務B此時的set k=k+1是在(1,2)的基礎上進行的操作

更新數據都是先讀後寫的,而這個讀,只能讀當前的值,稱爲當前讀。除了update語句外,select語句如果加鎖,也是當前讀

假設事務C不是馬上提交的,而是變成了下面的事務C’,會怎麼樣?

在這裏插入圖片描述

上圖中,事務C更新後沒有馬上提交,在它提交前,事務B的更新語句先發起了。雖然事務C還沒提交,但是(1,2)這個版本也已經生成了,並且是當前的最新版本

這時候涉及到了兩階段鎖協議,事務C沒提交,也就是說(1,2)這個版本上的寫鎖還沒釋放。而事務B是當前讀,必須要讀最新版本,而且必須加鎖,因此就被鎖住了,必須等到事務C釋放這個鎖,才能繼續它的當前讀

在這裏插入圖片描述

七、SQL優化

1、通過EXPLAIN分析SQL執行計劃

在這裏插入圖片描述

下面對圖示中的每一個字段進行說明:

1)id:每個執行計劃都有一個id,如果是一個聯合查詢,這裏還將有多個id

2)select_type:表示SELECT查詢類型,常見的有SIMPLE(普通查詢,即沒有聯合查詢、子查詢)、PRIMARY(主查詢)、UNION(UNION 中後面的查詢)、SUBQUERY(子查詢)等

3)table:當前執行計劃查詢的表,如果給表起別名了,則顯示別名信息

4)partitions:訪問的分區表信息

5)type:表示從表中查詢到行所執行的方式,查詢方式是SQL優化中一個很重要的指標,結果值從好到差依次是:system > const > eq_ref > ref > range > index > ALL

  • system/const:表中只有一行數據匹配,此時根據索引查詢一次就能找到對應的數據
  • eq_ref:使用唯一索引掃描,常見於多表連接中使用主鍵和唯一索引作爲關聯條件
  • ref:非唯一索引掃描,還可見於唯一索引最左原則匹配掃描
  • range:索引範圍掃描,比如:<,>,between等操作
  • index:索引全表掃描,此時遍歷整個索引樹
  • ALL:表示全表掃描,需要遍歷全表來找到對應的行

6)possible_keys:可能使用到的索引

7)key:實際使用到的索引

8)key_len:當前使用的索引的長度

9)ref:關聯 id 等信息

10)rows:查找到記錄所掃描的行數

11)filtered:查找到所需記錄佔總掃描記錄數的比例

12)Extra:額外的信息

2、數據庫設計方面

1)對查詢進行優化,應儘量避免全表掃描,首先應考慮在 whereorder by 涉及的列上建立索引

2)應儘量避免在 where 子句中對字段進行 null 值判斷,否則將導致引擎放棄使用索引而進行全表掃描,如: select id from t where num is null 可以在num上設置默認值0,確保表中num列沒有null值,然後這樣查詢: select id from t where num = 0

3)並不是所有索引對查詢都有效,SQL是根據表中數據來進行查詢優化的,當索引列有大量數據重複時,查詢可能不會去利用索引,如一表中有字段sex,male、female幾乎各一半,那麼即使在sex上建了索引也對查詢效率起不了作用

4)索引並不是越多越好,索引固然可以提高相應的 select 的效率,但同時也降低了 insertupdate 的效率,因爲 insertupdate 時有可能會重建索引,所以怎樣建索引需要慎重考慮,視具體情況而定。一個表的索引數最好不要超過6個,若太多則應考慮一些不常使用到的列上建的索引是否有必要

5)應儘可能的避免更新索引數據列,因爲索引數據列的順序就是表記錄的物理存儲順序,一旦該列值改變將導致整個表記錄的順序的調整,會耗費相當大的資源。若應用系統需要頻繁更新索引數據列,那麼需要考慮是否應將該索引建爲索引

6)儘量使用數字型字段,若只含數值信息的字段儘量不要設計爲字符型,這會降低查詢和連接的性能,並會增加存儲開銷。這是因爲引擎在處理查詢和連接時會逐個比較字符串中每一個字符,而對於數字型而言只需要比較一次就夠了

7)儘可能的使用 varchar/nvarchar 代替 char/nchar ,因爲首先變長字段存儲空間小,可以節省存儲空間,其次對於查詢來說,在一個相對較小的字段內搜索效率顯然要高些

3、SQL語句方面

1)應儘量避免在 where 子句中使用!=或<>操作符,否則將引擎放棄使用索引而進行全表掃描

2)應儘量避免在 where 子句中使用 or 來連接條件,否則將導致引擎放棄使用索引而進行全表掃描,如: select id from t where num=10 or num=20 可以這樣查詢: select id from t where num=10 union all select id from t where num=20

3)innot in 也要慎用,否則會導致全表掃描,如: select id from t where num in(1,2,3) 對於連續的數值,能用 between 就不要用 in 了: select id from t where num between 1 and 3

4)下面的查詢也將導致全表掃描: select id from t where name like ‘%abc%’

5)如果在 where 子句中使用參數,也會導致全表掃描。因爲SQL只有在運行時纔會解析局部變量,但優化程序不能將訪問計劃的選擇推遲到運行時;它必須在編譯時進行選擇。然 而,如果在編譯時建立訪問計劃,變量的值還是未知的,因而無法作爲索引選擇的輸入項。如下面語句將進行全表掃描: select id from t where num=@num 可以改爲強制查詢使用索引: select id from t with(index(索引名)) where num=@num

6)應儘量避免在 where 子句中對字段進行表達式操作,這將導致引擎放棄使用索引而進行全表掃描。如: select id from t where num/2=100 應改爲: select id from t where num=100*2

7)應儘量避免在where子句中對字段進行函數操作,這將導致引擎放棄使用索引而進行全表掃描。如: select id from t where substring(name,1,3)=’abc’–name以abc開頭的id,select id from t where datediff(day,createdate,’2005-11-30′)=0–‘2005-11-30’生成的id 應改爲: select id from t where name like ‘abc%’ select id from t where createdate>=’2005-11-30′ and createdate<’2005-12-1′

8)不要在 where 子句中的“=”左邊進行函數、算術運算或其他表達式運算,否則系統將可能無法正確使用索引

9)很多時候用 exists 代替 in 是一個好的選擇: select num from a where num in(select num from b) 用下面的語句替換: select num from a where exists(select 1 from b where num=a.num)

10)任何地方都不要使用 select * from t ,用具體的字段列表代替*

4、優化分頁查詢

通常是使用limit M,N+合適的order by來實現分頁查詢,這種實現方式在沒有任何索引條件支持的情況下,需要做大量的文件排序操作(file sort),性能將會非常得糟糕。如果有對應的索引,通常剛開始的分頁查詢效率會比較理想,但越往後,分頁查詢的性能就越差

這是因爲在使用limit的時候,偏移量M在分頁越靠後的時候,值就越大,數據庫檢索的數據也就越多。例如limit 10000,10這樣的查詢,數據庫需要查詢10010條記錄,最後返回10條記錄。也就是說將會有10000條記錄被查詢出來沒有被使用到

在這裏插入圖片描述

在這裏插入圖片描述

通過索引覆蓋掃描,使用子查詢的方式來優化分頁查詢:

在這裏插入圖片描述

在這裏插入圖片描述

5、不同count的用法

count()是一個聚合函數,對於返回的結果集,一行行地判斷,如果count函數的參數不是NULL,累計值就加1,否則不加。最後返回累計值

1)對於count(主鍵id)來說,InnoDB引擎會遍歷整張表,把每一行的id值都取出來,返回給Server層。Server層拿到id後,判斷是不可能爲空的,就按行累加

2)對於count(1)來說,InnoDB引擎遍歷整張表,但不取值。Server層對於返回的每一行,放一個數字1進入,判斷是不可能爲空的,按行累加

3)對於count(字段)來說,如果這個字段是定義爲not null的話,一行行地從記錄裏面讀出這個字段,判斷不能爲null,按行累加;如果這個字段定義允許爲null的話,那麼執行的時候,判斷到有可能是null,還要把值取出來在判斷一下,不是null才累加

4)對於count(*)來說,並不會把全部字段取出來,而是專門做了優化。不取值,count(*)肯定不是null,按行累加

按照效率排序count(字段) < count(主鍵id) < count(1) ≈ count(*),所以儘量使用count(*)

八、數據庫分庫分表

1、業務分庫

業務分庫指的是按照業務模塊將數據分散到不同的數據庫服務器。例如,一個簡單的電商網站,包括用戶、商品、訂單三個業務模塊,可以將用戶數據、商品數據、訂單數據分開放到三臺不同的數據庫服務器上,而不是將所有數據都放在一臺數據庫服務器上

在這裏插入圖片描述

業務分庫能夠分散存儲和訪問壓力,但也帶來了新的問題

1)join操作問題

業務分庫後,原本在同一個數據庫中的表分散到不同數據庫中,導致無法使用SQL的join查詢

2)事務問題

原本在同一個數據庫中不同的表可以在同一個事務中修改,業務分庫後,表分散到不同的數據庫中,無法通過事務統一修改

2、分表

單表數據拆分有兩種方式:垂直分表水平分表

在這裏插入圖片描述

垂直分表:表記錄相同但包含不同的列。例如,上圖的垂直拆分,會把表切分成兩個表,一個表包含ID、name、age、sex列,另外一個表包含ID、nickname、description列

水平分表:表的列相同但包含不同的行數據。例如,上圖的水平拆分,兩個表都包含ID、name、age、sex、nickname、description列,但是一個表包含的是ID從1到999999的行數,另一個表包含的是ID從1000000到9999999

單表進行切分後,是否要將切分後的多個表分散在不同的數據庫服務器中,可以根據實際的切分效果來確定,並不強制要求單表切分爲多表後一定要分散到不同數據庫中。原因在於單表切分爲多表後,新的表即使在同一個數據庫服務器中,也可能帶來可觀的性能提升,如果性能能夠滿足業務要求,是可以不拆分到多臺數據庫服務器的

1)、垂直分表

垂直分表適合將表中某些不常用且佔了大量空間的列拆分出去

垂直分表引入的複雜性主要體現在表操作的數量要增加

2)、水平分表

水平分表適合錶行數特別大的表

水平分表相比垂直分表,會引入更多的複雜性,主要表現在下面幾個方面:

1)路由

水平分表後,某條數據具體屬於哪個切分後的子表,需要增加路由算法進行計算,這個算法會引入一定的複雜性

常見的路由算法有:

範圍路由:選取有序的數據列作爲路由的條件,不同分段分散到不同的數據庫表中

範圍路由設計的複雜點主要體現在分段大小的選取上,分段太小會導致切分後字表數量過多,增加維護複雜度;分段太大可能會導致單表依然存在性能問題

範圍路由的優點是可以隨着數據的增加平滑地擴充新的表,例如現在的用戶是100萬,如果增加到1000萬,只需要增加新的標就可以了,原有的數據不需要動

缺點是分佈不均勻,假如按照1000萬來進行分表,有可能某個分段實際存儲的數據量只有100條,另外一個分段實際存儲的數據量有900萬條

Hash路由:選取某個列(或者某幾個列組合)的值進行Hash運算,然後根據Hash結果分散到不同的數據庫表中。以用戶ID爲例,假如一開始就規劃了10個數據庫表,路由算法可以簡單地用user_id%10的值來表示數據所屬的數據庫表編號,ID爲985的用戶放到編號爲5的子表中,ID爲10086的用戶放到編號爲6的子表中

Hash路由的優點是表分佈比較均勻,缺點是擴充新的表很麻煩,所有數據都要重分佈

配置路由:配置路由就是路由表,用一張獨立的表來記錄路由信息。以用戶ID爲例,新增一張user_router表,這個表包含user_id和table_id兩列,根據user_id就可以查詢對應的table_id

配置路由設計簡單,使用起來非常靈活,尤其是在擴充表的時候,只需要遷移指定的數據,然後修改路由表就可以了

配置路由的缺點就是必須多查詢一次,會影響整體性能;而且路由表本身如果太大,性能同樣可能成爲瓶頸

2)join操作

水平分表後,數據分散在多個表中,如果需要與其他表進行join查詢,需要在業務代碼或者數據庫中間件中進行多次join查詢,然後將結果合併

3)count()操作

count()相加:在業務代碼或者數據庫中間件中對每個表進行count()操作,然後將結果相加。這種方式實現簡單,缺點就是性能比較低

記錄數表:新建一張表,假如表名爲記錄數表,包含table_name、row_count兩個字段,每次插入或者刪除子表數據成功後,都更新記錄數表

4)order by操作

水平分表後,數據分散到多個子表中,排序操作無法在數據庫中完成,只能由業務代碼或者數據庫中間件分別查詢每個子表中的數據,然後彙總進行排序

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