🐈‍⬛

Mybatisの2ndレベルキャッシュ

2025/01/19に公開

概要

Mybatisにはセッション生存中のデータを保持する1stレベルキャッシュの他に、2ndレベルキャッシュが存在します。1stレベルキャッシュはデフォルトで有効ですが、2ndレベルキャッシュは <cache/> を使用して明示的に有効にする必要があります。

2ndレベルキャッシュを有効にすることで、異なるセッションでもキャッシュが有効となります。2ndレベルキャッシュの機能は公式サイトによると以下の通りです。

  • この SQL マップファイルに含まれる select ステートメントの結果は全てキャッシュされます。
  • この SQL マップファイルに含まれる insert, update, delete ステートメントを実行するとキャッシュがフラッシュされます。
  • このキャッシュは LRU アルゴリズムに基づいて置き換えられます。
  • このキャッシュは経過時間によってフラッシュされることはありません(つまり Flush Interval は設定されていないということです)。
  • このキャッシュはクエリ結果のリストまたはオブジェクトへの参照を 1024 個格納します。
  • このキャッシュは読み書き可能なキャッシュとして扱われます。これはつまり、取得したオブジェクトは共有されず、呼び出した側で安全に変更することができる(別の呼び出し元や他スレッドでの変更の影響を受けない)ということを意味しています。

以下はこの記事の続きとなる記事です。
Mybatisの1stレベルキャッシュ2

2ndレベルキャッシュの基本的な動作を以下で確認します。

確認環境

  • Java 21
  • Spring Boot 3.4.1
  • MyBatis Spring Boot Starter 3.0.4
  • PostgreSQL 16.4

依存関係

build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter:3.4.1")
    implementation("org.springframework.boot:spring-boot-starter-jdbc:3.4.1")
    implementation("org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.4")
    implementation("org.postgresql:postgresql:42.7.4")
}

データベース

以下のテーブルとデータを用います。

テーブル定義

テーブル定義
                     Table "public.example"
 Column |         Type          | Collation | Nullable | Default 
--------+-----------------------+-----------+----------+---------
 id     | character varying(4)  |           | not null | 
 memo   | character varying(50) |           |          | 

データ

exampleテーブル
  id  |   memo   
------+----------
 0001 | example1
 0002 | example2

確認プログラム

Spring Boot アプリケーション起動時に実行されるプログラムを作成して動作を確認します。以下のような構成になります。

構成
com.example
  +- ExampleApplication.java
  +- ExampleApplicationRunner.java
  +- ExampleRepository.java
  +- ExampleMapper.java
  +- ExampleMapper.xml

ExampleApplication はアプリケーションのエントリポイントです。

ExampleApplication.java
package com.example;

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

@SpringBootApplication
public class ExampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ExampleApplication.class, args);
    }
}

ExampleApplicationRunnerApplicationRunner を実装します。このクラスがアプリケーション起動時に呼び出されます。

ExampleApplicationRunner.java
package com.example;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class ExampleApplicationRunner implements ApplicationRunner {
    private final Logger log = LoggerFactory.getLogger(ExampleApplicationRunner.class);

    private ExampleRepository exampleRepository;

    public ExampleApplicationRunner(ExampleRepository exampleRepository) {
        this.exampleRepository = exampleRepository;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("トランザクション1");
        exampleRepository.get();
        log.info("トランザクション2");
        exampleRepository.get();
    }
}

ExampleRepository はデータソースへアクセスするためのクラスです。

ExampleRepository.java
package com.example;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

@Repository
public class ExampleRepository {
    private final Logger log = LoggerFactory.getLogger(ExampleRepository.class);

    private ExampleMapper exampleMapper;

    public ExampleRepository(ExampleMapper exampleMapper) {
        this.exampleMapper = exampleMapper;
    }

    @Transactional
    public void get() {
        log.info("クエリ1");
        exampleMapper.get("0001");
        log.info("クエリ2");
        exampleMapper.get("0001");
    }
}

ExampleMapper ではデータベース操作に対応するメソッドを宣言します。

ExampleMapper.java
package com.example;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface ExampleMapper {
    List<Map<String, Object>> get(@Param("id") String id);
}

ExampleMapper.xml にクエリを記述します。

ExampleMapper.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.example.ExampleMapper">
    <cache/>
    <select id="get" resultType="hashmap">
        select * from example where id = #{id};
    </select>
</mapper>

また、localCacheScopeSTATEMENT に設定して1stレベルキャッシュが使用されないように設定します。

application.properties
mybatis.configuration.local-cache-scope=STATEMENT

確認プログラムの実行結果

結果
com.example.ExampleApplicationRunner     : トランザクション1
o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [com.example.ExampleRepository.get]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
  -- 中略 --
com.example.ExampleRepository            : クエリ1
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15c4af7a]
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.0
o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@975629453 wrapping org.postgresql.jdbc.PgConnection@2370ac7a] will be managed by Spring
com.example.ExampleMapper.get            : ==>  Preparing: select * from example where id = ?;
com.example.ExampleMapper.get            : ==> Parameters: 0001(String)
com.example.ExampleMapper.get            : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15c4af7a]
com.example.ExampleRepository            : クエリ2
org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15c4af7a] from current transaction
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.0
com.example.ExampleMapper.get            : ==>  Preparing: select * from example where id = ?;
com.example.ExampleMapper.get            : ==> Parameters: 0001(String)
com.example.ExampleMapper.get            : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15c4af7a]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15c4af7a]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15c4af7a]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15c4af7a]
o.s.jdbc.support.JdbcTransactionManager  : Initiating transaction commit
o.s.jdbc.support.JdbcTransactionManager  : Committing JDBC transaction on Connection [HikariProxyConnection@975629453 wrapping org.postgresql.jdbc.PgConnection@2370ac7a]
org.postgresql.jdbc.PgConnection         :   setAutoCommit = true
o.s.jdbc.support.JdbcTransactionManager  : Releasing JDBC Connection [HikariProxyConnection@975629453 wrapping org.postgresql.jdbc.PgConnection@2370ac7a] after transaction
com.example.ExampleApplicationRunner     : トランザクション2
o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [com.example.ExampleRepository.get]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [HikariProxyConnection@912440831 wrapping org.postgresql.jdbc.PgConnection@2370ac7a] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [HikariProxyConnection@912440831 wrapping org.postgresql.jdbc.PgConnection@2370ac7a] to manual commit
org.postgresql.jdbc.PgConnection         :   setAutoCommit = false
com.example.ExampleRepository            : クエリ1
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351]
o.apache.ibatis.io.SerialFilterChecker   : As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.3333333333333333
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351]
com.example.ExampleRepository            : クエリ2
org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351] from current transaction
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.5
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351]

まずトランザクション1を確認します。下記に抜粋しました。

トランザクション1
com.example.ExampleApplicationRunner     : トランザクション1
o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [com.example.ExampleRepository.get]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
  -- 中略 --
com.example.ExampleRepository            : クエリ1
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15c4af7a]
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.0
o.m.s.t.SpringManagedTransaction         : JDBC Connection [HikariProxyConnection@975629453 wrapping org.postgresql.jdbc.PgConnection@2370ac7a] will be managed by Spring
com.example.ExampleMapper.get            : ==>  Preparing: select * from example where id = ?;
com.example.ExampleMapper.get            : ==> Parameters: 0001(String)
com.example.ExampleMapper.get            : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15c4af7a]
com.example.ExampleRepository            : クエリ2
org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15c4af7a] from current transaction
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.0
com.example.ExampleMapper.get            : ==>  Preparing: select * from example where id = ?;
com.example.ExampleMapper.get            : ==> Parameters: 0001(String)
com.example.ExampleMapper.get            : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15c4af7a]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15c4af7a]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15c4af7a]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@15c4af7a]

まずクエリ1ですが、最初のクエリ発行になるため当然キャッシュはありません。下記のようにログが出力されており、クエリを実行していることが分かります。

クエリ1
com.example.ExampleRepository            : クエリ1
  -- 中略 --
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.0
  -- 中略 --
com.example.ExampleMapper.get            : ==>  Preparing: select * from example where id = ?;
com.example.ExampleMapper.get            : ==> Parameters: 0001(String)
com.example.ExampleMapper.get            : <==      Total: 1

続いてクエリ2です。実行前はクエリ2から2ndレベルキャッシュが使用されると考えていました。しかし、クエリ実行のログが出力されています。同じトランザクション内では2ndレベルキャッシュは使用されないようです。

クエリ2
com.example.ExampleRepository            : クエリ2
  -- 中略 --
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.0
com.example.ExampleMapper.get            : ==>  Preparing: select * from example where id = ?;
com.example.ExampleMapper.get            : ==> Parameters: 0001(String)
com.example.ExampleMapper.get            : <==      Total: 1
org.mybatis.spring.SqlSessionUtils       : Releasing transactional 

localCacheScopeSESSION に設定すれば1stレベルキャッシュが使用できるため、同じトランザクション内では2ndレベルキャッシュは使用されないようになっているのかもしれません。

次にトランザクション2を確認します。

トランザクション2
com.example.ExampleApplicationRunner     : トランザクション2
o.s.jdbc.support.JdbcTransactionManager  : Creating new transaction with name [com.example.ExampleRepository.get]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.jdbc.support.JdbcTransactionManager  : Acquired Connection [HikariProxyConnection@912440831 wrapping org.postgresql.jdbc.PgConnection@2370ac7a] for JDBC transaction
o.s.jdbc.support.JdbcTransactionManager  : Switching JDBC Connection [HikariProxyConnection@912440831 wrapping org.postgresql.jdbc.PgConnection@2370ac7a] to manual commit
org.postgresql.jdbc.PgConnection         :   setAutoCommit = false
com.example.ExampleRepository            : クエリ1
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
org.mybatis.spring.SqlSessionUtils       : Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351]
o.apache.ibatis.io.SerialFilterChecker   : As you are using functionality that deserializes object streams, it is recommended to define the JEP-290 serial filter. Please refer to https://docs.oracle.com/pls/topic/lookup?ctx=javase15&id=GUID-8296D8E8-2B93-4B9A-856E-0A65AF9B8C66
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.3333333333333333
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351]
com.example.ExampleRepository            : クエリ2
org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351] from current transaction
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.5
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351]
org.mybatis.spring.SqlSessionUtils       : Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351]

クエリ1を確認すると Cache Hit Ratio が 0.3333333333333333 となっておりキャッシュにヒットしたことが分かります。

クエリ1
com.example.ExampleRepository            : クエリ1
org.mybatis.spring.SqlSessionUtils       : Creating a new SqlSession
  -- 中略 --
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.3333333333333333
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351]

次にクエリ2を確認します。キャッシュにヒットしたためヒット率が上がっており、Cache Hit Ratio が 0.5 になっています。

クエリ2
com.example.ExampleRepository            : クエリ2
org.mybatis.spring.SqlSessionUtils       : Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351] from current transaction
com.example.ExampleMapper                : Cache Hit Ratio [com.example.ExampleMapper]: 0.5
org.mybatis.spring.SqlSessionUtils       : Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1bb15351]

同じクエリを実行したとしてもステートメントが異なる場合はキャッシュは利用されません。これはステートメント単位でキャッシュが行われるためです。

2ndレベルキャッシュのプロパティ

2ndレベルキャッシュはプロパティの設定値で挙動が変わります。このようなプロパティがあるようです。

  • eviction
  • flushInterval
  • size
  • readOnly

以下、それぞれのプロパティの機能について公式サイトからの引用です。

指定可能な置換(eviction)ポリシーは以下の通りです。

LRU – Least Recently Used: 最も長いこと使われていないオブジェクトを優先的に削除します。
FIFO – First In First Out: 最初に登録されたオブジェクトから順番に削除します。
SOFT – Soft Reference: ガベージコレクターの状態と Soft Reference の規則に基づいてオブジェクトを削除します。
WEAK – Weak Reference: ガベージコレクターの状態と Weak Reference の規則に基づいて、より積極的にオブジェクトを削除します。
デフォルト値は LRU です。

flushInterval には、適切な時間(ミリ秒)を表す正の整数を指定することができます。 デフォルト値は指定なしで、キャッシュがフラッシュされるのはステートメント(insert, update, delete または flushCache が設定された select)が実行された場合のみです。

size には任意の正の整数を指定することができますが、実行環境で利用可能なメモリリソースとキャッシュされるオブジェクトのサイズに配慮してください。デフォルト値は 1024 です。

readOnly 属性には true または false を設定することができます。読み取り専用キャッシュはキャッシュされたオブジェクトのインスタンスをそのまま呼び出し元に返しますので、このオブジェクトを変更すべきではありません。 読み取り専用キャッシュの利点は非常に高速だということです。 読み書き可能なキャッシュは、キャッシュされたオブジェクトを複製して(シリアライゼーションを使います)返しますので読み取り専用と比較すると遅いですが安全です。 デフォルト値は false です。

まとめ

  • 2ndレベルキャッシュを有効にするとトランザクションが別でもキャッシュが有効になります
  • 同一トランザクションでは2ndレベルキャッシュが有効であってもキャッシュが使用されないようです

参考

  1. Mybatis.org (Mapper XML ファイル - chache)

Discussion