01 引言:高并发下的数据库"隐形杀手"
在高并发的后端系统中,数据库是核心但也是最脆弱的环节之一。
恶意ID查询攻击堪称数据库的"隐形杀手",今天咱们就拆解这套结合布隆过滤器+本地缓存的防护方案,让你的系统扛住这类攻击,稳如泰山。
02 恶意查询攻击:高并发下的数据库"致命伤"
在日常开发中,我们经常会遭遇这类恶意攻击场景:
- 攻击者通过自动化脚本,循环查询不存在的用户ID,每一次请求都绕开缓存直接穿透到数据库;
- 恶意用户批量枚举不存在的商品ID,持续消耗数据库连接和CPU资源;
- 刷单机器人伪造大量不存在的订单ID发起查询,既消耗资源又试图探测系统漏洞;
- 爬虫程序无差别遍历各类资源ID,导致数据库查询请求量暴增。
这类攻击看似简单,实则破坏力极强。因为不存在的ID无法被缓存命中(也就是缓存穿透),每一次请求都会直接打在数据库上。短时间内海量的无效查询,会让数据库CPU、IO飙升,轻则响应变慢,重则直接宕机,整个系统都会受牵连。
03 布隆过滤器:轻量级的"无效请求筛子"
面对缓存穿透问题,布隆过滤器是性价比极高的解决方案。
它是一种概率型数据结构,核心能力是快速判断"某个元素一定不存在"或"可能存在"------注意是"可能存在",因为它有极低的误判率,但"一定不存在"是绝对准确的。这恰好契合我们的需求:把所有无效的ID查询(一定不存在的)提前过滤掉,不让它们触及数据库。
和传统缓存方案比,布隆过滤器有两大核心优势:
- 占用内存极小,哪怕存储百万级ID,也只需要几MB内存;
- 查询速度极快,时间复杂度接近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 最佳实践总结
- 布隆过滤器优先:所有ID查询先过布隆过滤器,直接过滤掉100%不存在的ID,从源头减少无效数据库请求;
- 缓存分层设计:存在的用户缓存30分钟,不存在的用户缓存空值5分钟,兼顾缓存命中率和防攻击;
- 分布式适配:单机用本地布隆过滤器,分布式用"本地+Redis"多级过滤器,兼顾性能和一致性;
- 监控不可少:埋点命中/未命中指标,根据监控数据调整布隆过滤器的误判率和哈希函数数量;
- 异常降级:Redis布隆过滤器异常时,默认返回"可能存在",避免误过滤导致正常请求失败。
通过布隆过滤器+本地缓存+Redis缓存的三层防护,能有效抵御恶意ID查询攻击,让数据库只处理"有价值"的请求,大幅提升系统的稳定性和抗攻击能力。