缓存设计

缓存设计

目的:

  1. 加速访问
  2. 减轻后端服务的压力

原理:

  • 离用户更近的位置存储
  • 离应用更近的位置存储
  • 更高速的存储介质存储

需要缓存的内容

  • 热点数据
  • 静态资源

过期策略

  • 固定时间:比如指定缓存的时间是30分钟;
  • 相对时间:比如最近10分钟内没有访问的数据;
  • 事件通知: 缓存管理线程按事件通知进行删除

缓存淘汰算法

  • FIFO (First In First Out) 先进先出
  • LRU (Least Recently Used) 最近最少使用
  • LFU (Least Frequently Used) 在一段时间内使用频率最小的数据被移除缓存

缓存媒介

常用中间件: Varnish, Ngnix, Squid, Memcache, Redis, Ehcache 等;

缓存的内容: 文件, 数据, 对象;

缓存的介质: CPU, 内存(本地, 分布式), 磁盘(本地, 分布式)

分级缓存

用户侧

  • 客户端程序内
  • 浏览器 cookie, sessionStorage, localStorage, IndexDB

CDN缓存

  • 用户就近访问, 主要原理还是依靠智能 DNS, 主要缓存 HTML, CSS, JS, 视频等静态资源;

应用服务缓存

  • nginx 本地缓存: html/css/js/ico 等少量小文件
  • nginx + Lua Shared Dict 对简单的接口数据缓存
  • squid/Varnish: 大量静态资源, 如视频 / 图片
  • redis 缓存动态内容, 减轻数据库压力
  • 本地缓存, 即同一个主机或pod内附加一个缓存服务
  • 应用内缓存

存储侧缓存

  • 例如 mysql 增大 buffer_pool
  • SSD 和 SATA 区分冷热文件缓存

常见问题

数据一致性问题

  • 双写
  • 异步刷新
  • 先缓存后DB/先DB后缓存
  • 独立的缓存管理进程负责更新缓存

需要考虑一致性的地方

  • cache - db
  • cache - 多级缓存
  • cache - 副本

cache和db的前后更新顺序

insert key

先 write cache 再 DB;

假如缓存写成功, 但写数据库失败或响应延迟, 则下次读取(并发读)缓存时, 就出现脏读,所以这种方式不可取。

先 write DB, 再 cache;

假如写数据库成功, 但写缓存失败, 则下次读取(并发读)缓存时, 则读不到数据,读不到数据时, 再请求一次数据库, 并刷新缓存;

update key

先 del cache key, 再 update db, 再 add key

如果更新库失败, 则后续读 key 时会先读缓存, 引发 cache miss, 再读取库, 更新缓存, 并不会造成数据不一致;

如果是分布式环境, 则可能服务 A 在 del cache key 后, 未完成更新 db 时, 服务 B 读库, 并更新了 cache, 造成数据不一致。

解决办法:

  1. 锁, 对此 key 添加写锁, 如果更新库成功或失败后才释放
  2. 串行队列执行关于此 key 的业务
  3. 关于此 KEY 的业务定点到某一个 server 去串行执行;

缓存异步刷新

如果数据库操作和写缓存不在一个操作步骤中, 比如在分布式场景下, 无法做到同时写缓存或需要异步刷新(补救措施)时候;

解决办法: 看业务特性进行针对性解决。

热点数据缓存

即只能缓存少量的数据时, 将访问比例最大的部分数据进行缓存;

如: 最近登录用户, 最新活动数据

队列模型

  1. 建立一个定长队列,放置最近1000个用户的UID,最新用户放置在队列顶部;
  2. 当请求发送过来时,判断UID是否在队列中,是则访问热点缓存 不在队列中,则读取库或者冷缓存,然后将UID放入队列,并将数据写如热缓存;
  3. 定期移除队列的后200位UID,并且移除热缓存的key; 热点用户将一直存在于队列顶部区域,不会被移除

场景2: 某几个热点key被hash到同一个缓存节点, 节点失效或重启时影响极大;

办法: 定期或开发阶段 对缓存key进行统计分析, 业务上再进行key拆分;

缓存雪崩

雪崩是指当大量缓存集中失效时(ttl到期或重启缓存服务),大量的请求直接访问数据库的场景

应对方法:

  • 合理规划缓存的失效时间, 离散过期时间分布;
  • 合理评估数据库的负载压力, 限制连接数
  • 应用层熔断和限流机制;
  • 多级缓存设计,考虑备用缓存

缓存穿透

场景1: 某个key不存在, 大量请求频繁落到数据库上去查询并更新缓存;

场景2: 大量请求一些不存在的资源, 如通过遍历商品编号获取属性的恶意请求;

办法

  • 缓存预热, 提前写入缓存或推送到用户侧
  • 对空值的结果也进行缓存, ttl 时间稍短
  • 入口安全拦截措施阻挡恶意访问;
  • 只有获取到分布式锁的线程才能去数据库查询并更新缓存

太多分片导致的多次网络IO开销

例如一笔业务需要3次缓存操作;如果分为3个缓存实例,则有可能与3个设备发生3次网络IO,增加了业务时延; 办法:

  1. 连接池,避免新建连接
  2. 缓存分布方式优化,通过对key进行范围存储,避免hash的无序,来确保关联key存放于固定实例上
  3. 合并数据,将多次小操作,合并为一次大操作来减少网络IO

持久化问题

  1. 设计上要求缓存不需要持久化
  2. 如果缓存失效对数据库压力太大,还是需要持久化
  3. 缓存节点的复制,由异步改为双写
  4. 多级缓存

反向代理缓存

一般指在网站服务器机房部署代理服务器, 实现负载均衡, 数据缓存, 安全控制等功能

常用的代理缓存有 Varnish, Squid, Ngnix, nginx+lua 简单比较如下:

  • varnish 和 squid 是专业的 cache 服务, 多用于资源站进行对象缓存;
  • 简易的静态资源缓存推荐使用nginx
  • 少量简易的动态内容缓存可以使用nginx+lua或类似网关服务

分布式缓存

主要指缓存用户经常访问数据的缓存,数据源为数据库

一般起到热点数据访问和减轻数据库压力的作用

主要使用 Memcache and Redis

常用分片算法

  1. 哈希算法,哈希后取模
  2. 一致性哈希算法: 一致性 Hash 是将数据按照特征值映射到一个首尾相接的 Hash 环上,同时也将缓存节点映射到这个环上
  3. Range Based 算法: 例如根据key前缀分片

进程内缓存

应用内部自身的缓存, 如应用字典等常用数据, 一般配合外部缓存或二级双缓存

场景

  • 频繁访问的配置项目
  • 高并发时的小规模的热点数据, 如果活动配置

特点

  • 不需要序列化和反序列化,无网络开销, 速度最快
  • 总大小需要严格控制
  • 考虑数据一致性

二级缓存面临的分布式一致性问题

  1. 消息队列修改方案 应用在修改完自身缓存数据和数据库数据之后,给消息队列发送数据变化通知, 其他应用订阅了消息通知,在收到通知的时候修改缓存数据。

  2. Timer 修改方案 对"实时一致性"不敏感的情况下,每个应用都会启动一个 Timer,定时从数据库拉取最新的数据,更新缓存

CDN加速缓存

Content Delivery Network, 即内容分发网络

CDN 主要解决将数据缓存到离用户最近的位置, 一般缓存静态资源文件(页面, 脚本, 图片, 视频, 文件等);

工作路径:

  1. 客户端发起请求, 先进行 DNS 解析, cname 到 cdn 的 dns 负载均衡服务器;
  2. cdn 域名服务器将最近 CDN 节点的 IP 响应给用户
  3. 客户端请求最近的 CDN 节点;
  4. CDN 节点如果有缓存则响应缓存, 无缓存则回源获取数据后再响应。

动态接口加速 cdn除了做静态资源缓存,另一个主要作用是对动态接口的加速。

因为用户直接到源服务器的网络质量,不一定有 用户-cdn节点-源服务器 的网络质量好;

所以cdn的接口加速一般也会有较好的效果;

其它用途

  1. 减少源站带宽资源
  2. 抗攻击, 抵御 ddos 攻击

缓存一致性问题

  • 静态资源文件名采用hash命名方式
  • 合理配置过期时间
  • 调用厂商缓存刷新接口释放缓存

缓存架构设计要点

容量规划

  • 缓存内容的大小:总容量大小,主要key的大小,最大key的大小,单实例大小,主机内存大小,实例数;
  • 缓存内容的数量
  • 淘汰策略
  • 缓存的数据结构

性能优化

  • 每秒的读峰值
  • 每秒的写峰值
  • 线程模型
  • 预热方法
  • 缓存分片
  • 冷热数据的比例

高可用

  • 复制模型
  • 失效转移
  • 持久策略
  • 缓存重建: 迁移或清空后如何快速恢复

缓存监控

  • 容量
  • 可用性
  • 响应时间
  • 大对象, 慢查询
  • 命中率

注意事项

  • 是否有可能发生缓存穿透
  • 是否有大对象
  • 是否使用缓存实现分布式锁
  • 是否使用缓存支持的脚本(Lua)
  • 是否避免了 Race Condition(多线程竞争)

业务设计

  • 不同的业务单元使用隔离的缓存实例,避免干扰
  • 缓存超时时间:较长的超时时间会占用过多线程池连接数
  • key应该设置失效时间,避免内存不足
  • 失效时间不能集中,可加随机值,避免缓存雪崩
  • 低频访问的内容不需要放在缓存中
  • 单个key不宜过大,特别是redis这种单线程实例,会产生阻塞
  • redis危险命令: keys HGETALL 容易造成请求阻塞
  • 有大量的更新数据时,尤其是批量处理时,可以使用批量模式加速
  • 在通常情况下,读的顺序是先缓存,后数据库;写的顺序是先数据库,后缓存
  • 要考虑到如果后期缓存迁移时的业务连续性
  • 所有集合型的数据结构(哈希、列表等),则都要考虑为它设置最大限制,避免内存用光;
  • 考虑缓存降级,如主备切换期间,业务侧的缓存连接异常时,如何连接到备,或数据库
  • key使用不同的前缀进行逻辑隔离,避免不同业务的干扰
最后更新于