Mybatis14 缓存

Posted Silent1376

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Mybatis14 缓存相关的知识,希望对你有一定的参考价值。

1、什么是缓存?

- 缓存是指把经常需要读写的数据,保存到一个高速的缓冲区中,这个行为叫缓存

- 也可以是指被保存在高速缓冲区的数据,也叫缓存

 

2、Mybatis缓存

Mybatis中分为一级缓存和二级缓存

- 一级缓存,数据缓存在这个SqlSession的作用范围内

- 二级缓存,数据缓存在这个SqlSesssionFactory的作用范围内

 

一级缓存:

一级缓存是默认开启的,那么如何证实是开启的呢?

同一个SQL语句只会执行一次,并留下缓存,

如果在这个SqlSession存在的期间,再次调用,那么Mybatis将不会执行SQL

而是直接调用缓存执行

 

案例:

映射接口

User getUserById(Integer id);

映射器

    <select id="getUserById" resultType="user" parameterType="int">
        SELECT *
        FROM t_user
        WHERE id = #{id}
    </select>

测试类

    @Test
    public void getUserById(){
        SqlSession sqlSession = MybatisUtil.getSqlSession(true);
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

        User user1 = userMapper.getUserById(1);
        System.out.println(user1);

        User user2 = userMapper.getUserById(1);
        System.out.println(user2);

        User user3 = userMapper.getUserById(1);
        System.out.println(user3);

        User user4 = userMapper.getUserById(1);
        System.out.println(user4);

        sqlSession.close();
    }

测试结果:

[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Opening JDBC Connection
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Created connection 336371513.
[cn.dai.mapper.UserMapper.getUserById]-==>  Preparing: SELECT * FROM t_user WHERE id = ? 
[cn.dai.mapper.UserMapper.getUserById]-==> Parameters: 1(Integer)
[cn.dai.mapper.UserMapper.getUserById]-<==      Total: 1
User(id=1, last_name=阿伟, gender=0)
User(id=1, last_name=阿伟, gender=0)
User(id=1, last_name=阿伟, gender=0)
User(id=1, last_name=阿伟, gender=0)
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@140c9f39]
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Returned connection 336371513 to pool.

Process finished with exit code 0

这里可以看到SQL语句只执行了一次

4次查询只执行了一次,这证明了缓存的存在

 

也就是说,实际上缓存存放的数据是首次查询出来的一个结果

如果我们反复调用相同的结果,Mybatis就会从缓存中返回数据给我们

 

但是在查询不同情况下的值的时候,Mybatis还是无法调用缓存来完成

例如我们这样查询不同的数据出来:

    @Test
    public void getUserById(){
        SqlSession sqlSession = MybatisUtil.getSqlSession(true);
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

        User user1 = userMapper.getUserById(1);
        System.out.println(user1);

        User user2 = userMapper.getUserById(2);
        System.out.println(user2);

        User user3 = userMapper.getUserById(3);
        System.out.println(user3);

        User user4 = userMapper.getUserById(4);
        System.out.println(user4);

        sqlSession.close();
    }

结果就是不会触发缓存,因为每次查询的都不一样

[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Opening JDBC Connection
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Created connection 336371513.
[cn.dai.mapper.UserMapper.getUserById]-==>  Preparing: SELECT * FROM t_user WHERE id = ? 
[cn.dai.mapper.UserMapper.getUserById]-==> Parameters: 1(Integer)
[cn.dai.mapper.UserMapper.getUserById]-<==      Total: 1
User(id=1, last_name=阿伟, gender=0)
[cn.dai.mapper.UserMapper.getUserById]-==>  Preparing: SELECT * FROM t_user WHERE id = ? 
[cn.dai.mapper.UserMapper.getUserById]-==> Parameters: 2(Integer)
[cn.dai.mapper.UserMapper.getUserById]-<==      Total: 1
User(id=2, last_name=阿伟, gender=1)
[cn.dai.mapper.UserMapper.getUserById]-==>  Preparing: SELECT * FROM t_user WHERE id = ? 
[cn.dai.mapper.UserMapper.getUserById]-==> Parameters: 3(Integer)
[cn.dai.mapper.UserMapper.getUserById]-<==      Total: 1
User(id=3, last_name=杰哥, gender=0)
[cn.dai.mapper.UserMapper.getUserById]-==>  Preparing: SELECT * FROM t_user WHERE id = ? 
[cn.dai.mapper.UserMapper.getUserById]-==> Parameters: 4(Integer)
[cn.dai.mapper.UserMapper.getUserById]-<==      Total: 1
User(id=4, last_name=阿强, gender=0)
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@140c9f39]
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Returned connection 336371513 to pool.

Process finished with exit code 0

 

原理示意:

 

一级缓存失败的四种情况:

- 不在同一个SqlSession对象中【同一个SQL语句】

- 执行的语句的参数不一样,缓存中也不存在数据【就是上面的演示】

- 执行增、删、改、会清除缓存

- 自行手动清除

 

手动清除是指SqlSession调用清除缓存方法

sqlSession.clearCache();

为什么增、删、改、也会清除缓存?

是因为底层在SQL执行完默认就调用了这个方法清除了

 

二级缓存:

首先,二级缓存默认是不开启的,我们需要在Mybatis的核心配置文件中

配置关于二级缓存的SETTINGS选项,和在映射器的配置文件中加入cache标签

并且,需要被二级缓存的对象,必须要实现序列化接口

 

示意图:

 

开启二级缓存的配置操作:

1、核心配置中添加二级缓存配置

2、映射器加入cache标签

3、被缓存的对象所属类必须实现序列化接口

 

二级缓存开启配置

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

 

映射器配置cache标签

<cache/>

 

测试类

    public void cacheTest(){
        SqlSession sqlSession = MybatisUtil.getSqlSession(true);
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);

        User userById = mapper.getUserById(1);
        System.out.println(userById);
        
        sqlSession.close();
    }
    
    @Test
    public void sync(){
        cacheTest();
        cacheTest();
    }

如果不实现序列化接口,二级缓存在调用时,就会出现未序列化异常

[cn.dai.mapper.UserMapper]-Cache Hit Ratio [cn.dai.mapper.UserMapper]: 0.0
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Opening JDBC Connection
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Created connection 1025309396.
[cn.dai.mapper.UserMapper.getUserById]-==>  Preparing: SELECT * FROM t_user WHERE id = ? 
[cn.dai.mapper.UserMapper.getUserById]-==> Parameters: 1(Integer)
[cn.dai.mapper.UserMapper.getUserById]-<==      Total: 1
User(id=1, last_name=阿伟, gender=0)
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3d1cfad4]
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Returned connection 1025309396 to pool.

org.apache.ibatis.cache.CacheException: Error serializing object.  Cause: java.io.NotSerializableException: cn.dai.pojo.User

    at org.apache.ibatis.cache.decorators.SerializedCache.serialize(SerializedCache.java:94)
    at org.apache.ibatis.cache.decorators.SerializedCache.putObject(SerializedCache.java:55)
    at org.apache.ibatis.cache.decorators.LoggingCache.putObject(LoggingCache.java:49)
    at org.apache.ibatis.cache.decorators.SynchronizedCache.putObject(SynchronizedCache.java:43)
    at org.apache.ibatis.cache.decorators.TransactionalCache.flushPendingEntries(TransactionalCache.java:116)
    at org.apache.ibatis.cache.decorators.TransactionalCache.commit(TransactionalCache.java:99)
    at org.apache.ibatis.cache.TransactionalCacheManager.commit(TransactionalCacheManager.java:44)
    at org.apache.ibatis.executor.CachingExecutor.close(CachingExecutor.java:61)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.close(DefaultSqlSession.java:263)
    at BuildTest.cacheTest(BuildTest.java:59)
    at BuildTest.sync(BuildTest.java:64)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:59)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:56)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.BlockJUnit4ClassRunner$1.evaluate(BlockJUnit4ClassRunner.java:100)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:366)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:103)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:63)
    at org.junit.runners.ParentRunner$4.run(ParentRunner.java:331)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:79)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:329)
    at org.junit.runners.ParentRunner.access$100(ParentRunner.java:66)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:293)
    at org.junit.runners.ParentRunner$3.evaluate(ParentRunner.java:306)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:413)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
    at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
    at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)
Caused by: java.io.NotSerializableException: cn.dai.pojo.User
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
    at java.util.ArrayList.writeObject(ArrayList.java:766)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at java.io.ObjectStreamClass.invokeWriteObject(ObjectStreamClass.java:1140)
    at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1496)
    at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1432)
    at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1178)
    at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
    at org.apache.ibatis.cache.decorators.SerializedCache.serialize(SerializedCache.java:90)
    ... 35 more


Process finished with exit code -1

所以需要我们自己来把实体类序列化

 

再次测试:

[cn.dai.mapper.UserMapper]-Cache Hit Ratio [cn.dai.mapper.UserMapper]: 0.0
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Opening JDBC Connection
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Created connection 1025309396.
[cn.dai.mapper.UserMapper.getUserById]-==>  Preparing: SELECT * FROM t_user WHERE id = ? 
[cn.dai.mapper.UserMapper.getUserById]-==> Parameters: 1(Integer)
[cn.dai.mapper.UserMapper.getUserById]-<==      Total: 1
User(id=1, last_name=阿伟, gender=0)
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3d1cfad4]
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Returned connection 1025309396 to pool.
[cn.dai.mapper.UserMapper]-Cache Hit Ratio [cn.dai.mapper.UserMapper]: 0.5
User(id=1, last_name=阿伟, gender=0)

Process finished with exit code 0

可以看到二次调用时不再调用SQL查询,而是使用了二级缓存保留的数据返回结果

 

一些说明:

useCache属性,这个属性是放在SQL查询标签中的<SELECT>

默认TRUE(就是不写也表示开启的),表示使用二级缓存,前提是二级缓存是开启的

在上面的全局测试中已经演示了结果

如果更改为False就是取消这个SQL的二级缓存

测试结果就是第二次查询就需要再次调用SQL了

[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Opening JDBC Connection
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Created connection 1504642150.
[cn.dai.mapper.UserMapper.getUserById]-==>  Preparing: SELECT * FROM t_user WHERE id = ? 
[cn.dai.mapper.UserMapper.getUserById]-==> Parameters: 1(Integer)
[cn.dai.mapper.UserMapper.getUserById]-<==      Total: 1
User(id=1, last_name=阿伟, gender=0)
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@59af0466]
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Returned connection 1504642150 to pool.
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Opening JDBC Connection
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Checked out connection 1504642150 from pool.
[cn.dai.mapper.UserMapper.getUserById]-==>  Preparing: SELECT * FROM t_user WHERE id = ? 
[cn.dai.mapper.UserMapper.getUserById]-==> Parameters: 1(Integer)
[cn.dai.mapper.UserMapper.getUserById]-<==      Total: 1
User(id=1, last_name=阿伟, gender=0)
[org.apache.ibatis.transaction.jdbc.JdbcTransaction]-Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@59af0466]
[org.apache.ibatis.datasource.pooled.PooledDataSource]-Returned connection 1504642150 to pool.

Process finished with exit code 0

 

flushCache属性,这个属性是放在增、删、改、SQL语句中,

表示自动清除缓存,默认值TRUE,另外不要手贱改FALSE

如果不清除缓存,二次调用就从缓存的数据进行返回

 

为什么这么说?

- 先查询一个结果

  【主键:01,名字:阿伟,性别:男】

- 不清除缓存进行修改记录,变更为

  【主键:01,名字:杰哥,性别:男】

- 当二次查询时,Mybatis不会再调用SQL重新查询,

  直接跑到缓存中返回数据,这个查询返回的结果就是

  【主键:01,名字:阿伟,性别:男】

  但实际上数据库已经更改,这样查询返回的结果是不对的

  所以不要修改flushCache属性为False!!!

 

3、Cache标签:

当你在映射器中标注了此标签,Mybatis会默认开启这些功能:

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

1、最近最少使用算法

  【Least Recently Used 】LRU算法来清除不需要的缓存。

  移除符合这个描述的对象,优化内存空间

除此之外还有其他算法,这个属性值由eviction配置

2、先进先出算法

  FIFO,First In First Out 先进先出

  按对象进入缓存的顺序来移除

3、软引用算法

  SOFT,移除基于GC回收状态和软引用规则的对象

4、弱引用算法

  WEAK,弱引用,更积极的移除基于GC回收状态和弱引用规则的对象

 

当然,默认使用的是LRU最少使用原则清除

LRU
FIFO
SOFT
WEAK

 

二、可读可写的说明:

readOnly="true"

可读是共享对象的,所有的SqlSession如果需要调用这个二级缓存

指针就会直接引用缓存返回对象

可读是非共享的,所有SqlSession如果调用二级缓存,

那么Mybatis会分别new一个对象,并把缓存对象的属性值赋值给这些new出来的对象

指针则引用这些对象进写入修改

 

三、自定义二级缓存

可以通过实现你自己的缓存,或为其他第三方缓存方案创建适配器,来完全覆盖缓存行为。

<cache type="com.domain.something.MyCustomCache"/>

我们可以查看Mybatis的缓存实现类是怎么写的

package org.apache.ibatis.cache.impl;

import java.util.HashMap;
import java.util.Map;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.CacheException;

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

    public PerpetualCache(String id) {
        this.id = id;
    }

    public String getId() {
        return this.id;
    }

    public int getSize() {
        return this.cache.size();
    }

    public void putObject(Object key, Object value) {
        this.cache.put(key, value);
    }

    public Object getObject(Object key) {
        return this.cache.get(key);
    }

    public Object removeObject(Object key) {
        return this.cache.remove(key);
    }

    public void clear() {
        this.cache.clear();
    }

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

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

 

四、缓存的执行顺序

1、当我们执行一个查询语句的时候,mybatis会先去二级缓存中查询数据,如果二级缓存中没有,就到一级缓存中查找

2、如果一级缓存也没有,调用SQL执行

3、执行返回,并且结果保存进一级缓存

4、SqlSession关闭,一级缓存保存到二级缓存中

以上是关于Mybatis14 缓存的主要内容,如果未能解决你的问题,请参考以下文章

mybatis学习笔记(14)-mybatis整合ehcache

如何缓存片段视图

Mybatis复杂查询动态sql及缓存详解

Mybatis关于复杂的SQL查询的处理&Mybatis的缓存机制

Mybatis关于复杂的SQL查询的处理&Mybatis的缓存机制

Mybatis 学习笔记总结