Redis篇
什么是缓存穿透 ? 怎么解决 ?
缓存穿透是指查询一个不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。
解决方案有两种:
- 缓存空数据
- 布隆过滤器:我们通常都会用布隆过滤器来解决它。
你能介绍一下布隆过滤器吗?
布隆过滤器主要是用于检索一个元素是否在一个集合中。
它的底层主要是先去初始化一个比较大数组,里面存放的二进制0或1。在一开始都是0,当一个key来了之后经过3次hash计算,模于数组长度找到数据的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。
当然,布隆过滤器是有缺点的,其中之一就是可能会产生一定的误判率。我们一般可以设置这个误判率,通常不会超过5%。实际上,这个误判率是难以避免的,除非我们增加布隆过滤器使用的位数组的长度。但即便如此,5%以内的误判率对于一般的项目来说已经是可以接受的,不至于在高并发下导致数据库被压垮。
什么是缓存击穿 ? 怎么解决 ?
缓存击穿的意思是对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。
解决方案有两种方式:
解决方案一: 互斥锁,强一致,性能差
解决方案二: 逻辑过期,高可用,性能优,不能保证数据绝对一致
- 第一可以使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓存,否则重试get缓存的方法
- 第二种方案可以设置当前key逻辑过期,大概思路如下:
- 在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间
- 当查询的时候,从redis取出数据后判断时间是否过期
- 如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新的
当然两种方案各有利弊:
- 如果选择数据的强一致性,建议使用分布式锁的方案,性能上可能没那么高,锁需要等,也有可能产生死锁的问题
- 如果选择key的逻辑删除,则优先考虑的高可用性,性能比较高,但是数据同步这块做不到强一致。
什么是缓存雪崩 ? 怎么解决 ?
缓存雪崩意思是设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。
解决方案:
- 给不同的Key的TTL添加随机值
- 利用Redis集群提高服务的可用性:
哨兵模式、集群模式
- 给缓存业务添加降级限流策略:
Nginx、Spring Cloud Gateway
- 给业务添加多级缓存:
Guava或Caffeine
解决方案主要是可以将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
缓存三兄弟总结
缓存无中生有Key,布隆过滤null隔离。
缓存击穿过期Key,锁与非期解难题。
雪崩大量过期Key,过期时间要随机。
面试必考三兄弟,可用限流来保底。
redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)
双写一致性: 当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致。
允许延时一致的业务(采用异步通知)
- 使用
MQ
中间件,更新数据之后,通知缓存删除 - 利用
Canal
中间件,不需要修改业务代码,伪装为MySQL的一个从节点,Canal
通过读取binlong
数据更新缓存
就说我最近做的这个项目,里面有xxxx(根据自己的简历上写)的功能,数据同步可以有一定的延时(符合大部分业务)
我们当时采用的阿里的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。
强一致性(采用Redisson提供的读写锁)
- 共享锁:读锁
readLock
,加锁之后其它线程可以共享读操作 - 排他锁:也叫独占锁
writeLock
,加锁之后,阻塞其它线程读写操作
就说我最近做的这个项目,里面有xxxx(根据自己的简历上写)的功能,需要让数据库与redis高度保持一致,因为要求时效性比较高,我们当时采用的读写锁保证的强一致性。
我们采用的是redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是读方法和写方法上需要使用同一把锁才行。
你听说过延时双删吗?为什么不用它呢?
- 读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存
- 写操作:
延迟双删
(删除缓存—》修改数据库—》延时
删除缓存)
延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性,所以没有采用它。
Redis持久化
RDB
RDB全称 Redis DataBase Backup File(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据
-
主动备份
save #由Redis主进程来执行RDB,会阻塞所有命令bgsave #开启子进程执行RDB,避免主进程收到影响
-
Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:
# 900秒内,如果至少有一个key被修改,则执行bgsave save 900 1 # 300秒内,有10个key被修改,也会执行bgsave save 300 10 # 60秒内,有一万个key被修改,也会执行bgsave save 60 10000
RDB的执行原理: bgsave开始时会fork主进程得到子进程 共享
主进程的内存数据。完成fork后读取内存数据并写入RDB文件。
fork采用的是copy-on-write技术:
- 当主进程执行读操作时,访问共享内存
- 当主进程执行写操作时,则会拷贝一份数据,执行写操作
AOF
AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件。
-
AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF:
# 是否开启AOF功能,默认是no appendonly yes # AOf文件的名称 appendfilename "appendonly.aof"
-
AOF的命令记录的频率也可以通过redis.conf文件来配:
# 表示每执行一次写命令,立即记录到AOF文件 appendfsync always # 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区写到AOF文件,是默认方案 appendfsync everysec # 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写会磁盘 appendfsync no
配置项 刷盘时机 优点 缺点 always 同步刷盘 可靠性高,几乎不丢数据 性能差 everysec 每秒刷盘 性能适中 最多丢失1秒数据 no 操作系统控制 性能最好 可靠性差,可能丢失大量数据
因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行 bgrewriteaof
命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。
Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置:
# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上
auto-aof-rewrite-min-size 64mb
RDB与AOF对比
RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会 结合
两者来使用
RDB | AOF | |
---|---|---|
持久化方式 | 定时对整个内存做快照 | 记录每一次执行的命令 |
数据完整性 | 不完整,两次备份之前会丢失 | 相对完整,取决于刷盘策略 |
文件大小 | 会有压缩,文件体积小 | 记录命令,文件体积很大 |
宕机恢复速度 | 快 | 慢 |
数据恢复优先级 | 低,因为完整性不如AOF | 高,因为数据完整更高 |
系统资源占用 | 高,大量CPU和内存消耗 | 低,主要是磁盘IO资源但AOF重写时会占用大量CPU和内存资源 |
使用场景 | 可以容忍数分钟的数据丢失,追求更快的启动速度 | 对数据安全性要求较高 |
redis做为缓存,数据的持久化是怎么做的?
在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF
这两种持久化方式有什么区别呢?
RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。
AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中,当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据
这两种方式,哪种恢复的比较快呢?
RDB因为是二进制文件,在保存的时候体积也是比较小的,它恢复的比较快,但是它有可能会丢数据,我们通常在项目中也会使用AOF来恢复数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF文件中可以设置刷盘策略
数据过期策略
Redis对数据设置数据的有效时间,数据过期以后,就需要将数据从内存中删除掉。可以按照不同的规则进行删除,这种删除规则就叫做数据过期策略。
惰性删除: 设置该key过期时间后,我们不去管它,当需要该key时,我们再检查其是否过期,我们就删掉它,反之返回该key。
-
优点: 对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查
-
缺点: 对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放
set name zhangsan 10get name # 发现name过期了,直接删除key
定期删除: 每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中过期的key)
- 优点: 可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响。另外定期删除,也能有效释放过期占用的内存。
- 缺点: 难以确定删除操作执行的时长和频率
定时删除两种模式:
- SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,可以通过修改配置文件 redis.conf 的
hz
选项来调整这个次数 - FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
Redis的过期删除策略: 惰性删除 + 定期删除
两种策略进行配合使用。
数据淘汰策略
数据淘汰策略: 当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则叫做内存淘汰策略。
Redis支持8种不同策略来选择要删除的key:
- noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,
默认是该策略
- volatile-ttl:对设置了TTL的可以,比较key的剩余TTL值,TTL越小越先被淘汰
- allkeys-random:对全体key,随机进行淘汰
- volatile-random:对设置了TTL的key,随机进行淘汰
- allkeys-lru: 对全体key,基于LRU算法(最近最少使用)进行淘汰
- volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰
- allkeys-lfu: 对全体key,基于LFU算法(最少频率使用)进行淘汰
- volatile-lfu: 对设置了TTL的key,基于LFU算法进行淘汰
使用建议
- 优先使用 allkeys-lru 策略。充分利用 LRU 算法的优势,把最近常访问的数据保留在缓存中。 如果业务有明显的冷热数据区分,建议使用。
- 如果业务中数据访问频率差别不大,没有明显的冷热数据区分,建议使用 allkeys-random,随机选择淘汰
- 如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其它设置过期时间的数据
- 如果业务中有短时高频访问的数据,可以使用 allkeys-lfu 或 volatile-lfu 策略
数据库有1000万数据 ,Redis只能缓存20w数据, 如何保证Redis中的数据都是热点数据 ?
可以使用 allkeys-lru (挑选最近最少使用的数据淘汰)淘汰策略,那留下来的都是经常访问的热点数据
Redis的内存用完了会发生什么?
这个要看redis的数据淘汰策略是什么,如果是默认的配置,redis内存用完以后则直接报错
分布式锁
分布式锁使用的场景:集群情况下的定时任务、抢单、幂等性场景
Redis实现分布式锁主要利用Redis的 setnx
命令。setnx是 SET is not exists
(如果不存在,则SET)的简写
-
获取锁:
# 添加锁,NX是互斥,EX是设置超时时间 SET lock value NX EX 10
-
释放锁:
# 释放锁,删除即可 DEL key
为什么要使用分布式锁?
在分布式系统里,因为多个服务或应用实例可能同时运行在不同的服务器上,它们都有可能去操作同一个共享资源,比如数据库中的某条记录或某个缓存项。如果不对这些操作加以控制,就可能会遇到数据冲突、数据不一致或者重复处理的问题。这就像是一群人在没有协调的情况下同时去修改同一份文件,结果可想而知,文件内容会变得乱七八糟。
为了解决这个问题,我们就需要用到分布式锁。分布式锁就像是一个看门人,它确保在同一时间内,只有一个服务或应用实例能够进入并操作这个共享资源。这样,我们就可以避免数据冲突和不一致,确保操作的正确性和一致性。
Redis分布式锁如何实现?
在redis中提供了一个命令setnx(SET if not exists)
由于redis的单线程的,用了命令之后,只能有一个客户端对某一个key设置值,在没有过期或删除key的时候是其他客户端是不能设置这个key的
如何控制Redis实现分布式锁有效时长呢?
redis的setnx指令不好控制这个问题,我们可以采用的redis的一个框架redisson实现
在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在redisson中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了
还有一个好处就是,在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,它会自旋不断尝试获取锁,如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。
redisson实现的分布式锁是可重入的吗?
是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计数上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数
redisson实现的分布式锁能解决主从一致性的问题吗?
这个是不能的,比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,此时当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。
我们可以利用redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,并且要求在大多数redis节点上都成功创建锁,红锁中要求是redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。
但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁
如果业务非要保证数据的强一致性,这个该怎么解决呢?
redis本身就是支持高可用的,做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,建议使用zookeeper实现的分布式锁,它是可以保证强一致性的
Redis集群有哪些方案, 知道嘛?
在Redis中提供的集群方案总共有三种:主从复制、哨兵模式、Redis分片集群
- 主从和哨兵可以解决高可用、高并发读的问题
- 分片集群可以解决海量数据存储问题、高并发写的问题
那你能介绍一下主从同步吗?
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中
能说一下,主从同步数据的流程?
主从同步分为了两个阶段,一个是全量同步,一个是增量同步
全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的:
- 从节点请求主节点同步数据,其中从节点会携带自己的拷贝(replication) id和偏移量(offset)
- 主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致
- 在同时主节点会执行bgsave,生成rdb文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件,这样就保持了一致
- 如果在rdb生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件,这个就是全量同步
增量同步指的是,当从节点服务重启之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据,主节点还是判断是不是第一次请求,不是第一次就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步
怎么保证Redis的高并发高可用?
首先可以搭建主从集群,再加上使用redis中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主;同时Sentinel也充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端,所以一般项目都会采用哨兵的模式来保证redis的高并发高可用
redis集群脑裂,该怎么解决呢?
有的时候由于网络等原因可能会出现脑裂的情况,就是说,由于redis master节点和redis salve节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个salve为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将old master降为salve,这时再从新master同步数据,这会导致old master中的大量数据丢失。
关于解决的话,在redis的配置中可以设置:第一可以设置最少的salve节点个数,比如设置至少要有一个从节点才能同步数据,第二个可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失
redis的分片集群有什么作用?
分片集群主要解决的是,海量数据存储的问题,集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,就可以继续增大集群的高并发能力。同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点
Redis分片集群中数据是怎么存储和读取的?
Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围, key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,通过槽找到对应的节点进行存储
取值的逻辑是一样的
Redis是单线程的,但是为什么还那么快?
- 完全基于内存的,C语言编写
- 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
- 使用多路I/O复用模型,非阻塞IO
例如:bgsave 和 bgrewriteaof 都是在后台执行操作,不影响主线程的正常使用,不会产生阻塞
能解释一下I/O多路复用模型?
I/O多路复用是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能
其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器
在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
MySQL篇
MySQL中,如何定位慢查询?
在MySQL中提供了慢日志查询的功能,可以在MySQL的系统配置文件中开启这个慢日志的功能,并且也可以设置SQL执行超过多少时间来记录到一个日志文件中
SQL语句执行很慢, 如何分析呢?
如果一条sql执行很慢的话,我们通常会使用mysql自带的执行计划explain来去查看这条sql的执行情况,比如在这里面可以通过key和key_len检查是否命中了索引,如果本身已经添加了索引,也可以判断索引是否有失效的情况,第二个,可以通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或全盘扫描,第三个可以通过extra建议来判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复
什么是索引?
它是帮助MySQL高效获取数据的数据结构,主要是用来提高数据检索的效率,降低数据库的IO成本,同时通过索引列对数据进行排序,降低数据排序的成本,也能降低了CPU的消耗
索引的底层数据结构了解过嘛 ?
MySQL的默认的存储引擎InnoDB采用的B+树的数据结构来存储索引,选择B+树的主要的原因是:第一阶数更多,路径更短,第二磁盘读写代价B树更低,非叶子节点只存储指针,叶子阶段存储数据,第三B+树便于扫库和区间查询,叶子节点是一个双向链表
B树和B+树的区别是什么呢?
- 第一:在B树中,非叶子节点和叶子节点都会存放数据,而B+树的所有的数据都会出现在叶子节点,在查询的时候,B+树查找效率更加稳定
- 第二:在进行范围查询的时候,B+树效率更高,因为B+树都在叶子节点存储,并且叶子节点是一个双向链表
什么是聚簇索引什么是非聚簇索引 ?
-
聚簇索引主要是指数据与索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个,一般情况下主键作为聚簇索引
-
非聚簇索引指的是数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个,一般我们自己定义的索引都是非聚簇索引
什么是回表查询?
回表的意思就是通过二级索引找到对应的主键值,然后再通过主键值找到聚集索引中所对应的整行数据,这个过程就是回表(如果面试官直接问回表,则需要先介绍聚簇索引和非聚簇索引)
什么叫覆盖索引?
覆盖索引是指select查询语句使用了索引,在返回的列,必须在索引中全部能够找到,如果我们使用id查询,它会直接走聚集索引查询,一次索引扫描,直接返回数据,性能高
如果按照二级索引查询数据的时候,返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用select *,尽量在返回的列中都包含添加索引的字段
MYSQL超大分页怎么处理?
超大分页一般都是在数据量比较大时,我们使用了limit分页查询,并且需要对数据进行排序,这个时候效率就很低,我们可以采用覆盖索引和子查询来解决
先分页查询数据的id字段,确定了id之后,再用子查询来过滤,只查询这个id列表中的数据就可以了
因为查询id的时候,走的覆盖索引,所以效率可以提升很多
索引创建原则有哪些?
添加索引的字段是查询比较频繁的字段,一般也是作为查询条件,排序字段或分组的字段
通常创建索引的时候都是使用复合索引来创建,一条sql的返回值,尽量使用覆盖索引,如果字段的区分度不高的话,我们也会把它放在组合索引后面的字段
如果某一个字段的内容较长,我们会考虑使用前缀索引来使用,当然并不是所有的字段都要添加索引,这个索引的数量也要控制,因为添加索引也会导致增改删的速度变慢
什么情况下索引会失效 ?
- 违反最左前缀法则
- 范围查询右边的列,不能使用索引
- 不要再索引列上进行运算操作,索引将失效
- 字符串不加单引号,造成索引失效(类型转换)
- 以%开头的like模糊查询,索引失效
sql的优化的经验
sql优化的话,我们会从这几方面考虑,比如:建表的时候、使用索引、sql语句的编写、主从复制,读写分离,还有一个是如果数据量比较大的话,可以考虑分库分表
创建表的时候,如何优化的呢?
在定义字段的时候需要结合字段的内容来选择合适的类型,如果是数值的话,像tinyint、int 、bigint这些类型,要根据实际情况选择。如果是字符串类型,也是结合存储的内容来选择char和varchar或者text类型
平时对sql语句做了哪些优化呢?
SELECT语句务必指明字段名称,不要直接使用select * ,还有就是要注意SQL语句避免造成索引失效的写法;如果是聚合查询,尽量用union all代替union ,union会多一次过滤,效率比较低;如果是表关联的话,尽量使用innerjoin ,不要使用用left join right join,如必须使用 一定要以小表为驱动
事务的特性是什么?可以详细说一下吗?
事务ACID,分别指的是:
- 原子性: 事务是不可分割的最小操作单元,要么全部成功,要么全部失败
- 一致性:事务完成时,必须使所有的数据都保持一致状态
- 隔离性:数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境运行
- 持久性;事务一但提交或回滚,它对数据库中数据的改变就是永久的
我举个例子:
A向B转账500,转账成功,A扣除500元,B增加500元,原子操作体现在要么都成功,要么都失败
在转账的过程中,数据要一致,A扣除了500,B必须增加500
在转账的过程中,隔离性体现在A像B转账,不能受其他事务干扰
在转账的过程中,持久性体现在事务提交后,要把数据持久化(可以说是落盘操作)
并发事务带来哪些问题?
- 第一是脏读, 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的
- 第二是不可重复读:比如在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读
- 第三是幻读(Phantom read):幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读
怎么解决这些问题呢?MySQL的默认隔离级别是?
解决方案是对事务进行隔离
- 第一个是,未提交读(read uncommitted)它解决不了刚才提出的所有问题,一般项目中也不用这个
- 第二个是读已提交(read committed)它能解决脏读的问题的,但是解决不了不可重复读和幻读
- 第三个是可重复读(repeatable read)它能解决脏读和不可重复读,但是解决不了幻读,这个也是mysql默认的隔离级别
- 第四个是串行化(serializable)它可以解决刚才提出来的所有问题,但是由于让是事务串行执行的,性能比较低。所以,我们一般使用的都是mysql默认的隔离级别:可重复读
undo log和redo log的区别
其中redo log日志记录的是数据页的物理变化,服务宕机可用来同步数据,而undo log 不同,它主要记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据,比如我们删除一条数据的时候,那么undo log中会记录一个与原始删除操作相反的插入操作,如果发生回滚就执行逆操作
redo log保证了事务的持久性,undo log保证了事务的原子性和一致性
事务中的隔离性是如何保证的呢?(你解释一下MVCC)
事务的隔离性是由锁和MVCC实现的
其中MVCC的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,它的底层实现主要是分为了三个部分,第一个是隐藏字段,第二个是undo log日志,第三个是readView读视图
隐藏字段是指:在mysql中给每个表都设置了隐藏字段,有一个是trx_id(事务id),记录每一次操作的事务id,是自增的;另一个字段是roll_pointer(回滚指针),指向上一个版本的事务版本记录地址
undo log主要的作用是记录回滚日志,存储老版本数据,在内部会形成一个版本链,在多个事务并行操作某一行记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表
readView解决的是一个事务查询选择版本的问题,在内部定义了一些匹配规则和当前的一些事务id判断该访问那个版本的数据,不同的隔离级别快照读是不一样的,最终的访问的结果不一样。如果是rc隔离级别,每一次执行快照读时生成ReadView,如果是rr隔离级别仅在事务中第一次执行快照读时生成ReadView,后续复用
MySQL主从同步原理
MySQL主从复制的核心就是二进制日志(DDL(数据定义语言)语句和 DML(数据操纵语言)语句),它的步骤是这样的:
- 第一:主库在事务提交时,会把数据变更记录在二进制日志文件 Binlog 中
- 从库读取主库的二进制日志文件 Binlog ,写入到从库的中继日志 Relay Log
- 从库重做中继日志中的事件,将改变反映它自己的数据
框架篇
Spring框架中的单例bean是线程安全的吗?
不是线程安全的
当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这时多个线程会并发执行该请求对应的业务逻辑(成员方法),如果该处理逻辑中有对该单列状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。
Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。
比如:我们通常在项目中使用的Spring bean都是不可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。
如果你的bean有多种状态的话(比如 View Model对象),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用由“singleton”变更为“prototype”。
什么是AOP?
aop是面向切面编程,在spring中用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取公共模块复用,降低耦合,比如可以做为公共日志保存,事务处理等
Spring中的事务是如何实现的?
spring实现的事务本质就是aop完成,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
Spring中事务失效的场景有哪些?
- 数据库不支持事务
- 如果方法上异常捕获处理,自己处理了异常,没有抛出,就会导致事务失效,所以一般处理了异常以后,别忘了抛出去
- 如果方法抛出检查异常,如果报错也会导致事务失效,最后在spring事务的注解上,就是@Transactional上配置rollbackFor属性为Exception,这样不管是什么异常,都会回滚事务
- 如果方法上不是public修饰的,也会导致事务失效
- 如果事务方法被final或static修饰,由于Spring的AOP代理机制无法对final或static方法进行代理,因此这些事务方法将不会生效
- 事务传播性配置错误
spring事务的传播性?
- REQUIRED(必需):如果当前有事务则加入,否则新建事务
- SUPPORTS(支持):如果当前有事务则加入,无事务则非事务执行
- MANDATORY(强制):必须有事务,无事务则抛出异常
- REQUIRES_NEW(新建):总是新建事务,若当前有事务则挂起
- NOT_SUPPORTED(不支持):总是非事务执行,若当前有事务则挂起
- NEVER(禁止):不能有事务,若当前有事务则抛出异常
- NESTED(嵌套):若当前有事务则为当前事务创建嵌套事务,否则同REQUIRED
Spring的bean的生命周期?
- 实例化阶段:Spring容器根据配置创建Bean的实例
- 属性注入阶段:Spring将配置的属性值或依赖的Bean注入到新创建的Bean实例中
- Aware接口注入阶段:如果Bean实现了特定的Aware接口(如BeanNameAware),Spring会注入相关的上下文信息
- BeanPostProcessor的前置处理阶段:BeanPostProcessor允许在Bean初始化之前进行自定义逻辑处理
- 初始化阶段:Bean执行自定义的初始化逻辑,如通过@PostConstruct注解的方法或InitializingBean接口定义的方法
- 就绪使用阶段:Bean已完成初始化,可供应用程序使用
- BeanPostProcessor的后置处理阶段:BeanPostProcessor允许在Bean初始化之后进行额外的自定义逻辑处理
- 使用阶段:Bean在应用程序中执行其业务逻辑
- 销毁阶段:当Spring容器关闭时,Bean执行自定义的销毁逻辑,如通过@PreDestroy注解的方法或DisposableBean接口定义的方法
Spring中的循环引用?
循环依赖:循环依赖其实就是循环引用,也就是两个或两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于A
循环依赖在Spring中是允许存在,Spring框架依据三级缓存已经解决了大部分的循环依赖
- 一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的Bean对象
- 二级缓存:缓存早期的Bean对象(生命周期还没走完)
- 三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象
构造方法出现了循环依赖怎么解决?
由于bean的生命周期中构造函数是第一个执行的,spring框架并不能解决构造函数的的依赖注入,可以使用@Lazy懒加载,什么时候需要对象再进行Bean对象的创建
SpringMVC的执行流程知道吗?
- 用户发送请求到前端控制器DispatcherServlet,这是一个调度中心
- DispatcherServlet收到请求调用HandlerMapping(处理器映射器)
- HandlerMapping找到具体的处理器(可查找xml配置或注解配置),生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet
- DispatcherServlet调用HandlerAdapter(处理器适配器)
- HandlerAdapter经过适配调用具体的处理器(Handler/Controller)
- Controller执行完成返回ModelAndView对象
- HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet
- DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)
- ViewReslover解析后返回具体View(视图)
- DispatcherServlet根据View进行渲染(即将模型数据填充至视图中)
- DispatcherServlet响应用户
Springboot自动配置原理?
在Spring Boot项目中的引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:
- @SpringBootConfiguration(SpringBoot配置注解)
- @EnableAutoConfiguration(启用自动配置注解)
- @ComponentScan(组件扫描注解)
其中@EnableAutoConfiguration
是实现自动化配置的核心注解
该注解通过@Import
注解导入对应的配置选择器。关键的是内部就是读取了该项目和该项目引用的Jar包的的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名
在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中
一般条件判断会有像@ConditionalOnClass
这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用
Spring 的常见注解有哪些?
- 第一类是:声明bean,有@Component、@Service、@Repository、@Controller
- 第二类是:依赖注入相关的,有@Autowired、@Qualifier、@Resourse
- 第三类是:设置作用域 @Scope
- 第四类是:spring配置相关的,比如@Configuration,@ComponentScan 和 @Bean
- 第五类是:跟aop相关做增强的注解 @Aspect,@Before,@After,@Around,@Pointcut
SpringMVC常见的注解有哪些?
- @RequestMapping:用于映射请求路径
- @RequestBody:注解实现接收http请求的json数据,将json转换为java对象
- @RequestParam:指定请求参数的名称
- @PathViriable:从请求路径下中获取请求参数(/user/{id}),传递给方法形参
- @ResponseBody:注解实现将controller方法返回对象转化为json对象响应给客户端
- @RequestHeader:获取指定的请求头数据,还有像@PostMapping、@GetMapping这些
Springboot常见注解有哪些?
Spring Boot的核心注解是@SpringBootApplication , 他由几个注解组成 :
- @SpringBootConfiguration(SpringBoot配置注解): 组合了- @Configuration注解,实现配置文件的功能;
- @EnableAutoConfiguration(启用自动配置注解):打开自动配置的功能,也可以关闭某个自动配置的选项
- @ComponentScan(组件扫描注解):Spring组件扫描
MyBatis执行流程
- 读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件
- 构造会话工厂SqlSessionFactory,一个项目只需要一个,单例的,一般由spring进行管理
- 会话工厂创建SqlSession对象,这里面就含了执行SQL语句的所有方法
- 操作数据库的接口,Executor执行器,同时负责查询缓存的维护
- Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
- 输入参数映射
- 输出结果映射
Mybatis是否支持延迟加载?
支持
延迟加载的意思是:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据
Mybatis支持一对一关联对象和一对多关联集合对象的延迟加载
在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled = true | false,默认是关闭的
延迟加载的底层原理
延迟加载在底层主要使用的CGLIB动态代理完成的
-
第一是使用CGLIB创建目标对象的代理对象,这里的目标对象就是开启了延迟加载的mapper
-
第二个是当调用目标方法时,进入拦截器invoke方法,发现目标方法是null值,再执行sql查询
-
第三个是获取数据以后,调用set方法设置属性值,再继续查询目标方法,就有值了
Mybatis的一级、二级缓存了解吗?
mybatis的一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 SQLSession,当Session进行flush或close之后,该SQLSession中的所有Cache就将清空,默认打开一级缓存
关于二级缓存需要单独开启
二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQLSession,默认也是采用 PerpetualCache,HashMap 本地存储
如果想要开启二级缓存需要在全局配置文件和映射文件中开启配置才行
Mybatis的二级缓存什么时候会清理缓存中的数据?
当某一个作用域(一级缓存 Session/二级缓存Namespaces)进行了新增、修改、删除操作后,默认该作用域下所有 select 中的缓存将被 clear
微服务篇
Spring Cloud 组件有哪些?
早期我们一般认为的Spring Cloud五大组件是
- Eureka : 注册中心
- Ribbon : 负载均衡
- Feign : 远程调用
- Hystrix : 服务熔断
- Zuul/Gateway : 网关
随着SpringCloudAlibba在国内兴起 , 我们项目中使用了一些阿里巴巴的组件
-
注册中心/配置中心 Nacos
-
负载均衡 Ribbon
-
服务调用 Feign
-
服务保护 sentinel
-
服务网关 Gateway
服务注册和发现是什么意思?Spring Cloud 如何实现服务注册发现?
我理解的是主要三块大功能,分别是服务注册 、服务发现、服务状态监控
-
服务注册:服务注册是将服务实例的信息添加到Nacos注册中心,使其他服务能够发现并使用。
-
服务发现:服务发现是从Nacos注册中心获取可用的服务实例信息,以便服务消费者能够调用它们
-
服务监控:监控监视是Nacos对注册服务进行健康检查和状态监控,确保服务的高可用性和动态性
能说下nacos与eureka的区别吗?
共同点:
- Nacos与eureka都支持服务注册和服务拉取,都支持服务提供者心跳方式做健康检测
Nacos与Eureka的区别:
- Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
- 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
- Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
Nacos还支持配置中心,Eureka则只有注册中心
负载均衡如何实现的 ?
在服务调用过程中的负载均衡一般使用SpringCloud的Ribbon 组件实现 , Feign的底层已经自动集成了Ribbon , 使用起来非常简单
当发起远程调用时,ribbon先从注册中心拉取服务地址列表,然后按照一定的路由策略选择一个发起远程调用,一般的调用策略是轮询
Ribbon负载均衡策略有哪些 ?
-
RoundRobinRule:简单轮询服务列表来选择服务器
-
WeightedResponseTimeRule:按照权重来选择服务器,响应时间越长,权重越小
-
RandomRule:随机选择一个可用的服务器
-
ZoneAvoidanceRule:区域敏感策略,以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询(默认)
如果想自定义负载均衡策略如何实现 ?
提供了两种方式:
- 创建类实现IRule接口,可以指定负载均衡策略,这个是全局的,对所有的远程调用都起作用
- 在客户端的配置文件中,可以配置某一个服务调用的负载均衡策略,只是对配置的这个服务生效远程调用
什么是服务雪崩,怎么解决这个问题?
服务雪崩是指一个服务失败,导致整条链路的服务都失败的情形,一般我们在项目解决的话就是两种方案,第一个是服务降级,第二个是服务熔断,如果流量太大的话,可以考虑限流
服务降级: 服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃,一般在实际开发中与feign接口整合,编写降级逻辑
服务熔断: 默认关闭,需要手动打开,如果检测到 10 秒内请求的失败率超过 50%,就触发熔断机制。之后每隔 5 秒重新尝试请求微服务,如果微服务不能响应,继续走熔断机制。如果微服务可达,则关闭熔断机制,恢复正常请求
限流有哪些实现方式?
- nginx限流操作,nginx使用的漏桶算法来实现过滤,让请求以固定的速率处理请求,可以应对突发流量,我们控制的速率是按照ip进行限流,限制的流量是每秒20
- spring cloud gateway中支持局部过滤器RequestRateLimiter来做限流,使用的是令牌桶算法,可以根据ip或路径进行限流,可以设置每秒填充平均速率,和令牌桶总容量
限流常见的算法有哪些呢?
比较常见的限流算法有漏桶算法和令牌桶算法:
- 漏桶算法是把请求存入到桶中,以固定速率从桶中流出,可以让我们的服务做到绝对的平均,起到很好的限流效果
- 令牌桶算法在桶中存储的是令牌,按照一定的速率生成令牌,每个请求都要先申请令牌,申请到令牌以后才能正常请求,也可以起到很好的限流作用
- 它们的区别是,漏桶和令牌桶都可以处理突发流量,其中漏桶可以做到绝对的平滑,令牌桶有可能会产生突发大量请求的情况,一般nginx限流采用的漏桶,spring cloud gateway中可以支持令牌桶算法
什么是CAP理论?
CAP主要是在分布式项目下的一个理论。包含了三项,一致性、可用性、分区容错性
-
一致性(Consistency)是指更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致(强一致性),不能存在中间状态。
-
可用性(Availability) 是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。
-
分区容错性(Partition tolerance) 是指分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。
为什么分布式系统中无法同时保证一致性和可用性?
首先一个前提,对于分布式系统而言,分区容错性是一个最基本的要求,因此基本上我们在设计分布式系统的时候只能从一致性(C)和可用性(A)之间进行取舍。
如果保证了一致性(C):对于节点N1和N2,当往N1里写数据时,N2上的操作必须被暂停,只有当N1同步数据到N2时才能对N2进行读写请求,在N2被暂停操作期间客户端提交的请求会收到失败或超时。显然,这与可用性是相悖的。
如果保证了可用性(A):那就不能暂停N2的读写操作,但同时N1在写数据的话,这就违背了一致性的要求。
什么是BASE理论?
BASE是CAP理论中AP方案的延伸,核心思想是即使无法做到强一致性(StrongConsistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。它的思想包含三方面:
-
Basically Available(基本可用):基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用性,但不等于系统不可用。
-
Soft state(软状态):即是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
-
Eventually consistent(最终一致性):强调系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。其本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
分布式事务有哪些解决方案?
- seata的XA模式,CP,需要互相等待各个分支事务提交,可以保证强一致性,性能差
- seata的AT模式,AP,底层使用undo log实现,性能好
- seata的TCC模式,AP,西能较好,不过需要人工编码实现
- MQ模式实现分布式事务,在A服务写数据的时候,需要在同一个事物内发送消息到另外一个事物,异步,能最好
分布式服务的接口幂等性如何设计?
- 幂等:多次调用方法或者不会改变业务状态,可以
保证重复调用的结果和单词调用的结果一致
- 如果是新增数据,可以使用数据库的唯一索引
- 如果是新增或修改数据
- 分布式锁,性能较低
- 使用 token + redis 来实现,性能较好
- 第一次请求,生成一个唯一token存储redis,返回给前端
- 第二次请求,业务处理,携带之前的token,到redis进行验证,如果存在,可以执行业务,删除token;如果不存在,则直接返回,不处理业务
xxl-job篇
xxl-job路由策略有哪些?
xxl-job提供了很多的路由策略,比较常用的有:轮询、故障转移、分片广播、随机…
xxl-job任务执行失败怎么解决?
- 第一:路由策略选择故障转移,优先使用健康的实例来执行任务
- 第二,如果还有失败的,我们在创建任务时,可以设置重试次数
- 第三,如果还有失败的,就可以查看日志或者配置邮件告警来通知相关负责人解决
如果有大数据量的任务同时都需要执行,怎么解决?
部署多个实例,共同去执行这些批量的任务,其中任务的路由策略是分片广播
在任务执行的代码中可以获取分片总数和当前分片,按照取模的方式分摊到各个实例执行就可以了
消息中间件
RabbitMQ
RabbitMQ 如何保证消息不丢失?
我们要保证消息的不丢失,主要从三个层面考虑:
- 第一个是开启生产者确认机制,确保生产者的消息能到达队列,如果报错可以先记录到日志中,再去修复数据
- 第二个是开启持久化功能,确保消息未消费前在队列中不会丢失,其中的交换机、队列、和消息都要做持久化
- 第三个是开启消费者确认机制为auto,由spring确认消息处理成功后完成ack,当然也需要设置一定的重试次数,我们当时设置了3次,如果重试3次还没有收到消息,就将失败后的消息投递到异常交换机,交由人工处理
RabbitMQ消息的重复消费问题如何解决的?
- 每条消息设置一个唯一的标识id
- 幂等性方案:分布式锁、数据库
RabbitMQ中死信交换机知道吗?
当一个队列中的消息满足下列情况之一时,可以成为 死信(dead letter):
- 消费者使用
basic.reject
或basic.nack
声明消费失败,并且消息的requeue参数设置为false - 消息是一个过期消息,超时无人消费
- 要投递的队列消费堆积满了,最早的消息可能成为死信
如果该队列配置了 dead-letter-exchange
属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机为 死信交换机
(Dead Letter Exchange,简称DLX)
RabbitMQ延迟队列有了解过吗?
延迟队列插件实现延迟队列 DelayExchange
- 声明一个交换机,添加
delayed
属性为true
- 发送消息时,添加
x-delay
头,值为超时时间
如果有100万消息堆积在MQ , 如何解决 ?
解决消息堆积有三种思路:
- 增加更多消费者,提高消费速度
- 再消费者内开启线程池加快消息处理速度
- 扩大队列容积,提高堆积上限,采用惰性队列
- 在声明队列的时候可以设置属性
x-queue-mode
为lazy
,即为惰性队列 - 基于磁盘存储,消息上限高
- 性能比较稳定,但基于磁盘存储,受限于IO,时效性会降低
- 在声明队列的时候可以设置属性
RabbitMQ的高可用机制有了解过吗?
-
普通集群,或者叫标准集群(classic cluster),具备下列特征:
- 会在集群的各个节点间共享部分数据,包括:交换机、队列元信息不包含队列中的消息
- 当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回
- 队列所在节点宕机,队列中的消息就会丢失
-
镜像集群:本质是主从模式,具备下面的特征:
- 交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份。
- 创建队列的节点被称为该队列的主节点,备份到的其它节点叫做该队列的镜像节点。
- 一个队列的主节点可能是另一个队列的镜像节点
- 所有操作都是主节点完成,然后同步给镜像节点
- 主宕机后,镜像节点会替代成新的主
-
仲裁队列:仲裁队列是3.8版本以后才有的新功能,用来替代镜像队列,具备下列特征:
- 与镜像队列一样,都是主从模式,支持主从数据同步
- 使用非常简单,没有复杂的配置
- 主从同步基于Raft协议,强一致
Kafka
Kafka是如何保证消息不丢失?
需要从三个层面去解决这个问题:
- 生产者发送消息到
Brocker
丢失- 设置异步发送,发送失败使用回调进行记录或重发
- 失败重试,参数配置,可以设置重试次数
- 消息在
Brocker
中存储丢失- 发送确认
acks
,选择all
,让所有的副本都参与保存数据后确认
- 发送确认
- 消费者从
Brocker
接收消息丢失- 关闭自动提交偏移量,开启手动提交偏移量
- 提交方式,最好是同步 + 异步提交
Kafka中消息的重复消费问题如何解决的?
- 关闭自动提交偏移量,开启手动提交偏移量
- 提交凡是,最好是同步 + 异步提交
- 幂等性方案:分布式锁、数据库
Kafka是如何保证消费的顺序性?
问题原因: 一个 topic
的数据可能存储在不同的分区中,每个分区都有一个按照顺序的存储的偏移量,如果消费者关联了多个分区不能保证顺序性
解决方案:
- 发送消息时指定分区号
- 发送消息时按照相同的业务设置相同的
key
Kafka的高可用机制有了解过吗?
集群: 一个Kafka集群由多个 broker
实例组成,即使某一台宕机,也不耽误其它 broker
继续对外提供服务
复制机制:
- 一个
topic
有多个分区,每个分区有多个副本,有一个leader
,其余的是follower
,副本存储在不同的borker
中 - 所有的分区副本的内容都是相同的,如果
leader
发生故障时,会自动将某一个follower
提升为leader
,保证了系统的容错、高可用性
解释一下复制机制中的ISR
ISR(in-sync replica)需要同步复制保存的 follower
分区副本分为两类,一类是 ISR
,与 leader
副本同步保存数据,另外一个普通的副本,是异步同步数据,当 leader
挂掉之后,会优先从 ISR
副本列表中选取一个作为 leader
Kafka数据清理机制了解过吗?
Kafka存储结构:
- Kafka 中 topic 的数据存储在分区上,分区如果文件过大会分段存储 segment
- 每个分段都在磁盘上以索引(xxx.index)和日志文件(xxx.log)的形式存储
- 分段的好处是,第一能够减少单个文件内容的大小,查找数据方便,第二方便 kafka 进行日志清理
日志的清理策略有两个:
- 根据消息的保留时间,当消息保存的时间超过了指定的时间,就会触发清理,默认是168小时(7天)
- 根据 topic 存储的数据大小,当 topic 所占的日志文件大小大于一定的阈值,则开始删除最久的消息(默认关闭)
Kafka中实现高性能的设计有了解过吗?
Kafka 高性能,是多方面协同的结果,包括宏观架构、分布式存储、ISR 数据同步、以及高效的利用磁盘、操作系统特性等。主要体现有这么几点:
- 消息分区:不受单台服务器的限制,可以不受限的处理更多的数据
- 顺序读写:磁盘顺序读写,提升读写效率
- 页缓存:把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问
- 零拷贝:减少上下文切换及数据拷贝
- 消息压缩:减少磁盘IO和网络IO
- 分批发送:将消息打包批量发送,减少网络开销
常见集合篇
List相关面试题
为什么数组索引从0开始呢?假如从1开始不行吗?
- 在根据数组索引获取元素的时候,会用索引和寻址公式来计算内存所对应的元素数据,寻址公式是:数组的首地址 + 索引乘以存储数据的类型大小
- 如果数组的索引从1开始,寻址公式中,就需要增加一次减法操作,对于CPU来说就多了一次指令,性能不高
ArrayList底层的实现原理是什么?
- ArrayList底层是用动态的数组实现的
- ArrayList初试容量为0,当第一次添加数据的时候才会初始化容量为10
- ArrayList在进行扩容的时候是原来的1.5倍,每次扩容都需要拷贝数组
- ArrayList在添加数据的时候
- 确保数组已使用长度(size)+ 1 之后足够存下下一个数据
- 计算数组的容量,如果当前数组已使用长度 + 1 后大于当前的数组长度,则调用 grow 方法扩容(原来的1.5倍)
- 确保新增的数据有地方存储之后,则将新元素添加到位于 size 的位置上
- 返回添加成功/失败布尔值
ArrayList list = new ArrayList(10)中的list扩容几次?
该语句只是声明和实例了一个ArrayList,指定了容量为10,未扩容
如何实现数组和List之间的转换?
- 数组转List,使用 JDK 中
java.util.Arrays
工具类的asLIst方法 - List转数组,使用List的toArray方法。无参toArray方法返回Object数组,传入初始化长度的数组对象,返回该对象数组
Arrays.asList转List后,如果修改了数组内容,list受影响吗?
Arrays.asList转List后,如果修改了数组内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址
List用toArray转数组后,如果修改了List内容,数组受影响吗?
List用了toArray转数组后,如果修改了list内容,数组不会受影响,当调用了toArray以后,在底层是它进行了数组的拷贝,跟原来的元素就没有引用关系了,所以即使list修改了以后,数组也不受影响
ArrayList 和 LinkedList的区别是什么?
-
底层数据结构
-
ArrayList 是动态数组的数据结构实现
-
LinkedList 是双向链表的数据结构实现
-
-
操作数据效率
- ArrayList按照下标查询的时间复杂度O(1)(内存是连续的,根据寻址公式), LinkedList不支持下标查询
- 查找(未知索引): ArrayList需要遍历,链表也需要遍历,时间复杂度都是O(n)
- 新增和删除
- ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)
- LinkedList头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n)
-
内存空间占用
-
ArrayList底层是数组,内存连续,节省内存
-
LinkedList 是双向链表需要存储数据,和两个指针,更占用内存
-
-
线程安全
- ArrayList和LinkedList都不是线程安全的
- 如果需要保证线程安全,有两种方案:
- 在方法内使用,局部变量则是线程安全的
- 使用线程安全的ArrayList和LinkedList
HashMap相关面试题
说一下HashMap的实现原理?
HashMap的数据结构: 底层使用hash表数据结构,即数组和链表或红黑树
- 往 HashMap 中 put 元素时,利用 key 的 hashCode 重新 hash 计算出当前对象的元素在数组中的下标
- 存储时,如果出现 hash 值相同的 key,此时有两种情况
- 如果 key 相同,则覆盖原始值
- 如果 key 不同(出现冲突),则将当前的 key-value 放入链表或红黑树种
- 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值
HashMap的jdk1.7和jdk1.8有什么区别?
- JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可
- JDK1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8) 时并且数组长度达到64时,将链表转化为红黑树,以减少搜索时间。扩容 resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表
HashMap的put方法的具体流程
- 判断键值对数组是否为空或为null,否则执行resize()进行扩容(初始化)
- 根据键值key计算hash值得到数组索引
- 判断 table[i] == null,条件成立,直接新建节点添加
- 如果 table[i] == null,条件不成立:
- 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
- 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对
- 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话,判断数组长度是否大于64,如果数组长度也大于64的话把链表转换为红黑树,在红黑树中执行插入操作,遍历过程中若发现key已经存在直接覆盖value
- 插入成功后,判断实际存在的键值对数量size是否超过了最大容量 threshold (数组长度*0.75),如果超过了,进行扩容
讲一讲HashMap的扩容机制
-
在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75)
-
每次扩容的时候,都是扩容之前容量的2倍;
-
扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
- 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
- 如果是红黑树,走红黑树的添加
- 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
HashMap的寻址算法
- 计算对象的 hashCode()
- 再进行调用 hash() 方法进行二次哈希,hashCode值右移16为再异或运算,让哈希分布更为均匀
- 最后(capacity - 1) & hash 得到索引
为何HashMap的数组长度一定是2的次幂?
- 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模
- 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置 + oldCap
HashMap在1.7情况下的多线程死循环问题
在jdk1.7的hashmap中在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环
比如说,现在有两个线程
线程一:读取到当前的hashmap数据,数据中一个链表,在准备扩容时,线程二介入
线程二:也读取hashmap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来。比如原来的顺序是AB,扩容后的顺序是BA,线程二执行结束。
线程一:继续执行的时候就会出现死循环的问题。
线程一先将A移入新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,
所以B->A->B,形成循环。
当然,JDK 8 将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),尾插法,就避免了jdk7中死循环的问题
HashSet与HashMap的区别
-
HashSet实现了Set接口, 仅存储对象; HashMap实现了 Map接口, 存储的是键值对
-
HashSet底层其实是用HashMap实现存储的, HashSet封装了一系列HashMap的方法. 依靠HashMap来存储元素值,(利用hashMap的key键进行存储), 而value值默认为Object对象. 所以HashSet也不允许出现重复值, 判断标准和HashMap判断标准相同, 两个元素的hashCode相等并且通过equals()方法返回true
HashTable与HashMap的区别
主要区别:
区别 | HashTable | HashMap |
---|---|---|
数据结构 | 数组+链表 | 数组+链表+红黑树 |
是否可以为null | Key和value都不能为null | 可以为null |
hash算法 | key的hashCode() | 二次hash |
扩容方式 | 当前容量翻倍 +1 | 当前容量翻倍 |
线程安全 | 同步(synchronized)的,线程安全 | 非线程安全 |
在实际开中不建议使用HashTable,在多线程环境下可以使用ConcurrentHashMap类
并发编程篇
线程的基础知识
线程和进程的区别?
Java 中,线程作为最小调度单位,进程作为资源分配的最小单位
二者对比
- 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
并行和并发有什么区别?
现在都是多核CPU,在多核CPU下
- 并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU
- 并行是同一时间动手做多件事情的能力,4核CPU同时执行4个线程
创建线程的方式有哪些?
共有四种方式可以创建线程,分别是:
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口
- 线程池创建线程
runnable 和 callable 有什么区别?
- Runnable 接口run方法没有返回值;Callable接口call方法有返回值,是个泛型,和Future、FutureTask配合可以用来获取异步执行的结果
- Callalbe接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
- Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
线程的 run()和 start()有什么区别?
-
start(): 用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次
-
run(): 封装了要被线程执行的代码,可以被调用多次
线程包括哪些状态,状态之间是如何变化的?
新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、时间等待(TIMED_WALTING)、终止(TERMINATED)
- 创建线程对象是
新建状态
- 调用了 start() 方法转变为
可执行状态
- 线程获取到了CPU的执行权,执行结束是
终止状态
- 在可执行的过程中,如果没有获取CPU的执行权,可能会切换其他状态
- 如果没有获取锁(synchronized 或 lock)进入
阻塞状态
,获得锁再切换为可执行状态 - 如果线程调用了 wait() 方法进入
等待状态
,其它线程调用 notify() 唤醒后可切换为可执行状态 - 如果线程调用了 sleep(50) 方法,进入
计时等待状态
,到时间后可切换为可执行状态
- 如果没有获取锁(synchronized 或 lock)进入
新建 T1、T2、T3 三个线程,如何保证它们按顺序执行?
在多线程中有多种方法让线程按特定顺序执行,可以用线程类的 join
()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行
notify()和 notifyAll()有什么区别?
-
notifyAll:唤醒所有wait的线程
-
notify:只随机唤醒一个 wait 线程
在 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)的线程,线程会抛出InterruptedExcepting异常
- 打断正常的线程,可以根据打断状态来标记是否退出线程
线程中并发锁
synchronized关键字的底层原理
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
描述 | |
---|---|
重量级锁 | 底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。 |
轻量级锁 | 线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性 |
偏向锁 | 一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令 |
什么是JMM(Java内存模型)?
- JMM(Java Memory Model)Java内存模型,定义了
共享内存
中多线程程序读写操作
的行为规范,通过这些规则来规范对内存的读写操作从而保证了指令的正确性 - JMM 把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
- 线程跟线程之间相互隔离,线程跟线程交互需要通过主内存
CAS 了解吗?
- CAS 的全称是:Compare And Swap(比较并交换),它体现的一种乐观锁的思想,在无锁状态下保证线程操作数据的原子性
- CAS 使用到的地方很多:AQS框架、AtomicXXX原子类
- 在操作共享变量的时候使用的自旋锁,效率上更高一些
- CAS 的底层是调用的 Unsafe 类中的方法,都是操作系统提供的,其它语言实现
乐观锁和悲观锁的区别?
- CAS 是基于乐观锁的思想:最乐观的设计,不怕别的线程来修改共享变量,就算改了也没关系,再重试下
- synchronized 是基于悲观锁的思想:最悲观的估计,得防着其它线程来修改共享变量,当前线程上了锁别的线程都别想改,等当前线程改完了释放锁,其它线程才有机会
volatile 有什么用?
- 保证线程间的可见性: 用 volatile 修饰共享变量,能够防止编译器的优化发生,让一个线程对共享变量的修改对另一个线程可见
- 禁止进行指令重排序: 用 volatile 修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其它读写操作越过屏障,从而达到阻止重排序的效果
什么是AQS?
- AQS(抽象同步队列):是多线程中的队列同步器。是一种锁机制,它是做为一个
基础框架
使用的,像 ReentrantLock、Semaphore都是基于AQS实现的 - AQS内部维护了一个先进先出的双向队列,队列中存储排队的线程
- 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无锁状态),如果队列中有一个线程修改成功了state为1,则当前线程就相当于获取了资源
- 在对state修改的时候使用了CAS操作,保证了多个线程修改情况下的原子性
ReentrantLock的实现原理
- ReentrantLock 表示支持重入锁,调用lock方法获取了锁之后,再次调用 lock,是不会再阻塞
- ReentrantLock 主要利用
CAS + AQS
队列来实现 - 支持公平锁和非公平锁,在提供的构造器的无参默认是非公平锁,也可以传参设置为公平锁
synchronized和Lock有什么区别?
- 语法层面
- synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现
- Lock 是接口,源码由 jdk 提供,用 java 语言实现
- 使用 synchronized 时,退出同步代码块锁会自动释放,而使用 Lock 时,需要手动调用 unlock 方法释放锁
- 功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
- Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
- Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock
- 性能层面
- 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
- 在竞争激烈时,Lock 的实现通常会提供更好的性能
死锁产生的条件是什么?
- 互斥条件(Mutual Exclusion):资源不能被多个进程同时访问
- 保持和等待条件(Hold and Wait):一个进程至少持有一个资源,并等待获取一个当前被其他进程持有的资源
- 不可剥夺条件(No Preemption):资源只能由持有它的进程释放,不能被其他进程剥夺。
- 循环等待条件(Circular Wait):存在一个等待资源的循环,即进程集合中的一组进程 {P1, P2, …, Pn},P1 等待 P2 释放的资源,P2 等待 P3 释放的资源,…,Pn 等待 P1 释放的资源
如何进行死锁诊断?
- 当程序出现了死锁现象,我们可以使用JDK自带的工具:jps 和 jstack
- jps:输出JVM中运行的进程状态信息
- jstack:查看java进程内线程的堆栈信息,查看日志,检查是否有死锁,如若有死锁现象,需要查看具体代码分析后,可修复
- 可视化工具jconsole、VisualVM也可以检查死锁问题
说一下ConcurrentHashMap
- 底层数据结构:
- JDK1.7底层采用 分段的数组 + 链表 实现
- JDK1.8采用 数组 + 链表/红黑树 实现
- 加锁的方式:
- JDK1.7采用Segment分段锁,底层使用的是ReentrantLockLock
- JDK1.8采用CAS添加新节点,采用synchronized锁定链表/红黑树的首节点,相对Segment分段锁粒度更细,性能更好
Java程序中怎么保证多线程的执行安全?
- 原子性:一个线程在CPU中操作不可暂停,也不可中断,要不执行完成,要不不执行
- 内存可见性:让一个线程对共享变量的修改对另一个线程可见
- 有序性:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致
线程池
说一下线程池的核心参数
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数,最大线程数目 = (核心线程 + 救急线程的最大数目)
- keepAliveTime:生存时间,救急线程的生存时间,生存时间内没有新任务,此线程资源会释放
- unit:时间单位,救急线程的生存时间单位,如秒、毫秒等
- workQueue:阻塞队列,当没有空闲核心时,新来任务会加入到此队列排队,队列满了会创建救急线程执行任务
- threadFactory:线程工厂,可以指定线程对象的创建,例如设置线程名字、是否是守护线程等
- handler:拒绝策略,当所有线程都在繁忙,workQueue 也放满时,会触发拒绝策略
- AbortPolicy:直接抛出异常,默认策略
- CallerRunsPolicy:用调用者所在的线程来执行任务
- DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务
- DiscardPolicy:直接丢弃任务
线程池中有哪些常见的阻塞队列?
workQueue:当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
ArrayBLockingQueue:基于数组结构的有界阻塞队列,FIFO
LinkedBlockingQueue:基于链表结构的有界阻塞队列,FIFO
- DelayedWorkQueue:是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的
ArrayBlockingQueue的LinkedBlockingQueue区别
LinkedBlockingQueue | ArrayBlockingQueue |
---|---|
默认无界,支持有界 | 强制有界 |
底层是链表 | 底层是数组 |
是懒惰的,创建节点的时候添加数据 | 提前初始化 Node 数组 |
入队会生成新 Node | Node需要是提前创建好的 |
两把锁(头尾) | 一把锁 |
- LinkedBlockingQueue读和写各有一把锁,性能相对较好
- ArrayBlockingQueue只有一把锁,读和写公用,性能相对于LinkedBlockingQueue差一些
如何确定核心线程数?
- IO密集型任务:文件读写、DB读写、网络请求等,
核心线程数大小设置为 2N+1
- CPU密集型任务:计算型代码、BitMap转换、Gson转换等,
核心线程数大小设置为 N+1
线程池的种类有哪些?
在java.util.concurrent.Executors类中提供了大量创建连接池的静态方法,常见就有四种
- 创建使用固定线程数的线程池
- 核心线程数与最大线程数一样,没有救急线程
- 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
- 单线程化的线程池,它只会用唯一的工作线程来执行任 务,保证所有任务按照指定顺序(FIFO)执行
- 核心线程数和最大线程数都是1
- 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
- 可缓存线程池
- 核心线程数为0
- 最大线程数是Integer.MAX_VALUE
- 阻塞队列为SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作
- 提供了 “延迟” 和 “周期执行” 功能的线程池
为什么不建议用Executors创建线程池?
线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors 返回的线程池对象的弊端如下:
- FixedThreadPoo l和 SingleThreadPool :允许的请求队列长度为 Integer.MAX VALUE,可能会堆积大量的请求,从而导致 OOM
- CachedThreadPool:允许的创建线程数量为Integer.MAX VALUE,可能会创建大量的线程,从而导致 OOM
CountDownLatch(倒计时锁)
CountDownLatch(倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)
- 其中构造参数用来初始化等待计数值
- await() 用来等待计数归零
- countDown() 用来让计数减一
如何控制某个方法允许并发访问线程的数量?
在多线程中提供了一个工具类Semaphore,信号量。在并发的情况下,可以控制方法的访问量
- 创建Semaphore对象,可以给一个容量
- acquire() 可以请求一个信号量,这时候的信号量个数 -1
- release()释放一个信号量,此时信号量个数 +1
谈谈你对ThreadLocal的理解
- ThreadLocal 可以实现 资源对象 的线程隔离,让每个线程各用各的 资源对象,避免争用引发的线程安全问题
- ThreadLocal 同时实现了线程内的资源共享
- 每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象
- 调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
- 调用 get 方法,就是以 ThreadLocal 自己作为 key,当前线程中查找关联的资源值
- 调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
ThreadLocal内存泄漏问题
- ThreadLocalMap 中的 key 是弱引用,值为强引用
- key 会被GC释放内存,关联 value 的内存并不会释放
- 建议主动 remove 释放 key,value
JVM虚拟机篇
JVM由那些部分组成?
- 类加载器
- Java堆
- 方法区
- 虚拟机栈
- 本地方法栈
- 程序计数器
- 执行引擎
什么是程序计数器?
线程私有的,每个线程一份,内部保存字节码的行号。用于记录正在执行的字节码指令地址
什么是Java堆?
线程共享的区域:
主要用来保存对象实例,数组
等,内存不够则抛出OutOfMemoryError
异常- 组成:
新生代+ 老年代
新生代
被划分为三部分,伊甸区和两个大小严格相同的幸存者区老年代
主要保存生命周期长的对象,一般是一些老的对象
- JDK 1.7 和 1.8 的区别:
- 1.7 中有一个永久代,存储的是类信息、静态变量、常量、编译后的代码
- 1.8 移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出
什么是虚拟机栈?
- 每个线程运行时所需要的内存,称为虚拟机栈
- 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存
- 每个线程只能有一个活动栈帧,对应着当前执行的那个方法
垃圾回收是否涉及栈内存?
垃圾回收主要是回收堆内存,当栈帧弹栈以后,内存就会释放
栈内存分配越大越好吗?
未必,默认的栈内存统称为 1024K,栈内存过大会导致线程数变少
方法内的局部变量是否线程安全?
- 如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
- 如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全
什么情况下会导致栈内存溢出?
- 栈帧过多导致栈内存溢出,典型问题:递归调用
- 栈帧过大导致栈内存溢出
堆栈的区别是什么?
- 栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的。堆会被GC垃圾回收,而栈不会
- 栈内存是线程私有的,而堆内存是线程共有的
- 两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常
- 栈空间不足:java.lang.StackOverFlowError
- 堆空间不足:java.lang.OutOfMemoryError
什么是方法区?
- 方法区(Method Area)是各个线程
共享的内存区域
- 主要存储类的信息,运行时常量池
- 虚拟机启动的时候创建,关闭虚拟机时释放
- 如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError: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 ]
- 对于新生代,内部又被分为了三个区域。伊甸区,幸存者区(分为 from 和 to)
- 对象分代回收策略
- 新创建的对象,都会先分配到伊甸区
- 当伊甸区内存不足,标记伊甸区与 from(现阶段没有)的存活对象
- 将存活对象采用复制算法复制到 to 中,复制完毕后,伊甸区 和 from 内存得到释放
- 经过一段时间后伊甸区的内存又出现不足,标记伊甸区 to 区存活的对象,将其复制到 from 区
- 当幸存区对象熬过几次回收(最多15次),晋升到老年代(幸存区内存不足或大对象会提前晋升)
MinorGC、MixedGC、FullGC的区别是什么?
- MinorGC(young GC):发生在新生代的垃圾回收,暂停时间短(STW)
- MixedGC:新生代 + 老年代 部分区域的垃圾回收,G1收集器持有
- FullGC: 新生代 + 老年代 完整垃圾回收,暂停时间长(STW),应尽力避免
说一下JVM有哪些垃圾回收器?
- 串行垃圾回收器:Serial GC、Serial Old GC
- 并行垃圾回收器:Parallel Old GC、ParNew GC
- CMS(并发)垃圾回收器:CMS GC,作用在老年代
- G1垃圾回收器:作用在新生代和老年代
G1垃圾回收器知道吗?
- 应用于新生代和老年代
- 划分成多个区域,每个区域都可以充当 eden,survivor,humongous,其中 humongous 专为大对象准备
- 采用复制算法
- 响应时间与吞吐量兼顾
- 分成三个阶段:新生代回收(STW),并发标记(重新标记STW),混合回收
- 如果并发失败(即回收速度赶不上创建新对象的速度),会触发 Full GC
强引用、软引用、弱引用、虚引用的区别?
- 强引用:只有所有 GC Roots 对象都不通过强引用引用该对象,该对象才能被垃圾回收
- 软引用:仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收
- 弱引用:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
- 虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
JVM 调优的参数可以在哪里设置参数值?
- war 包部署在 tomcat 中设置: 修改
TOMCAT_HOME/bin/catalina.sh
文件 - jar 包部署在启动参数设置: java
-Xms512m -Xmx1024m
-jar xxx.jar
JVM 调优的参数都有哪些?
- 设置堆空间大小
- 虚拟机栈的设置
- 设置垃圾回收器
- 年轻代中伊甸区和两个幸存者区的大小比例
- 年轻代晋升老年代的阈值
JVM 调优工具有哪些?
命令工具:
- jps:进程状态信息
- jstack:查看 java 进程内线程的堆栈信息
- jmap:查看堆信息
- jhat:堆转储快照分析工具
- jstat:JVM 统计监测工具
可视化工具:
- jconsole:用于对 jvm 的内存、线程、类 的监控
- VisualVM:能够监控线程,内存情况
Java 内存泄漏怎么排查?
内存泄漏通常是指堆内存,通常是指一些大对象不被回收的情况
- 通过 jmap 或设置 jvm 参数获取堆内存快照 dump
- 通过工具,VisualVM 去分析 dump 文件,VisualVM 可以加载离线的 dump 文件
- 通过查看堆信息的情况,可以大概定位内存溢出是哪行代码储了问题
- 找到对应的代码,通过阅读上下文的情况,进行修复即可
CPU飙高怎么排查?
- 使用 top 命令查看 CPU 占用情况
- 通过 top 命令查看后,可以查看是哪一个进程 CPU 占用较高
- 使用 ps 命令查看进程中的线程信息
- 使用 jstack 命令查看进程中哪些线程出现了问题,最终定位问题