1. 程式人生 > 其它 >SimpleDB(核心部分的理解)

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

一個簡單的巢狀迴圈聯接(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的效能:

原來的匹配次數 = 外層錶行數 * 內層錶行數
優化後的匹配次數= 外層表的行數 * 內層表索引的高度

  1. 使用場景:只有內層表join的列有索引時,才能用到Index Nested-LoopJoin進行連線。
  2. 由於用到索引,如果索引是輔助索引而且返回的資料還包括內層表的其他資料,則會回內層表查詢資料,多了一些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也是通過事務提交時將所有頁面寫入磁碟這一步來實現的。