Mybatis二级缓存四种回收策略的原理分析

目录

LRU(Least Recently Used)算法

数据结构

工作原理

具体实现步骤

性能

应用场景

实现代码

代码解析

总结

FIFO(First In, First Out)算法

数据结构

工作原理

具体实现步骤

性能

应用场景

实现代码

代码解析

总结

SOFT(软引用)缓存回收策略

工作原理

具体实现步骤

性能

应用场景

实现代码

代码解析

总结

WEAK(弱引用)缓存回收策略

工作原理

具体实现步骤

性能

应用场景

实现代码

代码解析

总结

总结


LRU(Least Recently Used)算法

数据结构

LRU 算法主要依赖两个数据结构:

  1. 双向链表(Doubly Linked List):用于维护缓存的顺序,链表头部(head)是最近使用的节点,链表尾部(tail)是最久未使用的节点。
  2. 哈希表(HashMap):用于快速查找缓存中的数据项,以保证 O(1) 时间复杂度的访问和更新。

工作原理

LRU 算法的基本思想是,当缓存已满时,淘汰最久未使用的数据项。每次访问缓存时,都会将访问的数据项移动到链表头部,这样链表尾部的数据项就是最久未使用的,可以被优先淘汰。

具体实现步骤

  1. 初始化:创建一个固定大小的双向链表和一个哈希表。
  2. 访问数据
    • 命中缓存:如果数据在缓存中,将该数据项移动到链表头部。
    • 未命中缓存:如果数据不在缓存中,创建一个新的节点并插入到链表头部。如果缓存已满,移除链表尾部的节点(最久未使用的节点),并从哈希表中删除对应的键。
  3. 更新数据:更新数据时,将更新后的节点移动到链表头部。
  4. 淘汰数据:当缓存已满时,移除链表尾部的节点,并从哈希表中删除对应的键。

性能

  • 时间复杂度
    • 插入、删除、查找操作的时间复杂度均为 O(1)。
  • 空间复杂度
    • 需要额外的空间来维护双向链表和哈希表,其空间复杂度为 O(n)。

应用场景

LRU 算法广泛应用于以下场景:

  • 操作系统的页面置换算法
  • 数据库缓冲池
  • 浏览器缓存
  • CDN 缓存

实现代码

以下是用 Java 实现 LRU 缓存的代码:

import java.util.HashMap;
import java.util.Map;

public class LRUCache<K, V> {
    private final int capacity;
    private final Map<K, Node<K, V>> cache;
    private final DoublyLinkedList<K, V> list;

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

    public V get(K key) {
        Node<K, V> node = cache.get(key);
        if (node == null) {
            return null; // Cache miss
        }
        list.moveToHead(node);
        return node.value;
    }

    public void put(K key, V value) {
        Node<K, V> node = cache.get(key);
        if (node == null) {
            if (cache.size() == capacity) {
                Node<K, V> tail = list.removeTail();
                cache.remove(tail.key);
            }
            node = new Node<>(key, value);
            cache.put(key, node);
            list.addToHead(node);
        } else {
            node.value = value;
            list.moveToHead(node);
        }
    }

    private static class DoublyLinkedList<K, V> {
        private final Node<K, V> head;
        private final Node<K, V> tail;

        public DoublyLinkedList() {
            head = new Node<>(null, null);
            tail = new Node<>(null, null);
            head.next = tail;
            tail.prev = head;
        }

        public void addToHead(Node<K, V> node) {
            node.next = head.next;
            node.prev = head;
            head.next.prev = node;
            head.next = node;
        }

        public void moveToHead(Node<K, V> node) {
            removeNode(node);
            addToHead(node);
        }

        public Node<K, V> removeTail() {
            Node<K, V> res = tail.prev;
            removeNode(res);
            return res;
        }

        private void removeNode(Node<K, V> node) {
            node.prev.next = node.next;
            node.next.prev = node.prev;
        }
    }

    private static class Node<K, V> {
        K key;
        V value;
        Node<K, V> prev;
        Node<K, V> next;

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

代码解析

  1. 初始化:构造函数中初始化了缓存容量、哈希表和双向链表。
  2. 获取数据(get 方法)
    • 通过哈希表查找数据,如果存在,将该节点移动到链表头部,并返回值。
    • 如果数据不存在,返回 null。
  3. 插入数据(put 方法)
    • 通过哈希表查找数据,如果存在,更新值并将该节点移动到链表头部。
    • 如果数据不存在,先判断缓存是否已满,已满则移除尾部节点,再插入新节点到链表头部,并更新哈希表。

总结

LRU 算法通过双向链表和哈希表的结合,实现了高效的缓存访问和更新。其在操作系统、数据库、浏览器等场景中广泛应用,是一种经典且有效的缓存淘汰策略。

FIFO(First In, First Out)算法

数据结构

FIFO 算法主要依赖以下数据结构:

  1. 队列(Queue):一个先进先出的数据结构,用于维护缓存的顺序。队列头部(front)是最先进入缓存的数据,队列尾部(rear)是最近进入缓存的数据。

工作原理

FIFO 算法的基本思想是,当缓存已满时,淘汰最先进入缓存的数据项,即队列头部的数据项。

具体实现步骤

  1. 初始化:创建一个固定大小的队列。
  2. 访问数据
    • 命中缓存:如果数据在缓存中,直接返回数据。
    • 未命中缓存:如果数据不在缓存中,插入数据到队列尾部。如果缓存已满,移除队列头部的数据项,再插入新数据。
  3. 更新数据:更新数据时,将新数据插入到队列尾部,并删除队列头部的老数据(如果缓存已满)。
  4. 淘汰数据:当缓存已满时,移除队列头部的节点。

性能

  • 时间复杂度
    • 插入、删除、查找操作的时间复杂度均为 O(1)。
  • 空间复杂度
    • 需要额外的空间来维护队列,其空间复杂度为 O(n)。

应用场景

FIFO 算法适用于以下场景:

  • 数据流处理:如网络数据包的缓冲。
  • 文件系统缓存:如磁盘块的缓存。
  • 打印队列:最先打印的任务最先执行。

实现代码

以下是用 Java 实现 FIFO 缓存的代码:

import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;

public class FIFOCache<K, V> {
    private final int capacity;
    private final Map<K, V> cache;
    private final Queue<K> queue;

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

    public V get(K key) {
        return cache.get(key);
    }

    public void put(K key, V value) {
        if (cache.size() == capacity) {
            K oldestKey = queue.poll();
            cache.remove(oldestKey);
        }
        cache.put(key, value);
        queue.offer(key);
    }

    public static void main(String[] args) {
        FIFOCache<Integer, String> cache = new FIFOCache<>(3);
        cache.put(1, "one");
        cache.put(2, "two");
        cache.put(3, "three");
        System.out.println(cache.get(1)); // Output: one
        cache.put(4, "four");
        System.out.println(cache.get(1)); // Output: null, since it was evicted
        System.out.println(cache.get(2)); // Output: two
    }
}

代码解析

  1. 初始化:构造函数中初始化了缓存容量、哈希表和队列。
  2. 获取数据(get 方法)
    • 通过哈希表查找数据,如果存在,返回数据。
    • 如果数据不存在,返回 null。
  3. 插入数据(put 方法)
    • 如果缓存已满,移除队列头部的最旧数据,并从哈希表中删除对应的键。
    • 将新数据插入到哈希表和队列尾部。

总结

FIFO 算法通过队列实现了先进先出的缓存策略,简单且高效。在需要保证数据处理顺序的应用场景中,FIFO 算法非常适用。相比于 LRU 算法,FIFO 算法实现更加简单,但可能会出现“Belady's Anomaly”,即增加缓存大小反而增加缺页率的现象。

SOFT(软引用)缓存回收策略

工作原理

软引用缓存回收策略利用 Java 的软引用机制。当系统内存不足时,垃圾回收器会回收软引用指向的对象。软引用通常用于缓存实现,因为它允许在内存充足时保留缓存,在内存不足时释放缓存以防止内存溢出。

具体实现步骤

  1. 初始化:创建一个软引用缓存,使用 java.lang.ref.SoftReference 包装缓存对象。
  2. 访问数据
    • 命中缓存:如果数据在缓存中且未被回收,返回数据。
    • 未命中缓存:如果数据不在缓存中或已被回收,从数据源加载数据并放入缓存。
  3. 更新数据:更新数据时,使用软引用重新包装新的缓存数据。
  4. 回收数据:当系统内存不足时,垃圾回收器自动回收软引用指向的对象。

性能

  • 时间复杂度
    • 插入、删除、查找操作的时间复杂度通常为 O(1)。
  • 空间复杂度
    • 需要额外的空间来维护软引用,其空间复杂度为 O(n)。
  • 内存使用
    • 内存充足时,软引用缓存尽可能保留更多数据。
    • 内存不足时,垃圾回收器会回收软引用数据,防止内存溢出。

应用场景

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

  • 缓存实现:需要缓存数据但不希望缓存占用过多内存导致内存溢出。
  • 内存敏感应用:需要根据内存使用情况动态调整缓存占用。

实现代码

以下是用 Java 实现软引用缓存的代码:

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

public class SoftCache<K, V> {
    private final Map<K, SoftReference<V>> cache;

    public SoftCache() {
        this.cache = new HashMap<>();
    }

    public V get(K key) {
        SoftReference<V> ref = cache.get(key);
        return (ref != null) ? ref.get() : null;
    }

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

    public void remove(K key) {
        cache.remove(key);
    }

    public static void main(String[] args) {
        SoftCache<Integer, String> cache = new SoftCache<>();
        cache.put(1, "one");
        cache.put(2, "two");

        System.out.println(cache.get(1)); // Output: one
        System.gc(); // Suggest garbage collection
        System.out.println(cache.get(1)); // Output: one (if not collected)
    }
}

代码解析

  1. 初始化:构造函数中初始化了软引用缓存,使用 HashMap 存储软引用对象。
  2. 获取数据(get 方法)
    • 通过键查找软引用对象,如果软引用未被回收,返回缓存数据。
    • 如果软引用被回收,返回 null。
  3. 插入数据(put 方法)
    • 使用软引用包装新数据,并存储到 HashMap 中。
  4. 删除数据(remove 方法)
    • HashMap 中移除软引用对象。

总结

软引用缓存回收策略利用 Java 软引用机制,在内存充足时保留缓存数据,在内存不足时自动回收缓存数据,防止内存溢出。软引用缓存适用于内存敏感的缓存实现,保证缓存数据在内存紧张时能够被及时回收。相比于强引用缓存,软引用缓存更能动态适应内存变化。

WEAK(弱引用)缓存回收策略

工作原理

弱引用缓存回收策略利用 Java 的弱引用机制。弱引用在垃圾回收时比软引用更容易被回收。只要弱引用指向的对象没有强引用,垃圾回收器就会立即回收该对象。弱引用常用于缓存实现,因为它允许缓存对象在不被强引用时随时被回收。

具体实现步骤

  1. 初始化:创建一个弱引用缓存,使用 java.lang.ref.WeakReference 包装缓存对象。
  2. 访问数据
    • 命中缓存:如果数据在缓存中且未被回收,返回数据。
    • 未命中缓存:如果数据不在缓存中或已被回收,从数据源加载数据并放入缓存。
  3. 更新数据:更新数据时,使用弱引用重新包装新的缓存数据。
  4. 回收数据:当系统内存不足或垃圾回收器运行时,弱引用指向的对象会被立即回收。

性能

  • 时间复杂度
    • 插入、删除、查找操作的时间复杂度通常为 O(1)。
  • 空间复杂度
    • 需要额外的空间来维护弱引用,其空间复杂度为 O(n)。
  • 内存使用
    • 弱引用缓存在任何时候都可以被垃圾回收器回收,内存占用较少。

应用场景

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

  • 缓存实现:需要缓存数据但希望缓存占用的内存尽可能少。
  • 短命数据缓存:缓存的数据不需要长时间保留,可以随时被回收。

实现代码

以下是用 Java 实现弱引用缓存的代码:

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

public class WeakCache<K, V> {
    private final Map<K, WeakReference<V>> cache;

    public WeakCache() {
        this.cache = new HashMap<>();
    }

    public V get(K key) {
        WeakReference<V> ref = cache.get(key);
        return (ref != null) ? ref.get() : null;
    }

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

    public void remove(K key) {
        cache.remove(key);
    }

    public static void main(String[] args) {
        WeakCache<Integer, String> cache = new WeakCache<>();
        cache.put(1, "one");
        cache.put(2, "two");

        System.out.println(cache.get(1)); // Output: one
        System.gc(); // Suggest garbage collection
        System.out.println(cache.get(1)); // Output: null (if collected)
    }
}

代码解析

  1. 初始化:构造函数中初始化了弱引用缓存,使用 HashMap 存储弱引用对象。
  2. 获取数据(get 方法)
    • 通过键查找弱引用对象,如果弱引用未被回收,返回缓存数据。
    • 如果弱引用被回收,返回 null。
  3. 插入数据(put 方法)
    • 使用弱引用包装新数据,并存储到 HashMap 中。
  4. 删除数据(remove 方法)
    • HashMap 中移除弱引用对象。

总结

弱引用缓存回收策略利用 Java 弱引用机制,在不需要时随时回收缓存数据,减少内存占用。弱引用缓存适用于需要短暂缓存的场景,确保缓存数据在内存紧张时能够被及时回收。相比于软引用缓存,弱引用缓存更容易被回收,适合缓存频繁更新和访问的数据。

总结

  1. LRU

    • 优点:适用于频繁访问和更新的数据缓存,能够有效利用最近访问数据的特性。
    • 缺点:需要维护链表结构,可能带来额外的内存和时间开销。
  2. FIFO

    • 优点:实现简单,适用于有明显先进先出顺序的数据缓存。
    • 缺点:无法根据访问频率调整缓存策略,可能导致不常使用的数据占用缓存。
  3. SOFT

    • 优点:利用软引用机制,在内存不足时自动回收缓存对象,适用于希望缓存对象在内存不足时被回收的场景。
    • 缺点:回收时机不确定,可能导致缓存命中率降低。
  4. WEAK

    • 优点:利用弱引用机制,在不被强引用时随时回收缓存对象,适用于短命数据缓存。
    • 缺点:回收时机过于频繁,可能导致缓存命中率较低。

每种缓存回收策略都有其优缺点,具体选择哪种策略需要根据应用场景、数据访问模式和性能要求来确定。在实际应用中,可能需要结合多种策略以达到最佳效果。

  • 17
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值