面试题答案
一键面试缓存策略设计
- 读写策略
- 读策略:
- 采用
Cache - Aside
模式,即应用程序先从缓存中读取数据。如果缓存命中,直接返回数据;如果缓存未命中,再从数据库中读取,然后将数据写入缓存,并返回给应用。这样可以最大程度利用缓存,减少数据库压力。 - 对于读多写少的场景,可以考虑
Write - Through
策略的变种,即异步Write - Through
。先更新缓存,然后异步更新数据库,这样可以减少写操作的响应时间,但需要处理异步更新失败的重试机制。
- 采用
- 写策略:
- 对于写操作,采用
Write - Invalidate
策略。当数据更新时,先更新数据库,然后使相关缓存失效。这样能保证数据库和缓存最终一致性。为了防止并发写操作导致缓存不一致,可以在缓存失效时使用分布式锁(如Redis的SETNX命令实现简单分布式锁),确保同一时间只有一个写操作能使缓存失效。
- 对于写操作,采用
- 读策略:
- 缓存数据结构选择
- 根据业务数据的特点选择合适的缓存数据结构。例如,如果数据具有层级关系,可以使用Redis的Hash结构,将父级数据作为Hash的key,子级数据作为field - value对存储。如果是简单的键值对数据,直接使用普通的key - value结构即可。对于需要排序的数据,可以考虑使用Redis的Sorted Set结构。
- 缓存粒度控制
- 细粒度缓存:将数据按照最小可复用单元进行缓存,例如对于一个复杂订单系统,可以将订单的每个子项分别缓存。这样在部分数据更新时,只需要使对应的子项缓存失效,而不是整个订单缓存失效,减少缓存更新带来的性能开销。
- 粗粒度缓存:对于一些关联性强、更新频率低的数据,可以采用粗粒度缓存。例如,系统中的配置信息,将所有配置信息作为一个整体进行缓存,减少缓存管理的复杂度。
保证数据完整性和一致性
- 分布式事务与缓存一致性
- 使用两阶段提交(2PC)或三阶段提交(3PC)协议时,在准备阶段,先使相关缓存进入“预失效”状态(例如在缓存中设置一个特殊标识),如果提交阶段成功,再正式使缓存失效;如果回滚,取消“预失效”标识。
- 基于消息队列实现最终一致性。在分布式事务成功提交后,发送消息到消息队列,由消息队列消费者负责更新或失效相关缓存。通过消息的可靠投递和重试机制保证缓存操作的最终成功。
- 实时更新与同步和缓存一致性
- 对于实时数据更新,采用发布 - 订阅模式。当数据更新时,发布者向消息队列发布更新消息,多个订阅者(包括缓存更新服务)接收到消息后进行相应的缓存更新操作。
- 对于数据同步场景,例如主从数据库同步过程中,在主库更新成功后,通过消息通知从库以及缓存更新服务,确保缓存和从库数据的一致性。
缓存失效、穿透、雪崩问题应对方案
- 缓存失效
- 设置合理的过期时间:根据数据的更新频率和重要性设置不同的过期时间。对于更新频繁的数据,设置较短的过期时间;对于相对稳定的数据,设置较长的过期时间。可以采用随机过期时间(例如在固定过期时间基础上,上下浮动一定比例),避免大量缓存同时过期。
- 缓存过期后的处理:当缓存过期时,采用“兜底”策略,例如返回默认值或者从数据库读取旧数据(如果允许短暂的数据不一致),同时异步更新缓存,减少用户感知到的延迟。
- 缓存穿透
- 布隆过滤器:在查询数据前,先通过布隆过滤器判断数据是否存在。如果布隆过滤器判断不存在,则直接返回,避免查询数据库。布隆过滤器存在一定的误判率,但可以通过合理设置参数(如哈希函数个数、位数组大小)来降低误判率。
- 缓存空值:当查询数据库发现数据不存在时,将空值也缓存起来,并设置较短的过期时间,这样下次相同查询直接从缓存返回空值,而不会穿透到数据库。
- 缓存雪崩
- 构建多级缓存:例如采用本地缓存(如Guava Cache)和分布式缓存(如Redis)结合的方式。当分布式缓存出现雪崩时,本地缓存可以作为兜底,继续提供部分数据服务,减少对数据库的冲击。
- 限流与降级:对访问系统的流量进行限制,例如使用令牌桶算法或漏桶算法。当流量过大,接近系统承载能力时,进行降级处理,如返回默认数据、提示系统繁忙等,避免大量请求直接压垮数据库。
- 服务熔断:设置缓存服务的熔断机制,当缓存服务出现大面积故障时,熔断缓存服务,直接访问数据库,并启动降级策略,防止故障扩散到整个系统。