目录
1. 什么是缓存穿透?
缓存穿透是指查询一个根本不存在的数据,由于缓存中没有,所以每次请求都会打到数据库上,导致数据库压力过大。常见场景包括:
- 恶意攻击:攻击者故意查询不存在的key
- 业务逻辑:查询条件为空或无效参数
- 数据过期:热点数据突然过期,大量请求同时访问
2. 布隆过滤器简介
布隆过滤器(Bloom Filter)是一个很长的二进制向量和一系列随机映射函数,用于判断一个元素是否在集合中。它的特点是:
- 空间效率高:使用很少的内存就能表示大量数据
- 查询速度快:O(k)时间复杂度,k为哈希函数个数
- 存在误判:可能误判不存在的元素为存在,但不会误判存在的元素为不存在
3. Redisson布隆过滤器实现原理
3.1 核心数据结构
// Redisson布隆过滤器的核心数据结构
public class RBloomFilter<T> {
// 底层使用Redis的BitMap实现
private final RBitSet bits;
// 哈希函数数量
private final int hashIterations;
// 预期元素数量
private final long expectedInsertions;
// 误判率
private final double falseProbability;
}
3.2 哈希函数实现
Redisson使用多个哈希函数来减少冲突:
// 简化的哈希函数实现示例
public class BloomFilterHash {
/**
* 计算多个哈希值
* @param key 要哈希的key
* @param hashIterations 哈希函数数量
* @param size 位图大小
* @return 哈希值数组
*/
public static long[] getHashPositions(String key, int hashIterations, long size) {
long[] positions = new long[hashIterations];
// 使用双重哈希技术
long hash1 = hash(key);
long hash2 = hash(key + "salt");
for (int i = 0; i < hashIterations; i++) {
// 计算第i个哈希位置
positions[i] = Math.abs((hash1 + i * hash2) % size);
}
return positions;
}
private static long hash(String key) {
// 使用MurmurHash算法
return MurmurHash3.hash32(key);
}
}
3.3 添加元素过程
/**
* 向布隆过滤器添加元素
*/
public boolean add(T element) {
// 1. 计算多个哈希位置
long[] positions = getHashPositions(element.toString(), hashIterations, size);
// 2. 检查是否所有位置都已设置
boolean allSet = true;
for (long position : positions) {
if (!bits.get(position)) {
allSet = false;
break;
}
}
// 3. 如果所有位置都已设置,说明元素可能已存在
if (allSet) {
return false;
}
// 4. 设置所有哈希位置为1
for (long position : positions) {
bits.set(position);
}
return true;
}
3.4 查询元素过程
/**
* 检查元素是否可能存在
*/
public boolean contains(T element) {
// 1. 计算多个哈希位置
long[] positions = getHashPositions(element.toString(), hashIterations, size);
// 2. 检查所有位置是否都为1
for (long position : positions) {
if (!bits.get(position)) {
// 如果任何一个位置为0,则元素一定不存在
return false;
}
}
// 所有位置都为1,元素可能存在(存在误判)
return true;
}
4. 实际应用场景实例
4.1 创建布隆过滤器
@Component
public class BloomFilterService {
@Autowired
private RedissonClient redissonClient;
/**
* 创建布隆过滤器
*/
public <T> RBloomFilter<T> createBloomFilter(String name, long expectedInsertions, double falseProbability) {
RBloomFilter<T> bloomFilter = redissonClient.getBloomFilter(name);
// 初始化布隆过滤器
bloomFilter.tryInit(expectedInsertions, falseProbability);
return bloomFilter;
}
}
4.2 商品查询防穿透示例
@Service
public class ProductService {
@Autowired
private BloomFilterService bloomFilterService;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private ProductMapper productMapper;
private RBloomFilter<String> productBloomFilter;
@PostConstruct
public void init() {
// 创建布隆过滤器,预期100万商品,误判率0.01
productBloomFilter = bloomFilterService.createBloomFilter("product_bloom", 1000000, 0.01);
// 初始化时加载所有商品ID到布隆过滤器
loadAllProductIds();
}
/**
* 查询商品信息(防穿透)
*/
public Product getProductById(String productId) {
// 1. 先检查布隆过滤器
if (!productBloomFilter.contains(productId)) {
log.info("商品ID {} 在布隆过滤器中不存在", productId);
return null;
}
// 2. 查询Redis缓存
String cacheKey = "product:" + productId;
Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
log.info("从Redis缓存获取商品信息: {}", productId);
return product;
}
// 3. 查询数据库
product = productMapper.selectById(productId);
if (product != null) {
// 4. 更新缓存
redisTemplate.opsForValue().set(cacheKey, product, Duration.ofMinutes(30));
log.info("从数据库获取商品信息并更新缓存: {}", productId);
} else {
// 5. 缓存空值,防止缓存穿透
redisTemplate.opsForValue().set(cacheKey, null, Duration.ofMinutes(5));
log.warn("商品ID {} 在数据库中不存在,缓存空值", productId);
}
return product;
}
/**
* 加载所有商品ID到布隆过滤器
*/
private void loadAllProductIds() {
List<String> allProductIds = productMapper.selectAllProductIds();
for (String productId : allProductIds) {
productBloomFilter.add(productId);
}
log.info("成功加载 {} 个商品ID到布隆过滤器", allProductIds.size());
}
/**
* 新增商品时同步更新布隆过滤器
*/
public void addProduct(Product product) {
// 保存到数据库
productMapper.insert(product);
// 添加到布隆过滤器
productBloomFilter.add(product.getId());
// 更新缓存
String cacheKey = "product:" + product.getId();
redisTemplate.opsForValue().set(cacheKey, product, Duration.ofMinutes(30));
log.info("新增商品并更新布隆过滤器: {}", product.getId());
}
}
4.3 用户ID防穿透示例
@Service
public class UserService {
@Autowired
private RedissonClient redissonClient;
private RBloomFilter<String> userBloomFilter;
@PostConstruct
public void init() {
// 创建用户布隆过滤器
userBloomFilter = redissonClient.getBloomFilter("user_bloom");
userBloomFilter.tryInit(10000000, 0.001); // 1000万用户,误判率0.001
}
/**
* 检查用户是否存在
*/
public boolean isUserExists(String userId) {
return userBloomFilter.contains(userId);
}
/**
* 批量检查用户是否存在
*/
public Map<String, Boolean> batchCheckUsers(List<String> userIds) {
Map<String, Boolean> result = new HashMap<>();
for (String userId : userIds) {
result.put(userId, userBloomFilter.contains(userId));
}
return result;
}
}
5. 布隆过滤器的优缺点
5.1 优点
- 空间效率极高:1亿数据只需要约12MB内存
- 查询效率高:O(k)时间复杂度
- 误判可控:通过参数调整误判率
5.2 缺点
- 存在误判:可能将不存在的元素误判为存在
- 不支持删除:删除元素会影响其他元素
- 不支持计数:只能判断存在性,不能统计数量
6. 其他缓存穿透解决方案对比
6.1 缓存空值
// 缓存空值方案
public Product getProductWithNullCache(String productId) {
String cacheKey = "product:" + productId;
Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 查询数据库
product = productMapper.selectById(productId);
if (product != null) {
redisTemplate.opsForValue().set(cacheKey, product, Duration.ofMinutes(30));
} else {
// 缓存空值,设置较短过期时间
redisTemplate.opsForValue().set(cacheKey, null, Duration.ofMinutes(5));
}
return product;
}
6.2 互斥锁
// 互斥锁方案
public Product getProductWithLock(String productId) {
String cacheKey = "product:" + productId;
String lockKey = "lock:product:" + productId;
Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 获取分布式锁
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
// 双重检查
product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 查询数据库
product = productMapper.selectById(productId);
if (product != null) {
redisTemplate.opsForValue().set(cacheKey, product, Duration.ofMinutes(30));
} else {
redisTemplate.opsForValue().set(cacheKey, null, Duration.ofMinutes(5));
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
return product;
}
988

被折叠的 条评论
为什么被折叠?



