Redis 布隆过滤器使用说明
📖 概述
本项目实现了一个基于Redis的布隆过滤器,用于高效处理大规模S3 URL缓存和查询。布隆过滤器是一种概率性数据结构,能够以极小的内存占用快速判断元素是否存在于集合中。
🔧 实现原理
布隆过滤器核心概念
布隆过滤器由以下组件组成:
- 位数组(Bit Array): 一个长度为m的二进制数组,初始值全为0
- 哈希函数(Hash Functions): k个不同的哈希函数,将输入元素映射到位数组的不同位置
- 误判率(False Positive Rate): 布隆过滤器可能误报元素存在,但绝不会漏报
算法流程
1. 添加元素
def add_url(self, url: str) -> bool:
# 1. 使用k个哈希函数计算url的k个位置
positions = self._get_hash_positions(url)
# 2. 将位数组中这k个位置设置为1
for pos in positions:
self.redis_client.setbit(self.bloom_key, pos, 1)
# 3. 同时将URL添加到精确集合中(用于消除误判)
self.redis_client.sadd(self.exact_key, url)
2. 查询元素
def might_contain(self, url: str) -> bool:
# 1. 使用相同的k个哈希函数计算位置
positions = self._get_hash_positions(url)
# 2. 检查位数组中这k个位置是否都为1
for pos in positions:
if not self.redis_client.getbit(self.bloom_key, pos):
return False
return True
性能优化策略
1. 内存优化
- 位数组大小: 根据预期元素数量和误判率计算最优大小
- 哈希函数数量: 平衡内存使用和查询性能
- Redis压缩: 利用Redis的字符串压缩功能
2. 查询优化
- 批量操作: 使用Redis pipeline减少网络往返
- 缓存策略: 结合布隆过滤器和精确集合
- 异步处理: 支持异步操作提高并发性能
🚀 使用方法
1. 基本使用
from celery_task.s3.redis_bloom_filter import S3UrlCacheManager
# 创建缓存管理器
cache_manager = S3UrlCacheManager()
# 添加单个URL
await cache_manager.add_url("s3://bucket/file1.pdf")
# 批量添加URL
urls = ["s3://bucket/file1.pdf", "s3://bucket/file2.txt"]
await cache_manager.add_urls_batch(urls)
# 检查URL是否存在
exists = await cache_manager.might_contain("s3://bucket/file1.pdf")
exact_exists = await cache_manager.contains_exact("s3://bucket/file1.pdf")
2. 从S3迭代器缓存URL
# 获取S3文件迭代器
s3_iterator = s3_service.get_files_async_iterator(
batch_size=1000,
cache_urls_for_cleanup=True
)
# 定义进度回调函数
def progress_callback(cached_count):
if cached_count % 50000 == 0:
print(f"已缓存 {cached_count} 个URL")
# 缓存所有S3 URL
total_cached = await cache_manager.cache_s3_urls_from_iterator(
s3_iterator,
progress_callback
)
3. 查找孤立URL
# 获取Weaviate中的所有URL
weaviate_urls = ["s3://bucket/file1.pdf", "s3://bucket/file2.txt"]
# 使用布隆过滤器快速识别孤立URL
orphaned_urls = await cache_manager.find_orphaned_urls(weaviate_urls)
4. 缓存管理
# 获取缓存统计信息
stats = await cache_manager.get_stats()
print(f"布隆过滤器存在: {stats.get('bloom_exists', False)}")
print(f"精确URL数量: {stats.get('exact_urls_count', 0)}")
print(f"位数组大小: {stats.get('bit_array_size', 0)}")
# 清空缓存
await cache_manager.clear_cache()
📊 性能特点
内存使用
- 布隆过滤器: 每个URL约占用0.1-0.5字节(取决于误判率)
- 精确集合: 每个URL约占用50-100字节
- 总内存: 100万个URL约占用50-100MB
查询性能
- 布隆过滤器查询: O(k) 时间复杂度,k为哈希函数数量
- 精确查询: O(1) 平均时间复杂度
- 批量操作: 支持pipeline,大幅提升性能
误判率控制
- 默认误判率: 0.01 (1%)
- 可调整参数: 通过修改位数组大小和哈希函数数量
- 消除误判: 结合精确集合进行二次验证
⚙️ 配置参数
Redis配置
# Redis连接配置
REDIS_HOST = "localhost"
REDIS_PORT = 6379
REDIS_DB = 0
REDIS_PASSWORD = None
# 布隆过滤器配置
BLOOM_FILTER_KEY = "s3_urls_bloom"
EXACT_URLS_KEY = "s3_urls_exact"
布隆过滤器参数
# 预期元素数量(影响内存使用)
EXPECTED_ELEMENTS = 1000000
# 误判率(影响内存使用和查询性能)
FALSE_POSITIVE_RATE = 0.01
# 哈希函数数量(影响查询性能)
NUM_HASH_FUNCTIONS = 7
🔍 监控和调试
缓存统计
# 获取详细统计信息
stats = await cache_manager.get_stats()
print(f"""
缓存统计:
- 布隆过滤器存在: {stats.get('bloom_exists', False)}
- 精确URL数量: {stats.get('exact_urls_count', 0)}
- 位数组大小: {stats.get('bit_array_size', 0)}
- 哈希函数数量: {stats.get('num_hash_functions', 0)}
- 预期误判率: {stats.get('false_positive_rate', 0)}
""")
性能测试
import time
# 测试添加性能
start_time = time.time()
for i in range(10000):
await cache_manager.add_url(f"s3://bucket/file{i}.pdf")
add_time = time.time() - start_time
print(f"添加10000个URL耗时: {add_time:.2f}秒")
# 测试查询性能
start_time = time.time()
for i in range(10000):
await cache_manager.might_contain(f"s3://bucket/file{i}.pdf")
query_time = time.time() - start_time
print(f"查询10000个URL耗时: {query_time:.2f}秒")
🛠️ 故障排除
常见问题
1. Redis连接失败
# 检查Redis连接
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
try:
r.ping()
print("Redis连接正常")
except:
print("Redis连接失败")
2. 内存不足
# 检查Redis内存使用
info = r.info('memory')
print(f"Redis内存使用: {info['used_memory_human']}")
print(f"Redis内存峰值: {info['used_memory_peak_human']}")
3. 误判率过高
# 调整布隆过滤器参数
# 增加位数组大小或减少预期元素数量
EXPECTED_ELEMENTS = 500000 # 减少预期元素数量
FALSE_POSITIVE_RATE = 0.005 # 降低误判率
调试技巧
1. 启用详细日志
import logging
logging.basicConfig(level=logging.DEBUG)
2. 监控Redis操作
# 使用Redis MONITOR命令监控操作
# redis-cli monitor
3. 性能分析
import cProfile
import pstats
# 性能分析
profiler = cProfile.Profile()
profiler.enable()
# 执行操作
profiler.disable()
stats = pstats.Stats(profiler)
stats.sort_stats('cumulative')
stats.print_stats(10)
📚 最佳实践
1. 内存管理
- 根据实际数据量调整预期元素数量
- 定期清理过期缓存
- 监控Redis内存使用情况
2. 性能优化
- 使用批量操作减少网络往返
- 合理设置批处理大小
- 利用异步操作提高并发性能
3. 可靠性保证
- 定期备份缓存数据
- 实现缓存重建机制
- 添加错误处理和重试逻辑
4. 监控告警
- 监控缓存命中率
- 监控误判率变化
- 监控内存使用趋势
🔗 相关链接
本文档提供了布隆过滤器的完整使用指南,包括实现原理、使用方法、性能特点和故障排除。如有问题,请参考相关文档或联系开发团队。
- redis_bloom_filter.py
"""
Redis-based Bloom Filter for S3 URL caching and efficient orphaned document detection
This module implements a scalable solution for handling millions of S3 URLs using:
1. Redis-based Bloom Filter for fast membership testing
2. Redis Sets for exact URL storage
3. Optimized batch processing for large-scale data
"""
import hashlib
import math
from typing import Iterator, List, Optional, Set
import redis
from app.config.config import settings
from app.utils.logger import celery_logger as logger
class RedisBloomFilter:
"""
Redis-based Bloom Filter implementation for S3 URL caching
Features:
- False positive rate configurable
- Memory efficient for millions of URLs
- Automatic bit array sizing
- Batch operations for better performance
"""
def __init__(self, redis_client: redis.Redis, key_prefix: str = "s3_bloom",
expected_items: int = 1000000, false_positive_rate: float = 0.01):
"""
Initialize Redis Bloom Filter
Args:
redis_client: Redis client instance
key_prefix: Redis key prefix for bloom filter
expected_items: Expected number of items (for optimal sizing)
false_positive_rate: Desired false positive rate (0.01 = 1%)
"""
self.redis = redis_client
self.key_prefix = key_prefix
self.expected_items = expected_items
self.false_positive_rate = false_positive_rate
# Calculate optimal parameters
self.bit_size = self._calculate_bit_size()
self.hash_count = self._calculate_hash_count()
# Redis keys
self.bloom_key = f"{key_prefix}:bloom"
self.urls_set_key = f"{key_prefix}:urls"
self.metadata_key = f"{key_prefix}:meta"
logger.info(f"🎯 Initialized RedisBloomFilter: "
f"bit_size={self.bit_size}, hash_count={self.hash_count}, "
f"expected_items={expected_items}, fpr={false_positive_rate}")
def _calculate_bit_size(self) -> int:
"""Calculate optimal bit array size"""
# m = -n * ln(p) / (ln(2)^2)
n = self.expected_items
p = self.false_positive_rate
m = int(-n * math.log(p) / (math.log(2) ** 2))
return m
def _calculate_hash_count(self) -> int:
"""Calculate optimal number of hash functions"""
# k = (m/n) * ln(2)
k = int((self.bit_size / self.expected_items) * math.log(2))
return max(1, k)
def _hash_functions(self, item: str) -> List[int]:
"""Generate multiple hash values for an item"""
hashes = []
# Use different hash algorithms for diversity
hash1 = int(hashlib.md5(item.encode()).hexdigest(), 16)
hash2 = int(hashlib.sha1(item.encode()).hexdigest(), 16)
# Generate k hash values using double hashing
for i in range(self.hash_count):
hash_val = (hash1 + i * hash2) % self.bit_size
hashes.append(hash_val)
return hashes
async def add_url(self, url: str) -> bool:
"""
Add a URL to both bloom filter and exact set
Args:
url: S3 URL to add
Returns:
True if added successfully
"""
try:
# Add to bloom filter
hash_positions = self._hash_functions(url)
# Use pipeline for atomic operations
pipe = self.redis.pipeline()
# Set bits in bloom filter
for pos in hash_positions:
pipe.setbit(self.bloom_key, pos, 1)
# Add to exact URLs set
pipe.sadd(self.urls_set_key, url)
# Execute pipeline
pipe.execute()
return True
except Exception as e:
logger.error(f"❌ Failed to add URL to bloom filter: {e}")
return False
async def add_urls_batch(self, urls: List[str], batch_size: int = 1000) -> int:
"""
Add multiple URLs in batches for better performance
Args:
urls: List of S3 URLs to add
batch_size: Number of URLs to process in each batch
Returns:
Number of URLs successfully added
"""
added_count = 0
for i in range(0, len(urls), batch_size):
batch = urls[i:i + batch_size]
try:
# Use pipeline for batch operations
pipe = self.redis.pipeline()
for url in batch:
# Add to bloom filter
hash_positions = self._hash_functions(url)
for pos in hash_positions:
pipe.setbit(self.bloom_key, pos, 1)
# Add to exact URLs set
pipe.sadd(self.urls_set_key, url)
# Execute batch
pipe.execute()
added_count += len(batch)
logger.debug(f"📦 Added batch of {len(batch)} URLs to bloom filter")
except Exception as e:
logger.error(f"❌ Failed to add URL batch: {e}")
continue
logger.info(f"✅ Added {added_count} URLs to bloom filter")
return added_count
async def might_contain(self, url: str) -> bool:
"""
Check if URL might be in the set (bloom filter check)
Args:
url: URL to check
Returns:
True if URL might be in set (could be false positive)
False if URL is definitely not in set
"""
try:
hash_positions = self._hash_functions(url)
# Check all bit positions
pipe = self.redis.pipeline()
for pos in hash_positions:
pipe.getbit(self.bloom_key, pos)
results = pipe.execute()
# URL might be in set only if all bits are set
return all(bit == 1 for bit in results)
except Exception as e:
logger.error(f"❌ Bloom filter check failed: {e}")
return True # Conservative approach
async def contains_exact(self, url: str) -> bool:
"""
Check if URL exists in exact set (no false positives)
Args:
url: URL to check
Returns:
True if URL exists in exact set
"""
try:
return bool(self.redis.sismember(self.urls_set_key, url))
except Exception as e:
logger.error(f"❌ Exact set check failed: {e}")
return False
async def get_missing_urls(self, urls_to_check: List[str],
batch_size: int = 10000) -> Set[str]:
"""
Efficiently find URLs that are NOT in S3 (missing URLs)
This is optimized for the orphaned document cleanup use case:
1. First use bloom filter to quickly eliminate definitely present URLs
2. Then check exact set for potential matches
Args:
urls_to_check: List of URLs to check (from Weaviate)
batch_size: Batch size for processing
Returns:
Set of URLs that are missing from S3
"""
missing_urls = set()
total_checked = 0
bloom_filtered = 0
logger.info(f"🔍 Checking {len(urls_to_check)} URLs for missing entries")
for i in range(0, len(urls_to_check), batch_size):
batch = urls_to_check[i:i + batch_size]
for url in batch:
total_checked += 1
# Step 1: Bloom filter check (fast elimination)
if not await self.might_contain(url):
# Definitely not in S3
missing_urls.add(url)
bloom_filtered += 1
else:
# Might be in S3, need exact check
if not await self.contains_exact(url):
missing_urls.add(url)
if i % (batch_size * 10) == 0: # Log every 10 batches
logger.info(f"📊 Processed {total_checked}/{len(urls_to_check)} URLs, "
f"bloom filtered: {bloom_filtered}, missing: {len(missing_urls)}")
logger.info(f"✅ Found {len(missing_urls)} missing URLs out of {total_checked} checked "
f"(bloom filter eliminated {bloom_filtered} checks)")
return missing_urls
async def clear_cache(self) -> bool:
"""Clear all cached data"""
try:
pipe = self.redis.pipeline()
pipe.delete(self.bloom_key)
pipe.delete(self.urls_set_key)
pipe.delete(self.metadata_key)
pipe.execute()
logger.info("🧹 Cleared bloom filter cache")
return True
except Exception as e:
logger.error(f"❌ Failed to clear cache: {e}")
return False
async def get_cache_stats(self) -> dict:
"""Get cache statistics"""
try:
pipe = self.redis.pipeline()
pipe.memory_usage(self.bloom_key)
pipe.scard(self.urls_set_key)
pipe.exists(self.bloom_key)
results = pipe.execute()
return {
"bloom_memory_bytes": results[0] or 0,
"exact_urls_count": results[1] or 0,
"bloom_exists": bool(results[2]),
"estimated_bit_size": self.bit_size,
"hash_count": self.hash_count,
"expected_false_positive_rate": self.false_positive_rate
}
except Exception as e:
logger.error(f"❌ Failed to get cache stats: {e}")
return {}
class S3UrlCacheManager:
"""
High-level manager for S3 URL caching with bloom filter optimization
"""
def __init__(self, redis_client: Optional[redis.Redis] = None):
"""Initialize cache manager"""
if redis_client is None:
# Use default Redis connection
redis_client = redis.Redis(
host=getattr(settings, 'REDIS_HOST', 'localhost'),
port=getattr(settings, 'REDIS_PORT', 6379),
db=getattr(settings, 'REDIS_DB', 7),
decode_responses=True
)
self.bloom_filter = RedisBloomFilter(
redis_client=redis_client,
key_prefix="s3_orphan_cleanup",
expected_items=2000000, # 2M URLs
false_positive_rate=0.001 # 0.1% false positive rate
)
async def cache_s3_urls_from_iterator(self, s3_iterator,
progress_callback=None) -> int:
"""
Cache S3 URLs from async iterator
Args:
s3_iterator: Async iterator yielding batches of file info
progress_callback: Optional callback for progress updates
Returns:
Total number of URLs cached
"""
total_cached = 0
batch_urls = []
logger.info("🔄 Starting S3 URL caching process")
async for file_batch in s3_iterator:
# Extract URLs from file batch
urls = [file_info['url'] for file_info in file_batch]
batch_urls.extend(urls)
# Process in larger batches for efficiency
if len(batch_urls) >= 5000:
cached_count = await self.bloom_filter.add_urls_batch(batch_urls)
total_cached += cached_count
batch_urls = []
if progress_callback:
progress_callback(total_cached)
# Process remaining URLs
if batch_urls:
cached_count = await self.bloom_filter.add_urls_batch(batch_urls)
total_cached += cached_count
logger.info(f"✅ Cached {total_cached} S3 URLs in bloom filter")
return total_cached
async def find_orphaned_urls(self, weaviate_urls: List[str]) -> Set[str]:
"""
Find URLs that exist in Weaviate but not in S3 (orphaned)
Args:
weaviate_urls: List of URLs from Weaviate
Returns:
Set of orphaned URLs
"""
return await self.bloom_filter.get_missing_urls(weaviate_urls)
async def get_stats(self) -> dict:
"""Get caching statistics"""
return await self.bloom_filter.get_cache_stats()
async def clear_cache(self) -> bool:
"""Clear all cached data"""
return await self.bloom_filter.clear_cache()