SimpleDB(核心部分的理解)
SimpleDB(核心部分的理解)
Join演算法的優化
MySQL使用巢狀迴圈演算法來實現多表之間的聯接。MySQL的一個連線查詢涉及到兩張表的關聯,既然涉及到兩張表的關聯那麼就需要一定的演算法來組織資料。關聯的兩張表,一個叫做驅動表,一個叫做被驅動表。
現在有了兩張表,總要有一張表先被查詢,然後以此結果再去關聯另一張表。這個第一張表就被稱為驅動表,第二張表就稱為被驅動表,一般我們都要以查詢結果集相對小的那張表作為驅動表。
https://blog.csdn.net/huangzhilin2015/article/details/115632839
Nested-Loop Join Algorithms
在Mysql的實現中,Nested-Loop Join有3種實現的演算法:
- Simple Nested-Loop Join:SNLJ,簡單巢狀迴圈連線
- Index Nested-Loop Join:INLJ,索引巢狀迴圈連線
- Block Nested-Loop Join:BNLJ,快取塊巢狀迴圈連線
https://blog.csdn.net/u010841296/article/details/89790399
Simple Nested-Loop join
一個簡單的巢狀迴圈聯接(NLJ)演算法,迴圈從第一個表中依次讀取行,取到每行再到聯接的下一個表中迴圈匹配。這個過程會重複多次直到剩餘的表都被聯接了。假設表t1、t2、t3用下面的聯接型別進行聯接:
Table Join Type
t1 range
t2 ref
t3 ALL
巢狀迴圈演算法的聯接過程是這樣的:
for each row in t1 matching range {
for each row in t2 matching reference key {
for each row in t3 {
if row satisfies join conditions,
send to client
}
}
}
在專案CS183-proj3中,最簡單的join演算法就是採用這種方式實現的:
private TupleIterator nestedLoopJoin() throws DbException, TransactionAbortedException {
LinkedList<Tuple> tuples = new LinkedList<>();
int length1 = child1.getTupleDesc().numFields();
child1.rewind();
while (child1.hasNext()) {
Tuple left = child1.next();
child2.rewind();
while (child2.hasNext()) {
Tuple right = child2.next();
Tuple result;
if (joinPredicate.filter(left, right)) {//如果符合條件就合併來自兩個表的tuple作為一條結果
result = mergeTuples(length1, left, right);
tuples.add(result);
}
}
}
return new TupleIterator(getTupleDesc(), tuples);
}
Index Nested-Loop Join(減少內層表資料的匹配次數)
索引巢狀迴圈連線是基於索引進行連線的演算法,索引是基於內層表的,通過外層表匹配條件直接與內層表索引進行匹配,避免和內層表的每條記錄進行比較, 從而利用索引的查詢減少了對內層表的匹配次數,優勢極大的提升了 join的效能:
原來的匹配次數 = 外層錶行數 * 內層錶行數
優化後的匹配次數= 外層表的行數 * 內層表索引的高度
- 使用場景:只有內層表join的列有索引時,才能用到Index Nested-LoopJoin進行連線。
- 由於用到索引,如果索引是輔助索引而且返回的資料還包括內層表的其他資料,則會回內層表查詢資料,多了一些IO操作。
我們的DBMS沒有實現索引,因此這種方法並不適用。
Block Nested-Loop Join(減少內層表資料的迴圈次數)
快取塊巢狀迴圈連線通過一次性快取多條資料,把參與查詢的列快取到Join Buffer 裡,然後拿join buffer裡的資料批量與內層表的資料進行匹配,從而減少了內層迴圈的次數。一個塊巢狀迴圈聯接(BNL)演算法,將外迴圈的行快取起來,讀取快取中的行,減少內迴圈的表被掃描的次數。例如,如果10行讀入緩衝區並且緩衝區傳遞給下一個內迴圈,在內迴圈讀到的每行可以和緩衝區的10行做比較。這樣使內迴圈表被掃描的次數減少了一個數量級。
for each row in t1 matching range {
for each row in t2 matching reference key {
store used columns from t1, t2 in join buffer
if buffer is full {
for each row in t3 {
for each t1, t2 combination in join buffer {
if row satisfies join conditions,
send to client
}
}
empty buffer
}
}
}
if buffer is not empty {
for each row in t3 {
for each t1, t2 combination in join buffer {
if row satisfies join conditions,
send to client
}
}
}
在CS186-proj2中,實現了以上演算法:
private TupleIterator blockNestedLoopJoin() throws DbException, TransactionAbortedException {
LinkedList<Tuple> tuples = new LinkedList<>();
int blockSize = blockMemory / child1.getTupleDesc().getSize();//131072是MySql中該演算法的預設緩衝區大小
Tuple[] cacheBlock = new Tuple[blockSize];
int index = 0;
int length1 = child1.getTupleDesc().numFields();
child1.rewind();
while (child1.hasNext()) {
Tuple left = child1.next();
cacheBlock[index++] = left;
if (index >= cacheBlock.length) {//如果緩衝區滿了,就先處理快取中的tuple
child2.rewind();
while (child2.hasNext()) {
Tuple right = child2.next();
for (Tuple cacheLeft : cacheBlock) {
if (joinPredicate.filter(cacheLeft, right)) {//如果符合條件就合併來自兩個表的tuple作為一條結果
Tuple result = mergeTuples(length1, cacheLeft, right);
tuples.add(result);
}
}
}
Arrays.fill(cacheBlock, null);//清空,給下一次使用
index = 0;
}
}
if (index > 0 && index < cacheBlock.length) {//處理緩衝區中剩下的tuple
child2.rewind();
while (child2.hasNext()) {
Tuple right = child2.next();
for (Tuple cacheLeft : cacheBlock) {
//如果符合條件就合併來自兩個表的tuple作為一條結果,加上為null的判斷是因為此時cache沒有滿,最後有null值
if (cacheLeft == null) break;
if (joinPredicate.filter(cacheLeft, right)) {
Tuple result = mergeTuples(length1, cacheLeft, right);
tuples.add(result);
}
}
}
}
return new TupleIterator(getTupleDesc(), tuples);
}
Hash Join
Hash Join是做大資料集連線時的常用方式,優化器使用兩個表中較小(相對較小)的表利用Join Key在記憶體中建立散列表,然後掃描較大的表並探測散列表,找出與Hash表匹配的行。
這種方式適用於較小的表完全可以放於記憶體中的情況,這樣總成本就是訪問兩個表的成本之和。但是在表很大的情況下並不能完全放入記憶體,這時優化器會將它分割成若干不同的分割槽,不能放入記憶體的部分就把該分割槽寫入磁碟的臨時段,此時要求有較大的臨時段從而儘量提高I/O 的效能。它能夠很好的工作於沒有索引的大表和並行查詢的環境中,並提供最好的效能。
Sort Merge Join
Sort Merge Join 又叫 Merge Join,簡單說來就是將 Join 的兩個表,首先根據連線屬性進行排序,然後進行一次掃描歸併, 進而就可以得出最後的結果。從該演算法的特性可以看出,該演算法最大的消耗在於對內外表資料進行排序,而當連線列為索引列時,我們可以利用索引的有序性避免排序帶來的消耗。
在專案CS186-proj3同樣有這個演算法的實現,首先是模仿Block Nested-Loop Join,對兩個表都進行快取,從而減少兩個表的掃描次數:
private TupleIterator dbnlSortedJoin() throws DbException, TransactionAbortedException {
LinkedList<Tuple> tuples = new LinkedList<>();
int blockSize1 = blockMemory / child1.getTupleDesc().getSize();
int blockSize2 = blockMemory / child2.getTupleDesc().getSize();
Tuple[] leftCacheBlock = new Tuple[blockSize1];
Tuple[] rightCacheBlock = new Tuple[blockSize2];
int index1 = 0;
int index2 = 0;
int length1 = child1.getTupleDesc().numFields();
child1.rewind();
while (child1.hasNext()) {
Tuple left = child1.next();
leftCacheBlock[index1++] = left;//將左表的tuple放入左快取區
if (index1 >= leftCacheBlock.length) {//如果左表緩衝區滿了,就先處理快取中的tuple
//以下為使用另一個數組作為快取將右表分塊處理的過程,就是快取滿了就join一次,最後處理在快取中剩下的
//即下面的for裡面的並列的while和if塊是對右表的處理過程,可以看作一起來理解
child2.rewind();//每處理完一整個左快取區的tuples才rewind一次右表
while (child2.hasNext()) {
Tuple right = child2.next();
rightCacheBlock[index2++] = right;//將右表的tuple放入快取
if (index2 >= rightCacheBlock.length) {//如果右緩衝區滿了,就先處理快取中的tuple
sortedJoin(tuples, leftCacheBlock, rightCacheBlock, length1);
Arrays.fill(rightCacheBlock, null);//清空右快取區,以供繼續遍歷右表
index2 = 0;
}
}
if (index2 > 0 && index2 < rightCacheBlock.length) {//處理緩衝區中剩下的tuple
sortedJoin(tuples, leftCacheBlock, rightCacheBlock, length1);
Arrays.fill(rightCacheBlock, null);//清空右快取區,以供下一個左快取區對右快取區的遍歷
index2 = 0;
}
Arrays.fill(leftCacheBlock, null);//清空左快取區,以供繼續遍歷左表
index1 = 0;
}
}
if (index1 > 0 && index1 < leftCacheBlock.length) {//處理緩衝區中剩下的tuple
child2.rewind();
while (child2.hasNext()) {
Tuple right = child2.next();
rightCacheBlock[index2++] = right;//將右表的tuple放入快取
if (index2 >= rightCacheBlock.length) {//如果緩衝區滿了,就先處理快取中的tuple
sortedJoin(tuples, leftCacheBlock, rightCacheBlock, length1);
Arrays.fill(rightCacheBlock, null);//清空右快取區,以供繼續遍歷右表
index2 = 0;
}
}
if (index2 > 0 && index2 < rightCacheBlock.length) {//處理緩衝區中剩下的tuple
sortedJoin(tuples, leftCacheBlock, rightCacheBlock, length1);
}
}
return new TupleIterator(getTupleDesc(), tuples);
}
接下來就是比較,還有進行merge sort
:
private void sortedJoin(LinkedList<Tuple> tuples, Tuple[] lcb, Tuple[] rcb, int length1) {
// 去掉兩個Block的null值
int m = lcb.length - 1;
int n = rcb.length - 1;
for (; m > 0 && lcb[m] == null; m--) ;
for (; n > 0 && rcb[n] == null; n--) ;
Tuple[] leftCacheBlock = new Tuple[m + 1];
Tuple[] rightCacheBlock = new Tuple[n + 1];
System.arraycopy(lcb, 0, leftCacheBlock, 0, leftCacheBlock.length);
System.arraycopy(rcb, 0, rightCacheBlock, 0, rightCacheBlock.length);
int index1 = joinPredicate.getIndex1();
int index2 = joinPredicate.getIndex2();
//這幾個predicate用於每個陣列內部的tuple比較,故每個都要需要使用各自的index
JoinPredicate eqPredicate1 = new JoinPredicate(index1, Predicate.Op.EQUALS, index1);
JoinPredicate eqPredicate2 = new JoinPredicate(index2, Predicate.Op.EQUALS, index2);
JoinPredicate ltPredicate1 = new JoinPredicate(index1, Predicate.Op.LESS_THAN, index1);
JoinPredicate ltPredicate2 = new JoinPredicate(index2, Predicate.Op.LESS_THAN, index2);
JoinPredicate gtPredicate1 = new JoinPredicate(index1, Predicate.Op.GREATER_THAN, index1);
JoinPredicate gtPredicate2 = new JoinPredicate(index2, Predicate.Op.GREATER_THAN, index2);
//先對兩個快取區排序
Comparator<Tuple> comparator1 = new Comparator<Tuple>() {
@Override
public int compare(Tuple o1, Tuple o2) {
if (ltPredicate1.filter(o1, o2)) {
return -1;
} else if (gtPredicate1.filter(o1, o2)) {
return 1;
} else return 0;
}
};
Comparator<Tuple> comparator2 = new Comparator<Tuple>() {
@Override
public int compare(Tuple o1, Tuple o2) {
if (ltPredicate2.filter(o1, o2)) {
return -1;
} else if (gtPredicate2.filter(o1, o2)) {
return 1;
} else return 0;
}
};
Arrays.sort(leftCacheBlock, comparator1);
Arrays.sort(rightCacheBlock, comparator2);
//兩個陣列的當前訪問到的索引位置
int pos1, pos2;
switch (joinPredicate.getOperator()) {
case EQUALS:
pos1 = pos2 = 0;
while (pos1 < leftCacheBlock.length && pos2 < rightCacheBlock.length) {
Tuple left = leftCacheBlock[pos1];
Tuple right = rightCacheBlock[pos2];
//這2個Predicate用於來自兩個陣列的tuple比較,故使用index1和index2
JoinPredicate eqPredicate = new JoinPredicate(index1, Predicate.Op.EQUALS, index2);
JoinPredicate ltPredicate = new JoinPredicate(index1, Predicate.Op.LESS_THAN, index2);
if (eqPredicate.filter(left, right)) {
int begin1, end1, begin2, end2;
begin1 = pos1;
begin2 = pos2;
for (; pos1 < leftCacheBlock.length && eqPredicate1.filter(left, leftCacheBlock[pos1]); pos1++)
;
for (; pos2 < rightCacheBlock.length && eqPredicate2.filter(right, rightCacheBlock[pos2]); pos2++)
;
end1 = pos1;
end2 = pos2;
for (int i = begin1; i < end1; i++) {
for (int j = begin2; j < end2; j++) {
tuples.add(mergeTuples(length1, leftCacheBlock[i], rightCacheBlock[j]));
}
}
} else if (ltPredicate.filter(left, right)) {
pos1++;
} else {
pos2++;
}
}
break;
case LESS_THAN:
case LESS_THAN_OR_EQ:
pos1 = 0;
while (pos1 < leftCacheBlock.length) {
Tuple left = leftCacheBlock[pos1];
pos2 = rightCacheBlock.length - 1;
while (pos2 > 0) {
Tuple right = rightCacheBlock[pos2];
pos2--;
if (joinPredicate.filter(left, right)) {
Tuple result = mergeTuples(length1, left, right);
tuples.add(result);
} else break;
}
pos1++;
}
case GREATER_THAN:
case GREATER_THAN_OR_EQ:
pos1 = 0;
while (pos1 < leftCacheBlock.length) {
Tuple left = leftCacheBlock[pos1];
pos2 = 0;
while (pos2 < rightCacheBlock.length) {
Tuple right = rightCacheBlock[pos2];
pos2++;
if (joinPredicate.filter(left, right)) {
Tuple result = mergeTuples(length1, left, right);
tuples.add(result);
} else break;
}
pos1++;
}
break;
default:
throw new RuntimeException("JoinPredicate is Illegal");
}
}
幾種Join演算法的比較:
join 演算法 | Nested-Loop Join | Merge Join | Hash Join |
---|---|---|---|
使用條件 | 任何條件 | 等值或非等值連線(>,<,=,>=,<=),‘<>’除外 | 等值連線(=) |
相關資源 | CPU、磁碟I/O | 記憶體、臨時空間 | 記憶體、臨時空間 |
特點 | 當有高選擇性索引或進行限制性搜尋時效率比較高,能夠快速返回第一次的搜尋結果 | 當缺乏索引或者索引條件模糊時,Merge Join 比 Nested Loop 有效。非等值連線時,Merge Join比 Hash Join 更有效 | 當缺乏索引或者索引條件模糊時,Hash Join 比 Nested Loop 有效。通常比 Merge Join 快。在資料倉庫環境下,如果表的紀錄數多,效率高 |
缺點 | 當索引丟失或者查詢條件限制不夠時,效率很低;當表的紀錄數多時,效率低 | 所有的表都需要排序。它為最優化的吞吐量而設計,並且在結果沒有全部找到前不返回資料 | 建立雜湊表需要大量記憶體,第一次的結果返回較慢 |
參考資料
http://www.yixinhome.top/2020/03/31/Nested_Loop-Hash_Join-Merge_Join/
https://www.cnblogs.com/xiaohuizhenyoucai/p/10983783.html
http://blog.csdn.net/ghsau/article/details/43762027
https://blog.csdn.net/u010841296/article/details/89790399
查詢構建以及查詢優化
關於left-deep-tree的構建這一塊我還是沒有搞懂,不清楚最後是怎麼構建起來一棵樹的,但是在動態規劃那一部分我看懂了。這也是查詢優化的核心,就是利用動態規劃的思想構建出一個最優查詢計劃來。
這裡是網上一篇部落格中對動態規劃用於Join優化的一個理解:
4個表的join問題中,最底層的問題是單個表的訪問問題,比如T1的訪問可以有多種選擇,全表掃描,Range掃描,index掃描,單鍵值抽取(unique access)等等。先找到T1訪問的最佳方案,這個比較簡單,順序比較各種access方案即可。同理,找到T2, T3, T4各自的最佳訪問方法。記錄下這些最佳子方案,然後向上一層,找兩個表join的最佳方案,比如T1-T2兩個表join本來有很多種可選方案:T1-index-scan join T2-index-scan;T1-table-scan join T2-table-scan;T2-index-scan join T1-table-scan。。。可是,採用Dynamic Programming,T1和T2最優的訪問方法已經找到,所以在第二層,只需要考慮4種方案:採用nest loop join,T1 join T2還是T2 join T1;然後考慮採用merge join,T1 join T2還是T2 join T1;通過比較它們的cost,找到這4個方案的最佳方案。
用這種方法找到所有兩個表join的最佳方案,儲存下來。再進入第三層,如此重複一直到最頂層。duang的一下,找到了最優解。
這就是筆者理解中的Dynamic Programming。
而JoinOptimizer中最主要的方法,orderJoin()
就是實現了這個過程,而且一些實現起來比較麻煩的函式已經實現好了:
/**
* Compute a logical, reasonably efficient join on the specified tables. See
* PS4 for hints on how this should be implemented.
*
* @param stats Statistics for each table involved in the join, referenced by
* base table names, not alias
* @param filterSelectivities Selectivities of the filter predicates on each table in the
* join, referenced by table alias (if no alias, the base table
* name)
* @param explain Indicates whether your code should explain its query plan or
* simply execute it
* @return A Vector<LogicalJoinNode> that stores joins in the left-deep
* order in which they should be executed.
* @throws ParsingException when stats or filter selectivities is missing a table in the
* join, or or when another internal error occurs
*/
// TODO: 17-9-4 figure why problems occur when Qurey is "select * from ...."
public Vector<LogicalJoinNode> orderJoins(
HashMap<String, TableStats> stats,
HashMap<String, Double> filterSelectivities, boolean explain)
throws ParsingException {
// some code goes here
//Replace the following
// 1. j = set of join nodes
// 2. for (i in 1...|j|): // First find best plan for single join, then for two joins, etc.
// 3. for s in {all length i subsets of j} // Looking at a concrete subset of joins
// 4. bestPlan = {} // We want to find the best plan for this concrete subset
// 5. for s' in {all length i-1 subsets of s}
// 6. subplan = optjoin(s') // Look-up in the cache the best query plan for s but with one relation missing
// 7. plan = best way to join (s-s') to subplan // Now find the best plan to extend s' by one join to get s
// 8. if (cost(plan) < cost(bestPlan))
// 9. bestPlan = plan // Update the best plan for computing s
// 10. optjoin(s) = bestPlan
// 11. return optjoin(j)
int numJoinNodes = joins.size();
PlanCache pc = new PlanCache();
Set<LogicalJoinNode> wholeSet = null;
for (int i = 1; i <= numJoinNodes; i++) {
Set<Set<LogicalJoinNode>> setOfSubset = this.enumerateSubsets(this.joins, i);
for (Set<LogicalJoinNode> s : setOfSubset) {
if (s.size() == numJoinNodes) {
wholeSet = s;//將join節點的全集儲存下來最後用
}
Double bestCostSofar = Double.MAX_VALUE;
CostCard bestPlan = new CostCard();
for (LogicalJoinNode toRemove : s) {
CostCard plan = computeCostAndCardOfSubplan(stats, filterSelectivities, toRemove, s, bestCostSofar, pc);
if (plan != null) {
bestCostSofar = plan.cost;
bestPlan = plan;
}
}
if (bestPlan.plan != null) {
pc.addPlan(s, bestPlan.cost, bestPlan.card, bestPlan.plan);
}
}
}
return pc.getOrder(wholeSet);
}
好好看一看虛擬碼和實際程式碼,確實是用了動態規劃的思想來解決這個問題的。
事務實現
2PL(兩階段鎖協議)
在 InnoDB 事務中,行鎖是在需要的時候才加上的,但並不是不需要了就立刻釋放,需要等事務結束時才釋放,這就是兩階段鎖協議,分為加鎖階段和解鎖階段,所有的 unlock 操作都在lock操作之後。
兩階段鎖協議,整個事務分為兩個階段,前一個階段為加鎖,後一個階段為解鎖。在加鎖階段,事務只能加鎖,也可以操作資料,但不能解鎖,直到事務釋放第一個鎖,就進入解鎖階段,此過程中事務只能解鎖,也可以操作資料,不能再加鎖。兩階段鎖協議使得事務具有較高的併發度,因為解鎖不必發生在事務結尾。它的不足是沒有解決死鎖的問題,因為它在加鎖階段沒有順序要求。如兩個事務分別申請了A, B鎖,接著又申請對方的鎖,此時進入死鎖狀態。
資料庫併發控制的兩種機制:
https://www.cnblogs.com/zszmhd/p/3365220.html
Mysql行鎖:
https://www.huaweicloud.com/articles/d49a4e7f54e8980c9b0136501e1f6378.html
在Transaction實現這個專案中,2PL好像體現得並不明顯,加鎖和解鎖確實是分為兩階段的,但是解鎖是在事務結束之後統一釋放的,並沒有在事務結束之前釋放。這個應該也算滿足了兩階段鎖協議吧。
事務ACID實現
再看看事務的ACID是如何實現的,只是說一下自己的理解:
事務的原子性,即事務要麼成功提交要麼全部失敗回滾,這主要是在transactionComplete()
這個函式中體現的,因為所有的頁面都是被當前事務鎖定了,在完成事務或者中斷事務的時候會呼叫這個transactionComplete()
函式,用一個commit來指示其是否成功,首先需要釋放和這個transaction id
相關的所有頁面的鎖,然後如果commit
是true,意味著事務成功提交,這時候用flushPage()
來實現將所有頁面寫入磁碟,否則的話,就用revertTransaction()
來回滾頁面,這個函式用了lruPagesPool.reCachePage()
這個函式,把所有的頁面從磁碟重新讀取回記憶體,這樣所有頁面就是未修改之前的樣子,這就是"原子性"的體現。
事務的隔離性,主要體現在讀寫鎖的實現上,有以下規則:
共享鎖和互斥鎖有以下特性:
- 在事務可以讀取物件之前,它必須具有共享鎖。
- 在事務可以寫入物件之前,它必須具有互斥鎖。
- 多個事務可以在一個物件上擁有一個共享鎖。
- 只有一個事務可以在物件上具有互斥鎖。
- 如果事務t是唯一持有物件o共享鎖的事務,則t可以將其對o的鎖升級為互斥鎖。
如果某個事務請求了不應授予的鎖,則您的程式碼應阻塞,等待該鎖可用。
我們在grantSLock()
和grantXLoc()
兩個鎖的實現中,就充分考慮了以上原則,保證一個頁面在被多個事務請求的時候,不會出現讀寫的衝突,不會出現“丟失修改”,“未提交讀”等情況。
事務的永續性,也就是資料需要儲存到磁碟上,這個在transactionComplete()
這個函式中已經體現了,就是事務成功提交後,會把所有被這個事務鎖定的頁面都寫入磁碟中,這就是“永續性”的體現。
事務的隔離性,即一個事務所做的修改在最終提交前,對其他事務不可見,這也是由讀寫鎖的特性來實現的,即一個頁面被互斥鎖鎖定之後,不能被其他事務的共享鎖鎖定了,也就是防止未提交的修改被讀取了,這就是隔離性的體現。
實施NO STEAL / FORCE緩衝區管理策略。需要做:
- 如果髒頁被未提交的事務鎖定,則不應從緩衝池中清除髒頁(這稱為NO STEAL)。
- 在事務提交時,應將髒頁強行寫入磁碟(這稱為FORCE)。
NO STEAL是這樣實現的,在釋放一個頁面時,需要做以下判斷:
public synchronized void releasePage(TransactionId tid, PageId pid) {
// some code goes here
// not necessary for proj1
if (!lockManager.unlock(tid, pid)) {
//pid does not locked by any transaction
//or tid dose not lock the page pid
throw new IllegalArgumentException();
}
}
而在unlock()
中,主要就是移除這個頁面上的鎖:
public synchronized boolean unlock(TransactionId tid, PageId pid) {
ArrayList<LockState> list = (ArrayList<LockState>) lockStateMap.get(pid);
if (list == null || list.size() == 0) return false;
LockState ls = getLockState(tid, pid);
if (ls == null) return false;
list.remove(ls);
lockStateMap.put(pid, list);
return true;
}
如果移除鎖成功了,那這個頁面就可以從快取中清除了,但這個清除也不是主動去做的,是通過快取區的LRU策略進行的。這一步就沒有體現出來了,只有這個頁面到達連結串列末尾之後自動被清除了。
我們保證的只是,事務提交之後,所有頁面上的鎖都被解除了,我們用unlock()
不會出錯,於是我們把lockState
中的狀態移除,然後頁面就讓它通過LRU策略清除即可。
FORCE
也是通過事務提交時將所有頁面寫入磁碟這一步來實現的。