【Mybatis】14 缓存
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 [] [org.apache.ibatis.datasource.pooled.PooledDataSource]-Returned connection 336371513 to pool. Process finished with exit code 0
这里可以看到SQL语句只执行了一次
4次查询只执行了一次,这证明了缓存的存在
但是在查询不同情况下的值的时候,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(); }
也就是说,实际上缓存存放的数据是首次查询出来的一个结果
如果我们反复调用相同的结果,Mybatis就会从缓存中返回数据给我们
[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 [] [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 [] [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 [] [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 [] [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 [] [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关闭,一级缓存保存到二级缓存中