深入理解MyBatis缓存机制

       使用缓存可以是应用更快的获取数据,避免频繁的数据库交互,尤其是在查询越多、缓存命中率越高的情况下,使用缓存的作用就越明显。MyBatis作为持久层框架,提供了强大的查询缓存特性,可非常方便的配置和使用。MyBatis系统中默认定义了两级缓存:一级缓存和二级缓存

1、默认情况下,一级缓存(SqlSession级别的缓存,也称为本地缓存)是开启的,并且不能控制。
2、二级缓存需要手动开启和配置,他是和命名空间绑定的,即二级缓存需要在全局配置文件中开启,并且在sql映射文件中指定二级缓存使用的缓存中间件(MyBatis有自己的实现,但是毫不客气的说和专业的缓存中间件一比就是个弟弟!!!)
3、为了提高扩展性。MyBatis定义了缓存接口Cache。我们可以通过实现Cache接口来自定义二级缓存(或者使用第三方缓存)

1、一级缓存

先通过一个简单的演示看看MyBatis一级缓存是如何起作用的。

package test;

import com.xust.iot.beans.User;
import com.xust.iot.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;

public class AppTest {


private static SqlSessionFactory sqlSessionFactory;
private static Logger log = Logger.getLogger(AppTest.class);

@Before
public void init() {
String resource = "mybatis-config.xml";
try {
    InputStream inputStream = Resources.getResourceAsStream(resource);
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
    e.printStackTrace();
}

}

/**
* 测试MyBatis一级缓存的特性
*/
@Test
public void test01() {
SqlSession session = sqlSessionFactory.openSession(true);
try {
    StudentMapper sm = session.getMapper(StudentMapper.class);
    List<User> user1 = sm.getUserByName("小明");
    if (null != user1 && user1.size() > 0) {
	for (User u : user1) {
	    System.out.println(u.toString());
	}
    }
    System.out.println("第二次查询\"小明\"............");
    List<User> user2 = sm.getUserByName("小明");
    if (null != user1 && user1.size() > 0) {
	for (User u : user1) {
	    System.out.println(u.toString());
	}
    }
    System.out.println("user1==user?==>" + (user1 == user2));
} catch (Exception e) {
    log.error(e + "---" + new Date());
} finally {
    session.close();
}
}
}

运行结果截图如下:

       可以看到,两次查询值MyBatis只给数据库发送了一次SQL语句,但是两次查询的结果都是一样的,而且再往下发现两个List<User>对象竟然是同一个对象,之所以这样就是MyBatis的一级缓存在起作用。
       在同一个SqlSession中查询是MyBatis会把执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入PerpetualCache 的 HashMap本地缓存对象中。如果同一个SqlSession中执行的方法和参数完全一致,闹通过算法就会生成相同的键值,当Map缓存对象中已经存在该键值时,就会返回缓存中的对象,而不会再给数据库发sql语句了。
但是要注意,下面几种情况发生会使一级缓存会失效

  • 1. 不同的SqlSession用不同的一级缓存,他们之间的数据不能共享
  • 2. 就算是同一个SqlSession对象,但是如果执行的方法和参数不同也不行
  • 3. 默认情况下,在SqlSession期间执行任何一次增删改操作,一级缓存会被清空
  • 4. 手动清空一级缓存,一级缓存也会失效
下面分别举例说明: 由于MyBatis的一级缓存存在于SqlSession生命周期中,一次不同的SqlSession当然会有不同的一级缓存。
package test;

import com.xust.iot.beans.User;
import com.xust.iot.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;

public class AppTest {


private static SqlSessionFactory sqlSessionFactory;
private static Logger log = Logger.getLogger(AppTest.class);

@Before
public void init() {
String resource = "mybatis-config.xml";
try {
    InputStream inputStream = Resources.getResourceAsStream(resource);
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
    e.printStackTrace();
}

}


/**
* MyBatis的一级缓存存在于SqlSession生命周期中,在同一个SqlSession中查询是MyBatis会把
* 执行的方法和参数通过算法生成缓存的键值,将键值和查询结果存入一个Map中。
*/
@Test
public void test02() {
//第一个会话
SqlSession session = sqlSessionFactory.openSession(true);
StudentMapper sm = session.getMapper(StudentMapper.class);
List<User> user1 = sm.getUserByName("小明");
if (null != user1 && user1.size() > 0) {
    for (User u : user1) {
	System.out.println(u.toString());
    }
}

//第二个会话
System.out.println("开启新的会话......");
SqlSession session2 = sqlSessionFactory.openSession(true);
StudentMapper sm2 = session2.getMapper(StudentMapper.class);
List<User> user2 = sm2.getUserByName("小明");
if (null != user2 && user2.size() > 0) {
    for (User u : user2) {
	System.out.println(u.toString());
    }
}

session.close();
session2.close();
}


}

测试结果如下:

       MyBatis的<insert>、<delete>、<update>和<select>标签都有一个属性:flushCache,在默认情况下,对于增删改操作这个标签默认值是true,也就是每次操作后要清空缓存,而对于<select>操作这个属性默认值是false,也即不刷新缓冲。也就是下面这段代码要说明的:

package test;

import com.xust.iot.beans.User;
import com.xust.iot.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;

public class AppTest {


private static SqlSessionFactory sqlSessionFactory;
private static Logger log = Logger.getLogger(AppTest.class);

@Before
public void init() {
String resource = "mybatis-config.xml";
try {
    InputStream inputStream = Resources.getResourceAsStream(resource);
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
    e.printStackTrace();
}

}

@Test
public void test03() {

SqlSession session = sqlSessionFactory.openSession(true);
StudentMapper sm = session.getMapper(StudentMapper.class);
List<User> user1 = sm.getUserByName("小明");
if (null != user1 && user1.size() > 0) {
    for (User u : user1) {
	System.out.println(u.toString());
    }
}
//执行任意一次增删改操作,当前SqlSession的一级缓存立即被清空
System.out.println("删除一个用户......");
sm.deleteUserById(14);
System.out.println("删除完成.......");

//由于缓存被清了,因此还得给数据库发sql语句查询
StudentMapper sm2 = session.getMapper(StudentMapper.class);
List<User> user2 = sm2.getUserByName("小明");
if (null != user2 && user2.size() > 0) {
    for (User u : user2) {
	System.out.println(u.toString());
    }
}

session.close();
}

}

测试结果如下:

下面的这种情况就更直观了,但本质上和上面那种情况是一样的——都是一级缓存被清空了。

package test;

import com.xust.iot.beans.User;
import com.xust.iot.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;

public class AppTest {


private static SqlSessionFactory sqlSessionFactory;
private static Logger log = Logger.getLogger(AppTest.class);

@Before
public void init() {
String resource = "mybatis-config.xml";
try {
    InputStream inputStream = Resources.getResourceAsStream(resource);
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
} catch (IOException e) {
    e.printStackTrace();
}

}

@Test
public void test03() {

SqlSession session = sqlSessionFactory.openSession(true);
StudentMapper sm = session.getMapper(StudentMapper.class);
List<User> user1 = sm.getUserByName("小明");
if (null != user1 && user1.size() > 0) {
    for (User u : user1) {
	System.out.println(u.toString());
    }
}

//清除缓存
System.out.println("手动清除缓存......");
session.clearCache();

StudentMapper sm2 = session.getMapper(StudentMapper.class);
List<User> user2 = sm2.getUserByName("小明");
if (null != user2 && user2.size() > 0) {
    for (User u : user2) {
	System.out.println(u.toString());
    }
}

session.close();
}

}

测试结果如下:

2、二级缓存

  • 二级缓存默认也是采用 PerpetualCache,HashMap存储;
  • 二级缓存的存储作用域为 Mapper(确切说是namespace),即一个Mapper执行了insert、update或delete操作,不影响另外一个Mapper(不同namespace);
  • 二级缓存可自定义存储实现,如 Ehcache、redis;
  • 二级缓存开启后,需要对应的java Bean实现,并且这个java Bean要实现Serializable接口进行序列化
2.1 配置二级缓存

       二级缓存有两种配置方法,一种是基于Mapper.xml文件来配置;另一种就是基于Mapper.java接口来配置。下面分别来看看如何配置使用MyBatis的二级缓存:
       首先,无论是通过Mapper.xml文件来配置,还是通过Mapper.java接口来配置,都需要在mybatis-config.xml文件中通过settings设置显式地开启二级缓存:

<!--一些有关于mybatis运行行为的设置-->
<settings>
  <!--开启二级缓存-->
  <setting name="cacheEnabled" value="true"/>
  <!--其他的设置-->
</settings>
2.2 在Mapper.xml中配置二级缓存

       在xml文件中配置的方法很简单,在保证二级缓存的全局配置开启的情况下,在UserMapper.xml中只需要添加<cache></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.xust.iot.mapper.StudentMapper">

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

<select id="getUserByName" parameterType="string" resultType="user">
select * from user
<where>
  name=#{username}
</where>
</select>

<delete id="deleteUserById" parameterType="integer" >
delete from user
<where>
    id=#{userId}
</where>
</delete>

</mapper>

<cacha>标签的属性:
* eviction:缓存回收策略:flushInterval:缓存多长时间清空一次,默认不清空,单位毫秒 ms
     LRU——最少使用的,移除最长时间不适用的对象;
     FIFO——先进先出
     WEAK——弱引用,更积极的移除基于垃圾回收器状态和弱引用规则的对象
     SOFT——软引用,更积极的移除基于垃圾回收器状态和弱引用规则的对象
* flushInterval:缓存多久清空一次,默认不清空,时间毫秒 ms
* readOnly:缓存是否只读
* size:缓存觉存放元素个数,默认1024
* type:自定义缓存的全类名

2.3 在Mapper接口中配置二级缓存

       在接口中配置主要是借助@CacheNamespace这个注解,但是要注意:配置文件和接口注释是不能够配合使用的。只能通过全注解的方式或者全部通过xml配置文件的方式使用。也就是说你用了配置文件就不要用这种方式,用了接口配置的方式就别用xml配置文件的方式。如下:

package com.xust.iot.mapper;

import com.xust.iot.beans.User;
import org.apache.ibatis.annotations.*;
import java.util.List;

@CacheNamespace
public interface StudentMapper {

    /**
     * 查询所有姓名为name的用户
     * @param name
     * @return
     */
    @Select("select * from user where name=#{username}")
    @Options(useCache = true)
    public List<User> getUserByName(@Param("username") String name);

    /**
     * 删除id=userId的用户
     * @param id
     */
    @Delete("delete from user where id=#{userId}")
    public void deleteUserById(@Param("userId") Integer id);

}

并在mybatis-config.xml中重新配置Mapper映射文件:

<mappers>
   <!--  <mapper resource="mapper/StudentMapper.xml"></mapper>-->
    <mapper class="com.xust.iot.mapper.StudentMapper"/>      
</mappers>

4、使用自带的MyBatis二级缓存

       配置Mybatis二级缓存的方法有两种,只要配置好,二级缓存就可以工作了。但是在使用前需要注意的是,由于MyBatis使用SerializedCache序列化缓存来实现可读写缓存类,并通过序列化和反序列化来保证通过缓存获取数据时,得到的是一个新的实例。因此,如果配置了只读缓存,MyBatis就会使用Map来存储缓存值。而这个缓存类要求所有被序列化的对象必须实现Serializable接口 。因此我们的java Bean需要实现Serializable接口。

package com.xust.iot.beans;

import java.io.Serializable;

public class User implements Serializable {

    private static final long serialVersionUID = -8963153814502574628L;

    //其他属性
}

做好所有准备后,编写一个测试类来看看二级缓存的效果:

package test;

import com.xust.iot.beans.User;
import com.xust.iot.mapper.StudentMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.io.InputStream;
import java.util.Date;
import java.util.List;

public class AppTest {


    private static SqlSessionFactory sqlSessionFactory;
    private static Logger log = Logger.getLogger(AppTest.class);

    @Before
    public void init() {
        String resource = "mybatis-config.xml";
        try {
            InputStream inputStream = Resources.getResourceAsStream(resource);
            sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    @Test
    public void testL2Cache() {
        //第一个会话
        SqlSession session = sqlSessionFactory.openSession(true);
        StudentMapper sm = session.getMapper(StudentMapper.class);
        List<User> user1 = sm.getUserByName("小明");
        if (null != user1 && user1.size() > 0) {
            for (User u : user1) {
                System.out.println(u.toString());
            }
        }
        session.close();

        //第二个会话
        System.out.println("开启新的会话......");
        SqlSession session2 = sqlSessionFactory.openSession(true);
        StudentMapper sm2 = session2.getMapper(StudentMapper.class);
        List<User> user2 = sm2.getUserByName("小明");
        if (null != user2 && user2.size() > 0) {
            for (User u : user2) {
                System.out.println(u.toString());
            }
        }
        session2.close();
    }

}

测试结果:

5、MyBatis缓存的执行逻辑

1.当一个SqlSession第一次执行一次select后,查到数据后会首先把查询到的结果保存到一级缓存中
2.当该SqlSession被关闭或者提交后,保存在一级缓存中的数据会转移到二级缓存中(前提是正确开启并配置了二级缓存)
3.当另一个SqlSession第一次执行同样select时,首先会在二级缓存中找,如果没找到,就去自己的一级缓存中找,找到了就返回,如果没找到就去数据库查,MyBatis就是通过这样的机制从而减少了数据库压力提高了性能。

MyBatis的执行流程总结起来就是:二级缓存-->一级缓存-->数据库

注意事项
1. 如果SqlSession执行了DML操作(insert、update、delete),并commit了,那么mybatis就会清空当前mapper缓存中的所有缓存数据,这样可以保证缓存中的存的数据永远和数据库中一致,避免出现脏读
2. mybatis的缓存是基于[namespace:sql语句:参数]来进行缓存的,意思就是:SqlSession的HashMap存储缓存数据时,是使用[namespace:sql:参数]作为key,查询返回的语句作为value保存的。

留言区

还能输入500个字符