六、Mybatis缓存

⼀级缓存

  1. 在⼀个sqlSession中,对User表根据id进⾏两次查询,查看他们发出sql语句的情况

    1. @Test
    2. public void oneLevelCache() {
    3. // 第一次执行将结果放入缓存
    4. System.out.println(iRoleMapper.findById(2));
    5. // 同sqlSession直接从缓存中取
    6. System.out.println(iRoleMapper.findById(2));
    7. }

    通过console可以看到,实际上她只执行了一次sql

    1. ==> Preparing: select id, role_name roleName from role where id = ?
    2. ==> Parameters: 2(Integer)
    3. <== Columns: id, roleName
    4. <== Row: 2, 游客
    5. <== Total: 1
    6. [Role(id=2, roleName=游客)]
    7. [Role(id=2, roleName=游客)]
  2. 同样是对user表进⾏两次查询,但在两次查询之间进⾏了⼀次update操作

    1. @Test
    2. public void oneLevelCache2() {
    3. // 第一次执行将结果放入缓存
    4. System.out.println(iRoleMapper.findById(2));
    5. // 由于默认开启自动提交, sqlSession.commit() 会清除缓存信息
    6. Role role2 = new Role();
    7. role2.setId(2);
    8. role2.setRoleName("leader2");
    9. iRoleMapper.update(role2);
    10. // 再次执行sql
    11. System.out.println(iRoleMapper.findById(2));
    12. }

    此时mybatis会查询两次

    1. ==> Preparing: select id, role_name roleName from role where id = ?
    2. ==> Parameters: 2(Integer)
    3. <== Columns: id, roleName
    4. <== Row: 2, 游客
    5. <== Total: 1
    6. [Role(id=2, roleName=游客)]
    7. ==> Preparing: update role set role_name = ? where id = ?
    8. ==> Parameters: leader2(String), 2(Integer)
    9. <== Updates: 1
    10. ==> Preparing: select id, role_name roleName from role where id = ?
    11. ==> Parameters: 2(Integer)
    12. <== Columns: id, roleName
    13. <== Row: 2, leader2
    14. <== Total: 1
    15. [Role(id=2, roleName=leader2)]

    总结

    1、第⼀次发起查询⽤户id为1的⽤户信息,先去找缓存中是否有id为1的⽤户信息,如果没有,从数据
    查询⽤户信息。得到⽤户信息,将⽤户信息存储到⼀级缓存中。
    2、 如果中间sqlSession去执⾏commit操作(执⾏插⼊、更新、删除),则会清空SqlSession中的 ⼀
    级缓存,这样做的⽬的为了让缓存中存储的是最新的信息,避免脏读。
    3、 第⼆次发起查询⽤户id为1的⽤户信息,先去找缓存中是否有id为1的⽤户信息,缓存中有,直 接从
    缓存中获取⽤户信息。
    image.png

    ⼀级缓存原理探究与源码分析

    提到⼀级缓存就绕不开SqlSession,所以我们就直接从SqlSession,看看有没有创建缓存或者与缓存有关的属性或者⽅法
    image.png
    在SqlSession中发现好像只有clearCache()和缓存沾点关系,那么就直接从这个方法⼊手,分析源码时,我们要看它(此类)是谁,它的⽗类和⼦类分别又是谁,对如上关系了解了,才会对这个类有更深的认识,分析了⼀圈,你可能会得到如下这个流程图
    image.png
    再深⼊分析,流程⾛到Perpetualcache中的clear()⽅法之后,会调⽤其cache.clear()⽅法,那么这个cache是什么东⻄呢?点进去发现,
    cache其实就是private Map cache = new HashMap();也就是⼀个Map,所以说cache.clear()其实就是map.clear(),也就是说,缓存其实就是
    本地存放的⼀个map对象,每⼀个SqISession都会存放⼀个map对象的引⽤,那么这个cache是何时创建的呢?
    我们可以查看一下putObject(Object key, Object value)这个方法,这个方法是向缓存map里put,找一下哪里调用了这个方法。
    image.png
    排除一些不太认识的缓存类,最熟悉的就是BaseExecutor了,我们可以不断的向上查找调用当前方法的方法,最后得出下面这个流程图
    image.png
    queryFromDatabase的主要意思就是从数据库中查询后向cache中put缓存,这里有个重要的参数key,这个key其实就是在第三步中createCacheKey中创建的。(此处第4步query方法中,会调用 localCache.getObject(key),如果获取到则直接返回从缓存中获取的数据
    那么我们来分析下这个方法

    1. @Override
    2. public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    3. if (closed) {
    4. throw new ExecutorException("Executor was closed.");
    5. }
    6. CacheKey cacheKey = new CacheKey();
    7. cacheKey.update(ms.getId());
    8. cacheKey.update(rowBounds.getOffset());
    9. cacheKey.update(rowBounds.getLimit());
    10. cacheKey.update(boundSql.getSql());
    11. List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    12. TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    13. // mimic DefaultParameterHandler logic
    14. for (ParameterMapping parameterMapping : parameterMappings) {
    15. if (parameterMapping.getMode() != ParameterMode.OUT) {
    16. Object value;
    17. String propertyName = parameterMapping.getProperty();
    18. if (boundSql.hasAdditionalParameter(propertyName)) {
    19. value = boundSql.getAdditionalParameter(propertyName);
    20. } else if (parameterObject == null) {
    21. value = null;
    22. } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
    23. value = parameterObject;
    24. } else {
    25. MetaObject metaObject = configuration.newMetaObject(parameterObject);
    26. value = metaObject.getValue(propertyName);
    27. }
    28. cacheKey.update(value);
    29. }
    30. }
    31. if (configuration.getEnvironment() != null) {
    32. // issue #176
    33. cacheKey.update(configuration.getEnvironment().getId());
    34. }
    35. return cacheKey;
    36. }

    大概看一遍,其重要逻辑就是把MappedStatement、RowBounds、BoundSql、Object parameterObject、configuration.getEnvironment().getId()通过CacheKey的update方法构造CacheKey。

    configuration.getEnvironment().getId() 其实就是xml配置中environment 的id

  1. public void update(Object object) {
  2. int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
  3. count++;
  4. checksum += baseHashCode;
  5. baseHashCode *= count;
  6. hashcode = multiplier * hashcode + baseHashCode;
  7. updateList.add(object);
  8. }

update方法其实就是重新计算 count、checksum、hashcode并且把新的对象更到添加到updateList(private transient List<Object> updateList;),再看一下CacheKey的equals方法

  1. @Override
  2. public boolean equals(Object object) {
  3. if (this == object) {
  4. return true;
  5. }
  6. if (!(object instanceof CacheKey)) {
  7. return false;
  8. }
  9. final CacheKey cacheKey = (CacheKey) object;
  10. if (hashcode != cacheKey.hashcode) {
  11. return false;
  12. }
  13. if (checksum != cacheKey.checksum) {
  14. return false;
  15. }
  16. if (count != cacheKey.count) {
  17. return false;
  18. }
  19. for (int i = 0; i < updateList.size(); i++) {
  20. Object thisObject = updateList.get(i);
  21. Object thatObject = cacheKey.updateList.get(i);
  22. if (!ArrayUtil.equals(thisObject, thatObject)) {
  23. return false;
  24. }
  25. }
  26. return true;
  27. }

可以看到,比较两个CacheKey的关键就是上述内容。

二级缓存

⼆级缓存的原理和⼀级缓存原理⼀样,第⼀次查询,会将数据放⼊缓存中,然后第⼆次查询则会直接去缓存中取。但是⼀级缓存是基于sqlSession的,⽽⼆级缓存是基于mapper⽂件的namespace的,也就是说多个sqlSession可以共享⼀个mapper中的⼆级缓存区域,并且如果两个mapper的namespace 相同,即使是两个mapper,那么这两个mapper中执⾏sql查询到的数据也将存在相同的⼆级缓存区域中。
image.png

如何使用二级缓存

修改sqlMapConfig.xml,增加配置

  1. <settings>
  2. <setting name="cacheEnabled" value="true"/>
  3. </settings>

之后,修改需要二级缓存的mapper.xml文件,加入cache即可

  1. <cache/>

因为我们没有指定type,所以默认使用mybatis的二级缓存。
开启了⼆级缓存后,还需要将要缓存的pojo实现Serializable接⼝,为了将缓存数据取出执⾏反序列化操作,因为⼆级缓存数据存储介质多种多样,不⼀定只存在内存中,有可能存在硬盘中,如果我们要再取这个缓存的话,就需要反序列化了。所以mybatis中的pojo都去实现Serializable接口。
测试一下

  1. @Test
  2. public void twoLevelCache() {
  3. SqlSession sqlSession = sqlSessionFactory.openSession();
  4. IUserMapper iUserMapper = sqlSession.getMapper(IUserMapper.class);
  5. System.out.println(iUserMapper.findAll());
  6. sqlSession.close();
  7. SqlSession sqlSession1 = sqlSessionFactory.openSession();
  8. IUserMapper iUserMapper1 = sqlSession1.getMapper(IUserMapper.class);
  9. System.out.println(iUserMapper1.findAll());
  10. sqlSession1.close();
  11. }
  1. Cache Hit Ratio [com.lpy.mapper.IUserMapper]: 0.0
  2. Opening JDBC Connection
  3. Created connection 1615039080.
  4. Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@60438a68]
  5. ==> Preparing: SELECT *, o.id oid FROM user u left join orders o on o.uid = u.id
  6. ==> Parameters:
  7. <== Columns: id, username, password, birthday, id, order_time, total, uid, oid
  8. <== Row: 1, lucy, 1, 2020-12-02, 1, 2021-10-23 10:01:46.0, 5.00, 1, 1
  9. <== Row: 2, lip, 2, 2021-10-21, 2, 2021-10-25 10:01:46.0, 10.00, 2, 2
  10. <== Row: 3, sc, 3, 2021-10-21, 3, 2021-10-27 10:01:46.0, 15.00, 3, 3
  11. <== Total: 3
  12. ...
  13. Cache Hit Ratio [com.lpy.mapper.IUserMapper]: 0.5

首先可以看到不同的SqlSession只执行了一条sql,证明二级缓存生效了。其次可以看到第一查询缓存命中率 0%,是因为之前没有缓存,再次查询后命中率50%,是因为查询两次,有一次命中的缓存。
此处如果想既使xml中的配置二级缓存生效,又想mapper中的配置二级缓存生效,需要在Mapper接口加注解,name的值为当前接口的全类名

  1. @CacheNamespaceRef(name = "com.lpy.mapper.IUserMapper")

再来测试一下修改的情况

  1. @Test
  2. public void twoLevelCache2() {
  3. SqlSession sqlSession = sqlSessionFactory.openSession();
  4. IUserMapper iUserMapper = sqlSession.getMapper(IUserMapper.class);
  5. System.out.println(iUserMapper.findById(2));
  6. sqlSession.close();
  7. SqlSession sqlSession1 = sqlSessionFactory.openSession();
  8. IUserMapper iUserMapper1 = sqlSession1.getMapper(IUserMapper.class);
  9. User user = new User();
  10. user.setId(2);
  11. user.setUsername("lip");
  12. iUserMapper1.update(user);
  13. sqlSession1.commit();
  14. sqlSession1.close();
  15. SqlSession sqlSession2 = sqlSessionFactory.openSession();
  16. IUserMapper iUserMapper2 = sqlSession2.getMapper(IUserMapper.class);
  17. System.out.println(iUserMapper2.findById(2));
  18. sqlSession1.close();
  19. }
  1. Cache Hit Ratio [com.lpy.mapper.IUserMapper]: 0.0
  2. Opening JDBC Connection
  3. Created connection 230528013.
  4. Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dbd940d]
  5. ==> Preparing: select * from user where id = ?
  6. ==> Parameters: 2(Integer)
  7. <== Columns: id, username, password, birthday
  8. <== Row: 2, lip, 2, 2021-10-21
  9. <== Total: 1
  10. User(id=2, username=lip, password=2, birthday=2021-10-21, orders=null)
  11. Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dbd940d]
  12. Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@dbd940d]
  13. Returned connection 230528013 to pool.
  14. Opening JDBC Connection
  15. Checked out connection 230528013 from pool.
  16. Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dbd940d]
  17. ==> Preparing: update user set username = ? where id = ?
  18. ==> Parameters: lip(String), 2(Integer)
  19. <== Updates: 1
  20. Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@dbd940d]
  21. Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dbd940d]
  22. Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@dbd940d]
  23. Returned connection 230528013 to pool.
  24. Cache Hit Ratio [com.lpy.mapper.IUserMapper]: 0.0
  25. Opening JDBC Connection
  26. Checked out connection 230528013 from pool.
  27. Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dbd940d]
  28. ==> Preparing: select * from user where id = ?
  29. ==> Parameters: 2(Integer)
  30. <== Columns: id, username, password, birthday
  31. <== Row: 2, lip, 2, 2021-10-21
  32. <== Total: 1
  33. User(id=2, username=lip, password=2, birthday=2021-10-21, orders=null)
  34. Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@dbd940d]

此处可以看到同一级缓存,修改操作后又重新执行了sql

useCache和flushCache

在xml的statement中配置useCache=”false”则查询会禁用二级缓存,每次都查询数据库,默认为true,即会使用二级缓存。

  1. <select id="findAll" resultMap="userMap" useCache="false">
  2. SELECT *, o.id oid
  3. FROM user u
  4. left join orders o on o.uid = u.id
  5. </select>

同样的 如果配置flushCache=”false” 可以使增删改操作不清除缓存,但这样会造成脏读问题,所以一般默认刷新缓存就好。
注解方式:

  1. @Options(useCache = false)
  2. @Select("select * from user where id = #{id}")
  3. User findById(Integer id);
  4. @Options(flushCache = Options.FlushCachePolicy.FALSE)
  5. @Update("update user set username = #{username} where id = #{id}")
  6. void update(User user);

二级缓存整合redis

上⾯我们介绍了 mybatis⾃带的⼆级缓存,但是这个缓存是单服务器⼯作,⽆法实现分布式缓存。 那么什么是分布式缓存呢?假设现在有两个服务器1和2,⽤户访问的时候访问了 1 服务器,查询后的缓存就会放在 1 服务器上,假设现在有个⽤户访问的是2服务器,那么他在2服务器上就⽆法获取刚刚那个缓
存,如下图所示: