探秘MyBatis缓存原理:Cache接口与实现类源码分析

简介: 探秘MyBatis缓存原理:Cache接口与实现类源码分析

缓存

缓存即将数据存储在内存上,传统的数据库,大量的数据都会存储在硬盘之上,而硬盘的读写效率是大大低于内存的,所以缓存的价值是在程序和数据库之间搭建一个桥梁,将一部分数据存储在内存,提高用户的查询效率。这是一种典型的空间换时间的优化策略。接下来我们将会进入 MyBatis 源码,学习它的来龙去脉。

Cache 接口

MyBatis 的 Cache 接口是用于提供数据缓存功能的接口,它允许 MyBatis 将查询结果缓存起来,以提高查询性能。这个接口定义了缓存的基本行为和操作方法,同时也提供了默认的实现。通过观察 Cache 接口,我们不难发现 Cache 是基于键值对的结构进行设计的。

public interface Cache {
  /**
   * 返回缓存的唯一标识,一个系统中可以设置多个缓存
   */
  String getId();
  /**
   * 将查询结果放入缓存中。其中,key 通常是一个唯一标识符,可以是 SQL 语句的 ID 或者是参数对象的哈希值,value 则是查询结果对象。
   */
  void putObject(Object key, Object value);
  /**
   * 从缓存中获取指定 key 对应的查询结果对象。
   */
  Object getObject(Object key);
  /**
   * 从缓存中移除指定 key 对应的查询结果对象。
   */
  Object removeObject(Object key);
  /**
   * 清空缓存,移除所有的缓存对象。
   */  
  void clear();
  /**
   * 获取缓存的数据条数。
   */
  int getSize();
  
  /** 
   * 获取读写锁,但是后面 MyBatis 已将此方法废弃。
   */
  ReadWriteLock getReadWriteLock();
}

Cache 实现类

MyBatis 提供了几种默认的缓存实现类,每种实现类都有其特定的特点和适用场景。以下是 MyBatis 中常见的缓存实现类:

可以看到,除了 PerpetualCache 是 ibatis.cache.impl 包,其他都是位于 decorators 包下,表示这些都是装饰器,用来丰富 PerpetualCache 的核心功能。

PerpetualCache

PerpetualCache 是 MyBatis 中的一个简单缓存实现类,它是其他缓存实现的基础。PerpetualCache 是一个基于内存的缓存,它使用一个 HashMap 来存储缓存数据。与其他缓存实现不同的是,PerpetualCache 不会自动清理缓存数据,缓存中的数据会一直存在直到缓存对象被销毁。

PerpetualCache 的实现相对简单,它提供了基本的存储和获取缓存数据的功能,但不包含缓存的过期策略或者淘汰算法。因此,PerpetualCache 适用于数据量较小,但是频繁被访问的场景,或者是在不需要缓存过期策略和淘汰算法的情况下使用。

public class PerpetualCache implements Cache {
  private final String id;
  // 通过这个 HashMap 存储数据
  private Map<Object, Object> cache = new HashMap<Object, Object>();
  public PerpetualCache(String id) {
    this.id = id;
  }
  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }
  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }
  
  // 省略其他方法 ...
}

要使用 PerpetualCache,只需在 MyBatis 的配置文件中指定缓存的类型为 PerpetualCache 即可。例如:

<cache type="org.apache.ibatis.cache.impl.PerpetualCache"/>

PerpetualCache 的简单实现使得它在内存中存储缓存数据的操作非常高效,适用于对实时性要求较高的场景。但需要注意的是,由于 PerpetualCache 不会自动清理缓存数据,如果缓存数据量过大,可能会导致内存溢出的问题,因此在使用时需要谨慎考虑缓存数据的大小和生命周期。

FifoCache

FifoCache 是 MyBatis 中的一个缓存实现类,它是 PerpetualCache 的一个装饰器(Decorator)。FifoCache 的全称是 First In, First Out Cache,即先进先出缓存,这是一种缓存的换出策略。它在 PerpetualCache 的基础上添加了一个队列,用于记录缓存数据的访问顺序。当缓存数据的数量达到一定阈值(默认 1024)时,FifoCache 会按照数据的访问顺序,淘汰最早被访问的数据,以保持缓存数据的新鲜度。

FifoCache 的实现原理是通过一个队列来记录缓存数据的插入顺序。当有新的数据需要放入缓存时,它会被添加到队列的末尾;当缓存数据的数量达到一定阈值时,FifoCache 会从队列的头部开始移除一条数据,即最早被访问的数据。

public class FifoCache implements Cache {
  private final Cache delegate;
  
  // 通过这个队列记录缓存的顺序
  private final Deque<Object> keyList;
  private int size;
  public FifoCache(Cache delegate) {
    this.delegate = delegate;
    this.keyList = new LinkedList<Object>();
    this.size = 1024;
  }
  public void setSize(int size) {
    // 可以手动设置清理阈值
    this.size = size;
  }
  @Override
  public void putObject(Object key, Object value) {
    cycleKeyList(key);
    // 新增时判断是否要清理
    delegate.putObject(key, value);
  }
  
  private void cycleKeyList(Object key) {
    keyList.addLast(key);
    if (keyList.size() > size) {
      Object oldestKey = keyList.removeFirst();
      delegate.removeObject(oldestKey);
    }
  }
  @Override
  public Object getObject(Object key) {
    return delegate.getObject(key);
  }
  
  // 省略其他方法 ...
}

要使用 FifoCache,只需在 MyBatis 的配置文件中将需要被包装的缓存实现类包装在 FifoCache 中即可。例如:

<cache type="org.apache.ibatis.cache.decorators.FifoCache"/>

FifoCache 适用于需要按照访问顺序进行淘汰的场景,通常用于控制缓存数据的大小,避免缓存数据过多导致内存溢出或者性能下降。需要注意的是,由于 FifoCache 需要维护一个队列,可能会略微增加缓存的读写操作的开销,因此在性能要求较高的场景下需要进行评估和测试。

LruCache

LruCache 是 MyBatis 中的一个缓存实现类,它是 PerpetualCache 的一个装饰器(Decorator)。LruCache 的全称是 Least Recently Used Cache,即最近最少使用缓存,这是一种缓存的换出策略。它基于最近最少使用算法(LRU),在缓存数据达到一定大小时,会淘汰最近最少被访问的数据,以保持缓存数据的新鲜度。

LruCache 的实现原理是通过一个 LinkedHashMap 来存储缓存数据,并按照访问顺序进行排序。每当缓存数据被访问时,对应的条目会被移动到 LinkedHashMap 的尾部,表示最近被使用过。当缓存数据达到一定大小时,LruCache 会从 LinkedHashMap 的头部开始移除一条数据,即最近最少被访问的数据。

LinkedHashMap 是 Java 中的一个特殊 HashMap 实现,它除了具备 HashMap 的基本功能外,还额外维护了一个双向链表,用于记录元素的插入顺序或者访问顺序。这个链表可以按照插入顺序或者访问顺序(LRU)来排列元素。

在 LRUCache 中,我们希望根据元素的访问顺序来淘汰最近最少被使用的数据。这就需要利用 LinkedHashMap 的特性:

  1. 每当元素被访问时,LinkedHashMap 会将该元素移动到链表的末尾:这意味着当我们通过 get(key) 方法访问 LRUCache 中的某个元素时,LinkedHashMap 会将该元素移动到链表的末尾,表示它是最近被使用过的元素。
  2. 链表的末尾表示最近被使用过的元素:由于 LinkedHashMap 维护的链表是双向链表,末尾节点即为最近被使用过的节点,头部节点即为最早被使用过的节点。

通过 LinkedHashMap 的这种特性,LRUCache 可以在元素被访问时将其移动到链表的末尾,从而实现了最近被使用的元素位于链表末尾的效果。当需要淘汰缓存数据时,LRUCache 可以从链表的头部开始遍历,找到最早被使用的节点,并将其从缓存中移除。

public class LruCache implements Cache {
  private final Cache delegate;
  
  // 通过这个 Map 来存储访问顺序,构造的时候进行初始化,实际上是 LinkedHashMap。
  private Map<Object, Object> keyMap;
  private Object eldestKey;
  public LruCache(Cache delegate) {
    this.delegate = delegate;
    setSize(1024);
  }
  @Override
  public String getId() {
    return delegate.getId();
  }
  @Override
  public int getSize() {
    return delegate.getSize();
  }
  public void setSize(final int size) {
    keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
      private static final long serialVersionUID = 4267176411845948333L;
      @Override
      protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
        // 重写是否要删除最早元素的方法,加入大小判断的逻辑
        boolean tooBig = size() > size;
        if (tooBig) {
          eldestKey = eldest.getKey();
        }
        return tooBig;
      }
    };
  }
  @Override
  public void putObject(Object key, Object value) {
    delegate.putObject(key, value);
    cycleKeyList(key);
  }
  
  private void cycleKeyList(Object key) {
    keyMap.put(key, key);
    if (eldestKey != null) {
      delegate.removeObject(eldestKey);
      eldestKey = null;
    }
  }
  @Override
  public Object getObject(Object key) {
    keyMap.get(key); // 每次访问 Map 中的元素,LinkedHashMap 的元素位置都会变化,最先访问的放在最后面。
    return delegate.getObject(key);
  }
  // 省略其他方法 ...
}

要使用 LruCache,只需在 MyBatis 的配置文件中将需要被包装的缓存实现类包装在 LruCache 中即可。例如:

<cache type="org.apache.ibatis.cache.decorators.LruCache"/>

LruCache 适用于需要按照最近访问顺序进行淘汰的场景,通常用于控制缓存数据的大小,避免缓存数据过多导致内存溢出或者性能下降。需要注意的是,由于 LruCache 需要维护一个 LinkedHashMap,可能会略微增加缓存的读写操作的开销,因此在性能要求较高的场景下需要进行评估和测试。

ScheduledCache

ScheduledCache 是 MyBatis 中的一个缓存实现类,它是 PerpetualCache 的一个装饰器(Decorator)。ScheduledCache 提供了定时清理缓存数据的功能,可以定期清理过期的缓存数据,以避免缓存数据过多导致内存溢出或者性能下降。

ScheduledCache 的实现原理是内部维护了两个属性,清理时间的间隔(clearInterval),上次清理的时间(lastClear)。在 remove,get,put 的时候,会先判断一下时间,当前时间减去上次清理的时间是否大于间隔时间(默认一小时),如果是就直接清。并且将当前时间赋值给 lastClear。

这种清理是有问题的,这种将清理的触发的操作绑定给了操作,那如果说不操作,其实这个清理时间就没有意义,也没有清理操作,还是占用着内存空间。

public class ScheduledCache implements Cache {
  private final Cache delegate;
  // 清理时间的间隔
  protected long clearInterval;
  // 上次清理的时间
  protected long lastClear;
  public ScheduledCache(Cache delegate) {
    this.delegate = delegate;
    this.clearInterval = 60 * 60 * 1000; // 1 hour
    this.lastClear = System.currentTimeMillis();
  }
  public void setClearInterval(long clearInterval) {
    this.clearInterval = clearInterval;
  }
  @Override
  public int getSize() {
    clearWhenStale();
    return delegate.getSize();
  }
  @Override
  public void putObject(Object key, Object object) {
    clearWhenStale();
    delegate.putObject(key, object);
  }
  @Override
  public Object getObject(Object key) {
    return clearWhenStale() ? null : delegate.getObject(key);
  }
  @Override
  public Object removeObject(Object key) {
    clearWhenStale();
    return delegate.removeObject(key);
  }
  @Override
  public void clear() {
    lastClear = System.currentTimeMillis();
    delegate.clear();
  }
  private boolean clearWhenStale() {
    if (System.currentTimeMillis() - lastClear > clearInterval) {
      clear();
      return true;
    }
    return false;
  }
  
  // 省略其他方法 ...
}

要使用 ScheduledCache,只需在 MyBatis 的配置文件中将需要被包装的缓存实现类包装在 ScheduledCache 中即可。例如:

<cache type="org.apache.ibatis.cache.decorators.ScheduledCache">
    <property name="clearInterval" value="60000"/> <!-- 清理间隔时间,单位为毫秒 -->
</cache>

SoftCache

将 Key 和 Value 包装为 SoftEntry,保存在 delegate 中,并且自己持有 hardLinksToAvoidGarbageCollection(强引用的集合)和一个queueOfGarbageCollectedEntries(引用队列),在 put 和 remove 的时候,在调用 delegate 之前,会先从引用队列里面获取值,如果能获取值,就从 delegate 中移除。在 get 的时候会先从 delegate 获取,如果这个值存在,并且没有被 GC,给他增加强引用(添加到 hardLinksToAvoidGarbageCollection 里面去),并且如果强引用集合大于 numberOfHardLinks(默认是 256),就移除队尾元素。

public class SoftCache implements Cache {
  // 队列,持有一个强引用
  private final Deque<Object> hardLinksToAvoidGarbageCollection;
  // 软引用被回收之后放置的集合
  private final ReferenceQueue<Object> queueOfGarbageCollectedEntries;
  private final Cache delegate;
  // 有多少个强引用,默认是256
  private int numberOfHardLinks;
  public SoftCache(Cache delegate) {
    this.delegate = delegate;
    this.numberOfHardLinks = 256;
    this.hardLinksToAvoidGarbageCollection = new LinkedList<>();
    this.queueOfGarbageCollectedEntries = new ReferenceQueue<>();
  }
  @Override
  public String getId() {
    return delegate.getId();
  }
  // removeGarbageCollectedItems 方法是干嘛的?
  @Override
  public int getSize() {
    removeGarbageCollectedItems();
    return delegate.getSize();
  }
  public void setSize(int size) {
    this.numberOfHardLinks = size;
  }
  @Override
  public void putObject(Object key, Object value) {
    removeGarbageCollectedItems();
    // 将 key 和 value 包装为 SoftEntry 值。
    delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries));
  }
  
  @Override
  public Object getObject(Object key) {
    Object result = null;
    @SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache
    // 先从 delegate 中获取元素,这个元素是 SoftReference,
    SoftReference<Object> softReference = (SoftReference<Object>) delegate.getObject(key);
    if (softReference != null) {
      result = softReference.get();
      if (result == null) {
        delegate.removeObject(key);
      } else {
        // 如果不是null,说明当前的这个对象对象还没有被回收了,所以,添加到 hardLinksToAvoidGarbageCollection 里面,增加强引用关系
        // See #586 (and #335) modifications need more than a read lock
        synchronized (hardLinksToAvoidGarbageCollection) {
          hardLinksToAvoidGarbageCollection.addFirst(result);
          // 如果大于 numberOfHardLinks,就将 hardLinksToAvoidGarbageCollection 里面的尾元素移除
          if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) {
            hardLinksToAvoidGarbageCollection.removeLast();
          }
        }
      }
    }
    return result;
  }
  @Override
  public Object removeObject(Object key) {
    removeGarbageCollectedItems();
    return delegate.removeObject(key);
  }
  @Override
  public void clear() {
    synchronized (hardLinksToAvoidGarbageCollection) {
      hardLinksToAvoidGarbageCollection.clear();
    }
    removeGarbageCollectedItems();
    delegate.clear();
  }
  // 看这里的逻辑,好多都调用了这个方法,从 queueOfGarbageCollectedEntries 队列里面出队一个元素
  // 如果有,说明这个元素已经被 GC 要被 GC 回收了,那么就需要将 delegate 中的这个元素也移除掉
  // 问题?hardLinksToAvoidGarbageCollection 里面要不要移除?
  // 不需要,因为从 queueOfGarbageCollectedEntries 里面能出来的话,说明他已经是个垃圾了,没有强引用啦。
  private void removeGarbageCollectedItems() {
    SoftEntry sv;
    while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) {
      delegate.removeObject(sv.key);
    }
  }
  private static class SoftEntry extends SoftReference<Object> {
    private final Object key;
    SoftEntry(Object key, Object value, ReferenceQueue<Object> garbageCollectionQueue) {
      super(value, garbageCollectionQueue);
      this.key = key;
    }
  }
}

WeakCache

这个和 SoftCache 差不多,看看就可以,我这里就不写了…

LoggingCache

LoggingCache 是 MyBatis 中的一个缓存装饰器,它用于在缓存操作的前后打印日志,以便于调试和监控缓存的使用情况。

LoggingCache 主要是为了方便开发者跟踪缓存的使用情况和了解缓存操作的性能。它会在缓存的读取和写入操作之前后打印相应的日志信息,包括缓存键、缓存值、操作类型等。这样可以帮助开发者快速定位缓存相关的问题,并且可以通过日志信息了解缓存的命中率、缓存数据的更新频率等。

public class LoggingCache implements Cache {
  private final Log log;
  private final Cache delegate;
  // 请求次数
  protected int requests = 0;
  // 命中次数
  protected int hits = 0;
  public LoggingCache(Cache delegate) {
    this.delegate = delegate;
    this.log = LogFactory.getLog(getId());
  }
  @Override
  public Object getObject(Object key) {
    requests++;
    final Object value = delegate.getObject(key);
    if (value != null) {
      hits++;
    }
    if (log.isDebugEnabled()) {
      log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio());
    }
    return value;
  }
  private double getHitRatio() {
    return (double) hits / (double) requests;
  }
  // 省略其他方法 ...
}

要使用 LoggingCache,只需在 MyBatis 的配置文件中将需要被包装的缓存实现类包装在 LoggingCache 中即可。例如:

<cache type="org.apache.ibatis.cache.decorators.LoggingCache"/>

通过添加 LoggingCache,MyBatis 在执行缓存操作时会打印相应的日志信息,方便开发者进行调试和监控。需要注意的是,由于日志记录会对性能产生一定的影响,因此在生产环境中建议关闭 LoggingCache,或者仅在调试阶段开启。

BlockingCache

BlockingCache 是 MyBatis 中的一个缓存装饰器,它提供了线程安全的功能。在多线程环境下,当多个线程同时访问同一个 key 对应的缓存时,BlockingCache 可以确保只有一个线程可以执行查询操作,其他线程会等待直到查询完成后再获取结果。

BlockingCache 的实现原理是利用了 Java 中的 ReentrantLock(可重入锁)。当一个线程试图获取某个 key 对应的缓存时,如果发现该 key 对应的缓存正在被其他线程加载,则会阻塞当前线程,直到缓存加载完成。这样可以避免多个线程同时进行重复的查询操作,提高了缓存的利用率和系统的性能。

public class BlockingCache implements Cache {
  // 尝试获取锁的超时时间
  private long timeout;
  private final Cache delegate;
  // 为每一个 Key 搭配一个重入锁,构造时初始化为 ConcurrentHashMap
  private final ConcurrentHashMap<Object, ReentrantLock> locks;
  public BlockingCache(Cache delegate) {
    this.delegate = delegate;
    this.locks = new ConcurrentHashMap<Object, ReentrantLock>();
  }
  @Override
  public void putObject(Object key, Object value) {
    try {
      delegate.putObject(key, value);
    } finally {
      // 释放锁
      releaseLock(key);
    }
  }
  @Override
  public Object getObject(Object key) {
    // 获取锁
    acquireLock(key);
    Object value = delegate.getObject(key);
    if (value != null) {
      // 释放锁
      releaseLock(key);
    }        
    return value;
  }
  @Override
  public Object removeObject(Object key) {
    // despite of its name, this method is called only to release locks
    releaseLock(key);
    return null;
  }
  private ReentrantLock getLockForKey(Object key) {
    ReentrantLock lock = new ReentrantLock();
    ReentrantLock previous = locks.putIfAbsent(key, lock);
    return previous == null ? lock : previous;
  }
  
  private void acquireLock(Object key) {
    Lock lock = getLockForKey(key);
    if (timeout > 0) {
      try {
        // 在指定的时间内尝试获取锁。如果超过这个时间还未获取到锁,则放弃获取,并返回 false。
        boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
        if (!acquired) {
          throw new CacheException("Couldn't get a lock in " + timeout + " for the key " +  key + " at the cache " + delegate.getId()); 
        }
      } catch (InterruptedException e) {
        throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
      }
    } else {
      lock.lock();
    }
  }
  
  private void releaseLock(Object key) {
    ReentrantLock lock = locks.get(key);
    if (lock.isHeldByCurrentThread()) {
      lock.unlock();
    }
  }
  // 省略其他方法 ...
}

要使用 BlockingCache,只需在 MyBatis 的配置文件中将需要被包装的缓存实现类包装在 BlockingCache 中即可。例如:

<cache type="org.apache.ibatis.cache.decorators.BlockingCache"/>

通过添加 BlockingCache,MyBatis 在使用该缓存时就会自动添加了线程安全的功能。需要注意的是,由于 BlockingCache 使用了额外的线程同步机制,可能会略微增加缓存操作的开销,因此在性能要求较高的场景下需要进行评估和测试。

SerializbledCache

要用这个缓存,必须要实现 Serializable 接口,在 put 和 get 的时候会出现序列化和反序列化。序列化的目的是为了深拷贝,深拷贝是为了什么?安全。

SerializedCache 是 MyBatis 中的一个缓存装饰器(Decorator),它用于将缓存中的数据进行序列化和反序列化,以支持跨 JVM 实例的共享。

SerializedCache 的主要作用是将缓存中的对象转换成字节流进行存储,从而可以在不同的 JVM 实例之间进行传输和共享。这样可以在分布式系统中使用相同的缓存实例,从而提高了缓存数据的共享和利用率。

具体来说,当数据写入到缓存中时,SerializedCache 会将对象转换成字节流,并存储到底层的缓存中。当数据从缓存中读取时,SerializedCache 会将存储的字节流反序列化成对象,并返回给调用方使用。

public class SerializedCache implements Cache {
  private final Cache delegate;
  public SerializedCache(Cache delegate) {
    this.delegate = delegate;
  }
  @Override
  public void putObject(Object key, Object object) {
    // 主要看这个,Value 必须要实现 Serializable 接口,会有序列化。
    if (object == null || object instanceof Serializable) {
      delegate.putObject(key, serialize((Serializable) object));
    } else {
      throw new CacheException("SharedCache failed to make a copy of a non-serializable object: " + object);
    }
  }
  @Override
  public Object getObject(Object key) {
    Object object = delegate.getObject(key);
     // 反序列化
    return object == null ? null : deserialize((byte[]) object);
  }
  // 这里直接利用序列化来拷贝了一份,这不就是 copyOnWrite 吗
  // 序列化的操作没有啥可说的,就这里为啥要序列化?
  // 深拷贝呀,
  private byte[] serialize(Serializable value) {
    try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos)) {
      oos.writeObject(value);
      oos.flush();
      return bos.toByteArray();
    } catch (Exception e) {
      throw new CacheException("Error serializing object.  Cause: " + e, e);
    }
  }
  
  // 反序列化
  private Serializable deserialize(byte[] value) {
    SerialFilterChecker.check();
    Serializable result;
    try (ByteArrayInputStream bis = new ByteArrayInputStream(value);
        ObjectInputStream ois = new CustomObjectInputStream(bis)) {
      result = (Serializable) ois.readObject();
    } catch (Exception e) {
      throw new CacheException("Error deserializing object.  Cause: " + e, e);
    }
    return result;
  }
  public static class CustomObjectInputStream extends ObjectInputStream {
    public CustomObjectInputStream(InputStream in) throws IOException {
      super(in);
    }
    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) throws ClassNotFoundException {
      return Resources.classForName(desc.getName());
    }
  }
}

要使用 SerializedCache,只需在 MyBatis 的配置文件中将需要被包装的缓存实现类包装在 SerializedCache 中即可。例如:

<cache type="org.apache.ibatis.cache.decorators.SerializedCache"/>

需要注意的是,使用 SerializedCache 可能会增加序列化和反序列化的开销,并且存储的数据量会比原始对象要大,因此在使用时需要权衡性能和资源占用。通常情况下,建议在需要跨 JVM 实例共享缓存数据时使用 SerializedCache

TransactionalCache

TransactionalCache 是 MyBatis 中的一个缓存装饰器(Decorator),它提供了事务性缓存的功能。事务性缓存确保缓存中的数据与数据库中的数据保持一致,只有在事务成功提交后,缓存中的数据才会被更新或者删除。

TransactionalCache 的主要作用是在事务提交时,将缓存中的数据同步到数据库中。它通过拦截事务提交的操作,在事务成功提交后,更新或者删除缓存中的数据,以保持缓存和数据库的一致性。

具体来说,当事务提交时,TransactionalCache 会根据事务的操作类型(插入、更新、删除)来更新或者删除缓存中的相应数据。这样可以确保缓存中的数据与数据库中的数据保持一致,避免了因为缓存数据与数据库数据不一致而导致的数据异常问题。

public class TransactionalCache implements Cache {
  private static final Log log = LogFactory.getLog(TransactionalCache.class);
  private final Cache delegate;
  // 标志位
  private boolean clearOnCommit;
  // commit 的时候需要添加到缓存里面的实体
  private final Map<Object, Object> entriesToAddOnCommit;
  // get 的时候没有命中缓存的 key 的集合
  private final Set<Object> entriesMissedInCache;
  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<>();
    this.entriesMissedInCache = new HashSet<>();
  }
  @Override
  public Object getObject(Object key) {
    Object object = delegate.getObject(key);
    if (object == null) {
      // 在 get 的时候如果没有值,会放在 entriesMissedInCache 里面
      entriesMissedInCache.add(key);
    }
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }
  
  @Override
  public void putObject(Object key, Object object) {
    // put 不会直接调用 delegate 方法,会先放在 entriesToAddOnCommit 里面
    entriesToAddOnCommit.put(key, object);
  }
  @Override
  public Object removeObject(Object key) {
    // 不能手动移除
    return null;
  }
  @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }
  
  // 只有 commit 的时候,会先将 delegate 清除,之后将 entriesToAddOnCommit 里面的添加到 delegate 里面
  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
  }
  public void rollback() {
    unlockMissedEntries();
    reset();
  }
  private void reset() {
    clearOnCommit = false;
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
  }
  private void flushPendingEntries() {
    // 这里可以用 PutAll 操作
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    // 对于缓存没有命中,直接放一个 null
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }
  
  // 移除
  private void unlockMissedEntries() {
    for (Object entry : entriesMissedInCache) {
      try {
        delegate.removeObject(entry);
      } catch (Exception e) {
        log.warn("Unexpected exception while notifying a rollback to the cache adapter. "
            + "Consider upgrading your cache adapter to the latest version. Cause: " + e);
      }
    }
  }
}

要使用 TransactionalCache,只需在 MyBatis 的配置文件中将需要被包装的缓存实现类包装在 TransactionalCache 中即可。例如:

<cache type="org.apache.ibatis.cache.decorators.TransactionalCache"/>

需要注意的是,TransactionalCache 通常需要在事务管理器(TransactionManager)的支持下才能正常工作,因为它需要拦截事务提交的操作。另外,由于缓存与数据库的同步操作可能会对性能产生一定的影响,因此在性能要求较高的场景下需要进行评估和测试。


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
4天前
|
存储 缓存 监控
中间件Read-Through Cache(直读缓存)策略实现方式
【5月更文挑战第11天】中间件Read-Through Cache(直读缓存)策略实现方式
13 4
中间件Read-Through Cache(直读缓存)策略实现方式
|
4天前
|
存储 缓存 监控
中间件Read-Through Cache(直读缓存)策略注意事项
【5月更文挑战第11天】中间件Read-Through Cache(直读缓存)策略注意事项
9 2
|
4天前
|
存储 缓存 中间件
中间件Read-Through Cache(直读缓存)策略工作原理
【5月更文挑战第11天】中间件Read-Through Cache(直读缓存)策略工作原理
11 3
|
5天前
|
SQL 存储 算法
Mybatis-Plus- CRUD接口-主键策略-自动填充和乐观锁-分页-逻辑删除-条件构造器和常用接口
Mybatis-Plus- CRUD接口-主键策略-自动填充和乐观锁-分页-逻辑删除-条件构造器和常用接口
|
5天前
|
缓存 数据安全/隐私保护 UED
深入了解304缓存原理:提升网站性能与加载速度
深入了解304缓存原理:提升网站性能与加载速度
|
6天前
|
缓存 中间件 数据库
中间件Write-Through Cache(直写缓存)策略
【5月更文挑战第7天】中间件Write-Through Cache(直写缓存)策略
17 4
中间件Write-Through Cache(直写缓存)策略
|
6天前
|
存储 缓存 中间件
中间件Read-Through Cache(直读缓存)策略
【5月更文挑战第7天】中间件Read-Through Cache(直读缓存)策略
16 4
中间件Read-Through Cache(直读缓存)策略
|
6天前
|
存储 缓存 移动开发
html实现离线缓存(工作原理+怎么使用+应用场景)
html实现离线缓存(工作原理+怎么使用+应用场景)
18 0
|
6天前
|
Java 数据库连接 数据库
spring+mybatis_编写一个简单的增删改查接口
spring+mybatis_编写一个简单的增删改查接口
18 2
|
6天前
|
Java 数据库连接 数据库
MybatisPlus中IService接口有什么用?
MybatisPlus中IService接口有什么用?