最近,有朋友去面试,被问到这么一个问题:
现在有一个非常庞大的数据,假设全是 int 类型。现在我给你一个数,你需要告诉我它是否存在其中(尽量高效)。
需求其实很清晰,只需要判断一个数是否存在即可。但是这里有一个比较重要的前提:非常庞大的数据。
常规实现:
先不考虑这个条件,我们脑海中出现的第一种方案是什么呢?
我想大多数想到的都是用 HashSet 来存放数据,因为它的写入查询的效率都比较高。写入和判断元素是否存在都有对应的 API,所以实现起来比较简单。
为此,我写了一个Demo,利用 HashSet 来存放数据,同时为了展示效果,我把堆最大内存设置为64M,并加入 GC 日志的打印。
-Xms64m -Xmx64m -XX:+PrintHeapAtGC -XX:+
HeapDumpOnOutOfMemoryError
HashSetHeap:
/**
* 往Set存放1000W个数据
* @author K. L. Mao
* @create 2018/11/30
*/
public class HashSetHeap {
public static void hashSetTest(){
long start = System.currentTimeMillis();
int capacity = 10000000;
Set<Integer> set = new HashSet<>(capacity);
for (int i = 0; i < capacity; i++) {
set.add(i);
}
System.out.println(set.contains(2));
long end = System.currentTimeMillis();
System.out.println("执行时间:" + (end - start));
}
}
测试类,HeapTest:
/**
* 测试类
* @author K. L. Mao
* @create 2018/11/30
*/
public class HeapTest {
public static void main(String[] args){
HashSetHeap.hashSetTest();
}
运行测试类,控制台输出如下:
内存溢出!!!
可见,在内存有限的情况下,我们不能使用这种方式。
Bloom Filter
基于上面分析的条件,要实现这个需求最需要解决的是如何将庞大的数据 load 到内存中。
而我们是否可以换种思路,因为只是需要判断数据是否存在,也不是需要把数据查询出来,所以完全没有必要将真正的数据存放进去。
伟大的的科学家们已经帮我们想到了这样的需求。
BurtonHowardBloom 在 1970 年提出了一个叫做 BloomFilter (中文翻译:布隆过滤)的算法。
它主要就是用于解决:判断一个元素是否在一个集合中,但它的优势是只需要占用很小的内存空间以及有着高效的查询效率。
Bloom Filter 原理
下面来分析下它的实现原理。
官方的说法是:它是一个保存了很长的二进制向量,同时结合 Hash 函数实现的。
听起来比较绕,但是通过一个图就比较容易理解了。
如图所示:
- 首先需要初始化一个二进制的数组,长度设为 L (图中为 8),同时初始值全为0。
- 当写入一个 A1=1000 的数据时,需要进行 H 次 hash 函数的运算(这里为2次);与 HashMap 有点类似,通过算出 hashcode 与 L 取模后定位到0、2处,将该处的值设为1。
- A2=2000 也是同理计算后将4、7位置设为1。
- 当有一个 B1=1000 需要判断是否存在时,也是做两次 hash 运算,定位到0、2处,此时他们的值都为1,所以认为 B1=1000 存在于集合中。
- 当有一个 B2=3000 时,也是同理。第一次 Hash 定位到 index=4 时,数组中的值为1,所以再进行第二次 hash 运算,结果定位到 index=5 的值为0,所以认为 B2=3000 不存在于集合中。
整个的写入、查询的流程就是这样,汇总起来就是:
对写入的数据做 H 次 hash 运算定位到数组中的位置,同时将数据改为 1。当有数据查询时也是同样的方式定位到数组中。
一旦其中的有一位为 0 则认为数据肯定不存在于集合,否则数据可能存在于集合中。
所以,布隆过滤器有以下几个特点:
1、只要返回数据不存在,则肯定不存在。
2、返回数据存在,只能是大概率存在。
3、不能清除其中的数据。
第一点应该都能理解,重点解释下2、3点。
为什么返回存在的数据却可能存在呢,这其实也和HashMap
类似。
在有限的数组长度中,存放大量的数据,即便是再完美的 hash 算法也会有冲突,所以有可能两个完全不同的 A、B 两个数据最后定位到的位置是一样的。
这时拿 B 进行查询时那自然就是误报了。
删除数据也是同理,当我把 B 的数据删除了,其实也相当于是把 A 的数据删掉了,这样也会造成后续的误报。
基于以上的 Hash 冲突的前提,所以BloomFilter
有一定的误报率,这个误报率和 hash 算法的次数 H,以及数组长度 L 都有关。
自己实现一个布隆过滤
/**
* 自定义布隆过滤器
* @author K. L. Mao
* @create 2018/11/30
*/
@Data
public class BloomFilters {
/**
* 数组长度
*/
private int arraySize;
/**
* 数组
*/
private int[] array;
public BloomFilters(int arraySize){
this.arraySize = arraySize;
array = new int[arraySize];
}
/**
* 写入数据
* @param key
*/
public void add(String key){
int first = hashcode_1(key);
int second = hashcode_2(key);
int third = hashcode_3(key);
array[first % arraySize] = 1;
array[second % arraySize] = 1;
array[third % arraySize] = 1;
}
/**
* 判断数据是否存在
* @param key
* @return
*/
public boolean check(String key){
int first = hashcode_1(key);
int second = hashcode_2(key);
int third = hashcode_3(key);
int firstIndex = array[first % arraySize];
if (firstIndex == 0){
return false;
}
int secondIndex = array[second % arraySize];
if (secondIndex == 0){
return false;
}
int thirdIndex = array[third % arraySize];
if (thirdIndex == 0){
return false;
}
return true;
}
/**
* hash 算法1
* @param key
* @return
*/
private int hashcode_1(String key) {
int hash = 0;
int i;
for (i = 0; i < key.length(); ++i) {
hash = 33 * hash + key.charAt(i);
}
return Math.abs(hash);
}
/**
* hash 算法2
* @param data
* @return
*/
private int hashcode_2(String data) {
final int p = 16777619;
int hash = (int) 2166136261L;
for (int i = 0; i < data.length(); i++) {
hash = (hash ^ data.charAt(i)) * p;
}
hash += hash << 13;
hash ^= hash >> 7;
hash += hash << 3;
hash ^= hash >> 17;
hash += hash << 5;
return Math.abs(hash);
}
/**
* hash 算法3
* @param key
* @return
*/
private int hashcode_3(String key) {
int hash, i;
for (hash = 0, i = 0; i < key.length(); ++i) {
hash += key.charAt(i);
hash += (hash << 10);
hash ^= (hash >> 6);
}
hash += (hash << 3);
hash ^= (hash >> 11);
hash += (hash << 15);
return Math.abs(hash);
}
1、首先初始化了一个 int 数组。
2、写入数据的时候进行三次 hash 运算,同时把对应的位置置为1。
3、查询时同样的三次 hash 运算,取到对应的值,一旦有个值为0,则认为数据不存在。
下面来测试下:
bloomFilterTest:
public static void bloomFilterTest(){
long start = System.currentTimeMillis();
int capacity = 10000000;
BloomFilters bloomFilters = new BloomFilters(capacity);
for (int i = 0; i < capacity; i++) {
bloomFilters.add(i + "");
}
System.out.println(bloomFilters.check(2+ ""));
long end = System.currentTimeMillis();
System.out.println("执行时间:" + (end - start));
}
测试类,HeapTest:
/**
* 测试类
* @author K. L. Mao
* @create 2018/11/30
*/
public class HeapTest {
public static void main(String[] args){
// HashSetHeap.hashSetTest();
BloomFilters.bloomFilterTest();
}
执行结果如下:
只花了 1 秒钟就写入了 1000W 的数据同时做出来准确的判断。
当我把 10000000 这个数拿去查询时,却返回了 true,这也体现了 BloomFilter 的误报率。
我们可以提高数组长度以及 hash 计算次数来降低误报率,但是相应的 CPU、内存的消耗就会提高;这就需要根据业务自行权衡了。
仔细观察 GC 日志发现,自定义的布隆过滤器 fullGC 非常的频繁(12次),同时老年代的使用率也达到了 90%,接近崩溃的边缘。
总的来说,就是内存利用率做的不好。
其实,Google guava 包提供了现成的 API,下面来看看业界权威的实现。
private static void guavaTest(){
long start = System.currentTimeMillis();
int capacity = 10000000;
BloomFilter<Integer> filter = BloomFilter.create(
Funnels.integerFunnel(), capacity, 0.01);
for (int i = 0; i < capacity; i++) {
filter.put(i);
}
System.out.println(filter.mightContain(capacity));
long end = System.currentTimeMillis();
System.out.println("执行时间:" + (end - start));
}
也是同样的 1000W 的数据,执行没有问题。
观察 GC 日志,发现没有一次 fullGC,同时老年代的使用率很低。和自定义的一对比,这里明细要好很多,也可以写入更多的数据。
源码分析
来看看 Google 的guava
是如何实现的。
构造方法中有两个比较重要的参数,一个是预计存放多少数据,一个是可以接受的误报率。
我这里的 demo 分别是 1000W 和 0.01。
guava 会通过你预计的数量以及误报率帮你计算出你应当会使用的数组大小 numBits 以及需要计算几次 hash 函数 numHashFunctions。
put 写入函数
public <T> boolean put(T object, Funnel<? super T> funnel, int numHashFunctions, BloomFilterStrategies.LockFreeBitArray bits) {
long bitSize = bits.bitSize();
byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
long hash1 = this.lowerEight(bytes);
long hash2 = this.upperEight(bytes);
boolean bitsChanged = false;
long combinedHash = hash1;
for(int i = 0; i < numHashFunctions; ++i) {
bitsChanged |= bits.set((combinedHash & 9223372036854775807L) % bitSize);
combinedHash += hash2;
}
return bitsChanged;
}
- 根据 murmur3_128() 方法得到一个 128 位长度的 byte[]。
- 分别取高低 8 位,得到两个 hash 值。
- 再根据初始化时的 hash 运算次数进行 hash 运算。
bitsChanged |= bits.set((combinedHash & 9223372036854775807L) % bitSize);
其实也是 hash 取模 拿到 index 后去赋值1。
重点是 bits.set() 方法。
static final class LockFreeBitArray {
private static final int LONG_ADDRESSABLE_BITS = 6;
final AtomicLongArray data;
private final LongAddable bitCount;
LockFreeBitArray(long bits) {
this(new long[Ints.checkedCast(LongMath.divide(bits, 64L, RoundingMode.CEILING))]);
}
LockFreeBitArray(long[] data) {
Preconditions.checkArgument(data.length > 0, "data length is zero!");
this.data = new AtomicLongArray(data);
this.bitCount = LongAddables.create();
long bitCount = 0L;
long[] var4 = data;
int var5 = data.length;
for(int var6 = 0; var6 < var5; ++var6) {
long value = var4[var6];
bitCount += (long)Long.bitCount(value);
}
this.bitCount.add(bitCount);
}
boolean set(long bitIndex) {
if (this.get(bitIndex)) {
return false;
} else {
int longIndex = (int)(bitIndex >>> 6);
long mask = 1L << (int)bitIndex;
long oldValue;
long newValue;
do {
oldValue = this.data.get(longIndex);
newValue = oldValue | mask;
if (oldValue == newValue) {
return false;
}
} while(!this.data.compareAndSet(longIndex, oldValue, newValue));
this.bitCount.increment();
return true;
}
}
boolean get(long bitIndex) {
return (this.data.get((int)(bitIndex >>> 6)) & 1L << (int)bitIndex) != 0L;
}
}
其实 set 方法是LockFreeBitArray
的一个函数, LockFreeBitArray
就是真正存放数据的底层数据结构。
利用了一个AtomicLongArray
(线程安全的数组)来存放数据。
来看 set 方法:
- 在 set 之前,先通过 get() 判断这个数据是否存在于集合中,如果存在,则set 失败。
- 接下来就是通过位运算对
AtomicLongArray
进行赋值(采用 CAS保证线程安全) 。 - get() 方法的计算逻辑和 set 类似,只要判断不等于 0 就返回存在该值。
来看 mightContain 是否存在函数:
public <T> boolean mightContain(T object, Funnel<? super T> funnel, int numHashFunctions, BloomFilterStrategies.LockFreeBitArray bits) {
long bitSize = bits.bitSize();
long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong();
int hash1 = (int)hash64;
int hash2 = (int)(hash64 >>> 32);
for(int i = 1; i <= numHashFunctions; ++i) {
int combinedHash = hash1 + i * hash2;
if (combinedHash < 0) {
combinedHash = ~combinedHash;
}
if (!bits.get((long)combinedHash % bitSize)) {
return false;
}
}
return true;
}
前面几步的逻辑都是类似的,只是调用了刚才的 get() 方法判断元素是否存在而言。
缓存穿透
在互联网行业,应对高并发的场景,使用缓存,是很常见的手段。这样能够避免一些重复的请求,到达 DB,从而降低数据库压力。但是,在使用缓存时,我们应该注意一种场景,那就是缓存穿透。当客户端一直使用不存在的 key 请求服务端时,就会一直查询数据库,这样缓存就没起到作用了。
缓存穿透的解决方案:
1、缓存空对象
当客户端使用一个 key 请求服务器时,我们会先去缓存(如redis)去查询是否存在该 key 所对应的数据,如果存在则直接返回结果;如果不存在,则去数据库查询。此时,如果数据库有该数据,则将该数据写入缓存,如果数据库没有该数据,则将一个空对象写入缓存(一般设置过期时间为5分钟)。在5分钟之内,如果客户端仍然拿这个不存在的 key 请求服务端,就会直接返回一个空对象过去了,而不会去查库。由于实现比较简单,这里就不代码演示了。
2、布隆过滤器
我们上面介绍的布隆过滤器,也是解决缓存穿透的一个方案,并且很多大厂也都在用它。下面,我们就用代码示例,来看下它是怎么用的。
/**
* 布隆过滤器
* @author K. L. Mao
* @create 2018/12/1
*/
@Component
public class BloomFilters {
private static final int capacity = 1000000;
private static final double fpp = 0.01;
private BloomFilter<String> filter;
@Autowired
private CmfOrderService cmfOrderService;
@PostConstruct
public void init(){
filter = BloomFilter.create(Funnels.stringFunnel(Charset.forName("UTF-8")), capacity, fpp);
List<CmfOrder> cmfOrders = cmfOrderService.findAllCmfOrder();
if (cmfOrders.isEmpty()){
return;
}
cmfOrders.forEach(cmfOrder -> filter.put(cmfOrder.getPaymentSeqNo()));
}
/**
* 加入key
* @param key
*/
public void put(String key){
filter.put(key);
}
/**
* 判断是否存在
* @param key
* @return
*/
public boolean mightContain(String key){
return filter.mightContain(key);
}
}
首先,定义一个布隆过滤器的配置类,让 Spring 去管理它,这样我们就可以让该配置类在构造完成之后,调用 init() 方法。
在 init() 方法里面,我们是将库里的所有订单查询出来,并且将所有的支付流水号paymentSeqNo
赋值给 BloomFilter
。
这里我的预估容量是100W,允许误报率为0.01,你们可以根据实际的业务量进行调整。
/**
* 初始化支付单
*
* @param memberId
* @param orderNo
* @param fundChannelCode
*/
@Override
public void initCmfOrder(String memberId, String paymentSeqNo, String fundChannelCode) {
// 每个订单入库之前,将支付流水号加入布隆过滤器
bloomFilters.put(paymentSeqNo);
CmfOrder cmfOrder= new CmfOrder();
cmfOrder.setFundChannelCode(fundChannelCode);
cmfOrder.setMemberId(memberId);
cmfOrder.setPaymentSeqNo(paymentSeqNo);
cmfOrderMapper.save(cmfOrder);
}
每个订单入库之前,将支付流水号加入布隆过滤器。
/**
* 查询订单结果
*
* @param resultInfoForm
* @return
*/
@Override
public ResultInfoDTO getResultInfo(ResultInfoForm resultInfoForm) throws GlobalException {
log.info("查询订单结果,请求参数:{}", resultInfoForm);
String paymentSeqNo = resultInfoForm.getOrderNo();
// 到缓存查询
ResultInfoDTO resultInfoDTO = jedisService.getFromCache(CacheTypeEnum.RESULT_INFO.getText(), paymentSeqNo);
if (resultInfoDTO != null) {
log.info("该订单号已有缓存,直接取缓存数据,resultInfoDTO:{}", resultInfoDTO);
return resultInfoDTO;
}
if (!bloomFilters.mightContain(paymentSeqNo)){
log.warn("订单号不存在,paymentSeqNo:{}", paymentSeqNo);
throw new GlobalException(CodeMsg.ORDER_NO_ERROR);
}
//获取CmfOrder
final CmfOrder cmfOrder = acquireCmfOrderByPaymentSeqNo(paymentSeqNo);
return packageBindingResultInfo(cmfOrder);
}
查询订单结果时,先去缓存查:jedisService.getFromCache(key)
,如果有,直接返回结果;如果没有,则使用布隆过滤器判断是否存在该 key:bloomFilters.mightContain(key)
。如果布隆过滤器不存在该 key,则直接返回客户端错误信息;如果存在该 key,才允许其去查库。这样就能够抵挡住大部分不存在 key 的查库请求了。
但是,由于上面的BloomFilter
是 JDK 自带的,数据只保存在内存中,所以只适用单机环境,集群环境下是不适用的。那么,如何让其在集群环境也能支持呢?这时候我们就需要引入 Redis 的BizMap
存储类型。
-
基于Redis 的 BloomFilter
由于 Redis 的BizMap
存储的是二进制,正好和BloomFilter
的存储完全吻合。于是,我们可以使用 Redis 来实现集群环境下的BloomFilter
。
唯一需要注意的是 Redis 的BitMap
只支持2^32 大小,对应到内存也就是 512MB,数组的下标最大只能是2^32-1。不过这个限制可以通过构建多个 Redis 的Bitmap
通过 hash 取模的方式分散一下即可。万分之一的误判率,512MB 可以放下2亿条数据。
- RedisService
package yongda.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import redis.clients.jedis.ShardedJedis;
import redis.clients.jedis.ShardedJedisPool;
import yongda.fw.cache.JedisService;
/**
* @author K. L. Mao
* @create 2019/1/20
*/
@Component
public class RedisService {
@Autowired
private ShardedJedisPool jedisPool;
/**
* key 键对应的值 value 对应的 ascii 码,在 offset 的位置(从左向右数) 变为 value。
* @param key
* @param offset
* @param value
*/
public void putBitInCache(String key, long offset, boolean value) {
ShardedJedis jedis = jedisPool.getResource();
jedis.setbit(key, offset, value);
}
/**
* 获取 Bit
* @param key
* @return
*/
public String getBitFromCache(String key) {
ShardedJedis jedis = jedisPool.getResource();
return jedis.get(key);
}
/**
* 判断指定的位置 ASCII 码的 bit 位是否为1。
* @param key
* @param offset
* @return
*/
public boolean getBitFromCache(String key, int offset) {
ShardedJedis jedis = jedisPool.getResource();
return jedis.getbit(key, offset);
}
}
jedis.setbit(key, offset, value)
:key 键对应的值 value 对应的 ASCII 码,在 offset 的位置(从左向右数)设置为 value。
jedis.getbit(key, offset)
: 判断指定的位置 ASCII 码的 bit 位是否为 1。
- BloomFilter 工具类
package yongda.config;
import org.springframework.stereotype.Component;
import yongda.enums.CacheTypeEnum;
import yongda.service.CmfOrderService;
import yongda.util.DateUtil;
import yongda.vo.CmfOrder;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collection;
import java.util.Date;
import java.util.List;
/**
* 使用 redis 的 bizMap 实现 BloomFilter
* @param <E>
*/
@Component
public class BloomFilter<E> {
@Resource
private RedisService redisService;
@Resource
private CmfOrderService cmfOrderService;
// total length of the Bloom filter
private static int sizeOfBloomFilter;
// number of hash functions
private static int numberOfHashFunctions;
// 误差率
private static final double falsePositiveProbability = 0.01;
// 预计容量
private static final int expectedNumberOfElements = 1000000;
// encoding used for storing hash values as strings
private final Charset charset = Charset.forName("UTF-8");
// MD5 gives good enough accuracy in most circumstances. Change to SHA1 if it's needed
private static final String hashName = "MD5";
private static final MessageDigest digestFunction;
// The digest method is reused between instances
static {
MessageDigest tmp;
try {
tmp = java.security.MessageDigest.getInstance(hashName);
} catch (NoSuchAlgorithmException e) {
tmp = null;
}
digestFunction = tmp;
// numberOfHashFunctions = ceil(-ln(falsePositiveProbability)/ln2)
numberOfHashFunctions = (int) Math.ceil(-(Math.log(falsePositiveProbability) / Math.log(2)));
// sizeOfBloomFilter = ceil(numberOfHashFunctions*expectedNumberOfElements/ln2)
sizeOfBloomFilter = (int) Math.ceil(numberOfHashFunctions * expectedNumberOfElements / Math.log(2));
}
@PostConstruct
public void init(){
// 获取一个月前的日期
Date lastMonth = DateUtil.getDateByAdd("m", -1, new Date());
List<CmfOrder> cmfOrders = cmfOrderService.findCmfOrdersOfLastMonth(lastMonth);
if (cmfOrders.isEmpty()){
return;
}
redisService.deleteCache(CacheTypeEnum.ORDER_BLOOM_FILTER.getText());
cmfOrders.forEach(cmfOrder -> add(cmfOrder.getPaymentSeqNo().getBytes(charset)));
}
/**
* Adds an object to the Bloom filter. The output from the object's
* toString() method is used as input to the hash functions.
*
* @param element is an element to register in the Bloom filter.
*/
public void add(E element) {
add(element.toString().getBytes(charset));
}
/**
* Adds an array of bytes to the Bloom filter.
*
* @param bytes array of bytes to add to the Bloom filter.
*/
private void add(byte[] bytes) {
if (redisService.getBitFromCache(CacheTypeEnum.ORDER_BLOOM_FILTER.getText()) == null) {
redisService.putBitInCache(CacheTypeEnum.ORDER_BLOOM_FILTER.getText(), 0, false);
}
int[] hashes = createHashes(bytes, numberOfHashFunctions);
for (int hash : hashes) {
redisService.putBitInCache(CacheTypeEnum.ORDER_BLOOM_FILTER.getText(), Math.abs(hash % sizeOfBloomFilter), true);
}
}
/**
* Adds all elements from a Collection to the Bloom filter.
*
* @param c Collection of elements.
*/
public void addAll(Collection<? extends E> c) {
for (E element : c) {
add(element);
}
}
/**
* Returns true if the element could have been inserted into the Bloom filter.
* Use getFalsePositiveProbability() to calculate the probability of this
* being correct.
*
* @param element element to check.
* @return true if the element could have been inserted into the Bloom filter.
*/
public boolean contains(E element) {
return contains(element.toString().getBytes(charset));
}
/**
* Returns true if the array of bytes could have been inserted into the Bloom filter.
* Use getFalsePositiveProbability() to calculate the probability of this
* being correct.
*
* @param bytes array of bytes to check.
* @return true if the array could have been inserted into the Bloom filter.
*/
private boolean contains(byte[] bytes) {
int[] hashes = createHashes(bytes, numberOfHashFunctions);
for (int hash : hashes) {
if (!redisService.getBitFromCache(CacheTypeEnum.ORDER_BLOOM_FILTER.getText(), Math.abs(hash % sizeOfBloomFilter))) {
return false;
}
}
return true;
}
/**
* Returns true if all the elements of a Collection could have been inserted
* into the Bloom filter. Use getFalsePositiveProbability() to calculate the
* probability of this being correct.
*
* @param c elements to check.
* @return true if all the elements in c could have been inserted into the Bloom filter.
*/
public boolean containsAll(Collection<? extends E> c) {
for (E element : c) {
if (!contains(element)) {
return false;
}
}
return true;
}
/**
* 根据字节数组的内容生成摘要,并将结果分割成 4 字节的整数并存储在数组中。
* 调用 digest 函数,直到生成所需的 int 数。每次生成摘要之前,都需要加盐,并且 salt++。
*
* @param data specifies input data.
* @param hashes number of hashes/int's to produce.
* @return array of int-sized hashes
*/
private static int[] createHashes(byte[] data, int hashes) {
int[] result = new int[hashes];
int k = 0;
byte salt = 0;
while (k < hashes) {
byte[] digest;
synchronized (digestFunction) {
digestFunction.update(salt);
salt++;
digest = digestFunction.digest(data);
}
for (int i = 0; i < digest.length / 4 && k < hashes; i++) {
int h = 0;
for (int j = (i * 4); j < (i * 4) + 4; j++) {
h <<= 8;
h |= ((int) digest[j]) & 0xFF;
}
result[k] = h;
k++;
}
}
return result;
}
/**
* Calculates a hash code for this class.
*
* @return hash code representing the contents of an instance of this class.
*/
@Override
public int hashCode() {
int hash = 7;
hash = 61 * hash + sizeOfBloomFilter;
hash = 61 * hash + expectedNumberOfElements;
hash = 61 * hash + numberOfHashFunctions;
return hash;
}
}
原理是和 JDK 自带的BloomFilter
类似的,我们看add
方法,它先 Redis 缓存中是否有指定 key(如:orderBloomFilter) 的值,如果没有,则在 offset = 0 处,添加一个值为false
,即为 0;然后调用createHashes(byte[] data, int hashes)
方法,根据字节数组的内容生成digest
,并将结果分割成 4 字节的整数并存储在数组中,数组中的整数可以理解为每次hash
所得的hashcode
的值。最后,遍历hashcode
数组,将hashcode%sizeOfBloomFilter
取模所得下标所对应的值设为true
,即为 1。
contains
方法,同样调用createHashes(byte[] data, int hashes)
得到字节数组内容所对应的hashcode
数组。遍历hashcode
数组,如果有一个hashcode
所对应的下标的值不为1,则该数据不存在。反之,只有所有的hashcode
所对应的下标的值都为1,才能说明该数据已经存在。
补充:
问:十亿条淘宝购买记录,怎么获取出现最多的前十个?
这个问题,跟上面的问题类似,依旧是有限内存的海量数据处理的题目。这里由于需要获取数据信息,而不是判断是否存在,则不能使用布隆过滤器了。但是细想是一样的,那就是分而治之的思路:
1、对这海量数据进行 hash(id)%mod取摸,mod看数据规模,假如取1024;这样就将10亿数据分发到了1024个小文件上。
2、对每个小文件的 id 作为 key,出现次数作为 value 存储在 map 中,这样就能得到每个小文件上 value前10的map 集合。
3、将1024个小文件上的前10 map 集合进行整合,然后排序,取出前10 map 即可。