面试问题
1. Java
1.1. 基础
1.1.1. ==和equals
==:判断两个对象的地址是不是相等。即判断两个对象是不是同一个对象。(基本数据类型比较值,引用数据类型比较内存地址)
equals():判断两个对象是否相等,不能用于基本数据类型变量。
若类没有覆盖equals()方法,通过equals()比较时等价于“==”
若类覆盖了equals()方法,则会使用覆盖的equals()方法来判断
1.1.2. hashCode()和equals()
hashCode():获取哈希码,使用C/C++实现的,该方法通常用内存地址转换为整数之后返回。
重写equals时必须重写hashCode是因为:
(HashSet如何检查重复?)
如果两个对象相等, 则hashcode一定也是相同的。但是两个对象有相同的hashcode值,他们也不一定是相等的。以HashSet举例,HashSet会先计算对象的hashcode来判断对象的加入位置,同时也会与其他已经加入的对象的hashcode作比较。如果hashcode不相等,则HashSet会假设对象没有重复出现。如果有相同的hashcode,则会调用**equals()检查hashcode相同的对象是否真的相同。如果两者真的相同,HashSet不会让其加入成功。换句话说,如果不重写hashCode()**方法,则HashSet在判断时永远不会认为两个封装类的hashcode相同(因为他们的内存地址不会相同)
1.1.3. 泛型
提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。
在Java编译期间,所有的泛型信息都会被擦掉,这就是类型擦除
泛型的三种使用方式:
- 泛型类
- 泛型接口
- 泛型方法
泛型通配符
- ?:表示不确定的Java类型
- T(type):表示具体的一个Java类型
- K V(key value):分别表示Java键值中的key、value
- E(element):代表元素
1.1.4. 自动装箱和拆箱
- 装箱:将基本类型用他们对应的引用类型包装起来,实际上是调用了包装类的
valueOf()
方法 - 拆箱:将包装类型转换为基本类型,实际上是调用了包装类的
xxxValue()
方法
1.1.5. 常量池
Byte
、Short
、Integer
、Long
这4种包装呗默认创建了**[-128, 127]的缓存数据,Character
创建了[0, 127]**的缓存数据,Boolean
直接返回True
Or False
。
Float
和Double
没有实现常量池技术。
超出范围和new
一个对象,都是新建一个对象。调用valueOf()
时会调用常量池。
包装类对象之间值的比较,全部使用equals方法比较
1.1.6. String、StringBuffer、StringBuilder
- 可变性
String
类中使用final关键字修饰字符数组来保存字符串,private final char value[]
,所以是不可变的;
StringBuilder
与StringBuffer
都继承自AbstractStringBuilder
,使用字符数组保存字符串但是没有final
关键字修饰,所以是可变的。 - 线程安全性
String
中对象是不可变的,可以理解为常量,是线程安全的;
StringBuffer
对公共方法加了同步锁或者对调用方法加了同步锁,所以是线程安全的;
StringBuilder
没有对方法加同步锁,所以是非线程安全的。 - 性能
每次对String
进行改变的时候,都会产生一个新的String
对象,然后指针指向新对象;
StringBuffer
每次对本身进行操作;
StringBuilder
相比使用StringBuffer
能获得10%左右的性能提升,但要冒多线程不安全的风险。 - 总结
- 操作少量数据:
String
- 单线程操作大量数据:
StringBuilder
- 多线程操作大量数据:
StringBuffer
- 操作少量数据:
1.1.7. 反射
可以在运行时分析类以及执行类中的方法。通过反射可以获取任意一个类的所有属性和方法,并调用他们。
- 优点:让代码更加灵活、为各种框架提供开箱即用的功能提供了便利
- 缺点:在运行时有了分析操作类的能力,同样增加了安全问题。比如可以无视泛型参数的安全检查。
获取Class对象的4种方式:
-
知道具体类的情况
Class clz = TargetObject.class;
-
Class.forName()
传入类的路径Class clz = Class.forName("xx.xxx.TargetObject");
-
通过对象实例
instance.getClass()
TargetObject o = new TargetObject(); Class clz = o.getClass();
-
类加载器
xxxClassLoader.loadClass()
传入类的路径Class clz = ClassLoader.LoadClass("xx.xxx.TargetObject");
通过类加载器获取的Class对象不会进行初始化,即初始化步骤、静态块和静态对象不会得到执行。
1.2. 集合/容器
List
:存储的元素是有序的、可重复的Set
:无序的、不可重复的Map
:使用键值对(key-value)存储,key是无序的、不可重复的,value是无序的、可重复的
1.2.1. HashMap
JDK1.8之后,底层采用数组+链表/红黑树
来实现。数组是HashMap的主体,链表/红黑树则是解决哈希冲突而存在的。
链表转红黑树的条件:
- 链表长度大于阈值(默认为8)
- HashMap数组长度超过64
put过程:
- 获取当前元素的存放位置。key的hashCode经过扰动函数处理后得到hash值,然后通过
(n - 1) & hash
判断元素存放位置。 - 如果当前位置存在元素,需要判断存入元素与已有元素的hash值和key是否相同。相同直接覆盖,不同使用拉链法解决。
- 冲突解决。当链表长度大于阈值(默认为8)时,先调用
treefyBin()
方法,根据HashMap数组长度决定是否转换为红黑树。只有数组长度大于等于64才会进行转换红黑树操作,以减少搜索时间。否则,只是执行resize()
方法对数组扩容。 - 数组扩容会变为原来的2倍,使用头插法进行链表迁移。
扰动函数:防止一些实现比较差的hashCode()方法,使用扰动函数后可以减少碰撞。
两个关键参数:
- loadFactor加载因子
控制数组存放数据的疏密程度,loadFactor趋于1,那么数组中存放的数据(entry)也就越多越密,会使链表长度增加;loadFactor趋于0,数组中存放的数据(entry)越少,越稀。
loadFactor太大导致查找元素效率低,太小导致数组利用率低,存放数据会很分散。默认值0.75f
是一个比较好的临界值。
- threshold
threshold = capacity * loadFactor,当size >= threshold时,需要考虑对数组进行扩容。
默认容量为16,当数量达到16 * 0.75 = 12时,就需要进行扩容。
扩容是变为原来对两倍。
HashMap长度为什么是2的幂次方?
为了使HashMap存取高效,尽量减少碰撞,要尽量把数据分配均匀。在计算数组下标的时候,一定会涉及到%
取余操作。当取余操作中除数是2的幂次时,等价于其除数减一的与(&)操作。即hash % length == hash & (length - 1)
。二进制位操作&,比%运算效率要高,这就解释了为什么HashMap长度是2的幂次方。
HashMap多线程下不安全/导致死锁问题
在多线程环境下,扩容/rehash过程中容易出现死循环。
假设有两个线程对同一HashMap进行操作。且都执行到
transfer()
,线程1执行到next = e.next;
就被挂起,线程2执行完成,此时线程1指向rehash后的链表,会有顺序反转的情况。当调度回线程1之后,执行到next = e.next;
就会出现环形链表,导致死锁。
- 线程1中,e指向3,next指向7;线程2执行完rehash后,3和7的顺序发生了变化。
- 线程1执行
e = next
,导致e指向7,执行next = e.next
导致next指向3
- 线程1执行下一轮次时,会造成环形链表,执行
get(11)
便会出现死锁。
1.2.2. HashMap和HashTable区别
- 线程安全
HashMap
是非线程安全的,HashTable
是线程安全的,内部方法都经sunchronized
修饰。(现在已经废弃,使用ConcurrentHashMap
来保证线程安全) - 效率
因为线程安全问题,HashMap
效率高于HashTable
。 - 对Null key和Null value的支持
HashMap
可以存储null的key和value,但null作为键只能有一个;HashTable
不允许有null的key和value,否则会抛出异常。 - 初始容量大小和每次扩容容量大小
HashMap
初始大小为16,每次扩容变为原来的2n。HashTable
初始大小为11,每次扩容变为原来的2n + 1。 - 底层数据结构
JDK1.8以后,HashMap
在解决哈希冲突时,会在 ①链表长度大于阈值(默认为8)②数组长度大于等于64 时,转化为红黑树,以减少搜索时间。HashTable
没有这样的机制。
1.2.3. ConcurrentHashMap
JDK1.7中采用分段锁机制,划分成很多个Segment
,每一个Segment
是一个类似于HashMap
的结构,所以每一个Segment
的内部都可以扩容,但Segment
个数一旦初始化就不能改变。默认为16个,即默认最多支持16个线程并发。
JDK1.8中采用数组+链表/红黑树的底层结构。当冲突链表达到一定长度时,链表会转换成红黑树。
扩容会变为原来的2倍,使用头插法进行链表迁移。
1.2.4. ConcurrentHashMap和HashTable区别
- 底层数据结构
ConcurrentHashMap
JDK1.7使用分段数组+链表,JDK1.8使用数组+链表/红黑树 - 实现线程安全的方式
JDK1.7时ConcurrentHashMap
使用分段锁,多线程访问容器里的不同数据段的数据,就不会存在锁竞争。JDK1.8使用synchronized
和CAS操作
。而HashTable
使用synchronized
来控制同一把锁,来保证线程安全,效率十分低下。
CAS操作:Compare and Swap,涉及三个参数(内存地址V,旧的预期值A,要修改的新值B),更新一个变量的时候,只有当变量的预期值A和内存地址V中的实际值相同时,才会将内存地址V对应的值修改为B。可用“多线程自增操作”举例。
1.2.5. ArrayList
底层是数组队列,相当于动态数组,它的容量能动态增长。
扩容机制:
-
需要扩容时,将新容量设置为旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
-
如果新容量小于最小所需容量,则将最小所需容量作为新容量
if (newCapacity - minCapacity < 0) newCapacity = minCapacity;
-
如果最小所需容量小于最大容量,则新容量为
Integer.MAX_VALUE
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // 最小所需容量小于0则溢出
-
最后调用
Arrays.copyOf()
进行扩容
添加大量元素前最好先使用
ensureCapacity
方法,以减少增量重新分配的次数,提高效率。
1.2.6. LinkedList
是一个实现了List
接口和Deque
接口的双端链表。底层接口是链表,使它支持高校的插入和删除操作,实现了Deque
接口,使它也具有了队列的特性。linkedList
不是线程安全的,如果想使LinkedList
变成线程安全的,可以调用Collections
中的synchronizedList
方法。
1.2.7. ArrayList和LinkedList区别
- 线程安全
都是不同步的,都不保证线程安全 - 底层数据结构
ArrayList
底层是**Object
数组**
LinkedList
底层是双向链表 - 插入和删除操作受元素位置影响
ArrayList
数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。
LinkedList
链表存储,所以插入和删除元素的时间复杂度不受元素位置影响。 - 快速随机访问
由于底层数据结构的不同,ArrayList
支持随机元素访问,而LinkedList
不支持。 - 内存占用
ArrayList
的空间浪费主要体现在list列表结尾会预留一定的容量空间。
LinkedList
的空间浪费体现在每一个元素都需要一定额外空间存放直接前驱和直接后继的指针。
1.3. 锁
1.3.1. synchronized
synchronized
关键字解决的是多个线程之间访问资源的同步性,synchronized
关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
使用:
- 修饰实例方法
作用与当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
synchronized void method() {
// 业务代码
}
- 修饰静态方法
给当前类加锁,作用于类的所有对象实例,进入同步代码前要获得当前class的锁。
访问静态synchronized
方法占用的锁是当前类的锁,而访问非静态synchronized
方法占用的是当前实例对象锁。
synchronized static void method() {
// 业务代码
}
- 修饰代码块
指定加锁对象
synchronized(this|object)
表示进入同步代码库前要获得给定对象的锁。
synchronized(类.class)
表示进入同步代码前要获得当前class的锁。
synchronized(this) {
// 业务代码
}
总结:
synchronized
关键字加到static
静态方法和synchronized(class)
代码块上都是给Class类上锁。synchronized
关键字加到实例方法上是给实例对象加锁。
1.3.2. synchronized锁的分类/优化
级别从低到高依次是:
- 无锁
- 偏向锁
- 轻量级锁
- 重量级锁
锁是可以升级的,但不能降级。
偏向锁:
针对于一个线程而言,线程获得锁之后就不会再有解锁等操作,节省很多开销。
当一个线程访问同步块并获取锁时,会在锁对象的对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要进行CAS操作来加锁和解锁。
当其他线程尝试金正偏向锁时,持有偏向锁的线程才会释放锁,会升级成轻量级锁。
轻量级锁:
出现两个线程来竞争锁,偏向锁失效,升级成轻量级锁。
线程在执行同步块之前,JVM会现在当前线程的栈帧中创建锁记录空间,并尝试使用CAS替换锁记录指针。如果成功,当前线程获得锁;如果失败,表示其他线程竞争锁,当前线程使用自旋锁来获取锁。
解锁时,会使用CAS操作将当前线程锁记录还原,如果成功,表示没有竞争发生;如果失败,表示当前锁存在竞争,升级成重量级锁。
比较
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步代码方法的性能相差无几 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问的同步场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 始终得不到锁竞争的线程,使用自旋锁会消耗CPU | 追求响应时间,同步执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不消耗CPU | 线程堵塞,响应时间慢 | 追求吞吐量,同步块执行时间长 |
1.4. JVM
1.4.1. 内存模型
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆
- 方法区
- 直接内存(非运行时数据区的一部分)
1.4.2. 虚拟机栈
Java内存可以简单分为堆内存(heap)和栈内存(stack)。局部变量表主要存放各种基本数据类型和对象引用。
会出现两种错误:StackOverFlowError
(不允许动态扩展且超过最大深度)和OutOfMemoryError
(允许动态扩展但无法申请足够内存)。
Java栈中保存的主要内存是栈帧,每一次函数调用都会压入栈帧,调用结束后弹出栈帧。
1.4.3. 本地方法栈
虚拟机栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。
1.4.4. 堆
存放对象实例。是垃圾收集器管理的主要区域。基本都采用分代垃圾收集算法。
通常分为3个区域:
- 新生代(Eden、From、To)
- 老年代
- 永生代(JDK1.8以后被移除,使用元空间[直接内存])
对象在内存不同区域:
- 优先在Eden分配
- 大对象直接进入老年代(避免复制带来的低效率)
- 长期存活对象进入老年代(新生对象在From-To往复一次增加一岁,当对象年龄增加到一定程度[默认15],就会放到老年代中)
1.4.5. Java对象的创建
-
类加载检查
遇到new
指令时,首先在常量池中定位到这个类的符号引用,检查该类是否已被加载过、解析和初始化。没有则执行相应的类加载过程。 -
分配内存
在类加载检查通过后,虚拟机为新生对象分配内存。 -
初始化零值
这一步保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,访问到的是零值。 -
设置对象头
虚拟机对对象进行必要的设置,信息包括该对象是哪个类的实例、如何找到元数据信息、哈希值、GC分代年龄等信息,存放在对象头中。 -
执行init方法
按照开发人员的需求进行初始化,如执行构造方法。
1.4.6. GC垃圾回收机制
-
引用计数法
给对象添加一个引用计数器,有地方引用就+1;引用失效就-1.当计数器为0表明对象不再被使用。无法解决对象之间相互循环引用的问题。 -
可达性分析算法
基本思想:通过一系列成为GC Roots的对象(栈、方法区的引用对象、同步锁持有对象)为起点,从这些节点开始向下搜索,节点走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象不可用。 -
标记-清除算法
分为标记和清除两个阶段:首先标记出所有不需要回收的对象,再清除掉没被标记的对象。
存在两个问题:①效率问题 ②空间问题(产生大量不连续碎片) -
标记-复制算法
解决了效率问题
将内存分为大小相同的两块,每次使用其中一块,当该块内存使用完,就将存活对象复制到另一块区,再把使用的空间一次清除。 -
标记-整理算法
针对老年代提出的标记算法,标记存活对象,然后将所有存活对象向一端移动,直接请去掉边界以外的内存。 -
分代收集算法
根据不同的对象存活周期将内存分为几块,根据各个年代的特点选择合适的垃圾收集算法。
- 新生代:标记-复制算法,每次收集都会有大量对象死去。
- 老年代: 标记-清除/整理算法,对象存活率较高。
1.5. 流Stream(JDK1.8)
Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
这种风格将要处理的元素集合看作一种流,流在管道中传输,并且可以在管道的节点上进行处理,比如筛选、排序、聚合等。
stream of element -----> filter -----> sorted -----> map -----> collect
List<Integer> transactionsIds =
widgets.stream()
.filter(b -> b.getColor() == RED)
.sorted((x, y) -> x.getWeight() - y.getWeight())
.mapToInt(Widget::getWeight)
.sum();
数据源:流的来源,可以是集合、数组、I/O channel、生成器Generator等。
聚合操作:类似SQL语句一样的操作,比如filter、map、reduce、find、match、sorted等。
Pipelining:中间操作都会返回流对象本身。这样的多个操作串成一个管道,如同流式风格。这样可以对操作进行优化,如延迟执行和短路*。
内部迭代:以前对集合遍历都是通过Iterator或者For-Each方式,显示的在集合外部迭代,这是外部迭代。Stream提供了内部迭代方式,通过访问者模式实现。
常用方法:
- stream() - 创建流。
- parallelStream() - 创建并行流。
- forEach - 迭代流中每个数据。
- map - 映射每个元素到对应的结果。
- filter - 通过设置的条件过滤出元素。
- limit - 获取指定数量的流。
- sorted - 按照指定方式排序。
- Collectors -
collect(Collectos.xx)
实现归约操作,将流转换成集合和聚合元素。 - 统计summaryStatistics - 用于int、double、long等基本类型上,产生统计结果,如最大/小值、所有数之和、平均数等。
1.6. Lambda表达式
(parameters) -> expression
(parameters) -> { statements; }
特征:
- 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值。
- 可选参数圆括号:多个参数需要定义圆括号。
- 可选大括号: 主体包含多个语句,需要使用大括号。
- 可选返回关键字:带有大括号需要知名表达式返回了一个数值。
- 变量作用域:lambda可以访问外层局部变量,这个变量要么标记为
final
,要么不会被后续代码修改(隐式final含义)。
2. 数据库
2.1 MySQL
2.1.1. 索引
索引是一种用于快速查询和检索数据的数据结构。
常见的索引有:B树、B+树、Hash
优点:
- 加快数据检索的速度(减少检索的数据量)
- 创建唯一性索引,可以保证数据库表中每一行数据的唯一性
缺点:
- 创建和维护索引需要耗费许多时间。对表中数据进行删改时,有索引还要对索引修改,降低SQL执行效率
- 索引需要物理文件存储,耗费一定空间
不使用Hash作为索引:
- Hash冲突问题。
- Hash不支持顺序和范围查询。
不使用B树作为索引:
- B树所有节点既存放key也存放value,检索时IO效率较低;B+树只有叶子结点存放key和value,其他结点只存放key。
- B树的叶子结点是独立的,而B+树的叶子结点有一条引用链。
- B树检索过程相当于对每个节点关键字做二分查找,检索效率不稳定;B+树检索稳定,任何查找都是从根结点到叶子结点。
聚集索引:
聚集索引即索引结构和数据一起存放的索引。主键索引属于聚集索引。
优点:查询速度快,定位到索引的节点,相当于定位到了数据。
缺点:①依赖于有序数据 ② 更新代价大(修改索引列数据时对应的索引也要修改)。
非聚集索引:
非聚集索引即索引结构和数据分开存放的索引。二级索引属于非聚集索引。
优点:更新代价小。
缺点:①依赖于有序数据 ②会有回表(二次查询),第一次查到索引对应的主键,第二次再根据主键去查找数据。
2.1.2. 事务
一个或一组sql语句组成当执行单元,这个执行单元要么全部执行,要么全部不执行。
属性ACID
- 原子性A:操作不可分割,要么全部提交成功,要么全部失败回滚
- 一致性C:数据库在事务执行前后都保持一致性状态。所有事务对同一个数据的读取结果都是相同的。
- 隔离性I:一个事务所做的修改在最终提交以前,对其它事务是不可见的。
- 持久性D:事务提交后对数据库的改变是永久的
并发问题
对于两个事务t1、t2
- 丢失修改:t1先修改并提交生效,t2随后修改同一个数据,t2的修改覆盖了t1的修改
- 脏读:t1读取了已经被t2更新但还没提交的字段之后,若t2回滚,t1读取的内容就是临时且无效的
- 不可重复读:t1读取了一个字段,然后t2更新了该字段之后,t1再次读取同一字段,得到的值不同
- 幻读:t1读取一个字段,t2又插入一些新行,t1再读取同一个表,会多出几行
隔离级别
- read uncommitted读未提交:允许事务读取违背其他事务提交的更改,可能出现脏读、不可重复读和幻读
- read committed读已提交:只允许事务读取已经被其他事务提交的更改,可以避免脏读,但可能出现不可重复读和幻读
- repeatable read可重复读:(MySQL默认)确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间禁止其他事务对这个字段进行更新,避免脏读和不可重复读,仍可能出现幻读
- serializable串行化:确保事务可以从一个表中读取相同的行,在这个事务持续期间禁止其他事务对该表执行插入更新删除操作,所有并发问题都能避免,但性能低。
2.1.3. 日志文件
二进制日志binlog
记录所有数据库表结构变更、表数据修改。
事务日志
-
undo log
事务开始之前,在操作任何数据之前,首先将需要操作的数据备份到一个地方。
为了实现事务持久性。 -
redo log
将事务中操作的任何数据的最新版本备份到一个地方。
为了实现事务原子性。
2.1.3. 锁机制
读写锁X/S
- 互斥锁(Exclusive),简写为 X 锁,又称写锁。
- 共享锁(Shared),简写为 S 锁,又称读锁。
有以下规定:
- 一个事务对数据对象A加了X锁(表明读写),就可以进行读取和更新。加锁期间其他事务不能对A加任何锁。
- 一个事务对数据对象A加了S锁(表明只读),可以对A读取但不能更新。加锁期间其他事务能对A加S锁,不能加X锁。
存在问题:在行/表锁的情况下,事务T要对表A加X锁,需要检测是否有其他事务对表A或者表A中任意一行加了锁,对每一行都需要检测,十分耗时。
意向锁IX/IS
IX/IS都是表锁,表明想要在表中的某行数据加X/S锁。
有以下规定:
- 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁。
- 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。
2.1.4. 性能优化
查询优化:
where
后的字段适合作为索引提高查询效率。- 使用唯一字段索引。
- 使用短索引。对字符串列进行索引,指定一个前缀长度,节省索引空间和磁盘IO。
- 利用最左前缀。多列索引应该将使用频率最高的列放在最左。
- 不要过度索引。索引太多会导致磁盘占用高,
insert
和update
耗时增加。
表结构优化:
-
垂直拆分
主键和常用字段放在一张表中,主键和其他字段放在另一张表。
可以减少IO次数,但是查询所有数据需要join
查询。 -
水平拆分
根据某一列的值把数据放到多个独立的表中。
减少查询读取的数据量,提高IO速度,但会增加查询的复杂度。 -
逆规范化
增加冗余列,避免联合查询。
增加派生列,保存中间计算结果,避免重复计算。
重新组表,将经常联合查询的表组成一个表,减少联合查询。
2.2. Redis
Redis是一个数据库,与传统数据库不同的是,Redis的数据是存在内存中的。由于它是内存数据库,读写速度非常快,因此被广泛用于缓存方向。
Redis用处:
- 缓存
- 分布式锁
- 消息队列
Redis提供了多种数据类型来支持不同的业务场景,还支持事务、持久化、Lua脚本、多种集群方案。
2.2.1. 数据结构
-
string
一般用在需要计数的场景,如用户访问次数、热点文章点赞转发量等。 -
list
易于元素的插入/删除,但随机访问困难。
一般用在发布与订阅或者消息队列、慢查询。 -
hash
类似HashMap。
适合用于存储对象。 -
set
类似HashSet。
用于存放不能重复的数据,以及交集、并集、差集运算。 -
sorted set
相比set
增加了一个权重参数score
,使集合中的元素按score
进行有序排序。
适用于排序场景中,如礼物排行榜、弹幕消息等。 -
bitmap
存储连续的二进制数字(0/1),一个bit位来表示某个元素对应的值或者状态。
适用于需要保存状态信息并需要进一步对这些场景进行分析的场景。如用户签到情况、活跃用户情况、用户行为统计。
2.2.2. 删除策略
-
惰性删除
只会在取出key的时候进行过期检查,对CPU友好,但可能会造成太多key没有被删除。 -
定期删除
每隔一段时间抽取一批key执行过期删除key操作,对内存友好。Redis底层会通过限制删除操作执行的时长和频率来减少对CPU时间的负担。
Redis采用定期删除+惰性删除。
2.2.3. 内存淘汰机制
6种数据淘汰策略:
针对设置了过期时间的key:
- volatile-lru(最近最少使用)
- volatile-ttl(即将过期)
- volatile-random(随机)
- volatile-lfu(最不经常使用)
针对所有key:
- allkeys-lru(最近最少使用)
- allkeys-random(随机)
- allkeys-lfu(最不经常使用)
其他:
- no-eviction(禁止驱逐数据)
2.2.4. 持久化策略
快照持久化(RDB)
通过创建快照来获得存储在内存中的数据在某个时间点上的副本。
创建快照后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(主从结构,提高性能),并且快照留在原地以便重启服务器时使用。
AOF持久化
实时性更好,每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入到AOF文件中。
3种AOF方式:
- always:每次修改都写入,会降低Redis性能
- everysec:每秒同步一次,会将多个写命令同步到硬盘
- no:让操作系统决定何时同步
2.2.5. 缓存雪崩
概念:
缓存在同一时间大面积失效,后面的请求都直接落到数据库上,造成数据库短时间内承受大量请求。
解决办法:
针对Redis服务不可用:
- 采用Redis集群,避免单机出现问题导致整个缓存服务无法使用。
- 限流,避免同时处理大量请求。
针对热点缓存失效:
- 设置不同的失效时间。
- 缓存永不失效。
2.2.6. 缓存穿透
概念:
大量请求的key根本不存在于缓存中,导致请求直接到了数据库上。
解决办法:
-
缓存无效key
如果缓存和数据库都查不到某个key的数据,就写一个到Redis中并设置过期时间。 -
布隆过滤器
借助布隆过滤器,可以非常方便的判断一个给定数据是否存在于海量数据中。
布隆过滤器:判定某个元素不存在,该元素一定不存在;判定某个元素存在,该元素有可能不存在。
2.2.7. 一致性/读写策略
Cache Aside Pattern(旁路缓存模式)
- 遇到写请求,更新DB
- 删除cache
若删除cache失败,可以采用 ①缓存失效时间变短 ②增加cache更新重试机制。
Read/Write Through Pattern(读写穿透)
写(Write Through):
- 先查cache,cache中不存在,直接更新DB。
- cache中存在,先更新cache,然后cache服务自己更新DB(同步更新cache和DB)。
读(Read Through):
- 从cache中读取数据,读取到就直接返回。
- 读取不到,先从DB加载,写入到cache后返回响应。
Write Behind Pattern(异步缓存写入)
只更新缓存,不直接更新DB,使用异步批量方式更新DB。
适合数据经常变化但对数据一致性要求没那么高的场景,如浏览量、点击量。
3. 开发框架
Spring
IOC
AOP
Spring MVC
MyBatis
插件原理
延迟加载
4. 设计模式
4.1. 单例模式
作用:保证一个类只有一个实例,并且提供一个访问该实例的全局访问点。
常见场景:Windows任务管理器、回收站,项目中读取配置文件的类,数据库连接池
4.2. 工厂模式
作用:实现了创建者和调用者分离。实例化对象不使用new
,用工厂方法代替。
常见场景:日志记录器、数据库访问等。
3种类型:
- 简单工厂方法:
生产同一等级结构中的任意产品,新增产品要覆盖已有代码。 - 工厂方法:
生产同一等级结构中固定产品,支持增加任意产品,只需增加类。 - 抽象工厂方法:
围绕超级工厂创建其他工厂,相当于工厂的“工厂”。
4.3. 原型模式
作用:新建对象都是对原型对象的克隆,即深拷贝。
常见场景:类初始化较为耗费资源、一个对象有多个修改者。
- 实现Cloneable接口。
- 重写Clone()方法。
4.4. 建造者模式
作用:将一个复杂对象的构建与它的表示分离,使同样的构建过程可以创建不同的表示,在用户不知道对象构造过程和细节的情况下直接创建复杂对象。
常见场景:需要生成的对象具有复杂的内部结构。
4.5. 适配器模式
作用:使原本不兼容的接口能够一起工作,常用对象适配器,即关联关系实现。
常见场景:修改一个正常运行的系统接口。
4.6. 观察者模式
作用:在对象间的一对多的依赖关系中,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
常见场景:一个对象的改变导致其他若干对象也会发生改变。
4.7. 代理模式
作用:为其他对象提供一种代理以控制对这个对象的访问。
常见场景:远程代理、Cache代理等。
4.8. 策略模式
作用:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换,防止if...else
所带来的复杂和难以维护。
常见场景:让类动态的选择使用的一种或几种行为。
5. 计算机网络
5.1. TCP/IP各层的结构和功能
-
应用层
通过应用进程间的交互来完成特定网络应用。
协议:HTTP、SMTP、DNS -
运输层
负责向两台主机进程之间的通信提供通用的数据传输服务。
向应用层提供服务。
协议:TCP、UDP -
网络层
选择合适的网间路由和交换节点,确保数据及时传送。 -
数据链路层
负责相邻的两台主机之间的数据帧传送。 -
物理层
相邻计算机节点之间的比特流传送。
5.2. TCP
5.2.1. 三次握手
- 客户端 - 发送带有
SYN
标志的数据包。 - 服务端 - 发送带有
SYN/ACK
标志的数据包。 - 服务端 - 发送带有
ACK
标志的数据包。
原因:
三次握手的目的是建立可靠的通信信道,使通信双方确认对方的发送与接收是正常的。
5.2.2. 四次挥手
- 客户端 - 发送一个
FIN
,用来关闭客户端到服务器的数据传送。 - 服务器 - 收到
FIN
,发回一个ACK
,确认序号为收到的序号+1。 - 服务器 - 关闭与客户端的连接,发送一个
FIN
给客户端。 - 客户端 - 发回
ACK
报文确认,将确认序号设置为收到的序号+1。
任何一方在数据传送结束后发出连接释放的同志,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭TCP连接。
5.2.3. 保证可靠传输
- 应用数据被分割成合适的数据块。
- 对包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
- 校验和:校验和有差错便会丢弃这个报文段并不确认收到此报文段。
- 接收端会丢弃重复的数据。
- 流量控制:双方都有固定大小的缓冲空间,接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。(使用滑动窗口协议进行流量控制)
- 拥塞控制:网络拥塞时减少数据的发送。
- ARQ协议:基本原理是每发完一个分组就停止发送,等待对方确认,收到确认后再发下一个分组。
- 超时重传:发出一个段后会启动一个定时器,若不能及时收到一个确认,将重发这个段。
5.3. TCP和UDP区别
UDP在传送数据前不需要先建立连接,目的主机收到UPD报文后不需要给出任何确认,是一种无连接的不可靠交付。一般用于即时通信。
TCP提供面向连接的可靠服务,在传送数据前必须建立连接,传送结束后释放连接。不提供广播或多播服务。为了保证可靠传输,需要额外开销。一般用于文件传输、发送和接收邮件、远程登录等。
5.4. ARQ协议
自动重传请求通过使用确认和超时两个机制,在不可靠服务的基础上实现可靠信息传输。
5.4.1. 停止等待协议
基本原理:每发完一个分组就停止发送,等待对方确认(回复ACK),超时后还没有收到确认,会自动重传。直到收到确认后再发下一个分组。
接收方如果收到重复分组,就丢弃该分组,同时发送确认。
优点:简单。
缺点:信道利用率低。
异常情况:
-
确认丢失
确认消息在传输过程中丢失。
当A向B发送消息M时,B收到后发回确认,但在传输过程中丢失,而A不知道。在超时计时过后,A重传M,B再次收到后采取以下措施:①丢弃这个重复的消息 ②向A发送确认消息。 -
确认迟到:
确认消息在传输过程中迟到。
当A向B发送消息M时,B收到并发送确认。在超时时间内没有收到确认,A重传消息M,B接收到后继续发送确认消息。此时A收到了B第二次发送的确认消息。过了一会收到了B第一次发送的确认消息。A采取以下措施:①A收到重复确认后直接丢弃 ②B收到重复消息M后,直接丢弃。
5.4.2. 连续ARQ协议
连续ARQ协议可提高信道利用率。发送方维持了一个发送窗口,位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明这个分组为止的所有分组都已经正确收到。
优点:信道利用率高,即使确认丢失,也不必重传。
缺点:不能像发送方反映出已经正确接收到的所有分组信息。当中间第N个包丢失后,N后所有的包都需要重传。
5.5. 拥塞控制
拥塞控制防止过多数据注入到网络中。
TCP发送方要维持一个**拥塞窗口(cwnd)**的状态变量。拥塞控制窗口的大小取决于网络的拥塞程度,且动态变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接口窗口中较小的一个。
4种算法:
- 慢开始:由小到大逐渐增大发送窗口,cwnd初始值为1,每经过一个传播轮次,cwnd加倍。
- 拥塞避免:让cwnd缓慢增大,即每经过一个RTT就把发送方的cwnd加1。
- 快重传和快回复:能够快速的恢复丢失的数据包。当接收方接收到一个不按顺序的数据段,会立即给发送机发送一个重复确认。如果发送方收到三个重复确认,就认定确认段指出的数据段发生了丢失,并立即重传丢失的数据段。
5.6. 打开一个网页的过程
- DNS解析,查找域名的IP地址;
- 建立TCP连接;
- 浏览器向web服务器发送HTTP请求;
- 服务器处理请求应返回HTTP报文;
- 浏览器解析并渲染页面;
- 连接结束。
使用协议:
- DNS:获取域名对应IP;
- TCP:与服务器建立TCP连接;
- IP: 建立TCP连接时,需要发送数据,在网络层使用IP协议;
- OSPF:IP数据包在路由间传输时使用OSPF协议;
- ARP:路由器与服务器通信时,需要将IP地址转换为MAC地址;
- HTTP:TCP连接建立完成后,使用HTTP访问网页。
5.7. HTTP
5.7.1. 状态码
类别 | 原因 | |
---|---|---|
1XX | Informational(信息性状态码) | 接受的请求正在处理 |
2XX | Success(成功状态码) | 请求正常处理完毕 |
3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求} |
4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
5.7.2. HTTP长连接、短链接
HTTP/1.0默认使用短链接,也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就终端连接。
HTTP/1.1起,默认使用长连接,用以保持连接特性。长连接在响应头中加入代码Connection:keep-alive
.使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的TCP连接不会关闭,客户端再次访问时,会继续使用这一条已经建立的连接。这个连接不是一直存在的,有一个保持时间,在服务器上进行设定。
5.7.3. HTTP与用户状态
HTTP是一种无状态(stateless)协议,为了保存用户状态,使用Session机制。
Session主要作用时通过服务端记录用户状态。典型场景是购物车。服务端给特定的用户创建特定的Session就可以标识这个用户并跟踪,一般情况下服务器会在一定时间内保存这个Session,超过时间限制,就会销毁这个Session。
服务端Session保存常用方法是内存和数据库。通过在Cookie中附加一个Session ID来跟踪用户。
5.7.4. Cookie和Session区别
Cookie一般用来保存用户信息,保存在客户端。
- 在Cookie中保存已经登陆过的用户信息,下次访问页面时自动填写。
- 保持登录,下次再访问网站无需重新登陆。
使用Token实现,第一次登陆时存放Token在Cookie中,下次登陆时根据Token查找用户。重新登陆将Token重写。
- 访问登陆后网站的其他页面不需重新登陆。
Session通过服务端记录用户状态,保存在服务器。
5.7.5. HTTP和HTTPS区别
- 端口:HTTP默认端口为80,HTTPS默认端口443.
- 安全性和资源消耗:HTTP运行在TCP之上,传输内容是明文,客户端和服务器都无法验证对方身份。HTTPS是运行在SSL/TLS之上HTTP协议,SSL/TLS运行在TCP之上,所有传输内容都经过加密。加密采用了对称加密,而对称加密的密钥用服务器证书进行了非对称加密。
所以说,HTTP安全性没有HTTPS高,但是更为节省服务器资源。
5.8. RPC远程过程调用
RPC是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。
5.8.1. 基本原理
- 服务消费方(client)调用以本地调用方式调用服务。
- client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体。
- client stub找到服务地址,并将消息发送到服务端。
- server stub收到消息后进行解码。
- server stub根据解码结果调用本地的服务。
- 本地服务执行并将结果返回给server stub。
- server stub将返回结果打包成消息并发送至消费方。
- client stub接收到消息,并进行解码。
- 服务消费方得到最终结果。
RPC让分布式或者微服务系统中不同服务之间的调用像本地调用一样简单。
RPC只是一种设计,是概念性的东西,一般包括传输协议和序列化协议,而HTTP是一个协议。
Web Service是一套RPC规范,一般属于基于HTTP的、XML文本的、跨平台(平台中立)的,功能完善、体系成熟、支持事务、支持安全机制,广泛应用在金融、电信领域。
6. 操作系统
进程&线程
7. 分布式
7.1. CAP理论
C:Consistency(一致性):所有节点访问同一份最新的数据副本。
A:Availability(可用性):非故障的节点在合理的时间内返回合理的响应(不是错误或者超时的响应)。
P:Partition Tolerance(分区容错性):分布式系统出现网络分区的时候,仍然能够对外提供服务。
分区容错性P是必须要实现的,在此基础上只能满足可用性A或者一致性C。
如果系统发生“网络分区”,才需要考虑选择CP还是AP。
7.2. BASE理论
BA:Basically Available(基本可用):允许损失部分可用性,不等价于系统不可用。允许响应时间和系统功能上的损失。
S:Soft-state(软状态):允许系统中的数据存在中间状态,允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
E:Eventually Consistent(最终一致性):强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。
一致性的3种级别:
- 强一致性:系统写入了什么,读出来就是什么。
- 弱一致性:不一定可以读取到最新写入的值,也不保证多少时间之后读取到的数据是最新的,只会尽量保证某个时刻达到数据一致的状态。
- 最终一致性:弱一致性的升级版,系统保证会在一定时间内达到数据一致的状态。
核心思想:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
牺牲数据的一致性来满足系统的高可用性,系统中一部分数据不可用或者不一致时,仍需保持系统整体“主要可用”。
BASE理论本质上是对CAP中AP方案的一个补充。