面试题总结-2024面试技巧1.2 非技术面试问题回答注意点一、 java基础怎么理解面向对象?简单说说封装继承多态JAVA中的八大数据类型值传递和引用传递?== 与 equals 有什么区别?Integer a = 100;Integer b = 100; a==b 输出为?Integer a = 200;Integer b = 200; a==b 输出为?为什么?String、StringBuffer和StringBuild区别是什么?重载和重写有什么区别?什么是反射机制?什么是泛型?Spring创建的对象和自己手动new出来的对象有什么区别?接口和抽象类的区别是什么?BIO、NIO、AIO JAVA8有哪些新特性?二、java集合说说java中的集合?Java中线程安全的集合有哪些?ArrayList线程安全吗?ArrayList变成线程安全的方法有哪些?CopyOnWriteArrayList是如何实现线程安全的?HashMap实现原理介绍一下?HashMap的put流程介绍一下?ConcurrentHashMap怎么实现的?三、java多线程用过Java多线程吗?在哪用的,怎么用的?线程创建的方式有哪些?synchronized 工作原理? syncronized锁升级的过程讲一下ReentrantLock 的工作原理?ReentrantLock和synchronized有什么区别?介绍一下AQS的原理?ThreadLocal的原理?为什么会产生内存泄漏?介绍一下线程池的工作原理?有线程池参数设置的经验吗?四、java虚拟机JDK、JRE、JVM的关系?JVM的内存模型介绍一下?String s = new String(“abc”)执行过程中分别对应哪些内存区域?Java创建对象的过程?讲一下类的加载过程?什么是类加载器,类加载器有哪些?什么是双亲委派模型?请简述一下加载过程。并说一下优点?判断垃圾的方法有哪些?垃圾回收算法有哪些?标记清除算法的缺点是什么?垃圾回收器有哪些?G1垃圾回收器?CMS和G1的区别?MinorGC、MajorGC、FullGC的区别,什么场景触发FullGC?五、Spring说一下你对Spring的理解谈谈你对IOC和AOP的理解?他们的实现原理是什么?动态代理是什么?和静态代理有什么区别?JDK动态代理和CGLIB代理有什么区别?Spring如何解决循环依赖的问题?SpringIOC容器的一级二级三级缓存的作用?Spring事务的实现原理?Spring框架中都用到了那些设计模式?Spring事务在哪些情况下会失效?至少说出3种情况?SpringBean的生命周期?默认创建的Bean是单例,还有哪些类型?一共有几种?Spring AOP 和 AspectJ AOP 有什么区别?FactoryBean和BeanFactory有什么区别?Spring事务传播行为有多少种?分别是什么作用?(即问@Transactional的propagation参数)Spring的Bean默认是单例的不是线程安全的,怎么保证线程安全?SpringMVC工作流程是怎么样的?MyBatis工作流程Mybatis里的 # 和 $ 的区别?MyBatis一级缓存和二级缓存,作用域是多少?Springboot的启动类的启动流程是怎样的?为什么使用SpringBoot,SpringBoot比Spring好在哪里?SpringBoot的自动装配原理是什么?了解SpringCloud吗?说一下他和SpringBoot的区别HTTP与RPC的区别?Eureka工作原理?负载均衡策略有哪些算法?什么是服务熔断?什么是服务降级 ?线程隔离 熔断降级Dubbo工作原理?Dubbo注册中心挂了可以继续通信吗?Dubbo的执行流程Dubbo的安全性如何得到保障Dubbo中如何保证分布式事务?SpringCloud有哪些组件?怎么用的?说说"3PC"模式的过程。六、MySQL事务ACID的特性是什么?如何实现?MySQL事务的隔离级别有哪些?他们各有什么问题?什么是脏读、不可重复读、幻读?如何解决幻读?说说mysql MVCC多版本并发控制机制原理。MySQL里有哪些锁?执行一条SQL请求的过程是什么?Mysql存储引擎都有哪些? 分别有什么区别?优缺点是什么?索引是什么?有什么好处?MySQL中索引是怎么实现的?一页默认是16Kb,高度为3的B+数据最多能存储多少个数据?MySQL为什么用B+树结构?和其他结构比的优点?什么是聚簇索引,什么是非聚簇索引?他们之间有什么区别?什么是回表?索引优化详细讲讲?Mysql 什么情况下会导致“索引失效“?分别讲一下binlog、undolog、undolog?一条SQL语句的执行顺序?Explain关键字的作用?MySQL如何开启慢查询?MySQL主从复制了解吗?对于主从延迟有什么处理办法?mysql的分库和分表?常见的数据分片算法有哪些?七、RedisRedis为什么这么快?redis是单线程还是多线程?为什么?Redis数据类型有几种,说说每种对应的使用场景Zset底层是怎么实现的?Redis是单线程还是多线程?Redis持久化策略有几种?分别的优缺点是什么?Redis的内存淘汰策略?Redis的过期删除策略?Redis集群模式了解吗?什么时候进入fail状态?为什么Redis最大槽数是16384?哨兵机制的原理是什么?为什么Redis比Mysql快?Redis应用场景有哪些?Redis分布式锁的实现原理?Redis实现分布式锁存在哪些问题?如何解决?如何保证Redis和mysql数据缓存一致性问题?缓存穿透、击穿、雪崩是什么?怎么解决?布隆过滤器的原理了解吗?八、RabbitMQ消息中间件使用场景?五种基本的消息模式?交换机的种类有几种?常用的几种?什么叫死信队列和死信交换机?什么叫ttl?延迟队列的原理?消息成为死信的条件?如何保证消息不丢失?如何防止消息堆积/累计?集群分为几种?有什么区别?什么叫幂等性?幂等性问题如何解决?RabbitMQ怎么防止重复消费?九、ElasticSearch什么是正排索引?什么是倒排索引?十、zookeeper十一、数据结构冒泡排序算法快速排序算法十二、项目相关注册登录业务的开发?如何设计的?鉴权服务的开发?什么是RBAC?如何实现动态URL权限认证?怎么保证同一时间仅支持同一设备登录?在线人数统计?强制下线功能?简历服务的开发?怎么解决分布式事务数据一致性问题?缓存穿透、击穿、雪崩问题如何解决的?企业服务的开发?三级行业分类树如何设计?采用了哪种缓存同步策略?数据字典缓存为什么要使用Canel监听?工作服务的开发?为什么要引入多级缓存架构?如何设计与实现的?项目亮点是什么?你在项目中遇到的难点?怎么解决的?电商库存系统的防超卖和高并发扣减方案
面试前
面试中
面试后
做下简单的自我介绍?
为什么从上个公司离职?
你对加班的是怎么看的?
业务发展好、公司快速发展,项目紧急等加班非常乐意
自己也会合理评估工作,提高个人工作效率
有没女朋友,家住在哪里?
考查是否上班太远了,浪费时间,且容易换工作
你对自己未来职业规划是怎样的?
你认为自己有什么缺点?
平时有什么爱好或者兴趣?
你期望的薪资是多少?
薪酬最好写个范围,也看公司具体有哪些福利,比如餐补、交通、住房补助等,是否有13薪或者其他福利
你还有其他问题想问的吗?
面向过程强调以过程为核心,关注问题解决的步骤和流程,将问题分解为一系列的步骤,通过函数实现这些步骤并依次调用。
面向对象强调以对象为核心,通过对象来模拟现实世界中实体和它们之间的关系。面向对象的设计关注的问题本质和对象之间的交互。
java面向对象的三大特性包含:封装、继承、多态:
xxxxxxxxxx
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false 因为不在静态缓存池范围内,所以会重新new一个字符串对象实例。因此结果为false
// 为什么c == d 输出结果为false?
/**
* java 的Integer类内部实现了一个静态缓存池,用于存储特定范围内的整数值对应的Integer对象
* 默认情况下,这个范围是-127至128。当通过Integer.valueOf(int)方法创建一个在这个范围内的整数对象时,
* 并不会每次都生成新的对象实例,而是复用缓存池中的现有对象,会直接从内存中取
**/
反射的本质Java程序在运行期间通过class对象,反向获取类对象的各种信息的一个过程
java获取类的3种方式:1、Class.forName("类名") 2、类名.class。3、对象.getClass()
反射具有以下特性:
泛型是为了将类型参数化,在编译时保证数据类型的一致性。
BIO模型也称(blocking-io)同步阻塞IO模型。Acceptor监听到客户端的连接后会为每一个客户端创建一个新的线程进行处理,因此每个请求都需要有一个线程来处理,属于一种典型的一请求一应答模型,服务端的线程个数和客户端并发访问数呈1:1的正比关系,当并发量很大的时候会造成线程数过多导致系统宕机。
NIO模型也称(no blocking-io)同步非阻塞IO模型,是jdk1.4引入的包。基于Reactor模型来实现的,NIO通过使用单线程轮询多个连接的的方式来实现高效的处理方式。通过Selector(I/O多路复用)轮询大量的Channel,每次都会获取一批有事件的Channel,然后对每个请求启动一个线程处理即可。这种方式是非阻塞的,Selector一个线程就可以不停轮询channel,所有客户端请求都不会阻塞。
AIO(Asynchronous IO)也叫异步非阻塞IO模型。AIO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会阻塞在那,当后台处理完成后操作系统会通知相应的线程进行后续操作。
设想你要烧水这样一个场景:
在JDK1.7之前,HashMap的数据结构是数组+链表,在JDK1.8之后使用的是数组+链表+红黑树。
HashMap的put流程:
ReentrantLock是一个可重入锁,底层依赖CAS+AQS实现。
CAS就是比较并交换的意思,它里面有三个参数,分别是内存值、预期值、要修改的值。只有当内存值和预期值相等的时候才进行修改。这个操作是依赖于CPU底层指令的原子性操作。
AQS是一个构造锁和同步容器的框架。底层使用了一个先进先出的等待队列和一个volatile修饰的int类型的State变量
执行过程如下:
根据JVM8的规范,JVM运行时内存共分为java虚拟机栈、本地方法栈、方法区、堆、程序计数器五个部分
java虚拟机栈是每个线程私有的,随着线程的创建而创建。栈里面存着一种叫栈帧的东西,每个方法都会创建一个栈帧,栈帧中存放了局部变量表、操作数栈、方法出入口等信息。
本地方法栈和Java虚拟机栈类似,区别是Java虚拟机栈执行Java方法,本地方法执行native方法。
方法区是线程共享的区域,JDK7之前也叫永久代,存储已经被虚拟机加载的类信息、常量、静态变量等数据。在JDK8之后叫元空间,元空间属于本地内存由操作系统直接管理。
堆内存是JVM所有线程共享的部分,所有的对象和数组都在堆上进行分配。堆是JVM内存占用最大,管理最复杂的一个区域。在JDK1.8后,字符串常量也存放在堆中。
程序计数器是当前线程所执行的字节码行号指示器,是线程私有的内存空间。
在Java中创建对象的过程包含:
加载、链接(验证、准备、解析)、初始化
加载:
验证:验证的目的是确保class文件的字节流中的信息不会危害到虚拟机,主要完成四种验证
准备:在方法区为类的静态变量分配内存并初始化,准备阶段不分配类中实例变量的内存,实例变量的内存会在对象实例化时随着对象一起分配在java堆中
解析:该阶段主要完成符号引用到直接引用的跳转动作,解析动作不一定在初始化完成之前,也可能在初始化完成之后
初始化:真正开始执行类中定义的java程序代码
通过类的全限定名获取该类的二进制流的代码块叫做类加载器
有四种加载器
标记清除算法。标记清除算法先标记再清除,首先通过可达性分析,标记出所有需要回收的对象,然后统一清楚所有标记的对象。标记清除算法有两个缺陷。
复制算法。复制算法将内存分为两块,每次申请内存时都使用其中的一块,当内存不够时,将这一块内存中所有存活的复制到另一块上,然后再把已使用的内存整个清理掉。复制算法解决了空间碎片问题,但是带来了新的问题。
标记压缩算法。标记压缩算法的标记过程与标记清除算法的标记过程一致,但标记之后不会直接清理。而是将所有存活对象移动到内存的一端。移动结束后直接清理掉剩余部分。
分代回收算法。分代收集是将内存划分成新生代和老年代。分配的依据是对象的生存周期,或者说经历过的GC次数。对象创建时,一般在新生代申请内存,当经历一次GC之后如果对象还存活,那么对象的年龄+1,当年龄超过一定值(默认是15)后,该对象会进入老年代。
动态代理:通过提供一个代理类,让我们在调用目标方法的时候,不再直接对目标方法进行操作,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来,调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,AOP底层是动态代理,如果是接口采用JDK动态代理,如果是类采用CGLIB方式实现动态代理。
区别:
首先,Spring解决循环依赖有两个前提条件:
本质上解决循环依赖的问题就是三级缓存,通过三级缓存提前拿到未初始化完全的对象
Spring中的缓存
假设一个简单的循环依赖场景,A、B互相依赖。
A类中有个B类的属性, B类中有个A类的属性,启动是否报错?
A类的构造函数中new了个B类,B类的构造函数中new了个A类,启动是否报错?
为什么要三级缓存?二级不行吗?
Spring事务的本质是Spring AOP和数据库事务,Spring将数据库的事务操作提取为切面,通过AOP在方法执行前后增加数据库的事务操作。Spring在以下情况下事务会失效。
SpringBean的生命周期简单概括为4个阶段:
实例化。Spring容器根据Bean的定义,使用反射机制实例化Bean对象
属性赋值。一旦Bean实例化完成,Spring容器会通过依赖注入(DI)的方式开始对Bean的属性进行赋值。
初始化
使用
销毁
Spring AOP是基于动态代理实现,属于运行时增强,AspectJ AOP则属于编译时增强
AspectJ属于编译时增强,主要有三种方式
总结:Spring AOP只能在运行时织入,不需要单独编译,性能相比AspectJ编译织入的方式慢,而AspectJ只支持编译前后和类加载时织入,性能更好,功能更加强大。
Spring容器中大部分Bean都是无状态(不会保存数据)的,在某种程度上来说也是线程安全的
Bean有状态的情况下,
SpringBoot是用于构建单体应用的框架,而SpringCloud则是用于构建分布式系统中的微服务架构的工具,SpringCloud提供了服务发现、远程调用、负载均衡、断路器、网关等功能
线程隔离
熔断降级
可以,因为刚开始初始化的时候,消费者会将提供者的地址等信息拉取到本地缓存,所以注册中心挂了可以继续通信。
项目一启动,加载配置文件的时候,就会初始化,服务的提供方ServiceProvider就会向注册中心注册自己提供的服务,当消费者在启动时,就会向注册中心订阅自己所需要的服务,如果服务提供方有数据变更等,注册中心将基于长连接的形式推送变更数据给消费者。
三个阶段过程
阶段一:CanCommit
阶段二:PreCommit
事务协调者根据参与者的响应,如果阶段一返回的都是YES则执行事务预提交,如果阶段一返回有NO则中断事务操作
事务预提交:
中断事务:
阶段三:do Commit
执行提交:
中断事务:中断事务是因为出现了异常,比如协调者一方出现了问题,或者是协调者与参与者之间出现了故障。过程如下:
3PC协议的优点
脏读:如果一个事务读到了另一个未提交的事务修改过的数据,就意味着发生了脏读。
不可重复读:最开始读到的数据和事务结束前的任意时刻读到的同一批数据出现不一致的情况,就意味着发生了不可重复读现象。
幻读:一个事务在前后两次查询同一个范围时,由于其他事务的插入或删除操作,导致后一次查询的记录数量发生了变化。就意味着发生了幻读现象。
MVCC是基于undo log和ReadView(读视图)来实现事务隔离的,在执行update和delete操作时不会更改原始数据,而是会将每次操作详细记录在undoLog版本链表中,每一行记录都会有一个版本链表,记录了这条记录的所有变更。
读取数据的时候是用读视图来做可见性判断的,具体流程如下:
InnoDB:
MyISAM:
Memory引擎
实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在 2~4 层。MySQL 的 InnoDB 存储引擎在设计时是将根节点常驻内存的,也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作。
那为什么索引不用hashmap而用B+树?
为什么索引要用B+树而不用B树?
前缀索引优化。使用前缀索引是为了减小索引字段的大小,可以增加一个索引页中存储的索引值,有效提高索引的查询速度。在一些大字符串的字段作为索引时,使用前缀索引可以帮助我们减少索引项的小大。
利用覆盖索引来进行查询操作,来避免回表操作
尽量不要使用select * 来做查询。原因:如果主键索引是学号,另外我们还根据手机号也建了索引,如果我们where条件是手机号,分两种情况:
主键索引自增。使用自增主键可以让插入的新数据按照顺序添加到当前索引节点的位置,不需要重新移动已有的数据,提高效率。
业务上具有唯一特性的字段,即使是组合字段,也建成唯一索引。
超过三张表禁止join;join的字段数据类型保持绝对一致;多表关联的字段需要有索引
禁止使用左模糊或者全模糊,如果需要走搜索引擎来解决。
建组合索引的时候,区分度最高的放在最左边
SQL性能优化的目标至少达到range级别,要求是ref级别,如果可以是const最好
关联查询优化
注:驱动表是主表的意思,例如select* from t_emp t1 left join t_dep t2 on t1.id = t2.id , t1是驱动表,t2是被驱动表
答:from->where->group By->聚合函数->having分组->计算表达式->select->order By
Explain可以查询SQL的执行计划,主要用于分析SQL语句的执行过程,比如有没有走索引,有没有外部排序,没有没索引覆盖等等。
只针对select查询,可以显示出大概的执行流程和索引命中的情况。explain出来的信息id、type(数据扫描类型)、possible_key(可能用到的索引)、key(实际用到的索引)、key_len(索引的长度)、rows(扫描的数据行数)、ext
id: id相同,可以认为是同一组,执行顺序由上而下,id不同,id越大优先级越高。
type:
key_len:表示索引使用的字节数,根据这个值可以判断索引的使用情况
计算逻辑:
filtered:这个字段表示存储引擎返回的数据在server层过滤后,剩下多少满足查询的记录数量的比例,注意是百分比,不是具体记录数
Extra
MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过阀值的语句,具体指运行时间超过long_query_time值的SQL,则会被记录到慢查询日志中。
如何开启慢查询语句?
long_query_time
来定义构成慢查询的阈值,默认是10s。slow_query_log
为1(或ON
)并指定日志文件位置。log_queries_not_using_indexes
来记录所有没有使用索引的查询。分库是指按照业务将数据划分到多个独立的数据库中,每个数据库只负责存储部分数据,解决并发连接过多,单机mysql扛不住的问题
分表是将数据库中的表拆分成多个表,每个表只负责存储一部分数据,减轻单个表的压力,分表主要解决单表数据量太大,查询性能下降问题。常见的分库分表策略有垂直分库、垂直分表、水平分库、水平分表等。
常见的数据分片
Redis的读写操作都在内存,当Redis断电后,内存中的数据就会丢失,为了保证内存中的数据不丢失,Redis实现了数据持久化的机制,这个机制会把数据存储到磁盘。Redis提供AOF日志和RDB快照两种数据持久化方式
AOF日志是指每执行一条写操作命令,以命令追写的形式写入到一个日志里
Redis提供了三种写回硬盘的策略,在Redis.conf配置文件中可以配置,分别是
RDB快照是指将某一时刻的内存数据,以二进制的方式写入磁盘。
RDB(数据快照模式)默认开启的时RDB模式:定期存储,保存的是数据本身,存储文件是紧凑的
因为AOF日志记录的是操作命令,不是实际的数据,所以用AOF方法做故障恢复时,需要全量把日志都执行一遍,一旦AOF日志非常多,势必会造成Redis恢复操作缓慢,为了解决这个问题,Redis增加了RDB快照。
Redis提供了两个命令来生成RDB文件,分别是save和bgsave,他们的区别就在于是否在主线程里执行。
AOF的优点
AOF的缺点
RDB的优点
RDB的缺点
Redis的内存淘汰策略是在内存满了的时候,Redis会触发内存淘汰策略来淘汰一些不必要的内存资源
Redis的过期删除策略是将已过期的键值进行删除,Redis采用的删除策略是惰性删除+定期删除这两种策略搭配和使用,以求在合理使用CPU时间和避免内存浪费之间取得平衡。
惰性删除策略:
定期删除策略:
当Redis缓存数据量大到一台服务器无法缓存时,就需要使用Redis Cluster切片集群方案,Redis Cluster切片集群方案将数据分布在不同的服务器上以此降低单节点的依赖,提高Redis服务的读写性能。
Redis Cluster方案中,一个切片集群有16384个哈希槽,每个键值对都会根据key按照一定的算法分配到一个哈希槽中,具体执行过程如下:
redis集群去中心化:集群中所有的主节点都可以进行读写数据,不分主次,即Redis集群是去中心化的
集群进入fail状态的条件:
Redis最大的集群槽数是16384的原因:
在Redis的主从架构中,由于主从模式是读写分离的,如果master挂了,那么将没有master来服务客户端的写操作,也没有master给slave进行数据同步了。
这时需要恢复的话,需要人工进行切换,比较麻烦。因此在Redis2.8版本以后提供哨兵机制实现主从节点故障转移。哨兵节点会监测主节点是否存活,如果发现主节点挂了就会选举一个从节点切换为主节点,并且把新的主节点信息通知给从节点和客户端。
故障转移过程:
应用场景:
Redis分布式锁实现原理:
Redis实现分布式锁存在的问题:
xxxxxxxxxx
// 出现死锁的情况,可以在设置锁的时候设置一个过期时间
// 释放锁出现误删的问题,可以生成一个uuid,进行判断再删除
// Q2: 删除分布式锁问题
// 极端情况:
// A线程判断成功后,还未执行删除锁操作,锁恰好过期了;
// B线程获得分布式锁后,此时A线程开始执行删锁操作,把B线程的锁误删了
答Q2:判断和删除要原子操作,这里采用LUA脚本保证判断锁和删除锁是一个原子性操作。
// Q3: 分布式锁的过期时间问题:如果业务执行时间比我们预估的过期时间比要更长,此时会发生什么?
// 极端情况:
// 假设业务代码执行需要35s,过期时间我们设置为30s;
// 线程A分布式锁在30s的时候就自动过期了
// 此时B线程进来,AB两个进程操作就会存在依旧有共享资源的问题
答Q3:采用Redisson来实现分布式锁,实现自动续期机制。Redisson是可重入锁,所以无需重试,当前线程重入了多少次就需要释放多少次,默认非公平加锁,此处不需要设置过期时间,有默认过期时间并且会续期。Redisson内部提供了一个监控锁的看门狗,在redisson被关闭之前,不断延长锁的有效期,默认情况下,看门狗检查锁的时间是30s,可以通过config.setLockWatchdogTimeout(30000)来设置redisson看门狗检查锁的超时时间,如果设置了过期时间,则无看门狗机制
对于读数据,如果Cache中不命中,会从DB加载数据到Cache。对于写数据,先更新数据库,再删除缓存。
根据C(一致性)A(可用性)P(分区容错性)理论,缓存是通过牺牲强一致性C来提高性能。缓存系统适用的场景就是非强一致性的场景,保证了C(一致性)A(可用性)P(分区容错性)理论中A(可用性)P(分区容错性)
所以使用缓存提升性能必定会有数据更新的延迟,这需要我们根据自身的业务对缓存的过期时间进行合理评估。缓存过期时间太短请求可能会比较多的落到数据库上,这也意味着失去了缓存的优势;缓存过期时间设置太长的话系统会长时间读取脏数据。
我们可以通过一些方案进行优化,达到最终一致性效果。
问题:更新数据库成功,没来得及删除缓存或者删除缓存失败,那么其他线程从缓存中读取到的就是旧值,缓存不一致发生,怎么解决?
总结:删除缓存的两种方案:
生产者先发送消息到交换机,在发送给消息队列,这个链路中每个过程都有可能出错,导致消息没有正常发送
RabbitMQ有两种方式来控制消息的可靠性。分别是ConfirmCallback机制和ReturnCallback机制。
生产者:开启消息的确认机制。确保消息到达RabbitMQ
xxxxxxxxxx
publisher-confirm-type correlated # 发布消息成功到交换器后会触发回调方法
publisher-returns true # 交换机发送消息到队列后会触发回调方法
RabbitMQ:开启RabbitMQ持久化,交换机设置持久化、队列设置持久化、消息持久化,设置发送模式deliveryMode=2,代表持久化消息
消费者:ACK确认机制。关闭rabbitMQ的自动ACK,改为手动,这样可以避免消息还没处理完就ACK。
普通集群:默认的集群模式,某个节点挂了,该节点上的消息就不可用了,受影响的业务瘫痪,只能等待节点恢复重启可用(必须持久化消息情况下)
镜像集群:把需要的队列做成镜像队列,存在于多个节点,属于RabbitMQ的HA方案,三种HA策略模式:
1)同步至所有的 2)同步最多N个机器 3)只同步至符合指定名称的nodes
HA 镜像队列有一个很大的缺点就是: 系统的吞吐量会有所下降
网络延迟传输中,消费者出现异常或者消费者延迟消费,会造成进行MQ重试补偿,在重试过程中,可能会造成重复消费。
Zookeeper的工作机制?应用场景?ZK的特点是什么?
ZK从字母意思去理解就是动物管理员,是一个分布式环境协调组件。
ZK可以说是一种以观察者模式对节点中数据进行监听,如果节点数据发送变化,则发起事件变更通知。
应用场景:
ZK的特点
zookeeper中什么叫临时节点?永久节点?顺序节点?
zookeeper如何进行数据同步?
zookeeper如何实现分布式锁?
ZK实现分布式锁的核心原理是同一个节点只能被创建一次,如果第二个用户无法创建,表示当前业务被其他用户锁住了,直到第一个用户删除节点释放锁才能让后续线程创建新的锁
具体步骤:
xxxxxxxxxx
/**
* 对整数数组进行冒泡排序的测试方法。
* 冒泡排序是一种简单的排序算法,它重复地遍历要排序的数列,一次比较两个元素,
* 如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,
* 也就是说该数列已经排序完成。
*/
public void popSort() {
// 待排序的整数数组
int[] arrays = {1, 10, 7, 8, 9, 4};
// 外层循环控制遍历的次数
for (int i = 0; i < arrays.length - 1; i++) {
// 内层循环进行相邻元素的比较和交换
for (int j = i + 1; j < arrays.length; j++) {
// 如果前一个元素大于后一个元素,则交换它们的位置
if (arrays[i] > arrays[j]) {
int temp = arrays[i];
arrays[i] = arrays[j];
arrays[j] = temp;
}
}
}
// 打印排序后的数组
log.info(Arrays.toString(arrays));
}
xxxxxxxxxx
/**
* 快速排序测试方法
*/
public void quickSort() {
int[] arr = {3, 7, 8, 5, 2, 1, 9, 5, 4}; // 待排序数组
quickSort(arr, 0, arr.length - 1); // 调用快速排序方法
log.info(Arrays.toString(arr)); // 输出排序后的数组
}
/**
* 快速排序方法
*
* @param arr 待排序数组
* @param left 排序部分的左边界
* @param right 排序部分的右边界
*/
private void quickSort(int[] arr, int left, int right) {
if (left < right) {
int pivotIndex = partition(arr, left, right); // 获取分区后的基准元素索引
quickSort(arr, left, pivotIndex - 1); // 对基准元素左边的子数组进行快速排序
quickSort(arr, pivotIndex + 1, right); // 对基准元素右边的子数组进行快速排序
}
}
/**
* 分区方法,用于将数组分区,并返回基准元素的索引
*
* @param arr 待分区数组
* @param left 分区的左边界
* @param right 分区的右边界
* @return 基准元素的索引
*/
private int partition(int[] arr, int left, int right) {
int pivot = arr[right]; // 选择最右边的元素作为基准元素
int i = left - 1;
for (int j = left; j < right; j++) {
if (arr[j] <= pivot) { // 如果当前元素小于或等于基准元素,则交换
i++;
swap(arr, i, j);
}
}
swap(arr, i + 1, right); // 将基准元素放到正确的位置
return i + 1;
}
/**
* 交换数组中两个元素的位置
*
* @param arr 待交换元素的数组
* @param i 第一个元素的索引
* @param j 第二个元素的索引
*/
private void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
xxxxxxxxxx
熟练掌握Java基础,理解OOP编程思想,对类的加载机制、集合、多线程、IO流、反射、泛型、JAVA8新特性有深入的理解,具备良好的编码习惯。
熟练掌握SpringMVC、Spring、MyBatis三大框架整合,理解SpringMVC的运行流程,理解Spring框架IOC和AOP两大核心思想,能够使用MyBatis逆向工程和通用Mapper快速高效开发。
熟练使用MySQL数据库及JDBC编程事务,了解数据库的四种隔离级别,可解决脏读、幻读、不可重复读等问题,可保证数据库事务的ACID特性,具备Explain查看SQL语句执行流程,并建立高效索引对SQL语句进行优化。
熟练使用非关系型数据库Redis作为缓存工具,熟悉常用的数据类型及其使用场景,理解持久化、主从复制、Cluster等机制,能够基于Redis实现分布式锁、并解决缓存穿透、缓存雪崩、缓存击穿等缓存常见问题。
熟练使用SpringBoot、SpringCloud搭建微服务架构,理解SpringBoot自动化配置原理及独立打包部署,掌握SpringCloud中Eureka、Ribbon、Hystry、Feign、Gateway等组件的使用。
熟练使用ElasticSearch作为模板引擎,理解倒排索引原理,能够使用IK分词器对关键字进行匹配搜索,能够使用Kibana远程测试并调优DSL语句。
熟练使用RabbitMQ作为消息中间件,实现系统间的异步消息管理,了解死信队列、延迟队列等机制,能够基于RabbitMQ实现分布式事务。
熟悉JAVA并发包下常用工具类的使用,了解常用的锁机制以及CAS原理。
熟悉JVM内存模型和GC回收机制,了解对JVM内存空间及GC算法相关参数进行调优。
熟悉Docker容器技术,能够基于Docker对常用组件进行安装部署。
能够使用Git进行项目版本管理,使用Maven进行项目依赖管理及构建。
熟练掌握Linux常用命令,Docker容器化技术,能够基于Linux安装常用组件。
了解HTML、CSS、JavaScript、Vue、Thymeleaf等前端开发技术。
(一)招聘云SaaS系统
项目描述:
招聘云SaaS系统分为用户平台、企业管理平台、运营管理平台三大平台。整个项目基于微服务进行开发,项目分为用户服务、文件服务、简历服务、聊天服务、工作服务、企业服务、招聘服务、鉴权等多个微服务。整个项目采用前后端分离模式进行开发。
核心技术:SpringBoot、SpringCloud、Mybatis-Plus、MySQL、Redis、SpringSecurity、RabbitMQ、Elasticsearch、Canel、Zookeeper、OpenResty、ELK等
项目架构:
1.网关接入层采用云负载均衡器CLB,Nginx网关层采用OpenResty多级缓存架构,微服务网关集群采用SpringCloud GateWay进行统一搭建;
2.基于Nacos作为服务的注册中心和动态配置中心,对各个微服务进行统一的管理与配置;
3.基于SpringBoot+SpringCloud开发,采用OpenFeign进行服务间的RESTFul通信;
4.项目前后端分离,基于SpringSecurity+JWT进行授权认证,RBAC权限模型进行设计;
5.数据库采用MySQL搭建,MyBatis-Plus作为微服务持久层框架进行数据操作;
6.分布式缓存采用Redis,本地缓存基于Caffeine,采用MongoDB作为非关系型数据库进行信息快照的处理;
7.消息队列采用RabbitMQ作为消息中间件,实现异步解耦、延迟队列等功能;
8.基于Zookeeper进行分布式协调处理,实现了节点的动态监听和动态配置;
9.项目基于ELK等工具对日志进行收集、分析和可视化;
主要职责:
1.负责注册登录业务的开发。完成APP手机端一键注册登录、用户扫码登录、加盐登录等功能的开发,集成第三方短信SDK,基于RabbitMQ实现短信的异步发送,生成二维码令牌供用户进行扫描,用户扫码后生成预登录令牌并定期检查扫码状态完成登录页面跳转。
2.参与鉴权服务的开发。基于RBAC用户角色接口访问权限控制管理端脚手架开发,并应用于多个业务系统。支持动态URL权限认证,基于Redis解决JWT无法手动设置过期时间的弊端,保证同一用户在用一时间仅支持同一设备登录,以及在线人数统计、强制下线等功能的开发。
3.负责简历服务的开发。新用户简历初始化。借助本地消息表记录消息+ThreadLocal本地线程储存消息ID,自定义事务管理器批量发送消息至RabbitMQ,并手动ACK确认机制保证消息的可靠性投递,解决分布式事务数据的一致性问题;缓存用户简历信息进Redis,提升App端接口查询的性能,并解决缓存穿透、击穿、雪崩等问题。
4.负责企业服务的开发。完成三级行业分类树功能的开发,结合RabbitMQ延迟队列批量更新行业分类缓存数据,优化了缓存同步策略;完成数据字典相关接口的开发,基于CompletableFuture多线程异步编排优化数据字典查询接口,提升了接口的访问性能,并结合Canel异步监听数据变更实现缓存数据的同步刷新。
5.参与工作服务的开发。SpringBoot自启动时将系统配置提前进行缓存预热,结合Caffeine本地缓存+OpenResty共享字典缓存+Redis分布式缓存+Lua脚本实现多级缓存架构,将缓存前置网关,提升简历刷新接口响应时间50%,并基于CuratorZK实现服务集群节点的动态监听,实现缓存数据的同步。
6.参与检索服务的开发。基于RabbitMQ将简历信息同步至Elasticsearch,基于IK分词器对关键词进行匹配搜索,整合Elasticsearch实现了简历的复杂搜索,提升了检索效率。对数据可视化业务进行分析,基于Aggs统计聚合实现不同维度图表的数据可视化功能。
运营管理平台通过超级管理员Admin在后台添加用户,分配角色和权限点,登录时校验用户名和密码,生成带有平台前缀的JWT返回给前端
APP用户我们是使用短信一键注册登录,若用户没有注册过,对用户手机验证码校验通过后创建一个新用户并初始化用户简历信息。同时生成带有平台前缀的JWT令牌返回给前端;若用户注册过,则直接返回用户信息和令牌给前端
企业SAAS管理平台通过APP端扫码登录,扫码登录设计流程如下:
RBAC也叫基于角色的访问控制,抽象的概括就是谁是否可以对什么内容进行怎样的访问操作
在RBAC模型中,有三个基础组成部分,分别是用户、角色和权限。在项目中,我们设计了用户表、角色表、权限表。表与表通过中间表进行关联。一个用户可以分配多个角色,一个角色下可以分配多个权限。实现根据用户的角色来判断用户是否有权限访问特定资源的操作。具体操作步骤如下:
由于JWT无法手动失效,因此在项目中我们引入Redis存放JWT,在过滤器中会将当前请求的JWT与Redis中的JWT进行比对,若不一致则代表用户已注销或者用户在不同平台进行登录。以此来达到同一时刻仅支持同一设备进行登录的效果。
在线人数统计只需要根据JWT相关的键查询用户名列表,然后根据用户名列表查询在线用户返回给前端
强制下线功能主要根据前端传过来的用户列表,清除Redis中与这些用户列表相关的JWT信息即可
xxxxxxxxxx
参与工作服务的开发。SpringBoot自启动时将系统配置提前进行缓存预热,结合Caffeine本地缓存+OpenResty共享字典缓存+Redis分布式缓存+Lua脚本实现多级缓存架构,将缓存前置网关,提升简历刷新接口响应时间50%,并基于CuratorZK实现服务集群节点的动态监听,实现缓存数据的同步。
缓存是为了降低数据库崩溃的风险,但当我们流量激增的时候,达到千万级甚至亿级别流量时,缓存失效的情况下瞬间高并发流量会直接击穿数据库,因为我们的防护屏障只有Redis这一层,因此我们引入了多级缓存架构,更好的保护系统的抗风险能力。
多级缓存架构的本质就是在请求的各个节点处添加缓存,分摊tomcat的压力来提升整体的接口访问性能,因此我们引入了多级缓存架构
设计与实现
在网关层面:
在微服务层面:
我们项目的亮点就是引入了多级缓存架构,同时也引入了多级网关缓存分摊为服务集群的压力
遇到的难点
方案一:传统通过数据库保证不超卖
方案二:分离防超卖和数据持久化
扣减库存其实包含两个过程:第一步是超卖校验,第二步是扣减库存数据的持久化
实现原理是分离开防超卖和数据持久化,防超卖过程由Redis来完成,
我们可以使用Redis中Hash结构的hincrby命令来进行库存的扣减
xxxxxxxxxx
127.0.0.1:6379> hset iphone inStock 1 #设置苹果手机有一个可售库存
(integer) 1
127.0.0.1:6379> hget iphone inStock #查看苹果手机可售库存为1
"1"
127.0.0.1:6379> hincrby iphone inStock -1 #卖出扣减一个,返回剩余0,下单成功
(integer) 0
127.0.0.1:6379> hget iphone inStock #验证剩余0
"0"
127.0.0.1:6379> hincrby iphone inStock -1 #应用并发超卖但Redis单线程返回剩余-1,下单失败
(integer) -1
127.0.0.1:6379> hincrby iphone inStock 1 #识别-1,回滚库存加一,剩余0
(integer) 0
127.0.0.1:6379> hget iphone inStock #库存恢复正常
"0"
保证扣减的幂等性,通过订单号和商品防重码是否存在来决定是否扣减成功,通过setNX命令来决定扣减是否成功。
xxxxxxxxxx
// 初始化库存
127.0.0.1:6379> hset iphone inStock 1 #设置苹果手机有一个可售库存
(integer) 1
127.0.0.1:6379> hget iphone inStock #查看苹果手机可售库存为1
"1"
// 应用线程一扣减库存,订单号a100,jedis开启pipeline
127.0.0.1:6379> set a100_iphone "1" NX EX 10 #通过订单号和商品防重码
OK
127.0.0.1:6379> hincrby iphone inStock -1 #卖出扣减一个,返回剩余0,下单成功
(integer) 0
//结束pipeline,执行结果OK和0会一起返回
并发扣减后检验。为了防止并发扣减,需要对Redis的hincrby命令返回值是否为负数来判断是否发生高并发超卖,如果扣减后的结果为负数,需要反向执行hincrby把数据进行加回
单向保证。很多极端场景中,很难做到不超卖和少卖。我们选择不超卖单向保证,尽可能较少少卖概率。比如如果扣减Redis过程中,设置防重码成功,由于网络抖动扣减失败,重试的时候就会认为成功造成超卖。
通过Redis防超卖后,由任务引擎扣减库存,任务引擎通过订单号进行分库分表,将热点商品的落库分散开,消除数据库的热点。