MyBatis中的缓存机制

1、缓存概述

  • 缓存的概念:

缓存是应用程序和物理数据源之间数据存储区,其作用是为了降低应用程序对物理数据源访问的频次,从而提高应用程序的运行性能;


缓存内的数据是对物理数据源的复制,应用程序在运行时从缓存中读取数据,在特定的时刻或事件中会同步缓存和物理数据源的数据(保持数据的同步);

  • 计算机关于数据读取的一些硬件:

cpu【一级缓存,二级缓存】
内存【缓存】
硬盘【永久数据】

  • 缓存的应用场景:

比如说,游戏中的榜单,对整个玩家数据进行统计。
优化方案:将榜单统计数据储存在内存中,玩家请求榜单排行操作,程序直接从内存中获取,可以设置为五分钟重新读取一次,此时的数据也许并不准确;

短信验证码:注册账号,或者找回密码操作中都会收到一个验证码。验证码储存在缓存中,并在一定的时间之后验证码失效;

新闻信息:第一个人看大的新闻和第1000万看到的是一样的,除非新闻修改了,因此新闻也可以存储在缓存中,动态页面静态化,生成的是html;

  • MyBatis是常见的Java数据库访问层框架。在日常工作中,开发人员多数情况下是使用MyBatis的默认缓存配置,但是MyBatis缓存机制有一些不足之处,在使用中容易引起脏数据,形成一些潜在的隐患;
  • 【缓存的意义】

操作数据库的速率快、提升系统性能、减少数据库压力;
常用的缓存框架有:ehcache(hibernate推荐)memcached(键值对)、redis(支持数据类型,数据的运算);

2、memcached使用详解

  • Memcached是国外社区网站 LiveJournal 的开发团队开发的一套高性能的分布式内存对象缓存服务器。
  • 它将所有的数据统统保存在内存中,在内存中会维护一个巨大的 hash表,支持任意存储类型的数据,很多网站通过Memcached提高网站的访问速度,尤其是对于大型的需要频繁访问的网站,减少查询效率,提高查询速度;
  • 缓存的应用系统:

计算机体系存储系统模型扩展到应用也是一样,应用需要数据,数据哪里来?缓存(更快的存储)———>DB(较慢的存储),它们的工作流程大致如下图所示:

img

Memcache架构

服务端:Memcached 服务端,通过C语言编写而成;
客户端:Memcached API客户端,可以通过任何语言编写,如php、py等;

  • 特点:

1、为了提高性能,memcached中保存的数据都存储在memcached内置的内存存储空间中。由于数据仅存在于内存中,因此重启memcached、重启操作系统会导致全部数据消失;

2、基于libevent的事件处理:libevent是个程序库,它将Linux的epoll、BSD类操作系统的kqueue等事件处理功能封装成统一的接口。即使对服务器的连接数增加,也能发挥I/O的性能。memcached使用这个libevent库,因此能在Linux、BSD、Solaris等操作系统上发挥其高性能;

3、简单key/value存储:服务器不关心数据本身的意义及结构,只要是可序列化数据即可。存储项由"键、过期时间、可选的标志及数据"四个部分组成;

4、功能的实现一半依赖于客户端,一半基于服务器端:客户负责发送存储项至服务器端、从服务端获取数据以及无法连接至服务器时采用相应的动作;服务端负责接收、存储数据,并负责数据项的超时过期;

  • 运行架构:

img

Memcached缓存存储策略

使用内存缓存策略:Slab Allocation机制

【Slab Allocation机制的基本原理】:按照预先规定的大小,将分配的内存分割成特定长度的块(chunk),并把尺寸相同的块分成组,以完全解决内存碎片问题。但由于分配的是特定长度的内存,因此无法有效利用分配的内存。比如将100字节的数据缓存到128字节的chunk中,剩余的28字节就浪费了;按照预先规定的大小,将分配的内存分割成特定长度的内存块(chunk),再把尺寸相同的内存块分层组(chunk集合),这些内存不会释放,可以反复利用;
img

  • Slab Allocation 机制角色:

1.Chunk为固定大小的内存空间,默认为96Byte。

2.page对应实际的物理空间,1个page为1M。

3.同样大小的chunk又称为slab。

  • 客户端选择slab机制:

下面说明memcached如何针对客户端发送的数据选择slab并缓存到chunk中。memcached根据收到的数据的大小,选择最适合数据大小的slab。 memcached中保存着slab内空闲chunk的列表,根据该列表选择chunk, 然后将数据缓存于其中。如下图;

img

内存释放机制

Laxzy Expiration:Memcached每个被存取的对象都有唯一的标识符key,存取操作均通过key进行,例如可以把后端数据库中的select操作提取出来,然后对相应的SQL进行hash计算得出key,然后以这个key在memcached中查找数据,如果数据不存在,说明其尚未被写入缓存中,并设置一个失效时间(比如1小时),在失效时间内的数据都是从缓存中提取,这样就有效地减少了数据库的压力;

Least Recently Used(LRU):删除“最近最少使用”的记录的机制。当memcached的内存空间不足时,从最近未被使用的记录中搜索,并将其空间分配给新的记录。-M 参数禁止LRU功能,内存用尽时,memcached会返回错误,不建议使用memcached -M -m 1024;

Memcached优缺点

【优点】

1.读写性能优异,特别是高并发时和文件缓存比有明显优势;

2.memcached组建支持集群,并且是自动管理负载均衡;

3.开源,占用资源小,协议简单的软件,实现了数据库和web之间的数据缓存功能,减少数据库的检索次数,减少数据库的I/O,解决了架构数据库端的压力;

4.存储方式:内置于内存存储方式,存取的效率高,执行的速度快;

【缺点】

1.缓存空间有限:据说一台电脑的mem缓存开到2g以上会出现不稳定,数据无故丢失的现象;

2.掉电丢失数据:由于是把数据放在内存里的,所有一旦机器掉电,数据也就全部丢失了。

一般建议:而mem则适合放一些频繁更改的数据,比如可以把session数据放进mem;

安装

  • 首先下载Win下的Memcached,解压到指定目录。
  • memcached.exe -d install 安装memcached服务
  • 然后通过Memcached start memcached就启动了。

在这里插入图片描述

  • 常用命令:
-p 监听的端口 
-l 连接的IP地址, 默认是本机 
-d start 启动memcached服务 
-d restart 重起memcached服务 
-d stop|shutdown 关闭正在运行的memcached服务 
-d install 安装memcached服务 
-d uninstall 卸载memcached服务 
-u 以的身份运行 (仅在以root运行的时候有效) 
-m 最大内存使用,单位MB。默认64MB 
-M 内存耗尽时返回错误,而不是删除项 
-c 最大同时连接数,默认是1024 
-f 块大小增长因子,默认是1.25 
-n 最小分配空间,key+value+flags默认是48 
-h 显示帮助 ​       
  • 官网的地址为:http://memcached.org/

  • Java中memcached客户端程序有很多,这里我们使用xmemcached

——首先导入项目依赖:

<!-- https://mvnrepository.com/artifact/com.googlecode.xmemcached/xmemcached -->
    <dependency>
        <groupId>com.googlecode.xmemcached</groupId>
        <artifactId>xmemcached</artifactId>
        <version>2.4.6</version>
    </dependency>

——编写Java代码:

import net.rubyeye.xmemcached.MemcachedClient;
import net.rubyeye.xmemcached.MemcachedClientBuilder;
import net.rubyeye.xmemcached.XMemcachedClientBuilder;
import net.rubyeye.xmemcached.utils.AddrUtil;

public class memcacheTest {
    public static void main(String[] args) {
        MemcachedClientBuilder builder = new XMemcachedClientBuilder(AddrUtil.getAddresses("127.0.0.1:11211"));
        MemcachedClient memcachedClient;
        try {
            memcachedClient = builder.build();
            //第二个参数为存活时间,0 是不过期,单位为 秒 //
            //memcachedClient.set("hello3", 100, "Hello,xmemcached");
            String value = memcachedClient.get("hello3");
            System.out.println("hello=" + value);
//            memcachedClient.delete("hello");
//            value = memcachedClient.get("hello");
//            System.out.println("hello=" + value);
            memcachedClient.shutdown();
        } catch (Exception e) {
            System.err.println("MemcachedClient operation fail");
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

  • 客户端工具treeNMS,是BS架构的,用于查看缓存中的数据:

在这里插入图片描述
在这里插入图片描述

完成上面的一系列操作,需要同时开启memcached的客户端与服务端,并在pom.xml中导入xmemcached的jar包,最后在浏览器中输入客户端网址,进行查看此时缓存中的内容;

  • 注意:

1、打开memcached的方式有两种,第一种是直接双击打开,第二种是使用命令的方式:telnet 127.0.0.1 1121111211是memcached的端口;

2、服务端是一次性的,如果关闭服务端,缓存在里面的数据就都消失了;

3、一级缓存

  • 参考原文链接:https://tech.meituan.com/2018/01/19/mybatis-cache.html
  • 在应用程序过程中,我们有可能再一次数据库会话中,执行多次查询条件完全相同的SQL,MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能;具体执行过程如下图所示:

在这里插入图片描述

  • 每个SqlSession中持有了Excutor,每个Excutor中有一个LocalCache;当用户发起查询时,MyBatis根据当前执行的语句生成MapperStatement,在LocalCache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户;具体实现类关系图如下图所示:

在这里插入图片描述

  • 一级缓存配置:

开发者需要在MyBatis的配置文件中添加语句:

<setting name="localCacheScope" value="SESSION"/>

就可以使用一级缓存,共有两个选项,SESSION或者STATEMENT,默认是SESSION级别,即就是在一个Mybatis会话中执行的所有语句,都会共享这一个缓存;一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个STATEMENT有效,这里一定要注意大小写

一级缓存是SqlSession级别的缓存。在操作数据库时需要构造 sqlSession对象,在对象中有一个(内存区域)数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。

一级缓存的作用域是同一个SqlSession,在同一个sqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。当一个sqlSession结束后该sqlSession中的一级缓存也就不存在了,Mybatis默认开启一级缓存。

  • 一级缓存实验:

1、mapper执行多次相同的条件查询,结果是缓存的

@Test
public void select1(){
    AccountMapper mapper = sqlSession.getMapper(AccountMapper.class);
    System.out.println(JSON.toJSONString(mapper.selectByPrimaryKey(2), true));

    System.out.println("----------------------------------------");

    System.out.println(JSON.toJSONString(mapper.selectByPrimaryKey(2), true));
}

在这里插入图片描述

两次查询采用的是一个SqlSession和一个Mapper文件,只进行了一次查询;

2、不同的SqlSession,查询同一个条件

@Test
public void select2() {
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();

    AccountMapper mapper1 = sqlSession1.getMapper(AccountMapper.class);
    AccountMapper mapper2 = sqlSession2.getMapper(AccountMapper.class);

    System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));

    System.out.println("----------------------------------------");

    System.out.println(JSON.toJSONString(mapper2.selectByPrimaryKey(2), true));
}

在这里插入图片描述

最终结果是进行了两次查询;

3、同一个SqlSession,获取多次mapper,mapper执行多次相同的条件查询

@Test
public void select3() {
    SqlSession sqlSession1 = sqlSessionFactory.openSession();

    AccountMapper mapper1 = sqlSession1.getMapper(AccountMapper.class);
    AccountMapper mapper2 = sqlSession1.getMapper(AccountMapper.class);

    System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));

    System.out.println("----------------------------------------");

    System.out.println(JSON.toJSONString(mapper2.selectByPrimaryKey(2), true));
}

在这里插入图片描述

这里查询了一次;

4、不同查询条件,相同SqlSession

@Test
public void select3() {
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    AccountMapper mapper1 = sqlSession1.getMapper(AccountMapper.class);

    System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));

    System.out.println("----------------------------------------");

    System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(3), true));
}

在这里插入图片描述

这里很明显查询条件都不同,查询了两次;

5、当调用SqlSession的增删改查方法,会清空缓存数据,这样做是为了防止数据的脏读

@Test
public void select4() {
    SqlSession sqlSession = sqlSessionFactory.openSession();
    AccountMapper mapper = sqlSession.getMapper(AccountMapper.class);

    System.out.println(JSON.toJSONString(mapper.selectByPrimaryKey(2), true));

    System.out.println("----------------------------------------");

    Account account = new Account();
    account.setaNikename("mybatis缓存");
    account.setAname("Java");
    account.setApass("mybatis");

    mapper.insert(account);
    System.out.println("插入数据完成!");
    sqlSession.commit();

    System.out.println("----------------------------------------");

    System.out.println(JSON.toJSONString(mapper.selectByPrimaryKey(2), true));
}

在这里插入图片描述
6、不同的SqlSession,a在查询,b修改数据,a再次查询,数据就会不一致(a读取缓存)

@Test
public void select5() {
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    AccountMapper mapper1 = sqlSession1.getMapper(AccountMapper.class);
    AccountMapper mapper2 = sqlSession2.getMapper(AccountMapper.class);

    //a查询
    System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));

    System.out.println("----------------------------------------");

    //b修改
    Account account = new Account();
    account.setaNikename("mybatis缓存");
    account.setAname("Java");
    account.setApass("mybatis");
    account.setAid(2);
    mapper2.updateByPrimaryKey(account);

    System.out.println("修改数据完成!");
    sqlSession2.commit();

    System.out.println("----------------------------------------");

    System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
}

在这里插入图片描述
7、可以手动清除一级缓存

@Test
public void select6() {
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    AccountMapper mapper1 = sqlSession1.getMapper(AccountMapper.class);
    AccountMapper mapper2 = sqlSession2.getMapper(AccountMapper.class);

    //a查询
    System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));

    System.out.println("----------------------------------------");

    //b修改
    Account account = new Account();
    account.setaNikename("mybatis缓存");
    account.setAname("Java");
    account.setApass("mybatis");
    account.setAid(2);
    mapper2.updateByPrimaryKey(account);

    System.out.println("修改数据完成!");
    sqlSession2.commit();

    System.out.println("----------------------------------------");

    //手动清除缓存
    sqlSession1.clearCache();

    System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
}

在这里插入图片描述

清除缓存之后,进行了第二次的查询;

8、flushCache=true 清空缓存,每一次都是物理查询

	<select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap" flushCache="true">
    select 
    <include refid="Base_Column_List" />
    from account account
    where account.aid = #{aid,jdbcType=INTEGER}
  </select>

在这里插入图片描述

  • 小结一级缓存:

1、一级缓存确实存在,同一个sqlsession获取一个代理接口对象,不论mapper接口的查询方法执行几次,都会缓存查询结果的;当执行了sqlsession的增删改方法或者调用sqlsession.clearCache()方法或者<select>标签上的flushCache="true"都会清空缓存;

2、一级缓存有什么意义?

在同一个sqlsession中执行相同的多次查询条件,查询的结果会从缓存直接返回,降低物理查询的频次,提升性能;

3、sqlsession的作用域在方法块中,mapper接口的代理方法中,都持有一个sqlsession;

4、缓存有缓存命中率之说,缓存的命中率越高,缓存越有意义;

4、一级缓存源码分析

  • 一级缓存执行的时序图,如下图所示:

在这里插入图片描述
SqlSession:对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节;默认实现类是DefaultSqlSession

在这里插入图片描述
ExcutorSqlSession向用户提供操作数据库的方法,但和数据库操作有关的职责都会委托给Excutor;

在这里插入图片描述
如下图所示,执行器Excutor有若干实现类,为Excutor赋予了不同的能力:
在这里插入图片描述
在一级缓存的源码分析中,主要学习BaeExecutor的内部实现;

BaseExcutorBaseExcutor是一个实现了Excutor接口的抽象类,定义若干抽象方法,在执行的时候,把具体的操作委托给子类进行执行;

protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;
protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;
protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException;

在一级缓存的介绍中提到了对Local Cache的查询和写入是在Excutor内部完成的,其实Local CacheBaseExcutor内部的一个成员变量:

public abstract class BaseExecutor implements Executor {
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache;

CaChe:MyBatis中的Cache接口,提供了和缓存相关的最基本的操作,如下图所示:

在这里插入图片描述
有若干实现类,使用装饰者模式互相组装,提供丰富的操控缓存的能力,部分实现类如下图所示:
在这里插入图片描述
BaseExcutor成员变量之一的PerpetualCache,是对Cache接口最基本的实现,其实现非常简单,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作,如下面的代码所示:

public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();

在阅读相关类代码之后,从源代码层面对一级缓存工作中涉及到相关代码:

为执行和数据库的交互,首先需要初始化SqlSession,通过DefaultSqlSessionFactory开启SqlSession

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    ............
    final Executor executor = configuration.newExecutor(tx, execType);     
    return new DefaultSqlSession(configuration, executor, autoCommit);
}

在初始化SqlSesion时,会使用Configuration类创建一个全新的Executor,作为DefaultSqlSession构造函数的参数,创建Executor代码如下所示:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    // 尤其可以注意这里,如果二级缓存开关开启的话,是使用CahingExecutor装饰BaseExecutor的子类
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);                      
    }
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
}

SqlSession创建完毕后,根据Statment的不同类型,会进入SqlSession的不同方法中,如果是Select语句的话,最后会执行到SqlSessionselectList,代码如下所示:

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
      MappedStatement ms = configuration.getMappedStatement(statement);
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}

SqlSession把具体的查询职责委托给了Executor。如果只开启了一级缓存的话,首先会进入BaseExecutor的query方法。代码如下所示:

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

在上述代码中,会先根据传入的参数生成CacheKey,进入该方法查看CacheKey是如何生成的,代码如下所示:

CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
//后面是update了sql中带的参数
cacheKey.update(value);

在上述的代码中,将MappedStatement的 Id、SQL的offset、SQL的limit、SQL本身以及SQL中的参数传入了CacheKey这个类,最终构成CacheKey。以下是这个类的内部结构:

private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;

private int multiplier;
private int hashcode;
private long checksum;
private int count;
private List<Object> updateList;

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

首先是成员变量和构造函数,有一个初始的hachcode和乘数,同时维护了一个内部的updatelist。在CacheKey的update方法中,会进行一个hashcode和checksum的计算,同时把传入的参数添加进updatelist中。如下代码所示:

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);
}

同时重写了CacheKey的equals方法,代码如下所示:

@Override
public boolean equals(Object object) {
    .............
    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;
}

除去hashcode、checksum和count的比较外,只要updatelist中的元素一一对应相等,那么就可以认为是CacheKey相等。只要两条SQL的下列五个值相同,即可以认为是相同的SQL。

Statement Id + Offset + Limmit + Sql + Params

BaseExecutor的query方法继续往下走,代码如下所示:

list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
    // 这个主要是处理存储过程用的。
    handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}

如果查不到的话,就从数据库查,在queryFromDatabase中,会对localcache进行写入。

在query方法执行的最后,会判断一级缓存级别是否是STATEMENT级别,如果是的话,就清空缓存,这也就是STATEMENT级别的一级缓存无法共享localCache的原因。代码如下所示:

if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        clearLocalCache();
}

在源码分析的最后,我们确认一下,如果是insert/delete/update方法,缓存就会刷新的原因。

SqlSession的insert方法和delete方法,都会统一走update的流程,代码如下所示:

@Override
public int insert(String statement, Object parameter) {
    return update(statement, parameter);
  }
   @Override
  public int delete(String statement) {
    return update(statement, null);
}

update方法也是委托给了Executor执行。BaseExecutor的执行方法如下所示:

@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
    ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    clearLocalCache();
    return doUpdate(ms, parameter);
}

每次执行update前都会清空localCache。

至此,一级缓存的工作流程讲解以及源码分析完毕。

5、二级缓存

  • 在上文提到的一级缓存中,最大的共享范围就是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存;开启二级缓存之后,会使用CachingExcutor装饰Excutor,进入一级缓存的查询流程前,先CachingExecutor进行二级缓存的查询,具体的工作流程为:

在这里插入图片描述

  • 二级缓存开启之后,同一个namespace下的所有操作语句,都影响着一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量;

  • 当开启缓存之后,数据的查询的执行流程就是二级缓存——>一级缓存——>数据库

  • 二级缓存配置:

要正确的使用二级缓存,需要完成以下的配置:

1、

<setting name="cacheEnabled" value="true"/>

2、

MyBatis的映射文件XML中配置cache或者cache-ref

cache标签用于声明这个namespace使用二级缓存,并且可以自定义配置;

<cache/>

type:cache使用的类型,默认是PerpetualCache,这在一级缓存中提到过。
eviction:定义回收的策略,常见的有FIFO,LRU。
flushInterval: 配置一定时间自动刷新缓存,单位是毫秒。
size:最多缓存对象的个数。
readOnly: 是否只读,若配置可读写,则需要对应的实体类能够序列化。
blocking:若缓存中找不到对应的key,是否会一直blocking,直到有对应的数据进入缓存。

  • cache-ref代表引用别的命名空间的Cache配置,两个命名空间的操作使用的是同一个Cache。
<cache-ref namespace="mapper.StudentMapper"/>
  • 二级缓存实验:

1、不同的SqlSession,没有提交的时候不贡献缓存

@Test
public void select1() throws InterruptedException {
    AccountMapper mapper1 = sqlSessionFactory.openSession().getMapper(AccountMapper.class);
    AccountMapper mapper2 = sqlSessionFactory.openSession().getMapper(AccountMapper.class);

    System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));

    Thread.sleep(1000);
    System.out.println("----------------------------------------");

    System.out.println(JSON.toJSONString(mapper2.selectByPrimaryKey(2), true));
}

在这里插入图片描述

这里没有使用到缓存,是因为事务没有提交;提交之后:

在这里插入图片描述

2、二级缓存是基于namespace的,不同的namespace就算是执行相同的SQL,相同的参数也不会共享数据

@Test
public void select2() throws InterruptedException {
    SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
    SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
    AccountMapper mapper1 = sqlSession1.getMapper(AccountMapper.class);
    BooksDao mapper2 = sqlSession2.getMapper(BooksDao.class);

    System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
    sqlSession1.commit();

    System.out.println("----------------------------------------");

    System.out.println(JSON.toJSONString(mapper2.selectByPrimaryKey(2), true));
}

在这里插入图片描述

这里二级缓存是不起作用的;

3、不同的SqlSession,操作同一个namespace,进行增删改查的时候,清空namespace

@Test
public void select3() throws InterruptedException {
    SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
    SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
    AccountMapper mapper1 = sqlSession1.getMapper(AccountMapper.class);
    AccountMapper mapper2 = sqlSession1.getMapper(AccountMapper.class);

    System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
    sqlSession1.commit();

    System.out.println("----------------------------------------");

    mapper2.deleteByPrimaryKey(25);
    sqlSession2.commit();

    System.out.println("----------------------------------------");

    System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(2), true));
}

在这里插入图片描述

memcached缓存数据

  • 我们现在的数据存储在二级缓存默认的实现hashmap中,但是数据应该存储在缓存服务器当中,理论上来说,可以指定无数种二级缓存,只要程序实现mybatis中的Cache接口即可;
  • memcached——纯内存,redis——功能更强大,ehcache——hibernate推荐的缓存框架;
  • 如何在mybatis中使用memcached缓存数据;

步骤:

1、导入memcached for mybatis 缓存的jar包:

<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-memcached -->
<dependency>
    <groupId>org.mybatis.caches</groupId>
    <artifactId>mybatis-memcached</artifactId>
    <version>1.0.0</version>
</dependency>

2、编写memcached.properties的配置文件,"约定大于配置"思想:

#any string identifier
org.mybatis.caches.memcached.keyprefix=_biz-cache-wk_
#space separated list of ${host}:${port}
org.mybatis.caches.memcached.servers=127.0.0.1:11211
#org.mybatis.caches.memcached.servers=192.168.0.44:12000
#Any class that implementsnet.spy.memcached.ConnectionFactory
org.mybatis.caches.memcached.connectionfactory=net.spy.memcached.DefaultConnectionFactory
#the number of seconds in 30 days    the expiration time (in seconds)
org.mybatis.caches.memcached.expiration=6000
#flag to enable/disable the async get
org.mybatis.caches.memcached.asyncget=false
#the timeout when using async get
org.mybatis.caches.memcached.timeout=5
#the timeout unit when using async get
org.mybatis.caches.memcached.timeoutunit=java.util.concurrent.TimeUnit.SECONDS
#if true, objects will be GZIP compressed before putting them to
org.mybatis.caches.memcached.compression=false

#refuse time when connection refused
org.mybatis.caches.memcached.refuseperiod=1000

3、在mapper.xml中指定二级缓存的实现类:

<cache type="org.mybatis.caches.memcached.MemcachedCache"/>

4、进行测试:

开启memcached缓存的服务端,以及客户端软件TreeSoft,编写测试案例:

@Test
public void select1() throws InterruptedException {
    SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
    SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
    BooksDao mapper1 = sqlSession1.getMapper(BooksDao.class);
    BooksDao mapper2 = sqlSession2.getMapper(BooksDao.class);

    System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(3), true));
    sqlSession1.commit();

    System.out.println("----------------------------------------");

    System.out.println(JSON.toJSONString(mapper2.selectByPrimaryKey(3), true));
}

在这里插入图片描述
在这里插入图片描述

二级缓存起到了作用,并出现了Cache Hit Ratio [com.oracle.mapper.BooksDao]: 0.5

  • 可以发现,只有二级缓存才有日志信息的输出,一级缓存没有;

二级缓存中数据的脏读:

1、单表操作的时候,先查询数据,再修改数据,再查询数据,数据是没有问题的:因为二级缓存的作用域是整个namespace,出了这个范围,缓存不起作用;并且更新、修改、删除操作也会清空缓存;

@Test
public void select3() throws InterruptedException {
    SqlSession sqlSession1 = sqlSessionFactory.openSession(true);
    SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
    BooksDao mapper1 = sqlSession1.getMapper(BooksDao.class);
    BooksDao mapper2 = sqlSession1.getMapper(BooksDao.class);

    System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(3), true));
    sqlSession1.commit();

    System.out.println("----------------------------------------");

    Books books = new Books();
    books.setBookid(3);
    books.setBookname("C#入门");
    books.setBprice(200d);
    books.setAccountid(4);
    mapper2.updateByPrimaryKey(books);
    sqlSession2.commit();

    System.out.println("----------------------------------------");

    System.out.println(JSON.toJSONString(mapper1.selectByPrimaryKey(3), true));
}

在这里插入图片描述

2、多表联合查询的时候,读取数据是会存在问题的,因为二级缓存是基于namespace的,多表不属于一个namespace;

为了解决实验上述问题,可以使用Cache ref,让一个Mapper引用另外一个Mapper的命名空间,这样两个映射文件对应的SQL操作都使用的是同一块缓存了。不过这样做的后果是,缓存的粒度变粗了,多个Mapper namespace下的所有操作都会对缓存使用造成影响。

6、二级缓存源码分析

MyBatis二级缓存的工作流程和前文提到的一级缓存类似,只是在一级缓存处理前,用CachingExcutor装饰了BaseExcutor的子类,在委托具体职责给delegate之前,实现了二级缓存的查询和写入功能;

在这里插入图片描述
CachingExecutor的query方法,首先会从MappedStatement中获得在配置初始化时赋予的Cache。

Cache cache = ms.getCache();

本质上是装饰者模式的使用,具体的装饰链是:

SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache

在这里插入图片描述
以下是具体这些Cache实现类的介绍,他们的组合为Cache赋予了不同的能力。

  • SynchronizedCache:同步Cache,实现比较简单,直接使用synchronized修饰方法。
  • LoggingCache:日志功能,装饰类,用于记录缓存的命中率,如果开启了DEBUG模式,则会输出命中率日志。
  • SerializedCache:序列化功能,将值序列化后存到缓存中。该功能用于缓存返回一份实例的Copy,用于保存线程安全。
  • LruCache:采用了Lru算法的Cache实现,移除最近最少使用的Key/Value。
  • PerpetualCache: 作为为最基础的缓存类,底层实现比较简单,直接使用了HashMap。

然后是判断是否需要刷新缓存,代码如下所示:

flushCacheIfRequired(ms);

在默认的设置中SELECT语句不会刷新缓存,insert/update/delte会刷新缓存。进入该方法。代码如下所示:

private void flushCacheIfRequired(MappedStatement ms) {
    Cache cache = ms.getCache();
    if (cache != null && ms.isFlushCacheRequired()) {      
      tcm.clear(cache);
    }
}

MyBatis的CachingExecutor持有了TransactionalCacheManager,即上述代码中的tcm;TransactionalCacheManager中持有了一个Map,代码如下所示:

private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

这个Map保存了Cache和用TransactionalCache包装后的Cache的映射关系。TransactionalCache实现了Cache接口,CachingExecutor会默认使用他包装初始生成的Cache,作用是如果事务提交,对缓存的操作才会生效,如果事务回滚或者不提交事务,则不对缓存产生影响。

在TransactionalCache的clear,有以下两句。清空了需要在提交时加入缓存的列表,同时设定提交时清空缓存,代码如下所示:

@Override
public void clear() {
	clearOnCommit = true;
	entriesToAddOnCommit.clear();
}

CachingExecutor继续往下走,ensureNoOutParams主要是用来处理存储过程的,暂时不用考虑。

if (ms.isUseCache() && resultHandler == null) {
	ensureNoOutParams(ms, parameterObject, boundSql);

之后会尝试从tcm中获取缓存的列表。

List<E> list = (List<E>) tcm.getObject(cache, key);

在getObject方法中,会把获取值的职责一路传递,最终到PerpetualCache。如果没有查到,会把key加入Miss集合,这个主要是为了统计命中率。

Object object = delegate.getObject(key);
if (object == null) {
	entriesMissedInCache.add(key);
}

CachingExecutor继续往下走,如果查询到数据,则调用tcm.putObject方法,往缓存中放入值。

if (list == null) {
	list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
	tcm.putObject(cache, key, list); // issue #578 and #116
}

tcm的put方法也不是直接操作缓存,只是在把这次的数据和key放入待提交的Map中。

@Override
public void putObject(Object key, Object object) {
    entriesToAddOnCommit.put(key, object);
}

从以上的代码分析中,我们可以明白,如果不调用commit方法的话,由于TranscationalCache的作用,并不会对二级缓存造成直接的影响。因此我们看看Sqlsession的commit方法中做了什么。代码如下所示:

@Override
public void commit(boolean force) {
    try {
      executor.commit(isCommitOrRollbackRequired(force));

因为我们使用了CachingExecutor,首先会进入CachingExecutor实现的commit方法。

@Override
public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
}

会把具体commit的职责委托给包装的Executor。主要是看下tcm.commit(),tcm最终又会调用到TrancationalCache。

public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    flushPendingEntries();
    reset();
}

看到这里的clearOnCommit就想起刚才TrancationalCache的clear方法设置的标志位,真正的清理Cache是放到这里来进行的。具体清理的职责委托给了包装的Cache类。之后进入flushPendingEntries方法。代码如下所示:

private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    ................
}

在flushPendingEntries中,将待提交的Map进行循环处理,委托给包装的Cache类,进行putObject的操作。

后续的查询操作会重复执行这套流程。如果是insert|update|delete的话,会统一进入CachingExecutor的update方法,其中调用了这个函数,代码如下所示:

private void flushCacheIfRequired(MappedStatement ms) 

在二级缓存执行流程后就会进入一级缓存的执行流程,因此不再赘述。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值