文章目录
找工作找实习
脉脉(国内) 领英(外企)
简历注意事项
简历的结构
基本信息\教育背景\求职意向\工作经历*职业技能**项目经历*\个人优势荣誉
回答why获取好处缺点时,从安全性,稳定性,一致性,持久性,性能(处理能力),空间利用率,资源利用率,
在不同项目,优先级不同
银行这种安全性高的,安全性,稳定性,一致性>性能(处理能力),空间利用率,资源利用率
电商这种高并发量的一些业务,性能(处理能力),空间利用率,资源利用率>安全性,稳定性,一致性
面试技巧
项目回答
面试要按照star法则准备
S:Situation(情境)
T:Task(任务)
A:Action(行动)
R:Result(结果)
例如
- 我们当时面临
- 我要做什么,负责做什么
- 我做了什么具体方案
- 达到什么效果
八股文回答
回答问题按照what-why-how去回答
职业技能
- 放到简历的黄金位置
- 职业技能 = 必要技术+第三方技术
- 针对性准备,引导面试官针对性提问
- 写在简历上的必须能聊,不然就别写
项目描述
- 项目体现业务深度或技术深度
- 主导设计过XXX模块(0-1,研发或1-2,改进),不要说太大,逮着模块说
- 尽可能展示指标数据
如何找到适合的练手项目
gitee或github搜索开源项目,b站黑马项目课程
深入学习项目
- 技术选型,通用模块,可嵌套到大部分项目中
- 学习方式,多方位深入挖掘业务和技术
- 功能实现->常见问题->系统设计
笔试面试可能就是发过来一个牛客网的链接
不仅要投校招,也要投社招
游戏策划
还要重点学的东西,MySQL,JUC(多线程),JVM,jdbc,rabbitMQ,kafka,直接刷题也行
数据经常问,hashmap,数组什么的
黑马的java虚拟机,多线程,mysql,redis,八股文,javaguide
redis黑马的课程只看实战篇就可以了
csdn上两个最火的java八股文要看
简历二月开始就要投了,找实习
https://heuqqdmbyk.feishu.cn/wiki/RymLwLLWfieibHkjf17cKhY4nlf(问答文稿地址)
Redis篇
使用场景
- 缓存(重点问)
- 穿透\击穿\雪崩
- 双写一致\持久化
- 数据过期\淘汰策略
- 分布式锁(重点问)
- setnx
- redisson
- 计数器
- 保存token
- 消息队列
- 延迟队列
其他面试题
-
集群
- 主从
- 哨兵
- 集群
-
事务
-
redis为什么快
-
根据自己简历上的业务进行回答
我看你做的项目中,都用到了redis,你在最近的项目中哪些场景中使用了redis呢
你的业务遇到穿透\击穿和雪崩如何处理
缓存
缓存穿透
缓存穿透,怎么解决
缓存穿透:查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库
方案一,缓存空数据
方案二,布隆过滤器
检索一个元素是否在一个集合中
一个元素对应多个hash也是为了减小hash数组的大小
bitmap,只存0或1
什么是缓存击穿
缓存击穿:给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发请求可能会瞬间把DB压垮
方案一:互斥锁,强一致,性能差
方案二:逻辑过期,高可用,性能好,不能保证数据绝对一致
缓存雪崩
缓存雪崩是指同一时段大量的缓存key同时失效或者redis服务宕机,导致大量请求到达数据库,带来巨大压力
解决方案:
- 给不同的key的TTL添加随机值
- 利用redis集群提高服务的可用性
- 给缓存业务添加降级限流策略,降级可作为系统的保底策略,适用于穿透\击穿\雪崩
- 给业务添加多级缓存
<缓存三兄弟>
穿透无中生有key,布隆过滤null隔离
缓存击穿过期key,锁与逻辑解难题
雪崩大量过期key,过期时间要随机
面试必考三兄弟,可用限流来保底
双写一致
redis作为缓存,mysql的数据如何与redis进行同步呢(双写一致性)
一定要先设置前提(比如业务是更看重强一致性),根据自己项目的业务去答
介绍自己简历上的业务,我们当时是把文章的热点数据存入到了缓存中,虽然是热点数据,但是实时要求性并没有那么高,所以,我们当时采用的是异步的方案同步的数据
我们当时是把抢券的库存存入到了缓存中,这个需要实时的进行数据同步,为了保证数据的强一致,我们当时采用的是redisson提供的读写锁来保证数据的同步
那你介绍一下异步的方案(你来介绍一下redisson读写锁的这种方案)
- 允许延时一致的业务,采用异步通知
- 使用MQ中间件,更新数据之后,通知缓存删除
- 利用canal中间件,不需要修改业务代码,伪装为mysql的一个从节点,canal通过读取binlog数据更新缓存
- 强一致性的,采用redisson提供的读写锁
共享锁:读锁readlock,加锁之后,其他线程可以共享读操作,不允许写操作
排他锁:独占锁writelock,加锁后,阻塞其他线程读写操作
当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致
无论是先删缓存,还是先修改数据库,都是会导致脏数据出现在redis中的,因为redis主节点中的数据要同步到从节点要延时一会再删,所以写操作采用延迟双删
缓存持久化
redis作为缓存,数据的持久化是怎么做的
RDB全称redis database backup file(redis数据备份文件),也被称为redis数据快照.简单来说就是把内存中的所有数据都记录到磁盘中,当redis实例故障重启后,从磁盘读取快照文件,恢复数据
RDB的执行原理:bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入到RDB文件中
fork采用的时copy-on-write技术:
- 当主进程执行读操作时,访问共享内存
- 当主进程执行写操作时,则会拷贝一份数据,执行写操作
AOF全称Append only file(追加文件),redis处理的每一个写命令都会记录在AOF文件,可以看作是命令日志文件
RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用
数据的过期策略
redis的key过期之后,会立即删除吗
惰性删除:访问key的时候判断是否过期,如果过期,则删除
定期删除:定期检查一定量的key是否过期(slow模式+fast模型)
redis的过期删除策略:惰性删除+定期删除两种策略进行配合使用
数据的淘汰策略
当redis中的内存不够用时,此时在向redis中添加新的key,那么redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略
redis支持8种不同的策略来选择要删除的key:
noevition(默认策略):不淘汰任何key,但是内存满时不允许写入新数据
剩下的策略基本都是基于两个规则来实现(LRU和LFU):
- LRU(最近最少使用),用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高
- LFU(最少频率使用),会统计每个key的最近一段时间的访问频率,值越小淘汰优先级越高
比如,如果业务有明显的冷热数据区分,建议使用最近最常访问的数据留在缓存中,优先使用volatile-lfu
数据库有1000万数据,redis只能缓存20w数据,如何保证redis中的数据都是热点数据
使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略,留下来的都是经常访问的热点数据
redis的内存用完了会发生什么
主要看数据淘汰策略是什么,如果是默认的配置(noeviction),会直接报错
分布式锁
redis分布式锁,是如何实现的
还是需要结合项目中的业务进行回答,通常情况下,场景有:集群情况下的定时任务\抢单\幂等场景
redisson实现分布式锁如何合理的控制锁的有效时长
在redisson的分布式锁中,提供了一个watchdog(看门狗),一个线程获取锁成功以后,watchdog会给持有锁的线程续期(默认是每隔10秒续期一次)
redisson的这个锁,可以重入吗
可以重入,多个锁重入需要判断是否是当前线程,在redis中进行存储的时候使用的hash结构,来存储线程信息和重入的次数
redisson锁能解决主从数据一致的问题吗
不能解决,但是可以使用redisson提供的红锁来解决,但是这样的话,性能就太低了,如果业务中非要保证数据的强一致性,建议采用zookeeper实现的分布式锁
分布式锁要针对以下几个点回答问题:
- 利用set nx ex获取锁,并设置过期时间,保存线程标识(获取锁和释放锁的原子性)
- 释放锁时,先判断线程标识是否与自己一致,一致则删除锁,利用lua脚本保证redis命令原子性操作(判断锁和释放锁的原子性)
- 不可重入性
- 不可重试性
- 超时释放
- 主从一致性
其他问题
主从复制\同步
介绍一下redis的主从同步
单节点redis的并发能力是有上限的,要进一步提高redis的并发能力,就需要搭建主从集群,实现读写分离
一般都是一主多从,主节点负责写数据,从节点负责读数据
能说一下,主从同步数据的流程吗
replication id,offset可以理解为就是标志,replication id确定是否同步过,offset确定最近一次同步同步到哪了
全量同步:
- 从节点请求主节点同步数据(replication id,offset)
- 主节点判断是否是第一次请求,是第一次就与从节点同步版本信息(replication id和offset)
- 主节点执行bgsave,生成rdb文件后,发送给从节点去执行
- 在rdb生成执行期间,主节点会以命令的方式记录到缓冲区(一个日志文件)
- 把生成之后的命令日志文件发送给从节点进行同步
增量同步: - 从节点请求主节点同步数据,主节点判断不是第一次请求,不是第一次就获取从节点的offset值
- 主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步
哨兵模型和集群脑裂
怎么保证redis的高并发高可用
首先可以搭建主从集群,再加上使用Redis中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主;同时Sentinel也充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端(这里的客户端是指redis的客户端,不是用户),所以一般项目都会采用哨兵的模式来保证Redis的高并发高可用。
哨兵模式:实现主从集群的自动故障恢复(监控\自动故障恢复\通知)
你们使用redis是单点还是集群,哪种集群
一般主节点的写并发是8万左右,从节点并发是10万左右,一般项目都够用,1主1从就够用
主从(1主1从)+哨兵就可以了.单节点不超过10G内存,如果redis内存不足则可以给不同服务分配独立的redis主从节点
redis集群脑裂,该怎么解决呢
集群脑裂是由于主节点和从节点和sentinel(哨兵)处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,哨兵会将老的主节点降为从节点,这时再从新的master同步数据,就会导致数据丢失
解决:可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失
虽然哨兵升格了一个从节点为新的主节点,但是没办法通知旧的主节点降格为从节点,所以redis的客户端仍然与旧的主节点通信
redis提供了哨兵(sentinel)机制来实现主从集群的自动故障恢复:
- 监控:哨兵会不断检查您的master和slave是否按预期工作
- 自动故障恢复:如果master故障.哨兵会将一个slave提升为master,当故障实例恢复后也以新的master为主
- 通知:哨兵充当redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给redis的客户端
哨兵基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令: - 主观下线,如果某哨兵节点发现某实例未在规定时间响应,则认为该实例主观下线
- 客观下线,若超过指定数量(quorum)的哨兵都认为该实例主观下线,则该实例客观下线,quorum值最好超过哨兵实例数量的一半
哨兵选主规则 - 首先判断主从节点断开时间长短,如超过指定值就排除该从节点
- 然后判断从节点的slave-priority值,越小优先级越高
- 如果slave-priority一样,则判断slave节点的offset值,越大优先级越高
- 最后是判断slave节点的运行id大小,越小优先级越高
分片集群
redis的分片集群有什么作用
- 集群中有多个master,每个master保存不同数据
- 每个master都可以有多个slave节点
- master之间通过ping监测彼此健康状态
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点
redis分片集群中数据是怎么存储和读取的 - redis分片集群引入了哈希槽的概念,redis集群有16384个哈希槽
- 将16384个插槽分配到不同的实例
- 读写数据:根据key的有效部分计算哈希值,对16384取余(有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身作为有效部分)余数作为插槽,寻找插槽所在的实例
redis分片集群引入了哈希槽的概念,redis集群有16384个哈希槽,每个key通过CRC6校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽
redis在使用上以额外策略需要解决的问题:
- 高并发读(主从复制)
- 高可用性(哨兵模式)
- 海量数据存储(分片集群)
- 高并发写(分片集群)
redis是单线程的,为什么还那么快
redis是单线程的,但是为什么还那么快
- redis是纯内存操作,执行速度非常快
- 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
- 使用I/O多路复用模型
能解释一下I/O多路复用模型嘛
嗯~~,I/O多路复用是指利用单个线程来同时监听多个Socket,并且在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。
其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;
在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,I/O多路模型主要就是实现了高效的网络请求
I/O多路复用
I/O多路复用是指利用单个线程来同时监听多个socket,并在某个socket可读,可写时得到通知,从而避免无效的等待(相比于阻塞IO和非阻塞IO来说),充分利用CPU资源.目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程socket就绪的同时,把已就绪的socket写入到用户空间,不需要挨个遍历socket来判断是否就绪,提升了性能
redis网络模型
就是使用I/O多路复用结合事件的处理器来应对多个socket请求
- 连接应答处理器
- 命令回复处理器
- 命令请求处理器
- 用户空间和内核空间
用户空间<–>内核空间<–>硬件 - 常见的IO模型
- 阻塞IO,阻塞等待数据就绪和数据拷贝
- 非阻塞IO,不断尝试询问读取数据,数据就绪后,阻塞等待数据拷贝
- IO多路复用,调用select,指定要监听的socket集合,当有任意socket数据就绪,返回信号给用户应用,阻塞等待数据拷贝
- redis网络模型
SQL篇
优化
- 定位慢查询
- SQL执行计划
- 索引
- 存储引擎
- 索引底层数据结构
- 聚簇和非聚簇索引
- 索引创建原则
- 索引失效场景
- SQL优化经验
其他面试题
- 事务相关(事务特性,隔离级别,MVCC)
- 主从同步原理
- 分库分表
优化
如何定位慢查询
- 开源工具,调试工具Arthas,运维工具,prometheus\Skywalking
- mysql的配置文件中配置慢查询日志
//开启慢查询开关
slow_query_log=1
//设置慢日志的时间为2秒
long_query_time=2
配置完毕后,重新启动MySQL服务器,查看慢日志/var/lib/mysql/localhost-slow.log
MySQL中如何定位慢查询
我们当时做压测的时候有的接口非常慢,接口的响应时间超过了2秒以上,因为我们当时的系统部署了运维的监控系统skyealking,在展示的报表中可以看到是哪一个接口比较慢,并且可以分析这个接口哪部分比较慢,这里可以看到SQL的具体的执行时间,所以可以定位是哪个SQL出了问题
如果,项目中没有这种运维的监控系统,其实在MySQL中也提供了慢日志查询的功能,可以在MySQL的系统配置文件中开启这个慢日志的功能,并且也可以设置SQL执行超过多少时间记录到一个日志文件中,我记得上一个项目配置的是2秒.只要SQL执行时间超过了2秒就会记录到日志文件中,我们就可以在日志文件中找到执行比较慢的SQL了
语句执行很慢,如何分析
在做优化时,主要关注的字段type,possible_keys,key,key_len,rows,Extra
这个SQL语句执行很慢,如何分析呢
可以采用MySQL自带的分析工具explain
- 通过key和key_len检查是否命中了索引(索引本身存在是否有失效的情况)
- 通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或全盘扫描
- 通过extra建议判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复
索引及底层数据结构
了解过索引吗?(什么是索引)
索引在项目中还是比较常见的,它是帮助MySQL高效获取数据的数据结构,主要是用来提高数据检索的效率,降低数据库的IO,同时通过索引列对数据进行排序,降低数据排序的成本,也能降低CPU的消耗
索引的底层数据结构了解过嘛
MySQL的默认的存储引擎InnoDB采用的B+树的数据结构来存储索引,选择B+树的主要的原因是:
第一,阶数更多,路径更短,
第二,磁盘读写代价B+树更低,非叶子节点只存储指针,叶子节点存储数据,
第三,B+树便于扫库和区间查询,叶子节点是一个双向链表
B树和B+树的区别是什么呢
- 在B树中,非叶子节点和叶子节点都会存放数据,而B+树的所有数据都会出现在叶子节点,在查询的时候,B+树查找效率更加稳定
- 在进行范围查询的时候,B+树效率更高.因为B+树都在叶子节点存储,并且叶子节点是一个双向链表
聚集索引和非聚集索引(二级索引)
什么是聚集索引什么是非聚集索引
- 聚集索引主要是指数据与索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个,一般情况下主键作为聚集索引
- 二级索引指的是数据与索引分开存储,B+tree的叶子节点保存对应的主键,可以有多个,一般我们自己定义的索引都是二级索引
知道什么是回表查询嘛
其实跟刚才介绍的聚簇索引和非聚簇索引是有关系的,回表的意思是通过二级索引找到对应的主键值.然后再通过主键值找到聚集索引中所对应的整行数据,这个过程就是回表
如果面试官直接问回表,则需要先介绍聚集索引和二级索引
覆盖索引
知道什么叫覆盖索引嘛
覆盖索引是指查询使用了索引,返回的列,必须在索引中全部能够找到
- 使用id查询,直接走聚集索引查询,一次索引扫描,直接返回数据,性能高
- 如果返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用select *
*select 和select (全都列出来)的效率差在哪?
Mysql超大分页
Mysql超大分页怎么处理
在数据量比较大时,limit分页查询,需要对数据进行排序,效率低
解决方案:覆盖索引+子查询
索引创建原则
主要还是要结合自己的项目去说
索引创建原则有哪些
- 数据量较大,且查询比较频繁的表
- 常作为查询条件\排序\分组的字段
- 字段内容区分度高
- 内容较长\使用前缀索引
- 尽量联合索引
- 要控制索引的数量
- 如果索引列不能存储NULL值,在创建表时使用NOT NULL约束它
索引失效
一定要结合自己的项目去说
什么情况下索引会失效
- 违反最左前缀法则
- 范围查询右边的列,不能使用索引
- 不要在索引列上进行运算操作,索引将失效
- 字符串不加单引号,造成索引失效
- 以%开头的like模糊查询,索引失效
sql优化(规范)的经验
一定要结合自己的项目去说,增添真实感
谈一谈你对sql的优化的经验
- 表的设计优化,数据类型的选择
- 索引优化,索引创建原则
- sql语句优化,避免索引失效,避免使用select *
- 主从复制\读写分离\不让数据的写入,影响读数据
- 数据量很大时,分库分表
创建表的时候,你们是如何优化的呢
创建表时,我们主要参考《嵩山版》开发手册,选择字段类型时结合字段内容,
比如数值类型选择TINYINT、INT、BIGINT等,
字符串类型选择CHAR、VARCHAR或TEXT。
在使用索引的时候,是如何优化呢
在使用索引时,我们遵循索引创建原则,确保索引字段是查询频繁的,使用复合索引覆盖SQL返回值,避免在索引字段上进行运算或类型转换,以及控制索引数量。
你平时对SQL语句做了哪些优化呢
我对SQL语句的优化包括指明字段名称而不是使用SELECT *,避免造成索引失效的写法,
聚合查询时使用UNION ALL代替UNION,
表关联时优先使用INNER JOIN,
以及在必须使用LEFT JOIN或RIGHT JOIN时,确保小表作为驱动表。
事务
事务的特性
事务的特性是什么,可以详细说一下嘛
最好结合例子去说,能说的更清楚
ACID,分别指的是:原子性\一致性\隔离性\持久性
我举个例子:A向B转账500,A扣除500元,B增加500元,原子操作体现在要么都成功,要么都失败
在转账的过程中,数据要一致(一致性),A扣除了500,B必须增加500
在转账的过程中,隔离性体现在A向B转账,不能受其他事务干扰
在转账的过程中,持久性体现在事务提交后,要把数据持久化
ps:锁是事务的一部分,锁并不能保证一个事务内逻辑的原子性,比如在锁内部的逻辑执行一半报异常,或者执行一半就释放了锁,就没执行完,此时也不会像事务一样回滚
并发事务\隔离级别
并发事务带来哪些问题?怎么解决这些问题呢?MySQL的默认隔离级别是哪个?
并发事务的问题:
- 脏读:一个事务读到另一个事务未提交的数据
- 不可重复读:一个事务先后读取同一条记录,但两次读取的数据不同
- 幻读:一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了"幻影"
隔离级别:
read uncommitted 读未提交
read committed 读已提交 解决脏读
repeatable read 读可重复 解决脏读\不可重复读
serializable 串行化 解决脏读\不可重复读\幻读
隔离级别越高,数据越安全,但是性能越低
隔离级别默认repeatable read读可重复,一般都不会用到serializable 串行化,因为那就相当于放弃并发执行
undo log和redo log
undo log和redo log的区别
- redo log:记录的是数据页的物理变化,服务宕机可用来同步数据
- undo log:记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据,比如我们删除一条数据的时候,就会在undo log日志文件中新增一条对应的insert语句(相反的能恢复数据的语句),如果发生回滚就执行逆操作
- redo log保证了事务的持久性,undo log保证了事务的原子性和一致性
MVCC原理
事务中的隔离性是如何保证的呢
MySQL中的多版本并发控制,指维护一个数据的多个版本,使得读写操作没有冲突
- 隐藏字段
trx_id(事务id),记录每一次操作的事务id,是自增的
roll_pointer(回滚指针),指向上一个版本的事务版本记录地址 - undo log
回滚日志,存储老版本数据
版本链,多个事务并行操作某一行记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表 - readView解决的是一个事务查询选择版本的问题
根据readView的匹配规则和当前的一些事务id判断该访问那个版本的数据
不同的隔离级别快照读是不一样的,最终的访问的结果不一样
读已提交,每一次执行快照读时生成ReadView
读可重复,仅在事务中第一次执行快照读时生成readView(读视图),后续复用
其他
Mysql主从同步原理
Mysql主从同步原理是什么
MySQL主从复制的核心就是二进制日志binlog(DDL(数据定义语言)和DML(数据操纵语言))
- 主库在事务提交时,会把数据变更记录在二进制日志文件binlog中
- 从库读取主库的二进制日志文件Binlog,写入到从库的中继日志relaylog
- 从库重做中继日志中的事件,将改变反映它自己的数据
分库分表
你们项目中用过分库分表吗
- 业务介绍
根据自己简历上的项目,想一个数据量较大业务(请求数多或业务累计大)
达到了什么样的量级(单表1000万或超过20G) - 具体拆分策略
水平分库,将一个库的数据拆分到多个库中,解决海量数据存储和高并发的问题
水平分表,解决单表存储和性能问题
无论是水平分库还是水平分表都要考虑用中间件,sharding sphere还是mycat,这就是一个大坑,不如说了解过,但没用过,实际开发用的是阿里云服务,阿里云服务用的是sharding sphere
垂直分库,根据业务进行拆分,高并发下提高磁盘IO和网络连接数
垂直分表,冷热数据分离,多表互不影响
框架篇
spring中的单例bean是线程安全的吗
不是线程安全的
spring中的bean默认都是单例的
因为在spring中的bean中注入的一般都是无状态(不能被修改)的对象,没有线程安全问题
如果在bean中定义了可修改的成员变量,要考虑线程安全问题,可以使用多例或者加锁来解决
AOP相关
什么是AOP
面向切面编程,用于将哪些与业务无关,但对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合
你的项目有没有用到AOP(哪里可以用到AOP)
- 记录操作日志
- 缓存
- spring事务
spring中的事务如何实现
通过AOP功能,对方法前后进行拦截,在执行方法签开启事务,在执行方法后根据执行情况提交或回滚事务
spring事务失效的场景
- 异常捕获处理,自己处理了异常,没有抛出->手动抛出
- 抛出检查异常(编译时异常),配置rollbackFor属性为Exception
- 非public方法导致的事务失效,改为public
spring的bean的生命周期
构造函数->依赖注入->Aware接口->BeanPostProcessor#before->初始化方法->BeanPostProcessor#after->销毁bean
bean的循环依赖
什么是循环依赖
循环依赖,两个bean互相引用,形成闭环
如何解决
用三级缓存解决问题
- 一级缓存,单例池
- 二级缓存,缓存早期的bean对象(没有走完生命周期的bean对象)
- 三级缓存,缓存的是ObjectFactory,表示对象工厂,用来创建对象
构造方法出现循环依赖怎么办
用@Lazy进行懒加载,什么时候需要对象再进行bean对象的创建
SpringMVC的执行流程
SpringMVC中重要的组件
- DispatcherServlet(前端控制器)
- HandlerMapping(处理器映射器)
- HandleAdaptor(处理器适配器)
- ViewResolver(视图解析器)
SpringMVC的执行流程知道吗
视图版本(JSP)的前后端不分离的流程比较复杂
这里介绍前后端开发,接口开发版本
- 用户发送出请求到前端控制器DispatcherServlet
- DispatcherServlet收到请求调用HandlerMapping(处理器映射器)
- HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet
- DispatcherServlet调用HandleAdapter(处理器适配器)
- HandleAdaptor经过适配调用具体的处理器(Handler/Controller)
- 方法上添加了@ResponseBody (一般包含在@restController中)
- 通过HttpMessageConverter来返回结果转换为JSON并响应
Springboot自动配置原理
SpringBoot项目中的引导类上有一个注解@SpringBootApplication,此注解对三个注解进行了封装:
- @SpringBootConfiguration
- @EnableAutoConfiguration
- @ComponentScan
@EnableAutoConfiguration是实现自动化配置的核心注解,该注解通过@Import注解导入对应的配置选择器
内部是读取了该项目和该项目引用的Jar包的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名
在这些配置类中所定义Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中
条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用
Spring常见的注解
spring的常见注解有哪些
用于依赖注入,bean放入IOC容器中,IOC,DI
- @Component@Controller@Service@Respository,实例化bean
- @Autowired
- @Qualifier,结合@Autowired用于根据名称进行依赖注入
- @Scope,标注bean的作用范围
- @Configuration,指定当前类是一个spring配置类,当创建容器时会从该类上加载注解
- @ComponentScan,用于指定Spring在初始化容器时要扫描的包
- @Bean,用在方法上,将返回值放入容器
- @import,用于将其他配置类或组件类导入到当前的 Spring 配置类 中,通常是手动引入 Bean 的一种方式。
- @Aspect@Before@After@Around@Pointcut,用于AOP切面编程
springMVC常见的注解有哪些
用于请求响应 - @RequestMapping,映射请求路径
- @RequestParam,指定请求参数的名称
- @PathVariable,从请求路径中获取参数
- @RequestBody,将请求json转为Java对象
- @ResponesBody,将Controller返回值转化为json对象响应
- @RestController,@Controller+@ResponesBody
- RequestHeader,获取指定的请求头数据
Springboot常见注解有哪些 - @SpringBootConfiguration,组合了@Configuration注解,实现配置文件的功能
- @EnableAutoConfiguration,自动配置
- @ComponentScan,spring组件扫描,扫描那些带@Component及延伸注解的类
mybatis的执行流程
配置文件->构建会话工厂->创建会话->Executor执行器->MappedStatement对象->数据库
输入参数->MappedStatement对象->输出结果
mybatis的延迟加载
嵌套查询的时候可能用到,但一般不会用嵌套查询
mybatis是否支持延迟加载
mapper映射文件中启用延迟加载lazyloadingEnabled=true|false
延迟加载的底层原理知道吗
用CGLIB创建目标对象的代理对象,用反射invoke方法,检查目标方法是null值,执行sql查询
一级\二级缓存
mybatis的一级二级缓存用过吗
mybatis的二级缓存什么时候会清理缓存中的数据
微服务篇
spring cloud
- 服务注册 nacos\eureka
- 负载均衡 Ribbon负载均衡策略\自定义负载均衡
- 熔断\降级
- 监控 skywalking
业务相关
- 限流 漏桶算法\令牌桶算法
- 分布式事务 分布式理论CAP\BASE,分布式事务解决方案,seata
- 分布式服务接口幂等
- 分布式任务调度 xxl-job
springcloud
springcloud常见组件(必会题)
springCloud 5大组件有哪些
- Eureka,注册中心(Eureka现在可能停止更新了)
- Ribbon,负载均衡
- Feign,远程调用
- Hystrix,服务熔断
- Zuul/Gateway,网关
随着SpringCloudAlibaba在国内兴起,项目中还使用组件: - Nacos,注册中心\配置中心
- Ribbon,负载均衡
- Feign,服务调用
- sentinel,服务保护
- Gateway,服务网关
注册中心Nacos
服务注册和发现是什么意思?Spring Cloud如何实现服务注册发现
- 我们当时项目采用的eureka作为注册中心,这个也是spring cloud体系中的一个核心组件
- 服务注册,服务提供者需要把自己的信息注册到eureka,由eureka来保存这些信息,比如服务名称,ip,端口等
- 服务发现,消费者向eureka拉取服务列表信息,如果服务提供者有集群,则消费者会利用负载均衡算法,选择一个发起调用
- 服务监控,服务提供者会每隔30秒向eureka发送心跳,报告健康状态,如果eureka服务90秒没接收到心跳,从eureka中剔除
我看你之前也用过nacos,你能说下nacos与eureka的区别嘛 - Nacos与eureka的共同点(注册中心)
都支持服务注册和服务拉取
都支持服务提供者心跳方式做健康监测 - Nacos与eureka的区别(注册中心)
Nacos支持服务端主动检测提供者状态,临时实例采用心跳模式,非临时实例采用主动检测模式
临时实例心跳不正常会被剔除,非临时实例则不会被剔除(重点记)
Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式,Eureka采用AP方式 - Nacos还支持了配置中心,eureka则只有注册中心,也是选择使用nacos的一个重要原因
负载均衡Ribbon
你们项目负载均衡如何实现的
微服务的负载均衡主要使用了一个组件Ribbon,比如,我们在使用feign远程调用的过程中,底层的负载均衡就是使用了ribbon
Ribbon负载均衡策略有哪些?
- RoundRobinRule:简单轮询服务列表来选择服务器
- WeightedResponseTimeRule:按照权重来选择服务器,响应时间越长,权重越小
- RandomRule:随机选择一个可用的服务器
- ZoneAvoidanceRule:区域敏感策略,以区域可用的服务器为基础进行服务器的选择.使用Zone对服务器进行分类,这个Zone可以理解为一个机房,一个机架等.而后再对Zone内的多个服务做轮询(默认)
如果想自定义负载均衡策略如何实现? - 创建类实现IRule接口,可以指定负载均衡策略(全局)
- 在客户端的配置文件中,可以配置某一个服务调用的负载均衡策略(局部)
不用微服务架构可以有多个后端服务器嘛(我自己问的自己的问题)
多个后端服务器通常指的是在 单体应用 的基础上进行水平扩展,通过负载均衡器来分发请求。
但是微服务是水平扩展+垂直拆分
比如说一个项目有一千种服务,微服务就是一个服务一个(或多个)ip的端口\服务器,单体应用就是一千种服务一个或多个ip的端口\服务器
那肯定是微服务响应时间更快,符合高内聚低耦合的原则,专业的人做专业的事
就像瑞士军刀(占一个人的背包)和铲子\小刀\起瓶器\掏耳勺(占一个人的背包),当很多人需要不同的工具,还是铲子\小刀\起瓶器\掏耳勺更快
服务雪崩
什么是服务雪崩,怎么解决这个问题?
- 服务雪崩:一个服务失败,导致整条链路的服务都失败的情形
- 服务降级:服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃,一般在实际开发中与feign接口整合,编写降级逻辑
- 服务熔断:默认关闭,需要手动打开,如果检测到10秒内请求的失败率超过50%,就触发熔断机制.之后每隔5秒重新尝试请求微服务,如果微服务不能响应,继续走熔断机制,如果微服务可达,则关闭熔断机制,恢复正常请求
降级是针对某个接口的,熔断是针对整个服务的
java项目如果本地的代码和远程正在运行的项目的代码一致,可以通过一个工具修改本地代码来测试效果,这个工具叫什么(我自己想到一个工具,怕忘了)
JRebel可以实现热部署
微服务的监控
你们的微服务是怎么监控的?
我们项目中采用的skyWalking进行监控的
- skywalking主要可以监控接口\服务\物理实例的一些状态.特别是在压测的时候可以看到众多服务中哪些服务和接口比较慢,我们可以针对性的分析和优化
- 我们还在skywalking设置了告警规则,特别是在项目上线以后,如果报错,我们分别设置了可以给相关负责人发短信和发邮件,第一时间知道项目的bug情况,第一时间修复
为什么需要监控 - 问题定位
- 性能分析
- 服务关系
- 服务告警
业务问题
微服务限流
你们项目中有没有做过限流?怎么做的?
- 我们当时有一个活动,到了假期就会抢购优惠券,QPS最高可以达到2000,平时10-50之间,为了应对突发流量,需要做限流
- 常规限流,为了防止恶意攻击,保护系统正常运行,我们当时系统能够承受最大的QPS是XX(压测结果)
nginx限流
- 控制速率(突发流量),使用的漏桶算法来实现过滤,让请求以固定的速率处理请求,可以应对突发流量
- 控制并发数,限制单个ip的链接数和并发链接的总数
网关限流 - 在spring cloud gateway中支持局部过滤器requestRateLimiter来做限流,使用的是令牌桶算法
- 可以根据ip或路径进行限流,可以设置每秒填充平均速率,和令牌桶总容量
限流常见的算法有哪些?
回答漏桶算法和令牌桶算法
分布式系统理论
解释一下CAP和BASE
- CAP定理(一致性(consistency)\可用性(availability)\分区容错性(partition tolerance))
分布式系统节点通过网络连接,一定会出现分区问题§
当分区出现时,系统的一致性©和可用性(A)就无法同时满足 - BASE理论
基本可用(basically available)
软状态(soft state)
最终一致(eventually consistent) - 解决分布式事务的思想和模型
最终一致思想:各分支事务分别执行并提交,如果有不一致的情况,再想办法恢复数据(AP)(基于base理论)
强一致思想:各分支事务执行完业务不要提交,等待彼此结果,而后统一提交或回滚(CP)
一般问题的解决方案就是,费空间就省时间,强一致性就低可用性低响应时间低吞吐量
获得某个东西,就得失去某个东西
分布式事务解决方案
简历上写了微服务项目才会问
你们采用哪种分布式事务解决方案?
- 只要是发生了多个服务之间的写操作,都需要进行分布式事务控制
- 描述项目中采用的哪种方案(seata|MQ)
seata的XA模式,CP,需要互相等待各个分支事务提交,可以保证强一致性,性能差
seata的AT模式,AP,底层使用undo log实现,性能好
seata的TCC模式,AP,性能较好,不过需要人工编码实现
MQ模式实现分布式事务,在A服务写数据的时候,需要在同一个事务内发送消息到另外一个事务,异步,性能最好
Seata事务管理中的三个重要的角色:
- TC(transaction Coordinator)-事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚
- TM(transaction manager)-事务管理器:定义全局事务的范围\开始全局事务\提交或回滚全局事务
- RM(resource manager)-资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚
分布式服务的接口幂等性如何设计?
幂等:多次调用方法或接口不会改变业务状态,可以保证重复调用的结果和单词调用的结果一致
- 如果是新增数据,可以使用数据库的唯一索引
- 如果是新增或者修改数据
-分布式锁,性能较低- 使用token+redis来实现,性能较好,第一次请求,生成一个唯一token存入redis,返回给前端.第二次请求,业务处理,携带之前的token,到redis进行验证,如果存在,可以执行业务,删除token,不存在则直接返回,不处理业务
分布式任务调度
你们项目中使用了什么分布式任务调度?
比如,花呗月底给每个用户发还款通知,任务量大
一般用xxl-job解决
xxl-job路由策略有哪些
- ROUND(轮询)
- FALLOVER(故障转移),按照顺序依次进行心跳检测,第一个心跳检测成功的机器选定为目标执行器并发起调度
- SHARDING_BROADCAST(分片广播),广播触发对应集群中所有机器执行一次任务,同时系统自动传递分片参数,可根据分片参数开发分片任务
xxl-job任务执行失败怎么解决? - 路由策略选择故障转移,使用健康的实例来执行任务
- 设置重试次数
- 查看日志+邮件告警来通知相关负责人解决
如果有大数据量的任务同时都需要执行,怎么解决? - 让多个实例一块去执行(部署集群),路由策略分片广播
- 在任务执行的代码中可以获取总数和当前分片,按照取模的方式分摊到各个实例执行
分片广播快在所有分片并行执行,而轮询是顺序执行
消息中间件篇
RabbitMQ
- 消息不丢失
- 消息重复消费
- 消息堆积
- 延迟队列
- 死信队列
- 高可用机制
保证消息不丢失
RabbitMQ如何保证消息不丢失
- 开启生产者确认机制,确保生产者的消息能到达队列
- 开启持久化功能,确保消息未消费前在队列不会丢失
- 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
- 开启消费者失败重试机制,多次重试失败后将消息投递到异常交换机,交由人工处理
一个是通知生产者消息到MQ了,一个是通知MQ消息被消费者消费了,一个是防止MQ是否宕机
消息的重复消费
RabbitMQ消息的重复消费问题如何解决的?
我们当时消费者是设置了自动确认机制,当服务还没来得及给MQ确认的时候,服务宕机了,导致服务重启之后,又消费了一次消息,这样就重复消费了
因为我们当时处理的支付(订单|业务唯一标识),他有一个业务的唯一标识,我们再处理消息时,先到数据库查询一下,这个数据是否存在,如果不存在,说明没有处理过,这个时候就可以正常处理这个消息了.如果已经存在这个数据了,就说明消息重复消费了,我们就不需要再消费了
那你还知道其他的解决方案嘛?
其实这个是典型的幂等的问题,比如,redis分布式锁\数据库的锁都是可以的
死信交换机
RabbitMQ中死信交换机?(RabbitMQ延迟队列有了解过嘛)
- 我们当时一个XX业务使用到了延迟队列(超时订单\限时优惠\定时发布)
- 其中延迟队列就用到了死信交换机和TTL(消息存活时间)实现的
- 消息超时未消费就会变成死信(死信的其他情况:拒绝被消费,队列满了)
延迟队列插件实现延迟队列DelayExchange - 声明一个交换机,添加delayed属性为true
- 发送消息时,添加x-delay头,值为超时时间
消息堆积
RabbitMQ如果有100万消息堆积在MQ,如何解决(消息堆积怎么解决)
解决消息堆积有三种思路
- 增加更多消费者,提高消费速度(占用多台服务器资源)
- 在消费者内开启线程池加快消息处理速度(在一台服务器上占用更多资源)
- 扩大队列容积,提高堆积上限,采用惰性队列
- 在声明队列的时候可以设置属性x-queue-mode为lazy,即为惰性队列
- 基于磁盘存储,消息上限高
- 性能比较稳定,但基于磁盘存储,受限于磁盘IO,时效性会降低
高可用机制
RabbitMQ的高可用机制有了解过嘛
- 在生产环境中,我们当时采用的镜像模式搭建的集群,共有3个节点
- 镜像队列结构是一主多从(从就是镜像),所有操作都是主节点完成,然后同步给镜像节点
- 主宕机后,镜像节点会替代成为新的主(如果在主从同步完成前,主就已经宕机,可能出现数据丢失)
那出现丢数据怎么解决呢
我们可以采用仲裁队列,与镜像队列一样,都是主从模式,支持主从数据同步,主从同步基于raft协议,强一致.并且使用起来也非常简单,不需要额外的配置,在声明队列的时候只要指定这个是仲裁队列即可
三种队列集群方式 - 普通集群
- 镜像集群
- 仲裁队列集群
集合篇
面试中常被问集合,ArrayList,LinkedList,HashMap,ConcurrentHashMap
ArrayList
介绍一下什么是数组array
用连续的内存空间存储相同数据类型数据的线性数据结构
数组如何获取其他元素的地址值
寻址公式:baseAddress+idataTypeSize
为什么数组索引从0开始:,假如从1开始不行吗
从0开始:baseAddress+idataTypeSize
从1开始:baseAddress+(i-1)*dataTypeSize
从1开始多了一次减法指令运算,从0开始计算效率较高
查找的时间复杂度
通过下标,o(1)
未知下标,o(n)
已排序,二分查找,o(logn)
插入和删除时间复杂度
插入和删除,为了保证数组的内存连续性,连续挪动,时间复杂度都是o(n)
源码分析
源码基于JDK8
底层是依据数组实现的
这里建议去看C++的理论分析,源码分析还是不好理解,主要内容就是初始构造,添加和扩容的关系
空集合没有容量
容量和集合长度(大小)不是一个概念
ArraysList底层的实现原理是什么
- ArrayList底层是用动态的数组实现的
- ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
- 在进行扩容时是原来容量的1.5倍,每次扩容都需要拷贝数组
- 在添加数据时
ArrayList的size加1后如果大于当前的数组长度(length),则调用grow方法扩容(1.5倍)
确保新增的数据有地方存储后,将新元素添加到位于size的位置上
ArrayList的size加1后如果小于等于当前的数组长度(length),则不需要扩容
这里容量就是数组长度(length),ArrayList的size是位于数组的size索引位置
ArrayList list=new ArrayList(10)中的list扩容几次
只是声明和实例了一个ArrayList,指定了数组的长度为10,集合的容量和10,未扩容
如何实现数组和list之间的转换
数组转list,使用JDK中java.util.Arrays工具类的asList方法
List转数组,使用List的toArray方法,无参toArray方法返回Object数组,传入初始化长度的数组对象,返回该对象数组
用Arrays.asList转List后,如果修改了数组内容,list受影响吗
list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,只是引用了原数组,指向的都是同一个内存地址
list用toArray转数组后,如果修改了List内容,数组受影响吗
数组不会受影响,调用了toArray以后,在底层进行了数组的拷贝,跟原来的元素没关系了,
LinkedList
单向链表和双向链表的区别是什么
单向链表只有一个方向,节点只有一个后继指针next
双向链表支持两个方向,每个节点不止有一个后继指针next,还有前驱指针prev指向前面
双向链表和单向链表都有frist和last分别指向头和尾
链表操作数据的时间复杂度是多少
无论是单向链表还是双向链表,增删的平均时间复杂度都是O(n),主要是要有查询的过程
ArrayList和LinkedList的区别是什么
四个层面,结构,时间效率,空间效率,安全
- 底层数据结构
ArrayList是动态数组的数据结构实现
LinkedList是双向链表的数据结构实现 - 操作数据效率(增删改查)
ArrayList按照下标查询的时间复杂度O(1),linkedList不支持下标查询
查找:ArrayList需要遍历,链表也需要链表时间复杂度都是O(n)
新增和删除,ArrayList的平均时间复杂度是O(n)(准确来说查要O(n),删增也要O(n)),尾部是O(1)
LinkedList平均是O(n),头尾增删为O(1) - 内存空间占用
ArrayList底层是数组,内存连续,节省内存
LinkedList是双向链表需要存储数据和两个指针,更占内存 - 线程安全
ArrayList和LinkedList都不是线程安全的(比如,两个线程同时 add() 时,一个线程的修改可能覆盖另一个线程的修改)
如果要保证线程安全,两种方案:
在方法内使用,局部变量则是线程安全的
使用线程安全的ArrayList和LinkedList
List objects = Collections.synchronizedList(new ArrayList<>());
List objects1 = Collections.synchronizedList(new LinkedList<>());
HashMap
数据结构,数组+链表+红黑树,阿伟前面讲的不是这样的结构呀???
红黑树
红黑树的复杂度
二叉树->二叉搜索树(相比二叉树就是插入的时候排好了,找的时候方便)->红黑树(自平衡的二叉搜索树(BST))->B+树
遍历都可以使用前\中\后序遍历和层次遍历,但只有中序遍历是按排序遍历的
红黑树的规则都是为了让二叉树保证平衡,不形成极端的二叉树
查找,O(logn)
添加和删除,都要先查找(O(logn)),再旋转(O(1)),所以复杂度就是O(logn)
散列表HashTable
散列表又名哈希表
怎么解决哈希冲突
拉链法,数组的每个下标位置称为桶(bucket)或槽(slot),每个槽对应一条链表
hash冲突后的元素放到相同槽对应的链表或红黑树中(更好用的是红黑树,一是降低时间复杂度,二是防止恶意的Dos攻击,大量的访问攻击,将链表的时间复杂度O(n)降为O(logn))
HashMap的实现原理
HashMap的实现原理是什么
底层使用hash表数据结构,即数组+链表或红黑树
添加数据时,计算key的值确定元素在数组中的下标
- key相同则替换
- 不同则存入链表或红黑树中
获取数据通过key的hash计算数组下标获取元素
HashMap的jdk1.7和jdk1.8有什么区别
JDK1.8之前采用的拉链法,数组+链表
JDK1.8之后采用数组+链表+红黑树,链表长度大于8且数组长度大于64则会从链表转化为红黑树
HashMap的put方法的具体流程
HashMap的put方法的具体流程
- 判断键值对数组table是否为空或null,是的话,执行resize()进行扩容(初始化)
- 根据键值key计算hash值得到数组索引
- 判断table[i]==null,条件成立,直接新建节点添加
- 如果table[i]==null,不成立
- 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
- 判断table[i]是否为treeNode(红黑树),即table[i]是否是红黑树,则遍历,有相同key覆盖,无则插入
- 如果是链表,则遍历,有相同key覆盖,无则插入,判断链表长度是否大于8,大于8把链表转换为红黑树,在红黑树中执行插入操作
- 插入成功后,判断实际存在的键值对数量size是否超过了最大容量threshold(数组长度*0.75),如果超过,进行扩容
我这里有个疑问hashcode是如何计算的,如果扩容之后,hashcode的计算方法是否会变,如果变了,之前插入的数据按现在的hashcode算法是否还是定位到那个数组索引
HashMap的扩容机制
讲一讲HashMap的扩容机制
注意在旧数组中在一个桶中的节点,在扩容后可能分配到不同桶
在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次扩容都是达到扩容阈值(数组长度*0.75)
每次扩容的时候,都是扩容之前容量的2倍(数组的长度必须也肯定是2的n次幂)
扩容之后,会创建一个数组,需要把老数组中的数据挪到新的数组中
没有hash冲突的桶的节点,直接使用e.hash&(newCap-1)计算新数组的索引位置
如果是链表,遍历链表,判断每个节点(e.hash&oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+旧数组大小的位置
如果是红黑树,遍历红黑树,和链表节点的放置类似,但是红黑树的节点在新位置的添加和旧位置删除很复杂,如果插入的桶里面是红黑树,在新的位置还要考虑左旋右旋那些东西
为什么不管是这个桶是一个节点还是链表还是红黑树不都用e.hash&(newCap-1)计算节点的索引位置??
我不知道,但应该是甲酸效率更高
hashMap的寻址算法
为什么计算索引位置不取模而是按位与
采用e.hash&(newCap-1)而不是(newCap-1)%e.hash是因为e.hash&(newCap-1)性能更好,计算资源消耗小
为什么可以用(e.hash&oldCap)是否为0这么判断
因为老数组和新数组的长度的二进制数差异就在oldCap二进制数为1的那一位
e.hash&(Cap-1)计算数组的索引位置
比如,长度为16时,Cap是00001111
32时,Cap是00011111
oldCap是00010000,位与判断第4位是不是1就可以知道放在新位置还是不动
hashMap的寻址算法
计算对象的hashCode()
再进行调用hash()方法进行二次哈希,hashcode值右移16位再异或运算,让哈希分布更为均匀(扰动算法,hash^(hash>>16))
最后(capacity-1)&hash得到索引
为何HashMap的数组长度一定是2的次幂
计算索引时效率更高,如果是2的n次幂可以使用位与运算代替取模
扩容时重新计算搜因效率更高,hash&oldCap==0的元素留在原来位置,否则新位置=旧位置+oldCap
hashMap在1.7情况下的多线程死循环问题
hashMap在1.7情况下的多线程死循环问题
在jdk1.7的hashmap中在数组进行扩容的时候,因为链表是头插法,在进行数据前移的过程中,有可能导致死循环
所以JDK8中采用尾插法
具体过程很绕,这里就不赘述了
hashMap遍历时会遍历空桶吗?
会,hashMap遍历基于迭代器,当前桶遍历完,会遍历下一个桶
并发编程篇
线程的基础知识
线程和进程的区别
- 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
并行和并发
并行和并发的区别
在多核CPU下
- 并发是同一时间应对多件事情的能力,多个线程轮流在不同时间片使用一个或多个CPU
- 并行是同一时间动手做多件事情的能力,比如4核CPU同时执行4个线程
创建线程的方式
创建线程的方式有哪些
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 线程池创建线程(项目中使用方式)
runnable和callable有什么区别 - Runnable接口run方法没有返回值
- Callable接口call方法有返回值,需要FutureTask获取结果
- Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
run()和start()有什么区别
start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码,start方法只能被调用一次
run():封装了要被线程执行的代码,可以被调用多次
线程包含的状态,状态之间的变化
线程包含了哪些状态
新建\可运行\阻塞\等待\时间等待\终止
线程之间是如何变化的
创建线程对象是新建状态
调用start()方法转变为可运行状态
线程获取到CPU的执行权,执行结束是终止状态
在可运行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
- 如果没有获取锁(synchronized或lock)进入阻塞状态,获取锁再切换为可执行状态
- 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可切换为可执行状态
- 如果线程调用了sleep()方法,进入计时等待状态,到时间后可切换为可执行状态
线程按顺序执行
新建T1\T2\T3三个线程,如何保证它们按顺序执行
使用线程中的join方法解决
比如,在T2中调用T1.join(),阻塞调用此方法的线程进入timed_waiting直到线程T1执行完成后,此线程再继续执行
notify()和notifyAll()的区别
notify()和notifyAll()有什么区别
notifyAll:唤醒所有wait的线程
notify:只随机唤醒一个wait线程
java中wait和sleep方法的不同
java中wait和sleep方法有什么不同
共同点
wait(),wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态
不同点
方法归属不同
- sleep(long)是Thread的静态方法
- 而wait(),wait(long)都是Object的成员方法,每个对象都有
醒来时机不同 - 执行sleep(long)和wait(long)的线程都会在等待响应毫秒后醒来
- wait(long)和wait()还可以被notify唤醒,wait()如果不唤醒就一直等下去
- 它们都可以被打断唤醒(参考下面一个面试题)
锁特性不同(重点) - wait方法的调用必须先获取wait对象的锁,而sleep则无此限制
- wait方法执行后会释放对象锁,允许其他线程获得该对象锁(我放弃CPU,但你们可以用)
- 而sleep如果在synchronized代码块中执行,并不会释放对象锁(我放弃CPU,你们也用不了)
停止一个正在运行的线程
如何停止一个正在运行的线程
- 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
- 使用stop方法强行终止(不推荐,方法已作废)
- 使用interrupt方法中断线程
-
- 打断阻塞的线程(sleep\wait\join),线程会抛出interruptedException异常
-
- 打断正常的线程,可以根据打断状态来标志是否退出线程
线程的并发安全
synchronized的底层原理
synchronized关键字的底层原理
synchronized底层是Monitor
- synchronized对象锁采用互斥的方式让同一时刻至多只有一个线程能持有对象锁
- 它的底层由monitor实现的,monitor是jvm级别的对象(C++实现),线程获得锁需要使用对象(锁)关联monitor
- 在monitor内部有三个属性,分别是owner\entrylist\waitset
- 其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于waiting状态的线程
synchronized的底层原理进阶
轻量级锁\重量级锁
轻量级锁在线程间没有竞争时使用,都没竞争为什么要上锁,上锁是为了防止竞争,但是实际可能没竞争,这种情况用重量级锁性能低,一但竞争就会升级为重量级锁
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
- 重量级锁:底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
- 轻量级锁:线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
- 偏向锁:一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令
一旦锁发生了竞争,都会升级为重量级锁
JMM(java内存模型)
你谈谈JMM(java内存模型)
- JMM(Java Memory Model)Java内存模型,定义了共享内存中多线程程序读写操作的行为规范,通过这些规则了规范对内存的读写操作从而保证指令的正确性
- JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
- 线程跟线程之间是相互隔离,线程跟线程交互需要通过主内存
CAS
CAS的精髓是竞争,谁竞争成功,谁执行成功
CAS你知道吗
- CAS的全称是:Compare And Swap(比较再交换);它体现的一种乐观锁的思想,在无锁状态下保证线程操作数据的原子性
- CAS使用到的地方很多:AQS框架\Atomicxxx类
- 在操作共享变量的时候使用的自旋锁,效率上更高一些
- CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现
乐观锁和悲观锁的区别 - CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,无非就是重试重新执行
- synchronized是基于悲观锁的思想,最悲观的估计,得防着其他线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会
volatile
请谈谈你对volatile的理解
JVM虚拟机中有一个JIT(即时编译器)给代码做优化
volatile是一个关键字,可以修饰类的成员变量,类的静态成员变量,主要有两个功能
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说时立即可见的,volatile关键字回强制将修改的值立即写入主存
- 禁止进行指令重排序,可以保证代码执行有序性.底层实现原理是,添加了一个内存屏障,通过插入内存屏障禁止在内存屏障前后的指令执行重排序
AQS(队列同步器)
什么是AQS
- 是多线程中的队列同步器,是一种锁机制,它是作为一个基础框架使用的,将ReentrantLock\Semaphore都是基于AQS实现的
- AQS内部维护了一个先进先出的双向队列,队列中存储的排队的线程
- 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中的有一个线程修改state为1了,则当前线程就相当于获取了资源
- 在对state修改的时候使用的cas操作,保证多个线程修改的情况下原子性
ReentrantLock(可重入锁)
什么是ReentrantLock
- ReentrantLock表示支持重新进入的锁,调用lock方法获取了锁之后,再次调用lock,是不会再阻塞
- ReentrantLock主要利用CAS+AQS队列来实现
- 支持公平锁和非公平锁,在提供的构造器的中无参默认是非公平锁,也可以传参设置为公平锁
synchronized和Lock的区别
synchronized和Lock有什么区别
- 语法层面
synchronized是关键字,源码在jvm中,用C++实现
Lock是接口,源码由jdk提供,用java语言实现
使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁 - 功能层面
二者都属于悲观锁,都具备基本的互斥\同步\锁重入功能
Lock提供了许多synchronized不具备的功能,例如公平锁,可打断,可超时,多条件变量
Lock由适合不同场景的实现,如ReentrantLock,ReentrantReadWriteLock(读写锁) - 性能层面
在没有竞争时,synchronized做了很多优化,如偏向锁,轻量级锁,性能不赖
在竞争激烈时,Lock的实现通常会提供更好的性能
死锁产生的条件
死锁产生的条件是什么
一个线程需要同时获取多把锁,这时就容易发生死锁
如何进行死锁诊断
- 当程序出现了死锁现象,我们可以使用jdk自带的工具:jps和jstack
- jps:输出JVM中运行的进程状态信息
- jstack:查看java进程内线程的堆栈信息,查看日志,检查是否有死锁,如果有死锁现象,需要查看具体代码分析后,可修复
- 可视化工具jconsole\Visual也可以检查死锁问题
ConcurrentHashMap
你了解ConcurrentHashMap吗
1.底层数据结构
- JDK1.7底层采用分段的数组+链表实现
- JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表\红黑树
2.加锁的方式 - JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
- JDK1.8采用CAS添加新节点,采用synchronized锁定链表或红黑树的首节点,相对Segment分段锁粒度更细,性能更好
导致并发程序出现问题的根本原因是什么
导致并发程序出现问题的根本原因是什么
1.原子性 解决方法:synchronized\lock
2. 内存可见性 解决方法:volatile\synchronized\lock
3. 有序性 解决方法:volatile
线程池
new ThreadPoolExecutor(
3,//核心线程数
6,//最大线程数
60,//存活时间
TimeUnit.SECONDS,//存活时间时间单位
new ArrayBlockingQueue<>(3),//阻塞队列长度
Executors.defaultThreadFactory(),//线程工厂
new ThreadPoolExecutor.DiscardPolicy()//拒绝策略
);
线程池的执行原理
说一下线程池的核心参数或线程池的执行原理知道吗
corePoolSize 核心线程数目 - 池中会保留的最多线程数
maximumPoolSize 最大线程数目 - 核心线程数目 + 救急线程的最大数目
keepAliveTime 生存时间 - 救急线程的生存时间,生存时间内没有新任务,此线程资源会释放.也就是没有新的线程来到这个线程池的此救急线程资源空位,这个资源就会被释放掉,除掉这个空位
unit 时间单位 - 救急线程的生存时间单位,如秒\毫秒等
workQueue - 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
threadFactory 线程工厂 - 可以定义线程对象的创建,例如设置线程名字\是否是守护线程等
handler 拒绝celve - 当所有线程都在繁忙,workQueue也繁忙时,会出发拒绝策略
四种拒绝策略:
- abortPolicy:直接抛出异常,默认策略
- CallerRunsPolicy:用调用者所在的线程来执行任务
- DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务
- DiscardPolicy:直接丢弃任务
线程池中常见的阻塞队列
线程池中有哪些常见的阻塞队列
|LinkedBlockingQueue|ArrayBlockingQueue|
|默认无界,支持有界|强制有界|
|底层是链表|底层是数组|
|是懒惰的,创建节点的是哦胡添加数据|提前初始化Node数组|
|入队会生成新的Node|Node需要提前创建好|
|两把锁(头尾)|一把锁|
Jdk中提供了很多阻塞队列,开发中常见的有两个:ArrayBlockingQueue和LinkedBlockingQueue
ArrayBlockingQueue和LinkedBlockingQueue是Java中两种常见的阻塞队列,它们在实现和使用上有一些关键的区别。
首先,ArrayBlockingQueue是一个有界队列,它在创建时必须指定容量,并且这个容量不能改变。而LinkedBlockingQueue默认是无界的,但也可以在创建时指定最大容量,使其变为有界队列。
其次,它们在内部数据结构上也有所不同。ArrayBlockingQueue是基于数组实现的,而LinkedBlockingQueue则是基于链表实现的。这意味着ArrayBlockingQueue在访问元素时可能会更快,因为它可以直接通过索引访问数组中的元素。而LinkedBlockingQueue则在添加和删除元素时可能更快,因为它不需要移动其他元素来填充空间。
另外,它们在加锁机制上也有所不同。ArrayBlockingQueue使用一把锁来控制对队列的访问,这意味着读写操作都是互斥的。而LinkedBlockingQueue则使用两把锁,一把用于控制读操作,另一把用于控制写操作,这样可以提高并发性能。
核心线程数
如何确定核心线程数
- 高并发\任务执行时间短->(CPU核数+1),减少线程上下文的切换
- 并发不高\任务执行时间长
- IO密集型的任务->(CPU核数*2+1)
- 计算密集型任务->(CPU核数+1)
- 并发高\业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体框架的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置,设置参考(2)
线程池的种类有哪些
就是在java.util.concurrent.Executors中定义好核心参数的线程池
- newFixedTreadPool:创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
- newSingleThreadExecutor:创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行
- newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程,全是临时线程,没有核心线程
- newScheduleThreadPool:可以执行延迟任务的线程池,支持定时及周期性任务执行
为什么不建议用Executors创建线程池
为什么不建议用Executors创建线程池
在一些公司的开发手册中明确线程池不允许用Executor去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
弊端如下:
- FixedThreadPool和SingleThreadPool,允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM(内存溢出错误)
- CachedThreadPool,允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM.
多线程的使用场景
多线程的使用有时很难想到,因为总会忘记自己有多个cpu
ps:即使只有一个CPU,也可以和其他进程抢更多的时间片,执行更快
CountDownLatch
在我们项目上线之前,我们需要把数据库中的数据一次性同步到es索引库中但是当时的数据好像是1000万左右,一次性读取数据肯定不行(OOM异常),当时我就想到可以使用线程池的方式导入,利用CountDownLatch来控制,就能避免一次性加载过多,防止内存溢出
- 不能一次读取,内存溢出
- 不能循环读取,太慢
- 用多线程,快而且不会内存溢出
数据汇总
嗯~~,我想一下当时的场景[根据自己简历上的模块设计多线程场景]
参考场景一:
es数据批量导入
在我们项目上线之前,我们需要把数据量的数据一次性的同步到es索引库中,但是当时的数据好像是1000万左右,一次性读取数据肯定不行(oom异常),如果分批执行的话,耗时也太久了。所以,当时我就想到可以使用线程池的方式导入,利用CountDownLatch+Future来控制,就能大大提升导入的时间。
参考场景二:
在我做那个xx电商网站的时候,里面有一个数据汇总的功能,在用户下单之后需要查询订单信息,也需要获得订单中的商品详细信息(可能是多个),还需要查看物流发货信息。因为它们三个对应的分别三个微服务,如果一个一个的操作的话,互相等待的时间比较长。所以,我当时就想到可以使用线程池,让多个线程同时处理,最终再汇总结果就可以了,当然里面需要用到Future来获取每个线程执行之后的结果才行
参考场景三:
《黑马头条》项目中使用的
我当时做了一个文章搜索的功能,用户输入关键字要搜索文章,同时需要保存用户的搜索记录(搜索历史),这块我设计的时候,为了不影响用户的正常搜索,我们采用的异步的方式进行保存的,为了提升性能,我们加入了线程池,也就说在调用异步方法的时候,直接从线程池中获取线程使用
如何控制某个方法允许并发访问线程的数量
如何控制某个方法允许并发访问线程的数量
嗯~~,我想一下
- 在jdk中提供了一个Semaphore[sem?f??r]类(信号量)
- 它提供了两个方法,
- semaphore.acquire() 请求信号量,可以限制线程的个数,是一个正数,如果信号量是-1,就代表已经用完了信号量,其他线程需要阻塞了
- 第二个方法是semaphore.release(),代表是释放一个信号量,此时信号量的个数+1
ThreadLocal
谈谈你对ThreadLocal的理解
ThreadLocal 主要功能有两个,第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题,第二个是实现了线程内的资源共享
那你知道ThreadLocal的底层原理实现吗
- 在ThreadLocal内部维护了一个一个 ThreadLocalMap 类型的成员变量,用来存储资源对象
- 当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
- 当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
- 当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
那关于ThreadLocal会导致内存溢出这个事情,了解吗
我之前看过源码,我想一下 - 是因为ThreadLocalMap 中的 key 被设计为弱引用,它是被动的被GC调用释放key,不过关键的是只有key可以得到内存释放,而value不会,因为value是一个强引用。
- 在使用ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收,建议主动的remove 释放 key,这样就能避免内存溢出。
JVM虚拟机篇
JVM组成
程序计数器
什么是程序计数器
线程私有的,每个线程一份,内部保存的字节码的行号.用于记录正在执行的字节码指令的地址
堆
你能给我详细的介绍java堆吗
线程共享的区域:主要用来保存对象实例,数组等,内存不够则抛出OOM异常
组成:年轻代+老年代
- 年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区
- 老年代主要保存生命周期长的对象,一般是一些老的对象
JDK1.7和1.8的区别: - 1.7中有一个永久代,存储的是类信息\静态变量\常量\编译后的代码
- 1.8移除了永久代,把数据存储到了本地内存的元空间中,防止内存移除
栈
什么是虚拟机栈
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧组成.对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法
垃圾回收是否涉及栈内存
不涉及,垃圾回收主要指就是堆内存,当栈帧弹栈以后,内存就会释放
占内存分配越大越好吗
未必,默认的栈内存通常为1024K,栈帧过大会导致线程数变少
方法内的局部变量是否线程安全 - 如果方法内局部变量没有逃离方法的作用范围,是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
什么情况下会导致栈内存溢出 - 栈帧过多(递归调用)
- 栈帧过大
堆栈的区别是什么 - 栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储java对象和数组的,堆会GC垃圾回收,而栈不会
- 栈内存是线程私有的,而堆内存是线程公有的
- 两者异常错误不同,但如果栈内存或堆内存不足都会抛出异常
栈空间不足:StackOverFlowError
堆空间不足:OutOfMemoryError(OOM)
方法区
能不能解释一下方法区?
- 方法区是各个线程共享的内存区域
- 主要存储类的信息,运行时常量池
- 虚拟机启动的时候创建,关闭虚拟机时释放
- 如果方法区域中的内存无法满足分配请求,则会抛出OOM错误:Metaspace
介绍一下运行时常量池
常量池:可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名\方法名\参数类型\字面量等信息
当类被加载,它的常量池信息就会被放入运行时常量池,并把里面的符号地址变为真实地址
直接内存
你听说过直接内存吗
- 并不属于JVM中的内存结构,不由JVM进行管理.是虚拟机的系统内存
- 常见于NIO操作时,用于数据缓冲区,分配回收成本较高(这个就是直接内存的缺点),但读写性能高,不受JVM内存回收管理
类加载器
什么是类加载器
JVM只会二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让java程序能够启动起来
类加载器有哪些
- 启动类加载器(BootStrap ClassLoader):加载JAVA_HOME/jre/lib目录下的库
- 扩展类加载器(ExtClassLoader):主要加载JAVA_HOME/Jre/lib/ext目录中的类
- 应用类加载器(AppClassLoader):用于加载classPath下的类
- 自定义类加载器(CustomizeClassLoader):自定义类继承ClassLoader,实现自定义类加载规则
什么是双亲委派模型
加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类
JVM为什么采用双亲委派机制 - 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性
- 为了安全,保证类库API不会被修改
说一下类装载的执行过程? - 加载:查找和导入class文件
- 验证:保证加载类的准确性
- 准备:为类变量分配内存并设置类变量初始值
- 解析:把类中的符号引用转换为直接引用
- 初始化:对类的静态变量,静态代码块执行初始化操作
- 使用:JVM开始从入口方法开始执行用户的程序代码
- 卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象
垃圾回收
确定可被垃圾回收的算法
对象什么时候可以被垃圾器回收?
如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收
定位垃圾的方式有两种:
- 引用计数法
- 可达性分析算法
垃圾回收算法
JVM垃圾回收算法有哪些?
- 标记清除算法:垃圾回收分为2个阶段,分别是标记和清除,效率高,有磁盘碎片,内存不连续
- 标记整理算法:标记和清除和标记清除算法一样,但是需要将存活对象都向内存另一端移动,然后清理边界以外的垃圾,无碎片,对象需要移动,效率低
- 复制算法:将原有的内存空间一分为二,每次只用其中的而一块,正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收,无碎片,内存使用率低
分代回收
说一下JVM中的分代回收
堆的区域划分
- 堆被分为了两份:新生代和老年代(1:2)
- 对于新生代,内部又被分为了三个区域.Eden区,幸存者区survivor(分成from和to)(8:1:1)
对象的回收分代回收策略
- 新创建的对象,都会先分配到eden区
- 当伊甸园区内存不足,标记伊甸园与from(现阶段没有)的存活对象
- 将存活对象采用复制算法复制到to中,复制完毕后,伊甸园和from内存都得到释放
- 经过一段时间后伊甸园的内存又出现不足,标记eden区域to区存活的对象,将其复制到from区
- 当幸存区(新生代)对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会提前晋升)
MinorGC\Mixed GC\FullGC的区别是什么
- MinorGC(young GC)发生在新生代的垃圾回收,暂停时间短(STW)
- Mixed GC 新生代+老年代部分区域的垃圾回收,G1收集器特有
- FullGC,新生代+老年代完整垃圾回收,暂停时间长(STW),应尽力避免
STW(Stop-The-World),暂停所有应用程序线程,等待垃圾回收的完成
垃圾回收器
垃圾回收器是基于定位垃圾方法和垃圾回收算法实现的
说一下JVM有哪些垃圾回收器?
- 串行垃圾回收器,Serial GC\Serial Old GC
- 并行垃圾回收器,Parallel Old GC\ ParNew GC
- CMS(并发)垃圾回收器:CMS GC,作用在老年代
- G1垃圾回收器,作用在新生代和老年代
G1垃圾回收器
详细聊一下G1垃圾回收器
- 应用于新生代和老年代,在JDK9之后默认使用G1
- 划分成多个区域,每个区域都可以充当eden\survivor\old\humongous,其中humongous专为大对象准备
- 采用复制算法
- 响应时间与吞吐量兼顾
- 分成三个阶段:新生代回收(stw)\并发标记(重新标记stw)\混合收集
- 如果并发失败(即回收速度赶不上创建新对象速度),会触发Full GC
强引用\软引用\弱引用\虚引用
强引用\软引用\弱引用\虚引用
- 强引用:只要所有GC Roots能找到,就不会被回收
- 软引用:需要配合SoftReference使用,当垃圾多次回收,内存依然不够的时候会回收软引用对象
- 弱引用:需要配合WeakReference使用,只要进行了垃圾回收,就会把弱引用对象回收
- 虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由Reference Handler线程调用虚引用相关方法释放直接内存
JVM实践
调优参数设置
JVM调优的参数可以在哪里设置参数值
- war包部署,在tomcat中设置
修改TOMCAT_HOME/bin/catalina.sh文件 - jar包部署,在启动时参数设置
java -Xms512m -Xmx1024m -jar xxx.jar
JVM调优的参数有哪些
JVM调优的参数都有哪些
- 设置堆空间大小
设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小\最大大小之间收缩堆而产生额外的时间,通常把最大\初始大小设置为相同的值
多少合适,最大大小的默认值是物理内存的1/4,初始大小是物理内存的1/64
堆太小,可能会频繁导致年轻代和老年代的垃圾回收,会产生stw,暂停用户线程
堆大肯定好,但是有也风险,假如发生了fullgc,会扫描整个堆空间,暂停用户线程的时间长
-Xms:1024m //设置堆的初始化大小
-Xmx:1024m //设置堆的最大大小
- 虚拟机栈的设置
每个线程默认会开启1M的内存,但一般256K就够用
-Xss128k //对每个线程stack大小的调整
- 年轻代中Eden区和两个Survivor区的大小比例
默认是eden:from:to=8:1:1
-XXSurvivorRatio=8 //表示年轻代中的分配比率,这里eden就是8份,其他每个幸存区是一份,就是8:2的空间占比
- 年轻代晋升老年代阈值
-XX:MaxTenuringThreshold=threshold//默认是15,熬过回收的次数
- 设置垃圾回收收集器
-XX:+UseparallelGC
-XX:+UseparallelOldGC
-XX:+UserG1GC //使用G1
面试时,问到,要能说出几个
具体说明文档可参考oracle官网网站
JVM调优的工具
说一下JVM调优的工具
命令工具
- jps 进程状态信息
- jstack 查看java进程内线程的堆栈信息
- jmap 查看堆转信息
- jhat 堆转快照分析工具
- jstat JVM统计监测工具
可视化工具 - jconsole 用于对jvm的内存,线程,类的监控
- VisualVM 能够监控线程,内存情况
java内存泄漏的排查思路
内存泄漏通常是指堆内存,通常是指一些大对象不被回收的情况
内存泄漏一定程度下会导致内存溢出
- 通过jmap或者设置jvm参数获取堆内存快照dump
- 通过工具,VisualVM去分析dump文件,VisualVm可以加载离线的dump文件
- 通过查看堆信息的情况,可以大概定位内存泄漏\溢出是哪行代码出了问题
- 找到对应的代码,通过阅读上下文的情况,进行修复即可
CPU飙高的排查思路
CPU飙高的排查思路?
- 使用top命令查看占用cpu的情况
- 通过top命令查看后,可以查看是哪一个进程占用cpu较高
- 使用ps命令查看进程中的线程信息
- 使用jstack命令查看进程中哪些线程出现了问题,最终定位问题
设计模式
设计模式这块还是结合案例和代码好一些
工厂设计模式
简单工厂
- 所有的产品都共有一个工厂,如果新增产品,则需要修改代码,违反开闭原则
- 是一种编程习惯,可以借鉴这种编程思路
工厂方法模式
- 给每个产品都提供了一个工厂,让工厂专门负责对应的产品的生产,遵循开闭原则
- 项目中用的最多
抽象工厂方法模式
- 如果有多个纬度的产品需要配合生产时,优先建议采用抽象工厂(工厂的工厂)
- 一般企业开发中的较少
工厂模式也就是将相似功能的类汇聚在一起
开闭原则,一个软件实体, 如类, 模块, 函数等应该对扩展开放, 对修改封闭.
策略模式
面试的时候还是要结合业务去说
什么是策略模式
- 策略模式定义了一系列算法,并将每个算法封装起来,使他们可以互相替换,且算法的变化不会影响使用算法的客户
- 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中
登录案例(工厂+策略)
案例(工厂方法+策略)
- 介绍业务(登录\支付\解析excel\优惠等级)
- 提供了很多种策略,都让spring容器管理
- 提供一个工厂:准备策略对象,根据参数提供对象
##责任链设计模式 - 为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
- 比较常见的springmvc中的拦截器,web开发中的filter过滤器
- 通过处理抽象类自调继承类形成一条链路,依次处理来自用户的请求
技术场景
单点登录(可重点准备下)
单点登录是怎么实现的
- 先解释什么是单点登录
单点登录的英文名叫做:Single Sign On(简称SSO) - 介绍自己项目中涉及到的单点登录(即使没涉及过,也可以说实现的思路)
- 介绍单点登录的解决方案,以JWT为例
-
- 用户访问其他系统,会在网关判断token是否有效
-
- 如果token无效则会返回401(认证失败)前端跳转到登录页面
-
- 用户发送登录请求,返回浏览器一个token,浏览器把token保存到cookie
-
- 再去访问其他服务的时候,都需要携带token,由网关统一验证后路由到目标服务
权限认证(可重点准备下)
权限认证是如何实现的
- 后台管理系统的开发经验
- 介绍RBAC权限模型5张表的关系(用户、角色、权限)
- 权限框架:Spring security
上传数据的安全性(可重点准备下)
对称加密,文件加密和解密使用相同的密钥,即加密密钥也可以用作解密密钥
- 优点,加密速度快,效率高
- 缺点,相对不太安全(不要保存敏感信息)
非对称加密,公开密钥和私有密钥,公有密钥加密,私有密钥解密 - 优点,与对称加密相比,安全性更高
- 缺点,加密和解密速度慢,建议少量数据加密
上传数据的安全性你们是怎么控制的
使用非对称加密(或对称加密),给前端一个公钥让他把数据加密后传到后台,后台负责解密后处理数据 - 文件很大建议使用对称加密,不过不能保存敏感信息
- 文件较小,要求安全性高,建议采用非对称加密
棘手问题(必准备)
必须要提前准备,肯定会被问到
- 设计模式,工厂,策略,责任链…
- 线上BUG,CPU飙高,内存泄漏,线程死锁…
- 调优,慢接口,慢SQL,缓存方案…
- 组件封装,分布式(可重点准备下)锁,接口幂,分布式事务,支付通用,这个比较高级困难,一般不准备这个
日志采集(上线后的工作)
你们项目中日志怎么采集的
- 搭建了ELK日志采集系统
- 介绍ELK的三个组件:
-
- Elaticsearch是全文搜索分析引擎,可以对数据存储\搜索\分析
-
- Logstash是一个数据收集引擎,可以动态收集数据,可以对数据进行过滤\分析,将数据存储到指定的位置
- Kibana是一个数据分析和可视化平台,配合Elastic search对数据进行搜索,分析,图表化展示
查看日志的命令(上线后的工作)
- 实时监控日志的变化
实时监控日志的变化:tail -f xx.log
实时监控日志最后100行日志:tail -n 100 -f xx.log - 按照行号查询
-
- 查询日志尾部最后100行日志:tail -n 100 xx.log
-
- 查询日志头部开始100行日志:head -n 100 xx.log
-
- 查询某一个日志行号区间:cat -n xx.log | tail -n +100 | head -n 100
- 按照关键字找日志的信息
查询日志文件中包含debug的日志行号:cat -n xx.log | grep “debug” - 按照日期查询
sed -n ‘/2023-05-18 14:22:31.070/,/2023-05-13 14:22:31.070/p’ xx.log - 日志太多,处理方式
分页查询日志信息:cat -n xx.log | grep “debug” | more
筛选过滤以后,输出到一个文件:cat -n xx.log | grep “debug” > debug.txt
生产问题怎么排查(上线后的工作)
生产问题怎么排查
已经上线的bug排查的思路:
先分析日志,通常在业务中都会有日志的记录,或者查看系统日志,或者查看日志文件,然后定位问题
远程debug(通常公司的正式环境(生产环境是不允许debug的),一般远程debug都是公司的测试环境,方便调试代码)
远程启动项目,需要远程代码和本地的代码保持一致
怎么快速定位系统的瓶颈
- 压测(性能测试)
压测目的,给出系统当前的性能状况,定位系统性能瓶颈或潜在性能瓶颈
指标,响应时间,QPS,并发数,吞吐量,CPU利用率,内存使用率,磁盘IO,错误率
压测工具:LoadRunner,Apache jmeter
后端工程师,根据压测的结果进行解决或调优(接口慢,代码报错,并发达不到要求…) - 监控工具,链路追踪工具
监控工具,prometheus+Grafana
链路追踪工具,skywalking,Zipkin - 线上诊断工具Arthas(阿尔萨斯)
官网,https://arthas.aliyun.com
可以去阿尔萨斯官网找一两个案例练手