caffeine

Scalaから使用可能な高性能キャッシュライブラリ。ScaffeineラッパーによりScalaらしいAPIで利用でき、優れたヒット率と効率的なメモリ使用を実現。

GitHub概要

ben-manes/caffeine

A high performance caching library for Java

スター17,137
ウォッチ363
フォーク1,666
作成日:2014年12月13日
言語:Java
ライセンス:Apache License 2.0

トピックス

なし

スター履歴

ben-manes/caffeine Star History
データ取得日時: 2025/10/22 09:55

概要

CaffeineはJava向けの高性能キャッシュライブラリですが、Scalaからも利用可能です。Window TinyLFUアルゴリズムによる優れたヒット率、効率的なメモリ使用、非同期リフレッシュなどの機能を提供します。ScalaではScaffeineラッパーを使用することで、より慣用的なAPIで利用できます。

主な特徴

  • 高いヒット率: Window TinyLFUアルゴリズムによる効率的なキャッシュ管理
  • 柔軟な削除ポリシー: サイズベース、時間ベース、参照ベースの削除
  • 非同期サポート: Future/Promiseベースの非同期キャッシュ操作
  • 統計情報: 詳細なキャッシュ統計とモニタリング
  • Scala向けラッパー: Scaffeineによる慣用的なScala API

インストール

sbt - 直接使用

// build.sbt
libraryDependencies += "com.github.ben-manes.caffeine" % "caffeine" % "3.1.8"

sbt - Scaffeine(推奨)

// build.sbt
libraryDependencies += "com.github.blemale" %% "scaffeine" % "5.2.1"

sbt - ScalaCacheとの統合

// build.sbt
libraryDependencies ++= Seq(
  "com.github.cb372" %% "scalacache-caffeine" % "1.0.0-M6",
  "com.github.cb372" %% "scalacache-cats-effect" % "1.0.0-M6" // cats-effect統合
)

基本的な使い方

Caffeineの直接使用(Java風API)

import com.github.benmanes.caffeine.cache.{Caffeine, Cache}
import java.util.concurrent.TimeUnit

// 基本的なキャッシュの作成
val cache: Cache[String, String] = Caffeine.newBuilder()
  .maximumSize(10000)
  .expireAfterWrite(5, TimeUnit.MINUTES)
  .build[String, String]()

// データの操作
cache.put("key1", "value1")
val value = cache.getIfPresent("key1")
println(s"Value: $value") // Some(value1)

// 存在しない場合の処理
val computedValue = cache.get("key2", key => s"computed_$key")
println(s"Computed: $computedValue") // computed_key2

Scaffeineの使用(Scala風API)

import com.github.blemale.scaffeine.{Scaffeine, Cache}
import scala.concurrent.duration._

// Scalaらしいビルダー構文
val cache: Cache[Int, String] = 
  Scaffeine()
    .recordStats()
    .expireAfterWrite(1.hour)
    .maximumSize(500)
    .build[Int, String]()

// Option型での取得
cache.put(1, "one")
val maybeValue: Option[String] = cache.getIfPresent(1)
println(maybeValue) // Some("one")

// 関数を使った計算
val value = cache.get(2, i => s"number_$i")
println(value) // "number_2"

// 統計情報の取得
val stats = cache.stats()
println(s"Hit rate: ${stats.hitRate()}")

高度な使い方

LoadingCacheの実装

import com.github.blemale.scaffeine.{Scaffeine, LoadingCache}
import scala.concurrent.duration._

// データベースアクセスのシミュレーション
def fetchFromDatabase(id: Int): String = {
  println(s"Fetching user $id from database...")
  Thread.sleep(100) // 重い処理のシミュレーション
  s"User_$id"
}

// LoadingCacheの作成
val userCache: LoadingCache[Int, String] = 
  Scaffeine()
    .recordStats()
    .expireAfterWrite(10.minutes)
    .maximumSize(1000)
    .build(fetchFromDatabase)

// 使用例
val user1 = userCache.get(1) // DBから取得
val user1Again = userCache.get(1) // キャッシュから取得

// 複数のキーを一度に取得
val users = userCache.getAll(List(1, 2, 3))
println(users) // Map(1 -> User_1, 2 -> User_2, 3 -> User_3)

非同期キャッシュ(AsyncLoadingCache)

import com.github.blemale.scaffeine.{Scaffeine, AsyncLoadingCache}
import scala.concurrent.{Future, ExecutionContext}
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global

// 非同期データ取得
def fetchUserAsync(id: Long): Future[User] = Future {
  // APIコールのシミュレーション
  Thread.sleep(50)
  User(id, s"User_$id", s"[email protected]")
}

case class User(id: Long, name: String, email: String)

// 非同期LoadingCacheの作成
val asyncUserCache: AsyncLoadingCache[Long, User] = 
  Scaffeine()
    .recordStats()
    .expireAfterWrite(5.minutes)
    .maximumSize(10000)
    .buildAsyncFuture(fetchUserAsync)

// 使用例
val userFuture: Future[User] = asyncUserCache.get(123)
userFuture.foreach(user => println(s"Got user: $user"))

// 複数の非同期取得
val usersFuture: Future[Map[Long, User]] = 
  asyncUserCache.getAll(List(1, 2, 3))

カスタム削除ポリシー

import com.github.blemale.scaffeine.Scaffeine
import scala.concurrent.duration._

// 重み付けキャッシュ
case class CachedData(id: String, data: Array[Byte], priority: Int)

val weightedCache = Scaffeine()
  .weigher[String, CachedData]((key, value) => value.data.length)
  .maximumWeight(10 * 1024 * 1024) // 10MB
  .expireAfterAccess(1.hour)
  .removalListener[String, CachedData]((key, value, cause) => {
    println(s"Removed $key due to $cause")
  })
  .build[String, CachedData]()

// 可変的な期限切れ
val variableExpiryCache = Scaffeine()
  .expireAfter[String, CachedData](
    create = (key, value) => value.priority match {
      case 1 => 1.hour
      case 2 => 30.minutes
      case _ => 10.minutes
    },
    update = (key, value, currentDuration) => currentDuration,
    read = (key, value, currentDuration) => currentDuration
  )
  .maximumSize(1000)
  .build[String, CachedData]()

実践的な例

ScalaCacheとの統合

import scalacache._
import scalacache.caffeine._
import scalacache.memoization._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global

// Caffeineバックエンドの設定
implicit val caffeineCache: Cache[String] = CaffeineCache[String]

// メモ化の例
def expensiveOperation(param: Int): String = memoize(60.seconds) {
  println(s"Computing for $param...")
  Thread.sleep(1000)
  s"Result for $param"
}

// 条件付きキャッシング
def getUserData(userId: Long): Future[Option[User]] = cachingF(s"user:$userId")(ttl = Some(5.minutes)) {
  // データベースからユーザーを取得
  Future {
    println(s"Fetching user $userId from DB")
    Some(User(userId, s"User $userId", s"[email protected]"))
  }
}

Akka Actorでの使用

import akka.actor.{Actor, ActorLogging, Props}
import com.github.blemale.scaffeine.{Scaffeine, Cache}
import scala.concurrent.duration._

class CacheActor extends Actor with ActorLogging {
  
  private val cache: Cache[String, Any] = Scaffeine()
    .recordStats()
    .expireAfterWrite(10.minutes)
    .maximumSize(10000)
    .build[String, Any]()

  def receive: Receive = {
    case Get(key) =>
      val value = cache.getIfPresent(key)
      sender() ! CacheResponse(key, value)
      
    case Put(key, value) =>
      cache.put(key, value)
      sender() ! Ack
      
    case Delete(key) =>
      cache.invalidate(key)
      sender() ! Ack
      
    case GetStats =>
      val stats = cache.stats()
      sender() ! CacheStats(
        hitRate = stats.hitRate(),
        missCount = stats.missCount(),
        evictionCount = stats.evictionCount()
      )
  }
}

// メッセージ定義
case class Get(key: String)
case class Put(key: String, value: Any)
case class Delete(key: String)
case object GetStats
case object Ack
case class CacheResponse(key: String, value: Option[Any])
case class CacheStats(hitRate: Double, missCount: Long, evictionCount: Long)

object CacheActor {
  def props(): Props = Props(new CacheActor)
}

Play Frameworkでの使用

import javax.inject._
import play.api.cache.AsyncCacheApi
import com.github.blemale.scaffeine.{Scaffeine, AsyncLoadingCache}
import scala.concurrent.{Future, ExecutionContext}
import scala.concurrent.duration._
import scala.reflect.ClassTag

@Singleton
class CaffeineAsyncCache @Inject()(implicit ec: ExecutionContext) extends AsyncCacheApi {
  
  private val underlying = Scaffeine()
    .recordStats()
    .expireAfterWrite(1.hour)
    .maximumSize(10000)
    .buildAsync[String, Any]()

  def set(key: String, value: Any, expiration: Duration = Duration.Inf): Future[Unit] = {
    Future {
      underlying.put(key, Future.successful(value))
      ()
    }
  }

  def get[T: ClassTag](key: String): Future[Option[T]] = {
    underlying.getIfPresent(key) match {
      case Some(future) => future.map(value => Some(value.asInstanceOf[T]))
      case None => Future.successful(None)
    }
  }

  def remove(key: String): Future[Unit] = {
    Future {
      underlying.invalidate(key)
      ()
    }
  }

  def removeAll(): Future[Unit] = {
    Future {
      underlying.invalidateAll()
      ()
    }
  }
}

関数型プログラミングスタイル

import cats.effect._
import cats.implicits._
import com.github.blemale.scaffeine.{Scaffeine, Cache}
import scala.concurrent.duration._

trait CacheAlgebra[F[_], K, V] {
  def get(key: K): F[Option[V]]
  def put(key: K, value: V): F[Unit]
  def delete(key: K): F[Unit]
  def clear(): F[Unit]
}

class CaffeineCache[F[_]: Sync, K, V](
  maxSize: Long,
  ttl: FiniteDuration
) extends CacheAlgebra[F, K, V] {
  
  private val cache: Cache[K, V] = Scaffeine()
    .expireAfterWrite(ttl)
    .maximumSize(maxSize)
    .build[K, V]()

  def get(key: K): F[Option[V]] = 
    Sync[F].delay(cache.getIfPresent(key))

  def put(key: K, value: V): F[Unit] = 
    Sync[F].delay(cache.put(key, value))

  def delete(key: K): F[Unit] = 
    Sync[F].delay(cache.invalidate(key))

  def clear(): F[Unit] = 
    Sync[F].delay(cache.invalidateAll())
}

// 使用例
object CacheExample extends IOApp {
  def run(args: List[String]): IO[ExitCode] = {
    val cache = new CaffeineCache[IO, String, Int](1000, 5.minutes)
    
    for {
      _ <- cache.put("key1", 42)
      value <- cache.get("key1")
      _ <- IO(println(s"Value: $value"))
    } yield ExitCode.Success
  }
}

パフォーマンスのベストプラクティス

1. 適切な初期容量の設定

val optimizedCache = Scaffeine()
  .initialCapacity(1000) // 予想されるエントリ数に基づいて設定
  .maximumSize(10000)
  .build[String, String]()

2. 統計情報の活用

val monitoredCache = Scaffeine()
  .recordStats()
  .maximumSize(1000)
  .build[String, String]()

// 定期的な監視
import java.util.concurrent.Executors
import scala.concurrent.duration._

val scheduler = Executors.newScheduledThreadPool(1)
scheduler.scheduleAtFixedRate(() => {
  val stats = monitoredCache.stats()
  println(s"""
    |Cache Statistics:
    |  Hit Rate: ${stats.hitRate()}
    |  Miss Count: ${stats.missCount()}
    |  Load Count: ${stats.loadCount()}
    |  Eviction Count: ${stats.evictionCount()}
  """.stripMargin)
}, 0, 1, TimeUnit.MINUTES)

3. バルク操作の活用

val cache = Scaffeine()
  .maximumSize(10000)
  .build[Int, String]()

// 非効率:個別の操作
val inefficient = (1 to 100).map { i =>
  cache.get(i, i => s"Value_$i")
}.toList

// 効率的:バルク操作
val efficient = cache.getAll(1 to 100, keys => 
  keys.map(k => k -> s"Value_$k").toMap
)

他のScalaキャッシュライブラリとの比較

Caffeine vs ScalaCache

特徴Caffeine (Scaffeine)ScalaCache
パフォーマンス
Scala API○ (Scaffeine経由)
キャッシュバックエンドCaffeine専用複数対応
非同期サポート
タグベース削除×

Caffeine vs Guava Cache

特徴CaffeineGuava Cache
パフォーマンス
メモリ効率
アルゴリズムWindow TinyLFULRU
Scala統合

まとめ

CaffeineはScalaアプリケーションでも優れたパフォーマンスを発揮する高性能キャッシュライブラリです。Scaffeineラッパーを使用することで、Scalaらしい慣用的なAPIで利用でき、Future/Promiseベースの非同期処理とも良好に統合できます。ScalaCacheと組み合わせることで、さらに高度なキャッシング戦略を実装することも可能です。