什么是局部性原理?
數據預讀的思路是:磁盤讀寫并不是按需讀取,而是按頁預讀,一次會讀一頁的數據,每次加載更多的數據,以便未來減少磁盤IO
局部性原理:軟件設計要盡量遵循“數據讀取集中”與“使用到一個數據,大概率會使用其附近的數據”,這樣磁盤預讀能充分提高磁盤IO
B樹
B樹,如上圖,它的特點是:
(1)不再是二叉搜索,而是m叉搜索;
(2)葉子節點,非葉子節點,都存儲數據;
(3)中序遍歷,可以獲得所有節點;
非根節點包含的關鍵字個數j滿足,(┌m/2┐)-1 <= j <= m-1,節點分裂時要滿足這個條件
B+樹
(1)非葉子節點不再存儲數據,數據只存儲在同一層的葉子節點上;
(2)葉子之間,增加了鏈表,獲取所有節點,不再需要中序遍歷;
數據庫的索引最常用B+樹:
(1)很適合磁盤存儲,能夠充分利用局部性原理,磁盤預讀。
比如:每個節點是1頁的大小,4k,key占8個字節,j = 512, 根據(┌m/2┐)1 <= j <= m-1, m=j + 1,即513
(2)很低的樹高度,能夠存儲大量數據;
(3)索引本身占用的內存很小;
(4)能夠很好的支持單點查詢,范圍查詢,有序性查詢;
InnoDB的索引
InnoDB的索引有兩類索引,聚集索引(Clustered Index)與普通索引(Secondary Index)。
InnoDB的每一個表都會有聚集索引:
(1)如果表定義了PK,則PK就是聚集索引;
(2)如果表沒有定義PK,則第一個非空unique列是聚集索引;
(3)否則,InnoDB會創建一個隱藏的row-id作為聚集索引;
索引的結構是B+樹,這里不展開B+樹的細節,說幾個結論:
(1)在索引結構中,非葉子節點存儲key,葉子節點存儲value;
(2)聚集索引,葉子節點存儲行記錄(row);
(3)普通索引,葉子節點存儲了PK的值;
InnoDB的普通索引,實際上會掃描兩遍:
第一遍,由普通索引找到PK;
第二遍,由PK找到行記錄;
隔離性
事務ACID特性,其中I代表隔離性(Isolation)
隔離性是指多個并發事務之間要相互隔離。
并發事務之間相互干擾,可能導致事務出現讀臟,不可重復度,幻讀等問題
不隔離可能導致的問題
假設有InnoDB表:
t(id PK, name);
表中有三條記錄:
1, shenjian
2, zhangsan
3, lisi
case 1
事務A,先執行,處于未提交的狀態:
insert into t values(4, wangwu);
事務B,后執行,也未提交:
select * from t;
如果事務B能夠讀取到(4, wangwu)這條記錄,事務A就對事務B產生了影響,這個影響叫做“讀臟”,讀到了未提交事務操作的記錄。
case 2
事務A,先執行:
select * from t where id=1;
結果集為:
1, shenjian
事務B,后執行,并且提交:
update t set name=xxoo where id=1;
commit;
事務A,再次執行相同的查詢:
select * from t where id=1;
結果集為:
1, xxoo
這次是已提交事務B對事務A產生的影響,這個影響叫做“不可重復讀”,一個事務內相同的查詢,得到了不同的結果。
case 3
事務A,先執行:
select * from t where id>3;
結果集為:
NULL
事務B,后執行,并且提交:
insert into t values(4, wangwu);
commit;
事務A,首次查詢了id>3的結果為NULL,于是想插入一條為4的記錄:
insert into t values(4, xxoo);
結果集為:
Error : duplicate key!
事務A的內心OS是:你TM在逗我,查了id>3為空集,insert id=4告訴我PK沖突?
這次是已提交事務B對事務A產生的影響,這個影響叫做“幻讀”。
可以看到,并發的事務可能導致其他事務:
讀臟
不可重復讀
幻讀
事務的隔離級別:
按照SQL92標準,InnoDB實現了四種不同事務的隔離級別:
- 讀未提交(Read Uncommitted)
- 讀提交(Read Committed, RC)
- 可重復讀(Repeated Read, RR)
- 串行化(Serializable)
不同事務的隔離級別,實際上是一致性與并發性的一個權衡與折衷。
隔離級別實現
InnoDB使用不同的鎖策略(Locking Strategy)來實現不同的隔離級別。
(1)讀未提交:select不加鎖,可能出現讀臟;
(2)讀提交(RC):普通select快照讀,鎖select /update /delete 會使用記錄鎖,可能出現不可重復讀;
(3)可重復讀(RR):普通select快照讀,鎖select /update /delete 根據查詢條件情況,會選擇記錄鎖,或者間隙鎖/臨鍵鎖,以防止讀取到幻影記錄;
(4)串行化:select隱式轉化為select ... in share mode,會被update與delete互斥;
InnoDB默認的隔離級別是RR,用得最多的隔離級別是RC
mysql的鎖
默認的事務隔離級別為可重復讀(Repeated Read, RR)
記錄鎖(Record Locks)
記錄鎖,它封鎖索引記錄,例如:
select * from t where id=1 for update;
它會在id=1的索引記錄上加鎖,以阻止其他事務插入,更新,刪除id=1的這一行。
需要說明的是:
select * from t where id=1;
則是快照讀(SnapShot Read),它并不加鎖
間隙鎖(Gap Locks)
間隙鎖,它封鎖索引記錄中的間隔,或者第一條索引記錄之前的范圍,又或者最后一條索引記錄之后的范圍。
依然是上面的例子,InnoDB,RR:
t(id PK, name KEY, sex, flag);
表中有四條記錄:
1, shenjian, m, A
3, zhangsan, m, A
5, lisi, m, A
9, wangwu, f, B
這個SQL語句
select * from t
where id between 8 and 15
for update;
會封鎖區間,以阻止其他事務id=10的記錄插入。
為什么要阻止id=10的記錄插入?
如果能夠插入成功,頭一個事務執行相同的SQL語句,會發現結果集多出了一條記錄,即幻影數據。
間隙鎖的主要目的,就是為了防止其他事務在間隔中插入數據,以導致“不可重復讀”。
如果把事務的隔離級別降級為讀提交(Read Committed, RC),間隙鎖則會自動失效。
臨鍵鎖(Next-Key Locks)
臨鍵鎖,是記錄鎖與間隙鎖的組合,它的封鎖范圍,既包含索引記錄,又包含索引區間。
更具體的,臨鍵鎖會封鎖索引記錄本身,以及索引記錄之前的區間。
如果一個會話占有了索引記錄R的共享/排他鎖,其他會話不能立刻在R之前的區間插入新的索引記錄。
依然是上面的例子,InnoDB,RR:
t(id PK, name KEY, sex, flag);
表中有四條記錄:
1, shenjian, m, A
3, zhangsan, m, A
5, lisi, m, A
9, wangwu, f, B
PK上潛在的臨鍵鎖為:
(-infinity, 1]
(1, 3]
(3, 5]
(5, 9]
(9, +infinity]
臨鍵鎖的主要目的,也是為了避免幻讀(Phantom Read)。如果把事務的隔離級別降級為RC,臨鍵鎖則也會失效。
共享/排它鎖(Shared and Exclusive Locks)
(1)事務拿到某一行記錄的共享S鎖,才可以讀取這一行;
(2)事務拿到某一行記錄的排它X鎖,才可以修改或者刪除這一行;
意向鎖(Intention Locks)
InnoDB支持多粒度鎖(multiple granularity locking),它允許行級鎖與表級鎖共存,實際應用中,InnoDB使用的是意向鎖。
意向鎖是指,未來的某個時刻,事務可能要加共享/排它鎖了,先提前聲明一個意向。
意向鎖有這樣一些特點:
(1)首先,意向鎖,是一個表級別的鎖(table-level locking);
(2)意向鎖分為:
意向共享鎖(intention shared lock, IS),它預示著,事務有意向對表中的某些行加共享S鎖
意向排它鎖(intention exclusive lock, IX),它預示著,事務有意向對表中的某些行加排它X鎖
舉個例子:
select ... lock in share mode,要設置IS鎖;
select ... for update,要設置IX鎖;
(3)意向鎖協議(intention locking protocol)并不復雜:
事務要獲得某些行的S鎖,必須先獲得表的IS鎖
事務要獲得某些行的X鎖,必須先獲得表的IX鎖
(4)由于意向鎖僅僅表明意向,它其實是比較弱的鎖,意向鎖之間并不相互互斥,而是可以并行
插入意向鎖(Insert Intention Locks)
對已有數據行的修改與刪除,必須加強互斥鎖X鎖,那對于數據的插入,是否還需要加這么強的鎖,來實施互斥呢?插入意向鎖,孕育而生。
插入意向鎖,是間隙鎖(Gap Locks)的一種(所以,也是實施在索引上的),它是專門針對insert操作的。
多個事務,在同一個索引,同一個范圍區間插入記錄時,如果插入的位置不沖突,不會阻塞彼此
在MySQL,InnoDB,RR下:
t(id unique PK, name);
數據表中有數據:
10, shenjian
20, zhangsan
30, lisi
事務A先執行,在10與20兩條記錄中插入了一行,還未提交:
insert into t values(11, xxx);
事務B后執行,也在10與20兩條記錄中插入了一行:
insert into t values(12, ooo);
(1)會使用什么鎖?
(2)事務B會不會被阻塞呢?
回答:雖然事務隔離級別是RR,雖然是同一個索引,雖然是同一個區間,但插入的記錄并不沖突,故這里:
使用的是插入意向鎖
并不會阻塞事務B
自增鎖(Auto-inc Locks)
自增鎖是一種特殊的表級別鎖(table-level lock),專門針對事務插入AUTO_INCREMENT類型的列。最簡單的情況,如果一個事務正在往表中插入記錄,所有其他事務的插入必須等待,以便第一個事務插入的行,是連續的主鍵值。
與此同時,InnoDB提供了innodb_autoinc_lock_mode配置,可以調節與改變該鎖的模式與行為。
一,案例說明
MySQL,InnoDB,默認的隔離級別(RR),假設有數據表:
t(id AUTO_INCREMENT, name);
數據表中有數據:
1, shenjian
2, zhangsan
3, lisi
事務A先執行,還未提交:
insert into t(name) values(xxx);
事務B后執行:
insert into t(name) values(ooo);
問:事務B會不會被阻塞?
二,案例分析
InnoDB在RR隔離級別下,能解決幻讀問題,上面這個案例中:
(1)事務A先執行insert,會得到一條(4, xxx)的記錄,由于是自增列,故不用顯示指定id為4,InnoDB會自動增長,注意此時事務并未提交;
(2)事務B后執行insert,假設不會被阻塞,那會得到一條(5, ooo)的記錄;
此時,并未有什么不妥,但如果,
(3)事務A繼續insert:
insert into t(name) values(xxoo);
會得到一條(6, xxoo)的記錄。
(4)事務A再select:
select * from t where id>3;
得到的結果是:
4, xxx
6, xxoo
畫外音:不可能查詢到5的記錄,再RR的隔離級別下,不可能讀取到還未提交事務生成的數據。
這對于事務A來說,就很奇怪了,對于AUTO_INCREMENT的列,連續插入了兩條記錄,一條是4,接下來一條變成了6,就像莫名其妙的幻影。