Java面试抱佛脚
2023-01-30 08:49:50 3 举报
AI智能生成
总结了一些面试高频知识点,可能不全,但对于面试前的抱佛脚应该够用了
作者其他创作
大纲/内容
集合
集合框架图
HashMap
1.7
数组+链表
头插法
多线程下会造成 死锁和数据丢失
1.8
数组+链表/红黑树
链表长度超过阈值(默认8)则转换为红黑树,如果链表长度小于6,则转换为链表
为什么不都设置成8?
如果退化链表也设置为8,会导致频繁转换,浪费资源
如果退化链表也设置为8,会导致频繁转换,浪费资源
尾插
多线程下会造成 数据覆盖
扩容机制
负载因子默认0.75,可调整,但不建议,负载因子越大哈希冲突的可能性越大
当长度大于size*0.75,则触发扩容,扩容大小为原来的2倍
大小总是2次幂,主要是因为HashMap计算元素位置采用位运算,2次幂更方便计算,并且可均匀分布
put流程
1、判断键值对数组table[i]是否为空(null)或者length=0,是的话就执行resize()方法进行扩容。
2、不是就根据键值key计算hash值得到插入的数组索引i。
3、判断table[i]==null,如果是true,直接新建节点进行添加,如果是false,判断table[i]的首个元素是否和key一样,一样就直接覆盖。
4、判断table[i]是否为treenode,即判断是否是红黑树,如果是红黑树,直接在树中插入键值对。
5、如果不是treenode,开始遍历链表,判断链表长度是否大于8,如果大于8就转成红黑树,在树中执行插入操作,如果不是大于8,就在链表中执行插入;在遍历过程中判断key是否存在,存在就直接覆盖对应的value值。
6、插入成功后,就需要判断实际存在的键值对数量size是否超过了最大容量threshold,如果超过了,执行resize方法进行扩容
2、不是就根据键值key计算hash值得到插入的数组索引i。
3、判断table[i]==null,如果是true,直接新建节点进行添加,如果是false,判断table[i]的首个元素是否和key一样,一样就直接覆盖。
4、判断table[i]是否为treenode,即判断是否是红黑树,如果是红黑树,直接在树中插入键值对。
5、如果不是treenode,开始遍历链表,判断链表长度是否大于8,如果大于8就转成红黑树,在树中执行插入操作,如果不是大于8,就在链表中执行插入;在遍历过程中判断key是否存在,存在就直接覆盖对应的value值。
6、插入成功后,就需要判断实际存在的键值对数量size是否超过了最大容量threshold,如果超过了,执行resize方法进行扩容
重写equals必须重写hashcode
线程不安全
想要线程安全需要加synchronized,但是不推荐,一般用ConcurrentHashMap
线程安全还有一个HashTable
HashSet是唯一的,底层实现还是HashMap
LinkedHashMap
数组+链表+双向链表
继承了HashMap
实现了插入有序
ConcurrentHashMap
线程安全
1.7
数组+链表
segment分段锁实现同步
继承reentranLock
volatile修饰节点指针
1.8
数组+链表/红黑树
通过部分加锁和利用CAS(比较并交换)来实现同步,get时候不加锁,Node时都用volatile修饰
CAS失败通过自旋保证成功
ArrayList
数组
查询快,增删慢
动态扩容
默认大小10,grow方法每次扩容为1.5倍
扩容后arraycopy
cypyOf方法优化后,CPU对内存可以块操作,增删也不慢
线程不安全
保证线程安全可以用Collections包装
CopyOnWriteArrayList线程安全
文件系统copy-on-write机制:原理类似懒加载,等用到了再分配,cow则是先复制一份再写入,保证数据完整性
add()方法实现:先lock锁,然后复制数组到新数组中,add到新数组中,array指向新数组
耗内存,只能保证数据的最终一致性,不能保证实时一致性
LinkedList
链表
增删快,查询慢
线程不安全
算法
从1亿数据中,查出最大的1000个数
将数据全部排序
快排,时间复杂度 O(nlogn)
缺点
对内存要求比较高;而且只取最大的1000个数,对其他数的排序是多余的
最小堆法
算法的时间复杂度为O(nmlogm),空间复杂度是10000(常数)
取前1000个数组成最小堆,取最小堆最小值为n,从第1001个数开始遍历,与n进行比较,比n小则跳过,
比n大则替换n的值,再对最小堆排序取最小值n,继续遍历。
比n大则替换n的值,再对最小堆排序取最小值n,继续遍历。
基础
抽象类与接口区别
抽象类只能被子类继承,接口只能被实现
抽象类可以申明方法,实现方法;接口只能申明不能实现
抽象类定义的是普通变量;接口定义的是静态变量和公共变量
抽象类是重构的结果;接口是设计的结果
抽象类里有方法和属性;接口只能有抽象方法和常量
抽象类主要用来抽象类别;接口用来抽象功能
过滤器
主要过滤访问客户端的资源
场景
设置同一的编码
过滤敏感字符
登录校验
url级别访问的权限
Filter过滤器的特点
依赖servlet
init()在容器初始化的时候只执行一次
doFilter() 目标请求之前拦截执行,拦截之后需要放行才开始执行目标方法
Filter可以拦截所有请求。包括静态资源[css,js...]
基于函数回调实现
拦截器
拦截器是springmvc提供的,类似于过滤器。主要用于拦截用户请求并作相应的处理
场景
日志记录
权限、登录校验
preHandle拦截器的特点
不依赖servlet
preHandle() 在目标请求完成之前执行。有返回值Boolean类型,true:表示放行
postHandle() 在目标请求完成之后执行
拦截器只能拦截action请求。不包括静态资源[css,js...]
基于java反射机制实现。
在拦截器的生命周期中,可以多次被调用。
过滤器拦截器AOP三者区别
1、过滤器和拦截器触发时机不一样,过滤器是在请求进入容器后,但请求进入servlet之前进行预处理的。请求结束返回也是,是在servlet处理完后,返回给前端之前。
2、拦截器可以获取IOC容器中的各个bean,而过滤器就不行,因为拦截器是spring提供并管理的,spring的功能可以被拦截器使用,在拦截器里注入一个service,可以调用业务逻辑。而过滤器是JavaEE标准,只需依赖servlet api ,不需要依赖spring。
3、过滤器的实现基于回调函数。而拦截器(代理模式)的实现基于反射
4、Filter是依赖于Servlet容器,属于Servlet规范的一部分,而拦截器则是独立存在的,可以在任何情况下使用。
5、Filter的执行由Servlet容器回调完成,而拦截器通常通过动态代理(反射)的方式来执行。
6、Filter的生命周期由Servlet容器管理,而拦截器则可以通过IoC容器来管理,因此可以通过注入等方式来获取其他Bean的实例,因此使用会更方便。
2、拦截器可以获取IOC容器中的各个bean,而过滤器就不行,因为拦截器是spring提供并管理的,spring的功能可以被拦截器使用,在拦截器里注入一个service,可以调用业务逻辑。而过滤器是JavaEE标准,只需依赖servlet api ,不需要依赖spring。
3、过滤器的实现基于回调函数。而拦截器(代理模式)的实现基于反射
4、Filter是依赖于Servlet容器,属于Servlet规范的一部分,而拦截器则是独立存在的,可以在任何情况下使用。
5、Filter的执行由Servlet容器回调完成,而拦截器通常通过动态代理(反射)的方式来执行。
6、Filter的生命周期由Servlet容器管理,而拦截器则可以通过IoC容器来管理,因此可以通过注入等方式来获取其他Bean的实例,因此使用会更方便。
当有过滤器和拦截器时的执行流程
执行顺序是:过滤器--》拦截器--》切面,拦截规则也越来越细
使用场景
一般情况下数据被过滤的时机越早对服务的性能影响越小,因此我们在编写相对比较公用的代码时,优先考虑过滤器,然后是拦截器,最后是aop。比如权限校验,一般情况下,所有的请求都需要做登陆校验,此时就应该使用过滤器在最顶层做校验;日志记录,一般日志只会针对部分逻辑做日志记录,而且牵扯到业务逻辑完成前后的日志记录,因此使用过滤器不能细致地划分模块,此时应该考虑拦截器,然而拦截器也是依据URL做规则匹配,因此相对来说不够细致,因此我们会考虑到使用AOP实现,AOP可以针对代码的方法级别做拦截,很适合日志功能。
IO模型
同步阻塞(BIO)
同步非阻塞(NIO)
默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK
异步IO
IO多路复用
这里复用的是指复用一个或几个线程,用一个或一组线程处理多个IO操作,
减少系统开销小,不必创建和维护过多的进程/线程
减少系统开销小,不必创建和维护过多的进程/线程
原理
IO多路复用使用两个系统调用(select/poll/epoll和recvfrom),blocking IO只调用了recvfrom;select/poll/epoll 核心是可以同时处理多个connection,而不是更快,所以连接数不高的话,性能不一定比多线程+阻塞IO好,多路复用模型中,每一个socket,设置为non-blocking,阻塞是被select这个函数block,而不是被socket阻塞的。
机制
select机制
客户端操作服务器时就会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和exceptfds(异常)。select会阻塞住监视3类文件描述符,等有数据、可读、可写、出异常 或超时、就会返回;返回后通过遍历fdset整个数组来找到就绪的描述符fd,然后进行对应的IO操作。
优点:
几乎在所有的平台上支持,跨平台支持性好
缺点:
由于是采用轮询方式全盘扫描,会随着文件描述符FD数量增多而性能下降。
每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间)
默认单个进程打开的FD有限制是1024个,可修改宏定义,但是效率仍然慢。
客户端操作服务器时就会产生这三种文件描述符(简称fd):writefds(写)、readfds(读)、和exceptfds(异常)。select会阻塞住监视3类文件描述符,等有数据、可读、可写、出异常 或超时、就会返回;返回后通过遍历fdset整个数组来找到就绪的描述符fd,然后进行对应的IO操作。
优点:
几乎在所有的平台上支持,跨平台支持性好
缺点:
由于是采用轮询方式全盘扫描,会随着文件描述符FD数量增多而性能下降。
每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,并进行遍历(消息传递都是从内核到用户空间)
默认单个进程打开的FD有限制是1024个,可修改宏定义,但是效率仍然慢。
poll机制
基本原理与select一致,只是没有最大文件描述符限制,因为采用的是链表存储fd。
基本原理与select一致,只是没有最大文件描述符限制,因为采用的是链表存储fd。
epoll机制
epoll之所以高性能是得益于它的三个函数
1)epoll_create()系统启动时,在Linux内核里面申请一个B+树结构文件系统,返回epoll对象,也是一个fd
2)epoll_ctl() 每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加删除对应的链接fd, 绑定一个callback函数。
3)epoll_wait() 轮训所有的callback集合,并完成对应的IO操作
优点:
没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄
效率提高,使用回调通知而不是轮询的方式,不会随着FD数目的增加效率下降
内核和用户空间mmap同一块内存实现
epoll之所以高性能是得益于它的三个函数
1)epoll_create()系统启动时,在Linux内核里面申请一个B+树结构文件系统,返回epoll对象,也是一个fd
2)epoll_ctl() 每新建一个连接,都通过该函数操作epoll对象,在这个对象里面修改添加删除对应的链接fd, 绑定一个callback函数。
3)epoll_wait() 轮训所有的callback集合,并完成对应的IO操作
优点:
没fd这个限制,所支持的FD上限是操作系统的最大文件句柄数,1G内存大概支持10万个句柄
效率提高,使用回调通知而不是轮询的方式,不会随着FD数目的增加效率下降
内核和用户空间mmap同一块内存实现
多线程
进程、线程
进程是程序运行的基本单位,拥有独立的堆和方法区
线程是比进程更小的单元,进程内的线程共享堆和方法区,每个线程拥有独立的程序计数器、虚拟机栈、本地方法栈
为什么程序计数器、虚拟机栈、本地方法栈是线程私有的?
程序计数器私有是为了能够准确记录每个线程的字节码执行地址,线程切换时能回到正确的执行位置。
线程创建时会创建一个虚拟机栈,内部栈帧StackFrame保存着线程执行需要的方法
本地方法栈与虚拟机栈同理,保存一些本地方法的调用,保证线程中局部变量不被其他线程访问到
虽然是线程私有的,但是也不一定是线程安全的
优缺点
多核cpu时代,使用多线程可以提高cpu使用率,减小线程上下文切换的开销。
单核CPU不能同时进行多任务,单核同一时间只能处理一个线程任务
但是多线程的缺点明显,内存泄漏、上下文切换、死锁等问题
线程生命周期和状态
NEW(初始化还未status)、RUNNABLE(运行中)、WAITNG(等待)、TIMEWAITNG(超时等待)、BLOCKED(阻塞)、TERMINATED(终止)
sleep()和wait()方法主要区别就是:sleep不释放锁,wait释放锁
上下文切换
当程序从保存到再加载的过程被称为一次上下文切换
死锁
多个线程被阻塞,一个或多个线程都在等待某个资源的释放,造成无限期的阻塞
产生死锁的四个条件
互斥、请求保持、不剥夺、循环阻塞
避免死锁的方案
1、缩小锁的使用范围,比如只在使用共同变量的时候加锁
2、固定加锁的顺序,比如利用hash计算加锁顺序
3、设置可释放的定时锁
2、固定加锁的顺序,比如利用hash计算加锁顺序
3、设置可释放的定时锁
synchronized
解决多线程之间资源访问的同步性
前期是重量锁,java1.6优化后减少了锁操作的开销
修饰静态方法,或修改代码块,则都是给类加锁
修饰实例方法,是给对象实例加锁
构造方法不能使用synchronized修饰,因为构造方法本来就是线程安全的
原理:
修饰代码块主要是jvm字节码指令monitorenter 和 monitorexit 来记录开始和结束,结合锁计数器来实现同步
修饰方法主要是ACC_SYNCHRONIZED标识,标识指明该方法是一个同步方法
本质都是对对象监视器monitor的获取。
非公平锁
线程会先去尝试获取锁,获取不到才进队列或者自旋等待
不尝试获取锁,直接进入队列中等待锁释放,则为公平锁
锁膨胀
偏向锁
当前线程ID与markword存放的值不一致,CAS尝试更换线程ID
轻量级锁
偏向锁CAS失败后会升级为轻量级锁,也是CAS实现,只是多了将mark word拷贝到Lock Record的过程
重量级锁
CAS自选失败一定次数后再升级为重量级锁
volatile
防止jvm 指令重排
懒汉双重检测下的 创建对象,不能保证原子性,会造成指令重排,可以在属性上加volatile
保证变量的可见性
当共享变量在发生写操作时,会更改主存数据,并将其他缓存强制失效。
原理:基于缓存一致协议(MESI)
不能保证原子性
CAS
compare and swap 比较和交换
内存V,预期值 O,修改值 N;当V = O 时,将内存值修改为N
不属于锁,是一种轻量级的无锁算法
ABA问题
通过增加版本号解决
Java也提供了AtomicStampedReference类
自循环长时间执行不成功,消耗大
java8的LongAdder,使用了分段CAS和自动分段迁移来解决空循环和自循环等待问题
synchronized与volatile区别
volatile 只能修饰变量,sync能修饰方法和代码块
volatile 是轻量级的,比sync执行效率要快
volatile 只能保证数据的可见性,不能保证原子性,sync都可以保证
volatile 是解决变量在多线程之间的可见性,sync是保证资源在多线程之间的同步性
ThreadLocal
ThreadLocalMap是ThreadLocal的静态内部类,通过给每个线程都分配一个变量副本达到数据隔离的效果,来保证线程的安全
与synchronized相比
ThreadLocal是数据隔离,sync是锁机制保证变量只被一个线程访问,是数据共享
内存泄漏
因为key是弱引用,value是强引用,当没有被引用时,key可能会被垃圾回收机制清理掉,只留value永久保存,造成内存泄漏。
解决办法就是使用完ThreadLocal方法后手动调用remove()
ThreadLocal被回收&&线程被复⽤&&线程复⽤后不再调⽤ThreadLocal的set/get/remove⽅法 才可能发⽣内存泄露
在Spring事务上的体现
Spring事务原子性要求保证事务的操作需要在同一个数据库上执行,Spring就是通过ThreadLocal实现的,
ThreadLocal存储的类型是⼀个Map,Map中的key 是DataSource,value 是Connection(为了应对多
数据源的情况,所以是⼀个Map)⽤了ThreadLocal保证了同⼀个线程获取⼀个Connection对象,从⽽
保证⼀次事务的所有操作需要在同⼀个数据库连接上
ThreadLocal存储的类型是⼀个Map,Map中的key 是DataSource,value 是Connection(为了应对多
数据源的情况,所以是⼀个Map)⽤了ThreadLocal保证了同⼀个线程获取⼀个Connection对象,从⽽
保证⼀次事务的所有操作需要在同⼀个数据库连接上
4种引用
强引用
只要没有赋值null,就不会被GC回收
软引用
需要继承 SoftReference实现
内存充足不会回收,内存不足会回收
弱引用
需要继承WeakReference实现
只要发生GC,就会回收
虚引用
需要继承PhantomReference实现
跟踪对象垃圾回收的状态,当回收时通过引⽤队列做些「通知类」的⼯作
Lock
ReentrantLock可重入锁
可设置为公平锁
提供了比synchronized更多的功能,但是Lock不主动释放锁,容易造成死锁,资源竞争激烈的情况下性能可以维持常态。
ReadWriteLock
ReentrantReadWriteLock
WriteLock写锁
ReadLock读锁
线程池
提高资源利用率
实现Runnable接口和Callable接口区别
Runnable接口不返回结果或抛出异常
Callable可以
执行execute()方法submit()方法区别
execute方法不返回任务是否被线程池执行成功的结果,submit方法返回Future类型对象可判断是否执行成功
ThreadPoolExecutor
new FixedThreadPool
返回一个固定数量的线程池,没有空闲线程使用的任务在队列中等待
new SingleThreadExecutor
返回只有一个线程的线程池,其他在队列中等待
new CachedThreadPool
可根据实际情况调整线程数量的线程池,没有空闲线程执行任务时会创建线程处理任务
参数
corePoolSize核心线程数
最小可以同时运行的线程数
maximumPoolSize最大线程数
当队列容量满时,线程数变为最大线程数
workQueue阻塞队列
任务来时判断运行线程数是否达到核心线程数,达到的话任务进入队列
keepAliveTime等待线程销毁时间
当线程数量大于核心线程数时,等待时间超过keepAliveTime后销毁多余线程
unit
keepAliveTime时间单位
handler饱和策略
饱和策略
默认ThreadPoolExecutor.AbortPolicy,抛出错误拒绝新任务处理
ThreadPoolExecutor.CallerRunsPolicy,伸缩队列,延迟处理任务,不丢弃任务,但是影响性能。
JUC
java.util.concurrent包
包含了多线程并发常用的工具类,比如线程池、异步IO、轻量级任务框架等。
4类原子类型
基本类型(AtomicInteger整形、长整型、布尔型原子类)
数组类型
引用类型
对象属性修改类型
AQS
是一个构建锁和同步器的架构
ReentrantReadWriteLock、SynchronousQueue等都是基于AQS的
原理
多个线程共享一个空闲资源时,其中某个线程获取到后会被锁定,其他线程被放到了CLH队列锁中阻塞等待,CLH是个双向队列,当资源state被更改为0时,队列中其他线程获取资源。
AQS中state的原子性是通过CAS实现的,state又是被volatile修饰的,保证了可见性
组成
队列 + state 状态变量
对资源的共享方式
Exclusive独占
ReentrantLock为例,独占资源通过state判断,等于0才被释放,重入锁则state+1
Share共享
CountDownLatch为例,共享资源初始state=n为n个线程,当state=0则任务完成
CountDownLatch
允许count个线程阻塞在一个地方,直到所有线程执行完才继续下面的执行逻辑
计算机网络
TCP/IP网略体系结构
物理层
机械、电子等原始比特流传输
数据链路层
将原始比特流转化成逻辑传输线路
网略层
选择合适的网略路由和节点,确保数据的及时传输
IP/IPv6协议
传输层
负责两台主机进程之间的通信传输
TCP、UDP
会话层
不同机器之间用户建立和管理会话
RPC远程调用协议、SSL、TLS安全协议
表示层
对信息进行加密解密、压缩解压缩
应用层
为不同的网略应用提供不同的网略协议
HTTP、FTP、SMTP
TCP、UDP
TCP三次握手
客户端--->发送SYN标志的数据包--->服务端
服务端--->发送SYN/ACK标志的数据包--->客户端
客户端--->发送ACK标志的数据包--->服务端
TCP四次挥手
客户端--->发送FIN,关闭数据传输--->服务端
服务端--->发送ACK,收到序号加1--->客户端
服务端--->发送FIN,关闭与客户端的连接--->客户端
客户端--->发送ACK,收到序号加1--->服务端
TCP,UDP区别
TCP是面向连接的、数据传输可靠、传输形式是字节流、传输效率慢、所需资源较多、常用于文件邮件传输
UDP不面向连接、数据传输不可靠、传输形式是数据报文、传输效率快、所需资源少、常用于语音、视频、直播等
TCP保证数据传输的方法
切割应用数据为合适的数据块
给数据块编号并排序
校验和,确保端对端一致
利用滑动窗口实现流量控制
拥塞控制减少数据的传输
ARQ协议
超时重发
浏览器输入url到页面展示的过程
浏览器通过DNS解析查找url映射的ip地址
浏览器和对应ip的服务端进行TCP连接
浏览器发送HTTP请求,cookies会随请求发送给服务端
服务端接收请求参数等进行处理,返回HTTP报文
浏览器解析报文进行渲染展示
连接结束
HTTP超文本传输协议
各版本连接方式
1.0默认短连接
1.1默认长连接
HTTP2实现了多路复用,头部压缩、服务器推送。
HTTPS
对称加密、非对称加密、数字签名、数字证书
cookie
主要用来跟踪浏览器用户身份的会话
服务端分配了cookie后,以key-value形式存放到浏览器
浏览器存放容易被劫持,不安全
默认随着会话关闭,cookie失效,但是可以手动设置过期时间
session
通过服务端记录用户的状态
存放在服务器,用sessionid存到cookie中来区分客户端身份
较安全,但是用户量大会占用服务器资源
过期销毁,或者invalidate销毁
token
字符串,存放在cookie中
无状态,可扩展,对服务器压力小
无法主动过期
JVM
内存模型
堆
线程共享
是java虚拟机中占用内存最大的一块区域,主要存放对象实例和数组
是GC管理的主要区域,利用分代回收机制分为 新生代、老生代、永生代(1.7以后移除了)
方法区
线程共享
主要存储虚拟机加载的类、常量、静态变量、class文件代码
1.8以前是GC的永生代,1.8以后移除替换为元空间,主要是因为元空间是直接内存,出现内存溢出的几率下降
运行时常量池是方法区的一部分,但是1.7以后字符串常量池放到了堆中
栈
虚拟机栈
线程私有
由栈帧组成,每个栈帧包含:局部变量表、操作数栈、动态链接、方法出口等。
函数调用则压栈,调用结束则弹栈。
函数调用则压栈,调用结束则弹栈。
主要为虚拟机执行java方法,也就是字节码服务
两种Error
StackOverFlowError栈溢出
线程请求栈的深度超过了虚拟机栈的最大深度
OutOfMemoryError内存溢出
本地方法栈
线程私有
主要为虚拟机执行本地Native方法服务。与虚拟机栈相似
Native方法
hashcode、wait 、notify、notifyAll 等都是本地方法
程序计数器
线程私有
保证程序代码运行顺序。
多线程情况下,记录当前线程执行位置,这也是线程私有的原因。
多线程情况下,记录当前线程执行位置,这也是线程私有的原因。
源文件到代码运行的过程
编译
将源码文件编译成可供JVM读取的.class文件
编译过程会对源码文件进行 语法分析、语义分析、注解解析等,再形成字节码文件
加载
将class文件加载到JVM中
装载
查找并加载类的⼆进制数据,在JVM「堆」中创建⼀个java.lang.Class类的对象,并将类相关的信息存储在JVM「⽅法区」中
连接
对class的信息进⾏验证、为「类变量」分配内存空间并对其赋默认值
初始化
为类的静态变量赋予正确的初始值。
解释
把字节码转换为操作系统识别的指令
执行
操作系统把解释器解析出来的指令码,调⽤系统的硬件执⾏最终的程序指令
创建(new)对象的过程
类加载检查
检查指令的参数是否在常量池中定位到类的符号引用,是否已被加载过、解析或初始化过。
分配内存区域
在堆中分配的两种方式
指针碰撞
适用于堆内存规整,没有内存碎片的情况下。GC收集器算法为“标记--整理”
原理:将用过的内存和没用过的内存分开整理,用指针做分界线,分配给对象的内存大小作为指针移动的大小
空闲列表
适用于堆内存不规整情况下。GC收集算法为“标记--清除”
原理:虚拟机会维护一个列表,记录哪些内存块是可用的,分配给一块足够大的内存块给该对象
多线程下保证线程安全的方式
CAS+失败重试 的方法保证原子性
TLAB 预分配一块内存,当对象所需内存大于TLAB中剩余内存或者用尽时,使用CAS方式分配内存
初始化零值
将内存空间内的对象字段数据类型初始零值
设置对象头
设置基本信息到对象头中,比如 实例的类、对象的哈希值、对象GC分代年龄等
执行init方法
类加载机制
生命周期
加载、验证、准备、解析、初始化、使用、卸载
双亲委派机制
类加载器接收到请求,首先不会自己去加载,而是将请求委派给父加载器完成,每层加载器都这样,直到Bootstrap类(启动类)加载器中,父类反馈自己无法加载请求时,子类才会去加载
好处是java类具备了优先级关系,满足java体系建设,可以防⽌内存中存在多份同样的字节码
如何打破双亲委派机制
因为加载class核⼼的⽅法在LoaderClass类的loadClass⽅法上(双亲委派机制的核
⼼实现),那只要我⾃定义个ClassLoader,重写loadClass⽅法(不依照往上开始寻找类加载
器),那就算是打破双亲委派机制了
⼼实现),那只要我⾃定义个ClassLoader,重写loadClass⽅法(不依照往上开始寻找类加载
器),那就算是打破双亲委派机制了
分代回收算法
新生代
Eden 区、From s0区(Survior0)、To s1区
老年代
永久代/元空间
垃圾回收算法
标记-清除
标记不需要清除的对象,然后清除其他对象,会造成大量不连续的内存碎片
效率低,浪费空间
标记-复制
将内存分割成大小相同的两块区域,对一块区域被引用的对象进行标记,将标记对象复制到另一块区域中,然后对元区域对象进行全部清除
解决了效率问题,时间换空间,适用于新生代,也是为什么新生代GC效率比老年代高的原因
标记-整理
对有效的对象进行标记,然后整理挪动到一边,对边界以外的对象进行GC
合理利用了空间,但是效率低,适用于老年代这种不需要频繁GC,但需要空间的内存区域
GC分类
Minor GC
对新生代进行垃圾收集
当 JVM 无法为一个新的对象分配空间时(Eden区空间不足)会触发 Minor GC
Major GC
对老年代进行收集,但有的语境下可能是整堆回收
Full GC
整堆回收
触发条件
老年代内存区域不足
在Survivor区域的对象满足晋升到老年代的条件时,晋升进入老年代的对象大小大于老年代的可用内存,这个时候会触发Full GC
Metaspace区内存达到阈值
Metaspace(元空间)使用的是本地内存,而不是堆内存,也就是说在默认情况下Metaspace的大小只与本地内存大小有关。-XX:MetaspaceSize=21810376B(约为20.8MB)超过这个值就会引发Full GC,这个值不是固定的,是会随着JVM的运行进行动态调整的
垃圾回收器
Serial回收器
单线程回收器,回收过程中,必须暂停其他所有的工作线程
简单高效,但是会影响其他线程
新生代 标记-复制,老年代 标记-整理
ParNew回收器
多线程回收器,是Serial的多线程版本,回收线程执行中也会阻塞用户线程
常用于Server模式下虚拟机,可以与CMS收集器配合使用
新生代 标记-复制,老年代 标记-整理
Parallel Scavenge回收器
1.8的默认回收器,多线程的
高效率的利用了CPU
新生代 标记-复制,老年代 标记-整理
CMS回收器
并发收集器,回收线程和用户线程可并发执行
优点是并发收集,低停顿。缺点是 对CPU资源敏感、会产生大量空间碎片,无法处理浮动垃圾。
标记-清除算法
G1
并发收集器,高概率满足GC停顿要求,还保证了高吞吐量
保留了分代的概念,并且可设置预留停顿时间
标记-整理算法
jvm内存调优
调优原则围绕减少GC来、参考指标一般是 吞吐量、停顿时间、垃圾回收频率
常用命令
-Xmn 调整新生代堆内存大小
-Xms 设置最小堆空间;-Xmx 设置最大堆空间
-XX:MaxTenuringThreshold 设置对象晋升到老年代年龄的阈值
堆内存中对象分配的基本策略
优先分配到Eden新生代中
分配担保机制
遇到Eden内存满了,但是s代中也没有内存分配时会提前放到老年代中
遇到大对象比如字符串或数组时会直接放到老年代中,为了避免为大对象分配内存时触发分配担保机制带来的复制降低效率
判断对象死亡的方法
引用计数法
给对象加个计数器,被引用就+1,引用失效就-1,=0则未引用
可达性分析
都从从GC boots 对象根节点向下查找,没有被引用链关联的对象则判断为未引用对象。
性能调优
OOM
内存泄漏
线程死锁
锁征用
Java进程消耗CPU过高
JVM性能调优工具
1. 通过jps命令查看Java进程「基础」信息(进程号、主类)。这个命令很常⽤的就
是⽤来看当前服务器有多少Java进程在运⾏,它们的进程号和加载主类是啥
是⽤来看当前服务器有多少Java进程在运⾏,它们的进程号和加载主类是啥
2. 通过jstat命令查看Java进程「统计类」相关的信息(类加载、编译相关信息统
计,各个内存区域GC概况和统计)。这个命令很常⽤于看GC的情况
计,各个内存区域GC概况和统计)。这个命令很常⽤于看GC的情况
3. 通过jinfo命令来查看和调整Java进程的「运⾏参数」
4. 通过jmap命令来查看Java进程的「内存信息」。这个命令很常⽤于把JVM内存信
息dump到⽂件,然后再⽤MAT( Memory Analyzer tool 内存解析⼯具)把⽂件进⾏分析
息dump到⽂件,然后再⽤MAT( Memory Analyzer tool 内存解析⼯具)把⽂件进⾏分析
5. 通过jstack命令来查看JVM「线程信息」。这个命令⽤常⽤语排查死锁相关的问题
6. 还有近期⽐较热⻔的Arthas(阿⾥开源的诊断⼯具),涵盖了上⾯很多命令的功
能且⾃带图形化界⾯。这也是我这边常⽤的排查和分析⼯具
能且⾃带图形化界⾯。这也是我这边常⽤的排查和分析⼯具
系统调优步骤
1、查看是否是数据库问题。比如索引是否使用正确、需不需要分库分表等
2、考虑是否需要服务器硬件扩容
3、排查和优化应用代码层面问题。比如是否存在资源浪费、是否可以用并发处理请求
4、JVM层面排查优化,比如是否存在频繁GC的情况
5、网络和系统层面排查。查看CPU,内存,硬盘读写等指标是否正常
JVM 的 JIT优化技术
方法内联
把「⽬标⽅法」的代码复制到「调⽤的⽅法」中,避免发⽣真实的⽅法调⽤
逃逸分析
判断⼀个对象是否被外部⽅法引⽤或外部线程访问的分析技
术,如果「没有被引⽤」,就可以对其进⾏优化
术,如果「没有被引⽤」,就可以对其进⾏优化
Spring
设计模式
简单工厂模式
实质:由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类
Spring使用工厂模式可以通过BeanFactory创建bean
设计意义
松耦合。可以将原来硬编码的依赖通过spring的beanFactory这个工厂类来注入依赖。
bean的额外处理。通过spring接口的暴露,在实例化bean的阶段我们可以进行一些额外的处理,这些处理只需要
让bean实现对应的接口即可。
让bean实现对应的接口即可。
工厂模式
spring实现:FactoryBean接口
实现原理
实现了FactoryBean接口的bean是一类叫做factory的bean。
其特点是,spring会在使用getBean()调用获得该bean时,会自动调用该bean的getObject()方法,所以返回的不是factory这个bean,而是这个bean.getOjbect()方法的返回值。
其特点是,spring会在使用getBean()调用获得该bean时,会自动调用该bean的getObject()方法,所以返回的不是factory这个bean,而是这个bean.getOjbect()方法的返回值。
FactoryBean 和 BeanFactory 区别
BeanFactory是个Factory,也就是IOC容器或对象工厂,FactoryBean是个Bean。在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的。但对FactoryBean而言,这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似
代理模式
Spring AOP 通过动态代理的方式实现,
切面在应用运行过程中被织入。织入切面时AOP容器会为目标对象动态创建一个代理对象。
单例模式
Spring中Bean都是单例的
通过ConcurrentHashMap单例注册表实现单例
好处:节省创建对象所花费的时间,减小系统开销,减轻GC压力。
懒汉式
等⽤到的时候,才进⾏初始化
简单懒汉式(在⽅法声明时加锁)
DCL双重检验加锁(进阶懒汉式)
静态内部类(优雅懒汉式)
饿汉式
还没被⽤到,就直接初始化了对象
适配器模式
解决了不兼容类之间共同工作问题
Spring AOP 的advice通知、Spring MVC对 中 HandlerAdapter 适配器 对 Controller的适配
实现原理:HandlerAdatper根据Handler规则执行不同的Handler
观察者模式
对象行为类模式,即一个对象依赖另一个对象的变动而变动
Spring 事件驱动模型
事件角色
ApplicationEvent抽象类
监听器角色
ApplicationListener接口
发布者角色
ApplicationEventPublisher接口
装饰器模式
Spring中用到的装饰器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator。
实质:动态的给一个对象添加一些额外的职责
策略模式
Spring的资源访问Resource接口,这个接口提供了更强的资源访问能力。
一个类的行为或其算法可以在运行时更改
解决多重if。。else
几种设计模式之间的区别
装饰器:原来的方法(编码)已经不能满足新需求了,需要对其进行扩展(设计、测试),理论上我们可以对原方法无限地装饰下去,比如我们可以在"设计"之前再加个"需求分析",在"测试"之后再加个"部署实施"等等。也可以去掉某些装饰器。
适配器:原来的接口已经不兼容了,适配器在原对象和目标对象中间,通过对原对象兼容的那个接口,通过转换,调用目标对象那个不兼容的接口。举个不恰当的例子,一个英国人去买饭,听不懂中国服务员说什么(接口不兼容),这时候来了一个翻译(适配器),他能与英国人交流(接口适配成功),然后翻译不干活,而是通过中国服务员的活动,将结果再返回给英国人。
代理:你们要实现什么功能我不管,我只负责调用该调用的方法。有点类似前台MM,你们干什么我不管,你就告诉我你找谁,我给你找去。
模板方法:如果说装饰器是在原有的方法上扩展很多方法,那么模板方法模式就是将原来很多固定的方法抽出到父类里。一个是加法,一个是减法
适配器:原来的接口已经不兼容了,适配器在原对象和目标对象中间,通过对原对象兼容的那个接口,通过转换,调用目标对象那个不兼容的接口。举个不恰当的例子,一个英国人去买饭,听不懂中国服务员说什么(接口不兼容),这时候来了一个翻译(适配器),他能与英国人交流(接口适配成功),然后翻译不干活,而是通过中国服务员的活动,将结果再返回给英国人。
代理:你们要实现什么功能我不管,我只负责调用该调用的方法。有点类似前台MM,你们干什么我不管,你就告诉我你找谁,我给你找去。
模板方法:如果说装饰器是在原有的方法上扩展很多方法,那么模板方法模式就是将原来很多固定的方法抽出到父类里。一个是加法,一个是减法
Spring 包含的模块
Core
基础类库,包含基本所有功能,提供IOC依赖注入功能
,比如beans、core、context
,比如beans、core、context
AOP
提供面向切面编程
JDBC
数据库连接
JMS
java消息服务
Web
为创建web应用程序提供支持
Spring AOP & IoC
AOP
静态代理
实现类
动态代理
JDK 动态代理
实现接口
java反射机制生成一个代理接口的匿名类
调用具体方法的时候调用invokeHandler
反射:在运行状态中,对于任意一个类都能够知道它的属性和方法,
对于任意一个对象都能够调用他的属性和方法,
这样的动态获取属性和方法和动态调用属性和方法的功能就叫做反射。
对于任意一个对象都能够调用他的属性和方法,
这样的动态获取属性和方法和动态调用属性和方法的功能就叫做反射。
cjlib
asm字节码编辑技术动态创建类,基于classLoad装载
修改字节码生成子类去处理
常用于事务处理、日志管理、权限管理
Spring AOP 与 AspectJ区别
Spring AOP是运行时增强,AspectJ是编译时增强
切面多时,AspectJ性能较好
IoC
控制反转,是一种设计思想,即将原本程序中手动创建对象的控制权交给Spring框架处理
IoC容器是IoC实现的载体,其实就是一个Map,存放各种对象
IoC容器通过实现工厂模式,将需要创建的对象通过注解方式配置好,IoC容器会在程序需要的时候自动创建对象
DI,依赖注入,跟IoC是同一个概念不同的描述,
Bean
生命周期
1,实例化Bean(Instantiation)
加载元数据
Spring启动扫描xml/注解/JavaConfigBean信息,放到BeanDefinition中,
通过BeanDefinition描述对象信息,然后以BeanName为key,BeanDefinition为value,
放到BeanDefinitonMap中,遍历这个map,执行BeanFacttoryPostProcessor这个Bean工厂后置处理器。
通过BeanDefinition描述对象信息,然后以BeanName为key,BeanDefinition为value,
放到BeanDefinitonMap中,遍历这个map,执行BeanFacttoryPostProcessor这个Bean工厂后置处理器。
实例化对象
Spring通过反射把对象实例化
2,注入属性(Populate)
3,初始化
3.1,检查Aware的相关接口并设置相关依赖
比如想要获取Spring Bean,可以使用工具类实现ApplicationContextAware接口获取Bean
3.2,BeanPostProcessor中前置处理
before方法
3.3,是否实现InitializingBean接口
@PostConstruct就是initiali实现类
3.4,是否配置自定义的init-method
3.5,BeanPostProcessor后置处理
after方法
4,使用bean
5,销毁(Destruction)
5.1,是否实现DisposableBean接口
5.2,是否配置自定义的destory-method
作用域
singleton
唯一bean实例,默认单例的
prototype
每次请求都会创建一个bean,是多例的
request
每次http请求时,都会创建一个Bean,仅适用与WebApplicationContext环境
session
同一个Http Session共享一个Bean,不同的Session拥有不同的Bean,仅适用与WebApplicationContext坏境
将类声明为Spring 的bean的注解
@Autowired
自动装配bean
@Component
作用与类上
@Bean
作用于方法上
Spring MVC
@Controller 与 @RestController 区别
@Controller
返回一个页面
参数需要引用@ResponseBody才能返回JSON
@RestController
返回JSON或XML形式数据
原理
1,用户通过浏览器发起请求,前端控制器接收到请求,调用HandlerMapping解析Handler
2,解析到Handler后(也就是Controller)交由处理器进行处理,执行相对应的逻辑,返回ModelAndView
3,视图解析器将ModelAndView解析成浏览器可以识别的view。
4,最后再通过前端控制器返回给浏览器
Spring 事务
管理事务的方式
编程式事务
声明式事务
基于xml、基于注解
事务并发可能造成的问题
脏读
读取了上一个事务还未提交的数据
不可重复读
事务多次读取同一个数据,中途另一个事务更新了数据,当时前后读取的数据不一致
幻读
事务多次读取同一个数据集,中途另一个事务新增了数据,导致前后读取的数据集不一致
事务的隔离级别
DEFAULT(默认)
后端数据库默认的级别。Mysql默认是REPEATABLE_READ 可重复读,Oracle 默认READ_COMMITTED
读已提交
读已提交
READ_UNCOMMITTED 读未提交
允许读取并发事务未提交的数据。可能导致 脏读、不可重复读、幻读
READ_COMMITTED 读已提交
允许读取并发事务提交后的数据。可能导致 不可重复读、幻读
REPEATABLE_READ 可重复读
对同一字段多次读取数据都是一致的,除非是被本身事务修改。可能导致幻读
SERIALIZABLE 可串行化
最高隔离级别。所有事务逐个执行。并发事务问题都可防止,但会严重影响性能,不建议使用。
事务传播方式
REQUIRED
支持当前事务
没有事务则创建事务。Spring默认的事务。
SUPPORTS
支持当前事务
当前没有事务,则以非事务的方式运行。
MANDATORY
支持当前事务
当前没有事务则抛出异常。
REQUIRES_NEW
不支持当前事务
当前有事务,则把当前事务挂起。
NOT_SUPPORTS
不支持当前事务
按非事务方式执行,当前有事务则把当前事务挂起
NEVER
不支持当前事务
按非事务方式执行,当前有事务则抛出异常。
NESTED
嵌套事务
当前存在事务,则创建一个事务作为当前事务的嵌套事务运行,当前不存在事务,则创建一个事务。
@Transactional
修饰类或者方法,则该类下所有方法都会服从事务,预到异常按相应配置的事务传播方式进行回滚。
@Transactional(rollbackFor = Exception.class) 不配置rollbackFor属性,则按运行时异常进行回滚,配置了rollbackFor属性,则事务可在遇到非运行时异常时也回滚。
Spring事务原理
本质是对数据库事务的支持,首先获取连接器连接数据库
Spring AOP通过动态代理,实现事务的开启和回滚
对有@Transactional 注解的类或方法的bean进行代理
事务执行线程是同步的
Spring事务失效场景
数据库引擎不支持事务
比如mysql MyISAM引擎不支持事务,innoDB支持事务
没有被Spring所管理
比如service类不加@Service注解,启动时就不被加载成Bean,类里的方法不支持事务
方法不是 public 的
@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要私有方法加事务可以考虑AspectJ 代理模式
原因
被aop增强的方法都应该是public的,而不能是private的
类内部方法自身调用时
自己调用自己没有经过spring Bean的代理,加事务也不生效。解决自身调用需要注入一个自己的类,但是不推荐
数据源没有配置事务管理器
事务传播行为设置为不以事务进行(Propagation.NOT_SUPPORTED)
try catch 事务异常被吃
try catch 抛出的异常类型不是RunTimeException
@Transactional注解修饰的方法,加上rollbackfor属性值,指定回滚异常类型
循环依赖
情况
属性依赖可以解决
构造器依赖解决不了
属性依赖解决原理
spring bean对象都需要经历两个阶段,初始化和实例化
getBean()是否存在,不存在则实例化对象,然后实例化对象过程中发现属性依赖,则初始化依赖对象
通过递归循环,拿到三级缓存中的 半成品实现bean的实例化,然后向上递归对依赖对象进行实例。
三级缓存
一级singletonObjects
日常实际获取Bean的地方
二级 earlySingletonObjects
已实例化,但还没进行属性注入,由三级缓存放进来
三级 singletonFactories
value是一个对象工厂
为什么是三级?
Bean是单例的,A对象依赖B对象是有AOP的,三级缓存可以拿到代理对象。如果只有二级,则需要先去做AOP代理。
mybatis一级二级缓存
一级缓存 sqlSession
当一个sqlSession对象去查询一条记录的时候,一级缓存会将其缓存到其数据结构中,当再次去使用相同条件查询的时候就会直接从一级缓存中获取,而不会去数据库中查询
commit(dedlete、update、insert)操作,一级缓存就会被清空
mysql默认开启一级缓存
二级缓存 mapper(namespace)
mapper 对应的也就是 namespace,所以二级缓存是按照 namespace 进行区分的,如果两个 mapper 文件的 namespace 相同,那么这两个 mapper 中查出来的数据都会存在在这个 namespace 中
二级缓存适用于查询频繁但是数据实时性不高的场景
缺点是:刷新信息就是全表刷新,只要有一个commit操作,整个缓存全部清空
Redis
单线程模型--文件事件管理器
组件
多个socket
建立请求和redis之间的连接
IO多路复用程序
轮询监听socket,分配到队列中
文件事件分派器
分派队列中的socket 到 对应的事件处理器
事件处理器
连接应答处理器(将AE_READABLE 事件 与命令请求处理器)
命令连接处理器(将从连接应答处理器过来的 socket 中的key_value 读取出来,并在内存中做设置,然后连接命令回复处理器)
命令回复处理器(给客户端回复响应,返回给redis 服务端的 AE_WRITABLE 事件对应响应)
命令连接处理器(将从连接应答处理器过来的 socket 中的key_value 读取出来,并在内存中做设置,然后连接命令回复处理器)
命令回复处理器(给客户端回复响应,返回给redis 服务端的 AE_WRITABLE 事件对应响应)
redis单线程高效率的原因
基于内存
基于非阻塞的IO多路复用程序机制实现,只做轮询监听,不做处理。
单线程避免了多线程频繁上下文切换的损耗,避免了一些数据不一致、死锁等并发问题。
数据类型
基础
String
SDS(简单动态字符串)
二进制安全
记录字符串本身长度
AOF缓存区
k-v结构
常用于计数场景:用户访问次数、热点文章的点赞数量
常用命令:set、get、exists、strlen、del
List
双向链表
常用于发布订阅场景,比如消息队列、慢查询。
常用命令:lpush、lpop、rpush、rpop、lrange
Hash
hash表
常用于存放对象信息,比如用户信息、商品信息等
常用命令:hset、hmset、hget、hkeys、hvals
Set
无序唯一集合
常用于实现交、并集的操作。比如微博之间 共同关注、共同粉丝、共同爱好等
常用命令:sadd、spop、smembers、scard
sorted set/zset
有序唯一集合
常用于权重排序场景。比如直播中 实时直播间人数排行、礼物排行等消息排行
常用命令:zadd、zcard、zrange、zrevrange
原理
zipList
满足条件时使用:
元素数量小于128个。
所有member的长度都小于64字节
元素数量小于128个。
所有member的长度都小于64字节
实质是一个双向链表。虽然元素是按 score 有序排序的, 但对 ziplist 的节点指针只能线性地移动
skipList
不满足上面两个条件时使用跳表
跳表(skip List)是一种随机化的数据结构,基于并联的链表,实现简单,插入、删除、查找的复杂度均为O(logN)。
简单说来跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供O(logN)的时间复杂度
简单说来跳表也是链表的一种,只不过它在链表的基础上增加了跳跃功能,正是这个跳跃的功能,使得在查找元素时,跳表能够提供O(logN)的时间复杂度
高级
BitMaps
针对bit操作的集合,不是数据结构
常用于记录用户是否进行过搜索等
HyperLogLogs
计算唯一事物概率的数据结构。
常用于统计用户搜索次数
GEO
存储用户地理位置
底层实现
数据结构为 zset
Redis使用了geohash对经纬度信息进行的编码,将编码信息作为score 可以快速实现对经纬度的索引
geohash
1,将三维地球通过经纬度切割为二维的正方形
2,然后对所有的正方形用二进制进行01编码,对每块区域再进行更小粒度的编码划分,实现通过编码来找到对应经纬度
过期策略
惰性删除
查到该key时检查是否过期,过期则删除
对CPU友好
定期删除
到达一定时间时,随机删除一些过期的key
对内存友好
内存淘汰机制
volatile-lru
从已设置过期时间的数据集中删除不常用的数据
allkeys-lru
从键空间所有key中,挑选不常用的key删除。较常用
lru算法
使用HashMap结合双向链表,HashMap值为链表节点,新增时添加Node到队尾,修改时修改Node对应的值并移动到队尾,查询则直接移动Node到队尾。队头则是最不常用的key。
数据库有1000w数据,redis有50w数据,如何保证10w数据为热点数据?
限定 Redis 占用的内存,Redis 会根据自身数据淘汰策略,留下热数据到内存。
所以,计算一下 50W 数据大约占用的内存,然后设置一下 Redis 内存限制即可,并将淘汰策略为volatile-lru或者allkeys-lru
所以,计算一下 50W 数据大约占用的内存,然后设置一下 Redis 内存限制即可,并将淘汰策略为volatile-lru或者allkeys-lru
设置Redis最大占用内存:
打开redis配置文件,设置maxmemory参数,maxmemory是bytes字节类型
打开redis配置文件,设置maxmemory参数,maxmemory是bytes字节类型
持久化
rdb
快照模式,BGSAVE自动保存当前时间点的redis副本。
文件小,恢复数据快,可以将快照复制到其他从服务器中进行恢复。
容易丢失时间点后更改的数据,运行时宕机容易丢数据
aof
AOF文件。主要用来保存追加redis中执行的写命令到文件中,然后持久化到磁盘。
实时性好,保存的数据较全面,最多丢失一秒数据。持久化到磁盘。
文件大,恢复数据慢。
4.0后支持RDB和AOF混合持久化
高可用
集群
一主多备,读写分离
master复制写操作,slave负责读操作。redis高可用最少一主两备,slave可水平扩容
主从数据一致性
完全重同步
利用master rdb文件进行同步,新增的数据进入缓冲区,slave同步完rdb后同步缓冲区中的写数据。
部分重同步
一般用于slave机器宕机,导致和master机器数据不一致。
master和slave都有复制偏移量offset,如果不一致,从机会通过AOF文件中恢复部分写数据。
有一种情况是slave宕机期间,master机器换了。这种需要比较slave存放的runid记录master的ip是否前后一致。不一致则需要删除数据完全重同步。
master选举
slave发现自己的master挂了,会向其他节点发请求投票自己为master,如果其他master节点(slave不参与投票)超过半数同意则这台slave就成为了master,并广播消息通知其他节点。
哨兵
通过心跳监听master机器状态,若哨兵集群理性判断master宕机后,会选举slave机器为master。
脑裂问题
若因为网略问题,心跳监测不到master,哨兵以为master宕机,并重新进行了选举,这时会造成两个master都接收写命令,会造成数据的不一致。
解决
min-slaves-to-write 2,设置master下slave最小连接为两个
min-slaves-max-lag 10,设置slave同步master的数据的延迟时间最大为10秒,超出则不接收写操作。
分片集群
Redis Cluster(客户端路由)
原理:每个集群会有16384个槽,redis实例通过均分或者配置的方式将槽分完,每个实例都会存放其他实例有哪些槽值,这样当客户端有数据进来时,进行算法计算后对16384进行取模,模值对应槽,根据实例间映射也容易找到对应实例。
当集群中有实例新增或者删除,会将变化通知给其他实例,客户端查数据时会先去旧的找,旧的会返回新的实例给客户端,客户端更新映射。
当集群中有实例新增或者删除,会将变化通知给其他实例,客户端查数据时会先去旧的找,旧的会返回新的实例给客户端,客户端更新映射。
为什么槽值是16384个?
1、redis作者认为集群实例不会超过1000个,16384够均衡分配了。
2、实例过多,会导致redis的网络开销过大
2、实例过多,会导致redis的网络开销过大
与一致性哈希(哈希环)的区别?
哈希槽实现相对简单⾼效,每次扩缩容只需要动对应Solt(槽)的数据,⼀般不会动整个Redis实例。
哈希环实例变动时需要知道「哪⼀部分数据」受到影响了,需要进⾏对受影响的数据进⾏迁移
哈希环实例变动时需要知道「哪⼀部分数据」受到影响了,需要进⾏对受影响的数据进⾏迁移
常见问题
缓存雪崩
场景
某个时间里,大量缓存过期失效(热点缓存),这时有大量请求进来,就会穿透缓存直接访问数据库,造成数据库压力激增并宕机,
解决
发生前,给key设置随机过期时间
发生时,利用MQ进行限流,避免大量请求走数据库
发生后,重启宕机的redis服务,rdb和aof回复缓存数据
缓存穿透
场景
大量请求访问redis中不存在的值,导致直接穿透缓存访问数据库。
解决
设置过期时间较短的空缓存
布隆过滤器,判断访问的数据是否合法,不合法则直接抛出
原理:将元素放进布隆过滤器时,会计算哈希值,然后将位数组下标置为1。
当元素进行访问时,计算哈希值,判断位数组下标是否为1,不是则抛出。
当元素进行访问时,计算哈希值,判断位数组下标是否为1,不是则抛出。
缓存与数据库数据一致性
先删缓存,再更新数据库
高并发下会出现脏数据
延时双删机制:删缓存、更新数据库、一秒后再删缓存。
缓存设置过期时间
先更新数据库,再删缓存(推荐)
避免高并发的下的脏数据,但是原子性不太好
一致性hash
一致性Hash算法将整个哈希值空间通过对2进行取模,得到一个由32位无符号整型组成的一个虚拟的圆环,被称为哈希环
使用场景
redis集群使用中,将key均匀分布到各个节点中,但是当服务器增加或者减少时,节点中的key都需要变化,会导致大量key同一时间失效,造成缓存雪崩
一致性hash可以解决
新增服务器,只会导致新服务器所在圆环的点到上一个服务器所在圆环的点之间的key失效重排,不影响其他服务器上的key
删除服务器或者服务器宕机,也只会影响服务器到上一个服务器之间环上的key数据
哈希环数据倾斜问题
一致性Hash算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题
解决方法
通过虚拟节点方式,实际中通常将虚拟节点数设置为32甚至更大,因此即使很少的服务节点也能做到相对均匀的数据分布。
由实际节点虚拟复制而来的节点被称为"虚拟节点",即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器IP或主机名的后面增加编号来实现。
Mysql
事务隔离级别
读未提交
读已提交
可重复读
串行化/序列化
存储引擎
InnoDB
事务性存储引擎
支持行级锁、表级锁
支持外键
支持MVCC
事务
特性
原子性
原⼦性由undo log⽇志来保证,因为undo log记载着数据修改前的信息
隔离性
事务「并发」执⾏时,他们内部的操作不能互相⼲扰
利用锁实现4种隔离级别,控制脏读、幻读、重复读问题的产生
持久性
由redo log ⽇志来保证,记录了mysql的物理修改
一致性
log
binlog
记录数据库逻辑变更
事务提交后记录,多用于数据恢复
redolog
记录数据库物理变更
事务提交前记录,保证修改后的数据持久化磁盘后的数据一致性。实现了事务持久性
innoDB存储引擎层产生
undolog
回滚日志文件
用于事务回滚,记录MVCC中数据更新版本。实现事务原子性。
索引
索引提高查询效率,本质上就是将无序数据变有序
InnoDB索引底层数据结构是B+树
原因
MySQL数据是存到硬盘上的,不能一次性把数据查出来放到内存中。
红黑树是二叉树,一个Node节点只能存一个key-value。
B和B+树是"多路搜索树",一个节点可以存放多个数据,而且高度更低,检索就会更快。
B+树又因为非叶子节点不存放数据,叶子节点之间组成双向链表,方便遍历查询。
红黑树是二叉树,一个Node节点只能存一个key-value。
B和B+树是"多路搜索树",一个节点可以存放多个数据,而且高度更低,检索就会更快。
B+树又因为非叶子节点不存放数据,叶子节点之间组成双向链表,方便遍历查询。
为什么叶子节点要使用双向链表连接?
方便范围查询,如where id >= 10 ,只需要找到主键10所在的叶子结点,然后向后遍历即可
Hash索引
hash表
适用单一查询,范围查询速度慢。
B+树索引
多路平衡二叉树
相同层级节点之间指针互联,天然有序,范围查询不用全表扫描
B+树叶子节点可以存放多少数据
高度为2的树可以存放大约18720条,高度为3的大约能存放两千万条
单个叶子节点(页)中的记录数=16K/1K=16
聚簇索引
B+树叶子节点存储主键索引
非聚簇索引
B+树叶子节点存储非主键索引
聚簇索引和非聚簇索引的区别
叶子节点存放的数据不同,聚簇索引存放的是 整行数据,非聚簇索引存放的是 主键id,还需要回表才能查到数据。
回表
当我们使用非聚簇索引时,查出来还有其他列,但走索引树叶子节点只能查询出当前列和主键id,还需要通过id再去查询所需列,这就叫回表。
覆盖索引
执行的查询语句数据在索引中就能查到
联合索引
需要满足最左匹配原则
从最左开始,索引只用来判断key是否存在即是否相等,如果遇到范围查询就不能匹配了,则后续查询为线性查询
最左前缀原则
B+树结构的索引中,索引项是按照索引定义里面出现的字段顺序排序的,索引在查找的时候,可以快速定位到 ID 为 100的张一,然后直接向右遍历所有张开头的人,直到条件不满足为止。这种定位到最左数据然后向后查询的过程就是最左前缀原则
假如联合索引为(a,b,c)
where a = 1 and b = 1 and c = 1 或者 a = 1 and b = 1
走索引
连续且满足最左
where a = 1 and c = 1 或者 b=1 and c =1
不走索引
如果不连续时,只用到了a列的索引,b列和c列都没有用到
where a = 1 and b > 1 and c = 1
c用不到索引
因为b是范围查询,后续条件不走索引
where a = 1 and b > 1
走索引
多个列同时进行范围查找时,只有对索引最左边的那个列进行范围查找才用到B+树索引
不走索引的情况
like % 模糊查询
索引列参与计算,使用了函数
where对null判断,where不等于
or操作有一个字段没有索引
where后对字段进行表达式操作
where后面使用!<>比较符
联合索引不满足最左原则
锁
表锁
开销小,加锁快,不会死锁。
锁冲突概率高,并发低
表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如Web应用
场景
事务需要更新某张大表的大部分或全部数据
事务涉及多个表,比较复杂,可能会引起死锁,导致大量事务回滚,可以考虑表锁避免死锁
行锁
开销大,加锁慢,会造成死锁
减少数据库操作冲突,并发高
更适合于有大量按索引条件并发更新少量不同数据,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统
共享锁(读锁)
可以并发读取数据,未释放锁之前不能执行修改操作。
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
排它锁(写锁)
当前事务对数据加了写锁,该数据只支持当前事务读和写,其他事务不能对该数据进行加锁。
SELECT * FROM table_name WHERE ... FOR UPDATE
innoDB默认行锁
Record Lock(记录锁)
对索引项加锁
Gap Lock(间隙锁)
对索引项“间隙”加锁,和行锁联合使用可以解决 幻读的问题
间隙锁基于非唯一索引,它锁定一段范围内的索引记录。
使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
Next-key Lock(临键锁)
左开右闭的一个区间,是间隙锁和行锁的结合
解决了mysql 可重复读级别的幻读问题
唯一索引上:
如果条件为=5 ,间隙锁退化为行锁,也就是只会锁住条件中的那一行对象,
如果是>5,则会添加一个[5, ∞) 的一个next-key锁,5这个行锁和(5,∞)这个间隙锁
如果条件为=5 ,间隙锁退化为行锁,也就是只会锁住条件中的那一行对象,
如果是>5,则会添加一个[5, ∞) 的一个next-key锁,5这个行锁和(5,∞)这个间隙锁
非唯一索引上:
如果条件为5,那么mysql会通过5查询左右两边的一个间隙,也就是比5小的第一个值和比5大的第一个值,然后加一个间隙锁,
比如数据库还有两条数据的索引值为 3 和 7,那么mysql会加一个(3-5)[5](5-7]的这么一个间隙锁。
如果条件为5,那么mysql会通过5查询左右两边的一个间隙,也就是比5小的第一个值和比5大的第一个值,然后加一个间隙锁,
比如数据库还有两条数据的索引值为 3 和 7,那么mysql会加一个(3-5)[5](5-7]的这么一个间隙锁。
1、InnoDB 中的行锁的实现依赖于索引,一旦某个加锁操作没有使用到索引,那么该锁就会退化为表锁。
2、记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录。
3、间隙锁存在于非唯一索引中,锁定开区间范围内的一段间隔,它是基于临键锁实现的。
4、临键锁存在于非唯一索引中,该类型的每条记录的索引上都存在这种锁,它是一种特殊的间隙锁,锁定一段左开右闭的索引区间
2、记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录。
3、间隙锁存在于非唯一索引中,锁定开区间范围内的一段间隔,它是基于临键锁实现的。
4、临键锁存在于非唯一索引中,该类型的每条记录的索引上都存在这种锁,它是一种特殊的间隙锁,锁定一段左开右闭的索引区间
MVCC
解决加锁后读写性能问题
多版本并发控制
读不加锁,读写不冲突。读多写少的场景下极大增加了系统并发性能
mvcc怎么避免的脏读?
MVCC通过⽣成数据快照(Snapshot),并⽤这个快照来提供⼀定级别(语句级或事
务级)的⼀致性读取。
务级)的⼀致性读取。
read commit (读已提交)用到的是语句级快照
repeatable read (可重复复读)用到的是事务级快照
本质都是利用快照对比版本,read commit 对比的是修改语句的版本。repeatable read 对比的是事务修改的版本
原理
通过undolog获取版本链
利用隐藏字段DB_TRX_ID 记录不同事务Update时的版本,DB_ROLL_PT字段回滚指针将版本以先后顺序连接成Undo log链
DB_ROW_ID字段是为了给未声明主键的表自动生成隐藏主键。
一致性视图ReadView
读已提交
在每次查询前生成一个
可重复读
每次事务生成一个,后面的事务查询都基于这一个
包含几个字段:
trx_ids(尚未提交commit的事务版本号集合),
up_limit_id(下⼀次要⽣成的事务ID值),
low_limit_id(尚未提交版本号的事务ID最⼩值),
creator_trx_id(当前的事务版本号)
trx_ids(尚未提交commit的事务版本号集合),
up_limit_id(下⼀次要⽣成的事务ID值),
low_limit_id(尚未提交版本号的事务ID最⼩值),
creator_trx_id(当前的事务版本号)
优化
大表查询优化
限定查询范围
查询语句必须带限制数据范围
读写分离
垂直拆分
根据列将表进行拆分成多表
简化表结构,减小列数据,减少IO
水平拆分
表结构相同,但数据不同
开发规范上
1、是否能使⽤「覆盖索引」,减少「回表」所消耗的时间
2、考虑是否组建「联合索引」,如果组建「联合索引」,尽量将区分度最⾼的放在
最左边,并且需要考虑「最左匹配原则」
最左边,并且需要考虑「最左匹配原则」
3、对索引进⾏函数操作或者表达式计算会导致索引失效
4、利⽤⼦查询优化超多分⻚场景
5、通过explain命令来查看SQL的执⾏计划,看看⾃⼰写的SQL是否⾛了索引,⾛了
什么索引。通过show profile 来查看SQL对系统资源的损耗情况
什么索引。通过show profile 来查看SQL对系统资源的损耗情况
6、在开启事务后,在事务内尽可能只操作数据库,并有意识地减少锁的持有时间(⽐
如在事务内需要插⼊&&修改数据,那可以先插⼊后修改。因为修改是更新操作,会加⾏锁。
如果先更新,那并发下可能会导致多个事务的请求等待⾏锁释放)
如在事务内需要插⼊&&修改数据,那可以先插⼊后修改。因为修改是更新操作,会加⾏锁。
如果先更新,那并发下可能会导致多个事务的请求等待⾏锁释放)
自增主键表和自定义主键表索引速度
自增主键表每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页
如果使用非自增主键(如果身份证号或学号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页得中间某个位置,此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面
走对了索引,但是线上查询还是慢,怎么排查和解决?
1、排查是否是数据量大的原因,根据业务需求,看数据是否可以定期清理
2、如果数据不能删除,则考虑是否可以添加缓存,但是需要考虑数据是否是及时性的,缓存和数据库数据不一致问题
3、如果数据查询涉及到字符串搜索,则考虑ES,监听binlog保证ES和数据库数据一致性
4、考虑要不要做聚合表
数据库读写瓶颈怎么办?
1、读写分离,从机通过binlog实现数据一致性
2、分库分表
主键生成?(分布式id生成)
使⽤Redis和基于「雪花算法」实现
数据迁移(双写)
⼀、增量的消息各⾃往新表和旧表写⼀份
⼆、将旧表的数据迁移⾄新库
三、迟早新表的数据都会追得上旧表(在某个节点上数据是同步的)
四、校验新表和⽼表的数据是否正常(主要看能不能对得上)
五、开启双读(⼀部分流量⾛新表,⼀部分流量⾛⽼表),相当于灰度上线的过程
六、读流量全部切新表,停⽌⽼表的写⼊
七、提前准备回滚机制,临时切换失败能恢复正常业务以及有修数据的相关程序。
⼆、将旧表的数据迁移⾄新库
三、迟早新表的数据都会追得上旧表(在某个节点上数据是同步的)
四、校验新表和⽼表的数据是否正常(主要看能不能对得上)
五、开启双读(⼀部分流量⾛新表,⼀部分流量⾛⽼表),相当于灰度上线的过程
六、读流量全部切新表,停⽌⽼表的写⼊
七、提前准备回滚机制,临时切换失败能恢复正常业务以及有修数据的相关程序。
消息队列
作用
异步
多个业务系统接收到消息,可以异步处理相应逻辑
问题:消息消费成功则返回成功,但是可能其中有系统写库失败,就会造成数据不一致问题。
解耦
消息放到MQ中,其他系统可自由选择是否消费消息。
问题:外部系统对MQ依赖变强,如果MQ宕机可能导致多系统故障。
削峰
MQ在访问高峰时,拉取适量请求到数据库处理,起到削峰作用,挤压在MQ中的消息慢慢处理。
问题:消息挤压可能导致消息重复消费,消息丢失等问题,系统复杂性高。
RocketMQ
队列模型的分布式消息中间件,十万级以上吞吐量。阿里开发。
主题模式/发布订阅
通过使用一个Topic(主题)中配置多个队列并且每个队列维护每个消费者组的消费位置。
基本组成
Broker
主要负责消息的存储、查询消费
高可用
Topic分片分别存放在不同Broker服务上,一个Broker存放多个Topic。多Broker部署可提高并发能力。
支持主从部署,一个 Master 可以对应多个 Slave,Master 支持读写,Slave 只支持读。
Broker 会向集群中的每一台 NameServer 注册自己的路由信息
NameServer
Topic 路由注册中心,早期是zk,后面改了。支持 Broker 的动态注册和发现,保存 Topic 和 Borker 之间的关系
主要用来管理Broker和路由信息。解耦。
消费者和生产者通过NameServer获取Broker地址并通信。
Broker定期发送心跳包(包含自身的Topic)
通常也是集群部署,但是各 NameServer 之间不会互相通信, 各 NameServer 都有完整的路由信息
Producer
消息生产者
它会先和 NameServer 集群中的随机一台建立长连接,得知当前要发送的 Topic 存在哪台 Broker Master上,然后再与其建立长连接,支持多种负载平衡模式发送消息
Consumer
消息消费者
完整调用链路
先启动 NameServer 集群,各 NameServer 之间无任何数据交互,Broker 启动之后会向所有 NameServer 定期(每 30s)发送心跳包,包括:IP、Port、TopicInfo,NameServer 会定期扫描 Broker 存活列表,如果超过 120s 没有心跳则移除此 Broker 相关信息,代表下线。
这样每个 NameServer 就知道集群所有 Broker 的相关信息,此时 Producer 上线从 NameServer 就可以得知它要发送的某 Topic 消息在哪个 Broker 上,和对应的 Broker (Master 角色的)建立长连接,发送消息。
Consumer 上线也可以从 NameServer 得知它所要接收的 Topic 是哪个 Broker ,和对应的 Master、Slave 建立连接,接收消息。
高可用
支持的集群模式
多master
多master多slave异步复制
多master多slave双写
集群
NameService集群采用去中心化,一个broker发送心跳向所有的NameService。
Broker集群,采用主从,master/slave模式,同步/异步刷盘方式同步消息数据。
producer通过轮询的方式向每个队列中生产消息实现负载均衡。
consumer从broker中pull消息后,采用两种模式消费消息。
广播
一条消息发送给同一个消费组的所有消费者。
集群
一条消息发送给一个消费者。
刷盘机制(单个节点)
同步刷盘
持久化到磁盘的过程中需要等待刷盘成功的一个ACK
可靠性高,性能差,适用于金融等
异步刷盘
开启一个线程异步执行刷盘
降低读写延迟,提高性能,适用于验证码等场景
同步复制/异步复制(Broker主从模式下)
同步复制
消息同步双写到主从节点上才返回写入成功
异步复制
消息写到主节点上就返回写入成功。
RocketMQ不支持主从切换,所以如果主节点宕机,从节点消息会短暂消息不一致
顺序消费
Hash取模确定是同一个业务,放到同一个队列中
重复消费
幂等
对同一个消息的处理结果,执行多少次都不变
实现方式
redis 做前置处理
DB唯⼀索引做最终保证来实现幂等性的
消息丢失
生产阶段
可以利用消息的有序行检验是否消息丢失(生产者给发出的消息加入连续递增的序号,在consumer来检查这个序号的连续性)
利用拦截器机制,在Producer发送消息之前将序号注入进去,
在Consumer消费消息前拦截器校验序号的正确性
在Consumer消费消息前拦截器校验序号的正确性
RocketMQ是通过请求确认机制的方式保证消息不丢失
Producer生产消息后,需要收到broker正确接收到消息的返回才确定消息发送成功,否则重发
存储阶段
RocketMQ是通过刷盘的方式去持久化消息
利用主从保证消息的备份
消费阶段
Consumer消费成功会发送确认响应给Broker,如果Broker未收到消费确认响应,消费者下次还会再次拉取到该消息,进行重试。
分布式事务
事务消息(half半消息机制)+事务反查
本地事务和存储消息到消息队列才是同一个事务。产生了事务的最终一致性,因为整个过程是异步的,每个系统只要保证它自己那一部分的事务。
消息堆积
排查
生产端:一般当生产端发生积压(Broker正常的情况下)就要查看你的业务逻辑是否有异常的耗时步骤导致的。是否需要改并行化操作等。
Broker端:当Broker端发生积压我们首先要查看,消息队列内存使用情况,如果有分区的的话还得看每个分区积压的消息数量差异。当每个分区的消息积压数据量相对均匀的话,我们大致可以认为是流量激增。需要在消费端做优化,或者同时需要增加Broker节点(相当于存储扩容),如果分区加压消息数量差异很大的话(有的队列满了,有的队列可能还是空闲状态),我们这时候就要检查我们的路由转发规则是否合理,
消费端:在使用消息队列的时候大部分的问题都出在消费端,当消费速度小于生产速度很快就会出现积压,导致消息延迟,以至于丢失。这里需要重点说明一点的是,当消费速度小于生产速度的时候,仅增加消费者是没有用处的,因为多个消费者在同一个分区上实际是单线程资源竞争关系(当然还有一些冒险的单队列多消费者并行方式就是:消费者接到消息就ack成功再去处理业务逻辑,这样你就要承受消息丢失的代价),我们需要同时增加Broker上的分区数量才能解决这一问题。
下游消费系统如果宕机了,导致几百万条消息在消息中间件里积压,此时怎么处理?
首先要找到是什么原因导致的消息堆积,是Producer太多了,Consumer太少了导致的还是说其他情况,
总之先定位问题。然后看下消息消费速度是否正常,正常的话,可以通过上线更多consumer临时解决消息堆积问题。
总之先定位问题。然后看下消息消费速度是否正常,正常的话,可以通过上线更多consumer临时解决消息堆积问题。
如果Consumer和Queue不对等,上线了多台也在短时间内无法消费完堆积的消息怎么办?
• 准备一个临时的topic
• queue的数量是堆积的几倍
• queue分布到多Broker中
• 上线一台Consumer做消息的搬运工,把原来Topic中的消息挪到新的Topic里,不做业务逻辑处理,只是挪过去
• 上线N台Consumer同时消费临时Topic中的数据
• 改bug
• 恢复原来的Consumer,继续消费之前的Topic
• queue的数量是堆积的几倍
• queue分布到多Broker中
• 上线一台Consumer做消息的搬运工,把原来Topic中的消息挪到新的Topic里,不做业务逻辑处理,只是挪过去
• 上线N台Consumer同时消费临时Topic中的数据
• 改bug
• 恢复原来的Consumer,继续消费之前的Topic
堆积时间过长消息超时了?
RocketMQ中的消息只会在commitLog被删除的时候才会消失,不会超时。也就是说未被消费的消息不会存在超时删除这情况。
堆积的消息会不会进死信队列?
不会,消息在消费失败后会进入重试队列(%RETRY%+ConsumerGroup)
Kafka
消息模型
发布、订阅模式
组成
producer、consumer、Broker
Topic
Partition分区
这是与RocketMQ最大的区别,kafka采用分区的概念,与RocketMQ队列的概念基本相同
多副本机制
分区的多个副本之间会有一个leader,其他副本称为follower,leader接收生产者消息,follower同步leader消息。
leader故障,会选举一个同步程度相同的follower当leader
提高容灾能力
Kafka能这么快的原因就是实现了并⾏、充分利⽤操作系统cache、顺序写和零拷⻉
零拷贝
正常一次读写需要两次CPU拷贝,两次DMA拷贝,零拷贝就是将CPU拷贝省去
mmap
mmap是将读缓冲区的地址和⽤户空间的地址进⾏映射,实现读内核缓冲区
和应⽤缓冲区共享,从⽽减少了从读缓冲区到⽤户缓冲区的⼀次CPU拷⻉
和应⽤缓冲区共享,从⽽减少了从读缓冲区到⽤户缓冲区的⼀次CPU拷⻉
使⽤mmap的后⼀次读写就可以简化为:
⼀、DMA把硬盘数据拷⻉到读内核缓冲区。
⼆、CPU把读内核缓存区拷⻉⾄Socket内核缓冲区。
三、DMA把Socket内核缓冲区拷⻉⾄⽹卡
由于读内核缓冲区与⽤户空间做了映射,所以会省了⼀次CPU拷⻉
⼀、DMA把硬盘数据拷⻉到读内核缓冲区。
⼆、CPU把读内核缓存区拷⻉⾄Socket内核缓冲区。
三、DMA把Socket内核缓冲区拷⻉⾄⽹卡
由于读内核缓冲区与⽤户空间做了映射,所以会省了⼀次CPU拷⻉
sendfile
sendfile+DMA Scatter/Gather则是把读内核缓存区的⽂件描述符/⻓度信息发到
Socket内核缓冲区,实现CPU零拷⻉
Socket内核缓冲区,实现CPU零拷⻉
使⽤sendfile+DMA Scatter/Gather⼀次读写就可以简化为:
⼀、DMA把硬盘数据拷⻉⾄读内核缓冲区。
⼆、CPU把读缓冲区的⽂件描述符和⻓度信息发到Socket缓冲区。
三、DMA根据⽂件描述符和数据⻓度从读内核缓冲区把数据拷⻉⾄⽹卡
⼀、DMA把硬盘数据拷⻉⾄读内核缓冲区。
⼆、CPU把读缓冲区的⽂件描述符和⻓度信息发到Socket缓冲区。
三、DMA根据⽂件描述符和数据⻓度从读内核缓冲区把数据拷⻉⾄⽹卡
如何保证消费者消费消息不丢失?
⼀、从Kafka拉取消息(⼀次批量拉取500条,这⾥主要看配置)
⼆、为每条拉取的消息分配⼀个msgId(递增)
三、将msgId存⼊内存队列(sortSet)中
四、使⽤Map存储msgId与msg(有offset相关的信息)的映射关系
五、当业务处理完消息后,ack时,获取当前处理的消息msgId,然后从sortSet删除该msgId(此时代表已经处理过了)
六、接着与sortSet队列的⾸部第⼀个Id⽐较(其实就是最⼩的msgId),如果当前msgId<=sort Set第⼀个ID,则提交当前offset
七、系统即便挂了,在下次重启时就会从sortSet队⾸的消息开始拉取,实现⾄少处理⼀次语义
⼋、会有少量的消息重复,但只要下游做好幂等就OK了。
⼆、为每条拉取的消息分配⼀个msgId(递增)
三、将msgId存⼊内存队列(sortSet)中
四、使⽤Map存储msgId与msg(有offset相关的信息)的映射关系
五、当业务处理完消息后,ack时,获取当前处理的消息msgId,然后从sortSet删除该msgId(此时代表已经处理过了)
六、接着与sortSet队列的⾸部第⼀个Id⽐较(其实就是最⼩的msgId),如果当前msgId<=sort Set第⼀个ID,则提交当前offset
七、系统即便挂了,在下次重启时就会从sortSet队⾸的消息开始拉取,实现⾄少处理⼀次语义
⼋、会有少量的消息重复,但只要下游做好幂等就OK了。
Rocket 与 kafka区别
数据可靠性
RocketMQ:支持异步实时刷盘、同步刷盘、同步复制、异步复制。
kafka:使用异步刷盘方式,异步复制/同步复制
kafka:使用异步刷盘方式,异步复制/同步复制
性能方面
kafka单机写入TPS月在百万条/秒,消息大小为10个字节。
RocketMQ单机写入TPS单实例约7万条/秒,若单机部署3个broker,可以跑到最高12万条/秒,消息大小为10个字节
RocketMQ单机写入TPS单实例约7万条/秒,若单机部署3个broker,可以跑到最高12万条/秒,消息大小为10个字节
RocketMQ写入性能上不如kafka, 主要因为kafka主要应用于日志场景,而RocketMQ应用于业务场景,
为了保证消息必达牺牲了性能,且基于线上真实场景没有在RocketMQ层做消息合并,推荐在业务层自己做
为了保证消息必达牺牲了性能,且基于线上真实场景没有在RocketMQ层做消息合并,推荐在业务层自己做
单机队列数
RocketMQ支持的队列数远高于kafka支持的partition数,这样RocketMQ可以支持更多的consumer集群
消息投递的实时性
kafka与RocketMQ都支持长轮询,消息投递的延迟在几毫秒内。
消息失败重试
RocketMQ支持消费失败重试功能,主要用于第一次调用不成功,后面可调用成功的场景。而kafka不支持消费失败重试
消息有序
kafka不保证消息有序,RocketMQ可保证严格的消息顺序,即使单台Broker宕机,仅会造成消息发送失败,但不会消息乱序。
分布式消息事务
kafka不支持分布式事务消息
RocketMQ支持分布式事务消息
RocketMQ支持分布式事务消息
ActiveMQ
万级中间件
支持高可用
社区慢慢不活跃了
RabbitMQ
万级中间件
友好的管理界面
erlang语言开发,不能定制化使用
dubbo
Netty
基于NIO的client-server客户端服务器框架
优点
统一API,支持多种传输类型。
简单而强大的线程模型。
自带编解码器包解决TCP粘包/拆包问题。
成熟稳定,提供异步高性能的通信,Dubbo,RocketMQ等都用到了Netty。
TCP粘包/拆包
TCP传输的是一串没有界限的数据,它会根据缓存区的实际情况进行包的划分,所以业务上的一个包可能拆开发送,就是“拆包”,多个小包封装成一个大的数据包发送就是”粘包“。
线程模型
Reactor单线程模型
一个线程独立处理所有IO操作。
高并发场景下,限于cpu压力,有使用瓶颈,而且存在系统隐患。
Reactor多线程模型
在处理链部分增加了线程池,多数场景下可以满足性能要求。
在一些特殊场景下,比如服务器增加安全认证,并且高并发的场景下,也会出现性能问题。
Reactor主从模型
将Reactor分为 mainReactor和 subReactor,mainReactor负责监听server socket,将socket分派给subRecetor处理。
Netty线程模型可以根据参数进行配置。
调用链路
服务暴露过程
服务引用
SPI
容错机制
降级
负载均衡
随机,按权重设置随机概率(默认)
轮询
最少活跃调用数,相同的随机
一致性Hash,相同参数的请求总发给同一个生产者
选举算法
注册中心
Zookeeper
协议
工作原理
service层
provider 和 consumer 接口,留给用户实现
config层
放配置文件
proxy层
代理层,为provider 和consumer 生成代理,用来调用通信
registry层
provider 注册自己作为一个服务,consumer可以找注册中心去寻找自己要调用的服务
cluster层
provider可以部署到多个机器上,多个provider组成集群
monitor层
监控调用信息等
protocol层
负责发布者和消费者之间的网络通信
exchange层
负责信息交换
serialize层
序列化
微服务
Spring Cloud
Eureka
注册中心
Eureka Client 将服务器信息注册进 Eureka Server
Eureka Server 通过注册表保存数据
Feign
服务调用
接口或类上使用@FeignClient注解,会调用Feign创建的动态代理对象
Ribbon
负载均衡
轮询算法访问
Hystrix
熔断器
三种状态
关闭、开启、半开
默认关闭,当请求失败次数达到阈值(默认20)则状态为开启,直接将请求返回失败。
5秒后会更改状态为半开,如果有线程成功则熔断关闭
5秒后会更改状态为半开,如果有线程成功则熔断关闭
Zuul
服务网关
转发请求,统一做降级、限流、认证等
bus
消息总线
config
配置中心
Nacos
相比Eureka ,Nacos功能更强大更方便。并且Eureka在2.0后就停止维护了
服务发现和服务健康监测
支持基于 DNS 和基于 RPC 的服务发现
动态配置服务
提供了一个简洁易用的UI (控制台样例 Demo) 帮助管理所有的服务和应用的配置
分布式锁
场景:
Java单机可以通过API实现线程同步,但是分布式系统场景下,需要通过分布式锁保证各系统之间同线程方法调用后的数据一致性。
Zookeeper
通过zk的znode 目录的特性,临时有序节点来实现分布式锁
创建临时有序节点,/lock/目录
创建成功后判断节点是否是序号最小的节点,是则获取锁
不是的话监听上一级目录,上级释放锁时被唤醒,再判断是否是最小
Curator(zk开源的客户端,提供了分布式锁的实现)
acquire()、release()方法实现加锁释放锁
原理与临时有序节点目录实现基本相同
Redis
SET key value NX PX milliseconds 命令
通过key加锁,释放则删除key
PX指定过期时间,不设置过期时间如果Redis宕机,可能导致死锁
释放锁需要注意value是否一致,一致才能删除key
缺点:主从哨兵模式下,主机宕机切换时可能造成锁丢失问题
RedLock算法(Redis自带)
轮询向所有master设置key,多数设置成功则加锁成功
无法保证加锁过程一定正确,不推荐使用
Redisson(开源框架,实际落地时比较常用)
对Redis原生的Set key和 RedLock实现方式都支持
实现简单,通过lock,unlock方法即可实现
针对Set key过期时间结束其他线程获取到锁的问题的解决办法
watchdog(看门狗),在获取锁后狗会每隔10秒帮你重置过期时间,宕机会随着消失。
数据库
创建一个有唯一约束的表,获取锁则新增数据,释放锁则删除数据
悲观锁
指的是在操作数据的时候比较悲观,悲观地认为别人一定会同时修改数据,因此悲观锁在操作数据时是直接把数据上锁,直到操作完成之后才会释放锁,在上锁期间其他人不能操作数据。
冲突比较多的时候, 使用悲观锁(没有乐观锁那么多次的尝试)对于每一次数据修改都要上锁,如果在DB读取需要比较大的情况下有线程在执行数据修改操作会导致读操作全部被挂载起来,等修改线程释放了锁才能读到数据,体验极差。所以比较适合用在DB写大于读的情况。
悲观锁的实现方式也就是加锁,加锁既可以在代码层面(比如Java中的synchronized关键字),也可以在数据库层面(比如MySQL中的排他锁)。
乐观锁
指的是在操作数据的时候非常乐观,乐观地认为别人不会同时修改数据,因此乐观锁默认是不会上锁的,只有在执行更新的时候才会去判断在此期间别人是否修改了数据,如果别人修改了数据则放弃操作,否则执行操作。
冲突比较少的时候, 使用乐观锁(没有悲观锁那样耗时的开销) 由于乐观锁的不上锁特性,所以在性能方面要比悲观锁好,比较适合用在DB的读大于写的业务场景
乐观锁的实现方式主要有两种,一种是CAS(Compare and Swap,比较并交换)机制,一种是版本号机制。
缺点是可能会出现锁表的风险
解决库存超卖问题
可以使用悲观锁,分布式锁,乐观锁,队列串行化,异步队列分散,Redis 原子操作等方案
用分布式锁高并发场景下
分布式锁可以解决超卖的问题,但是基于分布式锁串行化的处理,没办法解决多用户对同一商品同一时间的订单请求,高并发场景下处理性能较弱,
优化方案:可以通过分段加锁思路解决,也就是将总库存分为多个小库存单元,高并发进来通过随机算法随机分配库存段的key进行业务处理,如果库存为0则自动释放锁换下一个库存段再加锁后进行业务处理。
缺点:实现较复杂,需要对数据进行分段存储。
需要注意的点比较多,比如随机算法、自动切断分段等
需要注意的点比较多,比如随机算法、自动切断分段等
搜索引擎
Lucene
倒排索引
就是关键字与文档id的映射。类似于关联表,用户输入关键字,找到倒排索引中关键字对应的docid,找到对应的文档返回
倒排索引中一个词项对应一个或多个文档
词项是根据字典顺序升序排列的
ElasticSearch
分布式的文档搜索引擎
支持PB级数据
组成
Node节点
Index
索引,每个索引包含一堆相似结构的文档数据
Type
每个索引可以有一个或多个type,是index的逻辑分类
Document
es中最小的单元,相当于mysql的行,每个索引包含很多document
field
每个document下有多个field,相当于数据字段
shard
数据分片,然后存放在多个机器上,提高吞吐量和性能
primary shard 用于写数据,然后同步到其他 replica shard中,数据可从两个shard中读取。
分布式架构设计
es集群分为多个节点,选举其中一个节点为master节点,负责监听元数据、其他节点状态及shard的分配,如果master宕机重新选举一个master
非master节点宕机,则将节点中primary shard 在其他机器上的 replica shard转化为primary,等机器重启后,将primary缺失的其他shard标记为replica
es写数据原理
1,客户端选择一个节点发送请求过去,该节点就是coordinating node协调节点,协调节点对document进行路由,转发请求到对应的node,由primary shard进行处理请求
协调节点是通过hash计算出primary shard所在位置
2,primary shard处理请求后,将数据同步到其他机器的replica shard上
primary shard写数据原理
主要4个核心:refresh、flush、translog、merge
接收到请求后写入内存buffer,同时写入到os cache中,buffer中搜不到数据,os中才能搜到,同时还同步一份数据到translog中
buffer每隔一秒就会触发refresh操作,也就是将buffer中的数据同步到os cache中,然后清空buffer,将os cache数据写入一个segment file磁盘文件中。refresh 每隔一秒操作也是es为什么被称为准实时的原因。
当translog达到一定长度会触发commit操作,commit后将文件落到磁盘为flush操作,默认30分钟一次,translog主要是为了保证数据不丢失
每秒生成的segment file文件较多时,为节省空间,es会触发merge操作,将segment file合并成一个较大的file
3,同步完数据后,协调节点返回结果给客户
es读数据原理
1,客户端选择一个节点发送读请求,该节点为协调节点,对doc id进行hash,找到对应的node,转发请求到该node
2,node通过随机轮询算法,找到primary shard 或其他任意一个relica shard上获取数据
3,找到数据后node将document 给协调节点,协调节点返回结果给客户端
es删除/更新数据原理
删除操作:cmmit时会生成.del文件,将doc标记为delete状态,在merge时会删除
更新操作:就是将原来的doc标记为delete状态,然后新写入一条记录
数据量亿级时如何提高es查询效率
1,分配给os cache的内存大小是es数据量的一半以上,搜索较快
2,只将索引放到cache中,通过es + hbase架构搜索
3,数据预热,通过缓存预热系统每隔一段时间去访问cache的热点数据,让热点数据提前进入cache中
Solr
与ES区别
实时建立索引时,solr会产生io阻塞,es不会
不断动态写数据时,solr检索效率会下降,es基本没变化
solr是利用zookeeper进行分布式管理,es自身带有分布式管理功能
solr支持多种数据格式(xml,json,csv),es只支持json
OAuth2.0
四种获得令牌的授权方式
授权码(authorization-code)
最常用,并且安全的授权方式
流程
第一步:用户访问页面
第二步:访问的页面将请求重定向到认证服务器
第三步:认证服务器向用户展示授权页面,等待用户授权
第四步:用户授权,认证服务器生成一个code和带上client_id发送给应用服务器
然后,应用服务器拿到code,并用client_id去后台查询对应的client_secret
第五步:将code、client_id、client_secret传给认证服务器换取access_token和
refresh_token
第六步:将access_token和refresh_token传给应用服务器
第七步:验证token,访问真正的资源页面
第二步:访问的页面将请求重定向到认证服务器
第三步:认证服务器向用户展示授权页面,等待用户授权
第四步:用户授权,认证服务器生成一个code和带上client_id发送给应用服务器
然后,应用服务器拿到code,并用client_id去后台查询对应的client_secret
第五步:将code、client_id、client_secret传给认证服务器换取access_token和
refresh_token
第六步:将access_token和refresh_token传给应用服务器
第七步:验证token,访问真正的资源页面
优点
都是后端操作,暴露可能性小,安全性高
token可通过refresh_token设置过期时间
缺点
请求频繁
适用场景
对安全性要求高的,目前基本主流都是用这种授权模式
隐藏式(implicit)
流程
第一步:用户访问页面时,重定向到认证服务器。
第二步:认证服务器给用户一个认证页面,等待用户授权。
第三步:用户授权,认证服务器想应用页面返回Token,没有code授权码这一步了,所以叫隐式
第四步:验证Token,访问真正的资源页面
第二步:认证服务器给用户一个认证页面,等待用户授权。
第三步:用户授权,认证服务器想应用页面返回Token,没有code授权码这一步了,所以叫隐式
第四步:验证Token,访问真正的资源页面
缺点
都是暴露到前端处理,安全性不高
优点
简单
场景
问卷调查、评论
密码式(password)
流程
跟隐藏式相似,不过是直接用账号密码去访问认证服务器
缺点
有局限性,必须保证应用端和认证服务器之间有超高的信任度
优点
请求次数少
场景
应用服务器和认证服务器都是自己公司的
客户端凭证(client credentials)
流程
第一步:用户访问应用客户端
第二步:通过客户端定义的验证方法,拿到token,无需授权
第三步:访问资源服务器A
第四步:拿到一次token就可以畅通无阻的访问其他的资源页面。
第二步:通过客户端定义的验证方法,拿到token,无需授权
第三步:访问资源服务器A
第四步:拿到一次token就可以畅通无阻的访问其他的资源页面。
缺点
最不安全的方式,需要保证client是足够信任的
优点
简单
场景
一般不会使用这种方式
Canal & Maxwell
用途
主要用来监听数据增量变化,处理一些数据同步的问题场景
场景
可以用来监听mysql数据变化,同步到redis中
可以用来做数据迁移
原理
canal 模拟mysql slave的交互协议,伪装自己是slave,然后想master发送dump 协议
mysql接收到dump协议后,发送 binlog给canal
canal解析 binlog字节流文件,拿到数据增量变化,同步给另一台服务
实现
开启mysql的binlog
docker安装canal,修改propertices文件配置,主要是名称和数据源
搭建canal微服务,用来监控canal服务器,获取binlog日志,解析数据,将数据更新到redis中
引包,yml配置
@EnableCanalClient
canal 与 Maxwell 区别
Canal是阿里公司使用Java开发,Maxwell是zendesk公司使用Java开发。
Canal支持高可用HA,支持断点续传。Maxwell不支持HA,但是支持断点续传,要想支持HA需要自己实现。
Canal由于有Client消费数据,针对binlog数据可以使用Client自定义数据格式,Maxwell支持Json数据写出到Kafka或Redis。
Canal只能获取MySQL最新数据,Maxwell支持Bootstrap,可以支持获取MySQL中历史数据。
Canal采用Server+client模式,Maxwell没有采用这种模式,直接将数据发送到Kafka或者Redis等
Maxwell 较 Canal 更轻量
Canal支持高可用HA,支持断点续传。Maxwell不支持HA,但是支持断点续传,要想支持HA需要自己实现。
Canal由于有Client消费数据,针对binlog数据可以使用Client自定义数据格式,Maxwell支持Json数据写出到Kafka或Redis。
Canal只能获取MySQL最新数据,Maxwell支持Bootstrap,可以支持获取MySQL中历史数据。
Canal采用Server+client模式,Maxwell没有采用这种模式,直接将数据发送到Kafka或者Redis等
Maxwell 较 Canal 更轻量
项目设计
如何设计一个高并发系统
系统拆分
将业务模块拆分为业务子系统,每个子系统连一个数据库
技术方案:spring cloud 、 dubbo
缓存
高并发读下,必须走缓存
技术方案:Redis,可高可用,哨兵集群,堆机器,有多少并发都能抗
MQ中间件
高并发写场景下,必须走MQ,解耦异步削峰
技术方案:RocketMQ、Kafka等
分库分表
如果免不了数据库承受高并发的要求时,那就拆表,每个表数量少一点
技术方案:Sharding-JDBC 不需要部署,性能好,但是跟系统耦合度高。Mycat 需要部署,运维成本高,但是对各个项目是透明的。
如何将未分库分表的系统切到分库分表上去?
方案1:系统停机,将老数据根据临时系统导入分库中
方案2:双写迁移。修改配置,将增删改操作写入新老俩库,部署后导入老库数据到新库,
新库中不存在则写入,存在则比较时间戳,确保新数据覆盖老数据,等俩个库数据一致后,
部署上线只写入新库的系统。
新库中不存在则写入,存在则比较时间戳,确保新数据覆盖老数据,等俩个库数据一致后,
部署上线只写入新库的系统。
id主键如何处理
数据库自增
在一个没有业务意义的库中新增数据,拿到自增id,然后插入到分库分表库中
实现简单,适用于因为大数据量才进行的分库分表,但是对高并发场景下有瓶颈
设置数据库sequence或者设置表自增步长
根据服务节点个数通过sequence做起始id,然后设置节点个数为步长
实现简单,但是不容易扩展,加了服务节点则会很麻烦
uuid
本地生成
唯一,但是长度太长,查询性能太差
snowflake雪花算法
由 时间戳 + 机器号+ 序列号 组成
twitter 开源的分布式 id 生成算法。
对分布式的id生成友好,性能高,但是部署实现起来较复杂
读写分离
ES
ES是分布式的,天然支撑高并发,可以将计算量大的搜索查询和统计类交给ES处理
0 条评论
下一页