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>
标签来自定义缓存行为:
<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算法通常使用以下两种数据结构来实现:
- 哈希表(Hash Map):用于快速定位缓存项,O(1)时间复杂度。
- 双向链表(Doubly Linked List):用于维护缓存项的使用顺序,允许快速添加和删除节点。
工作原理
- 缓存访问:当缓存被访问时(无论是读取还是写入),该缓存项会被视为“最近使用”的,并移动到双向链表的头部(最近使用的位置)。
- 缓存添加:当新数据被添加到缓存时,如果缓存未满,新数据会被添加到链表头部。如果缓存已满,则链表尾部的数据(最不常用的数据)会被移除,新数据添加到头部。
- 缓存淘汰:当缓存达到容量上限时,链表尾部的数据(最长时间未被使用的数据)会被移除,为新数据腾出空间。
具体实现步骤
- 初始化:创建一个空的哈希表和一个空的双向链表。
- 访问缓存:
- 检查数据是否在哈希表中:
- 如果在,更新该数据在链表中的位置(移动到头部),并返回数据。
- 如果不在,从数据源获取数据,添加到链表头部,并在哈希表中创建条目。
- 检查数据是否在哈希表中:
- 添加数据:
- 如果缓存未满,直接添加数据到链表头部,并在哈希表中创建条目。
- 如果缓存已满,先从链表尾部移除最不常用的数据,并从哈希表中删除相应条目,然后添加新数据到链表头部。
- 维护顺序:每次访问或添加数据时,都需要更新数据在双向链表中的位置,确保最近使用的数据总是在链表头部。
性能考虑
- 时间复杂度:LRU算法在访问和添加数据时都能保持O(1)的时间复杂度,这得益于哈希表和双向链表的结合使用。
- 空间复杂度:主要取决于缓存的大小,即存储的数据量。
应用场景
LRU算法广泛应用于操作系统的页面置换算法、Web服务器的图片或资源缓存、数据库查询结果缓存等领域,以提高系统性能和响应速度。
示例代码(伪代码)
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算法通常使用以下数据结构来实现:
- 队列(Queue):用于维护缓存项的顺序,确保最先进入的数据最先被移除。
- 哈希表(Hash Map):用于快速定位缓存项,提供O(1)时间复杂度的访问。
工作原理
- 缓存访问:当缓存被访问时(无论是读取还是写入),该缓存项会被视为“最近使用”的。
- 缓存添加:
- 如果缓存未满,新数据会被添加到队列的尾部。
- 如果缓存已满,队列头部的数据会被移除,新数据添加到队列尾部。
- 缓存淘汰:当缓存达到容量上限时,队列头部的数据(最先进入的数据)会被移除,为新数据腾出空间。
具体实现步骤
- 初始化:创建一个空的队列和一个空的哈希表。
- 访问缓存:
- 检查数据是否在哈希表中:
- 如果在,返回数据,但不需要移动数据在队列中的位置。
- 如果不在,从数据源获取数据,添加到队列尾部,并在哈希表中创建条目。
- 检查数据是否在哈希表中:
- 添加数据:
- 如果缓存未满,直接添加数据到队列尾部,并在哈希表中创建条目。
- 如果缓存已满,先从队列头部移除最旧的数据,并从哈希表中删除相应条目,然后添加新数据到队列尾部。
- 维护顺序:每次添加新数据时,都需要更新队列和哈希表。
性能考虑
- 时间复杂度:FIFO算法在访问和添加数据时都能保持O(1)的时间复杂度,这得益于哈希表的使用。
- 空间复杂度:主要取决于缓存的大小,即存储的数据量。
应用场景
FIFO算法由于其简单性,适用于那些对缓存一致性要求不高的场景。它可能不适用于那些频繁访问某些数据的应用程序,因为这些数据可能会被错误地移除。
示例代码(伪代码)
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 缓存策略的实现原理和详细步骤:
工作原理
- 软引用:软引用是一种比强引用(Strong Reference)弱,但比弱引用(Weak Reference)强的引用类型。软引用关联的对象在内存不足时可以被垃圾收集器回收,但只要内存足够,它们就会继续存活。
- 垃圾收集器:Java 的垃圾收集器会定期检查内存使用情况,并在内存不足时尝试回收软引用对象。
- 缓存管理:使用软引用实现的缓存会在内存不足时自动释放缓存对象,从而为新对象腾出空间。
具体实现步骤
- 初始化缓存:创建一个缓存容器,如
HashMap
,用于存储键和软引用对象的映射。 - 访问缓存:
- 当访问缓存时,首先检查软引用是否仍然有效(即其关联的对象是否已被回收)。
- 如果软引用有效,返回其关联的对象。
- 如果软引用无效,说明对象已被回收,可以重新从数据源获取数据,并创建新的软引用。
- 添加数据:
- 当添加新数据到缓存时,使用
SoftReference
包装该对象,并将其存储在缓存容器中。 - 由于软引用的特性,如果内存不足,这些对象可能会被垃圾收集器回收。
- 当添加新数据到缓存时,使用
- 内存回收:当系统内存不足时,垃圾收集器会尝试回收软引用对象。这使得缓存可以自动调整大小,释放不再需要的内存。
性能考虑
- 时间复杂度:访问和添加数据的时间复杂度通常为 O(1),因为
HashMap
提供了快速的键值对查找。 - 空间复杂度:缓存的大小取决于缓存对象的数量和每个对象的大小,但软引用允许在内存不足时自动回收对象,从而动态调整缓存大小。
应用场景
软引用缓存适用于以下场景:
- 内存敏感的应用程序:在内存资源有限的设备上,如移动设备或嵌入式系统,软引用缓存可以动态地释放内存。
- 大对象缓存:对于占用大量内存的对象,如图片或大型文档,软引用缓存可以在内存不足时自动释放这些对象。
- 可有可无的缓存:在某些情况下,缓存数据的丢失不会对应用程序的功能产生重大影响,软引用缓存是一个很好的选择。
示例代码(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 缓存策略的实现原理和详细步骤:
工作原理
- 弱引用:弱引用是一种对对象的引用,它不会阻止垃圾收集器回收其引用的对象。这意味着只要没有其他的强引用指向该对象,对象就可以被垃圾收集器回收。
- 垃圾收集器:Java 的垃圾收集器会定期执行,当它发现某个对象只被弱引用所引用时,就会回收该对象占用的内存。
- 缓存管理:使用弱引用实现的缓存允许对象在不再被使用时被快速回收,即使内存尚未不足。
具体实现步骤
- 初始化缓存:创建一个缓存容器,如
HashMap
,用于存储键和弱引用对象的映射。 - 访问缓存:
- 当访问缓存时,首先检查弱引用是否仍然有效(即其关联的对象是否已被回收)。
- 如果弱引用有效,返回其关联的对象。
- 如果弱引用无效,说明对象已被回收,可以重新从数据源获取数据,并创建新的弱引用。
- 添加数据:
- 当添加新数据到缓存时,使用
WeakReference
包装该对象,并将其存储在缓存容器中。 - 由于弱引用的特性,这些对象可能会在下一次垃圾收集时被回收。
- 当添加新数据到缓存时,使用
- 内存回收:当垃圾收集器执行时,它会检查所有弱引用,并回收那些只被弱引用的对象。
性能考虑
- 时间复杂度:访问和添加数据的时间复杂度通常为 O(1),因为
HashMap
提供了快速的键值对查找。 - 空间复杂度:缓存的大小取决于缓存对象的数量和每个对象的大小,但由于弱引用允许对象在下一次垃圾收集时被回收,因此缓存不会长时间占用大量内存。
应用场景
弱引用缓存适用于以下场景:
- 内存敏感的应用程序:在内存资源有限的设备上,如移动设备或嵌入式系统,弱引用缓存可以快速释放内存。
- 临时对象缓存:对于只在特定时间内需要的对象,使用弱引用缓存可以确保这些对象在不再需要时迅速被回收。
- 可丢弃的缓存:在某些情况下,缓存数据的丢失不会对应用程序的功能产生重大影响,弱引用缓存是一个很好的选择。
示例代码(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 的缓存机制非常灵活,可以通过简单的配置来满足不同的性能需求。合理地使用缓存可以显著提高应用程序的性能,尤其是在处理大量数据库查询时。然而,开发者需要注意缓存的一致性和并发问题,特别是在使用可读写缓存时。