Python 高手编程系列三千三百九十八:非确定性缓存
非确定性函数的缓存比记忆化更复杂。事实上由于这样的函数的每次执行可能给出不同的结果通常无法使用先前的很长时间的值。你需要做的是判断一个缓存的值的有效时间。在定义的时间段过去之后所存储的结果被认为是陈旧的并且高速缓存需要通过新值来刷新。非确定性函数的缓存通常依赖于某些外部状态这些状态在应用程序代码中很难跟踪。典型的示例组件如下。• 关系型数据库以及常用的任何类型的结构化数据存储引擎。• 通过网络连接Web API访问的第三方服务。• 文件系统。因此换句话说当你暂时使用预先计算的结果而不确定它们的表示状态是否与其他系统组件通常是后台服务的状态一致时在这种情况下可以使用非确定性缓存。注意这种缓存的实现显然是一种权衡。因此它在某种程度上与我们在 12.4“架构体系的权衡”中介绍的技术相关。如果每次都你舍弃从运行部分代码中得到的结果而是使用过去保存的结果那么你将面临使用过时的或表示不一致的系统状态的风险。这样你正在以性能和速度交换正确性且或完整性。当然只要与高速缓存交互所花费的时间小于函数所花费的时间这样的高速缓存就是高效的。如果它比简单重新计算的值更快一切手段都这样做这就是为什么只有在它值得的时候才会使用缓存合适的使用缓存有一定的代价。缓存的实际东西通常是与系统的其他组件交互的整个结果。如果要在与数据库通信时节省时间和资源那么昂贵的查询是值得缓存的。如果要减少 I/O 操作的数量你可能想要缓存非常频繁访问的文件例如配置文件的内容。缓存非确定性函数的技术实际上与缓存确定性函数中使用的技术非常相似。最显着的区别是它们通常需要选项根据其年龄使缓存的值无效。这意味着来自 functools 模块的 lru_cache()装饰器在这种情况下的使用将非常有限。扩展此功能以提供过期的特性应该不会太难所以我把它作为一个练习留给你。缓存服务我们说非确定性缓存可以使用本地进程内存实现但实际上很少这样做。这是因为本地进程内存在实用程序中作为用于在大型应用程序中的缓存存储器将会受到一定的限制。如果遇到这种情况当非确定性缓存是解决性能问题的首选解决方案时通常需要更多的解决方案。通常当你需要同时向多个用户提供数据或服务时非确定性缓存是你必须具有解决方案。如果是真的那么迟早你需要确保可以同时并发地为用户提供服务。虽然本地内存提供了一种在多个线程之间共享数据的方法但它可能不是适合所有应用程序的最佳并发模型。它不能很好地扩展所以你最终需要将应用程序作为多个进程来运行。如果你足够幸运你可能需要在数百或数千台机器上运行你的应用程序。如果你希望将高速缓存的值存储在本地内存中则意味着你的高速缓存需要在每个需要它的进程上复制一份。这不仅是整个资源的浪费。如果每个进程都有自己的缓存这已经是速度和一致性之间的权衡你如何保证所有缓存彼此一致特别是对于分布式后端的 Web 应用程序后续请求的一致性是一个严重的问题。在复杂的分布式系统中总是确保同一机器上托管的同一进程始终一致地为用户提供服务是非常困难的。这当然是可以在一定程度上但一旦你解决了这个问题还会出现很多其他的问题。如果你正在做一个需要服务多个并发用户的应用程序那么处理非确定性缓存的最好方法是使用一些专用服务。使用 Redis 或 Memcached 等工具这可以让你的所有应用程序进程共享相同的缓存结果。这既减少了宝贵的计算资源的使用又解决了由多个独立并且不一致的缓存引起的问题。Memcached如果你对缓存感兴趣Memcached 是一个非常受欢迎和久经考验的解决方案。一些大型应用程序如 Facebook 或维基百科使用此缓存服务器扩展其网站。在简单的缓存特性中它具有集群功能使得可以立即建立高效的分布式缓存系统。该工具是基于 Unix 的它可以运行于很多平台上并且很多编程语言都可以使用它。有许多 Python 客户端它们彼此略有不同但基本用法通常是相同的。与 Memcached 的简单交互主要有以下 3 个方法。• set(keyvalue)保存给定键的值。• 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’, expire10)取回 some_key 的值result client.get(‘some_key’)Memcached 的缺点之一是它被设计为将值存储为字符串或二进制块并且这与每个原生 Python 类型不兼容。实际上它只兼容一种类型—字符串。这意味着更复杂的类型需要被序列化以便可以成功存储在 Memcached 中。通常使用 JSON 序列化简单的数据结构。这里有一个在 pymemcached 中使用 JSON 序列化的例子import jsonfrom pymemcache.client.base import Clientdef json_serializer(key, value):if type(value) str:return value, 1return json.dumps(value), 2def json_deserializer(key, value,flags):if flags 1:return valueif flags 2:return json.loads(value)raise Exception(“Unknown serialization format”)client Client((‘localhost’, 11211), serializerjson_serializer,deserializerjson_deserializer)client.set(‘key’, {‘a’:‘b’, ‘c’:‘d’})result client.get(‘key’)另一个在使用每个缓存服务时非常常见的问题是在使用基于键/值存储原则的缓存服务时如何选择合适的键名称。如果缓存具有基本参数的简单函数调用对于这种情况问题通常很简单。你可以将函数名称及其参数转换为字符串并将它们连接在一起。你唯一需要关心的是如果你在应用程序的许多部分使用缓存要确保在为不同的函数创建的键之间没有冲突。更棘手的情况是缓存函数具有由字典或自定义类组成的复杂参数。在这种情况下你需要找到一种方法以一致的方式将这种调用签名转换为高速缓存的键。最后一个问题是Memcached 和许多其他缓存服务一样不喜欢很长的字符串作为键。通常键越短越好。长键可能会降低性能或只是不适合硬编码的服务限制。例如如果缓存整个 SQL 查询查询字符串本身通常是很好的唯一标识符可以用作键。但另一方面复杂的查询通常太长不能存储在典型的缓存服务中如 Memcached。通常的做法是计算MD5、SHA 或任何其他散列函数并将其用作缓存键。Python 标准库有一个 hashlib 模块它提供了几个常用的哈希算法的实现。记住计算哈希是有代价的。然而有时它是唯一可行的解决方案。当处理复杂类型时需要为这些将要使用的类型创建缓存的键它也是非常有用的技术。使用哈希函数时要注意的一个重要的事情是哈希冲突。没有哈希函数能保证冲突永远不会发生所以总是要确保知道这种可能性并且谨记这种风险。小结在本章中你学到了以下内容。• 如何定义代码的复杂度和一些降低复杂度的方法。• 如何从架构权衡的角度来提高性能。• 什么是缓存以及如何使用它来提高应用程序性能。在前面的方法中我们的优化努力主要集中在单个进程中。我们试图减少代码复杂度选择更好的数据类型或复用旧的函数结果。如果这些方法没有帮助我们使用近似少做或者延迟一会做通过这些方式做一些权衡。在下一章中我们将学习 Python 中的并发以及并行处理的技术。