使用缓存的常见问题
缓存一致性
出现场景
更新缓存的时候,如果是 先删除缓存,再更新数据库,则可能会导致缓存数据和源数据不一致的。
出现问题的场景如下:
@startuml
Application -[#F00]> Cache: 【更新】删除缓存
Application -[#00F]> Cache: 【查询】读取缓存
note right of Application: 更新操作和查询操作是两个并发请求
Cache -[#F00]> Cache: 【更新】删除缓存成功,但尚未更新数据库
Cache -[#00F]> Database: 【查询】没有命中缓存,读取数据库中的旧数据
Database -[#00F]> Cache: 【查询】更新旧数据至缓存
Cache -[#00F]> Application: 【查询】返回旧数据
Cache -[#F00]> Database: 【更新】更新数据库
Database -[#F00]> Database: 【更新】更新数据成功
Database -[#F00]> Cache: 【更新】没有更新新数据至缓存中
Cache -[#F00]> Application: 【更新】更新数据完成
note right of Cache: 此时缓存中的数据是旧数据
== 后续操作 ==
Application -[#00F]> Cache: 【查询】读取缓存
Cache -[#00F]> Application: 【查询】命中缓存,返回旧数据
@enduml
解决方案
使用缓存时,遵循 Cache Aside Pattern 模式,其具体逻辑如下:
失效:应用程序先从 Cache 获取数据,没有命中时从 Database 获取数据,并且在获取数据成功之后,保存至 Cache 中;
命中:应用程序从 Cache 获取数据,获取数据成功之后,直接返回;
更新:先更新数据至数据库中,更新成功之后,再删除 Cache 中的数据。
使用 Cache Aside Pattern 时,上述场景变更为:
@startuml
Application -[#F00]> Database: 【更新】更新数据库
Application -[#00F]> Cache: 【查询】读取缓存
note right of Application: 更新操作和查询操作是两个并发请求
Cache -[#00F]> Application: 【查询】命中缓存,返回缓存中的旧数据
Database -[#F00]> Database: 【更新】更新数据成功
Database -[#F00]> Cache: 【更新】删除缓存
Cache -[#F00]> Cache: 【更新】删除缓存成功
Cache -[#F00]> Application: 【更新】更新数据完成
note right of Cache: 此时缓存中的没有数据
== 后续操作 ==
Application -[#00F]> Cache: 【查询】读取缓存
Cache -[#00F]> Database: 【查询】没有命中缓存,读取数据库中的新数据
Database -[#00F]> Cache: 【查询】更新新数据至缓存
Cache -[#00F]> Application: 【查询】返回新数据
@enduml
参考资料
缓存穿透(不存在 key)
出现场景
如果应用程序一直查询未命中缓存的 key,那么查询请求便会一直访问数据库。
解决方案
从数据库中查询数据失败时,返回一个默认值,并设置相对短暂的失效时间,这样下次请求就会直接从缓存中获取这个默认值。
缓存雪崩(多个 key 失效)
出现场景
如果多个缓存的 key 同时失效,这样便会出现大量的未命中缓存的请求访问数据库。
解决方案
合理分布 key 的失效时间,例如在原有的失效时间上增加一个随机值。
缓存击穿(热点 key 失效)
出现场景
在高并发场景下,如果一个热点 key 失效,则会出现大量的并发请求访问数据库。
解决方案
方案一
定期从数据库查询数据,并更新到缓存中。但是,对于某些动态拼接的 key,这种方案是无效的。
方案二
对于热点 key,设置两个失效时间:一个是缓存即将失效的时间,另一个是真正的缓存失效时间。在缓存即将失效时,应用程序主动从数据库查询数据,并更新到缓存中。
值得注意的是,在缓存即将失效时,应用程序需要先加锁再访问数据库,避免出现并发访问数据库的场景。
对于热点 key 设置两个失效时间的方式如下:
通过在设置 value 时追加当前时间戳。缺点:需要同步缓存服务器和应用服务器的时间、失效时间与 value 耦合(可能导致取数据的时候需要额外的反序列化);
通过设置两个 key 的方式。缺点:缓存中 key 的数量翻倍。
最后更新于