Mybatis系列(六)缓存机制
一、Mybatis缓存机制
思想:
从数据中读取数据时,先去缓存中拿,如果缓存中有,直接从缓存中拿,如果没有,就去查数据库,然后同步到缓存中一份,下次查询的时候直接从缓存中拿就可以了。
- 一级缓存:线程级别的缓存;本地缓存;SqlSession(与数据库的一次会话)级别的缓存
- 二级缓存:全局范围的缓存,除过当前线程,Sqlsession能用外,其它的线程都可以使用
二、一级缓存
一级缓存,mybatis默认是开启的
一级缓存初体验:
Test.java:
@Test
public void testMybatisSelect03(){
//获取与数据库的一次会话
SqlSession sqlSession = sqlSessionFactory.openSession();
KeyDao keyDao = sqlSession.getMapper(KeyDao.class);
Key key = keyDao.getKeyById(1);
System.out.println(key);
System.out.println("----------------------------------------->");
Key key2 = keyDao.getKeyById(1);
System.out.println(key2);
sqlSession.close();
}
输出结果:
DEBUG 02-18 12:46:55,002 ==> Preparing: SELECT * FROM t_key k, t_lock l WHERE k.l_id = l.lid AND k.kid = ? (BaseJdbcLogger.java:145)
DEBUG 02-18 12:46:55,055 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 02-18 12:46:55,086 <== Total: 1 (BaseJdbcLogger.java:145)
Key(id=1, keyName=钥匙1, lock=Lock(id=1, lockName=锁1, keyList=null))
----------------------------------------->
Key(id=1, keyName=钥匙1, lock=Lock(id=1, lockName=锁1, keyList=null))
缓存失效的几种情况:
- 不同的SqlSession
Test.java:
@Test
public void testMybatisSelect03(){
//获取与数据库的一次会话
SqlSession sqlSession = sqlSessionFactory.openSession();
KeyDao keyDao = sqlSession.getMapper(KeyDao.class);
Key key = keyDao.getKeyById(1);
System.out.println(key);
System.out.println("----------------------------------------->");
//打开新的SqlSession
SqlSession sqlSession2 = sqlSessionFactory.openSession();
KeyDao keyDao2 = sqlSession2.getMapper(KeyDao.class);
Key key2 = keyDao2.getKeyById(1);
System.out.println(key2);
sqlSession.close();
sqlSession2.close();
}
测试结果:
DEBUG 02-18 12:58:56,376 ==> Preparing: SELECT * FROM t_key k, t_lock l WHERE k.l_id = l.lid AND k.kid = ? (BaseJdbcLogger.java:145)
DEBUG 02-18 12:58:56,425 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 02-18 12:58:56,458 <== Total: 1 (BaseJdbcLogger.java:145)
Key(id=1, keyName=钥匙1, lock=Lock(id=1, lockName=锁1, keyList=null))
----------------------------------------->
DEBUG 02-18 12:58:56,467 ==> Preparing: SELECT * FROM t_key k, t_lock l WHERE k.l_id = l.lid AND k.kid = ? (BaseJdbcLogger.java:145)
DEBUG 02-18 12:58:56,468 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 02-18 12:58:56,470 <== Total: 1 (BaseJdbcLogger.java:145)
Key(id=1, keyName=钥匙1, lock=Lock(id=1, lockName=锁1, keyList=null))
- 同一个SqlSession,不同的参数
Test.java:
@Test
public void testMybatisSelect03(){
//获取与数据库的一次会话
SqlSession sqlSession = sqlSessionFactory.openSession();
KeyDao keyDao = sqlSession.getMapper(KeyDao.class);
Key key = keyDao.getKeyById(1);
System.out.println(key);
System.out.println("----------------------------------------->");
//预编译Sql相同,但是参数不同
Key key2 = keyDao.getKeyById(2);
System.out.println(key2);
sqlSession.close();
}
测试结果:
DEBUG 02-18 13:02:00,224 ==> Preparing: SELECT * FROM t_key k, t_lock l WHERE k.l_id = l.lid AND k.kid = ? (BaseJdbcLogger.java:145)
DEBUG 02-18 13:02:00,304 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 02-18 13:02:00,340 <== Total: 1 (BaseJdbcLogger.java:145)
Key(id=1, keyName=钥匙1, lock=Lock(id=1, lockName=锁1, keyList=null))
----------------------------------------->
DEBUG 02-18 13:02:00,343 ==> Preparing: SELECT * FROM t_key k, t_lock l WHERE k.l_id = l.lid AND k.kid = ? (BaseJdbcLogger.java:145)
DEBUG 02-18 13:02:00,344 ==> Parameters: 2(Integer) (BaseJdbcLogger.java:145)
DEBUG 02-18 13:02:00,346 <== Total: 1 (BaseJdbcLogger.java:145)
Key(id=2, keyName=钥匙2, lock=Lock(id=2, lockName=锁2, keyList=null))
- 在同一个SqlSession期间执行了一次增删改操作
由于增删改操作会改变表中的数据,可能造成缓存中已存在的缓存数据与数据库中的数据不一致,因此mybatis默认执行增删改操作时清空了一下缓存,下次需要的时候再去数据库中查询
Test.java:
@Test
public void testMybatisSelect03(){
//获取与数据库的一次会话
SqlSession sqlSession = sqlSessionFactory.openSession();
KeyDao keyDao = sqlSession.getMapper(KeyDao.class);
Key key = keyDao.getKeyById(1);
System.out.println(key);
//执行一次增删改的任意一次操作
System.out.println("-------开始执行修改操作-------------------->");
Key k = new Key();
k.setId(1);
k.setKeyName("钥匙n");
keyDao.updateKeyById(k);
System.out.println("----------------------------------------->");
Key key2 = keyDao.getKeyById(1);
System.out.println(key2);
sqlSession.commit();
sqlSession.close();
}
测试结果
DEBUG 02-18 13:10:15,525 ==> Preparing: SELECT * FROM t_key k, t_lock l WHERE k.l_id = l.lid AND k.kid = ? (BaseJdbcLogger.java:145)
DEBUG 02-18 13:10:15,599 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 02-18 13:10:15,628 <== Total: 1 (BaseJdbcLogger.java:145)
Key(id=1, keyName=钥匙1, lock=Lock(id=1, lockName=锁1, keyList=null))
-------开始执行修改操作-------------------->
DEBUG 02-18 13:10:15,630 ==> Preparing: update t_key set kname = ? where kid = ? (BaseJdbcLogger.java:145)
DEBUG 02-18 13:10:15,636 ==> Parameters: 钥匙n(String), 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 02-18 13:10:15,640 <== Updates: 1 (BaseJdbcLogger.java:145)
----------------------------------------->
DEBUG 02-18 13:10:15,641 ==> Preparing: SELECT * FROM t_key k, t_lock l WHERE k.l_id = l.lid AND k.kid = ? (BaseJdbcLogger.java:145)
DEBUG 02-18 13:10:15,644 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 02-18 13:10:15,646 <== Total: 1 (BaseJdbcLogger.java:145)
Key(id=1, keyName=钥匙n, lock=Lock(id=1, lockName=锁1, keyList=null))
- 手动清空了缓存
Test.java:
@Test
public void testMybatisSelect03(){
//获取与数据库的一次会话
SqlSession sqlSession = sqlSessionFactory.openSession();
KeyDao keyDao = sqlSession.getMapper(KeyDao.class);
Key key = keyDao.getKeyById(1);
System.out.println(key);
//手动清空缓存
sqlSession.clearCache();
System.out.println("----------------------------------------->");
Key key2 = keyDao.getKeyById(1);
System.out.println(key2);
sqlSession.commit();
sqlSession.close();
}
测试结果:
DEBUG 02-18 13:14:02,715 ==> Preparing: SELECT * FROM t_key k, t_lock l WHERE k.l_id = l.lid AND k.kid = ? (BaseJdbcLogger.java:145)
DEBUG 02-18 13:14:02,790 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 02-18 13:14:02,814 <== Total: 1 (BaseJdbcLogger.java:145)
Key(id=1, keyName=钥匙n, lock=Lock(id=1, lockName=锁1, keyList=null))
----------------------------------------->
DEBUG 02-18 13:14:02,816 ==> Preparing: SELECT * FROM t_key k, t_lock l WHERE k.l_id = l.lid AND k.kid = ? (BaseJdbcLogger.java:145)
DEBUG 02-18 13:14:02,817 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 02-18 13:14:02,821 <== Total: 1 (BaseJdbcLogger.java:145)
Key(id=1, keyName=钥匙n, lock=Lock(id=1, lockName=锁1, keyList=null))
三、二级缓存
二级缓存
(second level cache),全局作用域缓存
二级缓存默认不开启
,需要手动配置
MyBatis提供二级缓存的接口以及实现,缓存实现要求POJO实现Serializable
接口
二级缓存在 SqlSession关闭
或提交
之后才会生效使用步骤
1、全局配置文件中开启二级缓存
2、需要使用二级缓存的映射文件处使用cache配置缓存
3、注意:POJO需要实现Serializable接口
二级缓存初体验
mybatis-config.xml:
<settings>
<!--开启mybatis二级缓存-->
<setting name="cacheEnabled" value="true"/>
</settings>
KeyDao.xml
<mapper namespace="com.cetc.dao.KeyDao">
<!--开启二级缓存-->
<cache></cache>
........
</mapper>
Test.java:
@Test
public void testMybatisSelect04(){
//获取与数据库的一次会话
SqlSession sqlSession = sqlSessionFactory.openSession();
KeyDao keyDao = sqlSession.getMapper(KeyDao.class);
Key key = keyDao.getKeyById(1);
System.out.println(key);
sqlSession.close();
System.out.println("----------------------------------------->");
SqlSession sqlSession2 = sqlSessionFactory.openSession();
KeyDao keyDao2 = sqlSession2.getMapper(KeyDao.class);
Key key2 = keyDao2.getKeyById(1);
sqlSession2.close();
System.out.println(key2);
}
测试结果:
DEBUG 02-18 15:46:19,961 Cache Hit Ratio [com.cetc.dao.KeyDao]: 0.0 (LoggingCache.java:62) //缓存命中率
DEBUG 02-18 15:46:20,361 ==> Preparing: SELECT * FROM t_key k, t_lock l WHERE k.l_id = l.lid AND k.kid = ? (BaseJdbcLogger.java:145)
DEBUG 02-18 15:46:20,432 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 02-18 15:46:20,460 <== Total: 1 (BaseJdbcLogger.java:145)
Key(id=1, keyName=钥匙n, lock=Lock(id=1, lockName=锁1, keyList=null))
----------------------------------------->
DEBUG 02-18 15:46:20,594 Cache Hit Ratio [com.cetc.dao.KeyDao]: 0.5 (LoggingCache.java:62)
Key(id=1, keyName=钥匙n, lock=Lock(id=1, lockName=锁1, keyList=null))
四、缓存的查询顺序
一级缓存与二级缓存不会存在同样的数据
- 如果二级缓存中存在,直接从二级缓存中查
- 如果二级缓存中没有,去一级缓存中查
- 如果一、二级缓存中都没有,就去数据库中查询
Test.java:
@Test
public void testMybatisSelect05(){
//获取与数据库的一次会话
SqlSession sqlSession = sqlSessionFactory.openSession();
KeyDao keyDao = sqlSession.getMapper(KeyDao.class);
Key key = keyDao.getKeyById(1);
System.out.println(key);
sqlSession.close();
System.out.println("----------------------------------------->");
SqlSession sqlSession2 = sqlSessionFactory.openSession();
KeyDao keyDao2 = sqlSession2.getMapper(KeyDao.class);
Key key2 = keyDao2.getKeyById(1);
sqlSession2.close();
System.out.println(key2);
}
@Test
public void testMybatisSelect04(){
//获取与数据库的一次会话
SqlSession sqlSession = sqlSessionFactory.openSession();
KeyDao keyDao = sqlSession.getMapper(KeyDao.class);
Key key = keyDao.getKeyById(1);
System.out.println(key);
//一级缓存关闭,将数据放到二级缓存
sqlSession.close();
System.out.println("----------------------------------------->");
SqlSession sqlSession2 = sqlSessionFactory.openSession();
KeyDao keyDao2 = sqlSession2.getMapper(KeyDao.class);
//从二级缓存中获取数据
Key key2 = keyDao2.getKeyById(1);
System.out.println(key2);
//sqlSession2此时还未关闭,显示的查询结果还是直接从二级缓存中直接查询的,
//说明以及二级缓存如果存在,会从二级缓存中直接查询到,不会再放到以及缓存中去
Key key3= keyDao2.getKeyById(1);
System.out.println(key3);
System.out.println("---------------------->以下是查询id=2的钥匙");
sqlSession2.close();
/**
* 一级缓存和二级缓存此时都还没有ID=2的钥匙信息,
* 1.首先当前会话没有关闭,会在一级缓存中存放ID=2的数据
* 2.其次二级缓存中没有ID=2的数据
* 3.首先查询二级缓存,二级缓存中没有,则去查询一级缓存,一级缓存如果没有,则查询数据库
*/
//此次是第四次从二级缓存中查询,之前已经命中了2次
SqlSession sqlSession3 = sqlSessionFactory.openSession();
KeyDao keyDao3 = sqlSession3.getMapper(KeyDao.class);
Key key4 = keyDao3.getKeyById(3);
System.out.println(key4);
//此次是第5次查询,之前命中了2次,此次二级缓存中没有,查询的是一级缓存
Key key5 = keyDao3.getKeyById(3);
System.out.println(key5);
sqlSession3.close();
}
测试结果:
DEBUG 02-18 16:30:48,748 Cache Hit Ratio [com.cetc.dao.KeyDao]: 0.0 (LoggingCache.java:62)
DEBUG 02-18 16:30:49,171 ==> Preparing: SELECT * FROM t_key k, t_lock l WHERE k.l_id = l.lid AND k.kid = ? (BaseJdbcLogger.java:145)
DEBUG 02-18 16:30:49,247 ==> Parameters: 1(Integer) (BaseJdbcLogger.java:145)
DEBUG 02-18 16:30:49,270 <== Total: 1 (BaseJdbcLogger.java:145)
Key(id=1, keyName=钥匙n, lock=null)
----------------------------------------->
DEBUG 02-18 16:30:49,363 Cache Hit Ratio [com.cetc.dao.KeyDao]: 0.5 (LoggingCache.java:62)
Key(id=1, keyName=钥匙n, lock=null)
DEBUG 02-18 16:30:49,364 Cache Hit Ratio [com.cetc.dao.KeyDao]: 0.6666666666666666 (LoggingCache.java:62)
Key(id=1, keyName=钥匙n, lock=null)
---------------------->以下是查询id=2的钥匙
DEBUG 02-18 16:30:49,365 Cache Hit Ratio [com.cetc.dao.KeyDao]: 0.5 (LoggingCache.java:62)
DEBUG 02-18 16:30:49,366 ==> Preparing: SELECT * FROM t_key k, t_lock l WHERE k.l_id = l.lid AND k.kid = ? (BaseJdbcLogger.java:145)
DEBUG 02-18 16:30:49,367 ==> Parameters: 3(Integer) (BaseJdbcLogger.java:145)
DEBUG 02-18 16:30:49,369 <== Total: 1 (BaseJdbcLogger.java:145)
Key(id=3, keyName=钥匙3, lock=null)
DEBUG 02-18 16:30:49,369 Cache Hit Ratio [com.cetc.dao.KeyDao]: 0.4 (LoggingCache.java:62)
Key(id=3, keyName=钥匙3, lock=null)
五、缓存原理以及配置
缓存原理图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sDdfH7PZ-1617176397720)(C:\Users\86173\Desktop\自我总结\images\mybatis\8.png)]
缓存有关设置:
- 全局setting的cacheEnable:
配置二级缓存的开关。一级缓存一直是打开的。 - select标签的useCache属性:
配置这个select是否使用二级缓存。一级缓存一直是使用的 - sql标签的flushCache属性:
增删改默认flushCache=true。sql执行以后,会同时清空一级和二级缓存。查询默认flushCache=false - sqlSession.clearCache():
只是用来清除一级缓存。 - 当在某一个作用域 (一级缓存Session/二级缓存Namespaces) 进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被clear。
六、整合ehcache
EhCache
是一个纯Java的进程内缓存框架,具有快速、精干等特点,是Hibernate中默认的CacheProvider。MyBatis定义了
Cache接口
方便我们进行自定义扩展。
使用步骤:
-
导入ehcache包,以及整合包,日志包
ehcache-core-2.6.8.jar mybatis-ehcache-1.0.3.jar slf4j-api-1.6.1.jar slf4j-log4j12-1.6.2.jar
-
编写ehcache.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<!-- 磁盘保存路径 -->
<diskStore path="D:\ehcache" />
<defaultCache
maxElementsInMemory="1"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="true"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
<!--
属性说明:
l diskStore:指定数据在磁盘中的存储位置。
l defaultCache:当借助CacheManager.add("demoCache")创建Cache时,EhCache便会采用<defalutCache/>指定的的管理策略
以下属性是必须的:
l maxElementsInMemory - 在内存中缓存的element的最大数目
l maxElementsOnDisk - 在磁盘上缓存的element的最大数目,若是0表示无穷大
l eternal - 设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断
l overflowToDisk - 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上
以下属性是可选的:
l timeToIdleSeconds - 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除,默认值是0,也就是可闲置时间无穷大
l timeToLiveSeconds - 缓存element的有效生命期,默认是0.,也就是element存活时间无穷大
diskSpoolBufferSizeMB 这个参数设置DiskStore(磁盘缓存)的缓存区大小.默认是30MB.每个Cache都应该有自己的一个缓冲区.
l diskPersistent - 在VM重启的时候是否启用磁盘保存EhCache中的数据,默认是false。
l diskExpiryThreadIntervalSeconds - 磁盘缓存的清理线程运行间隔,默认是120秒。每个120s,相应的线程会进行一次EhCache中数据的清理工作
l memoryStoreEvictionPolicy - 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略。默认是LRU(最近最少使用),可选的有LFU(最不常使用)和FIFO(先进先出)
-->
- 配置cache标签
KeyDao.xml:
<!--开启EhcacheCache二级缓存-->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"></cache>
补充:
若想在命名空间中共享相同的缓存配置和实例。可以使用 cache-ref 元素来引用另外一个缓存。
LockDao.xml:
<!--与KeyDao共用一个二级缓存-->
<cache-ref namespace="com.cetc.dao.KeyDao"/>