--- title: "02-深度解析:用户注册场景下的“缓存穿透”防御战" created: 2025-12-11 aliases: - 深度解析:用户注册场景下的“缓存穿透”防御战 tags: - 项目 --- # 深度解析:用户注册场景下的“缓存穿透”防御战 ## 视频 ![[image-d9251b0a.png]] ## **引子:缓存的双刃剑** 在如今微服务遍地的互联网项目中,使用缓存(比如 Redis)是提高效率最常用的手段。尤其是在 **读多写少** 的场景下,缓存能极大地降低数据库压力。 但是,引入缓存同样也带来了一系列需要解决的问题。今天我们要深入探讨的,就是其中最棘手的问题之一:**缓存穿透**。 ## **什么是缓存穿透?** ### **定义** 缓存穿透是指:查询的数据在 **缓存和数据库中都不存在**,导致每次查询都会"穿透"缓存,直接查询数据库。 ![[什么是缓存穿透-98c11728.jpg]] ### **危害有多大?** 我们都知道,缓存存在的意义就是为了缓解数据库的压力。那么问题来了: ![[数据库_vs_Redis_抗压能力对比-907aa3bf.jpg]] 如果短时间内发生大量缓存穿透请求,所有请求都直接打到数据库,**极有可能造成数据库宕机**。 ### **产生缓存穿透的原因** ![[缓存穿透的两大原因-974052a5.jpg]] ## **常见解决方案分析** 既然了解了缓存穿透的危害,我们来看看业界常见的解决方案,并分析它们各自的优缺点。 ### **方案一:缓存空对象** **思路**:当查询的数据在缓存和数据库中都不存在时,就缓存一个空结果(比如 `null`),并设置一个过期时间。 ```java public User getUser(String phone) { // 1. 查缓存 User user = redisCache.get(phone); // 2. 缓存有值(包括空值标记) if (user != null) { return user == NULL_MARKER ? null : user; } // 3. 查数据库 user = userMapper.selectByPhone(phone); // 4. 数据库也没有,缓存空值 if (user == null) { redisCache.set(phone, NULL_MARKER, 30, TimeUnit.SECONDS); return null; } // 5. 数据库有,缓存真实数据 redisCache.set(phone, user); return user; } ``` 流程示意: ![[缓存空对象方案-2da6a853.jpg]] 🤔 但问题来了:让我们用用户注册业务来分析 ![[用户注册业务分析缓存穿透-c6857ba6.jpg]] 结论:缓存空对象适合 空值能被复用 的场景。但对于用户注册这种每次查询的 key 都不同的业务,不太适合。 ### **方案二:分布式锁** **思路**:使用分布式锁,每次只允许一个请求去查询数据库,其他请求等待。 ```java public User getUser(String phone) { // 1. 查缓存 User user = redisCache.get(phone); if (user != null) { return user; } // 2. 获取分布式锁 String lockKey = "lock:user:" + phone; try { if (redisLock.tryLock(lockKey)) { // 3. 双重检查 user = redisCache.get(phone); if (user != null) { return user; } // 4. 查数据库 user = userMapper.selectByPhone(phone); // 5. 写入缓存 redisCache.set(phone, user); return user; } else { // 等待后重试 Thread.sleep(100); return getUser(phone); } } finally { redisLock.unlock(lockKey); } } ``` 流程示意: ![[分布式锁方案_-_串行化请求处理-8fbf75ce.jpg]] 🤔 问题分析: ![[注册模块分布式锁的问题-7027d37f.jpg]] 结论:分布式锁适合 并发量不高 的场景。对于大麦网这种高并发项目,不太适合。 ### **方案三:布隆过滤器** **思路**:在查询缓存之前,先用布隆过滤器判断数据是否存在。如果布隆过滤器说"不存在",就直接返回,不再查询缓存和数据库。 **布隆过滤器原理**: ![[布隆过滤器原理_-_Bloom_Filter-728b7088.jpg]] **布隆过滤器的特点**: | **特点** | **说明** | | --- | --- | | **空间效率极高** | 存储空间是常数 O(k),远小于存储原始数据 | | **查询效率极高** | 时间复杂度 O(k),k 是哈希函数个数 | | **支持海量数据** | 非常适合大数据场景 | | **存在误判** | 说"存在"可能是误判,说"不存在"一定准确 | | **无法删除** | 因为多个元素可能映射到同一位置 | 🤔 误判问题分析: ![[布隆过滤器的误判问题-d5cf89b0.jpg]] ## **用户注册业务的最终解决方案** 通过上文分析,我们发现每个方案都有各自的问题: | **方案** | **问题** | | --- | --- | | 缓存空对象 | 空值无法复用,不适合每次 key 都不同的场景 | | 分布式锁 | 串行执行,严重影响高并发性能 | | 布隆过滤器 | 存在误判,需要配合其他手段 | 那么,对于大麦网这种高并发系统,该如何解决呢? **答案是:从业务角度出发,多层防御!** ### **业务分析:用户注册场景** 在真实的业务场景中,大家在其他平台注册或登录时,应该遇到过这样的操作: ![[常见的注册流程-6a98cc6e.jpg]] **图形验证码的作用**: 1. **缓解突发流量**:增加了用户操作步骤,自然减缓请求速度 2. **防止机器人攻击**:机器很难自动完成图形验证 3. **防刷**:恶意用户无法批量发起请求 ### **最终方案:四层防御体系** ![[四层防御体系-59a9bad9.jpg]] ### **代码流程示意** ```java public void register(UserRegisterDto dto) { // ================== 第一层:验证码校验 ================== if (needCaptcha()) { boolean captchaValid = captchaService.verify(dto.getCaptchaToken()); if (!captchaValid) { throw new BusinessException("验证码校验失败"); } } // ================== 第二层:请求数限制 ================== // 根据数据库性能配置,比如每秒最多 1000 个注册请求 Long currentCount = redisCache.incr("register:count:" + currentSecond()); if (currentCount > REGISTER_LIMIT_PER_SECOND) { throw new BusinessException("系统繁忙,请稍后重试"); } // ================== 第三层:布隆过滤器 ================== boolean mayExist = bloomFilter.mightContain(dto.getPhone()); if (!mayExist) { // 布隆过滤器说"不存在",一定不存在,可以直接注册 doRegister(dto); // 注册成功后,将手机号加入布隆过滤器 bloomFilter.add(dto.getPhone()); return; } // ================== 第四层:数据库确认 ================== // 布隆过滤器说"存在",可能是误判,需要查数据库确认 User existUser = userMapper.selectByPhone(dto.getPhone()); if (existUser != null) { throw new BusinessException("该手机号已注册"); } // 数据库确认不存在(布隆误判),可以注册 doRegister(dto); bloomFilter.add(dto.getPhone()); } ``` ### **各层作用对比** | **防御层** | **作用** | **特点** | | --- | --- | --- | | **图形验证码** | 拦截机器人、缓解流量 | 从源头减少恶意请求 | | **请求数限制** | 保护数据库 | 兜底方案,可根据 DB 性能配置 | | **布隆过滤器** | 快速过滤不存在的数据 | O(1) 时间复杂度,极高效率 | | **数据库索引** | 处理误判情况 | 有索引时查询效率也很高 | ## **效果分析** ![[四层防御体系-59a9bad9.jpg]] ## **核心总结** | **问题** | **分析** | **解决方案** | | --- | --- | --- | | **缓存空对象不适用** | 每个用户手机号不同,空值无法复用 | 改用布隆过滤器 | | **分布式锁影响性能** | 串行执行,不适合高并发 | 改用无锁方案 | | **布隆过滤器有误判** | 说"存在"可能是误判 | 误判时查数据库确认 | | **恶意攻击防不住** | 纯技术手段有局限 | 图形验证码从业务层拦截 | | **突发流量冲击** | 可能压垮数据库 | 请求数限制作为兜底 | --- > *💡 **设计思想**:解决缓存穿透问题,不能只依赖单一的技术手段。**正所谓技术都是围绕业务来服务的**,一定要从业务角度出发,采用多层防御策略。图形验证码 + 请求数限制 + 布隆过滤器 + 数据库索引,层层过滤,才能在保证用户体验的同时,有效保护系统。* --- **企业级项目导航**:⬅️ [[01-布隆过滤器顶层设计深度讲解|01-布隆过滤器顶层设计深度讲解]] | 02-深度解析:用户注册场景下的“缓存穿透”防御战 | ➡️ [[03-(缓存穿透)用户注册业务深度解析|03-(缓存穿透)用户注册业务深度解析]]