涛桑java面试突击
- 简历篇[^1]
- Redis篇
- 使用场景(根据简历业务回答-引导!)
- 其他面试题
- 集合篇
- JVM篇
- 多线程篇
- 框架篇
- 微服务篇
- 消息中间件篇
- 数据库篇
- 企业场景篇
简历篇1
- Hr关注点:学历、院校、经验、年龄、跳槽频率
- 技术Hr关注点:技术、业务、加分项(管理经验,高并发,公有云,博客)
- 简历注意事项:
1.个人技能放C位
2.写在简历的必须能聊
3.引导面试官问你会的 - 面试官角色
1.资深开发/技术经理:技术最好,参与首轮,决定候选人
2.业务部门经理:技术一般,参与终面,决定薪资(思考能力、抗压能力)
3.HR:辅助考察:性格,沟通,合作,学习等能力 - 面试过程
1.自我介绍:我叫涛桑,我的工作经历主要分为3个阶段,巴拉巴拉
2.项目介绍:业务深度,难点,引导接下来的面试方向
Redis篇
使用场景(根据简历业务回答-引导!)
- 缓存:
1.穿透、击穿、雪崩
2.双写一致、持久化
3.数据过期、淘汰策略 - 分布式锁
1.setnx、redisson - 计数器:INCR自增
- 保存token:数据类型string
- 消息队列:数据类型list
- 延迟队列:数据类型zset
【缓存穿透】:访问数据库不存在的数据,虚无缥缈,仿佛透过缓存的身体一直查数据库,多发生在恶意接口访问
- 解决方案1:缓存空数据,查到为空的把null存到缓存中
- 优点:简单
- 缺点:消耗内存,可能发生不一致的情况
- 解决方案2:布隆过滤器:数组+多组hash函数计算是否存在(判断key是否存在)
- 优点:内存占用少,没有多余key
- 缺点:实现复杂,存在误判率(某个不该存在的key的多次hash均命中其他key的hash结果)
- 实现原理:
- 1.更新redis同时将key存储在布隆过滤器中,将key通过多次不同的hash计算将结果0/1存放于数组中
- 2.判断key是否存在,使用多次相同的hash计算判定结果对应位置是否都为1
【缓存击穿】:某个热点 key的过期时间到了,且恰好此时有大量并发请求,瞬间击穿DB
- 解决方案1:互斥锁:缓存重建时设置互斥锁,其他请求等待知道重建成功
- 特点:强一致,性能差
- 解决方案2:逻辑过期:热点数据不设置过期时间,改为新增过期时间字段(命中过期则返回过期+新建线程重建缓存)
- 特点:高可用,性能优,不保证绝对一致
【缓存雪崩】:大量/同时失效,或redis宕机,顾名思义,雪崩
- 解决方案1:给不同的key的过期时间添加随机值(业务中高频节点录入的数据)
- 解决方案2:利用redis集群的高可用性:哨兵模式、集群模式
- 解决方案3:保底策略业务添加降级限流策略:nginx或cloud的gateway
- 解决方案4:多级缓存Guava或Caffeine
【缓存一致性】:修改数据库后,同步更新缓存数据,保持数据一致性
- 问:redis作为缓存,数据库是如何与redis进行同步的呢?
- 答:首先一定介绍业务背景,(一致性要求高 / 允许延迟一致)
双写一致性级别:
- 最拉胯:直接删缓存(删缓存 - 修改数据库)
- 普通:延迟双删:(删缓存 - 修改数据库 - (延时)再删缓存 )
- 延时一致:
1.MQ中间件,更新数据后,通知缓存删除
2.阿里canal组件,伪装mysql从节点,读取binlog更新缓存 - 强一致:(性能不高)
1.分布式锁
2.Redisson读写锁:读readLock共享锁,可一起读,不能写;
写writeLock排他锁,不可同时读写
【缓存持久化】:RDB+AOF
RDB(Redis Database Backup file)
redis数据备份文件,数据快照,即将内存中所有数据记录到磁盘中,故障重启时读磁盘恢复
主动备份:
[root@localhost ~]# redis-cli
127.0.0.1:6397> save #redis主线程执行,阻塞所有命令
ok
127.0.0.1:6397> bgsave #redis子线程执行,避免主线程受影响
Background saving started
自动备份:redis.conf文件中
#900秒内,至少有一个key被修改,则执行bgsave
save 900 1
save 300 10
save 60 10000
执行原理
- fork主进程得到子进程,子进程共享主进程的内存数据(页表:记录虚拟地址与物理地址的映射关系)
- 子进程读取内存数据写入RDB文件中覆盖旧的RDB文件
- fork采用copy-on-write(COW奶牛技术)技术:当主进程读操作时使用共享内存,写操作时拷贝一份数据执行写
AOF(Append Only File追加文件)
redis处理每一个写命令都增量记录到AOF文件中,可以看做是命令日志文件,默认关闭
配置redis.conf(是否开启AOF丨配置刷盘频率 丨 配置bgrewriteaof重写)
#是否开启AOF功能,默认no
appendonly yes
#AOF文件名称
appendfilename "appendonly.aof"
#配置刷盘频率
appendfsync always #立即执行 丨 可靠性高,几乎不丢数据 丨 性能影响大
appendfsync everysec #每秒执行 丨 最多丢失1秒数据 丨 性能适中(默认)
appendfsync no #操作系统控制丨 可靠性较差,可能丢失数据 丨 性能最好
#配置bgrewriteaof重写规则,相同key多次写操作只保留最后一次,不然文件太大
#AOF文件比上次文件增长了多少百分比则触发重写
auto-aof-rewrite-percentage 100
#AOF文件体积超过多少则触发重写
auto-aof-rewrite-min-size 64mb
RDB与AOF对比
RDB | AOF | |
---|---|---|
持久化方式 | 定时对整个内存快照 | 增量记录每次写命令 |
数据完整性 | 不完整,两次备份会丢失 | 相对完整,取决于刷盘策略 |
文件大小 | 会有压缩,体积小 | 记录命令,体积大 |
宕机恢复速度 | 很快 | 慢 |
数据恢复优先级 | 低,因为数据完整性不如AOF | 高,因为完整性更高 |
系统资源占用 | 高,大量CPU占用和内存消耗 | 低,主要都是磁盘IO(重写会占CPU和内存) |
使用场景 | 容忍分钟数据丢失,追求启动速度 | 对数据安全性要求较高 |
【Redis数据过期策略】(惰性删除+定期删除 配合使用 )
- 问:redis的key过期后,会立即删除吗?(考察redis过期策略)
- 答:redis设置数据有效时间过期后,会按照不同的规则从内存中删除,这种删除策略包括惰性删除和定期删除
- 惰性删除:访问key时再检查是否过期,过期则删
- 优点:对CPU友好,不会浪费时间在用不到的key的过期检查上
- 缺点:对内存不友好,过期不用的key会一直存在
- 定期删除:定期对key进行检查,删除过期key(从一定数量的数据库中随机检查)
- SLOW模式:定时任务,执行频率默认10hz,每秒10次,每次不超过25ms,可以通过redis.conf配置文件的hz来调整次数
- FAST模式:执行频率不固定,但两次间隔不低于2ms,每次不超过1ms(尽量少的占用主进程的操作)
- 优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响,也可以定期释放内存
- 缺点:难以确定删除操作执行的时长和频率
【Redis数据淘汰策略】(默认noeviction,不淘汰,满了就报错 )
- 问:假如缓存过多,内存占满了怎么办?(考察redis淘汰策略)
- 答:redis内存不够用时,此时再添加新的key时,redis会按照某种规则将内存中的数据删除掉,这种规则称之为淘汰策略,redis支持8种不同的策略,默认是noeviction,就是不淘汰,满了则报错
配置maxmemory-policy noeviction
配置项 | 规则 | 使用建议 |
---|---|---|
noeviction | 不淘汰,满了则报错,不允许写入 | (默认) |
volatile-ttl | 对设置了TTL的key,所剩不多的先被淘汰 | |
volatile-random | 对设置了TTL的key,随机淘汰 | |
volatile-lru | 对设置了TTL的key,基于LRU算法淘汰 | 有置顶需求 |
volatile-lfu | 对设置了TTL的key,基于LFU算法淘汰 | 短时高频访问 |
allkeys-lru | 对全体key,基于LRU2算法淘汰(近期不热就被淘汰) | 有明显冷热数据区分 |
allkeys-lfu | 对全体key,基于LFU2算法淘汰(热度不高就被淘汰) | 短时高频访问 |
allkeys-random | 对全体key,随机淘汰 | 没有明显冷热数据区分 |
【Redis分布式锁】
- 使用场景(集群):库存、幂等、定时任务(统计定时修复任务,出过问题会修复多次)
- setnx:redis的setnx命令,set if not exists(如果不存在,则set)的简写
- 获取锁:set lock1 value1 NX EX 10(写到一条命令是为了原子性,ex是过期时间,不设置过期时间会导致宕机后无法释放锁导致死锁)
- 释放锁:del lock1
- Redisson:底层是setnx+lua脚本(保证原子性)
- 问1:redis实现分布式锁如何合理的控制锁的有效时长
- 答1:redisson实现的分布式锁的Watch dog看门狗技术,可以监听时长并续期,默认10s续期一次
- 问2:redisson是可重入锁吗
- 答2:是可重入,多个锁重入判定当前线程,redis中使用hash结构存储着线程信息和重入的次数
- 问3:redisson可以解决主从数据一致的问题吗
- 答3:不能解决,但是可以使用redisson提供的红锁来解决,但是性能太低,如果业务中非要保证强一致性,建议采用zookeeper实现的分布式锁,redis分布式锁满足AP3原则, zookeeper实现的分布式锁满足CP原则。
【Redis集群模式】
主从复制:为提高单点并发能力,一主多从,主写从读。
- 全量同步:
- 1.从节点启动时候请求主节点同步(发送repid+offset)
- 2.主节点通过repid判断是否是第一次,第一次则同步repid+offset信息
- 3.执行bgsave命令生成rdb快照发送给从节点执行
- 4.复制过程中将新增写命令记录到repl_baklog文件中
- 5.把生成之后的repl_baklog文件发送给从节点同步
- 增量同步:
- 1.从节点重启或定时(默认10s,可配置)请求主节点同步repid+offset
- 2.主节点根据repid判断是否是第一次,不是第一次则获取从节点的offset
- 3.主节点从repl_baklog文件中获取offset之后的数据发送给从节点同步
哨兵模式:为实现高可用!,主从集群的自动故障恢复
- 作用
- 监控:Sentinel(哨兵)会不断检查master和slave是否按预期工作
- 所有哨兵每1秒(默认)向所有实例发送ping命令
- 当有一个哨兵没有收到回应,判定你主观下线
- 当有一半哨兵没有收到回应,判定你客观下线(一半哨兵数量quorum可配置)
- 自动故障恢复:如果master故障。哨兵会将一个salve提升为master,故障恢复后以新的为主
- 通知:当集群发生故障master转移后,哨兵给客户端推送最新消息
- 监控:Sentinel(哨兵)会不断检查master和slave是否按预期工作
- 流程:
1.哨兵判定主节点挂了
2.多数哨兵判定主节点挂了
3.哨兵推举哨兵头子
4.哨兵头子筛选salve并推举为master
5.哨兵头子通知其他哨兵及其他从节点,改朝换代啦
6.哨兵头子通知客户端,以后请连接新主子
7.旧主子回来时候自动作为小弟归位 - 改朝换代标准
- 首先判定该salve与主节点断开时长,太长的不要
- 然后判定salve-priority配置,是否钦点,没有或一样的话继续
- 判定salve的offset大小,即存储量,谁多要谁
- 最后判定运行id,推举最小的,即最年轻的
- 哨兵脑裂
- 主节点网络波动,哨兵断感,客户端没有断,哨兵误判推举出一个新的主节点,此时客户端数据还是会写入旧的主节点,当旧主回来直接沦为从节点(清空数据并同步主节点数据),此时会丢失中间写入的数据
- 解决方案:redis.conf设置最少从节点数量或同步延迟时间,断感的旧主不满足配置则会自动拒绝请求
min-replicas-to-write 1 #表示最少的salve节点为1个
min-replicas-max-lag 5 #表示数据复制和同步的延迟不能超过5s
分片集群:为实现海量数据存储及高并发写的问题
- 集群有多主多从,每个master保存不同的数据
- master之间监控心跳,类似哨兵
- 客户端可以访问集群中任意节点,最终都会路由到正确的节点
- redis集群中16384个哈希槽,每个节点负责一部分
- 0-5461-10922-16383不同实例
- key通过CRC16校验后对16385取模决定在哪个槽里
其他面试题
- 事务----multi/discard
- 数据结构
redis为什么快
- 1.完全基于内存,C语言编写
- 2.采用单线程,避免上下文切换,6.0之后采用操作单线程+解析多线程
- 3.采用I/O多路复用模型
- redis速度瓶颈不在内存而在网络IO,传统阻塞I/O比较浪费时间,redis采用多路复用技术同时监听多个socket连接(redis多个客户端,之前靠轮询等待),当有读写需求时再去调用系统IO读写操作
- redis速度瓶颈不在内存而在网络IO,传统阻塞I/O比较浪费时间,redis采用多路复用技术同时监听多个socket连接(redis多个客户端,之前靠轮询等待),当有读写需求时再去调用系统IO读写操作
集合篇
算法复杂度分析
- 问:什么是算法时间复杂度
- 答:时间复杂度表示算法的执行时间与数据规模之间的增长关系
- 问:什么是算法空间复杂度
- 答:空间复杂度表示算法的存储空间与数据规模之间的增长关系
List
数组
int[] array = {0,1,2,3,4}
- 数组(Array)是一种连续的内存空间存储相同数据类型数据的线性数据结构
- 寻址公式(类比分页):array[1] = baseAddress + i * dataTypeSize
- baseAddress:数组的首地址
- dataTypeSize:数组中元素类型的大小,int型的4字节
【ArrayList】
List list = new ArrayList();
list.add(1);
- 问:ArrayList底层的实现原理是什么?
- ArrayList底层使用动态数组实现的
- ArrayList初始容量为0,第一次添加数据时初始化为10
- ArrayList进行扩容的时候是原来的1.5倍,每次扩容都需要拷贝数组Arrays.copyOf()
- ArrayList在添加数据时:
- 确保数组已使用长度size足够
- 计算数组容量,不足时调用grow方法扩容为原来的1.5倍
- 将新元素添加到size的位置上
- 返回添加成功boolean
- 问:如何实现数组和List之间的转换
- 数组转list(转换后修改数组会影响list):List list = Arrays.asList(strs);
- list转数组(转换后修改list不会影响数组):String[] array = list.toArray(new String[list.size()]);
【LinkedList】
- 单向链表:非连续、非顺序的存储结构,每个元素称之为结点(Node:包含data+next)
- 双向链表:非连续、非顺序的存储结构,每个元素称之为结点(Node:包含data+next+prev),支持双向遍历
【ArrayList和LinkedList的区别是什么?】
- 1.底层数据结构
- ArrayList是动态数组的数据结构实现
- LinkedList是双向链表的数据结构实现
- 2.操作数据效率
- ArrayList根据下标按照寻址公式查,LinkedLinst不支持下标查询
- 查找(未知索引):ArrayList需要遍历,LinkedList也需要链表遍历,时间复杂度都是O(n)
- 新增和删除
- ArrayList尾部插入和删除快(O(1));其他部分删除需要挪动数组慢(O(n))
- LinkedList头尾增删快(O(1));其他需要遍历链表慢(O(n))
- 3.内存占用
- ArrayList底层是数组,内存连续,节省内存
- LinkedList双向链表需保存data+next+prev,更占内存
- 4.线程安全
- 都不安全
- 如果需要线程安全有两种方案
- 在方法内使用,局部变量则是线程安全的
- 使用线程安全的ArrayList和LinkedList
List<Object> syncArrayList = Collections.synchronizedList(new ArrayList<>());
List<Object> syncLinkedList = Collections.synchronizedList(new LinkedList<>());
HashMap
【二叉树】(链表衍生)
- 二叉树:每个节点最多两个"叉",每个节点的左右子树也满足二叉树的定义
- 二叉搜索树(Binary Serach Tree,BST)又名二叉搜索树,有序二叉树:
- 树中任意节点的左子比本身小,右子比本身大
- 没有键值相等的节点
- 二叉搜索树的时间复杂度为O(logn)
【红黑树】(特殊的二叉树,二叉树有极限偏科情况,红黑树保证平衡)
- 红黑树(Red Black Tree):也是一种自平衡的二叉搜索树(BST),之前叫平衡二叉B树(Symmetric Binary B-Tree)
- 特质(保证平衡不偏科的限定)
- 节点要么红色要么黑色,根节点是黑色
- 所有叶子节点都是黑色的null
- 红色节点的子节点都是黑色
- 从任一节点到叶子节点的所有路径都包含相同数目的黑色节点
- 红黑树的查找、添加、删除的时间复杂度都是O(logn)
【散列表(Hash Table)】(数组演化)
- 散列表,又名哈希表/Hash表,是根据键key直接访问内存存储位置的值value的数据结构,数组演化而来,利用了下标寻址的特性
- 散列函数,将key映射为数组下标的函数。hashIndex = hash(key)
- 散列函数计算的散列值必须为**>=0的正整数**,因为需要作为下标
- 如果key1==key2,那么hash(key1) == hash(key2)
- 如果key1!=key2,那么hash(key1)!=hash(key2)(不太好实现,会发生哈希冲突)
- 散列冲突:又叫哈希冲突,哈希碰撞,指多个key映射到同一个数组下标的位置
- 散列冲突-拉链法
- 数组每个下标位置称之为桶(bucket)或者槽(slot)
- 每个桶对应一个链表
- 冲突的key以链表形式追加
- 当链表过长时为方便查询会转为红黑树
【HashMap的实现原理】
- HashMap的底层数据结构为散列表+链表/红黑树
- 添加数据put时,计算key的hash值确定数组下标
- key相同则替换
- key不相同则存入链表或红黑树中
- 获取数据get时,计算key的hash值确定数组下标获取元素
【HashMap的jdk1.7和jdk1.8有什么区别】
- jdk1.8之前采用的拉链法:数组+链表
- jdk1.8之后采用数组+链表+红黑树,链表长度大于8且数组长度大于64则会从链表转化为红黑树(两个条件须都满足,不满足64则会触发一次扩容),在resize扩容时,如果红黑树节点数小于6会退化为链表
- jdk1.7扩容链表采用头插法,jdk1.8扩容链表改为尾插法
【HashMap的put方法的具体流程】
- 1.判断数组table是否为空桶或为null,执行resize()扩容2倍
默认初始容量DEFAULT_INITIAL_CAPACITY值为16,
默认加载因子DEFAULT_LOAD_FACTOR值为0.75,
扩容阈值(threshold)=数组容量 * 加载因子,
懒加载初始化,Map<String,String> map = new HashMap<>();
此时并没有初始化数组,只是初始化了默认加载因子,第一次put的时候才会进行初始化数组
-
2.根据key计算hash值,确定数组下标
-
3.判断table[i]是否为空,为空则直接新建节点放入
-
4.不为空
- 判断key跟table[i]的首个元素是否相同,相同则覆盖value
- 不相同则判断是红黑树 OR 链表(是否为treeNode)
- 红黑树,则判断红黑树是否存在key,存在覆盖,不存在追加
- 链表,则判断链表是否存在key,存在覆盖,不存在追加(长度大于8转红黑树)
-
5.插入成功后,判断**++size > threshold**,如果超过,则resize()扩容
【HashMap的扩容机制】:新建数组,均摊数据
- 在添加元素或初始化的时候需要resize()扩容,第一次初始化数组长度为16,之后每次**达到扩容阈值(数组长度*0.75)**都会进行扩容
- 每次扩容都是之前的2倍
- 扩容过程,会新创建一个数组,将老数组的数据均摊到新数组中
- 如果是没有hash冲突的节点(即不是链表/红黑树的单节点),直接e.hash & (newCap-1)计算新索引位置
- 如果是红黑树,则红黑树拆分添加至新数组中;//TODO(实际拆分逻辑)
- 如果是链表,则遍历拆分链表,根据e.hash &oldCap判定新数组的位置
【HashMap其他面试题】
- 问:hashMap的寻址算法
- 答:两次hash,为了分布更均匀
- 1.计算对象的hashCode()
- 2.再次调用hash()方法进行二次哈希,为了让哈希分布更为均匀(hashcode右移16位再异或运算)
- 3.最后**(capacity-1) & hash**得到索引
- 问:为什么HashMap的数组长度一定是2的次幂?
- 答:方便位与运算代替取模
- 问:HashMap线程安全吗?
- 答:不安全,
- 在jdk1.7的hashmap中在数组进行扩容的时候,因为链表是头插法,在数据迁移过程中,有可能导致死循环,
- CurrentHashMap1.7采用了分段锁+unsafe类,保证线程安全
- 在jdk1.8中,采用尾插法,在并发put操作时会发生数据覆盖
- CurrentHashMap1.8采用了synchronized,保证线程安全
JVM篇
JVM组成
【JVM组成部分】
- 类加载系统:java代码转为字节码
- 运行时数据区:分配存储空间
- 执行引擎:字节码翻译为底层系统命令
- 本地方法接口:C/C++实现,一些系统本地方法
- 垃圾回收系统:垃圾回收
【什么是程序计数器】
- 用于记录正在执行的字节码的行号来找下一条指令,线程私有,线程你来我往执行指令
【堆】
- 定义:线程共享的区域,主要用来保存对象实例,数组等,堆内无内存会报OOM
- 组成:年轻代:老年代= 1:2,eden:survivor1:survivor2 = 8:1:1
- 年轻代
- Eden区
- Survivor1
- Survivor2
- 老年代:保存老的对象
- 年轻代
- jdk1.7和1.8的区别
- 1.7有一个永久代,存储类信息、静态变量、常量、编译后的代码
- 1.8中移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出
【什么是虚拟机栈】
- 每个线程运行需要的内存,称为虚拟机栈,先进后出
- 每个栈由多个栈帧(frame)组成,对应着每次方法调用所占用的内存
- 每个线程只能有一个活动栈帧,对应着正在执行的方法
- 问:垃圾回收涉及栈内存吗?
- 答:不涉及,垃圾回收主要指的是堆内存,栈帧弹栈以后内存就会释放
- 问:栈内存分配越大越好吗?
- 答:未必,默认栈内存通常为1024k,栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈改为2048k,则只能同时有256个线程活动
- 问:方法内的局部变量是否线程安全**?
- 如果方法内局部变量没有逃离方法作用范围,则是线程安全的
- 如果是局部变量引用了对象,并逃离了方法的作用范围,需要考虑线程安全
- 问:栈内存溢出的情况(java.lang.stackOverflowError)
- 栈帧过多导致:递归调用
- 栈帧过大导致
- 问:堆栈的区别是什么?
- 1.栈内存一般用来存储局部变量和方法调用,堆内存存放java对象和数组
- 2.堆会被GC垃圾回收,栈不会
- 3.栈线程私有,堆线程共有
- 4.栈空间不足:java.lang.stackOverflowError
- 5.堆空间不足:java.lang.OutOfMemoryError
【能不能解释下方法区】
- 方法区(Method Area)是各个线程共享的内存区域
- 1.7以前在堆中的永久代,1.8以后在本地内存的元空间中
- 主要存放类的信息,运行时常量池
- 虚拟机启动时创建,关闭时释放
- 如果方法区域中的内存无法满足分配请求,抛出OutOfMemoryError:Metaspace
- 问:介绍一下运行时常量池
- 常量池:.class文件中,记录类名、方法名、参数类型、字面量等信息的表
- 运行时常量池:当类被加载时,它的常量池信息会被放入运行时常量池,并把里面的符号地址变为真实地址
【你听过直接内存吗?】
- 并不属于JVM的内存结构,不由jvm管理,是虚拟机的系统内存
- 常见于NIO操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受jvm内存回收
类加载
【类加载器】:二进制字节码文件 == 》JVM
- 启动类加载器(Bootstrap):JAVA_HOME/jre/lib
- 扩展类加载器(Ext):JAVA_HOME/jre/lib/ext
- 应用类加载器(App):classpath
- 自定义类加载器:自定义加载规则
【双亲委派】先找父亲,有爷爷找爷爷,没有自己加载
- 避免类重复加载,保证唯一性
- 保证安全,API不被修改
- 不直接从上往下查是为了避免有多个同级自定义加载器无法选择
【类装载的执行过程】
- 加载
- 通过类的全名,获取类的二进制数据流
- 解析成方法区内的数据结构(Java类模型)
- 创建java.lang.Class类的实例表示该类型,作为方法区各类各种数据的访问入口
- 连接
- 验证
- 文件格式是否错误
- 语法是否错误
- 字节码是否合规
- 符号引用是否存在:Class文件常量池中记录了自己将要使用的类或方法
- 准备:为类变量分配内存并设置初始值
- static:分配空间+设置默认值(int的0)(赋值在初始化阶段)
- static+final+基础类型/string:分配空间+赋真正值
- static+final+引用类型:分配空间(赋值在初始化阶段)
- 解析:把类中符号引用转为直接引用
- 验证
- 初始化:对类的静态变量,静态代码块执行初始化操作(首次访问类的静态变量时)
- 父类未初始化,先初始化父类
- 子类访问父类静态变量,只触发父类初始化
- 多个静态变量和静态代码块,自上而下
- 使用: 从入口方法开始执行
- 调用静态变量、静态方法
- 使用new关键字创建实例
- 卸载:堆中class对象不再被引用,方法区中的二进制数据会被卸载,启动类加载器加载的类不会被卸载
垃圾回收
【如何定位垃圾】
- 引用计数法:对象被引用的次数,为0则被回收
缺点:循环引用会引发内存泄漏 - 可达性分析(默认):扫描对象,判定是否能沿着GC Root找到,找不到则可回收
- GC Root
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中**JNI(Native方法)**引用的对象
- GC Root
【如何清除垃圾】
- 标记清除算法(标记+回收)
- 步骤:
- 可达性分析进行标记
- 按标记回收
- 优点:标记和清除都很快
- 缺点:内存碎片化
- 步骤:
- 标记压缩算法(标记+回收+压缩):一般老年代用
- 标记复制算法(标记+复制+回收):两块相同的内存空间,一般年轻代用
- 步骤
- 可达性分析标记
- 把A存活的放到B,把A清理
- 把B存活的放到A,把B清理
- 优点:垃圾多时效率高,内存无碎片
- 缺点:2块内存只能用1块使用率低
- 步骤
- 分代回收算法
- 步骤:
- 新创建的对象会放在eden区(伊甸园区)
- eden内存不足,标记
- eden + A = B
- eden + B = A
- 15次后晋升老年代(A或B不足 / 大对象 会提前晋升)
- 步骤:
- 问:MinorGC、MixedGC、FullGC的区别?
- MinorGC(YoungGC):新生代,暂停时间短(STW4)
- MixedGC:新生代+老年代部分,G1收集器特有
- FullGC:新生代+老年代完整垃圾回收,STW较长,应尽力避免
【垃圾回收器】
- 串行垃圾回收器:单线程回收,阻塞其他线程
- Serial:新生代,采用复制算法
- Serial Old:老年代,采用标记压缩算法
- 并行垃圾回收器:多线程回收,阻塞其他线程,JDK8默认使用
- Parallel New:新生代,采用复制算法
- Parallel Old:老年代,采用标记压缩算法
- 并发垃圾回收器(CMS):多线程回收,不阻塞其他线程,老年代,标记清除算法
- G1垃圾回收器:新+老,JDK9默认
- 划分多个区域:eden + survivor + old + humongous(大对象)
- 复制算法
- 响应时间和吞吐量兼顾
- 阶段
- 新生代回收(阻塞 ):eden到阈值就转survivor,次数多了(survivor阈值)到old
- 并发标记(不阻塞):old多了(45%)
- 混合收集(阻塞,预期时间):垃圾多的old + survivor阈值 = old
- 并发失败(回收速度赶不上new速度),会触发FullGC
JVM调优
【堆空间大小】(初始Xms(默认1/46RAM) 丨 最大Xmx(默认1/4RAM))
- 部署方式
- war包部署:Tomacat_home/bin/catalina.sh
- jar包部署:java -Xms512m -Xmx1024m -jar xxxx.jar
- 调优建议
- 太小则会频繁GC,STW
- 太大FullGC太慢
- 推荐:尽量大,需考察计算器其他程序内存使用情况
【栈空间大小】(初始Xss(默认1M))
- 存放栈帧,调用参数、局部变量,一般256k够用,-Xss调优
【年轻代/老年代大小】
- 年轻代:默认Eden :Survivor = 8:1:1
- 调优:
- -XXSurvivorRatio = 8,Eden占总比
- 调优:
- 老年代:次数晋升阈值
- 调优: -XX:MaxTenuringThreshold = threshold
【选择垃圾回收器】
-XX:UseG1GC
-XX:UseParallelGC
-XX:UseParallelOldGC
【调优工具】
- 命令工具
- jps:进程状态工具
- jstack:线程堆栈信息
- jmap:堆转信息:jmap -dump:format=b,file=e:/aaa/bbb.hprof 56560[pid]
- vm参数生成
- jhat:堆转储快照分析工具
- jstat:JVM统计检测工具
- 可视化工具
- jconsloe:用于堆jvm的内存、线程、类的监控(jdk/bin/jconsole.exe)
- VisualVM:监控线程、内存情况(jdk/bin/jvisualvm.exe)
【内存泄漏】
- java虚拟机栈:StackOverflowError: 一般可能是递归
- 方法区/元空间:OutOfMemoryError:Metaspace:一般是动态加载的类太多
- 堆:OutOfMemoryError:java heap space:大对象未回收
- -XX:+HeapDumpOnOutOfMemoryError //开启生成dump
- -XX:HeapDumpPath=/home/app/dumps //配置dump地址
- VisualVM导入
【CPU飙高】
- 使用top命令查看cpu占用情况
- 找到cpu高的进程id:40940
- 找到进程所有的线程:ps H -eo pid,tid,%cpu | grep 40940
- 找到cpu高的线程id:40950
- 线程id转为十六进制:print “%x\n” 40950 //9ff4
- jstack 40940查询堆栈信息
多线程篇
线程基础
【线程和进程的区别】
- 进程包含线程
- 进程间内存独立,进程内线程间内存共享
- 线程上下文切换快
【创建线程的方式】
- 继承Thread类
- 重写run() + new Thread().start()
- 实现Runnable接口
- 重写run() + new Thread(new Runnable()).start()
- 实现Callable接口
- 重写call() + new Thread(new FutureTask(new Callable())).start()
- 线程池创建
- Executors.newFixedThreadPool(3).submit(new Runnable())
- 问:runnable和callable有什么区别?
- run方法没有返回值,call方法有返回值FutureTask
- call允许抛异常,run不允许抛异常
- 问:run()和start()方法有什么区别?
- run:线程将要执行的代码,可被当做普通方法多次调用
- start:启动线程,只能被调一次
【线程的状态】
- 新建(new):new Thread();
- 可运行(runnable):thread1.start();
- 终止(terminated):线程获取到CPU执行权,轮询多次执行完毕
- 阻塞(blocked):获取到CPU执行权,但是没获取到锁进入阻塞状态
- 等待(waiting):thread1.wait();需要被notify()唤醒后切换到runnable状态
- 计时等待(timed_waiting):thread1.sleep(50);进入计时等待状态,到时间后切换到runnable状态
【如何让线程按顺序执行】
- join():阻塞写这行代码的线程,等待调用join的线程执行完后,再执行原线程,插队
【notify随机唤醒 / notifyAll唤醒所有】
【wait和sleep的区别】
- 共同点: wait(),wait(50),sleep(50)效果都是让当前线程暂时放弃CPU使用权,进入阻塞
- 不同点:
- 方法归属不同
- sleep是Thread的静态方法
- wait(),wait(50)都是object的成员方法
- 醒来时机不同
- sleep(50)和wait(50)都会在等待相应毫秒后醒来
- wait可以被notify唤醒,不唤醒就一直等待
- 他们都可以被打断唤醒
- 锁特性不同
- wait方法的调用必须要先获取wait对象的锁,sleep无此限制
- wait方法执行后会释放锁
- sleep如果在syncchronized代码块中执行,并不会释放对象锁
- 方法归属不同
【如何停止正在运行的线程】
- 使用退出标志,让线程执行完正常退出:flag
- 使用stop方法强行终止,(不推荐,已作废)
- 使用interrupt方法中断线程
- 打断阻塞的线程(sleep、wait、join)的线程,抛异常InterruptedException
- 打断正常的线程,可根据打断状态标记是否退出线程
线程安全
【synchronized关键字的底层原理】
- synchronized【对象锁】采用互斥的方式让同一时刻只能有一个线程持有锁
- 底层是由Monitor实现,monitor是jvm级别的对象(C++实现),线程获得锁需要使用对象锁关联Monitor
- Monitor包含3个属性
- owner:关联获得锁的线程,只能有一个
- entryList:关联处于阻塞状态的线程
- waitset:关联处于waiting状态的线程
【synchronized关键字的底层原理-进阶】//TODO: 下次一定。。。
【JMM】java内存模型
- JMM(Java Memory Model)java内存模型,定义了共享内存中,多线程程序读写操作的行为规范,保证指令的正确性
- JMM把内存分为两块,一块是私有线程的工作内存,一块是所有线程的共享主内存
- 线程之间相互隔离,线程跟线程交互需要通过主内存
【CAS】Compare And Swap(比较并交换)
- 体现乐观锁的思想,在无锁状态下保证线程操作数据的原子性
- CAS使用的地方很多:AQS框架、AtomicXXX类
- 在操作共享变量的时候使用的自旋锁,效率跟高一些
- CAS的底层是调用Unsafe方法,由操作系统提供,其他语言编写
- 问:乐观锁和悲观锁的区别?
- CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程修改共享变量,就算改了也没事,再重试呗
- synchonized是基于悲观锁的思想,最悲观的估计,得防着其他线程来修改共享变量,我上锁你们别想改,我改完解开锁,你们再改
【Volatile关键字】
- 保证线程间的可见性:volatile修饰的共享变量,可告诉编译器(JIT)等别优化(vm参数-Xint可禁用即时编译器),让共享变量在线程间可见
- 禁止指令重排序:volatile修饰的共享变量,会在读、写共享变量时,加入不同屏障,阻止其他读写操作越过屏障,达到阻止重排序的效果,读开写尾
指令重排序:处理器为提高运行速度而做出违背代码原顺序的优化
【AQS】CAS修改state标识 + FIFO队列存线程
- 是多线程中的队列同步器,是一种锁机制,是作为基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的
- AQS内部维护了一个先进先出的队列,队列中存储排队的线程
- AQS内部维护了一个属性state,使用0/1标识是否持有锁
- 在对state修改的时候使用的是CAS操作,保证多个线程修改的原子性
- 问:AQS是公平锁还是非公平锁?
- 答:都可以实现。
- 公平锁:新线程进队列排队,从Head出队取锁
- 非公平锁:新线程与Head抢锁
【ReentrantLock】CAS+AQS队列
- ReentrantLock表示支持重新进入的锁,同线程内调用lock不会阻塞
- 主要利用CAS+AQS队列实现
- 支持公平锁和非公平锁,提供的构造器中无参默认非公平锁,带参公平锁
【Synchronized和Lock有什么区别?】
- 语法层面
- synchronized是关键字,源码在jvm中,用**C++**实现
- Lock是接口,源码jdk提供,java实现
- 使用synchronized时,退出代码块会自动释放锁,Lock需手动unlock
- 功能层面
- 二者都属于悲观锁、都具备互斥、同步、锁重入功能
- Lock提供了许多synchronized不具备的功能,例如公平锁、可打断、可超时、多条件变量
- Lock有适合不同场景的实现,如ReentrantLock、ReentrantReadWriteLock(读写锁)
- 性能层面
- 在没有竞争时,synchronized做了很多优化,如偏向锁,轻量级锁,性能还可
- 在竞争激烈时,Lock的实现通常会提供更好的性能
【死锁产生的条件】
- 产生的条件:一个线程同时获取多把锁,互相有纠缠,容易发生死锁
- 如何诊断
- 当程序出现死锁时,我们可使用jdk自带的jps和jstack
- jps:输出JVM的进程状态
- jstack:查看进程内线程的堆栈信息,查看日志,检查是否有死锁,根据具体代码修复
- 可视化工具jconsole、VisualVM也可以检查
【ConCurrentHashMap】
- 底层数据结构
- JDK1.7底层采用分段的数组+链表
- JDK1.8底层数据结构跟HashMap1.8一样,数组+链表+红黑树
- 加锁的方式
- JDK1.7采用Segment分段锁。底层使用ReentrantLock
- JDK1.8采用CAS添加新节点,synchronized锁定链表或红黑树的首节点,相对Segment锁粒度更细,性能更好
【导致并发程序出现问题的根本原因】
- 原子性:synchronized、lock
- 内存可见性:volatile、synchronized、lock
- 有序性:volatile
线程池
【线程核心参数/执行原理】
- corePoolSize:核心线程的数量
- maximumPoolSize:最大线程数目 = 核心线程数 + 空闲线程数
- keepAliveTime:空闲线程释放倒计时
- unit:释放倒计时的时间单位,秒、毫秒等
- workQueue:当没有核心线程时,新来的任务会加入到此队列排队,队列满时会创建空闲线程执行任务
- threadFactory:线程工厂,可以定制线程对象的创建,例如设置线程名字,是否是守护线程等
- handler:拒绝策略,当所有线程都在忙,队列也满时,会触发拒绝策略
- AbortPolicy:直接抛出异常,默认策略
- CallerRunsPolicy:用调用者所在的线程执行任务
- DiscardOldestPolicy:丢弃阻塞队列中最靠前的任务,并将当前任务放进去
- DiscardPolicy:直接丢弃任务
【线程中常见的阻塞队列】
- ArrayBlockingQueue:基于数组的有界阻塞队列,FIFO
- 强制有界
- 提前初始化数组
- Node需提前创建好
- 一把锁
- LinkedBlockingQueue:基于链表的有界阻塞队列,FIFO
- 默认无界,支持带参有界
- 懒加载,创建节点时再添加数据
- 入队生成新的Node
- 两把锁(头尾)
- DelayedWorkQueue:优先级队列,保证每次出队都是队列中执行时间最靠前的
- SynchronousQueue:不存储元素的阻塞队列,每个插入操作必等待一个移出操作
【如何确定核心线程数】
- 高并发、时间短:N+1
- 并发不高、时间长:
- IO密集型任务:一般来说文件读写、DB读写、网络请求
- 核心线程数大小设置为2N+1
- CPU密集型任务:一般来说计算型代码、Bitmap转换、Gson转换等
- 核心线程数大小设置为N+1
- IO密集型任务:一般来说文件读写、DB读写、网络请求
- 并发高,时间长:首先设计缓存、增加服务器、最后线程池设置
//查看机器CPU核数
System.out.println(Runtime.getRuntime().availableProcessors());
【线程池的种类】
- newFixedThreadPool:定长线程池,控制最大并发数,超出的会在队列中等待
- newSingleThreadExecutor:单线程线程池,只用唯一线程工作,保证所有线程顺序FIFO执行
- newCachedThreadPool:可缓存线程池,有任务时创建线程执行,无任务时回收
- newScheduledThreadPool:可延迟线程池,支持定时及周期性执行任务
【为什么不建议用Executors创建线程池】
使用场景
【多线程场景】
- 公司解析及保存实时聊天数据
- 使用SDK方式到企微批量拉取数据(实时1000/s,初始化可能上百万)
- 拉取到数据需根据企微非对称加密密钥解析数据(此时开启多线程解析)
- 解析完后发送kafka,消费端组装用户信息
- 保存批量存ES+Cassandra
- 文件类型分发文件Kafka,消费端异步并发上传(开启多线程(IO密集型))
- 公司每晚定时任务修复校准所有的统计数据
- 从数据库/ES/Cassandra查询各公司统计元数据
- 重新计算统计数据,并进行修复(开启多线程(CPU密集型))
- 按公司CountDownLatch
- 需要调用多个接口组装统计数据,各接口之间没有关系
- 使用线程池,添加**不同接口任务,Future接收接口返回值(**格式规范)
- future.get()可获取接口返回值,并可阻塞主线程,保证线程执行完后获取到返回值
【CountDownLatch】(为了等待线程池执行完再执行)
- CountDownLatch(闭锁/倒计时锁):用来进行线程同步协作,等待所有线程递减完成,再执行后续操作
- 构造函数用初始化等待计数值
- await()用来等待计数归零
- countDown()用来计数减一
- 场景:
- ES数据批量导入
- 计算总条数、设定每页条数、计算总页数设置为CountDownLatch(size)
- 开启线程池
- 分页查找,创建任务,提交到线程池执行
- countDownLatch.await();主线程等待
- 执行任务,每执行完一页,countDownLatch.countDown();计数减一
- 计数归零,主线程继续执行,总计时/返回值
- 统计数据按公司修复数据
- CountDownLatch(总公司数)
- 开启线程池
- 按公司修复数据,countDownLatch.countDown();计数减一
- countDownLatch.await();主线程等待
-
- 等待计数归零,主线程继续执行,总计时/返回值
- ES数据批量导入
【@Async异步调用】(支持线程池)
- 客服埋点异步推送:提升接口响应时间
【如何控制某个方法允许的并发线程数】Semaphore
- JUC包下提供一个工具类,底层AQS
- 场景:对于资源有明确访问数量限制的场景,常用于限流
- 使用方式
- 创建Semaphore对象,设定容量
- acquire()请求信号量,-1
- release()释放信号量,+1
【ThreadLocal】(线程各用各的)
- ThreadLocal可实现线程间对[资源对象]的隔离,避免线程安全问题
- 线程内资源共享
- 内部ThreadLocal有一个成员变量ThreadLocalMap存储资源对象
- set方法,自己为key,资源为value进行赋值
- get方法,自己为key,获取资源
- remove,移除当前线程关联的资源值
- ThreadLocal内存泄露问题
ThreadLocalMap中的key是弱引用,值为强引用,key会被GC释放,关联value内存不会释放,建议主动remove释放key、value
框架篇
Spring
【Spring中单例bean是线程安全的吗?】
- 不是线程安全的,spring的bean通常无状态,某种程度上也可以说是线程安全。
- 原型Bean:每次new新对象,线程之间不共享,不会有线程安全问题
- 单例Bean:所有线程共享单例Bean,存在资源竞争
- 如果bean是一个无状态bean(不会修改)则不存在线程安全问题
- 如果bean是有状态的,则需要开发人员自己保证线程安全
- 1.将bean的作用域改为**@Scope(value = “prototype”)**默认"singleton"
- 2.尽量不要在@Controller/@Service等容器中定义静态变量(静态变量线程不安全)
- 3.一定要定义变量的话,使用ThreadLocal来封装
【AOP】
- 问:什么是AOP?
- 答:面向切面编程,将无关业务却对多个对象产生影响的公共行为和逻辑,抽取公共模块服用,降低耦合
- 问:你项目中用到的AOP
- 答:记录操作日志、切换公有云混合云数据库(有事务无法切库),spring实现的事务
- 核心是:使用aop中的环绕通知+切点表达式(找到要记录日志的方法),通过环绕通知的参数获取请求方法的参数(类、方法、注解、请求方式等),保存到数据库
- 问:spring的事务是如何实现的?
- 答:本质就是AOP功能,方法前开启事务,方法后提交或回滚。
【spring中事务失效的场景】
-
1.异常捕获处理
- 原因:事务通知只有捕捉到目标抛出的异常,才会进行回滚,如果自己已经处理,则事务无法知悉
- 解决方案:在catch代码块中添加**throw new RuntimeException(e)**抛出。
-
2.抛出检查异常
- 原因:spring默认只会回滚非检查异常,即运行时异常
- 解决方案:配置**@Transactional(rollbackFor=Exception.class)**任何异常都回滚
-
3.非public方法导致事务失效
- 原因:spring为方法创建代理,添加事务通知,前提条件是该方式是public
- 解决方法:改为public方法。
【spring的bean的生活周期】
- 通过BeanDefinition获取bean的定义信息
- 调用构造函数实例化bean
- bean的依赖注入
- 处理Aware接口(BeanNameAware、BeanFactoryAware、ApplicationContextAware)
- Bean的后置处理器BeanPostProcessor-前置
- 初始化方法(InitializingBean、init-method)
- Bean的后置处理器BeanPostProcessor-后置
- 销毁Bean
【spring的循环依赖问题】
- 循环依赖:A依赖B,B依赖A
- 循环依赖在Spring中是允许存在的,spring框架依据三级缓存可解大部分循环依赖(spring2.6后默认禁止)
- 一级缓存(成品仓库):SingletonObjeces,单例池,存放初始化完成的bean对象
- 二级缓存(半成品仓库):earlySingletonObjects,存放实例化但未初始化完的bean对象
- 三级缓存(原材料仓库):singletonFactories,存放BeanFactory,可通过getObject()获取原始bean,(为解决有AOP行为的bean)
- 构造器注入循环依赖:由于构造器在依赖注入之前,spring处理不了,需手动处理
- @Lazy懒加载
【springMVC的执行流程】
- 视图阶段(JSP)
- 用户请求发送到DispacyerServlet(前端控制器,总调度)
- 总调度解析url去handlerMapping(处理器映射器)查询映射的类/方法等
- 返回一个处理器链(并不是单独的类和方法,而是一系列前后关系的调用链,比如拦截器)
- 总调度拿着调度链找HandlerAdapter(处理器适配器),
- 适配器调用具体的处理器(Controller/Handler)并返回ModelAndView
- 总调度将ModelAndView交给ViewReslover(视图解析器)渲解析后返回View对象
- 总调度根据View进行渲染视图+数据填充
- 总调度响应用户
- 前端后端分离
- HandlerAdapter调用处理器后
- 读取到方法上添加的**@ResponseBody**
- 通过HttpMessageConverter将返回结果转换为JSON并响应
【springBoot的自动配置原理】
- SpringBoot的引导类上有个注解**@SpringBootApplication**,封装了
- @SpringBootConfiguration:声明是一个配置类
- @ComponentScan:组件扫描,默认当前引导类所在包及其子包
- @EnableAutoConfiguration:自动化注解的核心注解
- 封装了**@Import**,配置了AutoConfigurationImportSelector(自动配置选择器)
- 选择器读取项目路径META-INF/spring.factories文件,包含所有内置配置类
- 选择器根据特定规则按需将配置类的所有Bean放入Spring容器中
- 比如**@ConditionalOnClass**:判断是否有对应的class文件,有则加载
【spring常用注解】
- Spring常用注解
- @Component、@Controller、@Service、@Repository:使用在类上实例化Bean
- @Autowried:使用在字段上根据类型依赖注入
- @Qualifier:使用在字段上根据名称依赖注入,一般结合
- @Scope:使用在类上,标注Bean的作用范围
- @Configuration:使用在类上,标注此类为配置类,创建容器时会加载
- @ComponentScan:使用在引导类上,标注扫描包范围
- @Bean:使用在方法上,标注该方法的返回值存储到Spring容器中
- @Import:使用Import导入的类会存储在Spring容器中
- @Aspect、@Before、@After、@Around、@Pointcut:用于切面编程(AOP)
- SpringMVC常用注解
- @RequestMapping:用于映射请求路径
- @RequestBody:注解实现接收http请求的json数据,将json转为java对象
- @RequestParam:指定请求参数的名称
- @PathViriable:在请求路径中获取请求参数(user/{id}),传递给方法的形参
- @ResponseBody:注解实现将controller方法返回对象转为json对象并响应给客户端
- @RequestHeader:获取指定请求头数据
- @RestController:@Controller+@ResponseBody
- SpringBoot常用注解
- @SpringBootConfiguration:组合了Configuration,标注配置类
- @ComponentScan:Spring组件扫描
- @EnableAutoConfiguration:自动配置功能
MyBatis
【MyBatis执行流程】
- 读取MyBatis-config.xml配置文件,加载运行环境和映射文件
- SpringBoot中改为yml配置+注解
- datasource: 数据库配置
- mapper-locations: classpath:mapper/*.xml(扫描mapper文件路径)
- type-aliases-package: com.xxx.pojo(简化resultType的包名)
- configuration:map-underscore-to-camel-case: true(下划线转驼峰)
- configuration:default-fetch-size: 100(默认查询批次数)
- configuration:default-statement-timeout: 30(默认超时时间,单位s)
- SpringBoot中改为yml配置+注解
- 构造会话工厂SqlSessionFactory
- 会话工厂创建SqlSession对象(包含了执行SQL语句的所有方法)
- 操作数据库的接口,Executor执行器,同时负责查询缓存的维护
- Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
- 输入参数映射:java转数据库类型
- 输出结果映射:数据库转java类型
【MyBatis的延迟加载问题】
- MyBatis是否支持延迟加载
- 支持,但默认是关闭的,lazyLoadingEnabled = false
- 延迟加载的意思就是在一对一或一对多关联查询时,用到再查,不用不查
- 局部可在<select fetchType = "lazy"/>配置单sql懒加载
- Mybatis延迟加载的底层原理知道吗
- 使用CGLIB创建目标对象的代理对象
- 当调用目标方法时,进入拦截器invoke方法,发现方法是null,执行sql查询
- 获取数据后,调用set方法赋值,再继续查询方法,就有值了
【MyBatis的一级缓存和二级缓存】
- 一级缓存:作用域SqlSession的本地HashMap,默认开启,当session进行flush或close后,会清空
- 二级缓存:作用域namespace的本地HashMap,默认关闭
- 开启方式:
- 配置文件cacheEnable = true
- XXXmapper.xml中添加 < cache/>
- 注意事项
- 二级缓存需要缓存的数据实现Serializable接口
- 只有会话关闭或提交后,一级缓存数据才会转移到二级缓存
- 开启方式:
- 当该作用域下数据库中数据进行了增、删、改后,默认将其作用域下所有select的缓存clear
- 使用场景:耗时较高的统计分析sql,(单服务开启)
微服务篇
SpringCloud
【核心组件】
- Eureka:注册中心
- Nacos:注册中心/配置中心
- Ribbon:负载均衡
- Feign:服务调用
- Hystrix:服务熔断
- sentinel:服务保护
- Zuul/Gateway:服务网关
【注册中心】
- 服务注册:服务提供者将自己的服务名称、ip、端口等信息注册到eureka/nacos
- 服务发现:消费者向eureka拉取服务信息列表,然后利用负载均衡算法选择一个发起调用
- 服务监控:服务提供者每30s向eureka发送心跳,eureka服务90s没有检测到心跳,则从信息列表中剔除
【Eureka和Nacos的区别】
- 共同点:
- 都支持服务注册与发现
- 都提供心跳健康检测
- 区别:
- Nacos支持服务主动检测健康:临时实例为心跳模式,非临时实例为主动检测模式
- 临时检测心跳不正常会剔除,非临时检测心跳不正常不会剔除
- Nacos支持服务列表变更的主动推送,更新更及时
- Nacos集群默认采用AP模式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP模式
- Nacos还支持配置中心,Eureka只有注册中心,且已停更
- Nacos支持服务主动检测健康:临时实例为心跳模式,非临时实例为主动检测模式
【负载均衡】
- 自带策略
- RandomRule:随机选择
- RoundRobinRule:轮询选择
- WeightedResponseTimeRule:响应权重选择
- ZoneAvoidanceRule(默认):区域敏感策略,按机房或机架分类然后轮询
- 自定义策略
- 实现IRule接口(全局)
- 配置yml文件,配置某service的负载均衡策略(局部)
【服务降级/熔断】:降级太多就会熔断
- 服务雪崩:一个服务失败导致之后链路服务都失败
- 服务降级:为避免下游服务失败,服务兜底方案
- 方案:在**@FeignClient中添加fallback**属性定义兜底实现类(降级)
- 服务熔断:默认关闭,手动开启后,当检测到10s内请求失败率超过了50%,就触发熔断机制,之后每隔5s取尝试请求,不响应则继续熔断,响应则关闭熔断恢复正常请求。
【服务监控/部署】
- Prometheus(普罗米修斯)
- 监控服务健康、集群健康、CPU/内存占用
- ES索引使用情况
- Kafka队列消费堆积情况
- 慢SQL、慢接口统计
- 健康报警
- 自定义监控规则
- Docker:容器化部署
- Jenkins:自动化构建工具
- Kibana:ES监控、可视化
微服务业务
【服务限流】
- 突发限流:为应对突发流量
- 常规限流:为防止恶意攻击,系统保持正常运行,系统能够承受最大QPS
- Nginx限流
- 控制速率(突发流量):使用漏桶算法过滤,让请求以固定速率通处理
- 控制并发:限制单个ip的连接数和并发连接的总数
- 网关限流
- SpringCloudGateway中支持局部过滤器RequestRateLimiter来做限流,使用令牌桶算法
- 可以根据ip或路径限流,可以设置每秒填充平均速率和令牌桶总容量
【CAP原则+BASE理论】
- CAP:一致性、可用性、分区容错性
- 分布式系统节点通过网络连接,一定会出现分区问题§
- 当分区出现时,系统的**一致性©和可用性(A)**就无法同时满足
- BASE理论
- 基本可用
- 软状态
- 最终一致
- 解决分布式事务的思想和模型
- 最终一致思想:各分支事务分别提交,如果不一致,则补偿恢复数据(AP)
- 强一致思想:各分支事务执行完业务不提交,等待彼此后统一提交或回滚(CP)
【分布式事务】
- seata
- seata的XA模式:CP模型,需要互相等待各分支事务提交,保证强一致性,性能差
- seata的AT模式:AP模型,底层使用undo log实现,性能好
- seata的TCC模式:AP,性能较好,不过需要人工编码实现
- MQ模式:在同一事务内本地写数据+发送消息,异步,性能最好,AP模型,失败需手动补偿
【分布式服务的接口幂等性】
- 幂等:多次调用方法或接口不会改变业务状态,重复调用和单次调用结果一致
- 如果是新增数据,可以使用数据库的唯一索引
- 如果是新增或修改数据
- **分布式锁,**性能较低
- 使用token+redis来实现,性能较好
- 第一次请求,生成唯一token存入redis,返回给前端
- 第二次请求,业务处理携带之前的token,redis验证,如果存在,则执行业务并删除token,如果不存在则返回不处理(高并发依然有漏洞,可setnx或redission)
【分布式任务调度】
- xxl-job
- 路由策略:xxl-job提供了很多的路由策略:轮询、故障转移、分片广播
- 任务失败解决
- 路由策略选择故障转移,使用健康的实例执行任务
- 设置重试次数
- 查看日志+邮件警告
- 大量任务同时执行怎么解决
- 部署集群让多个实例一起执行,路由策略分片广播
- 在任务执行的代码中可以获取分片总数和当前分片,按照取模的方式分摊到各个实例执行
- 问:如何确保任务执行一次,不被多实例执行
- 答:xxl-job单机策略或者分布式锁
消息中间件篇
RabbitMQ
【RabbitMQ如何保证消息不丢失】
- 开启生产者确认机制(publisher confirm),确保生产者的消息能到达交换机
- 失败后如何处理
- 回调方法即时重发
- 记录日志
- 保存数据库,定时重发,成功即删
- 失败后如何处理
- 开启持久化功能,确保消息在队列中不会丢失
- 交换机持久化
- 队列持久化
- 消息持久化
- 关闭消费者的确认机制为auto,改为手动确认消息处理成功后返回ack回执
- 开启消费者失败重试机制,多次重试失败后将消息投递到异常交换机,交由人工处理
【RabbitMQ重复消费问题】
- 原因:网络抖动 / 消费者挂了
- 方案:
- 每条消息设置唯一标识id
- 幂等方案:分布式锁、数据库锁(悲观锁、乐观锁)
【RabbitMQ延迟队列/死信交换机?】
- 场景:超时订单、限时优惠、定时发布
- 实现:
- 死信交换机 + TTL(消息存活时间)
- 消息超时未消费、拒绝消费、队列满了都会进入死信队列
- 延迟队列插件DelayExchange
- 声明一个交换机,添加delayed属性为true
- 发送消息时,添加x-delay头,值为超时时间
- 死信交换机 + TTL(消息存活时间)
【RabbitMQ消费堆积如何解决?】
- 增加消费者,提高消费速度
- 消费者内开启线程池加快消息处理速度
- 扩大队列容量,提高堆积上限,采用惰性队列
- 在声明队列的时候设置属性x-queue-mode为lazy,即为惰性队列
- 基于磁盘存储,消息上限高
- 性能比较稳定,但基于磁盘存储,受限于磁盘IO,时效性会降低
【RabbitMQ的高可用】
- 镜像模式搭建的集群,一主多从,主节点操作,从节点同步
- 主宕机后,镜像节点替代为新主(如果复制完成前宕机,则可能丢失数据)
- 这种情况下可以采用仲裁队列,主从基于Raft协议,强一致。
【Kafka如何保证消息不丢失】
- 生产者发送消息到Brocker丢失
- 设置异步发送,发送失败使用回调进行记录或者重发
- 失败重试,参数配置,可以设置重试次数
- 消息在Brocker中存储丢失
- 发送确认acks(0/1/all),选择all,确保所有副本保存成功
- 消费者从Brocker接收消息丢失
- 关闭自动提交偏移量,开启手动提交
- 提交方式为同步+异步提交
【Kafka消息重复消费】
- 关闭自动改为手动提交
- 提交方式同步+异步
- 唯一id
- 幂等方案
【Kafka如何保证消费顺序】
- 问题原因:一个topic数据可能存在不同的分区内,每个分区按顺序存储的偏移量,如果消费者关联了多个分区则不能保证顺序性
- 解决方案:
- 发送消息指定分区
- 发送消息按照业务设置相同的key
【Kafka高可用方案】
- 集群模式:一个集群由多个Broker实例组成,一个宕机,其他继续服务
- 分区备份机制(主从复制):同步保存副本(ISR)+普通异步副本,鸡蛋放在不同的篮子
【Kafka清理机制】
- kafka存储结构
- kafka种topic的数据存在分区上,当分区文件过大时会分段存储segment
- 每个分段都在磁盘上以index+log形式存储
- 分段的好处是:
- 减少单文件大小,查找方便
- 方便kafka清理日志
- 日志清理策略
- 根据消息保留时间,超过168小时(默认7天)就会触发清理
- 根据大小,大于一定阈值删除最久消息(默认关闭)
【Kafka的高性能设计】
- 消息分区:不受单台服务器的限制
- 顺序读写:磁盘顺序读写,提升读写效率
- 页缓存:把磁盘中的数据缓存到内存中,磁盘访问变为内存访问
- 零拷贝:减少上下文切换及数据拷贝
- 我从仓库拿出来+发快递 ===》》我从仓库发快递
- 消息压缩:减少磁盘IO和网络IO
- 分批发送:将消息打包批量发送,减少网络开销
数据库篇
MySql
【如何定位慢查询】
- 表象:页面加载过慢、接口压测响应时间长、普罗米修斯监控告警
- 如何定位
- 开源工具
- 调试工具:Arthas
- 运维工具:Prometheus、Skywaliking
- Mysql慢日志(/etc/my.cnf)配置
- slow_query_log = 1 #开启
- long_query_time = 2 # 慢sql阈值2s
- 开源工具
【如何分析慢查询】
- 慢查询一般由
- 聚合查询:新增临时表
- 多表查询:优化sql语句
- 表数据量过大查询:添加索引
- 深度分页查询
- SQL执行计划 explan / desc
- possible_key:可能使用到的索引,可能是多个
- key:当前sql实际命中的索引
- ken_len:当前命中索引占用的大小
- Extra:重要的额外信息
- using filesort(重要)
- using temporary(重要)
- using index(重要)
- using index condition:直接使用了索引,但是需要回表查询数据
- using where
- using join buffer
- impossible where
- select tables optimized away
- distinct
- type
- Null:未使用到表
- system:查询系统表
- const:主键查询
- eq_ref:主键查询/唯一索引查询
- ref:索引查询
- range:范围查询
- index:索引树查询
- all:全盘扫描
【索引】
- 什么是索引:查询快的目录
- 索引(index)是帮助mysql高效获取数据的一种数据结构(有序)
- 提高数据检索效率,降低数据库IO成本(不需要全表扫描)
- 通过索引列对数据进行排序,降低数据排序成本,降低CPU消耗
- 索引的底层数据结构(InnoDB引擎采用B+树)
- 阶数更多,路径更短
- 磁盘读写代价B+树更低,非叶子节点只存储指针,叶子节点存储数据
- B+树便于扫库和区间查询,叶子节点是一个双向链表
【聚簇索引?非聚簇索引】
- 聚簇索引(聚集索引):B+树的叶子节点保存了整行数据row(),有且只有一个
- 有主键选择主键
- 没有主键,使用第一个唯一索引(Unique)
- 没有主键,没有唯一键,自动生成一个rowid作为隐藏的聚簇索引
- 非聚簇索引(二级索引):B+树的叶子节点保存了对应的主键id,可以有多个
- 问:什么是回表查询?
- 答:通过二级索引查到对应的主键id,再去主键索引查询整行数据
【覆盖索引】不需要回表
- 查询使用了索引,并能在索引中找到所需的所有字段。
- 问:MySql超大分页怎么处理?
- 答:超大索引一般是数据量比较大时,limit分页查询,需要对数据进行排序,效率很低,我们可以用覆盖索引+子查询解决。
【索引创建的原则】
- 数据量较大,且查询比较频繁的表
- 常作为**查询条件(where)、排序(order by)、分组(group by)**操作的字段
- 字段内容区分度高
- 内容较长的字符串可以使用前缀索引
- 尽量使用联合索引
- 要控制索引的数量,越多维护索引的代价越大,会影响增删改的效率
- 如果索引不能存Null,在建表时添加not null,优化器可以更好的确定有效索引
【索引失效】
- 违反最左前缀法则
- 范围查询后面的列,索引失效
- 不要在索引列上进行运算操作,索引失效
- 字符串不加单引号,类型转换造成索引失效
- 以**%开头的Like模糊查询,索引失效**
【SQL优化】
- 表的设计优化
- 根据实际情况选择合适的数值类型
- 合适的字符串类型,char定长效率高,varchar可变长度
- 索引的创建及优化
- sql语句优化,
- 避免索引失效,
- 避免select * 回表
- 尽量union all代替union(多一次过滤)
- join优化,能用innerjoin就不用left join/right join,小表驱动大表
- 主从复制,读写分离,不让数据写影响读操作
- 分库分表
【事务】
- 事务是一组操作,统一提交或撤销,要么同时成功,要么同时失败
- 特性
- 原子性(Atomicity):不可分割的最小操作单元,要么一起成功,要么一起失败
- 一致性(Consistency):事务完成时,所有数据一致
- 隔离性(Isolation):数据库提供的隔离机制,保证事务在不受外部并发操作影响的独立环境下运行
- 持久性(Durability):事务一旦提交或回滚,对数据库的数据改变是永久的
【并发事务问题?MySQL的隔离级别】
- 并发事务的问题
- 脏读:一个事务读到另一个事务还未提交的数据
- 不可重复读:一个事务先后读取同一条记录,不一样
- 幻读:一个事务按条件查不到数据,插入数据时,发现已经有了,好像出现"幻影"
- 隔离级别
Read Uncommitted 未提交读:有脏读、不可重复读、幻读的问题
Read Committed 已提交读:有不可重复读、幻读的问题
Repeatable Read 可重复读(默认):有幻读问题
Serializable 串行化:没问题,性能差
【undo log和redo log】
- redo log:记录的是数据页的物理变化,服务宕机时用来同步数据
- undo log:记录逻辑日志,当事务回滚时,通过逆操作恢复原来的数据
- redo log保证了事务的持久性,undo log保证了事务的原子性和一致性
【MVCC】//TODO
【主从同步】binlog
- 主库在事务提交时,会把数据变更记录在二进制日志文件Binlog中,DDL+DML
- 从库开线程IOThread****读取主库的binlog,写入从库的中继日志Relay Log中
- 从库再开线程SQLThread执行relay log
【分库分表】
- 业务介绍:简历上的项目,数据量级(1000W或超过20G)
- 分库分表的时机
- 项目业务数据逐渐增多,或业务发展比较迅速
- 优化已经解决不了性能问题(主从读写分离、查询索引)
- IO瓶颈(磁盘IO、网络IO)
- CPU瓶颈(聚合查询、连接数太多)
- 具体拆分策略
- 垂直分库:根据业务拆分,高并发下提高磁盘IO和网络连接数
- 根据功能分为业务库、统计库、管理库
- 垂直分表:冷热字段数据分离,多表互不影响
- 客户字段50-100个,拆分为不同维度的表
- 水平分库:将一个库的表拆分为多个库,解决海量数据存储和高并发的问题
- 公有云、私有云、混合云数据库拆分
- 水平分表:解决单表存储和性能的问题
- 按年拆分,按公司私有化
- 垂直分库:根据业务拆分,高并发下提高磁盘IO和网络连接数
企业场景篇
设计模式
【简单工厂模式】(解耦new方法,但有违开闭原则5)
属编程习惯,非设计模式
- 抽象产品:定义产品规范,抽象类/接口
- 具体产品:实现或继承抽象产品的子类
- 具体工厂:提供创建产品的静态方法
【工厂方法模式】(剥离(工厂+产品),满足开闭原则)
- 抽象产品:定义产品规范,抽象类/接口
- 具体产品:实现或继承抽象产品的子类(由具体工厂创建,一一对应)
- 抽象工厂:提供创建产品的接口
- 具体工厂:实现抽象工厂,完成具体产品的创建
- 优缺点:类太多,但有新产品时只需要添加新的工厂+新的产品类即可,无需修改原代码,可插拔,满足开闭原则。
//工厂接口
@SPI("dubbo")
public interface RegistryFactory {
/**
* 连接注册中心.
*
* 连接注册中心需处理契约:<br>
* 1. 当设置check=false时表示不检查连接,否则在连接不上时抛出异常。<br>
* 2. 支持URL上的username:password权限认证。<br>
* 3. 支持backup=10.20.153.10备选注册中心集群地址。<br>
* 4. 支持file=registry.cache本地磁盘文件缓存。<br>
* 5. 支持timeout=1000请求超时设置。<br>
* 6. 支持session=60000会话超时或过期设置。<br>
*
* @param url 注册中心地址,不允许为空
* @return 注册中心引用,总不返回空
*/
@Adaptive({"protocol"})
Registry getRegistry(URL url);
}
//抽象工厂
public abstract class AbstractRegistryFactory implements RegistryFactory {
public Registry getRegistry(URL url) {
url = url.setPath(RegistryService.class.getName())
.addParameter(Constants.INTERFACE_KEY, RegistryService.class.getName())
.removeParameters(Constants.EXPORT_KEY, Constants.REFER_KEY);
String key = url.toServiceString();
// 锁定注册中心获取过程,保证注册中心单一实例
LOCK.lock();
try {
Registry registry = REGISTRIES.get(key);
if (registry != null) {
return registry;
}
//具体注册中心调到的方法
registry = createRegistry(url);
if (registry == null) {
throw new IllegalStateException("Can not create registry " + url);
}
REGISTRIES.put(key, registry);
return registry;
} finally {
// 释放锁
LOCK.unlock();
}
}
//具体工厂所需要实现的类
protected abstract Registry createRegistry(URL url);
}
//以zookeeper为例
public class ZookeeperRegistryFactory extends AbstractRegistryFactory {
private ZookeeperTransporter zookeeperTransporter;
public void setZookeeperTransporter(ZookeeperTransporter zookeeperTransporter) {
this.zookeeperTransporter = zookeeperTransporter;
}
public Registry createRegistry(URL url) {
return new ZookeeperRegistry(url, zookeeperTransporter);
}
}
【抽象工厂模式】(升级版工厂方法)
- 抽象产品:定义产品规范,抽象类/接口(同上)
- 具体产品:实现或继承抽象产品的子类(由具体工厂创建,一一对应)(同上)
- 抽象工厂:提供创建多种产品的接口
- 具体工厂:实现抽象工厂,完成具体产品的创建(同上)
【策略模式】
- 定义:通过对算法进行封装,将算法的实现和使用分开,并委派给不同的对象管理算法
- 角色
- **抽象策略(Strategy)**类:给出所有具体策略类的接口,接口/抽象类
- 具体策略(Concrete Strategy)类:实现了抽象策略的类,提供具体的算法实现
- 环境(Context)类:持有一个策略类的引用,供客户端调用
- 场景:同一行为的不同形式(算法)
- 订单支付策略(支付宝、微信、银行卡)
- 解析不同类型的excel(xls格式、xlsx格式)
- 不同打折促销算法(满300元9折、满500元8折、满1000元7折)
物流运费阶梯计价(5kg以下、5-10kg、10-20kg、20kg以上) - Feign负载均衡策略:轮询、随机、自定义、、、
【责任链模式】拦截器、过滤器
- 定义: 为避免请求处理的耦合,链上各负责模块各司其职,是自己的就处理,否则交给别人,直到处理完
- 角色:
抽象处理者(Handler):定义一个处理请求的接口,包含处理方法和下一个负责人
具体处理者(Concrete Handler):实现抽象处理者的处理方法,判断能干就干,不能干交给别人
客户类(Client):创建处理链,制定顺序并启动 - 优点:
- 降低了请求发送者和处理者的耦合度
- 增强了系统的可扩展性(新增流程)
- 增强了给对象指派职责的灵活性
- 简化对象之间连接
- 责任分担(满足单一职责原则6)
- 场景
- 内容审核:视频、文章、课程
- 订单创建:校验参数、填充订单、算价、落库、返佣
- 简易的流程审批:组长审批、主管审批、副总裁、总裁
技术场景
【单点登录SSO】
- 定义:单点登录(Single Sign On),简称SSO,只需要登录一次,就可以访问所有信任的应用系统
- 旧的登录:单体tomcat。session可共享。没问题
- 分布式/微服务登录:JWT、Oauth2、CAS
- JWT:
- 用户访问系统,网关判定token是否有效
- 无效则返回401认证失败并跳转到登录页
- 用户发送登录请求,返回浏览器JWTToken,浏览器保存到cookie中
- 再访问时,带着token,网关验证后路由到目标服务器
【权限认证】
- RBAC模型(Role-Based Access Control):基于角色的控制访问
- 3个基础部分组成:用户、角色、权限
- 具体实现
- 5张表:用户表、角色表、权限表、用户角色中间表、角色权限中间表
- 7张表:用户表、角色表、权限表、用户角色中间表、角色权限中间表、菜单表、权限菜单中间表
-权限框架:Apache shiro、SpringSecurity
【上传数据/网络传输的安全性】
-
答:给前端一个秘钥,将数据加密后传到后台,后台解密处理
-
对称加密(AES):文件加解密使用相同的秘钥
- 优点:加密速度快,效率高
- 缺点:相对不太安全(不要保存敏感信息)
-
非对称加密:公钥加密,私钥解密
- 优点:安全性较高
- 缺点:加密解密速度慢,建议少量数据
【项目难题如何解决】
- 设计模式(工厂、策略、责任链)
- 线上BUG(CPU彪高、内存泄露、线程死锁)
- 调优(慢接口、慢SQL、缓存方案)
- 组件封装(分布式锁、接口幂等、分布式事务、支付通用)
- 什么背景(技术问题)
- 过程(解决问题的过程)
- 最终落地方案
【项目日志怎么采集的】
- 为什么:日志是定位系统问题的重要手段
- 方式:
- 常规采集:按天保存日志
- ELK:即Elasticsearch、Logstash、Kibana
- ElasticSearch:全文搜索分析引擎,可对数据进行存储。搜索、分析
- Logstash是一个数据收集引擎,动态收集数据并存储
- Kibana是一个数据分析和可视化平台,配合es检索分析
【常见日志】
- 监控日志变化:tail -n 100 -f xx.log(最后100行)
- 按行号查询
- 尾部100行:tail -n 100 xx.log
- 头部100行:head -n 100 xx.log
- 行号区间:cat -n xx.log|tail -n +100 | head -n 100(查询100-200行)
- 按关键字查找
- cat xx.log |greo “debug”
- 按日期查询
- sed -n ‘/2023-10-18 00:00:00/,/2023-10-19 00:00:00/p’ xx.log
- 日志太多
分页查找:cat -n xx.log | grep “debug” | more回车翻页
过滤到文件:cat -n xx.log |grep “debug” > debug.txt
【生产问题怎么排查】
- 先分析日志
- 远程debug(一般不允许)
- 首先保证远程和本地代码一致
- 远程启动加支持远程的参数
- idea=》Edit Configurations=> Remote JVM debug
- 设置远程ip+端口+配置,然后点debug按钮
- 开始断点调试
【如何快速定位系统的瓶颈】
- 压测(性能测试)
- 压测目的:给出系统当前的性能状况,定位性能瓶颈或潜在瓶颈
- 指标:响应时间、QPS、并发数、吞吐量、CPU利用率、内存使用率、磁盘IO、错误率
- 压测工具:LoadRunner+Apache Jmeter
- 后端:接口慢、代码报错、并发
- 监控、链路追踪
- 监控:Prometheus+Grafana
- 链路追踪:skywalking、Zipkin
- 线上诊断工具Arthas(阿尔萨斯)
- 官网:https://arthas.aliyun.com/
- 官网:https://arthas.aliyun.com/