Hey, Nice to meet You. 

必有过人之节.人情有所不能忍者,匹夫见辱,拔剑而起,挺身而斗,此不足为勇也,天下有大勇者,猝然临之而不惊,无故加之而不怒.此其所挟持者甚大,而其志甚远也.          ☆☆☆所谓豪杰之士,

Mybatis3详解(十二)----Mybatis缓存

1、什么是Mybatis缓存

缓存就是将数据暂时存储在内存或硬盘中,当在查询数据时,如果缓存中有相同的数据就直接从缓存读取而不从数据库读取,从而减少Java应用与数据库的交互次数,这样就提升了程序的执行效率。比如查询 id = 1 的对象,第一次查询出对象之后会自动将该对象报存到缓存中,当下一次查询时,直接从缓存中去查找对象即可,无需再次访问数据库。

什么地方适用缓存:

  • 适用缓存:经常查询并且不经常改变的数据,数据的正确与否对最终结果影响不大。
  • 不适用缓存:经常改变的数据,数据的正确性与否对最终结果影响很大,比如:商品的库存,银行的存款,股市的牌价等等。

 

Mybatis提供了两种缓存,它们分别为:一级缓存和二级缓存。

  • 一级缓存:指的是SqlSession对象级别的缓存。当我们执行查询后,查询的结果会同时存入到SqlSession为我们提供的一块区域中,该区域的结构是一个HashMap,不同的SqlSession的缓存区域是互相不受影响的。当我们再次查询同样的数据,Mybatis会先去SqlSession的缓存区域中查询是否有,有的话直接拿出来用,没有则去数据库查询。当SqlSession对象消失后(被flush或close),Mybatis的一级缓存也就消失了。(一级缓存默认是启动的,而且是一直存在的)
  • 二级缓存:指的是Mapper对象(Namspace)级别的缓存(也可以说是SqlSessionFactory对象级别的缓存,由同一个SqlSessionFactory对象创建的SqlSession共享其缓存)。多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。(二级缓存Mybatis默认是关闭的,需要自己去手动配置开启或可以自己选择用哪个厂家的缓存来作为二级缓存)

      

一级缓存和二级缓存的区别:

  • 相同点:它们都是基于PerpetualCache 的 HashMap本地缓存,
  • 不同点:一级缓存作用域为SqlSession,而二级缓存作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache,Redis,Memcache等。

2、一级缓存

一级缓存是SqlSession对象级别的缓存,Mybatis会在SqlSession内部维护一个HashMap用于存储,缓存Key为hashcode+sqlid+sql,value则为查询的结果集,当执行查询时会先从缓存区域查找,如果存在则直接返回数据,否则从数据库查询,并将结果集写入缓存区。

一级缓存示例(以User举例):

①、创建User实体类:

/**
 * 用户实体类
 */
public class User {
    private int userId;
    private String userName;
    private int userAge;
    private Date userBirthday;
    private int userSex;
    private String userAddress;

    //getter、setter、toString方法省略......
}

②、编写UserMapper接口

/**
 * UserMapper接口
 */
public interface UserMapper {
    //查询所有用户
    List<User> selectAllUser();
    //根据id查询用户
    User selectUserById(Integer id);
}

③、编写UserMapper.xml映射文件

<?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.thr.mapper.UserMapper">
    <resultMap id="userMap" type="com.thr.pojo.User">
        <id property="userId" column="id"/>
        <result property="userName" column="username"/>
        <result property="userAge" column="age"/>
        <result property="userBirthday" column="birthday"/>
        <result property="userSex" column="sex"/>
        <result property="userAddress" column="address"/>
    </resultMap>

    <!-- 查询所有用户-->
    <select id="selectAllUser" resultMap="userMap">
        select * from t_user
    </select>
    <!--根据id查询用户-->
    <select id="selectUserById" parameterType="int" resultMap="userMap">
        select * from t_user where id = #{id}
    </select>
</mapper>

④、编写数据库连接、日志和全局配置文件

数据库连接文件

#数据库连接配置
database.driver=com.mysql.cj.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
database.username=root
database.password=root

日志文件

log4j.rootLogger=DEBUG, Console
#Console
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n
log4j.logger.java.sql.ResultSet=INFO
log4j.logger.org.apache=INFO
log4j.logger.java.sql.Connection=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG 

mybatis-config.xml全局配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <!--引入数据库配置文件-->
    <properties resource="db.properties"/>
    <!--配置别名-->
    <typeAliases>
        <package name="com.thr.pojo"/>
    </typeAliases>
    <!-- 配置环境.-->
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="${database.driver}"/>
                <property name="url" value="${database.url}"/>
                <property name="username" value="${database.username}"/>
                <property name="password" value="${database.password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <!-- 扫描包下的所有mapper接口并进行注册,规则必须是同包同名 -->
        <package name="com.thr.mapper"/>
    </mappers>
</configuration>

⑤、编写测试方法

//Mybatis的测试
public class MybatisTest {
    //定义SqlSessionFactory
    private SqlSessionFactory sqlSessionFactory = null;
    //定义SqlSession
    private SqlSession sqlSession = null;

    @Before//在测试方法执行之前执行
    public void getSqlSession(){
        //1、加载 mybatis 全局配置文件
        InputStream is = MybatisTest.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
        //2、创建SqlSessionFactory对象
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
        //3、根据 sqlSessionFactory 产生 session
        sqlSession = sqlSessionFactory.openSession();
    }
    //查询所有用户数据
    @Test
    public void testSelectAllUser(){
        //动态代理创建UserMapper对象
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        //第一次查询
        List<User> listUser1 = mapper.selectAllUser();
        for (User user1 : listUser1) {
            System.out.println(user1);
        }

        System.out.println("--------------------------");
        //第二次查询
        List<User> listUser2 = mapper.selectAllUser();
        for (User user : listUser2) {
            System.out.println(user);
        }
        sqlSession.close();
    }
    //根据Id查询一个用户数据
    @Test
    public void testSelectUserById(){
        //动态代理创建UserMapper对象
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        //第一次查询
        User user1 = mapper.selectUserById(1);
        System.out.println(user1);

        System.out.println("--------------------------");
        //第二次查询
        User user2 = mapper.selectUserById(1);
        System.out.println(user2);
        sqlSession.close();
    }
}

⑥、运行结果

查询所有数据:

image

根据id查询一个用户数据:

image

通过上面的运行结果可以发现,第二次查询并没有执行SQL语句,但是却获得了数据,说明是直接从缓存中读取的数据。

3、一级缓存的清空

注意:这里的缓存清空是针对一级缓存而言的。

以下的两个操作会导致一级缓存的清空

①、执行了insert、update、delete的sql语句,又或者只执行了commit操作,都会导致一级缓存失效。

注:添加、修改,删除操作不管有没有成功,只要你执行了增删改的SQL,缓存都会清空,即使没有通过commit方法提交,而二级缓存必须通过commit方法提交,才能清空缓存,因为二级缓存必须要在sqlSession关闭或者提交(commit)才能生效。

image

验证增删改是否成功:

image

image

验证只执行commit()方法:

image

image

②、手动清空,通过sqlSession.clearCache

image

4、二级缓存

二级缓存是Mapper对象或sqlSessionFactory对象的缓存,由同一个sqlSessionFactory对象创建的SqlSession共享其缓存。当多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。(二级缓存Mybatis默认是关闭的,需要自己去手动配置开启或可以自己选择用哪个厂家的缓存来作为二级缓存)

二级缓存举例:

①、启用二级缓存,在mybatis的全局配置文件中加入如下配置

    <!--开启二级缓存-->
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>

image

②、在UserMapper.xml配置文件中添加cache标签,让映射文件支持二级缓存

image

cache标签还可以配置其它参数,如:

<cache eviction="LRU" flushInterval="60000"  size="512"  readOnly="true" type=”xxxxx” />

cache标签属性介绍:

  • eviction:缓存的回收策略,有四个策略,默认的是LRU 。LRU — 最近最少使用,移除最长时间不被使用的对象;FIFO — 先进先出,按对象进入缓存的顺序来移除它们;SOFT — 软引用,移除基于垃圾回收器状态和软引用规则的对象;WEAK — 弱引用,更积极地移除基于垃圾收集器和弱引用规则的对象。
  • flushInterval:缓存刷新间隔,就是多久清空一次,默认不清空,单位毫秒,在执行配置了flushCache标签的SQL时清空。
  • size:缓存存放多少个元素,默认1024。
  • readOnly:是否只读,默认为false。false:读写,mybatis觉得获取的数据可能会被修改,mybatis会利用序列化和反序列化的技术克隆一份新的数据给你,这样虽然安全,但速度相对慢。true:只读,mybatis认为所有从缓存中获取数据的操作都是只读操作,不会修改数据。mybatis为了加快获取数据,直接就会将数据在缓存中的引用交给用户 ,这样不安全,但是速度快。
  • type:指定自定义缓存的全类名(实现Cache接口即可)。

③、实体对象实现序列化接口,使用Mybatis二级缓存需要将pojo对象实现java.io.Serializable接口,否则将出现序列化错误。

image

因为二级缓存有可能是存储在磁盘中,有文件的读写操作,所以映射的实体类要实现Serializable接口

④、编写UserMapper接口(参考一级缓存)。

⑤、编写UserMapper.xml配置文件(就多了个cache标签,其余参考一级缓存)。

⑥、编写测试方法

在编写测试代码时注意下面这句话非常重要:

注意:二级缓存在sqlSession关闭(sqlSession.close() )或者提交(sqlSession.commit() )时才会生效!主要是清空一级缓存,但是无法清空二级缓存。

//Mybatis的测试
public class MybatisTest1 {
    //定义SqlSessionFactory
    private SqlSessionFactory sqlSessionFactory = null;

    @Before//在测试方法执行之前执行
    public void getSqlSession(){
        //1、加载 mybatis 全局配置文件
        InputStream is = MybatisTest1.class.getClassLoader().getResourceAsStream("mybatis-config.xml");
        //2、创建SqlSessionFactory对象
        sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);

    }
    //查询所有用户数据
    @Test
    public void testSelectAllUser(){
        //定义两个不同SqlSession,但有同一个sqlSessionFactory
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();

        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        //第一次查询
        List<User> listUser1 = mapper1.selectAllUser();
        for (User user1 : listUser1) {
            System.out.println(user1);
        }
        System.out.println("----------------");
        //注意:二级缓存在sqlSession关闭或者提交才会生效!这里二选一
        sqlSession1.close();
        //sqlSession1.commit();

        UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
        //第一次查询
        List<User> listUser2 = mapper2.selectAllUser();
        for (User user2 : listUser2) {
            System.out.println(user2);
        }
        sqlSession2.close();
    }
    //根据Id查询一个用户数据
    @Test
    public void testSelectUserById(){
        //定义两个不同SqlSession,但有同一个sqlSessionFactory
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();

        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        //第一次查询
        User user1 = mapper1.selectUserById(1);
        System.out.println(user1);
        System.out.println("----------------");

        //注意:二级缓存在sqlSession关闭或者提交才会生效!这里二选一
        //sqlSession1.close();
        sqlSession1.commit();

        UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
        //第二次查询
        User user2 = mapper2.selectUserById(1);
        System.out.println(user2);

        sqlSession1.close();
        sqlSession2.close();
    }
}

⑦、运行结果

(1)、在没有关闭sqlSession和提交commit的情况下运行:

image

image

可以从运行结果发现查询两次分别执行了两次SQL,并且缓存的命中率为0.0,说明没有缓存。

(2)、查询所有数据:

image

可以看到第二次查询后的缓存命中率为0.5,意思是我们查询了两次,其中有一条SQL语句是从缓存中查询的数据,这里也就是第二条,所以缓存的命中率为0.5。如果你再复制增加一条一样的语句,那么缓存的命中率会变为0.66666666。

(3)、根据id查询一个用户数据:

image

 

使用注解开启二级缓存

只需要在对应的Mapper接口上增加如下注解,就可以开启二级缓存,非常的方便。

@CacheNamespace(blocking = true)

image

其它参数用逗号(,)隔开,比如:eviction、flushInterval、size等。

5、二级缓存的禁用与刷新(清空)

①、禁用二级缓存

useCache属性是用来禁用二级缓的,这个属性只有select标签有,它表示配置这个select是否使用二级缓存,默认为true,设置为false则表示禁止当前select使用二级缓存,即:

image

       或者通过注解来禁用二级缓存,如下:

image

②、刷新(清空)二级缓存

flushCache属性是用来刷新二级缓存的,表示是否刷新(清空)缓存。查询时默认为flushCache=false,因为查询时刷新缓存的话,就会导致一直去数据库查询,所以查询时必须要关闭;增删改默认为flushCache=true,因为mybatis执行数据的增删改sql语句后,数据库与缓存数据可能已经不一致,如果不执行刷新缓存则可能出现脏读的情况,sql语句执行后,会同时清空一级缓存和二级缓存。

这个属性一般全部默认即可,不用管,因为你也管不了,查询时不可能设置为true刷新缓存吧,增删改也不可能设置为false禁止刷新缓存吧。

image

通过注解来清空二级缓存:

image

6、使用redis做二级缓存

我们知道Mybatis它是一个优秀的持久层框架,但是它不是一个缓存框架,所以说它本身的缓存机制不是很好,我们一般都是整合第三方的缓存框架,比如常见的缓存框架ehcache、redis、memcache等等。这里暂时以简单的redis缓存框架为例,因为redis是现在用的最多的。

如果我们想要使用第三方的缓存,就必须实现Mybatis提供的cache接口,它自己有一个默认的实现类PerpetualCache,我们来看一下cache接口。

public interface Cache {    
  String getId();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  Object removeObject(Object key);
  void clear();
  int getSize();
  ReadWriteLock getReadWriteLock();
}

下面我们来实现如何使用redis做二级缓存,步骤如下:

①、redis的下载与安装参考链接(这里只以window版本为例):https://blog.csdn.net/WeiHao0240/article/details/100030637

②、添加maven包依赖

        <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.3.0</version>
        </dependency>

③、开启二级缓存

    <settings>
        <!--开启二级缓存-->
        <setting name="cacheEnabled" value="true"/>
        <!--日志-->
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>

④、编写序列化和反序列化工具类。

public class SerializableTools {
    /**
     * 反序列化
     *
     * @param bt
     * @return
     * @throws IOException
     * @throws Exception
     */
    public static Object byteArrayToObj(byte[] bt) throws Exception {
        ByteArrayInputStream bais = new ByteArrayInputStream(bt);
        ObjectInputStream ois = new ObjectInputStream(bais);
        return ois.readObject();
    }
    /**
     * 对象序列化
     *
     * @param obj
     * @return
     * @throws IOException
     */
    public static byte[] ObjToByteArray(Object obj) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(obj);
        return bos.toByteArray();
    }
}

⑤、实现Mybatis二级缓存org.apache.ibatis.cache.Cache接口

public class RedisCache implements Cache {
    // 初始化Jedis
    private Jedis jedis = new Jedis("127.0.0.1", 6379);
    /*
     *  MyBatis会把映射文件的命名空间作为
     *  唯一标识cacheId,标识这个缓存策略属于哪个namespace
     *  这里定义好,并提供一个构造器,初始化这个cacheId即可
     */
    private String cacheId;

    public RedisCache (String cacheId){
        this.cacheId = cacheId;
    }
    /**
     * 清空缓存
     */
    @Override
    public void clear() {
        // 但这方法不建议实现
    }
    @Override
    public String getId() {
        return cacheId;
    }
    /**
     * MyBatis会自动调用这个方法检测缓存
     * 中是否存在该对象。既然是自己实现的缓存
     * ,那么当然是到Redis中找了。
     */
    @Override
    public Object getObject(Object arg0) {
        // arg0 在这里是键
        try {
            byte [] bt = jedis.get(SerializableTools.ObjToByteArray(arg0));
            if (bt == null) {        // 如果没有这个对象,直接返回null
                return null;
            }
            return SerializableTools.byteArrayToObj(bt);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
    @Override
    public ReadWriteLock getReadWriteLock() {
        return new ReentrantReadWriteLock();
    }
    @Override
    public int getSize() {
        return Integer.parseInt(Long.toString(jedis.dbSize()));
    }
    /**
     * MyBatis在读取数据时,会自动调用此方法
     * 将数据设置到缓存中。这里就写入Redis
     */
    @Override
    public void putObject(Object arg0, Object arg1) {
        /*
         *  arg0是key , arg1是值
         *  MyBatis会把查询条件当做键,查询结果当做值。
         */
        try {
            jedis.set(SerializableTools.ObjToByteArray(arg0), SerializableTools.ObjToByteArray(arg1));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    /**
     * MyBatis缓存策略会自动检测内存的大小,由此
     * 决定是否删除缓存中的某些数据
     */
    @Override
    public Object removeObject(Object arg0) {
        Object object = getObject(arg0);
        try {
            jedis.del(SerializableTools.ObjToByteArray(arg0));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return object;
    }
}

⑥、修改UserMapper.xml配置文件,通过type属型指定自定义二级缓存实现

image

⑦、启动Redis

image

然后额外cmd在打开一个窗口,测试一下是否成功。

image

⑧、运行代码

image

image

我们再次执行看一下会怎样:

image

可以发现所有的命中率都是1.0,说明了100%是从缓存中取的数据,而且是从redis中读取的。

这里参考的链接:https://blog.csdn.net/qq_36311372/article/details/79090070

posted @ 2020-11-20 23:38  唐浩荣  阅读(998)  评论(1编辑  收藏  举报