Redisson布隆过滤器原理以及解决Redis缓存穿透方案
目录
1. 什么是缓存穿透?
2. 布隆过滤器简介
3. Redisson布隆过滤器实现原理
3.1 核心数据结构
3.2 哈希函数实现
3.3 添加元素过程
3.4 查询元素过程
4. 实际应用场景实例
4.1 创建布隆过滤器
4.2 商品查询防穿透示例
4.3 用户ID防穿透示例
5. 布隆过滤器的优缺点
5.1 优点
5.2 缺点
6. 其他缓存穿透解决方案对比
6.1 缓存空值
6.2 互斥锁
7. 实践建议
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. 设置所有哈希位置为1for (long position : positions) {bits.set(position);}return true;
}
3.4 查询元素过程
/*** 检查元素是否可能存在*/
public boolean contains(T element) {// 1. 计算多个哈希位置long[] positions = getHashPositions(element.toString(), hashIterations, size);// 2. 检查所有位置是否都为1for (long position : positions) {if (!bits.get(position)) {// 如果任何一个位置为0,则元素一定不存在return false;}}// 所有位置都为1,元素可能存在(存在误判)return true;
}
4. 实际应用场景实例
4.1 创建布隆过滤器
@Component
public class BloomFilterService {@Autowiredprivate 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 {@Autowiredprivate BloomFilterService bloomFilterService;@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowiredprivate ProductMapper productMapper;private RBloomFilter<String> productBloomFilter;@PostConstructpublic void init() {// 创建布隆过滤器,预期100万商品,误判率0.01productBloomFilter = 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 {@Autowiredprivate RedissonClient redissonClient;private RBloomFilter<String> userBloomFilter;@PostConstructpublic 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;
}