目次


memcached と Grails

第 2 回 memcached を Grails に統合する

Grails アプリケーションでの効率的なキャッシング

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: memcached と Grails

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:memcached と Grails

このシリーズの続きに乞うご期待。

Grails は、Groovy の動的な構文と、バックエンドに Java プラットフォームの威力を利用した Web アプリケーション・フレームワークです。memcached は汎用分散型メモリー・キャッシング・システムで、非常に大量のトラフィックを扱う Web サイトのいくつかでも使用されています。この 2 つを併せて使用することで、応答性とスケーラビリティーに極めて優れた Web アプリケーションを素早く構築することができます。

memcached と Grails を紹介するこの 2 回連載の後半では、memcached を既存の Grails アプリケーションに統合するプロセスを手順に沿って説明します。記事ではまず始めにこの記事で使用する Grails アプリケーションの概要を説明してから、このアプリケーションを開発環境にセットアップします。次に memcached クライアント API を紹介し、この API をラップする Grails サービスを作成していきます。

Grails アプリケーションに memcached を使用するためのコンポーネントがすべて揃ったら、それを最大限に利用する方法を知りたくなるのではないでしょうか。この記事では既存のアプリケーションのどこにキャッシング機能を注入するのが最も有効であるかを説明し、キャッシュに値を保存して、そこから値を取得する具体的な手法を説明します。そして最後に実践的な演習として、アプリケーションの Grails コントローラーの 1 つにキャッシングを実装し、キャッシング結果をテストしてみます。

Grails アプリケーション

memcached の実際の動作を確かめるには、これを動作させるための Grails サンプル・アプリケーションが必要です。アプリケーションはそれほど複雑である必要はありません。そこでこの記事では、ユーザーが一連の連絡先とそれぞれに関連付けられた情報を管理できるようにする単純な連絡先アプリケーションを作成します。このミニ CRUD アプリケーションの唯一の機能は、ユーザーが memcached を使って Contact オブジェクトを保存、取得できるようにすることだけです。まずは、開発環境で以下のコマンドを実行してください。

grails create-app

次にアプリケーション名を入力します。このサンプルには、contactmanager という名前を使用します。Grails がこのアプリケーションを作成したら、cd を実行して contactmanager ディレクトリーに移動し、アプリケーションを起動します。

cd contactmanager
grails run-app

ブラウザーを開いて http://localhost:8080/contactmanager にアクセスすると、すべてが順調に行われていれば Grails のウェルカム・ページが表示されるはずです。

ドメインを作成する

次に必要なのは、Contact ドメイン・クラスの定義です。複雑にならないように、このドメインには firstName、lastName、email という 3 つのプロパティーだけを追加します。プロパティーを追加した後の Contact ドメインはリスト 1 のようになります。

リスト 1. Contact ドメイン
class Contact {

    def String firstName
    def String lastName
    def String email

    static constraints = {
        firstName(maxLength: 50, blank: false)
        lastName(maxLength: 50, blank: false)
        email(blank: false, nullable: false, email:true)
    }
}

続いて、contactmanager/grails-app/domain ディレクトリーに Groovy クラスを作成する作業に移ります。

Controller を作成する

次に作成するのは、Contact を処理するためのアプリケーションである、Controller です。最初に以下のコマンドを実行します。

grails create-controller
Contact

新しく作成した ContactController を開き、リスト 2 のように変更してください。

リスト 2. scaffold 機能を Controller に追加する
class ContactController {

    def scaffold = Contact
}

上記のように変更することによって、デフォルトの Grails scaffold 機能が用意されることになります。今度は何らかのコンテンツを追加するために、BootStrap.groovy (起動時に Grails アプリケーションを初期化するためのファイル) を開いて、リスト 3 のように変更します。データベース初期化、動的構成、Groovy クラスへの大きな変更の注入は、いずれも共通して BootStrap.groovy が実行するタスクです。私はよく、開発データベースをデフォルト状態にするためにも、このファイルを使用しています。

リスト 3. Bootstrap.groovy を変更する
class BootStrap {

    def init = {servletContext ->

        (1..10000).each {i ->

            def Contact contact =

                new Contact(firstName: "Bob",

                lastName: "Johnson${i}",

                email: "bob.johnson${i}@email.com").save()
        }
    }

    def destroy = {  

    }
}

リスト 3 のコードの目的は、Grails アプリケーションのデフォルト HSQLDB データベースに 10,000 件の連絡先を作成することです。試しにアプリケーションを再起動して調べてみると、今何を操作しているのかがわかるはずです。

ContactController にロジックを追加する

Grails サンプル・アプリケーションを作成するための最後のステップは、クエリー実行時間を出力するための単純なロジックを ContactController に追加することです。後でこの情報を使用して、キャッシングによってアプリケーションのパフォーマンスがどれだけ向上したかを調べることになります。ロジックをコントローラーに追加する前に、Grails に対し、Contact の scaffold コードすべてを生成するように指示する必要があります。それには、grails generate-all コマンドを実行します。

grails generate-all

このコマンドを実行した後に contactmanager/grails-app/controllers/ContactController.groovy を開くと、Contact ドメインをサポートするために必要なすべてのクロージャーがあることがわかります。そのなかから list クロージャーを見つけて、リスト 4 のように時間を測定するためのステートメントを追加してください。

リスト 4. 時間を測定するためのステートメントを追加した list クロージャー
def list = {

    params.max = Math.min( params.max ? params.max.toInteger() : 10,  100)

    def startTime = new Date().getTime()
    def contactInstanceList = Contact.list(params)
    def contactInstanceTotal = Contact.count()
    def endTime = new Date().getTime()

    println "Transaction time was ${(endTime - startTime) / 1000} seconds."
 
    [contactInstanceList: contactInstanceList, contactInstanceTotal: 
        contactInstanceTotal]
}

リスト 4 のコードで追加している startTime は、現行のシステム時刻 (ミリ秒単位) に設定されます。その上で、Contacts のリストを取得するクエリー、そして Contacts の合計数を取得するクエリーを実行します。この 2 つのクエリーを実行した後、再び現在の時刻 (ミリ秒単位) を取得して endTime 変数に設定します。最後にクエリーの実行にかかった時間 (秒単位) を確認するために、開始時刻と終了時刻の差分を 1,000 で割った値を出力するというわけです。

grails run-app を実行してアプリケーションを再起動し、連絡先リストの結果をページング処理する間、コンソールを観察してください。

Grails アプリケーションに memcached を注入する方法

memcached クライアントを Grails アプリケーションに追加する際に最初に行わなければならない作業は、該当する jar ファイルをダウンロードして、そのファイルを contactmanager/lib ディレクトリーにコピーすることです。このサンプル・アプリケーションには memcached の Java クライアントである Spymemcached を使用しているので、この JAR をダウンロードしてください。この記事を執筆している時点での最新リリースは 2.3.1 です。

jar ファイルを contactmanager/lib ディレクトリーに配置したら、次は API を公開するための Groovy クラスを作成します。私はこの実装に Grails サービスを使うことにしましたが、それにはもっともな理由が 2 つあります。第 1 に、すべての Grails サービスは Spring Bean として管理されるため、自動的に Controller に注入することができます。第 2 に、Spring Bean としての Grails サービスでは、org.springframework.beans.factory.InitializingBean インターフェースにアクセスすることができるため、他のすべてのプロパティーが設定された後にサービスを初期化することができるからです。

それでは早速、サービスの作成に取り掛かります。任意の IDE またはエディターを開いて、リスト 5 の Groovy クラスを contactmanager/grails-app/services ディレクトリーに作成してください

リスト 5. MemcachedService
import net.spy.memcached.AddrUtil
import net.spy.memcached.MemcachedClient
import org.springframework.beans.factory.InitializingBean

class MemcachedService implements InitializingBean {

    static final Object NULL = "NULL"
    def MemcachedClient memcachedClient

    def void afterPropertiesSet() {
        memcachedClient = new MemcachedClient(AddrUtil.getAddresses("localhost:11211"))
    }

    def get(String key) {
        return memcachedClient.get(key)
    }

    def set(String key, Object value) {
        memcachedClient.set(key, 600, value)
    }

    def delete(String key) {
        memcachedClient.delete(key)
    }

    def clear() {
        memcachedClient.flush()
    }

    def update(key, function) {
        def value = function()
        if (value == null) value = NULL
        set(key, value)
        return value
    }

    def get(key, function) {
        def value = get(key)
        if (value == null) {
            value = update(key, function)
        }
        return (value == NULL) ? null : value;
    }
}

リスト 5 のメソッドの大部分は、get()set()delete()clear() など、おそらく皆さんが期待していたとおりのメソッドでしょう。その一方、それほど一般的ではない要素もいくつかあります。以下に、これらの要素を取り上げます。

まずは、以下の行に注目してください。

static final Object NULL = "NULL"

null を memcached に保存するときには、必ず上記の値を使用することになります。null をシリアライズすることは不可能ですが、memcached 内に配置されるすべてのオブジェクトはシリアライズ可能でなければならないため、この行が必要となります。

次に注目すべきメソッドは、afterPropertiesSet() メソッドです。前に説明したように、このメソッドはすべてのプロパティーが設定された後に、Spring によって呼び出されます。このメソッドには、MemcachedClient の新しいインスタンスを作成して memcached サーバーに接続するためのコードも追加しました。

最後に注目する価値のあるメソッドは、update()get() の 2 つです。key プロパティーおよび function プロパティーを引数に取るこの 2 つメソッドは、メモ化 (memoization) という、キャッシュの興味深い使用方法を示しています。

この 2 つのメソッドに対しては、検索対象の項目の key だけではなく、get() がその key を見つけられなかった場合に実行する function も渡しています。この手法は、前に処理された入力の計算を何度も繰り返さないために使用します。計算を繰り返す代わりに、コード内の単純な get() 呼び出しによって必要なオブジェクトを取得します。オブジェクトがキャッシュ内に見つからない場合にはそのオブジェクトを作成するだけでなく、その結果も保存します。メモ化がキャッシュとの対話を単純化する仕組みは、この記事の後半で詳しく説明します。

MemcachedService と ContactController との出会い

新しく作成した MemcachedServiceContactController に追加するには、以下の行を ContactController を追加します。

class ContactController {

    def memcachedService

    ...

}

この単純な行を追加することで、MemcachedService のインスタンスは自動的に Controller に注入されます。これでサービスは Controller で使用できるようになりましたが、Controller は一体何をキャッシュに入れればよいのでしょうか。

memcached の使用

キャッシュに入れるアプリケーション・データを決めるときには、以下の 2 つの単純なガイドラインを念頭に置いてください。この 2 つは確定されたガイドラインではないので、すべてのケースに当てはまるわけではありませんが、キャッシュに入れる内容を決める際の妥当な基準になります。

  • 頻繁に変更されるデータはキャッシュに入れないでください。キャッシュに入れたいデータが頻繁に変更される場合、キャッシュ内に保存された値を絶えず変更することになるため、キャッシュの価値が制限されてしまいます。
  • id で値を直接識別している場合、その値をキャッシュに入れる必要はありません。データベースは id を使ってその値を素早く検索することができます。

この Contact Manager アプリケーションの場合、上記のガイドラインに従えば、キャッシュに入れる必要があるデータは連絡先をページング処理するときに返されるデータであることは明らかです。このデータは、ContactControllerlist クロージャーのクエリーを実行して得られるものであることを思い出してください。

list クロージャーは、次の 2 つの値を返します。

  • contactInstanceList。データベースから取得した Contact のリストです。
  • contactInstanceTotal。データベース内に保存されている Contact の合計数を表します。

上記の項目は両方ともキャッシュング対象の候補ですが、まずは contactInstanceTotal から取り掛かります。

連絡先インスタンスの合計数をキャッシュに入れる

連絡先インスタンス・データをキャッシュに入れるためにはまず、Contact の合計数をキャッシュに入れるためのメソッドを ContactController に追加します。

リスト 6. getContactInstanceTotal()
def getUsername() {

    // dumb method to return a username
    // you are not implementing any security in this example
    return "my.username"
}

def getContactInstanceTotal(username) {

    def contactInstanceTotal = memcachedService.get("${username}:contactInstanceTotal") {

        def contactInstanceTotal = Contact.count()
        return contactInstanceTotal
    }
    return contactInstanceTotal
}

getUsername() は形だけのメソッドです。これが必要な理由は、セキュリティーに関してはこの記事では扱わないことから、サンプル・アプリケーションにはユーザーの概念がないためにすぎません。キャッシュに対する実際の操作が最初に開始されるのは、getContactInstanceTotal() のなかです。このメソッドは、「my.username:contactInstanceTotal」というキーを持つ memcachedService.get() メソッドを呼び出します。このキーがキャッシュ内に見つかると、その値が呼び出し側に返されます。見つからない場合は、get() に渡されたクロージャーが呼び出され、渡されたキーと一緒に戻り値がキャッシュに保存されます。このコードを ContactController に追加してから、以下の行を置換してください。

def contactInstanceTotal = Contact.count()

上記の行を以下の getContactInstanceTotal() 呼び出しに置き換えます。

def contactInstanceTotal = getContactInstanceTotal(getUsername())

アプリケーションを再起動し、memcached に telnet で接続して flush_all コマンドを実行してください。これによって、キャッシュがクリーンな状態にリセットされます。

telnet localhost 11211
Trying ::1...
Connected to localhost.
Escape character is '^]'.
flush_all
OK

次に、ブラウザーを開いて http://localhost:8080/contactmanager/contact/list にアクセスします。

このリンクにアクセスした後、memcached に telnet で接続し、以下のように getContactInstanceTotal() で使用されたキーに対して get を実行します。

get my.username:contactInstanceTotal
VALUE my.username:contactInstanceTotal 512 2
'
END

上記から、値はキャッシュに保存されていることがわかります。しかし、必ずしも期待する値が示されるわけではありません。なぜなら、値はシリアライズされており、String フォームにはなっていないからです。

キーを生成する

Contact Manager アプリケーションの Contact をキャッシュに保存するには (次に行う作業)、リクエストごとに一意に決まるキーを作成できなければなりません。contactInstanceTotal での場合と同じように 1 つのキーしか使わなければ、新しいリクエストが行われるごとに、キャッシュ内のデータを上書きすることになってしまいます。また、リクエストが一致する場合にはキーを確実に複製できるようにするための方法も必要です。

最も単純で、最も信頼できるキーの生成方法は、リクエストに渡されたパラメーターのダイジェストをキーとして使用し、そのキーの先頭に username を追加して使用する方法です。こうすれば、同じパラメーターを 2 回受け取ったとしても、同じ結果になるはずです (データが変更されていないことが前提です。これについては、次のセクションで説明します)。この手法はまた、パラメーターのダイジェストを生成することによって、一貫したキーを生成可能であることも保証します。

データのキャッシング

これまでの作業で、データをキャッシュに入れる準備は整いました。前述のとおり、これから list クロージャーに対する各リクエストの結果をキャッシュに入れます。この list クロージャーはかなり単純なもので、データベース内の Domain オブジェクトのコレクションをページングするために使用されます。このクロージャーに対してリクエストを行うと、一連のパラメーターがクロージャーに渡されます。これらのパラメーターは、Grails がデータベースに対してどのようにクエリーを実行するかを示すものです。これらのパラメーターの例を見るには、アプリケーションの Contact をページング処理しながら URL の変更を目で追ってください。また、以下の URL でもパラメーターを確認することができます。

http://localhost:8080/contactmanager/contact/list?offset=10&max=10

上記のリクエストには、offsetmax という 2 つのパラメーターが含まれます。このリクエストの結果は、10 の Contact が含まれるリストです。リストには、10 番目の Contact を先頭に 19 番目までの Contact が含まれます。データが変更されない限り、これらのパラメーターを指定すれば、データベースからは常に同じ結果が返されるはずです。したがって、このリクエストから params を取得してダイジェストを生成し、このキーと値のペアをキャッシュに保存することができます。そのために私が作成したメソッドをリスト 7 に記載します。

リスト 7. getCachedContactInstanceList()
def getCachedContactInstanceList(username) {

    params.max = Math.min(params.max ? params.max.toInteger() : 10, 100)

    println "PARAMS == ${params.toString()}"
    MessageDigest md = MessageDigest.getInstance("SHA");
    md.update(params.toString().getBytes('UTF-8'))
    def key = username + new BASE64Encoder().encode(md.digest())

    println "Using key ${key}."

    def cachedContactInstanceList = memcachedService.get(key) {

        def contactInstanceList = Contact.list(params)

        def serializableList = new ArrayList()

        contactInstanceList.each {

            serializableList.add([id: it.id, firstName: it.firstName, lastName: 
                it.lastName, email: it.email])
        }
        return serializableList
    }
    return cachedContactInstanceList
}

getCachedContactInstanceList() は至って単純なメソッドです。このメソッドはコントローラーに渡されたパラメーターを引数に取り、その String 値をバイト配列に変換してから MessageDigest に渡します。次に BASE64Encoder を使用して生成したキーの先頭に username を追加して key とします。これにより、同じセットの params が指定された場合には、確実に複製可能なキーを取得できることになります。

リスト 7 で唯一注意が必要な点は、結果を繰り返し処理し、各 Contact の内容をマップに変換し、それぞれのマップを ArrayList に保存していることです。これは、Grails の Domain オブジェクトはシリアライズすることができない一方、マップであればシリアライズすることができるためです。

このメソッドをサンプル Grails アプリケーションに追加した上で、以下の行を置換します。

def contactInstanceList = Contact.list(params)

上記の行を以下の getCachedContactInstanceList() 呼び出しに置き換えます。

def cachedContactInstanceList = getCachedContactInstanceList(getUsername())

Grails を再起動した後、リクエストのいくつかを繰り返し処理して Contact を数回ページング処理してください。この作業を行っている間、コンソールの stdout を観察していると、リクエストを繰り返すにつれてレスポンス時間が大幅に短くなっていくことがわかるはずです。

キャッシュを無効にする

キャッシュされたデータに対するレスポンス時間は大幅に短縮されますが、アプリケーションの連絡先を更新した場合にはどうなるでしょうか。新しい Contact を追加する前に、一番最後のページをブラウズして、そこにある Contact リストを確認してください。その上で、新規の Contact をいくつか追加し、もう一度最後のページをブラウズします。すると、新しい Contact がリストに含まれていないことに気付くはずです。問題は、処理されて返されるはずのリクエストの結果が、最初からキャッシュに格納されていることです。

この問題を解決する方法は、キャッシュを無効にすることですが、それにはどうしたらよいのでしょうか。すでにキャッシュに入れられたキーのレコードさえありません。そこで最初に必要となる作業は、無効にしなければならないキーのすべてを確実に検索できるようにすることです。そのための単純な方法としては、キーのリストをキャッシュに入れ、このリストをユーザーに関連付けるという方法が考えられます。

リスト 8 のスニペットを getCachedContactInstanceList() メソッドに追加してください。追加する場所は、キャッシュに格納された Contact を返す直前です。

リスト 8. 更新後の getCachedContactInstanceList()
def getCachedContactInstanceList(username) {

    ...

    // before I return the contacts, I need to add this key to the user's keyList
    def contactKeyList = memcachedService.get(username + ":contactKeyList")
    if (!contactKeyList) {

        contactKeyList = []
    }
    contactKeyList.add(key)
    memcachedService.set(username + ":contactKeyList", contactKeyList)

    return cachedContactInstanceList
}

次に、キャッシュを無効にするコードを追加します。キャッシュを無効にするメソッドは、以下の 4 つのステップを実行する必要があります。

  • キャッシュに格納されたキーを取得する
  • 取得したキーに関連付けられたキーと値のペアを削除する
  • キャッシュに格納されたキーが含まれる配列を削除する
  • contactInstanceTotal を表すキーと値のペアを削除する

リスト 9 に、キャッシュを無効にする完全なメソッドを記載します。

リスト 9. 更新後の invalidateContacts()
def invalidateContacts(username) {

    // delete the cached contacts
    def contactKeyList = memcachedService.get(username + ":contactKeyList")
    contactKeyList.each {

        memcachedService.delete(it)
    }

    // delete the list of keys
    memcachedService.delete(username + ":contactKeyList")

    // delete the contactInstanceTotal
    memcachedService.delete(username + ":contactInstanceTotal")
}

コード自体は単純明快で、前述の 4 つのステップを完了し、username と関連付けられた Contact に関するキャッシュ内の情報すべてを削除するというものです。このメソッドの呼び出しは、Contact データを変更することになるすべてのメソッド/クロージャーに追加しなければなりません。このアプリケーションのクロージャーには deletesave があります。クロージャーの変更内容はリスト 10 のとおりです。

リスト 10. 更新後の delete クロージャーと save クロージャー
def delete = {
    def contactInstance = Contact.get(params.id)
    if (contactInstance) {
        try {
            contactInstance.delete(flush: true)
            flash.message = "Contact ${params.id} deleted"
            invalidateContacts(getUsername())
            redirect(action: list)
        }
        catch (org.springframework.dao.DataIntegrityViolationException e) {
            flash.message = "Contact ${params.id} could not be deleted"
            redirect(action: show, id: params.id)
        }
    }
    else {
        flash.message = "Contact not found with id ${params.id}"
        redirect(action: list)
    }
}

def save = {
    def contactInstance = new Contact(params)
    if (!contactInstance.hasErrors() && contactInstance.save()) {
        flash.message = "Contact ${contactInstance.id} created"
        invalidateContacts(getUsername())
        redirect(action: show, id: contactInstance.id)
    }
    else {
        render(view: 'create', model: [contactInstance: contactInstance])
    }
}

リスト 10 で注意しなければならない点として、データが正常に変更されたことが確実になるまでは、キャッシュに格納された Contact を無効にしないでください。いずれかのクロージャーで最初にキャッシュを無効にすると、キャッシュを時期尚早に無効化してしまう恐れがあります。

結果をテストする

以上の変更を行った後、Contact Manager アプリケーションを再起動して telnet で memcached に接続し、更新後のアプリケーションの結果を確認してください。まず必要な作業はキャッシュを完全に空の状態にリセットすることなので、flush_all コマンドを実行します。

flush_all
OK

次にブラウザーを開き、Contact リストのページング処理を開始します。ページング処理を行っている間、コンソールを観察してください。Contact をキャッシュに保存するためにキーが使用されていることがわかるはずです。これらのキーを後で使用できるようにコピーした上で、いくつかの Contact を追加、削除、編集します。最初に気付くことは、データを変更すると、それに応じてデータが正しく表示されることです。最後のアクションとして新しい連絡先を追加し、Show Contact ページでのブラウズを終えてください。

telnet セッションに戻って、あらかじめコピーしておいた contactKeyListcontactInstanceTotal の 2 つのキーで get 操作を実行してみます。これらの値はすべてキャッシュから削除されているため、キャッシュが以降のリクエストすべてに対して適切な状態になっていることがわかるはずです。

まとめ

この記事では、Grails アプリケーションに効果的にキャッシング機能を組み込む 1 つの方法を説明しました。今回行ったように、memcached を使用して個々のリクエストの結果をキャッシュに入れると、Grails の組み込みページング機能の威力をフルに活用することができます。他の手段としては、ユーザーの連絡先すべてをキャッシュに入れてから、独自のページング・コードをまるごと作成するという方法も考えられます。これは手作業による方法ですが、場合によっては理にかなった方法となります。例えば私は、JSON ファイルを受け渡しする GWT/Grails アプリケーションに取り組んでいるときに、この方法を使用したことがあります。キャッシング対象のデータを JSON 表現のままで保存することで、結果を JSON に変換する必要がなくなり、処理速度が一層短縮される結果となりました。しかし大抵の目的には、この記事で紹介した手っ取り早い手法が功を奏します。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Open source, Java technology
ArticleID=442448
ArticleTitle=memcached と Grails: 第 2 回 memcached を Grails に統合する
publish-date=10062009