java面试总结
java基础
为什么java中只有值传递?
java中基本类型是通过copy传递值的,引用类型是通过copy引用传递的,所以java中只有值传递。
java序列化
java不建议使用自带序列化 Serializable
。缺点是: 只适用于java,无法跨语言;效率低,生成的字节数组较大;不安全。建议使用kryo、protobuf
等,java建议使用kryo
。
反射
通过反射获取类信息,会跳过jvm安全检查机制。灵活性好。
代理模式
- 静态代理: 为每一个类生成一个代理类,通过jvm编译成class文件。拓展性不好,每次原始类新增方法时,都需要同步为代理类拓展方法。每个类都要生成一个代理类,增加工作量。
- 动态代理:在jvm运行期间,自动生成字节码文件创建代理对象。拓展性好、简单。
- java动态代理:必须要基于接口进行代理。
InvocationHandler、Proxy
。性能更好。 - cglib代理:通过为原始类生成子类的方式实现代理。
MethodInterceptor、Enhancer
- java动态代理:必须要基于接口进行代理。
bigdecimal
java原生浮点数float、double计算可能存在精度的丢失。浮点数小数部分转二进制是通过*2取整数位来实现的。会出现无限循环。直接运算可能出现精度丢失。
使用bigdecimal不能使用equals方法比较适否相等,equals会比较精度,1.0和1.00是不想等的。需要通过.compareTo比较。不能通过new BigDecimal(float/double)
来创建。通过.valueOf()
创建。
Unsafe类
- 内存操作:allocateMemory分配内存,setMemory设置内存值,copyMemory复制内存,freeMemory释放内存
- 内存屏障:
loadFence、storeFence、fullFence
、解决指令重排序问题。 - cas: cas是cpu原语cmpxchg命令,比较并交换 compareAndSwap,参数包含(object, offset, expected, update)。
- class操作:类加载,静态变量操作。
- 锁: park挂起线程,unpark恢复线程。
- 对象操作:获取对象内字段偏移量,对象内字段内容修改,对象实例化。
- 数组操作:定位数组偏移量。
- 系统信息:指针大小,内存大小。
SPI机制
java实现的服务发现机制。META-INF/services/
目录下,接口全路径名文件,内容为实现类全路径名。
通过ServiceLoader
来加载实现类,存在并发问题。
语法糖
在jvm层面解语法糖,将语法糖变成普通的实现。for-each、基本类型自动装箱拆箱、try-with-resources、匿名内部类、lambda、泛型(java是假泛型,存在泛型擦除)、switch字符串,字符串计算hash,还是基于整数的。
集合
-
List:有序,可重复
- ArrayList: 底层结构为数组。
- 通过
new ArrayList()
创建集合对象时,数组是空的,并没有实际分配内存,只有添加第一个元素的时候,才会分配数组大小,默认数组大小为10,超过此大小时,将会触发1.5倍扩容。最大容量为Integer.MAX。只有新容量大于Integer.MAX-8且最小需要的容量大于Integer.MAX-8才会给这么大。
- 通过
- Vector:旧版本集合,不在使用,线程安全,性能差,数组实现。
- LinkedList: 底层结构为链表。
List统一使用ArrayList。
- ArrayList: 底层结构为数组。
-
Set:无序(遍历顺序与插入顺序不一致,按照hash值排序),不可重复
- HashSet: 底层结构为hashmap。
- LinkedHashSet:LinkedHashMap。
- TreeSet:TreeMap
-
Queue:队列,先进先出,有序可重复
- ArrayQueue:基于数组的双端队列
- PriorityQueue:
- BlockingQueue: 阻塞队列
-
Map:键值对
-
HashMap:1.8:数组+链表+红黑树,1.7:数组+链表。
- 为了方便通过hash值计算位置,所以数组长度必须为2的幂次方。同时为了方便扩容(当发生扩容时,要么就是在原位置,要么就是在原位置+原数组长度的位置)。
- 当链表长度大于8时,会触发转红黑树,转红黑树之前会查看数组是否大于64,不够的话会触发扩容。当树的大小小于6时,树会退化为链表。
- 当前大小大于阈值(capacity*负载因子默认0.75)时,会触发扩容。
-
LinkedHashMap:hashMap+双向链表,将key通过链表关联,保证顺序
-
Hashtable:数组+链表,线程安全,性能差,方法是synchronized
-
TreeMap:有序map,支持自定义排序,线程不安全,红黑树。
-
ConcurrentHashMap:
-
1.7
-
存储结构:segment数组+hashentry数组+链表。
-
初始化:segment数组默认大小为16且设置后不允许发生变化(不会扩容,允许创建时指定大小,最大1<<16)。初始化segment[0]节点,大小为2,
-
-
1.8
-
存储结构:数组+链表+红黑树。
初始化: 在put第一个元素的时候触发初始化。cas锁初始化entry数组,cas向桶中放入第一个节点,synchronized锁定数组中的某一个头节点,向链表中存放数据。synchronized性能:锁升级过程== 偏向锁、锁撤销、轻量级锁、重量级锁。
-
-
-
IO
-
输入流inputstream/输出流outputstream:
- file、buffer等都属于包装模式、inputstreamreader,outputstreamwriter适配器。
-
io模型:同步阻塞,同步非阻塞,多路复用,信号驱动,异步io,主要是针对网络io模型
-
bio:同步阻塞io,客户端read阻塞,等待内核写数据
-
nio:同步非阻塞,客户端read不阻塞,没有数据就返回-1,不会一致等待内核写数据,但是如果read读到数据的话,仍然是阻塞的,其他客户端必须要等待当前客户端read结束才能执行。通过轮询的方式来调用read方法,判断有没有数据,如果大量链接都没有数据的话,还是会轮询,浪费cpu。
-
多路复用: 多路复用其实用多种实现方式,select、poll、epoll。nio的缺陷是通过循环调用read判断是否有数据,每次read都是一次系统调用,存在用户态内核态的切换,而且如果cpu利用率不高(如果大量链接都都没有数据的话,那么这其实是一次无效请求)。
-
select\poll: select和poll将这个轮询的操作放在了内核中处理,只进行一次系统调用,由内核来告诉程序哪儿些连接有数据可以读。缺点是每次都要把所有的fd传递给内核,然后内核遍历所有的fd,返回可读的fd,仍然存在遍历。select可以传递的fd数量有限,poll取消了这个限制,并做了一些优化,但是整体区别不大。
-
epoll:epoll在内核中开辟了两块内存区域,一块是一个红黑树,用来存放注册的fd文件,一块是一个链表,用来存放就绪的连接。通过
epoll_create
来创建这两块内存空间。在有连接到达时通过epoll_ctl
命令来将fd注册到红黑树上,当某个fd有事件发生时,通过中断回调,会讲这个fd加入到链表中,这样当调用epoll_wait
时,就会将所有就绪的fd列表返回给服务端。
中断:cpu分为硬中断和软中断,硬中断就是cpu时钟中断,cpu快速响应硬中断,主要处理硬件相关的操作。软中断是为了防止cpu中断程序处理时间过长,cpu在硬中断后,将操作交给内核线程来处理。
-
多线程
- 进程和线程: 进程是操作系统分配资源的最小单位,线程是进程内分配资源的最小单位。
- 并发和并行: 并发是指单个cpu内多个线程交替执行,并行是指多个cpu下线程一起执行。
- 同步和异步:同步是指线程在某一操作下阻塞等待操作的结果返回,才能执行后面的操作;异步是指执行某一操作后不需要等待操作的结果就可以继续执行后续操作。
- 为什么使用多线程:为了压榨cpu资源,避免cpu空闲。同时也为了提高系统的吞吐量。使用多线程可能带来以下问题:死锁、线程安全问题(数据不一致)、内存泄漏。
- 线程生命周期:
- new:new Thread(),但是并没有调用start方法
- ready:调用了start方法,等待获取cpu资源;或者是线程运行时间片结束重新回到ready状态,或者是等待线程被notify唤醒,进入ready状态等待获取cpu资源。ready状态就是表示获取到cpu时间片前的状态。
- running:获取到了cpu资源,正在运行。一般会把ready和running统一称为running状态。
- waiting:调用了wait方法,进入等待状态。
- time_waiting:调用了sleep(timeout),wait(timeout)方法时进入超时等待状态。超时后自动唤醒。wait会释放锁,sleep不会释放锁。
- blocking:等待获取锁资源。
- Terminated:线程任务执行结束,退出。
- 上下文切换:多线程在同一个cpu下并发执行时,由于cpu是按照时间片来执行线程的,每次时间片结束,切换线程时,都需要保存当前线程的状态信息、运行条件等(上下文),以便下次再次执行此线程时恢复现场。同时cpu需要获取下一个要执行的线程的上下文。
- 死锁:线程a占用a资源,线程b占用b资源,线程a想要获取b资源,线程b想要获取a资源。就发生了死锁。避免死锁的方式有:
- 按照顺序获取资源,每个线程都要先获取a资源,才能获取b资源。
- 一次性获取所有资源
- 添加超时时间,当等待资源超时释放占有的资源。
- sleep和wait的区别:
- sleep不释放锁,wait释放锁。
- sleep通过Thread调用,wait通过锁定的资源对象调用
- sleep超时会自动唤醒,wait需要通过notify唤醒,wait(timeout)通过可以超时自动唤醒。
- sleep用于线程暂停,wait通过线程间通信。
- 乐观锁和悲观锁
- 乐观锁认为共享资源被多线程操作时都是安全的,不会产生问题,在提交时校验操作是否成功,失败就重试。悲观锁认为所有的操作都可能产生问题,同一时刻只能有一个线程可以操作共享资源。
- 乐观锁适用于读多的情况(乐观锁一般会通过循环来不停的进行重试,直到成功,如果写多的话,那么就会不停的重试,占用cpu资源)。悲观锁适用于写多的情况,使用独占锁。
- 乐观锁通过加版本号的方式来实现或者通过cas来实现,在使用cas操作时可能会存在aba问题。
- volatile: volatile关键字主要有两个作用:防止指令冲排序,保证线程间资源的可见性。主要是通过内存屏障来实现的。
- 指令冲排序:现代cpu为了提高效率,指令是存在并行执行的,当两个指令之间没有依赖关系,那么就可能发生指令重排序,(在单线程下即使发生指令重排序也不会有问题,因为指令重排序的前提就是不会对结果产生影响)但是在多线程下可能会出现问题。
- 共享资源可见性:在java内存模型中,每个线程都是有自己的虚拟机栈的,线程内创建的变量资源等默认都是分配在栈空间的。但是对于共享资源,资源是分配在堆内存上的,多线程想要操作变量时都会先将数据复制一份到自己的栈空间,然后在栈空间内对数据进行操作,操作完成后再将数据写入到堆内存中。那么在多线程情况下,同一份数据在多个线程内都有自己的副本,各自对副本的修改是没办法顺序同步的,也就是说线程a并不知道线程b已经修改了数据,那么线程a修改的其实是一个无效的数据。通过volatile关键字修饰的变量,在每次读数据时都会到主内存中去读取,而不是访问栈内的副本
- 内存屏障:内存屏障是操作系统提供的保证缓存一致的功能。主要有两个指令load、store。load代表从主内存读数据到cpu缓存中,store代表将数据写会主内存。两个命令组合就形成了内存屏障。组合有loadstore、storeload、loadload、storestore。
- volatile不能保证原子性:volatile只能保证每次都到主内存中读取数据,读到的数据是最新的,但是并不能保证写入的顺序性,不能保证在写会内存前,没有其他线程修改资源信息。
- synchronized:同步,保证线程有序执行。可以保证共享资源的原子性、可见性。
- synchronized通过加锁的方式,保证同一时刻只能有一个线程操作共享资源。同步代码块synchronized(对象),是通过monitorenter指令来加锁,锁信息存放在对象头markword中,通过monitorexit来释放锁。synchronized锁方法时,会为方法添加ACC_SYNCHRONIZED标识,标记次方法为同步方法,执行时,也是通过为对象加锁实现的。
- synchronized锁升级:synchronized在早期性能很差,后来进行了优化,出现了锁升级。锁针对对象来操作的,锁的状态变化为:无锁-> 偏向锁-> 轻量级锁-> 重量级锁。主要是通过markword的后2为来标识锁状态的。01无锁、偏向锁,00轻量级锁,10重量级锁。当偏向锁升级为轻量级锁时会发生锁撤销,也就是当前拥有偏向锁的线程会失去锁,重新开始竞争锁。锁只能升级,不能降级,轻量级锁是通过cas来获取锁,循环重试获取锁,当循环次数超过10次,或者等待线程数大于10,那么就会升级为重量级锁,重量级锁性能很差。
- 偏向锁撤销:偏向锁撤销需要等待全局安全点,所有线程暂停,这时候查看当前持有偏向锁的线程是否存活同时是否正在执行同步代码块,如果满足以上条件,那么偏向锁升级cas锁,仍然是此线程持有锁。
- AQS(abstractQueuedSynchronized):aqs核心思想就是被请求的共享资源空闲,那么请求资源的线程直接获取资源,同时资源状态改为锁定状态。如果请求的资源处于锁定状态,那么就需要一套阻塞线程等待、唤醒、锁分配机制,同时需要一个队列(CLH)维护等待的线程。
- aqs通过clh队列维护阻塞等待线程,并不存在真正的队列,而是通过节点间的链表关系来关联。
- aqs通过为state字段赋值来锁定资源,state为0代表资源空闲,state资源被锁定,则+1,重入锁就再+1,释放锁就要-1。当state重新变为0时,资源被释放。
- threadlocal:为每个线程创建自己的私有变量,其他线程无法访问,保证线程安全。
- 再thread对象中存在两个变量,
threadLocals、inhritableThreadLocals
。是threadLocalMap
类型的。ThreadLocalMap是一个只有数组维护的Map接口实现,key就是threadlocal对象本身,被使用弱引用包裹了,value就是线程的私有变量值。 - 在第一次调用threadlocal的set方法时,会判断线程的threadlocals是否被初始化,如果没有则初始化一个map,然后保存值,如果已经存在,那么通过hash算法确定位置,然后放入对应的位置。
- threadLocalMap只有数组,并没有链表,如果发生hash冲突时,就从hash位置向后找,找到第一个为空的位置,或者key为空的节点,插入此位置。
- 因为threadlocalmap的key为弱引用,因此当threadlocal被回收时,失去了强引用后,此key就可能被回收,就会发生内存泄漏。所以需要显示调用remove清除key。
- 再thread对象中存在两个变量,
- Atomic原子类:底层cas。
- 线程池:
- 线程池创建参数:
corePoolSize
: 核心线程数maxPoolSize
:最大线程数workQueue
:工作队列keepAliveTime
:超时时间timeUnit
: 超时时间单位threadFactory
: 线程工厂rejectedExecutionHandler
: 拒绝策略
- 线程池内部变量:
- ctl: 一个int类型数字,高3位代表线程池状态, 低29位代表线程池大小。
- workers:一个hashset集合,线程池实例。存放线程集合的地方
- 线程池工作流程:
- 查看当前线程池大小是否大于核心线程数,如果小于则创建线程执行。否则向下。
- 判断当前阻塞队列是否已满,如果不满则将任务放入阻塞队列,否则向下。
- 判断当前线程数量是否大于最大线程数,如果小于创建新的线程执行,否则执行拒绝策略。
- 线程池创建参数:
jvm
java内存区域
- 堆:线程共享区域,所有的对象创建后都在这里。
- 方法区:线程共享区域,存放类信息、方法信息、字段信息、常量、静态变量等信息。1.8后被移到元空间,常量池被移到堆中。
- 线程栈:线程私有
- 虚拟机栈:线程栈内java程序运行时使用
- 本地方法栈:跨语言调用时使用
- 程序计数器:记录下一行要执行的命令
- 元空间:1.8之后的永久代,方法区的内容。
jvm垃圾回收
-
堆内存布局:
- 年轻代:
- eden: 对象优先分配在此区域,当此区域内存不够时,会发起一次gc,如果清理后的剩余空间还是不够放,则对象直接进入老年代。
- survivor0(s0):eden发起gc时,会将存活的对象放入此区域,同时s1内的对象也会放入此区域,同时将s1清空。
- survivor1(s1):s0和s1交替使用,发生gc时,如果s0为空,就将s1的对象放入s0,将s1清空;如果s1为空,就讲s0的数据放入s1,将s0清空。每次s0->s1的数据复制,都会将对象的年龄+1,当满足条件时就会将此区域的数据放入老年代
- 老年代:存放大对象、长期存活对象的地方。
- 年轻代:
-
对象死亡判断方法:
- 引用计数法:对对象引用进行计数,如果计数为0,代表没有引用指向此对象,对象死亡,可以回收。无法解决循环引用的问题。
- 根可达算法:从一个根节点往下找,所有可以找到的对象都是存活的对象,否则都是死亡对象,可以被回收。
- 虚拟机栈:
- 本地方法栈:
- 方法区常量、静态变量:
- 同步锁持有的对象:
-
垃圾收集算法:
- 标记-清除:先标记可回收对象,然后清除此对象。会导致内存碎片。
- 标记-复制:将内存分为相等的两部分,同时只使用一半内存,回收时将存活的对象放入空的另一半内存。然后清除当前内存。内存利用率不高。
- 标记-整理:将所有存活的对象向一端移动,然后清除剩余的内存。没有内存碎片。同时内存利用率还高。
-
垃圾收集器:
-
serial:单线程回收器,回收速度慢。年轻代回收器。
-
serial old: 单线程老年代回收器。
-
parallel scavenge: 多线程回收器,年轻代。多线程只是在标记阶段是并发标记。
-
parallel old:多线程老年代回收器。
-
parallel new:parallel scavenge的优化版本,年轻代,主要是为了配合cms使用。
-
cms:多线程并发标记清除算法,主要分为4部:
- 初始标记:暂停所有线程,记录所有与根节点相连的对象。
- 并发标记:用户线程和gc线程同时执行,从根节点向下找所有可达对象
- 重新标记:gc线程查找并发标记阶段引用发生的变化
- 并发清除:用户线程和cpu线程同时执行。
cms垃圾收集器的缺点是:
- 并发清除会导致大量内存碎片。
- 无法处理浮动垃圾,只能等待下次垃圾回收。
-
G1: g1垃圾回收器采用分治算法来处理的。从整体上看属于标记-整理算法,从局部上看属于标记-复制算法。通过将一整块内存分割成一个一个的region,最大分为2048个region,每个分区最大是32MB。在g1中没有固定的年轻代和老年代划分,每个region都可以是年轻代/老年代。在发生gc时,将一个region内的存活对象复制到另一个region中,并标记此region的分代信息,就可以将之前的整个region清空了。
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
-
类文件详情
- 一个类文件结构主要包含如下内容:
- 类文件包含4位的标志位,例如java的是CAFEBABE。
- minor version:小版本号
- major version:大版本号
- 常量池
- 访问控制符
- 当前类、父类
- 接口列表
- 字段列表
- 方法列表
- 属性列表
- 类加载过程:
- 加载:加载类文件内容,转成class结构
- 链接
- 校验:校验类文件标志位是否合法,类文件内容是否合法
- 准备:将static修饰的变量赋值为默认值,将final修饰的变量赋值,
- 解析:替换引用
- 初始化
- 类加载器:双亲委派模型。
spring框架
@Transactional
注解原理
spring事物分为两种
-
编程式事物:通过编码的方式来实现事物,此方式对业务有入侵。编程式事物就是通过在代码中注入
TransactionManager
。@Autowired private PlatformTransactionManager platformTransactionManager; public void test() { // 定义事物属性信息,事物传播机制、事物隔离级别、事物超时时间 DefaultTransactionDefinition td = new DefaultTransactionDefinition(); // 设置传播机制 td.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED); // 设置隔离级别 td.setIsolationLevel(TransactionDefinition.ISOLATION_DEFAULT); // 设置事物超时时间 td.setTimeout(-1); TransactionStatus ts = platformTransactionManager.getTransaction(td); try { // 业务代码 // ...... platformTransactionManager.commit(ts); } catch (Exception e) { platformTransactionManager.rollback(ts); } }
-
声明式事物:通过注解的方式来引入事物,简单,对业务无入侵。声明式事物其实就是对业务代码做了层代理,在代理类中注入事物管理器,并管理事务的定义,提交,回滚等。代理类内的内容与上面硬编码的方式差不多。详见
TransactionInterceptor#invoke方法
spring bean生命周期(bean定义实例化过程)
主要代码如下:AbstractAutowireCapableBeanFactory#createBean
。建议看过下面流程再回过头来看代码
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {
...
// 5. 类加载阶段
Class<?> resolvedClass = resolveBeanClass(mbd, beanName);
if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) {
mbdToUse = new RootBeanDefinition(mbd);
mbdToUse.setBeanClass(resolvedClass);
}
...
// 6.1 处理调用 InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation方法,给一个机会再初始阶段就返回一个用户实现的代理类。
try {
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
return bean;
}
}
// 6.2 实例化bean
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
...
}
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
...
if (instanceWrapper == null) {
// 6.2 实例化bean
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = instanceWrapper.getWrappedInstance();
Class<?> beanType = instanceWrapper.getWrappedClass();
if (beanType != NullBean.class) {
mbd.resolvedTargetType = beanType;
}
// 6.3 最后一步,bean合并阶段
synchronized (mbd.postProcessingLock) {
if (!mbd.postProcessed) {
try {
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
"Post-processing of merged bean definition failed", ex);
}
mbd.postProcessed = true;
}
}
// 6. 将bean创建的bean加入3级缓存
if (earlySingletonExposure) {
...
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
...
Object exposedObject = bean;
try {
// 7 属性赋值阶段
populateBean(beanName, mbd, instanceWrapper);
// 8 bean初始化阶段
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
....
return exposedObject;
}
// 6.2 下面这个方法全部都是实例化bean阶段的调用
protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
// 6.2 返回一个指定的构造器:实现了 SmartInstantiationAwareBeanPostProcessor
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
// 在此方法内通过反射的方式创建bean实例
return autowireConstructor(beanName, mbd, ctors, args);
}
....
}
//7 属性赋值阶段
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
// 7.1 bean实例化后执行,调用InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation方法
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
return;
}
}
}
}
.......
// 处理自动装配的字段属性值
int resolvedAutowireMode = mbd.getResolvedAutowireMode();
if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// Add property values based on autowire by name if applicable.
if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}
// Add property values based on autowire by type if applicable.
if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
pvs = newPvs;
}
.......
// 7.2 属性赋值前,调用InstantiationAwareBeanPostProcessor#postProcessProperties 修改属性信息
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
if (filteredPds == null) {
filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
}
pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvsToUse == null) {
return;
}
}
pvs = pvsToUse;
}
}
}
.....
// 7.3 属性赋值
applyPropertyValues(beanName, mbd, bw, pvs);
}
}
// 8初始化阶段
protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
// 8.1 执行BeanFactoryAware等接口
invokeAwareMethods(beanName, bean);
}
Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
// 8.2 执行初始化前调用 BeanPostProcessor#postProcessBeforeInitialization
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}
try {
// 8.3 执行初始化方法
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init method failed", ex);
}
if (mbd == null || !mbd.isSynthetic()) {
// 8.4 执行初始化后调用 BeanPostProcessor#postProcessAfterInitialization
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}
return wrappedBean;
}
-
bean定义阶段。可以通过注解、xml、properties等方式来定义bean信息。
-
bean解析阶段。通过读取bean定义阶段的文件,解析bean定义信息,创建
beanDefinition
对象。 -
bean注册阶段。将第2阶段解析创建的
beanDefinition
对象注册进spring容器。BeanDefinitionRegistry
-
bean合并阶段。初始定义的bean信息可能并不完整,例如存在父类的情况,此阶段将父bean定义信息合并过来。
-
bean类加载阶段。找到bean定义时指定的类路径,加载类信息。
-
bean实例化阶段。调用
InstantiationAwareBeanPostProcessor
实现。- 实例化前:
BeanPostProcessor#postProcessBeforeInstantiation
。 - 实例化:
doCreateBean方法内,createBeanInstance方法
。此阶段如果有实现SmartInstantiationAwareBeanPostProcessor
类,那么将会调用此类的实例化方法。SmartInstantiationAwareBeanPostProcessor#determineCandidateConstructors
返回一个bean的构造器。然后对其使用反射初始化:BeanUtils#instantiateClass
。 - bean合并阶段:如果有实现
MergedBeanDefinitionPostProcessor
,那么将会调用此方法最后一次对bean定义信息作修改,在此操作之后,如果满足条件会将实例化的bean加入3级缓存。
- 实例化前:
-
bean属性赋值阶段。见
AbstractAutowireCapableBeanFactory#populateBean
-
实例化后:
InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation
。 -
bean属性赋值前阶段:调用
InstantiationAwareBeanPostProcessor#postProcessProperties
方法。在为bean实例初始属性赋值前对属性值进行修改,和下面的方法功能一样,下面的已经被弃用。 -
调用
InstantiationAwareBeanPostProcessor#postProcessPropertyValues
方法。已弃用。为了兼容,会在上面方法返回null时调用此方法。 -
bean属性赋值阶段:通过反射为bean属性进行赋值。
-
-
bean初始化阶段。通过
BeanPostProcessor接口实现
。-
调用部分实现了Aware接口的方法。
invokeAwareMethods(),调用BeanNameAware、BeanClassLoaderAware、BeanFactoryAware对应的方法
。 -
bean初始化前。调用所有的
BeanPostProcessor#postProcessBeforeInitialization
方法。 -
bean初始化。
-
调用
PostConstruct
注解标注的初始化方法。PostConstruct
注解的处理是在BeanPostProcessor
方法中实现的,因此实际调用方法的时机应该是在
postProcessBeforeInitialization
中。从功能上定义到这里了,所以它在下面所有初始化方法前调用。具体实现见:InitDestroyAnnotationBeanPostProcessor
-
如果实现了
InitializingBean
则调用对应的afterPropertiesSet
方法。 -
如果指定了init方法。
@Bean(initMethod="xxx")
则通过反射调用对应方法。
-
-
bean初始化后。调用所有的
BeanPostProcessor#postProcessAfterInitialization
方法。
-
-
所有单例bean初始化完成。调用所有实现了
SmartInitializingSingleton
接口的afterSingletonInstantiated
方法。 -
bean使用阶段。
-
bean销毁阶段。
spring 3级缓存原理
spring的3级缓存代码写在:DefaultSingletonBeanRegistry
类中。
/** 一级缓存 */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** 三级缓存,存放创建bean的函数式接口 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
/** 二级缓存 */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
3级缓存主要是用来解决单例bean循环依赖的问题的。spring容器中bean对象的创建是通过调用AbstractBeanFactory#doGetBean
方法实现的。跟着源码一层一层向下看(建议可以先看下面的处理流程再回过头来看代码):
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
.....
// 获取单例bean
Object sharedInstance = getSingleton(beanName);
....
// 从1、2、3级缓存都没有获取到,则创建类
if (mbd.isSingleton()) {
// 注意这里调用的是getSingleton(beanName, ()-> createBean())。 源码放在下面了
sharedInstance = getSingleton(beanName, () -> {
try {
// 创建bean,可以参照上面的bean生命周期
return createBean(beanName, mbd, args);
}
...
}
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 从一级缓存获取bean
Object singletonObject = this.singletonObjects.get(beanName);
// 如果一级缓存不存在
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 从二级缓存找bean
singletonObject = this.earlySingletonObjects.get(beanName);
// 二级缓存也不存在,而且允许循环依赖(默认是允许的)
if (singletonObject == null && allowEarlyReference) {
// 到3级缓存找创建bean的函数式接口
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
// 通过函数式接口创建一个bean,并将此bean放入2级缓存,并从3级缓存中移除此bean的工厂类
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return singletonObject;
}
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
....
synchronized (this.singletonObjects) {
// 先到一级缓存找
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
...
// 一级缓存没找到,调用工厂方法创建bean实例(此时bean实例在3级缓存)
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
...
if (newSingleton) {
addSingleton(beanName, singletonObject);
}
}
return singletonObject;
}
}
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
// 将bean加入一级缓存
this.singletonObjects.put(beanName, singletonObject);
// 从3级缓存删除
this.singletonFactories.remove(beanName);
// 从2级缓存删除
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
那么当发生循环引用时,例如类A依赖类B,类B依赖类A。假设此时先创建类A的bean。流程如下:
- 先到一级缓存查找类A的bean,找不到
- 到二级缓存找类A的bean,找不到
- 到3级缓存找创建类A的工厂方法。如果还是找不到,那么就会直接返回null了。
- 3层缓存都找不到类A的bean,调用
createBean
方法创建类A的bean,并将其加入3级缓存。 - 类A创建成功,然后beanA字段赋值,会发现需要注入字段类B的bean。那么先从1级缓存中找类B的bean,找不到。
- 到二级缓存中找类B的bean,仍然找不到
- 到3级缓存中找类B的工厂方法,如果仍然找不到,则返回null。
- 然后调用
createBean
方法创建类B的实例,并将其放入3级缓存。 - 初始化beanB,发现需要依赖A的实例,
- 然后beanB字段赋值,发现需要注入字段A。会调用
getBean
方法查找A的bean,然后从1、2、3级缓存找beanA,最终从3级缓存找到了beanA。然后将beanA从3级缓存移动到2级缓存,并从3级缓存移除。 - beanB初始化,创建完成。将bean B加入一级缓存,并从2、3级缓存中移除,并返回beanB到A的赋值阶段。
- A继续初始化,此时字段B已经可以从一级缓存中拿到了, 然后A初始化完成,将A加入一级缓存,并从2、3级缓存中移除。
spring aop原理
springaop自动装配底层是通过AnnotationAwareAspectJAutoProxyCreator
类来实现的。而这个类又实现了SmartInstantiationAwareBeanPostProcessor
接口,因此类的代理动作是在实例化阶段实现的。主要是通过下面这个方法来实现的。
default Object getEarlyBeanReference(Object bean, String beanName) throws BeansException {
return bean;
}
而AnnotationAwareAspectJAutoProxyCreator
的父类AbstractAutoProxyCreator
重写了这个方法:
@Override
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
this.earlyProxyReferences.put(cacheKey, bean);
return wrapIfNecessary(bean, beanName, cacheKey);
}
通过上面的bean生命周期,我们知道在bean实例化的时候,会将一个创建bean的函数式接口放入三级缓存。如果没有被aop代理,那么返回的就是实例化时创建的对象,但是如果类被代理的话,那么这里就会返回一个代理类。
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
....
// Create proxy if we have advice.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 创建代理类并返回
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
spring容器启动过程
spring容器启动主要看refersh方法,源码如下:
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 1. 容器准备刷新
prepareRefresh();
// 2. 创建bean容器,也就是DefaultListableBeanFactory
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 3. 准备工厂,以便后面在上下问中使用
prepareBeanFactory(beanFactory);
try {
// 4. 在工厂初始化准备好后,允许修改上下文信息,后置操作
postProcessBeanFactory(beanFactory);
// 5. 调用BeanFactoryPostProcessor类方法。通过BeanFactoryPostProcessor接口,可以在创建bean之前对bean信息进行修改
invokeBeanFactoryPostProcessors(beanFactory);
// 6. 注册BeanPostProcessor, 此时创建所有实现了BeanPostProcessor接口的bean,所有bean创建完成后加入容器中,后面再创建bean时就会调用BeanPostProcessor方法做前后置处理了。
registerBeanPostProcessors(beanFactory);
// 7. 国际化相关,暂不关心
initMessageSource();
// 8. 初始化事件广播器
initApplicationEventMulticaster();
// 9. 预留拓展方法,可以在容器初始化所有bean之前调用此方法创建特殊bean
onRefresh();
// 10. 注册监听器,将监听器注册到8初始化的事件广播器中
registerListeners();
// 11. 创建并初始化所有非懒加载的bean
finishBeanFactoryInitialization(beanFactory);
// 12. 容器创建完成
finishRefresh();
}
........
}
}
配置文件的加载在调用refresh方法之前。在springboot中是通过事件机制来加载的。springboot基于SPI机制实现了一套加载spring.factories配置的方法,在容器创建前注册了ConfigFileApplicationListener
监听器,默认配置文件application.*
的加载都是在这里的。
- 准备刷新容器。
- 设置容器的状态
- 初始化环境变量信息。
systemProperties、systemEnvironment
。此时环境变量已经被加载了
- 创建容器。
- 如果已经存在容器,那么就销毁容器并创建新的容器
- 加载beanDefinition信息。通过注解就扫描包,通过xml就加载xml解析。
- 准备工厂,用于在上下文中使用。
- 设置容器启动所需要加载的类信息
- 将环境变量注册入容器
- 在工厂初始化准备好后,允许修改上下文信息,后置操作。
- 创建并调用所有实现了
BeanFactoryPostProcessor
接口的方法 - 创建所有实现了
BeanPostProcessor
接口的bean,并在创建完成后统一加入到容器中 - 预留国际化相关,暂不关心。
- 初始化事件广播器
- 预留方法onRefresh()。
- 注册监听器
- 创建并初始化所有的bean。
- 容器创建完成,发布创建完成事件、清除缓存等。
spring自动装配原理
-
首先要搞清楚spring加载配置类的原理:在容器启动过程中,第二步加载
beanDefinition
信息时,会校验容器中是否存在ConfigurationClassPostProcessor
的bean定义,如果没有的话,就会注册这个类,这个类就是用来处理@Configuration
标识的类的。而这个类实现了BeanDefinitionRegistoryPostProcessor
接口,因此这个处理@Configuration
的过程是在第5步进行的。public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory); } public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) { ..... do { // 解析@Configuration类 parser.parse(candidates); .... }
找到这个方法后,点进去会跳到
ConfigurationClassParser
,而最终会调用一个doProcessConfigurationClass
的方法。protected final SourceClass doProcessConfigurationClass( ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) throws IOException { // 处理@Component注解的类 if (configClass.getMetadata().isAnnotated(Component.class.getName())) { // Recursively process any member (nested) classes first processMemberClasses(configClass, sourceClass, filter); } // Process any @PropertySource annotations for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), PropertySources.class, org.springframework.context.annotation.PropertySource.class)) { ... } // Process any @ComponentScan annotations Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { for (AnnotationAttributes componentScan : componentScans) { // The config class is annotated with @ComponentScan -> perform the scan immediately Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); // Check the set of scanned definitions for any further config classes and parse recursively if needed for (BeanDefinitionHolder holder : scannedBeanDefinitions) { BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition(); if (bdCand == null) { bdCand = holder.getBeanDefinition(); } if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) { parse(bdCand.getBeanClassName(), holder.getBeanName()); } } } } // Process any @Import annotations, 这里就是处理springboot自动装配的地方 processImports(configClass, sourceClass, getImports(sourceClass), filter, true); // Process any @ImportResource annotations AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); if (importResource != null) { String[] resources = importResource.getStringArray("locations"); Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader"); for (String resource : resources) { String resolvedResource = this.environment.resolveRequiredPlaceholders(resource); configClass.addImportedResource(resolvedResource, readerClass); } } // Process individual @Bean methods Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass); for (MethodMetadata methodMetadata : beanMethods) { configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); } // Process default methods on interfaces processInterfaces(configClass, sourceClass); // Process superclass, if any if (sourceClass.getMetadata().hasSuperClass()) { String superclass = sourceClass.getMetadata().getSuperClassName(); if (superclass != null && !superclass.startsWith("java") && !this.knownSuperclasses.containsKey(superclass)) { this.knownSuperclasses.put(superclass, configClass); // Superclass found, return its annotation metadata and recurse return sourceClass.getSuperClass(); } } // No superclass -> processing is complete return null; }
从类注释上可以看到这个类处理了
@Component、@ComponentScan、@PropertySources、@Import、@ImportResource、@Bean
等注解的解析。当类被扫描解析解析后,会将解析后的内容创建成
BeanDefinition
并注册进容器中。从这里可以看出,spring容器并不是在一开始就拿到了所有的bean定义信息的(但是在一开始就定义了ConfigurationClassPostProcessor
)。而是通过@Configuration
注解,在第5步的时候,加载出其他的bean定义信息:BeanFactoryPostProcessor、BeanPostProcessor、业务配置类等
。慢慢一步一步加载出更多的类。 -
springboot自动装配原理:我们首先查看自动装配注解:
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; Class<?>[] exclude() default {}; String[] excludeName() default {}; }
可以看到上面有一个
@Import({AutoConfigurationImportSelector.class})
。而我们在上面了解到spring容器在第5步的时候,加载@Configuraion
配置类定义时会加载@Import
配置类。查看这段源码:private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter, boolean checkForCircularImports) { ..... try { for (SourceClass candidate : importCandidates) { if (candidate.isAssignable(ImportSelector.class)) { Class<?> candidateClass = candidate.loadClass(); // 在这里解析并实例化了 ImportSelector 的类 ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class, this.environment, this.resourceLoader, this.registry); Predicate<String> selectorFilter = selector.getExclusionFilter(); if (selectorFilter != null) { exclusionFilter = exclusionFilter.or(selectorFilter); } // AutoConfigurationImportSelector 实现了这个接口,所以这里会走这个分支, 这里需要回到parse方法,在最后一行执行了 this.deferredImportSelectorHandler.process(); if (selector instanceof DeferredImportSelector) { this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector); } ..... } public void process() { .... handler.processGroupImports(); .... } public void processGroupImports() { // 重点是getImports()这个方法 grouping.getImports().forEach(entry -> { ..... } public Iterable<Group.Entry> getImports() { for (DeferredImportSelectorHolder deferredImport : this.deferredImports) { // 最终会执行到这个方法,而这个方法在AutoConfigurationImportSelector被重写了,这个方法就是加载spring.factories中的自动装配类的方法 this.group.process(deferredImport.getConfigurationClass().getMetadata(), deferredImport.getImportSelector()); } return this.group.selectImports(); } // AutoConfigurationImportSelector.Group#process public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) { ... // 主要看这个方法 AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector)deferredImportSelector).getAutoConfigurationEntry(annotationMetadata); .... } protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { ... // 进入下面这个方法 List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes); ..... } protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { // 可以点进这个方法看到this.getSpringFactoriesLoaderFactoryClass()返回值就是注解@EnableAutoConfiguration // 看到了熟悉的SpringFactoriesLoader,前面说过这个类是spring基于SPI的机制来实现的。因此它加载的内容就是 /META-INF/spring.factories下的指定类名(也就是EnableAutoConfiguration)下的类内容 List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct."); return configurations; } // 附:可以随便找一个spring-boot-xxx-autoconfiguration包,打开/META-INF/spring.factories // 可以看到如下内容: // org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\ org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
前面讲了这么多如何通过
EnableAutoConfiguration
来获取到需要自动装配的类,这种类上面都会有一个`@Configuration,那么就会继续递归解析这些配置类。- 需要注意的是,上面的过程中,除了
ImportSelector
接口的实现类会被实例化外,自动装配定义的类以及所有解析出来的@Configuration
等类都不会被实例化,只是记录下来信息,后面创建BeanDefinition
。而这些类的创建都是与普通bean一起在容器启动过程中的第11步创建的。
- 需要注意的是,上面的过程中,除了
Redis
认识redis
-
redis是什么?
redis是基于内存的数据库。
-
redis和memcache有什么区别?
相同点:两者都是基于内存的数据库,都适合拿来做缓存。都有缓存过期策略。性能都非常高
不同点:
- redis有持久化策略,memcache没有
- redis有多种数据结构,使用场景更多。memcache只支持key-value的字符串场景。
- redis原生支持集群,memcache没有原生集群,依赖客户端实现分片写入。
- redis支持发布订阅模型、lua脚本。memcache不支持。
-
为什么用redis作mysql的缓存?
因为redis的高性能和高并发特性。
redis的数据结构
-
string: redis并没有使用c语言自带的string,而是自定义了一套实现
SDS
。相比而言,sds可以存储二进制数据。获取字符串长度时间复杂度为O(1)(存储了字符串的长度)。可以用来做缓存、分布式锁等。当存储的内容为整形时,redis会使用int类型来存储。否则就会使用字节数组来存储。
-
list:再早期是基于压缩列表和双向链表实现的。后面改用了quicklist。可以做发布订阅队列。
当list中的每个元素都小于64字节,而且元素个数小于512时,使用压缩列表存储。否则使用双向链表存储。
压缩列表
:使用一块连续的内存存储列表数据。会引发连锁更新问题,影响性能quicklist
:压缩列表+双向链表的组合。 -
set:基于整数集合和hash表实现的。后面改用listpack和hash表。可以用来做统计并集、交集等操作。
当集合中每个元素都是整数,而且元素个数小于512时,使用整数集合,否则使用hash表。
-
hash:基于压缩列表和hash表实现的。后面改用了listpack和hash表。可以存对象。
当表中元素大小小于64字节,且元素个数小于512时,使用压缩列表。否则使用hash表。redis7.0使用listpack。
listpack:还是使用连续内存保存数据,quicklist仍然存在连续更新的问题。listpack解决了这个问题(不存钱一个节点的数据长度,存自己的数据长度)。
-
Zset
: 排序集合,基于压缩列表和跳表实现的。可以用来做排行榜等。当集合中每个元素大小小于64字节,且个数小于128时,用压缩列表。否则使用跳表。再redis7,废弃了压缩列表,改用listpack。
redis的rehash原理
redis使用hash表存储数据,使用链式结构处理hash冲突问题。但是如果hash冲突过多,链表过长就会影响查询效率,因此需要扩容。在redis的hash结构中,dict字段存放hash表,但是它是一个数组,可以放两个元素。默认使用dict[0]来存放数据,dict[1]是空的。当发生扩容时:
- 初始化dict[1]大小为dict[0]的2倍。
- 将dict[0]的数据迁移到dict[1]
- 释放dict[0]占用的空间,将dict[1]替换到dict[0]。将dict[1]初始化为空,为下一次rehash做准备。
如果hash中存放的数据多的话,从dict[0]迁移数据到dict[1]耗时长,会阻塞线程,影响性能。redis采用一种渐进式rehash的方式来完成数据迁移。
渐进式rehash
- 在每次执行新增操作时,会将数据存放在
dict[1]
后面统称hash表2。 - 当每次执行查询、更新、删除操作时。同时在两个hash表上执行。会将操作的key对应的hash表1的索引位置下的所有kv迁移到hash表2。
rehash触发条件
当负载因子大于1,如果不在执行aof/rdb,就出发rehash。
当负载因子大于5,无论有没有在执行持久化,都触发rehash。
redis的线程模型
redis再执行redis命令方面仍然是单线程的。但是redis引入了多线程来处理:
- 一个线程处理文件的关闭。
- 一个线程处理aof的刷盘。
- 一个线程处理lazzfree。
- 在6.0后,redis启动了多个io线程来处理从网卡读写数据。线程数可指定。
redis持久化
-
要实现数据不丢失,那么就需要涉及一系列的持久化,恢复操作。redis通过
RDB和AOF
来实现持久化及数据恢复。 -
AOF:每次操作就将操作命令写入到日志文件中。
- 优点: 记录详细,不容易丢数据。
- 缺点:日志体量太大。每次恢复的时候是一条一条读命令恢复数据的,太慢。
为了解决日志体量太大的问题,引入了aof重写机制。
当文件大小超过某个阈值时,会生成一个aof重写文件,然后读数据库中的数据,将数据转换成写命令的方式写入aof重写文件。当aof重写完毕就将重写文件重命名为aof文件,替换掉原来的文件。**记录key对应的最新值的命令,避免某些key因为历史命令过多,导致文件占用过大 ** 通过aof重写机制可以减少文件的大小。但是如果key本身数量就非常多的话,文件仍然很大,恢复速度太慢。
而且存在一个问题,aof重写过程中已经重写过的key被修改了。
解决办法:redis使用了aof重写缓冲区。将重写期间的写命令记录进重写缓冲区。当aof重写完毕,会对照重写缓冲区更新一次key。
-
RDB:每隔一段时间就将数据库中的数据做一个快照存储下来。
- 优点:文件小,恢复速度快。
- 缺点:时间频率难以固定,会丢数据。
使用子进程生成rdb快照。写时复制。
-
混合存储:redis使用了aof和rdb的两个优点,组成了混合存储。在每次持久化时,先生成一个rdb快照,然后后面在这个快照文件上使用aof记录后续的操作,直到下次持久化。
优点:借用了两种方式的优点,不容易丢数据了,文件也变小了。数据恢复也变快了。
缺点:兼容性变差了,如果拿着一份混合存储的文件到旧版本的redis恢复,会识别不了。可读性变差了,因为rdb文件并不能直接以文本的方式查看。
redis集群
-
集群服务高可用:
主从+哨兵
cluster分片集群
-
集群脑裂问题:
当主节点与超过指定数量的节点失联,不允许写。
主节点与从节点发送心跳延迟超过x秒,不允许写。
redis过期删除与内存淘汰
-
过期删除策略:常见的过期删除策略有三种
- 定时删除: 在设置一个key的同时,为key设置定时事件,在过期时通过事件来删除key。如果过期的key太多的话,cpu耗时在删除key上,会影响性能。
- 惰性删除: 不主动删除key,每次访问的时候检查是否过期,如果已经过期就删掉。占用空间,如果一个过期key后面没有再被访问,那么就不会被释放。
- 定期删除:每隔一段时间从数据库中随机抽查一部分key,并删除其中过期的key。优点是占用的资源少,同时可以删除过期的key。缺点是频率难确定,太频繁对cpu不好,太不频繁又占用内存。
-
redis使用了定期删除+惰性删除的方式,同时当一次定期删除超过25ms,就会结束这次操作。
-
redis持久化时,对过期键怎么处理:
- 使用aof时,在key过期被删除时,redis会向aof中写入一条del命令;在aof重写时,会删除过期的key。
- 使用rdb时,在生成rdb文件时会检查key是否过期,如果过期则不会被保存到新的rdb文件中。redis从rdb加载数据时,如果是主节点,不会加载过期key,如果是从节点,则加载所有的数据。
-
redis主从模式对过期键如何处理:
- 主节点:如果key已经过期,向aof中写入一条del命令。
- 从节点:依靠主节点同步删除命令。
-
redis内存满了会发生什么:内存淘汰策略
-
内存淘汰策略:
noeviction
:不删除,直接服务端报错,不对外提供服务了volatile-random
: 随机删除设置了过期时间的keyvolatile-ttl
: 删除快要过期的keyvolatile-lru
: 删除设置了过期时间的最久未使用的key(最近最少使用)volatile-lfu
: 删除设置了过期时间的使用最少的key(最近最不常用)allkeys-random
: 所有key随机删除allkeys-lru
: 所有key最近最少使用allkeys-lfu
: 所有key最近最不常用
-
LRU和LFU
-
LRU算法:最近最少使用,常用在缓存中。常见的LRU实现,都是通过一个链表维护元素,当某一个元素被访问时,如果在LRU缓存中,就将其移动到链表头,当LRU缓存中没有命中时,就讲元素添加到链表头并移除链表尾部元素。
-
redis并没有使用传统的lru算法,因为传统的需要维护链表,需要额外的数据结构以及存储。redis是通过在记录的数据结构中添加一个额外的字段(24位):最近一次使用时间 来判断要不要移除。
-
LRU算法的问题:可能会出现缓存污染。
缓存污染: 突然访问很多新数据,这些数据都不在缓存中,那么就会移除掉缓存中原有的数据,并添加这些新的数据,但是这些数据可能后面都不会再被访问,只用这一次。那么这些数据就会一直占用这些空间,直到因为有新加入的数据被淘汰。
-
-
LFU算法:因为LRU算法的缺陷,所以提出了LFU算法。它是按照数据的访问频率来删除数据的。如果一个数据过去的访问频率比较高,那么大概率它在未来的访问频率也会很高,就要保留。所以LFU算法会记录数据的访问次数,数据每被访问一次,就将他的访问次数+1。淘汰时淘汰访问次数最低的数据。
- redis中lfu算法是在lru算法的24位字段上高16位存时间戳,低8位存访问次数。
-
缓存设计
-
缓存击穿、缓存穿透、缓存雪崩。
-
缓存穿透: 不断请求一个redis中不存在的key,同时mysql中也不存在这个key,那么每次请求都会压到数据库,如果请求量大,mysql会崩。
解决方式:
- 向redis中缓存一个空值。
- 使用布隆过滤器。
-
缓存击穿:请求一个key时,这个key刚好过期了,如果此时并发量大, 那么就会导致压力都到数据库这边,导致数据库崩掉。
解决方式:
- 不给数据设置过期时间,通过更新数据库时更新redis缓存。
- 加锁,保证key过期时只有一个线程会访问数据库。
-
缓存雪崩:同时大量的key过期了,请求都压到mysql。
解决方式:
- 给key设置随机的过期时间,避免同时大量过期
- 不要设置过期时间
-
-
常见缓存更新策略:
-
旁路策略:缓存的更新交给用户程序常用
写策略:先更新数据库,再删除redis缓存(顺序不能反,必须先更新数据库,否则会产生数据不一致的问题)
读策略:先从缓存读数据,缓存命中直接返回。缓存没有命中,从数据库中查,查到数据放入缓存并返回给用户。
-
读传/写穿策略:缓存的更新交给redis自己,用户程序直接与缓存交互,缓存更新数据时同步更新数据库。不常用
-
写回策略:不会实时更新数据库,而是标记数据为脏的(dirty)。数据库的更新是异步的。主要是操作系统会使用这种策略不常用
-
MYSQL
-
sql执行流程:
- 连接器:客户端与服务端建立连接
- 缓存器:如果是select请求,先查询缓存。如果命中缓存则直接返回真正使用中,缓存一般会禁用,因为性能不高,如果表发生了更新操作,那么缓存中这个表的数据就会被清空。没有命中缓存则继续向下走
- 解析器:分为词法分析器和语法分析器。
- 词法分析器:根据关键字构建语法树
- 语法分析器:检查构建出的语法树是否满足mysql的规范
- 执行器:
- 预处理器:填充字段,校验表名字段名是否存在。
- 优化器:对sql进行优化,通过执行计划选择索引
- 执行器:调用存储引擎层执行sql查询数据。
-
mysql的行记录存储格式:
- 变长字段长度
- 空值列表
- 头信息
- row_id
- tx_id
- roll_ptr
- 行数据
-
索引:索引就相当于字典目录,通过索引可以快速定位数据的位置
- 索引分类
- 按照数据结构可以分为:b+树、hash、全文索引
- 按物理存储:聚簇索引(索引和数据放在一起)、非聚簇索引。
- 按字段:主键索引、唯一索引、前缀索引、普通索引
- 按字段个数:单列索引、复合索引。
- 当一个表字段经常用于查询(读多写少)、字段区分度高、常用语groupby、orderby就可以建立索引。
- 索引优化:前缀索引、索引覆盖、主键索引自增、索引列非空。
- 索引失效:对列加函数、模糊匹配,不符合最左匹配、or列非索引
- 索引分类
-
b+树:为什么要使用b+是作为mysql的存储结构?
- 二叉树: 容易瘸腿编程链表
- 平衡二叉树:因为子平衡,每次插入新节点都可能导致根节点的变化,节点变化频率太快。每个节点只能存两个数据,数据量大时树深度过高,io次数太多。
- b树:b树可以看成是M阶二叉树,每个节点可以存放的数据不止两条了。这样就可以解决树深度过高的问题,可以使树变得更矮更胖,存放更得的数据。但是b+树因为每个树枝节点还要存放数据,数据的大小是不确定的,因此每个节点可存放的数据范围也是不确定的,而且可能产生页分裂等。每次新增或删除节点时树发生变形,可能会导致根节点的替换。b树因为每个节点都存放数据,查询效率是不稳定的。不支持范围查询。
- b+树:与b树不同的是,只有叶子节点存放数据,树枝节点只存放指针和id,这样每个节点可存放的数据都是可计算的。效率稳定。而且b+树的叶子节点是通过链表链接的。方便使用范围查询。
-
事物:
-
事物的特性:原子性、一致性、隔离性、持久性
-
并行事物会产生的问题:脏读、幻读、不可重复读
-
事物的隔离级别:
- 读未提交:会产生脏读、幻读、不可重复读
- 读已提交:会产生幻读、不可重复读
- 可重复读:会产生幻读
- 串形化
-
mvcc:多版本并发控制器,通过mvcc可以读已提交、可重复读。可以避免幻读。
- 快照读:快照读是通过mvcc实现的。
- 当前读:通过加锁实现的。
快照读:在执行select语句时,会生成一个readview快照。readview内容包含:
creator_trx_id m_ids min_trx_id max_trx_id creator_trx_id
创建此快照的事物idm_ids
: 创建此快照时,活跃的事物id列表min_trx_id
活跃的事物id列表中最小的事物idmax_trx_id
下一次要创建的事物id。
当读取一条数据时,首先读数据的
trx_id
字段,获取最近更新这条数据的事物id,然后做如下判断:- 小于
min_trx_id
: 说明数据在创建快照前就已经提交了,那么数据可见。 - 大于
max_trx_id
: 代表数据在创建快照之后才更新的,不可见。 - 大于
min_trx_id
小于max_trx_id
, 判断是否在m_ids
中,如果在m_ids
中,代表数据是在创建快照后才提交的,当前数据不可见,那么就顺着数据的roll_ptr
字段找到上一个可见版本的数据。如果不在m_ids
中,那么代表在创建快照前已经提交了,数据可见。
-
可重复读:每次开启时候后生成一个快照,后面整个事物都是用这个快照。
-
读已提交:每次执行sql语句前都生成一个新的快照。
-
-
锁:mysql的锁有哪儿些:
-
表锁:
表锁、元数据锁、意向锁
-
行锁:
记录锁、间隙锁、临键锁、插入意向锁。间隙锁和临键锁只有可重复读才存在。
-
死锁了怎么办?
- 设置事物等待锁的超时时间
- 开启死锁检测
-
-
日志:mysql的日志有undo_log、redo_log、bin_log。其中undo_log和redo_log都是只有在innodb存储引擎下才存在的。
-
undo_log: 记录事物回滚日志,记录反操作sql。为了事物的原子性,实现回滚和mvcc。
-
redo_log: 记录所有发生改变的sql。两个数组组成唤醒缓冲区,大小是固定的。redo_log是正向的,会记录当前操作的会发生数据库修改的内容。会记录undo_log页发生的变化。
buffer_pool中存放有数据页、索引页、undo_log页等。这些页内容发生变化时会讲页标记为dirty。只有在事物提交时才会将这些变化刷写进磁盘,如果事物提交后,还没有来得及刷盘,此时mysql发生故障,那么就可能会造成数据丢失。所以通过redo_log来做数据的持久化。当数据页发生变化时,会先将页标记为脏页。然后在事物提交时生成一条redo_log日志,生成redo日志后就说明事物已经提交了,不需要立刻执行刷盘操作。后面如果mysql发生故障,可以通过redo_log来恢复脏页数据,保证数据不会丢失。实现事物的持久性
-
bin_log: 在mysql早期,只有bin_log,没有其他两个日志,bin_log就是用来记录每次的修改sql的。主要用来主从复制。
-
两阶段提交:在一个事物提交时,既要写redo_log日志,又要写bin_log日志。这两个日志都是异步写的,如何保证两个都能写成功?
- 如果写完redo_log日志,写bin_log时,操作系统断电了。等再次重启时,主库可以恢复数据,但是binlog丢失,从库无法同步这条数据,主从不一致。
- 如果先写bin_log,在写redo_log。数据同步到主库了,但是redolog里并没有,重启后,主库没有数据,但是从库有。
两阶段提交: mysql开启内部事务xa,写入redolog和binlog,将redolog拆分为两个阶段:
- prepare: 将内部事务id
xid
同时写入redolog。将redolog事务状态设置为prepare。同时持久化到磁盘。 - commit: 将xid写入binlog,并同时执行binlog持久化,只要binlog持久化成功了,就提交redolog。并将redolog的事务状态改为commit。
-
两阶段提交的问题:
- 磁盘id次数太多。引入了组提交的概念,减少磁盘io次数。
- 并发需要加锁,锁竞争激烈。
-
rocketmq
-
mq的作用:解藕、削峰、异步
-
mq的对比:rabbitmq、activemq、rocketmq、kafka
-
rabbitmq:
优点:基于内存,快,微妙级延迟;吞吐量w级;社区活跃有各种语言的客户端;mq功能完备,实现amqp协议。高可用主从架构
缺点:基于erlang开发,二次开发不方便。吞吐量太小。
-
activemq:最早期的mq。
优点:mq功能完善;微妙级;吞吐量万级;社区完备;高可用主从架构
缺点:可能会丢数据,社区活跃度不高
-
rocketmq:
优点:基于java开发,二开方便;mq功能较为完善;吞吐量10w级;ms级延迟;阿里出品,中文文档,学习方便;经过配置可以做到消息0丢失。;分布式架构
缺点:自定义协议,兼容性不太好;客户端只有java的,缺少其他语言的;
-
kafka:
优点:吞吐量10w级;ms级延迟;经过配置可以做到消息0丢失;分布式架构;
缺点:mq功能较为简单;可能会发生消息的重复消费;
-