数据类型和使用场景
String
使用场景
- 常规数据(比如 Session、Token、序列化后的对象、图片的路径)的缓存
- 计数比如用户单位时间的请求数(简单限流可以用到)、页面单位时间的访问数
- 分布式锁(利用
SETNX key value
命令可以实现一个最简易的分布式锁) - …
Hash
存储对象,相比于 String 序列化存储,可以少一个序列化反序列化的过程
Sorted Set
排行榜可以使用
缓存读写策略
Cache Aside Pattern(旁路缓存模式)
Cache Aside Pattern 中服务端需要同时维系 db 和 cache,并且是以 db 的结果为准。
写入:
- 更新 db
- 删除 cache
读取:
- 从 cache 中读取数据,读取到直接返回
- 如果从 cache 中读取不到数据,从 db 中读取数据,放入 cache ,然后返回
问题:
Q:可以先删除 cache ,再更新 db 吗?
A:不能,会有数据一致性问题。如果删除 cache, db 没有更新,又有一个请求进来了,cache 中就有旧数据了,此时更新 db 后,db 和 cache 数据不一致了。
Q:写入过程中,先更新 db, 在 删除 cache 就可以保证没有问题了吗?
A:不能,理论上还是会有数据一致性问题,但是概率较小,因为 cache 的写入比数据库的写入快的多。
两个请求,req1 和 req2, 此时 cache 中没有缓存。req1 请求数据 A -> 同时 req2 请求更新数据 A -> req1 将数据放入 cache。
Read/Write Through Pattern(读写穿透)
Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。
写入:
- 先查 cache,cache 中不存在,直接更新 db
- cache 存在,想更新 cache,然后由 cache 自己去更新 db
读取:
- 先从 cache 中读,存在则直接返回
- cache 中不存在,cache 去查询 db,写入 cache 中后返回
Write Behind Pattern(异步缓存写入)
Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 db 的读写。
但是,两个又有很大的不同:
- Read/Write Through 是同步更新 cache 和 db
- Write Behind 则是只更新 cache ,不直接更新 db,而是改为异步批量的方式来更新 db
如何保证数据一致性(延迟双删)
日常使用的 Cache Aside Pattern(旁路缓存模式)并不能准确的确保数据一致性,如果一致性要求比较高,可以使用一下方案。
实现过程:
- 删除 cache
- 更新 db
- 隔一段时间再次删除 cache
其中 “3. 隔一段时间再次删除 cache” 也有多种实现方式:
- 写入消息队列,利用消息队列实现二次删除(APP -写-> MySQL & APP -写-> MQ -删除-> Redis )
- 订阅数据库变更日志,再操作缓存(APP -写-> MySQL -Binlog-> Canal -投递-> MQ -删除-> Redis)
问题:第二次删除的延迟时间难以确定,中间的延迟时间其实还是会有 “脏数据” 存在
Redis 性能优化
Redis 问题
以下 3 个问题本质都是 cache 没有查询到数据,访问数据库,将数据库压崩了。
缓存穿透
key 不合理,根本不存在 cache 中,也不存在于 db 中
如:恶意攻击者故意制造大量非法的 key 发起请求,导致大量请求直接落到 db 中,db 中也没有相应数据。也就是大量请求最终直接落到 db 了,给 db 造成了巨大的压力。
解决方案:
- 缓存无效 key (如果数据库中查不到,就将查不到的数据也缓存到 redis 中,并设置合适的过期时间)
- 布隆过滤器 (可以快速判断出 key 是否存在于布隆过滤器,如果存在,执行后续查询,如果不存在,数据肯定无效,直接丢弃或者返回业务异常)
- 接口限流 (根据用户或者 IP 信息进行限流,对于频繁的异常访问,使用黑名单机制)
缓存击穿
key 为热点数据,存在 db 中,不存在 cache 中(通常是因为 cache 中的数据过期)
解决办法:
- 永不过期(不推荐):设置热点数据永不过期或者过期时间比较长
- 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期
- 加锁(看情况):在缓存失效后,通过设置互斥锁确保只有一个请求去查询数据库并更新缓存
缓存雪崩
cache 中的数据同一时间大面积失效,大量请求直达 db
解决办法:
- 设置随机的过期时间(可选):过期时间可以用固定时间加随机值,这样可以避免大量 key 同时实效
- 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期
- 持久缓存(看情况):一般不推荐缓存永不过期,但是针对关键性和变化不频繁的数据,也可以考虑这种方案
如何缓存预热
综上可的,缓存预热可以有效解决缓存击穿和缓存雪崩的情况,那么缓存预热应该如何进行呢?
- 使用定时任务,如 xxl-job, 用来定时触发预热逻辑
- 使用消息队列,如 kafka,用来异步的进行缓存预热(将需要预热的数据放入消息队列,由缓存服务消费消息队列中的数据,将对应数据进行缓存)
三种问题的对比
- 缓存穿透中,请求的 key 既不存在于 cache 中,也不存在于 db 中
- 缓存击穿中,请求的 key 对应的是热点数据 ,该数据存在 db 中,但不存在于 cache 中(通常是因为 cache 中的那份数据已经过期)
- 缓存击穿导致的原因主要是某个热点数据不存在与缓存中(通常是因为缓存中的那份数据已经过期)
- 缓存雪崩导致的原因是 cache 中的大量或者所有数据失效