FantasticMao 技术笔记
BlogGitHub
  • README
  • C & Unix
    • C
      • 《C 程序设计语言》笔记
      • C 语言中的陷阱
      • CMake 示例
      • GNU make
      • LLVM Clang
      • Nginx 常用模块
      • Vim 常用命令
    • Unix-like
      • 《深入理解计算机系统》笔记
      • 《UNIX 环境高级编程》笔记 - UNIX 基础知识
      • 《UNIX 环境高级编程》笔记 - 文件 IO
      • 《UNIX 环境高级编程》笔记 - 标准 IO 库
      • 《鳥哥的 Linux 私房菜》笔记 - 目录配置
      • 《鳥哥的 Linux 私房菜》笔记 - 认识与学习 bash
      • 《鳥哥的 Linux 私房菜》笔记 - 任务管理
      • OpenWrt 中的陷阱
      • iptables 工作机制
  • Go
    • 《A Tour of Go》笔记
    • Go vs C vsJava
    • Go 常用命令
    • Go 语言中的陷阱
  • Java
    • JDK
      • 《Java 并发编程实战》笔记 - 线程池的使用
      • 设计模式概览
      • 集合概览
      • HashMap 内部算法
      • ThreadLocal 工作机制
      • Java Agent
    • JVM
      • 《深入理解 Java 虚拟机》笔记 - Java 内存模型与线程
      • JVM 运行时数据区
      • 类加载机制
      • 垃圾回收算法
      • 引用类型
      • 垃圾收集算法
      • 垃圾收集器
    • Spring
      • Spring IoC 容器扩展点
      • Spring Transaction 声明式事务管理
      • Spring Web MVC DispatcherServlet 工作机制
      • Spring Security Servlet 实现原理
    • 其它
      • 《Netty - One Framework to rule them all》演讲笔记
      • Hystrix 设计与实现
  • JavaScript
    • 《写给大家看的设计书》笔记 - 设计原则
    • 《JavaScript 权威指南》笔记 - jQuery 类库
  • 数据库
    • ElasticSearch
      • ElasticSearch 概览
    • HBase
      • HBase 数据模型
    • Prometheus
      • Prometheus 概览
      • Prometheus 数据模型和指标类型
      • Prometheus 查询语法
      • Prometheus 存储原理
      • Prometheus vs InfluxDB
    • Redis
      • 《Redis 设计与实现》笔记 - 简单动态字符串
      • 《Redis 设计与实现》笔记 - 链表
      • 《Redis 设计与实现》笔记 - 字典
      • 《Redis 设计与实现》笔记 - 跳跃表
      • 《Redis 设计与实现》笔记 - 整数集合
      • 《Redis 设计与实现》笔记 - 压缩列表
      • 《Redis 设计与实现》笔记 - 对象
      • Redis 内存回收策略
      • Redis 实现分布式锁
      • Redis 持久化机制
      • Redis 数据分片方案
      • 使用缓存的常见问题
    • MySQL
      • 《高性能 MySQL》笔记 - Schema 与数据类型优化
      • 《高性能 MySQL》笔记 - 创建高性能的索引
      • 《MySQL Reference Manual》笔记 - InnoDB 和 ACID 模型
      • 《MySQL Reference Manual》笔记 - InnoDB 多版本
      • 《MySQL Reference Manual》笔记 - InnoDB 锁
      • 《MySQL Reference Manual》笔记 - InnoDB 事务模型
      • B-Tree 简述
      • 理解查询执行计划
  • 中间件
    • gRPC
      • gRPC 负载均衡
    • ZooKeeper
      • ZooKeeper 数据模型
    • 消息队列
      • 消息积压解决策略
      • RocketMQ 架构设计
      • RocketMQ 功能特性
      • RocketMQ 消息存储
  • 分布式系统
    • 《凤凰架构》笔记
    • 系统设计思路
    • 系统优化思路
    • 分布式事务协议:二阶段提交和三阶段提交
    • 分布式系统的技术栈
    • 分布式系统的弹性设计
    • 单点登录解决方案
    • 容错,高可用和灾备
  • 数据结构和算法
    • 一致性哈希
    • 布隆过滤器
    • 散列表
  • 网络协议
    • 诊断工具
    • TCP 协议
      • TCP 报文结构
      • TCP 连接管理
由 GitBook 提供支持
在本页
  • 缓存一致性
  • 出现场景
  • 解决方案
  • 参考资料
  • 缓存穿透(不存在 key)
  • 出现场景
  • 解决方案
  • 缓存雪崩(多个 key 失效)
  • 出现场景
  • 解决方案
  • 缓存击穿(热点 key 失效)
  • 出现场景
  • 解决方案
  1. 数据库
  2. Redis

使用缓存的常见问题

缓存一致性

出现场景

更新缓存的时候,如果是 先删除缓存,再更新数据库,则可能会导致缓存数据和源数据不一致的。

出现问题的场景如下:

@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 设置两个失效时间的方式如下:

  1. 通过在设置 value 时追加当前时间戳。缺点:需要同步缓存服务器和应用服务器的时间、失效时间与 value 耦合(可能导致取数据的时候需要额外的反序列化);

  2. 通过设置两个 key 的方式。缺点:缓存中 key 的数量翻倍。

最后更新于1年前

缓存更新的套路