深度解析:用户注册场景下的“缓存穿透”防御战

视频

image-d9251b0a

引子:缓存的双刃剑

在如今微服务遍地的互联网项目中,使用缓存(比如 Redis)是提高效率最常用的手段。尤其是在 读多写少 的场景下,缓存能极大地降低数据库压力。

但是,引入缓存同样也带来了一系列需要解决的问题。今天我们要深入探讨的,就是其中最棘手的问题之一:缓存穿透

什么是缓存穿透?

定义

缓存穿透是指:查询的数据在 缓存和数据库中都不存在,导致每次查询都会"穿透"缓存,直接查询数据库。

什么是缓存穿透-98c11728

危害有多大?

我们都知道,缓存存在的意义就是为了缓解数据库的压力。那么问题来了:

数据库_vs_Redis_抗压能力对比-907aa3bf

如果短时间内发生大量缓存穿透请求,所有请求都直接打到数据库,极有可能造成数据库宕机

产生缓存穿透的原因

缓存穿透的两大原因-974052a5

常见解决方案分析

既然了解了缓存穿透的危害,我们来看看业界常见的解决方案,并分析它们各自的优缺点。

方案一:缓存空对象

思路:当查询的数据在缓存和数据库中都不存在时,就缓存一个空结果(比如 null),并设置一个过期时间。

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

🤔 但问题来了:让我们用用户注册业务来分析

用户注册业务分析缓存穿透-c6857ba6

结论:缓存空对象适合 空值能被复用 的场景。但对于用户注册这种每次查询的 key 都不同的业务,不太适合。

方案二:分布式锁

思路:使用分布式锁,每次只允许一个请求去查询数据库,其他请求等待。

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

🤔 问题分析:

注册模块分布式锁的问题-7027d37f

结论:分布式锁适合 并发量不高 的场景。对于大麦网这种高并发项目,不太适合。

方案三:布隆过滤器

思路:在查询缓存之前,先用布隆过滤器判断数据是否存在。如果布隆过滤器说"不存在",就直接返回,不再查询缓存和数据库。

布隆过滤器原理

布隆过滤器原理_-_Bloom_Filter-728b7088

布隆过滤器的特点

特点 说明
空间效率极高 存储空间是常数 O(k),远小于存储原始数据
查询效率极高 时间复杂度 O(k),k 是哈希函数个数
支持海量数据 非常适合大数据场景
存在误判 说"存在"可能是误判,说"不存在"一定准确
无法删除 因为多个元素可能映射到同一位置

🤔 误判问题分析:

布隆过滤器的误判问题-d5cf89b0

用户注册业务的最终解决方案

通过上文分析,我们发现每个方案都有各自的问题:

方案 问题
缓存空对象 空值无法复用,不适合每次 key 都不同的场景
分布式锁 串行执行,严重影响高并发性能
布隆过滤器 存在误判,需要配合其他手段

那么,对于大麦网这种高并发系统,该如何解决呢?

答案是:从业务角度出发,多层防御!

业务分析:用户注册场景

在真实的业务场景中,大家在其他平台注册或登录时,应该遇到过这样的操作:

常见的注册流程-6a98cc6e

图形验证码的作用

  1. 缓解突发流量:增加了用户操作步骤,自然减缓请求速度
  2. 防止机器人攻击:机器很难自动完成图形验证
  3. 防刷:恶意用户无法批量发起请求

最终方案:四层防御体系

四层防御体系-59a9bad9

代码流程示意

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

核心总结

问题 分析 解决方案
缓存空对象不适用 每个用户手机号不同,空值无法复用 改用布隆过滤器
分布式锁影响性能 串行执行,不适合高并发 改用无锁方案
布隆过滤器有误判 说"存在"可能是误判 误判时查数据库确认
恶意攻击防不住 纯技术手段有局限 图形验证码从业务层拦截
突发流量冲击 可能压垮数据库 请求数限制作为兜底

💡 设计思想:解决缓存穿透问题,不能只依赖单一的技术手段。正所谓技术都是围绕业务来服务的,一定要从业务角度出发,采用多层防御策略。图形验证码 + 请求数限制 + 布隆过滤器 + 数据库索引,层层过滤,才能在保证用户体验的同时,有效保护系统。


企业级项目导航:⬅️ 01-布隆过滤器顶层设计深度讲解 | 02-深度解析:用户注册场景下的“缓存穿透”防御战 | ➡️ 03-(缓存穿透)用户注册业务深度解析