面试漫谈:缓存的三大场景与分布式锁
缓存三大问题 缓存穿透 缓存穿透指的是客户端请求查询一个在缓存中和数据库中都“绝对不存在”的数据。 由于缓存中没有(缓存未命中),请求会“穿透”缓存层,直接打到数据库层。如果这种请求量很大(例如,恶意的攻击者使用大量不存在的ID进行查询),数据库将承受巨大的压力,可能导致服务宕机。 做法 (Solutions) 缓存空值 (Cache Null Values) 做法: 当数据库查询返回为空(null)时,我们依然将这个“空结果”缓存起来,但为其设置一个较短的过期时间(TTL),例如60秒。 优点: 实现简单,立竿见影。 缺点: 会占用缓存空间(尽管很短)。 存在短暂的数据不一致性:如果在这60秒内,数据库中恰好创建了这条数据,缓存返回的依然是“空”,直到缓存过期。 布隆过滤器 (Bloom Filter) 做法: 布隆过滤器是一种高效的、概率性的数据结构,它可以用极小的空间判断一个元素“一定不存在”或“可能存在”。 流程: 启动时,将数据库中所有合法的Key加载到布隆过滤器中。 当请求到来时,先去布隆过滤器查询Key是否存在。 如果布隆过滤器判断“一定不存在”,则立即返回空,绝对不会去查缓存和数据库。 如果布隆过滤器判断“可能存在”,则继续执行后续的查询缓存、查询DB的流程。 优点: 空间效率和查询效率极高,完美解决了穿透问题。 缺点: 实现相对复杂。 存在“误判率”(False Positive):它判断“可能存在”时,数据实际上可能不存在(但它绝不会把“存在”的判为“不存在”)。 数据新增时,需要同步更新布隆过滤器(删除操作不支持或很麻烦)。 面试要点 Q: 什么是缓存穿透?它和击穿有什么区别? A: 穿透是查不存在的数据,导致请求绕过缓存直达DB。击穿是查存在的数据,但这个热点数据刚过期,导致请求直达DB。 Q: 两种解决方案(缓存空值 vs 布隆过滤器)如何选择? A: 缓存空值简单,适用于恶意攻击不频繁、允许短暂不一致的场景。布隆过滤器更可靠,适用于数据“黑名单”明确、查询QPS极高、对恶意攻击防护要求高的场景(例如,防止用户ID的恶意爬取)。 缓存击穿 缓存击穿指的是某一个访问量极高的热点Key (Hotspot Key),在它失效的那一瞬间,海量的并发请求同时涌入,导致这些请求全部“击穿”缓存,直接访问数据库,造成数据库瞬时压力剧增,甚至宕机。 做法 (Solutions) 互斥锁 / 分布式锁 (Mutex Lock / Distributed Lock) 做法: 这是最经典的解决方案。当缓存未命中时,不是所有线程都去查数据库。 线程A尝试获取该Key的分布式锁(例如 lock:product:123)。 线程A获取锁成功,开始查询数据库、重建缓存。 线程B、C、D… 发现缓存未命中,尝试获取锁,但失败。 线程B、C、D… 不去查数据库,而是进入“等待”状态(例如,自旋或休眠后重试)。 线程A重建缓存后,释放锁。 线程B、C、D… 再次(或被唤醒)查询时,直接命中缓存。 优点: 强一致性,只允许一个请求“穿透”到DB,有效保护数据库。 缺点: 实现复杂,需要引入分布式锁(如Redis或Zookeeper)。 性能下降,大量线程在锁上等待,导致系统吞吐量降低。 逻辑过期 (Logical Expiration)...
