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
トピックス
なし
スター履歴
データ取得日時: 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
| 特徴 | Caffeine | Guava Cache |
|---|---|---|
| パフォーマンス | ◎ | ○ |
| メモリ効率 | ◎ | ○ |
| アルゴリズム | Window TinyLFU | LRU |
| Scala統合 | ○ | △ |
まとめ
CaffeineはScalaアプリケーションでも優れたパフォーマンスを発揮する高性能キャッシュライブラリです。Scaffeineラッパーを使用することで、Scalaらしい慣用的なAPIで利用でき、Future/Promiseベースの非同期処理とも良好に統合できます。ScalaCacheと組み合わせることで、さらに高度なキャッシング戦略を実装することも可能です。