Python 高手编程系列三百九十八:非确定性缓存

非确定性函数的缓存比记忆化更复杂。事实上,由于这样的函数的每次执行可能给出
不同的结果,通常无法使用先前的很长时间的值。你需要做的是判断一个缓存的值的有效
时间。在定义的时间段过去之后,所存储的结果被认为是陈旧的,并且高速缓存需要通过
新值来刷新。
非确定性函数的缓存通常依赖于某些外部状态,这些状态在应用程序代码中很难跟踪。
典型的示例组件如下。
• 关系型数据库以及常用的任何类型的结构化数据存储引擎。
• 通过网络连接(Web API)访问的第三方服务。
• 文件系统。
因此,换句话说,当你暂时使用预先计算的结果,而不确定它们的表示状态是否与其
他系统组件(通常是后台服务)的状态一致时,在这种情况下可以使用非确定性缓存。
注意,这种缓存的实现显然是一种权衡。因此,它在某种程度上与我们在 12.4“架构
体系的权衡”中介绍的技术相关。如果每次都你舍弃从运行部分代码中得到的结果,而是
使用过去保存的结果,那么你将面临使用过时的或表示不一致的系统状态的风险。这样,
你正在以性能和速度交换正确性且(或)完整性。
当然,只要与高速缓存交互所花费的时间小于函数所花费的时间,这样的高速缓存就
是高效的。如果它比简单重新计算的值更快,一切手段都这样做!这就是为什么只有在它
值得的时候才会使用缓存;合适的使用缓存有一定的代价。
缓存的实际东西通常是与系统的其他组件交互的整个结果。如果要在与数据库通信时
节省时间和资源,那么昂贵的查询是值得缓存的。如果要减少 I/O 操作的数量,你可能想
要缓存非常频繁访问的文件(例如配置文件)的内容。
缓存非确定性函数的技术实际上与缓存确定性函数中使用的技术非常相似。最显着的
区别是,它们通常需要选项根据其年龄使缓存的值无效。这意味着来自 functools 模块
的 lru_cache()装饰器在这种情况下的使用将非常有限。扩展此功能以提供过期的特性
应该不会太难,所以我把它作为一个练习留给你。
缓存服务
我们说非确定性缓存可以使用本地进程内存实现,但实际上很少这样做。这是因为本
地进程内存在实用程序中作为用于在大型应用程序中的缓存存储器将会受到一定的限制。
如果遇到这种情况,当非确定性缓存是解决性能问题的首选解决方案时,通常需要更
多的解决方案。通常,当你需要同时向多个用户提供数据或服务时,非确定性缓存是你必
须具有解决方案。如果是真的,那么迟早你需要确保可以同时并发地为用户提供服务。虽
然本地内存提供了一种在多个线程之间共享数据的方法,但它可能不是适合所有应用程序
的最佳并发模型。它不能很好地扩展,所以你最终需要将应用程序作为多个进程来运行。
如果你足够幸运,你可能需要在数百或数千台机器上运行你的应用程序。如果你希望
将高速缓存的值存储在本地内存中,则意味着你的高速缓存需要在每个需要它的进程上复
制一份。这不仅是整个资源的浪费。如果每个进程都有自己的缓存,这已经是速度和一致
性之间的权衡,你如何保证所有缓存彼此一致?
特别是对于分布式后端的 Web 应用程序,后续请求的一致性是一个严重的问题。在复
杂的分布式系统中,总是确保同一机器上托管的同一进程始终一致地为用户提供服务是非
常困难的。这当然是可以在一定程度上,但一旦你解决了这个问题,还会出现很多其他的
问题。
如果你正在做一个需要服务多个并发用户的应用程序,那么处理非确定性缓存的最好
方法是使用一些专用服务。使用 Redis 或 Memcached 等工具,这可以让你的所有应用程序
进程共享相同的缓存结果。这既减少了宝贵的计算资源的使用,又解决了由多个独立并且
不一致的缓存引起的问题。
Memcached
如果你对缓存感兴趣,Memcached 是一个非常受欢迎和久经考验的解决方案。一些大
型应用程序(如 Facebook 或维基百科)使用此缓存服务器扩展其网站。在简单的缓存特性
中,它具有集群功能,使得可以立即建立高效的分布式缓存系统。
该工具是基于 Unix 的,它可以运行于很多平台上,并且很多编程语言都可以使用它。
有许多 Python 客户端,它们彼此略有不同,但基本用法通常是相同的。与 Memcached 的
简单交互主要有以下 3 个方法。
• set(key,value):保存给定键的值。
• get(key):获取给定键的值(如果存在)。
• delete(key):如果存在,删除给定键下的值。
下面是使用一个流行的 Python 包—pymemcached 与 Memcached 集成的示例:
from pymemcache.client.base import Client

在 localhost 的 11211 端口启动 Memcached 客户端

client = Client((‘localhost’, 11211))

将 some_value 以 some_key 为键缓存起来,并且在 10 秒后过期

client.set(‘some_key’, ‘some_value’, expire=10)

取回 some_key 的值

result = client.get(‘some_key’)
Memcached 的缺点之一是它被设计为将值存储为字符串或二进制块,并且这与每个原
生 Python 类型不兼容。实际上,它只兼容一种类型—字符串。这意味着更复杂的类型需
要被序列化,以便可以成功存储在 Memcached 中。通常使用 JSON 序列化简单的数据结构。
这里有一个在 pymemcached 中使用 JSON 序列化的例子:
import json
from pymemcache.client.base import Client
def json_serializer(key, value):
if type(value) == str:
return value, 1
return json.dumps(value), 2
def json_deserializer(key, value,flags):
if flags == 1:
return value
if flags == 2:
return json.loads(value)
raise Exception(“Unknown serialization format”)
client = Client((‘localhost’, 11211), serializer=json_serializer,
deserializer=json_deserializer)
client.set(‘key’, {‘a’:‘b’, ‘c’:‘d’})
result = client.get(‘key’)
另一个在使用每个缓存服务时非常常见的问题是,在使用基于键/值存储原则的缓存服
务时,如何选择合适的键名称。
如果缓存具有基本参数的简单函数调用,对于这种情况,问题通常很简单。你可以将
函数名称及其参数转换为字符串,并将它们连接在一起。你唯一需要关心的是,如果你在
应用程序的许多部分使用缓存,要确保在为不同的函数创建的键之间没有冲突。
更棘手的情况是缓存函数具有由字典或自定义类组成的复杂参数。在这种情况下,你需要找到一种方法,以一致的方式将这种调用签名转换为高速缓存的键。
最后一个问题是,Memcached 和许多其他缓存服务一样,不喜欢很长的字符串作为键。
通常,键越短越好。长键可能会降低性能或只是不适合硬编码的服务限制。例如,如果缓
存整个 SQL 查询,查询字符串本身通常是很好的唯一标识符,可以用作键。但另一方面,
复杂的查询通常太长,不能存储在典型的缓存服务中,如 Memcached。通常的做法是计算
MD5、SHA 或任何其他散列函数,并将其用作缓存键。Python 标准库有一个 hashlib 模块,
它提供了几个常用的哈希算法的实现。
记住,计算哈希是有代价的。然而,有时它是唯一可行的解决方案。当处理复杂类型
时,需要为这些将要使用的类型创建缓存的键,它也是非常有用的技术。使用哈希函数时
要注意的一个重要的事情是哈希冲突。没有哈希函数能保证冲突永远不会发生,所以总是
要确保知道这种可能性并且谨记这种风险。
小结
在本章中,你学到了以下内容。
• 如何定义代码的复杂度和一些降低复杂度的方法。
• 如何从架构权衡的角度来提高性能。
• 什么是缓存,以及如何使用它来提高应用程序性能。
在前面的方法中,我们的优化努力主要集中在单个进程中。我们试图减少代码复杂度,
选择更好的数据类型,或复用旧的函数结果。如果这些方法没有帮助,我们使用近似,少
做,或者延迟一会做,通过这些方式做一些权衡。
在下一章中,我们将学习 Python 中的并发以及并行处理的技术。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值