Ehcache とは
キャッシュ機能を提供する Java のライブラリ。読みは「いーえいちきゃっしゅ」。
Java におけるキャッシュライブラリのデファクトスタンダードらしく、 JSR-107(JCache) の実装でもある(ただし、ver2 を JCache として利用する場合は追加モジュールが必要になるそうです1)。
オープンソースで、ライセンスは Apache ライセンス ver 2。
元は Greg Luck という人が開発していたが、後に Terracotta, Inc. という会社がプロジェクトを買収し、以後はこの会社が開発を進めている。
Terracotta, Inc. は Software AG という会社の子会社で、 Terracotta BigMemory というデータストアの製品を開発している。
このため Ehcache は Terracotta BigMemory 向けの有償機能を多数用意している。
基本はメモリ上に保存しておきつつ、ローカルディスクに退避させることもできる。
レプリケーションやトランザクションの機能とかも提供してくれる。
2015/06/26 現在、 ver 3 の開発が GitHub で行われている。
(ver 3 からは、 JCache の機能がデフォルトで組み込まれる予定らしい)
インストール
    compile 'net.sf.ehcache:ehcache:2.10.0'
Maven Repository: net.sf.ehcache » ehcache » 2.10.0
Hello World
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            manager.addCache("myCache");
            Cache myCache = manager.getCache("myCache");
            myCache.put(new Element("hoge", "HOGE"));
            
            myCache = manager.getCache("myCache");
            Element element = myCache.get("hoge");
            System.out.println("myCache.hoge = " + element.getObjectValue());
        } finally {
            manager.shutdown();
        }
    }
}
myCache.hoge = HOGE
- まず、 CacheManager#getInstance()で、CacheManagerのインスタンスを取得する。- 
CacheManagerのインスタンスはシングルトンで管理されていて、getInstance()は常に同じインスタンスを返す。
 
- 
- 
CacheManager#addCache(String)で、キャッシュを追加できる。
- 
CacheManager#getCache(String)で、キャッシュを取得できる。
- キャッシュ(Cache)にはElementと呼ばれるキーバリューを持つオブジェクトを登録できる。- 登録には、 Cache#put(Element)を使う。
 
- 登録には、 
- 
Cache#get(String)で、該当するキーを持つElementを取得できる。
- 
Element#getObjectValue()で、Elementが持つ値を取得できる。
- 
CacheManagerは、最後にshutdown()する。
ストレージ
Ehcache では、次の3つのストレージを利用できる。
- Memory store
- Java のヒープメモリ。通常はここが使用される。
- シリアライズできないオブジェクトでも保存できる。
 
- Off-heap store
- メモリ上だが、ここに保存されたデータは GC の対象にならない。
- デフォルトでは使用できず、 Terracotta BigMemory を組み合わせる必要がある。
- シリアライズ可能なオブジェクトのみ保存できる。
 
- Disk store
- ファイルなどの、メモリ外のストレージ。
- メモリに収まりきらないときなどに、退避場所として利用できる。
- シリアライズ可能なオブジェクトのみ保存できる。
 
設定ファイル
キャッシュのサイズなどの設定は、 xml で記述する。
xml は ehcache.xml という名前で作成し、クラスパスの直下に配置する。
キャッシュを定義する
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
  
  <cache name="myCache"
         maxEntriesLocalHeap="0">
  </cache>
  
</ehcache>
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            Cache cache = manager.getCache("myCache");
            System.out.println(cache);
        } finally {
            manager.shutdown();
        }
    }
}
[ name = myCache status = STATUS_ALIVE eternal = false overflowToDisk = false maxEntriesLocalHeap = 0 maxEntriesLocalDisk = 0 memoryStoreEvictionPolicy = LRU timeToLiveSeconds = 0 timeToIdleSeconds = 0 persistence = none diskExpiryThreadIntervalSeconds = 120 cacheEventListeners: ; orderedCacheEventListeners:  maxBytesLocalHeap = 0 overflowToOffHeap = false maxBytesLocalOffHeap = 0 maxBytesLocalDisk = 0 pinned = false ]
- xml は、トップレベルに <ehcache>タグを宣言する。
- キャッシュの定義には、 <cache>タグを使用する。- 
name属性でキャッシュの名前を定義する。- 一意な名前を付ける必要がある。
 
- 
maxEntriesLocalHeap属性で、ヒープに保存できるElementの数を指定する。- 
0を指定したら、無制限になる。
 
- 
 
- 
ヒープに保存できる Element の個数を指定する
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
  <cache name="myCache"
         maxEntriesLocalHeap="3">
  </cache>
  
</ehcache>
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            Cache cache = manager.getCache("myCache");
            
            cache.put(new Element("one", "ONE"));
            cache.put(new Element("two", "TWO"));
            cache.put(new Element("three", "THREE"));
            cache.put(new Element("four", "FOUR"));
            
            System.out.println(cache.get("one"));
            System.out.println(cache.get("two"));
            System.out.println(cache.get("three"));
            System.out.println(cache.get("four"));
        } finally {
            manager.shutdown();
        }
    }
}
null
[ key = two, value=TWO, version=1, hitCount=1, CreationTime = 1434633777588, LastAccessTime = 1434633777588 ]
[ key = three, value=THREE, version=1, hitCount=1, CreationTime = 1434633777588, LastAccessTime = 1434633777588 ]
[ key = four, value=FOUR, version=1, hitCount=1, CreationTime = 1434633777588, LastAccessTime = 1434633777588 ]
- 
maxEntriesLocalHeapで、ヒープに保存できるElementの個数を指定できる。
- 指定した個数をオーバーした場合は、保存済みの Elementのどれかが破棄される。- どれが破棄されるかは、キャッシュアルゴリズムの設定による(詳細後述)。
 
登録してから一定時間過ぎたら破棄されるようにする
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
  <cache name="myCache"
         maxEntriesLocalHeap="0"
         timeToLiveSeconds="1">
  </cache>
  
</ehcache>
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            Cache cache = manager.getCache("myCache");
            
            put(cache, "hoge", "HOGE");
            
            sleep(500);
            
            print(cache, "hoge");
            
            sleep(600);
            
            print(cache, "hoge");
        } finally {
            manager.shutdown();
        }
    }
    
    private static void put(Cache cache, String key, Object value) {
        cache.put(new Element(key, value));
    }
    
    private static void print(Cache cache, String key) {
        Element e = cache.get(key);
        System.out.println(key + " : " + (e == null ? "<none>" : e.getObjectValue()));
    }
    
    private static void sleep(long ms) {
        try {
            Thread.sleep(ms);
            System.out.println("> " + ms + " ms...");
        } catch (InterruptedException e) {}
    }
}
> 500 ms...
hoge : HOGE
> 600 ms...
hoge : <none>
- 
timeToLiveSecondsで、Elementをキャッシュに登録してから破棄されるまでの生存時間を指定できる(秒指定)。
- 
0なら無制限。
最後にアクセスしてから一定時間過ぎたら破棄されるようにする
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
  <cache name="myCache"
         maxEntriesLocalHeap="0"
         timeToIdleSeconds="1">
  </cache>
  
</ehcache>
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            Cache cache = manager.getCache("myCache");
            
            put(cache, "hoge", "HOGE");
            
            sleep(500);
            
            print(cache, "hoge");
            
            sleep(600);
            
            print(cache, "hoge");
            
            sleep(1010);
            
            print(cache, "hoge");
        } finally {
            manager.shutdown();
        }
    }
    
    private static void put(Cache cache, String key, Object value) {
        cache.put(new Element(key, value));
    }
    
    private static void print(Cache cache, String key) {
        Element e = cache.get(key);
        System.out.println(key + " : " + (e == null ? "<none>" : e.getObjectValue()));
    }
    
    private static void sleep(long ms) {
        try {
            Thread.sleep(ms);
            System.out.println("> " + ms + " ms...");
        } catch (InterruptedException e) {}
    }
}
> 500 ms...
hoge : HOGE
> 600 ms...
hoge : HOGE
> 1010 ms...
hoge : <none>
- 
timeToIdleSecondsで、最後にアクセスしてから破棄されるまでの生存時間を指定できる(秒指定)。
- 
0なら無制限。
ローカルのテンポラリファイルに保存する
基本
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
  
  <cache name="myCache"
         maxEntriesLocalHeap="0">
    <persistence strategy="localTempSwap" />
  </cache>
  
</ehcache>
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.statistics.StatisticsGateway;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            Cache cache = manager.getCache("myCache");
            
            for (int i=0; i<1000; i++) {
                put(cache, "key-" + i, "value-" + i);
            }
            printCacheSize(cache);
            
            sleep(5);
            printCacheSize(cache);
            
            sleep(5);
            printCacheSize(cache);
            
            sleep(500);
            
            printCacheSize(cache);
            
        } finally {
            manager.shutdown();
        }
    }
    private static void printCacheSize(Cache cache) {
        StatisticsGateway statistics = cache.getStatistics();
        
        System.out.println(
            "======================\n" + 
            "heap size = " + statistics.getLocalHeapSize() + "\n" +
            "disk size = " + statistics.getLocalDiskSize() + "\n"
        );
    }
    
    private static void put(Cache cache, String key, Object value) {
        cache.put(new Element(key, value));
    }
    
    private static void sleep(long ms) {
        try {
            Thread.sleep(ms);
            System.out.println("> " + ms + " ms...");
        } catch (InterruptedException e) {}
    }
}
======================
heap size = 1000
disk size = 279
> 5 ms...
======================
heap size = 1000
disk size = 344
> 5 ms...
======================
heap size = 1000
disk size = 424
> 500 ms...
======================
heap size = 1000
disk size = 1000
- 
<cache>タグの子供に<persistence>タグを追加し、strategy="localTempSwap"と属性を指定する。
- 
localTempSwapを指定すると、ローカルにテンポラリファイルが作成され、そちらにもキャッシュが保存されるようになる。
- テンポラリファイルは、 JVM が終了すると削除される(永続化はされない)。
- 
storategyには、他にもlocalRestartableとかdistributedを指定できる。- 
localRestartableは、キャッシュ情報が永続化されるため、再起動してもキャッシュが生き残るっぽい(たぶん)。
- ただし、この属性はエンタープライズ版でないと利用できない。
 
- 
- 保存先を明示しない場合、 System.getProperty("java.io.tmpdir")で取得されたフォルダ配下にテンポラリファイルが作成される。
- キャッシュに保存した Elementは、全てテンポラリファイルにも出力される。- 「メモリストアがいっぱいになったら」ではないっぽい。
- でも、 ehcache.xml のサンプル とかには Swaps cache entries to disk when the cache is full.(キャッシュがいっぱいになったら、ディスクにキャッシュのエントリをスワップする) とか書いてる。
- 何か勘違いしているのか。それとも、どこか設定が間違っているのか。。。 わからん。。。
 
- テンポラリファイルへの出力は 非同期 で行われる。
- 
<persistence>タグにはsynchronousWritesという属性が設定できる。
- しかし、 strategy="localTempSwap"としている場合はfalseしか指定できない。
 
- 
メモリストアの上限値を設定した場合
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
    
  <cache name="myCache"
         maxEntriesLocalHeap="120">
    <persistence strategy="localTempSwap" />
  </cache>
  
</ehcache>
package sample.ehcache;
import java.util.function.Supplier;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.statistics.StatisticsGateway;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            Cache cache = manager.getCache("myCache");
            
            for (int i=0; i<1000; i++) {
                put(cache, "key-" + i, "value-" + i);
            }
            
            sleep(100);
            printStatistics(cache);
            
            print(cache, "key-0");
            printStatistics(cache);
            
            print(cache, "key-900");
            printStatistics(cache);
            
            print(cache, "key-900");
            printStatistics(cache);
            
        } finally {
            manager.shutdown();
        }
    }
    private static void printStatistics(Cache cache) {
        StatisticsGateway statistics = cache.getStatistics();
        
        System.out.println(
            "======================\n" + 
            "heap size = " + statistics.getLocalHeapSize() + "\n" +
            "heap hit  = " + statistics.localHeapHitCount() + "\n" +
            "disk size = " + statistics.getLocalDiskSize() + "\n" +
            "disk hit  = " + statistics.localDiskHitCount() + "\n"
        );
    }
    
    private static void print(Cache cache, String key) {
        printWithWhereFrom(cache, () -> {
            Element e = cache.get(key);
            return key + " : " + (e == null ? "<none>" : e.getObjectValue());
        });
    }
    
    private static void printWithWhereFrom(Cache cache, Supplier<String> sup) {
        StatisticsGateway statistics = cache.getStatistics();
        long heapHit = statistics.localHeapHitCount();
        
        String msg = sup.get();
        
        boolean isHeapHit = heapHit != statistics.localHeapHitCount();
        
        System.out.println(msg + " (from " + (isHeapHit ? "heap" : "disk") + ")");
    }
    
    private static void put(Cache cache, String key, Object value) {
        cache.put(new Element(key, value));
    }
    
    private static void sleep(long ms) {
        try {
            Thread.sleep(ms);
            System.out.println("> " + ms + " ms...");
        } catch (InterruptedException e) {}
    }
}
> 100 ms...
======================
heap size = 120
heap hit  = 0
disk size = 1000
disk hit  = 0
key-0 : value-0 (from heap)
======================
heap size = 120
heap hit  = 1
disk size = 1000
disk hit  = 0
key-900 : value-900 (from disk)
======================
heap size = 120
heap hit  = 1
disk size = 1000
disk hit  = 1
key-900 : value-900 (from heap)
======================
heap size = 120
heap hit  = 2
disk size = 1000
disk hit  = 1
- メモリストアには、指定した上限値までだけが保存されている。
- テンポラリファイルには、全てのデータが保存されている。
- ファイルから読み取ったデータはメモリストアに格納され、次回アクセス時はメモリストアから取り出すようになる。
ディスクに保存できる Element の数を指定する
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
    
  <cache name="myCache"
         maxEntriesLocalHeap="120"
         maxEntriesLocalDisk="500">
    <persistence strategy="localTempSwap" />
  </cache>
  
</ehcache>
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.statistics.StatisticsGateway;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            Cache cache = manager.getCache("myCache");
            
            for (int i=0; i<1000; i++) {
                put(cache, "key-" + i, "value-" + i);
            }
            
            sleep(1000);
            
            printCacheSize(cache);
            
        } finally {
            manager.shutdown();
        }
    }
    private static void printCacheSize(Cache cache) {
        StatisticsGateway statistics = cache.getStatistics();
        
        System.out.println(
            "======================\n" + 
            "heap size = " + statistics.getLocalHeapSize() + "\n" +
            "disk size = " + statistics.getLocalDiskSize() + "\n"
        );
    }
    
    private static void put(Cache cache, String key, Object value) {
        cache.put(new Element(key, value));
    }
    
    private static void sleep(long ms) {
        try {
            Thread.sleep(ms);
            System.out.println("> " + ms + " ms...");
        } catch (InterruptedException e) {}
    }
}
> 1000 ms...
======================
heap size = 120
disk size = 500
- 
maxEntriesLocalDisk属性で、ディスクに保存できるElementの上限を指定できる。
テンポラリファイルの作成先を指定する
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
  
  <diskStore path="F:\tmp\ehcache"/>
  
  <cache name="myCache"
         maxEntriesLocalHeap="120"
         maxEntriesLocalDisk="500">
    <persistence strategy="localTempSwap" />
  </cache>
  
</ehcache>
- 
<diskStore>タグのpath属性でテンポラリファイルの保存先を指定できる。
キャッシュアルゴリズムを指定する
キャッシュの容量がいっぱいになった状態で新しい Element が追加された場合、既に登録されている Element のうちどれかが破棄されることになる。
どの Element を破棄するかを決めるルールとして、キャッシュアルゴリズムを指定することができる。
次の実装を使って、 Ehcache がサポートしている3種類のキャッシュアルゴリズムの動作を確認してみる。
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            Cache cache = manager.getCache("myCache");
            
            putEach(cache, 3);
            
            printEach(cache, 4);
            
            print(cache, "key-1"); print(cache, "key-1");
            print(cache, "key-0"); print(cache, "key-0");
            print(cache, "key-2");
            
            put(cache, "key-3", "value-3");
            
            printEach(cache, 4);
            
        } finally {
            manager.shutdown();
        }
    }
    
    private static void put(Cache cache, String key, Object value) {
        cache.put(new Element(key, value));
    }
    
    private static void putEach(Cache cache, int size) {
        for (int i = 0; i < size; i++) {
            cache.put(new Element("key-" + i, "value-" + i));
        }
    }
    
    private static void print(Cache cache, String key) {
        Element e = cache.get(key);
        
        String value =
            (e == null) ? "<none>"
                        : "{ value=" + e.getObjectValue() +
                          ", LastAccessTime=" + e.getLastAccessTime() +
                          ", HitCount=" + e.getHitCount() + " }";
        
        System.out.println(key + " : " + value);
        sleep(1);
    }
    
    private static void printEach(Cache cache, int size) {
        System.out.println("\n================");
        for (int i = 0; i < size; i++) {
            print(cache, "key-" + i);
        }
        System.out.println();
    }
    
    private static void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {}
    }
}
LRU (Least Recently Used)
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
    
  <cache name="myCache"
         maxEntriesLocalHeap="3"
         memoryStoreEvictionPolicy="LRU">
  </cache>
  
</ehcache>
================
key-0 : { value=value-0, LastAccessTime=1434806550091, HitCount=1 }
key-1 : { value=value-1, LastAccessTime=1434806550093, HitCount=1 }
key-2 : { value=value-2, LastAccessTime=1434806550094, HitCount=1 }
key-3 : <none>
key-1 : { value=value-1, LastAccessTime=1434806550096, HitCount=2 }
key-1 : { value=value-1, LastAccessTime=1434806550097, HitCount=3 }
key-0 : { value=value-0, LastAccessTime=1434806550098, HitCount=2 }
key-0 : { value=value-0, LastAccessTime=1434806550099, HitCount=3 }
key-2 : { value=value-2, LastAccessTime=1434806550100, HitCount=2 }
================
key-0 : { value=value-0, LastAccessTime=1434806550101, HitCount=4 }
key-1 : <none>
key-2 : { value=value-2, LastAccessTime=1434806550103, HitCount=3 }
key-3 : { value=value-3, LastAccessTime=1434806550104, HitCount=1 }
- 
memoryStoreEvictionPolicy属性で、キャッシュアルゴリズムを指定できる。
- 
LRUは、参照されてから最も時間が経過しているElementが削除対象になる。
- 未指定の場合は、デフォルトで LRUになる。
FIFO (First In First Out)
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
    
  <cache name="myCache"
         maxEntriesLocalHeap="3"
         memoryStoreEvictionPolicy="FIFO">
  </cache>
  
</ehcache>
================
key-0 : { value=value-0, LastAccessTime=1434806573077, HitCount=1 }
key-1 : { value=value-1, LastAccessTime=1434806573078, HitCount=1 }
key-2 : { value=value-2, LastAccessTime=1434806573079, HitCount=1 }
key-3 : <none>
key-1 : { value=value-1, LastAccessTime=1434806573081, HitCount=2 }
key-1 : { value=value-1, LastAccessTime=1434806573082, HitCount=3 }
key-0 : { value=value-0, LastAccessTime=1434806573083, HitCount=2 }
key-0 : { value=value-0, LastAccessTime=1434806573084, HitCount=3 }
key-2 : { value=value-2, LastAccessTime=1434806573085, HitCount=2 }
================
key-0 : <none>
key-1 : { value=value-1, LastAccessTime=1434806573087, HitCount=4 }
key-2 : { value=value-2, LastAccessTime=1434806573088, HitCount=3 }
key-3 : { value=value-3, LastAccessTime=1434806573089, HitCount=1 }
- 
FIFOは、先に保存したものから順番に削除されていく。
- 先入れ先出し。
LFU (Least Frequently Used)
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
    
  <cache name="myCache"
         maxEntriesLocalHeap="3"
         memoryStoreEvictionPolicy="LFU">
  </cache>
  
</ehcache>
================
key-0 : { value=value-0, LastAccessTime=1434806629835, HitCount=1 }
key-1 : { value=value-1, LastAccessTime=1434806629836, HitCount=1 }
key-2 : { value=value-2, LastAccessTime=1434806629837, HitCount=1 }
key-3 : <none>
key-1 : { value=value-1, LastAccessTime=1434806629839, HitCount=2 }
key-1 : { value=value-1, LastAccessTime=1434806629840, HitCount=3 }
key-0 : { value=value-0, LastAccessTime=1434806629841, HitCount=2 }
key-0 : { value=value-0, LastAccessTime=1434806629842, HitCount=3 }
key-2 : { value=value-2, LastAccessTime=1434806629843, HitCount=2 }
================
key-0 : { value=value-0, LastAccessTime=1434806629844, HitCount=4 }
key-1 : { value=value-1, LastAccessTime=1434806629845, HitCount=4 }
key-2 : <none>
key-3 : { value=value-3, LastAccessTime=1434806629847, HitCount=1 }
- 
LFUは、最も参照頻度の低いモノから先に削除される。
設定ファイルを指定して読み込む
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
      
  <cache name="myCache"
         maxEntriesLocalHeap="0"
         timeToIdleSeconds="10">
  </cache>
</ehcache>
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.config.CacheConfiguration;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.create("./my-ehcache.xml");
        
        try {
            Cache cache = manager.getCache("myCache");
            CacheConfiguration config = cache.getCacheConfiguration();
            
            System.out.println(
                "MaxEntriesLocalHeap : " + config.getMaxEntriesLocalHeap() + "\n" +
                "TimeToIdleSeconds : " + config.getTimeToIdleSeconds()
            );
            
        } finally {
            manager.shutdown();
        }
    }
}
MaxEntriesLocalHeap : 0
TimeToIdleSeconds : 10
- 
CacheManager#create(String)で、ローカルに置いている任意の設定ファイルをパス指定で読み込むことができる。
- 
InputStreamを受け取ることもできる。
Cache の操作
Element を上書きする
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            manager.addCache("myCache");
            
            Cache myCache = manager.getCache("myCache");
            myCache.put(new Element("hoge", "HOGE"));
            
            String value = (String)myCache.get("hoge").getObjectValue();
            System.out.println("myCache.hoge = " + value);
            
            myCache.put(new Element("hoge", "hogeee"));
            
            System.out.println("myCache.hoge = " + myCache.get("hoge").getObjectValue());
        } finally {
            manager.shutdown();
        }
    }
}
myCache.hoge = HOGE
myCache.hoge = hogeee
- 同じキーの Elementをput()すれば、Elementを上書きできる。
Element を削除する
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            manager.addCache("myCache");
            
            Cache myCache = manager.getCache("myCache");
            myCache.put(new Element("hoge", "HOGE"));
            
            String value = (String)myCache.get("hoge").getObjectValue();
            System.out.println("myCache.hoge = " + value);
            
            myCache.remove("hoge");
            
            System.out.println("myCache.hoge = " + myCache.get("hoge"));
        } finally {
            manager.shutdown();
        }
    }
}
myCache.hoge = HOGE
myCache.hoge = null
- 
Cache#remove()で、指定したキーのElementを削除できる。
Read Overload
Thundering Herd 問題
例えば、データベース検索の結果をキャッシュに保存しておき、二回目以降同じキーで検索があれば、キャッシュに保存されたデータを返すようにしたとする。
実装にすると、以下のようなイメージ。
    private static String search(String key) {
        Element e = cache.get(key);
        
        if (e != null) {
            return (String)e.getObjectKey();
        }
        
        String value = database.get(key);
        cache.put(new Element(key, value));
        
        return value;
    }
この方法は、マルチスレッドで複数のアクセスがあった場合に問題が発生する。
もし複数のスレッドがこのメソッドを実行すると、タイミングによっては複数のスレッドが「まだキャッシュにデータが保存されていない」と判断してしまい、データベースアクセスが必要以上に発生してしまう可能性がある。
この問題を「Thundering Herd 問題」と言う。
synchronized で同期する回避策も考えられるが、後述するレプリケーションを使っていたりすると複数の JVM に跨った同期が必要になるので、 synchronized では対応しきれなくなる。
この問題の解決策として、 Ehcache には BlockingCache というデコレータクラスが用意されている。
まずは問題があるケースの実装
package sample.ehcache;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.IntStream;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
public class Main {
    private static Cache cache = CacheManager.getInstance().getCache("myCache");
    private static Map<String, String> dummyDatabase = new ConcurrentHashMap<>();
    static {
        dummyDatabase.put("hoge", "HOGE");
    }
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            IntStream.range(0, 3)
                     .parallel()
                     .forEach(i -> search("hoge"));
        } finally {
            manager.shutdown();
        }
    }
    
    private static String search(String key) {
        Element e = cache.get(key);
        
        if (e != null) {
            printKeyValue(key, e.getObjectValue(), "cache");
            return (String)e.getObjectValue();
        }
        
        String value = dummyDatabase.get(key);
        cache.put(new Element(key, value));
        
        printKeyValue(key, value, "database");
        return value;
    }
    
    private static void printKeyValue(String key, Object value, String from) {
        System.out.println("key=" + key + ", value=" + value + " (from " + from + ")");
    }
}
key=hoge, value=HOGE (from database)
key=hoge, value=HOGE (from database)
key=hoge, value=HOGE (from database)
全てデータベースからデータを取得してしまっている。
(データベースはダミー実装として Map を使用)
BlockingCache を使う
デコレーターファクトリを作成する
package sample.ehcache;
import java.util.Properties;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.constructs.CacheDecoratorFactory;
import net.sf.ehcache.constructs.blocking.BlockingCache;
public class MyDecoratorFactory extends CacheDecoratorFactory {
    @Override
    public Ehcache createDecoratedEhcache(Ehcache cache, Properties properties) {
        System.out.println("create decoreated cache");
        return new BlockingCache(cache);
    }
    @Override
    public Ehcache createDefaultDecoratedEhcache(Ehcache cache, Properties properties) {
        return new BlockingCache(cache);
    }
}
- まず、 CacheDecoratorFactoryを継承したファクトリクラスを作成する。
- とりあえず、それぞれのメソッドで BlockingCacheのインスタンスを返すように実装する。
設定ファイルでファクトリを指定する
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
    
  <cache name="myCache"
         maxEntriesLocalHeap="0">
    <cacheDecoratorFactory class="sample.ehcache.MyDecoratorFactory" />
  </cache>
  
</ehcache>
- ファクトリを作成したら、設定ファイルで <cacheDecoratorFactory>タグを使ってファクトリを設定する。
Ehcache に切り替え
package sample.ehcache;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.IntStream;
- import net.sf.ehcache.Cache;
+ import net.sf.ehcache.Ehcache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
public class Main {
-   private static Cache cache = CacheManager.getInstance().getCache("myCache");
+   private static Ehcache cache = CacheManager.getInstance().getEhcache("myCache");
    private static Map<String, String> dummyDatabase = new ConcurrentHashMap<>();
    static {
        dummyDatabase.put("hoge", "HOGE");
    }
    
    ...
- 利用する側は、 CacheManager#getEhcache()でEhcacheオブジェクトを取得するようにする。
動作確認
create decoreated cache
key=hoge, value=HOGE (from database)
key=hoge, value=HOGE (from cache)
key=hoge, value=HOGE (from cache)
- 初回だけデータベースアクセスになり、2回目以降はキャッシュからデータが取得されている。
Write Overload
Read Overload のように永続化されているデータとアプリケーションの間にキャッシュを挟んだ場合、キャッシュが更新されたときに永続化されているデータにも変更を反映しなければならない。
永続化層に変更を反映するタイミングは、大きく分けて次の2パターンが考えられる。
- キャッシュへの書き込みと同時に永続化層への反映を行う。
- キャッシュへの書き込みの後で、しばらくしてから非同期で反映を行う。
前者は単純で分かりやすいが、永続化層への書き込みがキャッシュへの書き込みと同じスレッドで実行されてしまう。
そのため、せっかくキャッシュを使って高速化を図っているのに、結局永続化層への書き込みがボトルネックとなり得てしまう。
後者の場合、キャッシュへの書き込みが終わると処理はすぐに呼び出し元に返される。
書き込まれたデータは少し間を置いてから、別のスレッドで永続化層にまとめて書き出される。
永続化層への書き込みは別スレッドで非同期で実行されるので、呼び出し元が待たされることはない。
また、キャッシュへの複数回の書き込みを後でまとめて永続化層に出力するので、無駄なアクセスを減らすこともできるメリットがある。
Ehcache は、どちらの方法もサポートしている。
基本
CacheWriter を実装する
package sample.ehcache;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
import net.sf.ehcache.CacheException;
import net.sf.ehcache.Element;
import net.sf.ehcache.writer.AbstractCacheWriter;
public class MyCacheWriter extends AbstractCacheWriter {
    @Override
    public void init() {
        print("init");
    }
    @Override
    public void dispose() throws CacheException {
        print("dispose");
    }
    @Override
    public void write(Element element) throws CacheException {
        print("write", Arrays.asList(element));
    }
    @Override
    public void writeAll(Collection<Element> elements) throws CacheException {
        print("writeAll", elements);
    }
    
    private void print(String method) {
        System.out.println("[" + Thread.currentThread().getName() + "] " + method);
    }
    
    private void print(String method, Collection<Element> elements) {
        print(method, elements.stream().map(this::toString).collect(Collectors.joining(", ", "[", "]")));
    }
    
    private String toString(Element e) {
        return "{key=" + e.getObjectKey() + ", value=" + e.getObjectValue() + "}";
    }
    
    private void print(String method, String msg) {
        System.out.println("[" + Thread.currentThread().getName() + "] " + method + " : " + msg);
    }
}
- 
CacheWriterというインターフェースを実装する。
- 
AbstractCacheWriterという抽象クラスが用意されている。
- 実装は標準出力に情報を書き出しているだけだが、実際に利用するときは永続化層への書き込みを実装することになる。
CacheWriterFactory を実装する
package sample.ehcache;
import java.util.Properties;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.writer.CacheWriter;
import net.sf.ehcache.writer.CacheWriterFactory;
public class MyCacheWriterFactory extends CacheWriterFactory {
    @Override
    public CacheWriter createCacheWriter(Ehcache cache, Properties properties) {
        return new MyCacheWriter();
    }
}
- 
CacheWriterFactoryを継承して作成する。
- 
createCacheWriter()で、CacheWriterのインスタンスを返すようにする。
設定ファイルで CacheWriterFactory を登録する
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
  
  <cache name="myCache"
         maxEntriesLocalHeap="0">
    <cacheWriter>
      <cacheWriterFactory class="sample.ehcache.MyCacheWriterFactory" />
    </cacheWriter>
  </cache>
</ehcache>
- 
<cache>タグの中に、<cacheWriter>と<cacheWriterFactory>タグで指定する。
動作確認
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            Cache cache = manager.getCache("myCache");
            
            print("begin");
            
            put(cache, "one", "ONE");
            put(cache, "two", "TWO");
            put(cache, "three", "THREE");
            
            print("end");
        } finally {
            manager.shutdown();
        }
    }
    private static void put(Cache cache, String key, Object value) {
        cache.putWithWriter(new Element(key, value));
    }
    
    private static void print(String msg) {
        System.out.println("[" + Thread.currentThread().getName() + "] " + msg);
    }
}
[main] init
[main] begin
[main] write : [{key=one, value=ONE}]
[main] write : [{key=two, value=TWO}]
[main] write : [{key=three, value=THREE}]
[main] end
[main] dispose
- デフォルトは、呼び出し元と同じスレッドで、即座に CacheWriterのメソッドが呼び出される。
- 
CacheWriterを使う場合は、キャッシュへの書き込みにCache#putWithWriter()メソッドを使用する。
非同期で書き込む
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
  
  <cache name="myCache"
         maxEntriesLocalHeap="0">
    <cacheWriter writeMode="write-behind">
      <cacheWriterFactory class="sample.ehcache.MyCacheWriterFactory" />
    </cacheWriter>
  </cache>
</ehcache>
[main] init
[main] begin
[main] end
[myCache write-behind] write : [{key=one, value=ONE}]
[myCache write-behind] write : [{key=two, value=TWO}]
[myCache write-behind] write : [{key=three, value=THREE}]
[main] dispose
- 
<cacheWriter>タグのwriteMode属性でwrite-behindを指定する。
- すると、 CacheWriterのメソッドの呼び出しが非同期になる。
いくつかの Element をまとめて書き出す
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
  
  <cache name="myCache"
         maxEntriesLocalHeap="0">
    <cacheWriter writeMode="write-behind"
                 writeBatching="true"
                 writeBatchSize="2">
      <cacheWriterFactory class="sample.ehcache.MyCacheWriterFactory" />
    </cacheWriter>
  </cache>
</ehcache>
[main] init
[main] begin
[main] end
[myCache write-behind] writeAll : [{key=one, value=ONE}, {key=two, value=TWO}]
[myCache write-behind] writeAll : [{key=three, value=THREE}]
[main] dispose
- 
writeBatching属性でtrueを指定すると、CacheWriterのwriteAll()メソッドが呼び出されるようになり、更新のあった複数のElementがまとめて渡されるようになる。
- 
writeBatchSize属性で、いくつのElementを同時に処理するかを指定する。デフォルトは1。
最後の更新だけを処理する
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
  
  <cache name="myCache"
         maxEntriesLocalHeap="0">
    <cacheWriter writeMode="write-behind"
                 writeCoalescing="true">
      <cacheWriterFactory class="sample.ehcache.MyCacheWriterFactory" />
    </cacheWriter>
  </cache>
</ehcache>
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            Cache cache = manager.getCache("myCache");
            
            print("begin");
            
            put(cache, "one", "ONE1");
            put(cache, "one", "ONE2");
            put(cache, "one", "ONE3");
            
            print("end");
        } finally {
            manager.shutdown();
        }
    }
    private static void put(Cache cache, String key, Object value) {
        cache.putWithWriter(new Element(key, value));
    }
    
    private static void print(String msg) {
        System.out.println("[" + Thread.currentThread().getName() + "] " + msg);
    }
}
[main] init
[main] begin
[main] end
[myCache write-behind] write : [{key=one, value=ONE3}]
[main] dispose
- 
writeCoalescing属性にtrueを指定すると、同じキーに対する更新は、最後の更新分だけがCacheWriterに渡されるようになる。
レプリケーション
Ehcache には、標準で RMI を利用したレプリケーション機能が用意されている。
(RMI 以外にも、 JMS と JGroups のサポートもあるらしい)
RMI は、 Remote Method Invocation の略。
別の VM 上にいるオブジェクトのメソッドを遠隔実行するための仕組みで、 Java の標準 API の1つとして提供されている。
TCP の上で動作する。
手順概略
RMI を利用したレプリケーションを使用する場合は、次の手順を踏むことになる。
- Peer Provider の Factory を作成・設定する。
- Peer Listener の Factory を作成・設定する。
- Cache の EventListener に RMI レプリケーション用のリスナーを登録する。
Peer Provider
連携相手を設定する。
マルチキャストを使った方法と、ホストを明示する方法の2つがある。
ここでは、ホストを明示する方法を試す。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
      
  <cacheManagerPeerProviderFactory
      class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
      properties="peerDiscovery=manual,
                  rmiUrls=//localhost:5002/myCache"
      />
  
  <cache name="myCache"
         maxEntriesLocalHeap="0">
  </cache>
</ehcache>
- 
<cacheManagerPeerProviderFactory>タグを使用する。
- 
classには、組み込みクラスであるRMICacheManagerPeerProviderFactoryを指定する。
- 
propertiesで、連携相手についての設定を記述する。- 
peerDiscoveryは、automaticかmanualのいずれかを指定する。- ここではホストを明示するので、 manualを指定する。
 
- ここではホストを明示するので、 
- 
rmiUrlsには、連携相手のホストを特定するための設定を記述する。- //<host>:<port>/<cache name>
- 縦線 |で複数指定可能。
 
 
- 
Peer Listener
連携の入り口を定義する。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
      
  <cacheManagerPeerProviderFactory
      class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
      properties="peerDiscovery=manual,
                  rmiUrls=//localhost:5002/myCache"
      />
  
+ <cacheManagerPeerListenerFactory
+     class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
+     properties="hostName=localhost, port=5001"
+     />
  
  <cache name="myCache"
         maxEntriesLocalHeap="0">
  </cache>
</ehcache>
- 
<cacheManagerPeerListenerFactory>タグで定義する。
- 
classには、組み込みクラスであるRMICacheManagerPeerListenerFactoryを指定する。
- 
propertiesで、連携の入り口を定義する。
Event Listener を登録する
キャッシュイベントを監視して、変更があればレプリケーション相手に通知するためのリスナーを登録する。
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
      
  <cacheManagerPeerProviderFactory
      class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
      properties="peerDiscovery=manual,
                  rmiUrls=//localhost:5002/myCache"
      />
  
  <cacheManagerPeerListenerFactory
      class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
      properties="hostName=localhost, port=5001"
      />
  
  <cache name="myCache"
         maxEntriesLocalHeap="0">
+   <cacheEventListenerFactory
+       class="net.sf.ehcache.distribution.RMICacheReplicatorFactory" />
  </cache>
</ehcache>
- 
<cache>タグの中に、<cacheEventListenerFactory>タグで登録する。
- 
classには、組み込みクラスであるRMICacheReplicatorFactoryを指定する。
動作確認
以上の設定をするだけで、レプリケーションは可能になる。
試しに簡単なプログラムを作ったので、それで動作確認をする。
上記ページから ehrep.jar をダウンロードする。
実行する
コマンドラインを2つ立ち上げて、それぞれで下記のように jar を実行する。
$ java -jar ehrep.jar sub1
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
> 1000 ms...
> 1000 ms...
> 1000 ms...
$ java -jar ehrep.jar sub2
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
> 1000 ms...
> 1000 ms...
> 1000 ms...
sub1 または sub2 を引数に渡して実行すると、 1 秒ごとにキャッシュにアクセスして、レプリケーションが実行されているか確認を始める。
次に、もう1つコマンドラインを立ち上げて、下記のように jar を実行する。
$ java -jar ehrep.jar main "Hello Replication!!"
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
[main] put msg("Hello Replication!!") into cache
第1引数に main を渡し、第2引数に文字列を渡す。
第2引数で指定した文字列は、 msg というキーでキャッシュに保存される。
すると、先に立ち上げていた2つのプログラムが、以下のように変化する。
...
> 1000 ms...
> 1000 ms...
[sub1] msg : Hello Replication!!
...
> 1000 ms...
> 1000 ms...
[sub2] msg : Hello Replication!!
main の方でキャッシュに保存した情報が sub1 と sub2 にレプリケーションされ、キャッシュから取得したメッセージが出力されている。
具体的な実装は こちら を参照。
設定ファイルは こっち 。
キャッシュを動的に作成する
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.config.CacheConfiguration;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            manager.addCache("dynamicCache");
            
            Cache cache = manager.getCache("dynamicCache");
            CacheConfiguration config = cache.getCacheConfiguration();
            
            System.out.println(
                "MaxElementsInMemory = " + config.getMaxElementsInMemory() + "\n" +
                "TimeToIdleSeconds = " + config.getTimeToIdleSeconds() + "\n" +
                "MaxElementsOnDisk = " + config.getMaxElementsOnDisk()
            );
            
        } finally {
            manager.shutdown();
        }
    }
}
MaxElementsInMemory = 10000
TimeToIdleSeconds = 120
MaxElementsOnDisk = 10000000
- 
CacheManager#addCache(String)メソッドで、キャッシュを動的に追加することができる。
- 自作の ehcache.xmlがクラスパス上に存在しない場合、この方法で作成したキャッシュの設定値には、ehcache-x.x.x.jarの中に入っているehcache-failsafe.xmlが使用される。
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
    <diskStore path="java.io.tmpdir"/>
    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            maxElementsOnDisk="10000000"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU">
        <persistence strategy="localTempSwap"/>
    </defaultCache>
</ehcache>
- この <defaultCache>の設定が適用されている。
動的に作成したキャッシュのデフォルト値を指定する
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
  
  <defaultCache maxEntriesLocalHeap="30">
  </defaultCache>
</ehcache>
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.config.CacheConfiguration;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            manager.addCache("dynamicCache");
            
            Cache cache = manager.getCache("dynamicCache");
            CacheConfiguration config = cache.getCacheConfiguration();
            
            System.out.println("MaxEntriesLocalHeap = " + config.getMaxEntriesLocalHeap());
            
        } finally {
            manager.shutdown();
        }
    }
}
MaxEntriesLocalHeap = 30
- 
ehcache.xmlを作成して、<defaultCache>タグを記述すれば、デフォルト値を設定できる。
動的に設定値を指定する
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.config.CacheConfiguration;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            CacheConfiguration config = new CacheConfiguration();
            config.setName("dynamicCache");
            config.setMaxEntriesLocalHeap(12);
            
            manager.addCache(new Cache(config));
            
            
            Cache cache = manager.getCache("dynamicCache");
            CacheConfiguration conf = cache.getCacheConfiguration();
            
            System.out.println("MaxEntriesLocalHeap = " + conf.getMaxEntriesLocalHeap());
            
        } finally {
            manager.shutdown();
        }
    }
}
MaxEntriesLocalHeap = 12
- 
CacheConfigurationのインスタンスを作成することで、設定値を動的に指定できる。
- 
CacheConfigurationインスタンスは、Cacheインスタンスを作成するときのコンストラクタ引数に渡す。
トランザクション
Ehcache には、トランザクションのサポートがついている。
グローバルトランザクションにも対応している。
トランザクションの分離レベルは、 READ COMMITTED。
ローカルトランザクション
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
  
  <cache name="myCache"
         maxEntriesLocalHeap="0"
         transactionalMode="local">
  </cache>
  
</ehcache>
package sample.ehcache;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import net.sf.ehcache.TransactionController;
public class Main {
    
    public static void main(String[] args) {
        CacheManager manager = CacheManager.getInstance();
        
        try {
            Cache cache = manager.getCache("myCache");
            
            withTransaction(() -> {
                put(cache, "hoge", "HOGE");
            });
            
            withTransaction(() -> {
                print(cache, "hoge");
            });
            withTransaction(() -> {
                put(cache, "fuga", "FUGA");
                throw new RuntimeException("test");
            });
            
            withTransaction(() -> {
                print(cache, "fuga");
            });
        } finally {
            manager.shutdown();
        }
    }
    
    private static void withTransaction(Runnable func) {
        TransactionController tx = CacheManager.getInstance().getTransactionController();
        try {
            tx.begin();
            
            func.run();
            
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        }
    }
    private static void put(Cache cache, String key, Object value) {
        cache.put(new Element(key, value));
    }
    private static void print(Cache cache, String key) {
        Element e = cache.get(key);
        System.out.println(key + " : " + (e == null ? "<none>" : e.getObjectValue()));
    }
}
hoge : HOGE
fuga : <none>
- 自前でトランザクションを制御する場合は、 <cache>のtransactionalMode属性でlocalを指定する。
- トランザクションの制御は、 TransactionControllerを使用する。
- 
TransactionControllerのインスタンスあ、CacheManager#getTransactionController()で取得できる。
- 
TransactionController#begin()でトランザクションを開始する。
- 
TransactionController#commit()でコミット、rollback()でロールバックできる。
- トランザクションが有効の場合、キャッシュから値を取得する場合もトランザクションが開始されている必要がある。
- トランザクションが開始されていない状態でキャッシュから値を取得しようとすると、 TransactionExceptionがスローされる。
 
- トランザクションが開始されていない状態でキャッシュから値を取得しようとすると、 
グローバルトランザクション
Ehcache はグローバルトランザクション(XA)をサポートしている。
また、特定の Java EE サーバー上で使用すれば、勝手に JNDI で TransactionManager を取得して、コンテナ管理のトランザクションに参加してくれる。
サポートしているサーバーは、 こちらのページ に記載されている。
GlassFish とか JBoss とか Weblogic とか、主要なサーバーはだいたいサポートされている。
実装
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
  
  <cache name="myCache"
         maxEntriesLocalHeap="0"
         transactionalMode="xa_strict">
  </cache>
  
</ehcache>
- 
transactionalModeにxa_strictを指定する。
package sample.ehcache.jta;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@Path("/sample")
@RequestScoped
public class SampleResource {
    
    @Inject
    private SampleEjb ejb;
    
    @GET
    public String method() {
        this.ejb.withCommit("hoge", "HOGE");
        try {
            this.ejb.withRollback("fuga", "FUGA");
        } catch (Exception e) {/*ignore*/}
        
        return this.ejb.get("hoge") + "\n" + this.ejb.get("fuga");
    }
}
- JAX-RS で入り口を作る。
- 
/api/sampleに GET でアクセスしたら、SampleResource#method()が実行されるようにしている。
package sample.ehcache.jta;
import javax.ejb.Stateless;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
@Stateless
public class SampleEjb {
    
    private Cache cache = CacheManager.getInstance().getCache("myCache");
    
    public void withCommit(String key, String value) {
        this.put(key, value);
    }
    
    public void withRollback(String key, String value) {
        this.put(key, value);
        throw new RuntimeException("test");
    }
    private void put(String key, Object value) {
        this.cache.put(new Element(key, value));
    }
    public void print(String key) {
        Element e = this.cache.get(key);
        System.out.println(key + " : " + (e == null ? "<none>" : e.getObjectValue()));
    }
}
- 
withCommit()は単純にキャッシュに値を保存するだけ。
- 
withRollback()は、キャッシュに値を保存した後にRuntimeExceptionをスローする。
実行結果
war に固めて、 GlassFish にデプロイする。
コンテキストパスは ehcache-jta になるようにデプロイする。
$ curl http://localhost:8080/ehcache-jta/api/sample
hoge : HOGE
fuga : <none>
RuntimeException をスローした方はロールバックされており、 EJB の宣言的トランザクションが効いているのが分かる。
参考
- Ehcache
- Ehcache | オープンソース | IT用語辞典 | 日立ソリューションズ
- Javaで使えるオープンソース・キャッシュライブラリ - CLOVER
- JCACHEの仕様が完成
- untitled - Ehcache.pdf
- 20140325_Blog001_BigMemory
- Terracotta | 株式会社ユニリタ - しなやかなITでお客様のビジネスとワークスタイルの変革を応援します
- キャッシュアルゴリズム - Wikipedia
- FIFO - Wikipedia
- Ehcache - Wikipedia
