Mr. Cappuccino’s 55th cup of coffee - Mybatis first-level cache & second-level cache

Overview

The smaller the cache, the faster the query speed, and the less cached data
. The larger the cache, the slower the query speed, and the more cached data.

In multi-level cache, it is common to query the first-level cache first and then the second-level cache. However, in Mybatis, the second-level cache is queried first and then the first-level cache.

In Mybatis, BaseExecutor belongs to the first-level cache executor, and CachingExecutor belongs to the second-level cache executor. Both adopt the decorator design pattern.

First-level cache: By default, the first-level cache is turned on and cannot be turned off. The first-level cache refers to the SqlSession-level cache. When the same SQL statement is used for querying in the same SqlSession, the second and subsequent times All queries will not be queried from the database, but will be obtained directly from the cache. The first-level cache can cache up to 1024 SQL statements.
Second level cache: The second level cache refers to the cache that can span SqlSession. It is a mapper-level cache. Different SqlSession for mapper-level cache can be shared, and additional third-party caches need to be integrated, such as Redis, MongoDB, oscache, ehcache, etc.

Note: The code demonstration in this article is adjusted based on the code of "XML-based Mapper Agent Development" in "Mybatis Environment Construction and Use".

L1 cache

Features

The first-level cache is also called local cache. In Mybatis, the first-level cache is implemented at the session level (SqlSession). This means that the scope of the first-level cache can only be in the same SqlSession, and in multiple different SqlSession. Invalid.

In Mybatis, the first-level cache is enabled by default and does not require any additional configuration.

Preparation before presentation

In order to see the effect of the demonstration, you need to add the following configuration to the mybatis-config.xml file

<settings>
    <!-- 打印sql日志 -->
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

Insert image description here

Effect demonstration

In the same 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();
    }
}

Running the above code, you can see that in the same SqlSession, the second query does not query the database, but directly reads the cached data.

Insert image description here

Source code debug analysis

BaseExecutor.java

Insert image description here
Insert image description here

In different 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();
    }
}

Running the above code, you can see that in different SqlSession, the two queries are all querying the database, which means that the first-level cache does not take effect.

Insert image description here

source code

Insert image description here
Insert image description here

How to disable the use of first-level cache

  1. Add randomly generated parameters to the SQL statement; (not recommended)
  2. Turn on the second level cache;
  3. Use SqlSession to force cache clearing;
  4. Each query uses a new SqlSession;
  5. Clear cache through configuration;

Under what circumstances will the first-level cache be cleared?

  1. Commit transaction/rollback transaction/force clear cache
sqlSession.commit();
sqlSession.rollback();
sqlSession.clearCache()

Take committing a transaction as an example, rolling back the transaction/forcibly clearing the cache is the same.

MybatisTest03.java

Insert image description here

DefaultSqlSession.java

Insert image description here

BaseExecutor.java

Insert image description here
Insert image description here

  1. When executing insert, update, delete statements

BaseExecutor.java

Insert image description here

  1. Clear the first level cache using configuration
<!-- 设置一级缓存作用域 -->
<setting name="localCacheScope" value="STATEMENT"/>

mybatis-config.xml

Insert image description here

BaseExecutor.java

Insert image description here

L2 cache

Features

The second-level cache is a mapper-level cache, which is implemented by integrating third-party cache. The scope of the second-level cache can be in different SqlSession.

In Mybatis, the second-level cache is enabled by default, but some additional configuration is required to take effect.

Preparation before presentation

Insert image description here

  1. Start Redis

Insert image description here

  1. Add pom dependency

pom.xml

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.0.1</version>
</dependency>
  1. Implement Cache class

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();
            }
        }
    }
}

Note: UserEntity needs to implement the serialization interface

UserEntity.java

Insert image description here

  1. Add configuration (userMapper.xml)

userMapper.xml

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

Insert image description here

Effect demonstration

In different 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();
    }
}

Running the above code, you can see that in different SqlSession, the first query reads the data in the database, while the second query reads the data in the cache.

Insert image description here

Note: The queried data is not stored in the cache at the first time, but is stored in the cache when the transaction (sqlSession1.close()) is submitted.

Insert image description here

source code

CachingExecutor.java

Insert image description here

Insert image description here

TransactionalCacheManager.java

Insert image description here

Insert image description here


Obtain the corresponding TransactionalCache object according to Cache (id = "mapper fully qualified name"), and temporarily store the data in the object.

Insert image description here


TransactionalCache.java

Insert image description here

When executing the sqlSession1.close() line of code, the temporarily stored data will be stored in the cache.

DefaultSqlSession.java

Insert image description here

CachingExecutor.java

Insert image description here

  1. If a transaction is submitted, the temporarily stored data will be stored in the cache first, and then the temporarily stored data will be cleared.

TransactionalCacheManager.java

Insert image description here

TransactionalCache.java

Insert image description here

Insert image description here

  1. If the transaction is rolled back, only the temporarily stored data will be cleared.

TransactionalCacheManager.java

Insert image description here

TransactionalCache.java

Insert image description here

Insert image description here

How to turn off the second level cache

Modify the configuration file (mybatis-config.xml)

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

Insert image description here

First-level cache (Spring integrates Mybatis)

When the transaction is not enabled, every time Spring is queried, the old SqlSession will be closed and a new SqlSession will be created, so the first-level cache is not effective at this time; when the transaction is enabled, the Spring template uses threadLocal to obtain the
current resource binding. The same SqlSession is specified, so the first-level cache is valid at this time;

Preparation before presentation

Project structure

Insert image description here

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);
    }
}

Effect demonstration

Do not start the transaction and call the interface multiple times

Insert image description here

first call

Insert image description here

second call

Insert image description here

The two calls obtained different SqlSession, and the first-level cache does not take effect.

Start a transaction and call the interface multiple times

Insert image description here

first call

Insert image description here

second call

Insert image description here

The two calls also obtain different SqlSession, and the first-level cache does not take effect.

Without starting the transaction, the query method is called multiple times in the interface.

Insert image description here

first call

Insert image description here

second call

Insert image description here

The two calls still obtain different SqlSession, and the first-level cache does not take effect.

Start the transaction and call the query method multiple times in the interface

Insert image description here

first call

Insert image description here

second call

Insert image description here

The two calls obtain the same SqlSession, and the first-level cache takes effect.

Summarize

The first-level cache will only take effect if the query is executed within the same transaction.

source code

MapperMethod.java

Insert image description here

In Spring's code integrating Mybatis, the SqlSessionTemplate class is added to enhance the functionality of the DefaultSqlSession class.

SqlSessionTemplate.java

Insert image description here

Insert image description here

Insert image description here

SqlSessionUtils.java

Insert image description here

Insert image description here

The prerequisite for obtaining the SqlSessionHolder object is that transactions are enabled. If the current thread has enabled a transaction, the SqlSession object will not be closed directly, but will be reused on the next call.

Guess you like

Origin blog.csdn.net/sinat_41888963/article/details/132043545