俺滴面经

Posted by 孙继峰 on December 16, 2020

真心希望这种船货崇拜的行业现行尽快改变. 现实就是这样, 无奈于现状, 想进大厂、高薪资就需要走这条路.

Java

JVM

JVM 内存区划分

: 线程共享的, 存储所有的实例对象, 也可能存储逃逸分析后的局部变量

方法区: 线程共享的, 存储类信息、常量、静态变量

虚拟机栈: 线程私有的, 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的Java方法调用

本地方法栈: 线程私有的, 支持对 native 方法的调用, Hotspot 在实现的时候将虚拟机栈和本地方法栈合并了.

程序计数器: 线程私有的, 程序计数器会存储当前线程正在执行的Java方法的JVM指令地址, 如果是在执行 native 方法,则是 null

运行时还有直接内存区

了解垃圾回收吗?

哪些是垃圾

最主要部分就是对象实例,都是存储在堆上的; 还有就是方法区中的元数据等信息,例如类不再使用,卸载该Java类。

  • 引用计数算法,就是为对象添加一个引用计数,用于记录对象被引用的情况,如果计数为0,即表示对象可回收。 Java并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系。
  • 可达性分析算法,就是将对象及其引用关系看作一个图,选定活动的对象作为 GC Roots,然后跟踪引用链条,如果一个对象和GC Roots之 间不可达,那么即可认为是可回收对象。JVM会把虚拟机栈中正在引用的对象、静态属性引用的对象和常量,作为GC Roots。

怎么回收垃圾

  • 复制算法,将活着的对象复制到 survivor 区域,拷贝过程中将对象顺序放置,就可以避免内存碎片化。 这么做的代价是,既然要进行复制,既要提前预留内存空间,有一定的浪费, 基于这个算法的吞吐率适合对年轻代进行回收, 因为年轻代中对象有70% - 95%会被清理.
  • 标记清除算法,首先进行标记工作,标识出所有要回收的对象,然后进行清除。不可避免的出现碎片化问题, 这就导致其不适合特别大的堆;
  • 标记整理,类似于标记-清除,但为避免内存碎片化,它会在清理过程中将对象移动,以确保移动后的对象占用连续的内存空间。基于这个算法的特点适合对老年代进行垃圾回收, 因为老年带的清理效率低.

垃圾收集器

JMM

所有的变量都存储在主内存中, 每个线程还有自己的工作内存, 线程的工作内存中保存了该线程使用到的变量, 这些变量都是的从主内存拷贝出的副本, 线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量, 不同的线程之间也无法直接访问对方工作内存中的变量,线程之间值的传递都需要通过主内存来完成

volatile

  • 在 JVM 层面上会为 volatile 变量加上 ACC_VOLATILE 的标识
  • 在 C++ 层面上会为 volatile 变量的读写加上内存屏障, 防止与其他变量的读写重排序
  • 对于读屏障, 操作系统会把 CPU 的高速缓存清理, 再读内存; 对于写屏障, 操作系统会把数据写到内存中, 而不是高速缓存

JVM 调优流程

内存空间调优

  1. 根据异常, 查看哪些区域空间不足 / 过盛
  2. 监控指标信息收集: SpringBoot Admin / Arthas / jvisualvm
  3. 调整内存区域大小
  4. 验证

GC吞吐调优

  1. 确定优化指标, 比如: 平均吞吐率大于90%
  2. GC 日志: -XX:+PrintGCDetails
  3. Parallel Scavenge
  4. 验证

GC停顿调优

  1. 确定优化指标, 比如: YoungGC 时间低于 100 ms / FullGC 时间低于 500 ms
  2. 开启 GC 日志: -XX:+PrintGCDetails
  3. Serial -> Parallel -> G1
  4. 验证

基础

对象的创建过程

  • 常量池中已经加载过了 ? 不在加载了 : 加载类
  • 为对象分配内存
  • 初始化默认值
  • 设置对象头(GC分带年龄、类元数据)

类加载过程

  1. 自定义类加载器
  2. AppClassLoader
  3. ExtClassLoader
  4. BootStrapClassLoader

都加载不了就会抛 ClassNotFoundException

好处就是保证核心库不会被破坏, 比如自定义一个 java.lang.String 是不会被加载进去的

输入流输出流

反射有用到过吗?

CGLib & JDK 代理

JDK 代理是基于反射和接口的, CGLib 是基于继承与字节码修改的, JDK 代理能代理实现了接口的类, CGLib 能代理未实现接口的类(除了final类) 1.8 之前 CGLib 效率高, 1.8之后是 JDK 代理效率高

并发

有用过锁吗?

有用到 synchronizedLock 与基于 Redis 的分布式锁.

synchronized 一开始并不重, 只是经过锁膨胀之后才慢慢变得重. 一开始只是无锁状态, 当一个线程获取锁时, 线程可以直接获取锁, 这时候膨胀为偏向锁, 通过比较 ThreadID 进行加锁. 当有多个线程交替获取锁时, 会膨胀为轻量级锁, 通过自选等待来加锁. 当有多个线程同时获取锁时, 会膨胀为重量级锁, 通过操作系统的 mutex 锁进行互斥, 这时候的消耗才是最高的, 申请和释放锁时都需要从用户态切换到内核态再切回用户态.

Lock 的底层实现基于 Unsafe::compareAndSet 方法, 对 volatile 变量进行 CAS 操作来进行加锁. Unsafe::compareAndSet 使用这个方法将工作内存与主内存的值比较, 成功则赋值. volatile 的语义, 它能保证对这个变量的写操作能立即导致其他线程的工作内存失效, 直接从主内存中获取该变量. volatileUnsafe::compareAndSet结合完成锁的功能.

基于 Redis 的分布式锁是使用开源客户端 Redisson 实现的. Redisson 本身提供了分布式锁的实现, 底层原理是基于 Lua Script, 加锁发送给 Redis Server 一段 Lua Script, 其中逻辑就是如果没有锁(exist), 那就加锁(hset)并设置过期时间(pexpire). 如果锁已存在(hexists), 就叠加一层锁(hincrby), 再续锁(pexpire). 使用 Lua Script 保证执行原子性. Redisson 的 API 都是基于 JUC 接口实现的, 锁是可重入锁, 加锁几次就需要解锁几次.

线程池参数

  • corePoolSize: 核心线程数, 初始化的时候会创建的线程数, 线程会常住, 除非设置了核心线程会过期的策略
  • maximumPoolSize: 最大线程数, 核心线程数加上非核心线程数的最大数
  • keepAliveTime: 空闲线程存活时间, 非核心线程空闲一段时间就会被销毁
  • unit: 时间单位
  • workQueue: 阻塞队列, 核心线程已满之后的任务会直接进入阻塞队列
  • threadFactory: 线程工厂, 在线程工厂中可以指定线程名, 便于在日志中进行排查
  • handler: 拒绝策略, 1.直接丢弃并抛异常, 2.丢弃不抛异常, 3.丢弃队列最前面的任务, 4.由调用者运行

线程池优势

  • 线程的创建需要在虚拟机栈,程序计数器中开辟空间, 频繁的创建与销毁会浪费系统资源
  • 便于管理, 统一实现等待策略、拒绝策略、线程创建策略等

线程状态

线程池如何维护线程空闲时间

wait sleep 区别

  • wait 会释放锁, sleep不会释放锁
  • wait 时可以被 notify 唤醒, sleep不能

ConcurrentHashMap的实现

1.7 : 分段锁

1.8 : 对哈希槽进行 CAS 来加锁

CopyOnWriteArrayList

读写分离

Spring

常用注解

  • @Configuration: 声明这是一个配置类, 会将这个类中 @Bean 修饰的方法返回值注入到容器中
  • @Autowire: 将容器中对象注入进来
  • @RequestMapping / @GetMapping / @PostMapping: 标识这是处理请求的方法
  • @Transactional: 声明这个方法是事务的, 执行成功提交, 执行抛异常回滚

AOP

没有事务的 a 调用有事务的 b, 结果如何

IOC

谈谈你对 Springboot Starter 的理解

Spring 应用引入 MyBatis

  • 引入 MyBatis、MyBatis-Spring、Spring-JDBC
  • 编写配置文件 (.properties / .xml)
  • 解析配置 (.properties)
  • 注入容器 (.properties)

SpringBoot 应用引入 MyBatis

  • 编写配置文件

原理:

  • SpringBoot 启动的时候会扫描项目依赖的 Jar 包, 寻找包含 META-INF/spring.factories 的 Jar 包
  • 加载 spring.factories 中配置的 AutoConfigure 类, 在其中配置的全部 Bean 注入到容器中

事务传播行为

  • PROPAGATION_REQUIRED: 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择
  • PROPAGATION_SUPPORTS: 支持当前事务,如果当前没有事务,就以非事务方式执行
  • PROPAGATION_MANDATORY: 使用当前的事务,如果当前没有事务,就抛出异常
  • PROPAGATION_REQUIRES_NEW: 新建事务,如果当前存在事务,把当前事务挂起
  • PROPAGATION_NOT_SUPPORTED: 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
  • PROPAGATION_NEVER: 以非事务方式执行,如果当前存在事务,则抛出异常
  • PROPAGATION_NESTED: 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作

Spring 用到了哪些设计模式

如何解决循环依赖

SpringMVC 流程

  1. 客户端请求到达DispatcherServlet
  2. 查找请求相关的HandlerMapping, 会返回一个Handler的调用链(拦截器)
  3. 适配Handler(参数校验&封装&别名映射、格式转换)
  4. 调用Handler, 返回ModleAndView
  5. 视图解析器将ModleAndView转换为视图

Springboot 初始化流程

SpringCloud

Nacos

对 Nacos 的了解

服务注册:Nacos Client 向 Nacos Server 注册自己

服务心跳:Nacos Client 会维护一个定时心跳来持续通知 Nacos Server, 说明服务一直处于可用状态

服务同步:Nacos Server 集群之间会互相同步服务实例,用来保证服务信息的一致性

服务发现:Nacos Client 在调用服务提供者的服务时,会请求 Nacos Server, 获取上面注册的服务清单, 并且缓存在 Nacos Client 本地, 同时会在 Nacos Client 开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存

服务健康检查:Nacos Server 会开启一个定时任务用来检查注册服务实例的健康情况, 对于超过 15s 没有收到客户端心跳的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例

配置中心: Nacos Client 会维护一个定时任务去拉取 Nacos Server 的最新配置, 并缓存一份在本地

@RefreshScope

Sentinel

限流算法

  • 令牌桶: 固定容量桶, 令牌以一定速度入桶
  • 漏桶: 固定容量队列, 请求入队
  • 计数器:
  • 固定窗口:
  • 滑动窗口:

Gateway

中间件

MySQL

SQL 优化有做过吗?

在上一家公司中处理过一些简单的 SQL 优化, 也是得益于我们选用的 PolarDB, 有业务需求那就建表建索引, 再有业务需求就加列加索引, 即使是大表也是毫秒级延时. 我就说一下我处理过的那些简单的 SQL 优化吧, 应用中有个离线模块, 模块中 SQL 的写法就比较随便了, 全表查询, 多表 JOIN 都有, 也因为这个出过一次事故, 实例的响应时间整体飙升, 找运维人员定位到是实例连接数不够了, 然后导出了一份慢 SQL Template, 发现有都是 OffLine 模块长期占用连接导致的. 我的做法就是把大 SQL 拆成小 SQL, 全表查询拆成了多个小查询, 创建一个累加器, 把每次小 SQL 的数据收集并聚合.

在当前这家公司处理过分表的优化, 背景就是运营人员需要根据上个月的月订单指标来进行市场分析, 进而产出市场决策, 大数据团队帮我们把业务的订单流按月聚合然后 sink 到了后端的 mysql 中, 后端按市场、城市、月份、AOI去查询指标. 这个表每个月会追加3个G的数据, 然后线上查询就越来越慢, 最慢的时候页面渲染需要近40秒, 通过 Arthas attach 到线上应用后发现查询数据库处阻塞的时间最久, 然后我们决定引入 Sharding-Sphere 进行分表, 先按照月份进行了分表(订单固化周期), 发布到灰度上看时间缩减到10秒以内, 我们部门是有5秒的最长响应时间要求的, 所以还需要继续优化, 我们进行了维度下钻, 按照月份-城市进行分表, 这样做的好处就是指标确实达到了要求, 但是发现 Sharding-Sphere 在应用启动的时候需要扫描表的元数据, 在指标按月分表, 表数量还不多的时候还好, 按照城市下钻后一个月会有近300张表, Sharding-Sphere 在启动的时候就需要扫描大量表的元数据, 一次应用启动时间需要30多分钟, 这个时间在容器环境中是无法被接收的, 一旦应用不能提供服务了, 杀掉容器再拉起一个容器需要30分钟, CICD流程需要半个小时也是结束不了的. 最后我们是按照虚拟城市组下钻的, 把城市按照订单量分了5个组, 应用启动时间达标了, 响应时间也达标了.

这是我遇到的两种优化方案, 对于 SQL 优化来说, 有一些模板, 比如:

  • 最大化利用索引: 能用索引覆盖到全部条件就让它别回表
  • 避免全表扫描: 能 limit 就别全表扫描
  • 减少交互次数: 能用 in 的时候就用 in, 不要在 for 循环中去查库

事务隔离级别

READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读

READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生

REPEATABLE-READ(可重读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。

SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读

如何实现可重复读

事务特性

原子性: 事务的原子性确保动作要么全部完成,要么完全不起作用;

一致性: 执行事务前后,数据保持一致;

隔离性 并发访问数据库时,一个用户的事物不被其他事物所干扰,各并发事务之间数据库是独立的;

持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。

INNODB

B+ 树

  • 矮胖树: 对比二叉树来说IO次数少
  • 叶子节点有链表: 对范围查询有优化

什么情况下不会走索引

  • 左模糊 / 全模糊查询
  • or 的条件不一样 (使用union可解决)
  • not in
  • !=
  • 隐式转换
  • 函数运算

什么情况进行分库, 什么情况进行分表

Redis

Redis数据结构

  • String: 字符串, 对应操作 get 、 set 、 del 、 incr、 decr
  • Hash: KV结构, 对应操作 hget、hmget、hdel、hincrby
  • List: 双向链表, 对应操作 lpush、lpop、rpush、rpop
  • Set: 不重复集合, 对应操作 sset srem sismember smembers
  • Zset: 按权重排序的集合, 对应操作 zadd zrange zscore

过期算法

  • 查询时Key过期直接删掉
  • 定时任务清理: 随机抽取N个Key, 检查是否过期, 过期就清理, 如果超过一定百分比会触发下一次清理.
  • 内存不足时会按照用户配置的策略清理: 直接拒绝、LRU(最近最少使用)、LFU(最不经常使用)

实现LRU、LFU

缓存问题

  • 缓存雪崩: 缓存的Key集体失效, 加随机超时时间
  • 缓存击穿: 热Key失效, 热Key永不失效 / 锁同步
  • 缓存穿透: 不存在的Key, 缓存空Key / 布隆过滤器

双写一致

  • 延时双删: 删缓存, 更新DB, (异步)sleep1s, (异步)删缓存, 业务侵入
  • 订阅binlog: 解析binlog、保留DELETE、UPDATE, 解析表名和数据id, 同步到缓存中 (重要操作走主库)

实现一个延时队列

轮询 Zset

实现一个死信队列

一致性哈希

将整个哈希空间组织成一个环, 假设一共有2^32-1个槽, 服务器节点通过一个哈希函数得到哈希值落到槽中, 请求参数通过一个哈希函数得到哈希值落到哈希槽中, 顺时针寻找最近的服务器, 让它处理请求. 对于服务器节点比较少的可以使用虚拟服务器节点, 减少负载不均的情况

优点: 如果有节点故障/新增节点的情况, 能尽可能少的保证数据同步/迁移

Redis 扩容

持久化机制

RBD: 定时持久化

AOF: 记录写操作

混合持久化: RBD 定期持久化 + AOF 完成两次 RBD之间的数据备份

MQ

如何处理消息重复

如何保证消息到达

client 重试 + 持久化

设计一个微信聊天群

Arthas

常用命令

  • watch: 查看方法的入参返回值
  • trace: 查看方法的调用链路
  • jad: 查看反编译后的源码

SkyWalking

原理

异步 tracingId 如何传递

分布式事务

TCC

XA

项目

如何推动 TDD 落地

  1. 参加TW线下技术分享接触到TDD
  2. 回到公司展开分享, 针对现有问题对应到TDD, 看看TDD能解决哪些
  3. 刻意练习2个月
  4. 建立度量, 推给Leader
    1. 领域模型测试覆盖度
    2. QA每次提测回归时间
    3. 自动化测试拦截的bug数量(阻止了多少次P0P1P2故障)
    4. 研发时间

商品服务从0到1的过程

背景: 商品逻辑耦合于上游各个服务x, 痛点就是难于管理, 决定拆分沉淀为商品服务.

确定通用语言: 为避免和产品、测试、开发人员沟通歧义, 我们首先确定了通用语言, 按照商品的生命周期划分出了几个概念, 我们把商家上传的商品叫商品审核单, 审核通过后买家可见的商品叫浏览商品, 买家下单的商品叫订单项, 退款流程中的商品叫商品售后单.

确定服务边界: 这个流程主要的目的就是确定哪些逻辑商品服务需要处理, 哪些逻辑商品服务不需要处理. 比如查询商品的活动价格, 按照行为名称看是和商品有关系, 但是仔细分析活动是属于运营子域的, 活动价也是运营子域的内容.

任务分解: 将每一个接口拆分成若干个 case, 去找测试、产品、原来的维护者确认, 避免开发完成后的扯皮与返工. 每一个 case 最后以测试用例的形式验证, 通过测试用例了, 就证明代码是可工作的, 功能是被测试、产品、原维护者认可的.

编写测试用例,写代码,通过测试用例,通过check style静态检查,找人review,测试,预发布,上线, 通过日志收集配置规则来报警, 通过Arthas来排查

解决过哪些技术难题?

背景: Gateway里打印了接口名与相应时间, 日志收集中配置了报警, 依赖报警感知接口超时率.

1.接口超时

排查: 从请求体上看一次请求了1000多个资源, 与上游业务确认后确定是正常的用户请求. 从代码中看不出任何猫腻, 借助 Arthas attach 到线上应用后对接口方法链路进行耗时分析.发现两处验证耗时的地方, 将 redis client 返回的数据(本身是Map结构,K:属性名, V:属性值)序列化为 json 又序列化为领域对象, 另一个是对象拷贝(BeanUtils::copyProperty).

解决: 使用 Hutool 的 BeanUtil::toBean 直接把 Map 转换为 Bean, 将属性拷贝全部替换为 Setter

验证: 运行全部测试用例, 确保未破坏已有功能, 运行JMH, 确保重构后的接口将再当前场景下不会再超时.

2.Redis 查询慢

排查: Arthas attach 线程观察到 redis client 执行缓慢, 找运维人员确认 RDS-Redis 实例, 连接数有波峰.

分析: 项目中有一些极其不恰当的 Redis 实践, 把整个表的数据都使用一个 Hash Key 存储了, 根据28原则, 那肯定也有大量长时间不被访问的 field. 更改存储结构需要改动很多点, 代价比较大.

解决: 使用 Redisson 的 RMapCache 对每一个 Field 设置过期时间, Redisson 用定时任务去删除过期的 Field.

3. 数据库死锁

排查: 错误都在log里写明白的 Deadlock found when trying to get lock; try restarting transaction

分析: 异常在 MyBatis-Plus 的 ServiceImpl::updateBatchById 方法中抛出, 在执行SqlSession::flushStatements 时会去对库里的记录加锁, 在并发情况下批量更新用户1、用户2, 与更新用户2、用户1, 获取锁的顺序不一致, 一个事务持有了用户1的锁, 请求用户2, 另外一个事务持久用户2的锁, 请求用户1的锁, 从而导致死锁.

解决: 破坏一个死锁的必要条件: 循环等待. 把这个条件破坏掉就可以了, 对于要进行批量更新的数据, 对其安装id排序, 在进行批量更新就没有问题了.

Linux

Linux 命令

  • ls : 显示当前目录下的目录和文件

  • mkdir: 创建目录

  • cd: 切换目录

  • touch: 创建文件

  • vim: 编辑文本

  • tail: 查看文件尾部

  • grep: 过滤文本

手撕编程题

冒泡排序

笛卡尔积

集合的交集

集合去重

判断一个IP是否合法

去重输出字符串(abccdce -> abcde)

超长整数求和

验证一个字符串是否是数字

100级台阶,每次只能走一步或者两步, 有多少中走法

三个线程交替打印ABC, 100次

快排

Double Check Lock

K个数组内部已经排好序了, 把排序结果合并

反转链表

网络

HTTP请求全流程

  1. 域名解析
    • 浏览器缓存
    • hosts文件
    • Local DNS Server
    • Root DNS Server
    • 顶级域名服务器(.com, .cn)
    • 域名服务提供商(阿里云, 百度云)
    • 返回域名对应的地址与TTL
  2. 客户端与服务端建立连接 - 三次握手
    • 客户端发送 SYN, 客户端进入 SYN-SENT 状态
    • 服务端发送 SYN-ACK, 服务端进入 SYN-RECEIVED 状态
    • 客户端发送 ACK, 客户端进入 ESTABLISH 状态, 服务端接收到后进入 ESTABLISH 状态
  3. 客户端发送请求行、请求头、请求体
  4. 服务端响应响应行、响应头、响应体
  5. 渲染页面
  6. 客户端与服务器断开连接 - 四次挥手
    • 客户端发送 FIN, 客户端进入 FIN-WAIT-1 状态
    • 服务端发送 ACK, 服务端进入 CLOSED-WAIT 状态
    • 服务端发送 FIN, 客户端进入 FIN_WAIT-2 状态
    • 客户端发送 ACK, 服务端进入 CLOSED 状态, 客户端在2个报文最大生存时间后进入 CLOSE 状态
  7. 如果还有页面还有其他的静态资源(.css, .jpg), 会发送额外的HTTP请求, 上面的流程会再来一遍, 这个有可能会打到CDN服务器.

你有什么问题想问我?

公司内有没有 DevOps / 敏捷文化? 都有什么相关活动?

是否有云原生文化?