面试漫谈:缓存的三大场景与分布式锁

缓存三大问题 缓存穿透 缓存穿透指的是客户端请求查询一个在缓存中和数据库中都“绝对不存在”的数据。 由于缓存中没有(缓存未命中),请求会“穿透”缓存层,直接打到数据库层。如果这种请求量很大(例如,恶意的攻击者使用大量不存在的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)...

November 14, 2025 · 501 words · Kurong

《Redis深度历险》记录

《Redis深度历险》记录 基础与应用 分布式锁 分布式锁用于高并发或分布式事务中保护临界资源。 最常见的实现方式是使用 SET 命令的 NX (不存在时设置)和 PX (设置过期时间)选项: 1 SET lock_name my_random_value NX PX 30000 lock_name : 锁的名称。 my_random_value : 唯一标识(通常使用 UUID)。 NX : 只有键不存在时才设置。 PX 30000 : 设置 30 秒的过期时间。 释放分布式锁时需要验证唯一标识是否匹配,防止误删其他客户端的锁。 超时问题 超时问题有三类: 锁过期,可以通过锁续约机制,即获取锁后启动一个守护线程,定期检查锁是否仍由自己持有,如果是则延长锁的过期时间。 客户端获取锁后崩溃,没有主动释放锁,锁就只能等待超时后才能被其他客户端获取。所以要设置合理的锁超时时间。 Redis 服务器和客户端之间的时钟不同步,可能导致锁提前或延后过期。尽量确保服务器和客户端时钟同步(NTP),同时适当增加锁的超时时间作为缓冲。 分布式锁的实现有 Redlock 算法、go-redis 中的简易 mutex 分布式锁。 延时队列 通过 List 来作为简易的消息队列使用。 通过 rpush , lpush 控制入队,rpop , lpop 控制出队。 使用 List 实现延时队列会出现一个问题,假设 List 为空,那么之后的 pop 操作就是死循环。此时可以使用阻塞读来避免这种情况,即使用 blpop, brpop 两个命令代替之前的出队命令。阻塞读在队列没有数据的时候,会立即进入休眠状态。 同时需要注意,List 长时间为空,Redis 客户端连接可能会因为空闲而被回收。所以需要加入重试机制。...

April 29, 2025 · 914 words · Kurong

《高效使用Redis》记录

《高效使用Redis》记录 基础数据结构解析 对象 Redis中的 RedisObject 的 c 定义: 1 2 3 4 5 6 7 8 #define LRU_BITS 24 typedef struct redisObject { unsigned type:4; // 数据类型 unsigned encoding:4; // 底层数据结构 unsigned lru:LRU_BITS; // 缓存淘汰时使用 int refcount; // 引用计数 void *ptr; // 指向实际存储位置 } robj; RedisObject 是 Redis 中数据结构的一个抽象,用它来存储所有的 key-value 数据。 下面是结构体中各个属性的。说明: type:用来表示对象类型; encoding:表示当前对象底层存储所采用的数据结构,如int、字典、压缩列表、跳跃表等等; lru:用于在配置文件中通过 maxmemory-policy 配置已用内存到最大内存限制时的缓存淘汰策略。 以 GET 命令为例,简单看看 Redis 中的 LRU 实现。使用 GET 后,会执行这段代码更新对象的 lru 属性: 1 2 3 4 5 if (server....

December 5, 2024 · 1410 words · Kurong