MyBatis中的缓存机制

文章目录

  • 1、缓存概述
  • 2、memcached使用详解
    • Memcache架构
    • Memcached缓存存储策略
    • 内存释放机制
    • Memcached优缺点
    • 安装
  • 3、一级缓存
  • 4、一级缓存源码分析
  • 5、二级缓存
    • memcached缓存数据
  • 6、二级缓存源码分析

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

——首先导入项目依赖:

<dependency><groupId>com.googlecode.xmemcachedgroupId><artifactId>xmemcachedartifactId><version>2.4.6version>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 accountwhere account.aid = #{aid,jdbcType=INTEGER}select>

在这里插入图片描述

  • 小结一级缓存:

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