PlayのCache APIでMemcachedやRedisの代わりにRDBを使う

Play frameworkはキャッシュのためのAPIplay.api.cahe というパッケージで提供しています。play.api.cache はそのバックエンドとして、CaffineやEhcacheが選択でき、またサードバーティのライブラリを使えばMemcached, Redisなどをバックエンドにすることができます。キャッシュと言うと高速化のためのものですが、Playのキャッシュは高速化の他に、単なるデータの一時保存のため、つまり単なるKey-Value Storeとして使われていたりもします。

Caffeineを使うとインメモリのキャッシュが簡単に実現できますが、アプリケーションサーバーのインスタンスが複数ある場合、各サーバーでキャッシュした内容を共有することができません。MemcachedやRedisを使えばインスタンス間でキャッシュした内容を共有できますが、MemcachedやRedisを管理する手間が発生します。

PlayのキャッシュAPIは使いたいけれど、MemcachedやRedisなどの新たなミドルウェアを導入したくない。という状況で、ならばRDBをバックエンドにしてしまおうと作ったのがdbcacheというライブラリです。実際のところ、MemcachedやRedisを使わなくてもRDBで中小規模のサービスでは事足りてしまいます。

https://github.com/tototoshi/dbcache

設定

テーブル追加

MySQLをバックエンドとして使う場合の例です。まずデータベースにcache_entriesテーブルを作成します。

CREATE TABLE `cache_entries` (
  `cache_key` varchar(191) PRIMARY KEY,
  `cache_value` mediumblob NOT NULL,
  `expired_at` datetime,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  INDEX (`expired_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

Play2.7.xの場合

Play 2.7.xを使っている場合は dbcache-mysqldbcache-play をbuild.sbtで追加します。

libraryDependencies ++= Seq(
  "com.github.tototoshi" %% "dbcache-mysql" % "0.3.0",
  "com.github.tototoshi" %% "dbcache-play" % "0.3.0"
)

Play2.6.xの場合

dbcache-play は最近追加したモジュールなので、Play 2.6.xには対応していません。Play2.6.xを使っている場合は dbcache-mysql だけを追加し、以下のコードをアプリケーションコードに追加します。

libraryDependencies ++= Seq(
  "com.github.tototoshi" %% "dbcache-mysql" % "0.2.0"
)
// Create adapter for play.api.cache
class DBCacheApi(myCache: DBCache) extends CacheApi {
  def set(key: String, value: Any, expiration: Duration): Unit =
    myCache.set(key, value, expiration)
  def get[A](key: String)(implicit ct: ClassTag[A]): Option[A] =
    myCache.get[A](key)
  def getOrElse[A: ClassTag](key: String, expiration: Duration)(orElse: => A) =
    myCache.getOrElse(key, expiration)(orElse)
  def remove(key: String) = myCache.remove(key)
}

DIの設定

あとはProviderを作ってDIしてあげるだけです。Scalikejdbcを使っている場合はこんな感じになります。

import java.sql.Connection

import com.github.tototoshi.dbcache.ConnectionFactory
import com.github.tototoshi.dbcache.mysql.MySQLCache
import com.github.tototoshi.dbcache.play.DBCacheApi
import javax.inject.{Inject, Provider}
import play.api.Environment
import play.api.cache.SyncCacheApi
import scalikejdbc.DB

class MySQLCacheApiProvider @Inject()(environment: Environment) extends Provider[SyncCacheApi] {

  private val connectionFactory = new ConnectionFactory {
    override def get(): Connection = {
      DB.autoCommitSession().connection
    }
  }
  override def get(): SyncCacheApi = {
    val mysqlCache = new MySQLCache(connectionFactory, environment.classLoader)
    new DBCacheApi(mysqlCache)
  }
}

EhCacheモジュールは無効にしておきます。

play.modules.disabled += "play.api.cache.ehcache.EhCacheModule"

あとは普通にPlayのキャッシュAPIを使えばデータベースに保存されるようになります。

運用について

期限切れのキャッシュデータがDBに残るのでバッチか何か動かしてたまに掃除してあげてください。

DELETE FROM cache_entries where expired_at < ${n日前}