0

    中间件面试题(2021优化版)

    2023.04.29 | admin | 243次围观

    原理:一个生产者,多个消费者,每个消费者都可以收到相同的消息。生产者将消息发送到交换机,交换机类型是fanout,不同的队列注册到交换机上,不同的消费者监听不同的队列,所有消费者都会收到消息。

    场景:邮件群发,群聊天,广播(广告);有一个商城,我们新添加一个商品后,可能同时需要去更新缓存和数据库。

    四、路由模式

    原理:生产者将消息发送给交换机,消息携带具体的routingkey。交换机类型是direct,交换机匹配与之绑定的队列的routingkey,分发到不同的队列上。

    场景:还是一样,有一个商城,新添加了一个商品,实时性不是很高,只需要添加到数据库即可,不用刷新缓存。

    五、主题模式

    原理:路由模式的一种,交换机类型是topic,路由功能添加了模糊匹配。星号(*)代表1个单词,#号(#)代表一个或多个单词。

    场景:还是一样,有一个商城,新添加了一个商品,实时性不是很高,只需要添加到数据库即可,数据库包含了主数据库mysql1和从数据库mysql2的内容,不用刷新缓存。

    六、RPC

    1、首先客户端发送一个reply_to和corrention_id的请求,发布到RPC队列中;

    2、服务器端处理这个请求,并把处理结果发布到一个回调Queue,此Queue的名称应当与reply_to的名称一致

    3、客户端从回调Queue中得到先前corrention_id设定的值的处理结果。如果碰到和先前不一样的corrention_id的值,将会忽略而不是抛出异常。

    @$如何保证高可用的?RabbitMQ 的集群

    RabbitMQ 是比较有代表性的,因为是基于主从(非分布式)做高可用性的,我们就以 RabbitMQ 为例子讲解第一种 MQ 的高可用性怎么实现。RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。

    单机模式,就是 Demo 级别的,一般就是你本地启动了玩玩儿的?,没人生产用单机模式

    普通集群模式,意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你创建的 queue,只会放在一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个 queue 的读写操作。

    镜像集群模式:这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个 RabbitMQ 节点都有这个 queue 的一个完整镜像,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把消息同步到多个实例的 queue 上。RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。

    权限管理

    权限管理,一般指根据系统设置的安全规则或者安全策略,用户可以访问而且只能访问自己被授权的资源,不多不少。

    认证授权

    权限管理包括身份认证和授权两部分,简称认证授权。对于需要访问控制的资源用户首先经过身份认证,认证通过后用户具有该资源的访问权限方可访问。

    身份认证

    判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和密码,看其是否与系统中存储的该用户的用户名和密码一致,来判断用户身份是否正确。对于采用指纹等系统,则出示指纹;对于硬件Key等刷卡系统,则需要刷卡。

    认证关键对象

    授权

    授权,即访问控制,控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源,对于某些资源没有权限是无法访问的。

    授权关键对象

    授权可简单理解为 who 对 what(which) 进行 How 操作:

    权限模型

    主体、资源、权限的数据模型表示。

    权限控制基于角色的访问控制

    RBAC基于角色的访问控制(Role-Based Access Control)是以角色为中心进行访问控制,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等,访问控制流程如下:

    图中的判断逻辑代码可以理解为:

    if(主体.hasRole("总经理角色id")){
       查询工资
    }

    缺点:以角色进行访问控制粒度较粗,如果上图中查询工资所需要的角色变化为总经理和部门经理,此时就需要修改判断逻辑为“判断主体的角色是否是总经理或部门经理”,系统可扩展性差。

    修改代码如下:

    if(主体.hasRole("总经理角色id") ||  主体.hasRole("部门经理角色id")){
       查询工资
    }

    基于资源的访问控制

    RBAC基于资源的访问控制(Resource-Based Access Control)是以资源为中心进行访问控制,比如:主体必须具有查询工资权限才可以查询员工工资信息等,访问控制流程如下:

    上图中的判断逻辑代码可以理解为:

    if(主体.hasPermission("wage:query")){
       查询工资
    }

    优点:系统设计时定义好查询工资的权限标识,即使查询工资所需要的角色变化为总经理和部门经理也只需要将“查询工资信息权限”添加到“部门经理角色”的权限列表中,判断逻辑不用修改,系统可扩展性强。

    基于url的访问控制

    基于url拦截是企业中常用的权限管理方法,实现思路是:将系统操作的每个url配置在权限表中,将权限对应到角色,将角色分配给用户,用户访问系统功能通过Filter进行过虑,过虑器获取到用户访问的url,只要访问的url是用户分配角色中的url则放行继续访问。

    粗颗粒度和细颗粒度什么是粗颗粒度和细颗粒度

    对资源类型的管理称为粗颗粒度权限管理,即只控制到菜单、按钮、方法,粗粒度的例子比如:用户具有用户管理的权限,具有导出订单明细的权限。

    对资源实例的控制称为细颗粒度权限管理,即控制到数据级别的权限,比如:用户只允许修改本部门的员工信息,用户只允许导出自己创建的订单明细。

    如何实现粗颗粒度和细颗粒度

    Shiro

    Apache Shiro 是一个功能强大,使用简单的Java安全框架,它为开发人员提供一个直观而全面的认证,授权,加密及会话管理的解决方案。

    Shiro能做什么呢?

    Shiro基本功能

    Apache Shiro是一个全面的、蕴含丰富功能的安全框架。

    Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。

    还有其他的功能来支持和加强这些不同应用环境下安全领域的关注点。特别是对以下的功能支持:

    注意:Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供,然后通过相应的接口注入给Shiro即可。

    Shiro运行原理

    从应用程序角度的来观察如何使用Shiro完成工作

    我们需要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是权限验证

    也就是说对于我们而言,最简单的一个Shiro应用:

    从以上也可以看出,Shiro不提供维护用户权限,而是通过Realm让开发人员自己注入。

    Shiro内部架构

    shiro组件如下:

    Subject:主体,任何与应用交互的用户。

    SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 StrutsPreparedAndExcutorFilter。它是 Shiro 的核心,所有具体的交互都通过 SecurityManager 进行控制。它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。

    Authenticator:认证器,负责主体认证,用户可以自定义实现,自定义认证策略

    Authrizer:授权器,或者叫访问控制器。它用来决定主体是否有权限进行相应的操作

    Realm:可以有1个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的。

    SessionManager:会话管理器,管理session的生命周期(可以实现单点登录)

    SessionDAO:数据访问对象,用于会话的 CRUD。

    CacheManager:缓存管理器。它来管理如用户、角色、权限等的缓存的。

    Cryptography:密码模块,Shiro 提供了一些常见的加密组件用于如密码加密/解密的。

    通过上面的各个组件我们可以完成认证、授权、会话管理、加密/解密、记住我等安全相关功能。

    @$Shiro认证授权流程身份认证流程

    流程如下:

    首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils.setSecurityManager() 设置;

    SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;

    Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;

    Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;

    Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。

    授权流程

    流程如下:

    首先调用 Subject.isPermitted*/hasRole*

    接口,其会委托给 SecurityManager,而 SecurityManager 接着会委托给 Authorizer;

    Authorizer 是真正的授权者,如果我们调用如 isPermitted(“user:view”),其首先会通过 PermissionResolver 把字符串转换成相应的 Permission 实例;

    在进行授权之前,Authorizer 会调用相应的 Realm 获取 Subject 相应的角色/权限用于匹配传入的角色/权限,如果有多个 Realm,会委托给 ModularRealmAuthorizer 进行循环判断,如果匹配如 isPermitted*/hasRole*

    会返回 true,否则返回 false 表示授权失败。继承 AuthorizingRealm 而不是实现 Realm 接口;

    推荐继承 AuthorizingRealm,重写认证和授权方法:AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token):表示获取用户认证信息;AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals):表示根据用户身份获取授权信息。这种方式的好处是当只需要身份验证时只需要获取身份验证信息而不需要获取授权信息。

    @$Shiro和Spring Security比较

    shiro和spring security都是安全框架,都具有认证授权,加密等功能,主要区别如下:

    Shiro 的配置和使用比较简单,Spring Security 上手复杂些

    Spring Security 有更好的社区支持,社区资源相对比 Shiro 更加丰富

    Shiro 依赖性低,不需要任何框架和容器,可以独立运行。Spring Security 依赖Spring容器,与Spring整合更加方便

    Spring Security 功能比 Shiro 更加丰富些,例如安全维护方面

    OAuth2 协议什么是第三方登录

    很多网站登录时,允许使用第三方网站的身份来进行登录,这称为“第三方登录”。比如知乎和慕课网等,可以使用微信,QQ,或微博来进行登录。一个网站想接入第三方登录,需要用到OAuth2这个协议。

    什么是OAuth2

    OAuth2是一个关于授权的开放网络标准,用来授权第三方应用,获取用户的数据。其最终的目的是为了给第三方应用颁发一个有时效性的令牌access_token,第三方应用根据这个access_token就可以去获取用户的相关资源,如头像,昵称,email这些信息。现在大家用的基本是2.0的版本。

    协议流程

    在详细介绍OAuth2协议流程之前,先来简单了解几个角色,方便后续的理解。

    了解了上面这些角色之后jar找不到或无法加载主类,来看下OAuth2.0的运行流程是怎么样的。

     +--------+                               +---------------+
    |       |--(A)- Authorization Request ->|   Resource   |
    |       |                               |     Owner     |
    |       |<-(B)-- Authorization Grant ---|               |
    |       |                               +---------------+
    |       |
    |       |                               +---------------+
    |       |--(C)-- Authorization Grant -->| Authorization |
    | Client |                               |     Server   |
    |       |<-(D)----- Access Token -------|               |
    |       |                               +---------------+
    |       |
    |       |                               +---------------+
    |       |--(E)----- Access Token ------>|   Resource   |
    |       |                               |     Server   |
    |       |<-(F)--- Protected Resource ---|               |
    +--------+                               +---------------+

    (A). 客户端向用户(Resource Owner)发送一个授权请求

    (B). 用户同意给客户端(Client)授权,并返回一个授权码(code)

    (C). 客户端使用刚才的授权码(code)去向授权服务器(Authorization Server)授权

    (D). 授权服务器校验通过后,会给客户端发放令牌(Access Token)

    (E). 客户端拿着令牌(Access Token),去向资源服务器(Resource Server)申请获取资源

    (F). 资源服务器确认令牌之后,给客户端返回受保护的资源(Protected Resource)

    授权方式

    在OAuth2当中,定义了四种授权方式,针对不同的业务场景:

    授权码模式

    OAuth2授权码模式完整流程图:

    NettyNetty 是什么?

    Netty是一个异步事件驱动的网络应用程序框架,用于快速开发高性能通信的服务器和客户端。Netty是基于nio的,它封装了jdk的nio,让我们使用起来更加方便灵活。

    Netty 高性能表现在哪些方面?Netty 的优势有哪些?

    Netty的线程模型?

    Netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程池和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件,由对应的Handler处理。

    单线程模型:所有I/O操作都由一个线程完成,即多路复用、事件分发和处理都是在一个Reactor线程上完成的。既要接收客户端的连接请求,向服务端发起连接,又要发送/读取请求或应答/响应消息。一个NIO 线程同时处理成百上千的链路,性能上无法支撑,速度慢,若线程进入死循环,整个程序不可用,对于高负载、大并发的应用场景不合适。

    多线程模型:有一个NIO 线程(Acceptor) 只负责监听服务端,接收客户端的TCP 连接请求;NIO 线程池负责网络IO 的操作,即消息的读取、解码、编码和发送;1 个NIO 线程可以同时处理N 条链路,但是1 个链路只对应1 个NIO 线程,这是为了防止发生并发操作问题。但在并发百万客户端连接或需要安全认证时,一个Acceptor 线程可能会存在性能不足问题。

    主从多线程模型:Acceptor 线程用于绑定监听端口,接收客户端连接,将SocketChannel 从主线程池的Reactor 线程的多路复用器上移除,重新注册到Sub 线程池的线程上,用于处理I/O 的读写等操作,从而保证mainReactor只负责接入认证、握手等操作;

    TCP 粘包/拆包的原因及解决方法?

    TCP是以流的方式来处理数据,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送。

    TCP粘包/分包的原因

    应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现象;

    解决方法

    什么是 Netty 的零拷贝?

    Netty 的零拷贝主要包含三个方面:

    Netty常见使用场景

    Netty常见的使用场景如下:

    工作原理架构

    初始化并启动Netty服务端过程如下:

    public static void main(String[] args) {
       // 创建mainReactor
       NioEventLoopGroup boosGroup = new NioEventLoopGroup();
       // 创建工作线程组
       NioEventLoopGroup workerGroup = new NioEventLoopGroup();

       final ServerBootstrap serverBootstrap = new ServerBootstrap();
       serverBootstrap
           // 组装NioEventLoopGroup
          .group(boosGroup, workerGroup)
           // 设置channel类型为NIO类型
          .channel(NioServerSocketChannel.class)
           // 设置连接配置参数
          .option(ChannelOption.SO_BACKLOG, 1024)
          .childOption(ChannelOption.SO_KEEPALIVE, true)
          .childOption(ChannelOption.TCP_NODELAY, true)
           // 配置入站、出站事件handler
          .childHandler(new ChannelInitializer<NioSocketChannel>() {
               @Override
               protected void initChannel(NioSocketChannel ch) {
                   // 配置入站、出站事件channel
                   ch.pipeline().addLast(...);
                   ch.pipeline().addLast(...);
              }
          });

       // 绑定端口
       int port = 8080;
       serverBootstrap.bind(port).addListener(future -> {
           if (future.isSuccess()) {
               System.out.println(new Date() + ": 端口[" + port + "]绑定成功!");
          } else {
               System.err.println("端口[" + port + "]绑定失败!");
          }
      });
    }

    结合上面的介绍的Netty Reactor模型,介绍服务端Netty的工作架构图:

    ZookeeperZooKeeper 是什么?Zookeeper的用途,使用场景

    ZooKeeper 是一个开源的分布式协调服务。它是一个为分布式应用提供一致性服务的软件,分布式应用程序可以基于 Zookeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。

    ZooKeeper 的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

    zookeeper原理?选举的原理是什么?

    Zookeeper集群

    Zookeeper的角色

    Zookeeper工作原理

    Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。

    Zab协议有两种模式,它们分 别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和 leader的状态同步以后,恢复模式就结束了。恢复模式结束后,Zab进入广播模式,状态同步保证了leader和Server具有相同的系统状态。

    为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上 了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个 新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。

    每个Server在工作过程中有三种状态:

    leader选举原理

    半数通过

    当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的leader,让所有的 Server都恢复到一个正确的状态。

    Zk的选举算法有两种:一种是基于basic paxos实现的,另外一种是基于fast paxos算法实现的。系统默认的选举算法为fast paxos。

    Zookeeper Watcher 机制 -- 数据变更通知

    Zookeeper 允许客户端向服务端的某个 Znode 注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,服务端会向指定客户端发送一个事件通知来实现分布式的通知功能,然后客户端根据 Watcher 通知状态和事件类型做出业务上的改变。

    工作机制:

    (1)客户端注册 watcher

    (2)服务端处理 watcher

    (3)客户端回调 watcher

    Watcher 特性总结:

    (1)一次性

    无论是服务端还是客户端,一旦一个 Watcher 被 触 发 ,Zookeeper 都会将其从相应的存储中移除。这样的设计有效的减轻了服务端的压力,不然对于更新非常频繁的节点,服务端会不断的向客户端发送事件通知,无论对于网络还是服务端的压力都非常大。

    (2)客户端串行执行

    客户端 Watcher 回调的过程是一个串行同步的过程。

    (3)轻量

    3.1、Watcher 通知非常简单,只会告诉客户端发生了事件,而不会说明事件的具体内容。

    3.2、客户端向服务端注册 Watcher 的时候,并不会把客户端真实的 Watcher 对象实体传递到服务端,仅仅是在客户端请求中使用 boolean 类型属性进行了标记。

    (4)watcher event 异步发送 watcher 的通知事件从 server 发送到 client 是异步的,这就存在一个问题,不同的客户端和服务器之间通过 socket 进行通信,由于网络延迟或其他因素导致客户端在不通的时刻监听到事件,由于 Zookeeper 本身提供了 ordering guarantee,即客户端监听事件后,才会感知它所监视 znode发生了变化。所以我们使用 Zookeeper 不能期望能够监控到节点每次的变化。Zookeeper 只能保证最终的一致性,而无法保证强一致性。

    (5)注册 watcher getData、exists、getChildren

    (6)触发 watcher create、delete、setData

    (7)当一个客户端连接到一个新的服务器上时,watch 将会被以任意会话事件触发。当与一个服务器失去连接的时候,是无法接收到 watch 的。而当 client 重新连接时,如果需要的话,所有先前注册过的 watch,都会被重新注册。通常这是完全透明的。只有在一个特殊情况下,watch 可能会丢失:对于一个未创建的 znode的 exist watch,如果在客户端断开连接期间被创建了,并且随后在客户端连接上之前又删除了,这种情况下,这个 watch 事件可能会被丢失。

    客户端注册 Watcher 实现

    (1)调用 getData()/getChildren()/exist()三个 API,传入 Watcher 对象

    (2)标记请求 request,封装 Watcher 到 WatchRegistration

    (3)封装成 Packet 对象,发服务端发送 request

    (4)收到服务端响应后,将 Watcher 注册到 ZKWatcherManager 中进行管理

    (5)请求返回,完成注册。

    服务端处理 Watcher 实现

    (1)服务端接收 Watcher 并存储

    接收到客户端请求jar找不到或无法加载主类,处理请求判断是否需要注册 Watcher,需要的话将数据节点的节点路径和 ServerCnxn(ServerCnxn 代表一个客户端和服务端的连接,实现了 Watcher 的 process 接口,此时可以看成一个 Watcher 对象)存储在WatcherManager 的 WatchTable 和 watch2Paths 中去。

    (2)Watcher 触发

    以服务端接收到 setData() 事务请求触发 NodeDataChanged 事件为例:

    2.1 封装 WatchedEvent

    将通知状态(SyncConnected)、事件类型(NodeDataChanged)以及节点路径封装成一个 WatchedEvent 对象

    2.2 查询 Watcher

    从 WatchTable 中根据节点路径查找 Watcher

    2.3 没找到;说明没有客户端在该数据节点上注册过 Watcher

    2.4 找到;提取并从 WatchTable 和 Watch2Paths 中删除对应 Watcher(从这里可以看出 Watcher 在服务端是一次性的,触发一次就失效了)

    (3)调用 process 方法来触发 Watcher

    这里 process 主要就是通过 ServerCnxn 对应的 TCP 连接发送 Watcher 事件通知。

    zk 节点宕机如何处理?

    Zookeeper 本身也是集群,推荐配置不少于 3 个服务器。Zookeeper 自身也要保证当一个节点宕机时,其他节点会继续提供服务。

    如果是一个 Follower 宕机,还有 2 台服务器提供访问,因为 Zookeeper 上的数据是有多个副本的,数据并不会丢失;如果是一个 Leader 宕机,Zookeeper 会选举出新的 Leader。

    ZK 集群的机制是只要超过半数的节点正常,集群就能正常提供服务。只有在 ZK节点挂得太多,只剩一半或不到一半节点能工作,集群才失效。

    所以

    3 个节点的 cluster 可以挂掉 1 个节点(leader 可以得到 2 票>1.5)

    2 个节点的 cluster 就不能挂掉任何 1 个节点了(leader 可以得到 1 票

    版权声明

    本文仅代表作者观点。
    本文系作者授权发表,未经许可,不得转载。

    发表评论