Mybatis 缓存
MyBatis 是一个优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 通过缓存来避免重复的 SQL 查询,提高查询性能。MyBatis 提供了两种缓存机制:一级缓存和二级缓存。
一级缓存(SqlSession 级别的缓存)
一级缓存是 MyBatis 默认开启的,它是一个基于 PerpetualCache 的 HashMap 本地缓存,其作用范围是一个 SqlSession。作用范围为一个 SqlSession 意味着在同一个 SqlSession 内进行查询时,不会重复执行相同的 SQL 语句,而是从缓存中获取。当 SqlSession 提交或回滚时,缓存会被清空。
- 实现原理
MyBatis 的一级缓存是通过 Executor
类实现的。在执行 SQL 查询时,先根据 SQL 语句、参数等生成一个缓存的 key。然后在缓存中查找是否存在相应的数据。如果存在,则直接从缓存中获取;如果不存在,则执行 SQL 查询并将结果放入缓存中。
- 局限性
- 作用范围仅限于一个 SqlSession,无法跨多个 SqlSession 共享。
- 在多个 SqlSession 中执行相同的 SQL 语句时,不能有效利用缓存,可能导致数据不一致。
一级缓存测试
基本代码实现和以前一样,这里为了方便,所以在以前代码基础上,进行测试:
- UserMapper 接口内容如下:
public interface UserMapper {
//查询所有用户
List<User> getUserList();
//根据ID查询用户信息
User getUserById(int id);
}
- 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="com.hb.dao.UserMapper">
<select id="getUserList" resultType="User">
select * from mybatis.user;
</select>
<select id="getUserById" resultType="User">
select * from mybatis.user where id = #{id}
</select>
</mapper>
- 测试类如下:
public class UserMapperTest {
@Test
public void getUserByIdTest() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.getUserById(1);
System.out.println(user1);
System.out.println("========================");
User user2 = mapper.getUserById(1);
System.out.println(user2);
sqlSession.close();
}
}
结果我们可以看到:
缓存失效的情况:
1、当我们查询不同的内容时;
2、对数据库内容的增加、修改、删除,会改变原来的数据,必定刷新缓存,会导致缓存失效,对数据库进行二次访问。
这里对 UserMapper 接口和 UserMapper.xml进行修改,加上 update 的相关操作然后修改测试代码:
@Test
public void getUserByIdTest() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.getUserById(1);
System.out.println(user1);
mapper.updateUserInfo(new User(4,"小孙","77777776"));
sqlSession.commit();
System.out.println("========================");
User user2 = mapper.getUserById(1);
System.out.println(user2);
sqlSession.close();
}
查看结果:
3、查询不同的的Mapper.xml
4、手动清理了缓存:在测试代码中的sqlSession生命周期内,执行了sqlSession.clearCache();
二级缓存(Mapper 级别的缓存)
二级缓存的作用范围是一个 Mapper,可以被多个 SqlSession 共享。二级缓存需要用户手动开启。开启方法是在 Mapper 的 XML 配置文件中添加 <cache />
标签。
这个简单语句的效果如下:
- 映射语句文件中的所有 select 语句的结果将会被缓存。
- 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
- 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
- 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
- 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
- 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
<注意:缓存只作用于 cache 标签所在的映射文件中的语句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的语句将不会被默认缓存。你需要使用 @CacheNamespaceRef 注解指定缓存作用域。>
这些属性可以通过 cache 元素的属性来修改。比如:
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
实现原理
二级缓存是通过 Cache
接口实现的。MyBatis 提供了多种二级缓存的实现,如 PerpetualCache
(基于 HashMap 实现)、FifoCache
(基于先进先出策略实现)、SoftCache
(基于软引用实现)等。
默认的清除策略是 LRU(最近最少使用:移除最长时间不被使用的对象)。
用户也可以自定义实现二级缓存。
当一个 SqlSession 查询数据时,会先从二级缓存中查找。如果二级缓存中不存在,则从一级缓存中查找。如果一级缓存中也不存在,则执行 SQL 查询,并将结果保存到一级缓存和二级缓存中。当一个 SqlSession 提交或回滚时,会将一级缓存中的数据同步到二级缓存中。
优势和注意事项
二级缓存的优势在于,它可以跨多个 SqlSession 共享数据,提高查询性能。但是,使用二级缓存时需要注意以下几点:
- 要保证缓存数据的线程安全。MyBatis 提供的实现都是线程安全的,但自定义实现时需要注意。
- 只有在
<cache />
标签中配置的 Mapper 才会启用二级缓存。如果不想启用缓存,可以在<select />
标签中添加useCache="false"
属性。 - 对于更新操作,MyBatis 会自动清空与更新相关的二级缓存。但在分布式环境下,需要注意缓存同步问题。可以考虑使用分布式缓存,如 Redis、Memcached 等。
- 二级缓存是事务性的。这意味着,当 SqlSession 完成并提交时,或是完成并回滚,但没有执行 flushCache=true 的 insert/delete/update 语句时,缓存会获得更新。
总之,MyBatis 的缓存机制可以大幅提高查询性能,但在使用时需要注意作用范围、线程安全、缓存同步等问题。在实际开发中,应根据业务需求选择合适的缓存策略。
自定义缓存
MyBatis 是一款优秀的 ORM 框架,它提供了一些默认的缓存配置来提高查询性能。不过有时候我们需要根据具体的业务需求来自定义缓存。
MyBatis 支持自定义缓存,可以通过实现 Cache 接口来实现自定义缓存。Cache 接口中定义了一些方法,包括 putObject、getObject、removeObject、clear 等,这些方法用于实现缓存的增加、查询、删除和清空等操作。
下面自定义一个缓存:
public class MyCache implements Cache {
private Map<Object, Object> cache = new HashMap<Object, Object>();
private String id;
public MyCache(String id) {
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public void clear() {
cache.clear();
}
@Override
public int getSize() {
return cache.size();
}
@Override
public ReadWriteLock getReadWriteLock() {
// 返回一个读写锁,用于并发控制
return null;
}
}
在这个示例中,我们实现了 Cache 接口,并且使用 HashMap 作为缓存的存储结构。在 putObject 方法中,将 key-value 对放入缓存中;在 getObject 方法中,根据 key 获取缓存中的 value;在 removeObject 方法中,根据 key 将缓存中的 key-value 对移除;在 clear 方法中,清空缓存。这些操作都是非常简单的。
需要注意的是,上面的示例中的 getReadWriteLock 方法返回了 null,这意味着我们没有提供并发控制,这个自定义缓存是线程不安全的。如果需要实现线程安全,可以返回一个读写锁,用于并发控制。
自定义缓存完成后,我们需要在 MyBatis 的配置文件中配置自定义缓存。假设我们的自定义缓存类是 MyCache,我们可以在 MyBatis 配置文件中增加配置:
<cache type="com.domain.something.MyCustomCache">
<property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>
其中,type 属性指定了自定义缓存的类名。
这样,我们就完成了自定义缓存的操作。需要注意的是,自定义缓存可能会影响数据的一致性,因此在使用自定义缓存时需要谨慎考虑。