趣味で作ってるアプリで永続化キャッシュを使いたいと思った。 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_value
のheavy_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の仕組みに乗っかっておくと、あとでバックエンドを切り替えられるので便利です。