【Mybatis】Mybatis之缓存、懒加载

469 阅读3分钟

这是我参与8月更文挑战的第12天,活动详情查看:8月更文挑战

上文介绍了Mybatis之使用注解实现增删改查。在上文文末也提到了,由于基于注解的方式可读性及可维护性较差,因此较少使用,包括在以后的各种Mybatis的功能演示过程中也不会涉及到使用注解,都会使用XML进行演示。本文将介绍一下Mybatis的缓存及懒加载的使用,以及存在的问题。

一级缓存

使用

Mybatis的一级缓存默认是开启的。因此直接使用即可。一级缓存是SqlSession级别的,因此只能在同一个SqlSession对象下使用。

  • Mapper接口中的方法
/**
 * 根据ID查询商品
 */
Purchase findByID(Integer id);
  • XML中的代码
<select id="findByID" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from purchase
    where id = #{id,jdbcType=INTEGER}
</select>
  • 测试代码及结果输出
@Test
public void oneLevelCache() {
    // 使用同一个SqlSession进行操作
    PurchaseMapper mapper = sqlSession.getMapper(PurchaseMapper.class);
    System.out.println("======第一次查询======");
    System.out.println(mapper.findByID(2));

    System.out.println("======第二次查询======");
    System.out.println(mapper.findByID(2));
}
======第一次查询======
DEBUG [main] - ==>  Preparing: select id, `name`, price, category from purchase where id = ? 
DEBUG [main] - ==> Parameters: 2(Integer)
DEBUG [main] - <==      Total: 1
Purchase{id=2, name='爆米花', price=8, category=2}
======第二次查询======
Purchase{id=2, name='爆米花', price=8, category=2}
  • 可以看到,第一次查询打印了SQL并查出了结果,但是第二次并没有打印SQL,但依然查询出了结果。这就是Mybatis的一级缓存。

  • 一级缓存默认是打开的,那怎么关闭呢?很简单,只需要在XML代码的select标签中将flushCache属性的值设置为true即可。

<select id="findByID" parameterType="java.lang.Integer" resultMap="BaseResultMap" flushCache="true">
    select
    <include refid="Base_Column_List" />
    from purchase
    where id = #{id,jdbcType=INTEGER}
</select>
  • 修改XML代码之后再次执行测试代码,可以看到,第二次查询也打印了SQL,此时一级缓存已经被关闭了。
======第一次查询======
DEBUG [main] - ==>  Preparing: select id, `name`, price, category from purchase where id = ? 
DEBUG [main] - ==> Parameters: 2(Integer)
DEBUG [main] - <==      Total: 1
Purchase{id=2, name='爆米花', price=8, category=2}
======第二次查询======
DEBUG [main] - ==>  Preparing: select id, `name`, price, category from purchase where id = ? 
DEBUG [main] - ==> Parameters: 2(Integer)
DEBUG [main] - <==      Total: 1
Purchase{id=2, name='爆米花', price=8, category=2}

问题

  • 一级缓存的问题由于是SqlSession级别的,因此主要在同一次请求中出现。例如,按如下图操作,在第二次select时,缓存中的数据1已经被修改,查询出来的是缓存中被修改过的数据1,而不是数据库中的真实数据1.
sequenceDiagram
Note left of Client: 三次操作处于同一事务中
Client ->> DB : 第一次select并使用数据1
activate Client
Client ->> DB : update缓存中的数据1
Client ->> DB : 第二次select并使用数据1
deactivate Client
@Test
@Transactional
public void oneLevelQuestion() {
    PurchaseMapper mapper = sqlSession.getMapper(PurchaseMapper.class);
    System.out.println("=========第一次查询并使用=======");
    Purchase purchase = mapper.findByID(2);
    System.out.println(purchase);
    //对缓存的数据进行修改
    purchase.setPrice(null);
    System.out.println("=========第二次查询并使用=======");
    Purchase purchase1 = mapper.findByID(2);
    System.out.println(purchase1);
}
=========第一次查询并使用=======
DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.z_run.mapper.PurchaseMapper]: 0.0
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 888611662.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@34f7234e]
DEBUG [main] - ==>  Preparing: select id, `name`, price, category from purchase where id = ? 
DEBUG [main] - ==> Parameters: 2(Integer)
DEBUG [main] - <==      Total: 1
Purchase{id=2, name='爆米花', price=18, category=2}
=========第二次查询并使用=======
DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.z_run.mapper.PurchaseMapper]: 0.0
Purchase{id=2, name='爆米花', price=null, category=2}
  • 两次查询的结果对象是相同的,可以确定第二次查询是走的缓存,而修改的操作也是直接修改的内存中的数据,因此造成了第一次使用数据1和第二次使用的数据1不同。

image.png

解决方案

  • 关闭一级缓存flushCache="true",两次查询都走SQL去查询数据,就保证不会因为缓存而导致数据被篡改。
  • 修改时不要修改缓存中的数据,将缓存中的数据拷贝出来,对拷贝出来的数据进行修改使用。

二级缓存

使用

Mybatis的二级缓存默认是关闭的,如果需要打开,需要进行一些配置。二级缓存是基于SqlSessionFactory级别的,也就是同一个SqlSessionFactory下的所有查询都使用同一个缓存,不同的SqlSessionFactory下的查询,缓存不相同。

  • 首先要将mybatis-config.xml配置文件中的全局缓存开关打开,这个开关默认是打开的。
<!--全局开启或关闭缓存,默认为true-->
<setting name="cacheEnabled" value="true"/>
  • 在XML文件中配置缓存对象
<!-- 开启二级缓存-->
<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
       size="1024"
       eviction="LRU"
       flushInterval="120000"
       readOnly="true"/>

<select id="findByID" parameterType="java.lang.Integer" resultMap="BaseResultMap" flushCache="false">
    select
    <include refid="Base_Column_List" />
    from purchase
    where id = #{id,jdbcType=INTEGER}
</select>
  • 测试代码及输出结果。可以看出,第二次查询是因为一级缓存的缘故,因此没有打印SQL,而第三次查询则是因为二级缓存的缘故没有打印SQL。
@Test
public void twoLevelCache() {
    // 获取SqlSessionFactory
    SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
    // 第一个SqlSession下的查询
    SqlSession sqlSession = getSqlSession(sqlSessionFactory);
    PurchaseMapper mapper = sqlSession.getMapper(PurchaseMapper.class);
    System.out.println("============第一个SqlSession下的第一次查询============");
    System.out.println(mapper.findByID(2));
    System.out.println("============第一个SqlSession下的第二次查询============");
    System.out.println(mapper.findByID(2));
    // 刷新缓存到SqlSessionFactory中
    sqlSession.commit();
    sqlSession.close();
    // 第二个SqlSession下的查询
    System.out.println("============第二个SqlSessionFactory下的第一次查询============");
    sqlSession = getSqlSession(sqlSessionFactory);
    mapper = sqlSession.getMapper(PurchaseMapper.class);
    System.out.println(mapper.findByID(2));
}
============第一个SqlSession下的第一次查询============
DEBUG [main] - ==>  Preparing: select id, `name`, price, category from purchase where id = ? 
DEBUG [main] - ==> Parameters: 2(Integer)
DEBUG [main] - <==      Total: 1
Purchase{id=2, name='爆米花', price=8, category=2}
============第一个SqlSession下的第二次查询============
DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.z_run.mapper.PurchaseMapper]: 0.0
Purchase{id=2, name='爆米花', price=8, category=2}
============第二个SqlSessionFactory下的第一次查询============
DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.z_run.mapper.PurchaseMapper]: 0.3333333333333333
Purchase{id=2, name='爆米花', price=8, category=2}

问题

  • 二级缓存由于是SqlSessionFactory级别的,粒度较粗,在分布式的环境下容易出现缓存更新不及时的问题。
  • 由于请求2与请求1不在同一个SqlSessionFactory中,因此请求2导致的数据更新不会使Client A中的缓存更新。
sequenceDiagram
Client A ->> DB : 请求1:读取数据1并缓存
activate Client A
Note left of Client A : SqlSessionFactory1
Note right of Client B : SqlSessionFactory2
Client B ->> DB : 请求2:更新数据1
Client A ->> Client A : 请求3:从缓存中再次读取数据1
deactivate Client A

懒加载

以下演示一个嵌套查询。

  • Mapper接口中的方法
/**
 * 根据商品分类ID查询商品
 */
Purchase findPurchaseByCategoryId(Integer category);

/**
 * 根据ID查询商品分类
 */
CategoryVO findCategoryById(Integer id);
  • XML中的代码
<resultMap id="CategoryNestedResultMap" type="org.apache.ibatis.z_run.pojo.CategoryVO">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="name" jdbcType="VARCHAR" property="name" />
    <collection column="id" property="purchases" select="findPurchaseByCategoryId"/>
</resultMap>

<select id="findCategoryById" parameterType="java.lang.Integer" resultMap="CategoryNestedResultMap">
    select
    id,name
    from category
    where id = #{id,jdbcType=INTEGER}
</select>

<sql id="Base_Column_List">
    id, `name`, price, category
</sql>

<select id="findPurchaseByCategoryId" parameterType="java.lang.Integer" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from purchase
    where id = #{id,jdbcType=INTEGER}
</select>
  • 测试代码及查询结果
@Test
public void lazyLoadTest() {
    PurchaseMapper mapper = sqlSession.getMapper(PurchaseMapper.class);
    CategoryVO categoryVO = mapper.findCategoryById(1);
    System.out.println(categoryVO);
}
DEBUG [main] - ==>  Preparing: select id,name from category where id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.z_run.mapper.PurchaseMapper]: 0.0
DEBUG [main] - ====>  Preparing: select id, `name`, price, category from purchase where category = ? 
DEBUG [main] - ====> Parameters: 1(Integer)
DEBUG [main] - <====      Total: 5
DEBUG [main] - <==      Total: 1
CategoryVO{id=1, name='饮料', purchases=[Purchase{id=1, name='可乐', price=3, category=1}, Purchase{id=8, name='火腿', price=3, category=1}, Purchase{id=9, name='火腿', price=3, category=1}, Purchase{id=10, name='火腿', price=3, category=1}, Purchase{id=11, name='火腿', price=3, category=1}]}

  • 如何实现懒加载?在collection标签上使用fetchType="lazy"即可。
<resultMap id="CategoryNestedResultMap" type="org.apache.ibatis.z_run.pojo.CategoryVO">
    <id column="id" jdbcType="INTEGER" property="id" />
    <result column="name" jdbcType="VARCHAR" property="name" />
    <collection column="id" property="purchases" select="findPurchaseByCategoryId" fetchType="lazy"/>
</resultMap>
  • 执行以下测试代码,发现在使用purchases属性之前,并没有发送SQL去查询purchases属性的值,这便是懒加载。
@Test
public void lazyLoadTest() {
    PurchaseMapper mapper = sqlSession.getMapper(PurchaseMapper.class);
    CategoryVO categoryVO = mapper.findCategoryById(1);
    System.out.println("==========使用purchases属性==========");
    categoryVO.getPurchases();
    System.out.println(categoryVO);
}
DEBUG [main] - ==>  Preparing: select id,name from category where id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
==========使用purchases属性==========
DEBUG [main] - Cache Hit Ratio [org.apache.ibatis.z_run.mapper.PurchaseMapper]: 0.0
DEBUG [main] - ==>  Preparing: select id, `name`, price, category from purchase where category = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 5
CategoryVO{id=1, name='饮料', purchases=[Purchase{id=1, name='可乐', price=3, category=1}, Purchase{id=8, name='火腿', price=3, category=1}, Purchase{id=9, name='火腿', price=3, category=1}, Purchase{id=10, name='火腿', price=3, category=1}, Purchase{id=11, name='火腿', price=3, category=1}]}

以上就是对于Mybatis中缓存、懒加载的介绍与实现。在生产中,一级缓存常用,二级缓存基本不用(容易出问题),懒加载也较少使用。