matobaの学んだこと

とあるPythonエンジニアのブログ。ソフトウェア開発、執筆活動、ライフログ。

Djangoで永続化キャッシュを使いたい

趣味で作ってるアプリで永続化キャッシュを使いたいと思った。 Djangoのキャッシュシステムを使うと簡単にできそうだったので、とりあえず手元で動くようにしてみた。 そのメモです。

ドキュメント

ドキュメントはこの辺を見ます。

https://docs.djangoproject.com/en/2.2/topics/cache/#database-caching

キャッシュの選択

Djangoではいくつかキャッシュのバックエンドを選べます。

  • memcached
  • database
  • filesystem
  • local-memory

あと、3rd partyのライブラリを入れるとRedisをバックエンドに使うこともできそうです。

https://github.com/sebleier/django-redis-cache

今回は、趣味で作ってるアプリのデータ分析で、中間データをキャッシュするとどれくらい高速化するかを試すのが目的ですので、とりあえずfilesystemを選択して進みます。

settings

filesystemにキャッシュする場合は、settingsに設定を追加します。

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache',
        'LOCATION': os.path.join(BASE_DIR, 'tmp/cache'),
    }
}

LOCATIONはキャッシュしたファイルを配置するディレクトリです。

cacheしてみる

djangoのキャッシュフレームワークを経由してキャッシュしてみます。

基本はドキュメントの抜粋です。

https://docs.djangoproject.com/en/2.2/topics/cache/#basic-usage

>>> from django.core.cache import cache
>>> cache.set('my_key', 'hello, world!', 30)
>>>
>>> cache.get('my_key')
'hello, world!'

無事にキャッシュできたようです。 実際には以下にファイルがありました。

$ ls tmp/cache/882421398f2305x4dc2bc98afef79b3c.djcache
tmp/cache/882421398f2305x4dc2bc98afef79b3c.djcache

pickleできるオブジェクトならキャッシュできるようです。

>>> cache.set('my_list', [1, 2, 3], 30)
>>> cache.get('my_list')
[1, 2, 3]
>>> value = cache.get('my_list')
>>> type(value)
<class 'list'>
>>> value[2]
3

30と言うのはキャッシュする秒数で、Noneを指定するとタイムアウトしないらしい。

キャッシュあれば使う

キャッシュがあれば使うのはこんなのです。 まあ、Djangoのドキュメントと同じですが。

>>> from datetime import datetime
>>> cache.get_or_set('timestamp-key1', datetime.now(), 5)
datetime.datetime(2019, 10, 2, 19, 38, 19, 846996)
>>> cache.get_or_set('timestamp-key1', datetime.now(), 5)
datetime.datetime(2019, 10, 2, 19, 38, 19, 846996)
>>> cache.get_or_set('timestamp-key1', datetime.now(), 5) # 5秒後
datetime.datetime(2019, 10, 2, 19, 38, 24, 948818)

便利ですね。

関数の結果をキャッシュするなら

便利と思って、Djangoのモデルに計算した値の中間結果をキャッシュしようとしました。

キャッシュする前はこんな感じになってるとします。caches_valueheavy_process(self.data) の部分がキャッシュしたい処理です。

class MyModel(models.Model):
    ....

    @property
    def caches_value(self):
        return heavy_process(self.data)

キャッシュするとこんな感じです。

class MyModel(models.Model):
    ....

    @property
    def caches_value(self):
        return cache.get_or_set(
            f'mymodels_cache:{self.pk}', heavy_process(self.data), None
        )

ただ、これだとうまくキャッシュされませんでした。

多分、 cache.get_or_set を実行する前に heavy_process(self.data) の部分が評価されてます。

heavy_process(self.data) の部分は遅延評価にしたいので、以下のようにすると、キャッシュが効いたように見えます。

class MyModel(models.Model):
    ....

    @property
    def caches_value(self):
        return cache.get_or_set(
            f'mymodels_cache:{self.pk}',
            lambda heavy_process(self.data),
            None
        )

計測してみる

キャッシュが効いてるかを念の為、確認しました。

キャッシュしなかった場合

>>> timeit.timeit('from app.models import MyModel;target = MyModel.objects.get(id=6);target.caches_value', number=10)
15.153339257987682

キャッシュした場合

>>> import timeit
>>> timeit.timeit('from app.models import MyModel;target = MyModel.objects.get(id=609);target.caches_value', number=10)
0.01263789099175483

十分に早くなったので、とりあえずは良さそうです。

終わり

とりあえずDjangoの仕組みに乗っかってキャッシュを使えるようにしてみました。

Djangoの仕組みに乗っかっておくと、あとでバックエンドを切り替えられるので便利です。