Mybatis源码之缓存模块分析

缓存这个东西在很多应用中都能看到它们的身影,这次就讲讲在Mybatis中的缓存是怎么应用的,虽然说吧Mybatis中的缓存基本不怎么用,用的更多是第三方组件redis、MongoDB、MemCache等等。

Mybatis的缓存是基于Map实现的,从缓存中读写数据是缓存模块的核心功能,但是除了这个功能Mybatis也提供了很多附加功能,比如防止缓存击穿、添加缓存清空策略等等,并且这些附加的功能属性可以随意组合到核心功能上。

缓存在Mybatis中的使用介绍

Myabtis中有两个缓存,一级缓存以及二级缓存,

一级缓存是默认开启的,而二级缓存是在配置文件中开启的,默认是开启,但是可以配置不开启,Mybatis配置参数定义

一级缓存的仅仅存在于同一SqlSession当中,如果关闭了SqlSession的话,那么这个缓存就没有了,

二级缓存是跨SqlSession的存在,同一个SqlSessionFactory是有效的,在多个SqlSession当中,也会存在多个二级缓存的存在,所以就容易出现数据脏读,个人建议二级缓存还是禁止比较好,使用第三方的缓存组件。

在对应的Mapper.xml当中使用:

eviction:使用什么类型的缓存,默认LRU缓存,缓存满后,将使用的最少的缓存去除;

flushInterval:刷新时间;

readOnly:是否只读;

size:缓存大小;

blocking:是否采用阻塞策略;

 <cache eviction="FIFO" flushInterval="6000" readOnly="false" size="1024"></cache>

 缓存的引用,直接引用对应的工作空间,引用后,就会直接使用这个引用的缓存:

 <cache-ref namespace="xxx.xxx.xxxDao"></cache-ref>

设计模式

先来看看这个模块用到了哪些设计模式

装饰器模式(Decorator Pattern)

定义:允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。

这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。

像这种组合附加功能的业务上也可以通过继承去组合相对应的属性,但是继承的方式是静态的,并不能控制增加新的属性,如果功能属性较多的话,那么使用继承就会有很多子类,可读性不强。

图示:

组件(Component):组件接口定义了全部组件类 和装饰器实现的行为;

组件实现类(ConcreteComponent):实现 Component接口,组件实现类就是被装饰器装饰的 原始对象,新功能或者附加功能都是通过装饰器添加 到该类的对象上的;

装饰器抽象类(Decorator):实现Component接 口的抽象类,在其中封装了一个Component 对象, 也就是被装饰的对象;

具体装饰器类(ConcreteDecorator):该实现类 要向被装饰的对象添加某些功能;

相对于继承来说,使用装饰器模式的灵活性更高,扩展性更强。

装饰器使用:

JDK中的IO输入流与输出流设计:

BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(new FileInputStream("c://a.txt")));

在Servlet API当中的对request的封装类HttpServletRequestWrapper;

源码分析

Mybatis的缓存模块就是使用的装饰器模式

BlockingCache示例:

同一个缓存的接口Cache(这个接口是缓存模块的核心接口,定义了缓存的基本操作):

public interface Cache {

  /**
   * @return The identifier of this cache
   */
  String getId();

  /**
   * @param key
   *          Can be any object but usually it is a {@link CacheKey}
   * @param value
   *          The result of a select.
   */
  void putObject(Object key, Object value);

  /**
   * @param key
   *          The key
   * @return The object stored in the cache.
   */
  Object getObject(Object key);

  /**
   * @param key
   *          The key
   * @return Not used
   */
  Object removeObject(Object key);

  /**
   * Clears this cache instance.
   */
  void clear();

  /**
   * Optional. This method is not called by the core.
   *
   * @return The number of elements stored in the cache (not its capacity).
   */
  int getSize();

  /**
   * Optional. As of 3.2.6 this method is no longer called by the core.
   * <p>
   * Any locking needed by the cache must be provided internally by the cache provider.
   *
   * @return A ReadWriteLock
   */
  default ReadWriteLock getReadWriteLock() {
    return null;
  }

}

在不同属性的实现类分别封装一个Cache对象,实现了每个附加功能的组合。

FifoCache:一个先进先出的缓存,如果缓存的数量达到了临界点,则将缓存时间最长的那个缓存去除。

LoggingCache:附加了日志以及统计功能。

 LruCache:如果缓存数据达到了临界点,会将访问次数最少的缓存数据清空。

ScheduledCache:会定时清除缓存的一个缓存。

SerializedCache:一个可以序列化以及反序列化的缓存。

SoftCache:软引用缓存,如果缓存被GC回收后,对应的缓存数据也会被清空。

WeakCache:弱引用缓存,如果缓存被GC回收后,对应的缓存数据也会被清空。

SynchronizedCache:同步运行的缓存。

TransactionalCache:一个存在事务提交和回滚的缓存。

BlockingCache:包含阻塞机制的缓存

在缓存模块中用到的缓存类实际上是PerpetualCache,以上的那些缓存类仅仅是附加功能,具体实现缓存功能的是PerpetualCache。

public class PerpetualCache implements Cache {

  private final String id;
  //数据被缓存的地方
  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();
  }

  @Override
  public boolean equals(Object o) {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    if (this == o) {
      return true;
    }
    if (!(o instanceof Cache)) {
      return false;
    }

    Cache otherCache = (Cache) o;
    return getId().equals(otherCache.getId());
  }

  @Override
  public int hashCode() {
    if (getId() == null) {
      throw new CacheException("Cache instances require an ID.");
    }
    return getId().hashCode();
  }

}

一个比较简单的基于Map的缓存实现类。

缓存击穿

对于数据库来说,缓存是一个很好的东西,它能够挡住大部分的数据请求,但是如果在缓存中找不到对应的key和value时,那么这个这些请求就会去请求数据库,如果这个请求是在高并发的情况或者数据是“热数据”的情况,那么数据库访问那么也会变成高并发访问查询,这就是缓存击穿了。

所以在设计缓存模块的时候就应该考虑到这个问题,在Mybatis里面这个问题已经得到解决,BlockingCache就是解决方法。

我们先来看看缓存击穿的一些解决办法:

第一种:如果当前缓存被击穿后,不应该让所有的请求去请求数据库,而是只让一个请求去请求数据库,其他线程等待,当数据查询完毕并缓存进去后,再让其他线程去查询缓存。

但是这个方法存在一个问题,面对大量的请求来说,就会有效率问题。

第二种:既然以上方法存在效率问题,那么就可以给同等类型的key上锁,让相同的请求只派出一个请求去请求数据库,其他同等类型key的请求就等待,当请求到后,那么就释放锁其他线程回去缓存中获取数据了。

但是如果key太多的话,会比较浪费资源。

综上所述:对应这种缓存的话,个人觉得应该要与业务挂钩,取得一个平衡点取设计缓存模块。

现在我们来看看BlockingCache是怎么玩的,其实就是给每个key上锁然后,请求完在释放锁。

我目前的Mybatis版本是最新的3.5.7,可能其他版本并非使用的事CountDownLatch,而是使用的ReentrantLock,基本都是一样,基于AQS同步,想要了解线程相关的东西,可以去看看我之前发布博客哟。

CacheKey

现在来看一下缓存的key是怎么组成的,缓存模块中的key值并非直接的使用SQL语句中的唯一ID啥的。

在Mybatis设计缓存时,就将key包装成一个对象,只有这个对象的hashCode相等,这两个key才能一致。

构成CacheKey的因素有四个:

1、mappedStatment的id ;

2、指定查询结果集的范围(分页信息);

3、查询所使用的SQL语句;

4、用户传递给SQL语句的实际参数值;

update方法

equals方法

Mybatis的缓存基本讲的差不多了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值