【Mybatis】缓存

一级缓存

 1 package com.wang;
 2 
 3 import com.wang.Mapper.UserMapper;
 4 import com.wang.utils.MyBatisUtils;
 5 import org.apache.ibatis.session.SqlSession;
 6 import org.junit.Test;
 7 
 8 public class test {
 9     @Test
10     public void test01(){
11         SqlSession sqlSession = MyBatisUtils.getSqlSession();
12         //一级缓存的生命周期是从sqlSession的创建开始到sqlSession.close()结束
13         try{
14             UserMapper mapper = sqlSession.getMapper(UserMapper.class);
15             System.out.println(mapper.getUserById(2));
16             System.out.println("===================================");
17             System.out.println(mapper.getUserById(2));
18         }finally {
19             sqlSession.close();//一级缓存生命周期结束
20         }
23     }
24 }

我们可以查看日志输出

Opening JDBC Connection 
Created connection 124407148.
==>  Preparing: select * from mybatis.user where id=?; #第1次SQL查询
==> Parameters: 2(Integer) #第2次SQL查询
<==    Columns: id, name, pwd
<==        Row: 2, ??, 333333
<==      Total: 1
[User(id=2, name=??, pwd=333333)]
===================================
[User(id=2, name=??, pwd=333333)]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@76a4d6c]
Returned connection 124407148 to pool.

根据日志输出,我们发现程序只在第一次调用 mapper.getUserById(2) 进行了一次SQL查询,第二次直接在缓存中获取SQL查询结果

缓存失效情况

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

 1  @Test
 2     public void test02(){
 3         SqlSession sqlSession = MyBatisUtils.getSqlSession();
 4         try{
 5             UserMapper mapper = sqlSession.getMapper(UserMapper.class);
 6             System.out.println(mapper.getUserById(2));
 7             System.out.println("================");
 8             mapper.updateUser(new User(1,"小猴","123456"));
 9             System.out.println("================");
10             System.out.println(mapper.getUserById(2));
11         }finally {
12             sqlSession.close();
13         }
14     }

我们可以查看对应的日志输出

Opening JDBC Connection
Created connection 124407148.
==>  Preparing: select * from mybatis.user where id=?; 
==> Parameters: 2(Integer)
<==    Columns: id, name, pwd
<==        Row: 2, ??, 333333
<==      Total: 1
[User(id=2, name=??, pwd=333333)]
================
==>  Preparing: update mybatis.user set name=?,pwd=? where id=?; 
==> Parameters: 小猴(String), 123456(String), 1(Integer)
<==    Updates: 0
================
==>  Preparing: select * from mybatis.user where id=?; 
==> Parameters: 2(Integer)
<==    Columns: id, name, pwd
<==        Row: 2, ??, 333333
<==      Total: 1
[User(id=2, name=??, pwd=333333)]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@76a4d6c]
Returned connection 124407148 to pool.

根据上述日志,我们可以发现在我们执行了增删改相关操作(对应上述代码中的mapper.updateUser),我们发现当再次执行mapper.getUserById(2)时,MyBatis重新执行了SQL查询,而不是从缓存中获取查询结果。

列举所有缓存失效的情况

  • 查询不同的东西
  • 增删改操作,可能会改变原来的数据,所以必定会刷新缓存!
  • 查询不同的Mapper.xml
  • 手动清除缓存

SqlSession手动清除缓存的方法是sqlSession.clearCache();

一级缓存默认是开启的,只在一次SqlSession中有效,也就是拿到连接到关闭连接这个区间段!

一级缓存相当于一个Map

 

 

 

 

二级缓存

默认情况下,只启用了本地的会话缓存,它仅仅对一个会话中的数据进行缓存。 要启用全局的二级缓存,只需要在你的 SQL 映射文件中添加一行:

<cache/>

基本上就是这样。这个简单语句的效果如下:

  • 映射语句文件中的所有 select 语句的结果将会被缓存。
  • 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
  • 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
  • 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
  • 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
  • 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。

提示 缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。

<cache/>

这些属性可以通过 cache 元素的属性来修改。比如:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

这个更高级的配置创建了一个 FIFO 缓存,每隔 60 秒刷新,最多可以存储结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此对它们进行修改可能会在不同线程中的调用者产生冲突。

可用的清除策略有:

  • LRU – 最近最少使用:移除最长时间不被使用的对象。
  • FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
  • SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
  • WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。

默认的清除策略是 LRU。

flushInterval(刷新间隔)属性可以被设置为任意的正整数,设置的值应该是一个以毫秒为单位的合理时间量。 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新。

size(引用数目)属性可以被设置为任意正整数,要注意欲缓存对象的大小和运行环境中可用的内存资源。默认值是 1024。

readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。

提示 二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。

 

二级缓存介绍

  • 二级缓存也叫全局缓存,一级缓存作用域太低了,所以诞生了二级缓存
  • 基于namespace级别的缓存,一个名称空间,对应一个二级缓存;
  • 工作机制
    •  一个绘画查询一条数据,这个数据就会被放到当前会话的一级缓存中;
    • 如果当前会话关闭了,这个会话对应的一级缓存就没了;但是我们想要的是,会话关闭了,一级缓存中的数据会被保存到二级缓存中
    • 新的会话查询信息,可以从二级缓存中获取内容
    • 不同的mapper查出的数据就会放在自己对应的缓存(map)中

 

 

如何开启全局缓存?

 

 

 

 1.显式地开启全家缓存

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

 

2.在对应的Mapper.xml文件中手动开启二级缓存
<?xml version="1.0" encoding="UTF8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--configuration-->

<mapper namespace="com.wang.Mapper.UserMapper">
<!--    在当期Mapper.xml中使用二级缓存-->
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
<select id="getUserById" parameterType="int" resultType="user" flushCache="false">
    select * from mybatis.user where id=#{id};
</select>
<update id="updateUser" parameterType="user" >
    update mybatis.user set name=#{name},pwd=#{pwd} where id=#{id};
</update>

</mapper>

也可以自定义一些缓存

3.测试2级缓存

 @Test
    public void test03(){
        //开启第一个SqlSession
        SqlSession sqlSession1 = MyBatisUtils.getSqlSession();
        try{
            UserMapper mapper = sqlSession1.getMapper(UserMapper.class);
            System.out.println(mapper.getUserById(2));
        }finally {
            sqlSession1.close();
        }
        //开启第二个SqlSession
        SqlSession sqlSession2 = MyBatisUtils.getSqlSession();
        try{
            UserMapper mapper = sqlSession2.getMapper(UserMapper.class);
            System.out.println(mapper.getUserById(2));
        }finally {
            sqlSession2.close();
        }
    }

查看相应的日志输出

pening JDBC Connection
Created connection 1312884893.
==>  Preparing: select * from mybatis.user where id=?; 
==> Parameters: 2(Integer)
<==    Columns: id, name, pwd
<==        Row: 2, ??, 333333
<==      Total: 1
[User(id=2, name=??, pwd=333333)]
Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@4e41089d]
Returned connection 1312884893 to pool.
Cache Hit Ratio [com.wang.Mapper.UserMapper]: 0.5
[User(id=2, name=??, pwd=333333)]

此时我们发现,当第一个SqlSession关闭时,其对应的一级缓存失效,对应的缓存内容放入到二级缓存中,新的会话查询时直接进入二级缓存进行查询。

二级缓存存在的问题

我们需要将实体类序列化!否则就会报错

二级缓存总结:

  • 只要开启了二级缓存,在同一个Mapper就有效
  • 所有的数据都会先放在一级缓存中
  • 只有当会话提交或者关闭的时候才会提交到二级缓存

Mybatis缓存原理

 

 

 

用户访问数据库顺序:

  • 首先看二级缓存中有没有
  • 再看一级缓存中有没有
  • 如果都没有查询数据库
  • sqlSession关闭时,一级缓存自动提交到二级缓存

 

自定义缓存-ehcache

Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存,Java EE和轻量级容器。它具有内存和磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序,一个gzip缓存servlet过滤器,支持REST和SOAP api等特点。

1.导入maven依赖

  <dependency>
            <groupId>org.mybatis.caches</groupId>
            <artifactId>mybatis-ehcache</artifactId>
            <version>1.2.1</version>
        </dependency>

2.在对应的Mapper.xml文件中开启缓存

<!--    使用自定义缓存-ehchance-->
    <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>

3.配置ehcache,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="java.io.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>

 

查看一下eacache的实现源码

/**
 *    Copyright 2010-2018 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.mybatis.caches.ehcache;

public class EhcacheCache extends AbstractEhcacheCache {

  /**
   * Instantiates a new ehcache cache.
   *
   * @param id
   *          the id
   */
  public EhcacheCache(String id) {
    super(id);
    if (!CACHE_MANAGER.cacheExists(id)) {
      CACHE_MANAGER.addCache(id);
    }
    this.cache = CACHE_MANAGER.getEhcache(id);
  }

}

我们翻阅一下Mybatis官网的说明

 

 

 

我们只需要实现了Cache接口,就行了

posted @ 2022-05-30 23:00  王广元  阅读(28)  评论(0编辑  收藏  举报
分享到: