Ehcache

Java向けオープンソースキャッシュライブラリ。アプリケーション内キャッシュから分散キャッシュまで対応。Hibernate、Spring Boot等と統合。

cache-serverJavaTerracottaclustered-cacheoff-heapJSR-107JCache

EhCache

EhCacheは、Javaアプリケーションの性能向上を目的とした高性能なキャッシュライブラリです。階層化ストレージ、JSR-107(JCache)準拠、Terracottaによるクラスタリング機能を提供し、エンタープライズアプリケーションでの豊富な実装実績を持つ、最も広く使用されているJavaキャッシュソリューションです。

概要

EhCacheは20年以上の歴史を持つ成熟したJavaキャッシュライブラリで、2024年現在もJavaエコシステムにおいて重要な位置を占めています。EhCache 3では従来版から大幅にアーキテクチャが刷新され、Java 8+対応、JSR-107準拠、マルチティア階層化ストレージ、Terracottaサーバーによる分散キャッシュ機能を実現しました。特にエンタープライズ環境では、Hibernateとの統合やSpring Frameworkでの標準的な利用実績により、信頼性の高いキャッシュソリューションとして採用されています。

特徴

主要機能

  • 階層化ストレージ: On-Heap、Off-Heap、Disk、Clusteredの4つのティアによる効率的なデータ管理
  • JSR-107準拠: 標準的なJCache APIの完全サポートと拡張機能の提供
  • 分散キャッシュ: Terracottaサーバーによるクラスタリングとスケールアウト対応
  • 高性能: On-Heapでの基本操作は500ns以下、並行アクセスでの最適化
  • 豊富な有効期限機能: TTL、TTI、カスタム有効期限ポリシーの柔軟な設定
  • トランザクション対応: XAトランザクションサポートによるACID保証

アーキテクチャ特性

EhCacheの技術的優位性:

  • 階層型データ管理: ホットデータを高速なティアに配置し、コールドデータを低速だが大容量なティアに配置
  • オフヒープストレージ: Javaヒープ外メモリ利用によるGCの影響回避と大容量キャッシュの実現
  • 非同期イベント処理: リスナーによるキャッシュイベントの効率的な処理
  • 柔軟なシリアライゼーション: カスタムシリアライザーによる性能最適化

パフォーマンス特性

  • On-Heap操作で500ns以下の高速アクセス
  • マルチコアCPU環境での並行アクセス最適化
  • 階層ストレージによる効率的なメモリ利用
  • Terracottaクラスタリングによるスケールアウト性能

メリット・デメリット

メリット

  • 成熟した実装: 20年以上の開発・運用実績による高い安定性
  • エンタープライズ対応: Hibernateとの統合やSpring Framework標準サポート
  • 豊富な機能: 階層化ストレージ、分散キャッシュ、トランザクション対応の包括的な機能セット
  • JSR-107準拠: 標準APIによる移植性とベンダーロックイン回避
  • 柔軟な設定: プログラマティック設定とXML設定による多様な構成オプション
  • スケーラビリティ: Terracottaによるクラスタリングでのテラバイト級キャッシュ対応

デメリット

  • 複雑な設定: 高機能ゆえの設定の複雑性と学習コストの高さ
  • メモリ使用量: Caffeineなど他のライブラリと比較して多いメモリ消費
  • 性能面での劣勢: 最新のライブラリ(Caffeine等)と比較した際の読み書き性能の差
  • レガシー感: 長い歴史ゆえの古い設計思想と最新アーキテクチャとのギャップ
  • Terracotta依存: 分散キャッシュ利用時のTerracottaサーバー運用の複雑性
  • Java 8+制約: 新しいJavaバージョンへの対応が必要

参考ページ

書き方の例

Mavenの依存関係追加

<!-- EhCache Core依存関係 -->
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.10.8</version>
</dependency>

<!-- Terracottaクラスタリング機能 -->
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache-clustered</artifactId>
    <version>3.10.8</version>
</dependency>

<!-- トランザクション機能 -->
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache-transactions</artifactId>
    <version>3.10.8</version>
</dependency>

<!-- Spring Boot統合 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

基本的なキャッシュ操作

import org.ehcache.Cache;
import org.ehcache.CacheManager;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.EntryUnit;
import org.ehcache.config.units.MemoryUnit;

public class EhCacheBasicExample {
    public static void main(String[] args) {
        // CacheManagerの作成
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
            .withCache("userCache",
                CacheConfigurationBuilder.newCacheConfigurationBuilder(
                    String.class, String.class,
                    ResourcePoolsBuilder.heap(100)
                        .offheap(10, MemoryUnit.MB)
                        .disk(100, MemoryUnit.MB, true)
                )
            )
            .build(true);

        // キャッシュの取得
        Cache<String, String> userCache = cacheManager.getCache("userCache", String.class, String.class);

        // データの追加
        userCache.put("user:1", "John Doe");
        userCache.put("user:2", "Jane Smith");
        userCache.put("user:3", "Bob Wilson");

        // データの取得
        String user1 = userCache.get("user:1");
        System.out.println("User 1: " + user1);

        // 存在確認
        boolean hasUser2 = userCache.containsKey("user:2");
        System.out.println("User 2 exists: " + hasUser2);

        // データの削除
        userCache.remove("user:3");

        // キャッシュサイズの確認
        System.out.println("Cache size: " + 
            java.util.stream.StreamSupport.stream(userCache.spliterator(), false).count());

        // CacheManagerのクローズ
        cacheManager.close();
    }
}

階層化ストレージの設定

import org.ehcache.PersistentCacheManager;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.EntryUnit;
import org.ehcache.config.units.MemoryUnit;

public class EhCacheTieredExample {
    public static void main(String[] args) {
        // 永続化ディレクトリの設定
        PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder()
            .with(CacheManagerBuilder.persistence(new File("cache-data")))
            .withCache("productCache",
                CacheConfigurationBuilder.newCacheConfigurationBuilder(
                    Long.class, Product.class,
                    ResourcePoolsBuilder.newResourcePoolsBuilder()
                        .heap(100, EntryUnit.ENTRIES)        // ヒープ: 100エントリ
                        .offheap(50, MemoryUnit.MB)           // オフヒープ: 50MB
                        .disk(500, MemoryUnit.MB, true)       // ディスク: 500MB (永続化)
                )
            )
            .build(true);

        Cache<Long, Product> productCache = persistentCacheManager.getCache("productCache", Long.class, Product.class);

        // 商品データの追加
        for (long i = 1; i <= 1000; i++) {
            Product product = new Product(i, "Product " + i, 29.99 + i);
            productCache.put(i, product);
        }

        // データアクセスパターンのシミュレーション
        for (int round = 0; round < 5; round++) {
            System.out.println("=== Round " + (round + 1) + " ===");
            
            // ホットデータへのアクセス
            for (long i = 1; i <= 20; i++) {
                long startTime = System.nanoTime();
                Product product = productCache.get(i);
                long endTime = System.nanoTime();
                
                if (round == 0) {
                    System.out.println("Product " + i + " access time: " + (endTime - startTime) + " ns");
                }
            }
            
            // コールドデータへのアクセス
            for (long i = 500; i <= 520; i++) {
                Product product = productCache.get(i);
            }
            
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                break;
            }
        }

        persistentCacheManager.close();
    }
    
    static class Product {
        private final Long id;
        private final String name;
        private final Double price;
        
        public Product(Long id, String name, Double price) {
            this.id = id;
            this.name = name;
            this.price = price;
        }
        
        // getters, equals, hashCode, toString methods...
    }
}

CacheLoaderWriterによるRead-Through/Write-Through

import org.ehcache.spi.loaderwriter.CacheLoaderWriter;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;

public class EhCacheLoaderWriterExample {
    
    // データベースシミュレーション
    private static final Map<String, User> DATABASE = new ConcurrentHashMap<>();
    
    public static void main(String[] args) {
        // データベースの初期化
        DATABASE.put("user:1", new User("user:1", "John Doe", "[email protected]"));
        DATABASE.put("user:2", new User("user:2", "Jane Smith", "[email protected]"));
        
        // CacheLoaderWriterの実装
        CacheLoaderWriter<String, User> loaderWriter = new CacheLoaderWriter<String, User>() {
            @Override
            public User load(String key) throws Exception {
                System.out.println("Loading from database: " + key);
                // データベースアクセスのシミュレーション
                Thread.sleep(100);
                return DATABASE.get(key);
            }
            
            @Override
            public void write(String key, User value) throws Exception {
                System.out.println("Writing to database: " + key);
                // データベース書き込みのシミュレーション
                Thread.sleep(50);
                DATABASE.put(key, value);
            }
            
            @Override
            public void delete(String key) throws Exception {
                System.out.println("Deleting from database: " + key);
                Thread.sleep(50);
                DATABASE.remove(key);
            }
        };
        
        // CacheManagerの作成
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
            .withCache("userCache",
                CacheConfigurationBuilder.newCacheConfigurationBuilder(
                    String.class, User.class,
                    ResourcePoolsBuilder.heap(100))
                .withLoaderWriter(loaderWriter)
            )
            .build(true);
            
        Cache<String, User> userCache = cacheManager.getCache("userCache", String.class, User.class);
        
        // Read-Through: キャッシュにないデータの自動ロード
        System.out.println("=== Read-Through Test ===");
        User user1 = userCache.get("user:1");  // データベースからロード
        System.out.println("First access: " + user1.getName());
        
        User user1Again = userCache.get("user:1");  // キャッシュからアクセス
        System.out.println("Second access: " + user1Again.getName());
        
        // Write-Through: キャッシュとデータベース両方への書き込み
        System.out.println("=== Write-Through Test ===");
        User newUser = new User("user:3", "Bob Wilson", "[email protected]");
        userCache.put("user:3", newUser);  // キャッシュとDBの両方に書き込み
        
        // Write-Through確認
        User user3 = userCache.get("user:3");  // キャッシュからアクセス
        System.out.println("New user from cache: " + user3.getName());
        
        // データベースに直接アクセスして確認
        User user3FromDB = DATABASE.get("user:3");
        System.out.println("New user from database: " + user3FromDB.getName());
        
        // Delete-Through: キャッシュとデータベース両方からの削除
        System.out.println("=== Delete-Through Test ===");
        userCache.remove("user:2");
        
        System.out.println("Remaining users in cache:");
        userCache.forEach((key, value) -> 
            System.out.println("  " + key + ": " + value.getName()));
            
        System.out.println("Remaining users in database:");
        DATABASE.forEach((key, value) -> 
            System.out.println("  " + key + ": " + value.getName()));
        
        cacheManager.close();
    }
    
    static class User {
        private final String id;
        private final String name;
        private final String email;
        
        public User(String id, String name, String email) {
            this.id = id;
            this.name = name;
            this.email = email;
        }
        
        public String getId() { return id; }
        public String getName() { return name; }
        public String email() { return email; }
    }
}

Spring Boot統合

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;

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

@Service
public class UserService {
    
    @Cacheable(value = "users", key = "#userId")
    public User findUser(Long userId) {
        // データベースアクセスのシミュレーション
        System.out.println("Fetching user from database: " + userId);
        simulateSlowDatabase();
        return new User(userId, "User " + userId, "user" + userId + "@example.com");
    }
    
    @CachePut(value = "users", key = "#user.id")
    public User updateUser(User user) {
        System.out.println("Updating user: " + user.getId());
        simulateSlowDatabase();
        return user;
    }
    
    @CacheEvict(value = "users", key = "#userId")
    public void deleteUser(Long userId) {
        System.out.println("Deleting user: " + userId);
        simulateSlowDatabase();
    }
    
    @CacheEvict(value = "users", allEntries = true)
    public void clearUserCache() {
        System.out.println("Clearing all user cache");
    }
    
    private void simulateSlowDatabase() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
# application.yml - EhCache設定
spring:
  cache:
    type: ehcache
    ehcache:
      config: classpath:ehcache.xml

# Cache names(JSR-107設定の場合)
# spring.cache.cache-names=users,products,orders
<!-- src/main/resources/ehcache.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.ehcache.org/v3"
        xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
        xsi:schemaLocation="
            http://www.ehcache.org/v3 https://www.ehcache.org/schema/ehcache-core-3.0.xsd
            http://www.ehcache.org/v3/jsr107 https://www.ehcache.org/schema/ehcache-107-ext-3.0.xsd">

    <cache alias="users">
        <key-type>java.lang.Long</key-type>
        <value-type>com.example.User</value-type>
        <expiry>
            <ttl unit="minutes">10</ttl>
        </expiry>
        <listeners>
            <listener>
                <class>com.example.CacheEventLogger</class>
                <event-firing-mode>ASYNCHRONOUS</event-firing-mode>
                <event-ordering-mode>UNORDERED</event-ordering-mode>
                <events-to-fire-on>CREATED</events-to-fire-on>
                <events-to-fire-on>UPDATED</events-to-fire-on>
                <events-to-fire-on>EXPIRED</events-to-fire-on>
                <events-to-fire-on>REMOVED</events-to-fire-on>
                <events-to-fire-on>EVICTED</events-to-fire-on>
            </listener>
        </listeners>
        <resources>
            <heap unit="entries">1000</heap>
            <offheap unit="MB">50</offheap>
        </resources>
    </cache>

    <cache alias="products">
        <key-type>java.lang.String</key-type>
        <value-type>com.example.Product</value-type>
        <expiry>
            <tti unit="minutes">5</tti>
        </expiry>
        <resources>
            <heap unit="entries">500</heap>
            <offheap unit="MB">20</offheap>
        </resources>
    </cache>

    <cache-template name="default">
        <key-type>java.lang.Object</key-type>
        <value-type>java.lang.Object</value-type>
        <expiry>
            <ttl unit="minutes">5</ttl>
        </expiry>
        <resources>
            <heap unit="entries">100</heap>
        </resources>
    </cache-template>

</config>

クラスタリング設定(Terracotta)

import org.ehcache.clustered.client.config.builders.ClusteringServiceConfigurationBuilder;
import org.ehcache.clustered.client.config.builders.TimeoutsBuilder;
import org.ehcache.config.units.MemoryUnit;

public class EhCacheClusteredExample {
    public static void main(String[] args) {
        // クラスタリングCacheManagerの作成
        PersistentCacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
            .with(ClusteringServiceConfigurationBuilder.cluster(
                URI.create("terracotta://localhost:9410/my-application"))
                .autoCreate(server -> server
                    .defaultServerResource("primary-server-resource")
                    .resourcePool("resource-pool-a", 32, MemoryUnit.MB)
                    .resourcePool("resource-pool-b", 16, MemoryUnit.MB))
                .timeouts(TimeoutsBuilder.timeouts()
                    .read(Duration.ofSeconds(10))
                    .write(Duration.ofSeconds(10))
                    .connection(Duration.ofSeconds(150)))
            )
            .withCache("clustered-cache",
                CacheConfigurationBuilder.newCacheConfigurationBuilder(
                    String.class, String.class,
                    ResourcePoolsBuilder.newResourcePoolsBuilder()
                        .heap(100, EntryUnit.ENTRIES)
                        .with(ClusteredResourcePoolBuilder.clusteredDedicated("primary-server-resource", 8, MemoryUnit.MB))
                )
            )
            .build(true);

        Cache<String, String> clusteredCache = cacheManager.getCache("clustered-cache", String.class, String.class);

        // 分散キャッシュの操作
        clusteredCache.put("shared-key-1", "Shared Value 1");
        clusteredCache.put("shared-key-2", "Shared Value 2");

        // 他のアプリケーションインスタンスからもアクセス可能
        String sharedValue = clusteredCache.get("shared-key-1");
        System.out.println("Shared value: " + sharedValue);

        cacheManager.close();
    }
}

統計情報とモニタリング

import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.CacheManagerBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.core.statistics.CacheStatistics;
import org.ehcache.core.statistics.DefaultStatisticsService;

public class EhCacheStatisticsExample {
    public static void main(String[] args) throws InterruptedException {
        // 統計サービスの有効化
        DefaultStatisticsService statisticsService = new DefaultStatisticsService();
        
        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
            .using(statisticsService)
            .withCache("monitoredCache",
                CacheConfigurationBuilder.newCacheConfigurationBuilder(
                    String.class, String.class,
                    ResourcePoolsBuilder.heap(100)
                )
            )
            .build(true);

        Cache<String, String> monitoredCache = cacheManager.getCache("monitoredCache", String.class, String.class);

        // キャッシュ操作の実行
        for (int i = 0; i < 1000; i++) {
            monitoredCache.put("key" + i, "value" + i);
        }

        // ランダムアクセスによるヒット率の測定
        Random random = new Random();
        for (int i = 0; i < 2000; i++) {
            int keyIndex = random.nextInt(1500);  // キーの範囲を広げてミスを発生させる
            String value = monitoredCache.get("key" + keyIndex);
        }

        // 統計情報の取得
        CacheStatistics statistics = statisticsService.getCacheStatistics("monitoredCache");

        System.out.println("=== EhCache Statistics ===");
        System.out.println("Cache hits: " + statistics.getCacheHits());
        System.out.println("Cache misses: " + statistics.getCacheMisses());
        System.out.println("Hit ratio: " + String.format("%.2f%%", statistics.getCacheHitPercentage()));
        System.out.println("Cache puts: " + statistics.getCachePuts());
        System.out.println("Cache removals: " + statistics.getCacheRemovals());
        System.out.println("Cache evictions: " + statistics.getCacheEvictions());

        // 定期的な統計モニタリング
        Thread monitoringThread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    Thread.sleep(5000);
                    
                    System.out.println("--- Cache Monitor ---");
                    System.out.println("Hit ratio: " + String.format("%.2f%%", statistics.getCacheHitPercentage()));
                    System.out.println("Total operations: " + (statistics.getCacheHits() + statistics.getCacheMisses()));
                    
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        });
        
        monitoringThread.start();
        Thread.sleep(30000);  // 30秒間監視
        monitoringThread.interrupt();

        cacheManager.close();
    }
}

イベントリスナーの実装

import org.ehcache.event.CacheEvent;
import org.ehcache.event.CacheEventListener;
import org.ehcache.event.EventType;
import org.ehcache.config.builders.CacheEventListenerConfigurationBuilder;

public class EhCacheEventListenerExample {
    
    public static void main(String[] args) {
        // カスタムイベントリスナーの実装
        CacheEventListener<String, String> listener = new CacheEventListener<String, String>() {
            @Override
            public void onEvent(CacheEvent<? extends String, ? extends String> event) {
                System.out.println("Cache Event: " + event.getType() + 
                    " - Key: " + event.getKey() + 
                    " - Old Value: " + event.getOldValue() + 
                    " - New Value: " + event.getNewValue());
            }
        };

        CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
            .withCache("eventCache",
                CacheConfigurationBuilder.newCacheConfigurationBuilder(
                    String.class, String.class,
                    ResourcePoolsBuilder.heap(50))
                .withService(CacheEventListenerConfigurationBuilder
                    .newEventListenerConfiguration(listener, EventType.CREATED, EventType.UPDATED, EventType.REMOVED, EventType.EXPIRED, EventType.EVICTED)
                    .unordered().asynchronous()
                )
            )
            .build(true);

        Cache<String, String> eventCache = cacheManager.getCache("eventCache", String.class, String.class);

        System.out.println("=== Event Listener Demo ===");
        
        // イベントの発生
        eventCache.put("event1", "First Value");     // CREATED イベント
        eventCache.put("event1", "Updated Value");   // UPDATED イベント
        eventCache.put("event2", "Second Value");    // CREATED イベント
        eventCache.remove("event2");                 // REMOVED イベント

        // 大量のデータで eviction イベントを発生
        for (int i = 0; i < 100; i++) {
            eventCache.put("evict" + i, "Value " + i);  // EVICTED イベントが発生
        }

        // 非同期イベントの完了を待つ
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        cacheManager.close();
    }
}

EhCacheは、その豊富な機能と長い実績により、特にエンタープライズJavaアプリケーションにおいて信頼性の高いキャッシュソリューションとして機能しています。