MyBatis緩存機制深度解析

2025-01-10 11:18 更新

MyBatis 是一個流行的 Java 持久層框架,它提供了對數(shù)據(jù)庫的簡單操作和映射。MyBatis 的緩存機制是其核心特性之一,它可以幫助開發(fā)者提高應用程序的性能,通過減少對數(shù)據(jù)庫的直接訪問次數(shù)來降低數(shù)據(jù)庫的負載。

1. MyBatis 緩存介紹

默認緩存行為

  • 局部的 session 緩存:MyBatis 默認開啟的緩存是局部的 session 緩存,這意味著每個 MyBatis session 都會有自己的緩存,這個緩存僅在當前 session 內(nèi)有效。它主要用于處理循環(huán)依賴和提升性能。

二級緩存(全局緩存)

  • 開啟二級緩存:要開啟 MyBatis 的二級緩存,需要在 SQL 映射文件中添加 <cache/> 標簽。這將允許跨多個 session 共享緩存。

緩存的基本屬性

  • select 語句緩存:所有 select 語句的結果都會被緩存。
  • 刷新機制:insert, update 和 delete 語句會觸發(fā)緩存的刷新。
  • LRU 算法:默認使用最近最少使用(Least Recently Used)算法來決定哪些緩存項應該被移除。
  • 無時間刷新:默認情況下,緩存不會根據(jù)時間間隔自動刷新。
  • 引用數(shù)量:默認情況下,緩存可以存儲 1024 個引用。
  • 可讀/可寫:默認情況下,緩存是可讀寫的,這意味著緩存的對象可以被調(diào)用者修改,而不會干擾其他調(diào)用者或線程。

高級緩存配置

  • eviction(回收策略):可以設置不同的回收策略,如 LRU、FIFO、SOFT 和 WEAK。
    • LRU:最近最少使用,移除最長時間不被使用的對象。
    • FIFO:先進先出,按對象進入緩存的順序移除。
    • SOFT:軟引用,基于垃圾收集器狀態(tài)和軟引用規(guī)則移除對象。
    • WEAK:弱引用,更積極地移除對象,基于垃圾收集器狀態(tài)和弱引用規(guī)則。
  • flushInterval(刷新間隔):可以設置一個時間間隔,以毫秒為單位,緩存會在該時間間隔后自動刷新。
  • size(引用數(shù)目):可以設置緩存中存儲的對象或列表的引用數(shù)量,需要根據(jù)可用內(nèi)存資源來決定。
  • readOnly(只讀):設置為 true 時,所有調(diào)用者將獲得緩存對象的相同實例,這些對象不能被修改,提供了性能優(yōu)勢。設置為 false 時,緩存對象可以被修改,但會返回對象的拷貝,這會降低性能。

配置示例

以下是一個配置示例,展示了如何使用 <cache> 標簽來自定義緩存行為:

<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>

  • eviction="FIFO":使用先進先出的策略來管理緩存。
  • flushInterval="60000":每 60 秒刷新一次緩存。
  • size="512":緩存可以存儲 512 個引用。
  • readOnly="true":緩存對象是只讀的,不能被修改。

2. 四種回收策略的原理分析

1. LRU

LRU(Least Recently Used)算法是一種常見的緩存回收策略,用于決定哪些數(shù)據(jù)應該被從緩存中移除以騰出空間給新數(shù)據(jù)。這種策略基于一個簡單的理念:如果數(shù)據(jù)在一段時間內(nèi)沒有被使用,那么它在未來被使用的可能性也相對較低。下面詳細介紹LRU算法的實現(xiàn)原理:

數(shù)據(jù)結構

LRU算法通常使用以下兩種數(shù)據(jù)結構來實現(xiàn):

  1. 哈希表(Hash Map):用于快速定位緩存項,O(1)時間復雜度。
  2. 雙向鏈表(Doubly Linked List):用于維護緩存項的使用順序,允許快速添加和刪除節(jié)點。

工作原理

  1. 緩存訪問:當緩存被訪問時(無論是讀取還是寫入),該緩存項會被視為“最近使用”的,并移動到雙向鏈表的頭部(最近使用的位置)。
  2. 緩存添加:當新數(shù)據(jù)被添加到緩存時,如果緩存未滿,新數(shù)據(jù)會被添加到鏈表頭部。如果緩存已滿,則鏈表尾部的數(shù)據(jù)(最不常用的數(shù)據(jù))會被移除,新數(shù)據(jù)添加到頭部。
  3. 緩存淘汰:當緩存達到容量上限時,鏈表尾部的數(shù)據(jù)(最長時間未被使用的數(shù)據(jù))會被移除,為新數(shù)據(jù)騰出空間。

具體實現(xiàn)步驟

  1. 初始化:創(chuàng)建一個空的哈希表和一個空的雙向鏈表。
  2. 訪問緩存
    • 檢查數(shù)據(jù)是否在哈希表中:
      • 如果在,更新該數(shù)據(jù)在鏈表中的位置(移動到頭部),并返回數(shù)據(jù)。
      • 如果不在,從數(shù)據(jù)源獲取數(shù)據(jù),添加到鏈表頭部,并在哈希表中創(chuàng)建條目。
  3. 添加數(shù)據(jù)
    • 如果緩存未滿,直接添加數(shù)據(jù)到鏈表頭部,并在哈希表中創(chuàng)建條目。
    • 如果緩存已滿,先從鏈表尾部移除最不常用的數(shù)據(jù),并從哈希表中刪除相應條目,然后添加新數(shù)據(jù)到鏈表頭部。
  4. 維護順序:每次訪問或添加數(shù)據(jù)時,都需要更新數(shù)據(jù)在雙向鏈表中的位置,確保最近使用的數(shù)據(jù)總是在鏈表頭部。

性能考慮

  • 時間復雜度:LRU算法在訪問和添加數(shù)據(jù)時都能保持O(1)的時間復雜度,這得益于哈希表和雙向鏈表的結合使用。
  • 空間復雜度:主要取決于緩存的大小,即存儲的數(shù)據(jù)量。

應用場景

LRU算法廣泛應用于操作系統(tǒng)的頁面置換算法、Web服務器的圖片或資源緩存、數(shù)據(jù)庫查詢結果緩存等領域,以提高系統(tǒng)性能和響應速度。

示例代碼(偽代碼)

class LRUCache {
    HashMap<Integer, Node> map;
    DoublyLinkedList cacheList;
    int capacity;


    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.map = new HashMap<>();
        this.cacheList = new DoublyLinkedList();
    }


    public get(int key) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            cacheList.moveToHead(node); // Move to head to mark as recently used
            return node.value;
        }
        return -1; // Not found
    }


    public put(int key, int value) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            node.value = value;
            cacheList.moveToHead(node);
        } else {
            Node newNode = new Node(key, value);
            map.put(key, newNode);
            cacheList.addHead(newNode);
            if (map.size() > capacity) {
                Node tail = cacheList.removeTail();
                map.remove(tail.key);
            }
        }
    }
}


class Node {
    int key;
    int value;
    Node prev;
    Node next;


    public Node(int key, int value) {
        this.key = key;
        this.value = value;
    }
}


class DoublyLinkedList {
    Node head;
    Node tail;


    public addHead(Node node) {
        // Add node to the head of the list
    }


    public removeTail() {
        // Remove node from the tail of the list and return it
    }


    public moveToHead(Node node) {
        // Move node to the head of the list
    }
}

以上是對LRU算法實現(xiàn)原理的詳細介紹,包括其數(shù)據(jù)結構、工作原理、具體實現(xiàn)步驟以及性能和應用場景。

2. FIFO

FIFO(First In, First Out)算法是一種簡單的緩存回收策略,它按照數(shù)據(jù)進入緩存的順序來決定哪些數(shù)據(jù)應該被移除。這種策略的核心思想是:最先進入緩存的數(shù)據(jù)將會是最先被移除的數(shù)據(jù)。FIFO算法在實現(xiàn)上相對簡單,但可能不如LRU(最近最少使用)算法那樣高效,特別是在某些訪問模式下。以下是FIFO算法的實現(xiàn)原理和詳細步驟:

數(shù)據(jù)結構

FIFO算法通常使用以下數(shù)據(jù)結構來實現(xiàn):

  1. 隊列(Queue):用于維護緩存項的順序,確保最先進入的數(shù)據(jù)最先被移除。
  2. 哈希表(Hash Map):用于快速定位緩存項,提供O(1)時間復雜度的訪問。

工作原理

  1. 緩存訪問:當緩存被訪問時(無論是讀取還是寫入),該緩存項會被視為“最近使用”的。
  2. 緩存添加
    • 如果緩存未滿,新數(shù)據(jù)會被添加到隊列的尾部。
    • 如果緩存已滿,隊列頭部的數(shù)據(jù)會被移除,新數(shù)據(jù)添加到隊列尾部。
  3. 緩存淘汰:當緩存達到容量上限時,隊列頭部的數(shù)據(jù)(最先進入的數(shù)據(jù))會被移除,為新數(shù)據(jù)騰出空間。

具體實現(xiàn)步驟

  1. 初始化:創(chuàng)建一個空的隊列和一個空的哈希表。
  2. 訪問緩存
    • 檢查數(shù)據(jù)是否在哈希表中:
      • 如果在,返回數(shù)據(jù),但不需要移動數(shù)據(jù)在隊列中的位置。
      • 如果不在,從數(shù)據(jù)源獲取數(shù)據(jù),添加到隊列尾部,并在哈希表中創(chuàng)建條目。
  3. 添加數(shù)據(jù)
    • 如果緩存未滿,直接添加數(shù)據(jù)到隊列尾部,并在哈希表中創(chuàng)建條目。
    • 如果緩存已滿,先從隊列頭部移除最舊的數(shù)據(jù),并從哈希表中刪除相應條目,然后添加新數(shù)據(jù)到隊列尾部。
  4. 維護順序:每次添加新數(shù)據(jù)時,都需要更新隊列和哈希表。

性能考慮

  • 時間復雜度:FIFO算法在訪問和添加數(shù)據(jù)時都能保持O(1)的時間復雜度,這得益于哈希表的使用。
  • 空間復雜度:主要取決于緩存的大小,即存儲的數(shù)據(jù)量。

應用場景

FIFO算法由于其簡單性,適用于那些對緩存一致性要求不高的場景。它可能不適用于那些頻繁訪問某些數(shù)據(jù)的應用程序,因為這些數(shù)據(jù)可能會被錯誤地移除。

示例代碼(偽代碼)

class FIFOCache {
    HashMap<Integer, Integer> map;
    LinkedList<Integer> queue;
    int capacity;


    public FIFOCache(int capacity) {
        this.capacity = capacity;
        this.map = new HashMap<>();
        this.queue = new LinkedList<>();
    }


    public get(int key) {
        if (map.containsKey(key)) {
            return map.get(key);
        }
        return -1; // Not found
    }


    public put(int key, int value) {
        if (map.containsKey(key)) {
            // Key already exists, update the value and remove the key from the queue
            queue.remove(map.get(key));
            map.put(key, value);
            queue.addLast(key);
        } else {
            if (map.size() >= capacity) {
                // Cache is full, remove the oldest item
                int oldestKey = queue.removeFirst();
                map.remove(oldestKey);
            }
            // Add new item
            map.put(key, value);
            queue.addLast(key);
        }
    }
}


class LinkedList {
    Node head;
    Node tail;


    public addLast(int value) {
        // Add value to the end of the list
    }


    public removeFirst() {
        // Remove the first element from the list and return it
    }
}


class Node {
    int value;
    Node next;


    public Node(int value) {
        this.value = value;
    }
}

以上是對FIFO算法實現(xiàn)原理的詳細介紹,包括其數(shù)據(jù)結構、工作原理、具體實現(xiàn)步驟以及性能和應用場景。FIFO算法雖然簡單,但在某些情況下可能不如LRU算法有效,特別是在數(shù)據(jù)訪問模式不均勻的情況下。

3. SOFT

SOFT(軟引用)是一種緩存回收策略,它在 Java 中通過 java.lang.ref.SoftReference 類實現(xiàn)。軟引用允許對象在內(nèi)存不足時被垃圾收集器回收,但只要內(nèi)存足夠,這些對象就可以繼續(xù)存活。這種策略特別適用于緩存機制,因為它可以在不影響應用程序功能的情況下,動態(tài)地釋放內(nèi)存資源。以下是 SOFT 緩存策略的實現(xiàn)原理和詳細步驟:

工作原理

  1. 軟引用:軟引用是一種比強引用(Strong Reference)弱,但比弱引用(Weak Reference)強的引用類型。軟引用關聯(lián)的對象在內(nèi)存不足時可以被垃圾收集器回收,但只要內(nèi)存足夠,它們就會繼續(xù)存活。
  2. 垃圾收集器:Java 的垃圾收集器會定期檢查內(nèi)存使用情況,并在內(nèi)存不足時嘗試回收軟引用對象。
  3. 緩存管理:使用軟引用實現(xiàn)的緩存會在內(nèi)存不足時自動釋放緩存對象,從而為新對象騰出空間。

具體實現(xiàn)步驟

  1. 初始化緩存:創(chuàng)建一個緩存容器,如 HashMap,用于存儲鍵和軟引用對象的映射。
  2. 訪問緩存
    • 當訪問緩存時,首先檢查軟引用是否仍然有效(即其關聯(lián)的對象是否已被回收)。
    • 如果軟引用有效,返回其關聯(lián)的對象。
    • 如果軟引用無效,說明對象已被回收,可以重新從數(shù)據(jù)源獲取數(shù)據(jù),并創(chuàng)建新的軟引用。
  3. 添加數(shù)據(jù)
    • 當添加新數(shù)據(jù)到緩存時,使用 SoftReference 包裝該對象,并將其存儲在緩存容器中。
    • 由于軟引用的特性,如果內(nèi)存不足,這些對象可能會被垃圾收集器回收。
  4. 內(nèi)存回收:當系統(tǒng)內(nèi)存不足時,垃圾收集器會嘗試回收軟引用對象。這使得緩存可以自動調(diào)整大小,釋放不再需要的內(nèi)存。

性能考慮

  • 時間復雜度:訪問和添加數(shù)據(jù)的時間復雜度通常為 O(1),因為 HashMap 提供了快速的鍵值對查找。
  • 空間復雜度:緩存的大小取決于緩存對象的數(shù)量和每個對象的大小,但軟引用允許在內(nèi)存不足時自動回收對象,從而動態(tài)調(diào)整緩存大小。

應用場景

軟引用緩存適用于以下場景:

  • 內(nèi)存敏感的應用程序:在內(nèi)存資源有限的設備上,如移動設備或嵌入式系統(tǒng),軟引用緩存可以動態(tài)地釋放內(nèi)存。
  • 大對象緩存:對于占用大量內(nèi)存的對象,如圖片或大型文檔,軟引用緩存可以在內(nèi)存不足時自動釋放這些對象。
  • 可有可無的緩存:在某些情況下,緩存數(shù)據(jù)的丟失不會對應用程序的功能產(chǎn)生重大影響,軟引用緩存是一個很好的選擇。

示例代碼(Java)

import java.lang.ref.SoftReference;
import java.util.HashMap;


public class SoftReferenceCache<K, V> {
    private HashMap<K, SoftReference<V>> cache = new HashMap<>();


    public V get(K key) {
        SoftReference<V> ref = cache.get(key);
        if (ref != null) {
            V value = ref.get();
            if (value != null) {
                return value;
            }
            // SoftReference has been cleared, remove it from the cache
            cache.remove(key);
        }
        return null;
    }


    public void put(K key, V value) {
        cache.put(key, new SoftReference<>(value));
    }
}

在這個示例中,SoftReferenceCache 使用 HashMap 存儲鍵和軟引用對象的映射。當訪問緩存時,首先檢查軟引用是否有效。如果軟引用無效,說明對象已被回收,可以重新從數(shù)據(jù)源獲取數(shù)據(jù),并創(chuàng)建新的軟引用。

小結

SOFT 緩存策略通過使用軟引用來實現(xiàn)緩存對象的自動回收,從而在內(nèi)存不足時動態(tài)地釋放內(nèi)存資源。這種策略特別適用于內(nèi)存敏感的應用程序,或者那些緩存數(shù)據(jù)丟失不會對應用程序功能產(chǎn)生重大影響的場景。

4. WEAK

WEAK(弱引用)是一種比軟引用(Soft Reference)更弱的引用類型,它允許對象在下一次垃圾收集時被回收,無論內(nèi)存是否足夠。在 Java 中,弱引用是通過 java.lang.ref.WeakReference 類實現(xiàn)的。弱引用通常用于實現(xiàn)緩存,其中對象的生命周期不需要超過引用本身的生命周期。以下是 WEAK 緩存策略的實現(xiàn)原理和詳細步驟:

工作原理

  1. 弱引用:弱引用是一種對對象的引用,它不會阻止垃圾收集器回收其引用的對象。這意味著只要沒有其他的強引用指向該對象,對象就可以被垃圾收集器回收。
  2. 垃圾收集器:Java 的垃圾收集器會定期執(zhí)行,當它發(fā)現(xiàn)某個對象只被弱引用所引用時,就會回收該對象占用的內(nèi)存。
  3. 緩存管理:使用弱引用實現(xiàn)的緩存允許對象在不再被使用時被快速回收,即使內(nèi)存尚未不足。

具體實現(xiàn)步驟

  1. 初始化緩存:創(chuàng)建一個緩存容器,如 HashMap,用于存儲鍵和弱引用對象的映射。
  2. 訪問緩存
    • 當訪問緩存時,首先檢查弱引用是否仍然有效(即其關聯(lián)的對象是否已被回收)。
    • 如果弱引用有效,返回其關聯(lián)的對象。
    • 如果弱引用無效,說明對象已被回收,可以重新從數(shù)據(jù)源獲取數(shù)據(jù),并創(chuàng)建新的弱引用。
  3. 添加數(shù)據(jù)
    • 當添加新數(shù)據(jù)到緩存時,使用 WeakReference 包裝該對象,并將其存儲在緩存容器中。
    • 由于弱引用的特性,這些對象可能會在下一次垃圾收集時被回收。
  4. 內(nèi)存回收:當垃圾收集器執(zhí)行時,它會檢查所有弱引用,并回收那些只被弱引用的對象。

性能考慮

  • 時間復雜度:訪問和添加數(shù)據(jù)的時間復雜度通常為 O(1),因為 HashMap 提供了快速的鍵值對查找。
  • 空間復雜度:緩存的大小取決于緩存對象的數(shù)量和每個對象的大小,但由于弱引用允許對象在下一次垃圾收集時被回收,因此緩存不會長時間占用大量內(nèi)存。

應用場景

弱引用緩存適用于以下場景:

  • 內(nèi)存敏感的應用程序:在內(nèi)存資源有限的設備上,如移動設備或嵌入式系統(tǒng),弱引用緩存可以快速釋放內(nèi)存。
  • 臨時對象緩存:對于只在特定時間內(nèi)需要的對象,使用弱引用緩存可以確保這些對象在不再需要時迅速被回收。
  • 可丟棄的緩存:在某些情況下,緩存數(shù)據(jù)的丟失不會對應用程序的功能產(chǎn)生重大影響,弱引用緩存是一個很好的選擇。

示例代碼(Java)

import java.lang.ref.WeakReference;
import java.util.HashMap;


public class WeakReferenceCache<K, V> {
    private HashMap<K, WeakReference<V>> cache = new HashMap<>();


    public V get(K key) {
        WeakReference<V> ref = cache.get(key);
        if (ref != null) {
            V value = ref.get();
            if (value != null) {
                return value;
            }
            // WeakReference has been cleared, remove it from the cache
            cache.remove(key);
        }
        return null;
    }


    public void put(K key, V value) {
        cache.put(key, new WeakReference<>(value));
    }
}

在這個示例中,WeakReferenceCache 使用 HashMap 存儲鍵和弱引用對象的映射。當訪問緩存時,首先檢查弱引用是否有效。如果弱引用無效,說明對象已被回收,可以重新從數(shù)據(jù)源獲取數(shù)據(jù),并創(chuàng)建新的弱引用。

小結

WEAK 緩存策略通過使用弱引用來實現(xiàn)緩存對象的快速回收,這對于內(nèi)存敏感的應用程序或臨時對象的緩存非常有用。這種策略允許應用程序在不犧牲內(nèi)存的情況下,臨時存儲和管理數(shù)據(jù)對象。

最后

MyBatis 的緩存機制非常靈活,可以通過簡單的配置來滿足不同的性能需求。合理地使用緩存可以顯著提高應用程序的性能,尤其是在處理大量數(shù)據(jù)庫查詢時。然而,開發(fā)者需要注意緩存的一致性和并發(fā)問題,特別是在使用可讀寫緩存時。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號