什么是缓存
术语缓存在计算机中无处不在。在应用程序设计的上下文中,它经常被用来描述应用程序开发人员利用单独的内存或低延迟的数据结构。缓存,用于临时存储或缓存信息的副本或引用,应用程序可能会在稍后的某个时间点重复使用,从而减轻重新访问或重新创建它的成本。
在 JCache 的上下文中,术语缓存描述了 Java 的技术开发人员使用缓存提供程序临时缓存 Java 对象。
什么时候引入缓存
引入缓存会提高系统复杂度,因为你要考虑缓存的失效、更新、一致性等问题。
冒着上述种种风险,仍能说服你引入缓存的理由,总结起来无外乎以下两种:
- 为缓解 CPU 压力而做缓存:譬如把方法运行结果存储起来、把原本要实时计算的内容提前算好、把一些公用的数据进行复用,这可以节省 CPU 算力,顺带提升响应性能。
- 为缓解 I/O 压力而做缓存:譬如把原本对网络、磁盘等较慢介质的读写访问变为对内存等较快介质的访问,将原本对单点部件(如数据库)的读写访问变为到可扩缩部件(如缓存中间件)的访问,顺带提升响应性能。
请注意,缓存虽然是典型以空间换时间来提升性能的手段,但它的出发点是缓解 CPU 和 I/O 资源在峰值流量下的压力,“顺带”而非“专门”地提升响应性能。这里的言外之意是如果可以通过增强 CPU、I/O 本身的性能(譬如扩展服务器的数量)来满足需要的话,那升级硬件往往是更好的解决方案,即使需要一些额外的投入成本,也通常要优于引入缓存后可能带来的风险。
缓存设计模式
Cache Aside
其中最简单、成本最低的 Cache Aside 模式是指:
- 读数据时,先读缓存,缓存没有的话,再读数据源,然后将数据放入缓存,再响应请求。
- 写数据时,先写数据源,然后失效(而不是更新)掉缓存。
读数据方面一般没什么出错的余地,但是写数据时,就有必要专门强调两点:一是先后顺序是先数据源后缓存。试想一下,如果采用先失效缓存后写数据源的顺序,那一定存在一段时间缓存已经删除完毕,但数据源还未修改完成,此时新的查询请求到来,缓存未能命中,就会直接流到真实数据源中。这样请求读到的数据依然是旧数据,随后又重新回填到缓存中。当数据源的修改完成后,结果就成了数据在数据源中是新的,在缓存中是老的,两者就会有不一致的情况。另一点是应当失效缓存,而不是去尝试更新缓存,这很容易理解,如果去更新缓存,更新过程中数据源又被其他请求再次修改的话,缓存又要面临处理多次赋值的复杂时序问题。所以直接失效缓存,等下次用到该数据时自动回填,期间无论数据源中的值被改了多少次都不会造成任何影响。
Cache Aside 模式依然是不能保证在一致性上绝对不出问题的,否则就无须设计出 Paxos 这样复杂的共识算法了。典型的出错场景是如果某个数据是从未被缓存过,请求会直接流到真实数据源中,如果数据源中的写操作发生在查询请求之后,结果回填到缓存之前,也会出现缓存中回填的内容与数据库的实际数据不一致的情况。但这种情况的概率是很低的,Cache Aside 模式仍然是以低成本更新缓存,并且获得相对可靠结果的解决方案。
Read/Write Through
Write Through 将数据同时写入高速缓存和相应的主内存位置。缓存的数据允许按需快速检索,而主存储器中的相同数据可确保在发生崩溃,电源故障或其他系统中断时不会丢失任何内容。
尽管直写可以最大程度地减少数据丢失的风险,但是每个写操作必须执行两次,并且这种冗余需要时间。活动的应用程序必须等待,直到将每个数据块都写入主内存和高速缓存中,然后才能开始下一个操作。因此,“数据保险”是以牺牲系统速度为代价的。
直写是无法容忍数据丢失的应用程序(例如银行和医疗设备控制)中首选的数据存储方法。
Write Behind Caching
回写在每次发生更改时,数据都会先写入高速缓存,但仅在指定的时间间隔或特定条件下,数据才会写入主存储中的相应位置。
JSR107 (JCache)
JCache 是 Java 的缓存 API。它由 JSR107 定义。它定义了供开发人员使用的标准 Java 缓存 API 和供实现者使用的标准 SPI(“服务提供者接口”)。
标准定义文档