缓存设计
目的:
- 加速访问
- 减轻后端服务的压力
原理:
- 离用户更近的位置存储
- 离应用更近的位置存储
- 更高速的存储介质存储
需要缓存的内容
- 热点数据
- 静态资源
过期策略
- 固定时间:比如指定缓存的时间是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, 造成数据不一致。
解决办法:
- 锁, 对此 key 添加写锁, 如果更新库成功或失败后才释放
- 串行队列执行关于此 key 的业务
- 关于此 KEY 的业务定点到某一个 server 去串行执行;
缓存异步刷新
如果数据库操作和写缓存不在一个操作步骤中, 比如在分布式场景下, 无法做到同时写缓存或需要异步刷新(补救措施)时候;
解决办法: 看业务特性进行针对性解决。
热点数据缓存
即只能缓存少量的数据时, 将访问比例最大的部分数据进行缓存;
如: 最近登录用户, 最新活动数据
队列模型
- 建立一个定长队列,放置最近1000个用户的UID,最新用户放置在队列顶部;
- 当请求发送过来时,判断UID是否在队列中,是则访问热点缓存 不在队列中,则读取库或者冷缓存,然后将UID放入队列,并将数据写如热缓存;
- 定期移除队列的后200位UID,并且移除热缓存的key; 热点用户将一直存在于队列顶部区域,不会被移除
场景2: 某几个热点key被hash到同一个缓存节点, 节点失效或重启时影响极大;
办法: 定期或开发阶段 对缓存key进行统计分析, 业务上再进行key拆分;
缓存雪崩
雪崩是指当大量缓存集中失效时(ttl到期或重启缓存服务),大量的请求直接访问数据库的场景
应对方法:
- 合理规划缓存的失效时间, 离散过期时间分布;
- 合理评估数据库的负载压力, 限制连接数
- 应用层熔断和限流机制;
- 多级缓存设计,考虑备用缓存
缓存穿透
场景1: 某个key不存在, 大量请求频繁落到数据库上去查询并更新缓存;
场景2: 大量请求一些不存在的资源, 如通过遍历商品编号获取属性的恶意请求;
办法
- 缓存预热, 提前写入缓存或推送到用户侧
- 对空值的结果也进行缓存, ttl 时间稍短
- 入口安全拦截措施阻挡恶意访问;
- 只有获取到分布式锁的线程才能去数据库查询并更新缓存
太多分片导致的多次网络IO开销
例如一笔业务需要3次缓存操作;如果分为3个缓存实例,则有可能与3个设备发生3次网络IO,增加了业务时延; 办法:
- 连接池,避免新建连接
- 缓存分布方式优化,通过对key进行范围存储,避免hash的无序,来确保关联key存放于固定实例上
- 合并数据,将多次小操作,合并为一次大操作来减少网络IO
持久化问题
- 设计上要求缓存不需要持久化
- 如果缓存失效对数据库压力太大,还是需要持久化
- 缓存节点的复制,由异步改为双写
- 多级缓存
反向代理缓存
一般指在网站服务器机房部署代理服务器, 实现负载均衡, 数据缓存, 安全控制等功能
常用的代理缓存有 Varnish, Squid, Ngnix, nginx+lua 简单比较如下:
- varnish 和 squid 是专业的 cache 服务, 多用于资源站进行对象缓存;
- 简易的静态资源缓存推荐使用nginx
- 少量简易的动态内容缓存可以使用nginx+lua或类似网关服务
分布式缓存
主要指缓存用户经常访问数据的缓存,数据源为数据库
一般起到热点数据访问和减轻数据库压力的作用
主要使用 Memcache and Redis
常用分片算法
- 哈希算法,哈希后取模
- 一致性哈希算法: 一致性 Hash 是将数据按照特征值映射到一个首尾相接的 Hash 环上,同时也将缓存节点映射到这个环上
- Range Based 算法: 例如根据key前缀分片
进程内缓存
应用内部自身的缓存, 如应用字典等常用数据, 一般配合外部缓存或二级双缓存
场景
- 频繁访问的配置项目
- 高并发时的小规模的热点数据, 如果活动配置
特点
- 不需要序列化和反序列化,无网络开销, 速度最快
- 总大小需要严格控制
- 考虑数据一致性
二级缓存面临的分布式一致性问题
-
消息队列修改方案 应用在修改完自身缓存数据和数据库数据之后,给消息队列发送数据变化通知, 其他应用订阅了消息通知,在收到通知的时候修改缓存数据。
-
Timer 修改方案 对"实时一致性"不敏感的情况下,每个应用都会启动一个 Timer,定时从数据库拉取最新的数据,更新缓存
CDN加速缓存
Content Delivery Network, 即内容分发网络
CDN 主要解决将数据缓存到离用户最近的位置, 一般缓存静态资源文件(页面, 脚本, 图片, 视频, 文件等);
工作路径:
- 客户端发起请求, 先进行 DNS 解析, cname 到 cdn 的 dns 负载均衡服务器;
- cdn 域名服务器将最近 CDN 节点的 IP 响应给用户
- 客户端请求最近的 CDN 节点;
- CDN 节点如果有缓存则响应缓存, 无缓存则回源获取数据后再响应。
动态接口加速 cdn除了做静态资源缓存,另一个主要作用是对动态接口的加速。
因为用户直接到源服务器的网络质量,不一定有 用户-cdn节点-源服务器 的网络质量好;
所以cdn的接口加速一般也会有较好的效果;
其它用途
- 减少源站带宽资源
- 抗攻击, 抵御 ddos 攻击
缓存一致性问题
- 静态资源文件名采用hash命名方式
- 合理配置过期时间
- 调用厂商缓存刷新接口释放缓存
缓存架构设计要点
容量规划
- 缓存内容的大小:总容量大小,主要key的大小,最大key的大小,单实例大小,主机内存大小,实例数;
- 缓存内容的数量
- 淘汰策略
- 缓存的数据结构
性能优化
- 每秒的读峰值
- 每秒的写峰值
- 线程模型
- 预热方法
- 缓存分片
- 冷热数据的比例
高可用
- 复制模型
- 失效转移
- 持久策略
- 缓存重建: 迁移或清空后如何快速恢复
缓存监控
- 容量
- 可用性
- 响应时间
- 大对象, 慢查询
- 命中率
注意事项
- 是否有可能发生缓存穿透
- 是否有大对象
- 是否使用缓存实现分布式锁
- 是否使用缓存支持的脚本(Lua)
- 是否避免了 Race Condition(多线程竞争)
业务设计
- 不同的业务单元使用隔离的缓存实例,避免干扰
- 缓存超时时间:较长的超时时间会占用过多线程池连接数
- key应该设置失效时间,避免内存不足
- 失效时间不能集中,可加随机值,避免缓存雪崩
- 低频访问的内容不需要放在缓存中
- 单个key不宜过大,特别是redis这种单线程实例,会产生阻塞
- redis危险命令: keys HGETALL 容易造成请求阻塞
- 有大量的更新数据时,尤其是批量处理时,可以使用批量模式加速
- 在通常情况下,读的顺序是先缓存,后数据库;写的顺序是先数据库,后缓存
- 要考虑到如果后期缓存迁移时的业务连续性
- 所有集合型的数据结构(哈希、列表等),则都要考虑为它设置最大限制,避免内存用光;
- 考虑缓存降级,如主备切换期间,业务侧的缓存连接异常时,如何连接到备,或数据库
- key使用不同的前缀进行逻辑隔离,避免不同业务的干扰