一、一级缓存
package cn.linst;
import cn.linst.mapper.UserMapper;
import cn.linst.model.SysUser;
import org.apache.ibatis.session.SqlSession;
import org.junit.Assert;
import org.junit.Test;
public class CacheTest extends BaseMapperTest {
@Test
public void testLlCache() {
//获取 SqlSession
SqlSession sqlSession = getSqlSession();
SysUser user1 = null;
try {
//获取 UserMapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//调用 selectById 方法,查询 id =1 的用户
user1 = userMapper.selectById(1L);
//对当前获取的对象重新赋值
user1.setUserName("New Name");
//再次查询获取 id 相同的用户
SysUser user2 = userMapper.selectById(1L);
//虽然没有更新数据库,但是这个用户名和 user1 重新赋值的名字相同
Assert.assertEquals("New Name", user2.getUserName());
// 无论如何, user2和 user1 完全就是同一个实例
Assert.assertEquals(user1, user2);
} finally {
//关闭当前的 sqlSession
sqlSession.close();
}
System.out.println ("开启新的 sqlSession");
// 开始另一个新的 session
sqlSession = getSqlSession();
try {
//获 UserMapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//调 selectById 方法,查询 id =1 的用户
SysUser user2 = userMapper.selectById(1L);
// 第二个 session 获取的用户名仍然是 admin
Assert.assertNotEquals("New Name", user2.getUserName());
//这里的 user2 和前一个 session 查询的结果是两个不同的实例
Assert.assertNotEquals(user1, user2);
//执行删除操作
userMapper.deleteById(2L);
//获取 user3
SysUser user3 = userMapper.selectById(1L);
//这里的 user2 user3 是两个不同的实例
Assert.assertNotEquals(user2, user3);
} finally {
//关闭 sqlSession
sqlSession.close();
}
}
}
运行结果:
DEBUG [main] - ==> Preparing: select * from sys_user where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
TRACE [main] - <== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
TRACE [main] - <== Row: 1, admin, 123456, admin_test@admin_test.email, <<BLOB>>, <<BLOB>>, 2020-01-01 01:11:12.0
DEBUG [main] - <== Total: 1
开启新的 sqlSession
DEBUG [main] - ==> Preparing: select * from sys_user where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
TRACE [main] - <== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
TRACE [main] - <== Row: 1, admin, 123456, admin_test@admin_test.email, <<BLOB>>, <<BLOB>>, 2020-01-01 01:11:12.0
DEBUG [main] - <== Total: 1
DEBUG [main] - ==> Preparing: delete from sys_user where id = ?
DEBUG [main] - ==> Parameters: 2(Long)
DEBUG [main] - <== Updates: 0
DEBUG [main] - ==> Preparing: select * from sys_user where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
TRACE [main] - <== Columns: id, user_name, user_password, user_email, user_info, head_img, create_time
TRACE [main] - <== Row: 1, admin, 123456, admin_test@admin_test.email, <<BLOB>>, <<BLOB>>, 2020-01-01 01:11:12.0
DEBUG [main] - <== Total: 1
UserMapper.xml:加上flushCache=“true”。
<select id="selectById" resultMap="userMap" flushCache="true">
select * from sys_user where id = #{id}
</select>
二、二级缓存
MyBatis二级缓存不同于一级缓存只存在于 SqlSession 的生命周期中,而是可以理解为存在于 SqlSessionFactory 的生命周期中。
当存在多个 SqlSessioηFactory时,它们的缓存都是绑定在各自对象上的,缓存数据在一般情况下是不相通的。只有在使用如 Redis
这样的缓存数据库时,才可以共享缓存。
1、配置二级缓存
二级缓存配置,在 MyBatis 全局配置 settings 中有一个参数 cacheEnabled ,这个参数是二级缓存的全局开关,默认值是 true ,初始状态
为启用状态。如果把这个参数设置为 false ,即使有后面 二级缓存配置,也不会生效。由于这个参数值默认为 true ,所以不必配置,如果想要配置,可以在mybatis-config.xml加入:
<?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="cacheEnabled" value="true"/>
</settings>
</configuration>
MyBatis 二级缓存是和命名空间绑定的。即 二级缓存需要配置在 Mapper.xml 映射文件中。或者配置在Mapper.java接口中。在映射文件中 命名空间就是 XML 根节点 mapper的namespace 属性。在 Mapper 接口中,命名空间就是接口的全限定名称。
1)Mapper.xml 中配置二级缓存
在保证二级缓存的全局配置开启的情况下,给 RoleMapper.xml 开启二级缓存只需要在UserMapp.xml 中添加</cache>
元素即可,添加后的 UserMapper.xml 如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.linst.mapper.UserMapper">
<cache/>
<!-- 其他配置-->
</mapper>
默认二级有以下效果:
映射语句文件中的所有 SELECT 语句将会被缓存。
映射语句文件中的所有 INSERT UPDATE DETELE 语句会刷新缓存。
缓存会使用 Least Recently Used (LRU ,最近最少使用的)算法来收回。
根据时间表(如 no Flush Int rv ,没有刷新间隔),缓存不会以任何时间顺序来刷新。
缓存会存储集合或对象(无论查询方法返回什么类型的值)的 1024 个引用。
缓存会被视为 read/write (可读/可写)的 意味着对象检索不是共享的,而且可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
如:
UserMapper.xml:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"
/>
这个配置创建了 FIFO 缓存,并每隔60秒刷新1次,存储集合或对象的 512个引用, 而且返回的对象被认为是只读的, 因此在不同线程中的调用者之间修改它们会导致冲突。
cache可配置的属性:
属性 | 描述 |
---|---|
eviction | LRU (最近最少使用的) 移除最长时间不被使用的对象,这是默认值;FIFO (先进先出〉 按对象进入缓存的顺序来移除它们;SOFT (软引用) 移除基于垃圾回收器状态和软引用规则的对象;WEAK (弱引用) 更积极地移除基于垃圾收集器状态和弱引用规则的对象。 |
flushInterval | 。可以被设置为任意 正整数 而且它 代表 1个合理的毫秒形式的时间段 。默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新 |
size | 可以被设置为任意正整数,要记住缓存的对象数目和运行环境的可用内存资源数目。默认值是 1024 |
readOnly | 属性可以被设置为 true false,只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改, 这提供了很重要的性能优势。读写的缓存会通过序列化返回缓存对象的拷贝 ,这种方式会慢一些,但是安全, 因此默认是 false |
2)Mapper 接口中配置二级缓存
当只使用注解方式配置二级缓存时,如果在 RoleMapper 接口中, 需要增加如下配置。
RoleMapper:
@CacheNamespace
public interface RoleMapper {
// 接口方法
}
@CacheNamespace中其他配置:
RoleMapper:
@CacheNamespace(
eviction = FifoCache.class,
flushInterval = 60000,
size = 512,
readWrite = true
)
public interface RoleMapper {
}
如果既接口配置又xml配置一起使用,会报错。需要同时配置,改成以下:
RoleMapper接口:
@CacheNamespaceRef(RoleMapper.class) // 参照缓存配置RoleMapper.class,即 RoleMapper.xml 中配置的缓存。
public interface RoleMapper {
}
或者
RoleMapper.xml:
这样配置后, XML 就会引用 Mapper 接口中配置的二级缓存,同样可以避免同时配置二级缓存导致的冲突。
<cache-ref narnespace="cn.linst.mapper.RoleMapper"/>
参照缓存除了能够通过引用其他缓存减少配置外,主要的作用解决脏读。
2、使用二级缓存
Mabatis使用 SerializedCache(org.apache.ibatis.cache.decorators.SerializedCache) 序列化缓存来实现可读写缓存类,井通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的实例。因此,如果配置为只读缓存,Mabatis就会使用 Map 来存储缓存值, 这种情况下 ,从缓存中获取的对象就是同一个实例。
因为使用可读写缓存,可以使用 SerializedCache 序列化缓存,这个缓存类要求所有被序列化的对象必须实现 Serializable(java.io.Serializable)接口。
1)SysRole
package cn.linst.model;
import cn.linst.type.Enabled;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
@Setter
@Getter
public class SysRole implements Serializable {
private static final long serialVersionUID = 5997519209500342536L;
//其他属性
}
2)RoleMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.linst.mapper.RoleMapper">
<!--二级缓存-->
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="false"
/>
</mapper>
3)RoleMapper接口:
package cn.linst.mapper;
import cn.linst.model.SysRole;
import org.apache.ibatis.annotations.*;
import org.apache.ibatis.cache.decorators.FifoCache;
import java.util.List;
@CacheNamespaceRef(RoleMapper.class)
public interface RoleMapper {
//...
}
4)测试类:
RoleMapperTest:
package cn.linst;
import cn.linst.mapper.RoleMapper;
import cn.linst.model.SysPrivilege;
import cn.linst.model.SysRole;
import org.apache.ibatis.session.SqlSession;
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
public class RoleMapperTest extends BaseMapperTest{
@Test
public void testL2Cache() {
//获 sqlSession
SqlSession sqlSession = getSqlSession() ;
SysRole role1 = null;
try {
//获取 RoleMapper 接口
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
//调用 selectById 方法,查询 id =1
role1 = roleMapper.selectById(1L);
//对当前获取的对象重新赋值
role1.setRoleName("New Name");
//再次查询获取 id 相同的用户
SysRole role2 = roleMapper.selectById(1L);
//虽然没有更新数据库,但是这个用户名和 role1 重新赋值的名字相同
Assert.assertEquals("New Name", role2.getRoleName());
// 无论如何, role2 role1 完全就是同一个实例
Assert.assertEquals(role1, role2);
} finally {
// 关闭当前的sqlsession
sqlSession.close();
}
System.out.println("开启新的 sqlSession");
// 开始另一个新的 session
sqlSession = getSqlSession() ;
try {
//获取 RoleMapper 接口
RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
//调用 selectById 方法,查询 id =1 的用户
SysRole role2 = roleMapper.selectById(1L);
//第二个 session 获取的 用户 名是 New Name
Assert.assertEquals("New Name", role2.getRoleName());
//这里的 role2 和前 session 查询的结果是两个不同的实例
Assert.assertNotEquals(role1, role2);
//获 role3
SysRole role3 = roleMapper.selectById(1L);
//这里的 role2 role3 是两个不同的实例
Assert.assertNotEquals(role2, role3);
} finally {
//关闭 sqlSession
sqlSession.close();
}
}
}
运行结果:
DEBUG [main] - Cache Hit Ratio [cn.linst.mapper.RoleMapper]: 0.0
DEBUG [main] - ==> Preparing: select id, role_name roleName, enabled, create_by createBy, create_time createTime from sys_role where id = ?
DEBUG [main] - ==> Parameters: 1(Long)
TRACE [main] - <== Columns: id, roleName, enabled, createBy, createTime
TRACE [main] - <== Row: 1, 管理员, 1, 1, 2020-01-01 17:02:14.0
DEBUG [main] - <== Total: 1
DEBUG [main] - Cache Hit Ratio [cn.linst.mapper.RoleMapper]: 0.0
开启新的 sqlSession
DEBUG [main] - Cache Hit Ratio [cn.linst.mapper.RoleMapper]: 0.3333333333333333
DEBUG [main] - Cache Hit Ratio [cn.linst.mapper.RoleMapper]: 0.5
MyBatis 默认提供的缓存实现是基于 Map 实现的内存缓存,己经可以满足基本的应用是当需要缓存大量的数据时,不能仅仅通过提高内存来使用 MyBatis 级缓存,还可以选择一些类 EhCache 的缓存框架或 Redis 缓存数据库等工具来保存 Mybatis 二级缓存数据。
三、集成 EhCache 缓存
1、添加依赖
pom.xml:
<!-- ehcache-->
<!-- https://mvnrepository.com/artifact/org.mybatis.caches/mybatis-ehcache -->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.0.3</version>
</dependency>
2、配置ehcache
ehcache.xml参考:
https://www.ehcache.org/ehcache.xml
在src/main/resources/目录下新建一个ehcache.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false"
monitoring="autodetect"
dynamicConfig="true">
<diskStore path="/Users/lst/Desktop/tmp"/>
<!-- 默认配置-->
<!--<defaultCache-->
<!--maxEntriesLocalHeap="3000"-->
<!--eternal="false"-->
<!--copyOnRead="true"-->
<!--copyOnWrite="true"-->
<!--timeToIdleSeconds="3600"-->
<!--timeToLiveSeconds="3600"-->
<!--overflowToDisk="true"-->
<!--diskPersistent="true"/>-->
<!-- 针对一个,指定name属性-->
<cache
name="cn.linst.mapper.RoleMapper"
maxEntriesLocalHeap="3000"
eternal="false"
copyOnRead="true"
copyOnWrite="true"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
overflowToDisk="true"
diskPersistent="true"/>
</ehcache>
RoleMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.linst.mapper.RoleMapper">
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
<!-- 其他配置-->
</mapper>
四、集成Redis缓存
1、引入依赖
<!-- redis-->
<dependency>
<groupId>org.mybatis.caches</groupId>
<artifactId>mybatis-redis</artifactId>
<version>1.0.0-beta2</version>
</dependency>
2、配置redis
在src/main/resources/目录下新建一个redis.properties:
host=localhost
port=6379
connectionTimeout=5OOO
soTimeout=5OOO
password=
database=O
clientName=
3、修改 RoleMapper.xml 中的缓存配置
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.linst.mapper.RoleMapper">
<cache type="org.mybatis.caches.redis.RedisCache"/>
<!-- 其他配置-->
</mapper>
五、脏数据的产生和避免
MyBatis 二级缓存是和命名空间绑定的 ,所以通常情况下每个 Mapper 映射文件都拥有自己的二级缓存,不同 Mapper 二级缓存互不影响。
当某几个表可以作为一个业务整体时,通常是让几个会关联的表同时使用同一个二级缓存,这样就能解决脏数据问题。
修改为参照缓存后,再次执行测试,这时就会发现在第二次查询用户和关联角色信息时并没有使用 级缓存,而是重新从数据库获取了数据。
如:
UserMapper.xml:
<mapper namespace="cn.linst.mapper.UserMapper">
<cache-ref namespace="cn.linst.mapper.RoleMapper" />
<!-- 其他配置-->
</mapper>
六、二级缓存适用场景
以查询为主的应用中,只有尽可能少的增、删、改操作。
绝大多数以单表操作存在时。
可以按业务划分对表进行分组时, 如关联的表比较少,可以通过参照缓存进行配置。