CodeRoadMap
路线图学习路径文章题库资源社区

浏览

首页路线图学习路径知识库题库文章资源社区我的学习
CodeRoadMap

中文编程学习导航:路线图、讲义与题库,进度可同步。

路线图学习路径文章题库社区

© 2026 CodeRoadMap

津ICP备2026012044号-1|coderoadmap@126.com
首页/Python 100 天/56.使用缓存

课程目录

  1. 0101.初识Python
  2. 0202.第一个Python程序
  3. 0303.Python语言中的变量
  4. 0404.Python语言中的运算符
  5. 0505.分支结构
  6. 0606.循环结构
  7. 0707.分支和循环结构实战
  8. 0808.常用数据结构之列表-1
  9. 0909.常用数据结构之列表-2
  10. 1010.常用数据结构之元组
  11. 1111.常用数据结构之字符串
  12. 1212.常用数据结构之集合
  13. 1313.常用数据结构之字典
  14. 1414.函数和模块
  15. 1515.函数应用实战
  16. 1616.函数使用进阶
  17. 1717.函数高级应用
  18. 1818.面向对象编程入门
  19. 1919.面向对象编程进阶
  20. 2020.面向对象编程应用
  21. 2121.文件读写和异常处理
  22. 2222.对象的序列化和反序列化
  23. 2323.Python读写CSV文件
  24. 2424.Python读写Excel文件-1
  25. 2525.Python读写Excel文件-2
  26. 2626.Python操作Word和PowerPoint文件
  27. 2727.Python操作PDF文件
  28. 2828.Python处理图像
  29. 2929.Python发送邮件和短信
  30. 3030.正则表达式的应用
  31. 3131.Python语言进阶
  32. 3232-33.Web前端入门
  33. 3334-35.玩转Linux操作系统
  34. 3436.关系型数据库和MySQL概述
  35. 3537.SQL详解之DDL
  36. 3638.SQL详解之DML
  37. 3739.SQL详解之DQL
  38. 3840.SQL详解之DCL
  39. 3941.MySQL新特性
  40. 4042.视图、函数和过程
  41. 4143.索引
  42. 4244.Python接入MySQL数据库
  43. 4345.Hive实战
  44. 4446.Django快速上手
  45. 4547.深入模型
  46. 4648.静态资源和Ajax请求
  47. 4749.Cookie和Session
  48. 4850.制作报表
  49. 4951.日志和调试工具栏
  50. 5052.中间件的应用
  51. 5153.前后端分离开发入门
  52. 5254.RESTful架构和DRF入门
  53. 5355.RESTful架构和DRF进阶
  54. 5456.使用缓存
  55. 5557.接入三方平台
  56. 5658.异步任务和定时任务
  57. 5759.单元测试
  58. 5860.项目上线
  59. 5961.网络数据采集概述
  60. 6062.用Python获取网络资源-1
  61. 6162.用Python解析HTML页面-2
  62. 6263.并发编程在爬虫中的应用
  63. 6363.Python中的并发编程-1
  64. 6463.Python中的并发编程-2
  65. 6563.Python中的并发编程-3
  66. 6664.使用Selenium抓取网页动态内容
  67. 6765.爬虫框架Scrapy简介
  68. 6866.数据分析概述
  69. 6967.环境准备
  70. 7068.NumPy的应用-1
  71. 7169.NumPy的应用-2
  72. 7270.NumPy的应用-3
  73. 7371.NumPy的应用-4
  74. 7472.深入浅出pandas-1
  75. 7573.深入浅出pandas-2
  76. 7674.深入浅出pandas-3
  77. 7775.深入浅出pandas-4
  78. 7876.深入浅出pandas-5
  79. 7977.深入浅出pandas-6
  80. 8078.数据可视化-1
  81. 8179.数据可视化-2
  82. 8280.数据可视化-3
  83. 8381.浅谈机器学习
  84. 8482.k最近邻算法
  85. 8583.决策树和随机森林
  86. 8684.朴素贝叶斯算法
  87. 8785.回归模型
  88. 8886.K-Means聚类算法
  89. 8987.集成学习算法
  90. 9088.神经网络模型
  91. 9189.自然语言处理入门
  92. 9290.机器学习实战
  93. 9391.团队项目开发的问题和解决方案
  94. 9492.Docker容器技术详解
  95. 9593.MySQL性能优化
  96. 9694.网络API接口设计
  97. 9795.使用Django开发商业项目
  98. 9896.软件测试和自动化测试
  99. 9997.电商网站技术要点剖析
  100. 10098.项目部署上线和性能调优
  101. 10199.面试中的公共问题
  102. 102100.补充内容
第 54 课1 天

56.使用缓存

通常情况下,Web应用的性能瓶颈都会出现在关系型数据库上,当并发访问量较大时,如果所有的请求都需要通过关系型数据库完成数据持久化操作,那么数据库一定会不堪重负。

课程内容

使用缓存

通常情况下,Web应用的性能瓶颈都会出现在关系型数据库上,当并发访问量较大时,如果所有的请求都需要通过关系型数据库完成数据持久化操作,那么数据库一定会不堪重负。优化Web应用性能最为重要的一点就是使用缓存,把那些数据体量不大但访问频率非常高的数据提前加载到缓存服务器中,这又是典型的空间换时间的方法。通常缓存服务器都是直接将数据置于内存中而且使用了非常高效的数据存取策略(哈希存储、键值对方式等),在读写性能上远远优于关系型数据库的,因此我们可以让Web应用接入缓存服务器来优化其性能,其中一个非常好的选择就是使用Redis。

Web应用的缓存架构大致如下图所示。

Django项目接入Redis

在此前的课程中,我们介绍过Redis的安装和使用,此处不再进行赘述。如果需要在Django项目中接入Redis,可以使用三方库django-redis,这个三方库又依赖了一个名为redis 的三方库,它封装了对Redis的各种操作。

安装django-redis。

pip install django-redis

修改Django配置文件中关于缓存的配置。

CACHES = {
    'default': {
        # 指定通过django-redis接入Redis服务
        'BACKEND': 'django_redis.cache.RedisCache',
        # Redis服务器的URL
        'LOCATION': ['redis://1.2.3.4:6379/0', ],
        # Redis中键的前缀(解决命名冲突)
        'KEY_PREFIX': 'vote',
        # 其他的配置选项
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
            # 连接池(预置若干备用的Redis连接)参数
            'CONNECTION_POOL_KWARGS': {
                # 最大连接数
                'max_connections': 512,
            },
            # 连接Redis的用户口令
            'PASSWORD': 'foobared',
        }
    },
}

至此,我们的Django项目已经可以接入Redis,接下来我们修改项目代码,用Redis为之写的获取学科数据的接口提供缓存服务。

为视图提供缓存服务

声明式缓存

所谓声明式缓存是指不修改原来的代码,通过Python中的装饰器(代理)为原有的代码增加缓存功能。对于FBV,代码如下所示。

from django.views.decorators.cache import cache_page


@api_view(('GET', ))
@cache_page(timeout=86400, cache='default')
def show_subjects(request):
    """获取学科数据"""
    queryset = Subject.objects.all()
    data = SubjectSerializer(queryset, many=True).data
    return Response({'code': 20000, 'subjects': data})

上面的代码通过Django封装的cache_page装饰器缓存了视图函数的返回值(响应对象),cache_page的本意是缓存视图函数渲染的页面,对于返回JSON数据的视图函数,相当于是缓存了JSON数据。在使用cache_page装饰器时,可以传入timeout参数来指定缓存过期时间,还可以使用cache参数来指定需要使用哪一组缓存服务来缓存数据。Django项目允许在配置文件中配置多组缓存服务,上面的cache='default'指定了使用默认的缓存服务(因为之前的配置文件中我们也只配置了名为default的缓存服务)。视图函数的返回值会被序列化成字节串放到Redis中(Redis中的str类型可以接收字节串),缓存数据的序列化和反序列化也不需要我们自己处理,因为cache_page装饰器会调用django-redis库中的RedisCache来对接Redis,该类使用了DefaultClient来连接Redis并使用了pickle序列化,django_redis.serializers.pickle.PickleSerializer是默认的序列化类。

如果缓存中没有学科的数据,那么通过接口访问学科数据时,我们的视图函数会通过执行Subject.objects.all()向数据库发出SQL语句来获得数据,视图函数的返回值会被缓存,因此下次请求该视图函数如果缓存没有过期,可以直接从缓存中获取视图函数的返回值,无需再次查询数据库。如果想了解缓存的使用情况,可以配置数据库日志或者使用Django-Debug-Toolbar来查看,第一次访问学科数据接口时会看到查询学科数据的SQL语句,再次获取学科数据时,不会再向数据库发出SQL语句,因为可以直接从缓存中获取数据。

对于CBV,可以利用Django中名为method_decorator的装饰器将cache_page这个装饰函数的装饰器放到类中的方法上,效果跟上面的代码是一样的。需要提醒大家注意的是,cache_page装饰器不能直接放在类上,因为它是装饰函数的装饰器,所以Django框架才提供了method_decorator来解决这个问题,很显然,method_decorator是一个装饰类的装饰器。

from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page


@method_decorator(decorator=cache_page(timeout=86400, cache='default'), name='get')
class SubjectView(ListAPIView):
    """获取学科数据的视图类"""
    queryset = Subject.objects.all()
    serializer_class = SubjectSerializer

编程式缓存

所谓编程式缓存是指通过自己编写的代码来使用缓存服务,这种方式虽然代码量会稍微大一些,但是相较于声明式缓存,它对缓存的操作和使用更加灵活,在实际开发中使用得更多。下面的代码去掉了之前使用的cache_page装饰器,通过django-redis提供的get_redis_connection函数直接获取Redis连接来操作Redis。

def show_subjects(request):
    """获取学科数据"""
    redis_cli = get_redis_connection()
    # 先尝试从缓存中获取学科数据
    data = redis_cli.get('vote:polls:subjects')
    if data:
        # 如果获取到学科数据就进行反序列化操作
        data = json.loads(data)
    else:
        # 如果缓存中没有获取到学科数据就查询数据库
        queryset = Subject.objects.all()
        data = SubjectSerializer(queryset, many=True).data
        # 将查到的学科数据序列化后放到缓存中
        redis_cli.set('vote:polls:subjects', json.dumps(data), ex=86400)
    return Response({'code': 20000, 'subjects': data})

需要说明的是,Django框架提供了cache和caches两个现成的变量来支持缓存操作,前者访问的是默认的缓存(名为default的缓存),后者可以通过索引运算获取指定的缓存服务(例如:caches['default'])。向cache对象发送get和set消息就可以实现对缓存的读和写操作,但是这种方式能做的操作有限,不如上面代码中使用的方式灵活。还有一个值得注意的地方,由于可以通过get_redis_connection函数获得的Redis连接对象向Redis发起各种操作,包括FLUSHDB、SHUTDOWN等危险的操作,所以在实际商业项目开发中,一般都会对django-redis再做一次封装,例如封装一个工具类,其中只提供了项目需要用到的缓存操作的方法,从而避免了直接使用get_redis_connection的潜在风险。当然,自己封装对缓存的操作还可以使用“Read Through”和“Write Through”的方式实现对缓存的更新,这个在下面会介绍到。

缓存相关问题

缓存数据的更新

在使用缓存时,一个必须搞清楚的问题就是,当数据改变时,如何更新缓存中的数据。通常更新缓存有如下几种套路,分别是:

  1. Cache Aside Pattern
  2. Read/Write Through Pattern
  3. Write Behind Caching Pattern

第1种方式的具体做法就是,当数据更新时,先更新数据库,再删除缓存。注意,不能够使用先更新数据库再更新缓存的方式,也不能够使用先删除缓存再更新数据库的方式,大家可以自己想一想为什么(考虑一下有并发的读操作和写操作的场景)。当然,先更新数据库再删除缓存的做法在理论上也存在风险,但是发生问题的概率是极低的,所以不少的项目都使用了这种方式。

第1种方式相当于编写业务代码的开发者要自己负责对两套存储系统(缓存和关系型数据库)的操作,代码写起来非常的繁琐。第2种方式的主旨是将后端的存储系统变成一套代码,对缓存的维护封装在这套代码中。其中,Read Through指在查询操作中更新缓存,也就是说,当缓存失效的时候,由缓存服务自己负责对数据的加载,从而对应用方是透明的;而Write Through是指在更新数据时,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由缓存服务自己更新数据库(同步更新)。刚才我们说过,如果自己对项目中的Redis操作再做一次封装,就可以实现“Read Through”和“Write Through”模式,这样做虽然会增加工作量,但无疑是一件“一劳永逸”且“功在千秋”的事情。

第3种方式是在更新数据的时候,只更新缓存,不更新数据库,而缓存服务这边会异步的批量更新数据库。这种做法会大幅度提升性能,但代价是牺牲数据的强一致性。第3种方式的实现逻辑比较复杂,因为他需要追踪有哪数据是被更新了的,然后再批量的刷新到持久层上。

缓存穿透

缓存是为了缓解数据库压力而添加的一个中间层,如果恶意的访问者频繁的访问缓存中没有的数据,那么缓存就失去了存在的意义,瞬间所有请求的压力都落在了数据库上,这样会导致数据库承载着巨大的压力甚至连接异常,类似于分布式拒绝服务攻击(DDoS)的做法。解决缓存穿透的一个办法是约定如果查询返回为空值,把这个空值也缓存起来,但是需要为这个空值的缓存设置一个较短的超时时间,毕竟缓存这样的值就是对缓存空间的浪费。另一个解决缓存穿透的办法是使用布隆过滤器,具体的做法大家可以自行了解。

缓存击穿

在实际的项目中,可能存在某个缓存的key某个时间点过期,但恰好在这个时间点对有对该key的大量的并发请求过来,这些请求没有从缓存中找到key对应的数据,就会直接从数据库中获取数据并写回到缓存,这个时候大并发的请求可能会瞬间把数据库压垮,这种现象称为缓存击穿。比较常见的解决缓存击穿的办法是使用互斥锁,简单的说就是在缓存失效的时候,不是立即去数据库加载数据,而是先设置互斥锁(例如:Redis中的setnx),只有设置互斥锁的操作成功的请求,才能执行查询从数据库中加载数据并写入缓存,其他设置互斥锁失败的请求,可以先执行一个短暂的休眠,然后尝试重新从缓存中获取数据,如果缓存还没有数据,则重复刚才的设置互斥锁的操作,大致的参考代码如下所示。

data = redis_cli.get(key)
while not data:
    if redis_cli.setnx('mutex', 'x'):
        redis.expire('mutex', timeout)
        data = db.query(...)
        redis.set(key, data)
        redis.delete('mutex')
    else:
        time.sleep(0.1)
        data = redis_cli.get(key)

缓存雪崩

缓存雪崩是指在将数据放入缓存时采用了相同的过期时间,这样就导致缓存在某一时刻同时失效,请求全部转发到数据库,导致数据库瞬时压力过大而崩溃。解决缓存雪崩问题的方法也比较简单,可以在既定的缓存过期时间上加一个随机时间,这样可以从一定程度上避免不同的key在同一时间集体失效。还有一种办法就是使用多级缓存,每一级缓存的过期时间都不一样,这样的话即便某个级别的缓存集体失效,但是其他级别的缓存还能够提供数据,避免所有的请求都落到数据库上。

← 上一课55.RESTful架构和DRF进阶下一课 →57.接入三方平台