进阶:查询缓存
关于
Mybatis一级缓存的作用域是同一个SqlSession,在同一个sqlSession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。当一个sqlSession结束后该sqlSession中的一级缓存也就不存在了。
Mybatis默认开启一级缓存。
Mybatis二级缓存是多个SqlSession共享的,其作用域是mapper的同一个namespace,不同的sqlSession两次执行相同namespace下的sql语句且向sql中传递参数也相同即最终执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高查询效率。
Mybatis二级缓存默认关闭。
一级缓存
- 一级缓存区域是根据SqlSession为单位划分的。
- 每次查询会先从缓存区域找,如果找不到再从数据库查询,并将查询到的数据写入缓存。
- Mybatis内部存储缓存使用一个HashMap。
- key为hashCode+sqlId+Sql语句。value为从查询出来映射生成的java对象;
- sqlSession执行insert、update、delete等操作commit提交后会清空缓存区域。
示例
@Test
public void testCache1() throws Exception{
SqlSession sqlSession = sqlSessionFactory.openSession();//创建代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//以下查询使用一个SqlSession
//第一次发起请求,查询id为1的用户
User user1 = userMapper.findUserById(1);
System.out.println(user1);
//第二次发起请求,查询id为1的用户
//与user1执行的是相同sql,可以使用一级缓存
User user2 = userMapper.findUserById(1);
System.out.println(user2);
//在同一个session执行更新
//执行commit操作,清空一级缓存,避免脏读。
User user_update = new User();
user_update.setId(1);
user_update.setUsername("李奎");
userMapper.updateUser(user_update);
session.commit();
//再次发起请求,查询id为1的用户
//缓存无相应内容,需要查询数据库
User user3 = userMapper.findUserById(1);
System.out.println(user3);
//关闭sqlSession
sqlSession.close();
}
二级缓存
- 二级缓存区域是根据mapper的namespace划分的,相同namespace的mapper查询数据放在同一个区域。
- 如果使用mapper代理方法每个mapper的namespace都不同,此时可以理解为二级缓存区域是根据mapper划分;
- 每次查询会先从缓存区域找,如果找不到再从数据库查询,并将查询到的数据写入缓存。
- Mybatis内部存储缓存使用一个HashMap。
- key为hashCode+sqlId+Sql语句。value为从查询出来映射生成的java对象;
- sqlSession执行insert、update、delete等操作commit提交后会清空缓存区域。
设置
SqlMapConfig.xml
设置 | 描述 | 允许值 | 默认值 |
---|---|---|---|
cacheEnabled | 对在此配置文件下的所有cache 进行全局性开/关设置。 | true | false | true |
Mybatis的二级缓存默认是不开启的,需要时在SqlMapConfig.xml中加入:
<setting name="cacheEnabled" value="true"/>
Mapper.xml
开启二级缓存不光需要进行全局设置,同时必须在需要的mapper.xml映射文件中启用二级缓存:
<cache />
<cache type="" />
- type:指定cache接口的实现类的类型
- 指定cache接口的实现类的类型
- Mybatis和ehcache整合,需要配置type为ehcache实现cache接口的类型
如:
<mapper namespace="cn.itcast.mybatis.mapper.UserMapper">
<!-- 开启本mapper的namespace下的二缓存 -->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
...
Cache参数
设置 | 描述 | 允许值 | 默认值 |
---|---|---|---|
flushInterval | 缓存刷新间隔(单位毫秒)。
|
任意正整数 | Not set |
size | 设置缓存结果对象或列表的引用大小。 | 任意正整数 | 1024 |
readOnly | 设置缓存只读。只读缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改。
|
true | false | false |
eviction | 缓存收回策略。 |
|
LRU |
如:
<cache eviction="FIFO" flushInterval="60000" size="512" readOnly="true"/>
- 使用了一个“以FIFO进行回收,并每隔 60 秒刷新,缓存Hashmap有512个引用,而且返回只读对象”的二级缓存。
statement
禁用缓存
在statement中设置useCache=false可以禁用当前statement(即当前select语句)的二级缓存,即每次查询都会发出sql去查询。
- 默认情况是true,即该sql使用二级缓存;
- 针对每次查询都需要最新的数据sql,要设置成useCache=false,禁用二级缓存;
如:
<select id="findOrderListResultMap" resultMap="ordersUserMap" useCache="false">
刷新缓存
在mapper的同一个namespace中,如果有其它insert、update、delete操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。
- 默认情况下为true(即刷新缓存),如果改成false则不会刷新。
- 一般下执行完commit操作都需要刷新缓存,以避免数据库脏读。
- (使用缓存时,如果手动修改数据库表中的查询数据会出现脏读)。
如:
<insert id="insertUser" parameterType="cn.itcast.mybatis.po.User" flushCache="true">
po
二级缓存需要查询结果映射的pojo对象实现java.io.Serializable接口实现序列化和反序列化操作
- 注意如果存在父类、成员pojo都需要实现序列化接口。
public class Orders implements Serializable
public class User implements Serializable
...
如:
public class User implements Serializable {
//属性名和数据库表的字段对应
private int id;
private String username;// 用户姓名
private String sex;// 性别
private Date birthday;// 生日
private String address;// 地址
//用户创建的订单列表
private List<Orders> ordersList;
示例
@Test
public void testCache2() throws Exception {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession();
SqlSession sqlSession4 = sqlSessionFactory.openSession();
// 创建代理对象
UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
// 第一次发起请求,查询id为1的用户
User user1 = userMapper1.findUserById(1);
System.out.println(user1);
//这里执行关闭操作,将sqlsession中的数据写到二级缓存区域
sqlSession1.close();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
// 第二次发起请求,查询id为1的用户
// 与sqlSession2请求相同,读取二级缓存,而不查询数据库
User user2 = userMapper2.findUserById(1);
System.out.println(user2);
//这里关闭操作,并将sqlsession中的数据写到二级缓存区域
sqlSession2.close();
//使用sqlSession3执行commit()操作
UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
User user = userMapper3.findUserById(1);
user.setUsername("张明明");
userMapper3.updateUser(user);
//执行提交,清空UserMapper下边的二级缓存
sqlSession3.commit();
sqlSession3.close();
UserMapper userMapper4 = sqlSession4.getMapper(UserMapper.class);
// 再一次发起请求,查询id为1的用户,
// 由于commit操作导致缓存被清空,这里重新发出sql操作
User user4 = userMapper4.findUserById(1);
System.out.println(user4);
//这里关闭操作,并将sqlsession中的数据写到二级缓存区域
sqlSession4.close();
}
整合ehcache(使用自定义缓存)
EhCache 是一个纯Java的进程内缓存框架,是一种广泛使用的开源Java分布式缓存,具有快速、精干等特点,是Hibernate中默认的CacheProvider。
而 Mybatis 的特长是sql操作,而非缓存数据的管理,为了提高缓存的性能将mybatis和第三方的缓存数据库整合,比如ehcache、memcache、redis等。
整合原理
mybatis提供二级缓存Cache接口:
它的默认实现类:
通过实现Cache接口,可以实现mybatis与其它缓存数据库整合。
如:
整合步骤
依赖
依赖包:
- ehcache-core-2.6.5.jar:ehcache核心包
- mybatis-ehcache-1.0.2.jar:桥接包
Maven坐标:
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.0.2</version>
</dependency>
EhCache配置文件
classpath下添加ehcache.xml:
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<diskStore path="D:\develop\ehcache" />
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>
配置 | 说明 |
---|---|
diskStore | 指定数据在磁盘中的存储位置 |
defaultCache | 当借助CacheManager.add("demoCache") 创建Cache时,EhCache便会采用<defalutCache/>指定的的管理策略
|
defalutCache的“必须属性” 属性 说明 值 maxElementsInMemory 在内存中缓存的element的最大数目 maxElementsOnDisk 在磁盘上缓存的element的最大数目,若是0表示无穷大 eternal 设定缓存的elements是否永远不过期。 - 如果为true,则缓存的数据始终有效;
- 如果为false,根据timeToIdleSeconds,timeToLiveSeconds判断过期
overflowToDisk 设定当内存缓存溢出的时候是否将过期的element缓存到磁盘上
defalutCache的“可选属性” 属性 说明 值 timeToIdleSeconds 当缓存在EhCache中的数据前后两次访问的时间超过timeToIdleSeconds的属性取值时,这些数据便会删除 默认值是0,也就是可闲置时间无穷大 timeToLiveSeconds 缓存element的有效生命期 默认是0,也就是element存活时间无穷大 diskSpoolBufferSizeMB 设置DiskStore(磁盘缓存)的缓存区大小 默认是30MB,每个Cache都应该有自己的一个缓冲区 diskPersistent 在VM重启的时候是否启用磁盘保存EhCache中的数据 默认是false diskExpiryThreadIntervalSeconds 磁盘缓存的清理线程运行间隔 默认是120秒,即每隔120s,相应的线程会进行一次EhCache中数据的清理工作 memoryStoreEvictionPolicy 当内存缓存达到最大,有新的element加入的时候, 移除缓存中element的策略 - LRU:最近最少使用(默认)
- LFU:最不常使用
- FIFO:先进先出
开启EhCache
修改mapper.xml文件,在cache中指定EhcacheCache:
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
根据需求调整缓存参数:
<cache type="org.mybatis.caches.ehcache.EhcacheCache" >
<property name="timeToIdleSeconds" value="3600"/>
<property name="timeToLiveSeconds" value="3600"/>
<!-- 同ehcache参数maxElementsInMemory -->
<property name="maxEntriesLocalHeap" value="1000"/>
<!-- 同ehcache参数maxElementsOnDisk -->
<property name="maxEntriesLocalDisk" value="10000000"/>
<property name="memoryStoreEvictionPolicy" value="LRU"/>
</cache>
namespace间共享缓存
对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新。 但你可能会想要在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可以使用 cache-ref 元素来引用另一个缓存。
<cache-ref namespace="com.someone.application.data.SomeMapper"/>
应用局限
使用场景:
对于访问多的查询请求且用户对查询结果实时性要求不高,此时可采用mybatis二级缓存技术降低数据库访问量,提高访问速度,业务场景比如:耗时较高的统计分析sql、电话账单查询sql等。
实现方法如下:通过设置刷新间隔时间,由mybatis每隔一段时间自动清空缓存,根据数据变化频率设置缓存刷新间隔flushInterval,比如设置为30分钟、60分钟、24小时等,根据需求而定。
局限性:
mybatis二级缓存对细粒度的数据级别的缓存实现不好,比如如下需求:
- “对商品信息进行缓存”:
- 由于商品信息查询访问量大,但是要求用户每次都能查询最新的商品信息,此时如果使用mybatis的二级缓存,就无法实现当一个商品变化时只刷新该商品的缓存信息而不刷新其它商品的信息。
- 因为mybaits的二级缓存区域以mapper为单位划分,当一个商品信息变化会将所有商品信息的缓存数据全部清空。解决此类问题需要在业务层根据需求对数据有针对性缓存。