Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存

Mybatis一级缓存&二级缓存

    • 概述
    • 一级缓存
      • 特点
      • 演示前准备
      • 效果演示
        • 在同一个SqlSession中
        • 在不同的SqlSession中
      • 源代码
      • 怎么禁止使用一级缓存
      • 一级缓存在什么情况下会被清除
    • 二级缓存
      • 特点
      • 演示前准备
      • 效果演示
        • 在不同的SqlSession中
      • 源代码
      • 怎么关闭二级缓存
    • 一级缓存(Spring整合Mybatis)
      • 演示前准备
      • 效果演示
        • 不开启事务,调用多次接口
        • 开启事务,调用多次接口
        • 不开启事务,接口中多次调用查询方法
        • 开启事务,接口中多次调用查询方法
        • 总结
      • 源代码

概述

缓存越小,查询速度越快,缓存数据越少
缓存越大,查询速度越慢,缓存数据越多

在多级缓存中,一般常见的是先查询一级缓存,再查询二级缓存,但在Mybatis中是先查询二级缓存,再查询一级缓存。

在Mybatis中,BaseExecutor属于一级缓存执行器,CachingExecutor属于二级缓存执行器,二者采用了装饰器设计模式。

一级缓存:默认情况下一级缓存是开启的,而且是不能关闭的,一级缓存是指SqlSession级别的缓存,当在同一个SqlSession中使用相同的SQL语句进行查询时,第二次以及之后的查询都不会从数据库查询,而是直接从缓存中获取,一级缓存最多缓存1024条SQL。
二级缓存:二级缓存是指可以跨SqlSession的缓存。是mapper级别的缓存,对于mapper级别的缓存不同的SqlSession是可以共享的,需要额外整合第三方缓存,例如Redis、MongoDB、oscache、ehcache等。

注:本文代码演示基于《Mybatis环境搭建与使用》中的“基于XML方式-mapper代理开发”的代码进行调整。

一级缓存

特点

一级缓存也叫本地缓存,在Mybatis中,一级缓存是在会话层面(SqlSession)实现的,这就说明一级缓存的作用范围只能在同一个SqlSession中,在多个不同的SqlSession中是无效的。

在Mybatis中,一级缓存是默认开启的,不需要任何额外的配置。

演示前准备

为了能够看到演示的效果,需要在mybatis-config.xml文件中加上以下配置

<settings>
    
    <setting name="logImpl" value="STDOUT_LOGGING"/>
settings>

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第1张图片

效果演示

在同一个SqlSession中

MybatisTest03.java

package com.mybatis.test;

import com.mybatis.entity.UserEntity;
import com.mybatis.mapper.UserMapper;
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 java.io.IOException;
import java.io.InputStream;
import java.util.List;

/**
 * @author honey
 * @date 2023-08-01 16:23:53
 */
public class MybatisTest03 {

    public static void main(String[] args) throws IOException {
        // 1.读取加载mybatis-config.xml(数据源、mybatis等配置)
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 2.获取sqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        // 3.根据mapper(namespace="UserMapper全限定名" + id="listUser")执行sql语句,并将查询到的数据映射成对象(orm)
        UserMapper mapper1 = sqlSession.getMapper(UserMapper.class);
        System.out.println("【一级缓存-在同一个SqlSession中】第一次查询");
        List<UserEntity> list1 = mapper1.listUser();
        System.out.println("list1:" + list1);

        UserMapper mapper2 = sqlSession.getMapper(UserMapper.class);
        System.out.println("【一级缓存-在同一个SqlSession中】第二次查询");
        List<UserEntity> list2 = mapper2.listUser();
        System.out.println("list2:" + list2);

        sqlSession.close();
    }
}

运行上面的代码可以看到,在同一个SqlSession中,第二次查询是没有去查询数据库的,而是直接读取的缓存数据。

源码Debug分析

BaseExecutor.java


在不同的SqlSession中

MybatisTest04.java

package com.mybatis.test;

import com.mybatis.entity.UserEntity;
import com.mybatis.mapper.UserMapper;
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 java.io.IOException;
import java.io.InputStream;
import java.util.List;

/**
 * @author honey
 * @date 2023-08-01 16:23:53
 */
public class MybatisTest04 {

    public static void main(String[] args) throws IOException {
        // 1.读取加载mybatis-config.xml(数据源、mybatis等配置)
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 2.获取sqlSession
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        // 3.根据mapper(namespace="UserMapper全限定名" + id="listUser")执行sql语句,并将查询到的数据映射成对象(orm)
        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        System.out.println("【一级缓存-在不同的SqlSession中】第一次查询");
        List<UserEntity> list1 = mapper1.listUser();
        System.out.println("list1:" + list1);

        UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
        System.out.println("【一级缓存-在不同的SqlSession中】第二次查询");
        List<UserEntity> list2 = mapper2.listUser();
        System.out.println("list2:" + list2);

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

运行上面的代码可以看到,在不同的SqlSession中,两次查询都是查询的数据库,也就是说一级缓存并没有生效。

源代码


Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第2张图片

怎么禁止使用一级缓存

  1. 在SQL语句上加上随机生成的参数;(不推荐)
  2. 开启二级缓存;
  3. 使用SqlSession强制清除缓存;
  4. 每次查询都使用新的SqlSession;
  5. 通过配置清除缓存;

一级缓存在什么情况下会被清除

  1. 提交事务/回滚事务/强制清除缓存
sqlSession.commit();
sqlSession.rollback();
sqlSession.clearCache()

以提交事务为例,回滚事务/强制清除缓存同理

MybatisTest03.java

DefaultSqlSession.java

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第3张图片

BaseExecutor.java

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第4张图片
Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第5张图片

  1. 在执行insert、update、delete语句时

BaseExecutor.java

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第6张图片

  1. 使用配置清除一级缓存

<setting name="localCacheScope" value="STATEMENT"/>

mybatis-config.xml

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第7张图片

BaseExecutor.java

二级缓存

特点

二级缓存是mapper级别的缓存,通过整合第三方缓存实现,二级缓存的作用范围可以在不同的SqlSession中。

在Mybatis中,二级缓存默认是开启的,但还需要做一些额外的配置才能生效。

演示前准备

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第8张图片

  1. 启动Redis

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第9张图片

  1. 添加pom依赖

pom.xml

<dependency>
    <groupId>redis.clientsgroupId>
    <artifactId>jedisartifactId>
    <version>3.0.1version>
dependency>
  1. 实现Cache类

RedisCache.java

package com.mybatis.cache;

import com.mybatis.utils.SerializeUtil;
import org.apache.ibatis.cache.Cache;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * @author honey
 * @date 2023-08-01 23:44:10
 */
public class RedisCache implements Cache {

    private final Jedis redisClient = createRedis();

    private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    private final String id;

    public RedisCache(final String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        this.id = id;
    }


    @Override
    public String getId() {
        return id;
    }

    @Override
    public void putObject(Object key, Object value) {
        System.out.printf("【存入缓存数据】key:%s,value:%s%n", key, value);
        redisClient.set(SerializeUtil.serialize(key), SerializeUtil.serialize(value));
    }

    @Override
    public Object getObject(Object key) {
        byte[] bytes = redisClient.get(SerializeUtil.serialize(key));
        if (bytes == null) {
            return null;
        }
        Object value = SerializeUtil.deserialize(bytes);
        System.out.printf("【读取缓存数据】key:%s,value:%s%n", key, value);
        return value;
    }

    @Override
    public Object removeObject(Object key) {
        return redisClient.expire(String.valueOf(key), 0);
    }

    @Override
    public void clear() {
        redisClient.flushDB();
    }

    @Override
    public int getSize() {
        return Integer.parseInt(redisClient.dbSize().toString());
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return readWriteLock;
    }

    protected static Jedis createRedis() {
        JedisPool pool = new JedisPool("127.0.0.1", 6379);
        return pool.getResource();
    }
}

SerializeUtil.java

package com.mybatis.utils;

import java.io.*;

/**
 * @author honey
 * @date 2023-08-02 00:50:37
 */
public class SerializeUtil {

    public static byte[] serialize(Object object) {
        ObjectOutputStream oos = null;
        ByteArrayOutputStream baos = null;
        try {
            // 序列化
            baos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(baos);
            oos.writeObject(object);
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            close(oos);
            close(baos);
        }
        return null;
    }

    public static Object deserialize(byte[] bytes) {
        ByteArrayInputStream bais = null;
        ObjectInputStream ois = null;
        try {
            // 反序列化
            bais = new ByteArrayInputStream(bytes);
            ois = new ObjectInputStream(bais);
            return ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            close(bais);
            close(ois);
        }
        return null;
    }

    /**
     * 关闭io流对象
     *
     * @param closeable closeable
     */
    public static void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

注意:UserEntity需要实现序列化接口

UserEntity.java

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第10张图片

  1. 添加配置(userMapper.xml)

userMapper.xml

<cache eviction="LRU" type="com.mybatis.cache.RedisCache"/>

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第11张图片

效果演示

在不同的SqlSession中

MybatisTest05.java

package com.mybatis.test;

import com.mybatis.entity.UserEntity;
import com.mybatis.mapper.UserMapper;
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 java.io.IOException;
import java.io.InputStream;
import java.util.List;

/**
 * @author honey
 * @date 2023-08-01 16:23:53
 */
public class MybatisTest05 {

    public static void main(String[] args) throws IOException {
        // 1.读取加载mybatis-config.xml(数据源、mybatis等配置)
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        // 2.获取sqlSession
        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        // 3.根据mapper(namespace="UserMapper全限定名" + id="listUser")执行sql语句,并将查询到的数据映射成对象(orm)
        UserMapper mapper1 = sqlSession1.getMapper(UserMapper.class);
        System.out.println("【二级缓存-在不同的SqlSession中】第一次查询");
        List<UserEntity> list1 = mapper1.listUser();
        System.out.println("list1:" + list1);

        sqlSession1.close();

        UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
        System.out.println("【二级缓存-在不同的SqlSession中】第二次查询");
        List<UserEntity> list2 = mapper2.listUser();
        System.out.println("list2:" + list2);

        sqlSession2.close();
    }
}

运行上面的代码可以看到,在不同的SqlSession中,第一次查询读取的是数据库中的数据,而第二次查询读取的是缓存中的数据。

注意:查询到的数据并不是在第一时间就存入缓存,而是在提交事务(sqlSession1.close())的时候才存入缓存。

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第12张图片

源代码

CachingExecutor.java

在这里插入图片描述

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第13张图片

TransactionalCacheManager.java

在这里插入图片描述

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第14张图片


根据Cache(id=“mapper全限定名”)获取对应的TransactionalCache对象,并将数据临时存放在该对象中。

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第15张图片


TransactionalCache.java

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第16张图片

在执行sqlSession1.close()这行代码时,会将临时存放的数据存入缓存。

DefaultSqlSession.java

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第17张图片

CachingExecutor.java

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第18张图片

  1. 如果是提交事务,则会先将临时存放的数据存入缓存,再将临时存放的数据清空

TransactionalCacheManager.java

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第19张图片

TransactionalCache.java

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第20张图片

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第21张图片

  1. 如果是回滚事务,则只会将临时存放的数据清空

TransactionalCacheManager.java

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第22张图片

TransactionalCache.java

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第23张图片

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第24张图片

怎么关闭二级缓存

修改配置文件(mybatis-config.xml)

<setting name="cacheEnabled" value="false"/>

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第25张图片

一级缓存(Spring整合Mybatis)

在未开启事务的情况下,每次查询Spring都会关闭旧的SqlSession而创建新的SqlSession,因此此时的一级缓存是没有生效的;
在开启事务的情况下,Spring模板使用threadLocal获取当前资源绑定的同一个SqlSession,因此此时一级缓存是有效的;

演示前准备

项目结构

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第26张图片

pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>comgroupId>
    <artifactId>springboot-mybatisartifactId>
    <version>1.0-SNAPSHOTversion>

    <properties>
        <maven.compiler.source>8maven.compiler.source>
        <maven.compiler.target>8maven.compiler.target>
    properties>

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.1.9.RELEASEversion>
        <relativePath/> 
    parent>

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>8.0.11version>
        dependency>
        
        <dependency>
            <groupId>org.mybatis.spring.bootgroupId>
            <artifactId>mybatis-spring-boot-starterartifactId>
            <version>1.1.1version>
        dependency>
        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
    dependencies>

project>

application.yml

server:
  port: 8080

spring:
  datasource:
    username: root
    password: admin
    url: jdbc:mysql://localhost:3306/db_mybatis?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      connection-timeout: 10000

UserMapper.xml


DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mybatis.mapper.UserMapper">
    <select id="listUser" resultType="com.mybatis.entity.UserEntity">
        select * from tb_user
    select>
mapper>

AppMybatis.java

package com.mybatis;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @author honey
 * @date 2023-08-02 02:58:16
 */
@SpringBootApplication
public class AppMybatis {

    public static void main(String[] args) {
        SpringApplication.run(AppMybatis.class);
    }
}

UserEntity.java

package com.mybatis.entity;

import lombok.Data;

/**
 * @author honey
 * @date 2023-08-02 03:03:19
 */
@Data
public class UserEntity {

    private Long id;

    private String name;
}

UserMapper.java

package com.mybatis.mapper;

import com.mybatis.entity.UserEntity;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

/**
 * @author honey
 * @date 2023-07-26 21:04:23
 */
@Mapper
public interface UserMapper {

    /**
     * 查询用户列表
     *
     * @return List
     */
    List<UserEntity> listUser();
}

UserController.java

package com.mybatis.controller;

import com.mybatis.entity.UserEntity;
import com.mybatis.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author honey
 * @date 2023-08-02 03:09:13
 */
@RestController
@RequiredArgsConstructor
public class UserController {

    private final UserMapper userMapper;

    @RequestMapping("listUser")
    public void listUser(){
        List<UserEntity> list = userMapper.listUser();
        System.out.println(list);
    }
}

效果演示

不开启事务,调用多次接口

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第27张图片

第一次调用

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第28张图片

第二次调用

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第29张图片

两次调用获取到的是不同的SqlSession,一级缓存不生效

开启事务,调用多次接口

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第30张图片

第一次调用

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第31张图片

第二次调用

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第32张图片

两次调用获取到的也是不同的SqlSession,一级缓存不生效

不开启事务,接口中多次调用查询方法

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第33张图片

第一次调用

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第34张图片

第二次调用

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第35张图片

两次调用获取到的依然是不同的SqlSession,一级缓存不生效

开启事务,接口中多次调用查询方法

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第36张图片

第一次调用

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第37张图片

第二次调用

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第38张图片

两次调用获取到的是相同的SqlSession,一级缓存生效

总结

只有在同一个事务内执行查询,一级缓存才会生效。

源代码

MapperMethod.java
Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第39张图片

在Spring整合Mybatis的代码中,新增了SqlSessionTemplate类对DefaultSqlSession类的功能进行增强。

SqlSessionTemplate.java

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第40张图片

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第41张图片

SqlSessionUtils.java

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存&二级缓存_第42张图片

能获取到SqlSessionHolder对象的前提是开启了事务。如果当前线程开启了事务,则不会直接关闭SqlSession对象,而是在下一次调用时复用SqlSession对象。

你可能感兴趣的:(mybatis,mr,mybatis,缓存)