Mybatis-缓存详解

什么是缓存?

  • 存在内存中的临时数据

  • 将用户经常查询的数据放在缓存中,用户去查询数据就不用从磁盘上(关系型数据库数据文件)查询,从缓存中查询,从而提高查询效率,解决了高并发系统的性能问题

  • 经常查询且不经常改变的数据适合使用缓存

为什么使用缓存?

缓存(即cache)的作用是为了减去数据库的压力,提高数据库的性能。缓存实现的原理是从数据库中查询出来的对象在使用完后不销毁,而是存储在内存(缓存)中,当再次需要获取该对象时,直接从内存中获取,不再向数据库执行select语句,减少对数据库的查询次数,提高了数据库的性能。缓存是使用Map集合存储数据。

可用的清除策略有:

  • LRU -- 最近最少使用:移除最长时间不被使用的对象。

  • FIFO -- 先进先出:按对象进入缓存的顺序来移除它们。

Mybatis缓存

MyBatis有一级缓存和二级缓存之分。

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

二级缓存是多个SqlSession共享的 ,其作用域是mapper的同一个namespace ,不同的sqlSession两次执行相同namespace下的sql语句且向sql中传递的参数也相同时,第一次执行完毕会将数据库中查询到的数据写到缓存(内存),第二次会直接从缓存中获取,从而提高了查询效率。MyBatis默认不开启二级缓存,需要在MyBtais全局配置文件中进行setting配置开启二级缓存。

为了提高扩展性,mybatis定义了缓存接口Cache。我们可以通过实现Cache接口来定义二级缓存

  • 映射语句文件中的所有 select 语句的结果将会被缓存。

  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。

  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。

  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。

一级缓存

MyBatis默认开启一级缓存,一级缓存只是相对于同一个SqlSession而言。所以在参数个sql完全一致的情况下,我们使用同一个SqlSession对象调用一个Mapper方法,往往只执行一次sql,使用SqlSession第一次查询后,MyBatis会将其放在缓存中,之后再查询时若没有缓存失效或超时,SqlSession都会取出当前缓存的数据,不会再发送sql到数据库。

测试步骤:

1.开启日志!

2.测试在一个Session中查询两次相同的记录

3.查看日志输出

缓存失效的情况:

1.查询不同的实体

java 复制代码
 @Test
 public void testQueryUserById(){
     SqlSession session = MybatisUtils.getSession();
     SqlSession session2 = MybatisUtils.getSession();
     UserMapper mapper = session.getMapper(UserMapper.class);
     UserMapper mapper2 = session2.getMapper(UserMapper.class);
 ​
     User user = mapper.queryUserById(1);
     System.out.println(user);
     User user2 = mapper2.queryUserById(1);
     System.out.println(user2);
     System.out.println(user==user2);
 ​
     session.close();
     session2.close();
 }

观察结果:发现发送了两条SQL语句!

结论:每个sqlSession中的缓存相互独立

2.增删改操作。可能会改变原来的数据,所以必定刷新缓存

增加方法

java 复制代码
//修改用户
 int updateUser(Map map);

编写SQL

java 复制代码
<update id="updateUser" parameterType="map">
     update user set name = #{name} where id = #{id}
 </update>

测试

java 复制代码
 @Test
 public void testQueryUserById(){
     SqlSession session = MybatisUtils.getSession();
     UserMapper mapper = session.getMapper(UserMapper.class);
 ​
     User user = mapper.queryUserById(1);
     System.out.println(user);
 ​
     HashMap map = new HashMap();
     map.put("name","kuangshen");
     map.put("id",4);
     mapper.updateUser(map);
 ​
     User user2 = mapper.queryUserById(1);
     System.out.println(user2);
 ​
     System.out.println(user==user2);
 ​
     session.close();
 }

观察结果:查询在中间执行了增删改操作后,重新执行了

结论:因为增删改操作可能会对当前数据产生影响

3.查询不同的mapper

java 复制代码
 @Test
 public void testQueryUserById(){
     SqlSession session = MybatisUtils.getSession();
     UserMapper mapper = session.getMapper(UserMapper.class);
     UserMapper mapper2 = session.getMapper(UserMapper.class);
 ​
     User user = mapper.queryUserById(1);
     System.out.println(user);
     User user2 = mapper2.queryUserById(2);
     System.out.println(user2);
     System.out.println(user==user2);
 ​
     session.close();
 }

观察结果:发现发送了两条SQL语句!很正常的理解

结论:当前缓存中,不存在这个数据

4.手动清理缓存

复制代码
  
java 复制代码
 SqlSession sqlSession = MybatisUtils.getsqlSession();
         UserMapper mapper = sqlSession.getMapper(UserMapper.class);
         User user = mapper.queryUserById(1);
         System.out.println(user);
         sqlSession.clearCache();//手动清理缓存
         System.out.println("=========");
         User user2 = mapper.queryUserById(1);
         System.out.println(user2);
         System.out.println(user == user2);

小结:

一级缓存默认开启,作用域为同一个Sqlsession,只在一次SQLsession中有效,也就是拿到连接和关闭连接之间

一级缓存就是一个map

二级缓存

二级缓存的作用域是SqlSessionFactory级别,整个应用程序只有一个。二级缓存区域是根据mapper的namespace划分的,相同的namespace的mapper查询的数据缓存在同一个区域,如果使用mapper代理方法,每一个mapper的namespace都不同,此时可以理解为二级缓存区域是根据mapper进行划分的。

每次查询都会先从缓存区域查找,如果找不到则从数据库进行查询,并将查询到的数据写入缓存。MyBtais内部缓存使用HashMap,key为hashCode+sqlid+sql语句,value为从查询出来映射生成的java对象。SqlSession执行任何一个update(修改)、delete(删除)、insert(新增)操作commit提交后都会清空缓存区域,防止脏读。

工作机制

  • 一个会话查询一条数据,这个数据就会被放在当前会话的一级缓存中

  • 如果当前会话关闭了,这个会话的一级缓存就没了,但是我们想要的是,会话关闭了,一级缓存的数据会被保存在二级缓存中

  • 新的会话查询信息,就可以从二级缓存中获取内容

  • 不同的mapper查出的数据会放在自己对应的缓存中

步骤:

1.开启全局缓存

XML 复制代码
 <!--        开启全局缓存-->
         <setting name="cacheEnabled" value="true"/>

2.在使用二级缓存的Mapper中开启

XML 复制代码
 <cache/>

也可以自定义一些参数

XML 复制代码
 <cache
         eviction="FIFO"
         flushInterval="60000"
         size="512"
         readOnly="true"/>
         //创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的

3.测试:

1.问题:我们要将实体类序列化!否则就会报错

复制代码
 org.apache.ibatis.cache.CacheException:

解决方式:在实体类实现接口: Serializable

java 复制代码
 public class User implements Serializable 

小结

  • 只要开启了二级缓存,在同一个Mapper下有效

  • 所有的数据都会先放在一级缓存中

  • 只有当会话提交,或关闭的时候·,才会提交到二级缓存中

Mybatis缓存原理

扩充:在正式业务中,如何保证多个数据库数据一致?使用主从复制

缓存只是读,写还是数据库,为读写分离,提高数据库性能

第三方缓存实现--EhCache

Ehcache是一种广泛使用的java分布式缓存,用于通用缓存;

要在应用程序中使用Ehcache,需要引入依赖的jar包

XML 复制代码
 <!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache -->
 <dependency>
     <groupId>org.mybatis.caches</groupId>
     <artifactId>mybatis-ehcache</artifactId>
     <version>1.2.3</version>
 </dependency>

在mapper.xml中使用对应的缓存即可

XML 复制代码
 <mapper namespace = "org.acme.FooMapper" > 
     <cache type = "org.mybatis.caches.ehcache.EhcacheCache" /> 
 </mapper>

编写ehcache.xml文件,如果在加载时未找到/ehcache.xml资源或出现问题,则将使用默认配置。

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
 <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
          updateCheck="false">
     <!--
        diskStore:为缓存路径,ehcache分为内存和磁盘两级,此属性定义磁盘的缓存位置。参数解释如下:
        user.home -- 用户主目录
        user.dir  -- 用户当前工作目录
        java.io.tmpdir -- 默认临时文件路径
      -->
     <diskStore path="./tmpdir/Tmp_EhCache"/>
     
     <defaultCache
             eternal="false"
             maxElementsInMemory="10000"
             overflowToDisk="false"
             diskPersistent="false"
             timeToIdleSeconds="1800"
             timeToLiveSeconds="259200"
             memoryStoreEvictionPolicy="LRU"/>
  
     <cache
             name="cloud_user"
             eternal="false"
             maxElementsInMemory="5000"
             overflowToDisk="false"
             diskPersistent="false"
             timeToIdleSeconds="1800"
             timeToLiveSeconds="1800"
             memoryStoreEvictionPolicy="LRU"/>
     <!--
        defaultCache:默认缓存策略,当ehcache找不到定义的缓存时,则使用这个缓存策略。只能定义一个。
      -->
     <!--
       name:缓存名称。
       maxElementsInMemory:缓存最大数目
       maxElementsOnDisk:硬盘最大缓存个数。
       eternal:对象是否永久有效,一但设置了,timeout将不起作用。
       overflowToDisk:是否保存到磁盘,当系统当机时
       timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
       timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
       diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
       diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
       diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
       memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
       clearOnFlush:内存数量最大时是否清除。
       memoryStoreEvictionPolicy:可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
       FIFO,first in first out,这个是大家最熟的,先进先出。
       LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
       LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
    -->
 ​
 </ehcache>

希望对大家有所帮助!

相关推荐
Hanson Huang2 小时前
【数据结构】堆排序详细图解
java·数据结构·排序算法·堆排序
路在脚下@2 小时前
Redis实现分布式定时任务
java·redis
xrkhy2 小时前
idea的快捷键使用以及相关设置
java·ide·intellij-idea
巨龙之路2 小时前
Lua中的元表
java·开发语言·lua
hnsqls2 小时前
Redis 常问知识
数据库·redis·缓存
花花鱼3 小时前
itext7 html2pdf 将html文本转为pdf
java·pdf
小丁爱养花3 小时前
驾驭 Linux 云: JavaWeb 项目安全部署
java·linux·运维·服务器·spring boot·后端·spring
我爱拉臭臭4 小时前
kotlin音乐app之自定义点击缩放组件Shrink Layout
android·java·kotlin
一个小白14 小时前
C++ 用红黑树封装map/set
java·数据库·c++
神奇小永哥4 小时前
redis之缓存雪崩
数据库·redis·缓存