SpringCacheを試してみた
SpringBootではSpringCacheというものがあり、内部的にはCaffineが利用されているということで確認してみました。baeldungの説明を参考に進めています。
キャッシュの設定は以下のようになります。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.