# Java面试手册 **Repository Path**: weafafafa/java-interview-handbook ## Basic Information - **Project Name**: Java面试手册 - **Description**: 配合黑马出品八股面试视频合集食用最佳 https://www.bilibili.com/video/BV1yT411H7YK/?spm_id_from=333.337.search-card.all.click - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 0 - **Created**: 2024-08-02 - **Last Updated**: 2025-02-09 ## Categories & Tags **Categories**: Uncategorized **Tags**: Java, java面试 ## README # JAVA面试手册 本人做的Java笔记 可配套于B站 java最新面试八股视频 https://www.bilibili.com/video/BV1yT411H7YK/?spm_id_from=333.337.search-card.all.click&vd_source=d66fada2f967ba7034d518bb27d6ce40 # Redis ## ①Redis 缓存篇 ### *Redis缓存三兄弟:缓存雪崩、缓存击穿、缓存穿透* #### 1.什么是缓存穿透 查询一个**不存在**的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查询数据库 **解决方案一**:缓存数据,查询返回结果为空,仍把这个空结果进行缓存 {key:1,value:null} 优点:简单 缺点:1.消耗内存 2.不一致 **解决方案二:** 在查缓存前加一个布隆过滤器 有点:内存占用较少,没有多余key 缺点:实现复杂,存在误判 #### 2.什么是布隆过滤器 布隆过滤器通过位图(bitmap)实现:一个以位为单位的数组,数组中每个单元智能存储二进制数0或1 通过多个hash函数获取hash值,根据hash将对应位置**改为1** **作用:** 检索一个元素是否在集合中 **出现误判怎么办:** 扩大hash函数使用数量、布隆计数过滤器(利于删除) #### 3.什么是缓存击穿 给某一个key设置了过期时间,当key过期时,恰好这个时间点对这个key有大量的**并发**请求,这些并发请求会把DB瞬间压垮(DB查到数据更新缓存来不及) **解决方案一:** 互斥锁 优点:强一致 (其他线程需要等待缓存更新) 缺点:性能差 **解决方案二:** 不设置过期时间 优点:性能好 缺点:不能保证缓存和DB数据一致 #### 4.什么是缓存雪崩 同一时段大量的缓存key同时失效或者服务器宕机,导致大量请求到达DB,把DB压垮 **解决方案一:** 给不同的Key的TTL添加随机值 **解决方案二:** 利用Redis集群提高服务的可用性 **解决方案三:** 给缓存业务添加降限流策略(**保底策略**) **解决方案四:** 给业务添加多级缓存 ### *缓存一致性* #### 5.双写一致 当修改了数据库的同时也要更新换成你的数据,保证缓存和数据库一致 **读操作:** 缓存命中,直接返回;缓存未命中查询数据库,更新缓存,设定超时时间 **写操作**: 1.延时双删(【读+写并发】下不能严格保持强一致):删除缓存->更新数据库->延时->删除缓存 2.先更新数据库、再删除缓存(【读+写并发】下同样不能严格避免强一致) 本质上时删除、更新缓存的速度很快,可以和前一条操作看作一个接近原子操作。 **保证强一致的业务场景**:分布式锁、读写锁 (读和读共享,写和其他读写互斥) **允许延迟的业务场景:** 采用异步通知,1.利用MQ中间件,更新数据后,通知缓存删除 2.利用canal中间件,伪装为mysql的一个从节点,通过读取binlong数据更新缓存 ### *Redis持久化* #### 6.什么是RDB RDB Redis数据备份文件(Redis Database Backupfile),也叫Redis数据快照。 把文件内存中的所有数据记录到磁盘中。 save:Redis主进程执行RDB bgsave:另外启动一个子进程执行RDB **RDB的执行原理**:bgsave开始会fork主进程得到子进程,读时共享,写时复制。 #### 7.什么是AOF AOF 追加文件(Append Only File),写命令日志文件。 AOF重写:`set num 123` 后记录`set num 666` ,重写后会变成只记录set num 666. #### 8.AOF持久化是怎么实现的 Redis提供了三种AOF日志回写硬盘的策略:Always(执行一次redis命令进行I/O一次)、Everysec(每隔1s一次)、No(交给操作系统),可靠性上从高到底、性能上从低到高。 随着命令越多,AOF文件体积会越来越大,为了避免日志过大,Redis进入AOF重写。 **AOF重写**: 1.启动bgwriteof子进程,读时共享物理内存、若主线程修改key-value时会发生写时复制 2.子进程会根据主进程的aof_buffer,重新生成一个AOF日志,当完成后替换旧的AOF日志 3.主进程在生成子进程AOF日志时若修改key-value,则放入AOF重写缓存区,追加到子进程生成的新AOF日志。 #### 9.RDB持久化时怎么实现的 RDB会开启bgsave子进程,同样,读时共享物理内存,写时复制: 若RDB持久化期间发生了主进程修改了key-value,则写时复制,新修改的key-value数据需要等到下一次的bgsave子进程处理了。 #### 10.大key如何影响持久化? 1.AOF写入磁盘:会阻塞主线程 2.AOF重写:如果AOF过大,会触发AOF重写,子进程bgwriteof会复制父进程页表,该页表可能很大。此外,当主进程修改大key,会发生写时复制,写时复制需要时间。 3.RDB:子进程bgsave会复制父进程save页面,该页表可能很大。此外,当主进程修改大key,会发生写时复制,写时复制需要时间。 #### 11.RDB和AOF对比 **执行方式:** RDB定时对整个内存做快照,AOF记录每一次执行命令 **数据完整性:** RDB不完整,AOF相对完整 **文件大小**:RDB有压缩相对小,AOF大 **恢复:** RDB快,AOF慢 **使用场景**:RDB用于可容忍数分钟数据丢失,追求更快启动速度场景;AOF用于数据安全性较高场景 所以有一种混合持久化方案:AOF文件前半部分时RDB格式全量数据,后半部分时AOF格式的增量数据。 ### *Redis过期淘汰策略* #### 12.key过期后怎么办 **1.定时删除**:在设置key过期时间,创建一个定时时间,时间到达时,事件处理器自动执行key的删除 **优点**:对内存占用友好 **缺点**:对性能不友好 **1.惰性删除**:使用key时才检查是否过期 **优点:** 对性能友好 **缺点**:对内存占用不友好 **3.定期删除**: 每隔一段时间,对一部分key进行检查,删除里面过期的key,直到下一次检查中过期key比例小于25% **两种模式:** slow(淘汰频率固定)、fast(淘汰频率不固定) 对性能和内存占用的一种折衷方案! 因此,Redis是**惰性删除+定期删除**。 ### *Redis数据淘汰策略* #### 13.内存不够怎么办 常见4种: 1.不淘汰任何key,但是不允许写入新数据(默认) 2.LRU(Least Recently Used) 删除最久没有被使用到的key 3.LFU(Least Frequently Used)删除使用频率最少的key 4.随机删除 注意:2、3、4可以针对过期数据就行淘汰、也可以针对所有数据进行淘汰。 除此以外,针对过期数据淘汰,还可以用ttl优先淘汰更早过期的key。 ## ②Redis分布式锁篇 #### 14.如何用redis实现分布式锁 使用setnx命令 获取锁:`SET lock value NX EX 10` 释放锁:`DEL key` #### 15.如何确定redis分布式锁的过期时间 watch dog给锁续期(默认每隔10s续期一次) 没有锁的线程会忙等待 lua脚本保证redis原子性 #### 16.redisson实现分布式锁可重入 利用hash结构记录线程id和锁重入次数 | KEY | VALUE | | ---- | ----------------------------------------------------------- | | 锁名 | {field:thread1, value:0} //field为线程id,value为重入次数 | #### 17.redisson锁能解决主从数据一致性吗 为了保证强一致性,使用红锁(red lock),保证java应用同时对n/2+1个redisson服务获取相同锁操作。但是**性能太低**,可以使用**zookeeper**保证数据强一致性。 ### ③Redis集群(高并发主从、高可用哨兵) #### 18.主从同步是什么 主从集群:主节点写操作,从节点读操作,**读写分离**。 #### 19.主从同步流程 **全量同步(第一次同步)**: 1. 从节点发起请求数据同步(replication id、offset),主节点判断是不是第一次同步,replid是否一致 2. 是第一次,返回主节点的数据版本信息,replid相同 3. 从节点请求同步->主节点执行bgsave,生成RDB发送到从节点->主节点记录RDB期间所有命令repl_baklog(根据offset)->主节点发送repl_baklog中命令 **增量同步(不是第一次同步**): 1.从节点请求主节点同步数据,主节点判断是不是第一次,不是第一次,replid不相同 2.主节点从repl_backlog_buffer获取offset值后的写操作数据,发送给从节点进行数据同步 #### 20.哨兵模式是什么 主节点宕机,主从集群故障自动恢复 **监控:** 检查主节点和从节点状态 **自动故障恢复:** 如果主节点故障,会选择一个从节点变成主节点 **通知:** 通知redis客户端redis服务的变更 #### 21.哨兵选主规则 1.判断主从节点断开时长,超过指定时常排除 2.判断优先级(config里设置),越小优先级越高 3.如果优先级一样,**判断从节点offset值,越大优先级越好** 4.最后判断从节点id,越小优先级越高 #### 22.脑裂 哨兵模式中,主节点并没有宕机,而是与其他从节点网络出现问题,导致哨兵又选了一个主节点。 客户端数据仍然传给原主节点,当网络正常后,原主节点变成从节点会删除自己的本地数据,做全量同步,此时客户端之前写的数据产生了丢失。 **如何解决脑裂:** 当主节点的从节点少于1个或者主节点和从节点数据同步的延时超过5秒,客户端不能写数据到该主节点。 #### 23.分片集群 为了解决: 1.海量数据存储,一个master主节点不够 2.写并发 **解决:** 1.集群中有多个master,每个master保存不同数据 2.每个master可以有多个slave节点 3.master通过ping检测彼此健康状态(不需要哨兵) 4.客户端请求可以访问集群中任意节点,最终都会转发到正确节点(根据key哈希值和哈希槽取余,具体公式为:CRC16(key) % 16384) 注:16384的原因是ping的消息头不需要65536(8kb)那么大,同时redis集群主节点一般不会超过1000个(超过网络可能拥塞),同时能降低redis文件的压缩率。 ### ④Redis单线程 #### **24.redis是单线程,为什么还是那么快** 1.纯内存操作 2.避免线程上下文切换和避免考虑线程安全 3.使用I/0多路复用模型 #### **25.I/O多路复用是什么** redis的性能瓶颈是网络I/0 三种方式:select poll epoll(与前二者不同,会在通知用户进程时就把socket写入用户空间,不需要挨个遍历socket来判断是否就绪) 核心:通知单个线程监听多个socket,并在某个socket可读可写得到通知,避免无效等待,充分利用cpu资源 #### **26.Redis网络模型** 使用I/0多路复用+事件处理器 应对多个socket请求 连接应答处理器 命令回复处理器:多线程处理回复时间 命令请求处理器:多线程增加命令转换速度 # MySQL ## ①MySQL优化 #### **1.在MySQL中,如何定位慢查询** **方案一**:开源工具 测试工具:Arthas 运维工具:Prometheus、Skywalking **方案二**:MySQL自带慢日志 long_query_time (执行耗时超过此值则记录) slow_query_log=1 (开启慢日志) #### **2.这个SQL语句执行很慢,如何分析** **explain**命令观察select语句 possible_key 当前sql可能会使用到的索引 key 当前sql实际命中的索引 key_len 索引占用的大小 type 这条sql的连接的类型 (性能好坏为null、system、const、eq_ref、ref、range、index、all) #### **3.什么是索引** 索引是帮助MySQL高效获取数据的数据结构(有序)。在数据之外,数据库系统还维护者满足特定查找算法的数据结构(B+树),这些数据结构以某种方式引用(指向)数据。 #### **4.为什么innodb默认为B+树,B树和B+树区别** 1.B树每一层根节点需要额外存储数据,而B+树只在最底层叶子节点存储数据 2.B+树最底层叶子节点之间采用了双向链表 **优势:** 磁盘读写代价更低 (只在最底层存储数据)、查询效率B+树稳定(都需要查到最底层)、B+树便于扫库和区间查询(双向链表) #### 5.聚簇索引和非聚簇索引 聚簇索引:数据与索引放到一块,B+树保存了整行数据,有且只有一个 非聚簇索引(二级索引):数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个 #### 6.回表查询 通过二级索引找到对应主键值,到聚簇索引中查找整行数据 #### 7.覆盖索引 查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到,不需要回表查询 #### **8.MySQL超大分页怎么处理** 在数据量比较大时,limit分页查询,效率低 `select * from a limit 90000,10` 差 `select * from a,(select id from a order by id limit 90000,10) b where a.id=b.id;` 好 解决方案:覆盖索引+子查询。 #### **9.创建索引的原则** 1.表数据量大,查询比较频繁 2.查询条件(where)、排序(order by)、分组(group by)操作的字段建立索引 3.尽量选择区分度高的列作为索引,尽量建立唯一索引。 4.如果是字符串,建议前缀索引 5.尽量使用联合索引,它很多时候可以覆盖索引 6.控制索引数量,索引越多,维护索引结构代价越大,影响增删改效率 7.如果索引列不能存储null值,建表时使用not null约束 #### 10.什么情况索引失效 1.**模** 模糊查询(左模糊、左右模糊) 2.**型** 数据类型不符 3.**数** 使用内部函数 4.**空** 索引字段为null 5.**运** 运算 6.**最** 最左前缀、范围查询右边列索引失效 7.**快** 全表查询速度比索引快 **口诀**:模型数空运最快 #### **11.谈谈你对sql优化的经验** 一、表的设计优化 1.设计合适的数值(tinyint int bigint ) 2.设计合适的字符串类型(char varchar)char定长,varchar可变 二、sql语句优化 1.避免select * 2.避免索引失效 3.join优化 建议用inner join 而不用left inner join 或者 right inner join。因为内连接会对两个表优化,把小表放在外边,大表放在里表(mysql只需要少量次数的连接来连接小表)。 4.union all代替 union,union 会过滤一次重复的行 三、主从复制、读写分离 如果读多写少,可以读和写分离(和redis类似) ## ②MySQL事务 #### 12.什么是事务 事务是一组操作的集合,它是一个不可分割的工作单位,要么同时成功,要么同时失败 #### 13.事物的特性 原子性、一致性、隔离性、持久性。结合案例说明。 #### 14.并发事务带来哪些问题,如何解决 并发事务问题:脏读、不可重复读、幻读 **脏读:** 一个事务读到另一个事务还没有提交的数据。 **不可重复读:** 一个事务先后读同一个记录,但两次的数据不同。 **幻读:** 真实的数据行发生改变(插入或者删除),而读感知不到。 解决办法:4种隔离级别 **未提交读:** 出现脏读、不可重复读、幻读 **读已提交:** 出现不可重复读、幻读 **可重复读:** 出现幻读 **串行化:** 不会出现问题 #### 15.事务的隔离性如何保证 1.排他锁:一个事务获得了一个数据行的锁,其他事务不能获取该行排他锁 2.mvcc:多版本并发控制 #### 16.事务中的隔离级别是如何保证的呢? MVCC。维护一个数据的多个版本,使读写操作没有冲突。 - 隐藏字段: trx_id(事务id),记录每一次操作的事务id,是自增的 roll_pointer(回滚指针),指向上一个版本的事务版本记录地址 - undo log: 回滚日志:存储老版本数据 版本链:多个事务并行操作某一行记录,记录不同事物修改数据的版本,通过roll_point形成链表 四个字段:活跃事务集合、最小事务、最大事务、当前事务。 - readView 根据readView的匹配规则和当前的一些事务id判断该访问哪个版本的数据 不同的隔离级别快照读是不一样的,最终访问界别不一样 读已提交:每一次执行快照读生成readView 可重复读:仅在事务第一次执行快照读时生成readView,后续复用 #### 17.事务的持久性、一致性、原子性如何保证 **undo log 和redo log的区别** redo log:记录的是数据页的物理变化,服务器宕机可以用来同步数据 undo log:记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据 redo log保证了事务的持久性,undo log保证了事务的一致性和原子性 ## ③MySQL其他 #### 18.MySQL主从同步 MySQL主从复制的核心就是二进制日志**Binlog** 1.主库事务提交,会把数据变更记录在Binlog中 2.从库读取Binlog,写入从库中继日志relay Log 3.从库重做中继日志中的事件,将改变反映它自己的数据 #### 19.分库分表 单表1000W或者超过20G 1.水平分库 将一个库的数据拆分到多个库中,解决海量数据存储和高并发的问题 2.水平分表 解决单表存储和性能的问题 3.垂直分库 根据业务进行拆分,高并发下提高磁盘IO和网络连接数 4.垂直分表 冷热数据分离 #### 20.MySQL有哪几种锁 1.全局锁: ```sql flush tables with read lock ``` 2.表级锁:表锁、元数据锁(MDL)、意向锁、AUTO-INC锁 3.行级锁:记录锁(Record Lock)、间隙锁(Gap Lock)、临键锁(Next-Key Lock) # 框架篇 #### 1.Spring框架的单例bean是线程安全的吗 **不是线程安全** Spring的@Scope注解默认值是singleton单例 因为一般bean中注入无状态对象,没有线程安全问题,如果在bean中定义可修改的成员变量,需要考虑线程安全问题,可以考虑多例或者加锁。 #### 2.什么是AOP **AOP:** 面向切面编程,公共模块复用,降低耦合 **AOP常见用途**:记录操作日志、缓存、spring实现事务,拦截器 它们使用aop的环绕通知around aop一般涉及的注解:@Aspect、@PointCut、五种通知方式 @Before、@Around、@AfterReturing、@AfterThrowing、@After **AOP实现原理**:动态代理 - 在运行时在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。 JDK 动态代理:通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口 。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类 CGLIB 动态代理:如果目标类没有实现接口,那么AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。 #### 3.**Spring中的事务如何实现的** 声明式事务,通过AOP,在业务进行前开启事务,结束之后提交或者回滚事务 #### 4.Spring中的事务失效场景 1.try_catch:事务通知只有捉到了目标抛出异常,才能后续回滚。如果目标自己处理掉,事务无法知晓 解决办法: ```java throw new RuntimeError(e) ``` 2.Spring默认只会回滚非检查异常 解决办法:配置rollbackFor属性 ```java @Transactional(rollbackFor=Exception.class) ``` 3.Spring为方法创建代理、添加事务、前提是方法public 解决办法:方法设置为public 4.Spring方法自调用@Transactional方法失效 解决办法:外部引用,注入自身代理类,把@Transactional方法变成一个Service #### 5.Spring的生命周期(构造+赋值分离) 1.调用BeanDefinition获取bean的定义信息 2.调用构造函数实例化bean 3.bean的依赖注入 4.处理Aware接口(BeanNameAware、BeanFactoryAware、ApplicationContextAware) 5.Bean的后置处理器BeanPostProcessor-前置 6.初始化方法(InitializingBean、init-method) 7.Bean的后置处理器BeanPostProcessor-后置 8.销毁bean #### 6.Spring的循环引用 循环依赖:两个或两个以上的bean互相持有对方,形成闭环 循环依赖在spring中允许,根据**三级缓存**解决大部分循环依赖 1.一级缓存:单例池,缓存已经经历了完整生命周期、已经初始化的bean对象 2.二级缓存:缓存早期bean对象(生命周期还没走完) 3.三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的 构造方法出现了循环依赖怎么解决? 使用@Lazy进行懒加载 #### 7.SpringMVC的执行流程 1.用户发送出请求到前端控制器DispatcherServlet(前端控制器) 2.DispatcherServlet收到请求调用HandlerMapping(处理器映射器) 3.HandlerMapping找到对应处理器,生成处理器对象以及处理器拦截器,返回DispatcherServlet 4.DispatcherServlet调用HandlerAdapter(处理器适配器) 5.HandlerAdapter经过处理器适配器调用具体的处理器(Handler/Controller) 6.Controller方法上添加@ResponseBody,通过HttpMessageConverter将结果返转为JSON并响应 #### 8.SpringBoot自动配置原理 1.SpringBoot的引导类有一个注解@SpirngBootApplication,这个注解对三个注解做了封装: @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan 2.@EnableAutoConfiguration是实现自动化配置的核心注解,它通过import导入配置选择器。 内部读取了Jar包的classpath路径下META-INF/spirng-factories文件中的配置类全类名,配置类中所定义的bean会根据**条件注解所指定条件**决定是否将其导入Spring容器 3.条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有bean放入spring容器中使用。 #### 9.Spring框架常见注解 **Spring:** @Component、@Controller、@Service、@Repository 使用在类用于实例化Bean @Autowired、@Qualifier 依赖注入 @Scope 标注Bean的作用范围 @Configuration 配置类,当创建容器时会从该类上加载注解 @ComponentScan 用于指定Spirng初始化容器要扫描的包 @Bean 用在方法上,标注该方法返回值存储到Spring容器中 @Import 使用@Import导入的类会被加载到IOC容器中 @Aspect、@Before、@After、@Around、@Pointcut AOP **SpringMVC:** @RequestMapping 用于映射请求,可用在类和方法上 @RequestBody http请求的json数据转换为java对象,接收前端 @RequestParam 指定请求参数名称 @PathVariable 路径参数 @ResponseBody Controller返回对象转化为json,返回给前端 @RequestHeader 获取指定的请求头 @RestController @Controller+@ResponseBody **SpringBoot:** @SpringBootConfiguration 组合了@Configuration注解,实现配置文件功能 @EnableAutoConfiguration 打开自动配置的功能 @ComponentScan spring组件扫描 #### **10.MyBatis执行流程** 加载配置文件->构建Sql会话工厂(SqlSessionFactory)->构建Sql会话(SqlSession)->Executor执行器->MappendStatement对象(Executor接口执行方法中有一个MappendStatement参数,封装了映射信息)->输入参数映射->输出参数映射 #### **11.MyBatis延迟加载** 延迟加载:需要用数据才加载,不需要数据就不加载 Mybatis支持一对一关联对象和一对多关联集合对象延迟加载 Mybatis配置文件可以通过lazyLoadingEnabled=true|false,默认关闭。 底层使用了**CGLIB**创建目标对象的代理对象 #### **12.MyBatis一级、二级缓存** 一级缓存,作用域Session,默认 二级缓存,作用域namespace和mapper,不依赖SqlSession 当某个作用域下缓存进行了**增删改**,该作用域下所有所有select中的缓存被clear # Java集合篇 ## ①List #### **1.ArrayList底层原理实现** 动态数组 初始化容量为0,除非使用有参构造 当第一次添加数据才会初始化容量为10或者>10的有参构造值,后续1.5倍扩容(grow(),拷贝数组) #### 2.如何实现数组和List之间的转换 数组转List:Arrays.asList(array) //List后续受数组影响 List转数组:list.toArray(new xx[list.size()]) //数组后续不受List影响 #### 3.ArrayList和LinkedList的区别 1.底层结构:ArrayList动态数组,LinkedList动态双链表 2.时间复杂度:ArrayList查O(1)、LinkedList查O(n) | ArrayList删除插入平均O(n)、LinkedList删除插入平均O(n) 3.空间占用:ArrayList占用内存更小,LinkedList更大 4.线程安全:都不安全,如需安全可以使用 ```java Collections.synchronizedList(new ArrayList()); Collections.synchronizedList(new LinkedList()); ``` ## ②Map #### **4.红黑树** 红黑树:自平衡二叉搜索树 红黑规则都是希望红黑树能够保持平衡 红黑树的时间复杂度:查找、添加、删除都是O(logn) 性质1:节点要么红色、要么黑色 性质2:根节点是黑色 性质3:叶子节点都是黑色空节点 性质4:红色节点子节点都是黑色 性质5:从任意节点到其叶子节点路径包含相同数目的黑色节点 #### **5.HashMap的实现原理** 数据结构:数组+链表或红黑树 1.根据key的hashCode重新hash计算出数组下标 2.存储时,如果出现hash相同的key: a.key相同,覆盖 b.key不相同,hash冲突,将key-value放入链表或红黑树 3.获取时,对比hash值位置和key值 #### 6.HashMap的jdk1.7和jdk1.8有什么区别 jdk1.7:数组+链表。头插法(多线程有环链表风险)。 jdk1.8:数组+链表|红黑树。尾插法(多线程无环链表风险)。数组长度达到64且链表长度超过8,链表变为红黑树(降低时间复杂度)。红黑树小于6节点退化成链表。 #### 7.HashMap的put方法具体流程 1.判断table是否为空,为空初始化大小为16的table 2.根据键值key计算hash得到数组索引 3.判断table[i]==null,成立直接节点添加 4.如果table[i]!=null: 4.1 key相同,覆盖value 4.2 key不同,是红黑树,在红黑树操作 4.3 key不同,是链表,尾插后判断是否要转化为红黑树 5.插入成功后,判断键值对数量size是否超过了最大容量threshold(数组长度*0.75),如果超过,进行扩容 #### 8.HashMap的扩容机制 resize():第一次添加数组初始初始化数组长度为16,后续超过数组长度*0.75就2倍扩容 扩容会创建新数组,把老数组数据移到新数组: 1.没有hash冲突,直接e.hash&(newCap-1) 2.如果是红黑树,走红黑树的添加 3.如果是链表,则需要遍历链表,可能拆分链表,判断(e.hash&oldCap)是否为0. 3.1 为0则在原位置上 3.2 不为0则原位置+原数组的大小 `e.hash&oldCap==0, 则(2oldCap-1)&e.hash=(oldCap-1)&e.hash` `e.hash&oldCap!=0, 则(2oldCap-1)&e.hash=(oldCap-1)&e.hash+oldCap` #### **9.HashMap的寻址运算** 1.计算对象的hashCode() 2.调用hash()方法进行二次哈希,hashcode值右移动16位再异或运算(使hash值分布更均匀) 3.(capacity-1)&hash得到索引位置 (与取模结果一样,CPU运行更快) #### 10.HashMap在jdk1.7的多线程死循环问题 jdk1.7多线程头插法会导致发生哈希冲突时,链表成环。 # Java并发篇 ## ①线程基础 #### 1.线程和进程的区别? 进程是资源分配的基本单元,线程是CPU调度的最小单元。 1.进程是正在运行的程序实例,包含了线程。 2.进程上下文切换开销较大,线程开销较小 3.不同进程使用不同内存空间,当前进程下的所有线程可以共享内存空间(堆区、元空间区) #### **2.并发与并行的区别?** 并发是同一时间段应对多件事情的能力 并行是同一时间点应对多个事务 #### 3.创建线程的方式有哪些? 1.继承Thread类 2.实现Callable接口 3.实现Runnable接口 4.使用线程池创建 #### 4.Runnable和Callable有什么区别? 1.Runnable接口run()没有返回值,Callable接口call()有返回值(配合泛型、FutreTask) 2.Callable接口的call()方法允许抛出异常,Runnable接口run()不允许抛出异常,只能内部消化 #### 5.线程的run()和start()有什么区别? start()用来启动线程,只能执行一次 run()就是普通的函数,可以被多次调用 #### 6.线程包括哪些状态,之间如何转换? 1.new(新建) 2.runnable(可运行) 3.waiting(等待) 4.time_waiting(计时等待) 5.blocked(阻塞) 6.terminated(终止) #### 7.T1、T2、T3三个线程,如何保证它们按照顺序执行? 1.可以使用join解决 2.共用一个互斥锁,全局变量对应哪个线程执行,执行完notifyAll() #### 8.notify()和notifyAll()的区别? notifyAll:唤醒所有wait线程 notify:随机唤醒一个wait线程 #### **9.wait()和sleep()的不同?** **共同点:** wait()和sleep()的效果都是让当前线程进入等待状态,放弃CPU执行权。 **不同点:** wait()必须与锁搭配使用,会释放锁,等通知(放弃CPU,你们可以用) sleep()如果在synchronized中,不会释放锁,一只等待直到计时结束(放弃CPU,你们也别想用) #### **10.如何停止一个正在运行的线程?** 1.使用退出标志 2.使用interrupt方法中断线程 2.1打断阻塞线程,会抛出InterruptedException异常,可以try-catch捕获 2.2打断正常线程,根据打断状态来标记是否退出线程,打断状态isInterrupted()函数来判断 ## ②线程安全 #### 11.synchronized关键字的底层原理 synchronized底层涉及偏向锁、轻量级锁、重量级锁,其中重量级锁由**Monitor**实现,是jvm级别的一个对象(锁)。他有三个属性: **Owner**:存储当前获取锁的线程,只可以有一个线程获取 **EntryList**:没有获取锁,阻塞状态的线程 **WaitSet**:调用了wait,等待状态的线程 **关联:** 当加了synchronized时,对象头的markword中会加入Monitor的引用地址(指针),从而指向它。 补充:对象在存储在堆中,具体又可分为1.对象头(markword、klassword) 2.实例数据(成员变量、成员函数等) 3.对其填充(保持8的整数倍) #### **12.轻量级锁**? **多个线程加锁的时间是错开的(无竞争),可以使用轻量级锁。** **轻量级锁修改了对象头的锁标志,相对重量级锁性能提升很多,每次修改都是CAS操作。** **加锁流程:** 1.在线程中创建一个Lock Record,将obj字段指向锁对象。 2.通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无所状态则修改成功,代表该线程获得了轻量级锁。 3.如果当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到重入计数的作用。 4.如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。 **解锁过程:** 1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。 2.如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue 3.如果Lock Record的Mark Word不为null,则利用CAS指令将对象的mark word恢复成无锁状态。失败则膨胀为重量级锁。 #### **13.偏向锁?** **一段时间内都被一个线程使用锁,可以使用偏向锁。** 第一次获取锁时,会有一个CAS操作,之后改线程再获取锁,只需要判断mark word中是否是自己的线程id即可。 #### **14.三种锁比较** 重量级锁:底层使用Monitor实现,里面涉及到用户态和内核态切换、线程上下文切换,性能差 轻量级锁:线程加锁时间错开,轻量级修改对象头的锁标志,相对于重量级锁性能提升很多。每次修改都是CAS操作,保证原子性 偏向锁:一段时间内都只被一个线程使用锁。第一次获取锁会有一个CAS操作,之后该线程再获取锁,只要判断mark word中是否是自己线程id即可,而不是开销较大的CAS命令 #### **15.JMM是什么?** JMM(java Memory Model)java内存模型,定义了**共享内存**中**多线程读写操作**的规范。 本质上,多个CPU处理执行多个线程,CPU之间使用的数据是L1、L2缓存,需要读写的数据在共享内存上。为了保障可见性(JIT会优化代码)和防止重排序(处理器会重排指令),所以通常使用synchronized或volatile实现。 #### **16.volatile的理解** ①保证线程的可见性 用volatile修饰共享变量,能够防止编译器优化发生,让一个线程对共享变量的修改对另一个线程可见 ②禁止指令重排序 用volatile修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果 #### **17.CAS的理解** CAS(Compare and Swap),乐观锁思想+自旋思想,在无锁状态保护线程操作原子性 底层是调用Unsafe类方法,由操作系统提供,其他语言实现 #### **18.乐观锁和悲观锁的区别** CAS基于乐观锁:最乐观估计,不怕别的线程来修改共享变量,**修改了也没关系,再重试** synchronized基于悲观锁:最悲观的估计,**我上锁了都别想操作**,除非我放弃锁 #### **19.AQS的理解** AQS(AbstractQueuedSynchronizer)多线程的抽象队列同步器,是锁基础框架,ReentranLock、Semaphore都是基于AQS实现 1.AQS维护了双链表的先进先出队列,队列存储排序线程 2.AQS有state属性,默认0(无锁),1(有锁) 3.线程对state修改时使用了cas操作,保证多线程修改的原子性 #### **20.ReentrantLock的实现原理** ReentrantLock支持重入、支持中断、支持公平锁和非公平锁、支持锁超时、多个条件变量 底层采用**CAS+AQS队列**实现 #### 21.Synchronized和Lock有什么区别? **1.语法层面:** synchronized是关键字,源码在jvm,c++实现 Lock是接口,jdk提供,java实现 使用synchronized时,退出同步代码块会自动释放,Lock需要手动finally释放 **2.功能层面:** 二者都属于悲观锁,支持互斥、同步、锁重入 Lock提供了许多synchronized不具备的功能,比如ReentrantLock 支持重入、支持中断、支持公平锁和非公平锁、支持锁超时、多个条件变量 **3.性能方面:** 在没有竞争时,synchronized做了很多优化,如偏向锁、轻量锁 在竞争激励时,Lock实现会提供更好的性能 #### **22.死锁?** Ⅰ死锁产生条件: 1.互斥条件 2.请求与保持 3.不可剥夺 4.头尾相连的循环等待 Ⅱ打破死锁: 1.一次性申请所有资源(破坏条件2)。 2.在线程满足一定条件时,释放掉已占有的资源。比如线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁。(破坏条件3) 3.设计时考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量。可以让系统为每类资源赋予一个编号,每个线程按照编号请求资源,然后释放则相反。(破坏条件4) 4.死锁检测。 Ⅲ检查死锁: 1.使用jps、jstack查看运行线程、线程运行情况 2.使用可视化工具jconsole、VisualVM #### 23.ConcureentHashMap 底层数据结构: jdk1.7采用分组(segment)的数组+链表实现 jdk1.8采用的数据结构跟Hashmap1.8的结构一样 加锁方式: jdk1.7采用segment分段锁,底层使用的是ReentrantLock jdk1.8采用CAS添加新节点,采用synchronized锁定链表或者红黑树首节点,相对Segment分段锁粒度更细,性能更好 #### **24.导致并发程序问题出现的根本原因是什么?** 原子性(synchronized,lock),可见性(volatile,synchronized,lock),有序性(volatile) ## ③线程池 #### 25.线程池的核心参数 ```java public ThreadPoolExecutor( int corePoolSize, //核心线程数目 int maximumPoolSize, //最大线程数目(核心线程+临时线程) long keepAliveTime, //临时线程的生存时间,生存时间内没有新任务,此线程资源会释放 TimeUnit unit, //临时线程的生存单位 BlockingQueueworkQueue, //当没有空闲核心线程,新来任务排队,队列满创建临时 线程 ThreadFactory threadFactory, //可以定制线程对象的创建,例如线程名字,守护线程等 RejectExecutionHandler handler) //拒绝策略,当所有线程都忙,队列满了,会触发拒绝策略 ``` AbortPolicy 拒绝任务并抛出异常 CallerRunsPolicy 调用线程执行该任务 DiscardOldestPolicy 丢弃队列中最老任务,放入新任务 DiscardPolicy 拒绝任务,但不抛出异常 #### 26.线程池中有哪些常见的阻塞队列 1.**ArrayBlockingQueue**: 基于数组结构的有界阻塞队列,FIFO 2.**LinkedBlockingQueue**:基于链表结构的有界阻塞队列,FIFO 3.DelayedWorkQueue:是一个优先级队列,每个任务都可以执行时间,执行时间靠前的优先出队 4.SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移除操作 **ArrayBlockingQueue和LinkedBlockingQueue区别:** ArrayBlockingQueue强制有界,底层数组,初始化数组,一把锁锁住整个数组,效率低 LinkedBlockingQueue可无界可有界,底层链表,创建节点才添加数据,两把锁(头尾),效率高 #### 27.如何确定核心线程数 ①高并发、任务执行时间短->(CPU核数+1),减少线程上下文切换 ②并发不高、任务执行时间长 IO密集型任务->(CPU核数*2+1) 计算密集任务->(CPU核数+1) #### **28.线程池的种类有哪些** Executors.newFiexedThreadPool(nThread)//固定大小线程池,核心线程是nThread,无临时线程 Executors.newSingleThreadExecutor() //适用于按照顺序执行的任务,只有一个核心线程 Executors.newCachedThreadPool() //适用于执行密集,但是执行时间较短,全都是临时线程,每来一个任务看看有没有临时线程,没有就创建临时线程,因为队列是空的 Executors.newScheduledThreadPool(nThread) //延时的线程,支持定时、周期性执行任务 #### 29.为什么不建议使用Executors创建线程池 FixedThreadPool和SingleThreadPool: 队列长度为Interger.MAX_VALUE,大量请求容易OOM(out of memory) CachedThreadPool: 最大线程为Interger.MAX_VALUE,容易OOM #### 30.多线程使用场景 1.数据汇总:通过多线程分别运行不依赖的业务,比串行单线程依次执行耗时少 2.异步线程:为了避免下一级方法影响上一级方法(性能考虑),可以使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法响应时间 3.批量导入:使用线程池+CountDownLatch批量把数据库数据导入到了ES中,避免OOM #### 31.如何控制某个方法并发访问线程的数量 使用Semaphore信号量: 1.创建Semaphore对象,给一个容量 2.acquire()可以请求一个信号量,信号量-1 3.release()可以释放一个信号量,信号量+1 #### 32.ThreadLocal的理解 1.ThreadLocal可以实现【资源对象】线程隔离,避免线程安全 2.ThreadLocal实现了线程内资源共享 3.每个线程内有一个ThreadLocalMap类型成员变量,用来存储资源对象 a) set方法,以ThreadLocal自己作为key,资源对象为value b) get方法,以ThreadLocal自己作为key,返回资源对象value c) remove方法,以ThreadLocal自己作为key,一处当前线程关联资源值 **内存泄露**:ThreadLocalMap中的key是弱引用,会被GC回收,而value是强引用,不会被回收。建议主动remove释放key、value。 # JVM ## ①JVM虚拟机篇 ### *JVM组成* #### 1.JVM是什么? JVM(Java Virtual Machine)Java虚拟机(java二进制字节码运行环境) 1.JVM和JDK、JRE的关系:JDK包括JRE包括JVM JDK(Java Development Kit),包含JRE和开发工具如java、javac、jar、javadoc JRE(Java Runtime Environment),包含JVM和java核心类库 2.JVM能实现一次编译、多次运行(不同系统),因为.class文件可以被不同系统上不同的JVM分别处理。 #### 2.什么是程序计数器? 线程私有的,记录正在执行的字节码指令地址 #### 3.什么是JAVA堆? 线程共享:主要用来保存对象实例,数组等,当内存无法分配满了,抛出OOM 组成:**年轻代+老年代** 年轻代分为Eden、S0、S1区 老年代存储生命周期较长的对象实例、数组 #### 4.什么是虚拟机栈? Java Virtual machine Stacks 每个线程运行时所需要的内存,成为虚拟机栈,先进后出 每个栈由多个栈帧(frame)组成,对应着每次方法调用时占用的内存 每个线程只有一个活动栈帧,对应当前执行的那个方法 1.垃圾回收不涉及栈内存,栈内存会自动弹出释放 2.栈内存默认分配1024K,过大会导致线程数变少 3.方法内的局部变量需要逃逸分析 4.栈帧过多导致内存溢出,比如递归调用 #### 5.堆栈区别是什么? 1.栈内存存储局部变量和方法调用,堆内存存储java对象和数组。栈不需要GC回收,堆需要。 2.多线程不共享栈内存,共享堆内存 3.两者内存不足都会报错,栈是StackOverFlow、堆是OutOfMemory。 #### **6.方法区是什么?** 线程共享:存储类信息、运行时常量池 启动虚拟机创建、关闭释放 内存不足抛出OutOfMemory:Metaspace **常量池:** 可以看作一张表,虚拟机指令根据常量表找到要执行的类名、方法名、参数类型、字面量等信息 **运行时常量:** 当类被加载,常量池信息放入运行时常量池,里面的符号地址变为真实地址 #### **7.你听过直接内存吗?** 并不属于JVM内存结构,是虚拟机系统内存 常见于NIO操作,用于数据缓冲区,分配回收成本较高,但读写性能高,不受JVM内存回收管理 ### *类加载器* #### 8.什么是类加载器 将字节码文件加载到JVM中,从而java程序能够启动起来 #### 9.类加载器有哪些? 启动类加载器(BootStrap ClassLoader):加载JAVA_HOME/jre/lib下的库 扩展类加载器(ExtClassLoader):加载JAVA_HOME/jre/lib/ext下的库 应用类加载器(AppClassLoader):用于加载classPath下的类 自定义类加载器(CustomizeClassLoader):自定义类继承ClassLoader,实现自定义加载规则 #### **10.什么是双亲委派模型?** 加载某一个类,会委托上一级的加载器加载,如果上级也有上级,则会向上继续委托,如果该类委托上级没有加载,则子加载器尝试加载该类 **为什么采用它:** 1.避免类重复被加载 2.为了安全,保证类库API不会被修改 #### 11.类装载的执行过程? 加载:查找和导入class文件 验证:保证加载类的准确性 准备:为类变量分配内存并设置类变量初始值 解析:把类中的符号引用转换为直接引用 初始化:对类的静态变量、代码块执行初始化操作 使用:JVM开始从入口方法执行用户程序 卸载:程序执行完毕,JVM销毁创建的class对象 ### *垃圾回收* #### 12.对象什么时候被回收? **引用计数法:** 记录对象引用次数,为0回收,但容易出现对象的循环引用,导致内存泄露 **可达性分析:** 通过GC Root引用链依次寻找对象,寻找不到就是垃圾 #### 13.JVM垃圾回收算法有哪些? **1.标记清除算法:** 通过可达性分析并标记垃圾,对垃圾回收 优点:标记和清楚速度较快 缺点:造成内存碎片 **2.标记整理算法(老年代)**:标记清除之后,还会把存活对象整理在一起,避免内存碎片 **3.标记复制算法(新生代)**:需要两块内存空间,存活对象放入另一块内存,并清空前一块内存 #### 14.JVM中的分代回收? 划分为新生代(Eden、s0、s1区)和老年代。 新创建的对象在新生代Eden区,当Eden内存不足时,垃圾回收算法将Eden区和s0(or s1)的存活对象复制到另外的s1(or s0)中,当幸存区对象熬过15次回收,进入老年代 #### 15.MinorGC、MixedGC、FullGC的区别? 1.YoungGC(MinorGC) 发生在新生代垃圾回收,暂停时间段 2.MixedGC 新生代+老年代部分区域垃圾回收,G1收集器特有 3.FullGC:新生代+老年代完整垃圾回收,暂停时间长,应避免 #### 16.JVM有哪些垃圾回收器? 1.串行垃圾回收器:serial作用新生代 复制算法,serial old作用老年代 整理算法 2.并行垃圾回收器:parallel new作用新生代 复制算法,parallel old作用于老年代 整理算法 3.CMS(并发)垃圾回收器:作用老年代,标记-清除,标记和清除时和用户工作线程并行执行。几次后可以进行一次整理防止内存碎片 4.G1垃圾回收器(jdk9之后默认):作用在新生代和老年代 #### 17.详细聊一下G1垃圾回收器? 三个阶段:**新生代回收(young GC)、混合回收(mixed GC)、全部回收(full GC)** 划分多个区域,每个区域都可以充当Eden、survivor、old、humongous 采用复制算法,响应时间和吞吐量兼顾 如果mixed GC过程中复制失败(内存不足),触发Full GC,通过serial GC 整理算法 #### 18.强引用、软引用、弱引用、虚引用的区别? **强引用:** 只需要GC Roots能找到,就不会回收 **软引用:** 需要配合SoftReference使用,当多次垃圾回收后内存依旧不够会回收软引用 **弱引用:** 需要配合WeakReference使用,只要进行垃圾回收,弱引用对象就被回收 **虚引用:** 必须配合引用队列,被引用对象回收时,将虚引用入队,由Reference Handler线程调用虚引用相关方法直接释放内存 # 微服务 ## ①SpringCloud #### 1.SpringCloud 5大组件有哪些? 通常情况下: 1.Nacos/Eureka:注册中心 2.Ribbon:负载均衡 3.Feign:远程调用 4.Sentinel:服务保护 5.Gateway:网关 #### 2.服务注册和发现是什么意思?Spring Cloud如何实现服务注册发现? 项目采用nacos作为服务中心,是SpringCloud体系一个核心组件。 1.服务注册:服务提供者需要把自己信息注册到nacos,比如服务名称,ip,端口等 2.服务发现:消费者向nacos拉去服务列表信息,如果服务提供者有集群,则消费者会负载均衡发起调用 3.服务监控:服务提供者会每隔x秒向nacos发送心跳,报告健康状态,如果nacos过了n*x秒还没收到心跳,从nacos中去除(对于临时实例) #### 3.Nacos和Eureka的区别? **共同点:** 1.都支持服务注册、拉取 2.都支持服务提供者心跳方式做健康检测 **不同点:** 1.Nacos支持服务端主动检测提供者状态:非临时实例主动检测、临时实例心跳模式 2.临时实例心跳不正常剔除,非临时实例则不会被剔除 3.Nacos支持服务列表变更的消息推送模式,服务列表更新更及时 4.Nacos集群默认采用AP方式,当集群中存在非临时实例,采用CP模式;Eureka采用AP方式 额外:Nacos还提供了配置中心,而Eureka只有注册中心 #### 4.项目如何实现负载均衡的? 微服务的负载均衡主要使用了一个组件Ribbon,比如,我在feign远程调用的过程中,底层负载均衡就是使用了Ribbon。 #### 5.Ribbon的负载均衡策略有哪些? 1.RoundRobinRule:轮询 2.WeightedResponseTimeRule:权重,响应时间越长,权重越小 3.RandomRule:随机 4.ZoneAvoidanceRule:区域敏感策略,先Zone对服务分类和选择,然后Zone内的多个服务做轮询(默认) #### 6.如何自定义负载均衡策略? 1.创建IRule接口,@Bean注解并让他被Spring容器管理,可以指定负载均衡策略(全局) 2.在客户端配置文件,可以配置某一服务调用的负载均衡策略(局部) #### 7.什么是服务雪崩,如何解决这个问题? **服务雪崩**:一个服务失败,导致整个链路的服务都失败的情况 解决: 1.**服务降级(针对某个服务内接口)**:下游服务自我保护的方式,确保服务不会受请求突增影响变得不可用,不可用走降级的逻辑,保证服务不会崩溃,一般与feign接口整合,,编写降级逻辑 2.**服务熔断(针对某个服务)**:默认关闭,需要手动打开。三种状态:关闭、打开、半-打开。如果检测10秒内请求失败50%,那么进入打开熔断状态,5秒后重新尝试请求下游服务,进入半-打开状态,如果不能响应,继续走熔断机制,否则正常请求,回到熔断关闭状态。 #### 8.你们的微服务怎么监控的? 采用skywalking进行监控 1.skywalking主要监控接口、服务、物理实例的一些状态。特别是压测的时候可以看到众多服务中哪个服务和接口比较慢,可以针对性分析和优化。 2.skywalking设置了告警规则,如果报错,第一时间给相关负责人发送邮件和短信。 ## ②微服务业务 #### 9.为什么要限流? 并发量大需要限流。 **解决办法**: 1.**nginx**限流: 漏桶算法:固定速率处理请求 控制并发数:限制单个ip的链接数和并发链接的总数 2.**网关限流** 令牌桶算法:依靠局部过滤器RequestRateLimiter,生成令牌,固定速率生成令牌,只有有令牌才能处理请求(应对流量突增) 可以根据ip或者路径限流,设置每秒填充平均速率,和令牌桶总容量 #### 10.解释一下CAP和BASE 1.CAP定理(一致性、可用性、分区容错性) 分布式通过网络连接,一定会出现分区问题(P) 当分区出现时,系统的一致性(C)和可用性(A)就无法同时满足 2.BASE理论 基本可用、软状态(中间状态)、最终一致 3.解决分布式事务的思想和模型: Ⅰ最终一致思想:各分支事务分别执行并提交,如果有不一致的情况,再想办法恢复数据(AP) Ⅱ 强一致思想:各分支事务执行完业务不需要提交,等待彼此都完成后统一提交或回滚(CP) #### 11.分布式事务解决方案? 只要发生了多个服务之间的**写**操作,都需要进行分布式事务控制 比如,采用seata|MQ 1.seta的XA模式,CP,需要互相等待各个分支事务提交,可以保证强一致性,性能差(银行) 2.seta的AT模式,AP,底层使用undo log实现,性能好(互联网) 3.seta的TCC模式,介于AP和CP,性能较好,不过需要人工编码实现(银行) 4.MQ模式实现分布式事务,在A服务写数据时,需要在同一个事务内发送消息到另外一个事务,异步,性能最好(互联网) #### **12.分布式服务的接口幂等性如何设计?** 幂等:多次调用某方法或者某接口,可以保证**重复调用的结果和单次调用的结果一致**(比如用户多次完成订单支付和一次订单支付) 1.如果新增数据,可以数据库唯一索引 2.如果新增或者修改数据(推荐) 2.1 **分布式锁**:性能低 2.2 **token+redis**:性能好(第一次请求生成token给前端,第二次携带token,到redis校验,通过则处理,并删除token,失败则直接返回不处理) #### **13.分布式任务调度:xxl-job路由策略有哪些?执行失败怎么办?大量数据都需要同时执行怎么解决?** **路由策略**:轮询、故障转移、分片广播... **执行失败怎么解决**: 1.路由策略故障转移,使用健康实例执行任务 2.设置重试次数 3.查看日志+邮件告警来通知相关负责人解决 **大数据量并行任务同时都需要执行**: 1.路由策略分片广播,多个实例一块去执行 2.任务按取模分配到各个实例执行 ## ③RabbitMQ #### **14.RabbitMQ如何保证消息不丢失** 1.生产者确认机制:保证生产者消息到达队列 2.MQ持久化:对交换机、队列、消息持久化,确保消息在队列中部丢失 3.开启消费者确认机制为auto:由Spring确认消息处理成功后完成ack,失败重试多次 注:消费者失败多次将消息投送到异常交换机,由人工处理 #### **15.RabbitMQ消息重复消费如何解决** 网络抖动or消费者挂了,导致RabbitMQ没有收到消费者确认,重复发送消息 解决办法:每条消息设置唯一的消息id,消费者第一次处理后记录,后续不处理 #### **16.RabbitMQ中死信交换机?(RabbitMQ延时队列有了解过吗)** 1.场景:比如超时订单 延时队列就是用到了**死信交换机**+**TTL**(消息存活时间)实现的 消息超时未消费就会变成死信(或者被拒绝消费、队列满了) 2.延迟队列插件实现延迟队列DelayExchange 声明一个交换机,添加delayed属性为true 发送消息时,添加x-delay头,值为超时时间 #### **17.RabbitMQ百万消息堆积在MQ,如何解决** 1.增加更多消费者,提高消费速度 2.在消费者内开启线程池加快消息处理速度 3.扩大队列容积,提高堆积上线,采用惰性队列(基于磁盘存储而非内存了) #### **18.RabbitMQ的高可用机制有了解过嘛** **可采用集群,镜像队列。** 1.镜像队列,本质是一主多从,所有操作都是主节点完成,然后同步给镜像节点。 2.主机宕机,镜像节点代替成为新的主节点(如果主从同步完成前,主就宕机了,可能数据丢失) **出现数据丢失怎么解决?** 采用仲裁队列,主从同步基于Raft协议,能保证强一致性 > 有错误请联系我,谢谢! > 邮箱:zylu2000@163.com > 禁止一切营利性用途,感兴趣的小伙伴欢迎收藏和引用本文。🙏🙏