一、MySql
1.什么是索引?
详细讲解索引原理的文章:MySQL索引背后的数据结构及算法原理_哲学渣的博客-CSDN博客_mysql索引背后的数据结构及算法原理
1.MySql建立表的时候是以ID为结点建立的一个B+树的数据结构,而索引则是以你所创立的字段为节点建立的B+树结构,也就相当于目录。
2.所以说建立索引是会占用内存的,以空间换取时间的策略。
3.MySql在查询的时候会对SQL查询进行优化,所以不是所有索引都会生效。例如:使用性别作为索引,应为索引表中大部分数据只有男和女,所以MySql会自动帮你优化掉,不进行索引查询。还有“like **123”,应为B+树是以首字母来查询的,所以左匹配是查不到的,但是右匹配可以,例如:“like 12**”。还有部分or运算和索引上的函数都会失效。
2.MySql事务
1.没有事务所产生的问题:
1)脏读:脏读是指一个事务正在访问数据,并且对数据进行了修改,但是这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
2) 不可重复读:不可重复读是指在一个事务内,多次读取同一个数据但是两次读取的数据不一样。
3) 幻读:幻读是指当事务不是独立执行时发生的一种现象,例如你查询一条数据不存在,当你要插入数据的时候,又被另一个事务插入了。
事务隔离级别由低到高为四种
- 读未提交(READ UNCOMITTED)---------在该隔离级别中,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。
- 读提交(READ COMMITTED)---------这是大多数数据库系统的默认隔离级别,它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变
- 可重复读(REPEATABLE READ)---它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。读取两次数据,每次都显示第一次的数据。
- 串行化(SERIALIZABLE)--在每个读取的数据行上加上共享锁
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMITTED(读未提交) | √ | √ | √ |
READ COMMITTED(读已提交) | × | √ | √ |
REPEATABLE READ(可重复读) | × | × | √ |
SERIALIZABLE(串行化) | × | × | × |
MySql默认隔离级别为可重复读。
3.数据库中char和varchar的区别
Char指定长度的字符,varchar可变长度的字符串
数据库中的char类型用于存储定长类型;效率比varchar要稍高,但是,占用空间比varchar要多
比如“asd”,对于char(9)就表示存储9个字节(包括6个空字节),在取值的时候就需要.trim()把两边的空号去掉。对于varchar(9),则是按照实际字节数存储的,只存储三个字节。
二、RocketMq
1.Rocket的消费模式
push模式和pull模式
集群模式和广播模式
2.Rocket组成
参考:(1条消息) rocketmq详解(全)_Ferao的博客-CSDN博客_rocketmq
1.NameServer 相当于注册中心
NameServer是集群部署的,各个NameServer之间不进行通信。所有Broker都会来各个NameServer中注册自己的信息;
2.Broker 节点
负责消息的存储和查询,支持主从部署,一个 Master 可以对应多个 Slave,Master 支持读写,Slave 只支持读。各个主副节点会进行数据的同步(0拷贝原理就在这里体现)
3.Consumer 消费者
可以集群部署。它也会先和 NameServer 集群中的随机一台建立长连接,得知当前要消息的 Topic 存在哪台 Broker Master、Slave上,然后它们建立长连接,支持集群消费和广播消费消息。
4.Producer 生产者
可以集群部署。它会先和 NameServer 集群中的随机一台建立长连接,得知当前要发送的 Topic 存在哪台 Broker Master上,然后再与其建立长连接,支持多种负载平衡模式发送消息。
5.简述流程:
先启动 NameServer 集群,各 NameServer 之间无任何数据交互,Broker 启动之后会向所有 NameServer 定期(每 30s)发送心跳包,包括:IP、Port、TopicInfo,NameServer 会定期扫描 Broker 存活列表,如果超过 120s 没有心跳则移除此 Broker 相关信息,代表下线。
这样每个 NameServer 就知道集群所有 Broker 的相关信息,此时 Producer 和Consumer 上线从 NameServer 就可以得知它要发送的某 Topic 消息在哪个 Broker 上,和对应的 Broker (Master 角色的)建立长连接,发送消息。
6.重复消费问题产生原因
产生原因有很多,主要分为业务层面和RocketMQ层面两种。
1>A发送消息到MQ需要B响应结束后给A反馈,但是B消费的时间比较长,触发了A的重试机制。或者类似的。
2>当RocketMQ成功接收到消息,并将消息交给消费者处理,如果消费者消费完成后还没来得及提交offset给RocketMQ,自己宕机或者重启了,那么RocketMQ没有接收到offset,就会认为消费失败了,会重发消息给消费者再次消费。
7.重复消费问题解决方案
一般是配合redsi,保存下来消息的ID,每次消费的时候查看redis是否有这个ID,这个Redis设不设置过期时间和设置为多少,要看业务对数量以及容错的大小。
8.零拷贝原理
在实际应用中,如果我们要把磁盘内容发送到远程服务器上,那么它必须要经过几个拷贝的过程。
前:磁盘->内核缓冲区->用户空间的缓冲区->内核空间的Socket Buffer->网卡缓冲区->服务器
后:磁盘->内核缓冲区->内核空间的Socket Buffer->网卡缓冲区->服务器
第一个从磁盘中去读取目标文件的内容拷贝到内核缓冲区
第二个CPU控制器把内核缓冲区中的数据拷贝到用户空间的缓冲区。
第三个在应用程序中调用write()方法,把用户空间缓冲区中的数据拷贝到内核空间的Socket Buffer中
第四个把在内核模式下的SocketBuffer中的数据赋值到网卡缓冲区。
最后网卡缓冲区会把数据发送到目标服务器
从上可以看出数据读取出来到发送出去要经历四次拷贝。在这四次拷贝中有两次拷贝是多余的。
第一从内核空间拷贝到用户空间,第二从用户空间再拷贝到用户空间,除此之外,由于用户空间建和内核空间的切换会带来CPU的上下文切换,对于CPU的性能也会造成影响。
而所谓的零拷贝就是把这两次多余的拷贝给忽略掉,应用程序可以直接把磁盘中的数据从内核中传送给socket,而不需要再去经过用户空间拷贝数据,零拷贝通过DMA,叫Direct Memory Access技术把文件内容复制到内核空间中的Read Buffer,接着把包含数据长度和位置的信息的文件描述符加载到Socket Buffer中,DMA引擎直接可以吧数据从内核空间传递到网卡设备,在这个流程中数据只经历了两次拷贝就把数据发送到网卡中并且减少了两次CPU的上下文切换对于效率是有非常大的提升 。在linux中零拷贝技术依赖于底层的sendfile()方法去实现而在java中是FileChannel.transferTo() 去实现。除此之外还有mmap的文件映射机制,它的原理是把磁盘文件映射到内存用户通过修改内存就可以修改磁盘文件,使用这种方式可以获得很大的I/O提升省去了用户空间到内核空间的开销
引用:零拷贝原理 - 云中哥 - 博客园 (cnblogs.com)
三、Redis
1.基础数据结构
1.String 2.Hash 3.List 4.Set (集合) 5.Zset(有序集合)
2.缓存穿透
1.什么是缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,这样每次请求都会去查库,不会查缓存,如果同一时间有大量请求进来的话,就会给数据库造成巨大的查询压力,甚至击垮 db 系统。比如查询用户id为-1的用户,应为物品不存在,所以不会记录到缓存中,导致一直访问db。
2.解决缓存穿透的方案
1>缓存中存入空值,但会导致大量的内存占用。所以引出了下面的方案
2>布隆过滤器(面试时只说这一种),我们把集合中的每一个值按照提供的 Hash 算法算出对应的 Hash 值,然后将 Hash 值对数 组长度取模后得到需要计入数组的索引值,并且将数组这个位置的值从 0 改成 1。在判断 一个元素是否存在于这个集合中时,你只需要将这个元素按照相同的算法计算出索引值,如 果这个位置的值为 1 就认为这个元素在集合中,否则则认为不在集合中。两个缺点 1.有一定的错误几率,有可能会把不是集合中的值查出来,不过不影响缓存穿透问题。2.不支持删除
3.缓存击穿
在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
解决方案也很简单,增加过期时间或者设置缓存的时候上锁即可。
4.缓存雪崩
当redis宕机或者大量缓存同一时间过期时造成的。解决方案,可以集群部署redis、设计多级缓存、过期时间随机值等。
5.为什么用redis比较快
1>基于内存。
2>高效的数据结构.String类型和跳表
3>采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求。
Redis:IO多路复用模型_Refuse_To_Delay的博客-CSDN博客_redis 多路复用模型
4>单线程,没有上下文切换,没有锁竞争
6.运行原理
7.redis持久化策略
两种策略RDB和AOF
RDB:根据指定的规则“定时”将内存中的数据存储在硬盘上,生成的快照。它是Redis默认的持久化方式
AOF:每次执行命令后将命令本身记录下来,每次执行命令都会将命令写入到aof文件中
四、Nacos
1.注册中心
发送心跳,客户端每几秒把ip、端口号、服务名、分组名、集群名等信息封装为一个Instance对象发送给nacos。
五、Feign
1.Feign的原理
对http协议的一些封装。
(2条消息) Spring Cloud Fegin 详解(一)_爱吃花生酱的猴子的博客-CSDN博客_fegin
六、Java锁
1.常见的几种锁
1.公平锁/非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁。反之就是非公平锁
2.可重入锁
可重入锁 ,指的是以线程为单位,当一个线程获取对象锁之后,这个线程可以再次获取本对象上的锁,而其他的线程是不可以的。synchronized 和 ReentrantLock 都是可重入锁。可重入锁的意义之一在于防止死锁。
3.独享锁/共享锁
独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。读写锁和互斥锁就是这么实现的。
4.乐观锁/悲观锁
乐观锁和悲观锁是一种思路并非具体实现。
乐观锁举例:再更新数据库一条数据之前查询这条数据的被操作次数为1,我更新完这个数据让操作数+1,再查询结果如果操作数不是2就回滚事务。
悲观锁:更新的时候直接上锁,不让别人操作就是悲观锁
5.偏向锁/轻量级锁/重量级锁
大多数情况下锁竞争是不会发生的,往往是一个线程多次获得同一个锁,于是引入了偏向锁,偏向锁不会被刻意的释放,如果没有竞争,线程再次请求锁时可以直接获得锁。
轻量级锁的性能介于偏向锁与重量级锁之间,在存在锁竞争的情况下,不需要让线程在阻塞与唤醒状态间切换。
重量级锁在JVM中有一个监视器(Monitor),保持了两个队列:锁竞争队列和信号阻塞队列,一个实现线程互斥,另一个实现线程同步。重量级锁在底层是靠操作系统的Mutex Lock实现的,线程在阻塞和唤醒状态间切换需要操作系统将线程在用户态与核心态之间转换,成本很高,所以最早的synchronized效率不高。
2.synchroized
四种状态 无锁、偏向锁、轻量级锁、重量级锁
可重入锁,涉及锁升级操作
3.锁升级
如果一个线程获得了锁,那么锁就进入偏向模式,当线程再次请求锁时,检查锁里保存的线程ID是否等于当前线程ID即可.
当第二个线程来竞争锁的时候偏向锁就会升级为轻量级锁。
若存在同一时间访问同一锁的情况,就会导致轻量级锁膨胀为重量级锁
4.CAS
compare and swap
CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值和A的值是一样的,那么就将内存里面的值更新成B。CAS是通过无限循环来获取数据的,若果在第一轮循环中,a线程获取地址里面的值被b线程修改了,那么a线程需要自旋,到下次循环才有可能机会执行。
①.CAS容易造成ABA问题。一个线程a将数值改成了b,接着又改成了a,此时CAS认为是没有变化,其实是已经变化过了,而这个问题的解决方案可以使用版本号标识,每操作一次version加1。在java5中,已经提供了AtomicStampedReference来解决问题。
②.CAS造成CPU利用率增加。之前说过了CAS里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu资源会一直被占用。
AutoInteger源码实现
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
七、线程
1.线程的几种实现方式
①继承Thread类 ②实现Runnable接口
③使用Callable和FutureTask实现有返回值的多线程
④使用线城池
2.ThreadLocal
ThreadLocal叫做线程变量,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。一般是用作静态变量再不同的线程中使用.
八、线程池
1.七大参数
corePoolSize:核心线程数
maximumPoolSize:最大线程数,此值必须大于1
keepAliveTime:多余空闲线程的存活时间。当前线程池数量超过核心线程数时,当空闲时间达到keepAliveTime时,多余空闲线程会被销毁直到核心线程数为止。
unit:keepAliveTime的单位
workQueue:线程池队列,里面放了被提交但是尚未执行的任务
threadFactory:表示线程池中工作线程的线程工厂,用于创建线程
handler:拒绝策略,当队列满了并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时,对任务的拒绝方式。
2.线程池队列workQueue
1>ArrayBlockingQueue
底层实现是数组,有界的堵塞队列,才用先进先出策略(FIFO)
传参的时候必须带上数组的大小
2>LinkedBlockingQueue
底层实现是链表,才用先进先出策略,吞吐量通常要高于ArrayBlockingQueue.
3>SynchronousQueue
一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态.
4>PriorityBlockingQueue
一个具有优先级的无限堵塞队列.底层实现是数组.
3.线程池拒绝策略
当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize时,如果还有任务到来就会采取任务拒绝策略
1>AbortPolicy(默认策略)
丢弃任务并抛出RejectedExecutionException异常
2>DiscardPolicy
丢弃任务,但是不抛出异常.
3>DiscardOldestPolicy
丢弃队列最前面的任务,然后重新提交被拒绝的任务
4>CallerRunsPolicy
由调用线程(提交任务的线程)处理该任务
4.线程池执行顺序
1.如果核心线程数有空闲则核心线程执行任务
2.如果核心线程已满,队列未满则放入队列
3.队列已满但最大线程数未满,则在额外的线程执行
4.最大线程数已满,则执行拒绝策略
九、Spring
1.Spring事务的传播机制
1>REQUIRED Spring默认的事务传播级别,使⽤该级别的特点是,如果上下⽂中已经存在事务,那么就加⼊到事务中执⾏,如果当前上下⽂中不存在事务,则新建事务执⾏。
2.>SUPPORTS 该传播级别的特点是,如果上下⽂存在事务,则⽀持事务加⼊事务,如果没有事务,则使⽤⾮事务的⽅式执⾏。所以说,并⾮所有的包在transactionTemplate.execute中的代码都会有事务⽀持。这个通常是⽤来处理那些并⾮原⼦性的⾮核⼼业务逻辑操作
3.>MANDATORY 该级别的事务要求上下⽂中必须要存在事务,否则就会抛出异常!配置该⽅式的传播级别是有效的控制上下⽂调⽤代码遗漏添加事务控制的保证⼿段。
4> REQUIRES_NEW 每次都要⼀个新事务,该传播级别的特点是,每次都会新建⼀个事务,并且同时将上下⽂中的事务挂起,执⾏当前新建事务完成以后,上下⽂事务恢复再执⾏。
5>. NOT_SUPPORTED 当前级别的特点就是上下⽂中存在事务,则挂起事务,执⾏当前逻辑,结束后恢复上下⽂的事务。
6>NEVER 要求上下⽂中不能存在事务,⼀旦有事务,就抛出runtime异
常,强制停⽌执⾏
7>NESTED 如果上下⽂中存在事务,则嵌套事务执⾏,如果不存在事务,则新建事务
2.AOP IOC
AOP:切面管理,在不修改业务代码的情况下对业务层进行增强,才用代理模式实现。常见的场景:性能检测、日志管理、缓存、拦截器和过滤器等。
IOC:Inversion of Control 俗称“控制反转”,是一个较为抽象的概念。意思是把一些权限交给另一个业务去解决。在Spring中就是你把new对象和销毁对象的这个操作交给了Spring来管理。主要目的也是为了松耦合。
3.bean的生命周期
1.实例化Bean:通过反射调用构造方法实例化对象。
2.依赖注入:装配Bean的属性
3.实现了Aware接口的Bean,执行接口方法:如顺序执行BeanNameAware、BeanFactoryAware、
ApplicationContextAware的接口方法。
4.Bean对象初始化前,循环调用实现了BeanPostProcessor接口的预初始化方法
(postProcessBeforeInitialization)
5.Bean对象初始化:顺序执行@PostConstruct注解方法、InitializingBean接口方法、init-method
方法
6.Bean对象初始化后,循环调用实现了BeanPostProcessor接口的后初始化方法
(postProcessAfterInitialization)
7.容器关闭时,执行Bean对象的销毁方法,顺序是:@PreDestroy注解方法、DisposableBean接口方法、destroy-method
4.Spring常用注解
5.循环依赖问题
十、类
1.类加载顺序
加载-验证-准备-解析-初始化-使用-卸载
或:加载-连接-初始化-使用-卸载
1.加载
(1)Java虚拟机将.class文件读入内存,并为之创建一个Class对象。
(2)任何类被使用时系统都会为其创建一个且仅有一个Class对象。
(3)这个Class对象描述了这个类创建出来的对象的所有信息,比如有哪些构造方法,都有哪些成员方法,都有哪些成员变量等
2.连接
(1)验证阶段。主要的目的是确保被加载的类(.class文件的字节流)满足Java虚拟机规范,不会造成安全错误。
(2)准备阶段。负责为类的静态成员分配内存,并设置默认初始值。
(3)解析阶段。将类的二进制数据中的符号引用替换为直接引用。
3.初始化
如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。
如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
静态代码块>初始化属性值>普通代码块>构造方法
注:类加载过程只是一个类生命周期的一部分,在其前,有编译的过程,只有对源代码编译之后,才能获得能够被虚拟机加载的字节码文件;在其后还有具体的类使用过程,当使用完成之后,还会在方法区垃圾回收的过程中进行卸载(垃圾回收)。