点赞、收藏、加关注,下次找我不迷路
一、啥是缓存?先把概念搞明白
好多新手一听到 "缓存" 就犯迷糊,其实这玩意儿没那么玄乎。咱打个比方,你去餐厅吃饭,厨师做菜之前,会先把常用的食材比如土豆、洋葱啥的提前切好放在备菜间,这样炒菜的时候直接拿就能用,不用现切,节省时间。这备菜间就是缓存,缓存就是把常用的数据提前存放在一个快速访问的地方,下次需要的时候直接拿出来用,不用再重新计算或者查询,能大大提高程序的运行效率。
比如说,你写了一个函数,用来计算一个很复杂的数值,每次调用都得花好几秒。如果这个函数需要频繁调用,那每次都重新计算就太浪费时间了。这时候就可以把计算结果缓存起来,第一次计算完存起来,后面再调用的时候直接返回缓存的结果,瞬间就快了。
二、缓存有啥用?好处多到你想不到
(一)提高程序运行效率
就像刚才说的计算复杂数值的函数,用了缓存之后,不用重复计算,时间直接节省下来了。尤其是在一些需要频繁调用的函数或者接口中,效果特别明显。比如一个电商网站的商品详情页,每次用户访问都要查询数据库获取商品信息,如果把这些信息缓存起来,用户再次访问的时候就能更快地显示出来,用户体验也更好。
(二)节省资源
重新计算或者查询数据需要消耗 CPU、内存、数据库连接等资源。用了缓存之后,减少了这些操作的次数,也就节省了资源。比如数据库,频繁的查询会给数据库带来很大的压力,甚至可能导致数据库性能下降,而缓存可以分担一部分压力,让数据库更稳定。
(三)提升用户体验
用户最讨厌的就是等待,页面加载慢、接口响应慢都会让用户很不爽。缓存可以让数据更快地返回,减少用户的等待时间,提升用户对系统的满意度。比如一个手机 APP,用户刷新页面时,如果数据是从缓存中获取的,瞬间就能显示出来,用户就会觉得这个 APP 很流畅。
三、Python 里的缓存技巧
(一)内置的 lru_cache 装饰器:简单好用,新手必备
Python 的 functools 模块里有一个 lru_cache 装饰器,简直是新手福音,用起来超简单。lru 是 Least Recently Used 的缩写,意思是最近最少使用,也就是说它会把最近最少使用的缓存数据淘汰掉,以保持缓存的大小在一定范围内。
1. 怎么用?
首先,你得导入 functools 模块里的 lru_cache 装饰器:
from functools import lru_cache
然后,在你想要缓存的函数上面加上 @lru_cache 装饰器就行了。比如说,我们有一个计算斐波那契数列的函数,斐波那契数列的计算是递归的,每次计算都需要重复计算很多中间值,效率很低,这时候就可以用 lru_cache 来缓存结果。
@lru_cache(maxsize=None)
def fib(n):
if n <= 1:
return n
else:
return fib(n-1) + fib(n-2)
这里的 maxsize 参数是用来设置缓存的最大大小的,默认是 128,如果设置为 None,就表示不限制缓存大小,但要注意,这可能会导致内存使用过多。还有一个参数 typed,默认是 False,如果设置为 True,会把不同类型的参数视为不同的键,比如 1 和 1.0 会被视为不同的参数,缓存不同的结果。
2. 效果咋样?用数据说话
我们来对比一下没加缓存和加了缓存的斐波那契函数的运行时间。我们计算 fib (30),没加缓存的时候,运行时间大概是 0.1 秒左右;加了缓存之后,第一次运行时间也是 0.1 秒左右,但是第二次运行的时候,直接从缓存中获取结果,运行时间几乎为 0。我们用表格来呈现一下:
情况 | 第一次运行时间(秒) | 第二次运行时间(秒) |
没加缓存 | 0.1 | 0.1 |
加了缓存 | 0.1 | 0.0001 |
可以看到,加了缓存之后,第二次运行时间大大缩短,效率提升明显。
(二)lru_cache 的局限性:别以为它万能,这些情况要注意
虽然 lru_cache 很好用,但它也有一些局限性。首先,它只能缓存那些参数是可哈希的函数,也就是说,参数必须是不可变类型,比如整数、字符串、元组等,如果你传递的是列表、字典等可变类型,就会报错。其次,它的缓存是存储在内存中的,如果数据量很大,会占用很多内存,可能会导致内存不足的问题。另外,它的缓存不会自动过期,一旦缓存了数据,就会一直存在,直到缓存大小超过 maxsize 被淘汰或者程序结束。
比如说,我们有一个函数,需要根据当前时间获取一些数据,每次调用的时间不同,结果也不同,但如果用 lru_cache 缓存的话,它会把不同时间的调用视为不同的参数,缓存不同的结果,这没问题。但是如果我们希望缓存的数据在一段时间后自动过期,比如 10 分钟后就不再使用缓存,lru_cache 就做不到了,这时候就需要用到其他的缓存方案。
(三)其他缓存方案:根据场景选合适的
1. functools.lru_cache 结合 ttl:给缓存加个过期时间
如果我们需要给缓存设置一个过期时间,比如 10 分钟后缓存失效,这时候可以结合使用 functools.lru_cache 和一个自定义的装饰器来实现。不过 Python 本身没有直接提供 ttl(生存时间)功能,需要我们自己实现。
我们可以写一个装饰器,在每次调用函数的时候,检查缓存是否过期,如果过期了就重新计算并更新缓存。这里给大家简单举个例子:
from functools import lru_cache
import time
def ttl_cache(ttl_seconds):
def decorator(func):
@lru_cache(maxsize=None)
def wrapper(*args, **kwargs, __ttl_time=None):
if __ttl_time is None:
__ttl_time = time.time()
current_time = time.time()
if current_time - __ttl_time > ttl_seconds:
# 缓存过期,重新计算
result = func(*args, **kwargs)
wrapper(*args, **kwargs, __ttl_time=current_time) # 更新缓存
return result
else:
return func(*args, **kwargs, __ttl_time=__ttl_time)
return wrapper
return decorator
@ttl_cache(ttl_seconds=60) # 设置缓存过期时间为60秒
def get_data():
# 模拟获取数据的操作,比如查询数据库
time.sleep(2)
return "最新数据"
这样,每次调用 get_data 函数时,缓存会在 60 秒后过期,重新获取数据。
2. 第三方库 cachetools:功能更强大,满足更多需求
如果觉得自己实现 ttl 比较麻烦,也可以使用第三方库 cachetools,它提供了更强大的缓存功能,包括 TTL 缓存、LRU 缓存等。首先,你需要安装 cachetools:
pip install cachetools
(1)TTLCache:按时间过期
TTLCache 可以设置每个缓存项的生存时间,超过时间就会自动失效。使用起来很简单:
from cachetools import TTLCache
cache = TTLCache(maxsize=100, ttl=60) # maxsize是缓存最大大小,ttl是生存时间(秒)
def get_data(key):
if key in cache:
return cache[key]
# 模拟获取数据的操作
data = "数据" + key
cache[key] = data
return data
这样,每个缓存项在 60 秒后就会失效。
(2)LRUCache:按最近最少使用淘汰
LRUCache 和 functools 的 lru_cache 类似,都是根据最近最少使用来淘汰缓存项,但 cachetools 的 LRUCache 可以更灵活地设置参数。
from cachetools import LRUCache
cache = LRUCache(maxsize=100) # maxsize是缓存最大大小
def get_data(key):
if key in cache:
return cache[key]
# 模拟获取数据的操作
data = "数据" + key
cache[key] = data
return data
3. 数据库缓存:Redis,分布式场景必备
如果你的程序是分布式的,多个服务器节点需要共享缓存,这时候就需要用到数据库缓存,比如 Redis。Redis 是一个高性能的键值对存储数据库,支持丰富的数据结构,并且可以设置缓存过期时间,还支持持久化等功能。
使用 Redis 缓存也很简单,首先需要安装 redis-py 库:
pip install redis
然后连接 Redis 服务器,进行缓存操作:
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
def get_data(key):
data = r.get(key)
if data:
return data.decode()
# 模拟获取数据的操作,比如查询数据库
data = "数据" + key
r.set(key, data)
r.expire(key, 60) # 设置缓存过期时间为60秒
return data
Redis 适合在分布式系统中使用,多个节点可以共享缓存,提高系统的性能和可扩展性。
四、缓存也不是万无一失
(一)缓存穿透:查不到的数据,把缓存打穿了
啥是缓存穿透呢?就是用户频繁查询一个不存在的数据,每次查询都要穿透缓存到数据库或者其他数据源去查询,导致数据库压力很大。比如说,有人恶意攻击,频繁查询一个不存在的用户 ID,每次都要去数据库查,而数据库中根本没有这个数据,缓存也没有,就会导致每次请求都打到数据库上。
怎么避免呢?可以在缓存中存储空值或者默认值,比如当查询一个不存在的数据时,在缓存中存一个空值,设置一个较短的过期时间,这样下次再查询的时候,就可以直接从缓存中获取空值,不用去数据库查了。
(二)缓存雪崩:缓存大面积失效,系统扛不住
缓存雪崩是指在同一时间段内,大量的缓存数据同时失效,导致大量的请求直接打到数据库或者其他数据源上,造成系统压力过大,甚至崩溃。比如说,缓存设置的过期时间都是相同的,当过期时间到达时,所有缓存同时失效,这时候大量请求涌进来,就会出现雪崩。
怎么避免呢?可以给缓存的过期时间加上一个随机值,让缓存的失效时间分散开来,不要集中在同一时间段。比如设置过期时间为 60 秒到 100 秒之间的随机值,这样就不会有大量缓存同时失效了。
(三)缓存和数据库一致性问题:缓存和数据库数据不一样了
当我们更新数据库中的数据时,如果没有及时更新缓存或者删除缓存,就会导致缓存中的数据和数据库中的数据不一致。比如说,你修改了用户的昵称,数据库中已经更新了,但缓存中还是旧的昵称,这时候用户查询时就会得到错误的数据。
怎么解决呢?一般有两种方式,一种是更新数据库的同时,更新缓存;另一种是更新数据库的同时,删除缓存,下次查询时重新从数据库加载数据到缓存。这两种方式各有优缺点,需要根据具体场景选择。比如,对于读多写少的场景,删除缓存可能更合适,因为写操作较少,删除缓存后重新加载的成本较低;对于写频繁的场景,更新缓存可能会带来更多的开销,这时候需要权衡。
说了这么多,总结一下:缓存是提高程序效率的重要技巧,Python 里有内置的 lru_cache 装饰器,简单好用,适合新手;还有第三方库 cachetools,功能更强大,能满足不同的需求;分布式场景下可以用 Redis 作为数据库缓存。同时,也要注意缓存带来的问题,避免踩坑。