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

相关推荐
不良人天码星5 小时前
redis-zset数据类型的常见指令(sorted set)
数据库·redis·缓存
简色8 小时前
题库批量(文件)导入的全链路优化实践
java·数据库·mysql·mybatis·java-rabbitmq
Lisonseekpan9 小时前
Java Caffeine 高性能缓存库详解与使用案例
java·后端·spring·缓存
lunzi_fly12 小时前
【源码解读之 Mybatis】【核心篇】--第5篇:Executor执行器体系详解
mybatis
沐浴露z16 小时前
分布式场景下防止【缓存击穿】的不同方案
redis·分布式·缓存·redission
Lisonseekpan17 小时前
Spring Boot 中使用 Caffeine 缓存详解与案例
java·spring boot·后端·spring·缓存
韩立学长18 小时前
【开题答辩实录分享】以《走失人口系统档案的设计与实现》为例进行答辩实录分享
mysql·mybatis·springboot
kfepiza21 小时前
Spring的三级缓存原理 笔记251008
笔记·spring·缓存
jun711821 小时前
msi mesi moesi cpu缓存一致性
缓存
Flash Dog1 天前
【MyBatis】——执行过程
java·mybatis