首页
缓存常见的问题
一、缓存一致性
更新数据时需要保证缓存和数据库的数据一致性。
一般来说,缓存有以下三种模式:
Read/Write Through
-
Read Through :查询缓存过程中由缓存服务负责加载缓存;
-
Write Through :缓存存在则更新缓存,由缓存服务负责同步更新数据库;如果缓存不存在直接更新数据库。
Write Behind
只操作缓存,由缓存服务自己更新数据库,与 Read/Write Through 模式区别是更新操作为非同步。
Cache Aside
一般场景下使用的模式,同一个操作内更新缓存和数据库。
-
先更新缓存,再更新数据库。
违背了隔离性原则,写操作数据还未持久化其他读操作就可以读取到该写操作对数据的更改。
-
先更新数据库,再更新缓存。
数据库事务回滚问题:缓存操作执行成功但是响应超时抛出异常,或者服务器宕机来不及提交事务,都会导致数据库回滚但缓存更新成功,出现不一致。
并发写问题:如果允许并发更新操作,可能数据库先更新的反而后更新缓存,数据库后更新的反而先更新缓存,出现不一致。
-
先删除缓存,再更新数据库。
并发读写问题:读操作在写操作删除缓存之后更新数据库之前读取到旧的数据并加载缓存,出现不一致。
-
(推荐)先更新数据库,再删除缓存。
并发读写问题:读操作在写操作之前读取到旧的数据,并在写操作完成之后加载缓存,可能出现不一致,但是概率极低,因为写操作耗时一般大大超过读操作耗时,可以视为不会出现不一致。
主从同步延迟问题:写操作完成之后但数据库主从同步完成前,读操作从从库读取到旧数据并加载缓存,出现不一致。
延时双删
先删除缓存,再更新数据库,延迟一段时间再次删除缓存。
延时的目的是为了等待并发读操作加载缓存完毕之后再删去不一致的缓存,延迟时间的确定是关键,太长会导致数据不一致的中间态时间过长,太短则可能出现不一致。
延时删除可以通过监听binlog,结合DelayQueue、WheelTimer或延时消息等实现。
二、缓存雪崩
大量的缓存在同一个时刻过期,缓存雪崩也会导致缓存击穿。
解决方案:
-
直接返回旧值,后台再异步刷新缓存。(推荐)
-
使用随机的缓存时间,在一定范围内浮动。
-
限制执行加载缓存的请求数量,如使用数据库连接池。
三、缓存击穿(缓存并发)
大量的请求访问某个缓存失效或不存在缓存的key,会导致大量请求查询数据库和更新缓存。
解决方案:
-
缓存预热:预先加载缓存,缺点是适用范围比较有限,也会有一定的空间浪费。
- 例如用户查询某笔订单,同时也加载该订单相关的商品信息缓存,这样如果用户去查看订单下的商品时则可以直接读取到商品信息的缓存。
-
锁:使用锁保证请求同一缓存的线程中只会有一个去执行加载,其他线程则阻塞等待,缺点是无法提高并发能力。
-
异步处理:先返回旧值,然后异步去加载新值,适合实时性要求不高的场景。
-
缓存永不过期:对缓存不设置过期时间或者定时刷新缓存,适合只读场景。
-
增加了系统复杂度,读操作不需要负责加载缓存,由系统实现加载和维护缓存;
-
同时更新数据时会出现缓存一致性问题,因为目前公认的解决一致性问题的方案是更新数据时淘汰缓存,这就意味着缓存不会一直存在,与该方案相悖。
-
四、缓存穿透
大量请求访问缓存和数据库都不存在的key。
解决方案:
-
key值先验:业务端要先对key做一定的参数合法性校验,之后才允许请求缓存和数据库,缺点是适用范围比较有限。
- 例:若用自增id作为key,可以记录下当前id最大值,大于该值则不允许请求。
-
缓存空值:缓存空值,缺点是空间占用率高,最好设置过期时间。
-
布隆过滤器:有一定的误判概率,时间复杂度与数据量无关,且不需要存储元素本身,缺点是无法删除元素、查询效率较低。
- 布隆过滤器判断存在可能会不存在,判断不存在则一定不存在。
五、缓存热key
热点分散:分布式缓存服务器中某些key是访问的热点,导致热点key所属节点承载的压力比其他节点大很多。
-
使用本地缓存:使用多级缓存方式,在本地定义一个LFU级别的缓存。
-
全节点备份:将热key备份到所有的节点,同时使用某种负载均衡策略分散查询请求,缺点是实现复杂,且无法应对突发热点。
突发热点:某段时间内某些key会成为访问的热点,导致缓存服务器也无法承受。
-
基于流式计算技术的缓存热点自动发现:使用大数据能力自动判断热点数据。
六、缓存大key
解决方案:
-
需要整体读取的string,可以考虑拆分为多个键(更推荐) 或hash,这样即可以部分读取也可以批量读取;
-
其它可部分读取的类型,可以使用某种hash策略拆分为多个桶。
大key删除:如果直接删除会长时间阻塞服务器,应该限制每次删除的数量,使用命令:hash(hscan)、set(sscan)、list(ltrim)、zset(zremrangebyrank)。Redis4.0开始支持Lazy Free,主动或被动删除的键会在后台线程中处理,主动删除使用UNLINK命令,被动删除需要开启对应的配置。
七、缓存抖动
分布式缓存中某个节点突然发生故障导致该节点缓存不可用。
解决方案:
- 哈希槽机制:Redis集群使用,槽的大小为固定16k,通过Gossip协议通知集群。
- 一致性哈希算法:将节点通过哈希算法映射到0~2^32的环形哈希空间上,如节点过少造成环倾斜则复制出多个虚拟节点。
八、熔断限流保护
系统可用性最后的壁垒,在设计缓存系统时一定要考虑好所有异常情况,同时对缓存服务器及数据库都要使用某种熔断限流保护措施。
九、多级缓存
多层级缓存可以有效提高整个系统的缓存查询效率和缓存组件的可用性,但缓存一致性问题的处理则更加复杂。一般方案是将本地内存作为一级缓存,全局缓存作为二级缓存,本地缓存查询不到则查询全局缓存最后才加载缓存,缓存淘汰时通过集群广播的方式通知所有服务器实例淘汰本地缓存。