缓存笔记(一)
缓存是通过将频繁读取的数据复制到应用程序的快速存储中,实现提高系统的性能和可伸缩性的技术。
当应用实例重复访问相同数据
时,特别是原始数据源相对较慢
、受竞态条件限制
时,或当网络延迟
可能导致访问速度慢时,缓存最有有效。
缓存类型
常见的缓存有两种:
- 内存缓存(In-Memory Caching)。其数据在运行应用实例的计算机本地保存
- 共享缓存(Shared Caching)。可在不同计算机上运行并可由多个实力访问的应用程序(如redis)
内存缓存
缓存的最基本类型是内存存储,存储在单个进程的地址空间中,并由该进程中运行的代码直接访问。内存缓存访问非常快,并有多种策略来保证存储少量的静态数据(缓存大小通常受托管进程的计算机上可用内存限制)。如果使用该模型同时运行的应用程序的多个实例,则每个应用程序实例将拥有自己的独立缓存,其中包含自己的数据副本。
图-1 内存缓存模型
内存缓存是数据某个时刻的快照,并不是动态的。在不同实例中保存的数据可能出现版本不同,实例查询出来的数据也不同
共享缓存
共享缓存通过将缓存放置在单独的位置(进程、服务器),来确保不同的应用实例访问相同的缓存数据。不用担心每个应用实例拿到的缓存数据不同。
图-2 共享缓存模型
共享缓存提供非常好的延展性,可由单机或者集群实现。但访问速度慢于内存缓存。
注意事项
缓存适用于读多写少的数据。
数据类型与填充策略
正确使用缓存的关键在于确定合适的数据,并在合适的时间对其缓存。通常使用两种填充策略:
- 数据在第一次被访问时写入缓存。只需要访问一次底层数据源。
- 数据在程序启动时全部(部分)写入缓存。不建议在大型缓存中使用,启动时会给底层数据源施加高负载。
填充策略的选择通常需要使用模式进行分析。
缓存可以处理不可变或者不常变的数据,但对与动态数据不太有用。缓存动态数据,当原始数据变更时,缓存数据会很快过期,保持缓存与原始数据同步的开销会降低缓存的效率。
缓存中的数据都是临时的,不要将有价值的数据存储在缓存中。如缓存不可用,可用最大程度的保证数据不丢失。
直读(Read-Through)、直写(Write-Through)和回写(Write-behind)
-
Read-throug
当应用系统向缓存服务请求数据时(例如使用key=x向缓存请求数据),如果缓存中并没有对应的数据存在(key=x的value不存在),缓存服务将向底层数据源的读取数据。如果数据在缓存中存在(命中key=x),则直接返回缓存中存在的数据。直读有效地按需缓存数据。
访问无效key可能缓存穿透
-
Write-Through
当应用实例对缓存中的数据进行更新时(例如调用put方法更新或添加条目),缓存系统会同步更新缓存数据和底层数据源。
图-3 直写流程
-
Write-behind
当应用系统对缓存中的数据进行更新时(例如调用put方法更新或添加条目),缓存系统会在指定的时间后向底层数据源更新数据。
图-4 回写流程
通常情况下,直读直写模式就能满足缓存需求。在数据变动频繁,不需要立即更新数据的情况下,使用回写更优。如应用实例修改缓存中的数据,并很快再次更新,运用回写就能避免一次数据源的写操作,较少数据竞争,提升性能。但如果缓存服务宕机后无法重建,或者系统要求数据变化日志的,无法使用该模式。
数据过期
通常情况下,缓存都是数据副本。在更新或者数据过期时,缓存数据将被删除,当应用实例重新查询是重新写入缓存。
合理的过期时间设置变得尤为重要。如果将其设置得太短,则对象将很快过期,从而降低使用缓存的好处,增加数据源读取。如果将时间段设置得太长,则可能会导致与数据源不同步。
数据长时间保持驻留,可能缓存写满,此时需要一个合理的数据淘汰策略,防止程序崩溃。
单数据过期可能缓存击穿,大量过期可能雪崩
并发管理
缓存通常供多个实例共享读取和修改其中的数据,也逃不过并发问题。根据数据的性质和冲突的可能性,可以采用两种并发方法之一:
- <b>乐观(Optimistic)</b> 应用程序更新缓存之前要检查缓存中的数据是否被更新改过。如果数据仍然相同,则可以进行更改。否则,应用程序必须决定是否对其进行更新。此方法适用于不经常更新或不太可能发生冲突的情况。
- <b>悲观(Pessimistic)</b> 应用程序在查询数据时将其锁定缓存中数据,以防止另一个实例更改数据。此过程可确保不会发生冲突,但会阻止其他需要处理相同数据的实例。悲观并发会影响解决方案的可伸缩性,并且仅应用于短暂的操作。此方法可能适用于发生冲突的可能性更高的情况,尤其是当应用程序更新缓存中的多个项目,并且必须确保一致性时。
参考资料