SpringBoot + 本地缓存 + 布隆过滤器:防止恶意 ID 查询打穿数据库

01 引言:高并发下的数据库"隐形杀手"

在高并发的后端系统中,数据库是核心但也是最脆弱的环节之一。

恶意ID查询攻击堪称数据库的"隐形杀手",今天咱们就拆解这套结合布隆过滤器+本地缓存的防护方案,让你的系统扛住这类攻击,稳如泰山。

02 恶意查询攻击:高并发下的数据库"致命伤"

在日常开发中,我们经常会遭遇这类恶意攻击场景:

  • 攻击者通过自动化脚本,循环查询不存在的用户ID,每一次请求都绕开缓存直接穿透到数据库;
  • 恶意用户批量枚举不存在的商品ID,持续消耗数据库连接和CPU资源;
  • 刷单机器人伪造大量不存在的订单ID发起查询,既消耗资源又试图探测系统漏洞;
  • 爬虫程序无差别遍历各类资源ID,导致数据库查询请求量暴增。

这类攻击看似简单,实则破坏力极强。因为不存在的ID无法被缓存命中(也就是缓存穿透),每一次请求都会直接打在数据库上。短时间内海量的无效查询,会让数据库CPU、IO飙升,轻则响应变慢,重则直接宕机,整个系统都会受牵连。

03 布隆过滤器:轻量级的"无效请求筛子"

面对缓存穿透问题,布隆过滤器是性价比极高的解决方案。

它是一种概率型数据结构,核心能力是快速判断"某个元素一定不存在"或"可能存在"------注意是"可能存在",因为它有极低的误判率,但"一定不存在"是绝对准确的。这恰好契合我们的需求:把所有无效的ID查询(一定不存在的)提前过滤掉,不让它们触及数据库。

和传统缓存方案比,布隆过滤器有两大核心优势:

  1. 占用内存极小,哪怕存储百万级ID,也只需要几MB内存;
  2. 查询速度极快,时间复杂度接近O(1),高并发下几乎无性能损耗。

04 核心实现方案:从布隆过滤器到安全查询链路

下面是防护恶意ID查询攻击的整体流程图,展示了从请求到响应的完整防护链路:
#mermaid-svg-BP5Gwbny1RaavdRQ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BP5Gwbny1RaavdRQ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BP5Gwbny1RaavdRQ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BP5Gwbny1RaavdRQ .error-icon{fill:#552222;}#mermaid-svg-BP5Gwbny1RaavdRQ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BP5Gwbny1RaavdRQ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BP5Gwbny1RaavdRQ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BP5Gwbny1RaavdRQ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BP5Gwbny1RaavdRQ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BP5Gwbny1RaavdRQ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BP5Gwbny1RaavdRQ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BP5Gwbny1RaavdRQ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BP5Gwbny1RaavdRQ .marker.cross{stroke:#333333;}#mermaid-svg-BP5Gwbny1RaavdRQ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BP5Gwbny1RaavdRQ p{margin:0;}#mermaid-svg-BP5Gwbny1RaavdRQ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BP5Gwbny1RaavdRQ .cluster-label text{fill:#333;}#mermaid-svg-BP5Gwbny1RaavdRQ .cluster-label span{color:#333;}#mermaid-svg-BP5Gwbny1RaavdRQ .cluster-label span p{background-color:transparent;}#mermaid-svg-BP5Gwbny1RaavdRQ .label text,#mermaid-svg-BP5Gwbny1RaavdRQ span{fill:#333;color:#333;}#mermaid-svg-BP5Gwbny1RaavdRQ .node rect,#mermaid-svg-BP5Gwbny1RaavdRQ .node circle,#mermaid-svg-BP5Gwbny1RaavdRQ .node ellipse,#mermaid-svg-BP5Gwbny1RaavdRQ .node polygon,#mermaid-svg-BP5Gwbny1RaavdRQ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BP5Gwbny1RaavdRQ .rough-node .label text,#mermaid-svg-BP5Gwbny1RaavdRQ .node .label text,#mermaid-svg-BP5Gwbny1RaavdRQ .image-shape .label,#mermaid-svg-BP5Gwbny1RaavdRQ .icon-shape .label{text-anchor:middle;}#mermaid-svg-BP5Gwbny1RaavdRQ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BP5Gwbny1RaavdRQ .rough-node .label,#mermaid-svg-BP5Gwbny1RaavdRQ .node .label,#mermaid-svg-BP5Gwbny1RaavdRQ .image-shape .label,#mermaid-svg-BP5Gwbny1RaavdRQ .icon-shape .label{text-align:center;}#mermaid-svg-BP5Gwbny1RaavdRQ .node.clickable{cursor:pointer;}#mermaid-svg-BP5Gwbny1RaavdRQ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BP5Gwbny1RaavdRQ .arrowheadPath{fill:#333333;}#mermaid-svg-BP5Gwbny1RaavdRQ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BP5Gwbny1RaavdRQ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BP5Gwbny1RaavdRQ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BP5Gwbny1RaavdRQ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BP5Gwbny1RaavdRQ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BP5Gwbny1RaavdRQ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BP5Gwbny1RaavdRQ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BP5Gwbny1RaavdRQ .cluster text{fill:#333;}#mermaid-svg-BP5Gwbny1RaavdRQ .cluster span{color:#333;}#mermaid-svg-BP5Gwbny1RaavdRQ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BP5Gwbny1RaavdRQ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BP5Gwbny1RaavdRQ rect.text{fill:none;stroke-width:0;}#mermaid-svg-BP5Gwbny1RaavdRQ .icon-shape,#mermaid-svg-BP5Gwbny1RaavdRQ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BP5Gwbny1RaavdRQ .icon-shape p,#mermaid-svg-BP5Gwbny1RaavdRQ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BP5Gwbny1RaavdRQ .icon-shape .label rect,#mermaid-svg-BP5Gwbny1RaavdRQ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BP5Gwbny1RaavdRQ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BP5Gwbny1RaavdRQ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BP5Gwbny1RaavdRQ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ID一定不存在
ID可能存在
缓存命中
缓存未命中
数据库存在
数据库不存在
用户查询请求

携带用户ID
布隆过滤器检查
直接返回null

拦截无效请求
查询Redis缓存
返回缓存数据
查询数据库
缓存用户数据30分钟
返回用户数据
缓存空值5分钟

防重复攻击
返回null
请求结束

4.1 自定义布隆过滤器实现

先实现一个基础的布隆过滤器,核心是多哈希函数+位图存储,注解已标注关键逻辑:

java 复制代码
import java.util.BitSet;

/**
 * 通用布隆过滤器实现
 * 核心原理:多哈希函数映射元素到位图,通过位图判断元素是否存在
 * @param <T> 支持任意类型的元素(需重写toString)
 */
public class BloomFilter<T> {
    // 位图核心存储结构,用于标记哈希后的位置
    private final BitSet bitSet;
    // 位图总大小,由预期元素数和误判率计算得出
    private final int bitSetSize;
    // 预期存储的元素数量
    private final int expectedNumberOfItems;
    // 哈希函数的数量,影响误判率
    private final int numberOfHashFunctions;
    // 哈希函数数组,多哈希降低误判率
    private final HashFunction[] hashFunctions;

    /**
     * 构造布隆过滤器
     * @param expectedNumberOfItems 预期存储的元素数量
     * @param falsePositiveProbability 允许的误判率(如0.01表示1%)
     */
    public BloomFilter(int expectedNumberOfItems, double falsePositiveProbability) {
        this.expectedNumberOfItems = expectedNumberOfItems;
        // 计算公式:根据预期元素数和误判率,推导位图最优大小
        this.bitSetSize = (int) Math.ceil(expectedNumberOfItems *
                Math.log(falsePositiveProbability) / Math.log(1.0 / (Math.pow(2.0, Math.log(2.0)))));
        // 计算公式:推导最优哈希函数数量,平衡误判率和性能
        this.numberOfHashFunctions = (int) Math.ceil(Math.log(2) * bitSetSize / expectedNumberOfItems);
        this.bitSet = new BitSet(bitSetSize);

        // 初始化多个哈希函数(不同seed,避免哈希碰撞)
        this.hashFunctions = new HashFunction[numberOfHashFunctions];
        for (int i = 0; i < numberOfHashFunctions; i++) {
            hashFunctions[i] = new SimpleHash(bitSetSize, i + 1);
        }
    }

    /**
     * 添加元素到布隆过滤器
     * @param element 待添加的元素(非null)
     */
    public void add(T element) {
        if (element != null) {
            // 每个哈希函数计算一个位置,位图标记为true
            for (HashFunction hashFunction : hashFunctions) {
                int position = hashFunction.hash(element.toString());
                bitSet.set(position);
            }
        }
    }

    /**
     * 判断元素是否可能存在于布隆过滤器中
     * @param element 待判断的元素
     * @return false=一定不存在;true=可能存在(有极小误判率)
     */
    public boolean contains(T element) {
        if (element == null) {
            return false;
        }
        // 只要有一个哈希位置未标记,说明元素一定不存在
        for (HashFunction hashFunction : hashFunctions) {
            int position = hashFunction.hash(element.toString());
            if (!bitSet.get(position)) {
                return false;
            }
        }
        // 所有哈希位置都标记,说明元素可能存在
        return true;
    }

    /**
     * 哈希函数接口
     */
    private interface HashFunction {
        int hash(String value);
    }

    /**
     * 简单哈希实现(结合seed避免单一哈希的碰撞问题)
     */
    private static class SimpleHash implements HashFunction {
        // 位图容量(用于取模)
        private int cap;
        // 哈希种子,不同种子生成不同哈希值
        private int seed;

        public SimpleHash(int cap, int seed) {
            this.cap = cap;
            this.seed = seed;
        }

        /**
         * 计算字符串的哈希值
         * @param value 待哈希的字符串
         * @return 哈希后对应的位图位置
         */
        @Override
        public int hash(String value) {
            int result = 0;
            int len = value.length();
            for (int i = 0; i < len; i++) {
                // 结合字符ASCII码和种子计算哈希
                result = seed * result + value.charAt(i);
            }
            // 取模保证哈希值在位图范围内
            return (cap - 1) & result;
        }
    }
}

4.2 安全查询服务:布隆过滤器+缓存+数据库

基于布隆过滤器封装查询逻辑,先过滤无效ID,再走缓存/数据库流程,避免数据库被无效请求冲击:

java 复制代码
import org.springframework.data.redis.core.RedisTemplate;
import java.time.Duration;

/**
 * 安全查询服务:整合布隆过滤器+Redis缓存+数据库,防止缓存穿透
 */
public class SafeQueryService {
    // 用户ID布隆过滤器,提前过滤不存在的ID
    private BloomFilter<Long> userBloomFilter;
    // Redis缓存模板,用于本地缓存之外的分布式缓存
    private RedisTemplate<String, Object> redisTemplate;
    // 用户数据持久层,操作数据库
    private UserRepository userRepository;

    /**
     * 根据用户ID查询用户信息
     * @param userId 用户ID
     * @return 存在则返回User对象,不存在返回null
     */
    public User getUserById(Long userId) {
        // 第一步:布隆过滤器快速过滤------不存在的ID直接返回null,不查缓存/数据库
        if (!userBloomFilter.contains(userId)) {
            return null;
        }

        // 第二步:查Redis缓存,命中则直接返回(避免查库)
        String cacheKey = "user:" + userId;
        User cachedUser = (User) redisTemplate.opsForValue().get(cacheKey);
        if (cachedUser != null) {
            return cachedUser;
        }

        // 第三步:缓存未命中,查数据库
        User user = userRepository.findById(userId);
        // 第四步:缓存回写------避免后续重复查库
        if (user != null) {
            // 存在的用户:缓存30分钟,正常缓存有效期
            redisTemplate.opsForValue().set(cacheKey, user, Duration.ofMinutes(30));
        } else {
            // 不存在的用户:缓存空值5分钟(短期防重复攻击)
            redisTemplate.opsForValue().set(cacheKey, null, Duration.ofMinutes(5));
        }

        return user;
    }
}

// 以下为配套基础类(简化版)
class User {}
interface UserRepository {
    User findById(Long userId);
}

4.3 布隆过滤器初始化:加载全量有效ID

系统启动时,把数据库中所有有效用户ID加载到布隆过滤器,确保过滤器能准确过滤:

java 复制代码
import java.util.List;

/**
 * 布隆过滤器初始化器:系统启动时加载全量有效ID
 */
public class BloomFilterInitializer {
    // 用户ID布隆过滤器
    private BloomFilter<Long> userBloomFilter;
    // 用户数据持久层,查询全量ID
    private UserRepository userRepository;

    /**
     * 初始化布隆过滤器:加载所有用户ID到过滤器中
     */
    public void initializeBloomFilter() {
        // 从数据库查询所有有效用户ID
        List<Long> allUserIds = userRepository.findAllIds();
        
        // 遍历ID,添加到布隆过滤器
        for (Long userId : allUserIds) {
            userBloomFilter.add(userId);
        }

        System.out.println("布隆过滤器初始化完成,共加载 " + allUserIds.size() + " 个ID");
    }
}

// 补充UserRepository的findAllIds方法
interface UserRepository {
    User findById(Long userId);
    List<Long> findAllIds();
}

05 高级优化策略:让布隆过滤器更适配生产环境

5.1 多级布隆过滤器:本地+Redis分布式

单机布隆过滤器无法适配分布式系统,结合本地+Redis布隆过滤器,兼顾性能和分布式一致性:

java 复制代码
/**
 * 多级布隆过滤器:本地过滤器(高性能)+ Redis分布式过滤器(全局一致)
 */
public class MultiLevelBloomFilter {
    // 本地布隆过滤器:优先查询,减少网络IO
    private final BloomFilter<Long> localBloomFilter;
    // Redis布隆过滤器:分布式环境下全局一致
    private final RedisBloomFilter redisBloomFilter;

    public MultiLevelBloomFilter() {
        // 本地过滤器:预设10万元素,误判率1%
        this.localBloomFilter = new BloomFilter<>(100000, 0.01);
        this.redisBloomFilter = new RedisBloomFilter();
    }

    /**
     * 判断ID是否可能存在
     * 逻辑:先查本地(快),本地不存在则直接返回;本地存在再查Redis(全局)
     * @param id 待判断的ID
     * @return false=一定不存在;true=可能存在
     */
    public boolean mightExist(Long id) {
        if (!localBloomFilter.contains(id)) {
            return false;
        }
        return redisBloomFilter.mightContain(id);
    }

    /**
     * 添加ID到多级过滤器
     * @param id 待添加的ID
     */
    public void add(Long id) {
        localBloomFilter.add(id);
        redisBloomFilter.add(id);
    }
}

5.2 动态更新:适配ID的增删场景

布隆过滤器不支持删除,所以用户删除时需要重新初始化;用户新增时实时添加ID:

java 复制代码
/**
 * 布隆过滤器动态更新器:适配用户增删场景
 */
public class DynamicBloomFilterUpdater {
    private BloomFilter<Long> bloomFilter;
    private BloomFilterInitializer bloomFilterInitializer;

    /**
     * 处理用户创建事件:新增用户ID实时添加到过滤器
     * @param event 用户创建事件(含新用户ID)
     */
    public void handleUserCreated(UserCreatedEvent event) {
        bloomFilter.add(event.getUserId());
    }

    /**
     * 处理用户删除事件:重新初始化过滤器(布隆不支持删除,只能全量刷新)
     * @param event 用户删除事件
     */
    public void handleUserDeleted(UserDeletedEvent event) {
        reinitializeBloomFilter();
    }

    /**
     * 重新初始化布隆过滤器:加载最新全量ID
     */
    private void reinitializeBloomFilter() {
        bloomFilterInitializer.initializeBloomFilter();
    }
}

// 配套事件类(简化版)
class UserCreatedEvent {
    private Long userId;
    public Long getUserId() { return userId; }
}
class UserDeletedEvent {}

5.3 监控埋点:掌握过滤器运行状态

通过监控指标,了解布隆过滤器的命中/未命中情况,便于调优:

java 复制代码
import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;

/**
 * 布隆过滤器监控器:统计命中/未命中指标,便于观测和调优
 */
public class BloomFilterMonitor {
    private final MeterRegistry meterRegistry;
    // 布隆过滤器命中计数器(ID可能存在)
    private final Counter bloomFilterHitCounter;
    // 布隆过滤器未命中计数器(ID一定不存在)
    private final Counter bloomFilterMissCounter;

    public BloomFilterMonitor(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        // 注册命中指标:bloom_filter.hit
        this.bloomFilterHitCounter = Counter.builder("bloom_filter.hit")
                .description("布隆过滤器命中次数(ID可能存在)")
                .register(meterRegistry);
        // 注册未命中指标:bloom_filter.miss
        this.bloomFilterMissCounter = Counter.builder("bloom_filter.miss")
                .description("布隆过滤器未命中次数(ID一定不存在)")
                .register(meterRegistry);
    }

    /**
     * 检查ID并记录监控指标
     * @param id 待检查的ID
     * @param filter 布隆过滤器实例
     * @return 过滤器的检查结果
     */
    public boolean checkAndMonitor(Long id, BloomFilter<Long> filter) {
        boolean mightExist = filter.contains(id);
        // 统计指标
        if (mightExist) {
            bloomFilterHitCounter.increment();
        } else {
            bloomFilterMissCounter.increment();
        }
        return mightExist;
    }
}

06 Redis布隆过滤器集成:适配分布式场景

如果系统是分布式部署,推荐使用Redis官方的布隆过滤器模块(RedisBloom),替代自定义实现,更稳定:

java 复制代码
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;

/**
 * Redis布隆过滤器:基于RedisBloom模块实现,适配分布式场景
 */
public class RedisBloomFilter {
    private RedisTemplate<String, Object> redisTemplate;
    // Redis布隆过滤器的key前缀
    private static final String BLOOM_FILTER_KEY = "bloom_filter:user_ids";

    /**
     * 判断ID是否可能存在于Redis布隆过滤器中
     * @param id 待判断的用户ID
     * @return false=一定不存在;true=可能存在
     */
    public boolean mightContain(Long id) {
        try {
            // 调用RedisBloom的BF.EXISTS命令
            return (Boolean) redisTemplate.execute(
                    (RedisCallback<Object>) con ->
                            con.executeCommand(
                                    new CommandObject<>("BF.EXISTS", BLOOM_FILTER_KEY, String.valueOf(id))));
        } catch (Exception e) {
            // 异常时返回true,避免误过滤(降级策略:走后续缓存/数据库流程)
            return true;
        }
    }

    /**
     * 添加ID到Redis布隆过滤器
     * @param id 待添加的用户ID
     */
    public void add(Long id) {
        try {
            // 调用RedisBloom的BF.ADD命令
            redisTemplate.execute(
                    (RedisCallback<Object>) con ->
                            con.executeCommand(
                                    new CommandObject<>("BF.ADD", BLOOM_FILTER_KEY, String.valueOf(id))));
        } catch (Exception e) {
            // 异常不抛错,保证主流程可用
            e.printStackTrace();
        }
    }

    // 简化版CommandObject(适配Redis命令执行)
    static class CommandObject<T> {
        private String command;
        private String key;
        private String value;

        public CommandObject(String command, String key, String value) {
            this.command = command;
            this.key = key;
            this.value = value;
        }
    }
}

07 最佳实践总结

  1. 布隆过滤器优先:所有ID查询先过布隆过滤器,直接过滤掉100%不存在的ID,从源头减少无效数据库请求;
  2. 缓存分层设计:存在的用户缓存30分钟,不存在的用户缓存空值5分钟,兼顾缓存命中率和防攻击;
  3. 分布式适配:单机用本地布隆过滤器,分布式用"本地+Redis"多级过滤器,兼顾性能和一致性;
  4. 监控不可少:埋点命中/未命中指标,根据监控数据调整布隆过滤器的误判率和哈希函数数量;
  5. 异常降级:Redis布隆过滤器异常时,默认返回"可能存在",避免误过滤导致正常请求失败。

通过布隆过滤器+本地缓存+Redis缓存的三层防护,能有效抵御恶意ID查询攻击,让数据库只处理"有价值"的请求,大幅提升系统的稳定性和抗攻击能力。

相关推荐
憧憬成为java架构高手的小白1 小时前
数据库期末复习笔记
数据库·笔记·oracle
10WTW011 小时前
个人思考记录(一)What u need in AI era
数据库·mongodb
六月雨滴1 小时前
Oracle 性能监控体系概述
数据库·oracle·dba
小旭95271 小时前
MySQL 主从复制、MyCat 读写分离与分库分表实战
java·数据库·sql·mysql·database
计算机安禾1 小时前
【算法分析与设计】第38篇:最近点对与分治在几何中的应用
java·服务器·网络·数据库·算法
柏舟飞流1 小时前
向量数据库:从底层原理到选型实战
数据库
__Witheart__1 小时前
Android 驱动编译为模块或者built-in内核
android·linux·数据库
ZC跨境爬虫1 小时前
SQL学习日志 Day_1:初识SQL,开启数据之旅
数据库·sql·学习
计算机安禾1 小时前
【算法分析与设计】第37篇:平面扫描与线段交问题
java·大数据·数据库·算法·机器学习