系统及其他相关问题整理

目录

1.遇到线上相关问题怎么排查?

2.高并发系统的限流如何实现?

3.高并发秒杀系统的设计?

补充问题:

秒杀并发情况下库存为负数问题

4.负载均衡如何设计?

服务端负载均衡

客户端负载均衡

负载均衡算法

静态负载均衡算法

动态负载均衡算法

5.假如双十一等一些促销有高并发访问量要来访问我们的数据,怎么样做到可靠的服务?

6.一个黑名单集合,数据量很大,快速查询一个值是否在集合里,怎么设计?

背景知识:

1.布隆过滤器基本介绍、特点及使用场景

2.布隆过滤器原理

3.简单实现一个布隆过滤

4.Guava实现布隆过滤及源码分析

补充问题:

一个网站有 20 亿 url 存在一个黑名单中,这个黑名单要怎么存?若此时随便输入一个 url,你如何快速判断该 url 是否在这个黑名单中?并且需在给定内存空间(比如:500M)内快速判断出。

7.常见的设计模式及应用场景。

参考书籍、文献和资料


备注:针对基本问题做一些基本的总结,不是详细解答!

1.遇到线上相关问题怎么排查?

以个人看法来看:

首先,必须了解当前的问题是什么?有什么现象?具体的业务场景是什么?先将遇到的问题进行明确!

然后,按当前的现象结合实际情况和业务日志来定位问题:网络原因?服务环境问题(CPU/IO/内存等)?数据库问题?Redis等中间件问题?JVM异常问题?业务场景本身存在的问题?

然后,按照具体问题来分析:

  • 网络原因:大的断网不用考虑,直接能知道,主要考虑网络抖动和网络攻击,tcpdump网络抓包,Wireshark分析网络包数据,进行综合分析;
  • 服务环境问题:使用相关命令定位查看---->top主要关注cpu的load以及比較耗cpu的进程---->free、vmstat查看内存---->iostat查看具体的IO情况---->netstat查看网络状态
  • 数据库问题:登录线上库,查看数据库连接情况:show processlist。查看当前数据库的连接情况。确实因为慢查询造成,须要手动kill。
  • Redis等中间件问题:一般这些都需要监控工具提前做监控,同时查看相关状态和相关配置问题,结合系统内来看;
  • JVM异常问题:常见的oom等去具体分析,大量请求造成的栈区oom,大bytes字节文件等和大量对象创建造成的堆区oom,采用反射大量动态创建对象rpc等原数据区oom等,个人看法,有误请指出!
  • 业务场景本身存在的问题:这个就是业务问题存在问题或开发过程中考虑不全存在的问题,需要梳理放入新的迭代去优化。

2.高并发系统的限流如何实现?

常见的限流算法有计数器、漏桶和令牌桶算法。

漏桶算法在分布式环境中消息中间件或者Redis都是可选的方案

令牌桶算法发放令牌的频率增加可以提升整体数据处理的速度,通过每次获取令牌的个数增加或者放慢令牌的发放速度和降低整体数据处理速度。而漏桶不行,因为它的流出速率是固定的,程序处理速度也是固定的。

相关见博客:https://blog.csdn.net/xiaofeng10330111/article/details/86772740

3.高并发秒杀系统的设计?

架构设计上要提前分析,梳理主流程和可能遇到的种种问题,优化设计,同时做到“4 要 1 不要”原则,也就是:数据要尽量少、请求数要尽量少、路径要尽量短、依赖要尽量少,以及不要有单点

  • 数据要尽量少,首先是指用户请求的数据能少就少。请求的数据包括上传给系统的数据和系统返回给用户的数据(通常就是网页)。还要求系统依赖的数据能少就少,包括系统完成某些业务逻辑需要读取和保存的数据,这些数据一般是和后台服务以及数据库打交道的。
  • 请求数要尽量少,用户请求的页面返回后,浏览器渲染这个页面还要包含其他的额外请求,比如说,这个页面依赖的 CSS/JavaScript、图片,以及 Ajax 请求等等都定义为“额外请求”,这些额外请求应该尽量少
  • 路径要尽量短,路径要尽量短所谓“路径”,就是用户发出请求到返回数据这个过程中,需求经过的中间的节点数。要缩短访问路径有一种办法,就是多个相互强依赖的应用合并部署在一起,把远程过程调用(RPC)变成 JVM 内部之间的方法调用
  • 依赖要尽量少所谓依赖,指的是要完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖。要减少依赖,我们可以给系统进行分级分析
  • 不要有单点系统中的单点可以说是系统架构上的一个大忌,因为单点意味着没有备份,风险不可控。

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。

  • 缓存:缓存比较好理解,在大型高并发系统中,如果没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪。使用缓存不单单能够提升系统访问速度、提高并发访问量,也是保护数据库、保护系统的有效方式。大型网站一般主要是“读”,缓存的使用很容易被想到。在大型“写”系统中,缓存也常常扮演者非常重要的角色。比如累积一些数据批量写入,内存里面的缓存队列(生产消费),以及HBase写数据的机制等等也都是通过缓存提升系统的吞吐量或者实现系统的保护措施。甚至消息中间件,你也可以认为是一种分布式的数据缓存。
  • 降级:服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随机服务。根据服务范围:可以砍掉某个功能,也可以砍掉某些模块。总之服务降级需要根据不同的业务需求采用不同的降级策略。主要的目的就是服务虽然有损但是总比没有好。
  • 限流:限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。

补充问题:

秒杀并发情况下库存为负数问题

  • for update显示加锁
  • 把udpate语句写在前边,先把数量-1,之后select出库存如果>-1就commit,否则rollback。
update products set quantity = quantity-1 WHERE id=3;
select quantity from products WHERE id=3 for update;
  • update语句在更新的同时加上一个条件
quantity = select quantity from products WHERE id=3;
update products set quantity = ($quantity-1) WHERE id=3 and queantity = $quantity;

4.负载均衡如何设计?

负载均衡建立在现有网络结构上,提供了一种廉价、有效、透明的方法扩展服务器的带宽、增加吞吐量、加强网络数据处理能力,以及提高网络的灵活性。以各种负载均衡算法为基础的分发策略决定了负载均衡的效果,根据服务器地址列表所存放的位置可以分为两大类,一类是服务器负载均衡,另一类是客服端负载均衡

服务端负载均衡

客户端发送请求到负载均衡器LB,负载均衡器负责将接收到的各个请求转发到运行中的某台服务节点上,然后接收到请求的微服务做响应处理,常见的有Apache、Nginx、HAProxy

实现机制比较忙简单,只需要在客服端与各个微服务实例之间架设集中式的负载均衡器即可,负载均衡器动态获取各个微服务运行时的信息,决定负载均衡的目标服务,若负载均衡器检测到某个服务已经不可用的时候就会自动移除该服务。

注意,负载均衡器运行在一台独立的服务器上并充当代理的作用,同时,需要注意的是当服务请求越来越大的时候,负载均衡器就会成为系统的瓶颈,同时若负载均衡器自身发生失败时,整体服务的调用都将发生失败。

客户端负载均衡

客户端负载均衡机制的主要优势就是不会出现集中式负载均均衡所产生的瓶颈问题,因为每个客户端都有自己的负载均衡器,负载均衡器失败也不会造成严重的后果,但是运行时的信息在多个负载均衡器之间进行服务配置信息的传递会在一定程度上加重网络流量负载。

实现上,需要在客服端程序里面自己设定一个调度算法,在向服务器发起请求的时候,先执行调度算法计算出目标服务器地址

客户端负载均衡比较适合于客户端具有成熟的调度库函数、算法以及API的工具和框架

负载均衡算法

大致可以分为两大类,即静态负载均衡算法和动态负载均衡算法。

静态负载均衡算法

主要指的是各种随机算法和轮询算法。

  • 轮询算法:将请求按顺序轮流地分配到后端服务器上,它均衡地对待后端的每一台服务器,而不关心服务器实际的连接数和当前的系统负载。
  • 随机算法:通过系统的随机算法,根据后端服务器的列表大小值来随机选取其中的一台服务器进行访问。由概率统计理论可以得知,随着客户端调用服务端的次数增多,其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果。
  • 加权轮询算法:不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,让其处理更多的请;而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。

动态负载均衡算法

根据服务器的实时性能分配连接是常见的动态策略所有涉及权重的静态算法都可以转变为动态算法。常见的有以下几种:

  • 最小连接数算法:比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器。
  • 源地址哈希算法:根据获取客户端的IP地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一IP地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问。

相关见博客:https://blog.csdn.net/xiaofeng10330111/article/details/85682513

5.假如双十一等一些促销有高并发访问量要来访问我们的数据,怎么样做到可靠的服务?

架构设计上要提前分析,梳理主流程和可能遇到的种种问题,优化设计,同时做到“4 要 1 不要”原则,也就是:数据要尽量少、请求数要尽量少、路径要尽量短、依赖要尽量少,以及不要有单点

  • 数据要尽量少,首先是指用户请求的数据能少就少。请求的数据包括上传给系统的数据和系统返回给用户的数据(通常就是网页)。还要求系统依赖的数据能少就少,包括系统完成某些业务逻辑需要读取和保存的数据,这些数据一般是和后台服务以及数据库打交道的。
  • 请求数要尽量少,用户请求的页面返回后,浏览器渲染这个页面还要包含其他的额外请求,比如说,这个页面依赖的 CSS/JavaScript、图片,以及 Ajax 请求等等都定义为“额外请求”,这些额外请求应该尽量少
  • 路径要尽量短,路径要尽量短所谓“路径”,就是用户发出请求到返回数据这个过程中,需求经过的中间的节点数。要缩短访问路径有一种办法,就是多个相互强依赖的应用合并部署在一起,把远程过程调用(RPC)变成 JVM 内部之间的方法调用
  • 依赖要尽量少所谓依赖,指的是要完成一次用户请求必须依赖的系统或者服务,这里的依赖指的是强依赖。要减少依赖,我们可以给系统进行分级分析
  • 不要有单点系统中的单点可以说是系统架构上的一个大忌,因为单点意味着没有备份,风险不可控。

在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流。

  • 缓存:缓存比较好理解,在大型高并发系统中,如果没有缓存数据库将分分钟被爆,系统也会瞬间瘫痪。使用缓存不单单能够提升系统访问速度、提高并发访问量,也是保护数据库、保护系统的有效方式。大型网站一般主要是“读”,缓存的使用很容易被想到。在大型“写”系统中,缓存也常常扮演者非常重要的角色。比如累积一些数据批量写入,内存里面的缓存队列(生产消费),以及HBase写数据的机制等等也都是通过缓存提升系统的吞吐量或者实现系统的保护措施。甚至消息中间件,你也可以认为是一种分布式的数据缓存。
  • 降级:服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级往往会指定不同的级别,面临不同的异常等级执行不同的处理。根据服务方式:可以拒接服务,可以延迟服务,也有时候可以随机服务。根据服务范围:可以砍掉某个功能,也可以砍掉某些模块。总之服务降级需要根据不同的业务需求采用不同的降级策略。主要的目的就是服务虽然有损但是总比没有好。
  • 限流:限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。

6.一个黑名单集合,数据量很大,快速查询一个值是否在集合里,怎么设计?

采用布隆过滤器,使用一个byte数组保存黑名单集合,使用布隆过滤器原理来判断快速查询一个值是否在集合里。

背景知识:

1.布隆过滤器基本介绍、特点及使用场景

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。

假设遇到这样一个问题:一个网站有 20 亿 url 存在一个黑名单中,这个黑名单要怎么存?若此时随便输入一个 url,你如何快速判断该 url 是否在这个黑名单中?并且需在给定内存空间(比如:500M)内快速判断出。

哈希算法得出的Integer哈希值最大为:Integer.MAX_VALUE=2147483647,意思就是任何一个URL的哈希都会在0~2147483647之间。那么可以定义一个2147483647长度的byte数组用来存储集合所有可能的值。为了存储这个byte数组,系统只需要:2147483647/8/1024/1024=256M比如:某个URL(X)的哈希是2,那么落到这个byte数组在第二位上就是1,这个byte数组将是:000….00000010,重复的,将这20亿个数全部哈希并落到byte数组中。

判断逻辑:如果byte数组上的第二位是1,那么这个URL(X)可能存在。为什么是可能?因为有可能其它URL因哈希碰撞哈希出来的也是2,这就是误判。但是如果这个byte数组上的第二位是0,那么这个URL(X)就一定不存在集合中。

多次哈希:为了减少因哈希碰撞导致的误判概率,可以对这个URL(X)用不同的哈希算法进行N次哈希,得出N个哈希值,落到这个byte数组上,如果这N个位置没有都为1,那么这个URL(X)就一定不存在集合中。

算法特点

  • 使用哈希判断,时间效率很高。空间效率也是其一大优势
  • 有误判的可能,需针对具体场景使用
  • 因为无法分辨哈希碰撞,所以不是很好做删除操作

使用场景

  • 黑名单
  • URL去重
  • 单词拼写检查
  • Key-Value缓存系统的Key校验
  • ID校验,比如订单系统查询某个订单ID是否存在,如果不存在就直接返回。

2.布隆过滤器原理

布隆过滤器(Bloom Filter)的核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k

系统及其他相关问题整理_第1张图片

以上图为例,具体的操作流程:假设集合里面有3个元素{x, y, z}哈希函数的个数为3

  • 首先将位数组进行初始化,将里面每个位都设置位0。
  • 对于集合里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。
  • 查询W元素是否存在集合中的时候,同样的方法将W通过哈希映射到位数组上的3个点。
  • 如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中。

注意:此处不能判断该元素是否一定存在集合中,可能存在一定的误判率。

可以从图中可以看到:假设某个元素通过映射对应下标为4,5,6这3个点。虽然这3个点都为1,但是很明显这3个点是不同元素经过哈希得到的位置,因此这种情况说明元素虽然不在集合中,也可能对应的都是1,这是误判率存在的原因。

整个的写入、查询的流程就是这样,汇总起来就是:对写入的数据做 H 次 hash 运算定位到数组中的位置,同时将数据改为 1 。当有数据查询时也是同样的方式定位到数组中。一旦其中的有一位为 0 则认为数据肯定不存在于集合,否则数据可能存在于集合中。

所以布隆过滤有以下几个特点:

  1. 只要返回数据不存在,则肯定不存在。
  2. 返回数据存在,但只能是大概率存在。
  3. 同时不能清除其中的数据。

第一点应该都能理解,重点解释下 2、3 点。

为什么返回存在的数据却是可能存在呢,这其实也和 HashMap 类似。

在有限的数组长度中存放大量的数据,即便是再完美的 Hash 算法也会有冲突,所以有可能两个完全不同的 A、B 两个数据最后定位到的位置是一模一样的。这时拿 B 进行查询时那自然就是误报了。

删除数据也是同理,当我把 B 的数据删除时,其实也相当于是把 A 的数据删掉了,这样也会造成后续的误报。

基于以上的 Hash 冲突的前提,所以 Bloom Filter 有一定的误报率,这个误报率和 Hash 算法的次数 H,以及数组长度 L 都是有关的

3.简单实现一个布隆过滤

基本思路:

  • 首先初始化了一个 int 数组
  • 写入数据的时候进行三次 hash 运算,同时把对应的位置置为 1
  • 查询时同样的三次 hash 运算,取到对应的值,一旦值为 0 ,则认为数据不存在

注意:提高数组长度以及 hash 计算次数可以降低误报率,但相应的 CPU、内存的消耗就会提高;这就需要根据业务需要自行权衡。

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);

        if (array[first % arraySize] == 0 || array[second % arraySize] == 0 
            array[third % arraySize] == 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);
    }
}

4.Guava实现布隆过滤及源码分析

为使性能效果和内存利用率做到最好,建议使用Guava BloomFilter实现。

    @Test
    public void guavaTest() {
        long star = System.currentTimeMillis();
        BloomFilter filter = BloomFilter.create(
                Funnels.integerFunnel(),
                10000000,
                0.01);

        for (int i = 0; i < 10000000; i++) {
            filter.put(i);
        }

        Assert.assertFalse(filter.mightContain(96998));
        long end = System.currentTimeMillis();
        System.out.println("执行时间:" + (end - star));
    }

源码分析如下:

static  BloomFilter create(
    Funnel funnel, long expectedInsertions, double fpp, Strategy strategy) {
    checkNotNull(funnel);
    checkArgument(expectedInsertions >= 0, "Expected insertions (%s) must be >= 0", expectedInsertions);
    checkArgument(fpp > 0.0, "False positive probability (%s) must be > 0.0", fpp);
    checkArgument(fpp < 1.0, "False positive probability (%s) must be < 1.0", fpp);
    checkNotNull(strategy);

    if (expectedInsertions == 0) {
      expectedInsertions = 1;
    }

    long numBits = optimalNumOfBits(expectedInsertions, fpp);
    int numHashFunctions = optimalNumOfHashFunctions(expectedInsertions, numBits);
    try {
      return new BloomFilter(new LockFreeBitArray(numBits), numHashFunctions, funnel, strategy);
    } catch (IllegalArgumentException e) {
      throw new IllegalArgumentException("Could not create BloomFilter of " + numBits + " bits", e);
    }
  }

从代码可以看出,需要4个参数,分别是

  • funnel 用来对参数做转化,方便生成hash值
  • expectedInsertions 预期插入的数据量大小,也就是上文公式中的n
  • fpp 误判率,也就是上文公式中的误判率p
  • strategy 生成hash值的策略,guava中也提供了默认策略,一般不需要你自己重新实现

从上面代码可知,BloomFilter创建过程中先检查参数的合法性

使用n和p来计算bitmap的大小m(optimalNumOfBits(expectedInsertions, fpp))

通过n和m计算hash函数的个数k(optimalNumOfHashFunctions(expectedInsertions, numBits))

这俩方法的具体实现如下:

static int optimalNumOfHashFunctions(long n, long m) {
    // (m / n) * log(2), but avoid truncation due to division!
    return Math.max(1, (int) Math.round((double) m / n * Math.log(2)));
  }
  static long optimalNumOfBits(long n, double p) {
    if (p == 0) {
      p = Double.MIN_VALUE;
    }
    return (long) (-n * Math.log(p) / (Math.log(2) * Math.log(2)));
  }

除此之外,BloomFilter除了提供创建和几个核心的功能外,还支持写入Stream或从Stream中重新生成BloomFilter,方便数据的共享和传输。

最关键的两个函数如下:

put函数和mightContain函数

MURMUR128_MITZ_64() {
    @Override
    public  boolean put(
        T object, Funnel funnel, int numHashFunctions, BitArray bits) {
      long bitSize = bits.bitSize();
      byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
      long hash1 = lowerEight(bytes);
      long hash2 = upperEight(bytes);

      boolean bitsChanged = false;
      long combinedHash = hash1;
      for (int i = 0; i < numHashFunctions; i++) {
        // Make the combined hash positive and indexable
        bitsChanged |= bits.set((combinedHash & Long.MAX_VALUE) % bitSize);
        combinedHash += hash2;
      }
      return bitsChanged;
    }

    @Override
    public  boolean mightContain(
        T object, Funnel funnel, int numHashFunctions, BitArray bits) {
      long bitSize = bits.bitSize();
      byte[] bytes = Hashing.murmur3_128().hashObject(object, funnel).getBytesInternal();
      long hash1 = lowerEight(bytes);
      long hash2 = upperEight(bytes);

      long combinedHash = hash1;
      for (int i = 0; i < numHashFunctions; i++) {
        // Make the combined hash positive and indexable
        if (!bits.get((combinedHash & Long.MAX_VALUE) % bitSize)) {
          return false;
        }
        combinedHash += hash2;
      }
      return true;
    }

抽象来看,put是写,mightContain是读,两个方法的代码有一点相似,都是先利用murmur3 hash对输入的funnel计算得到128位的字节数组,然后高低分别取8个字节(64位)创建2个long型整数hash1,hash2作为哈希值。循环体内采用了2个函数模拟其他函数的思想,即上文提到的gi(x) = h1(x) + ih2(x) ,这相当于每次累加hash2,然后通过基于bitSize取模的方式在bit数组中索引。

在put方法中,先是将索引位置上的二进制置为1,然后用bitsChanged记录插入结果,如果返回true表明没有重复插入成功,而mightContain方法则是将索引位置上的数值取出,并判断是否为0,只要其中出现一个0,那么立即判断为不存在。

再说一下底层bit数组的实现,主要代码如下:

static final class BitArray {
    final long[] data;
    long bitCount;

    BitArray(long bits) {
      this(new long[Ints.checkedCast(LongMath.divide(bits, 64, RoundingMode.CEILING))]);
    }

    // Used by serialization
    BitArray(long[] data) {
      checkArgument(data.length > 0, "data length is zero!");
      this.data = data;
      long bitCount = 0;
      for (long value : data) {
        bitCount += Long.bitCount(value);
      }
      this.bitCount = bitCount;
    }

    /** Returns true if the bit changed value. */
    boolean set(long index) {
      if (!get(index)) {
        data[(int) (index >>> 6)] |= (1L << index);
        bitCount++;
        return true;
      }
      return false;
    }

    boolean get(long index) {
      return (data[(int) (index >>> 6)] & (1L << index)) != 0;
    }

    /** Number of bits */
    long bitSize() {
      return (long) data.length * Long.SIZE;
    }
...
}

Guava没有使用java.util.BitSet,而是封装了一个long型的数组,另外还有一个long型整数,用来统计数组中已经占用(置为1)的数量,在第一个构造函数中,它把传入的long型整数按长度64分段(例如129分为3段),段数作为数组的长度,你可以想象成由若干个64位数组拼接成一个超长的数组,它的长度就是64乘以段数,即bitSize,在第二个构造函数中利用Long.bitCount方法来统计对应二进制编码中的1个数,这个方法在JDK1.5中就有了,其算法设计得非常精妙,有精力的同学可以自行研究。

另外两个重要的方法是set和get,在get方法中,参考put和mightContain方法,传入的参数index是经过bitSize取模的,因此一定能落在这个超长数组的范围之内,为了获取index对应索引位置上的值,首先将其无符号右移6位,并且强制转换成int型,这相当于除以64向下取整的操作,也就是换算成段数,得到该段上的数值之后,又将1左移index位,最后进行按位与的操作,如果结果等于0,那么返回false,从而在mightContain中判断为不存在。在set方法中,首先调用了get方法判断是否已经存在,如果不存在,则用同样的逻辑取出data数组中对应索引位置的数值,然后按位或并赋值回去。

到这里,对Guava中布隆过滤器的实现就基本讨论完了,简单总结一下:

  • BloomFilter类的作用在于接收输入,利用公式完成对参数的估算,最后初始化Strategy接口的实例;
  • BloomFilterStrategies是一个枚举类,具有两个实现了Strategy接口的成员,分别为MURMUR128_MITZ_32和MURMUR128_MITZ_64,另外封装了long型的数组作为布隆过滤器底层的bit数组,其中在get和set方法中完成核心的位运算。

补充问题:

一个网站有 20 亿 url 存在一个黑名单中,这个黑名单要怎么存?若此时随便输入一个 url,你如何快速判断该 url 是否在这个黑名单中?并且需在给定内存空间(比如:500M)内快速判断出。

答案如上!

7.常见的设计模式及应用场景。

这个后期出几篇关于这个的相关博客,具体会在这边做补充。

 

参考书籍、文献和资料

1.https://www.cnblogs.com/crossoverJie/p/10018231.html

2.https://cloud.tencent.com/developer/article/1533083

3.https://www.jianshu.com/p/88c6ac4b38c8

4.https://blog.csdn.net/xindoo/article/details/103183445

5.https://segmentfault.com/a/1190000012620152

你可能感兴趣的:(微服务架构与开发)