SpringCacheを試してみた

SpringBootではSpringCacheというものがあり、内部的にはCaffineが利用されているということで確認してみました。baeldungの説明を参考に進めています。

www.baeldung.com

github.com

キャッシュの設定は以下のようになります。CaffeineのBean定義で上限サイズやキャッシュの保有期間を設定します。次にCacheManagerのBean定義でCaffeineをセットしています。caffeineCacheManager.getCache(キャッシュ名)を呼び出すことでキャッシュ名毎のキャッシュを取得できまして、これはCacheManagerのBean定義で事前に呼び出す必要はなく、CacheManager をDIしたあと任意のキャッシュ名で取得できます。

@Bean
public Caffeine caffeineConfig1() {
    return Caffeine.newBuilder()
            .expireAfterWrite(60, TimeUnit.SECONDS)
            .maximumSize(20);
}

@Bean
public CacheManager cacheManager1( Caffeine caffeine) {
    CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
    caffeineCacheManager.getCache("addresses");
    caffeineCacheManager.setCaffeine(caffeine);
    return caffeineCacheManager;
}

実際に利用するのは以下の部分になります。@Cacheableを付けた関数はキャッシュ対象で、キャッシュにない場合は関数を実行し、キャッシュにあればそれを返します。また、cacheManager.getCache(キャッシュ名).get(キー)で直接キャッシュを取得することができ、直接セットする場合はcacheManager.getCache(キャッシュ名).put(キー, 値);になります。

@Service
public class AddressService
{
    private final static Logger LOG = LoggerFactory.getLogger(AddressService.class);

    @Autowired
    private CacheManager cacheManager;

    @Cacheable(cacheNames = "addresses")
    public String getAddress(long customerId)  {
        LOG.info("Method getAddress is invoked for customer {}", customerId);

        return "123 Main St";
    }

    public String getAddress2(long customerId) {
        if(cacheManager.getCache("addresses2").get(customerId) != null) {
            return cacheManager.getCache("addresses2").get(customerId).get().toString();
        }

        LOG.info("Method getAddress2 is invoked for customer {}", customerId);

        String address = "123 Main St";

        cacheManager.getCache("addresses2").put(customerId, address);

        return address;
    }
}

次に性能測定をしたいと思うのですが、比較対象としてLinkedHashMapを拡張しサイズ上限があるものを使おうと思います。 まずシングルスレッドで使うものとして以下を用意し

public class CapacityLinkedHashMap<K,V> extends LinkedHashMap<K,V> {
    int capacity;
    public CapacityLinkedHashMap(int capacity) {
        super(capacity);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return this.size() > capacity;
    }
}

次にマルチスレッドとしてBlockするようにしたものを作っておきます。

public class CapacityBlockingLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
    int capacity;

    public CapacityBlockingLinkedHashMap(int capacity) {
        super(capacity);
        this.capacity = capacity;
    }

    @Override
    public V put(K key, V value) {
        synchronized (this) {
            return super.put(key, value);
        }
    }

    @Override
    public V get(Object key) {
        synchronized (this) {
            return super.get(key);
        }
    }

    @Override
    public V remove(Object key) {
        synchronized (this) {
            return super.remove(key);
        }
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return this.size() > capacity;
    }
}

まず、性能測定用に以下のコントローラーを使います。

    @GetMapping("/performance")
    public ResponseEntity<String> perform() {
        String ret = "\n";
        for(int loop: Arrays.asList(1, 100, 10000, 1000000)) {
            ret += String.format("%25s[%d]: %dns\n", "SpringCache", loop, performCache(loop));
            ret += String.format("%25s[%d]: %dns\n", "LinkedHashMap", loop, performLinkedHashMap(loop));
            ret += String.format("%25s[%d]: %dns\n", "BlockingLinkedHashMap", loop, performBlockingLinkedHashMap(loop));
        };
        return ResponseEntity.ok(ret);
    }

    private long performCache(int loop) {
        int i = 0;
        long start = System.nanoTime();
        while(i < loop) {
            addressService.put(i, i);
            addressService.get(i);
            i++;
        }
        long end = System.nanoTime();
        return end - start;
    }

    private long performLinkedHashMap(int loop) {
        int i = 0;
        LinkedHashMap<Integer, Integer> linkedHashMap = new CapacityLinkedHashMap<>(20);
        long start = System.nanoTime();
        while(i < loop) {
            linkedHashMap.put(i, i);
            linkedHashMap.get(i);
            i++;
        }
        long end = System.nanoTime();
        return end - start;
    }

    private long performBlockingLinkedHashMap(int loop) {
        int i = 0;
        LinkedHashMap<Integer, Integer> linkedHashMap = new CapacityBlockingLinkedHashMap<>(20);
        long start = System.nanoTime();
        while(i < loop) {
            linkedHashMap.put(i, i);
            linkedHashMap.get(i);
            i++;
        }
        long end = System.nanoTime();
        return end - start;
    }

結果は以下のようになり、LinkedHashMapを拡張したものの方が早くはありますがSpringCacheでも十分早いことが分かります。

$ curl http://localhost:8080/performance
              SpringCache[1]: 110900ns
            LinkedHashMap[1]: 8500ns
    BlockingLinkedHashMap[1]: 8100ns
              SpringCache[100]: 73800ns
            LinkedHashMap[100]: 8800ns
    BlockingLinkedHashMap[100]: 5800ns
              SpringCache[10000]: 4647700ns
            LinkedHashMap[10000]: 336700ns
    BlockingLinkedHashMap[10000]: 344500ns
              SpringCache[1000000]: 255159800ns
            LinkedHashMap[1000000]: 35521100ns
    BlockingLinkedHashMap[1000000]: 42388200ns

次に複数スレッドのアクセスとして以下のコントローラーを用意します。

    @GetMapping("/async")
    public ResponseEntity<String> async() throws InterruptedException {
        String ret = "\n";
        for(int loop: Arrays.asList(1, 100, 10000, 1000000)) {
            ret += String.format("%25s[%d]: %dns\n", "SpringCache", loop, asyncCache(loop));
            ret += String.format("%25s[%d]: %dns\n", "LinkedHashMap", loop, asyncBlockingLinkedHashMap(loop));
        };
        return ResponseEntity.ok(ret);
    }

    private long asyncCache(int loop) throws InterruptedException {
        int i = 0;
        ExecutorService exec = Executors.newFixedThreadPool(10);
        long start = System.nanoTime();

        while(i < loop) {
            exec.execute(() -> {
                addressService.put(1, 1);
                addressService.get(1);
            });
            i++;
        }
        exec.shutdown();
        exec.awaitTermination(10, TimeUnit.SECONDS);
        long end = System.nanoTime();
        return end - start;
    }

    private long asyncBlockingLinkedHashMap(int loop) throws InterruptedException {
        int i = 0;
        LinkedHashMap<Integer, Integer> linkedHashMap = new CapacityBlockingLinkedHashMap<>(20);
        ExecutorService exec = Executors.newFixedThreadPool(10);
        long start = System.nanoTime();
        while(i < loop) {
            exec.execute(() -> {
                linkedHashMap.put(1, 1);
                linkedHashMap.get(1);
            });
            i++;
        }
        exec.shutdown();
        exec.awaitTermination(10, TimeUnit.SECONDS);
        long end = System.nanoTime();
        return end - start;
    }

結果は以下のようになりました。今回は10スレッドで指定しましたが以下のようにブロックありのLinkedHashMapと差が小さくなっていることが分かります。コア数、スレッド数が大きくなってくるとSpringCacheの方が早くなるのかもしれないです。

$ curl http://localhost:8080/async
              SpringCache[1]: 865600ns
            LinkedHashMap[1]: 669200ns
              SpringCache[100]: 4797300ns
            LinkedHashMap[100]: 4075800ns
              SpringCache[10000]: 24793800ns
            LinkedHashMap[10000]: 16038600ns
              SpringCache[1000000]: 745497500ns
            LinkedHashMap[1000000]: 305231600ns

今回確認した限りだとシングルスレッドでレイテンシーを気にするのであればLinkedHashMapを拡張したものを使うので良さそうに思いました。 ただSpringCacheは以下のページにあるように機能が豊富なのでレイテンシーが重要な場面でない場合はSpringCacheを優先して使うで良さそうに思いました。

Automatic loading of entries into the cache, optionally asynchronously.
Size-based eviction when a maximum is exceeded, based on frequency and recency.
Time-based expiration of entries, measured since last access or last write.
Asynchronous refreshing when the first stale request for an entry occurs.
Keys automatically wrap in weak references.
Values automatically wrap in weak or soft references.
Notification of evicted (or otherwise removed) entries.
Writes propagated to an external resource.
Accumulation of cache access statistics.

blog.coditas.com