java基础和jvm看:https://github.com/Snailclimb/JavaGuide
推荐:https://learn.lianglianglee.com/
为什么用设计模式
为了代码的解耦合和代码的拓展性和重用性,保证代码的可靠性
设计模式的分类3
总体来说设计模式分为三大类:
创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
设计模式原则(总结就是开闭原则,抽象化、解耦化【各个功能做自己的事】、聚合化):
开闭原则:对拓展开放、对修改关闭
里氏代换原则(傻逼名称):“开-闭”原则的补充,任何基类可以出现的地方,子类一定可以出现
依赖倒转原则:针对接口编程,依赖于抽象而不依赖于具体
接口隔离原则:各个接口功能相对独立
迪米特法则(又是一个外国傻逼):实体间独立性
合成复用原则:尽量使用合成/聚合的方式,而不是使用继承
总结:尽量使得修改关闭,新增开发,依赖与抽象而不依赖于具体,实体间应该独立,且基类可以出现的地方,子类一定可以出现,尽量采用合成复用的方式而不是进行抽象
什么情况使用设计模式、设计模式有什么缺点:
首先,什么是设计模式,设计我们应该清楚设计模式是做什么的,设计模式是为了能够使代码解耦合和高拓展性
1.单例
1.构造方法私有化
2.编写getInstant方法并静态化
3.创建一个私有对象
2.工厂模式
新建一个接口
新建n个不同的接口实现类
创建fatory工厂,通过制定的参数标识采用if-else模式判断需要使用哪个实现
2.1.抽象工厂模式
简单的工厂模式是对一种工厂的抽象,而抽象工厂模式是对多个工厂的抽象
3.代理模式(试算拦截各个调用方法的响应时间统计)
静态代理:
定义一个接口及其实现类
创建一个代理类同样实现这个接口
将目标对象注入进代理类,然后在代理类的对应方法调用目标类中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
动态代理
jdk proxy
定义一个接口及其实现类;
自定义 InvocationHandler 并重写invoke方法,在 invoke 方法中我们会调用原生方法(被代理类的方法)并自定义一些处理逻辑;
通过 Proxy.newProxyInstance(ClassLoader loader,Class>[] interfaces,InvocationHandler h) 方法创建代理对象
CGLIB 动态代理
定义一个类;
自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法,和 JDK 动态代理中的 invoke 方法类似;
通过 Enhancer 类的 create()创建代理类;
4.策略模式
新建Strategy策略接口
实现n个策略实现类
新建Context类,类中有策略成员,设置策略成员方法,调用策略处理方法
5.观察者模式
新建观察者Subject主题接口,接口中有新增、修改通知、删除方法
新建观察者Observer订阅人接口,接口中有接收变动通知方法
新建主题实现类,类中实现方法,增加Observer队列,在修改通知接口中依次取出队列的值使用接收变动通知方法通知Observer
新建具体订阅人实现类,实现具体订阅业务
(java自带的观察者模式,主题继承Observable类,其他步骤不变)
6.原型模式(Prototype)
7.模板方法模式(Template Method)
抽象类,存在一个抽象方法和编写调用抽象方法的方法
编写一个类继承抽象类,覆盖抽象类的抽象方法
使用:实例化该类,用父类接收其实例进行调用
8.适配器模式(Adapter),适用与去除if-else结构
适配器模式涉及3个角色:
源(Adaptee):需要被适配的对象或类型,相当于插头。可以是接口,可以是类,也可以是对象
适配器(Adapter):连接目标和源的中间对象,相当于插头转换器。
目标(Target):期待得到的目标,相当于插座。
Adapter拥有Adaptee源并实现Target接口
//对象适配器类
class ObjectAdapter implements Target
{
private Adaptee adaptee;
public ObjectAdapter(Adaptee adaptee)
{
this.adaptee=adaptee;
}
public void request()
{
adaptee.specificRequest();
}
}
//类适配器类
//目标接口
interface Target
{
public void request();
}
//适配者接口
class Adaptee
{
public void specificRequest()
{
System.out.println("适配者中的业务代码被调用!");
}
}
class ClassAdapter extends Adaptee implements Target
{
public void request()
{
specificRequest();
}
}
9.装饰器模式(Decorator)
10.责任链模式(车辆违章处理)
1.处理短租订单违章 2.处理分时订单违章 3.处理第三方订单的违章 4.处理工单的违章
11.建造者
区别于工厂,他是工厂的一种管理
12.策略模式与工厂模式的区别
普通的工厂是采用factory写if-else结构去创建并返回对象使用,例子,如发送sender功能,可以有邮件发送也可以有短信发送,然后通过工厂去封装,通过if-else得到对应的对象使用,缺点不够灵活,要写if-else
策略模式注重于行为,决定权在用户,如计算器中简单的加法、减法使用不同的策略,实现方式可以写一个策略方法或者新增一个hashMap容器,通过传进来不同的策略,去做一个计算,然后外部你想使用加法的算法器还是减法的算法器
,直接new不需要工厂就可以使用,实现的方法都在算法器当中
通俗讲:工厂的通过条件实例化类,策略是用户主动实例化类(既工厂是程序返回一个对象进行使用,而策略是指定一个对象进行使用)
分布式事务的解决方法:失败重试、分布式事务(2/3阶段提交)、消息一致性幂等校验、兜底核对(定时核对数据的一致性并手动及自动数据修复)
分布式事务具体框架:seat
分布式框架数据一致性解决方案:TCC(2pc的思想)
Try:主要是数据的校验或者资源的预留
Confirm:确认真正执行的任务,只操作Try阶段预留的资源
Cancel:取消执行,释放Try阶段预留的资源。
分布式事务框架TCC模式下在出现的问题:(核心的解决方案就是增加事务状态控制表)
幂等
问题:TC重复调用二阶段
解决:事务状态控制记录作为控制手段,只有存在INIT记录时才执行,存在CONFIRMED/ROLLBACKED记录时不再执行
空回滚
问题:TC回滚事务调用二阶段,但一阶段尚未执行
解决:事务状态控制记录作为控制手段,无记录时即为空回滚
资源悬挂
问题:TC回滚事务调用二阶段完成空回滚后,一阶段执行成功
解决:事务状态控制记录作为控制手段,二阶段发现无记录时插入记录,一阶段执行时检查记录是否存在
网络
何为io
数据从用户空间和内核空间相互转化的一个输入输出过程
计算机结构分为:控制器、运算器、存储器、输入输出设备
有哪些IO模型:同步阻塞、同步非阻塞(通过轮询时间片操作,避免了一直阻塞)、多路复用、异步IO、信号驱动IO
同步阻塞:应用程序发起 read 调用后,会一直阻塞,直到内核把数据拷贝到用户空间
同步非阻塞:基于轮询,而不基于多路复用器,基于用户层面,非阻塞IO不会交出CPU,而会一直占用CPU
多路复用:基于操作系统的支持,使用多路复用器,由操作系统完成查看数据是否就绪,而不是在用户态进行判断
异步IO:异步IO是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作
同步阻塞:传统的socket操作
同步非阻塞:基于用户线程,没有用到selector,而是轮询询问socket状态
例子:
while(true){
data = socket.read();
if(data!= error){ 处理数据break; }
}
多路复用:基于内核(本质是一个或者多个线程管理一堆socket)
复用器原理:根据文件描述符fds判断,将fds传递到内核进行监听,内核根据传进来的fds文件描述符是否是有效的,然后进行判断
select:基于数组,fd_set是固定大小的缓冲区bitmap,默认1024
poll:基于链表,与select不同的是,FD_SETSIZE没有固定的数值,可以不是1024,也可以是2048
select和poll的缺点:每次都要重新重复传递fds(内核开辟空间),每次内核被调用了之后,针对这次调用,触发了一个遍历fds全量的复杂度,时间复杂度为O(n)
epoll:基于hash表,将fd_set进行排序,有数据进来的放在最前面,并且传送当前已有数据的通道数量,循环遍历当前已准备好的通道即可,如 fd1、fd2、fd3、fd4、fd5,此时如果3、4通道有数据进来,则会变成fd3、fd4、fd1、fd2、fd5,此时for(fd<2;fd in fds)
问题:什么场景下使用哪种复用器:
Java对复用器做了一层封装,暴露给我们开发人员的统一是selector,当我们app在运行的时候,需要根据实际的os平台以及os版本,帮我们选择一个最合适的复用器
哪里调用了复用器:
在调用selector.select()时调用,根据os平台的不同,使用不同的复用器,在epoll情况下,对应的是epoll_wait方法,在select情况下,对应于select()方法,在poll情况下,对应于poll()方法
例子:Java NIO实际上就是多路复用IO
while (true) {
//这里会去询问是否有数据就绪,会进行阻塞,即当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();
// 当前事件来的时候的迭代器
Iterator ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
// 删除已选的key,以防重复处理
ite.remove();
// 客户端请求连接事件
......
}
}
异步io:基于内核回调机制,整个完整的io操作都是异步的,原理是在内核层使用线程异步(非应用层面实现的线程异步,应用层实现的叫异步处理,不能称之为异步io)
服务端:AsynchronousServerSocketChannel--CompletionHandler/Future接口
客户端:AsynchronousSocketChannel--socket.read/socket.write--completed/Future
信号驱动:基于Signal类,Signal.handle()方法用户注册信号处理器,Signal.raise用于激发信号,发起时,socket注册一个信号函数,用户线程继续执行,就绪时,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作
完整的IO操作:
查看数据是否就绪(异步使用CompletionHandler或者Future查询是否就绪)
进行数据拷贝(内核将数据拷贝到用户线程)
异步和同步:针对的是数据拷贝,针对用户线程和内核的交互而言,进行read/write调用的时候是否需等待完成并返回对应进程,阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO
同步:当用户发出IO请求操作之后,如果数据没有就绪,需要通过用户线程或者内核不断地去轮询数据是否就绪,当数据就绪时,再将数据从内核拷贝到用户线程
异步:IO操作的两个阶段都是由内核自动完成,然后发送通知告知用户线程IO操作已经完成。也就是说在异步IO中,不会对用户线程产生任何阻塞
阻塞和非阻塞:操作系统os层面,进行read和write时是否进行阻塞
阻塞:调用方法等待返回,如果数据没有就绪,则会一直在那等待,比如调用read方法
非阻塞:调用方法立即返回,如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪,比如read调用会告诉当前用户线程一个标志位,是就绪还是未就绪
例如AIO中:
客户端发送消息,使用客户端:AsynchronousSocketChannel的socket使用CompletionHandler异步发送、异步读取
服务端接收消息,使用服务端端:AsynchronousServerSocketChannel的accept使用CompletionHandler异步发送、异步读取
ChatGpt对netty使用NIO的解读:
Netty使用的是NIO模型,但是它在NIO的基础上实现了异步非阻塞模型。具体来说,Netty使用了Java NIO提供的Selector机制,实现了事件驱动的异步非阻塞通信模型。在Netty中,所有的I/O操作都是异步的,不会阻塞线程。当有数据可读或可写时,Netty会通知应用程序进行相应的处理。因此,虽然底层使用的是NIO模型,但Netty本身实现了更高层次的异步非阻塞通信模型,使得开发者可以更加方便地编写高性能、可扩展的网络应用程序。
直接内存和非直接内存:
直接内存为非堆内存,java中可以通过ByteBuffer.allocateDirect()申请,非直接内存为堆内存java中可以通过ByteBuffer.allocate()申请
堆内存创建快,读写慢。堆外内存,创建慢,读写快
java中io模型有:BIO、NIO、AIO
BIO属于同步阻塞
NIO属于I/O 多路复用模型(Buffer、Channel、Selector)与多路复用epoll
AIO属于异步IO
多路复用器有哪些:select、poll、epoll等
DMA控制器:
在进行I/O设备和内存的数据传输,数据的搬运工作都交由DMA控制器完成,而CPU不再参与任何与数据搬运有关的事情
Buffer:负责数据存储
Buffer(缓冲区)是一个用于存储特定基本类型数据的容器。除了boolean外,其余每种基本类型都有一个对应的buffer类。Buffer类的子类有ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer 。
socket读写的原理/Buffer原理:
队列queue,发送的队列和接收的队列
Channel:负责数据的传输,不负责存储,需要配合buffer使用
Channel(通道)表示到实体,如硬件设备、文件、网络套接字或可以执行一个或多个不同 I/O 操作(如读取或写入)的程序组件的开放的连接。Channel接口的常用实现类有FileChannel(对应文件IO)、DatagramChannel(对应UDP)、
SocketChannel和ServerSocketChannel(对应TCP的客户端和服务器端)。Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的,譬如:InputStream, OutputStream.而Channel是双向的,既可以用来进行读操作,又可以用来进行写操作。
Selector
Selector(选择器)用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。即用选择器,借助单一线程,就可对数量庞大的活动I/O通道实施监控和维护。
linux epoll实现原理:
Selector.open()-->SelectProvider.provider()--->DefaultselectorProvider.create()-->EpollSelectorProvider.openSelector()-->new EpollSelectorImpl()-->new EpollArrayWrapper-->epollCreate()得到epfd-->调用native的linux内核函数epoll_create实例
ServerChannel.register--SelectorImpl.register--->pollArrayWrapper.add(fd)将fd注册到EpollArrayWrapper这个集合中
selector.select-->EpollSelectorImpl.doSelect--pollArrayWrapper.poll-->epollCtl()进行事件绑定-->调用内核的epoll_ctl
线程updateRegistrations-->调用epollWait-->调用linux内核的epoll_wait方法
select实现原理:数组
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
int FD_ZERO(int fd, fd_set *fdset); // 一个 fd_set 类型变量的所有位都设为 0
int FD_CLR(int fd, fd_set *fdset); // 清除某个位时可以使用
int FD_SET(int fd, fd_set *fd_set); // 设置变量的某个位置位
int FD_ISSET(int fd, fd_set *fdset); // 测试某个位是否被置位
poll实现原理:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
int fd; // 文件描述符
short events; // 感兴趣的事件
short revents; // 实际发生的事件
};
poll、select、epoll区别:
select-- poll epoll
操作方式 遍历 遍历 回调
底层实现 数组 链表 红黑树
io效率 On On O1
最大连接数 1024 无上限 无上限
fd拷贝 每次调用select/poll,都需要 调用epoll_ctl拷贝进内核并保存,每次调用epoll_wait不拷贝
把fd集合从用户态拷贝到内核
dubbo通信,server处理完结果后,将结果消息发送给client,client收到很多消息,怎么知道哪个消息结果是原先哪个线程调用的
使用一个ID,让其唯一,放到map中,然后传递给服务端,ResponseFuture再服务端又回传回来,这样就知道结果是原先哪个线程的了
NIO和BIO比较
相比于传统的 BIO 模型来说, NIO 模型的最大改进是:
1. 使用比较少的线程便可以管理多个客户端的连接,提高了并发量并且减少的资源消耗(减少了线程的上下文切换的开销)
2. 在没有 I/O 操作相关的事情的时候,线程可以被安排在其他任务上面,以让线程资源得到充分利用。
使用 NIO 编写代码比较复杂
NIO中网络io中SocketChannelImpl读写数据流程:
1:读
从输入流读取数据,委托给IOUtil,从输入流读取数据写到ByteBuffer数组中,再将buffer添加到iovecwrapper的字节缓冲区数组,最后通过SocketDispatcher,从filedescriptor对应的输入流读取数据,写到iovecwrapper的缓冲区中
2:写
SocketChannelImpl写ByteBuffer数组方法,首先同步写锁,确保通道,输出流打开,连接建立委托给IOUtil,将ByteBuffer数组写到输出流中,这一过程为获取存放i个字节缓冲区的IOVecWrapper,最后通过SocketDispatcher,将iovecwrapper的缓冲区数据,写到filedescriptor对应的输出流中
访问一个URL经历了哪些过程
获取URL - > DNS解析 - > TCP连接 - >封装并发送报文 ->服务器接收并处理请求 - >返回报文 - >浏览器解析渲染页面 - > TCP断开连接
TCP结构:
TCP的连接是需要四个要素确定唯一一个连接:
(源IP,源端口号)+ (目地IP,目的端口号)
所以TCP首部预留了两个16位作为端口号的存储,而IP地址由上一层IP协议负责传递
源端口号和目地端口各占16位两个字节,也就是端口的范围是2^16=65535
另外1024以下是系统保留的,从1024-65535是用户使用的端口范围
TCP的序号和确认号:
32位序号 seq:Sequence number 缩写seq ,TCP通信过程中某一个传输方向上的字节流的每个字节的序号,通过这个来确认发送的数据有序,比如现在序列号为1000,发送了1000,下一个序列号就是2000。
32位确认号 ack:Acknowledge number 缩写ack,TCP对上一次seq序号做出的确认号,用来响应TCP报文段,给收到的TCP报文段的序号seq加1。
TCP的标志位
每个TCP段都有一个目的,这是借助于TCP标志位选项来确定的,允许发送方或接收方指定哪些标志应该被使用,以便段被另一端正确处理。
用的最广泛的标志是 SYN,ACK 和 FIN,用于建立连接,确认成功的段传输,最后终止连接。
SYN:简写为S,同步标志位,用于建立会话连接,同步序列号;
ACK:简写为.,确认标志位,对已接收的数据包进行确认;
FIN:简写为F,完成标志位,表示我已经没有数据要发送了,即将关闭连接;
PSH:简写为P,推送标志位,表示该数据包被对方接收后应立即交给上层应用,而不在缓冲区排队;
RST:简写为R,重置标志位,用于连接复位、拒绝错误和非法的数据包;
URG:简写为U,紧急标志位,表示数据包的紧急指针域有效,用来保证连接不被阻断,并督促中间设备尽快处理;
三次握手的状态:SYN_SENT、SYN_RCVD、ESTABLISHED
http三次握手四次挥手
三次握手(作用:确认发送成功)
Client向服务端发送连接请求,进入SYN_SENT状态,SYN=1、seq=J
Server收到客户端发送的请求并返回ACK确认是否可以连接请求,进入SYN_RCVD状态,SYN=1、ACK=1、seq=K、ack=J+1
客户端收到确认请求,发送数据,进入ESTABLISHED监听状态,ACK=1、ack=K+1
注:小写的ack代表的是头部的确认号Acknowledge number, 缩写ack,是对上一个包的序号进行确认的号,ack=seq+1。
大写的ACK,则是我们上面说的TCP首部的标志位,用于标志的TCP包是否对上一个包进行了确认操作,如果确认了,则把ACK标志位设置成1
四次挥手(由于TCP 是全双工信道,即客户端连接)
第一次挥手:
Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。
第二次挥手:
Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态、Client进入Fin_WAIT_2状态。
第三次挥手:
Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
第四次挥手:
Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
ssl四次握手:
客户端请求建立SSL链接,并向服务端发送一个随机数–Client random和客户端支持的加密方法,比如RSA公钥加密,此时是明文传输。 对应https原理与交互过程1阶段
服务端回复一种客户端支持的加密方法、一个随机数–Server random、授信的服务器证书和非对称加密的公钥。 对应https原理与交互过程2阶段
客户端收到服务端的回复后利用服务端的公钥,加上新的随机数–Premaster secret 通过服务端下发的公钥及加密方法进行加密,发送给服务器。 对应https原理与交互过程3、4、5、6阶段
服务端收到客户端的回复,利用已知的加解密方式进行解密,同时利用Client random、Server random和Premaster secret通过一定的算法生成HTTP链接数据传输的对称加密key–session key 对应https原理与交互过程5、6、7阶段
TCP如何保证数据的可靠传输(三次握手主要为了保证可靠性传输,通过ack和seq):
1.tpc给每个包一个编号,这个编号是有序的,接收方有序的接收数据
2.会进行流量控制
3.会进行拥阻控制
4.使用ARQ协议,一组发完了才能进行下一组发送
5.会进行超时重传
6.应用数据会分割成合适的块
7.会丢弃重复数据
8.会进行校验和
linux监听网络命令:tcpdump、netstat
在浏览器中输入 url 地址 ->> 显示主页的过程
DNS 解析
TCP 连接
发送 HTTP 请求
服务器处理请求并返回 HTTP 报文
浏览器解析渲染页面
连接结束
http的keepalive 和 tcp的keepalive
http协议的keepalive什么意思呢?想和对方保持长连接,长连接就是咱们俩三次握手建立连接后一直用这个连接发送数据
tcp的keepalive完全是另外一件事,主要作用是通过定时发送探测包来探测连接的对端是否存活的一种机制
健康检查应该用内核的keepalive还是应用层的http
应该用应用层的http定时进行http检查,使用内核的ping的keepalive,只能判断当前连接的端口号或者线程是通的,但是整个应用是否通需要使用http确定
为什么要三次握手,为什么要四次挥手:
建立可靠的通信信道(产生的序列号seq,保证服务端可以顺序接收),确认自己与对方的发送与接收是正常的
四次挥手为了断开连接前的确认操作,发送连接释放的信号
为什么建立连接是三次握手,而关闭连接却是四次挥手呢
四次挥手相对于三次握手中间的两个操作,ACk确认后再发送FIN包,在确认关闭之后服务端只是进入了close_wait状态仍能发送数据和待回收资源,两个步骤是分开的,而握手的时候只需要确认可以发送即可
什么是TCP可靠性传输:
所谓可靠传输是指在通讯时常见的丢包,乱序的条件下依然可以保障数据被依序接受,中间不丢数据。是靠序列号,滑动收发窗口, 接收方ACK 和 SACK, 发送方重传等机制保障的
TCP分包和粘包问题:
TCP每次数据发送都会以数据包的形式进行发送,而数据包发送是一段一段的进行发送,且是顺序发送,数据包格式中包含了数据头和数据信息,数据头信息中会存在长度信息,而服务器端只需要截取指定长度数据信息即可,然后获取该次数据的完整信息
分包可以分成三种:1.定长传输 2.以分隔符传输 3分成头部和body传输,头部存储当前字节长度
TCP长连接(http1.1开始默认都是长连接)和短连接:
理论:
短连接: 每次发起HTTP请求都要进行连接,然后再传输完后关闭连接。
长连接: 发起完HTTP请求后,并不关闭连接,当连接复用次数到一定次数,或者过了一定时间、或者客户端主动释放连接才会关闭连接。
从服务端代码确认层面的处理:
read调用的时候是否是循环read,对于短连接的时候,只需要read一次就可以了,对于长连接需要使用while循环不断read调用,直到客户端主动释放连接,服务端是不能主动释放连接的
https原理与交互过程(https://zhuanlan.zhihu.com/p/43789231):
对于浏览器分为:1请求网络链接、3证书的合法性校验、4生成对称加密随机密钥R、5用证书里的非对称加密公钥加密、6传输使用公钥加密后的R、10使用R解密网页内容
对于服务器:2确认加密算法和hash算法并返回证书、7用私钥解密R获取R、8使用R加密网页内容、9返回加密的网页内容
https交互过程涉及的名词和技术:
对称加密、非对称加密、CA认证、数字签名、摘要(Hash算法、MD5、SHA1等)、ssl(安全套接层协议)/TlS(安全传输层协议)、
TLS使用的密码技术
伪随机数生成器(为了生成随机对称密钥):秘钥生成随机性,更难被猜测
对称密码(用于加密数据):对称密码使用的秘钥就是由伪随机数生成,相较于非对称密码,效率更高
消息认证码(为了确保消息的发送是真实的发送方):保证消息信息的完整性、以及验证消息信息的来源
公钥密码(用于非对称加密):证书技术使用的就是公钥密码
数字签名(用于验证服务器给的证书是真实的证书,而不是伪造的,使用散列的hash函数或者md5):验证证书的签名,确定由真实的某个 CA 颁发
证书(保存公钥和基本信息):解决公钥的真实归属问题,降低中间人攻击概率
TSL与ssl区别:
SSL表示安全套接字层,而TLS表示传输层安全,tls使ssl增强版本,原理基本上一致,就是一些证书处理和伪随机数的生成改变了(ssl使用MAC,tls使用HMAC)
为什么要用对称加密+非对称加密
为了兼顾安全与效率,对数据进行对称加密,对称加密所要使用的密钥通过非对称加密传输。
为什么不能只用非对称加密
非对称加密耗性能,效率低,所以只做了一次非对称加密
为什么需要数字证书(CA机构)
证明浏览器收到的公钥一定是该网站的公钥,证明身份使用,类似于公安局发的身份证
为什么需要数字签名
为了数字证书的“防伪技术”,防篡改(CA机构是浏览器信任的机构,所以浏览器保有它的公钥)
浏览器如何保有CA机构的公钥
操作系统、浏览器本身会预装一些它们信任的根证书,如果其中会有CA机构的根证书,这样就可以拿到它对应的可信公钥了
多线程产生的问题:死锁、线程不安全、内存泄漏
多线程的内存泄漏问题:
AsyncTask和Runnable都使用了匿名内部类,那么它们将持有其所在Activity的隐式引用。如果任务在Activity销毁之前还未完成,那么将导致Activity的内存资源无法被回收,从而造成内存泄漏
死锁和死锁解决方案:
互斥条件:
请求与保持条件:一次性申请所有资源
不剥夺条件:获取不到锁释放自己的锁
循环条件:按顺序获取锁
内存泄漏:
资源未关闭造成的内存泄漏
单例周期长
非静态内部类创建静态实例造成的内存泄漏
线程造成的内存泄漏:AsyncTask和Runnable都使用了匿名内部类,使用静态内部类进行替换或者独立为一个类,也可以采用线程池替换,关闭使用shutdown方法
集合容器中的内存泄露
Handler造成的内存泄漏:也是持有Activity引用未释放,解决方式是实现Handler的接口静态化,Handler类中持有的对象弱引用化
如何保证线程安全:保证线程安全实际是保证程序的正确执行和共享数据的正确读写,实现方式如下
线程同步:使用关键字synchronized、CAS机制、使用原子类、volatile禁止指令重排和保证可见性
线程隔离:ThreadLocal
线程池的好处:
降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
线程的调度模型:
分时调度模型(非抢占式调度模型):指让所有的线程轮流获得cpu的使用权,并且平均分配每个线程占用的CPU的时间片
守护线程就是属于非抢占式调度模型,例子有,redission锁的看门狗模式,JVM的垃圾回收线程
抢占式调度模型:优先让可运行池中优先级高的线程占用CPU
调整各个线程的优先级方式有
让处于运行状态的线程调用Thread.sleep()方法
让处于运行状态的线程调用Thread.yield()方法
让处于运行状态的线程调用另一个线程的join()方法,当前处于运行状态的线程阻塞等到另一个线程结束
sleep、wait、yield区别:
sleep:sleep()用于休眠当前线程,sleep()不释放对象锁,让出CPU,线程状态为等待状态
wait:wait()用于线程同步或者线程之间进行通信,wait()释放对象锁,让出CPU,线程状态为等待状态/超时等待
yield:yield()临时暂停当前正在执行的线程,yield()仅释放线程所占用的CPU,线程状态为回到就绪状态
容器
hashMap长度为什么是2的幂次方(new HahsMap若没指定大小默认大小在put第一个值的时候默认大小为16)
为了尽可能的使数据在数组中分散均匀,减少hash碰撞并可以【提高运算效率】,hashMap采用了扰动函数(就是hashMap自己的hash方法)得到hash值,数据下标的位置运算为(n-1)& hash,其中n等于数组长度
例如:假设数组长度16,一个odj对象hashCode为x,则计算hash(obj)=(h = key.hashCode) ^ (h >>> 16)=x^(x>>>16)得到二进制的后四位,则数组的下标就为z=y % 16= y & (16-1),
要使得这个公式成立,则数组的大小必须是2幂次方
get方法如何确定key在数组中的位置:
先通过 hash(key) 得到对应的hash值,再通过 tab[(n-1) & hash] 来确定位置。hash(key)为(h = key.hashCode) ^ (h >>> 16)
hash(key)函数为(h = key.hashCode) ^ (h >>> 16)这个公式的作用
混合高位和地位来加大随机性
总结:通过异或移位运算加大hash值得随机性,通过&位运算提供运算效率,而如果需要用位运算则数组的长度必须是2的幂次方
new HashMap<>()的时候有没有指定容量
new 一个hashmap时并没有指定容量,只是计算出了loadfactor,在真正第一次put值的时候才会初始化容量,如果new HashMap<>()则默认为是16,如果指定了容量,则默认指定容量上限的2的幂次方倍,如new HashMap<>(7),则put第一个值时初始化容量为8,
当put到第7个值的时候++size > threshold会扩容到16,记住:第六个的时候并不会产生扩容
hashMap的头插法和多线程下的死循环:
关于头插法和尾插法:
jdk1.8尾插法出现在新增元素冲突的时候加入到链表的尾部,jdk1.7头插法指新增元素冲突的时候加入到链表的头部,扩容的时候会出现死循环的情况
死循环问题
jdk1.7和1.8都会出现死循环的情况
jdk1.7在扩容时出现死循环
jdk1.8链表转换为树,对树进行操作时
什么是AQS及其原理:
AQS为构建锁和同步器的框架,提供一套线程阻塞和唤醒的机制,采用双向队列,将暂时获取不到锁的线程放入队列中进行等待
CountDownLatch订单模块应用(mysql数据通过canal同步es)
canal订阅订单(更新插入删除log),提取log日志,解析log日志,循环每次取1000条数据,根据hash算法以订单维度放到不同的队列中,队列数是指定的,
并分多个线程执行任务队列中的数据,此时需要使用CountDownLatch,,标记同步es成功与失败的记录,当所有组线程都执行完毕之后,数据处理异常数量统计(进行canal回滚及响应操作)
定时提醒延时跑批业务(订单还车前延迟消息、订单排车延迟消息、畅游租订单取车延迟消息、畅游租订单还车延迟)
1.在特定节点,通过计算当前时间是否能够落到rocketmq的18个延时级别上,如果落不到对应的mq级别,则通过redis,计算分数为通知时间与当天0点的毫秒差值,addZSet存入zset
2.通过订单定时提醒跑批,创建不同的延时业务task,订单还车前延时消息和订单排车延时消息分成不同的两个task,task实现了Runnable
3.设置CountDownLatch,跑批是10分钟执行一次,设置CountDownLatch最长阻塞时间为10分钟。
4.调用线程池的submit方法,
5.task任务的处理,rangeByScore计算当次跑批拉取zset数据的score范围(跑批每10分钟执行一次,将当前时间往后推迟10分钟,计算10分钟内需要发送的消息)
6.判断当前消息是否落入rocketmq的延时级别,如果落入mq延时级别,放入到mq的延时级别,否则不做处理,当放入到rocketmq延时级别之后,zerm删除redis中的缓存数据
mysql存储过程应用(mysql分表通过canal同步整合到其他数据库中)
canal通过解析分表数据,解析所有分表数据将分表后缀剔除,执行插入数据的存储过程到对应的整合数据库中
对象实例只能分配在堆上吗:不是
对象实例如果未发生了逃逸会在栈上进行分配
JIT优化技术:方法内联和逃逸分析
方法内联:把「目标方法」的代码复制到「调用的方法」中,避免发生真实的方法调用,因为每次方法调用都会生成栈帧(压栈出栈记录方法调用位置等等)会带来一定的性能损耗,所以「方法内联」的优化可以提高一定的性能
逃逸分析:判断一个对象是否被外部方法引用或外部线程访问的分析技术,如果「没有被引用」,就可以对其进行优化,
1. 锁消除(同步忽略):该对象只在方法内部被访问,不会被别的地方引用,那么就一定是线程安全的,可以把锁相关的代码给忽略掉
2. 栈上分配:该对象只会在方法内部被访问,直接将对象分配在「栈」中(Java默认是将对象分配在「堆」中,是需要通过JVM垃圾回收期进行回收,需要损耗一定的性能,而栈内分配则快很多)
3. 标量替换/分离对象:当程序真正执行的时候可以不创建这个对象,而直接创建它的成员变量来代替。将对象拆分后,可以分配对象的成员变量在栈或寄存器上,原本的对象就无需分配内存空间了
什么是逃逸分析,举个例子
某些方法中的对象引用没有被返回或者未被外面使用(也就是未逃逸出去),则叫未逃逸,否则为逃逸,未逃逸的情况会在栈上分配对象,比如以下两种情况:
一、对象被赋值给堆中对象的字段和类的静态变量、
二、对象被传进了不确定的代码中去运行
public class EscapeTest {
public static Object globalVariableObject;
public Object instanceObject;
public void globalVariableEscape(){
globalVariableObject = new Object(); //静态变量,外部线程可见,发生逃逸
}
public void instanceObjectEscape(){
instanceObject = new Object(); //赋值给堆中实例字段,外部线程可见,发生逃逸
}
public Object returnObjectEscape(){
return new Object(); //返回实例,外部线程可见,发生逃逸
}
public void noEscape(){
synchronized (new Object()){
//仅创建线程可见,对象无逃逸
}
Object noEscape = new Object(); //仅创建线程可见,对象无逃逸
}
}
开启逃逸分析:-XX:+DoEscapeAnalysis
逃逸分析好处:
方法栈上的对象在方法执行完之后,栈桢弹出,对象就会自动回收。这样的话就不需要等内存满时再触发内存回收。这样的好处是程序内存回收效率高,并且GC频率也会减少,程序的性能就提高了
逃逸分析是方法级别的,因为JIT的即时编译是方法级别
什么代码是JIT编译的代码:(热点代码)(热点代码阈值:-XX:CompileThreshold)
1、被多次调用的方法
2、被多次执行的循环体
堆栈方法区分配
public class Fruit {
final static int constantPrice = 50; //50分配在常量池 ,constantPrice分配在方法区
final static String constantName = "水果"; //水果分配在常量池 ,constantName分配在方法区
static int staticPrice = 100; //100分配在常量池 ,staticPrice分配在方法区
static BigWaterMelon thin = new BigWaterMelon("皮薄", staticPrice); //thin分配引用在方法区,实例分配在堆 ,"皮薄"分配在常量池
int instancePrice = 200; //instancePrice引用在方法区,实例分配在堆 ,实例是200
BigWaterMelon juicy = new BigWaterMelon("多汁", instancePrice); //juicy分配引用在方法区,实例分配在堆 ,"多汁"分配在常量池
public static void main(String[] args) {
Fruit fruit = new Fruit(); //fruit实例分配在堆 ,实例是fruit
int mainThreadLocalPrice = 300; //mainThreadLocalPrice实例分配在堆 ,实例是300
}
整体方法调用过程:虚拟机栈出栈和入栈
jit方法调用计数器的过程
1.方法入口 2.判断是否存在编译的版本
3.存在则执行编译后的本地代码版本
4.不存在则
4.1方法调用计数器+1
4.2判断是否到达阈值
4.3向jit编译器提交编译请求
4.4以解释方式执行方法
5.方法返回
什么是热点代码(判断是否达到阈值调用次数)
(2)阈值设置
可以通过"-XX:CompileThreshold"参数设定;
· (3)阈值表示
(A)执行频率
方法调用执行频率:一段时间内方法被调用的次数;
可以通过"-XX:CounterHalfLifeTime"参数
(B)方法调用的绝对次数
可以通过"-XX:-UseCounterDecay"参数
CPU100%问题(跑批迁移欠款数据问题)
ps -ef|grep java找到java应用的进程
top -Hp pid找出占用CPU的当前进程的pid
echo "obase=16;2944" | bc输出pid2944表示16进制字符
jstack pid > pid.dump 线程栈导出到文件
内存泄漏问题排查经验:
top下对当前服务器内存有个大致了解
ps找到对应的进程id,并查看到对应的启动jvm信息
jstat -gcutil 17561 1000 10查看gc运行状态详细信息
分析找出内存占用超出预期的嫌疑对象:
方法1:下载dump文件使用MAT工具进行分析
方法2:堆Dump在线可视化分析: https://heaphero.io/
方法3:使用jhat命令获取分析报告,并在浏览器查看分析报告
OOM内存溢出问题(数据查询过多问题)
top下对当前服务器内存有个大致了解
ps -ef|grep java找到java进程
jmap -dump:format=b,file=heap.prof 17561生成dump文件
使用jhat命令获取分析报告或利用MAT分析dump文件(也可以用jdk自带的jvisualvm.exe)
选择 “list Object”、“with incoming references”
2PC
1、同步阻塞问题(不在本操作中的其他事务操作共享资源时会被阻塞)。
2、协调者挂机导致参与者阻塞。(参与者资源正在锁住资源,协调者挂机阻塞参与者)
3、数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。
而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象
3PC可以解决同步阻塞问题和协调者挂机导致参与则阻塞(CanCommit、PreCommit、DoCommit三个阶段)
1、引入超时机制。同时在协调者和参与者中都引入超时机制。
2、在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。
PreCommit阶段将undo和redo信息记录到事务日志中,对资源进行加锁(资源还未释放)
在协调者挂机之后,如果已经到达了PreCommit阶段且是yes,就算协调者挂机了收不到DoCommit也会默认执行事务并释放资源
KAFKA和rocketmq的区别
kafka不支持严格顺序消费,Broker宕机后,就会产生消息乱序
kafka不支持定时消息
kafka不支持同步刷盘,数据不可靠
kafka不支持tag过滤,但是支持partition
kafka不支持消息重试
kafka不支持事务,rocketMq可以做分布式事务
kafka单机写入速度更快(sendfile)
kafka在日志领域更成熟,rocketmq在业务应用领域更成熟
ThreadLocal解决hash冲突的方法
使用黄金分割数减少hash冲突,并通过开放地址法解决hash冲突
ThreadLocal原理
使用threadLocalMap存储局部变量数据,key存储的是每个ThreadLocal,value存储数据,key为弱引用,会产生垃圾,在通过set、get、remove过程会把key为null的数据清理
threadLocalMap数据结构
采用数组存储数据
清理过程
在set操作下,如果遇到key过期的数据(key为null,entry不为null),先进行探测式清理(在当前key为null往前查找key也为null的区间清理) 例如: a knull b c null knull c d e knull会找到knull c d e knull这个区间
expungeStaleEntry 探测式清理(分段清理)
cleanSomeSlots启发式清理(启发式清理包含了expungeStaleEntry,真正做清理的还是在expungeStaleEntry里)
ThreadLocal重要方法:
get、set、getMap、createMap、replaceStaleEntry、cleanSomeSlots、expungeStaleEntry、nextIndex、rehash、prevIndex
重要参数:
threshold、size、sz、HASH_INCREMENT、table
synchronized的底层Monitor实现:
ObjectMonitor() {
_count = 0; //用来记录该对象被线程获取锁的次数
_waiters = 0;
_recursions = 0; //锁的重入次数
_owner = NULL; //指向持有ObjectMonitor对象的线程
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
}
synchronized锁升级:对象刚开始建立时,属于无锁状态,当第一个线程进来时,如果偏向锁开启(-XX:+UseBiasedLocking),进行偏向锁加锁,当第二个线程进来时,此时锁的状态是偏向锁,此时需要将锁偏向撤销,
升级为轻量级锁,进行CAS锁竞争,未获取到锁的时候,膨胀为重量级锁,当持有锁的线程释放锁后,唤醒堵塞的线程, 然后线程再次竞争锁,但此时锁为重量级锁,不会
退化为轻量级锁,当线程执行完毕,即线程不存活的时候,重置锁状态为无锁
重量级锁 对应 10
轻量级锁 对应 00
无锁和偏向锁都对应01,这个时候需要倒数第三个字节加以区分
即 无锁 对应 001, 偏向锁 对应 101
偏向锁的撤销(线程B获取到了偏向锁,此时线程A进来)
偏向锁的撤销需要等待全局安全点(safe point,代表了一个状态,在该状态下所有线程都是暂停的,stop-the-world),到达全局安全点后,持有偏向锁的线程B也被暂停了。
检查持有偏向锁的线程B的状态(会遍历当前JVM的所有线程,如果能找到线程B,则说明偏向的线程B还存活着):
5.1 如果线程还存活,则检查线程是否还在执行同步代码块中的代码:
5.1.1 如果是,则把该偏向锁升级为轻量级锁,且原持有偏向锁的线程B继续获得该轻量级锁。
5.2 如果线程未存活,或线程未在执行同步代码块中的代码,则进行校验是否允许重偏向:
5.2.1 如果不允许重偏向,则将Mark Word设置为无锁状态(未锁定不可偏向状态),然后升级为轻量级锁,进行CAS竞争锁。
5.2.2 如果允许重偏向,设置为匿名偏向锁状态(即线程B释放偏向锁)。当唤醒线程后,进行CAS将偏向锁重新指向线程A(在对象头和线程栈帧的锁记录中存储当前线程ID)。
唤醒暂停的线程,从安全点继续执行代码
synchronized和volatile有序性的区别
synchronized的有序性是持有相同锁的两个同步块只能串行的进入,即被加锁的内容要按照顺序被多个线程执行,但是其内部的同步代码还是会发生重排序,使块与块之间有序可见。
volatile的有序性是通过插入内存屏障来保证指令按照顺序执行。不会存在后面的指令跑到前面的指令之前来执行。是保证编译器优化的时候不会让指令乱序。
通俗讲:synchronized保证了同步代码的有序性,volatile保证了指令的有序性
synchronized 是不能保证指令重排的。
synchronized可以保证原子性和可见性
concurrenthashmap
1.7采用的是sengment分段数组,默认为2,默认初始化16个并发级别,所以最大并发数是16,也可以指定,但是一旦指定之后就不可以改变
首先第一个线程进来的时候,先去获取对应的sengment分段数组,如果分段数组为null,则需要初始化分段数组,初始化对应hashEntry数组,产生线程竞争的时候采用cas进行竞争
1.8采用的是Node数组+链表+红黑树的结构
首先根据key计算出hashCode,
判断是否需要进行初始化
如果定位到的node为空则利用CAS写入,写入的时候判断是否需要进行扩容
如果位置不为空,判断是否正在扩容,正在扩容就帮助完成扩容,否则所有条件不满足使用synchronized锁写入数据,如果当前写入的链表数量大于阈值,则转化为红黑树
锁粗化:多个锁合并为一个锁
锁消除:通过JIT编译,经过逃逸分析,去除不可能存在锁竞争的锁
内存分配如何保证线程安全:
虚拟机采用两种方式来保证线程安全:CAS+失败重试、TLAB分配对象
一个对象的结构
MarkWord 8、ClassPoint类型指针4/8、实例数据、padding填充
一个【Mark word结构】对象头的结构(64位vm)
对于无锁状态
25位unused+31位hashcode+1unused+4分代年龄+1偏向锁+2锁标志位
对于偏向锁
54线程指针+2Epoch+1unused+4分代年龄+1偏向锁+2锁标志位
对于轻量级锁
62位指向线程栈中lockRecord的指针+2锁标志位
对于重量级锁
62位指向互斥量重量级锁monitor的指针+2锁标志位
注:分代年龄无论多少位的hotspot锁状态都是4
一个【Mark word结构】对象头的结构(32位vm)
无锁:25bit的hashCode+4bit的分代年龄+1bit值为0的偏向锁状态+2bit值为01锁标志位
偏向锁:23bit的线程id+2bitEpoch+4bit的分代年龄+1bit值为1的偏向锁状态+2bit值为01锁标志位
轻量级锁:30bit指向栈中锁记录的指针+2bit值为00锁标志位
重量级锁:30bit指向重量级锁记录的指针+2bit值为10锁标志位
自适应自旋锁:在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,jdk1.7之后后无法设置,默认就是自适应
Mark Word重量级锁没有分代年龄是因为它是一种基本的同步机制,不需要考虑对象的年龄和GC过程。它仅仅是一个二进制标志,用于控制并发访问共享资源的互斥性。因此,它不需要考虑对象的生命周期和垃圾回收的影响。
CompressOops压缩指针
OOP = “ordinary object pointer” 普通对象指针。 启用CompressOops后,会压缩的对象
为什么使用压缩指针
32位的对象引用(指针)占4个字节,而64位的对象引用占8个字节。也就是说,64位的对象引用大小是32位的2倍。64位JVM在支持更大堆的同时,由于对象引用变大却带来了性能问题,如增加了GC开销、降低CPU缓存命中率
CompressedOops的原理
32位内最多可以表示4GB(2^32),64位地址分为堆的基地址+偏移量,当堆内存<32GB时候,在压缩过程中,把偏移量/8后保存到32位地址。在解压再把32位地址放大8倍,所以启用CompressedOops的条件是堆内存要在4GB*8=32GB以内
一个简单的只有一个int属性的object对象的大小
在未开启UseCompressedOops压缩指针是(MarkWord8+对象指针8+int数据4+对齐填充4=24字节)
在开启UseCompressedOops压缩指针是(MarkWord8+对象指针4(被压缩了4)+int数据4=16字节)
一个简单的没有属性的object对象的大小
在未开启UseCompressedOops压缩指针是(MarkWord8+类型指针8+实例数据0+对齐填充0=16字节)
在开启UseCompressedOops压缩指针是(MarkWord8+类型指针4(被压缩了4)+实例数据0+对齐填充4=16字节)
哪些对象不会被压缩:
指向PermGen的Class对象指针,本地变量,堆栈元素,入参,返回值,NULL指针不会被压缩
常量折叠(只有编译器在程序编译期就可以确定值的常量)
String str3 = "str" + "ing" //这种情况会进行常量折叠
String a="str";
String b="ing";
String c=a+b; //不会进行常量折叠,会生成一个新的对象
注:尽量避免通过 new 的方式创建字符串,这样会生成一个新的对象
内存分配担保机制(简述当Eden分配不下,S区也无法进行分配的时候,当老年代空间足够就会把eden区对象分配到老年代)
当新分配的对象(对象未达到大对象阈值,因为到达阈值会直接进入老年代)在eden区分配不下时,且eden区中的对象无法分配到S0区时,判断eden区总现存对象大小是否大于老年代剩余可用空间,如果不大于先进行minorGc,否则进行fullGc(minorGC+majorGc),当执行完Gc操作新分配的
对象仍然无法在eden区分配时,且eden区现存对象小于老年代,则把eden中的对象放到老年代,此时将新对象放入老年代,如果仍然放不下,则抛出oom异常
无用的常量和无用类判断
无引用的常量为无用常量
无对象实例的类且被classLoader,对应的class对象被回收,再没有反射调用的类为无用的类,无用的类可以回收,但不是像对象一样必然回收
触发fullgc条件
堆老年代内存不足
方法区内存不足
通过Minor GC后进入老年代的平均大小大于老年代的可用内存
System.gc()方法的调用
concurrent mode failure(无法处理“浮动垃圾”出现,老年代的垃圾收集器从CMS退化为Serial Old,所有应用线程被暂停,停顿时间变长)
CMS优点:并发收集、低停顿
cms的缺点
对CPU资源敏感;
无法处理浮动垃圾;
它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生
CMS:
初始标记:STW、耗时短、仅标记GCRoot能直接关联到的对象
并发标记:不会STW,耗时长、从初始标记中的对象遍历整个对象图
重新标记:重新标记、耗时短、修正并发标记期间用户线程导致的标记变动记录,相对初始标记要长
并发清理:不会STW,产生浮动垃圾,并发失败,启用Serial old整理内存,停顿时间更长
标记流程如下:
刚开始,所有的对象都是白色,没有被访问。
将GC Roots直接关联的对象置为灰色。
遍历灰色对象的所有引用,遍历后灰色对象本身置为黑色,引用置为灰色。
重复步骤3,直到没有灰色对象为止。
结束时,黑色对象存活,白色对象回收。
三色标记:
黑色:对象本身及其所有的引用都被扫描过,黑色不能直接指向白色,不能被回收
灰色:对象本身被扫描,还存在至少一个引用没有被扫描
白色:没有被GC扫描,扫描结束后白色的可以被回收
总结:黑色对象存活、白色对象回收
漏标和错标:通过写屏障技术将用户线程引用关系记录下来,在重新标记的时候修正对象引用
漏标:漏标的意思就是存活的对象没有被标记,例子: A(黑)-->B(灰)---C(白),经过并发标记之后,A-->C,B-->null,此时C重新被C引用了,但是黑色是被扫描过的,在重新标记时不会被扫描
错标:错标指的是死掉的对象被标记了,例子:A(黑)-->B(灰)---C(白),GC线程已经遍历到B,此时刚好引用发生改变,A-->C,B-->null
下次处理场景:
漏标:该次重新标记处理
错标:下次垃圾回收处理
漏标通过写屏障解决
错标通过写屏障和增量更新解决
写屏障:类似于AOP加属性处理
增量更新:黑色对象增加了对白色对象的引用之后,将它的这个引用,记录下来,在最后标记的时候,再以这个黑色对象为根,对它的引用进行重新扫描
G1特点:
并行与并发、分代收集、空间整合、可预测的停顿时间(维护一个region优先列表,在可预测的停顿时间内回收n个region区域)
ldy jvm参数设置:
-XX:+UseParNewGC 新生代使用Parallel收集器
-XX:+UseConcMarkSweepGC 老年代回收期
XX:+CMSParallelRemarkEnabled 启用并行标记,降低标记停顿
-XX:NewRatio=3 新生代和老年代占比 1:3
-XX:CMSInitiatingOccupancyFraction=60 当老年代达到60%时,触发CMS垃圾回收
-XX:MaxTenuringThreshold=8 新生代GC晋升年龄
-XX:SoftRefLRUPolicyMSPerMB=1 表示每一MB的空闲内存空间可以允许软引用对象存活多久
-XX:+PrintGCDetails 输出GC详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+HeapDumpOnOutOfMemoryError 表示当JVM发生OOM时,自动生成DUMP文件
-XX:HeapDumpPath=xxx 表示生成DUMP文件的路径,也可以指定文件名称,如果不指定文件名
-Xloggc:xxx 指定GC日志文件的输出路径
-XX:ErrorFile=xxx 当由于目标应用程序中的错误而发生崩溃时
-Xms4G 初始化堆内存大小
-Xmx4G 堆内存最大值
-XX:MetaspaceSize=256M MetaspaceSize容量触发FGC的阈值,超过设定阈值后MetaspaceSize每扩容一次触发一次FGC
-XX:MaxMetaspaceSize=512M 用于设置metaspace区域的最大值,如果这个参数没有设置,那么就是通过mxbean拿到的最大值是-1,表示无穷大
-XX:NewSize=2G 新生代初始内存的大小 ,同时指定了NewRatio和NewSize,MaxNewSize,会分配NewRatio的比例给新生代
-XX:MaxNewSize=2G 新生代可被分配的内存的最大上限
-Drocketmq.client.logRoot=xxx 启动路径
JVM调优:
一般调优JVM可以参考:『吞吐量』、『停顿时间』和『垃圾回收频率』
1. 内存区域大小以及相关策略
2.IO密集型的可以稍微把「年轻代」空间加大些,因为大多数对象都是在年轻代就会灭亡。内存计算密集型的可以稍微把「老年代」空间加大些,对象存活时间会更长些
3.选择合适的垃圾回收器,以及各个垃圾回收器的各种调优参数
4.通过诊断命令和工具监控(jps、jstat、jmap)
5.最大堆和最小堆的大小一致,避免出现内存重新申请(malloc)
CPU 缓存模型(CUP--CPU缓存--主存RAM)
CPU Cache 缓存的是内存数据用于解决CPU处理速度和内存不匹配的问题,内存缓存的是硬盘数据用于解决硬盘访问速度过慢的问题。CPU 解决内存缓存不一致性问题可以通过“总线加锁”(已淘汰)或制定缓存一致协议或者其他手段来解决
CPU多级缓存一致性协议MESI
说白点:通过改变M、E、S、I不同状态解决
1. 如果是读操作,不做任何处理,只是从缓存中读取数据到寄存器
2. 如果是写操作,发出信号通知其它CPU将该变量的cache line置为无效状态,其它CPU在运行该变量读取的时候需要从主存更新数据。
JVM内存模型和java内存模型不是一个概念
jvm内存模型是用作内存管理,jmm内存模型抽象了线程和主内存之间的关系、主要目的是为了屏蔽系统和硬件的差异,避免一套代码在不同的平台下产生的效果不一致
JMM内存划分:主内存、工作内存,主内存对应jvm内存模型中的堆,是物理内存,工作内存对应的栈中的部分区域,是寄存器和高速缓存
JMM内存模型(线程--本地内存[寄存器]--主存)
定义了JVM如何与计算机的主存进行工作,JMM中,线程的工作内存(或叫做本地内存)指的就是 CPU Cache,所以有缓存一致性的问题,线程可以把变量保存本地内存(比如机器的寄存器)中,而不是直接在主存中进行读写
JMM缓存一致性如何解决:
把变量声明为volatile,这就指示 JVM,这个变量是共享且不稳定的,每次使用它都到主存中进行读取
关于volatile无法保证原子性问题:
如下代码:
public class UnsafeCounter {
private volatile long count;
private final long MAX = 10000;
public void counter() {
long start = 0;
while (start++ < MAX) {
count++;
}
}
public long getCount() {
return count;
}
}
两个线程在执行counter()的时候,count++会分成三步执行,读取--计算--赋值,线程1读取count=0,切换到线程2,count=0,计算count=1,赋值回主存,count=1,再切换回线程1,计算count=1,赋值回主存,此时,getCount的时候会发现count=1,而不是2
Java并发编程安全需要具备的三大特性:原子性、可见性和有序性。
jvm执行一个方法的流程
创建栈帧
保存main方法的程序计数器
修改线程的局部变量表开始指针指向待执行的方法
修改线程的操作数栈开始指针指向待执行方法
方法执行完后,修改线程的操作数栈开始指针指向虚拟机栈前一个方法,如果没有就是main方法
修改线程的局部变量表开始指针指向虚拟机栈前一个方法,如果没有就是main方法
恢复main方法的程序计数器
判断是否有返回值,有,就压入main方法的操作数栈
释放栈帧
java栈中什么是动态链接,动态链接的作用:
动态链接就是一个指向运行时常量池中栈帧所属方法的引用,作用为了将这些符号引用转换为调用方法的直接引用
什么是安全点safepoint
(安全点是指线程被暂停时,被暂停的这行字节码不会导致引用关系发生变化)安全点是jvm选来进行GC的线程中断点。线程在执行到安全点后询问GC标志位,若标志位标识将要进行GC,则程序主动中断(非抢占式,抢占式中断需要低优先级进程把cpu资源给到高优先级)挂起线程等待GC。
(通俗讲:jvm gc线程中断的时候,需要找到一个安全的点进行线程中断,询问GC标志位是否可以进行GC,要不然可能会导致引用关系改变)
安全点的选定基本上是根据"是否具有让程序长时间执行的特征"为标准进行选定的。目前会产生安全点的主要有:1.方法调用、2.循环跳转、3.异常跳转。
什么是安全区域(可以看做安全点的拉伸)
安全区域指,如果用户线程在GC线程STW主动中断前进入了sleep或者block状态,没有运行也就进入不了安全点。那么GC就无法开始STW,因为引用关系有可能发生变化。所以要引入安全区域,指能够确保在某一段代码片段中,
引用关系都是不会发生变化的。在这个区域中任意地方进行垃圾回收都是安全的。可以看做安全点的拉伸。线程要离开安全区域,要看GC是否完成了STW,没有就要等待STW完成才能离开安全区域
方法递归调用层数过多会可能会栈溢出,怎么优化?
可以用循环替代递归。减少方法的压栈
目前会产生安全点的主要有:1.方法调用、2.循环跳转、3.异常跳转。
mysql innodb和myIsam区别
myIsam不支持事务
myIsam不支持外键
myIsam不支持行级锁
myIsam不支持奔溃后恢复
myIsam不支持mvcc
mysql锁:
表锁、行锁、间隙锁gap lock(RC隔离级别不存在间隙锁)、临键锁next-key、MDL写锁(X锁)、MDL读锁(S锁)
分类:共享锁(S)、排他锁(X)
共享锁:读锁
select *.....lock in share mode;
一个事务开启了共享锁,其他事务只能进行读,不能进行CUD
排他锁:写锁
select *.....for update; / update xxxx;
当前事务开启了排他锁,其他事务都不能获取到锁
意向共享锁(IS):
在获取共享锁之前需要获取意向共享锁,数据库用来自动优化共享锁的一个锁
意向排他锁(IX):
在获取排他锁之前需要获取意向排他锁,数据库用来自动优化排他锁的一个锁
自增锁:
自增主键的一个锁,对于没有提交的记录,id已经生成了,那么就算事务回滚,id就会丢失掉当前生成的id序列,
如:插入id前id序列为1,2,3,4,此时插入5 ,6,如果5,6回滚了,此时继续自增会直接跳过5,6,比如来个7,那么现在序列就是1,2,3,4,7
记录锁:
行锁
间隙锁gap lock(RC隔离级别不存在间隙锁,对于更新一个间隙的操作,rc隔离级别只会存在行锁锁住区间中的每一行)
对一部分加锁,锁定一个范围,但不包含记录本身,一个间隙,(> < between),
间隙锁锁定范围分析:
对于聚簇索引:
对于指定查询某一条记录的加锁语句,如果该记录不存在,会产生记录锁和间隙锁,如果记录存在,则只会产生记录锁,如:WHERE `id` = 5 FOR UPDATE;
对于查找某一范围内的查询语句,会产生间隙锁,如:WHERE `id` BETWEEN 5 AND 7 FOR UPDATE;
例子:主键记录有1,5,7,11, 产生的间隙是:(-infinity, 1]、(1, 5]、(5, 7]、(7, 11]、(11, +infinity],between<5,7>,则锁定的是(5, 11] 注意:mysql版本是5.6和5.7是锁定(5, 11],如果是高版本8.0+,只会锁定(5, 7]
对于非聚簇索引:
在普通索引列上,不管是何种查询,只要加锁,都会产生间隙锁,这跟唯一索引不一样;
在普通索引跟唯一索引中,数据间隙的分析,数据行是优先根据普通索引排序,再根据唯一索引排序
例子,数据结构为
临键锁next-key
行锁+临键 (> < between) 锁定一个范围,并且锁定记录本身,左开,右闭 ,如果没有命中锁定记录,降级为间隙锁
锁与隔离级别关系:
幻读:临键锁解决
不可重复读:S锁解决
脏读:X锁解决
行锁原理:
行锁锁的是索引上的索引项加锁,故而在RR隔离级别下,条件列未命中索引会锁表!而在RC隔离级别下,只锁行,通俗讲,如果你没有索引或没命中索引,那么就锁表了
mysql四个特性及解决实现原理
原子性(Atomicity):利用undolog进行数据回滚
一致性(Consistency):通过原子性、隔离性、持久性共同保证一致性,不同库的数据一致性还需要用到binlog保证
隔离性(Isolation):锁机制、MVCC 等手段来保证事务的隔离性,SQL标准隔离级别有:ru、rc、rr、序列化,mysql默认使用rr
持久性(Durability):利用redolog将数据刷盘到磁盘
并发事务出现的问题(不指定某种数据库,可以是mysql、oracle)
脏读:当前事务看到其他事务未提交的事务,事务中总是能查到别人事务未提交的数据
不可重复读:发生在update操作上,事务中两次查询结果不一致
幻读:发生在insert和delete操作上,事务中两次查询结果不一致,比如select count(1) from table,事务1 第一次查询为5条,第二次查询和第一次查询中间事务2插入了1条数据,第二次查询得到6条,则为幻读
mysql存储方式,以数据页方式存放:SHOW TABLE STATUS
InnoDB 储存引擎支持有四种行储存格式:COMPACT、Redundant、Dynamic 和 COMPRESSED。默认为COMPACT
mysql在rr隔离级别下如何解决幻读:
在快照读读情况下,mysql通过mvcc来避免幻读。
在当前读读情况下,mysql通过next-key加锁来避免幻读
mysql分布式事务,必须使用SERIALIZABLE隔离级别
原因是因为innodb支持XA事务,而分布式事务有是多个独立的事务参与到一个全局事务中,要么所有事务都执行,要么都不执行
现实中有没有遇到锁未释放的情况
订单模块在结算欠款的时候需要更新收款记录和更新欠款表,订单有个逻辑,写了个死循环,导致长事务一直未释放,有个跑批需要更新收款记录信息,导致了该条记录一直未获取到锁,此时出现的是行级锁
红黑树特点:
1.每个节点只能是红色或者黑色。
2.根节点必须是黑色。
3.红色的节点,它的叶节点只能是黑色。
4.每条路径都有相同的黑色节点。
5.叶节点统一是nul节点
红黑树变色逻辑:
1.新插入节点一定是红色
2.当插入节点的父节点是红色,且它的叔叔节点是红色时,把父节点和叔叔节点改为黑色,爷爷节点改为红色
红黑树左旋逻辑:
父节点为红色,且叔叔节点为黑色,且当前节点在右子树上,则向左旋转
红黑树右旋逻辑:
父节点为红色,且叔叔节点为黑色,且当前节点在左子树上,则向右旋转
mysql索引不使用hash结构的原因:不支持顺序和范围查找
mysql:
聚簇索引(innodb主键索引):索引文件和数据文件是一起的,非叶子节点存放索引,叶子节点存放数据,使用辅助索引的时候先找到主键的key,通过主键的key再走一遍主索引
查询速度快
更新代价大
依赖于有序数据
非聚簇索引(mylsam,innodb的二级索引【普通索引、唯一索引、前缀索引、全文索引(innodb在mysql5.6才开始支持全文索引)】):索引文件和数据文件分开,叶子节点可能存放指针,也可能存放主键,所以缺点就是需要二次回表或者通过指针二次查询
更新代价比聚集索引要小
依赖于有序的数据
可能会二次查询(回表)
索引类别:
唯一索引、普通索引、前缀索引、全文索引
非聚簇索引一定回表查询吗,不一定,比如
SELECT id FROM table WHERE id=1;
SELECT name FROM table WHERE name='guang19';
数据库完整性:
实体完整性:主键不能取空值
参照完整性:外键
域完整性/用户定义的完整性:字段属性的约束,包括字段的值域、字段的类型和字段的有效规则(如小数位数)等约束
b树和b+树的区别:
b树(也叫b-树)所有节点都存放key和数据,b+树除了叶子节点其他节点都只存放key值,不存放数据,叶子节点存放所有的数据,b+树的叶子节点有一条链表
mysql日志:错误日志、查询日志、慢查询日志、事务日志、二进制日志
二进制日志:binlog(归档,server层)、redolog(重做,用于服务器崩溃恢复,存储引擎层)、undolog(事务日志,用于数据回滚)
redolog刷盘机制(调用fsync对redolog进行日志保存):0提交到redo log buffer,不刷盘 1提交立即刷盘 2.提交到redo log buffer内容并写入 page cache
redolog刷盘条件:
1.1秒轮询到达时刷盘
2.redo log buffer到达一半时进行刷盘
redolog为什么需要先存入page cache
因为数据页大小是16KB,刷盘比较耗时,这时候如果打包刷盘肯定是更好的
binlog格式:row、mixed、statement,使用canal时候需设置为row,因为row的sql语句的时间是原始的时间,statement时间为now()
mysql保证数据已执行的2阶段提交流程:先记录redolog为预提交,然后提交是先提交binlog日志,再提交redolog日志,redolog日志提交先判断是否为commit阶段,如果不是在判断是否有binlog日志,如果有也进行commit
mvcc:俗称多版本并发控制,为了解决数据库事务并发访问出现的读写冲突问题,做到了不加锁并发读,实现原理为使用【隐藏字段】读取上一个【Read View】判断数据可见性,找到对应的【undo log】知道找到可见的记录
Read View作用(针对当前读,维护的是一个活跃列表):
read view 主要是用来做可见性判断的,事务进行快照读操作的时候生成的读视图,它会判断每条记录的的数据,这条数据可能是真实的数据,也可能是undolog中的数据
(主要是将要被修改的数据的最新记录中的 DB_TRX_ID(即当前事务 ID )取出来,与系统当前其他活跃事务的 ID 去对比(由 Read View 维护),如果 DB_TRX_ID 跟 Read View
的属性做了某些比较,不符合可见性,那就通过 DB_ROLL_PTR 回滚指针去取出 Undo Log 中的 DB_TRX_ID 再比较,即遍历链表的 DB_TRX_ID(从链首到链尾,即从最近的一次修改查起),
直到找到满足特定条件的 DB_TRX_ID , 那么这个 DB_TRX_ID 所在的旧记录就是当前事务能看见的最新老版本)
结构如下class ReadView {
private:
trx_id_t m_low_limit_id; /* 大于等于这个 ID 的事务均不可见 */ maxid
trx_id_t m_up_limit_id; /* 小于这个 ID 的事务均可见 */ minid
trx_id_t m_creator_trx_id; /* 创建该 Read View 的事务ID */
trx_id_t m_low_limit_no; /* 事务 Number, 小于该 Number 的 Undo Logs 均可以被 Purge */
ids_t m_ids; /* 创建 Read View 时的活跃事务列表 */ 在m_low_limit_id和m_up_limit_id的非活跃线程均可以被 Purge线程删除
m_closed; /* 标记 Read View 是否 close */
}
什么时候产生事务id什么时候产生ReadView:
CUD操作才产生事务Id,S查询操作前才产生ReadView(RR级别在事务中只产生一次,RC每次select都会产生)
简述ReadView源码及判断可见性流程
判断id小于m_up_limit_id(minid),如为true
否则判断id大于等于m_low_limit_id(maxid),如为false
否则判断id是否在活跃列表m_ids,如为,则false,否则true
MVCC和undolog及ReadView之间的关系梳理
MVCC主要为解决读写冲突,依赖于undolog和ReadView,undolog主要用于事务回滚及当前读的情况下获取最新数据,readView视图主要作用维护了一个活跃事务列表,为在事务里在进行
select读取操作时产生一个判断事务可见性的功能从而通过事务Id读取到该读的数据
undolog
作用:提供事务回滚、提供mvcc找到并读取上一版本
分为:insert undolog和update undolog,在修改和删除时记录提交不可以直接删除undolog,因为还需要提供mvcc支持,需等待purge线程调用删除,
insert操作的记录只对自己事务可见,其它事务是不可见的,所以insert undo log可以在事务提交后直接删除而不需要进行purge操作
RC 和 RR 隔离级别下 MVCC 的差异
在 RC 隔离级别下的 每次select 查询前都生成一个Read View (m_ids 列表)
在 RR 隔离级别下只在事务开始后 第一次select 数据前生成一个Read View(m_ids 列表)
联动云采用RC级别的考量:
1.RR隔离级别下,存在间锁锁,导致出现死锁的几率比RC大的多
例子:select * from test where id <3 for update;在RR隔离级别下,存在间隙锁,可以锁住(2,5)这个间隙,防止其他事务插入数据!而在RC隔离级别下,不存在间隙锁,其他事务是可以插入数据
2.在RR隔离级别下,条件列未命中索引或条件列非索引列会锁表!而在RC隔离级别下,只锁行
例子:number列为非索引列
事务1:UPDATE test.test123 SET rule_no_version_old='1' WHERE number>8;
事务2:UPDATE test.test123 SET rule_no_version_old='2' WHERE number=3;
在RR隔离级别下,走非聚簇索引,进行全部扫描,事务2会被锁住,即将整个表锁上,RC隔离级别下,其先走聚簇索引,进行全部扫描,对对应的行加行锁
3.在RC隔离级别下,半一致性读(semi-consistent)特性增加了update操作的并发性
半一致性读:一个update语句,如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的版本,由MySQL上层判断此版本是否满足update的where条件。若满足(需要更新),
则MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁),说白点:就是在update匹配到的那一行数据已经上锁了,此时需要读取当前数据
4.RC隔离级别不支持 statement 格式的bin log,因为该格式的复制,会导致主从数据的不一致
MVCC在RR隔离级别下不能解决幻读的场景例子:
begin;
#假设users表为空,下面查出来的数据为空
select * from users; #没有加锁
注:#此时另一个事务提交了,且插入了一条id=1的数据
select * from users; #读快照,查出来的数据为空
update users set name='mysql' where id=1;#update是当前读,所以更新成功,并生成一个更新的快照
select * from users; #读快照,查出来id为1的一条记录,因为MVCC可以查到当前事务生成的快照
commit;
结论:所以单纯靠MVCC在RR隔离级别下是不能解决幻读问题的,只能配合next-key间隙锁显示进行加锁控制才能解决幻读问题
mysql Server层数据结构:
连接器、查询缓存、分析器、优化器、执行器
sql语句的执行过程
查询语句的执行流程如下:权限校验(如果命中缓存)--->查询缓存--->分析器--->优化器--->权限校验--->执行器--->引擎
更新语句执行流程如下:分析器---->权限校验---->执行器--->引擎---redo log(prepare 状态)--->binlog--->redo log(commit状态)
B-树特点:
1.所有键值分布在整颗树中(索引值和具体data都在每个节点里);
2.任何一个关键字出现且只出现在一个结点中;
3.每个节点可以存放多个key,节点中既存放key,也存放值
4.搜索有可能在非叶子结点结束(最好情况O(1)就能找到数据);
5.在关键字全集内做一次查找,性能逼近二分查找;
B+树与B-树的区别:
1.B+树内节点不存储数据,所有 data 存储在叶节点导致查询时间复杂度固定为 log(n)。而B-树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1)。
2.B+树叶节点两两相连可大大增加区间访问性,可使用在范围查询等,而B-树每个节点 key 和 data 在一起,则无法区间查找。
为什么使用b+树而不是红黑二叉树:
红黑二叉树高度高,进行的io次数就会更多,而b+树因为每个节点可以存储多个key,且每个节点都有多个字节点,树的高度不高,io次数都为logn,b+树可以用于做范围查找
hashMap为什么使用红黑二叉树不使用b+树
b+树一般用于数据存在磁盘,红黑树一般用于数据存在内存,且b+树插入要维护链表、排序、索引,b+树插入速度更慢一些,但是查找相应比红黑树要方便
mysql索引一个节点可以存多少key:mysql一个B+树的节点是一页,一页16k(可以设置),对于非聚簇索引来说叶节点可以存放指针,对于聚簇索引叶子节点存放的是数据,如果聚簇索引key为一个bigint为8字节,指针在InnoDB中为6字节,一共14字节,那么第二层中一页可
存储16*1024/14=1170,一个节点1170,假设一条记录为1k,那么第三层的一个叶子节点可以存放16条记录,那么第三层就有1170*1170*16=21902400,所以总得来说是根据记录大小和存储器存储能力决定
mysql排序:
当对sql进行order by排序的时候,需要尽可能的使用索引排序,如果无法使用索引排序的话,mysql就会使用文件排序
文件排序出现的几种情况:
order by 字段不是索引字段
order by 字段是索引字段,但是 select 中没有使用覆盖索引,如:select * from staffs order by age asc;
order by 中同时存在 ASC 升序排序和 DESC 降序排序,如:select a, b from staffs order by a desc, b asc;
order by 多个字段排序时,不是按照索引顺序进行 order by,即不是按照最左前缀法则,如:select a, b from staffs order by b asc, a asc;
文件排序分为:
双路排序(又叫回表排序模式):先根据相应的条件取出相应的排序字段和可以直接定位行 数据的行 ID,然后在 sort buffer 中进行排序,排序完后需要再次取回其它需要的字段
单路排序:是一次性取出满足条件行的所有字段,然后在sort buffer中进行排序
双路排序和单路排序的区别:1.放入到排序缓存(sort_buff)中一个是放入全部数据,一个是放入排序字段数据进行排序 2.双路排序会根据id回表查询数据 3.单路排序使用更多的空间
mysql如何判断使用双路排序还是单路排序:
查询字段是否大于max_length_for_sort_data,大于走双路,小于走单路
mysql字符集
ASCII
Mysql主从同步的实现原理:
主节点:
1.master先保存binlog
2.当slave连接到master的时候,master机器会为slave开启binlog dump线程。当master 的 binlog发生变化的时候,binlog dump线程会通知slave,并将相应的binlog内容发送给slave
从节点:
当主从同步开启的时候,slave上会创建2个线程。
I/O线程。该线程连接到master机器,master机器上的binlog dump线程会将binlog的内容发送给该I/O线程。该I/O线程接收到binlog内容后,再将内容写入到本地的relay log。
SQL线程。该线程读取I/O线程写入的relay log。并且根据relay log的内容对slave数据库做相应的操作。
InnoDB 存储引擎的文件格式是.ibd 文件,数据会按照表空间(tablespace)进行存储,分为共享表空间和独立表空间
共享表空间:多个数据表共用一个表空间,同时表空间也会自动分成多个文件存放到磁盘上
独立表空间:每张表都相互独立,不会影响到其他数据表
binlog没有记录如何做数据同步
1.如果我们之前没有做过全量备份,也没有开启 Binlog,那么我们还可以通过.ibd 文件进行数据恢复,采用独立表空间的方式可以很方便地对数据库进行迁移和分析。如果我们误删除(DELETE)某个数据表或者某些数据行,
也可以采用第三方工具 Percona Data Recovery Tool for InnoDB找回数据
2.如果已经开启了 Binlog,就可以使用闪回工具,比如 mysqlbinlog 或者 binlog2sql
.ibd文件损坏了,数据如何找回
设置innodb_force_recovery参数进行强制恢复,数据备份完了之后,关闭innodb_force_recovery,并重启数据库
如何强制恢复:
innodb_force_recovery参数一共有 7 种状态,除了默认的 0 以外,还可以为 1-6 的取值,分别代表不同的强制恢复措施,一般设置成1,可以读取数据表,并且对当前损坏的数据表进行分析和备份
大表结构修改操作:(大表操作产生的mdl写锁失败如何解决)
1.安装percona-toolkit插件。使用pt-online-schema-change命令。原理:建立一张新表,在新表中做表结构的修改,然后把旧表的数据copy到新表
2.在晚上低峰时进行操作
3.建立分表,或者做冷热处理,使单表数据量减少
聚集索引的缺点:
依赖于有序的数据 :因为 B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。
更新代价大 : 如果对索引列的数据被修改时,那么对应的索引也将会被修改,而且况聚集索引的叶子节点还存放着数据,修改代价肯定是较大的,所以对于主键索引来说,主键一般都是不可被修改的。
redis缓存策略
1.旁路缓存模式(订单主信息缓存都是使用该模式)
写 :
先更新DB
然后直接删除cache。
读 :
从 cache 中读取数据,读取到就直接返回
cache中读取不到的话,就从 DB 中读取数据返回
再把数据放到 cache 中
2.读写穿透(配置信息都是使用该类缓存模式)
写(Write Through):
先查 cache,cache 中不存在,直接更新 DB。
cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB(同步更新 cache 和 DB)
读(Read Through):
从 cache 中读取数据,读取到就直接返回 。
读取不到的话,先从 DB 加载,写入到 cache 后返回响应。
3.异步缓存写入
写: 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB
读: 同步更新 cache 和 DB
redis:redis在io模型中属于多路复用模型
redis不使用多线程的原因
1.单线程编程容易并且更容易维护;
2.Redis 的性能瓶颈不在CPU,主要在内存和网络;
3.多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。
redis支持多线程吗:redis6.0支持多线程,提高网络IO读写性能
过期的数据的删除策略:惰性删除 、定期删除
redis淘汰策略保持热点数据:lru(删除最近最少使用)、ttl(删除即将到期的数据)、random(随机删除)、lfu(以次数作为计量,删除最少被使用的)
no-eviction
当内存不足以容纳新写入数据时,新写入操作会报错,无法写入新数据,一般不采用。
allkeys-lru
当内存不足以容纳新写入数据时,移除最近最少使用的key,这个是最常用的
allkeys-random
当内存不足以容纳新写入的数据时,随机移除key
allkeys-lfu
当内存不足以容纳新写入数据时,移除最不经常(最少)使用的key
volatile-lru
当内存不足以容纳新写入数据时,在设置了过期时间的key中,移除最近最少使用的key。
volatile-random
内存不足以容纳新写入数据时,在设置了过期时间的key中,随机移除某个key 。
volatile-lfu
当内存不足以容纳新写入数据时,在设置了过期时间的key中,移除最不经常(最少)使用的key
volatile-ttl
当内存不足以容纳新写入数据时,在设置了过期时间的key中,优先移除过期时间最早(剩余存活时间最短)的key。
LRU实现原理:
1. 新数据插入到链表头部;
2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3. 当链表满的时候,将链表尾部的数据丢弃。
redis如何持久化:RDB快照副本二进制文件、AOF文件追加写操作(前者将当前数据保存到硬盘,后者则是将每次执行的写命令保存到硬盘)
redis的RDB、AOF共存下恢复流程:先判断AOF是否存在,存在直接加载AOF,不存在判断是否存在RDB,存在加载RDB
RDB、AOF优缺点
RDB:全量数据快照,文件小,恢复快,但无法保存最近一次快照之后的数据
AOF:可读性高,适合报错增量数据,数据不易丢失,但文件体积大,恢复时间长
RDB-AOF混合持久化方式;(RDB做镜像持久化,AOF增量持久化)
RDB中使用bgsave保存全量的数据,在保存的过程中可能会有增量的数据进来,这个时候AOF就进行记录保存,然后全量+增量 这样就快速完美的保存了所有的数据。
redis什么情况下会进行全量同步
1.第一次设置slave服务器并建立同步连接时,会发送sync命令,master启动一个后台进程,将数据快照保存到文件并同步给slave
2.slave断开连接重连的时候
3.在设置的周期同步时调用sync命令给master,进行全量同步
redis什么情况下进行增量同步
1.在进行完全量同步时,后续的增量数据进行增量同步即可
总结:主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步
redis数据类型及原理:
String:SDS动态字符串,属于二进制安全字符串,意思是redis可以包含任何数据,不会特殊处理任意一个字符,包括特殊字符(C语言中字符串是以特殊字符“\0”来作为字符串的结束标识)
int:使用整形值实现的字符串对象 存储数值类型
raw动态字符串实现的字符串对象 大于44个字节
embstr使用embstr编码的动态字符串实现(SDS数据结构) 小于44个字节
List:按元素插入顺序排列,quicklist快速链表(双向链表,由链表和ziplist结合,即按链表的节点分,每个节点使用压缩链表存储)
quicklist:ziplist+双向链表的结合,双向链表内存不连续,容易产生碎片,内存占用大,需要存储前驱和后继节点
ziplist:一整块连续的内存,存储效率高,不适用于大数据量的存储
Hash:ziplist(存储比较少的字符和数据,节点紧凑存储,内存空间分配是连续的)和hashtable
zipList:条件(1;哈希对象的键和值的字符串长度都小于64字节,2.保存的数量小于512)
hashtable:dict的数据结构
为什么使用hashtable【数组】而不用map【红黑树】:hashtable存储和搜索速度比map快
Hashtable的扩容:通过rehash,当数量大于length*0.75时进行扩容,扩容为length*2+1
Set:无序的不允许重复的数据结构
inset:可以理解为整数数组,只是这个数据结构是独立的struct
1.数据元素都是整形
2.元素个数小于set-max-inset-entries,默认512
hashtable:dict数据结构
ZSet:ziplist、(skiplist+dict),有序、不允许重复,ziplist转换成skiplist之后不可逆,
ziplist:
有序集合保存的元素数量小于默认值128个
有序集合保存的所有元素的长度小于默认值64字节
skiplist+dict:
为什么用跳表不用红黑树:跳表范围查找更快、跳表容易实现,红黑树需要通过旋转保持平衡
dict数据结构:其实就是有索引信息的hashtable
skipList:跳表,通过不同层级的进行类二分查找
dict和skipList的协同工作:两种数据结构通过指针来共享元素,所以不存在内存浪费,但是两个又都是独立的数据结构,只是共享了数据
为什么用了skipList还用dict:根据redis的需求而定,skiplist的优势在于范围查找,无需排序;dict的优势在于查找成员的分值时,时间复杂度为O(1),但是字典结构是hashtable,是无序的,如果类似范围查找则需要进行排序
什么场景下使用dict,什么场景下使用skipList:
redis的底层数据结构:
SDS动态字符串
链表
字典
跳跃表
整数集合
压缩列表
redis缓存雪崩、缓存穿透、缓存击穿
缓存击穿(单个key失效):是针对缓存中没有但数据库有的数据,当前key是一个热点key(例如一个秒杀活动),并发量非常大,在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
解决方案:分布式互斥锁(在查到缓存没有和即将查询db中间加上redis锁)、永不过期(需要配合淘汰策略)
缓存雪崩(多个key失效):是指大量Key同时失效,对这些Key的请求又会打到DB上,同样会导致数据库压力过大甚至挂掉。
解决方案:1.缓存高可用、2.采用多级缓存,本地进程作为一级缓存,redis作为二级缓存、3.尽量让不同的key的过期时间不同
缓存穿透(缓存和数据库都没有数据):是针对数据库和缓存中都没有的数据。
解决方案:缓存空对象、布隆过滤器
redis缓存一致性解决方案:(缓存的数据值≠数据库中的值;缓存或者数据库中存在旧值,导致其他线程读到旧数据)
读写请求串行化(数据强一致)
写请求和读请求前都需要获取到分布式锁
延时双删策略(最终一致性)
写请求在更新数据库的前后均删除一次缓存,第二次删除需要延迟一段时间
步骤1:删除缓存
步骤2:更新数据库
步骤3:延迟一定的时间
步骤4:再次删除缓存
步骤4再次删除的原因:可以删除可能在步骤1到2之间被读请求存入的脏数据。
步骤3延迟一定时间的原因:
(1)保证步骤1和2之间的读请求正常执行,让步骤4可以有效删除脏数据。
(2)如果数据库是主从模式,从库数据更新需要一定时间,防止读请求在没有缓存的情况下,读出从库中的旧数据放在缓存中
基于订阅binlog的同步机制(最终一致性)
MySQL产生新的更新操作(添加,删除等等),就可以把binlog相关消息推送(canal,kafka,rabbitMQ)至缓存服务(Redis),Redis根据binlog中的记录更新自身数据
canal同步es数据一致性保证
客户端ACK机制,当出现异常时,将异常捕获,记录下那次批处理失败,记录批处理失败的Id,当达到失败的阈值的时候进行失败回滚并发送告警邮件
redis持久化方式:
save。同步操作,会阻塞Redis。
描述 aof 处理增量数据
缺点
致命的问题,持久化的时候redis服务阻塞(准确的说会阻塞当前执行save命令的线程,但是redis是单线程的,所以整个服务会阻塞),不能继对外提供请求,
bgsave。调用linux的fork(),然后使用新的线程执行复制。但是fork期间也会阻塞Redis,但是阻塞时间通常很短。
原理:fork() + copyonwrite
描述 rdb 处理全量数据
优点
他可以一边进行持久化,一边对外提供读写服务
自动保存。Redis配置文件中设置了自动保存的触发机制,可以自定义修改,运行原理同bgsave
redis集群:
主从
高可用:可以实现读写分离,数据备份。但是并不是「高可用」的
哨兵:高可用
集群结构:n个(master+slave+sentinel)
哨兵模式引入了一个Sentinel系统去监视主服务器及其所属的所有从服务器。一旦发现有主服务器宕机后,会自动选举其中的一个从服务器升级为新主服务器以达到故障转移的目的
高可用:主从模式的「高可用」版本,其引入了Sentinel对整个Redis服务集群进行监控。但是由于只有一个主节点,因此仍然有写入瓶颈
哨兵功能的总结:
每个 Sentinel 可监视任意多个服务器,每个服务器也可被任意多个 Sentinel 监视;
多个监视同一主服务器的 Sentinel 视为一个集群,在被监视主服务器下线后,该集群将选举出一个 Sentinel Leader,由该 leader 对其进行故障转移
当有master挂壁了(
master是否挂了:先通过一个sentinel判断一个master是否在一个时间内响应主观下线,询问其他哨兵节点是否挂了,足够多的哨兵说挂了就进行客观下线,客观下线之后进行故障转移
故障转移:
选举 Sentinel leader,通过raft半数机制选举,用于执行故障转移,
由 Sentinel leader 从故障主服务器的所有从服务器中选一个做新的主服务器
向选出的新master 发送 slaveof no one命令将其变成 master
向其它slave服务器发送slaveof命令,让它们复制新的服务器
若下线的主服务器上线,则发送slaveof命令,让其降级为从服务器,复制新的主服务器
哨兵模式正常的读命令和写命令流程:
master服务可读写,slave服务为只读,当master服务接受到数据修改或写入的命令时,会异步将命令发送到slave上,以此保持master与slave上数据的一致性
具体流程:
redis的复制功能的实现,包括 同步(SYNC/PSYNC) 和 命令传播(command propagate) 两个操作:
在刚开启主从同步功能或者断线重连时,使用同步命令让slave的数据状态跟master保持一致
SYNC命令:redis2.8之前使用,slave向master发送sync命令,master会执行bgsave命令生成rdb文件,然后把rdb文件发送给slave,slave通过rdb恢复数据,达到与master一致的状态
PSYNC命令:因为SYNC命令只能执行全量同步,对于slave断线重连后只需要执行部分重同步就可以达到一致的情况,仍使用SYNC命令有很大的浪费,因为在【redis2.8】中,提供了PSYNC命令来取代SYNC命令
同步后,若master执行写命令,状态又将不一致,通过命令传播让slave执行同样的命令,使状态保持一致
cluster分片:目的内存的高拓展
集群结构:(1个master+n个slave)+(1个master+n个slave)......
高可用方面:提供了主从高可用,同时数据是分片保存在各个节点中的,可以支持高并发的写入与读取。当然实现也是其中最复杂的
去无心节点方式实现,集群将会通过分片方式保存数据库中的键值对
分片与槽位:
Redis Cluster的整个数据库将会被分为16384个哈希槽
Redis Cluster的高可用
Redis的每个节点都可以分为主节点与对应从节点。主节点负责处理槽,从节点负责复制某个主节点,并在主节点下线时,代替下线的主节点
cluster读写流程:
该槽归自己处理,进行处理;
该槽不归自己处理,返回一个MOVED错误,并把负责处理该槽的节点信息返回,引导客户端向正确的节点请求服务;
该槽数据目前正在迁移:
首先在自己的库中查找该键,若存在,处理;
若不存在,返回ASK错误,并给出新节点信息,引导客户端向新节点请求服务;客户端需要先向新节点发送asking命令,再发送正式的命令,否则将会收到一个moved错误
如何计算嘈的位置:
Redis 客户端通过 CRC16(key)%16383 计算出 Slot 的值
Redis集群中节点的通信机制:
goosip协议,节点之间通讯的目的是为了维护节点之间的元数据信息。这些元数据就是每个节点包含哪些数据,是否出现故障,通过gossip协议,达到最终数据的一致性
gossip协议常见的消息类型包含:
ping、pong、meet、fail等等
集群的故障检测
主观下线(pfail):当节点A检测到与节点B的通讯时间超过了cluster-node-timeout 的时候,就会更新本地节点状态,把节点B更新为主观下线。主观下线并不一定真正下线,需要判断客观下线
客观下线:当半数以上的主节点标记了节点B是主观下线时,便会触发客观下线的流程(该流程只针对主节点,如果是从节点就会忽略)
故障转移:
选出master:
故障节点下线后,如果是持有槽的主节点则需要在其从节点中找出一个替换它,从而保证高可用。此时下线主节点的所有从节点都担负着恢复义务,这些从节点会定时监测主节点是否进入客观下线状态,
如果是,则触发故障恢复流程。故障恢复也就是选举一个节点充当新的master,选举的过程是基于Raft协议选举方式来实现的
数据转移:
(1)被选中的从节点执行SLAVEOF NO ONE命令,使其成为新的主节点
(2)新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽全部指派给自己
(3)新的主节点对集群进行广播PONG消息,告知其他节点已经成为新的主节点
(4)新的主节点开始接收和处理槽相关的请求
备注:如果集群中某个节点的master和slave节点都宕机了,那么集群就会进入fail状态,因为集群的slot映射不完整。如果集群超过半数以上的master挂掉,无论是否有slave,集群都会进入fail状态。
面试题:
Jedis与Redisson对比有什么优缺点?
Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持;Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、
分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。Redisson支持一些java对象操作,如Bloom filter, BitSet, Set, SetMultimap等
Redis事务相关的命令有哪几个
MULTI、EXEC、DISCARD、WATCH
redis分区方案:
查询路由:客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点
代理分区:代理分区意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。
修改配置不重启Redis会实时生效吗?
针对运行实例,有许多配置选项可以通过 CONFIG SET 命令进行修改,而无需执行任何形式的重启。 从 Redis 2.2 开始,可以从 AOF 切换到 RDB 的快照持久性或其他方式而不需要重启 Redis。
redis应用场景:
做订单缓存:使用hash存储对象,使用String存储字符串
做分布式锁:setnx+expire+uuid解决服务器crash问题
做队列:消息重试机制,rocketmq消息发送失败放到redis队列中重复消费,rPush放入队列,lPop取出数据,如果失败继续push,总共重试次数为15次,一次取1000条,定时线程5分钟执行一次
一般来说lpop没有消息的时候需要sleep一下,订单这边并没有sleep,而是采用定时线程,如果不用定时线程,也可以用blpop,没有数据的时候线程阻塞知道数据到来,而如果需要生产一次消费多次,可以采用发布订阅模式,如果有数据丢失问题比如使用rocketmq存储
做自定义消息定时任务发送到Rocketmq(18个延时级别):使用zset数据类型,判断当前消息延迟级别是否落在rocketmq的延迟级别之上,没有调用addZset放到redis中,其中分数为通知时间与当天0点的毫秒差值,
采用定时任务rangeByScore获取获取消息,判断是否满足rocketmq的延迟级别,如果满足,直接放到rocketmq消息队列中并调用zrem删除元素
redis分布式锁出现的问题及解决方案:
非原子操作
场景:setNx和expire操作非原子性,如果setnx成功,expire设置失败,则key都是永久不失效,会导致内存不足
解决方案:set命令解决,set命令可以进行原子操作, jedis.set(lockKey, requestId, "NX", "PX", expireTime)
忘了释放锁
场景:未手动释放锁
解决方案:业务完成手动释放锁
释放了别人的锁
场景:不用服务之间或者多线程场景,线程A和线程B,A加锁成功并设置了超时时间,B等待A业务完成,此时A业务功能耗时很长,A的锁已到超时时间并自动释放了,此时B进来,B获取到了锁,此时A业务已经完成了进行释放锁,此时A释放的锁有可能就是B的锁
解决方案:
1.增加一个requestId,比如uuid,在加锁的时候放入到ThreadLocal中,在解锁的时候判断当前锁的uuid是不是自己的key,是则释放
2.使用redission看门狗对锁续期
3.lua脚本,查询锁是否存在和删除锁做原子操作
大量的失败请求
场景:秒杀场景、多线程文件目录创建
解决方案:自旋锁,为规定的时间内进行自旋操作,自旋到一定次数还未获取到锁则直接返回失败
锁重入问题
场景:递归过程中第一层获取到了锁,在第二层中获取锁失败异常
解决方案:使用可重入锁,比如使用redission的可重入锁
原理:锁没重入一次计数加1,释放锁的时候计数减1
锁竞争问题
场景:在读的场景比较多的情况下,锁的粒度大,导致的性能低下
解决方案:
1.采用读写锁,保证读写、写写并发操作上锁是互斥的,比如采用redssion锁,内部已经实现了读写锁的功能
2.锁分段,将一个大锁拆分成多个子锁(注:锁分段会造成业务系统复杂度增加,需要维护多个锁)
锁超时问题
场景:线程A业务功能在进行中,锁超时了
解决方案:
1.redission看门狗模型,使用守护线程定时任务TimerTask自动续期,设置一个总的超时时间,如果在总的超时时间还没有执行完就不再续期了
主从复制问题
场景:主节点负责写,从节点负责读,此时线程A在原来的主节点上进行加锁,加锁完成之后主节点挂了,没有把锁信息复制到对应的从节点,然后从节点升级为了主节点,那么从节点中没有锁的信息,线程B进来在新竞的主节点上加锁成功
解决方案:
1.使用redssion的RedissonRedLock锁,存储多个redis节点的信息,并对redis中所有节点(至少一半节点)进行加锁,如果所有节点(至少一半节点)加锁成功则成功,否则失败(这种情况会出现数据不一致的情况,如至少一半成功,
有可能其他节点会失败,对这么多节点加锁也有性能损耗),此方案只保证了AP,不保证CP
2.使用zookeeper的分布式锁,但会造成服务不可用,即在CAP定理中,zk只保证CP,不保证AP
总结:在大部分分布式锁的使用情况看,只使用一般的redis分布式锁就可以了,数据不一致可以采用最终一致性来解决,但是服务不可用对用户来说是最不好的体验
redis与pipeline(pipeline在命令行中没有,但是是支持pipeline的)
RTT概念:redis客户端执行一条命令分4个过程,发送命令-〉命令排队-〉命令执行-〉返回结果,这个过程称为Round trip time(简称RTT, 往返时间)
pipeline引入背景:mget mset有效节约了RTT,但大部分命令(如hgetall,并没有mhgetall)不支持批量操作,需要消耗N次RTT ,这个时候需要pipeline来解决这个问题
原理:队列,将一批命令打包到一个内部维护的queue里,然后建立socket与server交互,这时候是只会发送一次命令,也就是只会交互一次,然后queue内的命令都执行完后会一起返回结果,这样大大减少了通信的次数,自然降低了通信所耗费的时间
比如原来:n条命令(n次rtt)=n次网络时间+n次命令计算时间,现在一次pipeline(n条命令)=一次网络传输+n次命令计算时间
MongoDb组件:
Collections(相当于table)、Document(相当于record记录)、database
支持数据类型:可以为:null、boolean、String、Object、Array等
1.什么是MongoDB
MongoDB是一个文档数据库,提供好的性能,领先的非关系型数据库。采用BSON存储文档数据(BSON()是一种类json的一种二进制形式的存储格式,简称Binary JSON)
命名空间:
mongodb存储bson对象在丛集(collection)中.数据库名字和丛集名字以句点连结起来叫做命名空间(namespace)
索引:
索引的存储数据结构是B树,索引命名空间存储着对B树的根节点的指针。
为什么要在MongoDB中用"Code"数据类型
"Code"类型用于在文档中存储 JavaScript 代码。
MongoDB中使用"Object ID"数据类型
"ObjectID"数据类型用于存储文档id,有四部分组成:时间戳、客户端ID、客户进程ID、三个字节的增量计数
Srping
bean的作用域:singleton、prototype、request、session、global-session(其中,request、session和global-session这三个作用域只有在web开发中才会使用到。)
什么情况下使用prototype:
有实例变量的类
bean 的生命周期:实例化bean--填充属性--初始化bean
实例化一个bean-->设置对象属性(pupulateBean)-->检查aware相关接口-->调用前置beanPostProcessor处理-->检查是否实现初始化接口-->检查配置是否有init-method-->调用后置beanPostProcessor处理-->使用中-->检查是否实现DisposableBean接口-->调用destory方法
Spring事务:
分为编程式事务和声明式事务
事务属性分为
事务传播方式
TransactionDefinition.PROPAGATION_REQUIRED如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
TransactionDefinition.PROPAGATION_REQUIRES_NEW创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰
TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行,即大的包含小的,小的回滚不包含大的;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED
TransactionDefinition.PROPAGATION_MANDATORY如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
回滚规则:默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚,此时需要try..catch手动抛出异常,
Spring的中对SqlException的处理是先捕获并转换成TransientDataAccessException异常,如果TransientDataAccessException转换失败,还有兜底异常RecoverableDataAccessException,其中这两个异常都是继承自DataAccessException
隔离级别:与sql一样,ru、rc、rr、序列化(读取每行都进行加锁)
事务超时时间
是否只读:readonly属性一旦被设置后,数据库级别如果为提交读,那么同一个事务中,如果对两次结果集进行查询,中间间隔修改数据库,那么应该会是同一个结果集,相当于查询的时候采用的是重复读的隔离级别
错误的事务用法:
TransactionDefinition.PROPAGATION_SUPPORTS: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER: 以非事务方式运行,如果当前存在事务,则抛出异常。
什么情况下事务不会回滚:
1.访问权限问题,非public
2.方法用final修饰,spring事务基于AOP,方法被final修饰后不能被重写
3.方法内部使用
4.多线程调用,spring的事务是在一个连接中,而连接是存在ThreadLocal中,多线程的会就会出现问题
5.数据库本身就不支持事务,如Myasim
6.错误的传播特性
7.错误且自定义回滚规则
8.自己吞了异常或者手动抛了其他的错误回滚规则异常
事务注解原理:
@Transactional 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。
面试问题Synchronized和@Transactional同时使用,有什么问题?
@Transactional的事物是Spring的AOP开启的。进入这个方法前,AOP会先开启事物,然后进入方法,此时会加锁,当方法结束后,锁释放,然后才会提交事物。如果在释放锁和提交事物之间有其他线程请求,然后该线程继续加锁,这种情况会导致程序不安全。
所以应该在开启事物前加锁或者在事务中只锁定某一个需要锁定的代码块
例子:
@Transactional(rollbackFor=Exception.class)
public synchronized String demo(String req)throws Exception {
System.out.println("demo.......");
}
通俗讲:流程本身是这样,开启事务--执行同步代码块--同步代码块执行完毕--提交事务,即在同步代码块完毕释放锁到提交事务这中间的过程会导致线程不安全
Spring Boot 自动装配原理:spi
Spring ioc流程
通过XmlBeanFactroy进行XML配置的验证,获取Document,注册好beanDefinitions,对beanDefinitions标签进行解析得到AbstractBeanDefinion,这中间包括自定义标签的解析和默认标签的解析
完成bean的解析后进行bean的加载,加载过程中会将beanDefinitions转换为rootBeanDefinition,调用getBean方法,如果从缓存中得到了bean直接返回,没有获取到会调用createBean方法创建,调用doCreateBean进行bean的实例化,调用createBeanInstance简单初始化,通过
pupulateBean方法进行属性的填充,此时如果是创建单例bean的时候如果存在循环依赖,则需要解决循环依赖,得到的完整的bean将会被放入到一级缓存,并删除二三级缓存,bean的创建完成之后就是剩余的bean的生命周期的事了,initializeBean进行ioc的初始化
AbstractApplicationContext的refresh函数载入过程(在创建IoC容器前,如果已经有容器存在,则需要把已有的容器销毁和关闭,以保证在refresh之后使用的是新建立起来的IoC容器)
1.调用prepareRefresh初始化,获取容器的当时时间,同时给容器设置同步标识
2.调用obtainFreshBeanFactory启动refreshBeanFactory,获取到beanDefinition(获取解析器及aop代理创建器等)
3.调用prepareBeanFactory配置一些beanFactory特性,如类加载的特性等等
4.调用postProcessBeanFactory指定一些BeanPost事件
5.调用invokeBeanFactoryPostProcessors
6.registerBeanPostProcessors监听后置处理
7.调用initMessageSource初始化信息源
8.调用initApplicationEventMulticaster初始化容器事件传播器
9.调用子类的某些特殊Bean初始化方法onRefresh
10.为事件传播器注册监听registerListeners
11.调用finishBeanFactoryInitialization,初始化剩下的单例bean(此方法包含了ioc单例bean初始化、注解支持、BeanPostProcessor的执行、Aop的入口)
12.调用finishRefresh,完成发布容器的生命周期
13.发生异常后回调用destory方法和cancelRefresh,重置同步标志
Spring AOP实现方式
通过AspectJ实现
声明切面和切点,经过前置或者后置、环绕等方式进行通知
通过Advisor增强,通过jdk或者cglib方式代理
spring容器启动,每个bean的实例化之前都会先经过AbstractAutoProxyCreator类的postProcessAfterInitialization()这个方法
getAdvicesAndAdvisorsForBean方法会提取当前bean的所有增强方法,然后获取到适合的当前bean 的增强方法,然后对增强方法进行排序,最后返回
调用createProxy方法,创建代理。首先找到增强方法和增强处理器,先创建代理工厂proxyFactory,然后获取当前bean的增强器advisors,把获取到的增强处理器放到proxyFactory,最后创建jdk或者cglib的代理工厂
Spring aop在refresh容器入口流程:
1.获取解析器
AbstractApplicationContext#refresh-->obtainFreshBeanFactory-->refreshBeanFactory-->loadBeanDefinitions-->XmlBeanDefinitionReader#loadBeanDefinitions->doLoadBeanDefinitions-->registerBeanDefinitions-->doRegisterBeanDefinitions
-->parseBeanDefinitions(实际解析beanDefinitions)-->parseCustomElement-->NamespaceHandler#resolve->AopNamespaceHandler#init->AopNamespaceHandler#parse(AopNamespaceHandler#init注册解析器)-->AspectJAutoProxyBeanDefinitionParser#parse(在解析配置文件时一旦遇到aspectj-autoproxy就会使用该解析器)
2.获取代理创建器,(接着上面的方法)进行parse解析,自动注册代理创建器AnnotaAwareAspectJAutoProxyCreator
AspectJAutoProxyBeanDefinitionParser#parse->AopNamespaceUtils#registerAspectJAnnotationAutoProxyCreatorIfNecessary->AopConfigUtils#registerAspectJAnnotationAutoProxyCreatorIfNecessary->registerAspectJAnnotationAutoProxyCreatorIfNecessary
3.创建aop代理(AnnotaAwareAspectJAutoProxyCreator)(aop的实现是通过 postBeanProcess后置处理器,在初始化之后做代理操作的)
refresh-->finishBeanFactoryInitialization->preInstantiateSingletons->getBean->doGetBean-->getSingleton->createBean->doCreateBean->initializeBean->AnnotaAwareAspectJAutoProxyCreator#postProcessAfterInitialization-->wrapIfNecessary
-->createProxy(获取增强方法和处理器进行代理)
3.1.获取增强处理器
AbstractAutoProxyCreator#getAdvicesAndAdvisorsForBean-->findCandidateAdvisor
3.2.创建代理createProxy
createProxy->ProxyFactory.getProxy()->ProxyCreatorSupport.createAopProxy()->DefaultAopProxyFactory.createAopProxy()(proxy-target-class当为true为cglib,否则为jdk)
Spring Autowired源码:
refresh--finishBeanFactoryInitialization-getBean--doCreateBean--populateBean--autowireByName/autowireByType---AutowiredAnnotationBeanPostProcessor#postProcessPropertyValues--findAutowiringMetadata--inject--
applyPropertyValues--BeanDefinitionValueResolver#resolveValueIfNecessary--resolveReference--getBean
Spring使用多级缓存原因:
1.管理不同状态下的bean
2.循环依赖的问题: a依赖b,b依赖a
为什么使用三级缓存:
解决代理对象(如aop)循环依赖的问题(因为Aop通过singleFactory.getObject()得到的对象如果是两次去取会得到不一样的对象,从而使早期bean与最终不一致问题)。
单单使用一级别缓存无法解决正常的循环依赖问题
使用二级缓存(singletonObjects、singletonFactories[原有的三级])无法解决AOP代理中出现的循环依赖问题:
例: 1.a依赖b,b依赖a,c。c又依赖a。a,b,c均aop增强
先加载a,实例化的bean放到singletonFactories,填充b属性,b进行实例化,b放到singletonFactories,填充a,从a中singleFactory.getObject()拿到实例化的a填充,填充c,c进行实例化,c放到singletonFactories,
c进行a的填充,从a中singleFactory.getObject()拿到实例化的a填充【问题就出现在这里,这里拿到的a和b中填充的a不是一个对象,通过jdk/cglib代理会生成一个新的a代理对象】,接下来就是正常的操作
使用二级缓存:singletonObjects+earlySingletonObjects
例: a依赖b,b依赖a,同时a,b都被aop增强。
如果只使用二级缓存,且二级缓存缓存的是一个不完整的bean,这个时候a在设置属性的过程中去获取b(这个时候a还没有被aop的后置处理器增强【getEarlyBeanReference这个方法在设置属性之前,advice动态织入bean中】),创建b的过程中,b依赖a,b去缓存中拿a拿到的是没有经过代理的a。就有问题
spring的bean线程安全问题:
如果bean有状态(有实例变量的类,可以保存数据),则线程不安全
如果bean无状态,则线程安全
固:对有状态的 bean 要使用 prototype,作用域对无状态的 bean 使用 singleton 作用域
spring重要接口:
InitializingBean:refresh-->finishBeanFactoryInitialization-->initializeBean-->invokeInitMethods
BeanPostProcesor:refresh-->finishBeanFactoryInitialization-->initializeBean-->applyBeanPostProcessorsBeforeInitialization/applyBeanPostProcessorsAfterInitialization
DisposableBean:在使用完之后调用
ApplicationContextAware:setApplicationContext() 在 Bean 初始化结束后被调用
setApplicationContext() 在 Bean 初始化结束后被调用, 获取到spring容器的上下文,分两步:refresh-->prepareBeanFactory配置了ApplicationContextAwareProcessor处理器 refresh-->finishBeanFactoryInitialization-->initializeBean-->ApplicationContextAwareProcessor#applyBeanPostProcessorsBeforeInitialization#invokeAwareInterfaces
ApplicationListener:refresh-->finishRefresh
分两步:finishRefresh中调用,初始化:refresh-->initApplicationEventMulticaster 注册阶段:refresh-->registerListeners-->multicastEvent-->doInvokeListener,其中multicastEvent使用同步或者异步方式调用对于listener的onApplicationEvent事件
BeanNameAware:setBeanName(String name) 方法在 Bean 初始化后被调用,
refresh-->finishBeanFactoryInitialization-->initializeBean-->invokeAwareMethods
SpringBoot main方法执行完毕为什么程序还在继续
其使用内置的 tomcat容器运行,tomcat运行之后有一个 initialize 方法,里面有一个 while(stopAwait),一直在检查,因此并不会退出
public void await() {
// ...
if( port==-1 ) {
try {
awaitThread = Thread.currentThread();
while(!stopAwait) {
try {
Thread.sleep( 10000 );
} catch( InterruptedException ex ) {
// continue and check the flag
}
}
} finally {
awaitThread = null;
}
return;
}
// ...
}
SpringBoot tomcat是如何监听请求和分发请求的
Tomcat 监听和分发请求的过程就是通过 Servlet 容器来实现的。当客户端发送一个请求时,Tomcat 接收到请求并交给 Servlet 容器处理,Servlet 容器根据请求的 URL 找到对应的 Servlet,并调用其 service() 方法来处理请求,最后将响应发送给客户端。这个过程是基于 Servlet 规范的,Spring Boot 只是将其封装并提供了方便的配置和使用方式。
java Servlet原理,为什么启动了一个Servlet容器之后可以一直运行
Servlet容器能够一直运行是因为它使用了线程池和自动管理Servlet的生命周期等机制,能够持续地处理客户端请求,并通过错误处理机制确保容器的稳定运行。
Servlet是如何监听网络端口的
ervlet通过在web.xml文件中配置监听器(listener)来监听网络端口。监听器是一种特殊的Servlet,它可以监听特定的事件,并在事件发生时执行相应的操作。
Servlet使用什么监听网络端口的,和Nio的socket监听端口有什么区别
Servlet使用的是阻塞式的ServerSocket,而NIO的SocketChannel是非阻塞的,并且具有更好的扩展性和性能。
SpringBoot和Vertx哪个框架比较好
SpringBoot基于阻塞式的Servlet,而Vertx基于netty的nio及事件驱动,一个是阻塞一个是非阻塞,如果你需要快速启动一个传统的Java应用程序并享受Spring生态系统的好处,那么选择SpringBoot是一个不错的选择。
如果你需要构建高性能、可伸缩和响应式的应用程序,那么选择Vert.x可能更合适。
SpringBoot 接收到controller流程:
前端请求-->容器处理(如tomcat版本到达了8使用的是java NIO异步处理)-->分发给Servlet处理(同步)
Vertx
前端请求-->Netty使用reactor分发请求-->基于Router事件驱动找到对应的Hander处理(包括请求分发到哪个controller整个流程都是异步非阻塞的)
Play Framework
前端请求-->Netty使用reactor分发请求-->采用传统的MVC(Model-View-Controller)架构模式,没有基于事件驱动
controller原理:
单例,DispatcherServlet,统一在finishBeanFactoryInitialization加载
SpringCloud:https://www.bilibili.com/video/BV18E411x7eT
SpringCloud和SpringCloud Alibaba
SpringCloud SpringCloudAlibaba
注册中心 Eureka nacos
消息中间件 无(第三方替代方案:rabbitmq) RocketMQ
分布式事务解决方案 无(第三方替代方案:2pc) Seata
分布式调度服务 无(第三方替代方案:xxl-job) Alibaba Cloud SchedulerX
短信平台 无 Alibaba Cloud SMS
分布式配置中心 SpringCloudConfig nacos
熔断降级 Hystrix Sentinel
存储 无 Alibaba Cloud OSS
服务调用 feign feign
负载均衡 Ribbon Ribbon
SpringCloud通信为什么使用http协议,调用风格为什么使用restful
http是最通用的协议,可以很好地解决跨语言跨平台兼容性。比如Dubbo协议,无法简单地实现跨语言,只适用于dubbo。
http交互只是协议比较重,但不会慢太多,追求单机极致性能的确可以考虑换协议。目前微服务的一个重要理念就是水平扩展,慢这个问题通过业务设计还有多实例部署可以很好地提速。
http协议很成熟,并且特点为大家所熟知:1.支持客户/服务器模式。2.简单快速。3.灵活。4.无连接。5.无状态。
其实就是一个速度与开发成本还有兼容性相互妥协兼顾的结果
注册中心与服务发现:Eureka zk consul、nacos、Consul、etcd、redis、mysql
消息中间件:Kafka、RabbitMQ、RocketMQ、ActiveMQ
配置中心:SpringCloudConfig、Nacos、Apollo
RPC服务调用:Dubbo、SpringCloud rpc(基于feign的http调用)、Thrift、Motan
spring-cloud调用服务两种方式:Ribbon+RestTemplate、Feign
Eureka和Zookeeper注册中心的区别
SpringCloud和Dubbo都支持多种注册中心,不过目前主流来看SpringCloud用Eureka较多,Dubbo则以Zookeeper为主。两者存在较大的差异:
从集群设计来看:Eureka集群各节点平等,没有主从关系,因此可能出现数据不一致情况;ZK为了满足一致性,必须包含主从关系,一主多从。集群无主时,不对外提供服务
CAP原则来看:Eureka满足AP原则,为了保证整个服务可用性,牺牲了集群数据的一致性;而Zookeeper满足CP原则,为了保证各节点数据一致性,牺牲了整个服务的可用性。
服务拉取方式来看:Eureka采用的是服务主动拉取策略,消费者按照固定频率(默认30秒)去Eureka拉取服务并缓存在本地;ZK中的消费者首次启动到ZK订阅自己需要的服务信息,并缓存在本地。然后监听服务列表变化,以后服务变更ZK会推送给消费者。
一致性算法的定义
一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。
CP:服务可以不能用,但必须要保证数据的一致性。
AP:数据可以短暂不一致,但最终是需要一致的,无论如何都要保证服务的可用。
ZK为什么是满足CP:
使用了强一致性算法ZAB
nacos同时支持cp和AP
CP:使用了raft算法,用于选举leader,数据的一致是同步的
AP:也使用raft算法,用于选举leader,但是使用了Distro 协议,用于实现多节点数据同步和负载均衡。在 Distro 协议中,每个节点都可以作为数据源和数据同步的目标,通过数据的分发和同步,保证各个节点之间的数据一致性。,数据的一致是异步的
Nacos:nacos支持两种方式的注册中心,持久化和非持久化存储服务信息。
(默认)非持久直接存储在nacos服务节点的内存中,并且服务节点间采用去中心化的思想,服务节点采用hash分片存储注册信息
持久化使用Raft协议选举master节点,同样采用过半机制将数据存储在leader节点上,每个节点都存储了数据的副本
强一致性算法ZAB、raft与paxos的原理和区别:
Raft和ZAB算法是对Paxos算法的简化和改进,raft和zab leader选举都是采用过半机制,集群写入数据都是采用两阶段提交
ZAB:
leader选举:zxid+myid
数据写入两阶段提交
角色:
Leader:负责请求转发、读、写
Follower:只负责读,参与投票和选举,写请求转发给leader
Observer:没有投票权、选举权,不参与选举阶段,广播阶段,只参与读不参与写
注:observer存在的意义:增加系统吞吐量、伸缩性
ZAB四种状态
LOOKING:当节点认为集群中没有Leader,服务器会进入LOOKING状态,目的是为了查找或者选举Leader
FOLLOWING:follower角色
LEADING:leader角色
OBSERVING:observer角色
raft:
leader选举:term号+时间段的超时时间,当每个term号相同时,谁先超时谁就赢得选举
数据写入两阶段提交
角色:
Leader:负责发起心跳,响应客户端,创建日志,同步日志。
Candidate:Leader选举过程中的临时角色,由 Follower 转化而来,发起投票参与竞选。
Follower:接受Leader的心跳和日志同步数据,投票给 Candidate
broadcastTime(心跳时间) << electionTimeout(选举超时时间) << MTBF(单台机器健康时间)
限流:信号量、redis、令牌算法、漏桶算法、滑动时间窗口算法、分布式限流Sentinel、nginx、tomcat、Hystrix
RPC-dubbo
rpc原理:
服务消费端(client)以本地调用的方式调用远程服务;
客户端 Stub(client stub) 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体(序列化):RpcRequest;
客户端 Stub(client stub) 找到远程服务的地址,并将消息发送到服务提供端;
服务端 Stub(桩)收到消息将消息反序列化为Java对象: RpcRequest;
服务端 Stub(桩)根据RpcRequest中的类、方法、方法参数等信息调用本地的方法;
服务端 Stub(桩)得到方法执行结果并将组装成能够进行网络传输的消息体:RpcResponse(序列化)发送至消费方;
客户端 Stub(client stub)接收到消息并将消息反序列化为Java对象:RpcResponse ,这样也就得到了最终结果。over!
dubbo作用:提供服务自动注册、自动发现,
算法:基于软负载均衡算法
dubbo组件:服务消费者(Consumer)、注册中心(Registry)、监控中心(Monitor)【累计调用次数和调用时间】、Container(服务运行容器,负责加载、运行服务【提供者】)
Dubbo支持的通信协议:dubbo、rmi、hessian(dubbo默认)、http、webservice、thrift、redis
Dubbo支持的序列化协议:
dubbo-hessian-lite 是官方 hessian 的一个 Apache Dubbo 私有版本
Apache Avro™ 是一个数据序列化系统。
Jdk 序列化
JSON - fastjson:一个Java版本内的 JSON 解析器/生成器
FST: 一个快速的Java序列化工具
Kryo是一个Java版本的快速有效的二进制对象序列化框架
dubbo:采用单一长连接和NIO异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况
软负载均衡和硬负载均衡:
在系统服务器上安装相应负载均衡软件,进行相关的配置,达到均衡负载的目的,如nginx
直接在服务器和外部网络间安装硬负载均衡设备,如F5
负载均衡算法:轮询法、加权轮询法、加权随机法(dubbo默认负载均衡算法)、最小连接数法、随机法、源地址哈希法
dubbo工作流程:
服务提供者启动start,然后注册resgister服务,消费者订阅subscribe服务,如果没有订阅到服务,会不断尝试订阅。新的服务注册到注册中心以后,注册中心会将服务notify到
消费者,monitor监控consumer和provider调用次数
dubbo源码及原理实现:
1.spring启动中,spring基于dubbo.jar的Meta-inf/spring.handlers配置,找到对应的命名空间,回调DubboNamespaceHandler命名空间处理类,调用init方法获取解析器,通过DubboBeanDefinitionParser解析器
将xml标签解析成bean对象。如会将service解析为ServiceBean
2.其中serviceBean对象继承了ServiceConfig类,实现了InitializingBean, DisposableBean, ApplicationContextAware, ApplicationListener, BeanNameAware,覆写了ApplicationListener中的onApplicationEvent,该方法在spring bean
初始化完成之后会调用,其中就有一个export方法,该方法实现了服务的注册功能,将Bean对象转会为url格式,将所以Bean属性转成url的参数,基于不同的protocal进行服务暴露和引用
3.ReferenceBean通过FactoryBean来实现消费服务去注册中心拉取相应的服务
4.dubbo网络传输调用底层基于Socket或者NETTY实现
SPI的具体原理:将接口的实现类放在配置文件中,我们在程序运行过程中读取配置文件,通过反射加载实现类。
如何实现自定义负载均衡策略:实现 LoadBalance 接口,将这个实现类的路径写入到resources 目录下的 META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance文件中即可
dubbo微内核架构:说白点就是支持插件
注册中心和监控中心都宕机的话,服务都会挂掉吗?
不会。两者都宕机也不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表。注册中心和监控中心都是可选的,服务消费者可以直连服务提供者
既有 HTTP ,为啥用 RPC 进行服务调用
RPC 只是一种设计,http是一种协议,使用不同的协议一般也是为了适应不同的场景
NETTY:
Reactor线程模型:单线程模型、多线程模型、主从多线程模型
https://www.zhihu.com/question/26943938
https://www.cnblogs.com/dawen/archive/2011/05/18/2050358.html
Reactor模式
同步非阻塞模型
单 Reactor 单线程
可以看到进程里有 Reactor、Acceptor、Handler 这三个对象:
Reactor 对象的作用是监听和分发事件;
Acceptor 对象的作用是获取连接;
Handler 对象的作用是处理业务;
单 Reactor 单线程方案:
Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
缺点
第一个缺点,因为只有一个进程,无法充分利用 多核 CPU 的性能;
第二个缺点,Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟;
所以,单 Reactor 单进程的方案不适用计算机密集型的场景,只适用于业务处理非常快速的场景
案例实现:
Redis 是由 C 语言实现的,它采用的正是「单 Reactor 单进程」的方案,因为 Redis 业务处理主要是在内存中完成,操作的速度是很快的,性能瓶颈不在 CPU 上,所以 Redis 对于命令的处理是单进程的方案
单Reactor多线程
方案实现流程:
Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
上面的三个步骤和单 Reactor 单线程方案是一样的,接下来的步骤就开始不一样了:
Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给子线程里的 Processor 对象进行业务处理;
子线程里的 Processor 对象就进行业务处理,处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将响应结果发送给 client
优缺点:
单 Reator 多线程的方案优势在于能够充分利用多核 CPU 的能,那既然引入多线程,那么自然就带来了多线程竞争资源的问题。
要避免多线程由于竞争共享资源而导致数据错乱的问题,就需要在操作共享资源前加上互斥锁,以保证任意时间里只有一个线程在操作共享资源,待该线程操作完释放互斥锁后,其他线程才有机会操作共享数据。
事实上,单 Reactor 多进程相比单 Reactor 单线程实现起来很麻烦,主要因为要考虑子进程 <-> 父进程的双向通信,并且父进程还得知道子进程要将数据发送给哪个客户端。
而多线程间可以共享数据,虽然要额外考虑并发问题,但是这远比进程间通信的复杂度低得多,因此实际应用中也看不到单 Reactor 多进程的模式。
多Reactor多线程
方案详细说明如下:
主线程中的 MainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 对象中的 accept 获取连接,将新的连接分配给某个子线程;
子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入 select 继续进行监听,并创建一个 Handler 用于处理连接的响应事件。
如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应。
Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。
Proactor
异步非阻塞模式
实际案例:
redis:单 Reactor 单线程
Netty:都支持
kafka:主从Reactor多线程模型
NETTY线程模型:单线程模型、多线程模型、主从多线程模型
Mybatis
Dao接口里的方法,参数不同时,方法能重载吗
不能
Dao接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK 动态代理为 Dao 接口生成代理 proxy 对象,代理对象 mapperproxy 会拦截接口方法,转而执行MappedStatement所代表的 sql,然后将 sql 执行结果返回
Mybatis是如何进行分页的?分页插件的原理是什么?
Mybatis 使用 RowBounds 对象进行分页,它是针对ResultSet结果集执行的内存分页
Mybatis 是否支持延迟加载?如果支持,它的实现原理是什么?
Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis 配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false。
使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke()方法发现 a.getB()是 null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName()方法的调用。这就是延迟加载的基本原理。
Mybatis 中如何执行批处理?
使用 BatchExecutor 完成批处理
Mybatis 中如何指定使用哪一种 Executor 执行器?
在 Mybatis 配置文件中,可以指定默认的 ExecutorType 执行器类型,也可以手动给 DefaultSqlSessionFactory 的创建 SqlSession 的方法传递 ExecutorType 类型参数
Mybatis 是否可以映射 Enum 枚举类?
Mybatis 可以映射枚举类,不单可以映射枚举类,Mybatis 可以映射任何对象到表的一列上。映射方式为自定义一个 TypeHandler,实现 TypeHandler 的 setParameter()和 getResult()接口方法。
为什么说 Mybatis 是半自动 ORM 映射工具?它与全自动的区别在哪里?
Hibernate 属于全自动 ORM 映射工具,使用 Hibernate 查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取,所以它是全自动的。而 Mybatis 在查询关联对象或关联集合对象时,需要手动编写 sql 来完成,所以,称之为半自动 ORM 映射工具。
Mybatis一级缓存和二级缓存
一级缓存(默认开启):它指的是Mybatis中sqlSession对象的缓存,当调用SqlSession的修改、添加、删除、commit(),close等方法时,就会清空一级缓存
二级缓存(手动开启):他指的是Mybatis中SqlSessionFactory对象的缓存,由同一个SqlSessionFactory对象创建的SqlSession共享其缓存,但是其中缓存的是数据而不是对象,所以从二级缓存再次查询出的结果对象与第一次存入的对象是不一样的。
流程:(mybatis的的一级缓存是SqlSession级别的缓存,一级缓存缓存的是对象,当SqlSession提交、关闭以及其他的更新数据库的操作发生后,一级缓存就会清空。二级缓存是SqlSessionFactory级别的缓存,同一个SqlSessionFactory产生的SqlSession
都共享一个二级缓存,二级缓存中存储的是数据,当命中二级缓存时,通过存储的数据构造对象返回。查询数据的时候,查询的流程是二级缓存>一级缓存>数据库。)
Spring如何解决Mybatis一级缓存脏读问题(跨服务时,一级缓存在A服务还在,在B服务已经被修改提交之后)
Spring在SqlSessionInterceptor的invoke调用时,直接如果尚未开启事务,则sqlSession直接提交,则一级缓存就失效了
一级缓存周期:
一级缓存的作用范围是会话Session,如果会话一旦消亡那么一级缓存也跟着消亡
mybatis缓存数据持久化:
定时将缓存的数据刷新至硬盘,也可以使用第三方的集成工具如:Redis,来进行缓存的存储
MyBatis二级缓存自动清理
cache中配置溢出淘汰算法,默认是LRU,所以二级缓存也使用LinkedHashMap
二级缓存如何保证线程安全(为什么需要序列化):
在执行过程中,因为二级缓存是可跨线程的,所以可能导致两个线程拿到同一个对象。所以需要进行序列化,那么如果线程1修改了缓存中的数据,线程2去获取缓存中的数据,在线程2获取数据后,线程1进行回滚,从而导致了线程2的脏读。
为了解决这个问题,所以必须把这个对象序列化,这样两个线程拿到的对象是一样的,但是序列化的ID是不一样的,这样就可以很好的解决这个问题
mybatis使用流程:
1.读取Configuration配置(包括environments配置和mapper配置) 2.通过SqlSessionFactoryBuilder的builder方法获取sqlSessionFactory 3.通过factory工厂得到sqlSession 4.通过sqlSession执行sql语句
mybatis(SqlSessionFactoryBuilder)源码配置解析过程(https://www.cnblogs.com/zhjh256/p/8512392.html)
1.读取文件,将文件解析成流(字节流/字符流都可以),进入new XMLConfigBuilder(),创建XPathParser,其中默认了文档节点解析器为XMLMapperEntityResolver,通过XPathParser调用createDocument方法,通过SAX使用默认的文档解析器
将typeHandlers、mappers、environments、plugins标签等解析成一个document,调用XMLConfigBuilder.parse(),所有节点(typeHandlers、mappers、environments、plugins等)都经过evaluate()计算,最后包装成Configuration,通过
build方法new DefaultSqlSessionFactory(config)得到SqlSessionFactory
mybatis设计模式
策略、工厂、享元(pool连接池)、代理模式(mapper接口的代理,属于jdk的动态代理)、装饰器(cache实现)、模板方法模式(不同的执行器实现不同的方法,里面采用的是模板方法模式)等
mybatis mapper接口的动态代理原理:
通过MapperFactoryBean实现FactoryBean的getObject方法,然后通过MapperProxyFactory,进行jdk的代理生成代理对象
mybatis重要configration节点:properties, settings, typeAliases, typeHandlers, objectFactory, objectWrapperFactory, reflectorFactory, plugins, environments, databaseIdProvider, mappers
mybatis通过什么解析Sql的CRUD语句
mybatis通过语言驱动器LanguageDriver 其实叫做解析器进行sql的解析,可以实现自己的语言驱动器进行sql的解析,mybatis默认的语言驱动器为XMLLanguageDriver
静态标签解析和动态标签解析的两种方式:
静态标签使用RawSqlSource进行解析,动态标签使用DynamicSqlSource(比如使用了where、choose等标签或者使用${}的叫动态sql)
SqlSource:
SqlSource是XML文件或者注解方法中映射语句的实现时表示,通过SqlSourceBuilder.parse()方法创建,SqlSourceBuilder中符号解析器将mybatis中的查询参数#{}转换为?,并记录了参数的顺序。它只有一个方法getBoundSql用于获取映射语句对象的各个组成部分
SqlSource实现有如下:
StaticSqlSource:最终静态SQL语句的封装,其他类型的SqlSource最终都委托给StaticSqlSource。
RawSqlSource:原始静态SQL语句的封装,在加载时就已经确定了SQL语句,没有、等动态标签和${} SQL拼接,比动态SQL语句要快,因为不需要运行时解析SQL节点。
DynamicSqlSource:动态SQL语句的封装,在运行时需要根据参数处理、等标签或者${} SQL拼接之后才能生成最后要执行的静态SQL语句。
ProviderSqlSource:当SQL语句通过指定的类和方法获取时(使用@XXXProvider注解),需要使用本类,它会通过反射调用相应的方法得到SQL语句。
例如DynamicSqlSource源码:
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
//SqlNode的对应实现解析对应的Node
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); //获取SqlSourceBuilder
Class> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); //通过parse返回一个StaticSqlSource,其中的sql语句就是在这里生成的
BoundSql boundSql = sqlSource.getBoundSql(parameterObject); //通过StaticSqlSource的getBoundSql构建一个BoundSql对象返回
for (Map.Entry
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
}
return boundSql;
}
SqlNode:
SqlNode接口主要用来处理CRUD节点下的各类动态标签
mybatis如何解析和执行mapper文件中的sql:(关键名词:SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、document、resultMap、XMLLanguageDriver、sqlSource、sqlNode)
首先在SqlSessionFactoryBuilder执行build方法,对相关的配置文件进行解析,其中就有对mapper配置文件的解析,首先mybatis会使用SAX将文件解析成document,通过解析document中的每个节点得到configration,其中对mapper文件的处理在XMLMapperBuilder中
XMLMapperBuilder.parse方法进行mapper文件的标签的解析,包括resultMap和动态sql,其中resultmap的解析结果会放到resultMap的对象中,包括一些列名,type信息,对sql的解析使用对应的语言驱动器创建sqlSource,采用的是sqlSource进行标签解析(其中分为
静态的sql标签解析器和动态的标签解析器),在后续进行select、update调用时使用sqlNode(ChooseSqlNode、ForEachSqlNode、IfSqlNode、TrimSqlNode、SetSqlNode(委托给了TrimSqlNode)等)去处理CRUD节点下的动态标签,在使用的时候,sqlSource会将#{}替换成?
ResultMap对象:
ResultMap类维护了每个标签中的详细信息,比如id映射、构造器映射、属性映射以及完整的映射列表、是否有嵌套的resultMap、是否有鉴别器、是否有嵌套查询
Configuration(配置及sql容器)
Configuration是mybatis所有配置以及mapper文件的元数据容器。无论是解析mapper文件还是运行时执行SQL语句,都需要依赖与mybatis的环境和配置信息
mybatis缓存:
只要实现org.apache.ibatis.cache.Cache接口的任何类都可以当做缓存,cacheKey=ID + offset + limit + sql + parameterValues + environmentId
Mybatis一级缓存和二级缓存
一级缓存(默认开启):它指的是Mybatis中sqlSession对象的缓存,当调用SqlSession的修改、添加、删除、commit(),close等方法时,就会清空一级缓存
二级缓存(手动开启):他指的是Mybatis中SqlSessionFactory对象的缓存,由同一个SqlSessionFactory对象创建的SqlSession共享其缓存,但是其中缓存的是数据而不是对象,所以从二级缓存再次查询出得结果的对象与第一次存入的对象是不一样的。
流程:(mybatis的的一级缓存是SqlSession级别的缓存,一级缓存缓存的是对象,当SqlSession提交、关闭以及其他的更新数据库的操作发生后,一级缓存就会清空。二级缓存是SqlSessionFactory级别的缓存,同一个SqlSessionFactory产生的SqlSession
都共享一个二级缓存,二级缓存中存储的是数据,当命中二级缓存时,通过存储的数据构造对象返回。查询数据的时候,查询的流程是二级缓存>一级缓存>数据库。)
Mybatis缓存失效条件
对于一级缓存,commit/rollback都会清空一级缓存。
对于二级缓存,DML操作或者显示设置语句层面的flushCache属性都会使得二级缓存失效。
mybatis sql方法执行过程:
前置:SqlSessionFactoryBuilder的build步骤解析配置文件,通过SqlSessionFactory获取openSession,根据sql语句及Xml配置,sql可以分为以下两种
第一种,调用org.apache.ibatis.session.SqlSession的crud方法比如selectList/selectOne传递完整的语句id直接执行;
第二种,先调用SqlSession的getMapper()方法得到mapper接口的一个实现
(第一种比较少用,且第二种是基于第一种实现基础上,直接看第二种)
调用SqlSession.getMapper()方法,实际调用的是configuration.getMapper()到MapperRegistry.getMapper得到Mapper的实现代理,代理实际又通过代理类MapperProxy实现jdk动态代理得到,其中invoke方法会调用mapperMethod.execute方法,
接下来的execute都是根据第一种的调用方式基础上做的
select查询:
查询语句通过返回类型是void/many/map/one/cursor判断走不同的方法,以selectOne为例,selectOne以selectList为基础,判断selectList返回的是一个还是多个,多个的话会抛出异常,其中selectList处理为
1.获取MappedStatement 2.通过sqlSource的sqlNode解析sql获取BoundSql(需要执行的sql),查询缓存中是否存在,如果存在已执行的接口,直接返回,否则查询数据库 3.首先获取对应的执行器Executor,设置对应的超时时间等,
4.获取语句处理器,并调用参数处理器处理封装参数 5.调用doQuery方法进行查询 6.将查询接口通过结果集处理器将JDBC查询接口映射到Java对象或者返回Map(如调用selectMap方法)
update更新(insert和delete实际也是调用update):
1.调用update先设置一个dirty=true,默认自动提交和回滚 2.通过configuration获取MappedStatement 3.清空本地缓存 4.调用doUpdate方法获取对应的语句处理器 5.调用JDBC对应update的方法execute()(此处可以忽略,太细了。和select不同的是,他会在excute调用前后生成
前置id和后置id)
MappedStatement定义:
mapper文件或者mapper接口中每个映射语句都对应一个MappedStatement实例,它包含了所有运行时需要的信息比如结果映射、参数映射、是否需要刷新缓存等
XML映射文件关系:
Mybatis 将所有 Xml 配置信息都封装到 All-In-One 重量级对象 Configuration 内部。在 Xml 映射文件中,
执行期主要类总结
执行器Executor,执行器负责整个SQL执行过程的总体控制。
参数处理器ParameterHandler,参数处理器负责PreparedStatement入参的具体设置。
语句处理器StatementHandler,语句处理器负责和JDBC层具体交互,包括prepare语句,执行语句,以及调用ParameterHandler.parameterize()设置参数。
结果集处理器ResultSetHandler,结果处理器负责将JDBC查询结果映射到java对象。
Spring执行器有多少种(分为缓存执行器和非缓存执行器【二级,因为一级是设置不了的,默认的】,以下都是非缓存执行器):
batchExcute执行器、simpleExcute执行器、reuseExcute执行器
Mybatis 的插件运行原理,以及如何编写一个插件。(用于记录日志,统计运行时性能,为核心功能提供额外的辅助支持)
插件是在内部是通过拦截器实现的。编写一个插件需要实现 Mybatis 的 Interceptor 接口并复写 intercept()方法,然后在给插件编写注解
Mybatis 执行批量插入,能返回数据库主键列表吗?
能,JDBC 都能,Mybatis 当然也能。
mybatis与Spring整合源码及原理:
整合后配置文件移除了environments配置,映射文件保持不变,增加了两个bean(SqlSessionFactroyBean和MapperFactoryBean)具体整合代码执行流程如下:
通过SqlSessionFactroyBean构建SqlSessionFactroy,其中会解析configLocation得到xmlConfigBuilder(和SqlSessionFactoryBuilder的步骤一样),其中xmlConfigBuilder支持objectFactory、objectWrapperFactory、typeAliases、plugins,
最后还是通过SqlSessionFactroyBuilder通过解析后的xmlConfigBuilder构建SqlSessionFactroy对象,当通过getBean获取对应的实例时,其实是调用getObject函数返回this.sqlSessionFactroy实例,在创建MapperFactoryBean,首先会调用afterPropertiesSet函数下的
checkDaoConfig,该校验的是sqlSession是否为null,此时sqlSession初始化工作中实际已经被封装成了new SqlSessionTemplate(sqlSessionFactory),校验Mapper文件是否存在映射文件等,MapperFactoryBean不适于配置多个Mapper,如果需要配置多个mapper,可以使用
MapperScannerConfigurer,通过扫描对应的包,成批创建映射器,首先调用postProcessBeanDefinitionRegistry函数,首先完成PropertyResourceConfigure属性文件的加载,在根据配置属性生成过滤器(用于判断文件是否符合要求) ,然后扫描java文件,调用过程一样是
对mybatis的session.getMapper方法的调用过程
重要的bean及接口
SqlSessionFactroyBean:实现了initializingBean接口和FactoryBean(实现该接口在调用getBean时实际调用的是getObject方法,该bean是为了得到sqlSessionFatory在解析过程中使用的)
MapperFactoryBean:继承了SqlSessionDaoSupport,SqlSessionDaoSupport实现了initializingBean,MapperFactoryBean实现了FactoryBean(调用的getObject方法实际调用getMapper,所以推测出该接口是用作调用过程中使用的)
MapperScannerConfigurer:多mapper配置解析,实现了initializingBean接口和BeanDefinitionRegistryPostProcessor接口(该接口会在调用spring的refresh方法的invokeBeanFactoryPostProcessors)
SqlSessionTemplate:使用sqlSessionTemplate封装SqlSessionFactroy,调用是,如果是查询语句,会调用commit,清除一级缓存
Spring整合mybatis的bean(SqlSessionFactroyBean、MapperFactoryBean)是AOP代理的bean还是IOC的普通bean,mybatis的mapper接口生成的是代理对象是使用什么代理的
Spring整合mybatis的bean是普通的ioc bean,mybatis的mapper接口生成的是代理对象是jdk动态代理对象,通过MapperProxy代理,并通过MapperProxyFactory的Proxy获取代理对象
订单业务流程整理:
业务模式都分为:立即分时、立即短租、预约分时、预约短租
下单
分时下单:会员状态校验:用户登录、黑名单、欠款、押金缴纳、是否销户、是否冻结
车辆押金校验
通过不同类型获取不同类型的校验工厂做个性化校验
订单互斥规则:是否有在运行中的单、是否有未结算的单
通过抽象工厂获取订单创建器,生成订单数据、占用库存
发布订单创建成功事件(mq)
更新订单缓存
命中活动(区分于优惠券活动)
通知用户
自动取消订单MQ消息--未支付取消
发布自动取消订单的MQ消息--未取车取消
优惠活动被使用的MQ事件
保存下单时的收费标准
特惠车自动排车
订单数据同步第三方(多个)
排车
取消
试算
分时在途试算来说(分时的在途试算时先让rocketmq每秒发送5的延迟级别(1分钟)在后台试算,前端轮询触发1分钟取试算结果)
第一步校验订单相关信息
获取车辆实时信息
判断是否cache试算结果是否存在,不存在需要重新试算
为试算加锁
执行试算
试算完成更新缓存
指定试算器为分时试算器
获取订单试算实体dto,试算订单行程分段
拆分时间片,首先拿到redis缓存中的时间片List
2条是当前一分钟与前一分钟需要进行拆分,多条是如果出现系统服务重启,那么试算就不止1分钟,是一段时间,对一段时间的计算当然就是多条记录,最后将segNew通过list.add(segNew)放入到缓冲中,而前端轮询实际不进行试算,只负责获取数据
初始化分时所有的费用项计算器,放入到list中
调用前置beforeCalculat计算方法,初始化订单、定价、活动、参数、时长包等基础数据,计算试算订单行程分段
执行真实试算doCalculate
循环计算每个费用项
执行初始化方法,初始化当前费用项需要的数据
执行试算,并封装试算结果
对于分时公里时长试算,因为涉及高低峰,采用增量redis添加试算
执行额外方法处理,处理一些额外的费用数据
调用后置afterCalculate计算方法,如计算订单费用总价
得到试算各个费用项的结果,封装整个试算结果,如封装订单总价、计算订单欠费信息、阈值欠费标志,出现时长和里程异常后发送分时订单预警
更新整个试算结果,缓存到redis,然后返回结果
还车需要重新进行试算的原因:延迟还车或者超时还车或者强制还车
试算重构的原因及解决的问题:
订单组成员经过几轮更换,中间又做过几轮不彻底的重构优化,导致代码结构、风格混杂,代码分层耦合严重,部分接口职责不清,有些业务功能分散到多个类中,同一个方法中存在多次重复的查询和加锁等,
更不用说代码复杂度、代码规范、缺乏必要的单元测试等等。这些问题导致代码维护困难,细小的业务调整都会影响大量的功能,对开发和测试都带来大量不必要的工作量和风险。
整改后(性能、代码拓展性、测试的难度):
1、完成分时核心算法的整改:原算法每次都是做全量计算,计算量会比较大,新算法每次只做增量计算,计算次数少很多(理论上每次只计算一次)。
2、采用费用项为维度的试算设计,单独的一个费用项独立算费,便于维护和扩展。
3、采用全新的框架搭建:新的框架按照可扩展性,适配复杂场景的兼容性搭建。保证新的试算在调整业务的时候,只改动少量的费用项代码,或增加相关代码的情况就能实现。解决了原订单模块只要有稍微的改动都要做全量的代码回归测试。
同时也解决了开发难度的问题,不至于业务的调整导致大量的代码流程要修改。
4、试算模块单独独立出来便于维护:原来的试算除核心试算外,还有很多费用项的计算是在试算外进行的,容易出现遗漏,维护成本巨大。
短租场景下采用的设计
工厂、策略、组合、模板方法
工厂:ShortCalcSceneServiceFactory初始化试算器工厂IShortCalcSceneService接口,如短租还车试算器、短租下单试算器、短租续租试算器、短租超时试算器、第三方短租试算器,短租提前还车试算器、其中的接口方法为initCalculator
策略:IShortCalcSceneService中实现了spring的InitializingBean,每个试算器都实现了afterPropertiesSet方法,该方法中会把当前类ConcurrentHashMap
,根据ShortCalcSceneServiceFactory根据传入的试算主场景+试算子场景得到对应的试算器,如"01",第一个0表示他是短租类型的,1表示他是正在创建订单,也就是当前这个场景为短租下单试算
组合:IShortCalcSceneService中的initCalculator试算器中,他会把每个费用项计算器都放到一个list当中,供后面进行对应费用项的试算
模板方法:Calculator接口,其中有三个模板方法, 参数初始化init(),费用计算calc,附加费用addition,此方法仅供拓展
分时场景:(分时只有分时试算器)
1.executeCalc执行试算方法(策略模式)
2.calculators注册费用计算器方法(组合模式)
3.计算执行器CalculateExecutor四个模板方法,
calculators(注册费用项),注:对于还车类型,需要添加上异地还车费计算器、取车附加费、还车附加费、超停调度费计算器
beforeCalculate(前置计算)、
executeCalculate(执行试算)注:具体的试算器中有具体的私有化doCalculate方法
afterCalculate(后置试算)
4.Calculator费用计算器模板方法,init(参数初始化)、calc(费用计算)、addition(额外附加费)
5.组合模式,试算参数组装calculateParamsService
修改订单
取车
换车
续租
还车
结算
支付
冲减退款
订单欠款
订单评价
订单查询
违章押金
订单预警
订单微服务:
S0001:主流程微服务(主流程、结算、查询)
odc01:试算微服务
ordj:定时任务微服务
ode01:mq处理微服务
gw:应用网关微服务
mte01:canal同步mysql订单主信息表微服务
odt01:订单加油微服务
rocketmq组件封装:(整体而言是订阅topic,监听消费时进行消息分发)
ContextRefreshedListener实现ApplicationListener注册监听启动consumer和producer,获取consumer和producer的bean,暴露对应的mq方法到hashmap中,topicInstanceMap、topicMathodMap、topicTagsMap,
在初始化consumer的init方法当中注册监听,监听不同的消费方式
ContextRefreshedListener的onApplicationEvent实现代码如下:
context = event.getApplicationContext();
//初始化自定义的producer对象,其中包括rocketmq的普通消息生产者、事务监听器、事件消息生产者
producer = context.getBean(MqProducerClient.class);
//初始化rocketmq生产者的属性、调用start方法
producer.init(boxName, gearName);
//初始化自定义的consumer对象,其中包括rocketmq的自定义消息监听器和consumerGroupMap,自定义的consumer对象实现了DisposableBean
consumer = context.getBean(MqConsumerClient.class);
/* 初始化客户端的一些信息,比如注册对应的消息监听器,消费者消费的入口consumeMessage,其中以method级别进行消息消费,注册完监听之后启动consumer */
consumer.init(gearName, boxName);
/* 暴露MQ方法,并向MQ订阅指定的消息,添加对应的映射关系,供启动消费topicInstanceMap、topicMathodMap、topicTagsMap */
exportMethod();
//订阅topic
consumer.subcript();
服务衍生:
单体架构-->SOA面向服务-->微服务(提供解耦,更细的服务)—->领域模型
微服务设计、拆分原则:
(核心思想)遵守高内聚、低耦合原则,同时结合项目的实际情况,综合考虑业务领域、功能稳定性、应用性能、团队以及技术等因素
1、基于业务领域拆分,在领域模型设计时需对齐限界上下,围绕业务领域按职责单一性、功能完整性进行拆分,避免过度拆分造成跨微服务的频繁调用。
2、基于业务变化频率和业务关联拆分,识别系统中的业务需求变动较频繁的功能,考虑业务变更频率与相关度,并对其进行拆分,降低敏态业务功能对稳态业务功能的影响。
3、基于应用性能拆分,考虑系统功能性需求,识别系统中性能压力较大的模块,并优先对其进行拆分,提升整体性能,缩小潜在性能瓶颈模块的影响范围。
4、基于组织架构和团队规模,提高团队沟通效率。
5、基于不同功能的技术和架构异构以及系统复杂度
微服务的缺点:
分布式事务的问题
提升了运维的难度
性能降低
资源利用率高
微服务优点:
代码的独立。
微服务系统间的独立
数据的独立
业务的切分,降低了单个服务的复杂性,负责某一服务的开发人员,只需要了解自己相关的业务。
RPC和restFul的区别,与Http的区别
RPC是一类远程调用框架,是一种风格
http是一种网络协议,暴露接口直接使用,RPC一般用于底层之前服务之间的通信,效率更高
RPC是基于TCP实现的,RESTFUL是基于HTTP来实现的
HTTP封装的数据量更多所以数据传输量更大,所以RPC的传输速度是比RESTFUL更快
服务治理:
1.服务注册与发现
2.软负载均衡与容错
3.服务监控与统计
4.服务容量评估
5.服务编排
6.服务权限控制
7.服务分层架构
8.服务降级
...等等等等
注册中心的服务治理:
1.服务注册
2.服务续约
3.服务获取
4.服务下线
5.服务失效剔除
6.服务自我保护
7.服务调用
8.服务同步
DDD对应文档:DDD 模式从天书到实践 - 知乎
DDD和微服务的区别:
DDD是思想,微服务是架构,有实际的技术实现落地,架构的实际落地
什么是DDD领域驱动模型(提出者:Eric Evans):
所谓领域模型,通俗来讲就是基于某个领域(如租车领域、贷款领域、房地产领域)的知识来驱动于软件的设计,从而建立一套软件模型,确定业务与应用的边界,保证业务模型与代码模型的一致性,根据领域边界进行微服务拆分
DDD通俗讲:
只是一种思想,出现了第二次软件危机,系统复杂到一定程度的时候,需要解耦业务之间的关系的一种思路。通过定义软件开发的模式,通过抽象、封装,建立领域模型和领域服务,大家按照这种模式去开发。这需要上层决策者的领导,
需要开发者素质更高(非常熟悉设计模式的使用),从而提高软件的质量和可维护度。目前来说还没有一套实际的技术落地(微服务则是由实际的架构落地的)
DDD领域驱动设计:
软件架构的方法论,进行架构风格的填充
DDD(domain-driven design)与传统的软件设计的区别:
软件设计:项目经理经过大量的业务分析后,会基于现有需求整理出一个基本模型,再将结果传递给开发人员,这就是开发人员的需求文档,他们只需要照此开发便是。
DDD:它像是更小粒度的迭代设计,它的最小单元是领域模型(Domain Model),所谓领域模型就是能够精确反映领域中某一知识元素的载体,这种知识的获取需要通过与领域专家(Domain Expert)进行频繁的沟通才能将专业知识转化为领域模型
如何进行DDD领域设计:
1.建立对应的模型关系图
2.建立对应的层级结构
3.建立实体对象(属性或者状态,包括业务角色【收银员】、业务实体【商品、发票】、业务用例【角色和实体之间的工作流程】)
4.建立服务service(行为或者方法)
5.聚合根(即跟对象,比如用户customer,如果customer不存在了,相对应的customer的地址也不复存在)
业务对象模型的种类:
失血模型(基本不用):pojo对象-->service-->DB
对象只包含get、set方法
优缺点:简单,但是代码逻辑(如数据库连接)都写在service,难以维护
贫血模型:数据对象(属性和固有行为)-->service(非固有行为)[如订单的service里同时处理订单计算和库存计算]->持久层-->DB
优点:层次结构清晰,单向依赖(spring采用的就是贫血模型)
缺点:无法良好的应对非常复杂的逻辑以及场景
充血模型:数据对象(固有属性、非固有属性)-->业务逻辑层(整合、事务封装)-->持久层-->DB
优点:更加符合面向对象原则,业务层很薄,符合单一职责原则
缺点:职责不好划分,开发水平要求高,模型实体中包含了大量的操作,实例化增加很多不必要的实例化
涨血模型(基本不用):数据对象-->持久层-->DB
优点:简化分层架构,也符合面向对象设计原则
缺点:取消了业务逻辑层,直接在domain object上封装事务和授权,授权很多原本不属于这个领域对象的逻辑,代码耦合大,难维护
DDD分层架构设计(分层式的软件架构,TPC/IP是采用的网络分层架构):
三层架构
表现层(controller)--业务逻辑层(service)--数据访问层(dao/mapper)
DDD四层架构(松散分层架构,如用户界面层、应用层也可以调用基础设施层)
用户界面层model--应用层(与模型进行&实体无关的业务逻辑,如线程调度)--领域层domain(真实业务逻辑处理,包括对象、工厂、分页操作等)--基础设施层
用户界面层:model、装配器、controller
应用层:应用服务service、task任务定义
领域层:common公共代码、event领域事件、model领域模型(vo对象、entity对象、聚合对象、service领域服务、factory工厂类、额外的service领域服务类)
基础设施层:持久化机制、po持久化对象、repository仓储层等
DDD实践:
以用户中心为例:分四层
facade:门面层,提供controller接口
application:应用层,提供基础编排能力,比如一下简单的校验,还比如校验--业务--后置处理
domian:领域层,包含业务逻辑
dto:普通bean,用于传递
entity:数据库实体bean
mapper:存储mybatis mapper地方
repository:mybatis仓储
service:业务逻辑层
infrastructure:提供基础的功能,相当于common
acl(Anti-Corruption Layer):防腐层,用于与其他微服务之间进行数据转换
MVC是不是一个架构设计
不是,是一个设计模式,并且由于spring的存在,开发是不符合面向对象的
DCI【Data Context InterAction】架构(契合领域模型的设计)
对于MVC架构补充,面向对象量化的补充
Data对象数据对于领域模型中的业务角色
Context对象的场景对于领域模型中的业务实体
InterAction交互行为对于领域模型中的业务用例
领域设计涉及的几个阶段:
概念设计:概念关联图、概念类、领域类图、填充属性
逻辑设计:业务逻辑和业务模型
物理设计(这部分可能会从新推翻重做领域模型):所有的开发人员对业务的设计、所使用的具体的技术及存在的问题的汇总
领域驱动边界定义方式:
1.战略设计:从业务角度出发
领域(范围):本质是一个问题域,只要是同一个领域,那问题域也相同,如电商领域的问题是订单、库存、支付等,那么他跟政法、出行等相关问题就不是一个问题,即确定一个问题的范围
子域:子域意思是相对于领域来说,是更细粒度的划分,如电商的领域可以划分成订单、库存、支付等,库存又可以更细的划分如本地库存和第三方库存等
子域分为:
核心域:业务核心,所有的都要围绕核心业务进行
通用子域:整个领域都能用到的子域(认证、授权)
支撑子域:不包含核心竞争力的功能,也不包含通用的功能,但是又必须有(网点、城市运营、支付)
通用语言:领域的通用术语(预约短租、预约分时、立即分时、立即短租等等术语),能正确、简单清晰的表达业务
限界上下文出发:确定领域的边界和上下文的环境,保证领域内的一些术语业务相关对象有一个确切的含义,无歧义。定义了模型的适用范围(如苹果不大好吃,需要确定这句话讲了什么意思,是苹果,不大好吃,还是苹果不大,好吃)
2.战术设计:从技术角度出发
UML建模与领域模型的应用范围:
UML更适用于小范围的类与类之间的关系,对于复杂问题的建模,其中的引用关系错综复杂,建模难度太大
战略的频繁的改动,规模较小的时候不适用于领域模型
如何绘制领域模型图:
使用亿图图示(该工具也可以画流程图)
开发模式:
瀑布式开发:也就是从需求到设计,从设计到编码,从编码到测试,从测试到提交大概这样的流程,要求每一个开发阶段都要做到最好。
迭代式开发:不要求每一个阶段的任务做的都是最完美的,而是明明知道还有很多不足的地方,却偏偏不去完善它,而是把主要功能先搭建起来为目的,以最短的时间
螺旋开发:很大程度上是一种风险驱动的方法体系,因为在每个阶段之前及经常发生的循环之前,都必须首先进行风险评估。
敏捷开发:敏捷开发相比迭代式开发的周期可能更短,并且更加强调队伍中的高度协作。敏捷方法有时候被误认为是无计划性和纪律性的方法(强调于人),实际上更确切的说法是敏捷方法强调适应性而非预见性。
畅游组难点:
订单试算、数据信息同步,会员车辆定价库存数据之间的同步性
订单分润、订单后续结算欠款负结算之间的业务流程
热点账户问题:
热点账户带来的其实是性能问题,多笔交易需要给同一账户记账时,会产生一笔交易等待前一笔交易记账完成才能接着记账的问题,这样就会产生事务等待问题
热点账户高并发记账方案
并发控制:限流
汇总明细入账:实时交易全部 insert 账户明细数据,insert 的开销比 加锁开销少。 定时跑批将一段时间内的账务明细汇总成一条,一笔入账到指定账户。
缓冲入账:缓存入账,将入账过程异步处理,使用 MQ 消息队列
子账户拆分:就是将一个热点账户对应多个影子账户, 将账户余额分散到各个影子账户,这样就没有热点账户问题。 每次请求来的时候,通过 hash 选择合适的影子账户记账,这样账户请求形成了分散热点
吞吐量:
吞吐量是指在单位时间内中央处理器(CPU)从存储设备读取-处理-存储信息的量。
QPS和TPS(TPS<=QPS):
TPS即每秒处理事务数,包括:”用户请求服务器”、”服务器自己的内部处理”、”服务器返回给用户”,这三个过程,每秒能够完成N个这三个过程,TPS也就是3
QPS查询事务数,基本类似于TPS,但是不同的是,对于一个页面的一次访问,形成一个TPS;但一次页面请求,可能产生多次对服务器的请求,服务器对这些请求,就可计入QPS之中
通俗讲:
比如一次订单查询,对于用户来说,我完成一次请求时为1TPS,但对于服务器来说,我可能有调用订单两次,会员一次,车辆两次,那么对于订单服务器来说,我QPS就是两次,但对于客户端来说他的qps又只是一次
再比如查询订单系统,中间调用了两次车辆系统,对于订单系统来说,TPS=QPS=1,对于车辆系统来说TPS=QPS=2
性能测试环境最高TPS:会员--还车后查询用户获取成就 tps=4800
性能测试环境订单最高TPS:(读)订单-续保驾乘险提示信息tps=4785 (写)订单-订单评价提交 TPS=4375
生产环境TPS最高峰值是:1700+
什么是springboot自动装配:
通过注解或者一些简单的配置就能在 SpringBoot的帮助下实现某块功能
Springboot自动装配原理:
Spring Boot 通过@EnableAutoConfiguration开启自动装配,通过 SpringFactoriesLoader 最终加载META-INF/spring.factories中的自动配置类实现自动装配,
自动配置类其实就是通过@Conditional按需加载的配置类,想要其生效必须引入spring-boot-starter-xxx包实现起步依赖
源碼:SpringBootApplication -->@EnableAutoConfiguration-->AutoConfigurationImportSelector-->ImportSelector#selectImports-->getAutoConfigurationEntry-->isEnabled-->getCandidateConfigurations-->getExclusions-->AutoConfigurationEntry
@SpringBootApplication组成:
@EnableAutoConfiguration:启用SpringBoot的自动配置机制
@Configuration:允许在上下文中注册额外的 bean 或导入其他配置类
@ComponentScan:扫描被@Component (@Service,@Controller)注解的 bean,注解默认会扫描启动类所在的包下所有的类 ,可以自定义不扫描某些 bean。如下图所示,容器中将排除TypeExcludeFilter和AutoConfigurationExcludeFilter
log4j2-2.14.0版本安全漏洞问题
使用JNDI在打印日志的时候进行恶意注入
整个利用流程如下(不仅仅可以使用rmi进行远程方法调用,还可以通过LDAP进行目录访问,通过目录访问可以下载和加载恶意的.class脚本,加载对应的静态代码块):
目标代码中调用了InitialContext.lookup(URI),且URI为用户可控;
攻击者控制URI参数为恶意的RMI服务地址,如:rmi://hacker_rmi_server//name;
攻击者RMI服务器向目标返回一个Reference对象,Reference对象中指定某个精心构造的Factory类;
目标在进行lookup()操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例;
攻击者可以在Factory类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码,达到RCE的效果;
JNDI:
java命名服务和目录服务的一组接口,提供的是根据不同协议调用的能力,其中包括RMI、LDAP、DNS等等
JNDI原理:
JDNI是provider-based的技术,暴露了一个 API和一个服务供应接口(SPI)
LDAP:
轻量级目录访问协议,LDAP用统一的方式定义了如何访问目录服务中的内容,比如增加、修改、删除一个条目。属于一种“层次数据库”,相比关系型数据库,查询更快
RMI远程方法调用:
计算机之间对象互相调用对方函数,启动对方进程的一种机制,使用这种机制,某一台计算机上的对象在调用另外一台计算机上的方法时,使用的程序语法规则和在本地机上对象间的方法调用的语法规则一样
微服务拆分原则
微服务拆分过程中需严格遵守高内聚、低耦合原则,同时结合项目的实际情况,综合考虑业务领域、功能稳定性、应用性能、团队以及技术等因素
Iaas:基础设施服务
Paas:平台服务
Saas:软件服务
K8S
视频:https://www.bilibili.com/video/BV1Qv41167ck?p=20&spm_id_from=pageDriver
文档:https://gitee.com/yooome/golang/blob/main/k8s%E8%AF%A6%E7%BB%86%E6%95%99%E7%A8%8B/Kubernetes%E8%AF%A6%E7%BB%86%E6%95%99%E7%A8%8B.md
java学科:https://www.yuque.com/fairy-era/yg511q/szg74m
视频和笔记:https://www.aliyundrive.com/s/9CdYvvReCTz
k8s+harbor+docker+jenkins安装:
k8s参考(或者中文官网):https://gitee.com/yooome/golang/blob/main/k8s%E8%AF%A6%E7%BB%86%E6%95%99%E7%A8%8B/Kubernetes%E8%AF%A6%E7%BB%86%E6%95%99%E7%A8%8B.md
harbor参考:https://www.cnblogs.com/xiaobo-sre/p/13373004.html
harbor的jenkins账号:J123456j_
harbor账号:账号admin 密码Harbor12345
jenkins独立账号密码:admin admin
传统部署:每个应用部署在同一个机器硬件上
优点:简单,不需要其他技术的参与
缺点:程序之间会有影响,比如单个应用内存泄漏
虚拟化部署:在一台物理机上运行多个虚拟机,每个虚拟机都是独立的环境
优点:程序环境不会产生影响
缺点:每个虚拟机都增加操作系统,浪费了部分资源
容器化部署:共享操作系统,但又可以保证每个容器都有自己的文件系统、CPU、内存、进程空间等
容器管理:
一个容器挂机,如何让另一个容器立即启动替补停机的容器
当并发访问量变大的时候,如何做到横向拓展容器数量
容器管理的问题统称为“容器编排”问题,为了解决容器编排问题,就产生了一些容器编排的技术:
Swarm:Docker自己的容器编排工具
Mesos:Apache的一个资源统一管理工具,需要和Marathon结合使用
Kubernetes:Goolgle开源的容器编排工具(市场占有率高)
其他的是Kubernetes封装
Kubernetes功能:
自我修复:一旦某个容器崩溃,能够在1秒钟左右迅速启动新的容器,通过kubectl create
弹性伸缩:可以根据需要,自动对集群汇总正在运行的容器数量进行调整(kubectl scale命令实现Pod扩容或缩容)
服务发现:服务可以通过自动发现的形式找到它所依赖的服务,通过service--kube-proxy支持,
负载均衡:如果一个服务启动了多个容器,能够自动实现请求的的负载均衡,通过service--kube-proxy支持,
版本回退:如果发现发布的程序版本有问题,可以立即回退到原来的版本,通过deployment支持
存储编排:可以根据容器自身的需求自动创建存储卷,通过PV和PVC
等等......
K8S对资源的操作有三种:
命令式对象管理(kubectl run...):直接使用命令去操作kubernetes资源
命令式对象配置(kubectl create...):通过命令配置和配置文件去操作kubernetes资源
声明式对象配置apply:通过apply命令和配置文件去操作kubernetes资源
master:集群的控制平面,负责集群的决策 ( 管理 )
ApiServer : 资源操作的唯一入口,接收用户输入的命令,提供认证、授权、API注册和发现等机制
Scheduler : 负责集群资源调度,按照预定的调度策略将Pod调度到相应的node节点上
ControllerManager : 负责维护集群的状态,比如程序部署安排、故障检测、自动扩展、滚动更新等
Etcd :负责存储集群中各种资源对象的信息
node:集群的数据平面,负责为容器提供运行环境 ( 干活 )
Kubelet : 负责维护容器的生命周期,即通过控制docker,来创建、更新、销毁容器
KubeProxy : 负责提供集群内部的服务发现和负载均衡
Docker : 负责节点上容器的各种操作
K8S资源:
NameSpace:多套环境的资源隔离或者多租户的资源隔离
Pod:kubernetes集群进行管理的最小单元,程序要运行必须部署在容器中,而容器必须存在于Pod中,其中包括根容器和用户容器
Service:Service可以看作是一组同类Pod对外的访问接口。借助Service,应用可以方便地实现服务发现和负载均衡
Deployment:Pod控制器,Pod是最小的控制单元,当pod的资源出现故障时,会尝试进行重启或重建pod
Label:Label是kubernetes系统中的一个重要概念。它的作用就是在资源上添加标识,用来对它们进行区分和选择
Pod生命周期
pod创建过程
运行初始化容器(init container)过程
运行主容器(main container)
容器启动后钩子(post start)、容器终止前钩子(pre stop)
容器的存活性探测(liveness probe)、就绪性探测(readiness probe)
pod终止过程
Pod会出现5种状态
挂起(Pending):apiserver已经创建了pod资源对象,但它尚未被调度完成或者仍处于下载镜像的过程中
运行中(Running):pod已经被调度至某节点,并且所有容器都已经被kubelet创建完成
成功(Succeeded):pod中的所有容器都已经成功终止并且不会被重启
失败(Failed):所有容器都已经终止,但至少有一个容器终止失败,即容器返回了非0值的退出状态
未知(Unknown):apiserver无法正常获取到pod对象的状态信息,通常由网络通信失败所导致
pod的创建过程
用户通过kubectl或其他api客户端提交需要创建的pod信息给apiServer
apiServer开始生成pod对象的信息,并将信息存入etcd,然后返回确认信息至客户端
apiServer开始反映etcd中的pod对象的变化,其它组件使用watch机制来跟踪检查apiServer上的变动
scheduler发现有新的pod对象要创建,开始为Pod分配主机并将结果信息更新至apiServer
node节点上的kubelet发现有pod调度过来,尝试调用docker启动容器,并将结果回送至apiServer
apiServer将接收到的pod状态信息存入etcd中
Pod调度(一个Pod在哪个Node节点上运行)
自动调度:运行在哪个节点上完全由Scheduler经过一系列的算法计算得出
定向调度:NodeName、NodeSelector
亲和性调度:NodeAffinity、PodAffinity、PodAntiAffinity
污点(容忍)调度:Taints、Toleration
Pod控制器Deployment
自主式pod:kubernetes直接创建出来的Pod,这种pod删除后就没有了,也不会重建
控制器创建的pod:kubernetes通过控制器创建的pod,这种pod删除了之后还会自动重建
pod控制器类型
ReplicationController:比较原始的pod控制器,已经被废弃,由ReplicaSet替代
ReplicaSet:保证副本数量一直维持在期望值,并支持pod数量扩缩容,镜像版本升级
Deployment:通过控制ReplicaSet来控制Pod,并支持滚动升级、回退版本,ReplicaSet的一个拓展
Horizontal Pod Autoscaler:可以根据集群负载自动水平调整Pod的数量,实现削峰填谷,Deployment的一个拓展
DaemonSet:在集群中的指定Node上运行且仅运行一个副本,一般用于守护进程类的任务
Job:它创建出来的pod只要完成任务就立即退出,不需要重启或重建,用于执行一次性任务
Cronjob:它创建的Pod负责周期性任务控制,不需要持续后台运行,Job的一个拓展
StatefulSet:管理有状态应用
金丝雀发布
Deployment控制器支持控制更新过程中的控制,如“暂停(pause)”或“继续(resume)”更新操作
Servic
对提供同一个服务的多个pod进行聚合,并且提供一个统一的入口地址,Service在很多情况下只是一个概念,真正起作用的其实是kube-proxy服务进程,每个Node节点上都运行着一个kube-proxy服务进程
kube-proxy目前支持三种工作模式:
userspace 模式、 iptables 模式、ipvs 模式,三种模式负载均衡的支持和效率高低区别是逐步提升的
Service类型
ClusterIP:默认值,它是Kubernetes系统自动分配的虚拟IP,只能在集群内部访问
NodePort:将Service通过指定的Node上的端口暴露给外部,通过此方法,就可以在集群外部访问服务
LoadBalancer:使用外接负载均衡器完成到服务的负载分发,注意此模式需要外部云环境支持
ExternalName: 把集群外部的服务引入集群内部,直接使用
Ingress七层代理:Ingress相当于一个7层的负载均衡器,是kubernetes对反向代理的一个抽象,它的工作原理类似于Nginx,可以理解成在Ingress里建立诸多映射规则
NodePort和LoadBalancer缺点
NodePort方式的缺点是会占用很多集群机器的端口,那么当集群服务变多的时候,这个缺点就愈发明显
LB方式的缺点是每个service需要一个LB,浪费、麻烦,并且需要kubernetes之外设备的支持
Ingress七层代理
Ingress里建立诸多映射规则,Ingress Controller通过监听这些配置规则并转化成Nginx的反向代理配置 , 然后对外部提供服务
数据存储
简单存储:EmptyDir(临时存储)、HostPath(Node节点存储)、NFS(外部存储)
高级存储:PV、PVC
配置存储:ConfigMap、Secret
PV和PVC
PV的作用对接于具体的存储系统
PVC跟Pod打交道,用于声明需要使用哪个PV
版本回退功能:
通过deployment支持版本升级过程中的暂停、继续功能以及版本回退等诸多功能
RocketMq:https://www.bilibili.com/video/BV1L4411y7mn?from=search&seid=16258558511899161056&spm_id_from=333.337.0.0
文档:https://github.com/DillonDong/notes/blob/master/RocketMQ/RocketMQ-01.md
https://github.com/DillonDong/notes/blob/master/RocketMQ/RocketMQ-02.md
https://github.com/DillonDong/notes/blob/master/RocketMQ/RocketMQ-03.md
为什么用Mq,有什么用
应用解耦
系统的耦合性越高,容错性就越低。以电商为例,用户创建完订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个系统出现故障或者因为升级等原因暂时不可用,都回造成下单的异常,影响用户的体验。
流量削峰
应用系统如果遇到系统请求流量瞬间猛增,有可能会将系统压垮。如果有消息队列,遇到此情况,可以将大量请求存储起来,将一瞬间的峰值请求分散到一段时间进行处理,这样可以大大提高系统的稳定性
异步
用户调用一个接口的时候,可能该接口调用了别的方法。
Mq的优点和缺点:(幂等、消息丢失、消息顺序、数据一致性问题)
系统可用性降低(如何采用高可用)
系统复杂度变高了(增加了mq),比如如何保证避免重复消费、消息丢失、和保证消息的顺序性
分布式一致性问题
Mq概念:
Producer 生产者
Comsumer 消费者
Topic 主题
Message 消息
Tag 标签过滤,更细化的主题
Broker 接受和存储消息实体
NameServer 为producer和consumer提供路由信息
管理broker,生产的时候把消息存放到对应的broker,然后每个nameServer存放了该消息存在哪个broker中,消费的时候询问nameServer我应该从哪个broker取消息进行消费
集群特点:
nameServer是一个无状态节点,可集群部署,节点之间无任何信息同步
brokerId为0表示主,非0为从
producer与nameServer集群中得一个节点进行长连接,定期从nameServer取topic路由信息,并向提供topic服务的broker Master建立长连接,定时向master发送心跳。producer完全无状态,可集群部署
consumer与nameServer集群中得一个节点进行长连接,定期从nameServer取topic路由信息,可以从broker master和slave拿消息,由borker
rocketmq broker多主多从集群:
主从节点可以采用同步也可以异步进行通信
主节点挂机从节点不能自动切换为主机
主节点与主节点之间不进行通信
联动云当前配置:broker双主双从,同步复制,同步刷盘
消息发送:
同步消息
实现:MessageListenerConcurrently
异步消息
通过指定SendCallBack判断是否是异步消息
单向消息sendOneway
顺序消息(使用队列,默认broker有四个队列) 实现:MessageListenerOrderly
全局消息顺序(保证发送到消费者的所有消息都是顺序的,注:没必要)
局部消息顺序(保证某种类型or业务的消息是顺序被消费的)
处理方案:
生产者指定队列,消费者指定MqMessageListenerOrderly(原理是一个队列一个线程)顺序消费模式
延时消息
DelayTimeLevel,延迟只支持有数得延迟类型,最大2h
为什么不支持任意时间的消息延迟
所有延时消息以延时级别不同放到不同的延迟临时目录当中,所以不可能维护这么多的目录,而目前支持这么多的延迟消息只是为了平衡一下有延迟的需求而已
批量消息
使用List
过滤消息
tag过滤
sql过滤
如:consumer.subscribe("topic",MessageSelector.bySql("a>5"))
事务消息(提交状态、回滚状态、中间状态) 实现TransactionListener,性能不好
发送消息
broker服务端相应消息写入结果
根据发送结果执行本地事务
根据本地事务执行状态执行commit或者rollback
消息消费模式(默认负载均衡)
负载均衡CLUSTERING(联动云使用)
消息完整的在一个消费者端消费
广播模式BROADCASTING
多个消费者端每个都消费一次
联动云mq组件功能:
封装rocketmq,可以指定发送队列,也可以不指定发送队列,支持事务消息,支持sendOneway单向发送,不支持异步消息,
消息消费模式采用CLUSTERING,使用Push模式等待生产者推送消息进行消费,消费指定MqMessageListenerOrderly顺序消费模式
批量拉消息,一次最多拉多少条,默认32
批量消费,一次消费多少条消息。默认是1条。如果当前gear有批量消费,那么要大于等于批量消费数量,默认是5
rocketmq存储使用内存映射文件技术进行存储
什么是内存映射文件(mmap)技术:
是一种文件操作的方式,由操作系统支持,主要用于进行大文件的读写操作,效率更高
原理是:应用程序把文件虚拟地址映射到一个进程内存中,映射到多个页上,这样文件内的数据就可以用内存读/写指令来访问,对内存地址的读写即可,
也就是说文件的实际物理地址的松散的,但是虚拟地址是连续的,只需要把虚拟地址映射到用户空间(进程的内存)进行操作即可
存储介质:
关系型数据库DB(ActiveMQ)
文件系统(RocketMQ/Kafka/RabbitMQ),性能比使用关系型数据库要好
rocketmq文件
写入:顺序写
读取:mmap
文件iO的随机读写和顺序读写:随机IO存在一个寻址的过程,所以效率比较低
随机读写:FileWriter和FileReader、BufferedReader和BufferedWriter、FileInputStream和FileOutputStream
顺序读写:MappedByteBuffer读、RandomAccessFile指定position写
传统的文件读取和mmap对比:
传统需四步:磁盘--(DMA拷贝)-->内核内存(缓冲区)--(CPU read拷贝)-->用户内存--(CPU write拷贝)-->网卡驱动(socket缓冲)--(DMA)-->网卡,有些博客这么分:磁盘文件--页缓存--....................
--socket缓冲区--网络
从磁盘复制数据到内核态内存;
从内核态内存复制到用户态内存;
然后从用户态内存复制到网络驱动的内核态内存;
最后是从网络驱动的内核态内存复制到网卡中进行传输
通过使用mmap的“零拷贝”技术方式,可以省去向"内核内存-用户内存"的内存复制,提高速度。这种机制在Java中是通过MappedByteBuffer实现的
mmap零拷贝后的流程:磁盘--(DMA拷贝)-->内核内存---(cpu拷贝)-->网卡驱动(socket缓冲区)--(DMA)-->网卡(3次拷贝,4次上下文切换)
拓展:
linux内核2.4以后,socket缓冲区做了调整,增加了数据的位置和长度的信息
linux的sendfile减少了3(2)次拷贝【rocketmq使用的MappedByteBuffer三次拷贝】,可以减少一次拷贝,2次上下文切换,linux sendfile流程:
如果网卡支持SG-DMA,通过缓冲区(内核内存-->网卡驱动(socket缓冲区))将描述符(地址值)和数据长度传入网卡驱动,此时网卡就可以直接从缓冲区读取数据,减少了内核缓冲区到socket缓冲区的cpu拷贝过程,
即磁盘--(DMA拷贝)-->缓冲区--(DMA)-->网卡
mmap对应的java的api:MappedByteBuffer//文件内存映射,数据不会复制到用户空间,只在内核空间,与sendfile类似,但是应用程序可以直接操作该内存
传统io读写(2次拷贝)+网络发送(2次拷贝):File.read(file,buf,len), Socket.send(file,buf,len)
mmap与零拷贝的关系:
mmap是内存文件映射技术,mmap只是在进行读的时候用到了io零拷贝技术
mmap的缺点:
创建并维持地址空间与文件的映射关系,内核中需要有特定的数据结构来实现这一映射,这当然是有性能开销的,除此之外另一点就是缺页问题,page fault,所以不适用于大文件的读写映射,
mmap处理大文件要注意一点,如果你的系统是32位的话,进程的地址空间就只有4G,这其中还有一部分预留给操作系统,因此在32位系统下可能不足以在你的进程地址空间中找到一块连续的空间来映射该文件,在64位系统下则无需担心地址空间不足的问题
零拷贝技术的消息中间件应用:RocketMQ使用MMAP、Kafka使用sendfile
性能提升:7M左右的文件,使用传统的500ms,sendfile15ms,mmap200ms
为什么rocketmq不使用sendfile:
RocketMQ 选择了这种 mmap + write 方式,这种方式即使频繁调用,使用小块文件传输,效果会比 sendfile 更好
总结:是根据rocketmq的需求而定的,rocketmq需要进行小数据块的一个传输,数据块太大对jvm有一定影响
mmap和sendFile区别:
1.mmap适合小数据量读写,sendFile适合大文件传输
2.mmap需3次拷贝,四次上下文的切换,sendFile需要2次上下文的切换,最少2次数据拷贝
3.sendFile可以利用DMA方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)
什么时候选择rabbitmq:业务量不是很大,性能要求不高,简单的解耦可以使用rabbitmq,稳定性比rocketmq好,他使用erlang语言开发,erlang语言本身的高可用比java更好
kafka:大数据、日志,kafka比较难,配置比较多,小公司玩不转
mmap优缺点:
优点是:
操作文件就像操作内存一样,适合于对较大文件的读写。
缺点是:
文件如果很小,比如是小于4k的,比如60bytes,由于在内存当中的组织都是按页组织的,将文件调入到内存当中是一个页4k,这样其他的4096-60=4036 bytes的内存空间就会浪费掉了。
而且文件无法完成拓展,因为mmap到内存的时候,你所能够操作的范围就确定了,无法增加文件的长度。
如果系统频繁的使用mmap操作,而且每次mmap的size都不同,那么就会使得内存可能缺少足够的连续的内存空间
消息存储结构
CommitLog:存储消息的元数据
ConsumerQueue:存储消息在CommitLog的索引
Index:为了消息查询提供了一种通过key或时间区间来查询消息的方法,这种通过IndexFile来查找消息的方法不影响发送与消费消息的主流程
config:运行期间一些配置信息
abort:如果存在改文件寿命Broker非正常关闭
checkpoint:文件检查点,存储CommitLog文件最后一次刷盘时间戳、consumerquueue最后一次刷盘时间,index索引文件最后一次刷盘时间戳。
刷盘机制:
同步刷盘:消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘, 然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态(消息-->内存-->磁盘)
异步刷盘:消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大(消息-->堆外内存ByteBuffer--200ms-->MappedByteBuffer--500ms-->磁盘)
高可用性机制
broker:通过Master和Slave的配合达到高可用性的
namesrv:多节点的高可用,broker同步信息到namesrv时是每个节点都同步
生产者:把Topic的多个Message Queue创建在多个Broker组
消费者消费高可用:当Master不可用或者繁忙的时候,Consumer会被自动切换到从Slave,(注:slave并不会出现选举等动作变成master)
消息主从复制(brokerRole设置成ASYNC_MASTER、 SYNC_MASTER、SLAVE)
同步复制:master同步复制消息给slave
异步复制:master将消息异步复制给slave
负载均衡:
Producer负载均衡:使用轮询方式,轮询所有的message queue发送
Consumer负载均衡:
集群模式:同一条消息只能由订阅他的一个消费者进行消费
广播模式:同一个消息每个订阅他的消费者都消息一次
消费消息重试:(每条消息最多重试16次,可配置)
顺序消息的重试:当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时,应用会出现消息消费被阻塞的情况。
无序消息的重试:对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,您可以通过设置返回状态达到消息重试的结果。无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息
死信队列
消费消息失败达到一定次数之后,消息将进入死信队列,死信队列消息不会再被消费,且在文件中删除时间和正常消息一样,默认都是三天,排查出问题后可以在控制台--消息当中重新发送该消息
什么情况下会产生消费幂等问题:
发送时消息重复(多生产了):消息发送已被broker持久化,此时出现网络问题生产者客户端没有接收到应答,则会进行发送重试(默认三次),解决方案:发送同步消息,同步刷盘,同步主从复制
投递时消息重复(多推送消费了):消费者已经消费,由于网络问题没有给broker应答,则会进行重新消费 解决方案:手动ack,SUCCESS和SUSPEND_CURRENT_QUEUE_A_MOMENT
负载均衡时消息重复(扩缩容rebalance了):当消息队列Broker或客户端重启、扩容或缩容时,会触发 Rebalance,此时消费者可能会收到重复消息 解决方案:消息幂等,
如何保持幂等:
rocketmq不保证幂等性,需要自己进行业务校验
定时消息:
rocketmq不支持任意精度的定时消息发送,只支持18种,如果需要自定义定时消息,需要通过自身代码设计配合完成,当定时队列的消息偏移量offset为0时执行消息的任务
为什么rocketmq不支持任意精度的定时消息:
如果要支持任意时间精度定时调度,不可避免地需要在Broker层做消息排序,再加上持久化方面的考量,将不可避免的带来巨大的性能消耗,
每个定时级别都会创建一个调度任务,一个延迟级别对应SCHEDULE_TOPIC_XXXX主题下的一个消息消费队列
通俗讲:定时任务的消息会存放在store目录下的不同定时级别的队列号中(在控制台中是无法看到的),不同的定时消息级别有不同的目录,而每一个定时消息级别有设置了不同的定时消息队列SCHEDULE_TOPIC_XXXX轮询任务(即对offset减为0就可以放到对应的topic队列进行消息消费),
这个时候你就没办法支持任意精度的持久化存储了,因为做了,该怎么存储,存储之后怎么进行排序消费
消费者消息失败重试原理
先保存到Topic名称为“%RETRY%+consumerGroup”的重试队列(这里需要注意的是,这个Topic的重试队列是针对消费组,而不是针对每个Topic设置的),用于暂时保存因为各种异常而导致Consumer端无法消费的消息。
考虑到异常恢复需要一些时间,RocketMQ会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越大。RocketMQ对于重试消息的处理是先保存至Topic名称为“SCHEDULE_TOPIC_XXXX”的延迟队列中,
后台定时任务按照对应的时间进行Delay后重新保存至“%RETRY%+consumerGroup”的重试队列中。
注:每次消费失败的重试次数存放到ProcessQueue--->MessageExt#reconsumeTimes字段,其中判断是否超过重试次数比如在:ConsumeMessageOrderlyService#checkReconsumeTimes(List
生产消息失败重试原理:
源码:DefaultMQProducerImpl-->获取getRetryTimesWhenSendFailed-->循环调用time次【for (; times < timesTotal; times++) 】,如果发送成功则退出
namesrv和broker信息的同步:
使用长连接心跳检测机制,每30秒检测Broker是否存活,连续120S没有收到心跳包,NameServer将移除Broker的路由信息同时关闭Socket连接
Broker故障延迟机制(默认不启用):
如果开启了Broker故障延迟机制,当broker出现故障的时候,将此broker设置成需要规避的broker,并设置规避时长,生产者进行轮询选择队列的时候可以从规避的Broker中选择一个可用的Broker
批量消息发送:
底层调用的是this.defaultMQProducerImpl.send(batch(msgs)),会将多条消息整合MessageBatch 对象,MessageBatch 继承了Message对象
实时更新消息消费队列与索引文件
消息消费队文件、消息属性索引文件都是基于CommitLog文件构建的,当消息生产者提交的消息存储在CommitLog文件中,ConsumerQueue、IndexFile需要及时更新,否则消息无法及时被消费,
根据消息属性查找消息也会出现较大延迟。RocketMQ通过开启一个线程ReputMessageService,死循环Thread.sleep(1)来准实时转发CommitLog文件更新事件,相应的任务处理器根据转发的消息及时更新ConsumerQueue、IndexFile文件
消息队列和索引文件恢复
消息服务器Broker由于某个宕机,导致CommitLog、ConsumerQueue、IndexFile文件数据不一致,
存储文件加载:判断上一次是否异常退出。实现机制是Broker在启动时创建abort文件,在退出时通过JVM钩子函数删除abort文件。如果下次启动时存在abort文件。说明Broker时异常退出的,CommitLog与ConsumerQueue数据有可能不一致,需要进行修复。
其中包括正常恢复和异常恢复:
正常恢复:Broker正常停止再重启时,从倒数第三个文件开始恢复,如果不足3个文件,则从第一个文件开始恢复
异常恢复:异常停止则需要从最后一个文件往前走,找到第一个消息存储正常的文件。其次,如果CommitLog目录没有消息文件,如果消息消费队列目录下存在文件,则需要销毁。
过期文件删除机制:
1.默认每个文件过期时间为72小时(联动云48小时),deleteWhen删除时间默认为晚上4点
2.当磁盘空间不足时(默认为剩下不足10%)执行删除过期文件
RocketMQ消息传递模式:
pull拉模式:从broker拉取消息进行消费,RocketMQ使用一个单独的线程PullMessageService来负责消息的拉取,默认32条
push推模式:rocketmq没有真正意义上的推模式,只是在拉模式上的一层封装。
消息队列负载机制遵循一个通用思想:一个消息队列同一个时间只允许被一个消费者消费,一个消费者可以消费多个消息队列。
rocketmq顺序消费:
RocketMQ支持局部顺序消息消费,也就是保证同一个消息队列上的消息顺序消费。不支持消息全局顺序消费,如果要实现某一个主题的全局顺序消费,可以将该主题的队列数设置为1,牺牲高可用性。
ProcessQueue实现机制
ProcessQueue是MessageQueue在消费端的重现、快照。PullMessageService从消息服务器默认每次拉取32条消息,按照消息的队列偏移量顺序存放在ProcessQueue中,PullMessageService然后将消息提交到消费者消费线程池,消息成功消费后从ProcessQueue中移除。
消息传递的push模式:联动云使用push模式
RocketMQ未真正实现消息推模式,而是消费者主动向消息服务器拉取消息,RocketMQ推模式是循环向消息服务端发起消息拉取请求
消息消费过程源码:
PullMessageService负责对消息队列进行消息拉取,从远端服务器拉取消息后将消息存储ProcessQueue消息队列处理队列中,然后调用ConsumeMessageService#submitConsumeRequest方法进行消息消费,使用线程池来消费消息,
确保了消息拉取与消息消费的解耦。ConsumeMessageService支持顺序消息和并发消息
总结:PullMessageService得到PullRequest,PullRequest包含拉取的消息队列ProcessQueue,然后pullMessage方法中开启一个线程,ConsumeMessageService的submitConsumeRequest方法对ProcessQueue进行消费,找到对应监听器,调用consumeMessage方法即可
长轮询机制缺点:
如果开启了长轮询机制,PullRequestHoldService会每隔5s被唤醒去尝试检测是否有新的消息的到来才给客户端响应,或者直到超时才给客户端进行响应,消息实时性比较差,为了避免这种情况,
RocketMQ引入另外一种机制:当消息到达时唤醒挂起线程触发一次检查
Consumer Rebalance机制:
同的触发机制最终底层都调用了MQClientInstance的doRebalance方法,而在这个方法的源码中,并没有区分哪个消费者组需要进行Rebalance,只要任意一个消费者组需要Rebalance,这台机器上启动的所有其他消费者,也都要进行Rebalance
流程:
1 获得Rebalance元数据信息
2 进行队列分配
3 分配结果处理
KAFKA:一个轻量化的流计算库
Kafka的发布订阅模型:
producer--comsumer-zookeeper-topic-partition及其多副本--Segment
kafka broker如何保证消息高可用
每个topic都可以对应多个partition,而每个partition都有多个副本,分为leader和follower,如果leader所在的broker宕机,则可从follower进行消费
kafka如何保证顺序消费:
kafka虽然不能按照严格的顺序消费,但是可以通过指定消息发送到对应的partition进行指定
kafka消息丢失的三种情况和如何保证消息不丢失
发送时网络异常消息丢失
可以设置异步返回或获取发送结果
消费者挂了实际没有消费消息
在消费完成手动进行提交
Kafka 弄丢了消息,kafka中的leader所在服务器宕机,这时候follower少同步了一部分leader的消息
设置参数,对应参数可以参考官方文档
partition存储数据结构:
segment+稀疏索引
什么是segment:
Segment是parttion的一部分,他同时包含了索引文件和数据文件
什么是稀疏索引:
即kafka将数据流写入文件中的position位置,按offset划分,position可以认为是虚拟内存的位置,如0的offset到3的offset对应的position位置为0到495,那么索引文件对应的数据文件就可以表示为如下
索引 数据
offset position position message
0 0 0 message001
324 message002
3 495 495 message003
...
即一个partion对应多个segment,一个segment有索引文件组和日志文件组成,而索引文件和日志文件是有对应关系,这其中的关系使用的是稀疏索引进行关联
kafka中各个功能存在的问题和解决方案:
生产消息
批量发送
每次批量发送消息从而减少网络io
消息压缩:
gzip
高效序列化
ProtoBuf
内存池复用技术
Producer 一上来就会占用一个固定大小的内存块,比如 64MB,当需要创建一个新的 Batch 时,直接从内存池中取出一个 16 KB 的内存块即可
存储消息
io多路复用
Reactor 网络通信模型1+N+M和nio,其中1用来处理accept请求,N负责从 socket 中读写数据,M 个表示处理实际业务的线程
分区分段结构
磁盘顺序写mmapbytebuffer+randomaccess
pagecache
操作系统层面,刷盘时直接刷新到页的缓存中再到磁盘
发送消息
mmap
通过内存映射技术,将文件的物理地址通过虚拟地址进行内存映射,直接操作虚拟内存减少额外系统调用,增加查询消息的速度
稀疏索引
零拷贝
使用sendfile零拷贝技术
批量拉取
每次拉取多个消息的集合从而减少网络io
Rebalance机制:
如下情况可能会触发消费者rebalance
消费组里的consumer增加或减少了
动态给topic增加了分区
消费组订阅了更多的topic
rebalanc策略:
三种rebalance的策略:range()、round-robin(轮询)、sticky(粘性)
rebalance处理:
concurrenthashmap
1.7采用的是sengment分段数组,默认为2,默认初始化16个并发级别,所以最大并发数是16,也可以指定,但是一旦指定之后就不可以改变
首先第一个线程进来的时候,先去获取对应的sengment分段数组,如果分段数组为null,则需要初始化分段数组,初始化对应hashEntry数组,产生线程竞争的时候采用cas进行竞争
1.8采用的是Node数组+链表+红黑树的接口
首先根据key计算出hashCode,
判断是否需要进行初始化
如果定位到的node为空则利用CAS写入,写入的时候判断是否需要进行扩容
如果位置不为空,使用synchronized锁写入数据,如果当前写入的链表数量大于阈值,则转化为红黑树
类加载过程:
类加载--连接-初始化-使用-卸载
加载是指通过全限定类名获取到二进制字节流,得到class对象,这中间包括了双亲委派技术和类加载器
连接包括验证、准备、解析,这个过程与加载过程是交叉进行的
验证:指的是对class文件的合法性校验,包括元数据校验、文件格式校验、字节码校验、符号引用的校验
准备指的是分配内存并为类静态变量初始化零值
解析:将符号引用转换成直接引用
初始化:只对类的引用一些静态变量调用init方法直接初始化真实的值,初始化这部分的触发需要比如new对象时候会触发,有静态方法和静态变量时候会触发,调用子类时候会触发,反射调用时候会触发等等
卸载:
卸载条件:类中的实例已经被回收完了,
classloader已经被回收了
没有反射拥有class对象了,且没有其他地方在引用这个类了
只有自己定义的类加载器才会被回收,jdk自带的类加载器不会被回收,如bootstapclassloader(C++实现)、extClassLoader、appClassLoader
类加载的双亲委派模型:
好处:防止类的重复加载、避免java的核心api被篡改
自底向上判断类是否加载、自顶向下进行类加载
代码流程:首先调用loadClass方法,其中调用findclassloader尝试找到类加载器,如果没有找到,尝试从父加载器中寻找,找不到则使用bootstrap类加载器进行加载,然后调用findclass尝试加载类型
几种常用的信道复用技术:
频分复用
时分复用
波分复用
码分复用
几种带宽接入技术:
ADSL和FTTX
操作系统:管理计算机硬件与软件的程序,本质上也是计算机软件的程序
内存、磁盘、CPU关系:
CPU:中央处理单元(CntralPocessingUit)的缩写,也叫处理器,是计算机的运算核心和控制核心。电脑靠CPU来运算、控制。让电脑的各个部件顺利工作,起到协调和控制作用。
硬盘:存储资料和软件等数据的设备,有容量大,断电数据不丢失的特点。
内存:负责硬盘等硬件上的数据与CPU之间数据交换处理。特点是体积小,速度快,有电可存,无电清空,即电脑在开机状态时内存中可存储数据,关机后将自动清空其中的所有数据。
内核:操作系统的
描述一下你对操作系统的理解:
操作系统是计算机的一个软件,是计算机硬件和上层软件的一个桥梁,管理者系统的进程、内存、设备、文件,其中进程间通过信号量、socket、管道等通信方式进行通信,每个进程之间需要使用一定的调度算法进行调度运行,默认采用时间片轮转
的调度算法,而对于内存管理,则是对数据进行内存的分配和存储,则采用了两种方式的内存管理,分别是连续分配管理方式和非连续的管理分配方式。。。。
系统调用:
进行进程管理、内存管理、设备管理、文件管理
进程间的通信方式:
socket套接字、信号量、管道、消息队列、共享内存
线程间的同步方式:
互斥量、信号量、事件
进程的调度算法:
先到先服务(FCFS)调度算法 : 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
短作业优先(SJF)的调度算法 : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
时间片轮转调度算法 : 时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称 RR(Round robin)调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
多级反馈队列调度算法 :前面介绍的几种进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程 。
多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前被公认的一种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。
优先级调度 : 为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级
几种内存管理机制:
两种方式:连续分配管理方式和非连续分配管理方式
连续分配管理方式有:快式管理
非连续有:页式管理(默认64k),段式管理,段页式管理机制,页式物理真实存在的,段式逻辑,在页的基础上分的,段页式是先分段再分页
页表管理机制中有两个很重要的概念:快表和多级页表
存在两个问题:
虚拟地址到物理地址的转换要快。
解决虚拟地址空间大,页表也会很大的问题。
快表(可以理解为高速缓存):为了解决虚拟地址到物理地址的转换速度
多级页表:避免把全部页表一直放在内存中占用过多空间
分页机制和分段机制的共同点和区别
共同点:
分页机制和分段机制都是为了提高内存利用率,较少内存碎片。 页和段都是离散存储的,所以两者都是离散分配内存的方式。但是,每个页和段中的内存是连续的。
区别:
页的大小是固定的,由操作系统决定;而段的大小不固定,取决于我们当前运行的程序。分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段,能够更好满足用户的需要
CPU寻址了解吗?为什么需要虚拟地址空间?
现代处理器使用的是一种称为虚拟寻址(Virtual Addressing) 的寻址方式。使用虚拟寻址,CPU 需要将虚拟地址翻译成物理地址,这样才能访问到真实的物理内存。 实际上完成虚拟地址转换为物理地址转换的硬件是 CPU 中含有一个被称为 内存管理单元(Memory Management Unit, MMU) 的硬件
为什么要有虚拟地址空间,不用会有什么问题:1.会破坏操作系统 2.运行多个程序的时候会造成紊乱
什么是虚拟内存:
物理内存的映射,通过虚拟内存可以让软件程序拥有系统总的物理内存大小,比如360软件是10个G,我内存只有4个G,那我同样能运行,这还要依靠一个局部性原理
局部性原理:
在某个较短的时间段内,程序执行局限于某一小部分。程序访问的存储空间也局限于某个区域,通过虚拟存储器的虚拟内存技术实现,虚拟内存技术又基于内存管理的机制上实现,如请求分页存储管理,请求分段存储管理,请求段页式存储管理
包括缺页中断、虚拟地址转换、一定容量的内存和外存
分页与虚拟内存的请求分页存储管理的区别:
请求分页存储管理建立在分页管理之上,请求分页存储管理可以提供虚存,而分页存储管理却不能提供虚存,通俗讲:分页管理需要将一个作业的全部地址调入到主存,而请求分页存储管理只需要一部分调入到主存,从而需要实现下面的几个功能:
缺页中断、虚拟地址转换(逻辑地址到物理地址的变换)、一定容量的内存和外存
缺页中断:
如果需执行的指令或访问的数据尚未在内存(称为缺页或缺段),则由处理器通知操作系统将相应的页面或段调入到内存,然后继续执行程序;
页面置换算法:(当发生缺页中断时,如果当前内存中并没有空闲的页面,操作系统就必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。用来选择淘汰哪一页的规则叫做页面置换算法,我们可以把页面置换算法看成是淘汰页面的规则。
OPT 页面置换算法(最佳页面置换算法) :最佳(Optimal, OPT)置换算法所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。一般作为衡量其他置换算法的方法。
FIFO(First In First Out) 页面置换算法(先进先出页面置换算法) : 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰。
LRU (Least Recently Used)页面置换算法(最近最久未使用页面置换算法) :LRU算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰。
LFU (Least Frequently Used)页面置换算法(最少使用页面置换算法) : 该置换算法选择在之前时期使用最少的页面作为淘汰页。
CPU vs Kernel(内核)
操作系统的内核(Kernel)属于操作系统层面,而 CPU 属于硬件。
CPU主要提供运算,处理各种指令的能力。内核(Kernel)主要负责系统管理比如内存管理,它屏蔽了对硬件的操作。
操作系统运行级别:
用户态(user mode) : 用户态运行的进程或可以直接读取用户程序的数据。
系统态(kernel mode): 可以简单的理解系统态运行的进程或程序几乎可以访问计算机的任何资源,不受限制。
Linux操作系统常用命令:
ps
top
load average 1分钟负载 5分钟负载 15分钟负载情况,如果这个数值除以逻辑CPU的个数>5,证明负载情况就很高了
us 用户空间占用CPU百分比
sy 内核空间占用CPU百分比
cat
三剑客:
grep:查找文本
awk:文本分割并计算
sed:流编辑器
......
nacos和zk的区别:
nacos支持持久化和非持久化存储即有点 AP和CP 分布式一致性的概念,nacos的CP-持久化更像贴合zk的模式(过半机制),默认非持久化采用内存存储速度更快,而且分片存储,不利点就是某个服务节点挂掉,可能出现部分时间调用失败
zk的过半机制和ZAB两阶段提交
Nacos:nacos支持两种方式的注册中心,持久化和非持久化存储服务信息
zk选举流程(myid和ZXID,优先比较ZXID,再比较myid):
服务端启动时选举过程
1.每个server发起一个投票,先默认选者自己为leader
2.接受到其他服务端实例发来的选票
3.校验处理选票,优先比较ZXID,再比较myid
4.统计每一次选票,判断是否已大于一半
5.同步服务端实例状态
服务端运行期间进行的选举(Leader无法响应或者是宕机了,每个机器将要从自身的运行状态切换到选举状态)
1.更新自身状态为LOOKING
2.一样的选举流程
nacos和eureka的范围不同,
Nacos的阈值是针对某个具体Service的,而不是针对所有服务的;但Eureka的自我保护阈值是针对所有服务的。nacos支持CP和AP两种;eureka只支持AP。nacos使用netty,是长连接;eureka是短连接,定时发送。
Eureka自我保护:
Eureka Server 在运行期间会去统计心跳失败比例在 15 分钟之内是否低于 85%,如果低于 85%,Eureka Server 会将这些实例保护起来,让这些实例不会过期
缺点:保护期内如果服务刚好这个服务提供者非正常下线了,此时服务消费者就会拿到一个无效的服务实例,此时会调用失败
zk的选举状态:
LOOKING:寻找Leader服务的状态,处于当前状态后,将会进行Leader选举流程
FOLLOWING:代表当前服务端处于跟随者状态,表明是Follower服务
LEADING:代表当前服务端处于领导者状态,表明是Leader服务
OBSERVING:观察者状态,表明是Observer服务
Observer角色:Observer的主要作用是提高zookeeper集群的读性能
负责:
1、与leader同步数据
2、不参与leader选举,没有投票权。也不参与写操作的提议过程。
3、数据没有事务化到硬盘。即Observer只会把数据加载到内存。
负载均衡四层和七层的区别
1.区别:四层负载,说的是基于IP+端口的负载均衡;七层负载,说的是基于WEB请求,URL等应用信息的负载均衡。同理,还有基于二层和三成的。二层的就是基于MAC地址,二层负载均衡会通过一个虚拟MAC地址接受请求,
然后再分配到真实的MAC地址。三层负载就是通过一个虚拟IP地址,然后再分配到真实的IP。四层就是通过虚机的IP+端口接收请求,然后再分配到真实的服务器;七层就是通过虚机主机名或者URL接收请求,
再根据一些规则分配到真实的服务器,常见的应用是nginx。
2.所谓的负载均衡,就是根据请求的信息不同,来决定怎么样转发流量。四层负载均衡,就是根据请求的ip+端口,根据设定的规则,将请求转发到后端对应的IP+端口上。七层负载均衡,则是在四层基础上,再去考虑应用层的特征。
比如说一个WEB服务器的负载均衡,除了根据IP+80端口来判断之外,还可以根据七层URL,浏览器类别,来决定如何去转发流量。
3.四层交换机主要分析IP层和TCP/UDP层,实现四层流量负载,这种负载不关心七层的应用协议。七层的交换机除了支持四层之外,还要分析应用层,如HTTP协议、URL、cookie等信息。四层常见软件是haproxy,LVS,七层常见软件是nginx
数据结构与算法:
数据结构:
数组:
访问:O(1)//访问特定位置的元素
插入:O(n)//最坏的情况发生在插入发生在数组的首部并需要移动所有元素时
删除:O(n)//最坏的情况发生在删除数组的开头发生并需要移动第一元素后面所有的元素时
链表:
访问:O(n)
插入:O(1)
删除:O(1)
相比于数组会占用更多的空间,因为链表中每个节点存放的还有指向其他节点的指针。除此之外,链表不具有数组随机读取的优点
分类:单链表、双向链表、循环链表、双向循环链表
栈(stack):后进先出(LIFO, Last In First Out) 的原理运作。在栈中,push 和 pop 的操作都发生在栈顶,数组或者链表实现
访问:O(n)//最坏情况
插入删除:O(1)//顶端插入和删除元素
队列:先进先出( FIFO,First In, First Out) 的线性表
访问:O(n)//最坏情况
插入删除:O(1)//后端插入前端删除元素
分类:单队列和循环队列
图:由顶点的有穷非空集合和顶点之间的边组成的集合
顶点:图中的数据元素,我们称之为顶点
边:顶点之间的关系用边表示。
度:度表示一个顶点包含多少条边,在有向图中,还分为出度和入度
存储:邻接矩阵(二维数组)或者邻接表(链表)存储
搜索:
广度优先搜索:一层一层搜索
深度优先搜索:搜索完一个分支再搜索另一个分支
堆:一种特殊的二叉树,堆中的每一个节点值都大于等于(或小于等于)子树中所有节点的值。或者说,任意一个节点的值都大于等于(或小于等于)所有子节点的值
分类:大顶堆和小顶堆,都可用于排序
最大堆 :堆中的每一个节点的值都大于等于子树中所有节点的值
最小堆 :堆中的每一个节点的值都小于等于子树中所有节点的值
存储:数组
排序构建流程:
1.构建一个大顶堆/小顶堆
2.将第一个元素放到后面
3.再构建一个大顶堆/小顶堆(对刚才放到尾部的元素不做处理)
树:
树的定义:
一棵树中的任意两个结点有且仅有唯一的一条路径连通。
一棵树如果有 n 个结点,那么它一定恰好有 n-1 条边。
一棵树不包含回路。
树的分类:二叉树和多叉树
二叉树定义:两个叉的都叫二叉树
二叉树(Binary tree)是每个节点最多只有两个分支(即不存在分支度大于 2 的节点)的树结构。
二叉树 的分支通常被称作“左子树”或“右子树”。并且,二叉树 的分支具有左右次序,不能随意颠倒。
二叉树 的第 i 层至多拥有 2^(i-1) 个节点,深度为 k 的二叉树至多总共有 2^k-1 个节点
二叉树分类:
普通的二叉树
满二叉树
完全二叉树
二叉搜索树(二叉排序树、二叉查找树)
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉搜索树。
平衡二叉树(特殊的二叉排序树,中序遍历有序)
所有节点的左右子树的高度差小于1的二叉树
二叉树、二叉搜索树和二叉平衡树的区别:二叉树包括二叉搜索树,二叉平衡树又是特殊的二叉搜索树
二叉树的存储:链表或者数组
树的遍历:
前序遍历:根--左--右
中序遍历:左--根--右
后序遍历:左--右--根
红黑树(自平衡(self-balancing)的二叉排序树(BST):O(lgn)
每个节点非红即黑;
根节点总是黑色的;
每个叶子节点都是黑色的空节点(NIL节点);
如果节点是红色的,则它的子节点必须是黑色的(反之不一定);
从根节点到叶节点或空子节点的每条路径,必须包含相同数目的黑色节点(即相同的黑色高度)
二叉排序树和AVL树和红黑树对比:
平衡性:二叉排序树<红黑树
目的是利用颜色值作为二叉树的平衡对称性的检查,只要插入的节点“着色”满足红黑二色的规定,最短路径与最长路径不会相差的太远
最长路径与最短路径高度差:
红黑树根节点到叶子节点最长的路径都不会比最短的路径长出两倍
B-树和B+树:
B-树特点:
1.所有键值分布在整颗树中(索引值和具体data都在每个节点里);
2.任何一个关键字出现且只出现在一个结点中;
3.每个节点可以存放多个key,节点中既存放key,也存放值
4.搜索有可能在非叶子结点结束(最好情况O(1)就能找到数据);
5.在关键字全集内做一次查找,性能逼近二分查找;
B+树与B-树的区别:
1.B+树内节点不存储数据,所有 data 存储在叶节点导致查询时间复杂度固定为 log n。而B-树查询时间复杂度不固定,与 key 在树中的位置有关,最好为O(1)。
2.B+树叶节点两两相连可大大增加区间访问性,可使用在范围查询等,而B-树每个节点 key 和 data 在一起,则无法区间查找。
布隆过滤器:
用途及原理:布隆过滤器说存在,实际数据有可能存在,布隆过滤器中不存在,那么数据一定不存在
由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构
使用:
一个合适大小的位数组保存数据
n个不同的哈希函数
添加元素到位数组(布隆过滤器)的方法实现
判断给定元素是否存在于位数组(布隆过滤器)的方法实现。
SDS动态字符串:
结构:
struct sdshdr{
//记录buf数组中已使用字节的数量,等于SDS所保存字符串的长度
int len;
//记录buf数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
};
与常见的C字符串对比:
1、常数复杂度获取字符串长度
C字符串自身不记录自己的长度信息,计算len时为O(n),SDS本身有长度记录,计算len时为O(1)。
2、杜绝缓冲区溢出
当SDS改变时,SDS的API会首先检查SDS的空间是否满足修改所需的要求,如果不满足的话,API会自动将SDS的空间扩展至修改所需要的大小,然后才进行实际的修改操作。
3、减少修改字符串带来的内存分配次数
SDS通过空间预分配和惰性空间释放来减少修改字符串带来的内存分配次数。
4、二进制安全
SDS的buf属性不是保存的字符,而是一系列二进制数据,因此可以用来保存任何特殊数据格式。
跳表skipList:
概念:跳表使用的是空间换时间的思想,通过构建多级索引来提高查询效率,实现基于链表的“二分查找”,跳表是一种动态的数据结构,支持快速的查找、插入和删除操作,时间复杂度是 O(logn)
存储:使用链表分层存储
如何减少改变跳表空间复杂度:改变索引策略,动态的平衡执行效率和内存消耗
跳表层数计算:通过概率论的公式计算,理论层级为,第一层一般1,第二层1/4,第三层1/8...,该理论则说明了为什么跳表在存储小数据量的时候每一层的区间并不大,而redis在数据量少的情况下都采用二进制数组ziplist结构存储
压缩链表ziplist:经过特殊编码的双向链表,牺牲部分读写性能,来换取高效的内存空间利用率,每次都是从尾部开始遍历
压缩链表结构:
zlbytes 压缩链表占用的内存总字节数 可以直接对内存大小进行调整, 无需遍历整个链表获取大小 32 位
zltail 压缩链表尾节点的偏移量 长度 1 字节, 0xFF; pop 操作无需遍历整个链表 32 位
zllen 压缩链表存储的节点总数 zllen 大于 2^16 - 2 时, 需要遍历整个链表来获取长度 16 位
entryN 节点
zlend 压缩链表结束符 0xFF 8 位
entryN分为:
头部head:存放节点的编码类型和前一个元素的大小,因为内存是连续的,所以存放了上一个元素的大小和当前元素大小就可以知道上一个元素的位置,所以他并没有”链“这种概念
body:存放当前元素大小和当前元素内容,如果为数字,则存放数字,如果为字符串,则存放字符串的base64编码
适用:适用于存储小数据,与跳表相比,属于时间换空间
优势:使用连续的内存存储,存储效率高,不会出现像链表的内存碎片化
劣势:1.连续的内存在扩容的时候会导致内存的重分配,重新开辟内存是需要代价的,修改操作也需要进行realoc重分配操作 2.ziplist很长的时候,一次重分配进行数据的拷贝影响的性能更大,这就要求了ziplist的大小不能太大
快速链表quicklist:
结构:双向链表+压缩链表的组合
当双向链表里只有一个节点时退化为压缩链表
当双向链表每个节点只有一个压缩链表时退化为双向链表
hash:
解决hash冲突的方法:开放地址法(ThreadLocalMap)、建立公共溢出区、再hash、链地址法(HashMap)
redis字典dict:
相对于数组,链表更高层次的数据结构,可以通过键获取值,而hashMap和hashTable都是字典结构的一种实现,对redis来说是如下
dict:
type:dictType结构的指针,封装了很多可重载函数,如字符串有字符串的操作,int类型有int类型的操作
dictType结构描述:
hashFunction:对 key 生成 hash 值
keyDup:对 key 进行拷贝
valDup:对 val 进行拷贝
keyCompare:两个 key 的对比函数
keyDestructor:key 的销毁
valDestructor:val 的销毁
privdata:私有数据指针,当调用者在创建时用到
ht[2]:两个hashtable,ht[0]为主,ht[1]在渐进hash过程中才会用到,所谓渐进hash其实就是进行rehash,ht[1]在完成rehash之后,ht[0]的指针会指向ht[1]
dictht 字段描述:
table:哈希表数组,数组中存的是dictEntry元素
dictEntry 字段描述:
key:存储 key
v:dictEntry 在不同用途时存储不同的数据
next:hash 冲突时开链,单链表的 next 指针
size:哈希表大小,一般为2的幂次方
sizemask:哈希表大小掩码,用于计算索引值,等于 size-1,相当于计算hash用的hashcode % size=sizemask & hashcode,和hashmap的计算公式一样
used:哈希表已有节点的数量
rehashidx:增量 hash 过程过程中记录 rehash 执行到第几个元素了,当 rehashidx == -1 表示没有在做 rehash
通俗点讲:进行rehash的时候,如果哈希表中存了几千几百万的数据,不可能一次性复制到ht[1],需要一点点的复制,怎么一点点复制过去呢,就是需要rehash的时候,对于新增的数据
则直接往ht[1]新增元素,在查找的时候需要在ht[0]和ht[1]两边查找,只有一边有值,如果在ht[0],还需要转移到ht[1],在删除元素的时候也是两边操作,在完成rehash的时候,ht[0]
的指针指向ht[1],此时ht[1]变成了ht[1]的实例,而ht[1]的指针指向空,rehashidx变为-1
iterators:正在运行的迭代器数量
扩容条件:当size/tableSize>5,并不是像java中hashtable负载因子大于0.75时就会进行扩容
红黑树、redis字典、ziplist、skiplist比较:
hashmap采用红黑树与redis采用hashmap比较:hashmap引入红黑树是为了解决链表过长而是检索速度减慢问题,redis使用hashtable是因为redis采用了skiplist+字典,在范围查找的时候采用skiplist,查询单个值哈希冲突比较少的时候采用hashtable
skiplist与红黑树比较:红黑树数据结构更加复杂,编码更难,在做范围查找的时候,平衡树比skiplist操作要复杂,查询删除插入的复杂度大家都是O(logN)
ziplist和skiplist比较:ziplist属于时间换空间,skiplist属于空间换时间,ziplist内存都是连续的,内存存储效率高,内存碎片少,但是当数据比较大时,需要修改或者扩容时内存出现重分配,性能损耗也更大,所以需要ziplist+skiplist平衡两者之间的优劣势
对于存储的有序性:除了字典结构,另外三种都是有序的
漏桶算法:水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,漏桶算法能强行限制数据的传输速率
定义变量:
漏桶大小、桶的漏洞大小
令牌桶算法:与漏桶相反,令牌桶算法的原理是系统以恒定的速率产生令牌,然后把令牌放到令牌桶中,令牌桶有一个容量,当令牌桶满了的时候,再向其中放令牌,那么多余的令牌会被丢弃;
当想要处理一个请求的时候,需要从令牌桶中取出一个令牌,如果此时令牌桶中没有令牌,那么则拒绝该请求
例如:guava的RateLimiter
滑动时间窗口算法:使用双指针
比如,将一分钟划分成6块,限定一分钟可以进来100个请求,而6块区域每块限定最多10个请求,第一块时间过去了之后,指针开始时间指向第二块,同时将在尾部再生成一块10秒的区域出来即可,但是end指针还是指向原来的第6块
算法:
查找:
顺序查找:从头扫到尾
时间复杂度O(n)
折半查找O(log n):前提条件,数据有序,left-middle-right,比较middle,找得到就找得到
分块查找(索引查找):分层多个索引表,通过索引查找
散列表:hash咯
排序:
插入排序O(n^2)(折半插入):折半进行比较,一个一个插入
希尔排序O(nlogn):先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序
选择排序O(n^2):略
冒泡排序O(n^2):比较两个相邻元素
归并排序O(nlogn):使用分治法,先拆分成多个数组,再合并多个有序数组
快速排序O(nlogn):
会先从数组中的一个数当做基准数,比如最左边作为基准数,i=基准数位置,j=length-1,然后从两边进行检索,先从右边检索比基准数小的,再从左边检索比基准数大的,如果检索到了,就停下,然后交换两个元素,然后继续检索,当i==j相遇,
交换基准位置与i的位置,经过第一轮排序之后,i的左边都比基准数小,右边都比他大,递归进行第二轮排序
例子:【5,1,3,7,4】-->【5,1,3,4,7】-->【4,1,3,5,7】-->【3,1,4,5,7】-->【1,3,4,5,7】
堆排序O(nlogn):使用大顶堆或者小顶堆进行排序
基数排序O(n*k):略
桶排序O(n+k):略
计数排序O(n+k):略
递归:方法自己嵌自己
回溯:组合、排列等问题都可以使用,属于特殊的一种递归思路,
1、排列:从n个不同的元素中,取r个不重复的元素,按次序排列,称为从n个中取r个的无重复排列。
2、组合:从n个不同的元素中,取r个不重复的元素,组成一个子集,而不考虑其元素的顺序,称为从n个中取r个的无重组和。
思想:确定思路,写好框架
回溯剪枝:按照某种条件进行限制
贪心算法:只能求满足某些约束条件的可行解的范围
分治法
动态规划:最优解问题
1.确立子问题
2.确认边界,建立模型,建立状态转移公式
3.求解
例子:0-1背包、分割等和子集
双指针:定义start指针和end指针,随着条件start前移或者end后移
KMP:
位运算
&:1&1=1、 1&0=0 、0&0=0 、obj&obj=obj 、 (n-1) & 2的幂=n%
~取反 ^异或(相同为0,不同为1)
应用场景:hashmap数组大小的2的幂次方
功能 示例 位运算
去掉最后一位 101101—>10110 x>>1
在最后加一个0 101101—>1011010 x<<1
在最后加一个1 101101—>1011011 x<<1+1
把最后一位变成1 101101—>101101 x|1
把最后一位变成0 101101—>101100 x|1-1
把最后一位取反 101101—>101100 x^1
把右数第k位变成1 101001—>101101,k=3 x|(1<<(k-1))
把右数第k位变成0 101101—>101001,k=3 x&~(1<<(k-1))
右数第k位取反 101001—>101101,k=3 x^(1<<(k-1))
取末三位 100101—>101 x&7
取末k位 1101101—>1101,k=5 x & (1<<(k-1))
滑动窗口:利用的也是双指针
拓扑排序:
深度优先搜索
广度优先搜索
先计算元素入度边个数
计算保存当前顶点与之关联顶点
栈保存当前入度为0的顶点
遍历栈入度为0的顶点并更新当前顶点的关联顶点的入度边个数直到遍历完所有顶点
前缀和
技巧算法的一种,使用前面计算的和做某一种判断
例子【10,5,2,1,0,5,3】,tarSum=8,子集总和等于8的个数
首先维护一个Map
1 2 3 4 5 6 7
10 5 2 1 0 5 3
sum 10 15 17 18 18 23 26
减去tarsum 2 7 9 10 10 15 17
发现此时在sum中的key有 10,10,15,17,则子集为【5,2,1】 【5,2,1,0】 【2,1,0,5】 【5,3】
注:边界处理,对于【0,5,3,1】,target=8,map.put(0,1),得到的子集就有2个
核心代码:
ans+=map.getOrDefault(curSum-targetSum, 0);
map.put(curSum, map.getOrDefault(curSum, 0)+1);
......
总结:二分-->排序-->双指针-->动规-->dfs/bfs-->前缀和-->回溯...
lombok原理:
实现“JSR 269 API”的程序,即实现自身的AST语法树,在运行时编译动态生成代码
AST抽象语法树全名:
abstract syntax code,将真实语法实现隐藏在内部,对语法进行抽象化提供给用户使用,是对java文件解析的dom
实现步骤例子:
1.编写一个自定义注解@HelloWorld
2.编译一些Processor类处理注解,改类继承AbstractProcessor,AbstractProcessor实际实现了Processor接口
3.为class添加注解
@SupportedAnnotationTypes("com.processor.annotation.HelloWorld")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class) //注解处理器
4.实现的process方法使用TreeMaker创建JCTree(抽象语法树中元素基类)生成对应的代码
@AutoService(Processor.class) 原理:
声明了这个注解,使用spi原理,在META-INF目录下新建services目录创建一个Processor,在此嘎嘎嘎嘎嘎过文件中声明对应的实现类的全限定类名
lambda表达式原理:
实现函数式接口,底层使用抽象语法树在编译时编译成对应的代码,比如foreache还是会编译成基本的for循环
elasticsearch:
面试题:https://www.processon.com/view/6173c3331e08537415f832ee#outline
es整体集群架构:
集群:
Master节点:主要负责整个集群的状态
Data节点:主要负责整个集群的存储
Coordinating客户端节点:主要负责集群的分发请求
分片:将一个索引中的数据水平切分为多个shard,比如 0 1 2,其中0 1 2可能放在不同的节点上
分片副本:将分片复制成主分片和备份分片,主分片和备份分片一定放在不同的服务器上
elasticsearch中哪些节点复制存储数据,哪些节点可以提供查询服务:
es中每个几点都可以成为主节点,每个节点都可以提供存储和查询功能,但是会基于下面两个配置(默认情况都是true):
node.master:若为true,代表这个节点有可以成为主节点的资格,但真正成为主节点需要进行选举流程
node.data:若为true,代表节点可以存储数据。
总结:
对于master来说,一般设置node.data为false,容量一般就可以,对于data节点,容量需要更大,对于client节点,如果只负责处理请求,容量一般也够了
集群状态有三种颜色:
green 所有主要分片和复制分片都可用
yellow 所有主要分片可用,但不是所有复制分片都可用
red 不是所有的主要分片都可用
集群写入原理:
简单流程:
客户端选择一个node发送请求过去,这个node就是coordinating node (协调节点)
coordinating node,对document进行路由,将请求转发给对应的node
实际上的node上的primary shard处理请求,然后将数据同步到replica node
coordinating node,如果发现primary node和所有的replica node都搞定之后,就会返回请求到客户端
数据底层原理:
数据先写到buffer,此时还会写到translog中,此时数据还不能被检索到
buffer满了或者经过一段时间或者手动操作,数据写到OS cache,同时清空buffer,此时数据可以被检索到
然后每隔1秒,就会将OS cache的数据写入到segment file之中,一个索引可以有多个segment文件,多个segment也可以合并一个大的segment
将一个commit point写入到磁盘文件,里面标识着这个commit point对应的所有segment file,默认30分钟进行一次commit
执行完commit操作之后,删除translog
translog作用:
当机器宕机时,提供数据的修复
对于update操作
update操作是软删除插入操作
查询过程
1.请求发送给client节点,然后分发到不同的分片上请求数据
2.分片数据返回给client节点,在client节点进行汇总
3.由于采用的是倒排索引,所以client经过汇总得到的id再去对应的分片拿到document,直接返回给客户即可
更新过程
这个就是用新的数据全部覆盖以前的数据
重新创建一个document并把原来的标记为delete,需要进行删除的时候才会把文件删除
partial update, 就是制定需要更新的字段.
全量是把数据找出来,然后再java代码中进行修改,再放回去.
partial是直接提交需要修改的字段然后直接修改,在一个shard中进行,内部也是全量替换
删除过程
当要进行删除document的时候,只是把它标记为delete,当数据到达一定的时候再进行删除, 有点像JVM中标记清除法
slor与es对比:
Solr利用zk进行分布式管理,es自带分布式协调管理功能
Solr支持更多格式JSON、XML、CSV,eS只支持json
在实时搜索方面es更好,solr在进行CUD时会阻塞
ES与关系型数据库的对比:
Database table Row Column schema SQL
Index Type Document Field mapping DSL
以订单主信息为例:
index:order_info_alias相当于表order_master
type:在es中指的是_doc,不是指字段的type,在mysql中就是表的概念(注:从6.0版本开始,type已经被逐渐废弃,现在一个索引就只能创建一个类型了(_doc))
Document:相当于order_master
Field:orderNo
DSL语法详解:
创建index mapping:
动态映射:
不指定字段的类型映射,此种方式会有问题,比如一些数字字段,不指定类型的情况,可能会成为字符串类型,这种类型在范围查询的时候会有问题
静态映射
指定映射类型,常用
keyword和text区别:
text支持分词,全文检索,支持模糊、精确查询,不支持聚合,排序操作,文档比较大
keyword不进行分词,直接索引,支持模糊、支持精确匹配,支持聚合、排序操作
查询:
query
range:范围
match:可以匹配到经过分词处理的
term:不经过分词处理的
bool:多搜索条件查询
must(必须匹配,类似于数据库的 =),must_not(必须不匹配,类似于数据库的 !=),should(没有强制匹配,类似于数据库的 or),filter(过滤)
filter
aggs聚合
bucket:聚合搜索分组,如销售部门有张三和李四,开发部门有王五和赵六,根据部门分组得到结果就是连个bucket,销售部门bucket有张三李四,开发部门bucket有王五赵六
metric:总计分析,比如求和、求平均值、最小值等
sort排序
Highlight高亮
......
更新
put
删除:
delete
ELK:
fileBeat负责数据采集
kafka负责数据在消息队列存储和消费
logstash负责数据过滤
elasticsearch负责数据存储
kibana负责数据展示
fileBeat和logstash对比:
logstash是golang写的,fileBeat功能较少但是资源消耗也小,logstash资源消耗大,但是提供的功能插件比较多,比如grok插件
es索引原理:
倒排索引,即根据文档拿到Id,再通过id获取文档,正向索引为通过id获取文档
es分词处理器:
默认为字符分词处理器,不支持中文
中文分词处理器为IK分词处理器
IK字典主要包含:
主词典:比如“字典、负责、数据”
停词词典:比如“的,也”
es热更新:
修改IK分词处理器的源码,修改代码在加载字典数据的代码里,通过在数据库中存放自定义的拓展主词典和停用词,连接并且加载数据库中的主词典和停用词实现
es算分原理
基于TF/IDF算法
TF定义:词频,根据在一个文档关键词出现的次数多少,正相关
IDF:在所有文档中出现的次数,反相关
总结:TF-IDF与一个词在文档中的出现次数成正比,与该词在整个语言中的出现次数成反比,实际上就是一堆数学公式
es宽表和窄表:
宽表:字段比较多的表
窄表: 严格按照数据库设计三范式。尽量减少数据冗余,但是缺点是修改一个数据可能需要修改多张表
优缺点:窄表: 严格按照数据库设计三范式。尽量减少数据冗余,但是缺点是修改一个数据可能需要修改多张表
ES的性能调优技巧
配置层面调优:
1.内存锁定
bootstrap.memory_lock:true允许JVM锁住内存,禁止操作系统交换出去。
2.配置故障检测及时间
第一种是由master向集群的所有其他节点发起ping,验证节点是否处于活动状态
第二种是:集群每个节点向master发起ping,判断master是否存活,是否需要发起选举。故障检测需要配置以下设置使用 形如:discovery.zen.fd.ping_interval节点被ping的频率,默认为1s。
discovery.zen.fd.ping_timeout 等待ping响应的时间,默认为 30s,运行的集群中,master 检测所有节点,以及节点检测 master 是否正常。discovery.zen.fd.ping_retries ping失败/超时多少导致节点被视为失败,默认为3
3.适当加大队列数量,当出现GET /_cat/thread_pool,观察api中返回的queue和rejected,如果确实存在队列拒绝或者是持续的queue,可以酌情调整队列size
4.设置indices的内存熔断相关参数,根据实际情况进行调整,防止写入或查询压力过高导致OOM
5.创建shard,阻止新建shard时扫描集群内全部shard的元数据,提升shard分配速度
cluster.routing.allocation.disk.include_relocations: false,默认为true
系统层面
6.关闭交换分区,防止内存发生交换导致性能下降
7.确保配置mmap的最大映射数量,以便有足够的虚拟内存可用于mmapped文件,sysctl -w vm.max_map_count=262144 或者你可以在/etc/sysctl.conf 通过修改 vm.max_map_count 永久设置它
8.磁盘SSD配置正确
Elasticsearch使用方式调优
9.GET /_nodes/hot_threads&interval=30s,抓取30s的节点上占用资源的热线程,如果是merge线程占用了大量的资源,就应该考虑是不是创建index或者刷磁盘间隔太小,批量写入size太小造成的。
10.日志场景一般不需要评分,建议关闭评分
11.tranlog,在数据安全性不是要求很高的情况下可以使用异步写入,"index.translog.durability": "async"
12.禁止动态mapping,禁止使用动态映射
13.批量写入
14.segment merge段合并,如果发现merge占用了大量的资源,可以设置:index.merge.scheduler.max_thread_count:1,或者在低峰时期强制合并,降低segment的数量,减小内存消耗
15.使用alias
16.避免宽表,在索引中定义太多字段是一种可能导致映射爆炸的情况,这可能导致内存不足错误和难以恢复的情况,这个问题可能比预期更常见,index.mapping.total_fields.limit ,默认值是1000
17.避免稀疏索引,因为索引稀疏之后,对应的相邻文档id的delta值会很大,lucene基于文档id做delta编码压缩导致压缩率降低,从而导致索引文件增大。同时,ES的keyword,数组类型采用doc_values结构,每个文档都会占用一定的空间,即使字段是空值,所以稀疏索引会造成磁盘size增大,导致查询和写入效率降低
精度丢失问题及BigDecimal解决原理:
对于十进制整数在转化成二进制数时不会有精度问题,小数时候转换为二进制数为小数乘2取整,知道没有可以乘为止,比如0.9一直乘以2是乘不尽的
那么bigDecimal如何解决精度问题
十进制整数在转化成二进制数时不会有精度问题,小数扩大N倍让它在整数的维度上进行计算,并保留相应的精度信息,比如0.9+1为扩大10倍9+10=19,在获取结果时缩小10倍就是1.9
sharding-jdbc 分库分表的 4种分片策略
标准分片策略:只支持对单个分片健(字段)为依据的分库分表,并提供了两种分片算法 PreciseShardingAlgorithm(精准分片)和 RangeShardingAlgorithm(范围分片)
复合分片策略:不同的是复合分片策略支持对多个分片健操作
行表达式分片策略:使用 Groovy 表达式,如:ds-$->{order_id % 2}
Hint分片策略:自定义的 Hint分片算法类路径
反射安全及性能低下的原因:
安全:类型擦除后无视泛型的安全检测机制
性能低下:反射内部使用的是JNI类似于Unsafe的API,略过了很多JIT编译优化,如逃逸分析中的锁消除、标量替换、栈上分配
泛型上界通配符extends和下界通配符super及与T区别
?和T都可以表示不确定的类型
?用于泛型方法的调用代码和形参,不能用于定义类和泛型方法,可以与上界通配符和下界通配符一起使用
T 是一个类型形参,通常用于泛型类和泛型方法的定义,能与上界通配符使用
具体例子如下:
区别1:通过T来确保泛型参数的一致性
// 通过 T 来 确保 泛型参数的一致性
public
test(List
//通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型
public void
test(List extends Number> dest, List extends Number> src)
区别2:类型参数T可以多重限定而通配符不行
public static
区别3:通配符可以使用超类限定而类型参数不行
类型参数T:
T extends A
通配符?
? extends A
? super A
Class
Class
比如要在某个类中声明如下两种变量
// 可以
public Class> clazz;
// 不可以,因为 T 需要指定类型,如果一定要这样使用,必须要类中也要加伤类型参数T
public Class
还有例如:此段代码在编译期无视泛型的类型搽除,运行期可能会报强转错误
MultiLimit multiLimit = (MultiLimit)Class.forName("com.glmapper.bridge.boot.generic.MultiLimit").newInstance();
使用Class
并发容器实现原理:
ConcurrentHashMap :jdk1.7分段锁,jdk1.8写入如果key为空,采用CAS自旋写入,hashcode == MOVED == -1,则需要进行扩容,前面都不满足,synchronized 写入
CopyOnWriteArrayList:读取的是旧数组,每次修改copy新数组,然后替换旧数组
ConcurrentLinkedQueue:分为阻塞队列和非阻塞队列,阻塞队列可以通过BlockingQueue加锁来实现,非阻塞队列可以通过 CAS 操作实现
ArrayBlockingQueue:有界队列,并发控制采用可重入锁 ReentrantLock,ReentrantLock 底层采用AQS,AQS基于JNI的CAS
LinkedBlockingQueue:单向链表,无界队列
PriorityBlockingQueue:优先级队列,并发控制采用的是可重入锁 ReentrantLock
ConcurrentSkipListMap:多个链表+局部加锁
Semaphore、CountDownLatch 、CyclicBarrier
Semaphore(信号量)可以指定多个线程同时访问某个资源
CountDownLatch:count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕
CyclicBarrier:多组的CountDownLatch
ReentrantLock 和 ReentrantReadWriteLock
ReentrantReadWriteLock 可以保证多个线程可以同时读
各个版本JDK默认的垃圾回收器
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器G1
Linux top命令的load average
load average分别代表最近一分钟、最近5分钟、最近15分钟的负载(占用的CPU核心数)
例子:如果是单核CPU,负载等于1就是满负荷运转了,如果是四核、甚至更多核心的CPU,负载大于1这说明系统当前负载很小,不需要过多关心
1.查看cpu核数
cat /proc/cpuinfo |grep "cpu core"|wc -l
2.看一下是否开启超线程
cat /proc/cpuinfo | grep "processor" | wc -l
云原生:
概念:云原生是基于分布部署和统一运管的分布式云 ,以容器、微服务、DevOps等技术为基础建立的一套云技术产品体系。
通俗讲:使用云原生技术,开发者不需要考虑技术底层实现,就可以实现快速部署、按需伸缩、不停机交付,就是一个运维的运管系统
canal与ZooKeeper整合高可用
canal-server:将自己伪装成mysql slave的服务节点
ZooKeeper:实现了服务协调和故障转移,ZooKeeper监听两个路径:一个表示当前正在工作的服务端节点,一个表示实例集群的列表
监听正在工作的服务端节点的数据变化。这数据内容就是正在工作的服务端的IP和端口号,一旦发生变化,说明server换了,那客户端会重新建立一个和新地址的socket链接
监听针对某个canal实例下列表子节点个数变化。这个列表的内容实际就是某个实例的工作集群,当正在工作的server节点丢失时,会从这里随机抽一个
结论:ZooKeeper具有注册中心的作用,客户端可以对服务端地址进行动态获取
联动云pom版本号:
rocketmq:4.3.0
es:6.8.2
mysql:5.1.42
redis:2.9.0
canal:0.0.1
dubbo:2.5.4
zk:3.5.4
C10k和100
性能压测监控脚本,使用visualVM进行监控:
-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.port=8991 -Djava.rmi.server.hostname=10.91.18.125 -Dworker.id=1 cn.openlo.starter.BoxStartupStandalone
容器启动和重启:
重启nacos或docker容器中的其他服务: docker-compose down && docker-compose up -d
进入docker容器
docker ps找到容器id
docker exec -it 容器id /bin/bash
修改docker容器时间(禁止修改服务器时间,代码雪花算法生成器会报错)
1.进入docker容器
2.date -s 08/24/2006
date -s 13:02:00
nginx重启:sudo ./nginx -s reload
情况maven缓存:mvn clean install -U
nacos问题:
- "9849:9849"
- "9848:9848"
docker加载新的tar
docker load -i nova-ess-ps.tar
docker替换新镜像
image: 172.20.8.203/hairounova-test/nova-ess-p:feature_big_and_small_dev_4.0.0-221011182127
hosts文件位置:C:\Windows\System32\drivers\etc
docker运行的mysql容器配置查找:https://blog.csdn.net/CharisJ/article/details/123736216
docker ps
docker exec -it 容器id /bin/bash
which mysql
/usr/bin/mysql --verbose --help|grep -A 1 'Default options'
生成binlog日志:
mysqlbinlog --no-defaults --database=databaseName --base64-output=decode-rows -vv --skip-gtids=true --start-datetime='2022-08-09 00:00:00'
--stop-datetime='2022-09-11 00:00:00' /data/binlog.000023 > /binlog/sql.log
mysqlbinlog --no-defaults --base64-output=decode-rows -v binlog.000023> mytestdb.sql
mysql命令之锁表排查
-- 查询是否锁表
show open tables where in_use > 0;
-- 查询进程(如果您有SUPER权限,您可以看到所有线程。否则,您只能看到您自己的线程)
show processlist;
-- 杀死进程(就是[show processlist;]命令的id列)
kill xx;
-- 查看正在锁的事务
select * from information_schema.INNODB_LOCKS;
-- 杀死进程id(就是[select * from information_schema.INNODB_LOCKS; ]命令的trx_mysql_thread_id列)
kill 线程ID
-- 查看等待锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
-- 查看服务器状态
show status like '%lock%';
-- 查看超时时间:
show variables like '%timeout%';
--
SHOW STATUS LIKE 'Threads%';
--
show full processlist;
-- 查看最大连接数
show variables like '%max_connections%';
-- 查看每一个用户的最大连接数
show variables like '%max_user_connections%';
-- 查看当前连接中各个用户的连接数
select USER , count(*) from information_schema.processlist group by USER;
-- 查看当前连接中各个IP的连接数
select SUBSTRING_INDEX(host,':',1) as ip , count(*) from information_schema.processlist group by ip;
-- 查看当前连接中连接时间最长的的连接
select host,user,time,state,info from information_schema.processlist order by time desc limit 10;
-- 查询线上Mysql数据库的连接数配置
show variables like '%conn%';
java企业级工具介绍
IDEjava 开发工具:
idea
eclipse
SDS
文本编辑工具:
notepad++
LogView
Linux运维工具
xshell
xsftp
MobaXterm
FinalShell
数据库运维工具
navicat
DB2
Sqlyog
浏览器:
谷歌
火狐
画图工具
亿图
Visio
比较工具
beyond compare
远程工具
teamView
向日葵
redis
redis-desktop-manager
mongodb
Robo3T
虚拟机工具
Vmware
远程调用工具
Postman
框架
akka
Spring
SpringBoot
SpringCloud
....
数据库管理版本工具:
liquibase
DMS
代码扫描工具
SonarLint
FindBugs
PMD
代码版本管理工具
git
SVN
修改host工具
switchHosts.exe
Spring线程池的链路追踪及上下文传递context
线程池使用org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
链路追踪使用:org.springframework.cloud.sleuth.instrument.async.LazyTraceExecutor
上下文传递使用TtlExcutor或者使用TaskDecorator功能
windows设置虚拟机代理端口号
windows打开 powerShell 输入
netsh interface portproxy add v4tov4 listenport=8848 connectaddress=nova-vm connectport=8848
就是访问windows的8848等于访问虚拟机端口8848
查看代理的端口 netsh interface portproxy show all
删除代理端口 netsh interface portproxy delete v4tov4 listenport=80
添加代理端口
netsh interface portproxy add v4tov4 listenport=80 connectaddress=192.168.8.135 connectport=80
gradle切换成maven加载问题:
双击shift,搜索Maven;
选择Add maven Projects;
选择此项目的pom.xml文件,点击OK;
等待idea自动识别maven项目之后,右侧自动出现Maven图标。
如果出现关联project关联有问题,检查
File--Maven--ignored Files
File --> Invalidate Caches/Restart 清空缓存
graphql
GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。直译过来就是图查询语言,所以当他在处理图状数据的时候,会有很大的优势
graphql优缺点
优点
GraphQL 更快
最适合复杂系统和微服务
层次结构清晰与客户端和服务器端代码共享
缺点
无速率限制
查询深度和复杂度可能会比较深,存在低效请求
缓存实现复杂
graphql总结:
graphql结构就决定了应用场景更多用于与微服务结合,且更多适合做查询的业务
Vue环境安装:idea运行自己创建的vue文件(从头搭建VUE环境使用IDEA创建VUE项目)
新技术栈:
new:输送线vertx框架(底层netty异步框架)、play2框架、akka框架、中台siddhi、权限验证auth0+shiro、数据存储leveldb+redis+proto buffer、数据传输:ws、http、grpc
old:docker、springboot、nacos、redis+redisson、kafka、mysql、gradle+maven、jpa+mybatis、galileo、liquibase
公共:excel poi、lombok、jackson、swagger