Django-中间件、信号、缓存和调试+喵喵CRM客户管理缓存优化

2025-10-17

之前在测试视图函数所使用的SQL语句时设置过一个中间件,现在我们来简单看看中间件的使用。

中间件主要应用在鉴权、限流、日志等场景,要避免业务逻辑和同步耗时I/O。

这是之前的查询某个操作执行的SQL语句。在项目启动时,中间件会完成初始化,其中get_response是一个可调用对象,是下一个逻辑处理。__call__就是核心处理逻辑,每个请求都会执行一次这个逻辑。在视图执行之前,清空查询记录。然后get_response执行下一个逻辑(可能是中间件或者视图函数),然后获取查询次数,打印查询语句。

# 中间件小工具
from django.db import connection, reset_queries

class QueryCountMiddleware:
    # 用于查询某个操作执行的SQL语句
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        reset_queries()  # 每次请求前清空SQL记录
        response = self.get_response(request)
        num_queries = len(connection.queries)
        print(f"[SQL 查询次数] {request.path}: {num_queries}")
        for q in connection.queries:
            print(q['sql'], q['time'])
        return response

唔,这个是不是和python的装饰器有点类似,不过中间件是全局的,一旦启动(在settings.py里面注册),每一个HTTP请求都会生效。而装饰器更加细粒度,只是关注我们感兴趣的某个视图函数。

信号

信号(Signals)是一种广播和订阅系统,用于框架不同部分之间实现解耦通信,简单来说就是不同app之间连接,但是不需要app之间有任何了解或者依赖。信号有四个核心部分:

举个例子,比如两个app,Django自带的用户认证app,而另一个app是个人写得用于存储用户头像等额外信息。创建了一个新的用户,如果不用信号只能去修改用户认证app,而如果有信号,只需要发送一个信号,个人写的应用就会收听信号自动处理。

一个简单的示例:

# profiles/signals.py
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.dispatch import receiver
from .models import Profile # 假设你已经定义了 Profile 模型

@receiver(post_save, sender=User) # 使用装饰器进行“连接”
def create_user_profile(sender, instance, created, **kwargs):
    """
    当 User 实例被保存后,自动调用此函数
    """
    # created 是一个布尔值,True 表示这是新创建的记录
    if created:
        Profile.objects.create(user=instance)
        print(f"为新用户 {instance.username} 创建了 Profile!")

# 接收函数的参数解释:
# sender: 发送信号的模型类 (这里是 User)
# instance: 被保存的具体实例对象 (比如叫 'John' 的那个用户对象)
# created: 一个布尔值,如果是 True,表示是 INSERT (创建);如果是 False,表示是 UPDATE (更新)。
# **kwargs: 包含其他参数的字典。

注册信号处理器:

from django.apps import AppConfig

class ProfilesConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'profiles'

    def ready(self):
        # 当应用准备就绪时,导入信号处理模块
        # 这样 Django 就能发现 @receiver 装饰器并完成连接
        import profiles.signals

setting里配置:

INSTALLED_APPS = [
    # ...
    'profiles.apps.ProfilesConfig', # 或者直接写 'profiles',但显式指定更好
    # ...
]

常见信号:

缓存

之前我们试过权限放入缓存里,效果还可以。不过或许可以进一步优化一下,我们每次在不同的页面跳转(例如点击权限管理再点击客户管理),因为每次都要数据展示,所以每次都会去访问一下SQL数据库,实际上如果不更改信息,只是单纯的页面浏览,并不需要每次都访问。

对于上述场景,有两种常见思路,一种是设置有效期,比如设置1h后缓存自动失效;另一种是主动通知作废,就是数据库变动及时删除缓存并更新。第一种简单粗暴,但是有效期设置太长,容易出现数据不一致的问题,而如果有效期设置太短,缓存命中率就低,频繁访问数据库。第二种实时性更高,最大程度保证了数据一致性,但是需要在每次写操作后增加一个删除缓存的动作。

考虑到我们的应用场景,设置有效期显然难以满足需求,还是第二种更有效。不过也要适当结合第一种,作为兜底策略,防止因为某些异常(比如删除缓存失败)导致脏数据永远留在缓存里。

不过也不能每次更新都直接往缓存里写,毕竟可能有时候也不需要更新后的数据,而且如果其他用户在疯狂修改,总不能每次都把所有信息往缓存里塞吧。所以只需要一个通知最好,比如给缓存的客户信息数据加个版本号,有其他用户增删改数据后,仅仅需要版本号+1,然后下次访问时候如果版本号对应的数据不存在,再去访问数据库就好了。正好可以利用上述的信号机制。【旧版本数据怎么处理?】

原有的客户列表视图函数代码如下:

class CustomersSet(viewsets.ModelViewSet):
    serializer_class = CustomerSerializer
    filter_backends = [SearchFilter, OrderingFilter]
    search_fields = ["name", "email", "phone", "address"]
    ordering_fields = ["created_at", "updated_at", "name"]
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        return Customer.objects.filter(owner=self.request.user).select_related('owner')

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)
……

我们需要重写查询的函数list,其他的函数不变。

    def list(self, request, *args, **kwargs):
        """
        重写 list 方法,加入缓存逻辑
        """
        # 获取客户数据版本号
        version_key = 'crm:customers:version'
        version = cache.get(version_key, 1)  # 默认版本为1

        # 构造动态且唯一的缓存键
        query_params_str = json.dumps(sorted(request.query_params.items()))
        query_hash = hashlib.md5(query_params_str.encode('utf-8')).hexdigest()
        cache_key = f"crm:customers:list:u{request.user.id}:v{version}:{query_hash}"

        # 尝试从缓存获取数据
        cached_response_data = cache.get(cache_key)
        if cached_response_data:
            # 缓存命中
            return Response(cached_response_data)

        # 缓存未命中,执行原始的查询和序列化逻辑
        response = super().list(request, *args, **kwargs)

        # 将返回结果的数据部分写入缓存
        # 我们只缓存 response.data,因为 Response 对象本身不能被 pickle
        cache.set(cache_key, response.data, timeout=3600)  # 设置TTL为1h

        return response

下面我们新建一个信号相关的文件customer/signals.py:

from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.core.cache import cache
from .models import Customer

@receiver([post_save, post_delete], sender=Customer)
def update_customer_version(sender, instance, **kwargs):
    """
    当 Customer 模型实例被保存或删除后,递增客户数据版本号
    """
    version_key = 'crm:customers:version'
    try:
        cache.incr(version_key)
    except ValueError:  # 如果键不存在,Django的cache.incr会抛出ValueError
        cache.set(version_key, 2)  # 设为2,因为初始版本是1

← 返回