1、Spring AOP 底层原理
aop 底层是采用动态代理机制实现的:接口+实现类
如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用
JDK Proxy
,去创建代
理对象。
没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会
使用
Cglib
生成一个被代理对象的子类来作为代理。
就是由代理创建出一个和 impl 实现类平级的一个对象,但是这个对象不是一个真正的对象,
只是一个代理对象,但它可以实现和 impl 相同的功能,这个就是 aop 的横向机制原理,这
样就不需要修改源代码。
2、HashMap 的底层数据结构是怎样的 ?
JDK1.8 之前
JDK1.8 之前 HashMap 底层是
数组和链表
结合在一起使用也就是
链表散列
。
HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n -
1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在
元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,
直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了
防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。
JDK1.8 之后
当链表长度大于阈值(默认为 8)时,会首先调用 treeifyBin()方法。这个方法会根据
HashMap 数组来决定是否转换为红黑树。只有当数组长度大于或者等于 64 的情况下,才会
执行转换红黑树操作,以减少搜索时间。否则,就是只是执行 resize() 方法对数组扩容。
3、HashMap 的扩容机制是怎样的?
一般情况下,当元素数量超过阈值时便会触发扩容。每次扩容的容量都是之前容量的 2 倍。
HashMap 的
容量
是有上限的,必须小于 1<<30,即 1073741824。如果容量超出了这个
数,则不再增长,且
阈值
会被设置为 Integer.MAX_VALUE。
JDK7 中的扩容机制
空参数的构造函数:以默认容量、默认负载因子、默认阈值初始化数组。内部数组是空数
组。
有参构造函数:根据参数确定容量、负载因子、阈值等。
第一次 put 时会初始化数组,其容量变为不小于指定容量的 2 的幂数,然后根据负载因
子确定阈值。
如果不是第一次扩容,则
新容量=旧容量 x 2 ,新阈值=新容量 x 负载因子
。
JDK8 的扩容机制
空参数的构造函数:实例化的 HashMap 默认内部数组是 null,即没有实例化。第一次
调用 put 方法时,则会开始第一次初始化扩容,长度为 16。
有参构造函数:用于指定容量。会根据指定的正整数找到不小于指定容量的 2 的幂数,
将这个数设置赋值给阈值(threshold)。第一次调用 put 方法时,会将阈值赋值给容量,
然后让
阈值 = 容量 x 负载因子
。
如果不是第一次扩容,则容量变为原来的 2 倍,阈值也变为原来的 2 倍。(容量和阈值
都变为原来的 2 倍时,负载因子还是不变)。
此外还有几个细节需要注意:
首次 put 时,先会触发扩容(算是初始化),然后存入数据,然后判断是否需要扩容;
不是首次 put,则不再初始化,直接存入数据,然后判断是否需要扩容;
4、ConcurrentHashMap 的存储结构是怎样的?
Java7 中 ConcurrnetHashMap 使用的分段锁,也就是每一个 Segment 上同时只有一
个线程可以操作,每一个 Segment 都是一个类似 HashMap 数组的结构,它可以扩容,
它的冲突会转化为链表。但是 Segment 的个数一但初始化就不能改变,默认 Segment
的个数是 16 个。
Java8 中的 ConcurrnetHashMap 使用的 Synchronized 锁加 CAS 的机制。结构也由
Java7 中的
Segment 数组 + HashEntry 数组 + 链表
进化成了
Node 数组 + 链表 / 红
黑树
,Node 是类似于一个 HashEntry 的结构。它的冲突再达到一定大小时会转化成红
黑树,在冲突小于一定数量时又退回链表。
5、线程池大小如何设置?
CPU 密集型任务(N+1):
这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N
(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,
或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,
而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。
I/O 密集型任务(2N):
这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线
程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使
用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。
如何判断是 CPU 密集任务还是 IO 密集任务?
CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。
单凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间
相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
6、IO 密集=Ncpu*2 是怎么计算出来?
I/O 密集型任务
任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理
I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在
I/O 密集型任务的应用中,我们可以多配置一些线程。例如:数据库交互,文件上传下载,
网络传输等。IO 密集型,即该任务需要大量的 IO,即大量的阻塞,故需要多配置线程数
7、G1 收集器有哪些特点?
G1 的全称是 Garbage-First,意为垃圾优先,哪一块的垃圾最多就优先清理它。
G1 GC 最主要的设计目标是:将 STW 停顿的时间和分布,变成可预期且可配置的。
被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点:
并行与并发
:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者
CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线
程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
分代收集
:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留
了分代的概念。
空间整合
:与 CMS 的“标记-清理”算法不同,G1 从整体来看是基于“标记-整理”算
法实现的收集器;从局部上来看是基于“标记-复制”算法实现的。
可预测的停顿
:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共
同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明
确指定在一个长度为 M 毫秒的时间片段内。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的
Region(这也就是它的名字 Garbage-First 的由来)
8、你有哪些手段来排查 OOM 的问题?
增加两个参数 -XX:+HeapDumpOnOutOfMemoryError -
XX:HeapDumpPath=/tmp/heapdump.hprof,当 OOM 发生时自动 dump 堆内存信息
到指定目录。
同时 jstat 查看监控 JVM 的内存和 GC 情况,先观察问题大概出在什么区域。
使用 MAT 工具载入到 dump 文件,分析大对象的占用情况,比如 HashMap 做缓存未
清理,时间长了就会内存溢出,可以把改为弱引用。
9、请你谈谈 MySQL 事务隔离级别,MySQL 的默认隔离级别是什么?
为了达到事务的四大特性,数据库定义了 4 种不同的事务隔离级别:
READ-UNCOMMITTED(读取未提交):最低的隔离级别,允许脏读,也就是可能读取
到其他会话中未提交事务修改的数据,
可能会导致脏读、幻读或不可重复读
。
READ-COMMITTED(读取已提交): 只能读取到已经提交的数据。Oracle 等多数数
据库默认都是该级别 (不重复读),
可以阻止脏读,但是幻读或不可重复读仍有可能发
生
。
REPEATABLE-READ(可重复读):对同一字段的多次读取结果都是一致的,除非数据
是被本身事务自己所修改,
可以阻止脏读和不可重复读,但幻读仍有可能发生
。
SERIALIZABLE(可串行化):最高的隔离级别,完全服从 ACID 的隔离级别。所有的
事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,
该级别可以防止脏
读、不可重复读以及幻读
。
MySQL 默认采用的 REPEATABLE_READ 隔离级别。
10、可重复读解决了哪些问题?
可重复读的核心就是一致性读(consistent read);保证多次读取同一个数据时,其值都和事
务开始时候的内容是一致,禁止读取到别的事务未提交的数据,会造成幻读。
而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就
需要进入锁等待。
查询只承认在事务启动前就已经提交完成的数据。
可重复读解决的是重复读的问题,可重复读在快照读的情况下是不会有幻读,但当前读的
时候会有幻读。
11、对 SQL 慢查询会考虑哪些优化 ?
分析语句,是否加载了不必要的字段/数据。
分析 SQL 执行计划(explain extended),思考可能的优化点,是否命中索引等。
查看 SQL 涉及的表结构和索引信息。
如果 SQL 很复杂,优化 SQL 结构。
按照可能的优化点执行表结构变更、增加索引、SQL 改写等操作。
查看优化后的执行时间和执行计划。
如果表数据量太大,考虑分表。
利用缓存,减少查询次数。
12、谈一谈缓存穿透、缓存击穿和缓存雪崩,以及解决办法?
缓存穿透
问题:大量并发查询不存在的 KEY,在缓存和数据库中都不存在,同时给缓存和数据库
带来压力。
原因:一般而言,缓存穿透有 2 种可能性:业务数据被误删,导致缓存和数据库中都没
有数据。恶意进行 ddos 攻击。
分析:为什么会多次透传呢?不存在 一直为空,需要注意让缓存能够区分 KEY 不存在和
查询到一个空值。
解决办法:缓存空值的 KEY,这样第一次不存在也会被加载会记录,下次拿到有这个
KEY。Bloom 过滤或 RoaingBitmap 判断 KEY 是否存在,如果布隆过滤器中没有查到
这个数据,就不去数据库中查。在处理请求前增加恶意请求检查,如果检测到是恶意攻击,
则拒绝进行服务。完全以缓存为准,使用延迟异步加载的策略(异步线程负责维护缓存的
数据,定期或根据条件触发更新),这样就不会触发更新。
缓存击穿
问题:某个 KEY 失效的时候,正好有大量并发请求访问这个 KEY。
分析:跟穿透其实很像,属于比较偶然的。
解决办法:KEY 的更新操作添加全局互斥锁。完全以缓存为准,使用延迟异步加载的策
略(异步线程负责维护缓存的数据,定期或根据条件触发更新),这样就不会触发更新。
缓存雪崩
问题:当某一时刻发生大规模的缓存失效的情况,导致大量的请求无法获取数据,从而将
流量压力传导到数据库上,导致数据库压力过大甚至宕机。
原因:一般而言,缓存雪崩有 2 种可能性:大量的数据同一个时间失效:比如业务关系
强相关的数据要求同时失效 Redis 宕机
分析:一般来说,由于更新策略、或者数据热点、缓存服务宕机等原因,可能会导致缓存
数据同一个时间点大规模不可用,或者都更新。所以,需要我们的更新策略要在时间上合
适,数据要均匀分享,缓存服务器要多台高可用。
解决办法:更新策略在时间上做到比较平均。如果数据需要同一时间失效,可以给这批数
据加上一些随机值,使得这批数据不要在同一个时间过期,降低数据库的压力。使用的热
数据尽量分散到不同的机器上。多台机器做主从复制或者多副本,实现高可用。做好主从
的部署,当主节点挂掉后,能快速的使用从结点顶上。实现熔断限流机制,对系统进行负
载能力控制。对于非核心功能的业务,拒绝其请求,只允许核心功能业务访问数据库获取
数据。服务降价:提供默认返回值,或简单的提示信息。
13、LRU 是什么?如何实现?
最近最少使用策略 LRU(Least Recently Used)是一种缓存淘汰算法,是一种缓存淘汰机
制。
使用双向链表实现的队列,队列的最大容量为缓存的大小。在使用过程中,把最近使用的
页面移动到队列头,最近没有使用的页面将被放在队列尾的位置
使用一个哈希表,把页号作为键,把缓存在队列中的节点的地址作为值,只需要把这个页
对应的节点移动到队列的前面,如果需要的页面在内存中,此时需要把这个页面加载到内
存中,简单的说,就是将一个新节点添加到队列前面,并在哈希表中跟新相应的节点地址,
如果队列是满的,那么就从队尾移除一个节点,并将新节点添加到队列的前面。
14、什么是堆内存?参数如何设置?
堆内存是指由程序代码自由分配的内存,与栈内存作区分。
在 Java 中,堆内存主要用于分配对象的存储空间,只要拿到对象引用,所有线程都可
以访问堆内存。
-Xmx, 指定最大堆内存。 如 -Xmx4g. 这只是限制了 Heap 部分的最大值为 4g。这个内
存不包括栈内存,也不包括堆外使用的内存。
-Xms, 指定堆内存空间的初始大小。 如 -Xms4g。 而且指定的内存大小,并不是操作系
统实际分配的初始值,而是 GC 先规划好,用到才分配。 专用服务器上需要保持 –Xms
和 –Xmx 一致,否则应用刚启动可能就有好几个 FullGC。当两者配置不一致时,堆内存
扩容可能会导致性能抖动。
-Xmn, 等价于 -XX:NewSize,使用 G1 垃圾收集器 不应该 设置该选项,在其他的某些
业务场景下可以设置。官方建议设置为 -Xmx 的 1/2 ~ 1/4.
-XX:MaxPermSize=size, 这是 JDK1.7 之前使用的。Java8 默认允许的 Meta 空间无
限大,此参数无效。
-XX:MaxMetaspaceSize=size, Java8 默认不限制 Meta 空间, 一般不允许设置该选项。
-XX:MaxDirectMemorySize=size,系统可以使用的最大堆外内存,这个参数跟 -
Dsun.nio.MaxDirectMemorySize 效果相同。
-Xss, 设置每个线程栈的字节数。 例如 -Xss1m 指定线程栈为 1MB,与-
XX:ThreadStackSize=1m 等价
15、栈和队列,举个使用场景例子?
栈(
后进先出
)可以用于字符匹配,数据反转等场景
队列(
先进先出
)可以用于任务队列,共享打印机等场景
16、MySQL 为什么 InnoDB 是默认引擎?
聚集索引是指数据库表行中数据的物理顺序与键值的逻辑(索引)顺序相同。一个表只能有一
个聚簇索引,因为一个表的物理顺序只有一种情况,所以,对应的聚簇索引只能有一个。聚簇
索引的叶子节点就是数据节点,既存储索引值,又在叶子节点存储行数据。
Innodb 创建表后生成的文件有:
frm:创建表的语句
idb:表里面的数据+索引文件
17、MySQL 索引底层结构为什么使用 B+树?
哈希虽然能够提供 O(1) 的单数据行操作性能,但是对于范围查询和排序却无法很好地支
持,最终导致全表扫描;B 树能够在非叶节子点中存储数据,但是这也导致在查询连续数
据时可能会带来更多的随机 I/O,而 B+树的所有叶节点可以通过指针相互连接,能够减
少顺序遍历时产生的额外随机 I/O;
第一,B 树一个节点里存的是数据,而 B+树存储的是索引(地址),所以 B 树里一个节
点存不了很多个数据,但是 B+树一个节点能存很多索引,B+树叶子节点存所有的数据。
第二,B+树的叶子节点是数据阶段用了一个链表串联起来,便于范围查找。
18、B+ 树的叶子节点链表是单向还是双向?
双向链表
19、MVCC 是什么?它的底层原理是什么?
MVCC,多版本并发控制,它是通过读取历史版本的数据,来降低并发事务冲突,从而提高并
发性能的一种机制。
事务版本号
表的隐藏列
undo log
read view
20、undo log 具体怎么回滚事务 ?
举个例子:
对于 insert 类型的 sql,会在 undo log 中记录下方才你 insert 进来的数据的 ID,当你
想 roll back 时,根据 ID 完成精准的删除。
对于 delete 类型的 sql,会在 undo log 中记录方才你删除的数据,当你回滚时会将删
除前的数据 insert 进去。
对于 update 类型的 sql,会在 undo log 中记录下修改前的数据,回滚时只需要反向
update 即可。
对于 select 类型的 sql,别费心了,select 不需要回滚。
21、如何查询慢 SQL 产生的原因
分析 SQL 执行计划(explain extended),思考可能的优化点,是否命中索引等。
没有索引或者没有用到索引(这是查询慢最常见的问题,是程序设计的缺陷)。
内存不足。
网络速度慢。
是否查询出的数据量过大(可以采用多次查询,其他的方法降低数据量)。
是否返回了不必要的行和列。
锁或者死锁。
I/O 吞吐量小,形成了瓶颈效应。
sp_lock,sp_who,活动的用户查看,原因是读写竞争资源。
22、索引失效的情况有哪些?
like 以%开头索引无效,当 like 以&结尾,索引有效。
or 语句前后没有同事使用索引,当且仅当 or 语句查询条件的前后列均为索引时,索引
生效。
组合索引,使用的不是第一列索引时候,索引失效,即最左匹配规则。
数据类型出现隐式转换,如 varchar 不加单引号的时候可能会自动转换为 int 类型,这
个时候索引失效。
在索引列上使用 IS NULL 或者 IS NOT NULL 时候,索引失效,因为索引是不索引空值
得。
在索引字段上使用,NOT、 <>、!= 、时候是不会使用索引的,对于这样的处理只会进
行全表扫描。
对索引字段进行计算操作,函数操作时不会使用索引。
当全表扫描速度比索引速度快的时候不会使用索引。
23、一个 Redis 实例最多能存放多少的 keys?List、Set、Sorted Set 他们最
多能存放多少元素?
理论上 Redis 可以处理多达 232 的 keys,并且在实际中进行了测试,每个实例至少存放
了 2 亿 5 千万的 keys。我们正在测试一些较大的值。任何 list、set、和 sorted set 都可
以放 232 个元素。换句话说,Redis 的存储极限是系统中的可用内存值。
24、Redis 数据结构 压缩列表和跳跃表的区别
压缩列表(ziplist)本质上就是一个字节数组,是 Redis 为了节约内存而设计的一种线性
数据结构,可以包含多个元素,每个元素可以是一个字节数组或一个整数。
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指
针,从而达到快速访问节点的目的。跳跃表支持平均 O(logN)、最坏 O(N)复杂度
的节点查找,还可以通过顺序性操作来批量处理节点。
25、为什么数据量小的时候用压缩列表 ?
为了省内存。
26、Redis 主从同步是怎么实现的?
全量同步
master 服务器会开启一个后台进程用于将 redis 中的数据生成一个 rdb 文件,与此同时,
服务器会缓存所有接收到的来自客户端的写命令(包含增、删、改),当后台保存进程处理完
毕后,会将该 rdb 文件传递给 slave 服务器,而 slave 服务器会将 rdb 文件保存在磁盘并
通过读取该文件将数据加载到内存,在此之后 master 服务器会将在此期间缓存的
命令通过 redis 传输协议发送给 slave 服务器,然后 slave 服务器将这些命令依次作用于
自己本地的数据集上最终达到数据的一致性。
增量同步
从 redis 2.8 版本以前,并不支持部分同步,当主从服务器之间的连接断掉之后,master 服
务器和 slave 服务器之间都是进行全量数据同步。
从 redis 2.8 开始,即使主从连接中途断掉,也不需要进行全量同步,因为从这个版本开始
融入了部分同步的概念。部分同步的实现依赖于在 master 服务器内存中给每个 slave 服务
器维护了一份同步日志和同步标识,每个 slave 服务器在跟 master 服务器进行同步时都会
携带自己的同步标识和上次同步的最后位置。当主从连接断掉之后,slave 服务器隔断时间
(默认 1s)主动尝试和 master 服务器进行连接,如果从服务器携带的偏移量标识还在
master 服务器上的同步备份日志中,那么就从 slave 发送的偏移量开始继续上次的同步操
作,如果 slave 发送的偏移量已经不再 master 的同步备份日志中(可能由于主从之间断掉
的时间比较长或者在断掉的短暂时间内 master 服务器接收到大量的写操作),则必须进行
一次全量更新。在部分同步过程中,master 会将本地记录的同步备份日志中记录的指令依次
发送给 slave 服务器从而达到数据一致。
Redis 主从同步策略
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,
slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,
如不成功,要求从机进行全量同步。
27、Redis 持久化 RDB 和 AOF 优缺点
RDB
RDB 持久化方式,是将 Redis 某一时刻的数据持久化到磁盘中,是一种快照式的持久化方
法。
RDB 优点:
RDB 是一个非常紧凑(有压缩)的文件,它保存了某个时间点的数据,非常适用于数据的备
份。
RDB 作为一个非常紧凑(有压缩)的文件,可以很方便传送到另一个远端数据中心 ,非
常适用于灾难恢复。
RDB 在保存 RDB 文件时父进程唯一需要做的就是 fork 出一个子进程,接下来的工作全
部由子进程来做,父进程不需要再做其他 IO 操作,所以 RDB 持久化方式可以最大化
redis 的性能。
与 AOF 相比,在恢复大的数据集的时候,RDB 方式会更快一些。
RDB 缺点:
Redis 意外宕机时,会丢失部分数据。
当 Redis 数据量比较大时,fork 的过程是非常耗时的,fork 子进程时是会阻塞的,在这
期间 Redis 是不能响应客户端的请求的。
AOF
AOF 方式是将执行过的写指令记录下来,在数据恢复时按照从前到后的顺序再将指令都执行
一遍。
AOF 优点:
使用 AOF 会让你的 Redis 更加持久化。
AOF 文件是一个只进行追加的日志文件,不需要在写入时读取文件。
Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写 。
AOF 文件可读性高,分析容易。
AOF 缺点:
对于相同的数据来说,AOF 文件大小通常要大于 RDB 文件。
根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB。
28、谈谈自己对于 Spring AOP 的了解?
AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块
所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统
的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
29、 Spring Bean 容器的生命周期是什么样的?
Bean 容器找到配置文件中 Spring Bean 的定义。
Bean 容器利用 Java Reflection API 创建一个 Bean 的实例。
如果涉及到一些属性值 利用 set()方法设置一些属性值。
如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName()方法,传入 Bean 的
名字。
如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader()方法,
传入 ClassLoader 对象的实例。
如果 Bean 实现了 BeanFactoryAware 接口,调用 setBeanFactory()方法,传入
BeanFactory 对象的实例。
与上面的类似,如果实现了其他 *.Aware 接口,就调用相应的方法。
如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行
postProcessBeforeInitialization() 方法
如果 Bean 实现了 InitializingBean 接口,执行 afterPropertiesSet()方法。
如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行
postProcessAfterInitialization() 方法
当要销毁 Bean 的时候,如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方
法。
当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,
执行指定的方法。
30、RabbitMQ 如何保证消息不丢失 ?
生产者:
方案 1:开启 RabbitMQ 事务(同步,性能差)
方案 2:开启 confirm 模式(异步,性能较好)
MQ:(1)exchange 持久化 (2)queue 持久化 (3)消息持久化
消费者:关闭自动 ACK
1、TCP 和 UDP 区别?
TCP 基于连接,UDP 基于无连接。
TCP 要求系统资源较多,UDP 较少。
UDP 程序结构较简单。
TCP 保证数据正确性,UDP 可能丢包。
TCP 保证数据顺序,UDP 不保证。
2、TCP/IP 协议涉及哪几层架构?
应用层 传输层 互连网络层 网络接口层。
3、描述下 TCP 连接 4 次挥手的过程?为什么要 4 次挥手?
因为 TCP 是全双工,每个方向都必须进行单独关闭。关闭连接时,当 Server 端收到 FIN
报文时,很可能并不会立即关闭 SOCKET,所以只能先回复一个 ACK 报文,告诉 Client
端,”你发的 FIN 报文我收到了”。只有等到 Server 端所有的报文都发送完了,我才能发
送 FIN 报文,因此不能一起发送。故需要四步握手。
4、计算机插上电源操作系统做了什么?
加电––––打开电源开关,给主板和内部风扇供电。
启动引导程序––––CPU 开始执行存储在 ROM BIOS 中的指令。
开机自检––––计算机对系统的主要部件进行诊断测试。
加载操作系统––––计算机将操作系统文件从磁盘读到内存中。
检查配置文件,定制操作系统的运行环境––––读取配置文件,根据用户的设置对操作
系统进行定制。
准备读取命令和数据––––计算机等待用户输入命令和数据。
5、Linux 操作系统设备文件有哪些?
字符设备、块设备。
6、多线程同步有哪些方法?
使用 synchronized 关键字
wait 和 notify
使用特殊域变量 volatile 实现线程同步
使用重入锁实现线程同步
使用局部变量来实现线程同步
使用阻塞队列实现线程同步
使用原子变量实现线程同步
9、创建线程的三个方法是什么?
通过继承 Thread 类创建线程类。
实现 Runnable 接口创建线程类。
通过 Callable 和 Future 接口创建线程。
10、Java 怎么获取多线程的返回值?
主线程等待。
使用 Thread 的 join 阻塞当前线程等待。
实现 Callable 接口(通过 FutureTask 或线程池的 Future)。
11、线程池有哪几种创建方式?
Java 通过 Executors(jdk1.5 并发包)提供四种线程池,分别为:
newCachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵
活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队
列中等待。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执
行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
12、线程池参数有哪些?
corePoolSize 核心线程大小。
maximumPoolSize 线程池最大线程数量。
keepAliveTime 空闲线程存活时间。
unit 空间线程存活时间单位。
workQueue 工作队列。
threadFactory 线程工厂。
handler 拒绝策略。
13、线程池拒绝策略有哪些?
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出 RejectedExecutionException 异常
(默认拒绝策略)。
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被
拒绝的任务。
ThreadPoolExecutor.CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务。
14、你认为对线程池的核心参数实现自定义可配置,三个核心参数是?
corePoolSize
:
核心线程数线程数定义了最小可以同时运行的线程数量。
maximumPoolSize
:
当队列中存放的任务达到队列容量的时候,当前可以同时运行的线
程数量变为最大线程数。
workQueue
:
当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果
达到的话,信任就会被存放在队列中。
17、Java8 新特性有哪些了解?
接口的默认方法
Lambda 表达式
函数式接口
方法和构造函数引用
Lamda 表达式作用域
内置函数式接口
Optional
Streams(流)
Parallel Streams(并行流)
Maps
Date API(日期相关 API)
Annotations(注解)
18、什么时候用多线程、为什么要设计多线程?
高并发
系统接受实现多用户多请求的高并发时,通过多线程来实现。
线程后台处理大任务
一个程序是线性执行的。如果程序执行到要花大量时间处理的任务时,那主程序就得等待其执
行完才能继续执行下面的。那用户就不得不等待它执行完。
这时候可以开线程把花大量时间处理的任务放在线程处理,这样线程在后台处理时,主程序也
可以继续执行下去,用户就不需要等待。线程执行完后执行回调函数。
大任务
大任务处理起来比较耗时,这时候可以起到多个线程并行加快处理(例如:分片上传)。
好处:可以提高 CPU 的利用率。在多线程程序中,一个线程必须等待的时候,CPU 可以运
行其他的线程而不是等待,这样就大大提高了程序的效率。也就是说允许单个程序创建多个并
行执行的线程来完成各自的任务。
19、多线程越多效率越高吗?
不是
当线程总数较少时,线程越多,效率越高。
当线程总数较多时,由于线程本身调用耗时,线程越多,效率越低。
线程数越多会造成:
线程的生命周期开销非常高
消耗过多的 CPU 资源。
20、多线程会产生哪些并发问题 ?
安全性问题:在单线程系统上正常运行的代码,在多线程环境中可能会出现意料之外的结果。
活跃性问题:不正确的加锁、解锁方式可能会导致死锁 or 活锁问题。
性能问题:多线程并发即多个线程切换运行,线程切换会有一定的消耗并且不正确的加锁。
21、Mybatis 如何将对象转换成 SQL?
SQL 绑定是在加载 Mybatis 配置文件,然后扫描到哪个 mapper 子节点,再加载
mapper 映射文件,扫描里面的 SQL 节点,然后封装成对象(MappedStatement,在这个
对象的 SqlSource 封装着 sql 语句)。所有的配置信息保存在 Configuration 类,最后动
态代理执行的时候,取出来封装 sql 的对象,执行 sql。
22、虚拟内存是什么,虚拟内存的原理是什么?
虚拟内存是计算机系统内存管理的一种技术。
虚拟内存有以下两个优点:
虚拟内存地址空间是连续的,没有碎片。
虚拟内存的最大空间就是 cup 的最大寻址空间,不受内存大小的限制,能提供比内存更
大的地址空间。
当每个进程创建的时候,内核会为每个进程分配虚拟内存,这个时候数据和代码还在磁盘上,
当运行到对应的程序时,进程去寻找页表,如果发现页表中地址没有存放在物理内存上,而是
在磁盘上,于是发生缺页异常,于是将磁盘上的数据拷贝到物理内存中并更新页表,下次再访
问该虚拟地址时就能命中了。
23、栈会溢出吗?什么时候溢出?方法区会溢出吗?
栈是线程私有的,它的生命周期与线程相同,每个方法在执行的时候都会创建一个栈帧,用来
存储局部变量表,操作数栈,动态链接,方法出口等信息。局部变量表又包含基本数据类型,
对象引用类型。如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出
StackOverflowError 异常,方法递归调用产生这种结果。如果 Java 虚拟机栈可以动态扩展,
并且扩展的动作已经尝试过,但是无法申请到足够的内存去完成扩展,或者在新建立线程的时
候没有足够的内存去创建对应的虚拟机栈,那么 Java 虚拟机将抛出一个 OutOfMemory 异
常。(线程启动过多)。
方法区会发生溢出。
HotSpot jdk1.7 之前字符串常量池是方法区的一部分,方法区叫做“永久代”,在 1.7 之
前无限的创建对象就会造成内存溢出,提示信息:PermGen space 而是用 jdk1.7 之后,
开始逐步去永久代,就不会产生内存溢出。
方法区用于存放 Class 的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等,
如果动态生成大量的 Class 文件,也会产生内存溢出。常见的场景还有:大量 JSP 或动态
产生 JSP 文件的应用(JSP 第一次运行时需要编译为 java 类)、基于 OSGi 的应用(即
使是同一个类文件,被不同的类加载器加载也会视为不同的类)。
24、JVM 如何加载类的?
JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化。
加载
加载是类加载过程中的一个阶段, 这个阶段会在内存中生成一个代表这个类 java.lang.Class
对象, 作为方法区这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,
这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成
(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。
验证
这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要
求,并且不会危害虚拟机自身的安全。
准备
准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所
使用的内存空间。注意这里所说的初始值概念,比如一个类变量定义为:
实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080, 将 v 赋值为 8080 的 put static
指令是程序被编译后, 存放于类构造器方法之中。
但是注意如果声明为:
public static final int v = 8080;
在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属
性将 v 赋值为 8080。
解析
解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。符号引用就是 class 文件
中的
public static int v = 8080;
实际上变量 v 在准备阶段过后的初始值为 0 而不是 8080, 将 v 赋值为 8080 的 put static
指令是程序被编译后, 存放于类构造器方法之中。但是注意如果声明为:
在编译阶段会为 v 生成 ConstantValue 属性,在准备阶段虚拟机会根据 ConstantValue 属
性将 v 赋值为 8080。
初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加
载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序
代码。
26、描述 ThreadLocal(线程本地变量)的底层实现原理及常用场景?
实现原理:
每个 Thread 线程内部都有一个 ThreadLocalMap;以线程作为 key,泛型作为 value,
可以理解为线程级别的缓存。每一个线程都会获得一个单独的 map。
提供了 set 和 get 等访问方法,这些方法为每个使用该变量的线程都存有一份独立的副
本,因此 get 方法总是返回由当前执行线程在调用 set 时设置的最新值。
应用场景:
JDBC 连接
Session 管理
Spring 事务管理
调用链,参数传递
AOP
ThreadLocal 是一个解决线程并发问题的一个类,用于创建线程的本地变量,我们知道一个
对象的所有线程会共享它的全局变量,所以这些变量不是线程安全的,我们可以使用同步技术。
但是当我们不想使用同步的时候,我们可以选择 ThreadLocal 变量。例如,由于 JDBC 的
连接对象不是线程安全的,因此,当多线程应用程序在没有协同的情况下,使用全局变量时,
就不是线程安全的。通过将 JDBC 的连接对象保存到 ThreadLocal 中,每个线程都会拥有
属于自己的连接对象副本。
27、什么是微服务架构?
微服务架构就是将单体的应用程序分成多个应用程序,这多个应用程序就成为微服务,每个微
服务运行在自己的进程中,并使用轻量级的机制通信。这些服务围绕业务能力来划分,并通过
自动化部署机制来独立部署。这些服务可以使用不同的编程语言,不同数据库,以保证最低限
度的集中式管理。
28、微服务有哪些特点?
解耦 – 系统内的服务很大程度上是分离的。因此,整个应用程序可以轻松构建,更改和
扩展
组件化 – 微服务被视为可以轻松更换和升级的独立组件
业务能力 – 微服务非常简单,专注于单一功能
自治 – 开发人员和团队可以彼此独立工作,从而提高速度
持续交付 – 通过软件创建,测试和批准的系统自动化,允许频繁发布软件
责任 – 微服务不关注应用程序作为项目。相反,他们将应用程序视为他们负责的产品
分散治理 – 重点是使用正确的工具来做正确的工作。这意味着没有标准化模式或任何技
术模式。开发人员可以自由选择最有用的工具来解决他们的问题
敏捷 – 微服务支持敏捷开发。任何新功能都可以快速开发并再次丢弃
1、哪些情况下的对象会被垃圾回收机制处理掉?
利用可达性分析
算法
,虚拟机会将一些对象定义为 GCRoots,从 GCRoots 出发沿着引用链
向下寻找,如果某个对象不能通过 GCRoots 寻找到,虚拟机就认为该对象可以被回收掉。
哪些对象可以被看做是 GCRoots 呢?
1)虚拟机栈(栈帧中的本地变量表)中引用的对象;
2)方法区中的类静态属性引用的对象,常量引用的对象;
3)本地方法栈中 JNI(Native 方法)引用的对象;
对象不可达,一定会被垃圾收集器回收么?
即使不可达,对象也不一定会被垃圾收集器回收,1)先判断对象是否有必要执行 finalize()
方法,对象必须重写 finalize()方法且没有被运行过。2)若有必要执行,会把对象放到一个
队列中,JVM 会开一个线程去回收它们,这是对象最后一次可以逃逸清理的机会。
静态代理和动态代理的区别,什么场景使用?
代理是一种常用的设计模式,目的是:为其他对象提供一个代理以控制对某个对象的访问,
将两个类的关系解耦。代理类和委托类都要实现相同的接口,因为代理真正调用的是委托类
的方法。
区别:
静态代理:由程序员创建或是由特定工具生成,在代码编译时就确定了被代理的类是哪 一
个是静态代理。静态代理通常只代理一个类;
动态代理:在代码运行期间,运用反射机制动态创建生成。动态代理代理的是一个接口 下
的多个实现类;
实现步骤:
a.实现 InvocationHandler 接口创建自己的调用处理器;
b.给 Proxy 类提供 ClassLoader 和代理接口类型数组创建动态代理类;
c.利用反射机制得到动态代理类的构造函数;
d.利用动态代理类的构造函数创建动态代理类对象;
使用场景:Retrofit 中直接调用接口的方法;Spring 的 AOP 机制;
简述下 Java 的异常体系。
Java 中 Throwable 是所有异常和错误的超类,两个直接子类是 Error(错误)和
Exception(异常):
Error 是程序无法处理的错误,由 JVM 产生和抛出,如 OOM、ThreadDeath 等。这
些异常 发生时,JVM 一般会选择终止程序。
Exception 是程序本身可以处理的异常,又分为运行时异常(RuntimeException)(也叫
Checked Eception)和 非 运 行 时 异 常(不 检 查 异 常 Unchecked Exception)。 运
行 时 异 常 有 NullPointerException\IndexOutOfBoundsException 等,这些异常一般
是由程序逻辑错误引起 的,应尽可能避免。非运行时异常有
IOException\SQLException\FileNotFoundException 以及 由用户自定义的
Exception 异常等。
谈谈你对解析与分派的认识。
解析指方法在运行前,即编译期间就可知的,有一个确定的版本,运行期间也不会改变。解析
是静态的,在类加载的解析阶段就可将符号引用转变成直接引用。
分派可分为静态分派和动态分派,重载属于静态分派,覆盖属于动态分派。静态分派是指在
重载时通过参数的静态类型而非实际类型作为判断依据,在编译阶段,编译器可根据参数的静
态类型决定使用哪一个重载版本。动态分派则需要根据实际类型来调用相应的方法。
修改对象 A 的 equals 方法的签名,那么使用 HashMap 存放这个对象实例
的时候,会用哪个 equals 方法?
会调用对象对象的 equals 方法。
“==”如果是基本类型的话就是看他们的数据值是否相等就可以。 如果是引用类型的话,比
较的是栈内存局部变量表中指向堆内存中的指针的值是否相等。 “equals”如果对象的
equals 方法没有重写的话,equals 方法和“==”是同一种。hashcod 是返回对象实例内存
地址的 hash 映射。 理论上所有对象的 hash 映射都是不相同的。
Java 中实现多态的机制是什么?
多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编译时
不确定,在运行期间才确定,一个引用变量到底会指向哪个类的实例。这样就可以不用 修改
源程序,就可以让引用变量绑定到各种不同的类实现上。Java 实现多态有三个必要条件:
继承、重定、向上转型,在多态中需要将子类的引用赋值给父类对象,只有这样该引用才能
够具备调用父类方法和子类的方法。
说说你对 Java 反射的理解。
在运行状态中,对任意一个类,都能知道这个类的所有属性和方法,对任意一个对象,都能
调用它的任意一个方法和属性。这种能动态获取信息及动态调用对象方法的功能称为 java 语
言的反射机制。
反射的作用:开发过程中,经常会遇到某个类的某个成员变量、方法或属性是私有的,或只
对系统应用开放,这里就可以利用 java 的反射机制通过反射来获取所需的私有成员或是方法。
获取类的 Class 对象实例 Classclz=Class.forName("com.zhenai.api.
Apple
");
根据 Class 对象实例获取 Constructor 对 象 Constructor appConstructor =
clz.getConstructor();
使用 Constructor 对 象 的 newInstance 方 法 获 取 反 射 类 对 象 Object appleObj
= appConstructor.newInstance();
获取方法的 Method 对象
MethodsetPriceMethod=clz.getMethod("setPrice",int.class);
利用 invoke 方法调用方法 setPriceMethod.invoke(appleObj,14);
通过 getFields()可以获取 Class 类的属性,但无法获取私有属性,而
getDeclaredFields()可 以获取到包括私有属性在内的所有属性。带有 Declared 修饰的
方法可以反射到私有的方法, 没有 Declared 修饰的只能用来反射公有的方法,其他如
Annotation\Field\Constructor 也是如此。
11、说说你对 Java 注解的理解。
注解是通过@interface 关键字来进行定义的,形式和接口差不多,只是前面多了一个@
public@interfaceTestAnnotation{
}
使用时@TestAnnotation 来引用,要使注解能正常工作,还需要使用元注解,它是可以注解
到注解上的注解。元标签有@Retention、@Documented、@Target、@Inherited 和
@Repeatable 五种。
@Retention 说明注解的存活时间,取值有 RetentionPolicy.SOURCE 注解只在源码阶段保
留,在编译器进行编译时被丢弃;RetentionPolicy.CLASS 注解只保留到编译进行的时候,
并不会被加载到 JVM 中。RetentionPolicy.RUNTIME 可以留到程序运行的时候,它会被加
载进入到 JVM 中,所以在程序运行时可以获取到它们。
@Documented 注解中的元素包含到 javadoc 中去。
@Target 限定注解的应用场景,ElementType.FIELD 给属性进行注解;
ElementType.LOCAL_VARIABLE 可以给局部变量进行注解;ElementType.METHOD 可
以给方法进行注解;ElementType.PACKAGE 可以给一个包进行注解 ElementType.TYPE
可以给一个类型进行注解,如类、接口、枚举。
@Inherited 若一个超类被@Inherited 注解过的注解进行注解,它的子类没有被任何注解应
用 的话,该子类就可继承超类的注解;
注解的作用:
提供信息给编译器:编译器可利用注解来探测错误和警告信息
编译阶段:软件工具可以利用注解信息来生成代码、html 文档或做其它相应处理;
运行阶段:程序运行时可利用注解提取代码
注解是通过反射获取的,可以通过 Class 对象的 isAnnotationPresent()方法判断它是否应
用了某个注解,再通过 getAnnotation()方法获取 Annotation 对象
谈谈你对 Java 中 String 的了解。
String 类是 final 型,固 String 类不能被继承,它的成员方法也都默认为 final 方法。
String 对象一旦创建就固定不变了,对 String 对象的任何改变都不影响到原对象,相关
的任何改变 操作都会生成新的 String 对象。
String 类是通过 char 数组来保存字符串的,String 对 equals 方法进行了重定,比较
的是 值相等。
String a="test";String b="test";String c=newString("test");
a、b 和字面上的 test 都是指向 JVM 字符串常量池中的"test"对象,他们指向同一个对象。
而 new 关键字一定会产生一个对象 test,该对象存储在堆中。所以 newString("test")产生
了两个对象,保存在栈中的 c 和保存在堆中的 test。而在 java 中根本就不存在两个完全一
模一样的字符串对象,故在堆中的 test 应该是引用字符串常量池中的 test。
String str1="abc";//栈中开辟一块空间存放引用 str1,str1 指向池中 String 常量"abc"
String str2="def";//栈中开辟一块空间存放引用 str2,str2 指向池中 String 常量"def"
String str3=str1+str2;//栈中开辟一块空间存放引用 str3//str1+str2 通过 StringBuilder 的最后一步
toString()方法返回一个新的 String 对象"abcdef"
//会在堆中开辟一块空间存放此对象,引用 str3 指向堆中的(str1+str2)所返回的新 String 对象。
System.out.println(str3=="abcdef");//返回 false 因为 str3 指向堆中的"abcdef"对象,而
"abcdef"是字符池中的对象,所以结果为 false。JVM 对 Stringstr="abc"对象放在常量池是在编译
时做的 , 而 Stringstr3=str1+str2 是在运行时才知道的,new 对象也是在运行时才做的。
String 为什么要设计成不可变的?
字符串常量池需要 String 不可变。因为 String 设计成不可变,当创建一个 String 对象
时, 若此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的
对象。 如果字符串变量允许必变,会导致各种逻辑错误,如改变一个对象会影响到另一个
独立对象。
String 对象可以缓存 hashCode。字符串的不可变性保证了 hash 码的唯一性,因此可
以缓 存 String 的 hashCode,这样不用每次去重新计算哈希码。在进行字符串比较时,
可以直接比较 hashCode,提高了比较性能;
安全性。String 被许多 java 类用来当作参数,如 url 地址,文件 path 路径,反射机
制所 需的 Strign 参数等,若 String 可变,将会引起各种安全隐患。
Redis 常见的几种数据结构说一下?各自的使用场景?
string
介绍:string 数据结构是简单的 key-value 类型。
使用场景: 一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等
等。
list
介绍:
list
即是
链表
使用场景:发布与订阅或者说消息队列、慢查询。
hash
介绍:hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。
使用场景:系统中对象数据的存储。
set
介绍:set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元
素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的
选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不
能提供的。可以基于 set 轻易实现交集、并集、差集的操作
使用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景。
sorted set
介绍:和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按
score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中
HashMap 和 TreeSet 的结合体。
使用场景:需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含
直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等
信息。
bitmap
介绍:bitmap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表
示某个元素对应的值或者状态,key 就是对应元素本身 。我们知道 8 个 bit 可以组成一个
byte,所以 bitmap 本身会极大的节省储存空间。。
使用场景:适合需要保存状态信息(比如是否签到、是否登录...)并需要进一步对这些信息进
行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计(比如是否点赞过某个视
频)。。
谈一谈缓存穿透、缓存击穿和缓存雪崩,以及各自的解决方案?
缓存穿透
问题:大量并发查询不存在的 KEY,在缓存和数据库中都不存在,同时给缓存和数据库
带来压力。
原因:一般而言,缓存穿透有 2 种可能性:业务数据被误删,导致缓存和数据库中都没
有数据。恶意进行 ddos 攻击。
分析:为什么会多次透传呢?不存在 一直为空,需要注意让缓存能够区分 KEY 不存在和
查询到一个空值。
解决办法:缓存空值的 KEY,这样第一次不存在也会被加载会记录,下次拿到有这个
KEY。Bloom 过滤或 RoaingBitmap 判断 KEY 是否存在,如果布隆过滤器中没有查到
这个数据,就不去数据库中查。在处理请求前增加恶意请求检查,如果检测到是恶意攻击,
则拒绝进行服务。完全以缓存为准,使用延迟异步加载的策略(异步线程负责维护缓存的
数据,定期或根据条件触发更新),这样就不会触发更新。
缓存击穿
问题:某个 KEY 失效的时候,正好有大量并发请求访问这个 KEY。
分析:跟穿透其实很像,属于比较偶然的。
解决办法:KEY 的更新操作添加全局互斥锁。完全以缓存为准,使用延迟异步加载的策
略(异步线程负责维护缓存的数据,定期或根据条件触发更新),这样就不会触发更新。
缓存雪崩
问题:当某一时刻发生大规模的缓存失效的情况,导致大量的请求无法获取数据,从而将
流量压力传导到数据库上,导致数据库压力过大甚至宕机。
原因:一般而言,缓存雪崩有 2 种可能性:大量的数据同一个时间失效:比如业务关系
强相关的数据要求同时失效 Redis 宕机
分析:一般来说,由于更新策略、或者数据热点、缓存服务宕机等原因,可能会导致缓存
数据同一个时间点大规模不可用,或者都更新。所以,需要我们的更新策略要在时间上合
适,数据要均匀分享,缓存服务器要多台高可用。
解决办法:更新策略在时间上做到比较平均。如果数据需要同一时间失效,可以给这批数
据加上一些随机值,使得这批数据不要在同一个时间过期,降低数据库的压力。使用的热
数据尽量分散到不同的机器上。多台机器做主从复制或者多副本,实现高可用。做好主从
的部署,当主节点挂掉后,能快速的使用从结点顶上。实现熔断限流机制,对系统进行负
载能力控制。对于非核心功能的业务,拒绝其请求,只允许核心功能业务访问数据库获取
数据。服务降价:提供默认返回值,或简单的提示信息。
讲下 Kafka、RabbitMQ、RocketMQ 之间的区别是什么?
性能
消息中间件的性能主要衡量吞吐量,Kafka 的吞吐量比 RabbitMQ 要高出 1~2 个数量级,
RabbitMQ 的单机 QPS 在万级别,Kafka 的单机 QPS 能够达到百万级别。RocketMQ
单机写入 TPS 单实例约 7 万条/秒,单机部署 3 个 Broker,可以跑到最高 12 万条/秒,
消息大小 10 个字节,Kafka 如果开启幂等、事务等功能,性能也会有所降低。
数据可靠性
Kafka 与 RabbitMQ 都具备多副本机制,数据可靠性较高。RocketMQ 支持异步实时刷盘,
同步刷盘,同步 Replication,异步 Replication。
服务可用性
Kafka 采用集群部署,分区与多副本的设计,使得单节点宕机对服务无影响,且支持消息容
量的线性提升。RabbitMQ 支持集群部署,集群节点数量有多种规格。RocketMQ 是分布式
架构,可用性高。
功能
Kafka 与 RabbitMQ 都是比较主流的两款消息中间件,具备消息传递的基本功能,但在一些
特殊的功能方面存在差异,RocketMQ 在阿里集团内部有大量的应用在使用。
Kafka 的架构说一下?
整个架构中包括三个角色。
生产者(Producer):消息和数据生产者。
代理(Broker):缓存代理,Kafka 的核心功能。
消费者(Consumer):消息和数据消费者。
Kafka 给 Producer 和 Consumer 提供注册的接口,数据从 Producer 发送到 Broker,
Broker 承担一个中间缓存和分发的作用,负责分发注册到系统中的 Consumer。
Kafka 怎么保证消息不丢失?
生产者丢失消息的情况
生产者(Producer) 调用 send 方法发送消息之后,消息可能因为网络问题并没有发送过去。
为了确定消息是发送成功,我们要判断消息发送的结果,Kafka 生产者(Producer) 使用
send 方法发送消息实际上是异步的操作,我们可以通过 get()方法获取调用结果,但是这样
也让它变为了同步操作,可以采用为其添加回调函数的形式,示例代码如下:
ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, o);
future.addCallback(result -> logger.info("生产者成功发送消息到 topic:{} partition:{}的消息",
result.getRecordMetadata().topic(), result.getRecordMetadata().partition()),
ex -> logger.error("生产者发送消失败,原因:{}", ex.getMessage()));
Producer 的
retries
(重试次数)设置一个比较合理的值,一般是 3 ,但是为了保证消息不
丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,
避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网
络波动一次你 3 次一下子就重试完了
消费者丢失消息的情况
当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个
问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际
上并没有被消费,但是 offset 却被自动提交了。
解决办法也比较粗暴,我们手动关闭自动提交 offset,每次在真正消费完消息之后再自己手
动提交 offset 。
但是,细心的朋友一定会发现,这样会带来消息被重新消费的问题。比如你
刚刚消费完消息之后,还没提交 offset,结果自己挂掉了,那么这个消息理论上就会被消费两
次。
Kafka 弄丢了消息
试想一种情况:假如 leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选
出一个 leader ,但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消
息丢失。
当我们配置了
unclean.leader.election.enable = false
的话,当 leader 副本发生故障时就
不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了
消息丢失的可能性。
Kafka 怎么解决重复消费?
生产者发送每条数据的时候,里面加一个全局唯一的 id,消费到了之后,先根据这个 id
去比如 Redis 里查一下,之前消费过吗,如果没有消费过,就处理,然后这个 id 写
Redis。如果消费过就别处理了。
基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据
插入只会报错,不会导致数据库中出现脏数据。
介绍下 MySQL 聚簇索引与非聚簇索引的区别(InnoDB 与 Myisam 引
擎)?
聚集索引是指数据库表行中数据的物理顺序与键值的逻辑(索引)顺序相同。一个表只能有一
个聚簇索引,因为一个表的物理顺序只有一种情况,所以,对应的聚簇索引只能有一个。 聚
簇索引的叶子节点就是数据节点,既存储索引值,又在叶子节点存储行数据。
Innodb 创建表后生成的文件有:
frm:创建表的语句
idb:表里面的数据+索引文件
非聚集索引(MyISAM 引擎的底层实现)的逻辑顺序与磁盘上行的物理存储顺序不同。非聚
簇 索引的叶子节点仍然是索引节点,只不过有指向对应数据块的指针。索引命中后,需要回
表查 询。
Myisam 创建表后生成的文件有:
frm:创建表的语句 MYD:表里面的数据文件(myisam data)
MYI:表里面的索引文件(myisam index)
innodb 的次索引指向对主键的引用 (聚簇索引)
myisam 的次索引和主索引都指向物理行 (非聚簇索引)
然后给一个联合索引(a,b)和一个语句,select * from table where b = 'xxx',
判断是否能命中索引?为什么?
不能命中。
对于查询 SELECT * FROM TABLE WHERE a=xxx and b=xxx,显然是可以使用(a,b)
这个联合索引的。
对于单个的 a 列查询 SELECT * FROM TABLEWHERE a=xxx,也可以使用这个(a,b)
索引。
但对于 b 列的查询 SELECT *FROM TABLE WHERE b=xxx,则不可以使用这棵 B+树索
引。
在 innoDb 数据引擎中,可以发现叶子节点上的 b 值为 1、2、1、4、1、2,显然不是排序
的,因此对于 b 列的查询使用不到(a,b)的索引
Java 多线程有哪几种实现方式?
通过继承 Thread 类创建线程类
实现 Runnable 接口创建线程类
通过 Callable 和 Future 接口创建线程
用过 ConcurrentHashMap,讲一下他和 HashTable 的不同之处?
HashTable 就是实现了 HashMap 加上了 synchronized,而 ConcurrentHashMap
底层采用分段的数组+链表实现,线程安全
ConcurrentHashMap 通过把整个 Map 分为 N 个 Segment,可以提供相同的线程安
全,但是效率提升 N 倍,默认提升 16 倍。
并且读操作不加锁,由于 HashEntry 的 value 变量是 volatile 的,也能保证读取到最
新的值。
Hashtable 的 synchronized 是针对整张 Hash 表的,即每次锁住整张表让线程独占,
ConcurrentHashMap 允许多个修改操作并发进行,其关键在于使用了锁分离技术
扩容:段内扩容(段内元素超过该段对应 Entry 数组长度的 75%触发扩容,不会对整个
Map 进行扩容),插入前检测需不需要扩容,有效避免无效扩容
、Java 怎么实现线程安全?
使用同步代码块
使用同步方法
使用 Lock 锁机制, 通过创建 Lock 对象,采用 lock()加锁,unlock()解锁,来保护指
定的代码块
介绍下 Spring Bean 都有哪些作用域 ?
单例 singleton : bean 在每个 Spring IOC 容器中只有一个实例。
原型 prototype:一个 bean 的定义可以有多个实例。
request:每次 http 请求都会创建一个 bean。
session:在一个 HTTP Session 中,一个 bean 定义对应一个实例。
globalsession
application
注解 @Autowired 和 @Resource 有什么区别?
Resource 是 JDK 提供的,而 Autowired 是 Spring 提供的
Resource 不允许找不到 bean 的情况,而 Autowired 允许(@Autowired(required =
false))
指定 name 的方式不一样,@Resource(name =
"baseDao"),@Autowired()@Qualifier("baseDao")
Resource 默认通过 name 查找,而 Autowired 默认通过 type 查找
(1)@Autowired 与@Resource 都可以用来装配 bean,都可以写在
字段
或
setter
方法
上
(2)
@Autowired 默认按类型装配
,默认情况下必须要求依赖对象存在,如果要
允许 null
值
,可以设置它的
required 属性
为
false
。如果想
使用名称装配
可以
结合@Qualifier 注解
进
行使用。
(3)
@Resource
,
默认按照名称
进行装配,名称可以通过
name 属性
进行指定,如果没有
指定 name 属性,当注解写在
字段上
时,
默认取字段名
进行名称查找。如果注解写在 setter
方法上默认取属性名进行装配。当
找不到与名称匹配的 bean
时
才按照类型
进行装配。但是
需要注意的是,如果 name 属性一旦指定,就只会按照名称进行装配。
RPC 的实现基础?
需要有非常高效的网络通信,比如一般选择 Netty 作为网络通信框架;
需要有比较高效的序列化框架,比如谷歌的 Protobuf 序列化框架;
可靠的寻址方式(主要是提供服务的发现),比如可以使用 Zookeeper 来注册服务等等;
如果是带会话(状态)的 RPC 调用,还需要有会话和状态保持的功能;
CMS,G1 垃圾回收器中的三色标记了解吗?
三色标记算法思想
三色标记法是一种垃圾回收法,它可以让 JVM 不发生或仅短时间发生 STW(Stop The
World),从而达到清除 JVM 内存垃圾的目的。
三色标记法将对象的颜色分为了黑、灰、白,三种颜色。
黑色:
该对象已经被标记过了,且该对象下的属性也全部都被标记过了。(程序所需要的对
象)
;
灰色:
对象已经被垃圾收集器扫描过了,但是对象中还存在没有扫描的引用
(GC 需要从此对
象中去寻找垃圾)
;
白色:
表示对象没有被垃圾收集器访问过,即表示不可达。
CMS 解决办法:增量更新
在应对漏标问题时,CMS 使用了增量更新(Increment Update)方法来做,在一个未被标记
的对象(白色对象)被重新引用后,引用它的对象若为黑色则要变成灰色,在下次二次标记时
让 GC 线程继续标记它的属性对象(但还是存在漏标的问题)。
CMS 另两个致命缺陷
CMS 采用了 Mark-Sweep 算法,最后会产生许多内存碎片,当到一定数量时,CMS 无法
清理这些碎片了,CMS 会让 Serial Old 垃圾处理器来清理这些垃圾碎片,而 Serial Old 垃
圾处理器是单线程操作进行清理垃圾的,效率很低。
所以使用 CMS 就会出现一种情况,硬件升级了,却越来越卡顿,其原因就是因为进行
Serial Old GC 时,效率过低。
解决方案:使用 Mark-Sweep-Compact 算法,减少垃圾碎片
调优参数(配套使用):
-XX:+UseCMSCompactAtFullCollection 开启 CMS 的压缩
-XX:CMSFullGCsBeforeCompaction 默认为 0,指经过多少次 CMS FullGC 才进行压缩
当 JVM 认为内存不够,再使用 CMS 进行并发清理内存可能会发生 OOM 的问题,而不得
不进行 Serial Old GC,Serial Old 是单线程垃圾回收,效率低
解决方案:降低触发 CMS GC 的阈值,让浮动垃圾不那么容易占满老年代
调优参数:
-XX:CMSInitiatingOccupancyFraction 92% 可以降低这个值,让老年代占用率达到该值就
进行 CMS GC
G1 解决办法:SATB
SATB(Snapshot At The Beginning), 在应对漏标问题时,G1 使用了 SATB 方法来做,具体
流程:
在开始标记的时候生成一个快照图标记存活对象
在一个引用断开后,要将此引用推到 GC 的堆栈里,保证白色对象(垃圾)还能被 GC
线程扫描到(在**write barrier(写屏障)**里把所有旧的引用所指向的对象都变成非白的)
配合 Rset,去扫描哪些 Region 引用到当前的白色对象,若没有引用到当前对象,则回
收
G1 会不会进行 Full GC?
会,当内存满了的时候就会进行 Full GC;且 JDK10 之前的 Full GC,为单线程的,所以
使用 G1 需要避免 Full GC 的产生。
解决方案:
加大内存;
提高 CPU 性能,加快 GC 回收速度,而对象增加速度赶不上回收速度,则 Full GC 可
以避免;
降低进行 Mixed GC 触发的阈值,让 Mixed GC 提早发生(默认 45%)
、Kafka 是什么?主要应用场景有哪些?
Kafka 是一个分布式流式处理平台。这到底是什么意思呢?
流平台具有三个关键功能:
消息队列
:发布和订阅消息流,这个功能类似于消息队列,这也是 Kafka 也被归类为消
息队列的原因。
容错的持久方式存储记录消息流
: Kafka 会把消息持久化到磁盘,有效避免了消息丢失
的风险。
流式处理平台:
在消息发布的时候进行处理,Kafka 提供了一个完整的流式处理类库。
Kafka 主要有两大应用场景:
消息队列
:建立实时流数据管道,以可靠地在系统或应用程序之间获取数据。
数据处理:
构建实时的流数据处理程序来转换或处理数据流。
2、kafka 为什么有 topic 还要用 patition?
Kafka 可以将主题划分为多个分区(Partition),会根据分区规则选择把消息存储到哪个
分区中,只要分区规则设置的合理,那么所有的消息将会被均匀的分布到不同的分区中,
这样就实现了负载均衡和水平扩展。另外,多个订阅者可以从一个或者多个分区中同时消
费数据,以支撑海量数据处理能力。
producer 只需要关心消息发往哪个 topic,而 consumer 只关心自己订阅哪个 topic,
并不关心每条消息存于整个集群的哪个 broker。 为了性能考虑,如果 topic 内的消息
只存于一个 broker,那这个 broker 会成为瓶颈,无法做到水平扩展。所以把 topic 内
的数据分布到整个集群就是一个自然而然的设计方式。
Partition 的引入就是解决水平扩展问题的一个方案。
客户端和服务器之间最多能建立多少个连接 ?
65535 。
服务器的 ip ,端口号 ,客户端的 ip 都是确定的。 能变的只有客户端的端口号。
加网卡 ,保证四元组唯一,理论上能是客户端和服务器之间建立 10 万以上的连接 。
MySQL 索引分类?
单列索引
普通索引:MySQL 中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值
和空值,纯粹为了查询数据更快一点。
唯一索引:索引列中的值必须是唯一的,但是允许为空值,
主键索引:是一种特殊的唯一索引,不允许有空值。
组合索引:
多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使
用,使用组合索引时遵循最左前缀集合。
全文索引:
只有在 MyISAM 引擎上才能使用,只能在 CHAR,VARCHAR,TEXT 类型字段上使用全文
索引,介绍了要求,说说什么是全文索引,就是在一堆文字中,通过其中的某个关键字等,就
能找到该字段所属的记录行,比如有"你是个靓仔,靓女 ..." 通过靓仔,可能就可以找到该条
记录
空间索引:
空间索引是对空间数据类型的字段建立的索引,MySQL 中的空间数据类型有四种,
GEOMETRY、POINT、LINESTRING、POLYGON。在创建空间索引时,使用 SPATIAL 关
键字。要求,引擎为 MyISAM,创建空间索引的列,必须将其声明为 NOT NULL。
了解线程 & 进程的区别吗?
操作系统中可以拥有多个进程,一个进程里可以拥有多个线程,线程在进程内执行
进程和线程的区别
容易创建新线程。创建新进程需要重复父进程
线程可以控制同一进程的其他线程。进程无法控制兄弟进程,只能控制其子进程
进程拥有自己的内存空间。线程使用进程的内存空间,且要和该进程的其他线程共享这个
空间;而不是在进程中给每个线程单独划分一点空间。
(同一进程中的)线程在共享内存空间中运行,而进程在不同的内存空间中运行
线程可以使用 wait(),notify(),notifyAll()等方法直接与其他线程(同一进程)
通信;而,进程需要使用“进程间通信”(IPC)来与操作系统中的其他进程通信。
Java 进程间的几种通信方式?
管道( pipe ):管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘
关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
有名管道 (named pipe) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进
程间的通信。
信号量( semophore ) : 信号量是一个计数器,可以用来控制多个进程对共享资源的访
问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因
此,主要作为进程间以及同一进程内不同线程之间的同步手段。
消息队列( message queue ) : 消息队列是由消息的链表,存放在内核中并由消息队列
标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大
小受限等缺点。
信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
共享内存( shared memory ) :共享内存就是映射一段能被其他进程所访问的内存,这段
共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针
对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配
合使用,来实现进程间的同步和通信。
套接字( socket ) : 套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用
于不同及其间的进程通信。
多台服务器同时对一个数据定时任务,怎么处理 ?
对于一个定时任务,如果当前任务已经被某一个服务器处理后,另外一个服务器就不需要执行
这个任务了
在定时任务里加锁机制,等某台服务器获取权限,其他服务器将不再执行此次定时任务。
在数据库的创建定时任务控制表 job_controller,创建 updated_by 字段,用来存放执
行代码的服务器生成的序列号。创建 updateTime 字段,用于记录标记更新 update_by
的时间戳,也可以理解为上一次任务执行的时间戳。
在代码层面,在执行任务的时候,首先生成一个序列号,然后将序列号存储在当前任务的
记录上。然后再从数据库里查询当前记录的序列号,在做标记前,首先检查当前任务的上
一次执行时间离当前时间超过阈值(自己定义),如果超过则表明还没有其他节点执行该
任务,然后为 task 保存标签和当前运行时间。当然如果上一次运行时间为空的情况下,
也是允许标记的,如果数据库中的序列号与当前节点生成序列号相匹配,则执行任务的具
体逻辑,反之,则什么都不做处理。
常见分布式锁的几种实现方式?
基于数据库实现分布式锁
基于缓存实现分布式锁
基于 Zookeeper 实现分布式锁
Redis 分布式锁实现原理?
set px nx
守护线程,进行 renew
Redis 分布式锁实现: 先拿 setnx 来争抢锁,抢到之后,再用 expire(过期)给锁加一个
过期时间防止锁忘记了释放。
如果在 setnx 之后执行 expire 之前进程意外 crash 或者要重启维护了,那会怎么样:
set 指令有非常复杂的参数,这个应该是可以同时把 setnx 和 expire 合成一条指令来用
的!
、Redis 的数据类型及它们的使用场景?
string
key/value; 二进制安全的。意思是 redis 的 string 可以包含任何数据。比如 jpg 图片
或者序列化的对象 。一个键最大能存储 512MB。
hash
存储对象数据
list : 简单的字符串列表
关注列表
队列
set: string 类型的无序集合
共同关注列表
统计独立 IP
zset : (sorted set:有序集合),每个元素都会关联一个 double 类型的分数。redis 正是通
过分数来为集合中的成员进行从小到大的排序。
排行
带权重的消息队列
信号:(signal)是一种处理异步事件的方式。信号是比较复杂的通信方式,用于通知接
受进程有某种事件发生,除了用于进程外,还可以发送信号给进程本身。
信号量:(Semaphore)进程间通信处理同步互斥的机制。是在多线程环境下使用的一
种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。
简单地说,信号就是一种异步通信,通知进程某种事件的发生;信号量是进程/线程同步与互
斥的一种机制,保证进程/线程间之间的有序执行或对公共资源的有序访问。
select 和 epoll 的底层结构是什么原理
select:支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读/写等待队列用于支持
上层(用户层)所需的 BLOCK 或 NONBLOCK 操作。当应用程序通过设备驱动访问该设备时
(默认为 BLOCK 操作),若该设备当前没有数据可读或写,则将该用户进程插入到该设备驱
动对应的读/写等待队列让其睡眠一段时间,等到有数据可读/写时再将该进程唤醒。
select 就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/
写时唤醒。
epoll:epoll 由三个系统调用组成,分别是 epoll_create,epoll_ctl 和 epoll_wait。
epoll_create 用于创建和初始化一些内部使用的数据结构;epoll_ctl 用于添加,删除或者修
改指定的 fd 及其期待的事件,epoll_wait 就是用于等待任何先前指定的 fd 事件。
场景题:1 亿个数据取出最大前 100 个有什么方法?
最容易想到的方法是将数据全部排序,然后在排序后的集合中进行查找,最快的排序算法
的时间复杂度一般为 O(nlogn),如快速排序。
局部淘汰法,该方法与排序方法类似,用一个容器保存前 10000 个数,然后将剩余的所
有数字——与容器内的最小数字相比,如果所有后续的元素都比容器内的 10000 个数还
小,那么容器内这个 10000 个数就是最大 10000 个数。如果某一后续元素比容器内最
小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完这 1 亿个数,得
到的结果容器中保存的数即为最终结果了。此时的时间复杂度为 O(n+m^2),其中 m
为容器的大小,即 10000。
分治法,将 1 亿个数据分成 100 份,每份 100 万个数据,找到每份数据中最大的
10000 个,最后在剩下的 10010000 个数据里面找出最大的 10000 个。如果 100
万数据选择足够理想,那么可以过滤掉 1 亿数据里面 99%的数据。100 万个数据里面
查找最大的 10000 个数据的方法如下:用快速排序的方法,将数据分为 2 堆,如果大
的那堆个数 N 大于 10000 个,继续对大堆快速排序一次分成 2 堆,如果大的那堆个
数 N 大于 10000 个,继续对大堆快速排序一次分成 2 堆,如果大堆个数 N 小于
10000 个,就在小的那堆里面快速排序一次,找第 10000-n 大的数字;递归以上过程,
就可以找到第 1w 大的数。参考上面的找出第 1w 大数字,就可以类似的方法找到前
10000 大数字了。此种方法需要每次的内存空间为 10^64=4MB,一共需要 101 次这
样的比较。
Hash 法,如果这 1 亿个书里面有很多重复的数,先通过 Hash 法,把这 1 亿个数字
去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通
过分治法或最小堆法查找最大的 10000 个数。
采用最小堆法,首先读入前 10000 个数来创建大小为 10000 的最小堆,建堆的时间复
杂度为 O(mlogm)(m 为数组的大小即为 10000),然后遍历后续的数字,并于堆
顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,
则替换堆顶元素并重新调整堆为最小堆。整个过程直至 1 亿个数全部遍历完为止。然后
按照中序遍历的方式输出当前堆中的所有 10000 个数字。该算法的时间复杂度为 O
(nmlogm),空间复杂度是 10000(常数)。
kafka 如何保证消息可靠?
生产者丢失消息的情况
生产者(Producer) 调用 send 方法发送消息之后,消息可能因为网络问题并没有发送过去。
为了确定消息是发送成功,我们要判断消息发送的结果,Kafka 生产者(Producer) 使用
send 方法发送消息实际上是异步的操作,我们可以通过 get()方法获取调用结果,但是这样
也让它变为了同步操作,可以采用为其添加回调函数的形式,示例代码如下:
ListenableFuture<SendResult<String, Object>> future = kafkaTemplate.send(topic, o);
future.addCallback(result -> logger.info("生产者成功发送消息到 topic:{} partition:{}的消息",
result.getRecordMetadata().topic(), result.getRecordMetadata().partition()),
ex -> logger.error("生产者发送消失败,原因:{}", ex.getMessage()));
Producer 的
retries
(重试次数)设置一个比较合理的值,一般是 3 ,但是为了保证消息不
丢失的话一般会设置比较大一点。设置完成之后,当出现网络问题之后能够自动重试消息发送,
避免消息丢失。另外,建议还要设置重试间隔,因为间隔太小的话重试的效果就不明显了,网
络波动一次你 3 次一下子就重试完了
消费者丢失消息的情况
当消费者拉取到了分区的某个消息之后,消费者会自动提交了 offset。自动提交的话会有一个
问题,试想一下,当消费者刚拿到这个消息准备进行真正消费的时候,突然挂掉了,消息实际
上并没有被消费,但是 offset 却被自动提交了。
解决办法也比较粗暴,我们手动关闭自动提交 offset,每次在真正消费完消息之后再自己手
动提交 offset 。
但是,细心的朋友一定会发现,这样会带来消息被重新消费的问题。比如你
刚刚消费完消息之后,还没提交 offset,结果自己挂掉了,那么这个消息理论上就会被消费两
次。
Kafka 弄丢了消息
试想一种情况:假如 leader 副本所在的 broker 突然挂掉,那么就要从 follower 副本重新选
出一个 leader ,但是 leader 的数据还有一些没有被 follower 副本的同步的话,就会造成消
息丢失。
当我们配置了
unclean.leader.election.enable = false
的话,当 leader 副本发生故障时就
不会从 follower 副本中和 leader 同步程度达不到要求的副本中选择出 leader ,这样降低了
消息丢失的可能性。
消息队列的使用场景?
消息队列在实际应用中包括如下四个场景:
应用耦合:多应用间通过消息队列对同一消息进行处理,避免调用接口失败导致整个过程
失败;
异步处理:多应用对消息队列中同一消息进行处理,应用间并发处理消息,相比串行处理,
减少处理时间;
限流削峰:广泛应用于秒杀或抢购活动中,避免流量过大导致应用系统挂掉的情况;
消息驱动的系统:系统分为消息队列、消息生产者、消息消费者,生产者负责产生消息,
消费者(可能有多个)负责对消息进行处理;
乐观锁和悲观锁的理解及如何实现,有哪些实现方式?
悲观锁:
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会
上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多
这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 Java 里
面的同步原语 synchronized 关键字的实现也是悲观锁。
乐观锁:
顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更
新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适
用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制,
其实都是提供的乐观锁。在 Java 中 java.util.concurrent.atomic 包下面的原子变量类就是
使用了乐观锁的一种实现方式 CAS 实现的。
ArrayList 和 LinkedList 的区别在哪里?
数据结构实现:ArrayList :基于数组,便于按 index 访问,超过数组需要扩容,扩容成
本较高。LinkedList:使用链表实现,无需扩容。
随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList
是线性的数据存储方式,所以需要移动指针从前往后依次查找。
增加和删除效率:在非首尾的增删操作,LinkedList 要比 ArrayList 效率要高,因为
ArrayList 增删操作要影响数组内的其他数据的下标。
内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数
据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
线程安全:ArrayList 和 LinkList 都是不同步的,不保证线程安全。
综合来说,需要频繁读取集合中的元素时,更推荐使用 Arrayist,而在增删操作较多时,
更推荐使用 LinkedList。
LinkedList 的双向链表是链表的一种,它的每个数据结点中都有 2 个指针,分别指向直
接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便的访问它的
前驱结点和后继结点。
谈谈你对 SQL 注入式攻击的理解?
所谓 SQL 注入式攻击,就是攻击者把 SQL 命令插入到 Web 表单的输入域或页面请求的
查询字符串,
欺骗服务器执行恶意的 SQL 命令
。
如何防范 SQL 注入式攻击?
在利用表单输入的内容构造 SQL 命令之前,把所有输入内容过滤一番就可以了。过滤输入内
容可以按多种方式进行。
对于动态构造 SQL 查询的场合
a. 替换单引号,即把所有单独出现的单引号改成两个单引号,防止攻击者修改 SQL 命令的含
义。
b. 删除用户输入内容中的所有连字符
c. 对于用来执行查询的数据库帐户,限制其权限
。
用不同的用户帐户执行查询、插入、更新、
删除操作。
用存储过程来执行所有的查询。
限制表单或查询字符串输入的长度。
检查用户输入的合法性。
将用户登录名称、密码等数据加密保存。
检查提取数据的查询所返回的记录数量。
数据库事务的特性?
原子性:即不可分割性,事务要么全部被执行,要么就全部不被执行。
一致性或可串性。事务的执行使得数据库从一种正确状态转换成另一种正确状态
隔离性。在事务正确提交之前,不允许把该事务对数据的任何改变提供给任何其他事务,
持久性。事务正确提交后,其结果将永久保存在数据库中,即使在事务提交后有了其他故
障,事务的处理结果也会得到保存。
Redis 如何做内存优化?
尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,
所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的 web 系统中有一个用
户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的 key,而是应该把这个用户的
所有信息存储到一张散列表里面.
缓存穿透,缓存击穿,缓存雪崩都是咋回事?解决办法?
缓存穿透
问题:大量并发查询不存在的 KEY,在缓存和数据库中都不存在,同时给缓存和数据库
带来压力。
原因:一般而言,缓存穿透有 2 种可能性:业务数据被误删,导致缓存和数据库中都没
有数据。恶意进行 ddos 攻击。
分析:为什么会多次透传呢?不存在 一直为空,需要注意让缓存能够区分 KEY 不存在和
查询到一个空值。
解决办法:缓存空值的 KEY,这样第一次不存在也会被加载会记录,下次拿到有这个
KEY。Bloom 过滤或 RoaingBitmap 判断 KEY 是否存在,如果布隆过滤器中没有查到
这个数据,就不去数据库中查。在处理请求前增加恶意请求检查,如果检测到是恶意攻击,
则拒绝进行服务。完全以缓存为准,使用延迟异步加载的策略(异步线程负责维护缓存的
数据,定期或根据条件触发更新),这样就不会触发更新。
缓存击穿
问题:某个 KEY 失效的时候,正好有大量并发请求访问这个 KEY。
分析:跟穿透其实很像,属于比较偶然的。
解决办法:KEY 的更新操作添加全局互斥锁。完全以缓存为准,使用延迟异步加载的策
略(异步线程负责维护缓存的数据,定期或根据条件触发更新),这样就不会触发更新。
缓存雪崩
问题:当某一时刻发生大规模的缓存失效的情况,导致大量的请求无法获取数据,从而将
流量压力传导到数据库上,导致数据库压力过大甚至宕机。
原因:一般而言,缓存雪崩有 2 种可能性:大量的数据同一个时间失效:比如业务关系
强相关的数据要求同时失效 Redis 宕机
分析:一般来说,由于更新策略、或者数据热点、缓存服务宕机等原因,可能会导致缓存
数据同一个时间点大规模不可用,或者都更新。所以,需要我们的更新策略要在时间上合
适,数据要均匀分享,缓存服务器要多台高可用。
解决办法:更新策略在时间上做到比较平均。如果数据需要同一时间失效,可以给这批数
据加上一些随机值,使得这批数据不要在同一个时间过期,降低数据库的压力。使用的热
数据尽量分散到不同的机器上。多台机器做主从复制或者多副本,实现高可用。做好主从
的部署,当主节点挂掉后,能快速的使用从结点顶上。实现熔断限流机制,对系统进行负
载能力控制。对于非核心功能的业务,拒绝其请求,只允许核心功能业务访问数据库获取
数据。服务降价:提供默认返回值,或简单的提示信息。
数组和链表的区别?当数组内存过大时会出现什么问题?链表增删过多会
出现的什么问题?
数组静态分配内存,链表动态分配内存;
数组事先定义固定的长度,不能适应数据动态的增减的情况。当数据增加时,可能超出原
先定义的元素个数;当数据减少时,造成内存浪费;
链表动态地进行存储分配,可以适应数据动态地增减的情况
数组在内存中连续,链表不连续;
数组元素在栈区,链表元素在堆区;
(静态)数组从栈中分配空间,对于程序员方便快速,但是自由度小;
链表从堆中分配空间,自由度大但是申请管理比较麻烦。
数组利用下标定位,时间复杂度为 O(1),链表定位元素时间复杂度 O(n);
数组插入或删除元素的时间复杂度 O(n),链表的时间复杂度 O(1)。
当数组内存过大时会出现什么问题(堆内存溢出),链表增删过多会出现的什么问题(大
量内存碎片)
常见排序算法和分别的复杂度?
冒泡排序,O(n2),通过重复走完数组的所有元素,通过两两比较,直到没有数可以交换
的时候结束这个数,再到下个数,直到整个数组排好顺序。
插入排序,O(n2),每次从未排好序的数据堆中拿出一个数,插入到已排好序的数据队列
的正确位置。
选择排序,O(n2),每次从未排好序的数据堆中找到最小的数,插入到已排好序的数据队
列的头部。
快速排序,O(N*logN),以数据堆中的一个数为标准,将数据堆分为小于等于和大于该数
的两堆,对于分割后的两堆数再分别利用上述方法进行分割,以此类推,直到堆中只有一
个数为止。
堆排序,O(N*logN),将数据堆中的数两两组队排序,对于排序好的这些子堆再两两组队
排序,以此类推,直到只剩下一个堆。
归并排序,O(N*logN),基于堆的排序算法,分为最大堆和最小堆。排序分为两个过程堆
的构造和堆的排序。
jdk 1.8 的 JVM 内存划分模型 ,堆和栈的区别
方法区(method):被所有的线程共享。方法区包含所有的类信息和静态变量。(运行时
常量池)
堆(heap):被所有的线程共享,存放对象实例以及数组,Java 堆是 GC 的主要区域。
栈(stack):每个线程包含一个栈区,栈中保存一些局部变量等。(本地局部变量、操作数
栈、动态链接、返回地址)
程序计数器:是当前线程执行的字节码的行指示器。
本地方法栈
简单描述 MySQL 中,索引,主键,唯一索引,联合索引的区别,对数据
库的性能有什么影响(从读写两方面)?
索引是一种特殊的文件(InnoDB 数据表上的索引是表空间的一个组成部分),它们包含着
对数据表里所有记录的引用指针。
普通索引(由关键字 KEY 或 INDEX 定义的索引)的唯一任务是加快对数据的访问速度。
普通索引允许被索引的数据列包含重复的值。如果能确定某个数据列将只包含彼此各不相
同的值,在为这个数据列创建索引的时候就应该用关键字 UNIQUE 把它定义为一个唯一
索引。也就是说,唯一索引可以保证数据记录的唯一性。
主键,是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一
条记录,使用关键字 PRIMARY KEY 来创建。
索引可以覆盖多个数据列,如像 INDEX(columnA, columnB)索引,这就是联合索引。
索引可以极大的提高数据的查询速度,但是会降低插入、删除、更新表的速度,因为在执
行这些写操作时,还要操作索引文件。
I/O 模型有哪几种?
阻塞
I/O
, 非阻塞
I/O
模型,
I/O
复用模型,信号驱动
I/O
模型 ,异步
I/O
模型。
当你用浏览器打开一个链接的时候,计算机做了哪些工作步骤?
域名解析–> 发起 TCP 的 3 次握手 –> 建立 TCP 连接后发起 http 请求 –> 服务器响应
http 请求–>浏览器得到 html 代码 –> 浏览器解析 html 代码,并请求 html 代码中的资
源(如 js、css、图片等) –> 浏览器对页面进行渲染呈现给用户 。
幻读是什么,用什么隔离级别可以防止幻读?
幻读是一个事务在前后两次查询同一个范围的时候、后一次查询看到了前一次查询未看到的行。
在可重复读隔离级别下,普通的查询是快照读,是不会看到别的事务插入的数据的。因此,幻
读在“当前读”下才会出现。
SERIALIZABLE(可串行化)可以防止幻读:最高的隔离级别,完全服从 ACID 的隔离级别。
所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰。
、SpingBoot 也有定时任务?是什么注解?
在 SpringBoot 中使用定时任务主要有两种不同的方式,一个就是使用 Spring 中的
@Scheduled
注解,另一个则是使用第三方框架 Quartz。
使用 Spring 中的
@Scheduled
的方式主要通过
@Scheduled
注解来实现。
使用 Quartz ,则按照 Quartz 的方式,定义 Job 和 Trigger 即可。
请描述线程的生命周期,它们之间如何切换?
线程的生命周期包含 5 个阶段,包括:新建、就绪、运行、阻塞、销毁。
新建(NEW):就是刚使用 new 方法,new 出来的线程;
就绪(RUNNABLE):就是调用的线程的 start()方法后,这时候线程处于等待 CPU 分
配资源阶段,谁先抢的 CPU 资源,谁开始执行;
运行(RUNNING):当就绪的线程被调度并获得 CPU 资源时,便进入运行状态,run
方法定义了线程的操作和功能;
阻塞(BLOCKED):在运行状态的时候,可能因为某些原因导致运行状态的线程变成了
阻塞状态,比如 sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将
处于阻塞状态的线程唤醒,比如调用 notify 或者 notifyAll()方法。唤醒的线程不会立刻
执行 run 方法,它们要再次等待 CPU 分配资源进入运行状态;
Waiting(无限等待):一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进
入 Waiting 状态。进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 方法
或者 notifyAll 方法时才能够被唤醒。
销毁(TERMINATED):如果线程正常执行完毕后或线程被提前强制性的终止或出现异
常导致结束,那么线程就要被销毁,释放资源;
什么情况线程会进入 WAITING 状态?
一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态。进入这个
状态后不能自动唤醒,必须等待另一个线程调用 notify 方法或者 notifyAll 方法时才能够被
唤醒。
调用 Object 对象的 wait 方法,但没有指定超时值。
调用 Thread 对象的 join 方法,但没有指定超时值。
调用 LockSupport 对象的 park 方法。
简述多进程开发中 join 和 deamon 的区别?
join:当子线程调用 join 时,主线程会被阻塞,当子线程结束后,主线程才能继续执行。
deamon:当子进程被设置为守护进程时,主进程结束,不管子进程是否执行完毕,都会随着
主进程的结束而结束。
异步和同步、阻塞和非阻塞之间的区别?
同步
当一个 request 发送出去以后,会得到一个 response,这整个过程就是一个同步调用的过
程。哪怕 response 为空,或者 response 的返回特别快,但是针对这一次请求而言就是一
个同步的调用。
异步
当一个 request 发送出去以后,没有得到想要的 response,而是通过后面的 callback、状
态或者通知的方式获得结果。可以这么理解,对于异步请求分两步:
调用方发送 request 没有返回对应的 response(可能是一个空的 response);
服务提供方将 response 处理完成以后通过 callback 的方式通知调用方。
对于 1)而言是同步操作(调用方请求服务方),对于 2)而言也是同步操作(服务方回掉
调用方)。从请求的目的(调用方发送一个 request,希望获得对应的 response)来看,这
两个步骤拆分开来没有任何意义,需要结合起来看,而这整个过程就是一次异步请求。异步请
求有一个最典型的特点:需要 callback、状态或者通知的方式来告知调用方结果。
阻塞
阻塞调用是指调用方发出 request 的线程因为某种原因(如:等待系统资源)被服务方挂起,
当服务方得到 response 后就唤醒挂起线程,并将 response 返回给调用方。
非阻塞
非阻塞调用是指调用方发出 request 的线程在没有等到结果时不会被挂起,并且直到得到
response 后才返回。
阻塞和非阻塞最大的区别就是看调用方线程是否会被挂起。
为什么要分内核态和用户态?
假设没有这种内核态和用户态之分,程序随随便便就能访问硬件资源,比如说分配内存,程序
能随意的读写所有的内存空间,如果程序员一不小心将不适当的内容写到了不该写的地方,就
很可能导致系统崩溃。用户程序是不可信的,不管程序员是有意的还是无意的,都很容易将系
统干到崩溃。
正因为如此,Intel 就发明了 ring0-ring3 这些访问控制级别来保护硬件资源,ring0 的就是
我们所说的内核级别,要想使用硬件资源就必须获取相应的权限(设置 PSW 寄存器,这个操
作只能由操作系统设置)。操作系统对内核级别的指令进行封装,统一管理硬件资源,然后向
用户程序提供系统服务,用户程序进行系统调用后,操作系统执行一系列的检查验证,确保这
次调用是安全的,再进行相应的资源访问操作。**内核态能有效保护硬件资源的安全。
说下类加载器与类加载?加载的类信息放在哪个区域?
一个类型从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期将会经历加载
(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化
(Initialization)、使用(Using)和卸载(Unloading)七个阶段。其中验证、准备、解析三
个部分统称为连接(Linking)。
Java 虚拟机设计团队把类加载阶段中“
通过一个类的全限定名来获取描述该类的二进制流
”
这个动作放到 Java 虚拟机外部去实现。比便让程序应用自己决定如何取获取所需的类。实
现这个动作的代码被称为“
类加载器
”(Class Loader)。
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在 Java 虚拟机
中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
UDP 协议和 TCP 协议的区别?
TCP 基于连接,UDP 基于无连接
TCP 要求系统资源较多,UDP 较少
UDP 程序结构较简单
TCP 保证数据正确性,UDP 可能丢包
TCP 保证数据顺序,UDP 不保证
limit 1000000 加载很慢的话,你是怎么解决的呢?
方案一:如果 id 是连续的,可以这样,返回上次查询的最大记录(偏移量),再往下 limit
select id,name from employee where id>1000000 limit 10.
方案二:在业务允许的情况下限制页数:
建议跟业务讨论,有没有必要查这么后的分页啦。因为绝大多数用户都不会往后翻太多页。
方案三:order by + 索引(id 为索引)
select id,name from employee order by id limit 1000000,10
方案四:利用延迟关联或者子查询优化超多分页场景。(先快速定位需要获取的 id 段,然后
再关联)
SELECT a.* FROM employee a, (select id from employee where 条件 LIMIT 1000000,10 ) b
where a.id=b.id
MySQL 的索引分类是什么?
单列索引
普通索引:MySQL 中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值
和空值,纯粹为了查询数据更快一点。
唯一索引:索引列中的值必须是唯一的,但是允许为空值,
主键索引:是一种特殊的唯一索引,不允许有空值。
组合索引:
多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使
用,使用组合索引时遵循最左前缀集合。
全文索引:
只有在 MyISAM 引擎上才能使用,只能在 CHAR,VARCHAR,TEXT 类型字段上使用全文
索引,介绍了要求,说说什么是全文索引,就是在一堆文字中,通过其中的某个关键字等,就
能找到该字段所属的记录行,比如有"你是个靓仔,靓女 ..." 通过靓仔,可能就可以找到该条
记录
空间索引:
空间索引是对空间数据类型的字段建立的索引,MySQL 中的空间数据类型有四种,
GEOMETRY、POINT、LINESTRING、POLYGON。在创建空间索引时,使用 SPATIAL 关
键字。要求,引擎为 MyISAM,创建空间索引的列,必须将其声明为 NOT NULL。
MySQL 的主从复制了解吗?
主库将变更写入 binlog 日志,然后从库连接到主库之后,从库有一个 IO 线程,将主库的
binlog 日志拷贝到自己本地,写入一个 relay 中继日志中接着从库中有一个 SQL 线程会从中
继日志读取 binlog,然后执行 binlog 日志中的内容,也就是在自己本地再次执行一遍 SQL。
Spring 框架事务注解用什么注解?使用该注解的失效场景?
@Transactional
Transactional 注解应用在非 public 修饰的方法上@Transactional 注解属性
propagation 设置错误
@Transactional 注解属性 rollbackFor 设置错误
同一个类中方法调用,导致@Transactional 失效
异常被 catch“吃了”导致@Transactional 失效
final、finally、finallize?finally 是在 return 之前执行还是之后?finally 块
里的代码一定会执行吗?
final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代表不可以
继承扩展,final 的变量是不可以修改的,而 final 的方法也是不可以重写的(override)。
finally 是 Java 保证重点代码一定要被执行的一种机制。可以使用 try-finally 或者 try-
catch-finally 来进行类似关闭 JDBC 连接、保证 unlock 锁等动作。
finalize 是基础类 java.lang.Object 的一个方法,设计目的是保证对象在被垃圾收集前完
成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9 开始被标记为
deprecated。
finally 块的语句在 try 或 catch 中的 return 语句执行之后返回之前执行且 finally 里的修
改语句可能影响也可能不影响 try 或 catch 中 return 已经确定的返回值,若 finally 里也
有 return 语句则覆盖 try 或 catch 中的 return 语句直接返回。
finally 块里的代码不一定会执行。比如:
try 语句没有被执行到,如在 try 语句之前就返回了,这样 finally 语句就不会执行,这
也说明了 finally 语句被执行的必要而非充分条件是:相应的 try 语句一定被执行到。
在 try 块中有 System.exit(0**
15、I/O 多路复用实现方式有哪些?
select
poll
epoll
select、poll、epoll 区别有哪些?
select:它仅仅知道了,有 I/O 事件发生了,却并不知道是哪那几个流(可能有一个,多个,
甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行
操作。所以
select 具有 O(n)的无差别轮询复杂度
,同时处理的流越多,无差别轮询时间就
越长。
poll:poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个
fd 对应的设备状态,
但是它没有最大连接数的限制
,原因是它是基于链表来存储的.
epoll:
epoll 可以理解为 event poll
,不同于忙轮询和无差别轮询,epoll 会把哪个流发生了
怎样的 I/O 事件通知我们。所以我们说 epoll 实际上是
事件驱动(每个事件关联上 fd)
的,
此时我们对这些流的操作都是有意义的。(复杂度降低到了 O(1)),
通过红黑树和双链表数
据结构,并结合回调机制,造就了 epoll 的高效
,epoll_create(),epoll_ctl()和
epoll_wait()系统调用。
如何保证 Redis 中的数据不丢失?
单机单节点模式
使用 AOF 和 RDB 结合的方式
RDB 做镜像
全量持久化
,AOF 做
增量持久化
。因为 RDB 会耗费较长时间,不够实时,在
停机的时候会导致大量丢失数据,所以需要 AOF 来配合使用。
Redis 集群模式
master 节点持久化
如果采用了主从架构,那么建议必须开启 master node 的持久化!不建议用 slave node
作为 master node 的数据热备,因为那样的话,如果你关掉 master 的持久化,可能在
master 宕机重启的时候数据是空的,然后可能一经过复制,salve node 数据也丢了,
master 就会将空的数据集同步到 slave 上去,所有 slave 的数据全部清空。
Redis 断点续传
从 redis 2.8 开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,
那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。
主备切换的过程,可能会导致数据丢失
解决异步复制和脑裂导致的数据丢失
redis.conf 中
min-slaves-to-write 1
min-slaves-max-lag 10
要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒
如果说一旦所有的 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,
master 就不会再接收任何请求了
上面两个配置可以减少异步复制和脑裂导致的数据丢失。
如何保证 Redis 中的数据都是热点数据?
Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。Redis 提供 6 种
数据淘汰策略:
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的
数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据
淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据
淘汰
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-enviction(驱逐):禁止驱逐数据
Redis 持久化机制是如何做的?
RDB
RDB 持久化方式,是将 Redis 某一时刻的数据持久化到磁盘中,是一种快照式的持久化方
法。
RDB 优点:
RDB 是一个非常紧凑(有压缩)的文件,它保存了某个时间点的数据,非常适用于数据的备
份。
RDB 作为一个非常紧凑(有压缩)的文件,可以很方便传送到另一个远端数据中心 ,非
常适用于灾难恢复.
RDB 在保存 RDB 文件时父进程唯一需要做的就是 fork 出一个子进程,接下来的工作全
部由子进程来做,父进程不需要再做其他 IO 操作,所以 RDB 持久化方式可以最大化
redis 的性能.
与 AOF 相比,在恢复大的数据集的时候,RDB 方式会更快一些.
RDB 缺点:
Redis 意外宕机 时,会丢失部分数据
当 Redis 数据量比较大时,fork 的过程是非常耗时的,fork 子进程时是会阻塞的,在这
期间 Redis 是不能响应客户端的请求的。
AOF
AOF 方式是将执行过的写指令记录下来,在数据恢复时按照从前到后的顺序再将指令都执行
一遍。
AOF 优点:
使用 AOF 会让你的 Redis 更加持久化。
AOF 文件是一个只进行追加的日志文件,不需要在写入时读取文件。
Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写 。
AOF 文件可读性高,分析容易。
AOF 缺点:
对于相同的数据来说,AOF 文件大小通常要大于 RDB 文件
根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB
混合持久化方式
Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先
把
当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,
这
样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。
Redis 为什么在使用 RDB 进行快照时会通过子进程的方式进行实现?
通过 fork 创建的子进程能够获得和父进程完全相同的内存空间,父进程对内存的修改对
于子进程是不可见的,两者不会相互影响;
通过 fork 创建子进程时不会立刻触发大量内存的拷贝,内存在被修改时会以页为单位进
行拷贝,这也就避免了大量拷贝内存而带来的性能问题;
介绍下 MySQL 的主从复制原理?产生主从延迟的原因?
主从复制原理: 主库将变更写入 binlog 日志,然后从库连接到主库之后,从库有一个
IO 线程,将主库的 binlog 日志拷贝到自己本地,写入一个 relay 中继日志中。 接着从库
中有一个 SQL 线程会从中继日志读取 binlog,然后执行 binlog 日志中的内容,也就是
在自己本地再次执行一遍 SQL。
主从延迟:
a. 主库的从库太多
b. 从库硬件配置比主库差
c. 慢 SQL 语句过多
d. 主从库之间的网络延迟
e. 主库读写压力大
父进程如果宕掉,子进程会怎样?
如果父进程是会话首进程,那么父进程退出后,子进程也会退出;反之如果父进程不是会话首
进程,那么父进程退出后,子进程不会退出,而它的一个或多个子进程还在运行,那么这些子
进程就成为孤儿进程。
孤儿进程和僵尸进程有什么区别?
孤儿进程:父进程结束了,而它的一个或多个子进程还在运行,那么这些子进程就成为孤儿进
程(father died)。子进程的资源由 init 进程(进程号 PID = 1)回收。
僵尸进程:子进程退出了,但是父进程没有用 wait 或 waitpid 去获取子进程的状态信息,
那么子进程的进程描述符仍然保存在系统中,这种进程称为僵死进程。
MySQL 中有哪几种锁?
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度
最低。
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度
也最高。
页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之
间,并发度一般。
26、互斥锁(mutex)和自旋锁(spinlock)分别在什么场景使用?
在多核机器中,如果锁住的“事务”很简单,占用很少的时间,就应该使用 spinlock,这个
时候 spinlock 的代价比 mutex 会小很多。”事务”很快执行完毕,自旋的消耗远远小于陷
入 sleep 和 wake 的消耗。如果锁住“事务”粒度较大,就应该使用 mutex,因为如果用
spinlock,那么在“事务”执行过程中自旋很长时间还不如使得线程 sleep。
在单核机器中。spinlock 没有任何意义的,spinlock 只会浪费唯一核心的 cpu 时间片,这
个时刻没有任何线程会运行的。所以单核机器中,不论锁住的”事务”的粒度大小都要使用。
27、描述 Synchronized、ReentrantLock 的区别 ?
synchronized 是关键字,ReentrantLock 是 API 接口
Lock 需要手动加锁,手动释放锁
synchronized 不可中断,ReentrantLock 可中断、可超时
synchronized 是非公平锁,ReentrantLock 公平、非公平皆可
ReentrantLock 支持 Condition,多条件
HashMap 扩容操作是怎么实现的?
在 jdk1.8 中,resize 方法是在 hashmap 中的键值对大于阀值时或者初始化时,就调
用 resize 方法进行扩容;
每次扩展的时候,都是扩展 2 倍;
扩展后 Node 对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
ConcurrentHashMap 1.7 与 1.8 区别?
1.8 采用 synchronized 代替可重入锁 ReentrantLock (现代 JDK 中,synchronized
已经被不断优化,可以不再过分担心性能差异)
1.8 取消了 Segment 分段锁的数据结构,使用数组+链表+红黑树的结构代替
1.8 对每个数组元素加锁,1.7 对要操作的 Segment 数据段加锁
如何使用 Java 的反射?
通过一个全限类名创建一个对象
Class.forName(“全限类名”); 例如:com.mysql.jdbc.Driver Driver 类已经被加载到 jvm
中,并且完成了类的初始化工作就行了
类名.class; 获取 Class<?> clz 对象
对象.getClass();
获取构造器对象,通过构造器 new 出一个对象
Clazz.getConstructor([String.class]);
Con.newInstance([参数]);
通过 class 对象创建一个实例对象(就相当与 new 类名()无参构造器)
Cls.newInstance();
通过 class 对象获得一个属性对象
Field c=cls.getFields():获得某个类的所有的公共(public)的字段,包括父类中的字段。
Field c=cls.getDeclaredFields():获得某个类的所有声明的字段,即包括 public、private
和 proteced,但是不包括父类的声明字段
通过 class 对象获得一个方法对象
Cls.getMethod(“方法名”,class……parameaType);(只能获取公共的)
Cls.getDeclareMethod(“方法名”);(获取任意修饰的方法,不能执行私有)
M.setAccessible(true);(让私有的方法可以执行)
让方法执行
Method.invoke(obj 实例对象,obj 可变参数);-----(是有返回值的)
Java 常用集合及特点?
List:ArrayList、LinkedList、Vector、Stack Set:LinkedSet、HashSet、TreeSet
Queue->Deque->LinkedList。
Map:HashMap、LinkedHashMap、TreeMap Dictionary->HashTable->Properties。
Vector: 底层数据结构是数组,查询快,增删慢,线程安全,效率低,默认长度为 10,超
过会 100%延长,变成 20,浪费空间。
ArrayList :基于数组,便于按 index 访问,超过数组需要扩容,扩容成本较高。
LinkedList:使用链表实现,无需扩容。
HashSet:底层数据结构是哈希表(无序,唯一),通过 hashcode()和 equals()保证元素
唯一。
LinkedHashSet: 底层数据结构是链表和哈希表(FIFO 插入有序,唯一),由链表保证元
素有序,由哈希表保证元素唯一。
TreeSet:底层数据结构是红黑树(唯一,有序),通过自然排序和比较器排序保证元素有序,
根据比较返回值是否是 0 来保证元素唯一性。
TreeMap 是有序的。
HashMap :空间换时间,哈希冲突不大的情况下查找数据性能很高。
LinkedHashMap 基本特点:继承自 HashMap,对 Entry 集合添加了一个双向链表。
介绍 Spring MVC 的工作流程 ?
用户向服务端发送一次请求,这个请求会先到前端控制器 DispatcherServlet。
DispatcherServlet 接收到请求后会调用 HandlerMapping 处理器映射器。由此得知,
该请求该由哪个 Controller 来处理(并未调用 Controller,只是得知)
DispatcherServlet 调用 HandlerAdapter 处理器适配器,告诉处理器适配器应该要去
执行哪个 Controller
HandlerAdapter 处理器适配器去执行 Controller 并得到 ModelAndView(数据和视图),
并层层返回给 DispatcherServlet
DispatcherServlet 将 ModelAndView 交给 ViewReslover 视图解析器解析,然后返
回真正的视图。
DispatcherServlet 将模型数据填充到视图中
DispatcherServlet 将结果响应给用户
Spring 框架中用到了哪些设计模式?
工厂设计模式
: Spring 使用工厂模式通过 BeanFactory、ApplicationContext 创建
bean 对象。
代理设计模式
: Spring AOP 功能的实现。
单例设计模式
: Spring 中的 Bean 默认都是单例的。
模板方法模式
: Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的
对数据库操作的类,它们就使用到了模板模式。
包装器设计模式
: 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需
要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据
源。
观察者模式:
Spring 事件驱动模型就是观察者模式很经典的一个应用。
适配器模式
: Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中
也是用到了适配器模式适配 Controller。
为什么使用 Redis,有什么好处?
速度快,因为数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的
时间复杂度都是 O(1)
支持丰富数据类型,支持 string,list,set,sorted set,hash
支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不
执行
丰富的特性:可用于缓存,消息,按 key 设置过期时间,过期后将会自动删除
什么是面向对象,谈谈你的理解?
世间万物都可以看成一个对象。每个物体包括动态的行为和静态的属性,这些就构成了一个对
象。
你知道有哪些设计原则?
遵循单一职责原则
开放-封闭原则
里氏代换原则(LSP)
依赖倒置原则
接口隔离原则(Interface Segregation Principle)
迪米特法则(Law of Demeter)
在生产环境 Linux 服务器上,发现某台运行 Java 服务的服务器的
CPU100%,不借助任何可视化工具,怎么进行问题的定位?
top 找出进程 CPU 比较高 PID
top -Hp PID 打印 该 PID 进程下哪条线程的 CPU 占用比较高 tid
printf “%x\n” tid 将该 id 进行 16 进制转换 tidhex
jstack PID |grep tidhex 打印线程的堆栈信息
JDK 里面带的工具你知道哪些?
jstat:虚拟机进程状况工具
jinfo:Java 配置信息工具
jmap:Java 内存映像工具
jhat:虚拟机堆转储快照分析工具
jstack:Java 堆栈跟踪工具
JConsole: Java 监视与管理控制台
VisualVM: 多合一故障处理工具
GC root 有哪些?
Thread-存活的线程。
Java 虚拟机栈中的引用的对象。
方法区中的类静态属性引用的对象。 (一般指被 static 修饰的对象,加载类的时候就加
载到内存中。)
方法区中的常量引用的对象。
本地方法栈中的 JNI(native 方法)引用的对象。
Monitor Used-用于同步监控的对象。
静态 filed 声明和构造器哪个先执行?
filed 声明先执行。
所有 I/O 都被视为单个的字节的移动,通过一个称为 Stream 的对象一次移动一个字节。
流 I/O 用于与外部世界接触。它也在内部使用,用于将对象转换为字节,然后再转换回对
象。传统流 IO 的好处是使用简单,将底层的机制都抽象成流,但缺点就是性能不足。而
且 IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,该线程被阻
塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
原来的 I/O 库(在 java.io.*中) 与 NIO 最重要的区别是数据打包和传输的方式。 原来的
I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
NIO 性能的优势就来源于缓冲的机制(buffer 机制),不管是读或者写都需要以块的形
式写入到缓冲区中。NIO 实际上让我们对 IO 的操作更接近于操作系统的实际过程。
NIO 作为非阻塞式的 IO,它的优点就在于,1、它由一个专门的线程去处理所有的 IO
事件,并负责分发;2、事件驱动,只有事件到了才会触发,而不是同步的监听这个事件;
3、线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓
的线程切换。
当我们在执行持续性的操作(如上传下载)时,IO 的方式是要优于 NIO 的。分清情况,
合理选用。
NIO 相对于 IO 流的优势:
非阻塞
buffer 机制
流替代块
消息队列的在各种场景下如何选型?
优先级队列;队列设置最大的优先级,之后每条消息设置对应的优先级,队列根据消息优
先级进行消费,(在有可能队列堆积的情况才有意义);应用场景:不同业务消息推送。
延迟队列:消息发送后,并不想让消费者立即拿到消息,等待特定的事件后,消费者才能
拿到并消费;应用场景:订单系统中订单支付 30 分钟内没有支付成功,那么将这个订单
进行异常处理;远程操作智能设备在指定时间进行工作等。(rabbit 中没有延迟队列,但
可以借助死信队 列与 TTL 设置来完成)
死信队列:当消息在一个队列中变成死信之后,它能被重新被发送到另一个交换器(DLX
交换器)中,绑定 DLX 的队列就称为死信队列。
重试队列:消费端,一直不回传消费的结果,rocketmq 认为消息没收到,consumer 下
一次拉取,broker 依然会发送该消息(有次数限制)。重试队列其实可以看成是一种回
退队列,具体指消费端消费消息失败时,为防止消息无故丢失而重新将消息回滚到
Broker 中。
消费模式: 推模式:对于 kafka 而言,由 Broker 主动推送消息至消费端,实时性较好,
不过需要一定的流 制机制来确保服务端推送过来的消息不会压垮消费端。拉模式:对于
kafka 而言,消费端主动向 Broker 端请求拉取(一般是定时或者定量)消息,实时性较推
模式差,但是可以根据自身的处理能力而控制拉取的消息量。
消息回溯:重置消息 offset(如:kafka、rokcetMq) 一般消息在消费完成之后就被处
理了,之后再也不能消费到该条消息。消息回溯正好相反,是指消息在消费完成之后,还
能消费到之前被消费掉的消息。对于消息而言,经常面临的问题是“消息丢失”,至于是
真正由于消息中间件的缺陷丢失还是由于使用方的误用而丢失一般很难追查,如果消息中
间件本身具备消息回溯功能的话,可以通过回溯消费复现“丢失的”消息 进而查出问题的
源头之所在。消息回溯的作用远不止与此,比如还有索引恢复、本地缓存重建,有些业务
补偿方案也可以采用回溯的方式来实现。
消息堆积:流量削峰是消息中间件的一个非常重要的功能,而这个功能其实得益于其消息
堆积能力。从某种意义上来讲,如果一个消息中间件不具备消息堆积的能力,那么就不能
把它看做是一个合格的消息中间件。消息堆积分内存式堆积和磁盘式堆积。
消息持久化:持久化确保 MQ 的使用不只是一个部分场景的辅助工具,而是让 MQ 能
像数据库一样存储核心的数据。有些功能是默认不开启的,需要进行配置。
多租户: 也可以称为多重租赁技术,是一种软件架构技术,主要用来实现多用户的环境下
公用相同的系统或程序组件,并且仍可以确保各用户间数据的隔离性。RabbitMQ 就能够
支持多租户技术,每一个租户表示为一个 vhost,其本质上是一个独立的小型 RabbitMQ
服务器,又有自己独立 的队列、交换器及绑定关系等,并且它拥有自己独立的权限。
vhost 就像是物理机中的虚拟机 一样,它们在各个实例间提供逻辑上的分离,为不同程
序安全保密地允许数据,它既能将同一 个 RabbitMQ 中的众多客户区分开,又可以避免
队列和交换器等命名冲突。
跨语言支持: 对很多公司而言,其技术栈体系中会有多种编程语言,如 C/C++、JAVA、
Go、PHP 等,消息 中间件本身具备应用解耦的特性,如果能够进一步的支持多客户端语
言,那么就可以将此特性 的效能扩大。跨语言的支持力度也可以从侧面反映出一个消息中
间件的流行程度。
消息顺序消息:先进先出、 逐条进行消费顾名思义,消息顺序性是指保证消息有序。这个
功能有个很常见的应用场景就是 CDC(Change Data Chapture),以 MySQL 为例,
如果其传输的 binlog 的顺序出错,比如原本是先对一条数据加 1,然后再乘以 2,发送
错序之后就变成了先乘以 2 后加 1 了,造成了数据不一致。
安全机制: 在 Kafka 0.9 版本之后就开始增加了身份认证和权限控制两种安全机制。身
份认证是指客户端与服务端连接进行身份认证,包括客户端与 Broker 之间、Broker 与
Broker 之间、Broker 与 ZooKeeper 之间的连接认证,目前支持 SSL、SASL 等认证
机制。权限控制是指对客户端的读写操作进行权限控制,包括对消息或 Kafka 集群操作
权限控制。权限控制是可插拔的,并支持与外部的授权服务进行集成。对于 RabbitMQ
而言,其同样提供身份认证(TLS/SSL、SASL)和 权限控制(读写操作)的安全机制。
事务支持: 事务本身是一个并不陌生的词汇,事务是由事务开始(Begin Transaction)
和事务结束(End Transaction)之间执行的全体操作组成。支持事务的消息中间件并不
在少数,Kafka 和 RabbitMQ 都支持,不过此两者的事务是指生产者发生消息的事务,
要么发送成功,要么发送失败。消息中间件可以作为用来实现分布式事务的一种手段,但
其本身并不提供全局分布式事务的功能。
线程池如果满了会怎么样?
如果使用的是无界队列 Linke dBlockingQueue,也就是无界队列的话,没关系,继续添
加任务到阻塞队列中等待执行,因为 LinkedBlockingQueue 可以近乎认为是一个无穷大
的队列,可以无限存放任务
如果使用的是有界队列比如 ArrayBlockingQueue , 任务首先会被添加到
ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据 maximumPoolSize 的
值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,
那么则会使用拒绝策略 RejectedExecutionHandler 处理满了的任务,默认是
AbortPolicy。
什么是双亲委派机制,它有什么作用?
双亲委派机制的意思是除了顶层的启动类加载器以外,其余的类加载器,在加载之前,都会委
派给它的父加载器进行加载。这样一层层向上传递,直到祖先们都无法胜任,它才会真正的加
载。
通过带有优先级的层级关可以避免类的重复加载;
保证 Java 程序安全稳定运行,Java 核心 API 定义类型不会被随意替换
select、poll 和 epoll 什么区别
它们是 NIO 多路复用的三种实现机制,是有 Linux 系统提供。
select:无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作,会
维护一个文件描述符 FD 的集合 fd_set,将 fd_set 从用户空间复制到内核空间。x86
fd_set 是数组结构
poll:与 select 机制相似,fd_set 结构进行优化,突破操作系统限制,pollfd 代替
fd_set,链表结构
epoll:不再扫描所以 fd,只将用户关心的事件放在内核的一个事件表中,减少用户空间
和内核空间的数据拷贝。epoll 可以理解为 event poll,不同于忙轮询和无差别轮询,
epoll 会把哪个流发生了怎样的 I/O 事件通知我们。