写这篇文章的目的是为了在摸鱼或者备考时打开看一看,感受一下计算机知识海洋的浩瀚。本文部分内容是我自己的理解也有部分是网络上我认为总结的比较好进而抄录整理得来。
这里只列举几个我认为比较重要的区别:
带宽优化及网络连接的使用
1.0: 存在 TCP连接不能复用 以及 head of line blocking(由于连接数受限制且无法复用,所以后续请求会被前面的失败或者高延迟请求阻塞)的问题
1.1:支持持久连接,HTTP 1.1还允许客户端不用等待上一次请求结果返回,就可以发出下一次请求
2.0:多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。这是为了解决1.1 协议中浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制的问题。2.0在TCP之上还进行了二进制分帧,会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码 ,其中 HTTP1.x 的首部信息会被封装到 HEADER frame,而相应的 Request Body 则封装到 DATA frame 里面
OSI | TCP/IP | 描述 | 常见协议 | 设备 |
---|---|---|---|---|
应用层 | 应用层 | 为用户直接提供各种网络服务。 | HTTP,FTP | |
表示层 | 用于应用层数据的编码和转换功能 | telnet | ||
会话层 | 负责建立、管理和终止表示层实体之间的通信会话 | DNS | ||
传输层 | 传输层 | 建立了主机端到端的链接 | TCP,UDP | 四层交换机、四层路由器 |
网络层 | 网络层 | IP寻址来建立两个节点之间的连接 | IP ICMP | 路由器、三层交换机 |
链路层 | 链路层 | 使用链路层地址 (以太网使用MAC地址)来访问介质。 | ARP RARP PPP | 网桥、以太网交换机、网卡 |
物理层 | 物理层 | 通过物理介质传输比特流 | MLT-3 | 中继器、集线器、双绞线 |
因为TCP/IP五层模型更在意围绕TCP/IP协议展开的一系列通信协议的分层,因此它不包括其他一些不想干的协议。
UDP | TCP | |
---|---|---|
是否连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输,使用流量控制和拥塞控制 |
连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 |
传输方式 | 面向报文 | 面向字节流 |
首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
适用场景 | 适用于实时应用(IP电话、视频会议、直播等) | 适用于要求可靠传输的应用,例如文件传输 |
概念
TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾,出现粘包的原因是多方面的,可能是来自发送方,也可能是来自接收方。
产生原因
(1)发送方原因
TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量),而Nagle算法主要做两件事:
只有上一个分组得到确认,才会发送下一个分组收集多个小分组,在一个确认到来时一起发送。Nagle算法造成了发送方可能会出现粘包问题
(2)接收方原因
TCP接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。实际上,TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。
如何解决
(1)发送方
对于发送方造成的粘包问题,可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭算法。
(2)接收方
接收方没有办法来处理粘包现象,只能将问题交给应用层来处理。
(2)应用层
应用层的解决办法简单可行,不仅能解决接收方的粘包问题,还可以解决发送方的粘包问题。
解决办法:
格式化数据:每条数据有固定的格式(开始符,结束符),这种方法简单易行,但是选择开始符和结束符时一定要确保每条数据的内部不包含开始符和结束符。
发送长度:发送每条数据时,将数据的长度一并发送,例如规定数据的前4位是数据的长度,应用层在处理时可以根据长度来判断每个分组的开始和结束位置。
| 以太网首部 | IP首部 | TCP首部 | 应用数据 | 以太网尾部 |
三个拥塞控制机制
慢启动
加法式增加/乘法式减少拥塞窗口
快速重发和快速恢复
线性增加发送量
对网络接近满载来说是一个正确的方法;
但对从零开始起步的连接来说时间又太长;
Congestion Window (CW)
源端TCP为每个连接维护的一个状态变量;
限制在给定时间内容许传输的数据量,慢启动的最大值也受到CongestionWindow影响
容许的最大未确认字节数就是现在的CongestionWindow和AdvertisedWindow小的那个
MaxWindow = MIN(CongestionWindow, AdvertisedWindow)
可以发送的窗口大小,为可能新增的未确认字节数:
EffectiveWindow = MaxWindow - (LastByte - LastByteAcked)
但发生拥塞的时候一般CW < EffectiveWindow
TCP源慢启动之后:
如果超过ssthresh,可能会导致当拥塞程度上升就线性CW
当拥塞程度下降(收到ACK)时,就增加CW
上述二者合在一起就叫:加法式增加/乘法式减少 AIMD
观察主要原因:
包未交付,并导致duplicated ACK或超时,判定为拥塞丢包!
由于传输错误丢包是很少的!
TCP把收到1)duplicated ACK(对已经确认的字节再次确认),2)超时 两种现象解释为拥塞的信号
Tahoe TCP不区分这两种情况
sshthresh = cwnd /2
cwnd 重置为 1
进入慢启动过程
这就是乘法减少机制的一部分
Reno TCP超时的策略不变,但是面对duplicated ACK的实现是:
进入拥塞避免
cwnd = cwnd /2
ssthresh = cwnd
进入快速恢复算法——Fast Recovery
Tahoe引入快速重传,Reno引入快速恢复
快速重传
每次接收方收一个数据包
接收方响应一个ACK;(早先规定,后来优化)
当包到达失序,TCP因为较早的数据还没有到达,也不能应答含有这些包的数据;
TCP发送它上次发送了的那个相同的ACK
相同ACK的第二次发送称着Duplicate ACK,
当发送方看到重复ACK,知道对方收到了一个失序包,这说明这个ACK序号后面的包丢失了
实际情况中
发送方TCP等到3个Duplicate ACK后就重发丢失的包;即快速重传,比等待报文超时要快
快速恢复
当快速重传同时快速恢复(Duplicated ACK)
Fast Recovery算法如下:
三次握手的目的是为了确保双方都准备好,才建立连接。防止失效的连接请求(在网络节点中滞留时间长),导致客户端重复发起请求,造成错误和资源浪费。连接->确认->确认
四次挥手是为了保证被动关闭的一方可以将自己的数据发送完。关闭->确认->发送数据 ->关闭->确认
由上面的变迁图,首先调用close()发起主动关闭的一方,在发送最后一个ACK之后会进入time_wait的状态,也就说该发送方会保持2MSL时间之后才会回到初始状态。MSL值得是数据包在网络中的最大生存时间。产生这种结果使得这个TCP连接在2MSL连接等待期间,定义这个连接的四元组(客户端IP地址和端口,服务端IP地址和端口号)不能被使用。
1)为实现TCP全双工连接的可靠释放
由TCP状态变迁图可知,假设发起主动关闭的一方(client)最后发送的ACK在网络中丢失,由于TCP协议的重传机制,执行被动关闭的一方(server)将会重发其FIN,在该FIN到达client之前,client必须维护这条连接状态,也就说这条TCP连接所对应的资源(client方的local_ip,local_port)不能被立即释放或重新分配,直到另一方重发的FIN达到之后,client重发ACK后,经过2MSL时间周期没有再收到另一方的FIN之后,该TCP连接才能恢复初始的CLOSED状态。如果主动关闭一方不维护这样一个TIME_WAIT状态,那么当被动关闭一方重发的FIN到达时,主动关闭一方的TCP传输层会用RST包响应对方,这会被对方认为是有错误发生,然而这事实上只是正常的关闭连接过程,并非异常。
2)为使旧的数据包在网络因过期而消失
为说明这个问题,我们先假设TCP协议中不存在TIME_WAIT状态的限制,再假设当前有一条TCP连接:(local_ip, local_port, remote_ip,remote_port),因某些原因,我们先关闭,接着很快以相同的四元组建立一条新连接。本文前面介绍过,TCP连接由四元组唯一标识,因此,在我们假设的情况中,TCP协议栈是无法区分前后两条TCP连接的不同的,在它看来,这根本就是同一条连接,中间先释放再建立的过程对其来说是“感知”不到的。这样就可能发生这样的情况:前一条TCP连接由local peer发送的数据到达remote peer后,会被该remot peer的TCP传输层当做当前TCP连接的正常数据接收并向上传递至应用层(而事实上,在我们假设的场景下,这些旧数据到达remote peer前,旧连接已断开且一条由相同四元组构成的新TCP连接已建立,因此,这些旧数据是不应该被向上传递至应用层的),从而引起数据错乱进而导致各种无法预知的诡异现象。作为一种可靠的传输协议,TCP必须在协议层面考虑并避免这种情况的发生,这正是TIME_WAIT状态存在的第2个原因。
3)总结
具体而言,local peer主动调用close后,此时的TCP连接进入TIME_WAIT状态,处于该状态下的TCP连接不能立即以同样的四元组建立新连接,即发起active close的那方占用的local port在TIME_WAIT期间不能再被重新分配。由于TIME_WAIT状态持续时间为2MSL,这样保证了旧TCP连接双工链路中的旧数据包均因过期(超过MSL)而消失,此后,就可以用相同的四元组建立一条新连接而不会发生前后两次连接数据错乱的情况。
3.time_wait状态如何避免
首先服务器可以设置SO_REUSEADDR套接字选项来通知内核,如果端口忙,但TCP连接位于TIME_WAIT状态时可以重用端口。在一个非常有用的场景就是,如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时SO_REUSEADDR选项就可以避免TIME_WAIT状态。
主动关闭连接的一方必须维持TIME_WAIT状态等待FIN防止被动方重发导致抛出connection reset SocketException
。因此在处理大量短连接的时候可能导致这一现象。
A(攻击者)发送TCP SYN,SYN是TCP三次握手中的第一个数据包,而当这个服务器返回ACK以后,A不再进行确认,那这个连接就处在了一个挂起的状态,也就是半连接的意思,那么服务器收不到再确认的一个消息,还会重复发送ACK给A。这样一来就会更加浪费服务器的资源。A就对服务器发送非法大量的这种TCP连接,由于每一个都没法完成握手的机制,所以它就会消耗服务器的内存最后可能导致服务器死机,就无法正常工作了。更进一步说,如果这些半连接的握手请求是恶意程序发出,并且持续不断,那么就会导致服务端较长时间内丧失服务功能——这样就形成了DoS攻击。这种攻击方式就称为SYN泛洪攻击
如何防范?
最常用的一个手段就是优化主机系统设置。比如降低SYN timeout时间,使得主机尽快释放半连接的占用或者采用SYN cookie设置,如果短时间内收到了某个IP的重复SYN请求,我们就认为受到了攻击。我们合理的采用防火墙设置等外部网络也可以进行拦截
websocket相当于HTTP的一个补充协议,通过http request建立连接,不需要再发送request,之后保持一端与另外一端的TCP连接。
直接使用WebSocket(或SockJS)就很类似于使用TCP套接字来编写Web应用。STOMP在WebSocket之上提供了一个基于帧的线路格式(frame-based wire format)层,用来定义消息的语义。与HTTP请求和响应类似,STOMP帧由命令、一个或多个头信息以及负载所组成。例如,如下就是发送数据的一个STOMP帧:
SEND
destination:/app/marco
content-length:20
{\"message\":\"Marco!\"}
SSL (Secure Sockets Layer) 是用来保障你的浏览器和网站服务器之间安全通信,免受网络“中间人”窃取信息。
1、OPTIONS
返回服务器针对特定资源所支持的HTTP请求方法,也可以利用向web服务器发送‘*’的请求来测试服务器的功能性
2、HEAD
向服务器索与GET请求相一致的响应,只不过响应体将不会被返回。这一方法可以再不必传输整个响应内容的情况下,就可以获取包含在响应小消息头中的元信息
3、GET
向特定的资源发出请求
4、POST
向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
5、PUT
向指定资源位置上传其最新内容
6、DELETE
请求服务器删除Request-URL所标识的资源
7、TRACE
回显服务器收到的请求,主要用于测试或诊断
8、CONNECT
HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器
1xx:临时响应(Informational),需要请求者继续执行操作的状态代码,表示服务器正在接受请求。
2xx:成功状态码(Success),已成功接受客户端请求。
3xx:重定向状态码(Redirection),需要客户端做进一步操作来完成请求。
4xx:客户端错误(Client Error),客户端请求出错导致服务端无法正常完成请求。
5xx:服务端错误(Server Error),服务器出错未能成功处理服务端请求。
简单说下就是http 1.0里面302具有二义性,在http 1.1中加入303和307就是为了消除二义性。在HTTP 1.0的时候,302的规范是原请求是post不可以自动重定向,但是服务器和浏览器的实现是运行重定向。把HTTP 1.0规范中302的规范和实现拆分开,分别赋予HTTP 1.1中303和307,因此在HTTP 1.1中,303继承了HTTP 1.0中302的实现(即原请求是post,也允许自动进行重定向,结果是无论原请求是get还是post,都可以自动进行重定向),而307则继承了HTTP 1.0中302的规范(即如果原请求是post,则不允许进行自动重定向,结果是post不重定向,get可以自动重定向)。
HTTP请求报文由3部分组成( 请求行+请求头+请求体 )下面是一个完整的示例:
POST /a/b.html HTTP/1.1
Accept:image/jpeg
Refer:http://a.com/b.html
Accept-Language:zh-CN
Content-Length:110
name=tome&pass=2345
请求行: 包括 请求方法、对应URL、HTTP协议与版本
报文头:包含若干个属性,格式为“属性名:属性值”,服务端据此获取客户端的信息。
报文体:它将一个页面表单中的组件值通过param1=value1¶m2=value2的键值对形式编码成一个格式化串,它承载多个请求参数的数据。
HTTP的响应报文也由三部分组成( 响应行+响应头+响应体 )
HTTP/1.1 200 OK
Server:Appache-Coyote/1.1
Content-Type:application/json
6f
{
"password":"1234"
}
1、session保存在服务器,客户端不知道其中的信息;cookie保存在客户端,服务器能够知道其中的信息
2、session中保存的是对象,cookie中保存的是字符串
3、session不能区分路径,同一个用户在访问一个网站期间,所有的session在任何一个地方都可以访问到。而cookie中如果设置了路径参数,那么同一个网站中不同路径下的cookie互相是访问不到的
4、session需要借助cookie才能正常工作。如果客户端完全禁止cookie,session将失效。
如何解决分布式session问题?
表示性状态转移(representation state transfer)。简单来说,就是用URI表示资源,用HTTP方法(GET, POST, PUT, DELETE)表征对这些资源的操作。
进程是系统进行资源分配和调度的一个独立单位
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源
协程,是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
协程在子程序内部是可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行。
协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;
不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
参考了 https://blog.csdn.net/weixin_46416295/article/details/109262143
线程同步
如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样 的,而且其他的变量的值也和预期的是一样的,就是线程安全的
线程同步
Java中提供了同步机制 (synchronized) 来解决。有三种方式完成同步操作:
线程状态
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中, 有几种状态呢?在API中 java.lang.Thread.State这个枚举中给出了六种线程状态:
线程状态 | 导致状态发生条件 |
---|---|
NEW(新建) | 线程刚被创建,但是并未启动。还没调用start方法。 |
Runnable(可运行) | 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。 |
Blocked(锁阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状 态;当该线程持有锁时,该线程将变成Runnable状态。 |
Waiting(无限等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个 状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。 |
Timed Waiting(计时等待) | 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态 将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、 Object.wait。 |
Teminated(被终止) | 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。 |
java实现多线程有四种方式:继承Thread类、实现Runnable()接口、实现Callable接口、通过线程池启动多线程(实现runable)。
Callable通过FutureTask可以有返回值,而且可以抛出异常。
方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程
正常情况下,如果不做特殊的处理,在主线程中是不能够捕获到子线程中的异常的。如果想要在主线程中捕获子线程的异常,我们需要使用ExecutorService
:
ExecutorService exec = Executors.newCachedThreadPool(new HandleThreadFactory());
将我们自定义的工厂类传入。t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandle());
异常处理。Thread.UncaughtExceptionHandler
中的uncaughtException(Thread t, Throwable e)
方法。如果不需要每个线程单独设置异常处理的话,可以使用Thread的静态方法:Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandle());
wait方法的作用是将当前运行的线程挂起(即让其进入阻塞状态),直到notify或notifyAll方法来唤醒线程
只要在同一对象上去调用notify/notifyAll方法,就可以唤醒对应对象monitor上等待的线程了。
sleep方法的作用是让当前线程暂停指定的时间(毫秒),sleep方法是最简单的方法,唯一需要注意的是其与wait方法的区别。最简单的区别是,wait方法依赖于同步,而sleep方法可以直接调用。而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁。
yield方法的作用是暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止。yield方法只是将Running状态转变为Runnable状态。
join方法的作用是父线程等待子线程执行完成后再执行,换句话说就是将异步执行的线程合并为同步的线程。
obj.notifyAll()方法唤醒所有阻塞在 obj 对象上的沉睡线程,然后被唤醒的众多线程竞争 obj 对象的 monitor 占有权,最终得到的那个线程会继续执行下去,但其他线程继续阻塞。
每个对象都有一个唯一与之对应的内部锁(Monitor)。这里就涉及到锁池和等待池的概念。当多个线程想要获取某个对象的锁,而该对象已经被其他线程拥有,这些线程就会进入该对象的锁池等待锁的释放。当一个线程主动调用wait方法,就会进入等待池不会和其他线程竞争锁的持有。所以这时候就需要调用notify或者notifyAll方法,让该线程进入锁池重新参与锁的争取。notify只会随机选取一个线程,notifyAll则会将所有等待池中的线程加入到锁池。
线程池创建的三种方式。
singleThreadExecutor 创建单个线程
newFixedThreadpool 创建固定线程个数的线程
newCacheThreadpool 可伸缩
7大参数是使用原生线程池创建线程使用的参数。
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
1, //核心线程数
3, //最大线程数
3, //超时时间
TimeUnit.SECONDS,//时间单位
new LinkedBlockingDeque<>(3),//阻塞队列
Executors.defaultThreadFactory(),//一个线程工厂,一般使用默认的就OK
new ThreadPoolExecutor.AbortPolicy()//一种拒绝策略
);
核心线程指的是:在线程中默认创建的线程
最大线程:见名知意指的就是最大的线程数
阻塞队列:用于核心线程池满时,让需要"服务"的线程等待的地方
拒绝策略:指的是最大线程池满,阻塞队列也满的时候,新来的线程如何处置。
4种拒绝策略
new ThreadPoolExecuor.AbortPolicy :不处理这个线程,并抛出异常
new ThreadPoolExecuor.CallerRunsPolicy:这个多的线程不处理,而是交给创建这个线程的去处理
new ThreadPoolExecuor.DiscardPolicy:不处理这个线程,也不抛出异常
new ThreadPoolExecuor.DiscardOldestPolicy:尝试和最老的线程进行竞争,也不会抛出异常
参考:https://blog.csdn.net/ly199108171231/article/details/88098614这篇文章介绍的非常好,下面是对它内容的一些概述。
悲观锁和独占锁是一个意思,它假设一定会发生冲突,因此获取到锁之后会阻塞其他等待线程。这么做的好处是简单安全,但是挂起线程和恢复线程都需要转入内核态进行,这样做会带来很大的性能开销。悲观锁的代表是 synchronized。然而在真实环境中,大部分时候都不会产生冲突。悲观锁会造成很大的浪费。而乐观锁不一样,它假设不会产生冲突,先去尝试执行某项操作,失败了再进行其他处理(一般都是不断循环重试)。这种锁不会阻塞其他的线程,也不涉及上下文切换,性能开销小。代表实现是 CAS
Java 中的并发锁大致分为隐式锁和显式锁两种。隐式锁就是我们最常使用的 synchronized 关键字,显式锁主要包含两个接口:Lock 和 ReadWriteLock,主要实现类分别为ReentrantLock 和 ReentrantReadWriteLock,这两个类都是基于AQS(AbstractQueuedSynchronizer) 实现的
CAS是 compare and swap 的简写,即比较并交换。它是指一种操作机制。在 Unsafe 类中,调用代码如下:
unsafe.compareAndSwapInt(this, valueOffset, expect, update);
复制代码它需要三个参数,分别是内存位置 V,旧的预期值 A 和新的值 B。操作时,先从内存位置读取到值,然后和预期值A比较。如果相等,则将此内存位置的值改为新值 B,返回 true。如果不相等,说明和其他线程冲突了,则不做任何改变,返回 false。
这种机制在不阻塞其他线程的情况下避免了并发冲突,比独占锁的性能高很多。 CAS 在 Java 的原子类和并发包中有大量使用。CAS 底层是靠调用 CPU 指令集的 cmpxchg 完成的,当一个 CPU 核心将内存区域的数据读取到自己的缓存区后,它会锁定缓存对应的内存区域。锁住期间,其他核心无法操作这块内存区域。CAS 就是通过这种方式(缓存锁)实现比较和交换操作的原子性的。
ReentrantLock内部有两个内部类,分别是 FairSync 和 NoFairSync,对应公平锁和非公平锁。他们都继承自 Sync。Sync 又继承自AQS。
请求锁时有三种可能:
如果没有线程持有锁,则请求成功,当前线程直接获取到锁。
如果当前线程已经持有锁,则使用 CAS 将 state 值加1,表示自己再次申请了锁,释放锁时减1。这就是可重入性的实现。
如果由其他线程持有锁,那么将自己添加进等待队列。
理解 ReentrantLock 和 AQS 之后,再来理解读写锁就很简单了。读写锁有一个读锁和一个写锁,分别对应读操作和锁操作。锁的特性如下:
只有一个线程可以获取到写锁。在获取写锁时,只有没有任何线程持有任何锁才能获取成功;
如果有线程正持有写锁,其他任何线程都获取不到任何锁;
没有线程持有写锁时,可以有多个线程获取到读锁。
我们假设有两个线程同时使用CAS操作对同一个值为A的变量进行修改。
线程1:A -> B -> A
线程2:A -> C
由于线程2获取A之后,线程1对A进行了修改,所以理论上线程2对于C的修改是应该失败的,但由于线程2在写入C的时候的预期值是A所以这个操作最后成功了。ABA问题实际上就是说虽然CAS能够对预期值进行估计,但是却无法判断相同的预期值是否发生了修改。因此可以使用版本号来解决这个问题。
synchronized 是重量级锁,由于消耗太大,虚拟机对其做了一些优化。
自旋锁与自适应自旋
在许多应用中,锁定状态只会持续很短的时间,为了这么一点时间去挂起恢复线程,不值得。我们可以让等待线程执行一定次数的循环,在循环中去获取锁。这项技术称为自旋锁,它可以节省系统切换线程的消耗,但仍然要占用处理器。在 JDK1.4.2 中,自选的次数可以通过参数来控制。 JDK 1.6又引入了自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
锁消除
虚拟机在运行时,如果发现一段被锁住的代码中不可能存在共享数据,就会将这个锁清除。
锁粗化
当虚拟机检测到有一串零碎的操作都对同一个对象加锁时,会把锁扩展到整个操作序列外部。如 StringBuffer 的 append 操作。
轻量级锁
对绝大部分的锁来说,在整个同步周期内都不存在竞争。如果没有竞争,轻量级锁可以使用 CAS 操作避免使用互斥量的开销。
偏向锁
偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁。
简单概括volatile,它能够使变量在值发生改变时能尽快地让其他线程知道。
如果要了解volatile就必须先了解java的内存模型:
(1)每个线程都有自己的本地内存空间(java栈中的帧)。线程执行时,先把变量从内存读到线程自己的本地内存空间,然后对变量进行操作。
(2)对该变量操作完成后,在某个时间再把变量刷新回主内存。
volatile和synchronized区别
ThreadLocal是线程本地存储的一种实现方案。它并不是一个Thread,我们也可以称之为线程局部变量,在多线程并发访问时,ThreadLocal类为每个使用该变量的线程都创建一个变量值的副本,每一个线程都可以独立地改变自己的副本,而不会和其他线程的副本发生冲突。从线程的角度来看,就感觉像是每个线程都完全拥有该变量一样。
ThreadLocal的使用场合主要用来解决多线程情况下对数据的读取因线程并发而产生数据不一致的问题。ThreadLocal为每个线程中并发访问的数据提供一个本地副本,然后通过对这个本地副本的访问来执行具体的业务逻辑操作,这样就可以大大减少线程并发控制的复杂度;然而这样做也需要付出一定的代价,需要耗费一部分内存资源,但是相比于线程同步所带来的性能消耗还是要好上那么一点点。
链接:https://www.jianshu.com/p/ae653c30b4d9
例如:
//线程本地存储变量
private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
这样我们在任何时候操作THREAD_LOCAL_NUM.set(n);
都只操作了当前线程副本里的值,而不会影响其他线程。
和synchronized区别
在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用CPU。
在硬件层面,CPU会将接收到的一批指令按照其规则重排序,同样是基于CPU速度比缓存速度快的原因,和上一点的目的类似,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机可以在更大层面、更多指令范围内重排序。硬件的重排序机制参见《从JVM并发看CPU内存指令重排序(Memory Reordering)》
Java提供了两个关键字volatile
和synchronized
来保证多线程之间操作的有序性,volatile
关键字本身通过加入内存屏障来禁止指令的重排序,而synchronized
关键字通过一个变量在同一时间只允许有一个线程对其进行加锁的规则来实现。在单线程程序中,不会发生“指令重排”和“工作内存和主内存同步延迟”现象,只在多线程程序中出现。
对于final域,编译器和处理器要遵守两个重排序规则:
反射的常用类和函数:Java反射机制的实现要借助于4个类:Class,Constructor,Field,Method;其中class代表的是类对象,Constructor-类的构造器对象,Field-类的属性对象,Method-类的方法对象,通过这四个对象我们可以粗略的看到一个类的各个组成部分。
Class.forName 会尝试先去方法区(1.8以后就是 Metaspace) 中寻找有没有对应的类,如果没有则在 classpath 中找到对应的class文件, 通过类加载器将其加载到内存里。注意这个方法涉及本地方法调用,这里本地方法调用的切换、类的搜索以及类的加载都存在开销;newInstance ,开辟内存空间,实例化已经找到的 Class;getDeclaredMethod/getMethod 遍历 Class 的方法,以方法签名匹配出所需要的 Method 实例,也是比较重的开销所在;虽然 java.lang.Class 内部维护了一套名为 reflectionData 的数据结构,其本身是 SoftReference 的。Class 会将匹配成功的 method 缓存到这里,以供下次访问命中,这样一来会减轻 method 遍历的开销,但是会增加额外的堆空间消耗(以空间置换时间)
我们可以看到,invoke 方法的参数是 Object[] 类型,也就是说,如果方法参数是简单类型的话,需要在此转化成 Object 类型,例如 long ,在 javac compile 的时候 用了Long.valueOf() 转型,也就大量了生成了Long 的 Object, 同时 传入的参数是Object[]数值,那还需要额外封装object数组。
而在上面 MethodAccessorGenerator#emitInvoke 方法里我们看到,生成的字节码时,会把参数数组拆解开来,把参数恢复到没有被 Object[] 包装前的样子,同时还要对参数做校验,这里就涉及到了解封操作。
因此,在反射调用的时候,因为封装和解封,产生了额外的不必要的内存浪费,当调用次数达到一定量的时候,还会导致 GC。
通过上面的源码分析,我们会发现,反射时每次调用都必须检查方法的可见性(在 Method.invoke 里)
反射时也必须检查每个实际参数与形式参数的类型匹配性(在NativeMethodAccessorImpl.invoke0 里或者生成的 Java 版 MethodAccessor.invoke 里);
Method#invoke 就像是个独木桥一样,各处的反射调用都要挤过去,在调用点上收集到的类型信息就会很乱,影响内联程序的判断,使得 Method.invoke() 自身难以被内联到调用方。参见 http://www.iteye.com/blog/rednax…
另外,JDK 开发者们也给我们提供了一套优化思路,既然反射调用有性能问题,不好优化,那能不能将反射调用变成普通调用呢?inflation 正是这条思路的解决方案。通过类字节码的生成并加载,实现了 inflation 。好处就是这不仅减少了本地方法和普通方法来回切换的开销,变成普通方法调用后还能享受到 JIT 编译器的优化福利,其中方法内联是最重要的优化点。编译器采集足够多的运行时数据后,根据统计模型得知某个反射方法成为热点,此时该反射方法体有几率会被内联进调用者的方法体中。
常见的内存泄漏及解决方法
1、单例造成的内存泄漏
2、非静态内部类创建静态实例造成的内存泄漏
因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,从而导致Activity的内存资源不能被正常回收。
解决方法:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例
3、资源未关闭造成的内存泄漏
4、集合容器中的内存泄露
AQS,AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件。
AQS的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。 AQS使用一个int类型的成员变量state来表示同步状态,当state>0时表示已经获取了锁,当state = 0时表示释放了锁。它提供了三个方法(getState()、setState(int newState)、compareAndSetState(int expect,int update))来对同步状态state进行操作,当然AQS可以确保对state的操作是安全的。 AQS通过内置的FIFO同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态失败(锁)时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
参考:https://www.cnblogs.com/jackion5/p/12932343.html
CountDownLatch可以理解为是同步计数器,作用是允许一个或多个线程等待其他线程执行完成之后才继续执行,比如打dota、LoL或者王者荣耀时,创建了一个五人房,只有当五个玩家都准备了之后,游戏才能正式开始,否则游戏主线程会一直等待着直到玩家全部准备。在玩家没准备之前,游戏主线程会一直处于等待状态。如果把CountDownLatch比做此场景都话,相当于开始定义了匹配游戏需要5个线程,只有当5个线程都准备完成了之后,主线程才会开始进行匹配操作。
public static void countDownLatchTest() throws Exception{
CountDownLatch latch = new CountDownLatch(5);//定义了需要达到条件都线程为5个线程
new Thread(new Runnable() {
@Override
public void run() {
for (int i=0; i<12; i++){
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
long count = latch.getCount();
latch.countDown();/**相当于准备游戏成功*/
if(count > 0) {
System.out.println("线程" + Thread.currentThread().getName() + "组队准备,还需等待" + latch.getCount() + "人准备");
}else {
System.out.println("线程" + Thread.currentThread().getName() + "组队准备,房间已满不可加入");
}
}
}).start();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("游戏房间等待玩家加入...");
/**一直等待到规定数量到线程都完全准备之后才会继续往下执行*/
latch.await();
System.out.println("游戏房间已锁定...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
System.out.println("等待玩家准备中...");
/**一直等待到规定数量到线程都完全准备之后才会继续往下执行*/
latch.await();
System.out.println("游戏匹配中...");
}
本案例中有两个线程都调用了latch.await()方法,则这两个线程都会被阻塞,直到条件达成。当5个线程调用countDown方法之后,达到了计数器的要求,则后续再执行countDown方法的效果就无效了,因为CountDownLatch仅一次有效。
CountDownLatch的实现完整逻辑如下:
CyclicBarrier可以理解为一个循环同步屏障,定义一个同步屏障之后,当一组线程都全部达到同步屏障之前都会被阻塞,直到最后一个线程达到了同步屏障之后才会被打开,其他线程才可继续执行。
还是以dota、LoL和王者荣耀为例,当第一个玩家准备了之后,还需要等待其他4个玩家都准备,游戏才可继续,否则准备的玩家会被一直处于等待状态,只有当最后一个玩家准备了之后,游戏才会继续执行。
public static void CyclicBarrierTest() throws Exception {
CyclicBarrier barrier = new CyclicBarrier(5);//定义需要达到同步屏障的线程数量
for (int i=0;i<12;i++){
Thread.sleep(1000L);
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("线程"+Thread.currentThread().getName()+"组队准备,当前" + (barrier.getNumberWaiting()+1) + "人已准备");
barrier.await();/**线程进入等待,直到最后一个线程达到同步屏障*/
System.out.println("线程:"+Thread.currentThread().getName()+"开始组队游戏");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}
}).start();
}
}
本案例中定义了达到同步屏障的线程为5个,每当一个线程调用了barrier.await()方法之后表示该线程已达到屏障,此时当前线程会被阻塞,只有当最后一个线程调用了await方法之后,被阻塞的其他线程才会被唤醒继续执行。
另外CyclicBarrier是循环同步屏障,同步屏障打开之后立马会继续计数,等待下一组线程达到同步屏障。而CountDownLatch仅单次有效。
Semaphore字面意思是信号量,实际可以看作是一个限流器,初始化Semaphore时就定义好了最大通行证数量,每次调用时调用方法来消耗,业务执行完毕则释放通行证,如果通行证消耗完,再获取通行证时就需要阻塞线程直到有通行证可以获取。
public static void semaphoreTest() throws InterruptedException {
int count = 5;
Semaphore semaphore = new Semaphore(count);
System.out.println("初始化" + count + "个银行柜台窗口");
for (int i=0;i<10;i++){
Thread.sleep(1000L);
new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("用户"+Thread.currentThread().getName()+"占用窗口");
semaphore.acquire(1);//获取许可证
/**用户办理业务需要消耗一定时间*/
System.out.println("用户"+Thread.currentThread().getName()+"开始办理业务");
Thread.sleep(5000L);
semaphore.release();//释放许可证
System.out.println("用户"+Thread.currentThread().getName()+"离开窗口");
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
根据修改的数据类型,可以将JUC包中的原子操作类可以分为4类。
基本类型: AtomicInteger, AtomicLong, AtomicBoolean ;
数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
对象的属性修改类型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater
这些类存在的目的是对相应的数据进行原子操作。所谓原子操作,是指操作过程不会被中断,保证数据操作是以原子方式进行的。
原理:CAS+volatile + native方法来保证操作的原子性
在Java语言中,所有类似“ABC”的字面值,都是String类的实例;String类位于java.lang包下,是Java语言的核心类,提供了字符串的比较、查找、截取、大小写转换等操作;Java语言为“+”连接符(字符串连接符)以及对象转换为字符串提供了特殊的支持,字符串对象可以使用“+”连接其他对象。
注意的点:
StringBuilder
对象,转为append()
操作put
方法:对 key 进行 hash 运算,然后再与当前 map 最后一个下标(长度-1,分布更均匀)进行与运算确定其在数组中的位置,正是因为这个算法,我们可以得知 HashMap 中元素是无序的。get
方法:根据 key 的 hash 运算值获取数组中对应下标的内容。循环链表或红黑树,然后匹配 value 值直至获得对应的值或返回 null在接近临界点时,若此时两个或者多个线程进行put操作,都会进行resize(扩容)和reHash(为key重新计算所在位置),而reHash在并发的情况下可能会形成链表环。总结来说就是在多线程环境下,使用HashMap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。为什么在并发执行put操作会引起死循环?是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永远不为空,就会产生死循环获取Entry。jdk1.7的情况下,并发扩容时容易形成链表环,此情况在1.8时就好太多太多了。
因为在1.8中当链表长度达到阈值(默认长度为8)时,链表会被改成树形(红黑树)结构。如果删剩节点变成7个并不会退回链表,而是保持不变,删剩6个时就会变回链表,7不变是缓冲,防止频繁变换。
在JDK1.7中,当并发执行扩容操作时会造成环形链和数据丢失的情况。
在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。
使得equals成立的时候,hashCode相等,也就是a.equals(b)->a.hashCode() == b.hashCode(),或者说此时,equals是hashCode相等的充分条件,hashCode相等是equals的必要条件(从数学课上我们知道它的逆否命题:hashCode不相等也不会equals),但是它的逆命题,hashCode相等一定equals以及否命题不equals时hashCode不等都不成立。
hashmap里维护了一个modCount
变量,迭代器里维护了一个expectedModCount
变量,一开始两者是一样的。
每次进行hashmap.remove操作的时候就会对modCount+1,此时迭代器里的expectedModCount还是之前的值。
在下一次对迭代器进行next()调用时,判断是否HashMap.this.modCount != this.expectedModCount
,如果是则抛出异常。
解决方法:直接调用迭代器的remove方法。
基本上java集合类(包括list和map)在遍历时没用迭代器进行删除了都会报ConcurrentModificationException错误,这是一种fast-fail的机制,初衷是为了检测bug。
通俗一点来说,这种机制就是为了防止高并发的情况下,多个线程同时修改map或者list的元素导致的数据不一致,这是只要判断当前modCount != expectedModCount即可以知道有其他线程修改了集合。
CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue、ConcurrentLinkedDeque
其主要接口方法和HashMap是差不多的。但是,ConcurrentHashMap是使用了ReentranLock(可重入锁机制)来保证在多线程环境下是线程安全的。在JDK6、7后使用了分段锁,在JDK8使用CAS来实现。
线程安全的双端队列,当然也可以当栈使用。由于是linked的,所以大小不受限制的。
线程安全的队列。
基于跳表实现的线程安全的MAP。除了线程安全的特性外,该map还接受比较器进行排序的map,算法复杂度还是log(n)级别的。
基于4实现的set。
以资源换取并发。通过迭代器快照的方式保证线程并发的访问。
基于6实现的。
IOC(控制反转)理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。所以依赖注入 DI和控制反转(IOC)是从不同的角度的描述的同一件事情,就是指通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦,除此之外还有另一种方法:服务定位器(Service Locator)。IOC中最基本的技术就是“反射(Reflection)”编程,通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象。这种编程方式可以让对象在生成时才决定到底是哪一种对象。
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
在spring中@Aspect
注解用来描述一个切面类。@Component
注解将该类交给 Spring 来管理,下面列举spring中AOP相关注解
注解 | 用法 | 例子 |
---|---|---|
@Pointcut | 用来定义一个切点,即上文中所关注的某件事情的入口,切入点定义了事件触发时机。 | @Pointcut(“execution(* com.mutest.controller….(…))”) // 定义一个切面,拦截 com.itcodai.course09.controller 包和子包下的所有方法 |
@Around | @Around可以自由选择增强动作与目标方法的执行顺序,还可以选择可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值。 | |
@Before | 注解指定的方法在切面切入目标方法之前执行 | |
@After | 指定的方法在切面切入目标方法之后执行,也可以做一些完成某方法之后的 Log 处理。 | |
@AfterReturning | 可以用来捕获切入方法执行完之后的返回值,对返回值进行业务逻辑上的增强处理 | |
@AfterThrowing | 当被切方法执行过程中抛出异常时,会进入 @AfterThrowing 注解的方法中执行 |
继承、多态和封装。
隐藏类内部的实现机制。
重用父类的代码。
多态是指一种事物的变现出来的多种特性。Java实现多态有三个必要条件:继承、重写、向上转型(使用父类声明的指针指向子类实例)。
代理是设计模式的一种,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。Java中的代理分为三种角色:
静态:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。或者使用Cglib
代理动态生成新的子类,所以不需要实现接口。
动态:在程序运行时运用反射机制动态创建而成。由于创建出来的委托类实例继承自Proxy
类,由于java不支持多继承所以委托类必须使用接口。
java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
深入分析JDK动态代理为什么只能使用接口
生成的代理类继承了Proxy,我们知道java是单继承的,所以JDK动态代理只能代理接口。
Map<String, Object> map = new Map<>();
List<String> stringList = new ArrayList<>();
stringList.add("A");
//error : addAll(java.util.Collection extends java.lang.String>)in List cannot be applied to (java.util.List)
stringList.addAll(Arrays.asList());
上面这段代码在java7中会报错,但是在java8中可以编译通过。
3. 支持多重注解。之前同一个注解需要放在类似数组的结构中,现在可以直接多个@,只需要对注解标注为@Repeatable
。
4. stream接口。
5. 新增Optional接口,用来防止NullPointerException异常的辅助类型。在java8中不推荐返回null而是返回Optional。
6. 函数式接口(Functional Interface)。java.util.function 它包含了很多类,用来支持 Java的 函数式编程,该包中的函数式接口有:
Predicate
接受一个输入参数,返回一个布尔值结果。
7. Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可,这个特征又叫做扩展方法,示例如下:
interface Formula {
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}
::
关键字来传递方法或者构造函数引用,我们也可以引用一个对象的方法代码如下:converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted); // "J"
接下来看看构造函数是如何使用::关键字来引用的:
PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");
更可靠的配置,通过制定明确的类的依赖关系代替以前那种易错的类路径(class-path)加载机制。
强大的封装,允许一个组件声明它的公有类型(public)中,哪些可以被其他组件访问,哪些不可以。
这些特性将有益于应用的开发者、类库的开发者和java se平台直接的或者间接地实现者。它可以使系统更加健壮,并且可以提高他们的性能。
一个模块是一个被命名的,代码和数据的自描述的集合。它的代码有一系列包含类型的包组成,例如:java的类和接口。它的数据包括资源文件(resources)和一些其他的静态信息。一个或更多个requires
项可以被添加到其中,它通过名字声明了这个模块依赖的一些其他模块,在编译期和运行期都依赖的。最后,exports
项可以被添加,它可以仅仅使指定包(package)中的公共类型可以被其他的模块使用。
module com.foo.bar {
requires org.baz.qux;
exports com.foo.bar.alpha;
exports com.foo.bar.beta;
}
Java 语言中的泛型,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(RawType,也称为裸类 型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的 Java 语言来说,ArrayList<int>与 ArrayList<String>就是同一 个类,所以泛型技术实际上是 Java 语言的一颗语法糖,Java 语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛 型。
将一段 Java 代码编译成 Class 文件,然后再用字节码反编译工具进行反编译后,将会发现泛型都不见了,程序又变回了 Java 泛型 出现之前的写法,因为泛型类型都变回了原生类型.
根据JVM规范,JVM 内存共分为虚拟机栈,堆,方法区,程序计数器,本地方法栈五个部分。
程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。
注意:如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。
每个方法被执行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法返回地址、附加信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。具体栈帧参考Java虚拟机运行时栈帧结构(周志明书上P237页)
本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码
对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。
方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。
用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。
一般来讲有两种垃圾判断的方法,分别是引用计数和可达性分析法。JVM主流采用的是可达性分析算法,因为在Java中引用计数法无法处理循环引用的问题。
为避免循环引用的问题,c++中会将互相引用的两个对象中,其中一个引用 weak_ptr,因为 weak_ptr 不会使引用计数加1,所以就不会出现这种互相拖着对方的事情了。
又比如说redis,因为其内部不存在深层次的嵌套,因此也就不存在循环引用的隐患。
JVM采用了另一种方法:定义一个名为"GC Roots"的对象作为起始点,这个"GC Roots"可以有多个,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,即可以进行垃圾回收。
可以作为GC Roots的对象:
标记完成后就可以对标记为垃圾的对象进行回收了。但是这种策略的缺点很明显,回收后内存碎片很多,如果之后程序运行时申请大内存,可能会又导致一次GC。
将内存分为两块,标记完成开始回收时,将一块内存中保留的对象全部复制到另一块空闲内存中。实现起来也很简单,当大部分对象都被回收时这种策略也很高效。但这种策略也有缺点,可用内存变为一半了
怎样解决呢?办法总是多过问题的。可以将堆不按1:1的比例分离,而是按8:1:1分成一块Eden和两小块Survivor区,每次将Eden和Survivor中存活的对象复制到另一块空闲的Survivor中。这三块区域并不是堆的全部,而是构成了新生代。如果回收时,空闲的那一小块Survivor不够用了怎么办?这就是老年代的用处。当不够用时,这些对象将直接通过分配担保机制进入老年代。‘
根据老年代的特点,采用回收掉垃圾对象后对内存进行整理的策略再合适不过,将所有存活下来的对象都向一端移动。
参考:https://www.cnblogs.com/chenpt/p/9803298.html
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1
Serial收集器是最基本的、发展历史最悠久的收集器。
特点:单线程、简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程手机效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
应用场景:适用于Client模式下的虚拟机。
除了使用多线程外其余行为均和Serial收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。
特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题
应用场景:ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。
与吞吐量关系密切,故也称为吞吐量优先收集器。吞吐量=运行用户代码时间/CPU总执行时间。
用于精确吞吐量的两个参数:1.控制最大垃圾收集停顿时间参数 2.直接设置吞吐量大小的参数。
特点:属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器类似)。
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)
Serial Old是Serial收集器的老年代版本。
特点:同样是单线程收集器,采用标记-整理算法。
应用场景:主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用。
Server模式下主要的两大用途:
在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用。
作为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。
是Parallel Scavenge收集器的老年代版本。
特点:多线程,采用标记-整理算法。
应用场景:注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器。
特点: 针对老年代;基于标记-清除算法实现。并发收集、低停顿。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
CMS收集器的运行过程分为下列4步:
初始标记:标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
并发标记:进行GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。
并发清除:对标记的对象进行清除回收。
CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS收集器的缺点:
对CPU资源非常敏感。
无法处理浮动垃圾,可能出现Concurrent Model Failure失败而导致另一次Full GC的产生。
因为采用标记-清除算法所以会存在空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC。
一款面向服务端应用的垃圾收集器。
特点如下:
并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-The-World停顿时间。部分收集器原本需要停顿Java线程来执行GC动作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
分代收集:G1能够独自管理整个Java堆,并且采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
空间整合:使用标记-整理算法。G1运作期间不会产生空间碎片,收集后能提供规整的可用内存。
可预测的停顿:G1除了追求低停顿外,还能建立可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间段内,消耗在垃圾收集上的时间不得超过N毫秒。
G1为什么能建立可预测的停顿时间模型?
因为它有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的大小,在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。这样就保证了在有限的时间内可以获取尽可能高的收集效率。
G1与其他收集器的区别:
其他收集器的工作范围是整个新生代或者老年代、G1收集器的工作范围是整个Java堆。在使用G1收集器时,它将整个Java堆划分为多个大小相等的独立区域(Region)。虽然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔离的,他们都是一部分Region(不需要连续)的集合。
G1收集器存在的问题:
Region不可能是孤立的,分配在Region中的对象可以与Java堆中的任意对象发生引用关系。在采用可达性分析算法来判断对象是否存活时,得扫描整个Java堆才能保证准确性。其他收集器也存在这种问题(G1更加突出而已)。会导致Minor GC效率下降。
G1收集器是如何解决上述问题的?
采用Remembered Set来避免整堆扫描。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用对象是否处于多个Region中(即检查老年代中是否引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆进行扫描也不会有遗漏。
如果不计算维护 Remembered Set 的操作,G1收集器大致可分为如下步骤:
初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)
并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)
最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set Logs里面,把Remembered Set Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)
筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)
命令查看:jmap(jmap -heap PID)
参考:https://www.cnblogs.com/CtripDBA/p/14210220.html
一台region server的内存使用主要分成两部分:
JVM内存即我们通常俗称的堆内内存,这块内存区域的大小分配在HBase的环境脚本中设置,在堆内内存中主要有三块内存区域,20%分配给hbase regionserver rpc请求队列及一些其他操作,80%分配给memstore + blockcache
java direct memory即堆外内存,
其中一部分内存用于HDFS SCR/NIO操作,另一部分用于堆外内存bucket cache,其内存大小的分配同样在hbase的环境变量脚本中实现
BlockCache用于读缓存;MemStore用于写流程,缓存用户写入KeyValue数据;还有部分用于RegionServer正常RPC请求运行所必须的内存;
Hbase服务是基于JVM的,其中对服务可用性最大的挑战是jvm执行full gc操作,此时会导致jvm暂停服务,这个时候,hbase上面所有的读写操作将会被客户端归入队列中排队,一直等到jvm完成gc操作, 服务在遇到full gc操作时会有如下影响
hbase服务长时间暂停会导致客户端操作超时,操作请求处理异常。服务端超时会导致region信息上报异常丢失心跳,会被zk标记为宕机,导致regionserver即便响应恢复之后,也会因为查询zk上自己的状态后自杀,此时hmaster 会将该regionserver上的所有region移动到其他regionserver上。
具体参数:
export HBASE_MASTER_OPTS=" -Xms8g -Xmx8g -Xmn1g
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70"
初始堆及最大堆设置为最大物理内存的2/3,128G/3*2 ≈80G,在某些度写缓存比较小的集群,可以近一步缩小。
InitiatingHeapOccupancyPercent代表了堆占用了多少比例的时候触发MixGC,默认占用率是整个 Java 堆的45%,改参数的设定取决于IHOP > MemstoreSize%+WriteCache%+10~20%,避免过早的MixedGC中,有大量数据进来导致Full GC
G1HeapRegionSize 堆中每个region的大小,取值范围【1M…32M】2^n,目标是根据最小的 Java 堆大小划分出约 2048 个区域,即heap size / G1HeapRegionSize = 2048 regions
ParallelGCThreads设置垃圾收集器并行阶段的线程数量,STW阶段工作的GC线程数,8+(logical processors-8)(5/8)
ConcGCThreads并发垃圾收集器使用的线程数量,非STW期间的GC线程数,可以尝试调大些,能更快的完成GC,避免进入STW阶段,但是这也使应用所占的线程数减少,会对吞吐量有一定影响
G1NewSizePercent新生代占堆的最小比例,增加新生代大小会增加GC次数,但是会减少GC的时间,建议设置5/8对应负载normal/heavy集群
G1HeapWastePercent触发Mixed GC的堆垃圾占比,默认值5
G1MixedGCCountTarget一个周期内触发Mixed GC最大次数,默认值8
这两个参数互为增加到10/16,可以有效的减少1S+ Mixed GC STW times
MaxGCPauseMillis 垃圾回收的最长暂停时间,默认200ms,如果GC时间超长,那么会逐渐减少GC时回收的区域,以此来靠近此阈值,一般来说,按照群集的重要性 50/80/200来设置
verbose:gc在日志中输出GC情况
export HBASE_REGIONSERVER_OPTS="-XX:+UseG1GC
-Xms75g –Xmx75g
-XX:InitiatingHeapOccupancyPercent=83
-XX:G1HeapRegionSize=32M
-XX:ParallelGCThreads=28
-XX:ConcGCThreads=20
-XX:+UnlockExperimentalVMOptions
-XX:G1NewSizePercent=8
-XX:G1HeapWastePercent=10
-XX:MaxGCPauseMillis=80
-XX:G1MixedGCCountTarget=16
-XX:MaxTenuringThreshold=1
-XX:G1OldCSetRegionThresholdPercent=8
-XX:+ParallelRefProcEnabled
-XX:-ResizePLAB
-XX:+PerfDisableSharedMem
-XX:-OmitStackTraceInFastThrow
-XX:+PrintFlagsFinal
-verbose:gc
-XX:+PrintGC
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-XX:+PrintAdaptiveSizePolicy
-XX:+PrintGCDetails
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintTenuringDistribution
-XX:+PrintReferenceGC
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=100M
Xloggc:${
HBASE_LOG_DIR}/gc-regionserver$(hostname)-`date +'%Y%m%d%H%M'`.log
-Dcom.sun.management.jmxremote.port=10102
$HBASE_JMX_BASE"
参考:【JVM】浅谈双亲委派和破坏双亲委派
对于任意一个类,都需要由加载它的类加载器和这个类本身来一同确立其在Java虚拟机中的唯一性。
基于上述的问题:如果不是同一个类加载器加载,即时是相同的class文件,也会出现判断不想同的情况,从而引发一些意想不到的情况,为了保证相同的class文件,在使用的时候,是相同的对象,jvm设计的时候,采用了双亲委派的方式来加载类。
双亲委派:如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。
举个通俗点的例子来说,JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。
我们进一步了解类加载器间的关系(并非指继承关系),主要可以分为以下4点
在JVM中表示两个class对象是否为同一个类对象存在两个必要条件:类的完整类名必须一致,包括包名。加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
由此可见,JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。类加载的过程总体上可以分为5部分。
加载
在加载阶段,JVM主要完成下面三件事:
①、通过一个类的全限定名来获取定义此类的二进制字节流。
②、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
③、在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
验证
作用是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备
准备阶段正式为类变量分配内存并设置类变量初始值的阶段,这些内存是在方法区中分配的。上面说的是类变量,也就是被 static 修饰的变量,不包括实例变量。实例变量会在对象实例化时随着对象一起分配在堆中。
解析
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经在内存中存在。
()
方法的过程。总体来说设计模式分为三大类:
该模式主要目的是使内存中保持1个对象
单例模式有5种,分别是懒汉式、饿汉式、双重校验锁、静态内部类和枚举。
private volatile static CSVService csvService;
/**
* 获取单例 双检锁/双重校验锁(DCL,即 double-checked locking)
* @return
*/
public static CSVService getInstance(){
if (csvService == null) {
synchronized (CSVService.class) {
if (csvService == null) {
csvService = new CSVService();
}
}
}
return csvService;
}
该模式主要功能是统一提供实例对象的引用
public class Factor {
public ClassDao getDao(){
ClassDao dao = new ClassDaoImpl();
return dao;
}
}
这个模式是将行为的抽象,即当有几个类有相似的方法,将其中通用的部分都提取出来,从而使扩展更容易
public class Addition extends Operation{
@Override
public float parameter(float a, float b){
return a+b;
}
}
客户端可以调用门面角色的方法。此角色知晓相关的(一个或者多个)子系统的功能和责任。在正常情况下,门面角色会将所有从客户端发来的请求委派到相应的子系统去。
将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。在这样的设计模式中,有以下几个角色:
Builder:为创建一个产品对象的各个部件指定抽象接口。
ConcreteBuilder:实现Builder的接口以构造和装配该产品的各个部件,定义并明确它所创建的表示,并提供一个检索产品的接口。
Director:构造一个使用Builder接口的对象,指导构建过程。
Product:表示被构造的复杂对象。ConcreteBuilder创建该产品的内部表示并定义它的装配过程,包含定义组成部件的类,包括将这些部件装配成最终产品的接口。
适配器模式(有时候也称包装样式或者包装)将一个类的接口适配成用户所期待的。一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。
装饰模式是在不必改变原类文件和使用继承的情况下,能对现有对象的内容或功能性稍加修改。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
代理模式 : 为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。目的为了去除核心对象的复杂性
外观模式是通过在必要的逻辑和方法的集合前创建简单的外观接口,隐藏调用对象的复杂性。
观察者模式也做发布订阅模式(Publish/subscribe),在项目中常使用的模式。定义:定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖它的对象都会得到通知并被自动更新。
命令模式是一个高内聚的模式,其定义为:将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。访问者模式适用于数据结构相对稳定算法又易变化的系统。因为访问者模式使得算法操作增加变得容易。若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。访问者模式的优点是增加操作很容易,因为增加操作意味着增加新的访问者。访问者模式将有关行为集中到一个访问者对象中,其改变不影响系统数据结构。其缺点就是增加新的数据结构很困难。
遇到好的有典型性的SQL题会放在这里。
有一个courses 表 ,有: student (学生) 和 class (课程)。
请列出所有超过或等于5名学生的课。
SELECT class FROM courses GROUP BY class HAVING COUNT(DISTINCT student) >= 5;
where 是group by之前进行筛选,having是group by 之后进行统计的筛选,一般having会和group by一起使用,结合聚合函数,统计之后进行筛选。
SELECT class FROM (SELECT COUNT(DISTINCT(student)) AS num, class FROM courses GROUP BY class) b WHERE num >= 5;
select id,course,score from student where id in (select id from student where score in (select max(score) from student group by course))
left join
,在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。
SELECT p.LastName, p.FirstName, o.OrderNo
FROM Persons p
LEFT JOIN Orders o
ON p.Id_P=o.Id_P
ORDER BY p.LastName
right join
,在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。
full join
,在两张表进行连接查询时,返回左表和右表中所有有/没有匹配的行。
inner join
(内连接),在两张表进行连接查询时,只保留两张表中完全匹配的结果集。
应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num=10 or num=20
可以这样查询:
select id from t where num=10 union all select id from t where num=20
应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
下面的查询也将导致全表扫描:select id from t where name like '%abc%'
应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num is null
可以在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0
对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where num/2=100
应改为:
select id from t where num=100*2
如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。
查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来说,查询缓存的命中率会非常低。除非你的业务就是有一张静态表,很长时间才会更新一次。比如,一个系统配置表,那这张表上的查询才适合使用查询缓存。
分析器。分析器先会做“词法分析”。做完了这些识别以后,就要做“语法分析”。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个SQL语句是否满足 MySQL 语法。
优化器
经过了分析器,MySQL就知道你要做什么了。在开始执行之前,还要先经过优化器的处理。优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。
执行器
MySQL 通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。
开始执行的时候,要先判断一下你对这个表 T 有没有执行查询的权限,如果没有,就会返回没有权限的错误,如下所示。
mysql> select * from T where ID=10;
ERROR 1142 (42000): SELECT command denied to user 'b'@'localhost' for table 'T'
如果有权限,就打开表继续执行。打开表的时候,优化器就会根据表的引擎定义,去使用这个引擎提供的接口。
比如我们这个例子中的表 T 中,ID 字段没有索引,那么执行器的执行流程是这样的:
1、调用 InnoDB 引擎接口取这个表的第一行,判断ID值是不是10,如果不是则跳过,如果是则将这行存在结果集中;
2、调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
3、执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
至此,这个语句就执行完成了。
对于有索引的表,执行的逻辑也差不多。第一次调用的是 “取满足条件的第一行” 这个接口,之后循环取 “满足条件的下一行” 这个接口,这些接口都是引擎中已经定义好的。
你会在数据库的慢查询日志中看到一个 rows_examined 的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。
在有些场景下,执行器调用一次,在引擎内部则扫描了多行,因此引擎扫描行数跟 rows_examined 并不是完全相同的。
UPDATE `lists` SET `updated_at` = *** WHERE (`interval` = 1 and started_at = 4) --事务1
UPDATE `lists` SET `updated_at` = *** WHERE (`interval` = 2 and started_at = 3) -- 事务2
MySQL Server 通过条件 ( interval = 1) 在索引上进行当前读,会将 index_interval 这个二级索引中满足 interval = 1 全部返回,并加上互斥锁,避免数据被其他并发事务修改。(锁住二级索引)
因为要修改的数据是主键所在的行数据 (聚簇索引),所以第二步还需要**通过二级索引对应的主键 ID **读取实际的数据。这也是一个当前读,读取之后会对数据加上互斥锁,锁住主键 (就是死锁日志中的 index PRIMARY ··· Record lock)。
由于上述原因所以可能会所锁住一些不相关的主键,导致本不会发生冲突的事务发生死锁。
解决
通过上面的梳理,如果没有使用 index merge 则本案的死锁情况可以避免。 所以加上一组联合索引即可解决本文场景下的死锁问题。(执行计划得用组合索引)
alter table lists add index u_started_at_interval(started_at
, interval
)
如上的二级索引有两层,第一层以 started_at 为序, 第二层以 interval 为序,结构示例如下:
100(2,3,5,7,9), 101(70), 105(34,35,37,80) ···
100, 101, 105 为第一层 started_at, 括号内的为第二层,彼此都有序。当执行计划使用这个联合索引进行当前读时,因为二级索引有序, 相同的条件当前读加锁的顺序是一样的,不会出现交叉,不同条件的当前读不会有重合的主键数据,这就避免了上面数据交叉且加锁顺序相反的情况,避开了死锁。
死锁的场景复杂多变,本案例只是九牛一毛,加联合索引的方案也只是减少了某种数据分布情况下的死锁概率。
在开发中,经常会做这类的判断需求:根据字段值查询(有索引),如果不存在,则插入;否则更新。
当对未存在的行进行锁的时候(即使条件为主键),mysql是会锁住一段范围(有gap锁),由于gap锁是可以重复添加的,所以事务直到插入的时候才会出现由于多个gap锁导致的死锁。
解决
对于这种死锁的解决办法是:(update or insert if existed)
insert into t3(xx,xx) on duplicate key update `xx`='XX';
用mysql特有的语法来解决此问题。因为insert语句对于主键来说,插入的行不管有没有存在,都会只有行锁
原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)
其中一致性又分为数据库约束(外键等)的完整和业务逻辑的完整。
事务:事务就是将很多个操作集中在一块形成一个有限的操作集,然后对之及进行执行;对于一个事务的执行结果只有两种结果,一是全部执行成功并提交到数据库中,对数据进行持久的影响,二是事务中有一个或者多个操作没能成功执行最终导致事务的执行整体失败,进而回滚到事务开始之前的数据库状态
隔离级别
类别 | 解释 |
---|---|
读未提交数据 | 就是允许事务读取还没有被其他事务提交的数据,这种情况下会出现脏读、幻读、不可重复读的问题 |
读已提交数据 | 只允许事务读取其他事务已经提交的数据,此种级别解决了脏读的问题,但还是不能彻底解决不可重复读、幻读的问题 |
可重复读 | 确保事务能够多次从一个字段中读取相同的数据值,在此事务期间不允许其他事务进行对数据变更,避免了脏读、不可重复的问题,但是幻读仍可能出现 |
可串行化(序列化) | 确保一个事务可以从一个表中读取相同的行,在此事务持续的期间禁止其他事务对该表进行更新、插入、删除等操作,可以避免所有的并发问题,但是此种情形性能极低 |
常见的数据库中 , Oracle数据库是支持读已提交,MySQL数据库是支持可重复读的,随着隔离级别的提升,对应性能效率会降低,所以在具体选择时,需要综合各个方面进行考虑。
每次对记录进行改动,都会记录一条undo
日志,每条undo日志也都有一个roll_pointer
属性(INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表。对该记录每次更新后,都会将旧值放到一条undo日志中,就算是该记录的一个旧版本,随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含生成该版本时对应的事务id。
对于使用READ UNCOMMITTED
隔离级别的事务来说,直接读取记录的最新版本就好了,对于使用SERIALIZABLE
隔离级别的事务来说,使用加锁的方式来访问记录。对于使用READ COMMITTED
和REPEATABLE READ
隔离级别的事务来说,就需要用到我们上边所说的版本链了,核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。所以设计InnoDB的大叔提出了一个ReadView的概念,这个ReadView中主要包含当前系统中还有哪些活跃的读写事务,把它们的事务id放到一个列表中,我们把这个列表命名为为m_ids。这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
如果被访问版本的trx_id属性值小于m_ids列表中最小的事务id,表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问
如果被访问版本的trx_id属性值大于m_ids列表中最大的事务id,表明生成该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问。
如果被访问版本的trx_id属性值在m_ids列表中最大的事务id和最小事务id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本,如果最后一个版本也不可见的话,那么就意味着该条记录对该事务不可见,查询结果就不包含该记录。
从上边的描述中我们可以看出来,所谓的MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使用READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执行普通的SEELCT操作时访问记录的版本链的过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统性能。READ COMMITTD、REPEATABLE READ这两个隔离级别的一个很大不同就是生成ReadView的时机不同,READ COMMITTD在每一次进行普通SELECT操作前都会生成一个ReadView(可重复读,要求从事务开始的时候),而REPEATABLE READ只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复这个ReadView就好了。
即使一个事务对所有行都加上排他锁,还是无法保证其他事务不会有新的数据行插入。这也是为什么”幻读”会被单独拿出来解决的问题。因此,为了解决幻读问题,innodb引入了新的锁,也即是间隙锁(gap lock)。
顾名思义,间隙锁,锁的就是两个值之间的空隙。这样,当执行select * from t20 where d=5 for update的时候,就不止给数据库已有的6行记录加上了行锁,还同时加了7个间隙锁,这样就确保无法在插入新的记录。也就是说这时候,在一行扫描的过程中,不仅将给行上加了行锁,还给行两边的空隙(加锁单位是next-key lock
也就是该行前后两项数据-不存在该行的时候,否则为前开后闭区间),也加上了间隙锁。
但是间隙锁不一样,跟间隙锁存在冲突关系的,是”往这个间隙中插入一个记录”这个操作。间隙锁之间是不存在冲突关系的。
间隙锁的出现主要集中在同一个事务中先delete 后 insert的情况下, 当我们通过一个参数去删除一条记录的时候, 如果参数在数据库中存在, 那么这个时候产生的是普通行锁, 锁住这个记录, 然后删除, 然后释放锁。如果这条记录不存在,问题就来了, 数据库会扫描索引,发现这个记录不存在, 这个时候的delete语句获取到的就是一个间隙锁,然后数据库会向左扫描扫到第一个比给定参数小的值, 向右扫描扫描到第一个比给定参数大的值, 然后以此为界,构建一个区间, 锁住整个区间内的数据, 一个特别容易出现死锁的间隙锁诞生了。
undo log
的生成,并且回滚日志必须先于数据持久化到磁盘上页(Page)是 Innodb 存储引擎用于管理数据的最小磁盘单位。常见的页类型有数据页、Undo 页、系统页、事务数据页等。
事务一旦提交,其所作做的修改会永久保存到数据库中,此时即使系统崩溃修改的数据也不会丢失。先了解一下MySQL的数据存储机制,MySQL的表数据是存放在磁盘上的,因此想要存取的时候都要经历磁盘IO,然而即使是使用SSD磁盘IO也是非常消耗性能的。为此,为了提升性能InnoDB提供了缓冲池(Buffer Pool),Buffer Pool中包含了磁盘数据页的映射,可以当做缓存来使用:
读数据:会首先从缓冲池中读取,如果缓冲池中没有,则从磁盘读取在放入缓冲池;
写数据:会首先写入缓冲池,缓冲池中的数据会定期同步到磁盘中;
上面这种缓冲池的措施虽然在性能方面带来了质的飞跃,但是它也带来了新的问题,当MySQL系统宕机,断电的时候可能会丢数据!
此处我选择从第三者角度来进行描述,便于区分理解;
存储结构:MyISAM在磁盘上存储文件有三类:<1>.frm 负责存储表定义信息 <2>.myd 数据文件的相关信息 <3>.myi 索引文件相关信息; InnoDB文件有两类:<1>idb 数据文件 <2> frm 定义相关信息文件
存储空间: MyISAM可以被压缩,存储空间较小,能够支持三类不同的存储格式:静态表、动态表、压缩表;InnoDB需要更加多的内存及存储,需要在内存中建立专用的缓冲池进行缓冲数据和索引;
可移植性、备份恢复: MyISAM以文件形式进行数据存储,跨平台进行数据库的移植时比较便捷,备份恢复也可单独的对具体的表进行;InnoDB具有免费方案,能够拷贝数据文件,备份binlog或者是使用mysqldump ,但是在处理大型数据量时不方便(如几个TG);
事务支持: MyISAM强调性能,每一个查询具有原子性,执行速度上比InnoDB快,不提供事务支持(不支持commit / roolback操作,);InnoDB提供事务支持,外键等给及数据库功能(commit roolback 数据库的崩溃修复等);
全文索引: MyISAM支持FullTEXT类型的全文索引;InnoDB不支持全文索引,但是可以通过使用Sphinx插件进行实现全文索引功能;
表主键: InnoDB要求是必须要有主键的,MyISAM则能接受没有主键;
表的行数: MyISAM具有一个计数器,能够对自己操作表行数进行记录保存,使用select count ( *) from tablename; 就可得到,InnoDB 不保存相应的行数记录值,要知道具体的行数需要进行遍历,耗时费力; 献上一题,若是一个表有6行,两个引擎中都有,然后均删除第4、5、6行数据,关机/断电/其他意外导致数据库重启了;添加数据时行号问题(可自行思考实验,这样记得更深);
外键的支持: InnoDB支持外键,MyISAM不支持;
锁: InnoDB支持行级锁,粒度更小;MyISAM则提供表级锁;
共享锁(S锁)
共享 (S) 用于不更改或不更新数据的操作(只读操作),如 SELECT 语句。
如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
排他锁(X锁)
该锁也称为独占锁,用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。确保不会同时同一资源进行多重更新。
如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。
使用:在需要执行的语句后面加上 for update
就可以了。
乐观锁
乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了。
通常实现是这样的:在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1。也就是先查询出那条记录,获取出version字段,如果要对那条记录进行操作(更新),则先判断此刻version的值是否与刚刚查询出来时的version的值相等,如果相等,则说明这段期间,没有其他程序对其进行操作,则可以执行更新,将version字段的值加1;如果更新时发现此刻的version值与刚刚获取出来的version的值不相等,则说明这段期间已经有其他程序对其进行操作了,则不进行更新操作。
SELECT * from city where id = "1" lock in share mode;
由于对于city表中,id字段为主键,就也相当于索引。执行加锁时,会将id这个索引为1的记录加上锁,那么这个锁就是行锁。
索引是数据表中一个或者多个列进行排序的数据结构
B+树是 B-Tree的变形,Mysql 实际使用的 B+Tree作为索引的数据结构。
只在叶子结点带有指向记录的指针。B+树只在叶子结点存储数据,其它非叶子结点可以存储更多信息(对B-Tree的优化)。叶子结点通过指针相连。叶子结点里面有从左到右指针,它是一个双向指针,通过这个指针就可以非常方便实现范围查询。
索引的优缺点:
1、优点:创建索引可以大大提高系统的性能。
第一、通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
第二、可以大大加快 数据的检索速度,这也是创建索引的最主要的原因。
第三、可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
第四、在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
第五、通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
2、缺点:
增加索引有如此多的优点,为什么不对表中的每一个列创建一个索引呢?这是因为,增加索引也有许多不利的一个方面:
第一、创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
第二、索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间。如果要建立聚簇索引,那么需要的空间就会更大。
第三、当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
当MySQL中字段为int
类型时,搜索条件where num=‘111‘ 与where num=111都可以使用该字段的索引。
当MySQL中字段为varchar
类型时,搜索条件where num=‘111‘ 可以使用索引,where num=111 不可以使用索引
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
MVCC的好处/作用
MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读
MVCC的实现原理
在InnoDB下的Compact行结构,有三个隐藏的列
列名 | 是否必须 | 描述 |
---|---|---|
row_id | 否 | 行ID,唯一标识一条记录(如果定义主键,它就没有啦) |
transaction_id | 是 | 事务ID |
roll_pointer | 是 | DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本 |
innodb会为每一行添加两个字段,分别表示该行创建的版本和删除的版本,填入的是事务的版本号,这个版本号随着事务的创建不断递增.具体各种数据库操作的实现:
select:满足以下两个条件innodb会返回该行数据:(1)该行的创建版本号小于等于当前版本号,用于保证在select操作之前所有的操作已经执行落地。(2)该行的删除版本号大于当前版本或者为空。删除版本号大于当前版本意味着有一个并发事务将该行删除了。
insert:将新插入的行的创建版本号设置为当前系统的版本号。
delete:将要删除的行的删除版本号设置为当前系统的版本号。
update:不执行原地update,而是转换成insert + delete。将旧行的删除版本号设置为当前版本号,并将新行insert同时设置创建版本号为当前版本号。
其中,写操作(insert、delete和update)执行时,需要将系统版本号递增。 由于旧数据并不真正的删除,所以必须对这些数据进行清理,innodb会开启一个后台线程执行清理工作,具体的规则是将删除版本号小于当前系统版本的行删除,这个过程叫做purge。 通过MVCC很好的实现了事务的隔离性,可以达到repeated read级别,要实现serializable还必须加锁。
两个或更多个列上的索引被称作联合索引,联合索引又叫复合索引。mysql建立多列索引(联合索引)有最左前缀的原则,即最左优先,如:
如果有一个2列的索引(col1,col2),则已经对(col1)、(col1,col2)上建立了索引;
如果有一个3列索引(col1,col2,col3),则已经对(col1)、(col1,col2)、(col1,col2,col3)上建立了索引;
b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+树是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道第一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。
什么时候索引会失效
聚簇索引是将数据行的内容放在B_tree的叶子节点中,节点列存放数据列,由于不能把数据行放在两个不同的地方,所以每个表只能有一个聚簇索引。MySql的聚簇索引只能将主键作为索引。如果没有主键,则选择唯一的非空索引,如果都没有,则隐式定义主键,但是这样很不好,相邻键值相距会很远。
通俗的解释就是例如InnoDB使用了聚簇索引,在主索引B+树的叶子节点保存数据行的内容,辅助键索引B+树保存关键字到主键的映射,因此若对非主键列进行条件搜索,则需要两个步骤。而MyISM使用的是非聚簇索引,表数据存储在独立的地方,主键索引索引B+树和辅助键索引B+树的叶子节点都保存指向真正的表数据的指针。
总结:聚簇索引和非聚簇索引的区别在于聚簇索引的叶子节点里包括了数据行的内容,而非聚簇索引叶子节点里不是,可以是数据存放的地址,也可以是二级索引指向表的主键值。
覆盖索引:一个索引包含所有要查询的字段的值。
优点:
①:索引条目通常远小于数据行,因此如果只读取索引,可以极大的减小数据访问量,同样索引也更容易放入内存,所以对于I/O密集型的应用有很大帮助。
②:索引是按照列顺序存储的。
③:有的存储引擎如MyIsam只在内存中缓存索引,数据依赖于操作系统缓存,因此覆盖索引避免了频繁的系统调用。
回表查询,先定位主键值,再定位行记录,它的性能较扫一遍索引树更低。
在explain查询计划优化章节,即explain的输出结果Extra字段为Using index时,能够触发索引覆盖。
常见的方法是:将被查询的字段,建立到联合索引里去。
https://juejin.cn/post/6844904073955639304
对于联合索引来说只不过比单值索引多了几列,而这些索引列全都出现在索引树上。对于联合索引,存储引擎会首先根据第一个索引列排序,如我们可以单看第一个索引列,横着看,如,1 1 5 12 13…他是单调递增的;如果第一列相等则再根据第二列排序,依次类推就构成了上图的索引树,上图中的b列都等于1时,则根据c排序,此时c列也相等则按d列排序
当我们的SQL语言可以应用到索引的时候,比如select * from T1 where b = 12 and c = 14 and d = 3; 也就是T1表中a列为4的这条记录。存储引擎首先从根节点(一般常驻内存)开始查找,第一个索引的第一个索引列为1,12大于1,第二个索引的第一个索引列为56,12小于56,于是从这俩索引的中间读到下一个节点的磁盘文件地址,从磁盘上Load这个节点,通常伴随一次磁盘IO,然后在内存里去查找。当Load叶子节点的第二个节点时又是一次磁盘IO,比较第一个元素,b=12,c=14,d=3完全符合,于是找到该索引下的data元素即ID值,再从主键索引树上找到最终数据。
当创建**(a,b,c)联合索引时,相当于创建了(a)单列索引**,(a,b)联合索引以及**(a,b,c)联合索引**
想要索引生效的话,只能使用 a和a,b和a,b,c三种组合;当然,我们上面测试过,a,c组合也可以,但实际上只用到了a的索引,c并没有用到!
主从复制原理
分库分表
Mongo-DB是一个文档数据库,可提供高性能,高可用性和易扩展性。mongodb中有多个databases,每个database可以创建多个collections,collection是底层数据分区(partition)的单位,每个collection都有多个底层的数据文件组成。
在多台计算机上存储数据记录的过程称为分片。这是一种MongoDB方法,可以满足数据增长的需求。它是数据库或搜索引擎中数据的水平分区。每个分区称为分片或数据库分片。在多台服务器之间,同步数据的过程称为复制。它通过不同数据库服务器上的多个数据副本提供冗余并提高数据可用性。复制有助于防止数据库丢失单个服务器。
因为不涉及范围查询。
Hbase 中的每张表都通过行键 (rowkey) 按照一定的范围被分割成多个子表(HRegion),默认一个 HRegion 超过 256M 就要被分割成两个,由 HRegionServer 管理,管理哪些 HRegion 由 Hmaster 分配。 HRegion 存取一个子表时,会创建一个 HRegion 对象,然后对表的每个列族 (Column Family) 创建一个 store 实例, 每个 store 都会有 0个或多个 StoreFile 与之对应,每个 StoreFile 都会对应一个 HFile , HFile 就是实际的存储文件,因此,一个 HRegion 还拥有一个 MemStore 实例。
实时查询,可以认为是从内存中查询,一般响应时间在 1 秒内。HBase 的机制是数据先写入到内存中,当数据量达到一定的量(如 128M),再写入磁盘中, 在内存中,是不进行数据的更新或合并操作的,只增加数据,这使得用户的写操作只要进入内存中就可以立即返回,保证了 HBase I/O 的高性能。
在 hbase 中每当有 memstore 数据 flush 到磁盘之后,就形成一个 storefile, 当 storeFile 的数量达到一定程度后,就需要将 storefile 文件来进行 compaction 操作。Compact 的作用:
按指 定 RowKey 获 取 唯 一 一 条 记 录 , get 方法( org.apache.hadoop.hbase.client.Get ) Get的方法处理分两种 : 设置了ClosestRowBefore和没有设置的 rowlock 主要是用来保证行的事务性,即每个get 是以一个 row 来标记的.一个 row 中可以有很多 family 和 column。
按指定的条件获取一批记录,scan 方法(org.apache.Hadoop.hbase.client.Scan)实现条件查询功能使用的就是 scan 方式
- scan 可以通过 setCaching 与 setBatch 方法提高速度(以空间换时间);
- scan 可以通过 setStartRow 与 setEndRow 来限定范围([start,end]start? 是闭区间,end 是开区间)。范围越小,性能越高;
- scan 可以通过 setFilter 方法添加过滤器,这也是分页、多条件查询的基础。
在 HBase 中无论是增加新行还是修改已有的行,其内部流程都是相同的。HBase 接到命令后存下变化信息,或者写入失败抛出异常。默认情况下,执行写入时会写到两个地方:预写式日志(write-ahead log,也称 HLog)和 MemStore。HBase 的默认方式是把写入动作记录在这两个地方,以保证数据持久化。只有当这两个地方的变化信息都写入并确认后,才认为写动作完成。
MemStore 是内存里的写入缓冲区,HBase 中数据在永久写入硬盘之前在这里累积。当MemStore 填满后,其中的数据会刷写到硬盘,生成一个HFile。HFile 是HBase 使用的底层存储格式。HFile 对应于列族,一个列族可以有多个 HFile,但一个 HFile 不能存储多个列族的数据。在集群的每个节点上,每个列族有一个MemStore。大型分布式系统中硬件故障很常见,HBase 也不例外。
设想一下,如果MemStore 还没有刷写,服务器就崩溃了,内存中没有写入硬盘的数据就会丢失。HBase 的应对办法是在写动作完成之前先写入 WAL。HBase 集群中每台服务器维护一个 WAL 来记录发生的变化。WAL 是底层文件系统上的一个文件。直到WAL 新记录成功写入后,写动作才被认为成功完成。这可以保证 HBase 和支撑它的文件系统满足持久性。
大多数情况下,HBase 使用Hadoop分布式文件系统(HDFS)来作为底层文件系统。如果 HBase 服务器宕机,没有从 MemStore 里刷写到 HFile 的数据将可以通过回放 WAL 来恢复。你不需要手工执行。Hbase 的内部机制中有恢复流程部分来处理。每台 HBase 服务器有一个 WAL,这台服务器上的所有表(和它们的列族)共享这个 WAL。你可能想到,写入时跳过 WAL 应该会提升写性能。但我们不建议禁用 WAL, 除非你愿意在出问题时丢失数据。如果你想测试一下,如下代码可以禁用 WAL: 注意:不写入 WAL 会在 RegionServer 故障时增加丢失数据的风险。关闭 WAL, 出现故障时 HBase 可能无法恢复数据,没有刷写到硬盘的所有写入数据都会丢失。
宕机分为 HMaster 宕机和 HRegisoner 宕机.
如果是 HRegisoner 宕机,HMaster 会将其所管理的 region 重新分布到其他活动的 RegionServer 上,由于数据和日志都持久在 HDFS 中,该操作不会导致数据丢失,所以数据的一致性和安全性是有保障的。
如果是 HMaster 宕机, HMaster 没有单点问题, HBase 中可以启动多个HMaster,通过 Zookeeper 的 Master Election 机制保证总有一个 Master 运行。即ZooKeeper 会保证总会有一个 HMaster 在对外提供服务。
从zookeeper中获取.ROOT.表的位置信息,在zookeeper的存储位置为/hbase/root-region-server;
根据.ROOT.表中信息,获取.META.表的位置信息;
META.表中存储的数据为每一个region存储位置;
2. 向hbase表中插入数据
hbase中缓存分为两层:Memstore 和 BlockCache
首先写入到 WAL文件 中,目的是为了数据不丢失;
再把数据插入到 Memstore缓存中,当 Memstore达到设置大小阈值时,会进行flush进程;
flush过程中,需要获取每一个region存储的位置。
BlockCache 主要提供给读使用。读请求先到 Memtore中查数据,查不到就到 BlockCache 中查,再查不到就会到磁盘上读,并把读的结果放入 BlockCache 。
BlockCache 采用的算法为 LRU(最近最少使用算法),因此当 BlockCache 达到上限后,会启动淘汰机制,淘汰掉最老的一批数据。
一个 RegionServer 上有一个 BlockCache 和N个 Memstore,它们的大小之和不能大于等于 heapsize * 0.8,否则 hbase 不能启动。默认 BlockCache 为 0.2,而 Memstore 为 0.4。对于注重读响应时间的系统,应该将 BlockCache 设大些,比如设置BlockCache =0.4,Memstore=0.39。这会加大缓存命中率。
优化手段主要有以下四个方面
Region
如果没有预建分区的话,那么随着region中条数的增加,region会进行分裂,这将增加I/O开销,所以解决方法就是根据你的RowKey设计来进行预建分区,减少region的动态分裂。
HFile
HFile是数据底层存储文件,在每个memstore进行刷新时会生成一个HFile,当HFile增加到一定程度时,会将属于一个region的HFile进行合并,这个步骤会带来开销但不可避免,但是合并后HFile大小如果大于设定的值,那么HFile会重新分裂。为了减少这样的无谓的I/O开销,建议估计项目数据量大小,给HFile设定一个合适的值
关闭Compaction,在闲时进行手动Compaction
因为HBase中存在Minor Compaction和Major Compaction,也就是对HFile进行合并,所谓合并就是I/O读写,大量的HFile进行肯定会带来I/O开销,甚至是I/O风暴,所以为了避免这种不受控制的意外发生,建议关闭自动Compaction,在闲时进行compaction
批量数据写入时采用BulkLoad
如果通过HBase-Shell或者JavaAPI的put来实现大量数据的写入,那么性能差是肯定并且还可能带来一些意想不到的问题,所以当需要写入大量离线数据时建议使用BulkLoad
开启过滤,提高查询速度
开启BloomFilter,BloomFilter是列族级别的过滤,在生成一个StoreFile同时会生成一个MetaBlock,用于查询时过滤数据
使用压缩:一般推荐使用Snappy和LZO压缩
RowKey设计:应该具备以下几个属性
散列性:散列性能够保证相同相似的rowkey聚合,相异的rowkey分散,有利于查询
简短性:rowkey作为key的一部分存储在HFile中,如果为了可读性将rowKey设计得过长,那么将会增加存储压力
唯一性:rowKey必须具备明显的区别性
业务性:举些例子
假如我的查询条件比较多,而且不是针对列的条件,那么rowKey的设计就应该支持多条件查询
如果我的查询要求是最近插入的数据优先,那么rowKey则可以采用叫上Long.Max-时间戳的方式,这样rowKey就是递减排列
列族的设计
列族的设计需要看应用场景
多列族设计的优劣
优势:
HBase中数据时按列进行存储的,那么查询某一列族的某一列时就不需要全盘扫描,只需要扫描某一列族,减少了读I/O;
其实多列族设计对减少的作用不是很明显,适用于读多写少的场景。
劣势:
降低了写的I/O性能。原因如下:数据写到store以后是先缓存在memstore中,同一个region中存在多个列族则存在多个store,每个store都一个memstore,当其实memstore进行flush时,属于同一个region的store中的memstore都会进行flush,增加I/O开销。
在 Hbase 的表中,每个列族对应 Region 中的一个Store,Region的大小达到阈值时会分裂,因此如果表中有多个列族,则可能出现以下现象:
一个Region中有多个Store,如果每个CF的数据量分布不均匀时,比如CF1为100万,CF2为1万,则Region分裂时导致CF2在每个Region中的数据量太少,查询CF2时会横跨多个Region导致效率降低。
如果每个CF的数据分布均匀,比如CF1有50万,CF2有50万,CF3有50万,则Region分裂时导致每个CF在Region的数据量偏少,查询某个CF时会导致横跨多个Region的概率增大。
多个CF代表有多个Store,也就是说有多个MemStore(2MB),也就导致内存的消耗量增大,使用效率下降。
Region 中的 缓存刷新 和 压缩 是基本操作,即一个CF出现缓存刷新或压缩操作,其它CF也会同时做一样的操作,当列族太多时就会导致IO频繁的问题。
LSM树(Log Structured Merge Tree,结构化合并树)的思想非常朴素,就是将对数据的修改增量保持在内存中,达到指定的大小限制后将这些修改操作批量写入磁盘(由此提升了写性能),是一种基于硬盘的数据结构,与B-tree相比,能显著地减少硬盘磁盘臂的开销。当然凡事有利有弊,LSM树和B+树相比,LSM树牺牲了部分读性能,用来大幅提高写性能。
读取时需要合并磁盘中的历史数据和内存中最近的修改操作,读取时可能需要先看是否命中内存,否则需要访问较多的磁盘文件(存储在磁盘中的是许多小批量数据,由此降低了部分读性能。但是磁盘中会定期做merge操作,合并成一棵大树,以优化读性能)。LSM树的优势在于有效地规避了磁盘随机写入问题,但读取时可能需要访问较多的磁盘文件。
核心思想的核心就是放弃部分读能力,换取写入的最大化能力,放弃磁盘读性能来换取写的顺序性。极端的说,基于LSM树实现的HBase的写性能比Mysql高了一个数量级,读性能低了一个数量级。
插入数据可以看作是一个N阶合并树。数据写操作(包括插入、修改、删除也是写)都在内存中进行,
数据首先会插入内存中的树。当内存树的数据量超过设定阈值后,会进行合并操作。合并操作会从左至右便利内存中树的子节点 与 磁盘中树的子节点并进行合并,会用最新更新的数据覆盖旧的数据(或者记录为不同版本)。当被合并合并数据量达到磁盘的存储页大小时。会将合并后的数据持久化到磁盘,同时更新父节点对子节点的指针。
读数据 磁盘中书的非子节点数据也被缓存到内存中。在需要进行读操作时,总是从内存中的排序树开始搜索,如果没有找到,就从磁盘上的排序树顺序查找。
在LSM树上进行一次数据更新不需要磁盘访问,在内存即可完成,速度远快于B+树。当数据访问以写操作为主,而读操作则集中在最近写入的数据上时,使用LSM树可以极大程度地减少磁盘的访问次数,加快访问速度。
删除数据 前面讲了。LSM树所有操作都是在内存中进行的,那么删除并不是物理删除。而是一个逻辑删除,会在被删除的数据上打上一个标签,当内存中的数据达到阈值的时候,会与内存中的其他数据一起顺序写入磁盘。 这种操作会占用一定空间,但是LSM-Tree 提供了一些机制回收这些空间。
Spring框架为开发Java应用程序提供了全面的基础架构支持。它包含一些很好的功能,如依赖注入和开箱即用的模块,如:Spring JDBC 、Spring MVC 、Spring Security、 Spring AOP 、Spring ORM 、Spring Test。
Spring Boot基本上是Spring框架的扩展,它消除了设置Spring应用程序所需的XML配置,为更快,更高效的开发生态系统铺平了道路。
以下是Spring Boot中的一些特点:
1:创建独立的spring应用
2:嵌入Tomcat, Jetty Undertow 而且不需要部署他们
3:提供的“starters” poms来简化Maven配置
4:尽可能自动配置spring应用
5:提供生产指标,健壮检查和外部化配置
6:没有绝对代码生成和XML配置要求
以下内容摘录自:Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理,具体的参数对比我觉得意义不大,这里将作者的评价列出来。
一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了;
后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高;
不过现在确实越来越多的公司,会去用 RocketMQ,确实很不错(阿里出品),但社区可能有突然黄掉的风险,对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。
所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。
如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范
提到这两个工具就必须先了解ORM(Object Relational Mapping),即对象关系映射。它的实现思想就是将关系数据库中表的数据映射成为对象,以对象的形式展现,这样开发人员就可以把对数据库的操作转化为对这些对象的操作。因此它的目的是为了方便开发人员以面向对象的思想来实现对数据库的操作。
Servlet是一种独立于操作系统平台和网络传输协议的服务器端的Java应用程序。
每个Servlet都对应一个URL地址,可以作为显式URL引用调用,或嵌入在HTML中并从Web应用程序返回。
对于每个Web应用,都可以存在一个配置文件web.xml,存放关于Servlet的名称、对应的Java类文件、URL地址映射等信息。自JavaEE6后,JavaEE规范推荐使用注解来配置Web组件。
Web容器收到请求后,Web容器会产生一个新的线程来调用Servlet的service(),service()方法检查HTTP请求类型(GET、POST、PUT、DELETE等),然后相应调用doGet()、doPost()、doPut()、doDelete()等方法。
一个Servlet同一时刻只有一个实例。
当多个请求发送到同一个Servlet,服务器会为每个请求创建一个新线程来处理。
链接:https://www.jianshu.com/p/280c7e720d0c
首先,对于一个web应用,其部署在web容器中,web容器提供其一个全局的上下文环境,这个上下文就是ServletContext,其为后面的spring IoC容器提供宿主环境;
其 次,在web.xml中会提供有contextLoaderListener。在web容器启动时,会触发容器初始化事件,此时 contextLoaderListener会监听到这个事件,其contextInitialized方法会被调用,在这个方法中,spring会初始 化一个启动上下文,这个上下文被称为根上下文,即WebApplicationContext,这是一个接口类,确切的说,其实际的实现类是 XmlWebApplicationContext。这个就是spring的IoC容器,其对应的Bean定义的配置由web.xml中的 context-param标签指定。在这个IoC容器初始化完毕后,spring以WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE为属性Key,将其存储到ServletContext中,便于获取;
再 次,contextLoaderListener监听器初始化完毕后,开始初始化web.xml中配置的Servlet,这里是DispatcherServlet,这个servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个servlet请 求。DispatcherServlet上下文在初始化的时候会建立自己的IoC上下文,用以持有spring mvc相关的bean。在建立DispatcherServlet自己的IoC上下文时,会利用WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE先从ServletContext中获取之前的根上下文(即WebApplicationContext)作为自己上下文的parent上下文。有了这个 parent上下文之后,再初始化自己持有的上下文。这个DispatcherServlet初始化自己上下文的工作在其initStrategies方 法中可以看到,大概的工作就是初始化处理器映射、视图解析等。这个servlet自己持有的上下文默认实现类也是 XmlWebApplicationContext。初始化完毕后,spring以与servlet的名字相关(此处不是简单的以servlet名为 Key,而是通过一些转换,具体可自行查看源码)的属性为属性Key,也将其存到ServletContext中,以便后续使用。这样每个servlet 就持有自己的上下文,即拥有自己独立的bean空间,同时各个servlet共享相同的bean,即根上下文(第2步中初始化的上下文)定义的那些 bean。
使用内存数据库做缓存提高系统性能和并发能力。
SDS 定义:
struct sdshdr{
//记录buf数组中已使用字节的数量
//等于 SDS 保存字符串的长度
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
链表
通过为链表设置不同类型的特定函数,Redis链表可以保存各种不同类型的值,除了用作列表键,还在发布与订阅、慢查询、监视器等方面发挥作用
字典
Redis的字典底层使用哈希表实现,每个字典通常有两个哈希表,一个平时使用,另一个用于rehash时使用,使用链地址法解决哈希冲突。
跳跃表
跳跃表通常是有序集合的底层实现之一,表中的节点按照分值大小进行排序。可参考:https://www.cnblogs.com/hunternet/p/11248192.html
整数集合(intset)
整数集合是集合键的底层实现之一,底层由数组构成,升级特性能尽可能的节省内存。
压缩列表
压缩列表是Redis为节省内存而开发的顺序型数据结构,通常作为列表键和哈希键的底层实现之一。
不管是定期采样删除还是惰性删除都不是一种完全精准的删除,就还是会存在key没有被删除掉的场景,所以就需要内存淘汰策略进行补充。
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,(一般没人用)
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(最常用)
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,一般没人用
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(一般不太合适)
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除
参考:https://www.cnblogs.com/justinli/p/6809791.html
redis是基于内存的数据库,提供了内存数据持久化到文件的两种方式,一种是写RDB文件方式,另一种是写AOF文件,默认执行的是RDB文件持久化方式。
redis如何同步RDB文件 ,通常情况下,redis通过读取配置文件定期保存数据库的状态到RDB文件
Redis除了提供RDB文件的持久化方式外,还提供了AOF持久化机制,与RDB保存数据库的键值对的方式不同的是,AOF提供的持久化机制保存的是redis执行的命令
参考:https://blog.csdn.net/zeb_perfect/article/details/54135506
缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
解决方案
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决方案
缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案
使用互斥锁(mutex key)
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
"提前"使用互斥锁(mutex key):
在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。‘
“永远不过期”
把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)基于zookeeper临时有序节点可以实现的分布式锁。
参考:https://gitee.com/shishan100/Java-Interview-Advanced/blob/master/docs/high-concurrency/redis-consistence.md
Cache Aside Pattern最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
更新的时候,先更新数据库,然后再删除缓存。
上述方法可以解决初级的问题,但显然不够严谨。更严谨的方案是使用队列等待缓存更新:
更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。
一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。
这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤
redis是基于内存的,内存的读写速度非常快;
redis是单线程的,省去了很多上下文切换线程的时间;
redis使用多路复用技术,可以处理并发的连接。非阻塞IO 内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间。
参考:https://blog.csdn.net/wteruiycbqqvwt/article/details/90299610
(1)select==>时间复杂度O(n)
它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
(2)poll==>时间复杂度O(n)
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
(3)epoll==>时间复杂度O(1)
epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
参考:https://www.jianshu.com/p/486b0965c296
同步阻塞 IO(blocking IO)、 同步非阻塞 IO(nonblocking IO)、IO 多路复用( IO multiplexing)、信号驱动式IO(signal-driven IO)、异步非阻塞 IO(asynchronous IO)
在一个典型的一主多从的系统中,slave在整个体系中起到了数据冗余备份和读写分离的作用。当master遇到异常终端后,需要从slave中选举一个新的master继续对外提供服务,这种机制在前面提到过N次,比如在zk中通过leader选举、kafka中可以基于zk的节点实现master选举。所以在redis中也需要一种机制去实现master的决策,redis并没有提供自动master选举功能,而是需要借助一个哨兵来进行监控。
Redis提供一些命令SETNX,GETSET,可以方便实现分布式锁机制。
SETNX命令(SET if Not eXists)
语法:
SETNX key value
功能:
当且仅当 key 不存在,将 key 的值设为 value ,并返回1;若给定的 key 已经存在,则 SETNX 不做任何动作,并返回0。
GETSET命令
语法:
GETSET key value
功能:
将给定 key 的值设为 value ,并返回 key 的旧值 (old value),当 key 存在但不是字符串类型时,返回一个错误,当key不存在时,返回nil。
GET命令
语法:
GET key
功能:
返回 key 所关联的字符串值,如果 key 不存在那么返回特殊值 nil 。
DEL命令
语法:
DEL key [KEY …]
功能:
删除给定的一个或多个 key ,不存在的 key 会被忽略。
实现
SETNX 可以直接加锁操作,比如说对某个关键词foo加锁,客户端可以尝试
SETNX foo.lock <current unix time>
如果返回1,表示客户端已经获取锁,可以往下操作,操作完成后,通过
DEL foo.lock
命令来释放锁。
如果返回0,说明foo已经被其他客户端上锁,如果锁是非堵塞的,可以选择返回调用。如果是堵塞调用,就需要进入以下个重试循环,直至成功获得锁或者重试超时。
处理死锁
在上面的处理方式中,如果获取锁的客户端端执行时间过长,进程被kill掉,或者因为其他异常崩溃,导致无法释放锁,就会造成死锁。所以,需要对加锁要做时效性检测。因此,我们在加锁时,把当前时间戳作为value存入此锁中,通过当前时间戳和Redis中的时间戳进行对比,如果超过一定差值,认为锁已经时效,防止锁无限期的锁下去,但是,在大并发情况,如果同时检测锁失效,并简单粗暴的删除死锁,再通过SETNX上锁,可能会导致竞争条件的产生,即多个客户端同时获取锁。
C1获取锁,并崩溃。C2和C3调用SETNX上锁返回0后,获得foo.lock的时间戳,通过比对时间戳,发现锁超时。
C2 向foo.lock发送DEL命令。
C2 向foo.lock发送SETNX获取锁。
C3 向foo.lock发送DEL命令,此时C3发送DEL时,其实DEL掉的是C2的锁。
C3 向foo.lock发送SETNX获取锁。
此时C2和C3都获取了锁,产生竞争条件,如果在更高并发的情况,可能会有更多客户端获取锁。所以,DEL锁的操作,不能直接使用在锁超时的情况下,幸好我们有GETSET方法,假设我们现在有另外一个客户端C4,看看如何使用GETSET方式,避免这种情况产生。
C1获取锁,并崩溃。C2和C3调用SETNX上锁返回0后,调用GET命令获得foo.lock的时间戳T1,通过比对时间戳,发现锁超时。
C4 向foo.lock发送GESET命令,
GETSET foo.lock <current unix time>
并得到foo.lock中老的时间戳T2
如果T1=T2,说明C4获得时间戳。
如果T1!=T2,说明C4之前有另外一个客户端C5通过调用GETSET方式获取了时间戳,C4未获得锁。只能sleep下,进入下次循环中。
命令 | 描述 |
---|---|
LPUSH | 从左边插入数据 |
RPUSH | 从右边插入数据 |
LPOP | 从左边取出一个数据 |
RPOP | 从右边取出一个数据 |
BLPOP | 取数据时如果没有数据会等待指定时间 |
BRPOP | 取数据时如果没有数据会等待指定时间 |
命令 | 描述 |
---|---|
PUBLISH | 指令可用于发布一条消息,格式 PUBLISH channel message |
SUBSCRIBE | 指令用于接收一条消息,格式 SUBSCRIBE channel |
SUBSCRIBE | 可以使用指令UNSUBSCRIBE退订,如果不加参数,则会退订所有由SUBSCRIBE指令订阅的频道。 |
PSUBSCRIBE | Redis还支持基于通配符的消息订阅,使用指令PSUBSCRIBE (pattern subscribe) |
Sortes Set(有序列表)
可以自定义消息ID,在消息ID有意义时,比较重要。
stream
Redis5.0带来了Stream类型。从字面上看是流类型,但其实从功能上看,应该是Redis对消息队列(MQ,Message Queue)的完善实现。
命令 | 描述 |
---|---|
XADD | 命令用于在某个stream(流数据)中追加消息 |
XREAD | 从Stream中读取消息 |
XGROUP | 用于管理消费者组,提供创建组,销毁组,更新组起始消息ID等操作 |
XREADGROUP | 分组消费消息操作 |
Stream 是基于 RadixTree 数据结构实现的。
spring的启动其实就是IOC容器的启动
首先,对于一个web应用,其部署在web容器中,web容器提供其一个全局的上下文环境,这个上下文就是ServletContext
,其为后面的spring IoC容器提供宿主环境;
其 次,在web.xml中会提供有contextLoaderListener。在web容器启动时,会触发容器初始化事件,此时 contextLoaderListener会监听到这个事件,其contextInitialized方法会被调用,在这个方法中,spring会初始 化一个启动上下文,这个上下文被称为根上下文,即WebApplicationContext
,这是一个接口类,确切的说,其实际的实现类是 XmlWebApplicationContext
。这个就是spring的IoC容器,其对应的Bean定义的配置由web.xml中的 context-param标签指定。在这个IoC容器初始化完毕后,spring以WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE为属性Key,将其存储到ServletContext
中,便于获取;
再次,contextLoaderListener监听器初始化完毕后,开始初始化web.xml中配置的Servlet,这里是DispatcherServlet
,这个servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个servlet请 求。DispatcherServlet
上下文在初始化的时候会建立自己的IoC上下文,用以持有spring mvc相关的bean。在建立DispatcherServlet
自己的IoC上下文时,会利用WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE先从ServletContext
中获取之前的根上下文(即WebApplicationContext
)作为自己上下文的parent上下文。有了这个 parent上下文之后,再初始化自己持有的上下文。这个DispatcherServlet
初始化自己上下文的工作在其initStrategies
方 法中可以看到,大概的工作就是初始化处理器映射、视图解析等。这个servlet自己持有的上下文默认实现类也是 XmlWebApplicationContext
。初始化完毕后,spring以与servlet的名字相关(此处不是简单的以servlet名为 Key,而是通过一些转换,具体可自行查看源码)的属性为属性
Spring是通过递归的方式获取目标bean及其所依赖的bean的;Spring实例化一个bean的时候,是分两步进行的,首先实例化目标bean,然后为其注入属性。
结合这两点,也就是说,Spring在实例化一个bean的时候,是首先递归的实例化其所依赖的所有bean,直到某个bean没有依赖其他bean,此时就会将该实例返回,然后反递归的将获取到的bean设置为各个上层bean的属性的。
事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播。
创建一个服务提供者 (eureka client),当client向server注册时,它会提供一些元数据,例如主机和端口,URL,主页等。Eureka server 从每个client实例接收心跳消息。 如果心跳超时,则通常将该实例从注册server中删除。
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。
Feign是一个声明式的伪Http客户端,它使得写Http客户端变得更简单。使用Feign,只需要创建一个接口并注解。Feign默认集成了Ribbon,并和Eureka结合,默认实现了负载均衡的效果。
定义一个feign接口
@FeignClient(value = "service-hi")
public interface SchedualServiceHi {
@RequestMapping(value = "/hi", method = RequestMethod.GET)
String sayHiFromClientOne(@RequestParam(value = "name") String name);
}
当有hi
请求的时候,通过调用sayHiFromClientOne
自动将请求分配到其他注册了service-hi
的主机。
@RestController
public class HiController {
@Autowired
SchedualServiceHi schedualServiceHi;
@RequestMapping(value = "/hi",method = RequestMethod.GET)
public String sayHi(@RequestParam String name){
return schedualServiceHi.sayHiFromClientOne(name);
}
}
在Spring Cloud可以用RestTemplate+Ribbon和Feign来调用。为了保证其高可用,单个服务通常会集群部署。由于网络原因或者自身的原因,服务并不能保证100%可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩”效应。
@TableName
:映射数据库表名@TableFieId
:映射非主键字段@TableId
:表主键标识@Version
:更新时,实体对象的version属性必须有值,才会生成SQL @Version
@TableField(value="version", fill = FieldFill.INSERT, update="%s+1")
protected Long version;
Mapper中一个提供了9个顶层标签,除了1个已经过期的我们不需要去了解,另外8个都是必须要掌握的,只要熟练掌握了标签的使用,使用MyBatis才能如鱼得水。
select用来映射查询语句,比如下面就是一个简单的select标签的使用:
<select id="listUserByUserName" parameterType="String" resultType="lwUser">
select user_id,user_name from lw_user where user_name=#{userName}
select>
<insert
id="insertLwUser"
parameterType="lwUser"
parameterMap="deprecated"
flushCache="true"
statementType="PREPARED"
keyProperty=""
keyColumn=""
useGeneratedKeys=""
timeout="20"
databaseId="mysql"
lang="">
update用来映射更新语句。
delete用来映射删除语句。
这个元素可以被用来定义可重用的 SQL 代码段,可以包含在其他语句中。
如下是一个最简单的例子:
<select id="selectUserAddress" resultType="com.lonelyWolf.mybatis.model.UserAddress">
select <include refid="myCloumn">include> from lw_user_address
select>
<sql id="myCloumn" >
id,address
sql>
MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制。但是默认情况下只开启了一级缓存,即局部的session缓存,如果想要开启二级缓存。那么就需要使用到cache标签
<cache
type="com.lonelyWolf.xxx"
eviction="FIFO"
flushInterval="60000"
readOnly="true"
size="512"/>
在 Kafka 中,Topic 是一个存储消息的逻辑概念,可以认为是一个消息集合。每条消息发送到 Kafka 集群的消息都有一个类别。物理上来说,不同的 Topic 的消息是分开存储的,每个 Topic 可以有多个生产者向它发送消息,也可以有多个消费者去消费其中的消息。
每个 Topic 可以划分多个分区(每个 Topic 至少有一个分区),同一 Topic 下的不同分区包含的消息是不同的。每个消息在被添加到分区时,都会被分配一个 offset,它是消息在此分区中的唯一编号,Kafka 通过 offset 保证消息在分区内的顺序,offset 的顺序不跨分区,即 Kafka 只保证在同一个分区内的消息是有序的。
Kafka 中最基本的数据单元就是消息,而一条消息其实是由 Key + Value 组成的(Key 是可选项,可传空值,Value 也可以传空值),这也是与 ActiveMQ 不同的一个地方。在发送一条消息时,我们可以指定这个 Key,那么 Producer 会根据 Key 和 partition 机制来判断当前这条消息应该发送并存储到哪个 partition 中(这个就跟分片机制类似)。我们可以根据需要进行扩展 Producer 的 partition 机制(默认算法是 hash 取 %)。
要注意的是如果 Consumer 数量比 partition 数量多,会有的 Consumer 闲置无法消费,这样是一个浪费。如果 Consumer 数量小于 partition 数量会有一个 Consumer 消费多个 partition。Kafka 在 partition 上是不允许并发的。Consuemr 数量建议最好是 partition 的整数倍。 还有一点,如果 Consumer 从多个 partiton 上读取数据,是不保证顺序性的,Kafka 只保证一个 partition 的顺序性,跨 partition 是不保证顺序性的。增减 Consumer、broker、partition 会导致 Rebalance。
在 Kafka 中,同一个 Group 中的消费者对于一个 Topic 中的多个 partition 存在一定的分区分配策略。
在 Kafka 中,存在两种分区分配策略,一种是 Range(默认)、另一种是 RoundRobin(轮询)。通过partition.assignment.strategy 这个参数来设置。
Range strategy(范围分区)
Range 策略是对每个主题而言的,首先对同一个主题里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。假设我们有10个分区,3个消费者,排完序的分区将会是0,1,2,3,4,5,6,7,8,9;消费者线程排完序将会是C1-0, C2-0, C3-0。然后将 partitions 的个数除于消费者线程的总数来决定每个消费者线程消费几个分区。如果除不尽,那么前面几个消费者线程将会多消费一个分区。
RoundRobin strategy(轮询分区)
轮询分区策略是把所有 partition 和所有 Consumer 线程都列出来,然后按照 hashcode 进行排序。最后通过轮询算法分配partition 给消费线程。如果所有 Consumer 实例的订阅是相同的,那么 partition 会均匀分布。
当出现以下几种情况时,Kafka 会进行一次分区分配操作,也就是 Kafka Consumer 的 Rebalance
Kafka Consuemr 的 Rebalance 机制规定了一个 Consumer group 下的所有 Consumer 如何达成一致来分配订阅 Topic 的每个分区。而具体如何执行分区策略,就是前面提到过的两种内置的分区策略。而 Kafka 对于分配策略这块,提供了可插拔的实现方式,也就是说,除了这两种之外,我们还可以创建自己的分配机制。
谁来执行 Rebalance 以及管理 Consumer 的 group 呢?
Consumer group 如何确定自己的 coordinator 是谁呢,消费者向 Kafka 集群中的任意一个 broker 发送一个 GroupCoord inatorRequest 请求,服务端会返回一个负载最小的 broker 节点的 id,并将该 broker 设置为 coordinator。
在 Rebalance 之前,需要保证 coordinator 是已经确定好了的,整个 Rebalance 的过程分为两个步骤,Join和Syncjoin:表示加入到 Consumer group中,在这一步中,所有的成员都会向 coordinator 发送 joinGroup 的请求。一旦所有成员都发了 joinGroup请求,那么 coordinator 会选择一个 Consumer 担任 leader 角色,并把组成员信息和订阅信息发送给消费者
分区中的所有副本统称为AR (Assigned Repllicas)。所有与leader副本保持一定程度同步的副本(包括Leader)组成ISR(In-Sync Replicas),ISR集合是AR集合中的一个子集。消息会先发送到leader副本,然后follower副本才能从leader副本中拉取消息进行同步,同步期间内follower副本相对于leader副本而言会有一定程度的滞后。前面所说的“一定程度”是指可以忍受的滞后范围,这个范围可以通过参数进行配置。与leader副本同步滞后过多的副本(不包括leader)副本,组成OSR(Out-Sync Relipcas),由此可见:AR=ISR+OSR。在正常情况下,所有的follower副本都应该与leader副本保持一定程度的同步,即AR=ISR,OSR集合为空。
Leader副本负责维护和跟踪ISR集合中所有的follower副本的滞后状态,当follower副本落后太多或者失效时,leader副本会吧它从ISR集合中剔除。如果OSR集合中follower副本“追上”了Leader副本,之后再ISR集合中的副本才有资格被选举为leader,而在OSR集合中的副本则没有机会(这个原则可以通过修改对应的参数配置来改变)
KafkaProducer的消息在send()之后,可能会经过拦截器、序列化器、分区器之后才会真正到达Kafka-Broker中的Partition中
在Kafka中,高水位的作用主要有两个
**Log End Offset(LEO)**表示副本写入下一条消息的位移值。注意,数字 15 所在的方框是虚线,这就说明,这个副本当前只有 15 条消息,位移值是从 0 到 14,下一条新消息的位移是 15。显然,介于高水位和 LEO 之间的消息就属于未提交消息。这也从侧面告诉了我们一个重要的事实,那就是:同一个副本对象,其高水位值不会大于 LEO 值。
高水位和 LEO 是副本对象的两个重要属性。Kafka 所有副本都有对应的高水位和 LEO 值,而不仅仅是 Leader 副本。只不过 Leader 副本比较特殊,Kafka 使用 Leader 副本的高水位来定义所在分区的高水位。换句话说,分区的高水位就是其 Leader 副本的高水位。
生产者数据的不丢失
kafka 的 ack 机制:在 kafka 发送数据的时候,每次发送消息都会有一个确认反馈机制,确保消息正常的能够被收到。
消费者数据的不丢失
通过 offset commit 来保证数据的不丢失,kafka 自己记录了每次消费的 offset 数值,下次继续消费的时候,接着上次的 offset 进行消费即可。
保存的数据不丢失
kafka按照partition保存, 数据可以保存多个副本 , 副本中有一个副本是 Leader,其余的副本是 follower , follower会定期的同步leader的数据
Kafka 的事务处理,主要是允许应用可以把消费和生产的 batch 处理(涉及多个 Partition)在一个原子单元内完成,操作要么全部完成、要么全部失败。为了实现这种机制,我们需要应用能提供一个唯一 id,即使故障恢复后也不会改变,这个 id 就是 TransactionnalId(也叫 txn.id,后面会详细讲述),txn.id 可以跟内部的 PID 1:1 分配,它们不同的是 txn.id 是用户提供的,而 PID 是 Producer 内部自动生成的(并且故障恢复后这个 PID 会变化),有了 txn.id 这个机制,就可以实现多 partition、跨会话的 EOS 语义。
当用户使用 Kafka 的事务性时,Kafka 可以做到的保证:
上面是从 Producer 的角度来看,那么如果从 Consumer 角度呢?Consumer 端很难保证一个已经 commit 的事务的所有 msg 都会被消费,有以下几个原因:
因此为了解决上述问题,Kafka大体采用了三个措施一起来解决这个问题。
LSO
Kafka添加了一个很重要概念,叫做LSO,即last stable offset。对于同一个TopicPartition,其offset小于LSO的所有transactional message的状态都已确定,要不就是committed,要不就是aborted。而broker对于read_committed的consumer,只提供offset小于LSO的消息。这样就避免了consumer收到状态不确定的消息,而不得不buffer这些消息。
Aborted Transaction Index
对于每个LogSegment(对应于一个log文件),broker都维护一个aborted transaction index. 这是一个append only的文件,每当有事务被abort时,就会有一个entry被append进去。
Consumer端根据aborted transactions的消息过滤(以下对只针对read_committed的consumer)
consumer端会根据fetch response里提供的aborted transactions里过滤掉aborted的消息,只返回给用户committed的消息。
包括三种角色:leader,candidate和follower。
follow:所有节点都以follower的状态开始,如果没有收到leader消息则会变成candidate状态。
candidate:会向其他节点拉选票,如果得到大部分的票则成为leader,这个过程是Leader选举。
leader:所有对系统的修改都会先经过leader。
其有两个基本过程:
Leader选举:每个candidate随机经过一定时间都会提出选举方案,最近阶段中的票最多者被选为leader。
同步log:leader会找到系统中log(各种事件的发生记录)最新的记录,并强制所有的follow来刷新到这个记录。
Raft一致性算法是通过选出一个leader来简化日志副本的管理,例如,,日志项(log entry)只允许从leader流向follower。
两个阶段分别是准备(prepare)和提交(commit)。准备阶段解决大家对哪个提案进行投票的问题,提交阶段解决确认最终值的问题。
简单来说,提案者发出提案后,收到一些反馈,有两种结果,一种结果是自己的提案被大多数节点接受了,另外一种是没被接受,没被接受就过会再试试。
提案者收到来自大多数的接受反馈,也不能认为这就是最终确认。因为这些接收者并不知道自己刚反馈的提案就是全局的绝对大多数。
所以,引入新的一轮再确认阶段是必须的,提案者在判断这个提案可能被大多数接受的情况下,发起一轮新的确认提案。这就进入了提交阶段。
提交阶段的提案发送出去,其他阶段进行提案值比较,返回最大的,所以提案者收到返回消息不带新的提案,说明锁定成功,如果有新的提案内容,进行提案值最大比较,然后替换更大的值。如果没有收到足够多的回复,则需要再次发出请求。
一旦多数接受了共同的提案值,则形成决议,称为最终确认的提案。
地址映射过程中,若在页面中发现所要访问的页面不再内存中,则产生缺页中断。当发生缺页中断时操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。下面列出几种常见的置换算法。
每个页面都可以用在该页面首次被访问前所要执行的指令数进行标记。最佳页面置换算法规定:标记最大的页应该被置换。这个算法唯一的一个问题就是它无法实现。当缺页发生时,操作系统无法知道各个页面下一次是在什么时候被访问。虽然这个算法不可能实现,但是最佳页面置换算法可以用于对可实现算法的性能进行衡量比较。
这种算法的实质是,总是选择在主存中停留时间最长(即最老)的一页置换,即先进入内存的页,先退出内存。
当必须置换一个页面时,LRU算法选择过去一段时间里最久未被使用的页面。
因实现LRU算法必须有大量硬件支持,还需要一定的软件开销。所以实际实现的都是一种简单有效的LRU近似算法:
一种LRU近似算法是最近未使用算法(Not Recently Used,NUR)。它在存储分块表的每一表项中增加一个引用位,操作系统定期地将它们置为0。当某一页被访问时,由硬件将该位置1。过一段时间后,通过检查这些位可以确定哪些页使用过,哪些页自上次置0后还未使用过。就可把该位是0的页淘汰出去,因为在之前最近一段时间里它未被访问过。
在采用最少使用置换算法时,应为在内存中的每个页面设置一个移位寄存器,用来记录该页面被访问的频率。该置换算法选择在之前时期使用最少的页面作为淘汰页。由于存储器具有较高的访问速度,例如100 ns,在1 ms时间内可能对某页面连续访 问成千上万次,因此,通常不能直接利用计数器来记录某页被访问的次数,而是采用移位寄存器方式。每次访问某页时,便将该移位寄存器的最高位置1,再每隔一定时间(例如100 ns)右移一次。这样,在最近一段时间使用最少的页面将是∑Ri最小的页。
在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中。这个沙盘就是虚拟地址空间(Virtual Address Space),在32位模式下它是一个4GB的内存地址块。虚拟地址通过页表(Page Table)映射到物理内存,页表由操作系统维护并被处理器引用。
名称 | 存储内容 |
---|---|
栈 | 局部变量、函数参数、返回地址等 |
堆 | 动态分配的内存 |
BSS段 | 未初始化或初值为0的全局变量和静态局部变量 |
数据段 | 已初始化且初值非0的全局变量和静态局部变量 |
代码段 | 可执行代码、字符串字面值、只读变量 |
在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载,并在内存中为这些段分配空间。栈也由操作系统分配和管理;堆由程序员自己管理,即显式地申请和释放空间。
BSS段、数据段和代码段是可执行程序编译时的分段,运行时还需要栈和堆。
从 Linux 内核的角度来说,其实它并没有线程的概念。Linux 把所有线程都当做进程来实现,它将线程和进程不加区分的统一到了 task_struct 中。线程仅仅被视为一个与其他进程共享某些资源的进程,而是否共享地址空间几乎是进程和 Linux 中所谓线程的唯一区别。Linux中,进程和线程都被维护为一个task_struct
结构,线程和进程被同等对待来进行调度。
Linux通过socket睡眠队列来管理所有等待socket的某个事件的process,同时通过wakeup机制来异步唤醒整个睡眠队列上等待事件的process,通知process相关事件发生。通常情况,socket的事件发生的时候,其会顺序遍历socket睡眠队列上的每个process节点,调用每个process节点挂载的callback函数。在遍历的过程中,如果遇到某个节点是排他的,那么就终止遍历,总体上会涉及两大逻辑:(1)睡眠等待逻辑;(2)唤醒逻辑。
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的。
select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
内核态与用户态是操作系统的两种运行级别,当程序运行在3级特权级上时,就可以称之为运行在用户态。因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;
当程序运行在0级特权级上时,就可以称之为运行在内核态。
运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态(比如操作硬件)。
这两种状态的主要差别是
用户级线程(user level thread):对于这类线程,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。在应用程序启动后,操作系统分配给该程序一个进程号,以及其对应的内存空间等资源。应用程序通常先在一个线程中运行,该线程被成为主线程。在其运行的某个时刻,可以通过调用线程库中的函数创建一个在相同进程中运行的新线程。用户级线程的好处是非常高效,不需要进入内核空间,但并发效率不高。
内核级线程(kernel level thread):对于这类线程,有关线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只能调用内核线程的接口。内核维护进程及其内部的每个线程,调度也由内核基于线程架构完成。内核级线程的好处是,内核可以将不同线程更好地分配到不同的CPU,以实现真正的并行计算。
事实上,在现代操作系统中,往往使用组合方式实现多线程,即线程创建完全在用户空间中完成,并且一个应用程序中的多个用户级线程被映射到一些内核级线程上,相当于是一种折中方案。
大内核,就是将操作系统的全部功能都放进内核里面,包括调度、文件系统、网络、设备驱动器、存储管理等等,组成一个紧密连接整体。大内核的优点就是效率高,但是很难定位bug,拓展性比较差,每次需要增加新的功能,都要将新的代码和原来的内核代码重新编译。
微内核与单体内核不同,微内核只是将操作中最核心的功能加入内核,包括IPC、地址空间分配和基本的调度,这些东西都在内核态运行,其他功能作为模块被内核调用,并且是在用户空间运行。微内核比较好维护和拓展,但是效率可能不高,因为需要频繁地在内核态和用户态之间切换。
在五状态模型里面,进程一共有5中状态,分别是创建、就绪、运行、终止、阻塞。
运行状态就是进程正在CPU上运行。在单处理机环境下,每一时刻最多只有一个进程处于运行状态。
就绪状态就是说进程已处于准备运行的状态,即进程获得了除CPU之外的一切所需资源,一旦得到CPU即可运行。
阻塞状态就是进程正在等待某一事件而暂停运行,比如等待某资源为可用或等待I/O完成。即使CPU空闲,该进程也不能运行。
运行态→阻塞态:往往是由于等待外设,等待主存等资源分配或等待人工干预而引起的。
阻塞态→就绪态:则是等待的条件已满足,只需分配到处理器后就能运行。
运行态→就绪态:不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,这时候就变成就绪态。例如时间片用完,或有更高优先级的进程来抢占处理器等。
就绪态→运行态:系统按某种策略选中就绪队列中的一个进程占用处理器,此时就变成了运行态。
死锁产生的根本原因是多个进程竞争资源时,进程的推进顺序出现不正确。
互斥:每个资源要么已经分配给了一个进程,要么就是可用的。
占有和等待:已经得到了某个资源的进程可以再请求新的资源。
不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。
死锁预防是指通过破坏死锁产生的四个必要条件中的一个或多个,以避免发生死锁。
破坏互斥:不让资源被一个进程独占,可通过假脱机技术允许多个进程同时访问资源;
破坏占有和等待:有两种方案,已拥有资源的进程不能再去请求其他资源。一种实现方法是要求进程在开始执行前请求需要的所有资源。要求进程请求资源时,先暂时释放其当前拥有的所有资源,再尝试一次获取所需的全部资源。
破坏不可抢占:有些资源可以通过虚拟化方式实现可抢占;
破坏循环等待:有两种方案:一种方法是保证每个进程在任何时刻只能占用一个资源,如果要请求另一个资源,必须先释放第一个资源;另一种方法是将所有资源进行统一编号,进程可以在任何时刻请求资源,但要求进程必须按照顺序请求资源。
可以允许系统进入死锁状态,但会维护一个系统的资源分配图,定期调用死锁检测算法来检测途中是否存在死锁,检测到死锁发生后,采取死锁恢复算法进行恢复。
死锁检测方法如下:
在资源分配图中,找到不会阻塞又不独立的进程结点,使该进程获得其所需资源并运行,运行完毕后,再释放其所占有的全部资源。也就是消去该进程结点的请求边和分配边。
使用上面的算法进行一系列简化,若能消去所有边,则表示不会出现死锁,否则会出现死锁。检测到死锁后,就需要解决死锁。目前操作系统中主要采用如下几种方法:
取消所有死锁相关线程,简单粗暴,但也确实是最常用的把每个死锁线程回滚到某些检查点,然后重启连续取消死锁线程直到死锁解除,顺序基于特定最小代价原则连续抢占资源直到死锁解除。
分页就是说,将磁盘或者硬盘分为大小固定的数据块,叫做页,然后内存也分为同样大小的块,叫做页框。当进程执行的时候,会将磁盘的页载入内存的某些页框中,并且正在执行的进程如果发生缺页中断也会发生这个过程。页和页框都是由两个部分组成的,一个是页号或者页框号,一个是偏移量。分页一般是有硬件来完成的,每个页都对应一个页框,它们的对应关系存放在一个叫做页表的数据结构中,页号作为这个页表的索引,页框号作为页表的值。操作系统负责维护这个页表。
Linux文件系统里面有文件和目录,组成一个树状的结构,树的每一个叶子节点表示文件或者空目录。每个文件基本上都由两部分组成:
inode:一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 编号;
block:记录文件的内容,文件太大时,会占用多个 block。
除此之外还包括:
superblock:记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等;
block bitmap:记录 block 是否被使用的位图。
当要读取一个文件的内容时,先在 inode 中查找文件内容所在的所有 block,然后把所有 block 的内容读出来。
在计算机中,负数以原码的补码形式表达。
原码:一个正数,按照绝对值大小转换成的二进制数;一个负数按照绝对值大小转换成的二进制数,然后最高位补1,称为原码。
反码:正数的反码与原码相同,负数的反码为对该数的原码除符号位外各位取反。
补码:正数的补码与原码相同,负数的补码为对该数的原码除符号位外各位取反,然后在最后一位加1.
红黑树是一种含有红黑结点并能自平衡的二叉查找树。
性质,它必须满足下面性质:
操作:
左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。如图3。
右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。如图4。
变色:结点的颜色由红变黑或由黑变红。
AVL的平衡通过平衡因子(bf=左树深度-右树深度)和左单旋、右单旋、左右双旋、右左双旋四种操作保证。
删除旋转红黑树保证最多三次,而AVL树则可能是对数级别的。但是真实序列里影响不大。