Redis 用作缓存的注意事项
背景
在一些高并发的场景中,为了避免流量过大打崩数据库,通常会在用户请求和数据库使用 Redis 作为缓存,将频繁查询的数据写入到缓存中,利用 Reids 的高性能来应对大部分流量,保证服务的可用性。但是,一旦引入了缓存,就会带来使用缓存带来的一系列问题。
缓存异常问题
缓存异常的问题包括缓存穿透,缓存击穿,缓存雪崩
缓存穿透
缓存穿透指要访问的数据既不在 Redis 缓存中,也不在数据库中(数据库的数据被误删了),导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。(缓存变成了“摆设”)
解决方案
- 查询前做好参数校验(重要)
- 缓存空值或默认值。针对查询的数据,在 Redis 中缓存一个空值或者默认值(比较鸡肋)
- 使用布隆过滤器,在写入缓存时,先在布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过布隆过滤器快速判断数据是否存在,不存在就不同查库
缓存击穿
缓存击穿是指,针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,即热点数据过期,紧接着,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库处理其他请求。
可以将缓存击穿看作雪崩的一个子集
解决方案
- 使用互斥锁。在访问数据库前加个锁,确保只有一个线程能够访问数据库,查询数据库之后将数据写回缓存,后面的请求就能走缓存
- 不设置过期时间,由后台异步更新缓存,或者在过期之前通知后台线程更新缓存
缓存雪崩
缓存雪崩是指大量的应用请求无法在 Redis 缓存中进行处理,直接发送到数据库层,导致数据库层的压力激增
主要发生在两种场景:大量缓存数据在同一时间过期和 Redis 故障宕机
解决方案
针对大量 key 同一时间过期的情况:
- 均匀设置过期时间,避免同一时间过期
- 使用互斥锁。保证同一时间只有一个线程来构建缓存
- 不设置过期时间,由后台异步更新缓存
针对 Redis 不可用的情况: 紧急处理
- 服务熔断,避免影响其他服务
- 请求限流
事先预防
- 事先构建高可用的集群,如 Redis Cluster 集群
缓存的类型
缓存一般有两种使用方法,分别是只读缓存和读写缓存
只读缓存
顾名思义,只会从缓存中读数据,也就是常说的旁路缓存。
应用要读取数据的话,先在 Redis 中寻找数据是否存在。如果发生了缓存缺失,就从数据库中读出来并且写到数据库中。
所有的数据写请求,直接在数据库中增删改。对于删改的数据来说,如果 Redis 已经缓存了相应的数据,需要将这些缓存删除。
读写缓存
服务端把 cache 当作主要的数据存储,从中读取数据并将数据写入。
除了读请求会发送到缓存进行处理,所有的写请求也会发送到缓存,在缓存中直接对数据进行增删改操作。这种方式能够快速返回给业务应用,提升响应速度。但是如果 Redis 发生了掉电,数据可能会丢失,给业务带来风险。
针对业务对数据可靠性和缓存性能的要求,有同步直写和异步写回两种方案。
同步直写
适合写多的场景,通常需要结合分布式锁
优先考虑了数据的可靠性。写请求在发给缓存的同时,也会发给数据库进行处理,等到缓存和数据库都完成后才会返回给客户端。
将数据写回 DB 是同步的,能够保证数据的强一致,但是会阻塞 Redis,降低访问性能
异步写回
优先考虑了响应延迟,将所有写请求都先在缓存中处理。等到 cache 写满时,才异步地将数据同步到 DB,cache 和 DB 的数据会出现短暂的不一致。
这种模式写性能非常高,适合数据经常变化但一致性要求不高的场景,如统计点赞量,浏览量。
缓存和 DB 的一致性问题
在业务系统中,一旦使用了缓存,就需要注意保证缓存和 DB 的一致性,否则容易出现问题。
- 对于读写缓存,想要保证数据的一致性,需要使用同步直写的策略,并且要保证缓存和 DB 的更新具有原子性。
- 对于只读缓存,如果有新数据,直接写入 DB;当有数据需要修改时,将缓存中对应的数据标记为无效(删除),需要保证更新 DB 和删除缓存的原子性。
TIP
为了保证一致性,一般采用先更新 DB,后删除缓存的策略,就能够满足一般的业务场景了,如果需要强保证一致性,就需要引入额外的重试机制来保障,同时成本也会提高,需要结合业务衡量是否值得
需要注意的是,【先删除缓存,再更新数据库】策略不是银弹,在【读+写】并发请求是也有可能导致不一致。 此时可以考虑使用延迟双删。先删除缓存,然后更新 DB,然后等待一段时间后再次删除缓存,但是这个等待时间很难把握。