# -*- coding: utf-8 -*- """ Created on 2018/12/13 4:11 PM --------- @summary: --------- @author: Boris @email: boris_liu@foxmail.com """ import hashlib import math import threading import time from struct import unpack, pack from feapder.utils.redis_lock import RedisLock from . import bitarray def make_hashfuncs(num_slices, num_bits): if num_bits >= (1 << 31): fmt_code, chunk_size = "Q", 8 elif num_bits >= (1 << 15): fmt_code, chunk_size = "I", 4 else: fmt_code, chunk_size = "H", 2 total_hash_bits = 8 * num_slices * chunk_size if total_hash_bits > 384: hashfn = hashlib.sha512 elif total_hash_bits > 256: hashfn = hashlib.sha384 elif total_hash_bits > 160: hashfn = hashlib.sha256 elif total_hash_bits > 128: hashfn = hashlib.sha1 else: hashfn = hashlib.md5 fmt = fmt_code * (hashfn().digest_size // chunk_size) num_salts, extra = divmod(num_slices, len(fmt)) if extra: num_salts += 1 salts = tuple(hashfn(hashfn(pack("I", i)).digest()) for i in range(num_salts)) def _make_hashfuncs(key): if isinstance(key, str): key = key.encode("utf-8") else: key = str(key).encode("utf-8") i = 0 for salt in salts: h = salt.copy() h.update(key) for uint in unpack(fmt, h.digest()): yield uint % num_bits i += 1 if i >= num_slices: return return _make_hashfuncs class BloomFilter(object): BASE_MEMORY = 1 BASE_REDIS = 2 def __init__( self, capacity: int, error_rate: float = 0.00001, bitarray_type=BASE_REDIS, name=None, redis_url=None, ): if not (0 < error_rate < 1): raise ValueError("Error_Rate must be between 0 and 1.") if not capacity > 0: raise ValueError("Capacity must be > 0") # given M = num_bits, k = num_slices, P = error_rate, n = capacity # k = log2(1/P) # solving for m = bits_per_slice # n ~= M * ((ln(2) ** 2) / abs(ln(P))) # n ~= (k * m) * ((ln(2) ** 2) / abs(ln(P))) # m ~= n * abs(ln(P)) / (k * (ln(2) ** 2)) num_slices = int(math.ceil(math.log(1.0 / error_rate, 2))) bits_per_slice = int( math.ceil( (capacity * abs(math.log(error_rate))) / (num_slices * (math.log(2) ** 2)) ) ) self._setup(error_rate, num_slices, bits_per_slice, capacity) if bitarray_type == BloomFilter.BASE_MEMORY: self.bitarray = bitarray.MemoryBitArray(self.num_bits) self.bitarray.setall(False) elif bitarray_type == BloomFilter.BASE_REDIS: assert name, "name can't be None " self.bitarray = bitarray.RedisBitArray(name, redis_url) else: raise ValueError("not support this bitarray type") def _setup(self, error_rate, num_slices, bits_per_slice, capacity): self.error_rate = error_rate self.num_slices = num_slices self.bits_per_slice = bits_per_slice self.capacity = capacity self.num_bits = num_slices * bits_per_slice self.make_hashes = make_hashfuncs(self.num_slices, self.bits_per_slice) self._is_at_capacity = False self._check_capacity_time = 0 def __repr__(self): return "".format(self.bitarray) def get(self, keys, to_list=False): is_list = isinstance(keys, list) keys = keys if is_list else [keys] is_exists = [] offsets = [] for key in keys: hashes = self.make_hashes(key) offset = 0 for k in hashes: offsets.append(offset + k) offset += self.bits_per_slice old_values = self.bitarray.get(offsets) for i in range(0, len(old_values), self.num_slices): is_exists.append(int(all(old_values[i : i + self.num_slices]))) if to_list: return is_exists else: return is_exists if is_list else is_exists[0] @property def is_at_capacity(self): """ 是否容量已满, 1的个数满位数组的一半的时,则看做已满 比较耗时 半小时检查一次 @return: """ # if self._is_at_capacity: # return self._is_at_capacity # # if not self._check_capacity_time or time.time() - self._check_capacity_time > 1800: # bit_count = self.bitarray.count() # if bit_count and bit_count / self.num_bits > 0.5: # self._is_at_capacity = True # # self._check_capacity_time = time.time() # # return self._is_at_capacity if self._is_at_capacity: return self._is_at_capacity bit_count = self.bitarray.count() if bit_count and bit_count / self.num_bits > 0.5: self._is_at_capacity = True return self._is_at_capacity def add(self, keys): """ Adds a key to this bloom filter. If the key already exists in this filter it will return False. Otherwise True. keys support list @param keys: list or one key @return: """ if self.is_at_capacity: raise IndexError("BloomFilter is at capacity") is_list = isinstance(keys, list) keys = keys if is_list else [keys] is_added = [] offsets = [] for key in keys: hashes = self.make_hashes(key) offset = 0 for k in hashes: offsets.append(offset + k) offset += self.bits_per_slice old_values = self.bitarray.set(offsets, 1) for i in range(0, len(old_values), self.num_slices): is_added.append(1 ^ int(all(old_values[i : i + self.num_slices]))) return is_added if is_list else is_added[0] class ScalableBloomFilter(object): """ 自动扩展空间的bloomfilter, 当一个filter满一半的时候,创建下一个 """ BASE_MEMORY = BloomFilter.BASE_MEMORY BASE_REDIS = BloomFilter.BASE_REDIS def __init__( self, initial_capacity: int = 100000000, error_rate: float = 0.00001, bitarray_type=BASE_REDIS, name=None, redis_url=None, ): if not error_rate or error_rate < 0: raise ValueError("Error_Rate must be a decimal less than 0.") self._setup( initial_capacity, error_rate, name, bitarray_type, redis_url=redis_url ) def _setup(self, initial_capacity, error_rate, name, bitarray_type, redis_url): self.initial_capacity = initial_capacity self.error_rate = error_rate self.name = name self.bitarray_type = bitarray_type self.redis_url = redis_url self.filters = [] self.filters.append(self.create_filter()) self._thread_lock = threading.RLock() self._check_capacity_time = 0 def __repr__(self): return "".format(self.filters[-1].bitarray) def create_filter(self): filter = BloomFilter( capacity=self.initial_capacity, error_rate=self.error_rate, bitarray_type=self.bitarray_type, name=self.name + str(len(self.filters)) if self.name else self.name, redis_url=self.redis_url, ) return filter def check_filter_capacity(self): """ 检测filter状态,如果已满,加载新的filter @return: """ if ( not self._check_capacity_time or time.time() - self._check_capacity_time > 1800 ): if self.bitarray_type == ScalableBloomFilter.BASE_MEMORY: with self._thread_lock: while True: if self.filters[-1].is_at_capacity: self.filters.append(self.create_filter()) else: break self._check_capacity_time = time.time() else: # 全局锁 同一时间只有一个进程在真正的创建新的filter,等这个进程创建完,其他进程只是把刚创建的filter append进来 key = ( f"ScalableBloomFilter:{self.name}" if self.name else "ScalableBloomFilter" ) with RedisLock(key=key, redis_url=self.redis_url) as lock: if lock.locked: while True: if self.filters[-1].is_at_capacity: self.filters.append(self.create_filter()) else: break self._check_capacity_time = time.time() def add(self, keys, skip_check=False): """ Adds a key to this bloom filter. If the key already exists in this filter it will return False. Otherwise True. keys support list @param keys: list or one key @param skip_check: add directly,not check if is exist in bloomfilters @return: """ self.check_filter_capacity() current_filter = self.filters[-1] if skip_check: return current_filter.add(keys) else: is_list = isinstance(keys, list) keys = keys if is_list else [keys] not_exist_keys = list(set(keys)) # 检查之前的bloomfilter是否存在 # 记录下每级filter存在的key,不存在的key继续向下检查 for filter in reversed(self.filters): current_filter_is_exists = filter.get( not_exist_keys, to_list=True ) # 当前的filter是否存在 not_exist_keys_temp = [] for key, is_exist in zip(not_exist_keys, current_filter_is_exists): if not is_exist: # 当前filter不存在的key 需要继续向下检查 not_exist_keys_temp.append(key) not_exist_keys = not_exist_keys_temp if not not_exist_keys: break # 仍有不存在的关键词,记录该关键词 if not_exist_keys: current_filter.add(not_exist_keys) # 比较key是否已存在, 内部重复的key 若不存在啊则只留其一算为不存在,其他看作已存在 for i, key in enumerate(keys): for j, not_exist_key in enumerate(not_exist_keys): if key == not_exist_key: keys[i] = 1 not_exist_keys.pop(j) break else: keys[i] = 0 is_added = keys return is_added if is_list else is_added[0] def get(self, keys): self.check_filter_capacity() is_list = isinstance(keys, list) keys = keys if is_list else [keys] # 最终会修改为 [0, 1, ...] 0表示不存在 1 已存在 not_exist_keys = list(set(keys)) # 检查之前的bloomfilter是否存在 # 记录下每级filter存在的key,不存在的key继续向下检查 for filter in reversed(self.filters): current_filter_is_exists = filter.get( not_exist_keys, to_list=True ) # 当前的filter是否存在 not_exist_keys_temp = [] for checked_key, is_exist in zip(not_exist_keys, current_filter_is_exists): if not is_exist: # 当前filter不存在的key 需要继续向下检查 not_exist_keys_temp.append(checked_key) not_exist_keys = not_exist_keys_temp if not not_exist_keys: break # 比较key是否已存在, 内部重复的key 若不存在啊则只留其一算为不存在,其他看作已存在 for i, key in enumerate(keys): for j, not_exist_key in enumerate(not_exist_keys): if key == not_exist_key: keys[i] = 0 not_exist_keys.pop(j) break else: keys[i] = 1 is_exists = keys return is_exists if is_list else is_exists[0] @property def capacity(self): """Returns the total capacity for all filters in this SBF""" return sum(f.capacity for f in self.filters)