Redis缓存穿透与布隆过滤器解决方案
什么是缓存穿透
缓存穿透是指查询一个不存在的数据,由于缓存中不存在,每次请求都会穿透到数据库,导致数据库压力过大。

布隆过滤器原理
布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否在集合中。
特点:
- 判断不存在:100%准确
- 判断存在:可能存在误判(有概率说存在实际不存在)
- 无法删除元素(除非使用增强版Counting Bloom Filter)
使用布隆过滤器解决缓存穿透的流程
请求 -> 布隆过滤器 -> 缓存 -> 数据库
- 请求先经过布隆过滤器
- 布隆过滤器判断不存在 → 直接返回(拦截)
- 布隆过滤器判断存在 → 查询缓存
- 缓存命中 → 返回数据
- 缓存未命中 → 查询数据库
- 数据存在 → 写入缓存并返回
- 数据不存在 → 不写入缓存(或写入空值)
Java实现示例
使用Google Guava的布隆过滤器(单机版)
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.nio.charset.Charset;
@Component
public class BloomFilterService {
private BloomFilter<String> bloomFilter;
// 预计数据量
private static final int EXPECTED_INSERTIONS = 1000000;
// 误判率
private static final double FPP = 0.001;
@PostConstruct
public void init() {
bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
EXPECTED_INSERTIONS,
FPP
);
// 初始化时加载所有用户ID到布隆过滤器
loadAllUserIds();
}
private void loadAllUserIds() {
// 从数据库加载所有用户ID
List<String> userIds = userMapper.getAllUserIds();
for (String userId : userIds) {
bloomFilter.put(userId);
}
}
public boolean mightContain(String userId) {
return bloomFilter.mightContain(userId);
}
// 新增用户时更新布隆过滤器
public void addUserId(String userId) {
bloomFilter.put(userId);
}
}
使用Redis BitMap实现布隆过滤器(分布式版)
import redis.clients.jedis.Jedis;
import org.springframework.stereotype.Component;
import java.util.BitSet;
@Component
public class RedisBloomFilter {
private static final String BLOOM_KEY = "user:bloom";
// 布隆过滤器参数
private static final int BIT_SIZE = 2 << 28; // 2^29 = 5.36亿
private static final int NUM_HASH_FUNCTIONS = 5;
private Jedis jedis;
/**
* 添加元素到布隆过滤器
*/
public void add(String value) {
int[] offsets = getHashOffsets(value);
for (int offset : offsets) {
jedis.setbit(BLOOM_KEY, offset, true);
}
}
/**
* 判断元素是否可能存在
*/
public boolean mightContain(String value) {
int[] offsets = getHashOffsets(value);
for (int offset : offsets) {
if (!jedis.getbit(BLOOM_KEY, offset)) {
return false; // 只要有一个位为0,肯定不存在
}
}
return true; // 所有位都为1,可能存在(有误判)
}
/**
* 计算多个哈希函数的偏移量
*/
private int[] getHashOffsets(String value) {
int[] offsets = new int[NUM_HASH_FUNCTIONS];
int h1 = Math.abs(value.hashCode());
int h2 = Math.abs(h1 >> 16);
for (int i = 1; i <= NUM_HASH_FUNCTIONS; i++) {
offsets[i-1] = Math.abs((h1 + i * h2) % BIT_SIZE);
}
return offsets;
}
}
使用Redisson(推荐方案)
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private UserRepository userRepository;
private RBloomFilter<String> bloomFilter;
@PostConstruct
public void init() {
// 创建布隆过滤器
bloomFilter = redissonClient.getBloomFilter("userBloomFilter");
// 初始化(预计数据量100万,误判率0.001)
bloomFilter.tryInit(1000000L, 0.001);
// 加载现有数据
loadExistingData();
}
private void loadExistingData() {
List<String> userIds = userRepository.getAllUserIds();
for (String userId : userIds) {
bloomFilter.add(userId);
}
}
public User getUserById(String userId) {
// 1. 布隆过滤器判断
if (!bloomFilter.contains(userId)) {
// 肯定不存在,直接返回
return null;
}
// 2. 查询缓存
String cacheKey = "user:" + userId;
User user = getFromCache(cacheKey);
if (user != null) {
return user;
}
// 3. 查询数据库
user = userRepository.findById(userId);
if (user != null) {
// 写入缓存
setToCache(cacheKey, user);
}
return user;
}
// 新增用户时更新布隆过滤器
public void addUser(User user) {
userRepository.save(user);
bloomFilter.add(user.getId());
}
}
项目中的完整缓存方案
@Service
public class OptimizedUserService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
private RBloomFilter<String> bloomFilter;
@PostConstruct
public void init() {
bloomFilter = redissonClient.getBloomFilter("userBloomFilter");
bloomFilter.tryInit(1000000L, 0.001);
}
public User getUserById(String userId) {
// 1. 布隆过滤器过滤
if (!bloomFilter.contains(userId)) {
log.warn("用户不存在,已被布隆过滤器拦截: {}", userId);
return null;
}
// 2. 查询缓存
String cacheKey = "user:" + userId;
String cacheValue = redisTemplate.opsForValue().get(cacheKey);
// 3. 缓存命中
if (cacheValue != null) {
// 判断是否是空值缓存
if ("NULL".equals(cacheValue)) {
return null;
}
return JSON.parseObject(cacheValue, User.class);
}
// 4. 缓存未命中,查询数据库
User user = userMapper.findById(userId);
// 5. 缓存处理
if (user != null) {
// 数据存在,写入缓存
redisTemplate.opsForValue().set(
cacheKey,
JSON.toJSONString(user),
30,
TimeUnit.MINUTES
);
} else {
// 数据不存在,缓存空值(防止缓存穿透)
redisTemplate.opsForValue().set(
cacheKey,
"NULL",
5,
TimeUnit.MINUTES
);
}
return user;
}
}
布隆过滤器参数配置建议
| 参数 | 说明 | 建议值 |
|---|---|---|
| expectedInsertions | 预计数据量 | 根据业务评估,预留20%余量 |
| fpp (false positive probability) | 误判率 | 001 (千分之一) |
| bitSize | 位图大小 | N * ln(FPP) / (ln2)^2 |
| numHashFunctions | 哈希函数数量 | (bits / N) * ln2 |
优缺点分析
优点:
- 节省内存(比HashSet节省90%以上空间)
- 查询速度快(O(k),k为哈希函数数量)
- 能有效防止缓存穿透
缺点:
- 存在误判率(不能100%确定存在)
- 无法删除元素
- 需要定期重建(数据量变化大时)
最佳实践建议
- 定期重建:定期从数据库全量加载数据重建布隆过滤器
- 配合空值缓存:布隆过滤 + 空值缓存双重保险
- 监控误判率:监控实际误判率,适时调整参数
- 冷热数据分离:热点数据使用布隆过滤器,冷数据使用其他方案
这种方案能有效解决缓存穿透问题,将无效请求拦截在Redis之前,保护数据库安全。