通过Signal实现依赖于Model的缓存策略

Django为我们提供了一个异常强大的缓存框架,支持配置多种不同的缓存后端,同时允许你定制自己的缓存后端,并通过一致的API对缓存数据进行存取,通过它我们可以构建一个高性能Web应用。

Django的缓存系统如此强大,但在实际应用中,如果仅仅依赖于缓存框架本身的缓存机制,很难在性能和实效上取得很好的平衡。我们知道Django的缓存API中,缓存的失效有三种情况:一是缓存到达了我们人为指定的缓存过期时间;二是我们手动调用cache.delete()方法来手动清除缓存;三是缓存空间不足由系统自动释放掉。我们在做Django应用开发时,其实绝大部份开发是面向数据库的开发,而数据的查询也是较为耗时的操作之一,一个有效的页面可能包括十几甚至上百个数据查询,这些数据查询操作耗时累加起来,将会是影响页面访问速度的最大根源,如果能够有效的对数据查询操作结果进行缓存,将能极大的提升系统的运行效率。但我们面临着这样的问题,如果采用第一种也就是指定缓存过期时间的方法,那么我的数据库数据更改后,需要等到过期时间后才能反映到页面上,这对于实时性要求比较高的情况是不适用的,如果缓存时间设置过短,虽然可以提升页面的及时性,但缓存的利用率却不能得到很好的发挥(因为数据的变更时间点是不可预知的,而缓存过期时间的设置却是固定的)。如果使用第二种方法,我们就必需在恰当的时间点对缓存进行清除,才能保证缓存的最大利用率。那么如果有一种方法能够在数据库中的数据变更时,及时的通知到我们,这样我们就可以在数据变动时手动调用cache.delete()方法来清除缓存,从而即能够保证页面的实时性,又能够获得最大的缓存利用率。今天我们就一起来聊聊如何通过Django的Signal实现依赖于Model的缓存策略。

上面我们讲到了我们需要在数据库中的数据变更时获得到通知,而Django的Signal则是获得该通知的不二选择。那么什么是Signal呢?Signal译为“信号”,是Django内置的一种信号分发机制,它允许应用在发生某些动作的时候,可以在应用的其它地方能够获得相应的通知。Django内部已经提供了一组信号,允许用户在特定的动作发生时获取相应的通知的能力,这其中就包括了:

  1. django.db.models.signals.pre_save:当Model调用save()方法进行数据保存前发出的信号。
  2. django.db.models.signals.post_save:当Model调用save()方法进行数据保存后发出的信号。
  3. django.db.models.signals.pre_delete:当Model调用delete()方法删除数据前发出的信号。
  4. django.db.models.signals.post_delete:当Model调用delete()方法删除数据后发出的信号。
  5. django.db.models.signals.m2m_changed:当Model的ManyToManyField字段发生变更时发出的信号。

以上列出了与Model相关的信号,那么通过这些信号,我们就可以获知数据变更的通知,进而调用cache.delete()方法及时的清理无效的缓存,从而达到即保证页面了的实时性,又能够获得最大的缓存利用率。那么Signal如何使用呢,看下面的代码:

# 先声明一个方法,用于接受信号后执行的操作
def my_callback(sender, instance, **kwargs):
    """
    :param sender:保存的Model类。
    :param instance:保存的Model的实例。
    """
    print("%s保存完成!" % instance)

# 导入信号
from django.db.models.signals import post_save

# 连接信号
post_save.connect(my_callback, sender=Model)    # sender参数用于只接收指定的Model类的post_save信号

以上代码即实现了一个简单的信号监听及处理的过程,关于Signal的更多内容,请参考官方文档 https://docs.djangoproject.com/en/1.10/topics/signals/

了解了Signal的使用,下面我们再来看看缓存API的简单使用:

# 导入缓存模块
from django.core.cache import cache

# 设置缓存数据 set(key, value, timeout)
cache.set('my_key', 'hello, world!', 30)

# 获取缓存数据get(key)
cache.get('my_key')

# 删除缓存delete()
cache.delete('my_key')

更多缓存的使用请参见官方文档 https://docs.djangoproject.com/en/1.10/topics/cache/

好了,了解了Signal和Cache的基本使用方法,我们来实现一个简单的依赖于Model的缓存方法:

# 假设我们有以下Model类
class Info(models.Model):
    title = models.CharField(max_length=256, verbose_name=_('标题'))
    …
# 实现一个依赖于整个模型所有实例的缓存

# 声明一个获取数据的原始方法
def _get_info_top10_list():
    qs = Info.objects.all()[:10]
    return list(qs)

# 声明一个缓存键值
CACHE_KEY_INFO_TOP10_LIST = “info_top10_list”

# 声明一个从缓存获取数据的方法
def get_info_top10_list():
    data = cache.get(CACHE_KEY_INFO_TOP10_LIST)
    if data is None:
        data = _get_info_top10_list()
        cache.set(CACHE_KEY_INFO_TOP10_LIST, data)
    return data

# 声明一个监听Model数据保存删除的回调,用以更新缓存
def callback_for_info_top10_list(sender, instance, **kwargs)
    cache.delete(CACHE_KEY_INFO_TOP10_LIST)

# 连接信号,以触发缓存更新
post_save.connect(callback_for_info_top10_list, sender=Info)
post_delete.connect(callback_for_info_top10_list, sender=Info)
# 实现一个依赖于模型单个实例的缓存

# 声明一个获取数据的原始方法
def _get_info (info_id):
    try:
        info = Info.objects.get(pk=info_id)
    except Info.ObjectDoesNotExist:
        info = None
    return info

# 声明一个构建缓存键的方法
def build_cachekey_info(info_id):
    return ‘info_%s’ % info_id

# 声明一个从缓存获取数据的方法
def get_info(info_id):
    key = build_cachekey_info(info_id)
    data = cache.get(key)
    if data is None:
        data = _get_info(info_id)
        cache.set(key, data)
    return data

# 声明一个监听Model数据保存删除的回调,用以更新缓存
def callback_for_info(sender, instance, **kwargs)
    key = build_cachekey_info(instance.pk)
    cache.delete(key)

# 连接信号,以触发缓存更新
post_save.connect(callback_for_info, sender=Info)
post_delete.connect(callback_for_info, sender=Info)

看了以上代码,是不是感觉有点麻烦呢,如果我们需要将很多的方法都转成依赖于Model的缓存方法,每次写这么一大段的代码不仅相当繁琐,而且可读性也降低了。那有没有什么办法能够让我们更专注于业务本身并保持代码的简洁呢?通过观察上面的代码,我们可以发现代码绝大部分是相同的,那么我们可以考虑将相应的实现逻辑进行封装,同进借助Python语言的装饰器语法,将能够大大的简化我们的代码。看看以下代码是如何进行封装的:

# coding=UTF-8
from __future__ import unicode_literals

from django.core.cache import cache
from django.db.models import signals


def _build_model_cachekey(method, model_class, mid=None):
    """
    构建一个基于Model类的的缓存键。
    :param 原始方法名。
    :param model_class: Model的类。
    :param mid: Model类的实例的唯一标识,通常为pk,也可为任意唯一标识值。
    :return: 基于Model类的的缓存键。
    """
    opts = model_class._meta
    key = "__model_cache_%s_%s_%s" % (method.__name__, opts.app_label, opts.model_name)
    if not mid is None:
        key = "%s_%s" % (key, mid)
    return key

def _get_mid(instance):
    """
    获取Model类的实例的唯一标识的默认方法。
    :param instance: Model类的实例。
    :return: Model类的实例的唯一标识。
    """
    return instance.pk


# Model 数据缓存信号回调方法的集合,将信号回调方法存至全局变量,\
# 避免该方法因无引用而被系统回收,从而导致信号回调不会执行的问题
_model_data_cahce_signals_methods = []


def model_data_cache_method(model_class):
    """
    生成一个用于将一个方法转换为一个依赖于Model的缓存的方法的装饰器。
    用法:
        @model_data_cache_method(Model)
        def get_data_method():
            return Model.objects.filter()
    :param model_class: 依赖的Model类。
    :return: 一个用于将一个方法转换为一个依赖于Model的缓存的方法的装饰器。
    """
    def decorator(method):
        def get_data_method():
            key = _build_model_cachekey(method, model_class)
            data = cache.get(key)
            if data is None:
                data = method()
                cache.set(key, data)
            return data

        def callback_for_method(sender, instance, **kwargs):
            key = _build_model_cachekey(method, model_class)
            cache.delete(key)

        _model_data_cahce_signals_methods.append(callback_for_method)

        signals.post_save.connect(callback_for_method, sender=model_class)
        signals.post_delete.connect(callback_for_method, sender=model_class)

        return get_data_method
    return decorator


def model_instance_data_cache_method(model_class, get_mid_method=None):
    """
    生成一个用于将一个方法转换为一个依赖于Model的单个实例的缓存的方法的装饰器。
    用法:
        @model_data_cache_method(Model)
        def get_data_method(mid):
            return Model.objects.get(pk=mid)
    :param model_class: 依赖的Model类。
    :param get_mid_method: 获取Model实例唯一标识的方法,如果不指定,则默认为获取实例的pk值。
                            注意:此方法返回的Model实例唯一标识应与参数method的第一个参数相一致。
    :return: 一个用于将一个方法转换为一个依赖于Model的单个实例的缓存的方法的装饰器。
    """
    if get_mid_method is None:
        get_mid_method = _get_mid

    def decorator(method):
        def get_data_method(mid):
            key = _build_model_cachekey(method, model_class, mid=mid)
            data = cache.get(key)
            if data is None:
                data = method(mid)
                cache.set(key, data)
            return data

        def callback_for_method(sender, instance, **kwargs):
            key = _build_model_cachekey(method, model_class, mid=get_mid_method(instance))
            cache.delete(key)

        _model_data_cahce_signals_methods.append(callback_for_method)

        signals.post_save.connect(callback_for_method, sender=model_class)
        signals.post_delete.connect(callback_for_method, sender=model_class)

        return get_data_method
    return decorator

以上我们完成了依赖于Model及依赖于Model的单个实例的缓存方法装饰器,那么如何使用呢?看一下代码:

@model_data_cache_method(Info)
def get_infos():
    return list(Info.objects.all())

@model_instance_data_cache_method(Info)
def get_info(mid):
    return Info.objects.get(pk=mid)

infos = get_infos()
info = get_info(1)
from django.contrib.auth.models import Group

@model_data_cache_method(Group)
def get_groups():
    return list(Group.objects.all())

@model_instance_data_cache_method(Group, get_mid_method=lambda instance : instance.name)
def get_group(name):
    return Group.objects.get(name=name)

groups = get_groups()
group = get_group("group_name")

好了,通过上面的封装,我们的代码是不是变得简洁多了。

今天给大家介绍了如何通过结合Signal的方式来实现依赖Model的缓存策略,当然实际工作当中会存在比上例更为复杂的依赖于Model的缓存需求(比如依赖于多个Model类或者多个Model实例等),这里要根据具体情况进行针对性的开发,希望通过本文能够给大家一个抛砖引玉的作用。好了,今天就到这吧,Bye~

    原文作者:Kidwind
    原文地址: https://www.jianshu.com/p/9ea88e2d2394
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞