简历里的项目介绍
说清楚项目的背景?
获得了什么成果?
做了什么设计?对于工作多年的同学来说
小米服务器端
1.hashMap扩容为什么是2的幂次?
减小hash碰撞,方便扩容
2.G1收集器介绍一下
参考
1⃣️并行与并发
2⃣️分代收集
3⃣️可预测的停顿
G1把内存划分成很多小块, 每个小块会被标记为E/S/O中的一个,从而可以垃圾回收工作更彻底的并行化
它回收的大致过程是这样的:
- 在垃圾回收的最开始有一个短暂的时间段(Inital Mark)会停止应用(stop-the-world)
- 然后应用继续运行,同时G1开始Concurrent Mark
- 再次停止应用,来一个Final Mark (stop-the-world)
- 最后根据Garbage First的原则,选择一些内存块进行回收。(stop-the-world)
每个小块会被标记为E/S/O中的一个,意思是每个region会被分为新生代的E区或S区或者老年代的O区。回收之后,每一个region都会再重新分配,因此分配哪个区是不固定的
G1的另一个显著特点他能够让用户设置应用的暂停时间,为什么G1能做到这一点呢?也许你已经注意到了,G1回收的第4步,它是“选择一些内存块”,而不是整代内存来回收,这是G1跟其它GC非常不同的一点,其它GC每次回收都会回收整个Generation的内存(Eden, Old), 而回收内存所需的时间就取决于内存的大小,以及实际垃圾的多少,所以垃圾回收时间是不可控的;而G1每次并不会回收整代内存,到底回收多少内存就看用户配置的暂停时间,配置的时间短就少回收点,配置的时间长就多回收点,伸缩自如。 (阿里面试)
因此G1比较适合内存稍大一点的应用(一般来说至少4G以上)
cms和g1的区别
(1)使用范围不一样:CMS收集器是老年代的收集器,而G1收集器收集范围是老年代和新生代
(2)STW的时间:CMS收集器以最小的停顿时间为目标的,吞吐量不高;G1收集器可预测垃圾回收的停顿时间
(3)垃圾碎片:CMS收集器是使用“标记-清除”算法进行的垃圾回收,容易产生内存碎片;G1收集器使用的是“标记-整理”算法,进行了空间整合,降低了内存空间碎片;
(4)垃圾回收的过程不一样
3.分布式事务
参考
二阶段提交和三阶段提交
两阶段提交(Two-Phase Commit,简称2PC)和三阶段提交(Three-Phase Commit,简称3PC)是分布式系统中用于确保数据一致性的两种不同的事务提交协议。它们之间的主要区别:
- 1.阶段数量:两阶段提交协议包含两个阶段,即准备阶段(Prepare Phase)和提交阶段(Commit Phase)。而三阶段提交协议则包含三个阶段,即准备阶段(Prepare Phase)、预提交阶段(Pre-Commit Phase)和提交阶段(Commit Phase)
- 2.资源占用:在两阶段提交协议的准备阶段,参与者会执行所有必要的操作,但不提交事务,这可能会占用一些资源。相比之下,三阶段提交协议的预提交阶段只是进行校验,不执行实际的操作,因此不会占用资源。这有助于减少不必要的资源浪费。(这也是为什么会出现三阶段提交的原因。三阶段提交先进行资源预检查,可以极大程度避免资源的激烈占用)
- 3.两阶段提交协议只在协调者中引入了超时机制。如果协调者在等待参与者的反馈时超时,它会发送回滚指令。然而,三阶段提交协议在协调者和参与者中都引入了超时机制。如果协调者在等待参与者的反馈或参与者在等待协调者的指令时超时,它们都会采取相应的措施,如中断事务或进行提交。
- 4.阻塞问题:两阶段提交协议存在阻塞等待的问题。在提交阶段,如果协调者等待参与者的反馈时发生网络故障或协调者故障,可能会导致整个系统阻塞。而三阶段提交协议通过引入预提交阶段和超时机制,可以在一定程度上解决这个问题。
总的来说,两阶段提交和三阶段提交的主要区别在于它们的阶段数量、资源占用、超时机制和阻塞问题。选择哪种协议取决于具体的应用场景和需求。例如,如果系统对资源占用比较敏感,那么三阶段提交可能是一个更好的选择。如果系统对网络故障和故障恢复有较高的要求,那么可能需要考虑使用其他协议,如Paxos或Raft。
TCC
TCC(Try-Confirm-Cancel)是分布式事务中的另一种提交协议,也被称为“补偿式事务”。TCC 与 2PC 和 3PC 的主要区别在于它采用了业务补偿的方式来确保分布式事务的一致性。TCC 模型要求业务代码在设计时就需要支持两种操作:Try(尝试执行)和 Confirm/Cancel(确认/取消)。
下面是 TCC 的基本流程:
- Try 阶段:业务逻辑首先尝试执行,完成所有必要的业务检查(一致性),预留必须的业务资源(准备阶段),但并不真正提交事务。这个阶段的主要目的是确保在后续操作中,事务能够成功提交或能够被回滚。
- Confirm 阶段:在 Try 阶段成功后,进入 Confirm 阶段。在这个阶段,会真正执行业务操作,并释放 Try 阶段预留的资源。如果这个阶段成功,则分布式事务提交成功。
- Cancel 阶段:如果 Try 阶段或 Confirm 阶段失败,则进入 Cancel 阶段。在这个阶段,会执行补偿操作,撤销 Try 阶段所做的所有更改,释放所有预留的资源。如果这个阶段成功,则分布式事务回滚成功。
CC 的优点:
- 非阻塞:与 2PC 不同,TCC 的 Confirm/Cancel 操作可以异步执行,因此不会阻塞系统资源。
- 灵活性:TCC 允许业务逻辑根据具体情况来定义如何执行补偿操作,提供了更大的灵活性。
- 避免单点故障:由于 Confirm/Cancel 操作可以分布在不同的节点上执行,因此能够避免单点故障。(其实意思就是,TCC的进度数据保存在了数据库而非二阶段的内存,因此服务挂掉对事务执行影响不大)
TCC 的缺点:
- 业务侵入:TCC 要求业务代码在设计时就需要支持 Try-Confirm-Cancel 的逻辑,这可能会增加业务开发的复杂性和工作量。
- 一致性保障:需要业务开发者确保补偿操作的正确性和完整性,否则可能会影响分布式事务的一致性。
在实际应用中,TCC 通常与分布式事务框架(如 Seata)结合使用,来简化分布式事务的管理和处理。这些框架可以帮助开发者更容易地实现 TCC 模型,并提供一致性保障。
4.kafka的offset在什么时候提交的?
kafka消费者会保存其消息进度,也就是offset。
我们更新的方式,是手动更新。fetch到消息后,等消费完,再调用commit方法手动更新offset;如果消费失败,则offset也不会更新,此消息会被重复消费一次
5.分布式锁
redis分布式锁的缺点:主要是setnx和expire不是原子的
用zk作为分布式锁可解决。zk分布式锁的原理
获取分布式锁的总体思路
a、在获取分布式锁的时候在locker节点下创建临时顺序节点,释放锁的时候删除该临时节点。
b、客户端调用createNode方法在locker下创建临时顺序节点,然后调用getChildren(“locker”)来获取locker下面的所有子节点,注意此时不用设置任何Watcher。
c、客户端获取到所有的子节点path之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。
d、如果发现自己创建的节点并非locker所有子节点中最小的,说明自己还没有获取到锁,此时客户端需要找到比自己小的那个节点,然后对其调用exist()方法,同时对其注册事件监听器。
e、之后,让这个被关注的节点删除,则客户端的Watcher会收到相应通知,此时再次判断自己创建的节点是否是locker子节点中序号最小的,如果是则获取到了锁,如果不是则重复以上步骤继续获取到比自己小的一个节点并注册监听。
5.线程池参数设置原则
参考
6.redis持久化的策略
(1)RDB:快照形式是直接把内存中的数据保存到一个dump的文件中,定时保存,保存策略。 当Redis需要做持久化时,Redis会fork一个子进程,子进程将数据写到磁盘上一个临时RDB文件中。当子进程完成写临时文件后,将原来的RDB替换掉。
(2)AOF:把所有的对Redis的服务器进行修改的命令都存到一个文件里,命令的集合。使用AOF做持久化,每一个写命令都通过write函数追加到appendonly.aof中。aof的默认策略是每秒钟fsync一次,在这种配置下,就算发生故障停机,也最多丢失一秒钟的数据。 缺点是对于相同的数据集来说,AOF的文件体积通常要大于RDB文件的体积。根据所使用的fsync策略,AOF的速度可能会慢于RDB。 Redis默认是快照RDB的持久化方式。
美团数据开发
1.String.intern()使用原理 String类型“== ” 比较
如果String s=xx;xx是常量,则s指向的是常量xx在方法区中的常量池中的地址;
xx是对象,如new String(“xx”),则s执行的是堆中的一个对象,此对象包含了指向常量xx在方法区中的常量池中的地址。
String.intern()是一个Native方法,底层调用C++的 StringTable::intern方法实现。当通过语句str.intern()调用intern()方法后,JVM 就会在当前类的常量池中查找是否存在与str等值的String,若存在则直接返回常量池中相应Strnig的引用;若不存在,则会在常量池中创建一个等值的String,然后返回这个String在常量池中的引用。因此,只要是等值的String对象,使用intern()方法返回的都是常量池中同一个String引用,所以,这些等值的String对象通过intern()后使用==是可以匹配的。
参考
2.单例模式的双检锁
为什么对象要用volatile修饰
下面的第二步和第三步可能会重排序,导致instance获取的不是完整的对象
- 分配一块内存空间
- 在这块内存上初始化一个DoubleCheckLock的实例
- 将声明的引用instance指向这块内存
3.String,StringBuffer与StringBuilder的区别
String:不可变字符序列
StringBuffer:可变字符序列,效率低,线程安全
StringBuilder:可变字符序列,效率高,线程不安全
StringBuffer与StringBuilder每当我们用它们对字符串做操作时,实际上是在一个对象上操作的,不像String一样创建一些对象进行操作,所以速度就快了
StringBuilder和StringBuffer继承自同一个父类AbstractStringBuilder, 我们可以推断出它俩的方法都是差不多的. 通过查看源码也发现确实如此, 只不过StringBuffer在方法上添加了 synchronized关键字, 证明它的方法绝大多数方法都是线程同步方法. 也就是说在多线程的环境下我们应该使用StringBuffer以保证线程安全, 在单线程环境下我们应使用StringBuilder以获得更高的效率。
4.过往项目的亮点
(1)两级缓存架构,单服务的高并发
(2)日志扫描 forkjoinpool
(3)调度架构,理论上无限扩容,能处理无限多的任务
5.过往项目的难点、遇到的问题
(1)kafka消息积压
6.java的8中基本类型和所占字节数
六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型
byte
8位、有符号的,以二进制补码表示的整数
范围为-2^7 到 2^7-1
byte 类型用在大型数组中节约空间,主要代替整数,因为 byte 变量占用的空间只有 int 类型的四分之一
short
16 位、有符号的以二进制补码表示的整数
范围为-2^15 到 2^15 - 1
一个short变量是int型变量所占空间的二分之一
int
32位、有符号的以二进制补码表示的整数
范围为 -2^31 到 2^31 - 1
long
64 位、有符号的以二进制补码表示的整数
范围是 -2^63 到 2^63 -1
float
单精度、32位、符合IEEE 754标准的浮点数
double
双精度、64 位、符合IEEE 754标准的浮点数
boolean
一位
char
一个单一的 16 位 Unicode 字符
以byte为例,符号位0表示正1表示负
对于正数,(0)1111111是2^7-1
对于负数,(1)0000000表示-128,而(1)1111111表示-1。因为在计算机中,负数是用补码表示的。对于128,二进制为10000000,补码表示时先取反,再+1。即01111111=>10000000,表示-128.
7.boolean占几个字节呢?
分两种情况:
单个的boolean变量,在编译之后,使用的是int类型,也就是true转换为1,false转换为0
boolean a=true,此时a为4个字节,占32位
boolean类型的数组,在编译后,使用的是byte[]
boolean[] a=new boolean[2],此时每个boolea占1个字节,为8位
百度图数据库
1.float double为什么会精度丢失?
根本原因是10进制转化为2进制的问题
十进制小数如何转化为二进制数
算法是乘以2直到没有了小数为止。举个例子,0.9表示成二进制数
0.9*2=1.8 取整数部分 1
0.8(1.8的小数部分)*2=1.6 取整数部分 1
0.6*2=1.2 取整数部分 1
0.2*2=0.4 取整数部分 0
0.4*2=0.8 取整数部分 0
0.8*2=1.6 取整数部分 1
0.6*2=1.2 取整数部分 0
......... 0.9二进制表示为(从上往下): 1100100100100......
注意:上面的计算过程循环了,也就是说2永远不可能消灭小数部分,这样算法将无限下去。很显然,小数的二进制表示有时是不可能精确的
2.新建对象,如果需要放到hashMap作为key时,是否需要重写hashcode和equals方法?
需要。hashcode默认是比较内存地址的,如果两个对象的属性一样,我们本意是这两个对象作为key应该是同一个key,但是key做比较时,两个对象的内存地址不同,因此需要重写,一般重写为属性的hashcode的计算公式如name.hashcode+10age;
equals同样要重写为对象的属性是否相等
3.数组在内存中的存储?
基本类型的数组在初始化之后,数组中的元素都会赋初始值,比如
byte,short,int,long会初始化为0;
boolean初始化为false;
引用类型(类、对象、接口、数组)在初始化后,初始值为null;
int[] a=new int[1024];
Integer[] b=new Integer[1024];
4.一个类初始化的问题
class Price {
static Price P = new Price(2.7);
static double apple = 20;//加上final后 输出结果为17.3
double Price;
public Price(double orange) {
Price = apple - orange;
}
}
public class PriceTest {
public static void main(String[] args) {
System.out.println(Price.P.Price);//结果为-2.7
}
}
原因:Price在类加载的准备阶段,会给变量赋初始值,此时P=null,apple=0.0。初始化时按照先后顺序,先执行new Price(2.7),此时Price=0.0-2.7=-2.7
如果给apple加上final,此时在编译时20会加入常量池,apple会替换为20
5.公平锁非公平锁的实现原理
底层是一个双向链表
公平锁的话相当于是队列,线程抢锁时需要判断前一个节点是否为空,为空才会去抢锁,先进先出
非公平锁,不做判断,直接抢锁,每个线程都有机会拿到锁
6.(1)cpu百分百原因分析
top 查看进程情况
top -Hp 25567 查看进程下的线程信息
printf ‘%x\n’ 26250 获取26250的16进制数为668a
jstack25567 |grep -A 30668a 得到该线程栈信息
(2)内存飙升的处理方法
跟上面的很相似
先top 查看进程情况,详情里面是会展示进程的CPU和内存的占用的,得到内存占用过高的进程
如果进程有问题,直接kill掉即可
如果是程序问题,想定位到某个程序,就仍然用上面的方法
top -Hp 进程号 详情里有内存信息定位到县城
然后把线程号转位16进制
jstack 进程号|grep ‘线程号’ 拿到线程堆栈信息就可以定位到代码
7.含有抽象方法的类一定是抽象类,抽象类不一定有抽象方法
8.深拷贝和浅拷贝区别是什么?
浅拷贝的对象的属性如果是引用类型,则拷贝后的对象的该引用属性与原引用属性指向的是同一个对象地址,会相互影响
浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象;
结果分析: 两个引用student1和student2指向不同的两个对象,但是两个引用student1和student2中的两个teacher引用指向的是同一个对象,所以说明是浅拷贝
9.java对象的内存大小是多少个字节?如果是继承了某个对象的子对象呢?
对象内存计算
对象内存大小=对象头+实例数据+对齐填充
reference类型在32位系统上每个占用4bytes, 在64位系统上每个占用8bytes
默认开启对象指针压缩
(1)对对象头的影响
以64位机器为例,对象头:开启指针压缩为12个字节,关闭指针压缩为16个字节
static class A {
int a;
}
关闭指针压缩: 16(对象头)+4(实例数据)=20不是8的倍数,因此需要对齐填充 16+4+4(padding)=24
开启指针压缩: 12+4=16已经是8的倍数了,不需要再padding
(2)对于reference
64位机器上reference类型占用8个字节,开启指针压缩后占用4个字节
static class B2 {
int b2a;
Integer b2b;
}
关闭指针压缩: 16+4+8=28不是8的倍数,需要对齐填充 16+4+8+4(padding)=32
开启指针压缩: 12+4+4=20不是8的倍数,需要对齐填充12+4+4+4(padding)=
(3)数组
64位机器上,数组对象的对象头占用24个字节(8字节MarkWord+8字节类型指针+8字节数组长度),启用压缩之后占用16个字节(8字节MarkWord+4字节类型指针+4字节数组长度)
new Integer[0]
因为数组对象中无任何引用,
未开启压缩:24bytes
开启压缩后:16bytes
new Integer[3]
未开启压缩:24(对象头)+ 83 = 48,不需要padding;
开启压缩:16(对象头)+ 43 = 28,需要对齐填充 28 + 4(padding) = 32,其他依次类推。
自定义类的数组
static class B3 {
int a;
Integer b;
}
new B3[3]占用的内存大小:注意里面有三个引用对象
未开启压缩:24(对象头)+ 83 = 48
开启压缩后:16(对象头)+ 43 + 4(padding) = 32
(4)复合对象
static class B {
int a;
int b;
}
static class C {
int ba;
B[] as = new B[3];
C() {
for (int i = 0; i < as.length; i++) {
as[i] = new B();
}
}
}
C对象本身的大小:
未开启压缩:16(对象头)+ 4(ba)+ 8(as引用的大小)+ 4(padding) = 32
开启压缩:12(对象头)+ 4(ba)+4(as引用的大小)+ 4(padding) = 24
C对象占用的空间总大小:
C对象本身的大小+数组对象的大小+B对象的大小。
未开启压缩:
(16 + 4 + 8+4(padding)) + (24+ 8*3) +(16+4+4)3 = 152bytes
开启压缩:
(12 + 4 + 4 +4(padding)) + (16 + 43 +4(数组对象padding)) + (12+4+4+4(B对象padding)) *3= 128bytes
(5)继承关系
首先存放父类中的成员,接着才是子类中的成员, 父类也要按照 8 byte 规定
public static class D {
byte d1;
}
public static class E extends D {
byte e1;
}
计算E对象的大小:
未开启压缩:16(对象头) + 父类(1(d1) + 7(padding)) + 1(e1) + 7(padding) = 32
开启压缩:12(对象头) + 父类(1(d1) + 7(padding)) + 1(e1) + 3(padding) = 24
10.布隆过滤器
超大的位数组和k个哈希函数
布隆过滤器添加元素
将要添加的元素给k个哈希函数
得到对应于位数组上的k个位置
将这k个位置设为1
布隆过滤器查询元素
将要查询的元素给k个哈希函数
得到对应于位数组上的k个位置
如果k个位置有一个为0,则肯定不在集合中
如果k个位置全部为1,则可能在集合中
11.什么是一致性hash?
解决分布式缓存的
通过一致性hash算法这种方法,判断一个对象应该被缓存到那台服务器上的,将缓存服务器与被缓存对象都映射到hash环上以后,从被缓存对象的位置出发,沿着顺时针方向遇到的第一个服务器,就是当前对象将要缓存的服务器,由于被缓存对象与服务器hash后的值都是固定的,所以服务器不变的情况下,一张图片必定会被缓存到固定的服务器上,那么,当下次访问这张图片时,只要再次使用相同的算法进行计算,即可算出这张图片被缓存在那个服务器上,直接去对应的服务器上查找即可
12.数据库
(1)数据库的三范式:列不可再分;属性完全依赖于主键;属性不依赖于其它非主属性 属性直接依赖于主键
(2)acid是什么:原子性、一致性、隔离性、持久性是事务的四个特性
(3)char 和 varchar 的区别:
区别一,定长和变长
char 表示定长,长度固定,varchar表示变长,即长度可变
区别二,效率
因为其长度固定,char的存取速度还是要比varchar要快得多,方便程序的存储与查找;但是char也为此付出的是空间的代价,因为其长度固定,所以会占据多余的空间,可谓是以空间换取时间效率。varchar则刚好相反,以时间换空间
varchar2虽然比char节省空间,但是一个varchar2列经常被修改,而且每次修改的数据长度不同,这会引起“行迁移的现象”,
而这造成的多余的I/O,是数据库设计中尽量避免的,在这种情况下使用char代替varchar2会更好些
区别三,存储的容量不同
对 char 来说,最多能存放的字符个数 255,和编码无关。
而 varchar 呢,最多能存放 65532 个字符
(4)数据库中的乐观锁和悲观锁
悲观锁:必须先取锁再访问修改
乐观锁:不加锁,提交时进行冲突检测和数据更新。其实现方式有一种比较典型的就是Compare and Swap(CAS)技术
乐观锁可能会遇到ABA问题,那就,每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少通过一个单独的可以顺序递增的version字段。除了version以外,还可以使用时间戳,因为时间戳天然具有顺序递增性
13.redis数据的删除策略?
定时删除:创建一个定时器,当key设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作
惰性删除:数据到达过期时间,不做处理。等下次访问该数据时,如果未过期,返回数据,发现已过期,删除,返回不存在
定期删除:redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的
14.类初始化的顺序?
15.进程间的通信方式有哪些?
管道;
消息队列;
共享存储;
信号量Semaphore;
套接字Socket;
16.new指令对象的创建过程
第一步,首先将去检查这个指令的参数能否在常量池中定位到一个类的符号引用。并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。如果没有,那就先执行类加载的过程
第二步,在堆中为新生对象分配内存。对象所需内存的大小在类加载完成之后便可完全确定
第三步,将分配到的内存空间都初始化为零值(不包括对象头)
第四步,对对象进行一些必要的设置,比如这个对象是哪个类的实例、如何才能找到类的元数据、对象的哈希码、对象的GC分代年龄等信息
第五步,执行 new 指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正的对象才算创建完成
17.什么是伪共享?
伪共享是多线程系统(每个处理器有自己的局部缓存)中一个众所周知的性能问题。缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享
18.TCP如何保证可靠传输?
在TCP的连接中,数据流必须以正确的顺序送达对方。TCP的可靠性是通过顺序编号和确认(ACK)来实现的
19.如何破坏双亲委派模型?
重写loadclass方法,不遵循规范让父类加载器加载即可
20.设计和实现lru算法,说出需要的结构和相关的操作
21.redis和zk分布式锁怎么实现?
setnx
节点下建立顺序子节点
两种分布式锁有什么优缺点?
redis读写性能高,效率高
zk可靠性高,不需要有过期时间
redis分布式锁如果事务耗时较长会导致什么问题?怎么解决?
A事务执行业务方法时间太长,此时A事务的redis锁超时被释放,B拿到锁开始执行B的业务方法,这时A刚好执行完成,在finally里释放的锁其实是B的锁,出现问题
解决方法:setnx的时候,value设置为可以标志的有意义的值。比如如果业务方法执行的肯定是不同的事务,那么value可以用事务id表示,释放锁时先get看是不是自己的事务id,不是的话就不能释放
其实zk可以解决上面的业务方法执行时间过长的问题
redis面试问题汇总
redis面试问题
22.mysql慢查询怎么分析?
方法一:通过查询日志文件
需要mysq设置为开启慢查询日志、并设置慢查询时间阈值。我们找到慢查询日志文件查看即可定位到sql
方法二:show processlist 命令
如果慢查询还在进行中,则只能通过如上命令即时查看,可以查看到进程信息、执行的sql、执行状态
通过上面的方法定位到sql之后,再通过explain分析sql优化即可
23.静态内部类和非静态内部类的区别
(1)生成一个静态内部类对象不需要外部类成员,静态内部类的对象可以直接生成:Outer.Inner in = new Outer.Inner();而不需要通过生成外部类对象来生成。这样实际上使静态内部类成为了一个顶级类
(2)静态内部类可以有静态成员,而非静态内部类则不能有静态成员
(3)静态内部类的非静态成员可以访问外部类的静态变量,而不可访问外部类的非静态变量
(4)非静态内部类的非静态成员可以访问外部类的非静态变量
24.分布式一致性协议
25.跳跃表
26.数仓分层设计理念
(1)ODS 原始数据层
(2)DW 数据仓库层
(3)DA 数据应用层(APP)
27.什么是跳跃表?它的时间复杂度是多少?
Skip List可以很好解决有序链表查找特定值的困难。它具有以下几个特征:
(1)一个跳表应该由几个层(level)组成
(2)第一层包含所有的元素,即原始表
(3)每一层都是一个有序链表
(4)如果元素出现在了第i层,则所有比i小的层都包含该元素
(5)第i层的元素通过一个down指针指向下一层的相同值的元素
(6)在每一层中,-1和1两个元素都出现,分别代表INT_MIN和INT_MAX
(7)top指针指向最高层的第一个元素
效率可比拟二叉树,查找、新增、删除的时间复杂度是O(logn),是典型的用空间换时间的操作
28.布隆过滤器原理,应用场景
布隆过滤器是一种数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”
布隆过滤器(Bloom Filter)的核心实现是一个超大的位数组和几个哈希函数。首先将位数组进行初始化,将里面每个位都设置位0。保存元素时,将元素依次通过那几个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。查询元素是否存在时,同样的方法将元素通过哈希映射到位数组上的所有的点,如果其中有一个点不为1,那么说明该元素肯定不存在
应用场景:
28.redis的数据类型和底层结构
29.java的反射机制
在运行状态中,对于任意一个类,都能够获取到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用对象的方法的功能就称为java语言的反射机制。
它的应用,比如jdk的动态代理
30.如何往一个List里面插入一个String?
这个考察的是泛型的类型擦除。在编译之后,强类型会擦去。因此可以用一个临时变量,将List< String>转为List
List<String> list = new ArrayList<>()
// 临时变量,将泛型变成原生态类型
List temp = list;
temp.add(1);
list.add("abc");
System.out.println(temp);
或者使用反射,在运行期类型已经擦除了,变为Object。我们可以通过反射调用add(Object)方法插入
List<String> list1 = new ArrayList<>();
list1.add("test1");
// 添加int类型数据报错
// list1.add(20);
Class class2 = list1.getClass();
try {
Method method1 = class2.getMethod("add", Object.class);
// 通过反射机制操作list1
method1.invoke(list1, 20);
// 输出结果[test1, 20]
System.out.println(list1);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
31.内核态和用户态是指什么?哪些命令是用户态那些命令是用户态?
内核态:运行操作系统程序,操作硬件。比如启动I/O 内存清零 修改程序状态字 设置时钟 允许/禁止终端 停机
用户态:运行用户程序
32.Redis不是一直号称单线程效率也很高吗,Redis 6.0 推出为什么又采用多线程了?
redis的单线程与多线程
我们所说的Redis单线程,指的是"其网络IO和键值对读写是由一个线程完成的",也就是说,Redis中只有网络请求模块和数据操作模块是单线程的。而其他的如持久化存储模块、集群支撑模块等是多线程的。
33.ConcurrentHashMap锁的是什么?
锁的是桶上的Node节点。那么如果桶上没有节点,那么它是怎么操作的?比如put的时候,桶为空,多个线程会同时put同一个桶,这时候是通过cas实现的
34.观察者模式说一说
指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。通常是用一个List存放观察者
35.redis快的原因?详细说一下IO多路复用
单线程、内存、IO多路复用。
36.同步、异步、阻塞、非阻塞的关系和区别?
多路复用
37.Select count(1)、count(* )、count(col)区别?
在Innodb引擎下,这俩在高版本的MySQL(5.5及以后,5.1的没有考证)是没有什么区别的。
COUNT ( ) 函数的具体含义:它可以统计某个列值的数量,也可以统计行数。在统计列值时要求列值是非空的(不统计NULL)。如果在COUNT()的括号中定了列或者列表达式,则统计的就是这个表达式有值的结果数。COUNT()的另一个作用是统计结果集的行数。当MySQL确认括号内的表达式值不可能为空时,实际上就是在统计行数。最简单的就是当我们使用COUNT()的时候,这种情况下通配符 * 并不像我们猜想的那样扩展成所有的列,实际上,他会忽略所有列而直接统计所有的行数。
问题是Innodb是通过主键索引来统计行数的吗?结论是:如果该表只有一个主键索引,没有任何二级索引的情况下,那么COUNT()和COUNT(1)都是通过通过主键索引来统计行数的。如果该表有二级索引,则COUNT(1)和COUNT(*)都会通过占用空间最小的字段的二级索引进行统计
。为什么有二级索引会用二级索引呢?这里统计行数的操作,查询优化器的优化方向就是选择能够让IO次数最少的索引,也就是基于占用空间最小的字段所建的索引(每次IO读取的数据量是固定的,索引占用的空间越小所需的IO次数也就越少)。而Innodb的主键索引是聚簇索引(包含了KEY,除了KEY之外的其他字段值,事务ID和MVCC回滚指针)所以主键索引一定会比二级索引(包含KEY和对应的主键ID)大,也就是说在有二级索引的情况下,一般COUNT()都不会通过主键索引来统计行数,在有多个二级索引的情况下选择占用空间最小的。
参考
38.mysql的undolog、redolog、binglog的作用?
binlog 是server层的日志,而redo log 和undo log都是引擎层(innodb)的日志
binlog:binlog 是作为mysql操作记录归档的日志,这个日志记录了所有对数据库的数据、表结构、索引等等变更的操作。也就是说只要是对数据库有变更的操作都会记录到binlog里面来
。binlog就相当于我们银行卡的流水
undolog:undo log是保存的是数据的历史版本,通过历史版本让数据在任何时候都可以回滚到某一个事务开始之前的状态。undo log除了进行事务回滚的日志外还有一个作用,就是为数据库提供MVCC多版本数据读的功能。
redolog:redo log 能保证对于已经COMMIT的事务产生的数据变更,即使是系统宕机崩溃也可以通过它来进行数据重做,达到数据的一致性,这也就是事务持久性的特征,一旦事务成功提交后,只要修改的数据都会进行持久化,不会因为异常、宕机而造成数据错误或丢失,所以解决异常、宕机而可能造成数据错误或丢是redo log的核心职责。
redo log记录的数据变更粒度和binlog的数据变更粒度是不一样的,也正因为这个binlog是没有进行崩溃恢复事务数据的能力的。redo log则是记录着磁盘数据的变更日志,以磁盘的最小单位“页”来进行记录。
参考
39.redis的底层结构
参考
简单动态字符串——对应string
struct sdshdr{
//记录buf数组中已使用字节的数量
//等于 SDS 保存字符串的长度
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
它是redis自定义的一种结构体,一个字符数组char[] buff,用于保存字符串,属性len记录保存的字符串的长度,free记录buff字符数组中未被使用的字节数量
好处:(1)获取字符串长度的时间复杂度为O(1)
(2)进行修改时,原生字符串修改N次需要N次内存分配,而简单动态字符串通过惰性分配内存,最多只需要N次
链表——list
有前后指针
typedef struct listNode{
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
}listNode
redis自己实现了一个双向链表。
list是通过双端链表实现的,内部有头尾指针
typedef struct list{
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数
void (*free) (void *ptr);
//节点值释放函数
void (*free) (void *ptr);
//节点值对比函数
int (*match) (void *ptr,void *key);
}list;
字典——hash
字典又被称为映射(hash),是一种保持键值对的数据结构。字典中的每一个键 key 都是唯一的,通过 key 可以对值来进行查找或修改。
redis的字典使用hash表作为底层结构:
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于 size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
}dictht
哈希表是由table数组组成,table中每个元素都是dictEntry。dictEntry 结构定义如下
包含一个key、val、next指针。当发生hash冲突时,使用链地址法(拉链法)解决冲突
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
}dictEntry
hash算法:和hashMap类似,也是先求hash值,再取模
扩容和收缩:
扩容——基于原hash表已保存的节点数2
收缩——基于原hash表已保存的节点数1/2
渐进式rehash:
什么叫渐进式 rehash?也就是说扩容和收缩操作不是一次性、集中式完成的,而是分多次、渐进式完成的。如果保存在Redis中的键值对只有几个几十个,那么 rehash 操作可以瞬间完成,但是如果键值对有几百万,几千万甚至几亿,那么要一次性的进行 rehash,势必会造成Redis一段时间内不能进行别的操作。所以Redis采用渐进式 rehash,这样在进行渐进式rehash期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上进行查找。但是进行 增加操作,一定是在新的哈希表上进行的。
跳跃表——zset
整数集合intset——set
整数集合(intset)是Redis用于保存整数值的集合抽象数据类型,它可以保存类型为int16_t、int32_t 或者int64_t 的整数值,并且保证集合中不会出现重复元素。
typedef struct intset{
//编码方式
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
}intset;
整数集合的每个元素都是 contents 数组的一个数据项,它们按照从小到大的顺序排列,并且不包含任何重复项
压缩列表——作为列表键和哈希键的底层实现
压缩列表是为了节省内存空间。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。
压缩列表的原理:压缩列表并不是对数据利用某种算法进行压缩,而是将数据按照一定规则编码在一块连续的内存区域,目的是节省内存。
40.G1是新生代和老年代收集器,那么新生代和老年代回收的过程分别是什么?
41.mysql 的a b c的联合索引,查询使用select * from table where a=N and b>M and c=P;
问题:查询时c会使用到索引么?
c不会使用到联合索引。联合索引遵循最左匹配原则,遇到范围查询(>、<、between、like)就会停止匹配。原因是,引擎在索引树上匹配索引字段的时候,使用的是二分查找算法来查找子树,因此a能使用索引,b也能,但是b是范围查询,导致c不是有序的,就不能使用二分查找算法了,因此c不能使用到索引
42.redis分布式锁和zk分布式锁的区别
redis 分布式锁和 zk 分布式锁的对比
(1)redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。
(2)zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。
(3)如果是 redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。
注:zk创建分布式锁有两种方式,一种是创建临时节点、一种是创建临时顺序节点
计算机网络
43.HTTP协议与TCP/IP协议的关系?
44.HTTP的长连接和短连接是什么?
在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个HTML或其他类型的Web页中包含有其他的Web资源(如JavaScript文件、图像文件、CSS文件等),每遇到这样一个Web资源,浏览器就会重新建立一个HTTP会话。
而从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:
Connection:keep-alive
在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。
HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接。
长连接可以省去较多的TCP建立和关闭的操作,减少浪费,节约时间;
短连接对于服务器来说管理较为简单,存在的连接都是有用的连接,不需要额外的控制手段。但如果客户请求频繁,将在TCP的建立和关闭操作上浪费时间和带宽
45.如何理解HTTP协议是无状态的?
HTTP协议是无状态的,指的是协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。也就是说,打开一个服务器上的网页和上一次打开这个服务器上的网页之间没有任何联系。HTTP是一个无状态的面向连接的协议,无状态不代表HTTP不能保持TCP连接,更不能代表HTTP使用的是UDP协议(无连接)
46.HTTP 和 HTTPS 的区别?
(1)端口:HTTP的URL由“http://”起始且默认使用端口80,而HTTPS的URL由“https://”起始且默认使用端口44
(2)安全性和资源消耗: HTTP协议运行在TCP之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS是运行在SSL/TLS之上的HTTP协议,SSL/TLS 运行在TCP之上。所有传输的内容都经过加密,加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密。所以说,HTTP 安全性没有 HTTPS高,但是 HTTPS 比HTTP耗费更多服务器资源。
47.为什么TCP链接需要三次握手,两次不可以么,为什么?
为了防止 已失效的链接请求报文突然又传送到了服务端,因而产生错误。
48.TCP协议如何来保证传输的可靠性?
(1)数据包校验:目的是检测数据在传输过程中的任何变化,若校验出包有错,则丢弃报文段并且不给出响应,这时TCP发送数据端超时后会重发数据;
(2)对失序数据包重排序:既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。TCP将对失序数据进行重新排序,然后才交给应用层
(3)丢弃重复数据:对于重复数据,能够丢弃重复数据
(4)应答机制:当TCP收到发自TCP连接另一端的数据,它将发送一个确认。这个确认不是立即发送,通常将推迟几分之一秒
(5)超时重发:当TCP发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段
49.从输入网址到获得页面的过程?
(1)浏览器查询 DNS,获取域名对应的IP地址
(2)浏览器获得域名对应的IP地址以后,浏览器向服务器请求建立链接,发起三次握手
(3)TCP/IP链接建立起来后,浏览器向服务器发送HTTP请求
(4)服务器接收到这个请求,并根据路径参数映射到特定的请求处理器进行处理,并将处理结果及相应的视图返回给浏览器
(5)浏览器解析并渲染视图,若遇到对js文件、css文件及图片等静态资源的引用,则重复上述步骤并向服务器请求这些资源
(6)浏览器根据其请求到的资源、数据渲染页面,最终向用户呈现一个完整的页面
50.当前实例对象锁后进入 synchronized 代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个 synchronized 方法,再次请求当前实例锁时,将被允许。需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于 synchronized 是基于 monitor 实现的,因此每次重入,monitor 中的计数器仍会加 1
51.序列化和反序列化
序列化是指把一个Java对象变成二进制字节流,本质上就是一个byte[]数组。只有变成字节流之后,才可以在网络上传输或持久化到文件,对象是无法在网络上传输或持久化到对象的。
有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。
在使用RPC时,我们必须使传输对象实现Serializable接口。那为什么在springboot项目中,controller接收和返回的对象里,没有实现Serializable接口呢?难道只有RPC的参数对象才需要序列化,http的参数对象不需要序列化?
答案显然是错误的。在springboot里,获取的请求对象用@RequestBody、返回的对象用@ResponseBody。有这两个注解,就会使用spring的内置的序列化和反序列化功能,默认通过jackson序列化功能把对象转化为二进制数据和反解析
52.缓存和数据库一致性问题
缓存和数据库一致性问题,看这篇就够了
- 想要提高应用的性能,可以引入「缓存」来解决
- 引入缓存后,需要考虑缓存和数据库一致性问题,可选的方案有:「更新数据库 + 更新缓存」、「更新数据库 + 删除缓存」
- 更新数据库 + 更新缓存方案,在「并发」场景下无法保证缓存和数据一致性,解决方案是加「分布锁」,但这种方案存在「缓存资源浪费」和「机器性能浪费」的情况
- 采用「先删除缓存,再更新数据库」方案,在「并发」场景下依旧有不一致问题,解决方案是「延迟双删」,但这个延迟时间很难评估
- 采用「先更新数据库,再删除缓存」方案,为了保证两步都成功执行,需配合「消息队列」或「订阅变更日志」的方案来做,本质是通过「重试」的方式保证数据最终一致
- 采用「先更新数据库,再删除缓存」方案,「读写分离 + 主从库延迟」也会导致缓存和数据库不一致,缓解此问题的方案是「延迟双删」,凭借经验发送「延迟消息」到队列中,延迟删除缓存,同时也要控制主从库延迟,尽可能降低不一致发生的概率
通过如上思考,可以得出
1、性能和一致性不能同时满足,为了性能考虑,通常会采用「最终一致性」的方案
2、掌握缓存和数据库一致性问题,核心问题有 3 点:缓存利用率、并发、缓存 + 数据库一起成功问题
3、失败场景下要保证一致性,常见手段就是「重试」,同步重试会影响吞吐量,所以通常会采用异步重试的方案
4、订阅变更日志的思想,本质是把权威数据源(例如 MySQL)当做 leader 副本,让其它异质系统(例如 Redis / Elasticsearch)成为它的 follower 副本,通过同步变更日志的方式,保证 leader 和 follower 之间保持一致
53.redis其他问题
缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级
一、缓存雪崩
原有缓存失效,所有原本应该访问缓存的请求都去查询数据库了