10.美团财务科技后端一面
# 10.美团财务科技后端一面
# 项目
前几个问题是个人项目相关,可以根据问题思考一下,提炼一下项目中的难点以及收获
# 1、是个人进行项目开发的吗?如何进行系统设计的?
# 2、项目是否已经上线?
# 3、在项目开发中遇到的难点?如何解决?
# 4、项目开发过程中有什么收获?
# 5、Zset 底层数据结构,为什么要用Zset?
Zset 的底层实现是:压缩列表 + 跳表
什么时候使用压缩列表?
- 有序集合保存的元素个数小于 128 个
- 有序集合保存的所有元素成员的长度都必须小于 64 字节
否则使用跳表
跳表在 Redis 中的作用就是作为有序集合类型的底层数据结构,
跳表中每个节点保存着其他节点的指针,高层的指针越过的元素数量大于等于低层的指针,因此在跳表中,在查找元素时可以一次跳过多个节点,当找到大于或等于目标元素的节点后,再使用普通指针开始移动(可以向后移动,也可以向前移动,跳表含有前边节点的指针)寻找目标元素,跳表可以在 O(logn)
的时间内遍历跳表
跳表结构图:
至于为什么要用 Zset 结合自己的项目说的合理即可
# 6、你如何保证缓存与数据库一致性,延时双删的第二次删除缓存失败了呢,如何解决?
这个要看延时双删具体如何实现了,先说一下它的流程:
用户发起更新数据的请求
1、删除数据对应缓存
2、更新数据库
3、休眠一段时间(等待数据库更新完毕之后)
4、再次删除数据对应缓存
这里要休眠一段时间等待第二次删除缓存,因此可以通过 Redisson 的延时队列来做,将第二次删除缓存的信息发送到延时队列中,等待被消费
在消费的时候可以去对失败操作进行处理,从延时队列中取出消息进行消费,如果失败了,可以进行指定次数的重试,如果只是因为网络抖动失败,那么一般重试就可以成功消费了,但是如果是 Redis 出现了问题,那么这一段时间内重试可能都还会存在问题,因此要考虑将失败的消息给放在数据库中存储起来,再定时进行消费对失败的消息进行补偿(不过 Redis 宕机的概率很小,一般不会发生)
上边是个人思路,如有不足欢迎补充
# 7、Redis 如何存储热点数据?
热点数据就是某些数据访问频率过高
首先对于热点数据要先可以检测到,检测之后再看具体如何解决
检测的话,可以使用京东零售开源的 hotkey 框架,是一个轻量级通用热 key 探测中间件,可以快速将热数据推送到 JVM 内存,减少大量请求对下游服务、Redis、MySQL 的冲击
那么热点数据的存储的话,可以存储一份到 JVM 内存中,进一步提升数据的查询性能,可以选用 Caffeine (Java 高性能缓存库)来管理 Redis 的热点数据,Caffeine 是基于 Google Guava 改进的,因此 Caffeine 的性能表现、缓存命中率相对来说更好
# 8、项目为什么要用 ThreadLocal?ThreadLocal 如何保证线程隔离的?
项目为什么要用 ThreadLocal ,这个可以基于 ThreadLocal 的特性来回答
在多线程环境下,有些数据是你当前线程上下文中要使用到的,但是如果每次调用方法都将这些数据传递过去,或者每次使用的时候都重新过去是比较麻烦的,并且也会带来一定的时间开销,因此可以存储在 ThreadLocal 中,在当前线程的上下文中进行使用
这样在用到数据的时候,直接从 ThreadLocal 中取出来,既保证了线程之间的数据隔离性,又保证了较好的性能(不必重复去获取)
ThreadLocal 如何保证线程隔离的?
如下图,在每一个线程 Thread 中都会定义一个 ThreadLocalMap 属性,该属性就存储 ThreadLocal 所对应的值,那么每个线程中都有一份 ThreadLocalMap 的变量,以此来实现线程隔离
扩展:ThreadLocal 正确使用姿势
ThreadLocal 的使用规范:将 ThreadLocal 变量定义为 private static final ,并且在使用完,记得通过 try finally 来 remove 掉,避免出现脏数据
扩展:ThreadLocal 内存泄漏问题
这里假设将 ThreadLocal 定义为方法中的局部变量,那么当线程进入该方法的时候,就会将 ThreadLocal 的引用给加载到线程的栈 Stack 中
如上图所示,在线程栈 Stack 中,有两个变量,ThreadLocalRef 和 CurrentThreadRef,分别指向了声明的局部变量 ThreadLocal ,以及当前执行的线程
而 ThreadLocalMap 中的 key 是弱引用,当线程执行完该方法之后,Stack 线程栈中的 ThreadLocalRef 变量就会被弹出栈,因此 ThreadLocal 变量的强引用(ThreadLocalRef)消失了,那么 ThreadLocal 变量只有 Entry 中的 key 对他引用,并且还是弱引用,因此这个 ThreadLocal 变量会被回收掉,导致 Entry 中的 key 为 null,而 value 还指向了对 Object 的强引用,因此 value 还一直存在 ThreadLocalMap 变量中
由于 ThreadLocal 被回收了,无法通过 key 去访问到这个 value,导致这个 value 一直无法被回收, ThreadLocalMap 变量的生命周期是和当前线程的生命周期一样长的,只有在当前线程运行结束之后才会清除掉 value,因此会导致这个 value 一直停留在内存中,导致内存泄漏
当然 JDK 的开发者想到了这个问题,在使用 set get remove 的时候,会对 key 为 null 的 value 进行清理,使得程序的稳定性提升。
当然,我们要保持良好的编程习惯,在线程对于 ThreadLocal 变量使用的代码块中,在代码块的末尾调用 remove 将 value 的空间释放,防止内存泄露。
ThearLocal 内存泄漏的根源是:
由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏
ThreadLocal 正确的使用方法:
- 每次使用完 ThreadLocal 都调用它的 remove() 方法清除数据
- 将 ThreadLocal 变量定义成 private static final,这样就一直存在 ThreadLocal 的强引用,也能保证任何时候都能通过 ThreadLocal 的弱引用访问到 Entry 的 value 值,进而清除掉
# 八股
# 9、如何理解面向对象?
面向对象 是具有对象概念的编程范式,面向对象将程序实现分为了一个个独立的对象,各个对象之间又互相调用, 将对象作为程序的基本单元抽取出来 ,提升了可维护性、可复用性、灵活性
为何需要面向对象设计思想?
在没有面向对象的时候,随着软硬件环境逐渐复杂,代码的可维护性很差,因此急需一种设计思想来提升软件的 可维护性
而面向对象的可复用性正解决了这个问题,因此面向对象逐渐成为程序开发中的主流思想
# 10、封装、继承、多态的优势
面向对象中的核心概念为:封装、继承、多态
封装 隐藏了对象内部的实现细节,对外暴露出一个接口供其他对象使用
继承 新创建的对象继承现有对象的属性和方法,提升代码复用性
多态 允许一个类有多种不同的实现形式,解耦调用方与实现方,提升程序灵活性
# 11、Java 程序从代码编写完到程序运行起来,它经历了什么样的一个过程?
Java 程序的加载是借助于 JVM 虚拟机来进行加载的,因此这里面试官考察的是 JVM 虚拟机的类加载过程
1、编写代码:开发人员编写的代码文件为 .java
文件,
2、编译代码:Java 编译器将 .java
文件编译为二进制的 .class
文件
接下来进入到 JVM 加载 .class
文件的流程,分为加载、链接、初始化三个阶段:
加载
1、通过二进制字节流加载 class 文件
2、创建该 class 文件在方法区的运行时数据结构
3、创建字节码对象 Class 对象
链接
1、验证:目的在于确保 class 文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性
主要包括四种验证:文件格式验证、元数据验证、字节码验证、符号引用验证
2、准备:为类变量(即静态变量)分配内存并且设置类变量的默认初始值,即零值。
这里不包含用 final 修饰的 static 变量,因为 final 修饰的变量在编译为 class 字节码文件的时候就会分配了,准备阶段会显式初始化
这里不会为实例变量分配初始化,类变量会分配在方法区,而实例变量是会随着对象一起分配到 Java 堆中
3、解析:将常量池内的符号引用转换为直接引用的过程
事实上,解析操作往往会伴随着 JVM 在执行完初始化之后再执行
符号引用就是一组符号来描述所引用的莫表。符号引用的字面量形式明确定义在《java虚拟机规范》的 Class 文件格式中。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型等。
初始化
虚拟机在初始化阶段才真正开始执行类中编写的 Java 程序代码
初始化阶段就是执行类构造器 <clinit>()
方法的过程,<clinit>()
是 Javac 编译器自动生成的,该方法由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并生成的,如果一个类中没有静态代码块, 也没有变量赋值的动作,那么编译器可以不为这个类生成 <clinit>()
方法
之后 JVM 执行 Java 代码的 main() 方法,程序就开始执行
# 12、类是怎么被类加载器加载的?
采用双亲委派机制进行加载,JVM 在加载类的 class 文件时,Java虚拟机采用的是 双亲委派机制 ,即把请求交给父类加载器去加载
工作原理:
- 如果一个类加载器收到了类加载请求,他并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
- 如果父类加载器也存在其父类加载器,则继续向上委托
- 如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成类加载任务,则会由自家在其尝试自己去加载
优势:
- 避免类的重复加载
- 保护程序安全,防止核心API被篡改(例如,如果我们自定义一个java.lang.String类,然后我们去new String(),我们会发现创建的是jdk自带的String类,而不是我们自己创建的String类)
扩展:哪里打破了双亲委派机制?为什么要打破?
- 在实际应用中,可能存在 JDK 的基础类需要调用用户代码,例如:SPI 就打破双亲委派模式(打破双亲委派意味着上级委托下级加载器去加载类)
- 比如,数据库的驱动,Driver 接口定义在 JDK 中,但是其实现由各个数据库的服务上提供,由系统类加载器进行加载,此时就需要
启动类加载器
委托子类加载器去加载 Driver 接口的实现
- 比如,数据库的驱动,Driver 接口定义在 JDK 中,但是其实现由各个数据库的服务上提供,由系统类加载器进行加载,此时就需要
# 13、两个全限定类名相同的类可以同时被加载吗?
两个全限定类名相同的类可以同时被加载
参考:Tomcat类加载器 (opens new window)
JVM 在确定类的唯一性时,不仅考虑类的全限定名,还考虑了类加载器。这意味着,即使两个类的全限定名相同,只要它们被不同的类加载器加载,它们就被视为不同的类
这里以 Tomcat 为例来解释 ,Tomcat 是可以部署多个应用的,那么多个应用之间就可能会出现全限定类名相同的类,如果不可以同时被加载,毫无疑问应用会出现问题
因此 Tomcat 是通过自定义类加载器类 实现了可以同时加载多个全限定类名相同的类
Tomcat 会为部署的每个应用都生成一个类加载器实例:WebAppClassLoader,这样每个应用就可以使用自己的类加载器去加载自己的类,从而达到应用之间的隔离,不会出现类冲突
Tomcat 还利用了自定义加载器实现了热部署功能: 因为 class 文件是由类加载器来完成的,如果 class 文件变更了,Tomcat 就会将原来的类加载器销毁,再创建新的类加载器来加载修改后的文件,以实现热部署功能
Tomcat 打破了 JVM 的双亲委派机制: 因为 Tomcat 为了保证应用之间类的隔离性,通过 WebAppClassLoader 加载当前应用下的 class 文件,不会委派给父类加载器,打破了双亲委派机制
Tomcat 中的类加载器如下图 ,上边的 3 个是 JVM 默认的类加载器,下边的是 Tomcat 中定义的类加载器
# 14、堆和栈有什么区别?
堆 是在 JVM 启动时创建的内存区域去实现对象、数组与运行时常量的内存分配,它是虚拟机管理最大的,也是垃圾回收的主要内存区域
栈 是存储了线程执行的方法,每个方法作为一个栈帧存储,栈帧存储了方法的局部变量表、操作数栈、动态链接和方法返回地址
等信息
栈内存为线程私有的空间,每个方法在执行时都会创建一个栈帧,执行该方法时,就会将该方法所对应的栈帧入栈
# 15、若程序OOM,该如何处理?
首先需要拿到 堆转储快照 进行分析,查看导致 OOM 的对象是否有必要存在,并且分析清除是因为哪些对象导致了 OOM
如果是内存泄漏导致 OOM,可以查看
泄露对象
到GC Roots
的引用链,找到泄露对象在哪里被引用导致无法被回收如果不是内存泄漏,那么说明内存中的对象都是存活的,导致 OOM,这时应该检查虚拟机的堆内存设置是否有向上调整的空间。并且检查是否存在
对象生命周期过长
、存储结构不合理
的情况,减少程序运行中的内存消耗。
扩展:因为存储结构不合理而导致的堆内存溢出(来自于《深入理解Java虚拟机第3版》)
举例:使用 HashMap<Long, Long>
存储大量的数据,会导致浪费大量的空间,因为 HashMap 的空间效率使用太低。
对于一个 HashMap<Long, Long>
来说,有效数据只有 Key、Value 的两个 long 型数据,占 16字节,long 数据被包装为 java.lang.Long 对象后,就分别具有 8 字节的 Mark Word、8字节的 Klass 指针、8字节的 long 型数值。两个 Long 对象组成 Map.Entry 之后,又多了 16 字节的对象头、8字节的 next 字段、4字节的 int 型的 hash 字段、4字节的空白填充(为了对齐)还有 HashMap 中对这个 Entry 的 8 字节的引用,这样实际占用的内存为:(Long(24byte) * 2) + Entry(32byte) + HashMap Ref(8byte)=88byte
,空间效率仅仅为 16byte / 88byte = 18%。
MySQL的索引结构是什么?
B+树索引的优势?
MySQL的事务?
事务的原子性如何保障的?
假设设计用户表,表的量级在百万,其中包含基本的信息,像用户ID、用户姓名、用户手机号、注册登录的用户名和用户密码。那么现在你会怎么设计这个索引?
假如查询注册日期在七天前的所有用户,你会如何设计查询语句,你会怎么建立索引?
计算机网络的七层协议分别是什么,HTTP属于哪一层?
TCP与UDP的区别
你对HashMap的了解
为什么HashMap是线程不安全的,哪个操作可能导致它线程不安全,如果仍想用你该如何解决?
ConcurrentHashMap如何保证线程安全的?
CAS是什么?
java开发过程中有遇到什么异常吗,常见的异常有哪些,检查异常和非检查异常是什么?
遇到异常如何处理?
# 算法题
leetcode 15 三数之和