网易Java后台开发实习一面

本文最后更新于 2024年8月9日 中午

网易一面(Java后端开发实习)

项目

你的RPC框架相较于市面上一些主流框架有什么优势呢

这个感觉好像没办法回答自己的rpc框架有多好,因为市面上的主流rpc框架都做得已经很成熟了感觉,我当时回答的是,这个项目我是逐步进行优化的,从最开始的基于socket通信和动态代理进行服务调用,后期引入netty框架提高通信性能,然后又将zookeeper作为注册中心,解耦服务之间的依赖性,还实现了多种序列化方式,多种负载均衡算法和多种限流算法,还引入了重试机制和熔断机制。整体做下来感觉学到了很多东西,了解一些rpc框架的设计思想。

为什么使用fastjosn和protobuf作为序列化框架,哪个更好一些

选取合适的序列化协议,要从性能、时间开销、空间开销、通用性、兼容性和安全性上综合进行考虑,以下对几种常见的序列化方式进行比较

Java原生的Serializable

优点

  • 实现较为简单
  • 兼容性高,可以方便地在Java应用内部进行对象持久化和传输。

缺点

  • 序列化后的数据较大,速度相对较慢
  • 不支持跨语言处理,仅适用于Java环境

json

优点

  • 可读性好:JSON数据以文本形式存在,易于人类阅读和编写,方便调试和日志记录。
  • 跨语言支持:几乎所有主流编程语言都提供了JSON的解析和生成库,使得JSON成为跨语言数据交换的理想选择。

缺点

  • 效率较低:相对于二进制序列化格式(如Protobuf和Hessian),JSON的解析和序列化效率较低,特别是在处理大型数据结构时。

Protobuf

优点

  • 高效:Protobuf使用二进制编码,相比JSON和XML等文本格式,序列化后的数据更小,解析速度更快。
  • 向前向后兼容:Protobuf支持数据结构的向前和向后兼容,可以在不破坏旧程序的情况下更新数据结构。

缺点

  • 可读性差:Protobuf序列化后的数据是二进制格式,不易于人类直接阅读。
  • 需要定义文件:使用Protobuf需要先定义数据结构(.proto文件),然后生成序列化/反序列化的代码。

Hessian

优点

  • 高效:Hessian是一个轻量级的remoting on http工具,提供了RMI的功能,采用二进制RPC协议,序列化效率高。
  • 简单易用:Hessian协议简单,实现起来相对容易。

缺点

  • 可读性差:Hessian序列化后的数据也是二进制格式,不易于人类直接阅读。
  • 安全性不足:Hessian传输没有加密处理,对于安全性要求高的应用可能不适用。
  • 生态系统支持:相对于JSON和Protobuf,Hessian的生态系统支持可能较少。

对于Rpc框架来说,使用Protobuf或者Hessian这种序列化后为二进制格式的数据,在消息传输上相比于Json,会更加高效

说一下粘包问题并描述你是怎么解决的使用netty有什么变化(相较于socket)

问题描述:netty默认底层通过TCP 进行传输,TCP是面向流的协议,接收方在接收到数据时无法直接得知一条消息的具体字节数,不知道数据的界限。由于TCP的流量控制机制,发生沾包或拆包,会导致接收的一个包可能会有多条消息或者不足一条消息,从而会出现接收方少读或者多读导致消息不能读完全的情况发生。

解决方法:在发送消息时,先告诉接收方消息的长度,让接收方读取指定长度的字节,就能避免这个问题;项目中通过自定义的消息传输协议来实现对沾包问题的解决。

使用netty框架来代替传统的socket通信的好处:

  • 大大提高通信性能,传统的socket基于BIO(同步阻塞)进行数据传输的,而netty基于NIO(同步非阻塞)进行数据传输
  • netty的功能较为强大,提供了对TCP、UDP、HTTP等多种协议的支持,并且很容易定制和扩展
  • netty提供了简单易用的API,而socket实现起来较为繁琐

说一下rpc和其他通信协议(比如说http有什么区别),分布式服务中为什么要用它呢

RPC(Remote Procedure Call,远程过程调用)与HTTP请求的区别主要体现在以下几个方面:

  1. 接口设计:RPC是一种面向过程的调用,接口设计更灵活,可以自定义参数、返回值等。而HTTP请求则遵循RESTful原则,接口设计更规范化,参数和返回值都有一定的格式要求。
  2. 传输协议:RPC通常使用自定义的传输协议,例如gRPC使用H2协议,Dubbo使用Dubbo协议。而HTTP请求则使用HTTP协议进行通信。
  3. 性能:由于RPC使用自定义的传输协议,相对于HTTP请求更加高效,能够提供更高的性能。
  4. 异步性:RPC支持异步调用,可以非阻塞地执行远程过程调用,提高系统的并发能力。而HTTP请求通常是同步的,阻塞等待服务器响应。
  5. 服务发现和注册:RPC通常使用注册中心实现服务的发现和注册,便于服务的动态部署和管理。而HTTP请求则没有这样的机制。

为什么我们需要RPC?主要有以下几个原因:

  1. 跨语言:RPC可以支持多种语言,不同语言编写的服务端和客户端可以通过RPC进行通信,提高了系统的可扩展性。
  2. 高性能:RPC相对于HTTP请求更加高效,能够提供更高的性能,适用于对性能要求较高的场景。
  3. 异步调用:RPC支持异步调用,可以提高系统的并发能力,适用于需要处理大量并发请求的场景。
  4. 服务治理:RPC通常使用注册中心实现服务的发现和注册,便于服务的动态部署和管理,提高了系统的可维护性。

总之,RPC相对于HTTP请求具有更高的性能、更好的可扩展性和可维护性,适用于需要高性能、跨语言、动态部署的分布式系统。

为什么采用Zookeeper作为注册中心,他相较于有什么优势

微服务技术栈:常见注册中心组件,对比分析 - 知了一笑 - 博客园 (cnblogs.com)

这个好像选择zookeeper真的不太合适🤣,面试的时候还问我为什么不选择Nacos,我答了一大堆zookeeper的比如具有强一致性,还有监听机制,,这个我万万没想到啊,Nacos更适合做注册中心

大致原因如下,社区活跃,经过大规模业务验证,不但可以作为微服务注册中心,也支持作RPC框架Dubbo的注册中心,且有完善的中文文档,总结下来就一句话:通用中间件,省时;文档详细,省心。

netty的零拷贝问题

传统零拷贝技术可看视频:Netty 什么是零拷贝机制 面试官必问_哔哩哔哩_bilibili

Netty零拷贝技术

Netty零拷贝技术具体实现细节请见文章:[16 IO 加速:与众不同的 Netty 零拷贝技术 (lianglianglee.com)](https://learn.lianglianglee.com/专栏/Netty 核心原理剖析与 RPC 实践-完/16 IO 加速:与众不同的 Netty 零拷贝技术.md)

MySQL的隔离级别是如何实现的

MySQL的隔离级别有:读未提交,读已提交,可重复读以及串行化

  • 对于读未提交,直接读取当前最新的数据就可以了
  • 读提交和可重复读,是通过Read View来实现的,区别在于Read View创建时机的不同,可以把它理解为一个数据快照。读提交隔离级别的是在每个语句执行前都会重新生成一个Read View,而可重复读隔离级别是在启动事务时生成一个Read View,然后整个事务期间都在用这个Read View
  • 串行化是通过加读写锁的方式来避免并行访问的

说一下Spring中的事务是怎么一回事

首先呢事务是逻辑上的一组操作,要么都执行,要么都不执行。

事务的四大特性(ACID)

  • 原子性Atomicity):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
  • 一致性Consistency):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
  • 隔离性Isolation):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
  • 持久性Durability):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

在事务中只有保证了事务的持久性、原子性和隔离性,一致性才能得到保障。也就是说 A、I、D 是手段,C 是目的!

Spring支持两种事务管理:编程式事务管理、声明式事务管理

编程式事务管理:通过 TransactionTemplate或者TransactionManager手动管理事务,实际应用中很少使用

声明式事务管理:推荐使用(代码侵入性最小),实际是通过 AOP 实现(基于@Transactional 的全注解方式使用最多)。

事务的属性主要有:隔离级别、传播行为、回滚规则、是否只读、事务超时。

@Transactional的常用配置参数总结

事务传播行为

事务传播行为是为了解决业务层方法之间互相调用的事务问题。

  • **PROPAGATION_REQUIRED**:如果外部方法没有开启事务的话,Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。如果外部方法开启事务并且被Propagation.REQUIRED的话,所有Propagation.REQUIRED修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务均回滚。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /*
    如果我们上面的aMethod()和bMethod()使用的都是PROPAGATION_REQUIRED传播行为的话,两者使用的就是同一个事务,只要其中一个方法回滚,整个事务均回滚。
    */
    @Service
    Class A {
    @Autowired
    B b;
    @Transactional(propagation = Propagation.REQUIRED)
    public void aMethod {
    //do something
    b.bMethod();
    }
    }
    @Service
    Class B {
    @Transactional(propagation = Propagation.REQUIRED)
    public void bMethod {
    //do something
    }
    }

  • **PROPAGATION_REQUIRES_NEW**:创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /*
    如果我们上面的bMethod()使用PROPAGATION_REQUIRES_NEW事务传播行为修饰,aMethod还是用PROPAGATION_REQUIRED修饰的话。如果aMethod()发生异常回滚,bMethod()不会跟着回滚,因为 bMethod()开启了独立的事务。但是,如果 bMethod()抛出了未被捕获的异常并且这个异常满足事务回滚规则的话,aMethod()同样也会回滚,因为这个异常被 aMethod()的事务管理机制检测到了。
    */
    @Service
    Class A {
    @Autowired
    B b;
    @Transactional(propagation = Propagation.REQUIRED)
    public void aMethod {
    //do something
    b.bMethod();
    }
    }

    @Service
    Class B {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void bMethod {
    //do something
    }
    }
  • **PROPAGATION_NESTED**:在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在。如果外部方法无事务,则单独开启一个事务,与 PROPAGATION_REQUIRED 类似。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    /*
    如果 bMethod() 回滚的话,aMethod()不会回滚。如果 aMethod() 回滚的话,bMethod()会回滚。
    */
    @Service
    Class A {
    @Autowired
    B b;
    @Transactional(propagation = Propagation.REQUIRED)
    public void aMethod {
    //do something
    b.bMethod();
    }
    }

    @Service
    Class B {
    @Transactional(propagation = Propagation.NESTED)
    public void bMethod {
    //do something
    }
    }

事务隔离属性

  • TransactionDefinition.ISOLATION_DEFAULT :使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别。
  • TransactionDefinition.ISOLATION_READ_UNCOMMITTED :最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
  • TransactionDefinition.ISOLATION_READ_COMMITTED : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生
  • TransactionDefinition.ISOLATION_REPEATABLE_READ : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • TransactionDefinition.ISOLATION_SERIALIZABLE : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。

事务超时属性

所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为-1,这表示事务的超时时间取决于底层事务系统或者没有超时时间。

事务只读属性

对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。

事务回滚规则

这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚。

了解Java中的代理模式吗

代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些自定义的操作。

简单来说就是 我们使用代理对象来代替对真实对象(real object)的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

静态代理

静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(比如接口一旦新增加方法,目标对象和代理对象都要进行修改)且麻烦(需要对每个目标类都单独写一个代理类)。静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。

静态代理实现的步骤

  • 定义一个接口及其实现类;
  • 创建一个代理类同样实现这个接口
  • 将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。

动态代理

相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类。动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

Java中动态代理主要有两种:JDK动态代理和CGLIB动态代理

JDK动态代理实现的步骤:

  • 定义一个接口及其实现类;
  • 自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;
  • 通过 Proxy.newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h) 方法创建代理对象;

JDK 动态代理有一个最致命的问题是其只能代理实现了接口的类。

CGLIB动态代理实现步骤

  • 定义一个类;
  • 自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;
  • 通过 Enhancer 类的 create()创建代理类;

JDK动态代理 VS CGLIB动态代理

  • JDK 动态代理只能代理实现了接口的类或者直接代理接口,而 CGLIB 可以代理未实现任何接口的类。 另外, CGLIB 通过继承方式实现代理,CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
  • 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。

md5加密算法可靠吗?为什么?

即使是最安全 MD 算法 MD5 也存在被破解的风险,攻击者可以通过暴力破解或彩虹表攻击等方式,找到与原始数据相同的哈希值,从而破解数据。为了增加破解难度,通常可以选择加盐。盐(Salt)在密码学中,是指通过在密码任意固定位置插入特定的字符串,让哈希后的结果和使用原始密码的哈希结果不相符,这种过程称之为“加盐”。加盐之后就安全了吗?并不一定,这只是增加了破解难度,不代表无法破解。

kafka的消息丢失了怎么办

生产者丢失消息的情况

生产者(Producer) 调用send方法发送消息之后,消息可能因为网络问题并没有发送过去。

所以,我们不能默认在调用send方法发送消息之后消息发送成功了。为了确定消息是发送成功,我们要判断消息发送的结果。但是要注意的是 Kafka 生产者(Producer) 使用 send 方法发送消息实际上是异步的操作,我们可以通过 get()方法获取调用结果,但是这样也让它变为了同步操作,但是一般不推荐这么做!可以采用为其添加回调函数的形式,如果消息发送失败的话,我们检查失败的原因之后重新发送即可!

消费者丢失消息的情况

我们知道消息在被追加到 Partition(分区)的时候都会分配一个特定的偏移量(offset)。偏移量(offset)表示 Consumer 当前消费到的 Partition(分区)的所在的位置。Kafka 通过偏移量(offset)可以保证消息在分区内的顺序性。当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际上并没有被消费,但是 offset 却被自动提交了。解决办法也比较粗暴,我们手动关闭自动提交 offset,每次在真正消费完消息之后再自己手动提交 offset 。

Kafka弄丢了消息

我们知道 Kafka 为分区(Partition)引入了多副本(Replica)机制。分区(Partition)中的多个副本之间会有一个叫做 leader 的家伙,其他副本称为 follower。我们发送的消息会被发送到 leader 副本,然后 follower 副本才能从 leader 副本中拉取消息进行同步。生产者和消费者只与 leader 副本交互。你可以理解为其他副本只是 leader 副本的拷贝,它们的存在只是为了保证消息存储的安全性。

试想一种情况:假如 leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选出一个 leader ,但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消息丢失。

  • 设置 acks = all:acks 的默认值即为 1,代表我们的消息被 leader 副本接收之后就算被成功发送。当我们配置 acks = all 表示只有所有 ISR 列表的副本全部收到消息时,生产者才会接收到来自服务器的响应. 这种模式是最高级别的,也是最安全的,可以确保不止一个 Broker 接收到了消息. 该模式的延迟会很高。
  • 设置 replication.factor >= 3:为了保证 leader 副本能有 follower 副本能同步消息,我们一般会为 topic 设置 replication.factor >= 3。这样就可以保证每个 分区(partition) 至少有 3 个副本。虽然造成了数据冗余,但是带来了数据的安全性。
  • 设置 min.insync.replicas > 1:这样配置代表消息至少要被写入到 2 个副本才算是被成功发送。min.insync.replicas 的默认值为 1 ,在实际生产中应尽量避免默认值 1。
  • 设置 unclean.leader.election.enable = false:当 leader 副本发生故障时就不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了消息丢失的可能性。

如何保证redis和数据库中的数据一致性

使用旁路缓存模式:更新数据库,然后直接删除缓存。

缓存删除失败的解决方案

说一下你的登录逻辑

用户首次登录时,输入账号,密码和验证码,首先前端会校验账号是否合法,比如一些非法字符呀等等,如果合法,数据传送到后端对存在session中的验证码进行检查,如果正确接着通过账号在数据库中查找用户信息,如果能找到,校验密码的正确性,将用户输入的密码加盐并进行md5加密后跟数据库中的密码进行对比,如果正确,则登录成功,同时会生成一个登录凭证,如果用户勾选了记住我选项,则设置凭证有效实现为15天,否则为12小时,并将ticket的标识存放到cookie中,用户下次再登录,会首先拿到cookie中的ticket标识,然后通过标识在数据库中找到ticket,并将当前时间与过期时间做对比,如果不超时,则自动登录,,否则需要重新输入登录信息进行登录。

登录逻辑中,如何防止被其他用户窃取

CSRF攻击定义:假设浏览器已经登陆过,服务器给浏览器发送的ticket也被浏览器存到了cookie中,这时浏览器又向服务器发送了一个get请求,这个请求用于打开一个带有表单的页面,这时浏览器本应该去填这个页面然后提交,但是浏览器并没有而是去访问了另一个X网站,X网站窃取了用户的cookie,X网站就可以模仿用户去访问服务器并对表单进行提交。简而言之,CSRF攻击就是某网站通过窃取用户cookie中的凭证模拟用户身份去访问服务器,并利用表单去向服务器提交数据。

解决方案

你该如何从0设计一个本地缓存组件,说一下大概框架思路

需要考虑的点

  • 数据结构:数据该用什么类型的结构进行存储
  • 对象上限:一般指定为1024,达到上限后进行缓存淘汰
  • 缓存策略
  • 过期时间:过期之后如何进行删除
  • 线程安全问题如何保证
  • 持久化机制
  • 阻塞机制

教你设计一个超牛逼的本地缓存!-腾讯云开发者社区-腾讯云 (tencent.com)

算法

跳跃得分

解题思路:动态规划+单调队列

1696. 跳跃游戏 VI - 力扣(LeetCode)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public int maxResult(int[] nums, int k) {
int n = nums.length;
int[] f = new int[n];
f[0] = nums[0];
Deque<Integer> q = new ArrayDeque<>();
q.add(0);
for(int i = 1; i < n; ++i) {
//出
if(q.peekFirst() < i - k)
q.pollFirst();
//转移
f[i] = f[q.peekFirst()] + nums[i];
//入
while(!q.isEmpty() && f[i] >= f[q.peekLast()])
q.pollLast();
q.add(i);
}
return f[n - 1];
}
}

网易Java后台开发实习一面
https://love-enough.github.io/2024/08/07/网易Java后台开发实习一面/
作者
GuoZihan
发布于
2024年8月7日
更新于
2024年8月9日
许可协议