MyBatis缓存配置
使用缓存的好处是1.更快地获取数据2.避免频繁的数据库交互。
一般提到MyBatis缓存的时候,都是指二级缓存。一级缓存(也叫本地缓存)默认会启用,并且不能控制。
MyBatis全局配置
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" >
<configuration>
<settings>
<setting name="logImpl" value="LOG4J"/>
<!-- 开启数据库列驼峰转化-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 开启二级缓存;学习一级缓存时请置为false-->
<setting name="cacheEnabled" value="true"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="" value=""/>
</transactionManager>
<dataSource type="UNPOOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.15.79:3306/mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<package name="tk.mybatis.simple.mapper"/>
</mappers>
</configuration>
一个获取sqlSession的类BaseMapperTest代码:
public class BaseMapperTest {
public static final Logger LOGGER = LoggerFactory.getLogger(BaseMapperTest.class);
private static SqlSessionFactory sqlSessionFactory;
@BeforeClass
public static void init(){
try {
// 1.通过Resource工具类将mybatis-config.xml配置文件读取Reader
// 2.通过SqlSessionFactoryBuilder建造类使用Reader创建SqlSessionFactory工厂对象
// 在创建SqlSessionFactory对象的过程中,首先解析mybatis-config.xml配置文件,读取
// 配置文件中的mappers配置后会读取全部的Mapper.xml进行具体方法的解析,在这些解析完成后
// ,SqlSessionFactory就包含了所有的属性配置和执行SQL的信息
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public SqlSession getSqlSession(){
return sqlSessionFactory.openSession();
}
}
介绍-一级缓存
实践-使用一级缓存
SysUserMapper接口
/**
* 通过id查用户
*
* @param id 用户id
* @return 用户
*/
SysUser queryById(Long id);
SysUserMapper.xml,其中有一个属性为flushCache,默认为false
<!--查询单个-->
<select id="queryById" flushCache="false" resultMap="BaseResultMap">
select
id, user_name, user_password, user_email, user_info, head_img, create_time
from mybatis.sys_user
where id = #{id}
</select>
测试类
public void myTestL1Cache(){
//获取sqlSession
SqlSession sqlSession = getSqlSession();
// 存储第一个sqlSession中查询出的用户数据,用来和第二个sqlSession查询结果做比较
SysUser user1 = null;
try {
SysUserMapper userMapper = sqlSession.getMapper(SysUserMapper.class);
// 查询id为1的用户
user1 = userMapper.queryById(1L);
// 重置用户名
user1.setUserName("xxxxx");
// 重新查询,本次查询会调用mybatis的一级缓存
SysUser user2 = userMapper.queryById(1L);
// 观察两个user对象的信息
LOGGER.info("==================第1个sqlSession========================");
LOGGER.info("user1的信息:{}\nuser2的信息:{}",user1,user2);
} finally {
sqlSession.close();
}
LOGGER.info("============开启新的sqlSession===================");
//开始新的session
sqlSession = getSqlSession();
try {
SysUserMapper userMapper = sqlSession.getMapper(SysUserMapper.class);
// 查询id为1的用户
SysUser user3 = userMapper.queryById(1L);
// 观察两个user对象的信息
LOGGER.info("==================第2个sqlSession========================");
LOGGER.info("user1的信息:{}\nuser3的信息:{}",user1,user3);
// 执行删除操作
SysUser condition = new SysUser() {{
setId(2L);
}};
userMapper.deleteById(condition);
// 获取user4
SysUser user4 = userMapper.queryById(1L);
// 观察两个user对象的信息
LOGGER.info("user3的信息:{}\nuser4的信息:{}",user3,user4);
} finally {
sqlSession.close();
}
}
测试结果:
1.第一段try finally代码解析:
发现开启新的sqlSession之前只进行了一次查询的sql,代码中调用了两次查询方法。获取user1后更新了userName的值,再获取user2对象后,发现user1和user2仍然是同一个对象,这就是因为mybatis的一级缓存,在同一个sqlSession中调用同样的方法并且参数一致时,只会执行一次sql。
MyBatis的一级缓存存在于sqlSession的生命周期中,同一个sqlSession中查询时,MyBatis会把执行的方法和参数转化生成缓存的键值,将键值和查询结果存入一个Map对象中。如果同一个sqlSession中执行的方法和参数完全一致,那么通过算法生成的键值也会相同,当Map缓存对象中已经存在该键值时,则会返回缓存中的对象。
所以,查询出user1时,MyBatis会将querById和1L生成一个键值,把user1作为值缓存起来;当我们更新user1的userName时,user1这个对象本身的引用没有变;当我们继续调用查询方法querById和参数1L时,生成的键值和上一次查询相同,就去缓存中取对应的值;取出来的值就是之前缓存的user1;所以user1和user2指向的是同一个对象。
2.第二段try finally解析
在关闭第一个sqlSession后,又重新获取了一个sqlSession,因此又重新查询了user2,这时输出了查询的sql,user2是一个新的实例。这是因为一级缓存和sqlSession绑定,只存在于sqlSession的生命周期中。
接下来执行了一个deleteById操作,使用上面同样的查询方法和参数获取了user3实例,发现user3和user2是完全不同的两个对象。这是因为任何的INSERT UPDATE DELETE操作都会清空一级缓存
==> Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ?
==> Parameters: 1(Long)
<== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<== Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<== Total: 1
==================第1个sqlSession========================
user1的信息:tk.mybatis.simple.model.SysUser@673be18f
user2的信息:tk.mybatis.simple.model.SysUser@673be18f
============开启新的sqlSession===================
==> Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ?
==> Parameters: 1(Long)
<== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<== Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<== Total: 1
==================第2个sqlSession========================
user1的信息:tk.mybatis.simple.model.SysUser@673be18f
user3的信息:tk.mybatis.simple.model.SysUser@38234a38
==> Preparing: delete from mybatis.sys_user where id = ?
==> Parameters: 2(Long)
<== Updates: 0
==> Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ?
==> Parameters: 1(Long)
<== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<== Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<== Total: 1
user3的信息:tk.mybatis.simple.model.SysUser@38234a38
user4的信息:tk.mybatis.simple.model.SysUser@c94fd30
实践-清空一级缓存
如果把select标签中的flushCache属性值改为true时,上面测试方法执行结果如下:发现调用了两次查询的sql,当flushCache改为true时,会在查询数据前清空当前的一级缓存,因此该select方法每次都会从数据库中查询数据,此时的user2和user1就会成为两个不同的实例。
==> Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ?
==> Parameters: 1(Long)
<== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<== Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<== Total: 1
==> Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ?
==> Parameters: 1(Long)
<== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<== Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<== Total: 1
==================第1个sqlSession========================
user1的信息:tk.mybatis.simple.model.SysUser@39de3d36
user2的信息:tk.mybatis.simple.model.SysUser@6ce86ce1
============开启新的sqlSession===================
==> Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ?
==> Parameters: 1(Long)
<== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<== Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<== Total: 1
==================第2个sqlSession========================
user1的信息:tk.mybatis.simple.model.SysUser@39de3d36
user3的信息:tk.mybatis.simple.model.SysUser@32c726ee
==> Preparing: delete from mybatis.sys_user where id = ?
==> Parameters: 2(Long)
<== Updates: 0
==> Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ?
==> Parameters: 1(Long)
<== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<== Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<== Total: 1
user3的信息:tk.mybatis.simple.model.SysUser@32c726ee
user4的信息:tk.mybatis.simple.model.SysUser@76a2ddf3
总结:
1.一级缓存出现在SqlSession的生命周期中,缓存是以键值对方式存在的,键是执行的方法和参数生成的值,值是sql返回的结果。
2.一级缓存会和SqlSession绑定
3.UPDATE INSERT DELETE方法会清空当前SqlSession中的一级缓存
介绍-二级缓存
二级缓存存在于SqlSessionFactory的生命周期中。当存在多个SqlSessionFactory时,它们的缓存都是绑定在各自对象上的,缓存数据一般情况下不相通。
实践-配置二级缓存
配置在mybatis的全局配置settings中,设置项为<setting name="cacheEnabled" value="true"/>
;cacheEnabled默认MyBatis是启用状态。
除了全局配置外,二级缓存需要关联mapper,关联方式可以是关联xml或接口。
1.xml中配置二级缓存
cache标签是和resultMap平级的。
<mapper namespace="tk.mybatis.simple.mapper.SysUserMapper">
<cache/>
</mapper>
默认的二级缓存有如下效果:
- 映射语句文件中的所有SELECT语句被缓存
- 映射语句文件中的所有INSERT UPDATE DELETE语句会清除缓存
- 缓存使用Least Recently Used(LRU,最近最少使用)算法回收
- 根据时间表,缓存不会以任何时间顺序来刷新
- 缓存会存储集合或对象(无论查询方法返回什么类型的值)的1024个引用
- 缓存被视为read/write(可读/写)的,意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
这些属性都可以修改,如下:该配置创建了先进先出缓存,每个60秒刷新一次,存储集合或对象的512个引用,而且返回的对象只读。因此在不同线程中的调用者之间修改它们会导致冲突。
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
- eviction回收策略
- LRU:移除最长时间不被使用的对象,默认
- FIFO:按对象进入缓存的顺序来移除,先进先出
- SOFT(软引用):移除基于垃圾回收器状态和软引用规则的对象
- WEAK(弱引用):更积极地移除基于垃圾收集器状态和弱引用规则的对象
- flushInterval刷新间隔:支持任意的正整数,代表一个合理的毫秒形式的时间段。默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新
- size引用数目:支持任意正整数,默认1024
- readOnly只读:支持true或false。只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,性能好。可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式慢一些,但是安全,默认为false
2.Mapper接口中配置二级缓存
@CacheNamespace
public interface SysUserMapper {}
@CacheNamespace注解内容如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface CacheNamespace {
Class<? extends Cache> implementation() default PerpetualCache.class;
//回收策略
Class<? extends Cache> eviction() default LruCache.class;
//刷新周期
long flushInterval() default 0L;
//默认存储对象或集合引用个数
int size() default 1024;
//可读可写
boolean readWrite() default true;
boolean blocking() default false;
}
@CacheNamespace注解不能与<cache/>
同时使用。
可以是@CacheNamespaceRef注解与<cache/>
同时使用或@CacheNamespace与与<cache-ref/>
同时使用。
例如:
@CacheNamespaceRef(SysUserMapper.class)
public interface SysUserMapper {}
<mapper namespace="tk.mybatis.simple.mapper.SysUserMapper">
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
</mapper>
实践-使用二级缓存
配置可读写的缓存时,MyBatis使用SerializedCache序列化缓存来实现可读写缓存类,并通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的实例。
配置为只读缓存,MyBatis就会使用Map来存储缓存值,从缓存中获取的对象是同一个实例。
开启一级和二级缓存,并使用可读写缓存配置
使用可读写缓存,可以使用SerializedCache序列化缓存。这个缓存类要求所有被序列化的对象必须实现Serializable接口。
SysUser
public class SysUser implements Serializable {
private static final long serialVersionUID = 854674092994608168L;
....
}
SysUserMapper
@CacheNamespaceRef(SysUserMapper.class)
public interface SysUserMapper {}
SysUserMapper.xml,注意其中readOnly为false并且select标签中flushCache值为false;它们分别表示可读写缓存和开启一级缓存
<mapper namespace="tk.mybatis.simple.mapper.SysUserMapper">
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="false"/>
<!--查询单个-->
<select id="queryById" flushCache="false" resultMap="BaseResultMap">
select
id, user_name, user_password, user_email, user_info, head_img, create_time
from mybatis.sys_user
where id = #{id}
</select>
</mapper>
测试方法
public void testL2Cache(){
SqlSession sqlSession = getSqlSession();
SysUser user1 = null;
try {
SysUserMapper mapper = sqlSession.getMapper(SysUserMapper.class);
user1 = mapper.queryById(1L);
user1.setUserName("xxxx");
SysUser user2 = mapper.queryById(1L);
LOGGER.info("==================第1个sqlSession========================");
LOGGER.info("user1的信息:{}\nuser2的信息:{}",user1,user2);
} finally {
sqlSession.close();
}
sqlSession = getSqlSession();
try {
SysUserMapper userMapper = sqlSession.getMapper(SysUserMapper.class);
// 查询id为1的用户
SysUser user3 = userMapper.queryById(1L);
// 观察两个user对象的信息,应该会直接从缓存中获取,但是如果是可读写缓存,则不是同一个对象
LOGGER.info("==================第2个sqlSession========================");
LOGGER.info("比较第1个sqlSession中查询结果user1与第2个sqlSession查询结果user3");
LOGGER.info("user1的信息:{}\nuser3的信息:{}",user1,user3);
// 获取user4
SysUser user4 = userMapper.queryById(1L);
// 观察两个user对象的信息,应该会直接从缓存中获取,但是如果是可读写缓存,则不是同一个对象
LOGGER.info("比较第2个sqlSession中查询结果user3与第2个sqlSession查询结果user4");
LOGGER.info("user3的信息:{}\nuser4的信息:{}",user3,user4);
} finally {
sqlSession.close();
}
}
测试结果,我们整个测试方法中有4次调用userMapper.queryById(1L)方法,但是实际执行的sql只有第一次;第一个sqlSession中user1与user2是同一个对象,这是因为开启了一级缓存,获取user2时直接从一级缓存中获取;当第一个sqlSession关闭时,会保存查询的数据到二级缓存中,在这之后二级缓存才有了数据,这也是为什么前两次出现的Cache Hit Ratio [tk.mybatis.simple.mapper.SysUserMapper]: 0.0
中的值都是0.0。第二个sqlSession中user1和user3不是同一个对象,获取user3没有查询sql而是从二级缓存中获取,由于是可读写缓存,通过反序列化出user3,就是一个新的对象了,user3和user4也不是同一个对象,user4也是从二级缓存中获取的,也是一个新的对象。
Cache Hit Ratio [tk.mybatis.simple.mapper.SysUserMapper]: 0.0
==> Preparing: select id, user_name, user_password, user_email, user_info, head_img, create_time from mybatis.sys_user where id = ?
==> Parameters: 1(Long)
<== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
<== Row: 1, admin, 123456, admin@mybatis.tk, <<BLOB>>, <<BLOB>>, 2021-01-28 21:57:32.0
<== Total: 1
Cache Hit Ratio [tk.mybatis.simple.mapper.SysUserMapper]: 0.0
==================第1个sqlSession========================
user1的信息:tk.mybatis.simple.model.SysUser@764faa6
user2的信息:tk.mybatis.simple.model.SysUser@764faa6
Cache Hit Ratio [tk.mybatis.simple.mapper.SysUserMapper]: 0.3333333333333333
==================第2个sqlSession========================
比较第1个sqlSession中查询结果user1与第2个sqlSession查询结果user3
user1的信息:tk.mybatis.simple.model.SysUser@764faa6
user3的信息:tk.mybatis.simple.model.SysUser@19e4fcac
Cache Hit Ratio [tk.mybatis.simple.mapper.SysUserMapper]: 0.5
比较第2个sqlSession中查询结果user3与第2个sqlSession查询结果user4
user3的信息:tk.mybatis.simple.model.SysUser@19e4fcac
user4的信息:tk.mybatis.simple.model.SysUser@52c3cb31
总结:开启二级缓存后,每一次查询都会从二级缓存获取值,如果是第一次调用查询,控制台会输出Cache Hit Ratio [xx]: 0.0
,即命中率为0,当本次的sqlSession还未关闭时,继续调用相同的查询,返回的结果通过一级缓存获取数据,控制台也会输出Cache Hit Ratio [xx]: 0.0
,即二级缓存中还未有数据。当本次sqlSession结束后,就会把所有查询的数据缓存到二级缓存中;后续的sqlSession中查询时,如果是开启的可读写,则会从缓存中反序列化出查询结果对象,这是一个新指向地址的对象;如果开启的是只读,则获取的对象指向的都是同一个地址。
MyBatis默认提供的缓存实现是基于Map实现的内存缓存,已经可以满足基本的应用。当需要缓存大量数据时,不能只靠MyBatis的二级缓存,可以选择EhCache和Redis来保存二级缓存数据。