MyBatis-23MyBatis缓存配置【二级缓存】

简介: MyBatis-23MyBatis缓存配置【二级缓存】

20191208100504276.png

概述


MyBatis-22MyBatis缓存配置【一级缓存】 中介绍了MyBatis默认的一级缓存,了解即可。

这里我们来看下工作中最常用的二级缓存。

MyBaits的二级缓存可以理解为存在于SqlSessionFactory的生命周期中。

目前还没接触过同时存在多个SqlSessionFactory的情况,但可以知道当存在多个SqlSessionFactory时,他们的缓存对象都是绑定在各自对象上的,缓存数据在一般情况下是不相通的。 只有在使用如redis这样的缓存数据库时,才可以共享缓存。


二级缓存的配置


全局开关cacheEnabled


在MyBatis的全局配置settings中有一个参数 cacheEnabled , 这个参数是二级缓存的全局开关,默认为true,初始状态为启用状态。 如果设置为false ,即使后面的二级缓存配置,也不会生效。 默认为true,所以不用配置。


MyBatis的二级缓存是和命名空间绑定的,即二级缓存需要配置在Mapper.xml映射文件中或者配置在Mapper.java接口中。 在映射文件中,命名空间就是XML根节点mapper的namespace属性。 在Mapper接口中,命名空间就是接口的全限定名称。


Mapper.xml中配置二级缓存


在保证二级缓存全局配置开启的情况下,如果想要给PrivilegeMapper.xml开启二级缓存只需要在PrivilegeMapper.xml中添加 <cache/>元素即可。

<?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接口和XML文件关联的时候, namespace的值就需要配置成接口的全限定名称 -->
<mapper namespace="com.artisan.mybatis.xml.mapper.PrivilegeMapper">
  <cache/>
  <!-- 其他配置 -->
</mapper> 

默认的二级缓存功能如下:


映射语句文件中所有的select语句将会被缓存


映射语句文件中所有的insert update delete 语句会刷新缓存


缓存会使用(Least Flush Interval,LRU最近最少使用的)算法来收回


根据时间表(如 no Flush Interval,没有刷新间隔),缓存不会以任何时间顺序来刷新


缓存会存储集合或对象(无论查询方法返回什么类型的值)的1024个引用


缓存会被视为read/wriete(可读/可写)的,意味着对象检索不是共享的,而且可以安全的被调用者修改,而不干扰其他调用者或者线程所做的潜在修改。


所有的这些属性都是可以通过缓存元素的属性来修改,比如

<cache
    eviction="FIFO"
    flushInterval="60000"
    size="512"
    readOnly="true"/>

这个更高级的配置创建了一个FIFP缓存,每隔60S刷新一次,存储集合或对象的512个引用,而且返回的对象被认为是只读的,因而在不同线程中的调用者之间修改它们会导致冲突。


cache可以配置的属性如下:


eviction(收回策略)

LRU 最近最少使用的,移除最长时间不被使用的对象,这是默认值

FIFO 先进先出,按对象进入缓存的顺序来移除它们

SOFT 软引用,移除基于垃圾回收器状态和软引用规则的对象

WEAK 弱引用,更积极的移除基于垃圾收集器状态和弱引用规则的对象

flushInterval(刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。 默认情况不设置,即没有刷新间隔,缓存仅仅在调用语句时刷新


size(引用数目)可以被设置为任意的正整数,要记住缓存的对象数目和运行环境的可用内存资源数目,默认1024


readOnly(只读)属性可以被设置为true后者false。 只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势。 可读写的缓存会通过序列化返回缓存对象的拷贝,这种方式会慢一些,但很安全,因此默认为false


Mapper接口中配置二级缓存


使用注解的方式时,如果想对注解方式启用二级缓存,还需要在Mapper接口中进行配置,如果Mapper接口也存在对应的XML映射文件,两者同时开启缓存时,还需要特殊配置。


只使用注解方式配置二级缓存

当只使用注解方式配置二级缓存时,比如RoleMapper接口中,则需要增加如下配置

@CacheNamespace
public interface RoleMapper {
  ....
}


只需要增加@CacheNamespace(org.apache.ibatis.annotations.CacheNamespace),该注解同样可以配置各项属性

@CacheNamespace{
  eviction = FifoCache.class,
  flushInterval = 60000,
  size = 512,
  readWrite = true 
}


这里的readWrite属性和XML中的readOnly属性一样,用于配置缓存是否为只读类型,在这里true为读写,false为只读,默认为true。


同时使用注解方式和XML映射文件时

当同时使用注解方式和XML映射文件时,如果同时配置了上述的二级缓存(使用xml 以及接口上标注了@CacheNamespace),会抛出如下异常

Cache collection already contains value for **


这是因为Mapper接口和对应的XML文件是相同的命名空间,想使用二级缓存,两者必须同时配置(如果接口不存在使用注解方式的方法,可以只在XML中配置),因此按照上面的方式进行配置就出错,这个时候应该使用参照缓存,

在Mapper接口中,参照缓存配置如下

@CacheNamespaceRef(RoleMapper.class)
public interface RoleMapper{
}


MyBatis很少会同时使用Mapper接口注解方式和XML映射文件,所以参照缓存并不是为了解决这个问题而设计的,参照缓存主要是为了解决脏读


二级缓存的使用


前提:实体类实现Serializable接口


由于MyBatis配置的是可读写的缓存,而MyBatis使用SerializedCache序列化缓存来实现可读写缓存类,并通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的示例。 因此如果配置为只读缓存,MyBatis会使用Map来存储缓存值,这种情况下,从缓存中获取的对象就是同一个实例。


因为使用可读写缓存,可以使用SerializedCache序列化缓存,这个缓存类要求所有被序列化的对象必须实现java.io.Serializable接口,所以需要修改SysPrivilege实体类

public class SysPrivilege implements Serializable {
  private static final long serialVersionUID = 6315662516417216377L;
  // 其他保持不变
}


示例

实体类SysPrivilege实现Serializable接口

public class SysPrivilege implements Serializable {
  private static final long serialVersionUID = 6315662516417216377L;
  // 其他保持不变
}


PrivilegeMapper接口类增加接口方法

/**
   * 
   * 
   * @Title: selectPrivilegeByIdWithCache
   * 
   * @Description: 二级缓存测试方法 ,实体类SysPrivilege必须要实现Serializable
   * 
   * @param id
   * @return
   * 
   * @return: SysPrivilege
   */
  SysPrivilege selectPrivilegeByIdWithCache(Long id);


PrivilegeMapper.xml中配置对单表操作的SQL

<select id="selectPrivilegeByIdWithCache"  resultType="com.artisan.mybatis.xml.domain.SysPrivilege">
    SELECT
      id,
      privilege_name privilegeName,
      privilege_url privilegeUrl
    FROM
      sys_privilege
    WHERE
      id = #{id}
  </select>


单元测试

@Test
  public void selectPrivilegeByIdWithCacheTest() {
    logger.info("selectPrivilegeByIdWithCacheTest");
    SqlSession sqlSession =  getSqlSession();
    SysPrivilege sysPrivilege = null;
    try {
      // 获取接口
      PrivilegeMapper privilegeMapper = sqlSession.getMapper(PrivilegeMapper.class);
      // 调用接口方法
      sysPrivilege = privilegeMapper.selectPrivilegeByIdWithCache(1L);
      sysPrivilege.setPrivilegeName("New Priv");
      // 再次调用相同的接口方法,查询相同的用户
      logger.info("再次调用相同的接口方法,查询相同的用户 Begin");
      SysPrivilege sysPrivilege2 = privilegeMapper.selectPrivilegeByIdWithCache(1L);
      logger.info("再次调用相同的接口方法,查询相同的用户 End");
      // 一级缓存在同一个sqlSession中,虽然没有更新数据库,但是会使用一级缓存
      Assert.assertEquals("New Priv", sysPrivilege2.getPrivilegeName());
      // sysPrivilege 和 sysPrivilege2 是同一个实例
      Assert.assertEquals(sysPrivilege, sysPrivilege2);
    } finally {
      // sqlSession关闭后,在二级缓存开启的前提下,会写入二级缓存
      sqlSession.close();
    }
    logger.info("重新获取一个SqlSession");
    sqlSession = getSqlSession();
    try {
      // 获取接口
      PrivilegeMapper privilegeMapper = sqlSession.getMapper(PrivilegeMapper.class);
      // 调用接口方法
      SysPrivilege sysPrivilege2 = privilegeMapper.selectPrivilegeByIdWithCache(1L);
      sysPrivilege.setPrivilegeName("New Priv");
      // 第二个session获取的权限名为 New Priv
      Assert.assertEquals("New Priv", sysPrivilege2.getPrivilegeName());
      // 这里的sysPrivilege2 和 前一个session中的sysPrivilege不是同一个实例
      Assert.assertNotEquals(sysPrivilege, sysPrivilege2);
      // 获取sysPrivilege3
      SysPrivilege sysPrivilege3 = privilegeMapper.selectPrivilegeByIdWithCache(1L);
      // 这里的sysPrivilege2 和sysPrivilege3是两个不同的实例
      Assert.assertNotEquals(sysPrivilege2, sysPrivilege3);
    } finally {
      // sqlSession关闭后,在二级缓存开启的前提下,会写入二级缓存
      sqlSession.close();
    }
  }


日志

2018-05-07 14:21:22,949  INFO [main] (BaseMapperTest.java:26) - sessionFactory bulit successfully
2018-05-07 14:21:22,949  INFO [main] (BaseMapperTest.java:29) - reader close successfully
2018-05-07 14:21:22,960  INFO [main] (PrivilegeMapperTest.java:38) - selectPrivilegeByIdWithCacheTest
2018-05-07 14:21:23,010 DEBUG [main] (LoggingCache.java:62) - Cache Hit Ratio [com.artisan.mybatis.xml.mapper.PrivilegeMapper]: 0.0
2018-05-07 14:21:23,080 DEBUG [main] (BaseJdbcLogger.java:145) - ==>  Preparing: SELECT id, privilege_name privilegeName, privilege_url privilegeUrl FROM sys_privilege WHERE id = ? 
2018-05-07 14:21:23,270 DEBUG [main] (BaseJdbcLogger.java:145) - ==> Parameters: 1(Long)
2018-05-07 14:21:23,320 TRACE [main] (BaseJdbcLogger.java:151) - <==    Columns: id, privilegeName, privilegeUrl
2018-05-07 14:21:23,320 TRACE [main] (BaseJdbcLogger.java:151) - <==        Row: 1, 用户管理, /users
2018-05-07 14:21:23,340 DEBUG [main] (BaseJdbcLogger.java:145) - <==      Total: 1
2018-05-07 14:21:23,340  INFO [main] (PrivilegeMapperTest.java:48) - 再次调用相同的接口方法,查询相同的用户 Begin
2018-05-07 14:21:23,340 DEBUG [main] (LoggingCache.java:62) - Cache Hit Ratio [com.artisan.mybatis.xml.mapper.PrivilegeMapper]: 0.0
2018-05-07 14:21:23,340  INFO [main] (PrivilegeMapperTest.java:50) - 再次调用相同的接口方法,查询相同的用户 End
2018-05-07 14:21:23,350  INFO [main] (PrivilegeMapperTest.java:60) - 重新获取一个SqlSession
2018-05-07 14:21:23,360 DEBUG [main] (LoggingCache.java:62) - Cache Hit Ratio [com.artisan.mybatis.xml.mapper.PrivilegeMapper]: 0.3333333333333333
2018-05-07 14:21:23,360 DEBUG [main] (LoggingCache.java:62) - Cache Hit Ratio [com.artisan.mybatis.xml.mapper.PrivilegeMapper]: 0.5


日志分析:


第一部分第一次和第二次查询的sysPrivilege 和 sysPrivilege2 是完全相同的实例,这是使用的是默认的一级缓存,所以返回同一个实例。


当调用close方法关闭sqlSession后,sqlSession才回保存查询数据到二级缓存中。 在这之后二级缓存才有了缓存数据。 所以第一次里看到的两次查询时,命中率都是0 。


重新开一个sqlSession,再次获取sysPrivilege 时,因为缓存中有了数据,没有查询数据库,而是输出了命中率,这是的命中率为 0.3333333333333333 , 查询3次,命中1次,因此是1/3


紧接着第4次查询,加上上次的命中,2/4 ,命中率为0.5


注意: 我们为了测试,在获取到数据后,调用了setPrivilegeName方法,实际中并不需要,避免人为产生脏数据。


注意事项(重要)


MyBatis二级缓存的使用场景


  1. 只能在【只有单表操作】的表上使用缓存,不只是要保证这个表在整个系统中只有单表操作,而且和该表有关的全部操作必须全部在一个namespace下
  2. 在可以保证查询远远大于insert,update,delete操作的情况下使用缓存,这一点需要保证在1的前提下才可以


避免使用二级缓存


二级缓存带来的好处远远比不上他所隐藏的危害。

二级缓存


  • 缓存是以namespace为单位的,不同namespace下的操作互不影响。
  • insert,update,delete操作会清空所在namespace下的全部缓存。
  • 通常使用MyBatis Generator生成的代码中,都是各个表独立的,每个表都有自己的namespace。


为什么避免使用二级缓存


在符合【MyBatis二级缓存的使用场景】的要求时,并没有什么危害。

其他情况就会有很多危害了。


针对一个表的某些操作不在他独立的namespace下进行。

例如在UserMapper.xml中有大多数针对user表的操作。但是在一个XXXMapper.xml中,还有针对user单表的操作。


这会导致user在两个命名空间下的数据不一致。如果在UserMapper.xml中做了刷新缓存的操作,在XXXMapper.xml中缓存仍然有效,如果有针对user的单表查询,使用缓存的结果可能会不正确。


更危险的情况是在XXXMapper.xml做了insert,update,delete操作时,会导致UserMapper.xml中的各种操作充满未知和风险。


有关这样单表的操作可能不常见.


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


首先不管多表操作写到那个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>

像上面这个查询,你会写到那个xml中呢??


不管是写到RoleMapper.xml还是UserRoleMapper.xml,或者是一个独立的XxxMapper.xml中。如果使用了二级缓存,都会导致上面这个查询结果可能不正确。


如果你正好修改了这个用户的角色,上面这个查询使用缓存的时候结果就是错的。


这点应该很容易理解。


在我看来,就以MyBatis目前的缓存方式来看是无解的。多表操作根本不能缓存。


如果你让他们都使用同一个namespace(通过<cache-ref>)来避免脏数据,那就失去了缓存的意义。


实际上就是说,二级缓存不能用


如何挽救二级缓存

想更高效率的使用二级缓存是解决不了的

建议放弃二级缓存,在业务层使用可控制的缓存代替更好。


更多注意事项请参考刘老师的博客深入了解MyBatis二级缓存

相关文章
|
2月前
|
存储 缓存 NoSQL
mybatisplus一二级缓存
MyBatis-Plus 继承并优化了 MyBatis 的一级与二级缓存机制。一级缓存默认开启,作用于 SqlSession,适用于单次会话内的重复查询;二级缓存需手动开启,跨 SqlSession 共享,适合提升多用户并发性能。支持集成 Redis 等外部存储,增强缓存能力。
|
6月前
|
Oracle 关系型数据库 Java
【YashanDB知识库】Mybatis-Plus适配崖山配置
【YashanDB知识库】Mybatis-Plus适配崖山配置
|
6月前
|
Java 数据库连接 微服务
微服务——MyBatis配置——事务管理
本段内容主要介绍了事务管理的两种类型:JDBC 和 MANAGED。JDBC 类型直接利用数据源连接管理事务,依赖提交和回滚机制;而 MANAGED 类型则由容器全程管理事务生命周期,例如 JEE 应用服务器上下文,默认会关闭连接,但可根据需要设置 `closeConnection` 属性为 false 阻止关闭行为。此外,提到在使用 Spring + MyBatis 时,无需额外配置事务管理器,因为 Spring 模块自带的功能可覆盖上述配置,且这两种事务管理器类型均无需设置属性。
88 0
|
6月前
|
Java 数据库连接 数据库
微服务——MyBatis配置——多环境配置
在 MyBatis 中,多环境配置允许为不同数据库创建多个 SqlSessionFactory。通过传递环境参数给 SqlSessionFactoryBuilder,可指定使用哪种环境;若忽略,则加载默认环境。`environments` 元素定义环境配置,包括默认环境 ID、事务管理器和数据源类型等。每个环境需唯一标识,确保默认环境匹配其中之一。代码示例展示了如何构建工厂及配置 XML 结构。
86 0
|
6月前
|
缓存 Java 数据库连接
微服务——MyBatis配置——常见配置
本文介绍了 MyBatis 的常见配置及其加载顺序。属性配置优先级为:方法参数传递的属性 &gt; resource/url 属性中配置 &gt; properties 元素中指定属性。同时列举了多个关键配置项,如 `cacheEnabled`(全局缓存开关)、`lazyLoadingEnabled`(延迟加载)、`useGeneratedKeys`(使用 JDBC 自动生成主键)等,并详细说明其作用、有效值及默认值。这些配置帮助开发者优化 MyBatis 的性能与行为。
92 0
|
4月前
|
缓存 Java 数据库连接
Mybatis一级缓存详解
Mybatis一级缓存为开发者提供跨数据库操作的一致性保证,有效减轻数据库负担,提高系统性能。在使用过程中,需要结合实际业务场景选择性地启用一级缓存,以充分发挥其优势。同时,开发者需注意其局限性,并做好事务和并发控制,以确保系统的稳定性和数据的一致性。
139 20
|
6月前
|
缓存 Java 数据库连接
Mybatis一级缓存、二级缓存详讲
本文介绍了MyBatis中的查询缓存机制,包括一级缓存和二级缓存。一级缓存基于同一个SqlSession对象,重复查询相同数据时可直接从缓存中获取,减少数据库访问。执行`commit`操作会清空SqlSession缓存。二级缓存作用于同一namespace下的Mapper对象,支持数据共享,需手动开启并实现序列化接口。二级缓存通过将数据存储到硬盘文件中实现持久化,为优化性能,通常在关闭Session时批量写入缓存。文章还说明了缓存的使用场景及注意事项。
191 7
Mybatis一级缓存、二级缓存详讲
|
7月前
|
缓存 Java 数据库连接
十、MyBatis的缓存
十、MyBatis的缓存
126 6
|
6月前
|
Java 数据库连接 数据库
微服务——SpringBoot使用归纳——Spring Boot集成MyBatis——MyBatis 介绍和配置
本文介绍了Spring Boot集成MyBatis的方法,重点讲解基于注解的方式。首先简述MyBatis作为持久层框架的特点,接着说明集成时的依赖导入,包括`mybatis-spring-boot-starter`和MySQL连接器。随后详细展示了`properties.yml`配置文件的内容,涵盖数据库连接、驼峰命名规范及Mapper文件路径等关键设置,帮助开发者快速上手Spring Boot与MyBatis的整合开发。
790 0
|
6月前
|
缓存 Java 数据库连接
MyBatis篇-常见配置
本文介绍了 MyBatis 的常见配置及事务管理相关内容。首先概述了 MyBatis 属性加载顺序,方法参数属性优先级最高。接着列举了几个常见配置属性,如 cacheEnabled、lazyLoadingEnabled 等,并说明其作用与默认值。在多环境配置部分,讲解如何通过 SqlSessionFactoryBuilder 指定环境,以及 environments 元素的配置细节。最后讨论了两种事务管理模式:JDBC 和 MANAGED,分别适用于不同场景,并指出在使用 Spring 模块时无需额外配置事务管理器。