关于MyBatis的缓存详解

MyBatis 是一个流行的 Java 持久层框架,它提供了对数据库的简单操作和映射。MyBatis 的缓存机制是其核心特性之一,它可以帮助开发者提高应用程序的性能,通过减少对数据库的直接访问次数来降低数据库的负载。

1. MyBatis 缓存介绍

默认缓存行为

  • 局部的 session 缓存:MyBatis 默认开启的缓存是局部的 session 缓存,这意味着每个 MyBatis session 都会有自己的缓存,这个缓存仅在当前 session 内有效。它主要用于处理循环依赖和提升性能。

二级缓存(全局缓存)

  • 开启二级缓存 :要开启 MyBatis 的二级缓存,需要在 SQL 映射文件中添加 <cache/> 标签。这将允许跨多个 session 共享缓存。

缓存的基本属性

  • select 语句缓存:所有 select 语句的结果都会被缓存。
  • 刷新机制:insert, update 和 delete 语句会触发缓存的刷新。
  • LRU 算法:默认使用最近最少使用(Least Recently Used)算法来决定哪些缓存项应该被移除。
  • 无时间刷新:默认情况下,缓存不会根据时间间隔自动刷新。
  • 引用数量:默认情况下,缓存可以存储 1024 个引用。
  • 可读/可写:默认情况下,缓存是可读写的,这意味着缓存的对象可以被调用者修改,而不会干扰其他调用者或线程。

高级缓存配置

  • eviction(回收策略) :可以设置不同的回收策略,如 LRU、FIFO、SOFT 和 WEAK。
    • LRU:最近最少使用,移除最长时间不被使用的对象。
    • FIFO:先进先出,按对象进入缓存的顺序移除。
    • SOFT:软引用,基于垃圾收集器状态和软引用规则移除对象。
    • WEAK:弱引用,更积极地移除对象,基于垃圾收集器状态和弱引用规则。
  • flushInterval(刷新间隔):可以设置一个时间间隔,以毫秒为单位,缓存会在该时间间隔后自动刷新。
  • size(引用数目):可以设置缓存中存储的对象或列表的引用数量,需要根据可用内存资源来决定。
  • readOnly(只读):设置为 true 时,所有调用者将获得缓存对象的相同实例,这些对象不能被修改,提供了性能优势。设置为 false 时,缓存对象可以被修改,但会返回对象的拷贝,这会降低性能。

配置示例

以下是一个配置示例,展示了如何使用 <cache> 标签来自定义缓存行为:

xml 复制代码
<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)算法是一种常见的缓存回收策略,用于决定哪些数据应该被从缓存中移除以腾出空间给新数据。这种策略基于一个简单的理念:如果数据在一段时间内没有被使用,那么它在未来被使用的可能性也相对较低。下面详细介绍LRU算法的实现原理:

数据结构

LRU算法通常使用以下两种数据结构来实现:

  1. 哈希表(Hash Map):用于快速定位缓存项,O(1)时间复杂度。
  2. 双向链表(Doubly Linked List):用于维护缓存项的使用顺序,允许快速添加和删除节点。
工作原理
  1. 缓存访问:当缓存被访问时(无论是读取还是写入),该缓存项会被视为"最近使用"的,并移动到双向链表的头部(最近使用的位置)。
  2. 缓存添加:当新数据被添加到缓存时,如果缓存未满,新数据会被添加到链表头部。如果缓存已满,则链表尾部的数据(最不常用的数据)会被移除,新数据添加到头部。
  3. 缓存淘汰:当缓存达到容量上限时,链表尾部的数据(最长时间未被使用的数据)会被移除,为新数据腾出空间。
具体实现步骤
  1. 初始化:创建一个空的哈希表和一个空的双向链表。
  2. 访问缓存
    • 检查数据是否在哈希表中:
      • 如果在,更新该数据在链表中的位置(移动到头部),并返回数据。
      • 如果不在,从数据源获取数据,添加到链表头部,并在哈希表中创建条目。
  3. 添加数据
    • 如果缓存未满,直接添加数据到链表头部,并在哈希表中创建条目。
    • 如果缓存已满,先从链表尾部移除最不常用的数据,并从哈希表中删除相应条目,然后添加新数据到链表头部。
  4. 维护顺序:每次访问或添加数据时,都需要更新数据在双向链表中的位置,确保最近使用的数据总是在链表头部。
性能考虑
  • 时间复杂度:LRU算法在访问和添加数据时都能保持O(1)的时间复杂度,这得益于哈希表和双向链表的结合使用。
  • 空间复杂度:主要取决于缓存的大小,即存储的数据量。
应用场景

LRU算法广泛应用于操作系统的页面置换算法、Web服务器的图片或资源缓存、数据库查询结果缓存等领域,以提高系统性能和响应速度。

示例代码(伪代码)
java 复制代码
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算法实现原理的详细介绍,包括其数据结构、工作原理、具体实现步骤以及性能和应用场景。

2. FIFO

FIFO(First In, First Out)算法是一种简单的缓存回收策略,它按照数据进入缓存的顺序来决定哪些数据应该被移除。这种策略的核心思想是:最先进入缓存的数据将会是最先被移除的数据。FIFO算法在实现上相对简单,但可能不如LRU(最近最少使用)算法那样高效,特别是在某些访问模式下。以下是FIFO算法的实现原理和详细步骤:

数据结构

FIFO算法通常使用以下数据结构来实现:

  1. 队列(Queue):用于维护缓存项的顺序,确保最先进入的数据最先被移除。
  2. 哈希表(Hash Map):用于快速定位缓存项,提供O(1)时间复杂度的访问。
工作原理
  1. 缓存访问:当缓存被访问时(无论是读取还是写入),该缓存项会被视为"最近使用"的。
  2. 缓存添加
    • 如果缓存未满,新数据会被添加到队列的尾部。
    • 如果缓存已满,队列头部的数据会被移除,新数据添加到队列尾部。
  3. 缓存淘汰:当缓存达到容量上限时,队列头部的数据(最先进入的数据)会被移除,为新数据腾出空间。
具体实现步骤
  1. 初始化:创建一个空的队列和一个空的哈希表。
  2. 访问缓存
    • 检查数据是否在哈希表中:
      • 如果在,返回数据,但不需要移动数据在队列中的位置。
      • 如果不在,从数据源获取数据,添加到队列尾部,并在哈希表中创建条目。
  3. 添加数据
    • 如果缓存未满,直接添加数据到队列尾部,并在哈希表中创建条目。
    • 如果缓存已满,先从队列头部移除最旧的数据,并从哈希表中删除相应条目,然后添加新数据到队列尾部。
  4. 维护顺序:每次添加新数据时,都需要更新队列和哈希表。
性能考虑
  • 时间复杂度:FIFO算法在访问和添加数据时都能保持O(1)的时间复杂度,这得益于哈希表的使用。
  • 空间复杂度:主要取决于缓存的大小,即存储的数据量。
应用场景

FIFO算法由于其简单性,适用于那些对缓存一致性要求不高的场景。它可能不适用于那些频繁访问某些数据的应用程序,因为这些数据可能会被错误地移除。

示例代码(伪代码)
java 复制代码
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算法实现原理的详细介绍,包括其数据结构、工作原理、具体实现步骤以及性能和应用场景。FIFO算法虽然简单,但在某些情况下可能不如LRU算法有效,特别是在数据访问模式不均匀的情况下。

3. SOFT

SOFT(软引用)是一种缓存回收策略,它在 Java 中通过 java.lang.ref.SoftReference 类实现。软引用允许对象在内存不足时被垃圾收集器回收,但只要内存足够,这些对象就可以继续存活。这种策略特别适用于缓存机制,因为它可以在不影响应用程序功能的情况下,动态地释放内存资源。以下是 SOFT 缓存策略的实现原理和详细步骤:

工作原理
  1. 软引用:软引用是一种比强引用(Strong Reference)弱,但比弱引用(Weak Reference)强的引用类型。软引用关联的对象在内存不足时可以被垃圾收集器回收,但只要内存足够,它们就会继续存活。
  2. 垃圾收集器:Java 的垃圾收集器会定期检查内存使用情况,并在内存不足时尝试回收软引用对象。
  3. 缓存管理:使用软引用实现的缓存会在内存不足时自动释放缓存对象,从而为新对象腾出空间。
具体实现步骤
  1. 初始化缓存 :创建一个缓存容器,如 HashMap,用于存储键和软引用对象的映射。
  2. 访问缓存
    • 当访问缓存时,首先检查软引用是否仍然有效(即其关联的对象是否已被回收)。
    • 如果软引用有效,返回其关联的对象。
    • 如果软引用无效,说明对象已被回收,可以重新从数据源获取数据,并创建新的软引用。
  3. 添加数据
    • 当添加新数据到缓存时,使用 SoftReference 包装该对象,并将其存储在缓存容器中。
    • 由于软引用的特性,如果内存不足,这些对象可能会被垃圾收集器回收。
  4. 内存回收:当系统内存不足时,垃圾收集器会尝试回收软引用对象。这使得缓存可以自动调整大小,释放不再需要的内存。
性能考虑
  • 时间复杂度 :访问和添加数据的时间复杂度通常为 O(1),因为 HashMap 提供了快速的键值对查找。
  • 空间复杂度:缓存的大小取决于缓存对象的数量和每个对象的大小,但软引用允许在内存不足时自动回收对象,从而动态调整缓存大小。
应用场景

软引用缓存适用于以下场景:

  • 内存敏感的应用程序:在内存资源有限的设备上,如移动设备或嵌入式系统,软引用缓存可以动态地释放内存。
  • 大对象缓存:对于占用大量内存的对象,如图片或大型文档,软引用缓存可以在内存不足时自动释放这些对象。
  • 可有可无的缓存:在某些情况下,缓存数据的丢失不会对应用程序的功能产生重大影响,软引用缓存是一个很好的选择。
示例代码(Java)
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 存储键和软引用对象的映射。当访问缓存时,首先检查软引用是否有效。如果软引用无效,说明对象已被回收,可以重新从数据源获取数据,并创建新的软引用。

小结

SOFT 缓存策略通过使用软引用来实现缓存对象的自动回收,从而在内存不足时动态地释放内存资源。这种策略特别适用于内存敏感的应用程序,或者那些缓存数据丢失不会对应用程序功能产生重大影响的场景。

4. WEAK

WEAK(弱引用)是一种比软引用(Soft Reference)更弱的引用类型,它允许对象在下一次垃圾收集时被回收,无论内存是否足够。在 Java 中,弱引用是通过 java.lang.ref.WeakReference 类实现的。弱引用通常用于实现缓存,其中对象的生命周期不需要超过引用本身的生命周期。以下是 WEAK 缓存策略的实现原理和详细步骤:

工作原理
  1. 弱引用:弱引用是一种对对象的引用,它不会阻止垃圾收集器回收其引用的对象。这意味着只要没有其他的强引用指向该对象,对象就可以被垃圾收集器回收。
  2. 垃圾收集器:Java 的垃圾收集器会定期执行,当它发现某个对象只被弱引用所引用时,就会回收该对象占用的内存。
  3. 缓存管理:使用弱引用实现的缓存允许对象在不再被使用时被快速回收,即使内存尚未不足。
具体实现步骤
  1. 初始化缓存 :创建一个缓存容器,如 HashMap,用于存储键和弱引用对象的映射。
  2. 访问缓存
    • 当访问缓存时,首先检查弱引用是否仍然有效(即其关联的对象是否已被回收)。
    • 如果弱引用有效,返回其关联的对象。
    • 如果弱引用无效,说明对象已被回收,可以重新从数据源获取数据,并创建新的弱引用。
  3. 添加数据
    • 当添加新数据到缓存时,使用 WeakReference 包装该对象,并将其存储在缓存容器中。
    • 由于弱引用的特性,这些对象可能会在下一次垃圾收集时被回收。
  4. 内存回收:当垃圾收集器执行时,它会检查所有弱引用,并回收那些只被弱引用的对象。
性能考虑
  • 时间复杂度 :访问和添加数据的时间复杂度通常为 O(1),因为 HashMap 提供了快速的键值对查找。
  • 空间复杂度:缓存的大小取决于缓存对象的数量和每个对象的大小,但由于弱引用允许对象在下一次垃圾收集时被回收,因此缓存不会长时间占用大量内存。
应用场景

弱引用缓存适用于以下场景:

  • 内存敏感的应用程序:在内存资源有限的设备上,如移动设备或嵌入式系统,弱引用缓存可以快速释放内存。
  • 临时对象缓存:对于只在特定时间内需要的对象,使用弱引用缓存可以确保这些对象在不再需要时迅速被回收。
  • 可丢弃的缓存:在某些情况下,缓存数据的丢失不会对应用程序的功能产生重大影响,弱引用缓存是一个很好的选择。
示例代码(Java)
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 存储键和弱引用对象的映射。当访问缓存时,首先检查弱引用是否有效。如果弱引用无效,说明对象已被回收,可以重新从数据源获取数据,并创建新的弱引用。

小结

WEAK 缓存策略通过使用弱引用来实现缓存对象的快速回收,这对于内存敏感的应用程序或临时对象的缓存非常有用。这种策略允许应用程序在不牺牲内存的情况下,临时存储和管理数据对象。

最后

MyBatis 的缓存机制非常灵活,可以通过简单的配置来满足不同的性能需求。合理地使用缓存可以显著提高应用程序的性能,尤其是在处理大量数据库查询时。然而,开发者需要注意缓存的一致性和并发问题,特别是在使用可读写缓存时。

相关推荐
门牙咬脆骨38 分钟前
【Redis】redis缓存击穿,缓存雪崩,缓存穿透
数据库·redis·缓存
门牙咬脆骨39 分钟前
【Redis】GEO数据结构
数据库·redis·缓存
WindFutrue1 小时前
使用Mybatis向Mysql中的插入Point类型的数据全方位解析
数据库·mysql·mybatis
AiFlutter2 小时前
Java实现简单的搜索引擎
java·搜索引擎·mybatis
Dlwyz5 小时前
问题: redis-高并发场景下如何保证缓存数据与数据库的最终一致性
数据库·redis·缓存
天天扭码5 小时前
五天SpringCloud计划——DAY1之mybatis-plus的使用
java·spring cloud·mybatis
吴半杯8 小时前
Redis-monitor安装与配置
数据库·redis·缓存
ö Constancy9 小时前
设计LRU缓存
c++·算法·缓存
小王码农记9 小时前
vue中路由缓存
前端·vue.js·缓存·typescript·anti-design-vue
会code的厨子9 小时前
Redis缓存高可用集群
redis·缓存