- 👉《图解网络》 :500 张图 + 15 万字贯穿计算机网络重点知识,如HTTP、HTTPS、TCP、UDP、IP等协议
- 👉《图解系统》:400 张图 + 16 万字贯穿操作系统重点知识,如进程管理、内存管理、文件系统、网络系统等
- 👉《图解MySQL》:重点突击 MySQL 索引、存储引擎、事务、MVCC、锁、日志等面试高频知识
- 👉《图解Redis》:重点突击 Redis 数据结构、持久化、缓存淘汰、高可用、缓存数据一致性等面试高频知识
- 👉《Java后端面试题》:涵盖Java基础、Java并发、Java虚拟机、Spring、MySQL、Redis、计算机网络等企业面试题
- 👉《大厂真实面经》:涵盖互联网大厂、互联网中厂、手机厂、通信厂、新能源汽车厂、银行等企业真实面试题
大家好,我是小林。
哈啰出行 25 届开发岗的校招薪资如下,年薪在 25-35w 之间,属于互联网中厂级别的薪资范围:
- 18k x 14.5 = 26.1w,同学背景本科双一流,办公地点上海
- 21k x 14.5 = 30w,同学背景硕士,办公地点杭州
- 24k x 14.5 = 34.8w,同学背景本科 985,办公地点上海
哈啰出行的公积金是按 7%缴纳,训练营也有同学拿到哈啰 offer,开的是 21k,手上还有一个小厂 25k 的,但是考虑到哈啰出行的平台更大一些,选择了去了哈啰出行。
那哈啰的面试难度如何呢?
之前有个社招同学面哈啰出行反馈说,面试难度不算难,问的都是偏基础的内容,但是最后有算法题,平时算法练的少,最后没做出来,比较可惜。
这次带大家来看看,哈啰出行 Java 后端开发校招面经,面试难度跟互联网大厂差距不算很大,这次的面经面了 50 分钟,技术方面从Java、MySQL、Redis、中间件、网络进行的拷打,最后还有一个算法手撕环节。
大家觉得哈啰出行面试难度如何?
Java
JVM内存区域介绍一下?
根据 JDK 8 规范,JVM 运行时内存共分为虚拟机栈、堆、元空间、程序计数器、本地方法栈五个部分。还有一部分内存叫直接内存,属于操作系统的本地内存,也是可以直接操作的。
JVM的内存结构主要分为以下几个部分:
- 元空间:元空间的本质和永久代类似,都是对 JVM 规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
- Java 虚拟机栈:每个线程有一个私有的栈,随着线程的创建而创建。栈里面存着的是一种叫 “栈帧” 的东西,每个方法会创建一个栈帧,栈帧中存放了局部变量表(基本数据类型和对象引用)、操作数栈、方法出口等信息。栈的大小可以固定也可以动态扩展。
- 本地方法栈:与虚拟机栈类似,区别是虚拟机栈执行 Java 方法,本地方法栈执行 native 方法。在虚拟机规范中对本地方法栈中方法使用的语言、使用方法与数据结构没有强制规定,因此虚拟机可以自由实现它。
- 程序计数器:程序计数器可以看成是当前线程所执行的字节码的行号指示器。在任何一个确定的时刻,一个处理器(对于多内核来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器,我们称这类内存区域为 “线程私有” 内存。
- 堆内存:堆内存是 JVM 所有线程共享的部分,在虚拟机启动的时候就已经创建。所有的对象实例和数组都在堆上分配,这部分空间可通过 GC 进行回收。当申请不到空间时会抛出
OutOfMemoryError
。堆是 JVM 内存占用最大、管理最复杂的一个区域。JDK 1.8 后,字符串常量池和运行时常量池从永久代中剥离出来,存放在堆中。 - 直接内存:直接内存并不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。在 JDK 1.4 中新加入了 NIO 类,引入了一种基于通道 (Channel) 与缓冲区(Buffer)的 I/O 方式,它可以使用 native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆中的
DirectByteBuffer
对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
垃圾回收算法和机制介绍一下
- 标记-清除算法:标记-清除算法分为“标记”和“清除”两个阶段,首先通过可达性分析,标记出所有需要回收的对象,然后统一回收所有被标记的对象。标记-清除算法有两个缺陷,一个是效率问题,标记和清除的过程效率都不高,另外一个就是,清除结束后会造成大量的碎片空间。有可能会造成在申请大块内存的时候因为没有足够的连续空间导致再次 GC。
- 复制算法:为了解决碎片空间的问题,出现了“复制算法”。复制算法的原理是,将内存分成两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上。然后将然后再把已使用的内存整个清理掉。复制算法解决了空间碎片的问题。但是也带来了新的问题。因为每次在申请内存时,都只能使用一半的内存空间。内存利用率严重不足。
- 标记-整理算法:复制算法在 GC 之后存活对象较少的情况下效率比较高,但如果存活对象比较多时,会执行较多的复制操作,效率就会下降。而老年代的对象在 GC 之后的存活率就比较高,所以就有人提出了“标记-整理算法”。标记-整理算法的“标记”过程与“标记-清除算法”的标记过程一致,但标记之后不会直接清理。而是将所有存活对象都移动到内存的一端。移动结束后直接清理掉剩余部分。
- 分代回收算法:分代收集是将内存划分成了新生代和老年代。分配的依据是对象的生存周期,或者说经历过的 GC 次数。对象创建时,一般在新生代申请内存,当经历一次 GC 之后如果对还存活,那么对象的年龄 +1。当年龄超过一定值(默认是 15,可以通过参数
-XX:MaxTenuringThreshold
来设定)后,如果对象还存活,那么该对象会进入老年代。
新生代和老年代的区别是什么?
在基于分代收集算法的垃圾回收机制里,Java 堆内存被划分为新生代和老年代,它们在对象特性、内存大小、垃圾回收算法、回收频率等方面存在明显区别:
- 对象特性的区别:新生代:大多数新创建的对象会被分配到新生代。这些对象的生命周期通常较短,很多对象在创建后不久就不再被使用,成为垃圾对象等待回收。例如在一个 Web 应用中,每次处理请求时创建的临时对象、方法调用时创建的局部变量对象等,大多都存于新生代。老年代:存储的是经过多次垃圾回收仍然存活的对象。这些对象的生命周期较长,可能会在整个应用的运行过程中一直存在。比如数据库连接池对象、缓存对象等,它们会在系统中长期驻留,最终会被转移到老年代。
- 垃圾回收算法的区别:新生代:主要使用复制算法进行垃圾回收。将新生代内存分为一个较大的 Eden 区和两个较小的 Survivor 区(通常比例为 8:1:1 )。新对象优先分配在 Eden 区,当 Eden 区满时,会触发 Minor GC(新生代垃圾回收),将 Eden 区和一个 Survivor 区中存活的对象复制到另一个 Survivor 区,然后清空 Eden 区和之前使用的 Survivor 区。经过多次 Minor GC 后仍然存活的对象会被晋升到老年代。老年代:一般采用标记 - 清除算法或者标记 - 整理算法。标记 - 清除算法先标记出存活对象,然后清除未标记的对象,但会产生内存碎片;标记 - 整理算法在标记存活对象后,将存活对象移动到一端,然后清理掉边界以外的内存,避免了内存碎片问题,但移动对象会带来一定的性能开销。老年代的垃圾回收称为 Major GC 或 Full GC,通常比 Minor GC 耗时更长。
- 垃圾回收频率的区别:新生代:由于新生代中的对象大多生命周期较短,很快就会成为垃圾对象,所以新生代的垃圾回收(Minor GC)频率较高。Minor GC 的速度相对较快,因为需要复制的存活对象较少。老年代:老年代中的对象生命周期长,垃圾对象相对较少,因此老年代的垃圾回收(Major GC 或 Full GC)频率较低。但一旦触发老年代的垃圾回收,由于老年代内存空间大、对象数量多,回收过程会比较耗时,对系统性能的影响也更大。
常见的线程池类型及其好处说一下?
ScheduledThreadPool
:可以设置定期的执行任务,它支持定时或周期性执行任务,比如每隔 10 秒钟执行一次任务,我通过这个实现类设置定期执行任务的策略。FixedThreadPool
:它的核心线程数和最大线程数是一样的,所以可以把它看作是固定线程数的线程池,它的特点是线程池中的线程数除了初始阶段需要从 0 开始增加外,之后的线程数量就是固定的,就算任务数超过线程数,线程池也不会再创建更多的线程来处理任务,而是会把超出线程处理能力的任务放到任务队列中进行等待。而且就算任务队列满了,到了本该继续增加线程数的时候,由于它的最大线程数和核心线程数是一样的,所以也无法再增加新的线程了。CachedThreadPool
:可以称作可缓存线程池,它的特点在于线程数是几乎可以无限增加的(实际最大可以达到Integer.MAX_VALUE
,为 2^31-1,这个数非常大,所以基本不可能达到),而当线程闲置时还可以对线程进行回收。也就是说该线程池的线程数量不是固定不变的,当然它也有一个用于存储提交任务的队列,但这个队列是 SynchronousQueue,队列的容量为0,实际不存储任何任务,它只负责对任务进行中转和传递,所以效率比较高。SingleThreadExecutor
:它会使用唯一的线程去执行任务,原理和 FixedThreadPool 是一样的,只不过这里线程只有一个,如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。这种线程池由于只有一个线程,所以非常适合用于所有任务都需要按被提交的顺序依次执行的场景,而前几种线程池不一定能够保障任务的执行顺序等于被提交的顺序,因为它们是多线程并行执行的。SingleThreadScheduledExecutor
:它实际和 ScheduledThreadPool 线程池非常相似,它只是 ScheduledThreadPool 的一个特例,内部只有一个线程。
IOC和AOP的概念说一下?
Spring IoC和AOP 区别:
- IoC:即控制反转的意思,它是一种创建和获取对象的技术思想,依赖注入(DI)是实现这种技术的一种方式。传统开发过程中,我们需要通过new关键字来创建对象。使用IoC思想开发方式的话,我们不通过new关键字创建对象,而是通过IoC容器来帮我们实例化对象。 通过IoC的方式,可以大大降低对象之间的耦合度。
- AOP:是面向切面编程,能够将那些与业务无关,却为业务模块所共同调用的逻辑封装起来,以减少系统的重复代码,降低模块间的耦合度。Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理。
在 Spring 框架中,IOC 和 AOP 结合使用,可以更好地实现代码的模块化和分层管理。例如:
- 通过 IOC 容器管理对象的依赖关系,然后通过 AOP 将横切关注点统一切入到需要的业务逻辑中。
- 使用 IOC 容器管理 Service 层和 DAO 层的依赖关系,然后通过 AOP 在 Service 层实现事务管理、日志记录等横切功能,使得业务逻辑更加清晰和可维护。
是否使用过AOP,具体怎么用的?
我在实际项目里使用过 AOP,它在处理日志记录、事务管理这类横切关注点时非常实用。使用的步骤如下:
- 添加依赖:如果是 Maven 项目,要在
pom.xml
里添加 Spring AOP 和 AspectJ 的依赖,让项目能支持 AOP 功能。 - 定义切面类:创建一个类,用
@Aspect
注解标记它为切面,用@Component
注解把它交给 Spring 容器管理。在类中,通过@Pointcut
定义切入点,比如execution(* com.example.service.*.*(..))
可以匹配指定包下所有类的所有方法;再用@Before
、@After
等注解定义通知,这些通知会在切入点方法执行前后执行相应逻辑。
@Aspect
@Component
public class LoggingAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
@Before("serviceMethods()")
public void beforeAdvice(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature().getName());
}
@After("serviceMethods()")
public void afterAdvice(JoinPoint joinPoint) {
System.out.println("After method: " + joinPoint.getSignature().getName());
}
}
- 启用 AOP 自动代理:在 Spring Boot 项目中默认已启用;传统 Spring 项目可在配置文件加
<aop:aspectj-autoproxy/>
,或者用 Java 配置类加@EnableAspectJAutoProxy
注解。 - 编写业务逻辑类:创建正常的业务类和方法,这些方法会被 AOP 增强。
@Service
public class UserService {
public void createUser(String username) {
System.out.println("Creating user: " + username);
}
}
MySQL
MySQL InnoDB的数据结构是什么?
MySQL InnoDB 引擎是用了B+树作为了索引的数据结构。
B+Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。
主键索引的 B+Tree 如图所示:
比如,我们执行了下面这条查询语句:
select * from product where id= 5;
这条语句使用了主键索引查询 id 号为 5 的商品。查询过程是这样的,B+Tree 会自顶向下逐层进行查找:
- 将 5 与根节点的索引数据 (1,10,20) 比较,5 在 1 和 10 之间,所以根据 B+Tree的搜索逻辑,找到第二层的索引数据 (1,4,7);
- 在第二层的索引数据 (1,4,7)中进行查找,因为 5 在 4 和 7 之间,所以找到第三层的索引数据(4,5,6);
- 在叶子节点的索引数据(4,5,6)中进行查找,然后我们找到了索引值为 5 的行数据。
数据库的索引和数据都是存储在硬盘的,我们可以把读取一个节点当作一次磁盘 I/O 操作。那么上面的整个查询过程一共经历了 3 个节点,也就是进行了 3 次 I/O 操作。
B+Tree 存储千万级的数据只需要 3-4 层高度就可以满足,这意味着从千万级的表查询目标数据最多需要 3-4 次磁盘 I/O,所以B+Tree 相比于 B 树和二叉树来说,最大的优势在于查询效率很高,因为即使在数据量很大的情况,查询一个数据的磁盘 I/O 依然维持在 3-4次。
表锁、行锁和读写锁的区别是什么?
在 MySQL 里,根据加锁的范围,可以分为全局锁、表级锁和行锁三类。
-
全局锁:通过flush tables with read lock 语句会将整个数据库就处于只读状态了,这时其他线程执行以下操作,增删改或者表结构修改都会阻塞。全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
-
表级锁:MySQL 里面表级别的锁有这几种:
-
表锁:通过lock tables 语句可以对表加表锁,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。
-
元数据锁:当我们对数据库表进行操作时,会自动给这个表加上 MDL,对一张表进行 CRUD 操作时,加的是 MDL 读锁;对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
-
意向锁:当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。意向锁的目的是为了快速判断表里是否有记录被加锁。
-
行级锁:InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。
- 记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的,满足读写互斥,写写互斥
- 间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
- Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
InnoDB和MyISAM的区别?
- 事务:InnoDB 支持事务,MyISAM 不支持事务,这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一。
- 索引结构:InnoDB 是聚簇索引,MyISAM 是非聚簇索引。聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚簇索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
- 锁粒度:InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。
- count 的效率:InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快。
如何判断一个SQL语句是否是慢SQL?
可以通过开启慢查询日志,比如把慢查询的时间阈值设置为 2 秒,那么执行 sql 耗时超过 2 秒的 sql 就会被记录到慢查询日志,我们就可以查看慢查询日志,来找到耗时超过 2 秒的慢 sql。
然后再通过 explian 执行计划来分析慢 sql,重点关注执行计划中的以下信息:
- 索引使用情况:如果执行计划中没有使用索引或者使用了不合适的索引,可能会导致 SQL 语句执行缓慢。
- 扫描行数:扫描行数过多会增加查询的时间成本,如果执行计划中显示扫描了大量的行数,需要考虑优化查询条件或添加合适的索引。
- 连接类型:不同的连接类型对性能的影响不同,例如全表扫描的连接类型性能较差,应尽量避免。
Redis
Redis常用的数据结构有哪些?
Redis 提供了丰富的数据类型,常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
随着 Redis 版本的更新,后面又支持了四种数据类型:BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。Redis 五种数据类型的应用场景:
- String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
- List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
- Hash 类型:缓存对象、购物车等。
- Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
- Zset 类型:排序场景,比如排行榜、电话和姓名排序等。
Redis 后续版本又支持四种数据类型,它们的应用场景如下:
- BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
- HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
- GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;
- Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。
如何使用Redis结合token实现用户登录?
实现步骤:
- 用户登录验证:当用户提交登录请求时,服务器会对用户输入的用户名和密码进行验证,检查其是否与数据库中存储的信息匹配。
- 生成 Token:如果用户验证通过,服务器会为该用户生成一个唯一的 Token。Token 可以是一个随机字符串,例如使用 UUID 生成,它将作为用户在后续请求中的身份标识。
- 将 Token 存储到 Redis:把生成的 Token 与用户信息(如用户 ID)关联起来,并存储到 Redis 中,同时为 Token 设置一个过期时间,以确保登录状态的时效性。
- 返回 Token 给客户端。服务器将生成的 Token 返回给客户端,客户端在后续的请求中需要携带该 Token。
- 验证 Token:客户端在发送请求时,会将 Token 包含在请求头或请求参数中。服务器接收到请求后,会从 Redis 中查找该 Token 是否存在且未过期。若存在且未过期,则认为用户已登录,允许其访问受保护的资源;否则,要求用户重新登录。
如何用分布式锁保证每人只能领取一张优惠券?
- 加锁:在用户尝试领取优惠券时,以用户 ID 作为锁的键,使用 Redis 的
SETNX
命令尝试获取锁。如果返回1
,表示成功获取锁,用户可以继续进行优惠券领取操作;如果返回0
,表示锁已被其他线程持有,该用户不能领取优惠券。为了避免锁被永久持有,需要为锁设置一个过期时间,防止出现死锁,可以在SETNX
命令中加上 PX 选项。
// lock_key 就是 key 键;
// unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作;
// NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
// PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。
SET lock_key unique_value NX PX 10000
- 解锁:在用户完成优惠券领取操作后,需要释放锁。可以使用
DEL
命令删除锁的键,但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性,这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
中间件
RabbitMQ的特性你知道哪些?
RabbitMQ 以 可靠性、灵活性 和 易扩展性 为核心优势,适合需要稳定消息传递的复杂系统。其丰富的插件和协议支持使其在微服务、IoT、金融等领域广泛应用,比较核心的特性有如下:
- 持久化机制:RabbitMQ 支持消息、队列和交换器的持久化。当启用持久化时,消息会被写入磁盘,即使 RabbitMQ 服务器重启,消息也不会丢失。例如,在声明队列时可以设置
durable
参数为true
来实现队列的持久化:
import pika
connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
channel = connection.channel()
# 声明一个持久化队列
channel.queue_declare(queue='durable_queue', durable=True)
- 消息确认机制:提供了生产者确认和消费者确认机制。生产者可以设置
confirm
模式,当消息成功到达 RabbitMQ 服务器时,会收到确认消息;消费者在处理完消息后,可以向 RabbitMQ 发送确认信号,告知服务器该消息已被成功处理,服务器才会将消息从队列中删除。 - 镜像队列:支持创建镜像队列,将队列的内容复制到多个节点上,提高消息的可用性和可靠性。当一个节点出现故障时,其他节点仍然可以提供服务,确保消息不会丢失。
- 多种交换器类型:RabbitMQ 提供了多种类型的交换器,如直连交换器(Direct Exchange)、扇形交换器(Fanout Exchange)、主题交换器(Topic Exchange)和头部交换器(Headers Exchange)。不同类型的交换器根据不同的规则将消息路由到队列中。例如,扇形交换器会将接收到的消息广播到所有绑定的队列中;主题交换器则根据消息的路由键和绑定键的匹配规则进行路由。
RPC的概念是什么?
RPC 即远程过程调用,允许程序调用运行在另一台计算机上的程序中的过程或函数,就像调用本地程序中的过程或函数一样,而无需了解底层网络细节。
一个典型的 RPC 调用过程通常包含以下几个步骤:
- 客户端调用:客户端程序调用本地的一个 “伪函数”(也称为存根,Stub),并传入所需的参数。这个 “伪函数” 看起来和普通的本地函数一样,但实际上它会负责处理远程调用的相关事宜。
- 请求发送:客户端存根将调用信息(包括函数名、参数等)进行序列化,然后通过网络将请求发送到服务器端。
- 服务器接收与处理:服务器端接收到请求后,将请求信息进行反序列化,然后找到对应的函数并执行。
- 结果返回:服务器端将函数的执行结果进行序列化,通过网络发送回客户端。
- 客户端接收结果:客户端接收到服务器返回的结果后,将其反序列化,并将结果返回给调用者。
常见的 RPC 框架:
- gRPC:由 Google 开发的高性能、开源的 RPC 框架,支持多种编程语言,使用 Protocol Buffers 作为序列化协议,具有高效、灵活等特点。
- Thrift:由 Facebook 开发的跨语言的 RPC 框架,支持多种数据传输协议和序列化格式,具有良好的可扩展性和性能。
- Dubbo:阿里巴巴开源的高性能 Java RPC 框架,提供了服务治理、集群容错、负载均衡等功能,广泛应用于国内的互联网企业。
网络
HTTP状态码的含义你知道哪些?
HTTP 状态码分为 5 大类
- 1xx 类状态码属于提示信息,是协议处理中的一种中间状态,实际用到的比较少。
- 2xx 类状态码表示服务器成功处理了客户端的请求,也是我们最愿意看到的状态。
- 3xx 类状态码表示客户端请求的资源发生了变动,需要客户端用新的 URL 重新发送请求获取资源,也就是重定向。
- 4xx 类状态码表示客户端发送的报文有误,服务器无法处理,也就是错误码的含义。
- 5xx 类状态码表示客户端请求报文正确,但是服务器处理时内部发生了错误,属于服务器端的错误码。
其中常见的具体状态码有:
- 200:请求成功;
- 301:永久重定向;302:临时重定向;
- 404:无法找到此页面;405:请求的方法类型不支持;
- 500:服务器内部出错。
RESTful API的设计原则是什么?
RESTful API 设计规范是指设计和开发 RESTful API 时应遵循的一些规范和准则。下面介绍一些常见的设计规范:
1、使用 HTTP 动词来表达操作
RESTful API 中的操作应该使用 HTTP 动词来表达,例如 GET、POST、PUT、DELETE 等,以确保对资源的操作被明确表示和限制。如下所示:
GET /users/1
POST /users
PUT /users/1
DELETE /users/1
2、使用名词来表示资源
RESTful API 中应该使用名词来表示资源,而不是动词,以避免歧义和混淆。例如:
GET /users/1
GET /orders/1
3、使用 URI 来定位资源
RESTful API 应该使用 URI 来定位资源,以确保每个资源都有一个唯一的标识符。URI 应该具有层级结构,以便表示资源之间的关系。例如:
GET /users/1/orders/1
4、使用查询参数来过滤和分页
RESTful API 应该使用查询参数来过滤和分页资源,例如:
GET /users?gender=male
GET /users?page=1&pageSize=10
5、使用 HTTP 状态码来表示请求结果
RESTful API 应该使用 HTTP 状态码来表示请求结果,以便客户端能够根据状态码进行处理。例如:
- 200:请求成功
- 201:资源创建成功
- 400:请求参数错误
- 401:未授权访问
- 403:表示禁止访问资源。
- 404:表示未找到资源。
- 500:表示服务器内部错误。
6、使用 JSON 或 XML 来表示数据
RESTful API 应该使用 JSON 或 XML 来表示数据,以便不同的客户端能够方便地进行数据解析和处理。例如:
GET /users/1
{
"id": 1,
"name": "Tom",
"age": 25
}
7、使用版本号来管理 API
RESTful API 应该使用版本号来管理 API 的不同版本,以便支持旧版 API 的兼容性和平稳升级。例如:
GET /v1/users/1
8、使用 HATEOAS 来提高 API 的可发现性
HATEOAS(Hypermedia As The Engine Of Application State)是指使用超媒体作为应用程序状态的引擎,从而提高 RESTful API 的可发现性。通过使用 HATEOAS,客户端可以通过 API 返回的链接自主地遍历 API,并进行资源的操作。
例如:
GET /users/1
{
"id": 1,
"name": "Tom",
"age": 25,
"links": [
{
"rel": "orders",
"href": "/users/1/orders"
},
{
"rel": "edit",
"href": "/users/1/edit"
}
]
}
上述代码中的 links 字段包含了与当前资源相关的链接,客户端可以通过这些链接来访问其他资源。
算法
- 求数组的最大连续和。
- 👉《图解网络》 :500 张图 + 15 万字贯穿计算机网络重点知识,如HTTP、HTTPS、TCP、UDP、IP等协议
- 👉《图解系统》:400 张图 + 16 万字贯穿操作系统重点知识,如进程管理、内存管理、文件系统、网络系统等
- 👉《图解MySQL》:重点突击 MySQL 索引、存储引擎、事务、MVCC、锁、日志等面试高频知识
- 👉《图解Redis》:重点突击 Redis 数据结构、持久化、缓存淘汰、高可用、缓存数据一致性等面试高频知识
- 👉《Java后端面试题》:涵盖Java基础、Java并发、Java虚拟机、Spring、MySQL、Redis、计算机网络等企业面试题
- 👉《大厂真实面经》:涵盖互联网大厂、互联网中厂、手机厂、通信厂、新能源汽车厂、银行等企业真实面试题