ScalaCache + Caffeine + Cats Effectでインメモリキャッシュを実装する

ScalaのキャッシュライブラリScalaCacheを試してみました。 ScalaCacheを使うと既存のコードを大きく変更せずにキャッシュ機能を追加することができます。また必要に応じてキャッシュの実装も切り替えできます。
2019.10.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

Scalaでインメモリキャッシュを実装する必要があってのでScalaCacheというライブラリを試してみました。

ScalaCacheとは

ScalaCacheはいくつかのキャッシュライブラリのファサードとなるライブラリです。 次のような特徴があります。

  • Scalaでの利用に適したシンプルなAPIを提供している
  • 少ない手間でScalaのアプリケーションにキャッシュを導入できる

A facade for the most popular cache implementations, with a simple, idiomatic Scala API. Use ScalaCache to add caching to any Scala app with the minimum of fuss.

キャッシュの実装としては以下を利用できます。

  • Google Guava
  • Memcached
  • Ehcache
  • Redis
  • Caffeine
  • cache2k
  • OHC

使い方

Getting Startedにもありますが下記のように使います。

import scalacache._
  import scalacache.caffeine.CaffeineCache
  import cats.implicits._

  final case class Cat(id: Int, name: String, colour: String)

  object Cat {
    val duchess:Cat = Cat(1, "Duchess", "white")
    val thomas:Cat = Cat(2, "Thomas", "orange")
    val marie:Cat = Cat(3, "Marie", "white")
  }

  //キャッシュインスタンス生成: 実装はCaffeine
  implicit val cache:Cache[Cat] = CaffeineCache[Cat]

  //モードの指定(Try)
  import scalacache.modes.try_._

  //エントリを追加
  put(Cat.duchess.id)(Cat.duchess)
  //参照
  get(Cat.duchess.id) //Success(Some(Cat(1,Duchess,white)))
  //キャッシュミス
  get(Cat.thomas.id) //Success(None)

  //削除
  remove(Cat.duchess.id) //Success(())
  remove(Cat.marie.id) //Success(())

  //キャッシュの更新処理を指定
  cachingF(Cat.thomas.id) (ttl = None)(Try {
    println(s"where is the cat(id=${Cat.thomas.id}) ??")
    Cat.thomas
  })
  //where is the cat(id=2) ??
  //Success(Cat(2,Thomas,orange))

以下ポイントをいくつか説明します。

キャッシュインスタンスの生成と、モードの指定

使用するキャッシュの実装と、コンテナの型に合わせたmodeをimplicitで定義しておきます。 ここでは実装としてJavaのインメモリキャッシュの実装であるCaffeineを選択し、コンテナ型はTryを指定するのでscalacache.modes.try_._ をインポートしています。

  //キャッシュインスタンス生成: 実装はCaffeine
  implicit val cache:Cache[Cat] = CaffeineCache[Cat]

  //モードの指定(Try)
  import scalacache.modes.try_._

キャッシュの実装の詳細

キャッシュの実装にどのようなものがあるかや初期化時の詳細についてはCache Implementationsに詳しいです。 (Caffeineでは詳細の指定はスキップできますが、例えばRedisを使う実装ではRedisサーバーのホスト名とポートを指定する必要があります。)

mode(コンテナ型の指定)

ScalaCacheではキャッシュへの操作を任意の副作用コンテナでラップできます。 例えばTry, Future, cats.effect.IO などです。 これによりキャッシュレイヤのAPIの型をプロジェクトで使っている型に柔軟に合わせることができます。 コンテナ型はキャッシュ操作関数のimplicit parameter modeで指定します。

キャッシュの操作

キャッシュの操作はscalacacheパッケージに定義された関数で行います。 これらの関数には前述のキャッシュインスタンスとモードをimplicit parameterとして指定します。

  //エントリを追加
  put(Cat.duchess.id)(Cat.duchess)
  //参照
  get(Cat.duchess.id) //Success(Some(Cat(1,Duchess,white)))
  //キャッシュミス
  get(Cat.thomas.id) //Success(None)

cachingF

cachingFによってキャッシュの更新処理を指定することができます。 更新処理はモードで指定したコンテナ型のものを指定します。 これによって既存のコードでキャッシュなしで値の参照をしていたコードを、cachingFに置き換えるだけでキャッシュ機能を追加することができます。

  //猫ちゃんを探す骨の折れる仕事
  def getCat(id:Int): Try[Cat] = ???

  //before(キャッシュなし)
  getCat(Cat.thomas.id)

  //after(キャッシュあり)
  cachingF(Cat.thomas.id) (ttl = None)(getCat(Cat.thomas.id))

Cats Effectと組み合わせて使う例

さてここで、Cats Effectを使って実装されているデータベースや外部APIの呼び出し処理を再利用する形でキャッシュを使ったサンプルを作ってみます。 この例ではcachingFの第2引数のttlを指定して、キャッシュが一定時間経過後に無効になることを確認しています。 先ほどまでのTryの例とキャッシュの使い方はほとんど変わっていないことがわかると思います。

package example

import cats.effect.{ExitCode, IO, IOApp}
import cats.implicits._
import scalacache._
import scalacache.caffeine._

import scala.concurrent.duration._

object IOExample extends IOApp {

  // キャッシュのエントリの型
  type E = String

  // キャッシュの実装
  implicit val cache: Cache[E] = CaffeineCache[E]
  // IO用のモード
  implicit val mode: Mode[IO] = scalacache.CatsEffect.modes.async

 // キャッシュのTTL
  val ttl = 100.millis

  // データソース(API, DB)から値を参照する既存のコンポーネント
  def fetch(key: String): IO[E] = IO(println(s"cache missed for ${key}")) *> IO.pure(s"value-${key}")

 // キャッシュから値を参照する、キャッシュに存在しなければデータソースを参照
  def find(key:String): IO[E] = cachingF(key)(ttl.some)(fetch(key))


  override def run(args: List[String]): IO[ExitCode] = for {
    _ <- find("k1")
    _ <- find("k2")
    _ <- find("k1")
    _ <- find("k2")
    _ <- timer.sleep(ttl) *> IO(println("sleep")) //キャッシュが無効になるまで待つ
    _ <- find("k1")
    _ <- find("k2")
    _ <- find("k1")
    _ <- find("k2")
  } yield ExitCode.Success
}

出力

cache missed for k1
cache missed for k2
sleep
cache missed for k1
cache missed for k2

まとめ

ScalaのキャッシュライブラリScalaCacheを試してみました。 ScalaCacheを使うと既存のコードを大きく変更せずにキャッシュ機能を追加することができます。また必要に応じてキャッシュの実装も切り替えできます。

おまけ

scalacache-cats-effectには依存ライブラリとしてcats-effectが含まれています。プロジェクトで使っているバージョンと一致しない場合には下記のように除外した方がいいです。

libraryDependencies += "com.github.cb372" %% "scalacache-cats-effect" % scalaCacheVersion excludeAll(ExclusionRule(organization="org.typelevel"))