5. 缓存模块

news2024/11/24 14:26:43

缓存概述

对于缓存功能,相信大家都十分熟悉了。一旦我们发现系统的性能存在瓶颈需要优化时,可能第一时间想到的方式就是加缓存。缓存本质上是一种空间换时间的技术,它将计算结果保存在距离用户更近、或访问效率更高的存储介质中,进而降低请求处理耗时,提升系统性能。

作为一款成熟的开源框架,MyBatis 自然也提供了缓存的功能,它在执行查询语句时首先尝试从缓存获取,避免频繁与数据库交互,大大提升了查询效率。MyBatis 内部有所谓的一级缓存和二级缓存,这个会在后面的章节中详细阐述,本次仅讨论缓存的内部实现。

MyBatis 缓存定义

我们首先来看下 MyBatis 的 Cache 接口,它定义了缓存的基本行为:

/**
 * MyBatis缓存接口
 */
public interface Cache {
  //获取缓存唯一ID
  String getId();
  
  //保存元素
  void putObject(Object key, Object value);

  //查询元素
  Object getObject(Object key);
 
  //删除元素
  Object removeObject(Object key);

  //清空缓存
  void clear();

  //获取缓存元素数量
  int getSize();

  //获取缓存操作的读写锁
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

可以看到,这个接口定义十分简单,就是对于缓存的基础 CRUD 操作。

我们知道,缓存的本质其实就是一个 Map ,MyBatis 内置了一个最基础的缓存实现 PerpetualCache ,其底层就是使用了一个 HashMap 来维护元素:

/**
 * 最基础的缓存实现,本质上就是一个HashMap
 */
public class PerpetualCache implements Cache {
  //缓存ID
  private final String id;

  //底层使用HashMap来维护缓存元素
  private final Map<Object, Object> cache = new HashMap<>();

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

  @Override
  public Object removeObject(Object key) {
    return cache.remove(key);
  }

  @Override
  public void clear() {
    cache.clear();
  }
  //...省略非必要代码

}

这个缓存的实现看上去平平无奇,任何人都能写得出来。那么现在问题来了,MyBatis 这样一个成熟的 ORM 框架,缓存功能肯定不会如此初级,它势必要为缓存提供各种额外的扩展功能,比如淘汰策略、定时清空功能、防击穿、打印日志等。那么,MyBatis 是如何在缓存的基础实现上,动态扩展这些功能的呢?

基于继承的扩展

想要对一个类进行功能上的扩展,我们第一时间就会想到继承。 没错,通过继承确实可以很方便地在现有的类上增加额外的功能。举个例子:如果我们想要为缓存增加 LRU 淘汰策略,只需要新建一个 LRUCache 实现类,继承 PerpetualCache ,在内部增加 LRU 算法实现即可。同理,如果需要具有打印日志功能的缓存,就要再创建一个LoggingCache 类。这种解决方案看似可以满足需求。

但是在实际的应用场景中,缓存的能力是需要动态排列组合和扩展的。使用过 MyBatis 的同学应该经常会用到如下形式的缓存配置:

<!--开启二级缓存配置-->
<cache eviction="LRU" flushInterval="60000" blocking="true" size="512"/>

这段配置定义了如下的缓存功能:

  • 开启 MyBatis 二级缓存。
  • 设置缓存内的元素数量上限为 512 个。
  • 缓存淘汰策略是 LRU。
  • 需要每隔 60s 需要对缓存进行清空。
  • 当缓存查询结果为空时,需要通过加锁的方式从 DB 中查询数据,避免大量缓存击穿造成数据库过载。

类似这样的配置,就要求缓存实现类能够动态扩展 LRU、定时清空、阻塞查询等功能。这样一来,如果依然通过继承的方式实现,就需要再创建 LRUScheduledBlockingCache 类。
而且,由于所有功能是动态增加的,事先并不知道客户端会选择哪几个功能,那么就需要提前把所有功能排列组合地实现一遍,如 LRUScheduledCacheScheduledBlockingCacheLRUBlockingCache

最大的问题在于,每扩展一个新的功能,就需要把所有已有的缓存再排列组合一遍,最终的结果就是类爆炸。

组合优于继承。

Decorator Pattern 装饰器模式

既然通过继承的方式实现缓存功能并不可取,那么 MyBatis 是如何实现缓存的动态扩展的呢?老规矩,设计模式又来了。这里用到的是 Decorator Pattern 装饰器模式。
先来看下装饰器模式的 UML 结构图:
Decorator Pattern
(图片来源:https://refactoring.guru/design-patterns/decorator)

其中包含了以下核心角色:

  • Component 接口:定义了被装饰的对象和装饰器的公共行为。
  • ConcreteComponent:被装饰对象的具体实现,内部包含了一些基础功能。
  • BaseDecorator:装饰器的基类,它内部维护了被装饰的实例,并且实现了装饰器的公共方法。
  • ConcreteDecorator:具体的装饰器,扩展了额外的功能,并且可以动态地包装在原始对象之上。

MyBatis 的缓存实现与装饰器模式的对应关系如下:

装饰器模式角色MyBatis 缓存实现具体功能
Component 接口Cache 接口定义缓存接口
ConcreteComponentPerpetualCache缓存的基础实现,需要被包装的原始对象
ConcreteDecoratorLruCache 类、BlockingCache 类、LoggingCache 类…具体的缓存装饰器,用于包装 PerpetualCache,以实现缓存功能的动态扩展

具体的类结构图可以参考:
装饰器模式最大的作用,就是为已有的组件动态地扩展新的功能。

注:MyBatis 中并没有为缓存装饰器定义公共的基类,这与标准的装饰器模式不完全一致,但是并不影响具体功能。

装饰器模式最大的作用,就是可以基于已有的组件,动态地扩展新的功能。 例如前面例子中具有多种功能的二级缓存,就可以采用下面这种方式创建:

Cache cache = new ScheduledCache(new BlockingCache(new LruCache(new PerpetualCache())));

这样一来,客户端就可以任意增加自己想要的缓存功能。相较于继承,装饰器模式使得组件在运行期可以根据需要动态的添加功能,甚至对添加的新功能进行自由的组合,具有很强的灵活性与可扩展性。

MyBatis 缓存装饰器

介绍完了 MyBatis 缓存的设计思想之后,我们一起来看几个比较有意思的缓存装饰器实现。

LruCache

首先我们来看下 LruCacheLRU(Least recently used,最近最少使用)可能是最常用的缓存淘汰策略了,它按照访问的顺序将缓存元素排队,当缓存容量达到上限时,会优先将最久未被访问的元素剔除掉。 JDK 中提供了一个集合 LinkedHashMap,它底层采用链表实现,并且支持按照访问顺序排序,将最近被访问到的元素放在链表头部。MyBatis 就直接使用 LinkedHashMap 来实现了一个简单的 LRU 缓存:

/**
 * 基于LRU淘汰算法的缓存装饰器
 */
public class LruCache implements Cache {
    
    private final Cache delegate; //包装底层的缓存实现
    
    private Map<Object, Object> keyMap; //存储所有缓存元素
    
    private Object eldestKey; //记录最早被访问的key,用于淘汰
    
    //...省略非必要代码
    
    public void setSize(final int size) {
        //基于LinkedHashMap实现LRU机制
        keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
            private static final long serialVersionUID = 4267176411845948333L;
            
            //重写removeEldestEntry方法,记录eldestKey
            @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;
        }
    }
    
}

BlockingCache

BlockingCache 的作用是:当查询缓存 miss 时,对当前线程加锁,保证同一时刻只有一个线程去 DB 执行查询操作,这样就避免了高并发场景下,缓存失效造成的大量击穿,实现了对数据库的保护。

/**
 * 阻塞式缓存装饰器
 * 当查询缓存miss时,对当前线程加锁,保证同一时刻只有一个线程去DB执行查询操作。
 * 避免了高并发场景下,缓存失效造成的大量击穿实现了对数据库的保护。
 */
public class BlockingCache implements Cache {
  private long timeout; //阻塞的超时时间
  
  private final Cache delegate; //包装底层的缓存实现
  
  private final ConcurrentHashMap<Object, CountDownLatch> locks;  //缓存key维度的锁

  //...省略非必要代码
  
  @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;
  }

  //...省略非必要代码

  //加锁操作
  private void acquireLock(Object key) {
    CountDownLatch newLatch = new CountDownLatch(1);
    while (true) {
      CountDownLatch latch = locks.putIfAbsent(key, newLatch);
      if (latch == null) {
        break;
      }
      try {
        if (timeout > 0) {
          boolean acquired = latch.await(timeout, TimeUnit.MILLISECONDS);
          if (!acquired) {
            throw new CacheException(
                "Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId());
          }
        } else {
          latch.await();
        }
      } catch (InterruptedException e) {
        throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e);
      }
    }
  }
  
  //释放锁操作
  private void releaseLock(Object key) {
    CountDownLatch latch = locks.remove(key);
    if (latch == null) {
      throw new IllegalStateException("Detected an attempt at releasing unacquired lock. This should never happen.");
    }
    latch.countDown();
  }

  public long getTimeout() {
    return timeout;
  }

  public void setTimeout(long timeout) {
    this.timeout = timeout;
  }
}

但是,对 BlockingCache 缓存的每次操作都会阻塞当前线程,尽管是根据 Cache Key的细粒度锁,但是对性能还是有一定的影响,而且使用不当还存在死锁的风险。因此这个类的作者 Eduardo Macarron 也说了,这是一个简单且低效的版本:

Simple and inefficient version of EhCache’s BlockingCache decorator. It sets a lock over a cache key when the element is not found in cache. This way, other threads will wait until this element is filled instead of hitting the database.
By its nature, this implementation can cause deadlock when used incorrectly.
@author Eduardo Macarron

ScheduledCache

ScheduledCache 的作用就是增加了缓存的定时清理功能。这个清理操作是 lazy 模式的,即在每次 get 和 put 操作时,会校验距离上次执行 clear 操作的时间是否已超过 clearInterval。如果超过,则执行一次 clear 。

/**
 * 具备定时清理功能的缓存装饰器
 */
public class ScheduledCache implements Cache {
  private final Cache delegate; //包装底层的缓存实现
  
  protected long clearInterval; //清理的时间间隔
  
  protected long lastClear; //记录上次清理的时间

  //...省略非必要代码
  
  @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;
  }

}

CacheKey 的设计

在介绍完了 MyBatis 的缓存功能之后,最后我们来看一下缓存 Key 的设计。MyBatis 涉及到的查询场景十分复杂,具体查询的 SQL 语句、SQL 参数、分页信息等等因素,都会影响到缓存是否命中,使用简单的 String 或者 Long 类型变量作为做为缓存 Key 是肯定是无法满足需求的。那么 MyBatis 中的缓存 Key 是如何设计的呢?

MyBatis 定义了 CacheKey 类,其中封装了所有影响缓存命中的因素,主要包括:

  • mappedStatment 的 id(Cache id)
  • 指定查询结果集的范围(分页信息)
  • 查询所使用的 SQL 语句
  • 用户传递给 SQL 语句的实际参数值

CacheKey 封装了以上这些信息,并重写了 hashCode()equals() 方法:

/**
 * 缓存Key定义
 * 内部封装了所有影响缓存命中的因素
 */
public class CacheKey implements Cloneable, Serializable {
  //...省略非必要代码
  
  private final int multiplier; //乘积因子
  private int hashcode; //hashCode
  private long checksum;  //校验和
  private int count;  //影响因素的数量
  private List<Object> updateList; //影响因素列表

  public CacheKey() {
    this.hashcode = DEFAULT_HASHCODE;
    this.multiplier = DEFAULT_MULTIPLIER;
    this.count = 0;
    this.updateList = new ArrayList<>();
  }

  public CacheKey(Object[] objects) {
    this();
    updateAll(objects);
  }

  public int getUpdateCount() {
    return updateList.size();
  }
  
  //每次增加CacheKey的影响因素,都会重新计算一遍内部的各种校验值
  public void update(Object object) {
    int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);

    count++;
    checksum += baseHashCode;
    baseHashCode *= count;

    hashcode = multiplier * hashcode + baseHashCode;

    updateList.add(object);
  }

  public void updateAll(Object[] objects) {
    for (Object o : objects) {
      update(o);
    }
  }
  
  //判断两个CacheKey是否相同
  @Override
  public boolean equals(Object object) {
    if (this == object) {
      return true;
    }
    if (!(object instanceof CacheKey)) {
      return false;
    }

    final CacheKey cacheKey = (CacheKey) object;

    if ((hashcode != cacheKey.hashcode) || (checksum != cacheKey.checksum) || (count != cacheKey.count)) {
      return false;
    }

    for (int i = 0; i < updateList.size(); i++) {
      Object thisObject = updateList.get(i);
      Object thatObject = cacheKey.updateList.get(i);
      if (!ArrayUtil.equals(thisObject, thatObject)) {
        return false;
      }
    }
    return true;
  }

  @Override
  public int hashCode() {
    return hashcode;
  }

  //...省略非必要代码
}

如果两个 CacheKeyhashCode() 相等,且 equals() 方法返回 true,则认为是同一个查询操作,可以直接从缓存中获取数据。

小结

本篇详细介绍了 MyBatis 缓存模块的底层原理,包括缓存的基础实现、具备各种扩展功能的缓存装饰器,以及缓存 Key 的设计思想。个人认为,缓存模块中的精髓就是装饰器设计模式的灵活运用,它使得用户在使用缓存时,可以根据不同的需求来灵活地定制化功能。这种设计思想非常值得我们借鉴。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/766253.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用supervisor启动进程open files too many问题

今天线上出现了open files too many的问题&#xff0c;查看问题&#xff1a; 1. ulimit -a查看系统最大值发现可以开启的文件句柄只有1024个 果断修复&#xff1a; 1. 查看全局配置文件 ls /etc/security/limits.d/ 比如环境中有如下配置文件&#xff0c;20-nproc.conf名字可…

e2e测试框架之Cypress

谈起web自动化测试&#xff0c;大家首先想到的是Selenium&#xff01;随着近几年前端技术的发展&#xff0c;出现了不少前端测试框架&#xff0c;这些测试框架大多并不依赖于Selenium&#xff0c;这一点跟后端测试框架有很大不同&#xff0c;如Robot Framework做Web自动化测试本…

linux -rw-r--r-x的含义

-rw-r--r-x的含义 权限显示位一共为10位&#xff0c;分为四段&#xff0c;从第2位算起&#xff0c;每3个1组 -rw-r--r-x-表示为普通文件文件所属用户拥有的权限rw-&#xff1a;426该用户所属组拥有的权限r--&#xff1a;4其他用户拥有的权限r-x&#xff1a;415 操作英文对应数…

计及需求响应和电能交互的多主体综合能源系统主从博弈优化调度策略(Matlab代码实现)

目录 &#x1f4a5;1 概述 &#x1f4da;2 运行结果 &#x1f389;3 参考文献 &#x1f308;4 Matlab代码、数据、文章 &#x1f4a5;1 概述 文献来源&#xff1a; 摘要&#xff1a;针对区域综合能源系统多物理系统耦合和多利益主体参与特点,如何考虑各运行主体调度地位与调度…

Spring初识(二)

前言 经过前面的学习,我们已经知道spring就是包含了众多方法的Ioc,那么既然是容器,就具备两个功能,我们接下来就是要介绍以下两个功能: 1.将对象存储到容器(spring)中: 2.从容器(spring)中将对象取出来. 这两个功能就应发出来,spring的创建和使用. 一.Spring创建 我们先来说…

web浏览器在线预览Excel,PDF,world文档解决方案

众所周知啊&#xff0c;在web浏览器中是无法直接预览Excel、world文档等文件的&#xff0c;PDF有的浏览器是打开预览&#xff0c;有的浏览器是跳转到下载页&#xff0c;行为不一致也是让开发者头疼的事情。 今天给大家提供一个解决方案&#xff0c;实现office文件在线预览的解…

vue3中使用Vue.Draggable的clone模式及遇到的坑

最近有个需求是从左边一个列表中&#xff0c;拖动列表元素到右边列表中&#xff0c;并且不是移动拖拽&#xff0c;而是复制拖拽&#xff0c;元素在右边列表中可以重复&#xff0c;比如左边是参加某个游戏的队员名单&#xff0c;右边是出场顺序&#xff0c;队员可以重复上场。类…

网络运维工作是什么的?

互联网运维工作&#xff0c;以服务为中心&#xff0c;以稳定、安全、高效为三个基本点&#xff0c;确保公司的互联网业务能够 724 小时为用户提供高质量的服务。 运维人员对公司互联网业务所依赖的基础设施、基础服务、线上业务进行稳定性加强&#xff0c;进行日常巡检发现服务…

【数学建模】数据预处理

在数学建模赛题中&#xff0c;官方给所有选手的数据可能受到主观或客观条件的影响有一定的问题&#xff0c;如果不进行数据的处理而直接使用的话可能对最终的结果造成一定的影响&#xff0c;因此为了保证数据的真实性和建模结果的可靠性&#xff0c;需要在建模之前对数据进行相…

VS工程项目中属性中无Qt设置问题解决方案

VS工程项目中属性中无Qt设置问题解决方案 若VS工程中&#xff0c;创建的是Qt工程&#xff0c;或者从Qt Creator工程转换为VS 工程时&#xff0c;VS项目属性中确无Qt Project Setttings等设置时&#xff0c;可通过如下方案解决 1. 右键项目&#xff0c;在下拉框中选择Qt项 2.…

微信号长时间不用会被腾讯回收

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 估计很多人不知道的一个冷知识&#xff1a;你的微信号并不归你本人所有&#xff0c;你只有账号的使用权&#xff0c;微信账号的所有权归腾讯公司所有。如果长期不使用&#xff0c;或者是违规&#…

MySQL—创建和管理表(六)

1.数据库相关操作 -- 1 创建数据库 -- 1.1 创建一个保存员工信息的数据库 CREATE DATABASE employees; -- 1.2 其他相关指令 -- 1.2.1 查看当前所有数据库 SHOW DATABASES; -- 1.2.2 “使用”一个数据库&#xff0c;使其作为当前数据库 USE employees;-- 2 命名规则 -- 见ppt …

虹科分享 | MACsec-先进的车载网络安全解决方案

Media Access Control Security&#xff08;简称 MACsec&#xff09;是以太网上最先进的安全解决方案。它为以太网上传输的几乎所有帧提供完整性保护、重放保护和可选的机密性保护。与其他解决方案相比&#xff0c;包括了单播、组播和广播消息以及在第2层上运行的所有协议。 M…

3D虚拟展厅和VR全景展厅该如何选择

导言&#xff1a; 在当今数字化时代&#xff0c;传统展厅已逐渐演变为3D虚拟展厅和VR全景展厅。这些技术的广泛应用为企业带来了全新的营销和展示方式。 一&#xff0e;3D虚拟展厅的特点和优势 3D虚拟展厅是一种基于3D技术的虚拟展示空间&#xff0c;通过计算机图像和模拟技术…

【C语言进阶(九)】常见内存错误以及柔性数组

&#x1f493;博主CSDN主页:杭电码农-NEO&#x1f493;   ⏩专栏分类:C语言学习分享⏪   &#x1f69a;代码仓库:NEO的学习日记&#x1f69a;   &#x1f339;关注我&#x1faf5;带你学习更多C语言知识   &#x1f51d;&#x1f51d; 常见内存错误 1. 前言2. 对NULL指针…

伦敦银价格一览表

一目均衡表&#xff08;Ichimoku Kinko Hyo&#xff09;是日本在二次大战前发明的行情分析方法&#xff0c;是全世界技术分析的鼻祖&#xff0c;其功能是提供市场的方向及入市位&#xff0c;被广泛应用于股市、债市和贵金属市场之中。日文KINKO的意思为时空平衡点&#xff0c;H…

Blueprint —— 入门笔记2

BP_Character Animation BlueprintBlend Space 1D 角色按键动画 命令slomo 0.1减慢时间&#xff0c;用于测试&#xff1b; 玩家输入&#xff1a;位移、旋转、缩放&#xff1b; 碰撞检测&#xff1a;执行动作等&#xff1b; 游戏开始 地图&#xff0c;开始游戏页面 打开地图 输入…

用于视觉跟踪的在线特征选择研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

如何在Appium中使用AI定位

当我们在写自动化测试脚本的时候&#xff0c;传统情况下一定要知道元素的属性&#xff0c;如id、name、class等。那么通过AI的方式定位元素可能就不需要知道元素的属性&#xff0c;评价人对元素的判断来定位&#xff0c;比如&#xff0c;看到一个搜索框&#xff0c;直接使用ai:…

Tomcat安装及配置教程(IDEA整合Tomcat)

目录 友情提醒第一章、Tomcat下载与安装1.1&#xff09;Tomcat介绍1.2&#xff09;官网下载 第二章、Tomcat配置环境变量2.1&#xff09;windows环境变量配置2.2&#xff09;验证Tomcat配置是否成功2.3&#xff09;报错解决 第三章、IDEA整合Tomcat3.1&#xff09;打开IDEA开发…