欢迎光临
我们一直在努力

【Java后端技术亮点】布隆过滤器防缓存穿透:千万级数据下几MB内存的极致防护

黑客用脚本每秒查询10万次不存在的用户ID,数据库直接被打崩——这就是缓存穿透。本文将揭秘如何用几MB内存的布隆过滤器,轻松挡住恶意攻击,守护你的数据库。

在这里插入图片描述

文章目录

    • 一、场景引入:一场有预谋的攻击
      • 1.1 真实案例
      • 1.2 什么是缓存穿透?
      • 1.3 缓存穿透的危害场景
    • 二、解决方案:布隆过滤器
      • 2.1 什么是布隆过滤器?
      • 2.2 布隆过滤器原理
      • 2.3 误判率计算
    • 三、实战代码:从零实现布隆过滤器防缓存穿透
      • 3.1 Redisson布隆过滤器实现(推荐)
      • 3.2 布隆过滤器预热服务
      • 3.3 业务层使用布隆过滤器
      • 3.4 自研Guava布隆过滤器(单机场景)
      • 3.5 布谷鸟过滤器(支持删除)
    • 四、高级进阶:多层防护架构
      • 4.1 完整防护链路
      • 4.2 布隆过滤器定时重建
    • 五、预判问题与解答
      • Q1:布隆过滤器的误判率可以降低到0吗?
      • Q2:布隆过滤器不支持删除,怎么办?
      • Q3:布隆过滤器和缓存空值有什么区别?
      • Q4:布隆过滤器预热数据量太大怎么办?
      • Q5:布隆过滤器对性能有影响吗?
    • 六、面试高频考点
      • 考点1:布隆过滤器的优缺点?
      • 考点2:如何解决布隆过滤器的误判问题?
      • 考点3:Redis的布隆过滤器插件和Redisson有什么区别?
      • 考点4:布隆过滤器除了防缓存穿透还有什么用?
    • 七、总结与最佳实践
      • 7.1 核心要点回顾
      • 7.2 适用场景
      • 7.3 性能提升数据
    • 八、参考与拓展

一、场景引入:一场有预谋的攻击

1.1 真实案例

某电商平台上线新活动,突然遭受攻击:

攻击时间线:
T+0秒:攻击脚本启动,每秒10万次请求
T+1秒:请求参数:userId=999999999(数据库不存在)
T+2秒:Redis缓存未命中(Key不存在)
T+3秒:请求打到MySQL,数据库CPU飙升
T+10秒:数据库连接池耗尽,服务不可用
T+30秒:运维发现异常,紧急封IP

问题根源:攻击者故意查询不存在的数据,每次请求都穿透缓存打到数据库。

1.2 什么是缓存穿透?

缓存穿透是指查询一个不存在的数据,由于缓存中也没有,每次请求都会打到数据库。

正常请求流程:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 请求 │────→│ Redis │────→│ MySQL │
│ user=1 │ │ 命中 │ │ 不查询 │
└─────────┘ └─────────┘ └─────────┘

缓存穿透流程:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 请求 │────→│ Redis │────→│ MySQL │
│ user=999│ │ 未命中 │────→│ 查询 │
│(不存在) │ │ │ │ 无结果 │
└─────────┘ └─────────┘ └─────────┘
↑_____________________________|
每次请求都走这条路径

1.3 缓存穿透的危害场景

场景攻击方式危害程度
恶意攻击 批量查询不存在的ID 🔴 极高
业务误用 前端传错参数 🟡 中
数据删除 查询已删除的数据 🟠 高
爬虫扫描 遍历ID范围 🟠 高

二、解决方案:布隆过滤器

2.1 什么是布隆过滤器?

布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,用于判断一个元素可能存在或一定不存在。

布隆过滤器核心特性:

┌─────────────────────────────────────────────────────────────┐
│ │
│ 查询结果: │
│ ├── "可能存在" → 有一定误判率(可能不存在但被判断为存在) │
│ └── "一定不存在" → 100%准确(不存在就是不存在) │
│ │
│ 特点: │
│ – 零误杀:不会把存在的判断为不存在 │
│ – 空间极小:1000万数据只需约6MB │
│ – 无删除:标准布隆过滤器不支持删除(有改进版) │
│ │
└─────────────────────────────────────────────────────────────┘

2.2 布隆过滤器原理

布隆过滤器工作原理:

1. 初始化:创建一个全为0的bit数组
┌─────────────────────────────────────┐
│ 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 │
└─────────────────────────────────────┘

2. 添加元素:使用多个Hash函数计算位置,将对应bit置为1

添加 "user:100":
Hash1("user:100") = 2 → 置为1
Hash2("user:100") = 5 → 置为1
Hash3("user:100") = 11 → 置为1

┌─────────────────────────────────────┐
│ 0 0 1 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 │
└─────────────────────────────────────┘
↑ ↑ ↑

3. 查询元素:计算Hash位置,如果所有位置都是1,则"可能存在"

查询 "user:100":
Hash1 = 2 (是1) ✓
Hash2 = 5 (是1) ✓
Hash3 = 11 (是1) ✓
→ 结果:可能存在 ✓

查询 "user:999"(不存在):
Hash1 = 3 (是0) ✗
→ 结果:一定不存在 ✓

2.3 误判率计算

/**
* 布隆过滤器参数计算
*
* 假设:
* – 预期数据量 n = 100万
* – 期望误判率 p = 0.01 (1%)
*
* 计算公式:
* – 最优bit数组大小 m = -n * ln(p) / (ln(2)^2)
* – 最优Hash函数个数 k = m * ln(2) / n
*
* 计算结果:
* – m ≈ 958万bit ≈ 1.2MB
* – k ≈ 7个Hash函数
*/


三、实战代码:从零实现布隆过滤器防缓存穿透

3.1 Redisson布隆过滤器实现(推荐)

/**
* Redisson布隆过滤器配置
* 开箱即用,支持分布式场景
*/

@Configuration
@Slf4j
public class RedissonBloomFilterConfig {

@Autowired
private RedissonClient redissonClient;

/**
* 用户ID布隆过滤器
* 预期100万用户,误判率1%
*/

@Bean
public RBloomFilter<String> userBloomFilter() {
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("bloom:user");

// 初始化:预期元素数量100万,误判率1%
bloomFilter.tryInit(1000000L, 0.01);

log.info("✅ 用户布隆过滤器初始化完成,预期数据量: 100万, 误判率: 1%");
return bloomFilter;
}

/**
* 商品ID布隆过滤器
* 预期10万商品,误判率0.1%
*/

@Bean
public RBloomFilter<String> productBloomFilter() {
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("bloom:product");

// 初始化:预期元素数量10万,误判率0.1%
bloomFilter.tryInit(100000L, 0.001);

log.info("✅ 商品布隆过滤器初始化完成,预期数据量: 10万, 误判率: 0.1%");
return bloomFilter;
}
}

3.2 布隆过滤器预热服务

/**
* 布隆过滤器预热服务
* 系统启动时将已有数据加载到布隆过滤器
*/

@Component
@Slf4j
public class BloomFilterPreheatService implements CommandLineRunner {

@Autowired
private RBloomFilter<String> userBloomFilter;

@Autowired
private RBloomFilter<String> productBloomFilter;

@Autowired
private UserMapper userMapper;

@Autowired
private ProductMapper productMapper;

@Override
public void run(String... args) {
log.info("🚀 开始预热布隆过滤器…");

StopWatch stopWatch = new StopWatch();
stopWatch.start();

// 预热用户布隆过滤器
preheatUserBloomFilter();

// 预热商品布隆过滤器
preheatProductBloomFilter();

stopWatch.stop();
log.info("✅ 布隆过滤器预热完成,耗时: {}ms", stopWatch.getTotalTimeMillis());
}

/**
* 预热用户布隆过滤器
*/

private void preheatUserBloomFilter() {
log.info("📦 开始预热用户布隆过滤器…");

// 清空旧数据(支持重建)
userBloomFilter.delete();
userBloomFilter.tryInit(1000000L, 0.01);

// 分批查询用户ID
int batchSize = 1000;
long lastId = 0L;
int totalCount = 0;

while (true) {
List<Long> userIds = userMapper.selectIdList(lastId, batchSize);

if (CollUtil.isEmpty(userIds)) {
break;
}

// 批量添加到布隆过滤器
for (Long userId : userIds) {
userBloomFilter.add("user:" + userId);
}

totalCount += userIds.size();
lastId = userIds.get(userIds.size() 1);

// 每10000条打印一次进度
if (totalCount % 10000 == 0) {
log.info("⏳ 用户布隆过滤器预热进度: {} 条", totalCount);
}
}

log.info("✅ 用户布隆过滤器预热完成,共 {} 条", totalCount);
}

/**
* 预热商品布隆过滤器
*/

private void preheatProductBloomFilter() {
log.info("📦 开始预热商品布隆过滤器…");

productBloomFilter.delete();
productBloomFilter.tryInit(100000L, 0.001);

List<Long> productIds = productMapper.selectAllIds();

if (CollUtil.isNotEmpty(productIds)) {
for (Long productId : productIds) {
productBloomFilter.add("product:" + productId);
}
}

log.info("✅ 商品布隆过滤器预热完成,共 {} 条", CollUtil.isEmpty(productIds) ? 0 : productIds.size());
}
}

3.3 业务层使用布隆过滤器

/**
* 用户服务(使用布隆过滤器防缓存穿透)
*/

@Service
@Slf4j
public class UserService {

@Autowired
private RBloomFilter<String> userBloomFilter;

@Autowired
private StringRedisTemplate redisTemplate;

@Autowired
private UserMapper userMapper;

/**
* 获取用户信息(布隆过滤器防护版)
*/

public UserVO getUserById(Long userId) {
String bloomKey = "user:" + userId;
String cacheKey = "user:detail:" + userId;

// Step 1: 布隆过滤器判断
if (!userBloomFilter.contains(bloomKey)) {
log.debug("🛡️ 布隆过滤器拦截不存在用户: {}", userId);
return null; // 用户一定不存在,直接返回
}

// Step 2: 查询Redis缓存
String json = redisTemplate.opsForValue().get(cacheKey);
if (StrUtil.isNotBlank(json)) {
return JSON.parseObject(json, UserVO.class);
}

// Step 3: 查询数据库
User user = userMapper.selectById(userId);
if (user == null) {
// 布隆过滤器误判(极小概率),缓存空值防止再次穿透
redisTemplate.opsForValue().set(cacheKey, "null", 5, TimeUnit.MINUTES);
return null;
}

// Step 4: 写入缓存
UserVO userVO = convertToVO(user);
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(userVO),
30, TimeUnit.MINUTES);

return userVO;
}

/**
* 新增用户(同步更新布隆过滤器)
*/

@Transactional
public void addUser(UserDTO dto) {
// 1. 插入数据库
User user = convertToEntity(dto);
userMapper.insert(user);

// 2. 添加到布隆过滤器
userBloomFilter.add("user:" + user.getId());

// 3. 写入缓存
String cacheKey = "user:detail:" + user.getId();
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(convertToVO(user)),
30, TimeUnit.MINUTES);

log.info("✅ 新增用户并同步布隆过滤器: {}", user.getId());
}

/**
* 删除用户(布隆过滤器无法删除,需要其他策略)
*/

@Transactional
public void deleteUser(Long userId) {
// 1. 删除数据库
userMapper.deleteById(userId);

// 2. 删除缓存
String cacheKey = "user:detail:" + userId;
redisTemplate.delete(cacheKey);

// 3. 注意:标准布隆过滤器不支持删除!
// 方案1:使用布谷鸟过滤器(支持删除)
// 方案2:定时重建布隆过滤器
// 方案3:使用Counting Bloom Filter

log.info("🗑️ 删除用户(布隆过滤器保留): {}", userId);
}
}

3.4 自研Guava布隆过滤器(单机场景)

/**
* Guava布隆过滤器(单机场景)
* 适合不依赖Redis的轻量级应用
*/

@Component
@Slf4j
public class GuavaBloomFilterService {

// 用户布隆过滤器
private BloomFilter<String> userBloomFilter;

// 商品布隆过滤器
private BloomFilter<String> productBloomFilter;

@PostConstruct
public void init() {
// 创建布隆过滤器
userBloomFilter = BloomFilter.create(
Funnels.stringFunnel(StandardCharsets.UTF_8),
1000000, // 预期数据量
0.01 // 误判率
);

productBloomFilter = BloomFilter.create(
Funnels.stringFunnel(StandardCharsets.UTF_8),
100000,
0.001
);

log.info("✅ Guava布隆过滤器初始化完成");
}

/**
* 添加用户到布隆过滤器
*/

public void addUser(Long userId) {
userBloomFilter.put("user:" + userId);
}

/**
* 判断用户可能存在
*/

public boolean mightContainUser(Long userId) {
return userBloomFilter.mightContain("user:" + userId);
}

/**
* 获取布隆过滤器统计信息
*/

public void printStats() {
log.info("用户布隆过滤器预期插入数量: {}", userBloomFilter.approximateElementCount());
log.info("用户布隆过滤器预期误判率: {}", userBloomFilter.expectedFpp());
}
}

3.5 布谷鸟过滤器(支持删除)

/**
* 布谷鸟过滤器(Cuckoo Filter)
* 支持删除操作,是布隆过滤器的升级版
*/

@Component
@Slf4j
public class CuckooFilterService {

@Autowired
private RedissonClient redissonClient;

private RCuckooFilter<String> userCuckooFilter;

@PostConstruct
public void init() {
userCuckooFilter = redissonClient.getCuckooFilter("cuckoo:user");

// 初始化:预期100万数据
userCuckooFilter.tryInit(1000000L);

log.info("✅ 布谷鸟过滤器初始化完成");
}

/**
* 添加元素
*/

public void add(Long userId) {
userCuckooFilter.add("user:" + userId);
}

/**
* 删除元素(布谷鸟过滤器支持删除)
*/

public void delete(Long userId) {
userCuckooFilter.delete("user:" + userId);
}

/**
* 查询元素
*/

public boolean contains(Long userId) {
return userCuckooFilter.contains("user:" + userId);
}
}


四、高级进阶:多层防护架构

4.1 完整防护链路

/**
* 缓存穿透多层防护服务
* 布隆过滤器 + 缓存空值 + 接口限流
*/

@Service
@Slf4j
public class CachePenetrationProtectService {

@Autowired
private RBloomFilter<String> userBloomFilter;

@Autowired
private StringRedisTemplate redisTemplate;

@Autowired
private UserMapper userMapper;

/**
* 多层防护获取用户
*
* 防护层级:
* L1: 接口限流(防止恶意攻击)
* L2: 布隆过滤器(快速判断不存在)
* L3: 缓存空值(防止重复查询不存在数据)
* L4: 互斥锁(防止缓存击穿)
*/

@SentinelResource(value = "getUser", blockHandler = "getUserBlockHandler")
public UserVO getUserWithProtect(Long userId) {
String bloomKey = "user:" + userId;
String cacheKey = "user:detail:" + userId;

// L1: 参数校验
if (userId == null || userId <= 0) {
throw new BizException("非法用户ID");
}

// L2: 布隆过滤器判断
if (!userBloomFilter.contains(bloomKey)) {
log.debug("🛡️ 布隆过滤器拦截: {}", userId);
return null;
}

// L3: 查询缓存
String json = redisTemplate.opsForValue().get(cacheKey);

// 缓存空值判断
if ("null".equals(json)) {
log.debug("🛡️ 缓存空值拦截: {}", userId);
return null;
}

if (StrUtil.isNotBlank(json)) {
return JSON.parseObject(json, UserVO.class);
}

// L4: 互斥锁防止缓存击穿
String lockKey = "lock:user:" + userId;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);

if (Boolean.TRUE.equals(locked)) {
try {
// 双重检查
json = redisTemplate.opsForValue().get(cacheKey);
if (StrUtil.isNotBlank(json)) {
return "null".equals(json) ? null : JSON.parseObject(json, UserVO.class);
}

// 查询数据库
User user = userMapper.selectById(userId);

if (user == null) {
// 缓存空值,5分钟过期
redisTemplate.opsForValue().set(cacheKey, "null", 5, TimeUnit.MINUTES);
return null;
}

// 写入缓存
UserVO vo = convertToVO(user);
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(vo),
30, TimeUnit.MINUTES);

return vo;

} finally {
redisTemplate.delete(lockKey);
}
} else {
// 其他线程等待后重试
ThreadUtil.sleep(100);
return getUserWithProtect(userId);
}
}

/**
* 限流降级处理
*/

public UserVO getUserBlockHandler(Long userId, BlockException ex) {
log.warn("🚫 用户查询被限流: {}", userId);
throw new BizException("系统繁忙,请稍后再试");
}
}

4.2 布隆过滤器定时重建

/**
* 布隆过滤器定时重建
* 解决删除数据导致的误判问题
*/

@Component
@Slf4j
public class BloomFilterRebuildScheduler {

@Autowired
private RedissonClient redissonClient;

@Autowired
private UserMapper userMapper;

/**
* 每周日凌晨3点重建布隆过滤器
*/

@Scheduled(cron = "0 0 3 ? * SUN")
public void rebuildUserBloomFilter() {
log.info("🔄 开始重建用户布隆过滤器…");

String tempName = "bloom:user:temp";
String activeName = "bloom:user";

try {
// 1. 创建临时布隆过滤器
RBloomFilter<String> tempFilter = redissonClient.getBloomFilter(tempName);
tempFilter.tryInit(1000000L, 0.01);

// 2. 加载最新数据
int batchSize = 1000;
long lastId = 0L;
int totalCount = 0;

while (true) {
List<Long> userIds = userMapper.selectIdList(lastId, batchSize);
if (CollUtil.isEmpty(userIds)) {
break;
}

for (Long userId : userIds) {
tempFilter.add("user:" + userId);
}

totalCount += userIds.size();
lastId = userIds.get(userIds.size() 1);
}

// 3. 原子替换
RBloomFilter<String> activeFilter = redissonClient.getBloomFilter(activeName);
activeFilter.delete();
tempFilter.rename(activeName);

log.info("✅ 用户布隆过滤器重建完成,共 {} 条", totalCount);

} catch (Exception e) {
log.error("❌ 布隆过滤器重建失败", e);
// 清理临时过滤器
redissonClient.getBloomFilter(tempName).delete();
}
}
}


五、预判问题与解答

Q1:布隆过滤器的误判率可以降低到0吗?

A:理论上不可能降到0,但可以通过调整参数降低:

// 误判率与空间占用的关系
// 100万数据:
// 误判率1% → 约1.2MB
// 误判率0.1% → 约1.8MB
// 误判率0.01% → 约2.4MB

// 实际应用中,1%的误判率已经足够低
// 即使误判,也只是多查一次Redis/DB,影响很小

Q2:布隆过滤器不支持删除,怎么办?

A:有三种解决方案:

方案优点缺点适用场景
布谷鸟过滤器 支持删除 空间利用率略低 需要频繁删除的场景
定时重建 简单可靠 有重建窗口期 删除不频繁的场景
Counting Bloom Filter 支持删除 空间占用4倍 内存充足的场景

Q3:布隆过滤器和缓存空值有什么区别?

A:两者是互补关系,不是替代关系:

布隆过滤器:
– 作用:快速判断数据是否可能存在
– 优点:内存极小,判断极快
– 缺点:有一定误判率,不支持删除

缓存空值:
– 作用:缓存查询结果为空的情况
– 优点:100%准确
– 缺点:占用Redis内存,需要设置过期时间

最佳实践:两者结合使用
1. 先用布隆过滤器快速过滤
2. 再用缓存空值兜底

Q4:布隆过滤器预热数据量太大怎么办?

A:采用分批加载+异步加载策略:

// 分批加载
int batchSize = 1000;
while (true) {
List<Long> ids = mapper.selectIdList(lastId, batchSize);
if (ids.isEmpty()) break;

for (Long id : ids) {
bloomFilter.add("user:" + id);
}

// 每批休眠10ms,避免CPU飙高
Thread.sleep(10);
}

// 异步加载(不阻塞启动)
@Async("taskExecutor")
public void asyncPreheat() {
preheatBloomFilter();
}

Q5:布隆过滤器对性能有影响吗?

A:影响极小,几乎可以忽略:

操作时间复杂度实际耗时
添加元素 O(k) ~1μs
查询元素 O(k) ~1μs
Hash计算 O(1) ~0.1μs

k是Hash函数个数(通常6-8个),单次查询只需几微秒。


六、面试高频考点

考点1:布隆过滤器的优缺点?

参考答案:

优点:
1. 空间效率极高:1000万数据只需约6MB
2. 查询速度快:O(k)时间复杂度,微秒级
3. 无漏报:不存在的数据100%能判断出来
4. 保密性好:不存储原始数据

缺点:
1. 有误判率:可能把不存在的判断为存在
2. 不支持删除:标准布隆过滤器无法删除(可用布谷鸟过滤器解决)
3. 无法获取元素列表:只能判断存在性
4. 需要提前预估数据量

考点2:如何解决布隆过滤器的误判问题?

参考答案:

1. 调整参数:
– 增大bit数组(降低误判率,但占用更多内存)
– 增加Hash函数个数

2. 结合其他手段:
– 布隆过滤器判断"可能存在"后,再查Redis确认
– 即使误判也只是多查一次缓存,影响很小

3. 使用改进版:
– 布谷鸟过滤器:支持删除,误判率更低
– Counting Bloom Filter:支持删除

4. 定期重建:
– 清理已删除数据,降低误判率累积

考点3:Redis的布隆过滤器插件和Redisson有什么区别?

参考答案:

特性RedisBloom插件Redisson
部署方式 需要安装插件 纯Java客户端
分布式 天然支持 基于Redis实现
性能 更高(C实现) 稍低(Java实现)
功能 基础功能 功能更丰富
易用性 需要额外部署 开箱即用

考点4:布隆过滤器除了防缓存穿透还有什么用?

参考答案:

1. 邮箱黑名单过滤:快速判断邮箱是否在黑名单
2. URL去重:爬虫已访问URL判断
3. 推荐去重:给用户推荐时过滤已看过的内容
4. 数据库查询优化:先判断数据是否存在,避免无效查询
5. 日志去重:判断日志是否已处理过
6. 比特币轻节点:SPV验证交易是否存在


七、总结与最佳实践

7.1 核心要点回顾

布隆过滤器防缓存穿透核心流程:

┌─────────────────────────────────────────────────────────────┐
│ 1. 系统启动预热 │
│ ├── 从数据库加载已有数据ID │
│ ├── 分批添加到布隆过滤器 │
│ └── 支持定时重建(解决删除问题) │
│ │
│ 2. 请求处理流程 │
│ ├── L1: 参数校验(拦截非法请求) │
│ ├── L2: 布隆过滤器(快速判断不存在) │
│ ├── L3: 查询Redis缓存 │
│ ├── L4: 缓存空值(防止重复穿透) │
│ └── L5: 互斥锁查DB(防止缓存击穿) │
│ │
│ 3. 数据变更同步 │
│ ├── 新增:同步添加到布隆过滤器 │
│ ├── 删除:定时重建或使用布谷鸟过滤器 │
│ └── 更新:不影响布隆过滤器 │
└─────────────────────────────────────────────────────────────┘

7.2 适用场景

场景是否推荐说明
用户系统 ✅ 强烈推荐 防止恶意查询不存在用户
商品系统 ✅ 强烈推荐 防止爬虫遍历商品ID
订单系统 ✅ 推荐 防止查询不存在订单
配置系统 ⚠️ 可选 数据量小,可直接缓存
日志系统 ✅ 推荐 URL去重、日志去重

7.3 性能提升数据

某社交平台实测数据(遭受攻击时):

指标无防护布隆过滤器防护提升
数据库QPS 100,000 500 99.5%↓
接口RT 2000ms 20ms 99%↓
内存占用 6MB 极小
拦截率 0% 99.9% 大幅提升

八、参考与拓展

  • RedisBloom官方文档
  • Redisson布隆过滤器文档
  • 布谷鸟过滤器论文

互动讨论:你在项目中遇到过缓存穿透问题吗?除了布隆过滤器还有什么解决方案?欢迎在评论区分享!

如果本文对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,持续获取更多Java后端技术干货!

赞(0)
未经允许不得转载:171主机测评 » 【Java后端技术亮点】布隆过滤器防缓存穿透:千万级数据下几MB内存的极致防护
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址