Mr. Cappuccino的第55杯咖啡——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>
    <!-- 打印sql日志 -->
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

在这里插入图片描述

效果演示

在同一个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中,两次查询都是查询的数据库,也就是说一级缓存并没有生效。

在这里插入图片描述

源代码

在这里插入图片描述
在这里插入图片描述

怎么禁止使用一级缓存

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

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

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

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

MybatisTest03.java

在这里插入图片描述

DefaultSqlSession.java

在这里插入图片描述

BaseExecutor.java

在这里插入图片描述
在这里插入图片描述

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

BaseExecutor.java

在这里插入图片描述

  1. 使用配置清除一级缓存
<!-- 设置一级缓存作用域 -->
<setting name="localCacheScope" value="STATEMENT"/>

mybatis-config.xml

在这里插入图片描述

BaseExecutor.java

在这里插入图片描述

二级缓存

特点

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

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

演示前准备

在这里插入图片描述

  1. 启动Redis

在这里插入图片描述

  1. 添加pom依赖

pom.xml

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.0.1</version>
</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

在这里插入图片描述

  1. 添加配置(userMapper.xml)

userMapper.xml

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

在这里插入图片描述

效果演示

在不同的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())的时候才存入缓存。

在这里插入图片描述

源代码

CachingExecutor.java

在这里插入图片描述

在这里插入图片描述

TransactionalCacheManager.java

在这里插入图片描述

在这里插入图片描述


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

在这里插入图片描述


TransactionalCache.java

在这里插入图片描述

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

DefaultSqlSession.java

在这里插入图片描述

CachingExecutor.java

在这里插入图片描述

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

TransactionalCacheManager.java

在这里插入图片描述

TransactionalCache.java

在这里插入图片描述

在这里插入图片描述

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

TransactionalCacheManager.java

在这里插入图片描述

TransactionalCache.java

在这里插入图片描述

在这里插入图片描述

怎么关闭二级缓存

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

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

在这里插入图片描述

一级缓存(Spring整合Mybatis)

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

演示前准备

项目结构

在这里插入图片描述

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>

    <groupId>com</groupId>
    <artifactId>springboot-mybatis</artifactId>
    <version>1.0-SNAPSHOT</version>

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

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <dependencies>
        <!-- web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.1.1</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </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

<?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.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<UserEntity>
     */
    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);
    }
}

效果演示

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

在这里插入图片描述

第一次调用

在这里插入图片描述

第二次调用

在这里插入图片描述

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

开启事务,调用多次接口

在这里插入图片描述

第一次调用

在这里插入图片描述

第二次调用

在这里插入图片描述

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

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

在这里插入图片描述

第一次调用

在这里插入图片描述

第二次调用

在这里插入图片描述

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

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

在这里插入图片描述

第一次调用

在这里插入图片描述

第二次调用

在这里插入图片描述

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

总结

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

源代码

MapperMethod.java

在这里插入图片描述

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

SqlSessionTemplate.java

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

SqlSessionUtils.java

在这里插入图片描述

在这里插入图片描述

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

猜你喜欢

转载自blog.csdn.net/sinat_41888963/article/details/132043545