MyBatis 的一级、二级缓存机制

缓存

什么是缓存

缓存是存在于内存中的临时数据。

为什么使用缓存

使用缓存减少和数据库的交互次数,提高执行效率。(因为查询数据库是一件很费时很费效率的事,还涉及一些硬盘等io操作,而缓存是存在内存中的,读取都很快,而且效率高)

什么样的数据能使用缓存,什么样的数据不能使用

适用于缓存

经常查询并且不经常改变的;
数据的正确与否对最终结果影响不大的;

不适用于缓存

经常改变的数据;
数据的正确与否对最终结果影响很大的;
例如:商品的库存,银行的汇率,股市的牌价;

MyBatis 一级缓存、二级缓存关系

在这里插入图片描述
一级缓存是 SqlSession 级别的缓存。在操作数据库时需要构造 SqlSession 对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的是 SqlSession 之间的缓存数据区(HashMap)是互相不影响。
二级缓存是 Mapper 级别的缓存,多个 SqlSession 去操作同一个 Mapper 的 sql 语句,多个 SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。

1. 一级缓存

1.1 什么是一级缓存mybatis

默认情况下只会开启一级缓存,也就是局部的 session 会话缓存。
每一个 session 会话都会有各自的缓存,是局部的。

1.2 一级缓存配置

<setting name="localCacheScope" value="SESSION"/>

在 MyBatis 的配置文件中添加上面语句,就可以使用一级缓存。共有两个选项,SESSION 或者 STATEMENT。
默认是 SESSION 级别,即在一个 MyBatis 会话中执行的所有语句,都会共享这一个缓存。
一种是 STATEMENT 级别,可以理解为缓存只对当前执行的这一个 Statement 有效;STATEMENT 级别粒度更细。

1.3 什么情况下会命中一级缓存

必须是在一个会话 Session当中,相同的 namespace(同一个命名空间 -> 同一个mapper文件) , sql 和 参数
不能够在查询之前执行 clearCache
中间不能执行 任何 update ,delete , insert (会将SqlSession中的数据全部清空)

mybatis清除一级缓存的几种方法

  1. 主动调用清理缓存的方法
sqlSession.clearCache()
  1. 提交事务,或者关闭session。
sqlSession.commit();
sqlSession.close();
  1. 执行增删改操作回清理缓存

1.4 内部结构

SqlSession 是一个接口,提供了一些 CRUD 的方法,而 SqlSession 的默认实现类是 DefaultSqlSession,DefaultSqlSession 类持有 Executor 接口对象,而 Executor 的默认实现是 BaseExecutor 对象,每个 BaseExecutor 对象都有一个 PerpetualCache 缓存,也就是上图的 Local Cache。
在这里插入图片描述

PerpetualCache

内部持有 HashMap,对一级缓存的操作实则是对 HashMap 的操作。

public class PerpetualCache implements Cache {
    private final String id;
    private Map<Object, Object> cache = new HashMap();
    ...
}

1.5 clear() == map.clear()

在这里插入图片描述
也就是说一级缓存的底层数据结构就是 HashMap。所以说 cache.clear() 其实就是 map.clear(),也就是说,缓存其实是本地存放的一个 map 对象,每一个 SqlSession 都会存放一个 map 对象的引用。

public class PerpetualCache implements Cache {
    ...
    private Map<Object, Object> cache = new HashMap();

    public void clear() {
        this.cache.clear();
    }
    ...
}

insert/delete/update 方法, 清空 localCache

而为了保证缓存里面的数据肯定是准确数据避免脏读,每次我们进行数据修改后(增、删、改操作)就会执行commit操作,清空缓存区域。

1.6 Mybatis的一级缓存时序图

在这里插入图片描述

1.7 一级缓存实验

一级缓存同一个会话共享数据

@Test
public void firstLevelCacheFindUserById() {
    // 第一次查询id为1的用户
    User user1 = userMapper.findUserById(1);
    // 第二次查询id为1的用户
    User user2 = userMapper.findUserById(1);

    System.out.println(user1);
    System.out.println(user2);

    System.out.println(user1 == user2);
}

在这里插入图片描述
我们可以看到,只有第一次真正查询了数据库,后续的查询使用了一级缓存。

同一个会话如果有更新操作则缓存清除

增加了对数据库的修改操作,验证在一次数据库会话中,如果对数据库发生了修改操作,一级缓存是否会失效。

@Test
public void firstLevelCacheOfUpdate() {
    // 第一次查询id为1的用户
    User user1 = userMapper.findUserById(1);
    System.out.println(user1);

    // 更新用户
    User user = new User();
    user.setId(2);
    user.setUsername("tom");

    System.out.println("更新了" + userMapper.updateUser(user) + "个用户");

    // 第二次查询id为1的用户
    User user2 = userMapper.findUserById(1);
    System.out.println(user2);

    System.out.println(user1 == user2);
}

在这里插入图片描述
我们可以看到,在修改操作后执行的相同查询,查询了数据库,一级缓存失效。

一级缓存在多会话中会导致脏数据

开启两个 SqlSession,在 sqlSession1 中查询数据,使一级缓存生效,在 sqlSession2 中更新数据库,验证一级缓存只在数据库会话内部共享。

@Test
public void firstLevelCacheOfScope() {
    SqlSession sqlSession2 = sqlSessionFactory.openSession(true);
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

    System.out.println("userMapper读取数据: " + userMapper.findUserById(1));
    System.out.println("userMapper读取数据: " + userMapper.findUserById(1));

    // 更新用户
    User user = new User();
    user.setId(1);
    user.setUsername("andy");
    System.out.println("userMapper2更新了" + userMapper2.updateUser(user) + "个用户");

    System.out.println("userMapper读取数据: " + userMapper.findUserById(1));
    System.out.println("userMapper2读取数据: " + userMapper2.findUserById(1));
}

在这里插入图片描述
sqlSession2 更新了 id 为 1 的用户的姓名,从 riemann 改为了 andy,但 session1 之后的查询中,id 为 1 的学生的名字还是 riemann,出现了脏数据,也证明了之前的设想,一级缓存只在数据库会话内部共享。

解决方式:在配置一级缓存作用范围的时候将其设置为 STATEMENT,那么缓存仅对当前执行的语句有效,当语句执行完毕后,缓存就会被清空。

<settings>
  <setting name="localCacheScope" value="STATEMENT"/>
</settings>

2. 二级缓存

2.2 二级缓存工作流程

在上文中提到的一级缓存中,其最大的共享范围就是一个 SqlSession 内部,如果多个 SqlSession 之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰 Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示。
在这里插入图片描述
二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被多个 SqlSession 共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

MyBatis 是默认关闭二级缓存的,因为对于增删改操作频繁的话,那么二级缓存形同虚设,每次都会被清空缓存。

2.2 二级缓存配置

和一级缓存默认开启不一样,二级缓存需要我们手动开启。

  1. 全局配置文件 SqlMapConfig.xml
<!--开启二级缓存-->
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>
  1. 在 UserMapper.xml 文件中开启二级缓存

mapper 代理模式

<!--开启二级缓存-->
<cache />

注解开发模式

@CacheNamespace(implementation = PerpetualCache.class) // 开启二级缓存
public interface UserMapper {
}

开启二级缓存后,还需要将要缓存的实体类去实现 Serializable 序列化接口,为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们再取出这个缓存的话,就需要反序列化。所以 MyBatis 的所有 pojo 类都要去实现 Serializable 序列化接口。

二级缓存何时存入

在关闭sqlsession后(close),才会把该sqlsession一级缓存中的数据添加到namespace的二级缓存中。

二级缓存如何清空

当对SqlSession执行更新操作(update、delete、insert)后并执行commit时,不仅清空其自身的一级缓存(执行更新操作的效果),也清空二级缓存(执行commit()的效果)。

2.3 二级缓存实验

测试二级缓存与 SqlSession 无关

@Test
public void secondLevelCache() {
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();

    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);

    // 第一次查询id为1的用户
    User user1 = userMapper1.findUserById(1);
    sqlSession1.close(); // 清空一级缓存
    System.out.println(user1);

    // 第二次查询id为1的用户
    User user2 = userMapper2.findUserById(1);
    System.out.println(user2);

    System.out.println(user1 == user2);
}

在这里插入图片描述
第一次查询时,将查询结果放入缓存中,第二次查询,即使 sqlSession1.close(); 清空了一级缓存,第二次查询依然不发出 sql 语句。
这里的你可能有个疑问,这里不是二级缓存了吗?怎么 user1 与 user2 不相等?

这是因为二级缓存的是数据,并不是对象。而 user1 与 user2 是两个对象,所以地址值当然也不想等。

测试执行 commit(),二级缓存数据清空

@Test
public void secondLevelCacheOfUpdate() {
    SqlSession sqlSession1 = sqlSessionFactory.openSession();
    SqlSession sqlSession2 = sqlSessionFactory.openSession();
    SqlSession sqlSession3 = sqlSessionFactory.openSession();

    UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
    UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
    UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);

    // 第一次查询id为1的用户
    User user1 = userMapper1.findUserById(1);
    sqlSession1.close(); // 清空一级缓存

    User user = new User();
    user.setId(3);
    user.setUsername("edgar");
    userMapper3.updateUser(user);
    sqlSession3.commit(); //清空二级缓存

    // 第二次查询id为1的用户
    User user2 = userMapper2.findUserById(1);
    sqlSession2.close();

    System.out.println(user1 == user2);
}

在这里插入图片描述
在 sqlSession3 更新数据库,并提交事务后,sqlsession2 的 UserMapper namespace 下的查询走了数据库,没有走 Cache。

多表操作一定不能使用缓存

首先不管多表操作写到那个namespace下,都会存在某个表不在这个namespace下的情况。
例如两个表:role和user_role,如果我想查询出某个用户的全部角色role,就一定会涉及到多表的操作。

<select id="selectUserRoles" resultType="UserRoleVO">
    select * from user_role a,role b where a.roleid = b.roleid and a.userid = #{userid}
</select>

不管是写到RoleMapper.xml还是UserRoleMapper.xml,或者是一个独立的XxxMapper.xml中。如果使用了二级缓存,都会导致上面这个查询结果可能不正确。
如果你正好修改了这个用户的角色,上面这个查询使用缓存的时候结果就是错的。
这点应该很容易理解。

useCache 和 flushCache

<select id="findAll" resultMap="userMap" useCache="false" flushCache="true">
    select * from user u left join orders o on u.id = o.uid
</select>

设置 statement 配置中的 flushCache=“true” 属性,默认情况下为 true,即刷新缓存,一般执行完 commit 操作都需要刷新缓存,flushCache=“true” 表示刷新缓存,这样可以避免增删改操作而导致的脏读问题。默认不要配置。

<select id="findAll" resultMap="userMap" useCache="false">
    select * from user u left join orders o on u.id = o.uid
</select>

useCache 是用来设置是否禁用二级缓存的,在 statement 中设置 useCache=“false”,可以禁用当前 select 语句的二级缓存,即每次都会去数据库查询。

参考文章:
深入浅出 MyBatis 的一级、二级缓存机制
Mybatis 一级缓存
mybatis的一级缓存和二级缓存

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值