跳转至

rest_framework_cache优化接口访问速度

场景

在DRF中,有一些高频的列表接口(如供前端搜索下拉选择的数据源接口),使用缓存可以大幅度的提高接口的响应速度。

项目介绍

django-rest-framework-cache是一个嵌入Serializer层去缓存数据的工具,通过重写Serializerto_representation方法来实现缓存。若你重写了ModelSerializerto_representation方法。

Github仓库地址

安装

pip install rest-framework-cache

settings.py:

INSTALLED_APPS = (
    ...
    'rest_framework_cache',
)

urls.py:

from rest_framework_cache.registry import cache_registry

cache_registry.autodiscover()

配置

缓存源

CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
        'LOCATION': '127.0.0.1:11211',
    },
    'rest_backend': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        'LOCATION': 'unique-snowflake',
    }
}

REST_FRAMEWORK_CACHE = {
    'DEFAULT_CACHE_BACKEND': 'rest_backend',
}

全局超时时长

REST_FRAMEWORK_CACHE = {
    'DEFAULT_CACHE_TIMEOUT': 86400, # Default is 1 day
}

用法

from rest_framework import serializers

# You must import the CachedSerializerMixin and cache_registry
from rest_framework_cache.serializers import CachedSerializerMixin
from rest_framework_cache.registry import cache_registry

from .models import Comment


class CommentSerializer(serializers.ModelSerializer, CachedSerializerMixin):

    class Meta:
        model = Comment


cache_registry.register(CommentSerializer)

注意事项:django-rest-framework-cache通过重写Serializerto_representation方法来实现缓存。若你重写了ModelSerializerto_representation方法,因为Python3的MRO算法的原因,Serializer使用时应先继承CachedSerializerMixin:

class CommentSerializer(CachedSerializerMixin, CustomModelSerializer):

实测效果

数据列表返回的数据共有310条类似下列的数据:

{
    "id": 310,
    "name": "芒果",
    "no": "600006",
    "unit_group": {
        "id": 1,
        "name": "重量",
        "minimum_unit": {
            "id": 4,
            "name": "g",
            "display_name": "克"
        }
    }
}

在CPU:i7-7700 3.6G 内存:16GB 系统:Win764位,使用requests工具访问100次的平均用时如下:

使用Cached前:0.459s 使用LocMemCache后:0.197s 使用MemcachedCache后:0.0538s

在CPU:G4400 0.8G 内存:8GB 系统:Ubuntu14.04位,使用requests工具访问100次的平均用时如下:

使用Cached前:0.639s 使用LocMemCache后:0.319s 使用MemcachedCache后:0.0817s

django-rest-framework-cache使用LocMemCache优化后可以缩短用时至原先的1/2,使用MemcachedCache优化后可以缩短用时至原先的1/8

Issue:嵌套的FK数据或者M2M数据修改后不生效问题(Updated in 2018/08/02)

经过一个多月的使用,发现此插件有一个不完美的地方。以上述的评论模型Comment为例,假设评论有一个外键叫所属人owner,在列表评论的时候,使用的Serializer如下:

class CommentSerializer(serializers.ModelSerializer, CachedSerializerMixin):
    owner = UserSerializer(read_only=True)

    class Meta:
        model = Comment

当评论列表的数据被缓存下来以后,再去修改评论中对应owner的信息,再次访问该评论列表取到的依旧是未修改前的用户信息。这是因为缓存的serializer信息,只有在该serializer对应的Model实例被修改后才会刷新,也就是说只有在Comment实例被修改时缓存才会被刷新,修改Comment对应的外键User实例并不会去刷新缓存。

对此插件做如下改进,重写CachedSerializerMixin,使其可以选择某一些字段不进行缓存:

from collections import OrderedDict
from django.utils.functional import cached_property

from rest_framework import serializers
from rest_framework.fields import SkipField
from rest_framework.relations import PKOnlyObject

from rest_framework_cache.cache import cache
from rest_framework_cache.settings import api_settings
from rest_framework_cache.utils import get_cache_key


class CachedSerializerMixin(serializers.ModelSerializer):

    def _get_cache_key(self, instance):
        request = self.context.get('request')
        protocol = request.scheme if request else 'http'
        return get_cache_key(instance, self.__class__, protocol)

    @cached_property
    def _not_cache_fields(self):
        not_cache_fields = getattr(self.Meta, 'not_cache_fields', [])

        return [
            field for field in self.fields.values()
            if field.field_name in not_cache_fields
        ]

    @cached_property
    def _cacheable_fields(self):
        not_cache_fields = getattr(self.Meta, 'not_cache_fields', [])

        return [
            field for field in self.fields.values()
            if not field.write_only and field.field_name not in not_cache_fields
        ]

    def fields_to_representation(self, instance, fields, ret):
        for field in fields:
            try:
                attribute = field.get_attribute(instance)
            except SkipField:
                continue

            check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
            if check_for_none is None:
                ret[field.field_name] = None
            else:
                ret[field.field_name] = field.to_representation(attribute)
        return ret

    def to_representation(self, instance):
        """
        Checks if the representation of instance is cached and adds to cache
        if is not.
        """
        key = self._get_cache_key(instance)
        cached = cache.get(key)
        if cached:
            ret = cached
        else:
            ret = OrderedDict()

            ret = self.fields_to_representation(instance, self._cacheable_fields, ret)
            cache.set(key, ret, api_settings.DEFAULT_CACHE_TIMEOUT)

        ret = self.fields_to_representation(instance, self._not_cache_fields, ret)
        return ret

用法:

class UserSerializer(serializers.ModelSerializer, CachedSerializerMixin):

    class Meta:
        model = User


cache_registry.register(UserSerializer)

class CommentSerializer(serializers.ModelSerializer, CachedSerializerMixin):
    owner = UserSerializer(read_only=True)

    class Meta:
        model = Comment
        not_cache_fields = ('owner',)


cache_registry.register(CommentSerializer)

使用not_cache_fields选项指定CommentSerializer不缓存owner字段,该字段由UserSerializer的缓存读取,这样就可以解决上述问题了。

已提交PR,不过按作者的情况来看可能不会合并了。我Fork出来修改好的库见此处,需要注意的是因为精力有限的原因该改进仅在djangorestframework==3.8.2的基础上进行了测试。