背景:今天在公司的一个项目中查询一个数据,一个query方法,在同一个事务中查询了两次,但是第二次查询的结果始终和第一次完全一致,刚开始以为是查询方法写作了,但是后面一想只有参数不一致,没有什么异同,断点模式追踪发现是mybatis自带的缓存机制导致,顺便记录下。

一、什么是查询缓存

mybatis提供查询缓存,用户减轻数据压力,提供数据库性能,并且提供一级缓存和二级缓存。如下图


1、一级缓存是sqlsession级别的缓存,在操作数据库时需要构造sqlsession对象,在对象中有一个(内存区域)数据结构(hashmap)用于存储缓存数据。不同的sqlsession之间的缓存数据区域(hashmap)是互相不影响的。

    一级缓存的作用域是同一个sqlsession,在同一个sqlsession中两次执行相同的sql语句,第一次执行完毕会将数据库中查询的数据写入搭配缓存(内存),第二次会从缓存中获取数据,将不在从数据库查询,从而提高查询效率。当一个sqlsession结束后该sqlsession中的一级缓存也就不存在,mybatis默认开启一级缓存。

2、二级缓存是mapper级别的缓存,多个sqlsession去操作同一个mapper的sql语句,多个sqlsession区操作数据得到数据会存在二级缓存区域,多个sqlsession可以共用二级缓存,二级缓存是跨sqlsession的。

    二级缓存是多个sqlsession共享的,起作用域是mapper的同以一个namespace,不同的sqlsession两次执行相同的namespace下的sql语句且向sql传递的参数也相同即最终执行相同的sql语句,第一次执行完毕会将数据写到缓存(内存),第二次会从缓存中获取数据将不再从数据库查询,从而提高效率,mybatis默认没有开启二级缓存需要setting全部参数中配置开启二级缓存。

如果缓存中有数据就将不会从数据库获取,提高系统性能。

 二、一级缓存

    第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。如果sqlSession去执行commit操作(执行插入、更新、删除),清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息。
    一级缓存的应用:开发中,是将mybatis和spring进行整合开发,事务控制在service中,一个service方法中包括很多mapper方法调用,一个service中相同的调用mapper中相同方法,后面调取会直接从一级缓存中取数据。方法结束,sqlSession关闭 sqlsession关闭后就销毁数据结构,清空缓存Service结束sqlsession关闭。如果是执行两次service调用查询相同的用户信息,不走一级缓存,因为Service方法结束,sqlSession就关闭,一级缓存就清空。

三、二级缓存

  

    mybatis二级缓存需要配置开启,二级缓存与一级缓存区别,二级缓存的范围更大,多个sqlSession可以共享一个UserMapper的二级缓存区域。数据类型仍然为HashMap。UserMapper有一个二级缓存区域(按namespace分,如果namespace相同则使用同一个相同的二级缓存区),其它mapper也有自己的二级缓存区域(按namespace分)。每一个namespace的mapper都有一个二缓存区域,两个mapper的namespace如果相同,这两个mapper执行sql查询到数据将存在相同的二级缓存区域中。
    开启二级缓存:只需要在mapper.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 namespace="com.cqax.business.dao.PictureMapper">
	<!-- 开启mybatis二级缓存 -->
	<cache/>  
	
	
  <resultMap id="BaseResultMap" type="com.cqax.business.model.Picture">
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="created" jdbcType="TIMESTAMP" property="created" />
    <result column="type" jdbcType="INTEGER" property="type" />
    <result column="updated" jdbcType="TIMESTAMP" property="updated" />
    <result column="operator_id" jdbcType="BIGINT" property="operatorId" />
    <result column="picture_id" jdbcType="BIGINT" property="pictureId" />
    <result column="student_id" jdbcType="BIGINT" property="studentId" />
    <result column="sign_id" jdbcType="BIGINT" property="signId" />
    <result column="signsecretkey" jdbcType="VARCHAR" property="signsecretkey" />
    <result column="transferstatus" jdbcType="INTEGER" property="transferstatus" />
    <result column="deviceid" jdbcType="VARCHAR" property="deviceid" />
    <result column="fingerstatus" jdbcType="TINYINT" property="fingerstatus" />
    <result column="sendtimer" jdbcType="INTEGER" property="sendtimer" />
  </resultMap>

四、mybatis刷新缓存(清空缓存)

在mapper的同一个namespace中,如果有其它insert、update、delete操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。 设置statement配置中的flushCache='true' 属性,默认情况下为true即刷新缓存,如果改成false则不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。
比如:

<insertid='insertUser' parameterType='cn.itcast.mybatis.po.User' flushCache='true'>
一般下执行完commit操作都需要刷新缓存,flushCache=true表示刷新缓存默认情况下为true,我们不用去设置它,这样可以避免数据库脏读。

五、Mybatis Cache参数

flushInterval(刷新间隔)可以被设置为任意的正整数,而且它们代表一个合理的毫秒形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句时刷新。size(引用数目)可以被设置为任意正整数,要记住你缓存的对象数目和你运行环境的可用内存资源数目。默认值是1024。readOnly(只读)属性可以被设置为true或false。只读的缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。可读写的缓存会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false。
例如:

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

这个更高级的配置创建了一个 FIFO 缓存,并每隔 60 秒刷新,存数结果对象或列表的 512 个引用,而且返回的对象被认为是只读的,因此在不同线程中的调用者之间修改它们会导致冲突。可用的收回策略有, 默认的是 LRU:

1.LRU – 最近最少使用的:移除最长时间不被使用的对象。

2.FIFO – 先进先出:按对象进入缓存的顺序来移除它们。

3.SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。

4.WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。

六、顺带说下mybatis的xml文件sql中每个字段配置属性

id 在命名空间中唯一的标识符,可以被用来引用这条语句。
parameterType 将会传入这条语句的参数类的完全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过 TypeHandler 推断出具体传入语句的参数,默认值为 unset。
resultType 从这条语句中返回的期望类型的类的完全限定名或别名。注意如果是集合情形,那应该是集合可以包含的类型,而不能是集合本身。使用 resultType 或 resultMap,但不能同时使用。
resultMap 外部 resultMap 的命名引用。结果集的映射是 MyBatis 最强大的特性,对其有一个很好的理解的话,许多复杂映射的情形都能迎刃而解。使用 resultMap 或 resultType,但不能同时使用。
flushCache 将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:false。
useCache 将其设置为 true,将会导致本条语句的结果被二级缓存,默认值:对 select 元素为 true。
timeout 这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)。
fetchSize 这是尝试影响驱动程序每次批量返回的结果行数和这个设置值相等。默认值为 unset(依赖驱动)。
statementType STATEMENT,PREPARED 或 CALLABLE 的一个。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
resultSetType FORWARD_ONLY,SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一个,默认值为 unset (依赖驱动)。
databaseId 如果配置了 databaseIdProvider,MyBatis 会加载所有的不带 databaseId 或匹配当前 databaseId 的语句;如果带或者不带的语句都有,则不带的会被忽略。
resultOrdered 这个设置仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组了,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。
resultSets 这个设置仅对多结果集的情况适用,它将列出语句执行后返回的结果集并每个结果集给一个名称,名称是逗号分隔的。

下面特别说明下之前遇到的问题

1.flushCache
当为select语句时:flushCache默认为false,表示任何时候语句被调用,都不会去清空本地缓存和二级缓存。
当为insert、update、delete语句时:flushCache默认为true,表示任何时候语句被调用,都会导致本地缓存和二级缓存被清空。

2.useCache
当为select语句时:useCache默认为true,表示会将本条语句的结果进行二级缓存。
当为insert、update、delete语句时:useCache属性在该情况下没有。

当为select语句的时候,如果没有去配置flushCache、useCache,那么默认是启用缓存的,所以,如果有必要,那么就需要人工修改配置,修改结果类似下面:
<select id="save" parameterType="XX" flushCache="true" useCache="false">
    ……
</select>
update 的时候如果 flushCache="false",则当你更新后,查询的数据数据还是老的数据。