我们在写API处理逻辑时,难免会遇到数据处理耗时长的难题。解决办法有两种,一是优化操作逻辑,降低处理时长;二是利用缓存技术,先给用户返回一些数据,即便返回的是过期的数据,用户体验也比等待白屏好很多。
优化逻辑需要很多的智力投入,快速开发时,我们往往没有功夫去逐一优化。第二种办法更简单,更一劳永逸。
缓存技术有很多,但基本思路是一致的:将数据保存在更快的访问介质(缓存)中,如果用户请求的数据在缓存中有现成的,则返回缓存中的数据,同时根据需要更新缓存。
在Django开发中,很多教程会提到利用Redis等第三方工具实现缓存,需要投入额外的精力学习。这里提供一种更简单易用的办法,直接利用Django本身(以及Django rest framework),重载Response即可实现类似效果,对于中小型项目来说,也足够用了。
思路
利用数据库作为缓存介质,新建一个 APIResponse
模型,专门用来缓存高复杂度API的运算结果。一个请求过来,我们先从APIResponse
中看是否已有缓存,有的话就先返回缓存数据,然后根据需要更新APIResponse
。
# models.py: 定义缓存模型
from collections.abc import Callable
from datetime import datetime
from django.db import models
class APIResponse(models.Model):
"""
接口数据缓存模型
"""
# 超过AGE(秒),就认为缓存过期
AGE = 300
# API的标识符
signature = models.CharField(max_length=50, unique=True)
# 缓存的数据
data = models.JSONField(null=True)
# 缓存时间
date = models.DateTimeField(auto_now=True)
@property
def seconds(self) -> int:
now = datetime.now()
return (now - self.date).seconds
@property
def is_expired(self) -> bool:
return bool(self.pk and self.seconds > self.AGE)
def refresh(self, get_data: Callable) -> None:
"""
新建或者过期,则更新数据库 get_data()为更新后值
get_data: 耗时操作
"""
if self.pk is None or self.data is None or self.is_expired:
self.data = get_data()
self.save()
具体实现
API标记
API的标志,我们用请求request
的路径(包含参数)的哈希值来表征:signature = hashlib.md5(request.get_full_path().encode()).hexdigest()
先返回后更新的实现
重载Response
类,通过close()
方法,实现先返回,后更新
from rest_framework.response import Response
from public_api.models import APIResponse
class CachedResponse(Response):
def __init__(self, cached_obj, update_function, initial_wait=True, **kwargs):
"""
initial_wait 首次访问,是否等待计算结果
"""
assert isinstance(cached_obj, APIResponse)
data = cached_obj.data
if initial_wait and cached_obj.data is None:
cached_obj.data = update_function()
cached_obj.save()
data = cached_obj.data
super().__init__(data, **kwargs)
self._update_function = update_function
self._cached_obj = cached_obj
def close(self):
"""
先返回
然后更新
"""
super().close()
self._cached_obj.refresh(self._update_function)
调用方法
def api( request):
""" 类似这样处理请求 """
signature = hashlib.md5(request.get_full_path().encode()).hexdigest()
cached, _ = APIResponse.objects.get_or_create(signature=signature)
def get_data():
"""耗时的操作"""
xxx = request.GET.get("xxx", None)
# 省略业务逻辑
data = xxxxx
return data
return CachedResponse(cached, get_data)
分析
请求到来时,程序会先判断缓存是否命中,没有命中的话则先计算,后返回;缓存命中则优先返回缓存数据,然后根据API是否过期,适时更新缓存。
大型项目中,现在常用的Redis做缓存,它的缓存介质是内存,而本文的缓存介质是数据库,无需额外引入第三方工具
,更简单直观,适合中小型项目的开发使用。