Redis 缓存
Redis 缓存
Redis 可作为关系型数据库的缓存,存储热点数据加快访问速度
服务端缓存:在访问 DB 后将请求参数作为 key,回包内容作为 value 进行缓存
客户端缓存:对服务端 RPC 调用后,将结果存储在客户端
缓存模式
Cache Aside 旁路缓存
最常见的策略
读策略:
应用服务查询数据是否在缓存上,在则直接返回缓存数据,否则从数据库查询并放到缓存中
除了查库后加载这种模式,如果业务有需要,还可以预加载数据到缓存
写策略:
- 先更新数据库
- 后删除缓存
Read/Write Through 读/写穿透
读穿透:
应用服务不和缓存直接交互,而是访问数据服务,数据服务查询数据是否在缓存上,不在则从数据库查询
写穿透:
所有的写操作都经过缓存,每次向缓存中写数据的时候,缓存会把数据持久化到对应的数据库中去,且这两个操作都在一个事务中完成
因此,只有两次都写成功了才是最终写成功了
- 在更新缓存前先加个分布式锁,保证同一时间只运行一个请求更新缓存,就不会产生并发问题,引入了锁对于写入性能会带来影响
- 在更新完缓存时,给缓存加上较短的过期时间,即使出现缓存不一致的情况,缓存的数据也会很快过期
Write Behind
Write Through 的异步更新版本,在写入一段时间后将数据一起写入数据库
缓存一致性
缓存一致性指缓存和数据库中数据的一致性
完全避免缓存不一致只有使用锁,包括 CAS 乐观锁,分布式锁(悲观锁),分布式事务
实践过程中也可以使用延时双删极大的降低不一致概率
订阅 binglog 可以避免写线程相互竞争, 但避免不了读写线程竞争
不完全解决
过期依赖
更新时仅更新数据库不处理缓存,等待 Redis 缓存过期失效后从 Mysql 拉取新数据
优点:
- 开发成本低
- 管理成本低
缺点: - 过期时间的设定考验业务能力,太短缓存频繁失效,太长缓存不一致时间长
删除缓存
删除缓存可以保证缓存中不因为更新出现错误数据,但可能因为删除失败而使得旧数据长时间保留
弥补方式
- 过期时间 - 通过为缓存设立过期时间弥补删除缓存失败带来的更新失败
- 重试机制 - 引入消息队列,将删除缓存加入消息队列确保删除成功
- 如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,如果重试超过一定次数还是没有成功,就需要向业务层发送报错信息
- 如果删除缓存成功,把数据从消息队列中移除,避免重复操作
- 延时双删 - 令写线程等待一段时间,基本确认读线程都结束后再次删除缓存
删除一个数据,相比更新一个数据更加轻量级,出问题的概率更小
在实际业务中,缓存的数据可能不是直接来自数据库表,也许来自多张底层数据表的聚合。比如商品详情信息,在底层可能会关联商品表、价格表、库存表等,如果更新了一个价格字段,那么就要更新整个数据库,还要关联的去查询和汇总各个周边业务系统的数据,这个操作会非常耗时
从另外一个角度,不是所有的缓存数据都是频繁访问的,更新后的缓存可能会长时间不被访问,所以说,从计算资源和整体性能的考虑,更新的时候删除缓存,等到下次查询命中再填充缓存,是一个更好的方案
系统设计中有一个思想叫 Lazy Loading,适用于那些加载代价大的操作,删除缓存而不是更新缓存,就是懒加载思想的一个应用
订阅 binlog
将搭建的消费服务作为 Mysql 的一个从服务器,订阅 Mysql 的 binlog 日志,解析日志内容后更新至 redis
锁
CAS
CAS 乐观锁当且仅当客户端最后一次取值后该 key 没有被其他客户端修改的情况下,才允许当前客户端将新值写入
分布式锁
在每次更新前获取分布式排他锁保证一致性
缓存异常
缓存异常的三个常见问题分别是缓存雪崩、缓存击穿、缓存穿透
通常会给缓存数据设置过期时间,缓存过期后重新从数据库中取数据并更新至缓存
缓存雪崩
缓存雪崩指同一时间发生大量缓存数据的过期或 Redis 故障宕机时,大量请求直接访问数据库导致数据库压力过大崩溃
解决方法
- 大量数据过期
- 根据业务数据有效期进行分类错峰,比如 A 类90分钟,B 类80分钟,C 类70分钟
- 过期时间使用固定时间+随机值的形式,稀释集中到期的 key 的数量
- 超热数据永不过期(人工/脚本维护刷新)
- 故障宕机
- 服务熔断/请求限流
- 通过主从节点方式构建 Redis 高可靠集群
缓存击穿
缓存击穿指缓存中某个热点数据过期后被大量的请求访问,高并发请求直接访问数据库导致数据库崩溃
解决方案:
- 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值
- 设置热点数据永不过期,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间
- 服务熔断/请求限流
缓存穿透
缓存穿透指大量请求访问不存在的数据导致数据库压力骤增
解决方案:
- 限制非法请求,在 API 入口处对其进行检测拦截
- 缓存空值或默认值,针对查询的不存在数据在缓存中设置空值或默认值
- 使用布隆过滤器,在写入数据库时进行标记,业务线程确认缓存失效时可快速判断数据是否存在,避免访问数据库
布隆过滤器基于哈希实现,存在 hash 冲突的可能
布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据
布隆过滤器是一个 bit 向量或者说 bit 数组
一个值被映射到布隆过滤器的方式是使用多个不同的哈希函数生成多个哈希值,并将 bit 数组中对应的位置置 1
多个值之间会产生覆盖(哈希值对应的位置相同),因此不能判断一个值一定存在
如果一个值对应的位置上为 0,则其一定不存在
特殊 Key
Hot Key
原因: Hot key 引发缓存系统异常,主要是因为突发热门事件发生时,超大量的请求访问热点事件对应的 key,流量集中打在一个缓存节点机器,这个缓存机器很容易被打到物理网卡、带宽、CPU 的极限,从而导致缓存访问变慢、卡顿
解决方案:
首先找到 Hot Key
- 分散处理 - 将单个 Hot Key 分散为 hotkey#1、hotkey#2、hotkey#3,……hotkey#n,将 n 个 key 分散存在多个缓存节点,然后 client 端请求时,随机访问其中某个后缀的 hotkey,这样就可以把热 key 的请求打散,避免一个缓存节点过载
- 对缓存提前进行多副本+多级结合的缓存架构设计
- 如果热 key 较多,可以通过监控体系对缓存的 SLA 实时监控,通过快速扩容来减少热 key 的冲击
- 业务端还可以使用本地缓存,将这些热 key 记录在本地缓存,来减少对远程缓存的冲击
Big Key
大 key,是指在缓存访问时,部分 Key 的 Value 过大,进而引发的读写、加载易超时的现象
原因:
- 如果业务中这种大 key 很多,而这种 key 被大量访问,缓存组件的网卡、带宽很容易被打满
- 如果大 key 缓存的字段较多,每个字段的变更都会引发对这个缓存数据的变更,同时这些 key 也会被频繁地读取,读写相互影响
- 大 key 一旦被缓存淘汰,从数据库重新加载可能需要花费很多时间
解决方案:
- 如果数据存在 Redis 中,比如业务数据存 set 格式,大 key 对应的 set 结构有几千几万个元素,这种写入 Redis 时会消耗很长的时间,导致 Redis 卡顿,此时可以扩展新的数据结构,同时让 client 在这些大 key 写缓存之前,进行序列化构建,然后通过 restore 一次性写入
- 将大 key 分拆为多个 key,尽量减少大 key 的存在。同时由于大 key 一旦穿透到 DB,加载耗时很大,所以可以对这些大 key 进行特殊照顾,比如设置较长的过期时间,比如缓存内部在淘汰 key 时,同等条件下,尽量不淘汰这些大 key