小公司工作 6 年,后面怎么走?

沉默王二

共 16290字,需浏览 33分钟

 ·

2024-04-25 14:44

大家好,我是二哥呀。

我们每个人,都在不断和命运抗争着,哪怕有时候我们会不甘心、会堕落、会迷茫、会焦虑,但总有那么一刻,内心是充满渴望的,想要去努力、想要去改变、想要有一番作为(⛽️)。

星球里就有这样一位球友,在小公司工作六年了,平凡而不平庸,他 18 年毕业,参加过 Java 后端的培训,还被骗过去外包。

后来经过自己的努力去了一家做电动自行车相关软件的自研公司,一直工作到现在,目前薪资 22k+,技术上不能说多牛,但能独当一面,像 Spring Cloud、Docker、k8s、Zookeeper、Netty 等等这些技术栈,也都能熟练应用,并且熟知一些底层原理。

他希望“自己能更上一层楼”,最近把简历也发给我帮忙优化,我看完后其实是大受震撼的,因为他的简历内容真的远超我的预期。

给大家简单看一下我优化后的专业技能(我不全贴,免得大家直接照着他的简历抄):

  • 1、消息队列:掌握 RocketMQ,如消息丢失、重复消费,顺序消费、回溯消费、死信队列、分布式事务等,同时了解 RabbitMQ。
  • 2、ZooKeeper:熟练使用 ZooKeeper,并了解其核心原理,如 Leader 选举、Paxos 算法、ZAB 一致性协议、 Watch 机制等,并有将其应用于分布式锁和服务协调的实战经验。
  • 3、Netty:熟悉 Netty,掌握零拷贝、消息拆包粘包处理及心跳机制。
  • 4、分布式系统:熟悉分布式系统的常见解决方案:分布式事务使用、缓存和数据库一致性、分布式锁等,了解 CAP、BASE 等算法。

其实对于我们大多数普通人来说,没有一帆风顺、没有平步青云,有的只是走好脚下的每一步路,不焦虑,不气馁,踏踏实实,你的付出也终将会有所回报。

这次我们就以《Java 面试指南》中小公司面经合集同学 1 为例,来看看小公司的面试官都喜欢问哪些问题,好做到知彼知己百战不殆。

让天下所有的面渣都能逆袭 😁

可以看得出,仍然是二哥一直强调的 Java 后端四大件,Java 基础(包括 JVM 和并发编程)、Spring 全家桶(包括 Spring Boot)、MySQL 和 Redis,所以准备的时候一定要有的放矢,针对性地去复习,才能事半功倍。

Java 基础(详细)

==和equals()有什么区别?

在 Java 中,== 操作符和 equals() 方法用于比较两个对象:

①、==:用于比较两个对象的引用,即它们是否指向同一个对象实例。

如果两个变量引用同一个对象实例,== 返回 true,否则返回 false

对于基本数据类型(如 int, double, char 等),== 比较的是值是否相等。

②、equals() 方法:用于比较两个对象的内容是否相等。默认情况下,equals() 方法的行为与 == 相同,即比较对象引用,如在超类 Object 中:

public boolean equals(Object obj) {
    return (this == obj);
}

然而,equals() 方法通常被各种类重写。例如,String 类重写了 equals() 方法,以便它可以比较两个字符串的字符内容是否完全一样。

二哥的 Java 进阶之路,String的equals()源码

举个例子:

String a = new String("沉默王二");
String b = new String("沉默王二");

// 使用 == 比较
System.out.println(a == b); // 输出 false,因为 a 和 b 引用不同的对象

// 使用 equals() 比较
System.out.println(a.equals(b)); // 输出 true,因为 a 和 b 的内容相同

String变量直接赋值和构造方法赋值==比较相等吗?

直接使用双引号为字符串变量赋值时,Java 首先会检查字符串常量池中是否已经存在相同内容的字符串。

如果存在,Java 就会让新的变量引用池中的那个字符串;如果不存在,它会创建一个新的字符串,放入池中,并让变量引用它。

使用 new String("abc") 的方式创建字符串时,实际分为两步:

  • 第一步,先检查字符串字面量 "abc" 是否在字符串常量池中,如果没有则创建一个;如果已经存在,则引用它。
  • 第二步,在堆中再创建一个新的字符串对象,并将其初始化为字符串常量池中 "abc" 的一个副本。
三分恶面渣逆袭:堆与常量池中的String

也就是说:

String s1 = "沉默王二";
String s2 = "沉默王二";
String s3 = new String("沉默王二");

System.out.println(s1 == s2); // 输出 true,因为 s1 和 s2 引用的是字符串常量池中同一个对象。
System.out.println(s1 == s3); // 输出 false,因为 s3 是通过 new 关键字显式创建的,指向堆上不同的对象。

String都有哪些常用方法?

有很多,比如说前面提到到的 equals,hashCode,substring,indexOf 等等。

抽象类和接口有什么区别?

一个类只能继承一个抽象类;但一个类可以实现多个接口。所以我们在新建线程类的时候一般推荐使用实现 Runnable 接口的方式,这样线程类还可以继承其他类,而不单单是 Thread 类。

抽象类符合 is-a 的关系,而接口更像是 has-a 的关系,比如说一个类可以序列化的时候,它只需要实现 Serializable 接口就可以了,不需要去继承一个序列化类。

Java容器有哪些?List、Set还有Map的区别?

Java 集合框架可以分为两条大的支线:

①、Collection,主要由 List、Set、Queue 组成:

  • List 代表有序、可重复的集合,典型代表就是封装了动态数组的 ArrayList 和封装了链表的 LinkedList;
  • Set 代表无序、不可重复的集合,典型代表就是 HashSet 和 TreeSet;
  • Queue 代表队列,典型代表就是双端队列 ArrayDeque,以及优先级队列 PriorityQueue。

②、Map,代表键值对的集合,典型代表就是 HashMap。

二哥的 Java 进阶之路:Java集合主要关系

线程创建的方式?Runable和Callable有什么区别?

Java 中创建线程主要有三种方式,分别为继承 Thread 类、实现 Runnable 接口、实现 Callable 接口。

第一种,继承 Thread 类,重写 run()方法,调用 start()方法启动线程。

class ThreadTask extends Thread {
    public void run() {
        System.out.println("看完二哥的 Java 进阶之路,上岸了!");
    }

    public static void main(String[] args) {
        ThreadTask task = new ThreadTask();
        task.start();
    }
}

这种方法的缺点是,由于 Java 不支持多重继承,所以如果类已经继承了另一个类,就不能使用这种方法了。

第二种,实现 Runnable 接口,重写 run() 方法,然后创建 Thread 对象,将 Runnable 对象作为参数传递给 Thread 对象,调用 start() 方法启动线程。

class RunnableTask implements Runnable {
    public void run() {
        System.out.println("看完二哥的 Java 进阶之路,上岸了!");
    }

    public static void main(String[] args) {
        RunnableTask task = new RunnableTask();
        Thread thread = new Thread(task);
        thread.start();
    }
}

这种方法的优点是可以避免 Java 的单继承限制,并且更符合面向对象的编程思想,因为 Runnable 接口将任务代码和线程控制的代码解耦了。

第三种,实现 Callable 接口,重写 call() 方法,然后创建 FutureTask 对象,参数为 Callable 对象;紧接着创建 Thread 对象,参数为 FutureTask 对象,调用 start() 方法启动线程。

class CallableTask implements Callable<String{
    public String call() {
        return "看完二哥的 Java 进阶之路,上岸了!";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        CallableTask task = new CallableTask();
        FutureTask<String> futureTask = new FutureTask<>(task);
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println(futureTask.get());
    }
}

这种方法的优点是可以获取线程的执行结果。

启动一个线程是run()还是start()?

在 Java 中,启动一个新的线程应该调用其start()方法,而不是直接调用run()方法。

当调用start()方法时,会启动一个新的线程,并让这个新线程调用run()方法。这样,run()方法就在新的线程中运行,从而实现多线程并发。

class MyThread extends Thread {
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }

    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start(); // 正确的方式,创建一个新线程,并在新线程中执行 run()
        t1.run(); // 仅在主线程中执行 run(),没有创建新线程
    }
}

如果直接调用run()方法,那么run()方法就在当前线程中运行,没有新的线程被创建,也就没有实现多线程的效果。

来看输出结果:

main
Thread-0

也就是说,start() 方法的调用会告诉 JVM 准备好所有必要的新线程结构,分配其所需资源,并调用线程的 run() 方法在这个新线程中执行。

三分恶面渣逆袭:start方法

Spring 全家桶

介绍Spring IoC 和 AOP?

所谓的IoC(控制反转,Inversion of Control),就是由容器来控制对象的生命周期和对象之间的关系。以前是我们想要什么就自己创建什么,现在是我们需要什么容器就帮我们送来什么。

引入IoC之前和引入IoC之后

也就是说,控制对象生命周期的不再是引用它的对象,而是容器,这就叫控制反转

三分恶面渣逆袭:控制反转示意图

talk is cheap,show me the code,我们来看一个例子,没有 IoC 之前:

我需要一个女朋友,刚好大街上突然看到了一个小姐姐,人很好看,于是我就自己主动上去搭讪,要她的微信号,找机会聊天关心她,然后约她出来吃饭,打听她的爱好,三观。。。

有了 IoC 之后:

我需要一个女朋友,于是我就去找婚介所,告诉婚介所,我需要一个长的像赵露思的,会打 Dota2 的,于是婚介所在它的人才库里开始找,找不到它就直接说没有,找到它就直接介绍给我。

婚介所就相当于一个 IoC 容器,我就是一个对象,我需要的女朋友就是另一个对象,我不用关心女朋友是怎么来的,我只需要告诉婚介所我需要什么样的女朋友,婚介所就帮我去找。

Spring 倡导的开发方式就是这样,所有的类创建都通过 Spring 容器来,不再是开发者去 new,去 = null 销毁,这些创建和销毁的工作都交给 Spring 容器来。

于是,对于某个对象来说,以前是它控制它依赖的对象,现在是所有对象都被 Spring 控制,这就是控制反转

图片来源于网络

AOP,也就是 Aspect-oriented Programming,译为面向切面编程。

简单点说,就是把一些业务逻辑中的相同代码抽取到一个独立的模块中,让业务逻辑更加清爽。

三分恶面渣逆袭:横向抽取

举个例子,假如我们现在需要在业务代码开始前进行参数校验,在结束后打印日志,该怎么办呢?

我们可以把日志记录数据校验这两个功能抽取出来,形成一个切面,然后在业务代码中引入这个切面,这样就可以实现业务逻辑和通用逻辑的分离。

三分恶面渣逆袭:AOP应用示例

业务代码不再关心这些通用逻辑,只需要关心自己的业务实现,这样就实现了业务逻辑和通用逻辑的分离。

我们来回顾一下 Java 语言的执行过程:

三分恶面渣逆袭:Java 执行过程

AOP 的核心是动态代理,可以使用 JDK 动态代理来实现,也可以使用 CGLIB 来实现。

Spring框架使用到的设计模式?

三分恶面渣逆袭:Spring中用到的设计模式

Spring 框架中用了蛮多设计模式的:

①、工厂模式:IoC 容器本身可以看作是一个巨大的工厂,负责创建和管理 Bean 的生命周期和依赖关系。

像 BeanFactory 和 ApplicationContext 接口都提供了工厂模式的实现,负责实例化、配置和组装 Bean。

②、代理模式:AOP 的实现就是基于代理模式的,如果配置了事务管理,Spring 会使用代理模式创建一个连接数据库的代理对象,来进行事务管理。

③、单例模式:Spring 容器中的 Bean 默认都是单例的,这样可以保证 Bean 的唯一性,减少系统开销。

④、模板模式:Spring 中的 JdbcTemplate,HibernateTemplate 等以 Template 结尾的类,都使用了模板方法模式。

比如,我们使用 JdbcTemplate,只需要提供 SQL 语句和需要的参数就可以了,至于如何创建连接、执行 SQL、处理结果集等都由 JdbcTemplate 这个模板方法来完成。

④、观察者模式:Spring 事件驱动模型就是观察者模式很经典的一个应用,Spring 中的 ApplicationListener 就是观察者,当有事件(ApplicationEvent)被发布,ApplicationListener 就能接收到信息。

⑤、适配器模式:Spring MVC 中的 HandlerAdapter 就用了适配器模式。它允许 DispatcherServlet 通过统一的适配器接口与多种类型的请求处理器进行交互。

MyBatis

Mybatis#()和$()有什么区别?

在 MyBatis 中,#{}${} 是两种不同的占位符,#{} 是预编译处理,${} 是字符串替换。

三分恶面渣逆袭:#{}和${}比较

①、当使用 #{} 时,MyBatis 会在 SQL 执行之前,将占位符替换为问号 ?,并使用参数值来替代这些问号。

由于 #{} 使用了预处理,它能有效防止 SQL 注入,可以确保参数值在到达数据库之前被正确地处理和转义。

<select id="selectUser" resultType="User">
  SELECT * FROM users WHERE id = #{id}
</select>

②、当使用 ${} 时,参数的值会直接替换到 SQL 语句中去,而不会经过预处理。

这就存在 SQL 注入的风险,因为参数值会直接拼接到 SQL 语句中,假如参数值是 1 or 1=1,那么 SQL 语句就会变成 SELECT * FROM users WHERE id = 1 or 1=1,这样就会导致查询所有用户的结果。

${} 通常用于那些不能使用预处理的场合,比如说动态表名、列名、排序等,要提前对参数进行安全性校验。

<select id="selectUsersByOrder" resultType="User">
  SELECT * FROM users ORDER BY ${columnName} ASC
</select>

MySQL

MySQL的四个隔离级别以及默认隔离级别?

事务的隔离级别定了一个事务可能受其他事务影响的程度,MySQL 支持的四种隔离级别分别是:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。

三分恶面渣逆袭:事务的四个隔离级别

什么是读未提交?

读未提交是最低的隔离级别,在这个级别,当前事务可以读取未被其他事务提交的数据,以至于会出现“脏读”、“不可重复读”和“幻读”的问题。

什么是读已提交?

当前事务只能读取已经被其他事务提交的数据,可以避免“脏读”现象。但不可重复读和幻读问题仍然存在。

什么是可重复读?

确保在同一事务中多次读取相同记录的结果是一致的,即使其他事务对这条记录进行了修改,也不会影响到当前事务。

是 MySQL 默认的隔离级别,避免了“脏读”和“不可重复读”,也在很大程度上减少了“幻读”问题。

什么是串行化?

最高的隔离级别,通过强制事务串行执行来避免并发问题,可以解决“脏读”、“不可重复读”和“幻读”问题。

但会导致大量的超时和锁竞争问题。

A事务未提交,B事务上查询到的是旧值还是新值?

在 MySQL 的默认隔离级别(可重复读)下,如果事务 A 修改了数据但未提交,事务 B 将看到修改之前的数据。

这是因为在可重复读隔离级别下,MySQL 将通过多版本并发控制(MVCC)机制来保证一个事务不会看到其他事务未提交的数据,从而确保读一致性。

编写SQL语句哪些情况会导致索引失效?

  • 在索引列上使用函数或表达式:如果在查询中对索引列使用了函数或表达式,那么索引可能无法使用,因为数据库无法预先计算出函数或表达式的结果。例如:SELECT * FROM table WHERE YEAR(date_column) = 2021
  • 使用不等于(<>)或者 NOT 操作符:这些操作符通常会使索引失效,因为它们会扫描全表。
  • 使用 LIKE 操作符,但是通配符在最前面:如果 LIKE 的模式串是以“%”或者“_”开头的,那么索引也无法使用。例如:SELECT * FROM table WHERE column LIKE '%abc'
  • OR 操作符:如果查询条件中使用了 OR,并且 OR 两边的条件分别涉及不同的索引,那么这些索引可能都无法使用。
  • 如果 MySQL 估计使用全表扫描比使用索引更快时(通常是小表或者大部分行都满足 WHERE 子句),也不会使用索引。
  • 联合索引不满足最左前缀原则时,索引会失效。

说说 SQL 的隐式数据类型转换?

在 SQL 中,当不同数据类型的值进行运算或比较时,会发生隐式数据类型转换。

比如说,当一个整数和一个浮点数相加时,整数会被转换为浮点数,然后再进行相加。

SELECT 1 + 1.0-- 结果为 2.0

比如说,当一个字符串和一个整数相加时,字符串会被转换为整数,然后再进行相加。

SELECT '1' + 1-- 结果为 2

数据类型隐式转换会导致意想不到的结果,所以要尽量避免隐式转换。

二哥的 Java 进阶之路

可以通过显式转换来规避这种情况。

SELECT CAST('1' AS SIGNED INTEGER) + 1-- 结果为 2

了解的MVCC吗?

MVCC 是多版本并发控制(Multi-Version Concurrency Control)的简称,主要用来解决数据库并发问题。

在支持 MVCC 的数据库中,当多个用户同时访问数据时,每个用户都可以看到一个在某一时间点之前的数据库快照,并且能够无阻塞地执行查询和修改操作,而不会相互干扰。

在传统的锁机制中,如果一个事务正在写数据,那么其他事务必须等待写事务完成才能读数据,MVCC 允许读操作访问数据的一个旧版本快照,同时写操作创建一个新的版本,这样读写操作就可以并行进行,不必等待对方完成。

在 MySQL 中,特别是 InnoDB 存储引擎,MVCC 是通过版本链和 ReadView 机制来实现的。

什么是版本链?

在 InnoDB 中,每一行数据都有两个隐藏的列:一个是 DB_TRX_ID,另一个是 DB_ROLL_PTR。

  • DB_TRX_ID,保存创建这个版本的事务 ID。
  • DB_ROLL_PTR,指向 undo 日志记录的指针,这个记录包含了该行的前一个版本的信息。通过这个指针,可以访问到该行数据的历史版本。

假设有一张hero表,表中有一行记录 name 为张三,city 为帝都,插入这行记录的事务 id 是 80。此时,DB_TRX_ID的值就是 80,DB_ROLL_PTR的值就是指向这条 insert undo 日志的指针。

三分恶面渣逆袭:表记录

接下来,如果有两个DB_TRX_ID分别为100200的事务对这条记录进行了update操作,那么这条记录的版本链就会变成下面这样:

三分恶面渣逆袭:update 操作

当事务更新一行数据时,InnoDB 不会直接覆盖原有数据,而是创建一个新的数据版本,并更新 DB_TRX_ID 和 DB_ROLL_PTR,使得它们指向前一个版本和相关的 undo 日志。这样,老版本的数据不会丢失,可以通过版本链找到。

由于 undo 日志会记录每一次的 update,并且新插入的行数据会记录上一条 undo 日志的指针,所以可以通过这个指针找到上一条记录,这样就形成了一个版本链。

三分恶面渣逆袭:版本链

说说什么是 ReadView?

ReadView(读视图)是 InnoDB 为了实现一致性读(Consistent Read)而创建的数据结构,它用于确定在特定事务中哪些版本的行记录是可见的。

ReadView 主要用来处理隔离级别为"可重复读"(REPEATABLE READ)和"读已提交"(READ COMMITTED)的情况。因为在这两个隔离级别下,事务在读取数据时,需要保证读取到的数据是一致的,即读取到的数据是在事务开始时的一个快照。

当事务开始执行时,InnoDB 会为该事务创建一个 ReadView,这个 ReadView 会记录 4 个重要的信息:

  • creator_trx_id:创建该 ReadView 的事务 ID。
  • m_ids:所有活跃事务的 ID 列表,活跃事务是指那些已经开始但尚未提交的事务。
  • min_trx_id:所有活跃事务中最小的事务 ID。它是 m_ids 数组中最小的事务 ID。
  • max_trx_id :事务 ID 的最大值加一。换句话说,它是下一个将要生成的事务 ID。

乐观锁和悲观锁,库存的超卖问题的原因和解决方案?

①、乐观锁

乐观锁基于这样的假设:冲突在系统中出现的频率较低,因此在数据库事务执行过程中,不会频繁地去锁定资源。相反,它在提交更新的时候才检查是否有其他事务已经修改了数据。

可以通过在数据表中使用版本号(Version)或时间戳(Timestamp)来实现,每次读取记录时,同时获取版本号或时间戳,更新时检查版本号或时间戳是否发生变化。

如果没有变化,则执行更新并增加版本号或更新时间戳;如果检测到冲突(即版本号或时间戳与之前读取的不同),则拒绝更新。

②、悲观锁

悲观锁假设冲突是常见的,因此在数据处理过程中,它会主动锁定数据,防止其他事务进行修改。

可以直接使用数据库的锁机制,如行锁或表锁,来锁定被访问的数据。常见的实现是 SELECT FOR UPDATE 语句,它在读取数据时就加上了锁,直到当前事务提交或回滚后才释放。

如何解决库存超卖问题?

按照乐观锁的方式:

UPDATE inventory SET count = count - 1version = version + 1 WHERE product_id = 1 AND version = current_version;

按照悲观锁的方式:

在事务开始时直接锁定库存记录,直到事务结束。

START TRANSACTION;
SELECT * FROM inventory WHERE product_id = 1 FOR UPDATE;
UPDATE inventory SET count = count - 1 WHERE product_id = 1;
COMMIT;

Redis

Redisson的底层原理?以及与SETNX的区别?

图片来源于网络

Redisson 是一个基于 Redis 的 Java 驻内存数据网格(In-Memory Data Grid),提供了一系列 API 用来操作 Redis,其中最常用的功能就是分布式锁。

RLock lock = redisson.getLock("lock");
lock.lock();
try {
    // do something
finally {
    lock.unlock();
}

普通锁的实现源码是在 RedissonLock 类中,也是通过 Lua 脚本封装一些 Redis 命令来实现的的,比如说 tryLockInnerAsync 源码:

二哥的 Java 进阶之路:RedissonLock

其中 hincrby 命令用于对哈希表中的字段值执行自增操作,pexpire 命令用于设置键的过期时间。比 SETNX 更优雅。

Redis的持久化方式?RDB和AOF的区别?

Redis 支持两种主要的持久化方式:RDB(Redis DataBase)持久化和 AOF(Append Only File)持久化。这两种方式可以单独使用,也可以同时使用。

三分恶面渣逆袭:Redis持久化的两种方式

RDB 是一个非常紧凑的单文件(二进制文件 dump.rdb),代表了 Redis 在某个时间点上的数据快照。非常适合用于备份数据,比如在夜间进行备份,然后将 RDB 文件复制到远程服务器。但可能会丢失最后一次持久化后的数据。

AOF 的最大优点是灵活,实时性好,可以设置不同的 fsync 策略,如每秒同步一次,每次写入命令就同步,或者完全由操作系统来决定何时同步。但 AOF 文件往往比较大,恢复速度慢,因为它记录了每个写操作。

Redis宕机哪种恢复的比较快?

在 Redis 4.0 版本中,混合持久化模式会在 AOF 重写的时候同时生成一份 RDB 快照,然后将这份快照作为 AOF 文件的一部分,最后再附加新的写入命令。

三分恶面渣逆袭:混合持久化

这样,当需要恢复数据时,Redis 先加载 RDB 文件来恢复到快照时刻的状态,然后应用 RDB 之后记录的 AOF 命令来恢复之后的数据更改,既快又可靠。

参考链接

  • 三分恶的面渣逆袭:https://javabetter.cn/sidebar/sanfene/nixi.html
  • 二哥的 Java 进阶之路:https://javabetter.cn

ending

一个人可以走得很快,但一群人才能走得更远。二哥的编程星球已经有 5100 多名球友加入了,如果你也需要一个良好的学习环境,戳链接 🔗 加入我们吧。这是一个编程学习指南 + Java 项目实战 + LeetCode 刷题的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。

两个置顶帖「球友必看」和「知识图谱」里已经沉淀了非常多优质的学习资源,相信能帮助你走的更快、更稳、更远

欢迎点击左下角阅读原文了解二哥的编程星球,这可能是你学习求职路上最有含金量的一次点击。

最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。

浏览 2261
4点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
4点赞
评论
收藏
分享

手机扫一扫分享

分享
举报