写这篇文章的目的是为了在摸鱼或者备考时打开看一看,感受一下计算机知识海洋的浩瀚。本文部分内容是我自己的理解也有部分是网络上我认为总结的比较好进而抄录整理得来。
文章目录
- 1. 网络类
- 1.1. HTTP/1.0/1.1/2.0的区别
- 1.2. OSI七层模型与TCP/IP五层模型
- 1.3. TCP与UDP的区别
- 1.4. TCP粘包问题
- 1.5. 网络数据包结构
- 1.6. TCP
- 1.7. SYN泛洪攻击
- 1.8. 在浏览器输入url后并回车发生了哪些过程?
- 1.9. HTTP与HTTPS的区别
- 1.10. Websocket
- 1.11. SSL连接的过程、对称加密、非对称加密
- 1.12. HTTP请求的各种方法、状态码
- 1.13. GET和POST的区别
- 1.14. 请求行、请求头、请求体、响应行、响应头、响应体都包括什么
- 1.15. Session和Cookie的区别以及如何解决分布式session问题
- 1.16. DNS解析过程
- 1.17. REST
- 1.18. CSRF攻击及防御
- 1.19. XSS攻击
- 2. 语言类
- 2.1. 进程和线程的区别
- 2.2. 协程与线程
- 2.3. 线程安全的定义、线程的状态
- 2.4. 多线程的实现方式(Runnable和Callable的区别)、start/run方法的区别
- 2.5. 子线程异常捕捉
- 2.6. wait()/notify()/sleep()/yield()/join()几个方法的意义
- 2.7. notifyAll实现原理及等待池和锁池的概念
- 2.8. 线程池的创建方式,7大参数、阻塞队列、拒绝策略、大小
- 2.9. 乐观锁CAS、悲观锁 synchronized和ReentrantLock、实现原理以及区别
- 2.10. Lock与synchronized的区别
- 2.11. ABA问题
- 2.12. 锁优化:偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等
- 2.13. volatile和synchronized的区别及JAVA内存模型
- 2.14. ThreadLocal线程本地存储原理
- 2.15. 指令重排序
- 2.16. final关键字
- 2.17. 反射
- 2.18. 内存泄漏问题
- 2.19. AQS同步队列器原理,CLH队列
- 2.20. AQS组件:ReentrantReadWriteLock、CountDownLatch、CyclicBarrier、Semaphore原理掌握
- 2.21. JUC原子类
- 2.22. JDK
- 2.23. Java集合
- 2.24. 集合框架的多线程实现类
- 2.25. 谈谈依赖注入 IOC
- 2.26. 谈谈面向切面编程 AOP
- 2.27. Java注解
- 2.28. Java中面向对象的特性
- 2.29. 动态代理和静态代理
- 2.30. Spring的两种动态代理:Jdk和Cglib 的区别和实现
- 2.31. JAVA版本特性
- 2.32. java如何实现泛型?
- 2.33. JVM
- 2.34. 类加载:双亲委派
- 2.35. 设计模式
- 3. 数据库
- 3.1. 必备技能 寿司SQL
- 3.2. SQL
- 3.3. SQL优化
- 3.4. SQL执行过程
- 3.5. 死锁及优化
- 3.6. ACID
- 3.7. 数据库事务、隔离级别、Innodb和Myisam的区别
- 3.8. InnoDB、MyISAM引擎的区别
- 3.9. 共享锁、排他锁、悲观锁、乐观锁、行锁、表锁
- 3.10. 索引的数据结构(B+树)、索引优缺点
- 3.11. MySQL索引到底使用int还是varchar?
- 3.12. MVCC
- 3.13. 最左前缀原则、索引优化
- 3.14. 聚簇索引、覆盖索引
- 3.15. 分页查询怎么用索引优化?
- 3.16. 数据库三大范式
- 3.17. 主从复制、主从一致、分库分表
- 3.18. mysq的binlog三种模式的区别(row,statement,mixed)
- 3.19. MongoDB
- 3.20. HBase
- 4. 框架类
- 5. 工具类
- 6. OS类
- 7. 算法
1. 网络类
1.1. HTTP/1.0/1.1/2.0的区别
这里只列举几个我认为比较重要的区别:
带宽优化及网络连接的使用
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 里面
报文头压缩
1.x:header压缩。前面提到HTTP1.x的header很多时候都是重复多余的。选择合适的压缩算法可以减小包的大小和数量。
2.0:HTTP2.0 消息头的压缩算法采用 HPACK(增量更新),而非 SPDY 采用的DEFLATE。
缓存处理
1.0 :在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,
1.1 : HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
错误通知的管理
1.1: 在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
Host头处理
1.0 : 在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。
1.1 :HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。
服务器推送
1.x 、2.0 :服务器推送是在客户端请求之前发送数据的机制。当一个请求是由主页发起的,那么服务器可能会响应主页的内容、样式表和图片等,因为服务器知道客户端会用这些东西. 最大的好处可以缓存,那么就可以实现共享缓存资源。
1.2. OSI七层模型与TCP/IP五层模型
OSI | TCP/IP | 描述 | 常见协议 | 设备 |
---|---|---|---|---|
应用层 | 应用层 | 为用户直接提供各种网络服务。 | HTTP,FTP | |
表示层 | 用于应用层数据的编码和转换功能 | telnet | ||
会话层 | 负责建立、管理和终止表示层实体之间的通信会话 | DNS | ||
传输层 | 传输层 | 建立了主机端到端的链接 | TCP,UDP | 四层交换机、四层路由器 |
网络层 | 网络层 | IP寻址来建立两个节点之间的连接 | IP ICMP | 路由器、三层交换机 |
链路层 | 链路层 | 使用链路层地址 (以太网使用MAC地址)来访问介质。 | ARP RARP PPP | 网桥、以太网交换机、网卡 |
物理层 | 物理层 | 通过物理介质传输比特流 | MLT-3 | 中继器、集线器、双绞线 |
1.2.1. 为什么有5层7层之分
因为TCP/IP五层模型更在意围绕TCP/IP协议展开的一系列通信协议的分层,因此它不包括其他一些不想干的协议。
1.3. TCP与UDP的区别
UDP | TCP | |
---|---|---|
是否连接 | 无连接 | 面向连接 |
是否可靠 | 不可靠传输,不使用流量控制和拥塞控制 | 可靠传输,使用流量控制和拥塞控制 |
连接对象个数 | 支持一对一,一对多,多对一和多对多交互通信 | 只能是一对一通信 |
传输方式 | 面向报文 | 面向字节流 |
首部开销 | 首部开销小,仅8字节 | 首部最小20字节,最大60字节 |
适用场景 | 适用于实时应用(IP电话、视频会议、直播等) | 适用于要求可靠传输的应用,例如文件传输 |
1.4. TCP粘包问题
概念
TCP粘包就是指发送方发送的若干包数据到达接收方时粘成了一包,从接收缓冲区来看,后一包数据的头紧接着前一包数据的尾,出现粘包的原因是多方面的,可能是来自发送方,也可能是来自接收方。
产生原因
(1)发送方原因
TCP默认使用Nagle算法(主要作用:减少网络中报文段的数量),而Nagle算法主要做两件事:
只有上一个分组得到确认,才会发送下一个分组收集多个小分组,在一个确认到来时一起发送。Nagle算法造成了发送方可能会出现粘包问题
(2)接收方原因
TCP接收到数据包时,并不会马上交到应用层进行处理,或者说应用层并不会立即处理。实际上,TCP将接收到的数据包保存在接收缓存里,然后应用程序主动从缓存读取收到的分组。这样一来,如果TCP接收数据包到缓存的速度大于应用程序从缓存中读取数据包的速度,多个包就会被缓存,应用程序就有可能读取到多个首尾相接粘到一起的包。
如何解决
(1)发送方
对于发送方造成的粘包问题,可以通过关闭Nagle算法来解决,使用TCP_NODELAY选项来关闭算法。
(2)接收方
接收方没有办法来处理粘包现象,只能将问题交给应用层来处理。
(2)应用层
应用层的解决办法简单可行,不仅能解决接收方的粘包问题,还可以解决发送方的粘包问题。
解决办法:
格式化数据:每条数据有固定的格式(开始符,结束符),这种方法简单易行,但是选择开始符和结束符时一定要确保每条数据的内部不包含开始符和结束符。
发送长度:发送每条数据时,将数据的长度一并发送,例如规定数据的前4位是数据的长度,应用层在处理时可以根据长度来判断每个分组的开始和结束位置。
1.5. 网络数据包结构
| 以太网首部 | IP首部 | TCP首部 | 应用数据 | 以太网尾部 |
1.6. TCP
1.6.1. TCP拥塞控制
三个拥塞控制机制
慢启动
加法式增加/乘法式减少拥塞窗口
快速重发和快速恢复
1.6.1.1. 慢启动
线性增加发送量
对网络接近满载来说是一个正确的方法;
但对从零开始起步的连接来说时间又太长;
- Congestion Window是发送方设置的一个变量
- 指数式增加CW,而不是线性。
- 开始,源把CW置为1(一个包);
- 然后每收到一个ACK,将CW*2
- 当该包的ACK到达后,TCP可以发2个包;
- 当收到2个ACK后,CW*2,发4个包,故每个RTT内包数翻一倍
- 如果发生丢包后,,源端再次进入慢启动;当超过一个发送方设置的阈值:ssthresh(slowstart threshold),CW会变为线性增加,这其实是一种拥塞避免
1.6.1.2. 拥塞窗口和可发送窗口
Congestion Window (CW)
源端TCP为每个连接维护的一个状态变量;
限制在给定时间内容许传输的数据量,慢启动的最大值也受到CongestionWindow影响
容许的最大未确认字节数就是现在的CongestionWindow和AdvertisedWindow小的那个
MaxWindow = MIN(CongestionWindow, AdvertisedWindow)
可以发送的窗口大小,为可能新增的未确认字节数:
EffectiveWindow = MaxWindow - (LastByte - LastByteAcked)
但发生拥塞的时候一般CW < EffectiveWindow
1.6.1.3. 加法式增加/乘法式减少拥塞窗口
TCP源慢启动之后:
如果超过ssthresh,可能会导致当拥塞程度上升就线性CW
当拥塞程度下降(收到ACK)时,就增加CW
上述二者合在一起就叫:加法式增加/乘法式减少 AIMD
1.6.1.4. 拥塞感知
观察主要原因:
包未交付,并导致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
1.6.1.5. 快速重传和快速恢复
Tahoe引入快速重传,Reno引入快速恢复
快速重传
每次接收方收一个数据包
接收方响应一个ACK;(早先规定,后来优化)
当包到达失序,TCP因为较早的数据还没有到达,也不能应答含有这些包的数据;
TCP发送它上次发送了的那个相同的ACK
相同ACK的第二次发送称着Duplicate ACK,
当发送方看到重复ACK,知道对方收到了一个失序包,这说明这个ACK序号后面的包丢失了
实际情况中
发送方TCP等到3个Duplicate ACK后就重发丢失的包;即快速重传,比等待报文超时要快
快速恢复
当快速重传同时快速恢复(Duplicated ACK)
Fast Recovery算法如下:
- 把ssthresh设置为cwnd的一半
- 把cwnd再设置为等于ssthresh的值,不再从1开始(具体实现有些为ssthresh+3,因为三个重复的ACK,表示有三个报文离开网络了,可以新增三个)
- 重新进入拥塞避免阶段,再收到重复ACK,拥塞窗口+1(不会加剧拥塞,因为有ACK报文,说明可以增加发送)
- 收到新的报文ACK时候,ssthresh变为第一步的大小(说明所有丢失的报文都收到了,可以快速恢复)
1.6.2. TCP三次握手和四次挥手的目的
三次握手的目的是为了确保双方都准备好,才建立连接。防止失效的连接请求(在网络节点中滞留时间长),导致客户端重复发起请求,造成错误和资源浪费。连接->确认->确认
四次挥手是为了保证被动关闭的一方可以将自己的数据发送完。关闭->确认->发送数据 ->关闭->确认
1.6.3. TIME_WAIT
- time_wait状态如何产生
由上面的变迁图,首先调用close()发起主动关闭的一方,在发送最后一个ACK之后会进入time_wait的状态,也就说该发送方会保持2MSL时间之后才会回到初始状态。MSL值得是数据包在网络中的最大生存时间。产生这种结果使得这个TCP连接在2MSL连接等待期间,定义这个连接的四元组(客户端IP地址和端口,服务端IP地址和端口号)不能被使用。
- time_wait状态产生的原因
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状态。
1.6.4. 服务器产生大量TIME_WAIT的原因
主动关闭连接的一方必须维持TIME_WAIT状态等待FIN防止被动方重发导致抛出connection reset SocketException
。因此在处理大量短连接的时候可能导致这一现象。
1.7. SYN泛洪攻击
A(攻击者)发送TCP SYN,SYN是TCP三次握手中的第一个数据包,而当这个服务器返回ACK以后,A不再进行确认,那这个连接就处在了一个挂起的状态,也就是半连接的意思,那么服务器收不到再确认的一个消息,还会重复发送ACK给A。这样一来就会更加浪费服务器的资源。A就对服务器发送非法大量的这种TCP连接,由于每一个都没法完成握手的机制,所以它就会消耗服务器的内存最后可能导致服务器死机,就无法正常工作了。更进一步说,如果这些半连接的握手请求是恶意程序发出,并且持续不断,那么就会导致服务端较长时间内丧失服务功能——这样就形成了DoS攻击。这种攻击方式就称为SYN泛洪攻击
如何防范?
最常用的一个手段就是优化主机系统设置。比如降低SYN timeout时间,使得主机尽快释放半连接的占用或者采用SYN cookie设置,如果短时间内收到了某个IP的重复SYN请求,我们就认为受到了攻击。我们合理的采用防火墙设置等外部网络也可以进行拦截
1.8. 在浏览器输入url后并回车发生了哪些过程?
- 解析URL
- DNS解析
- 浏览器与网站建立TCP连接
- 请求和传输数据
- 浏览器渲染页面
1.9. HTTP与HTTPS的区别
- https协议需要到ca申请证书,一般免费证书很少,需要交费。
- http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议。
- http和https使用的是完全不同的连接方式用的端口也不一样,前者是80,后者是443。
- http的连接很简单,是无状态的。
- HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。
1.10. Websocket
websocket相当于HTTP的一个补充协议,通过http request建立连接,不需要再发送request,之后保持一端与另外一端的TCP连接。
1.10.1. 使用Stomp消息
直接使用WebSocket(或SockJS)就很类似于使用TCP套接字来编写Web应用。STOMP在WebSocket之上提供了一个基于帧的线路格式(frame-based wire format)层,用来定义消息的语义。与HTTP请求和响应类似,STOMP帧由命令、一个或多个头信息以及负载所组成。例如,如下就是发送数据的一个STOMP帧:
SEND
destination:/app/marco
content-length:20
{\"message\":\"Marco!\"}
1.11. SSL连接的过程、对称加密、非对称加密
SSL (Secure Sockets Layer) 是用来保障你的浏览器和网站服务器之间安全通信,免受网络“中间人”窃取信息。
- 当你的浏览器向服务器请求一个安全的网页(通常是 https://)服务器就把它的证书和公匙发回来
- 浏览器检查证书是不是由可以信赖的机构颁发的,确认证书有效和此证书是此网站的
- 使用公钥加密了一个随机对称密钥,包括加密的URL一起发送到服务器
- 服务器用自己的私匙解密了你发送的钥匙。然后用这把对称加密的钥匙给你请求的URL链接解密
- 服务器用你发的对称钥匙给你请求的网页加密。你也有相同的钥匙就可以解密发回来的网页了
1.12. HTTP请求的各种方法、状态码
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),服务器出错未能成功处理服务端请求。
常见状态码:
200 OK 客户端请求成功
302 Found 重定向,客户端对新的URL发出新的Request
304 Not Modified 客户端可直接使用缓存文件
400 Bad Request 客户端请求有语法错误,不能被服务器理解
403 Forbidden 服务器收到请求,但是拒绝提供服务
404 Not Found 请求资源不存在
500 Internal Server Error 服务器发生不可预期的错误
502 Bad Gateway:作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应
503 Server Unavailable 服务器当前不能处理客户端的请求,一段时间后可能恢复 正常
504 Gateway Time-out:作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI标识出的服务器,例如HTTP、FTP、LDAP)或者辅助服务器(例如DNS)收到响应
1.12.1. 302和303的区别
简单说下就是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可以自动重定向)。
1.12.2. 2xx的状态码
201 请求成功并且服务器创建了新的资源。
202 接受请求但没创建资源;
203 返回另一资源的请求;
204 服务器成功处理了请求,但没有返回任何内容;
205 服务器成功处理了请求,但没有返回任何内容;
206 处理部分请求;
1.13. GET和POST的区别
- get数据一般在url里以query string的形式传输,post在body里。
- 语义不同,get一般表示获取资源,post表示提交资源。
1.14. 请求行、请求头、请求体、响应行、响应头、响应体都包括什么
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.15. Session和Cookie的区别以及如何解决分布式session问题
1、session保存在服务器,客户端不知道其中的信息;cookie保存在客户端,服务器能够知道其中的信息
2、session中保存的是对象,cookie中保存的是字符串
3、session不能区分路径,同一个用户在访问一个网站期间,所有的session在任何一个地方都可以访问到。而cookie中如果设置了路径参数,那么同一个网站中不同路径下的cookie互相是访问不到的
4、session需要借助cookie才能正常工作。如果客户端完全禁止cookie,session将失效。
如何解决分布式session问题?
- 直接将信息存储在cookie中
- session复制,在集群中的几台服务器之间同步session对象,使每台服务器上都保存了所有用户的session信息
- 使用nginx进行session绑定,基于nginx的ip-hash策略,可以对客户端和服务器进行绑定,同一个客户端就只能访问该服务器,无论客户端发送多少次请求都被同一个服务器处理
- 基于redis存储session
1.16. DNS解析过程
- 在浏览器中输入 www.qq.com 域名,操作系统会先检查自己本地的hosts文件是否有这个网址映射关系,如果有,就先调用这个IP地址映射,完成域名解析。
- 如果hosts里没有这个域名的映射,则查找本地DNS解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。
- 如果hosts与本地DNS解析器缓存都没有相应的网址映射关系,首先会找TCP/ip参数中设置的首选DNS服务器,在此我们叫它本地DNS服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。
- 如果要查询的域名,不由本地DNS服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个IP地址映射,完成域名解析,此解析不具有权威性。
- 如果本地DNS服务器本地区域文件与缓存解析都失效,则根据本地DNS服务器的设置(是否设置转发器)进行查询,如果未用转发模式,本地DNS就把请求发至13台根DNS,根DNS服务器收到请求后会判断这个域名(.com)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个IP。本地DNS服务器收到IP信息后,将会联系负责.com域的这台服务器。这台负责.com域的服务器收到请求后,如果自己无法解析,它就会找一个管理.com域的下一级DNS服务器地址(http://qq.com)给本地DNS服务器。当本地DNS服务器收到这个地址后,就会找http://qq.com域服务器,重复上面的动作,进行查询,直至找到www.qq.com主机。
- 如果用的是转发模式,此DNS服务器就会把请求转发至上一级DNS服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根DNS或把转请求转至上上级,以此循环。不管是本地DNS服务器用是是转发,还是根提示,最后都是把结果返回给本地DNS服务器,由此DNS服务器再返回给客户机。
从客户端到本地DNS服务器是属于递归查询,而DNS服务器之间就是的交互查询就是迭代查询。
1.17. REST
表示性状态转移(representation state transfer)。简单来说,就是用URI表示资源,用HTTP方法(GET, POST, PUT, DELETE)表征对这些资源的操作。
1.18. CSRF攻击及防御
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF,是一种对网站的恶意利用。成因就是网站的cookie在浏览器中不会过期,只要不关闭浏览器或者退出登录,那以后只要是访问这个网站,都会默认你已经登录的状态。而在这个期间,攻击者发送了构造好的csrf脚本或包含csrf脚本的链接,可能会执行一些用户不想做的功能(比如是添加账号等)。这个操作不是用户真正想要执行的。
防御:
第一步:用户访问某个表单页面。
第二步:服务端生成一个Token,放在用户的Session中,或者浏览器的Cookie中。
第三步:在页面表单附带上Token参数。(可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。)
第四步:用户提交请求后,服务端验证表单中的Token是否与用户Session(或Cookies)中的Token一致, 一致为合法请求,不是则非法请求。
在前后端分离的前提下(例如使用ajax提交数据)设置不了token,可以给 cookie 新增 SameSite 属性,通过这个属性可以标记哪个 cookie 只作为同站 cookie (即第一方 cookie,不能作为第三方 cookie),既然不能作为第三方 cookie ,那么别的网站发起第三方请求时,第三方网站是收不到这个被标记关键 cookie,后面的鉴权处理就好办了。这一切都不需要做 token 生命周期的管理,也不用担心 Referer 会丢失或被中途被篡改。
1.19. XSS攻击
XSS 攻击是指攻击者在网站上注入恶意的客户端代码,通过恶意脚本对客户端网页进行篡改,从而在用户浏览网页时,对用户浏览器进行控制或者获取用户隐私数据的一种攻击方式。
2. 语言类
2.1. 进程和线程的区别
进程是系统进行资源分配和调度的一个独立单位
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源
2.2. 协程与线程
协程,是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。
协程在子程序内部是可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行。
2.2.1. 协程的优势
协程的特点在于是一个线程执行,那和多线程比,协程有何优势?
极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;
不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
2.3. 线程安全的定义、线程的状态
参考了 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方法而死亡。 |
2.4. 多线程的实现方式(Runnable和Callable的区别)、start/run方法的区别
java实现多线程有四种方式:继承Thread类、实现Runnable()接口、实现Callable接口、通过线程池启动多线程(实现runable)。
Callable通过FutureTask可以有返回值,而且可以抛出异常。
方法run()称为线程体。通过调用Thread类的start()方法来启动一个线程
2.5. 子线程异常捕捉
正常情况下,如果不做特殊的处理,在主线程中是不能够捕获到子线程中的异常的。如果想要在主线程中捕获子线程的异常,我们需要使用ExecutorService
:
- 首先在创建线程工厂的时候
ExecutorService exec = Executors.newCachedThreadPool(new HandleThreadFactory());
将我们自定义的工厂类传入。 - 在工厂类中为线程设置
t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandle());
异常处理。 - 重写
Thread.UncaughtExceptionHandler
中的uncaughtException(Thread t, Throwable e)
方法。
如果不需要每个线程单独设置异常处理的话,可以使用Thread的静态方法:Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandle());
2.6. wait()/notify()/sleep()/yield()/join()几个方法的意义
wait方法的作用是将当前运行的线程挂起(即让其进入阻塞状态),直到notify或notifyAll方法来唤醒线程
只要在同一对象上去调用notify/notifyAll方法,就可以唤醒对应对象monitor上等待的线程了。
sleep方法的作用是让当前线程暂停指定的时间(毫秒),sleep方法是最简单的方法,唯一需要注意的是其与wait方法的区别。最简单的区别是,wait方法依赖于同步,而sleep方法可以直接调用。而更深层次的区别在于sleep方法只是暂时让出CPU的执行权,并不释放锁。而wait方法则需要释放锁。
yield方法的作用是暂停当前线程,以便其他线程有机会执行,不过不能指定暂停的时间,并且也不能保证当前线程马上停止。yield方法只是将Running状态转变为Runnable状态。
join方法的作用是父线程等待子线程执行完成后再执行,换句话说就是将异步执行的线程合并为同步的线程。
2.7. notifyAll实现原理及等待池和锁池的概念
obj.notifyAll()方法唤醒所有阻塞在 obj 对象上的沉睡线程,然后被唤醒的众多线程竞争 obj 对象的 monitor 占有权,最终得到的那个线程会继续执行下去,但其他线程继续阻塞。
每个对象都有一个唯一与之对应的内部锁(Monitor)。这里就涉及到锁池和等待池的概念。当多个线程想要获取某个对象的锁,而该对象已经被其他线程拥有,这些线程就会进入该对象的锁池等待锁的释放。当一个线程主动调用wait方法,就会进入等待池不会和其他线程竞争锁的持有。所以这时候就需要调用notify或者notifyAll方法,让该线程进入锁池重新参与锁的争取。notify只会随机选取一个线程,notifyAll则会将所有等待池中的线程加入到锁池。
2.8. 线程池的创建方式,7大参数、阻塞队列、拒绝策略、大小
线程池创建的三种方式。
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:尝试和最老的线程进行竞争,也不会抛出异常
2.9. 乐观锁CAS、悲观锁 synchronized和ReentrantLock、实现原理以及区别
参考: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 之后,再来理解读写锁就很简单了。读写锁有一个读锁和一个写锁,分别对应读操作和锁操作。锁的特性如下:
只有一个线程可以获取到写锁。在获取写锁时,只有没有任何线程持有任何锁才能获取成功;
如果有线程正持有写锁,其他任何线程都获取不到任何锁;
没有线程持有写锁时,可以有多个线程获取到读锁。
2.10. Lock与synchronized的区别
- Lock是一个接口,属于JDK层面的实现;而synchronized属于Java语言的特性,其实现有JVM来控制(代码执行完毕,出现异常,wait时JVM会主动释放锁)。
- synchronized在发生异常时,会自动释放掉锁,故不会发生死锁现(此时的死锁一般是代码逻辑引起的);而Lock必须在finally中主动unlock锁,否则就会出现死锁。
- Lock能够响应中断,让等待状态的线程停止等待;而synchronized不行。
- 通过Lock可以知道线程是否成功获得了锁,而synchronized不行。
- Lock提高了多线程下对读操作的效率。
2.11. ABA问题
我们假设有两个线程同时使用CAS操作对同一个值为A的变量进行修改。
线程1:A -> B -> A
线程2:A -> C
由于线程2获取A之后,线程1对A进行了修改,所以理论上线程2对于C的修改是应该失败的,但由于线程2在写入C的时候的预期值是A所以这个操作最后成功了。ABA问题实际上就是说虽然CAS能够对预期值进行估计,但是却无法判断相同的预期值是否发生了修改。因此可以使用版本号来解决这个问题。
2.11.1. 使用AtomicStampedReference避免ABA问题
对于需要自己进行CAS处理的地方,我们可以使用“AtomicStampedReference”来进行数据的处理。它既支持泛型,同时还可以避免传统CAS中ABA的问题,使数据更加安全。
private final static AtomicStampedReference<Integer> stamp = new AtomicStampedReference<>(100, 1);
stamp.compareAndSet(100, 200, stamp.getStamp(), stamp.getStamp() + 1);
stamp.compareAndSet(200, 100, stamp.getStamp(), stamp.getStamp() + 1);
AtomicStampedReference
主要是使用CAS机制更新新的值reference和时间戳stamp。而最终调用的底层是一个本地的方法对数据进行的修改。
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
2.12. 锁优化:偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等
synchronized 是重量级锁,由于消耗太大,虚拟机对其做了一些优化。
自旋锁与自适应自旋
在许多应用中,锁定状态只会持续很短的时间,为了这么一点时间去挂起恢复线程,不值得。我们可以让等待线程执行一定次数的循环,在循环中去获取锁。这项技术称为自旋锁,它可以节省系统切换线程的消耗,但仍然要占用处理器。在 JDK1.4.2 中,自选的次数可以通过参数来控制。 JDK 1.6又引入了自适应的自旋锁,不再通过次数来限制,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
锁消除
虚拟机在运行时,如果发现一段被锁住的代码中不可能存在共享数据,就会将这个锁清除。
锁粗化
当虚拟机检测到有一串零碎的操作都对同一个对象加锁时,会把锁扩展到整个操作序列外部。如 StringBuffer 的 append 操作。
轻量级锁
对绝大部分的锁来说,在整个同步周期内都不存在竞争。如果没有竞争,轻量级锁可以使用 CAS 操作避免使用互斥量的开销。
偏向锁
偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,当这个线程再次请求锁时,无需再做任何同步操作,即可获取锁。
2.13. volatile和synchronized的区别及JAVA内存模型
简单概括volatile,它能够使变量在值发生改变时能尽快地让其他线程知道。
如果要了解volatile就必须先了解java的内存模型:
(1)每个线程都有自己的本地内存空间(java栈中的帧)。线程执行时,先把变量从内存读到线程自己的本地内存空间,然后对变量进行操作。
(2)对该变量操作完成后,在某个时间再把变量刷新回主内存。
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
volatile和synchronized区别
- volatile本质是在告诉jvm当前变量在寄存器中的值是不确定的,需要从主存中读取,synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
- volatile仅能使用在变量级别,synchronized则可以使用在变量,方法.
- volatile仅能实现变量的修改可见性,而synchronized则可以保证变量的修改可见性和原子性.
《Java编程思想》上说,定义long或double变量时,如果使用volatile关键字,就会获得(简单的赋值与返回操作)原子性。 - volatile不会造成线程的阻塞,而synchronized可能会造成线程的阻塞.
- 当一个域的值依赖于它之前的值时,volatile就无法工作了,如n=n+1,n++等。如果某个域的值受到其他域的值的限制,那么volatile也无法工作,如Range类的lower和upper边界,必须遵循lower<=upper的限制。(其他线程也有可能同步修改,无法保证其他线程可以知道自己栈里的情况)
- 使用volatile而不是synchronized的唯一安全的情况是类中只有一个可变的域
2.14. ThreadLocal线程本地存储原理
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区别
- 采用synchronized进行同步控制,但是效率略低,使得并发变同步(串行)
- 采用ThreadLocal线程本地存储,为每个使用该变量的线程都存储一个本地变量副本(线程互不相干)
2.15. 指令重排序
在虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用CPU。
在硬件层面,CPU会将接收到的一批指令按照其规则重排序,同样是基于CPU速度比缓存速度快的原因,和上一点的目的类似,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机可以在更大层面、更多指令范围内重排序。硬件的重排序机制参见《从JVM并发看CPU内存指令重排序(Memory Reordering)》
Java提供了两个关键字volatile
和synchronized
来保证多线程之间操作的有序性,volatile
关键字本身通过加入内存屏障来禁止指令的重排序,而synchronized
关键字通过一个变量在同一时间只允许有一个线程对其进行加锁的规则来实现。在单线程程序中,不会发生“指令重排”和“工作内存和主内存同步延迟”现象,只在多线程程序中出现。
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-LevelParallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
2.16. final关键字
- final成员变量表示常量,只能被赋值一次,赋值后值不再改变(final要求地址值不能改变;当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化
- 使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义,不能被重写;第二个原因是效率,final方法比非final方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。(注:类的private方法会隐式地被指定为final方法)
- 当用final修饰一个类时,表明这个类不能被继承。
2.16.1. final实现原理
对于final域,编译器和处理器要遵守两个重排序规则:
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
原因:编译器会在final域的写之后,插入一个StoreStore屏障 - 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
编译器会在读final域操作的前面插入一个LoadLoad屏障
2.17. 反射
反射的常用类和函数:Java反射机制的实现要借助于4个类:Class,Constructor,Field,Method;其中class代表的是类对象,Constructor-类的构造器对象,Field-类的属性对象,Method-类的方法对象,通过这四个对象我们可以粗略的看到一个类的各个组成部分。
2.17.1. 常用的反射流程
- Class.forName 找到类;
- newInstance 开辟内存空间,构造实例;
- getDeclaredMethod 根据方法签名搜索方法体;
- method.invoke 执行方法调用。
2.17.2. 反射调用为什么开销大?
Class.forName 会尝试先去方法区(1.8以后就是 Metaspace) 中寻找有没有对应的类,如果没有则在 classpath 中找到对应的class文件, 通过类加载器将其加载到内存里。注意这个方法涉及本地方法调用,这里本地方法调用的切换、类的搜索以及类的加载都存在开销;newInstance ,开辟内存空间,实例化已经找到的 Class;getDeclaredMethod/getMethod 遍历 Class 的方法,以方法签名匹配出所需要的 Method 实例,也是比较重的开销所在;虽然 java.lang.Class 内部维护了一套名为 reflectionData 的数据结构,其本身是 SoftReference 的。Class 会将匹配成功的 method 缓存到这里,以供下次访问命中,这样一来会减轻 method 遍历的开销,但是会增加额外的堆空间消耗(以空间置换时间)
- Method#invoke 方法会对参数做封装和解封操作
我们可以看到,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…
- JIT 无法优化
另外,JDK 开发者们也给我们提供了一套优化思路,既然反射调用有性能问题,不好优化,那能不能将反射调用变成普通调用呢?inflation 正是这条思路的解决方案。通过类字节码的生成并加载,实现了 inflation 。好处就是这不仅减少了本地方法和普通方法来回切换的开销,变成普通方法调用后还能享受到 JIT 编译器的优化福利,其中方法内联是最重要的优化点。编译器采集足够多的运行时数据后,根据统计模型得知某个反射方法成为热点,此时该反射方法体有几率会被内联进调用者的方法体中。
2.18. 内存泄漏问题
常见的内存泄漏及解决方法
1、单例造成的内存泄漏
2、非静态内部类创建静态实例造成的内存泄漏
因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,从而导致Activity的内存资源不能被正常回收。
解决方法:将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例
3、资源未关闭造成的内存泄漏
4、集合容器中的内存泄露
2.19. AQS同步队列器原理,CLH队列
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同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
2.20. AQS组件:ReentrantReadWriteLock、CountDownLatch、CyclicBarrier、Semaphore原理掌握
参考:https://www.cnblogs.com/jackion5/p/12932343.html
2.20.1. CountDownLatch
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的实现完整逻辑如下:
- 初始化CountDownLatch实际就是设置了AQS的state为计数的值
- 调用CountDownLatch的countDown方法时实际就是调用AQS的释放同步状态的方法,每调用一次就自减一次state值
- 调用await方法实际就调用AQS的共享式获取同步状态的方法acquireSharedInterruptibly(1),这个方法的实现逻辑就调用子类Sync的tryAcquireShared方法,只有当子类Sync的tryAcquireShared方法返回大于0的值时才算获取同步状态成功,否则就会一直在死循环中不断重试,直到tryAcquireShared方法返回大于等于0的值,而Sync的tryAcquireShared方法只有当AQS中的state值为0时才会返回1,否则都返回-1,也就相当于只有当AQS的state值为0时,await方法才会执行成功,否则就会一直处于死循环中不断重试。
2.20.2. CyclicBarrier
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仅单次有效。
2.20.3. Semaphore
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();
}
}
2.21. JUC原子类
根据修改的数据类型,可以将JUC包中的原子操作类可以分为4类。
基本类型: AtomicInteger, AtomicLong, AtomicBoolean ;
数组类型: AtomicIntegerArray, AtomicLongArray, AtomicReferenceArray ;
引用类型: AtomicReference, AtomicStampedRerence, AtomicMarkableReference ;
对象的属性修改类型: AtomicIntegerFieldUpdater, AtomicLongFieldUpdater, AtomicReferenceFieldUpdater
这些类存在的目的是对相应的数据进行原子操作。所谓原子操作,是指操作过程不会被中断,保证数据操作是以原子方式进行的。
原理:CAS+volatile + native方法来保证操作的原子性
2.22. JDK
2.22.1. String的实现原理
在Java语言中,所有类似“ABC”的字面值,都是String类的实例;String类位于java.lang包下,是Java语言的核心类,提供了字符串的比较、查找、截取、大小写转换等操作;Java语言为“+”连接符(字符串连接符)以及对象转换为字符串提供了特殊的支持,字符串对象可以使用“+”连接其他对象。
- String类被final关键字修饰,意味着String类不能被继承,并且它的成员方法都默认为final方法;字符串一旦创建就不能再修改。
- String类实现了Serializable、CharSequence、 Comparable接口。
- String实例的值是通过字符数组实现字符串存储的。
注意的点:
- "+"在字面量两边的时候,编译器会自动优化为一个字符串,除非编译器无法确定或存在final修饰符。另外一种情况则会隐式创建
StringBuilder
对象,转为append()
操作 - 每当创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。常量池是通过一个StringTable类实现的,它是一个Hash表。
- 如果不是用双引号声明的String对象,可以使用String提供的intern方法。intern 方法是一个native方法,intern方法会从字符串常量池中查询当前字符串是否存在,如果存在,就直接返回当前字符串;如果不存在就会将当前字符串放入常量池中。
- String是不可变字符序列,StringBuilder和StringBuffer是可变字符序列。StringBuilder是非线程安全的,StringBuffer是线程安全的。
2.22.2. HashMap实现
- HashMap 底层数据结构为 Node 类型数组,而 Node 的数据结构为链表。
- 负载因子默认为 0.75,当 HashMap 当前已使用容量大于当前大小 * 负载因子时,自动扩容一倍空间
- 树型阀值就是当链表长度超过这个值时,将 Node 的数据结构修改为红黑树,以便优化查找时间,默认值为8
put
方法:对 key 进行 hash 运算,然后再与当前 map 最后一个下标(长度-1,分布更均匀)进行与运算确定其在数组中的位置,正是因为这个算法,我们可以得知 HashMap 中元素是无序的。get
方法:根据 key 的 hash 运算值获取数组中对应下标的内容。循环链表或红黑树,然后匹配 value 值直至获得对应的值或返回 null- 1.8我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”
2.22.3. LinkedHashMap
- LinkedHashMap是继承于HashMap,是基于HashMap和双向链表来实现的。
- HashMap无序;LinkedHashMap有序,可分为插入顺序和访问顺序两种。如果是访问顺序,那put和get操作已存在的Entry时,都会把Entry移动到双向链表的表尾(其实是先删除再插入)。
- LinkedHashMap存取数据,还是跟HashMap一样使用的Entry[]的方式,双向链表只是为了保证顺序。
- LinkedHashMap是线程不安全的。
2.23. Java集合
2.23.1. HashMap为什么线程不安全?
在接近临界点时,若此时两个或者多个线程进行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操作时会发生数据覆盖的情况。
2.23.2. equals()和hashcode()
使得equals成立的时候,hashCode相等,也就是a.equals(b)->a.hashCode() == b.hashCode(),或者说此时,equals是hashCode相等的充分条件,hashCode相等是equals的必要条件(从数学课上我们知道它的逆否命题:hashCode不相等也不会equals),但是它的逆命题,hashCode相等一定equals以及否命题不equals时hashCode不等都不成立。
2.23.3. hashmap遍历时用map.remove方法为什么会报错?
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即可以知道有其他线程修改了集合。
2.24. 集合框架的多线程实现类
CopyOnWriteArrayList、CopyOnWriteArraySet、ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue、ConcurrentLinkedDeque
2.24.1. ConcurrentHashMap
其主要接口方法和HashMap是差不多的。但是,ConcurrentHashMap是使用了ReentranLock(可重入锁机制)来保证在多线程环境下是线程安全的。在JDK6、7后使用了分段锁,在JDK8使用Synchronized对Node头节点加锁和volatile对指针修饰来实现。
2.24.2. ConcurrentLinkedDeque
线程安全的双端队列,当然也可以当栈使用。由于是linked的,所以大小不受限制的。
2.24.3. ConcurrentLinkedQueue
线程安全的队列。
2.24.4. ConcurrentSkipListMap
基于跳表实现的线程安全的MAP。除了线程安全的特性外,该map还接受比较器进行排序的map,算法复杂度还是log(n)级别的。
2.24.5. ConcurrentSkipSet
基于4实现的set。
2.24.6. CopyOnWriteArrayList
以资源换取并发。通过迭代器快照的方式保证线程并发的访问。
2.24.7. CopyOnWriteArraySet
基于6实现的。
2.25. 谈谈依赖注入 IOC
IOC(控制反转)理论提出的观点大体是这样的:借助于“第三方”实现具有依赖关系的对象之间的解耦。所以依赖注入 DI和控制反转(IOC)是从不同的角度的描述的同一件事情,就是指通过引入IOC容器,利用依赖关系注入的方式,实现对象之间的解耦,除此之外还有另一种方法:服务定位器(Service Locator)。IOC中最基本的技术就是“反射(Reflection)”编程,通俗来讲就是根据给出的类名(字符串方式)来动态地生成对象。这种编程方式可以让对象在生成时才决定到底是哪一种对象。
2.26. 谈谈面向切面编程 AOP
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 注解的方法中执行 |
2.27. Java注解
注解的好处:本来可能需要很多配置文件,需要很多逻辑才能实现的内容,就可以使用一个或者多个注解来替代,这样就使得编程更加简洁,代码更加清晰。
注解这一概念是在java1.5版本提出的,说Java提供了一种原程序中的元素关联任何信息和任何元数据的途径的方法。
常见的作用有以下几种:
- 生成文档。这是最常见的,也是java 最早提供的注解。常用的有@see @param @return 等;
- 跟踪代码依赖性,实现替代配置文件功能。比较常见的是spring 2.5 开始的基于注解配置。作用就是减少配置。现在的框架基本都使用了这种配置来减少配置文件的数量;
- 在编译时进行格式检查。如@Override放在方法前,如果你这个方法并不是覆盖了超类方法,则编译时就能检查出;
2.28. Java中面向对象的特性
继承、多态和封装。
2.28.1. 封装
隐藏类内部的实现机制。
2.28.2. 继承
重用父类的代码。
2.28.3. 多态
多态是指一种事物的变现出来的多种特性。Java实现多态有三个必要条件:继承、重写、向上转型(使用父类声明的指针指向子类实例)。
2.29. 动态代理和静态代理
代理是设计模式的一种,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。Java中的代理分为三种角色:
- 代理类(ProxySubject)
- 委托类(RealSubject)
- 接口(Subject)
静态:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。或者使用Cglib
代理动态生成新的子类,所以不需要实现接口。
动态:在程序运行时运用反射机制动态创建而成。由于创建出来的委托类实例继承自Proxy
类,由于java不支持多继承所以委托类必须使用接口。
2.30. Spring的两种动态代理:Jdk和Cglib 的区别和实现
java动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。
而cglib动态代理是利用asm开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。
- 如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP
- 如果目标对象实现了接口,可以强制使用CGLIB实现AOP
- 如果目标对象没有实现了接口,必须采用CGLIB库,spring会自动在JDK动态代理和CGLIB之间转换
2.30.1. 为什么动态代理要实现接口
生成的代理类继承了Proxy,我们知道java是单继承的,所以JDK动态代理只能代理接口。
2.31. JAVA版本特性
2.31.1. Java7
- 创建泛型对象时应用类型推断。eg:
Map<String, Object> map = new Map<>();
- switch语句块中允许以字符串作为分支条件。
- try-with-resources(一个语句块中捕获多种异常)。
2.31.2. Java8
- lambda表达式。允许将功能当作方法参数或代码当作数据。lambda标识还可以更简洁的方式表示只有一个方法的接口(函数式接口)的实例。
- 改进的类型推断。JDK8里,类型推断不仅可以用于赋值语句,而且可以根据代码中上下文里的信息推断出更多的信息
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<java.lang.Object>)
stringList.addAll(Arrays.asList());
上面这段代码在java7中会报错,但是在java8中可以编译通过。
3. 支持多重注解。之前同一个注解需要放在类似数组的结构中,现在可以直接多个@,只需要对注解标注为@Repeatable
。
4. stream接口。
5. 新增Optional接口,用来防止NullPointerException异常的辅助类型。在java8中不推荐返回null而是返回Optional。
6. 函数式接口(Functional Interface)。java.util.function 它包含了很多类,用来支持 Java的 函数式编程(接受函数过程传递),该包中的函数式接口有:
Predicate<T>
接受一个输入参数,返回一个布尔值结果。
7. Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可,这个特征又叫做扩展方法,示例如下:
interface Formula {
double calculate(int a);
default double sqrt(int a) {
return Math.sqrt(a);
}
}
- ::方法与构造函数引用。Java 8 允许你使用
::
关键字来传递方法或者构造函数引用,我们也可以引用一个对象的方法代码如下:
converter = something::startsWith;
String converted = converter.convert("Java");
System.out.println(converted); // "J"
接下来看看构造函数是如何使用::关键字来引用的:
PersonFactory<Person> personFactory = Person::new;
Person person = personFactory.create("Peter", "Parker");
2.31.3. Java9
- 模块化。模块化系统的主要目的如下:
更可靠的配置,通过制定明确的类的依赖关系代替以前那种易错的类路径(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;
}
2.32. java如何实现泛型?
Java 语言中的泛型,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(RawType,也称为裸类 型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的 Java 语言来说,ArrayList<int>与 ArrayList<String>就是同一 个类,所以泛型技术实际上是 Java 语言的一颗语法糖,Java 语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛 型。
将一段 Java 代码编译成 Class 文件,然后再用字节码反编译工具进行反编译后,将会发现泛型都不见了,程序又变回了 Java 泛型 出现之前的写法,因为泛型类型都变回了原生类型.
2.33. JVM
2.33.1. 内存模型
根据JVM规范,JVM 内存共分为虚拟机栈,堆,方法区,程序计数器,本地方法栈五个部分。
2.33.1.1. 程序计数器(线程私有)
程序计数器是一块很小的内存空间,它是线程私有的,可以认作为当前线程的行号指示器。
注意:如果线程执行的是个java方法,那么计数器记录虚拟机字节码指令的地址。如果为native【底层方法】,那么计数器为空。这块内存区域是虚拟机规范中唯一没有OutOfMemoryError的区域。
2.33.1.2. Java栈(虚拟机栈)
每个方法被执行的时候都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法返回地址、附加信息。每一个方法被调用的过程就对应一个栈帧在虚拟机栈中从入栈到出栈的过程。具体栈帧参考Java虚拟机运行时栈帧结构(周志明书上P237页)
2.33.1.3. 本地方法栈
本地方法栈是与虚拟机栈发挥的作用十分相似,区别是虚拟机栈执行的是Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务,可能底层调用的c或者c++,我们打开jdk安装目录可以看到也有很多用c编写的文件,可能就是native方法所调用的c代码
2.33.1.4. 堆
对于大多数应用来说,堆是java虚拟机管理内存最大的一块内存区域,因为堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。
2.33.1.5. 方法区
方法区同堆一样,是所有线程共享的内存区域,为了区分堆,又被称为非堆。
用于存储已被虚拟机加载的类信息、常量、静态变量,如static修饰的变量加载类的时候就被加载到方法区中。
2.33.2. 垃圾判断算法
一般来讲有两种垃圾判断的方法,分别是引用计数和可达性分析法。JVM主流采用的是可达性分析算法,因为在Java中引用计数法无法处理循环引用的问题。
2.33.2.1. 引用计数
为避免循环引用的问题,c++中会将互相引用的两个对象中,其中一个引用 weak_ptr,因为 weak_ptr 不会使引用计数加1,所以就不会出现这种互相拖着对方的事情了。
又比如说redis,因为其内部不存在深层次的嵌套,因此也就不存在循环引用的隐患。
2.33.2.2. 可达性分析
JVM采用了另一种方法:定义一个名为"GC Roots"的对象作为起始点,这个"GC Roots"可以有多个,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,即可以进行垃圾回收。
可以作为GC Roots的对象:
- 虚拟机栈中引用的对象(栈帧中的本地变量)
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中引用的对象
2.33.3. 垃圾回收算法
2.33.3.1. 标记-清除
标记完成后就可以对标记为垃圾的对象进行回收了。但是这种策略的缺点很明显,回收后内存碎片很多,如果之后程序运行时申请大内存,可能会又导致一次GC。
2.33.3.2. 标记-复制
将内存分为两块,标记完成开始回收时,将一块内存中保留的对象全部复制到另一块空闲内存中。实现起来也很简单,当大部分对象都被回收时这种策略也很高效。但这种策略也有缺点,可用内存变为一半了
怎样解决呢?办法总是多过问题的。可以将堆不按1:1的比例分离,而是按8:1:1分成一块Eden和两小块Survivor区,每次将Eden和Survivor中存活的对象复制到另一块空闲的Survivor中。这三块区域并不是堆的全部,而是构成了新生代。如果回收时,空闲的那一小块Survivor不够用了怎么办?这就是老年代的用处。当不够用时,这些对象将直接通过分配担保机制进入老年代。‘
2.33.3.3. 标记-整理
根据老年代的特点,采用回收掉垃圾对象后对内存进行整理的策略再合适不过,将所有存活下来的对象都向一端移动。
2.33.4. 垃圾回收器
参考:https://www.cnblogs.com/chenpt/p/9803298.html
新生代收集器:Serial、ParNew、Parallel Scavenge
老年代收集器:CMS、Serial Old、Parallel Old
整堆收集器: G1
2.33.4.1. Serial 收集器
Serial收集器是最基本的、发展历史最悠久的收集器。
特点:单线程、简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
应用场景:适用于Client模式下的虚拟机。
2.33.4.2. ParNew收集器其实就是Serial收集器的多线程版本。
除了使用多线程外其余行为均和Serial收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。
特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题
应用场景:ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。
2.33.4.3. Parallel Scavenge 收集器
与吞吐量关系密切,故也称为吞吐量优先收集器。吞吐量=运行用户代码时间/CPU总执行时间。
用于精确吞吐量的两个参数:1.控制最大垃圾收集停顿时间参数 2.直接设置吞吐量大小的参数。
特点:属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器类似)。
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)
2.33.4.4. Serial Old 收集器
Serial Old是Serial收集器的老年代版本。
特点:同样是单线程收集器,采用标记-整理算法。
应用场景:主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用。
Server模式下主要的两大用途:
在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用。
作为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。
2.33.4.5. Parallel Old 收集器
是Parallel Scavenge收集器的老年代版本。
特点:多线程,采用标记-整理算法。
应用场景:注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器。
2.33.4.6. CMS一种以获取最短回收停顿时间为目标的收集器。
特点: 针对老年代;基于标记-清除算法实现。并发收集、低停顿。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如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。
2.33.4.7. G1收集器
一款面向服务端应用的垃圾收集器。
特点如下:
并行与并发: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停顿时间来制定回收计划。(可并发执行)
2.33.5. JVM参数调优
命令查看:jmap(jmap -heap PID)
2.33.5.1. HBase jvm参数调整
一台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上。
具体参数:
- 新生代用并行回收器、老年代用并发回收器,另外配置了CMSInitiatingOccupancyFraction,当老年代内存使用率超过70%就开始执行CMS GC,减少GC时间,Master任务比较轻,一般设置4g、8g左右,具体按照群集大小评估
export HBASE_MASTER_OPTS=" -Xms8g -Xmx8g -Xmn1g
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=70"
- RegionServer专有的启动参数,RegionServer大堆,采用G1回收器,G1会把堆内存划分为多个Region,对各个Region进行单独的GC,最大限度避免Full GC及其影响
初始堆及最大堆设置为最大物理内存的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"
2.34. 类加载:双亲委派
对于任意一个类,都需要由加载它的类加载器和这个类本身来一同确立其在Java虚拟机中的唯一性。
基于上述的问题:如果不是同一个类加载器加载,即时是相同的class文件,也会出现判断不想同的情况,从而引发一些意想不到的情况,为了保证相同的class文件,在使用的时候,是相同的对象,jvm设计的时候,采用了双亲委派的方式来加载类。
双亲委派:如果一个类加载器收到了加载某个类的请求,则该类加载器并不会去加载该类,而是把这个请求委派给父类加载器,每一个层次的类加载器都是如此,因此所有的类加载请求最终都会传送到顶端的启动类加载器;只有当父类加载器在其搜索范围内无法找到所需的类,并将该结果反馈给子类加载器,子类加载器会尝试去自己加载。
2.34.1. 类加载过程
举个通俗点的例子来说,JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A的相关信息,于是JVM就会到相应的class文件中去寻找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。
我们进一步了解类加载器间的关系(并非指继承关系),主要可以分为以下4点
- 启动类加载器,由C++实现,没有父类。
- 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null
- 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader
- 自定义类加载器,父类加载器肯定为AppClassLoader。
在JVM中表示两个class对象是否为同一个类对象存在两个必要条件:类的完整类名必须一致,包括包名。加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
由此可见,JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。类加载的过程总体上可以分为5部分。
-
加载
在加载阶段,JVM主要完成下面三件事:
①、通过一个类的全限定名来获取定义此类的二进制字节流。
②、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
③、在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。 -
验证
作用是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 -
准备
准备阶段正式为类变量分配内存并设置类变量初始值的阶段,这些内存是在方法区中分配的。上面说的是类变量,也就是被 static 修饰的变量,不包括实例变量。实例变量会在对象实例化时随着对象一起分配在堆中。 -
解析
解析阶段是虚拟机将常量池中的符号引用替换为直接引用的过程。
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。
直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那么引用的目标必定已经在内存中存在。
- 初始化
初始化阶段是执行类构造器<clinit>()
方法的过程。
2.35. 设计模式
总体来说设计模式分为三大类:
- 创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
- 结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
- 行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
2.35.1. 单例模式
该模式主要目的是使内存中保持1个对象
2.35.1.1. 编写单例模式
单例模式有5种,分别是懒汉式、饿汉式、双重校验锁、静态内部类和枚举。
2.35.1.2. 双重校验锁
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;
}
2.35.2. 工厂模式
该模式主要功能是统一提供实例对象的引用
public class Factor {
public ClassDao getDao(){
ClassDao dao = new ClassDaoImpl();
return dao;
}
}
2.35.3. 策略模式
这个模式是将行为的抽象,即当有几个类有相似的方法,将其中通用的部分都提取出来,从而使扩展更容易
public class Addition extends Operation{
@Override
public float parameter(float a, float b){
return a+b;
}
}
2.35.4. 门面模式
客户端可以调用门面角色的方法。此角色知晓相关的(一个或者多个)子系统的功能和责任。在正常情况下,门面角色会将所有从客户端发来的请求委派到相应的子系统去。
2.35.5. 建造模式
将一个复杂的对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。在这样的设计模式中,有以下几个角色:
Builder:为创建一个产品对象的各个部件指定抽象接口。
ConcreteBuilder:实现Builder的接口以构造和装配该产品的各个部件,定义并明确它所创建的表示,并提供一个检索产品的接口。
Director:构造一个使用Builder接口的对象,指导构建过程。
Product:表示被构造的复杂对象。ConcreteBuilder创建该产品的内部表示并定义它的装配过程,包含定义组成部件的类,包括将这些部件装配成最终产品的接口。
2.35.6. 适配器模式
适配器模式(有时候也称包装样式或者包装)将一个类的接口适配成用户所期待的。一个适配允许通常因为接口不兼容而不能在一起工作的类工作在一起,做法是将类自己的接口包裹在一个已存在的类中。
2.35.7. 装饰模式
装饰模式是在不必改变原类文件和使用继承的情况下,能对现有对象的内容或功能性稍加修改。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。
2.35.8. 代理模式(委托模式)
代理模式 : 为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。目的为了去除核心对象的复杂性
2.35.9. 外观模式
外观模式是通过在必要的逻辑和方法的集合前创建简单的外观接口,隐藏调用对象的复杂性。
2.35.10. 观察者模式
观察者模式也做发布订阅模式(Publish/subscribe),在项目中常使用的模式。定义:定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖它的对象都会得到通知并被自动更新。
2.35.11. 命令模式(Command)
命令模式是一个高内聚的模式,其定义为:将一个请求封装成一个对象,从而让你使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。
2.35.12. 访问者模式(Visitor)
访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。访问者模式适用于数据结构相对稳定算法又易变化的系统。因为访问者模式使得算法操作增加变得容易。若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。访问者模式的优点是增加操作很容易,因为增加操作意味着增加新的访问者。访问者模式将有关行为集中到一个访问者对象中,其改变不影响系统数据结构。其缺点就是增加新的数据结构很困难。
3. 数据库
3.1. 必备技能 寿司SQL
遇到好的有典型性的SQL题会放在这里。
3.1.1. 超过5名学生的课
有一个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;
3.1.2. 查询出各科成绩最高分的同学
select id,course,score from student where id in (select id from student where score in (select max(score) from student group by course))
3.2. SQL
3.2.1. Left Join 和 Right Join
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
(内连接),在两张表进行连接查询时,只保留两张表中完全匹配的结果集。
3.3. SQL优化
-
应尽量避免在 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
3.4. SQL执行过程
- 连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接。
- 查询缓存。MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端。
如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,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 并不是完全相同的。
3.5. 死锁及优化
3.5.1. 同时Update
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, 括号内的为第二层,彼此都有序。当执行计划使用这个联合索引进行当前读时,因为二级索引有序, 相同的条件当前读加锁的顺序是一样的,不会出现交叉,不同条件的当前读不会有重合的主键数据,这就避免了上面数据交叉且加锁顺序相反的情况,避开了死锁。
死锁的场景复杂多变,本案例只是九牛一毛,加联合索引的方案也只是减少了某种数据分布情况下的死锁概率。
3.5.2. 先查询在插入
在开发中,经常会做这类的判断需求:根据字段值查询(有索引),如果不存在,则插入;否则更新。
当对未存在的行进行锁的时候(即使条件为主键),mysql是会锁住一段范围(有gap锁),由于gap锁是可以重复添加的,所以事务直到插入的时候才会出现由于多个gap锁导致的死锁。
解决
对于这种死锁的解决办法是:(update or insert if existed)
insert into t3(xx,xx) on duplicate key update `xx`='XX';
用mysql特有的语法来解决此问题。因为insert语句对于主键来说,插入的行不管有没有存在,都会只有行锁
3.6. ACID
原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)
其中一致性又分为数据库约束(外键等)的完整和业务逻辑的完整。
3.7. 数据库事务、隔离级别、Innodb和Myisam的区别
事务:事务就是将很多个操作集中在一块形成一个有限的操作集,然后对之及进行执行;对于一个事务的执行结果只有两种结果,一是全部执行成功并提交到数据库中,对数据进行持久的影响,二是事务中有一个或者多个操作没能成功执行最终导致事务的执行整体失败,进而回滚到事务开始之前的数据库状态
隔离级别
类别 | 解释 |
---|---|
读未提交数据 | 就是允许事务读取还没有被其他事务提交的数据,这种情况下会出现脏读、幻读、不可重复读的问题 |
读已提交数据 | 只允许事务读取其他事务已经提交的数据,此种级别解决了脏读的问题,但还是不能彻底解决不可重复读、幻读的问题 |
可重复读 | 确保事务能够多次从一个字段中读取相同的数据值,在此事务期间不允许其他事务进行对数据变更,避免了脏读、不可重复的问题,但是幻读仍可能出现 |
可串行化(序列化) | 确保一个事务可以从一个表中读取相同的行,在此事务持续的期间禁止其他事务对该表进行更新、插入、删除等操作,可以避免所有的并发问题,但是此种情形性能极低 |
常见的数据库中 , Oracle数据库是支持读已提交,MySQL数据库是支持可重复读的,随着隔离级别的提升,对应性能效率会降低,所以在具体选择时,需要综合各个方面进行考虑。
3.7.1. 脏读、幻读和不可重复读
- 脏读:脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
- 不可重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
- 幻读:是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
3.7.2. mysql如何解决不可重复读和读未提交?MVCC
每次对记录进行改动,都会记录一条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就好了。
3.7.3. mysql如何解决幻读?间隙锁
即使一个事务对所有行都加上排他锁,还是无法保证其他事务不会有新的数据行插入。这也是为什么”幻读”会被单独拿出来解决的问题。因此,为了解决幻读问题,innodb引入了新的锁,也即是间隙锁(gap lock)。
顾名思义,间隙锁,锁的就是两个值之间的空隙。这样,当执行select * from t20 where d=5 for update的时候,就不止给数据库已有的6行记录加上了行锁,还同时加了7个间隙锁,这样就确保无法在插入新的记录。也就是说这时候,在一行扫描的过程中,不仅将给行上加了行锁,还给行两边的空隙(加锁单位是next-key lock
也就是该行前后两项数据-不存在该行的时候,否则为前开后闭区间),也加上了间隙锁。
但是间隙锁不一样,跟间隙锁存在冲突关系的,是”往这个间隙中插入一个记录”这个操作。间隙锁之间是不存在冲突关系的。
间隙锁的出现主要集中在同一个事务中先delete 后 insert的情况下, 当我们通过一个参数去删除一条记录的时候, 如果参数在数据库中存在, 那么这个时候产生的是普通行锁, 锁住这个记录, 然后删除, 然后释放锁。如果这条记录不存在,问题就来了, 数据库会扫描索引,发现这个记录不存在, 这个时候的delete语句获取到的就是一个间隙锁,然后数据库会向左扫描扫到第一个比给定参数小的值, 向右扫描扫描到第一个比给定参数大的值, 然后以此为界,构建一个区间, 锁住整个区间内的数据, 一个特别容易出现死锁的间隙锁诞生了。
3.7.4. 事务如何实现的?
- redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。
- undo用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。undo log有两个作用:提供回滚和多个行版本控制(MVCC)。
3.7.4.1. undo log
- 每条数据变更(insert/update/delete)操作都伴随一条
undo log
的生成,并且回滚日志必须先于数据持久化到磁盘上 - 所谓的回滚就是根据回滚日志做逆向操作,比如delete的逆向操作为insert,insert的逆向操作为delete,update的逆向为update等。
3.7.4.2. redo log
页(Page)是 Innodb 存储引擎用于管理数据的最小磁盘单位。常见的页类型有数据页、Undo 页、系统页、事务数据页等。
事务一旦提交,其所作做的修改会永久保存到数据库中,此时即使系统崩溃修改的数据也不会丢失。先了解一下MySQL的数据存储机制,MySQL的表数据是存放在磁盘上的,因此想要存取的时候都要经历磁盘IO,然而即使是使用SSD磁盘IO也是非常消耗性能的。为此,为了提升性能InnoDB提供了缓冲池(Buffer Pool),Buffer Pool中包含了磁盘数据页的映射,可以当做缓存来使用:
读数据:会首先从缓冲池中读取,如果缓冲池中没有,则从磁盘读取在放入缓冲池;
写数据:会首先写入缓冲池,缓冲池中的数据会定期同步到磁盘中;
上面这种缓冲池的措施虽然在性能方面带来了质的飞跃,但是它也带来了新的问题,当MySQL系统宕机,断电的时候可能会丢数据!
3.7.5. Innodb默认隔离级别
InnoDB默认是可重复读的(REPEATABLE READ)。mysql binlog的格式三种:statement,row,mixed,在 5.0之前只有statement一种格式,而主从复制存在了大量的不一致,故选用repeatable(防止并发事务导致statement执行结果不一致)。
3.7.5.1. 在RC级别下,主从复制用什么binlog格式?
在该隔离级别下,用的binlog为row格式,是基于行的复制!Innodb的创始人也是建议binlog使用该格式!
3.8. InnoDB、MyISAM引擎的区别
此处我选择从第三者角度来进行描述,便于区分理解;
-
存储结构: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则提供表级锁;
3.9. 共享锁、排他锁、悲观锁、乐观锁、行锁、表锁
-
共享锁(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的记录加上锁,那么这个锁就是行锁。
- 表锁
表锁,和行锁相对应,给这个表加上锁。
3.10. 索引的数据结构(B+树)、索引优缺点
索引是数据表中一个或者多个列进行排序的数据结构
B+树是 B-Tree的变形,Mysql 实际使用的 B+Tree作为索引的数据结构。
只在叶子结点带有指向记录的指针。B+树只在叶子结点存储数据,其它非叶子结点可以存储更多信息(对B-Tree的优化)。叶子结点通过指针相连。叶子结点里面有从左到右指针,它是一个双向指针,通过这个指针就可以非常方便实现范围查询。
索引的优缺点:
1、优点:创建索引可以大大提高系统的性能。
第一、通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
第二、可以大大加快 数据的检索速度,这也是创建索引的最主要的原因。
第三、可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
第四、在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
第五、通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
2、缺点:
增加索引有如此多的优点,为什么不对表中的每一个列创建一个索引呢?这是因为,增加索引也有许多不利的一个方面:
第一、创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
第二、索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间。如果要建立聚簇索引,那么需要的空间就会更大。
第三、当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
3.11. MySQL索引到底使用int还是varchar?
当MySQL中字段为int
类型时,搜索条件where num=‘111‘ 与where num=111都可以使用该字段的索引。
当MySQL中字段为varchar
类型时,搜索条件where num=‘111‘ 可以使用索引,where num=111 不可以使用索引
3.12. MVCC
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还必须加锁。
3.13. 最左前缀原则、索引优化
两个或更多个列上的索引被称作联合索引,联合索引又叫复合索引。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来搜索才能知道下一步去哪里查询。
什么时候索引会失效
- 如果条件中有or,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)。注意:要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引
- 对于多列索引,不是使用的第一部分,则不会使用索引(即不符合最左前缀原则)
- like查询是以%开头
- 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
- 如果mysql估计使用全表扫描要比使用索引快,则不使用索引
3.14. 聚簇索引、覆盖索引
聚簇索引是将数据行的内容放在B_tree的叶子节点中,节点列存放数据列,由于不能把数据行放在两个不同的地方,所以每个表只能有一个聚簇索引。MySql的聚簇索引只能将主键作为索引。如果没有主键,则选择唯一的非空索引,如果都没有,则隐式定义主键,但是这样很不好,相邻键值相距会很远。
通俗的解释就是例如InnoDB使用了聚簇索引,在主索引B+树的叶子节点保存数据行的内容,辅助键索引B+树保存关键字到主键的映射,因此若对非主键列进行条件搜索,则需要两个步骤。而MyISM使用的是非聚簇索引,表数据存储在独立的地方,主键索引索引B+树和辅助键索引B+树的叶子节点都保存指向真正的表数据的指针。
总结:聚簇索引和非聚簇索引的区别在于聚簇索引的叶子节点里包括了数据行的内容,而非聚簇索引叶子节点里不是,可以是数据存放的地址,也可以是二级索引指向表的主键值。
覆盖索引:一个索引包含所有要查询的字段的值。
优点:
①:索引条目通常远小于数据行,因此如果只读取索引,可以极大的减小数据访问量,同样索引也更容易放入内存,所以对于I/O密集型的应用有很大帮助。
②:索引是按照列顺序存储的。
③:有的存储引擎如MyIsam只在内存中缓存索引,数据依赖于操作系统缓存,因此覆盖索引避免了频繁的系统调用。
3.14.1. 回表查询
回表查询,先定位主键值,再定位行记录,它的性能较扫一遍索引树更低。
3.14.2. 索引覆盖
在explain查询计划优化章节,即explain的输出结果Extra字段为Using index时,能够触发索引覆盖。
常见的方法是:将被查询的字段,建立到联合索引里去。
3.14.3. 联合索引
对于联合索引来说只不过比单值索引多了几列,而这些索引列全都出现在索引树上。对于联合索引,存储引擎会首先根据第一个索引列排序,如我们可以单看第一个索引列,横着看,如,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并没有用到!
3.15. 分页查询怎么用索引优化?
3.15.1. limit分页查询性能问题
MySQL Limit 语法格式:
SELECT * FROM table LIMIT [offset,] rows | rows OFFSET offset
分页查询时,我们会在 LIMIT 后面传两个参数,一个是偏移量(offset),一个是获取的条数(limit)。当偏移量很小时,查询速度很快,但是当 offset 很大时,查询速度就会变慢。
3.15.2. 优化方法
对于LIMIT分页查询的性能优化,主要思路是利用覆盖索引字段定位数据,然后再取出内容
1)子查询分页方式
SELECT t1.* FROM tbl_works t1
WHERE t1.id in (SELECT t2.id from (SELECT id FROM tbl_works limit 100000, 10) as t2) // 53.9ms
2)join 分页方式
SELECT * FROM tbl_works t1
JOIN (SELECT id from tbl_works WHERE status=1
limit 100000, 10) t2
ON t1.id = t2.id // 53.6 ms
3.16. 数据库三大范式
- 第一范式(1NF):列不可再分。如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式。
- 第二范式(2NF)属性完全依赖于主键。确保表中的每列都和主键相关
- 第三范式(3NF)属性不依赖于其它非主属性 ,属性直接依赖于主键。每一列数据都和主键直接相关,而不能间接相关。
3.17. 主从复制、主从一致、分库分表
主从复制原理
- 首先, MySQL主库在事务提交时会把数据变更作为事件 Events 记录在二进制日志文件Binlog中; MySQL主库上的 sync_binlog参数控制 Binlog日志刷新到磁盘。
- 主库推送二进制日志文件 Binlog中的事件到从库的中继日志 Relay Log, 之后从库根据中继日志 Relay Log重做数据变更操作,通过逻辑复制以此来达到主库和从库的数据一致。
- MySQL通过3个线程来完成主从库间的数据复制:其中 Binlog Dump线程跑在主库上, I/0线程和 SQL线程跑在从库上。当在从库上启动复制时,首先创建I/0程连接主库,主库随后创建 Binlog Dump线程读取数据库事件并发送给 I/0线程, I0线程获取到事件数据后更新到从库的中继日志 Relay Log中去,之后从库上的 SQL线程读取中继日志RelayLog中更新的数据库事件并应用
- 通过 SHOW PROCESSLIST命令在主库上査看 BinlogDump线程,从 BinlogDump 线程的状态可以看到, Mysql的复制是主库主动推送日志到从库去的,是属于“推”日志的方式来做同步。同样地,在从库上通过 SHOW PROCESSLIST可以看到l/O线程和 SQL线程, l/O线程等待主库上的 Binlog Dump线程.发送事件并更新到中继日志 RelayLog, SQL线程读取中继日志并应用变更到数据库。
分库分表
- 垂直(纵向)切分
垂直切分常见有垂直分库和垂直分表两种。垂直分表是基于数据库中的"列"进行,某个表字段较多,可以新建一张扩展表,将不经常用或字段长度较大的字段拆分出去到扩展表中。垂直分库就是根据业务耦合性,将关联度低的不同表存储在不同的数据库。做法与大系统拆分为多个小系统类似,按业务分类进行独立划分。 - 水平(横向)切分
当一个应用难以再细粒度的垂直切分,或切分后数据量行数巨大,存在单库读写、存储性能瓶颈,这时候就需要进行水平切分了。水平切分分为库内分表和分库分表,是根据表内数据内在的逻辑关系,将同一个表按不同的条件分散到多个数据库或多个表中,每个表中只包含一部分数据,从而使得单个表的数据量变小,达到分布式的效果。
3.18. mysq的binlog三种模式的区别(row,statement,mixed)
- Row
日志中会记录成每一行数据被修改的形式,然后在slave端再对相同的数据进行修改,只记录要修改的数据,只有value,不会有sql多表关联的情况。
优点:在row模式下,bin-log中可以不记录执行的sql语句的上下文相关的信息,仅仅只需要记录那一条记录被修改了,修改成什么样了,所以row的日志内容会非常清楚的记录下每一行数据修改的细节,非常容易理解。而且不会出现某些特定情况下的存储过程和function,以及trigger的调用和出发无法被正确复制问题。
缺点:在row模式下,所有的执行的语句当记录到日志中的时候,都将以每行记录的修改来记录,这样可能会产生大量的日志内容。
- statement
每一条会修改数据的sql都会记录到master的binlog中,slave在复制的时候sql进程会解析成和原来master端执行多相同的sql再执行。
优点:在statement模式下首先就是解决了row模式的缺点,不需要记录每一行数据的变化减少了binlog日志量,节省了I/O以及存储资源,提高性能。因为他只需要记录在master上所执行的语句的细节以及执行语句的上下文信息。
缺点:在statement模式下,由于他是记录的执行语句,所以,为了让这些语句在slave端也能正确执行,那么他还必须记录每条语句在执行的时候的一些相关信息,也就是上下文信息,以保证所有语句在slave端被执行的时候能够得到和在master端执行时候相同的结果。另外就是,由于mysql现在发展比较快,很多的新功能不断的加入,使mysql的复制遇到了不小的挑战,自然复制的时候涉及到越复杂的内容,bug也就越容易出现。在statement中,目前已经发现不少情况会造成Mysql的复制出现问题,主要是修改数据的时候使用了某些特定的函数或者功能的时候会出现,比如:sleep()函数在有些版本中就不能被正确复制,在存储过程中使用了last_insert_id()函数,可能会使slave和master上得到不一致的id等等。由于row是基于每一行来记录的变化,所以不会出现,类似的问题。
3.19. MongoDB
Mongo-DB是一个文档数据库,可提供高性能,高可用性和易扩展性。mongodb中有多个databases,每个database可以创建多个collections,collection是底层数据分区(partition)的单位,每个collection都有多个底层的数据文件组成。
3.19.1. MongoDB中的分片是什么
在多台计算机上存储数据记录的过程称为分片。这是一种MongoDB方法,可以满足数据增长的需求。它是数据库或搜索引擎中数据的水平分区。每个分区称为分片或数据库分片。在多台服务器之间,同步数据的过程称为复制。它通过不同数据库服务器上的多个数据副本提供冗余并提高数据可用性。复制有助于防止数据库丢失单个服务器。
3.19.2. 为什么MongoDB用BTree做索引
因为不涉及范围查询。
3.19.3. MongoDB自动分片策略
数据划分(partitioning)关键问题是怎么样将一个集合中的数据均衡的分布在集群中的节点上。 MongoDB 数据划分的是在集合的层面上进行的,它根据片键来划分集合中的数据。
- 使用片键的取值范围指定数据块
- 按照片键哈希值来作为数据块的划分区间依据
- 取值有限的片键,但是它也有一个很难解决的问题:块有可能无限制的增长。想想基于用户ID的片键,假如有几个特殊用户,他们上传了上百万个文件,那么一个块里就可能只有一个用户ID,这个块能拆分么?不能,因为用户ID是最小的粒度,拆分了查询就没法路由到数据。
3.19.3.1. 一个好的片键必须包含的特性
- 保证CRUD能利用局限性 --> 升序片键的优点
- 将插入数据均匀分布到各个分片上 --> 随机片键的优点
- 有足够的粒度进行块拆分 --> 粗粒度片键的优点
除此之外,也可以选择我们经常查询的字段作为片键,这类分片键可以使得查询时mongos仅仅将查询发送给特定的mongod实例,不需要等待多个实例返回数据后再进行合并。
3.20. HBase
3.20.1. 存储结构
Hbase 中的每张表都通过行键 (rowkey) 按照一定的范围被分割成多个子表(HRegion),默认一个 HRegion 超过 256M 就要被分割成两个,由 HRegionServer 管理,管理哪些 HRegion 由 Hmaster 分配。 HRegion 存取一个子表时,会创建一个 HRegion 对象,然后对表的每个列族 (Column Family) 创建一个 store 实例, 每个 store 都会有 0个或多个 StoreFile 与之对应,每个 StoreFile 都会对应一个 HFile , HFile 就是实际的存储文件,因此,一个 HRegion 还拥有一个 MemStore 实例。
3.20.2. HBase实时查询
实时查询,可以认为是从内存中查询,一般响应时间在 1 秒内。HBase 的机制是数据先写入到内存中,当数据量达到一定的量(如 128M),再写入磁盘中, 在内存中,是不进行数据的更新或合并操作的,只增加数据,这使得用户的写操作只要进入内存中就可以立即返回,保证了 HBase I/O 的高性能。
3.20.3. 简述 HBASE 中 compact 用途是什么,什么时候触发,分为哪两种,有什么区别,有哪些相关配置参数?
在 hbase 中每当有 memstore 数据 flush 到磁盘之后,就形成一个 storefile, 当 storeFile 的数量达到一定程度后,就需要将 storefile 文件来进行 compaction 操作。Compact 的作用:
- 合并文件
- 清除过期,多余版本的数据
- 提高读写数据的效率
3.20.4. 描述 Hbase 中 scan 和 get 的功能以及实现的异同
按指 定 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 方法添加过滤器,这也是分页、多条件查询的基础。
3.20.5. Hbase 内部存储机制
在 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 可能无法恢复数据,没有刷写到硬盘的所有写入数据都会丢失。
3.20.6. HBase 宕机如何处理?
宕机分为 HMaster 宕机和 HRegisoner 宕机.
如果是 HRegisoner 宕机,HMaster 会将其所管理的 region 重新分布到其他活动的 RegionServer 上,由于数据和日志都持久在 HDFS 中,该操作不会导致数据丢失,所以数据的一致性和安全性是有保障的。
如果是 HMaster 宕机, HMaster 没有单点问题, HBase 中可以启动多个HMaster,通过 Zookeeper 的 Master Election 机制保证总有一个 Master 运行。即ZooKeeper 会保证总会有一个 HMaster 在对外提供服务。
3.20.7. hbase写数据
- 获取region存储位置信息
写数据和读数据一般都会获取hbase的region的位置信息。大概步骤为:
从zookeeper中获取.ROOT.表的位置信息,在zookeeper的存储位置为/hbase/root-region-server;
根据.ROOT.表中信息,获取.META.表的位置信息;
META.表中存储的数据为每一个region存储位置;
- 向hbase表中插入数据
hbase中缓存分为两层:Memstore 和 BlockCache
首先写入到 WAL文件 中,目的是为了数据不丢失;
再把数据插入到 Memstore缓存中,当 Memstore达到设置大小阈值时,会进行flush进程;
flush过程中,需要获取每一个region存储的位置。
3.20.8. 从hbase中读取数据
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。这会加大缓存命中率。
3.20.9. HBase优化方法
优化手段主要有以下四个方面
- 减少调整
减少调整这个如何理解呢?HBase中有几个内容会动态调整,如region(分区)、HFile,所以通过一些方法来减少这些会带来I/O开销的调整
Region
如果没有预建分区的话,那么随着region中条数的增加,region会进行分裂,这将增加I/O开销,所以解决方法就是根据你的RowKey设计来进行预建分区,减少region的动态分裂。
HFile
HFile是数据底层存储文件,在每个memstore进行刷新时会生成一个HFile,当HFile增加到一定程度时,会将属于一个region的HFile进行合并,这个步骤会带来开销但不可避免,但是合并后HFile大小如果大于设定的值,那么HFile会重新分裂。为了减少这样的无谓的I/O开销,建议估计项目数据量大小,给HFile设定一个合适的值
- 减少启停
数据库事务机制就是为了更好地实现批量写入,较少数据库的开启关闭带来的开销,那么HBase中也存在频繁开启关闭带来的问题。
关闭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压缩
- 合理设计
在一张HBase表格中RowKey和ColumnFamily的设计是非常重要,好的设计能够提高性能和保证数据的准确性
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开销。
3.20.10. 为什么不建议在 HBase 中使用过多的列族
在 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频繁的问题。
3.20.11. LSM
LSM树(Log Structured Merge Tree,结构化合并树)的思想非常朴素,就是将对数据的修改增量保持在内存中,达到指定的大小限制后将这些修改操作批量写入磁盘(由此提升了写性能),是一种基于硬盘的数据结构,与B-tree相比,能显著地减少硬盘磁盘臂的开销。当然凡事有利有弊,LSM树和B+树相比,LSM树牺牲了部分读性能,用来大幅提高写性能。
读取时需要合并磁盘中的历史数据和内存中最近的修改操作,读取时可能需要先看是否命中内存,否则需要访问较多的磁盘文件(存储在磁盘中的是许多小批量数据,由此降低了部分读性能。但是磁盘中会定期做merge操作,合并成一棵大树,以优化读性能)。LSM树的优势在于有效地规避了磁盘随机写入问题,但读取时可能需要访问较多的磁盘文件。
核心思想的核心就是放弃部分读能力,换取写入的最大化能力,放弃磁盘读性能来换取写的顺序性。极端的说,基于LSM树实现的HBase的写性能比Mysql高了一个数量级,读性能低了一个数量级。
3.20.11.1. LSM操作
插入数据可以看作是一个N阶合并树。数据写操作(包括插入、修改、删除也是写)都在内存中进行,
数据首先会插入内存中的树。当内存树的数据量超过设定阈值后,会进行合并操作。合并操作会从左至右便利内存中树的子节点 与 磁盘中树的子节点并进行合并,会用最新更新的数据覆盖旧的数据(或者记录为不同版本)。当被合并合并数据量达到磁盘的存储页大小时。会将合并后的数据持久化到磁盘,同时更新父节点对子节点的指针。
读数据 磁盘中书的非子节点数据也被缓存到内存中。在需要进行读操作时,总是从内存中的排序树开始搜索,如果没有找到,就从磁盘上的排序树顺序查找。
在LSM树上进行一次数据更新不需要磁盘访问,在内存即可完成,速度远快于B+树。当数据访问以写操作为主,而读操作则集中在最近写入的数据上时,使用LSM树可以极大程度地减少磁盘的访问次数,加快访问速度。
删除数据 前面讲了。LSM树所有操作都是在内存中进行的,那么删除并不是物理删除。而是一个逻辑删除,会在被删除的数据上打上一个标签,当内存中的数据达到阈值的时候,会与内存中的其他数据一起顺序写入磁盘。 这种操作会占用一定空间,但是LSM-Tree 提供了一些机制回收这些空间。
4. 框架类
4.1. Spring与Spring Boot的区别与联系
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配置要求
4.2. Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别
以下内容摘录自:Kafka、ActiveMQ、RabbitMQ、RocketMQ 区别以及高可用原理,具体的参数对比我觉得意义不大,这里将作者的评价列出来。
一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了;
后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高;
不过现在确实越来越多的公司,会去用 RocketMQ,确实很不错(阿里出品),但社区可能有突然黄掉的风险,对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。
所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。
如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范
4.3. mybatis与hibernate的联系与区别
提到这两个工具就必须先了解ORM(Object Relational Mapping),即对象关系映射。它的实现思想就是将关系数据库中表的数据映射成为对象,以对象的形式展现,这样开发人员就可以把对数据库的操作转化为对这些对象的操作。因此它的目的是为了方便开发人员以面向对象的思想来实现对数据库的操作。
- 无论MyBatis或Hibernate都可以称为ORM框架,Hibernate的设计理念是完全面向POJO的,而MyBatis不是。
- Hibernate基本不再需要编写SQL就可以通过映射关系来操作数据库,是一种全表映射的体现,而MyBatis需要我们提供SQL去运行。程序员不用精通SQL,只要懂得操作POJO就能够操作对应数据库的表。
- Hibernate和MyBatis的增、删、查、改.对于业务逻辑层来说大同小异,对于映射层而言Hibernate的配置不需要接口和SQL.相反MyBatis是需要的。对于Hibernate而言,不需要编写大量的SQL,就可以完全映射,同时提供了日志、缓存、级联(级联比MyBatis强大)等特性,此外还提供HQL (Hibernate Query Language)对POIO进行操作,使用十分方便,但是它也有致命的缺陷:由于无须SQL,当多表关联超过3个的时候,通过Hibernate的级联会造成太多性能的丢失。MyBatis 可以自由书写SQL、支持动态SQL、处理列表、动态生成表名,支持存储过程。这样就可以灵活地定义查询语句,满足各类需求和性能优化的需要,这些在互联网系统中是十分重要的。
4.4. Servlet
Servlet是一种独立于操作系统平台和网络传输协议的服务器端的Java应用程序。
4.4.1. 调用Servlet
每个Servlet都对应一个URL地址,可以作为显式URL引用调用,或嵌入在HTML中并从Web应用程序返回。
对于每个Web应用,都可以存在一个配置文件web.xml,存放关于Servlet的名称、对应的Java类文件、URL地址映射等信息。自JavaEE6后,JavaEE规范推荐使用注解来配置Web组件。
4.4.2. 处理请求
Web容器收到请求后,Web容器会产生一个新的线程来调用Servlet的service(),service()方法检查HTTP请求类型(GET、POST、PUT、DELETE等),然后相应调用doGet()、doPost()、doPut()、doDelete()等方法。
4.4.3. 多个请求
一个Servlet同一时刻只有一个实例。
当多个请求发送到同一个Servlet,服务器会为每个请求创建一个新线程来处理。
4.5. Spring的初始化
链接: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。
5. 工具类
5.1. Redis
5.1.1. Redis为什么要作为缓存?
使用内存数据库做缓存提高系统性能和并发能力。
5.1.2. redis和memcached的区别
- Redis和Memcache都是将数据存放在内存中,都是内存数据库。不过memcache还可用于缓存其他东西,例如图片、视频等等;
- Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,hash等数据结构的存储;
- 虚拟内存–Redis当物理内存用完时,可以将一些很久没用到的value 交换到磁盘;
- 过期策略–memcache在set时就指定,例如set key1 0 0 8,即永不过期。Redis可以通过例如expire 设定,例如expire name 10;
- 分布式–设定memcache集群,利用magent做一主多从;redis可以做一主多从。都可以一主一从;
- 存储数据安全–memcache挂掉后,数据没了;redis可以定期保存到磁盘(持久化);
- 灾难恢复–memcache挂掉后,数据不可恢复; redis数据丢失后可以通过aof恢复;
- Redis支持数据的备份,即master-slave模式的数据备份;
- 应用场景不一样:Redis出来作为NoSQL数据库使用外,还能用做消息队列、数据堆栈和数据缓存等;Memcached适合于缓存SQL语句、数据集、用户临时性数据、延迟查询数据和session等。
5.1.3. Redis常用数据结构以及数据结构底层
- 简单动态字符串
Redis使用简单字符串SDS作为字符串的表示,相对于C语言字符串,SDS具有常数复杂度获取字符串长度,杜绝了缓存区的溢出,减少了修改字符串长度时所需的内存重分配次数,以及二进制安全能存储各种类型的文件,并且还兼容部分C函数。
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为节省内存而开发的顺序型数据结构,通常作为列表键和哈希键的底层实现之一。
5.1.4. 定期删除、惰性删除
- 定时删除 节约内存,无占用 不分时段占用CPU资源,频度高 拿时间换空间
- 惰性删除 内存占用严重 延时执行,CPU利用率高 拿空间换时间
- 定期删除 内存定期随机删除 每秒花费固定的CPU资源维护内存 随机抽查,重点抽查
5.1.5. 内存淘汰机制
不管是定期采样删除还是惰性删除都不是一种完全精准的删除,就还是会存在key没有被删除掉的场景,所以就需要内存淘汰策略进行补充。
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,(一般没人用)
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(最常用)
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,一般没人用
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(一般不太合适)
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除
5.1.6. 持久化机制
参考:https://www.cnblogs.com/justinli/p/6809791.html
redis是基于内存的数据库,提供了内存数据持久化到文件的两种方式,一种是写RDB文件方式,另一种是写AOF文件,默认执行的是RDB文件持久化方式。
redis如何同步RDB文件 ,通常情况下,redis通过读取配置文件定期保存数据库的状态到RDB文件
Redis除了提供RDB文件的持久化方式外,还提供了AOF持久化机制,与RDB保存数据库的键值对的方式不同的是,AOF提供的持久化机制保存的是redis执行的命令
5.1.7. 缓存穿透,缓存击穿,缓存雪崩
参考: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里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期
5.1.8. 并发竞争key问题
分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)基于zookeeper临时有序节点可以实现的分布式锁。
5.1.9. 如何保证缓存与数据库的双写一致性
Cache Aside Pattern最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。
读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
更新的时候,先更新数据库,然后再删除缓存。
上述方法可以解决初级的问题,但显然不够严谨。更严谨的方案是使用队列等待缓存更新:
更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。
一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。
这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤
5.1.10. Redis为什么是单线程还这么快?
-
redis是基于内存的,内存的读写速度非常快;
-
redis是单线程的,省去了很多上下文切换线程的时间;
-
redis使用多路复用技术,可以处理并发的连接。非阻塞IO 内部实现采用epoll,采用了epoll+自己实现的简单的事件框架。epoll中的读、写、关闭、连接都转化成了事件,然后利用epoll的多路复用特性,绝不在io上浪费一点时间。
5.1.11. select、poll、epoll区别
参考: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的实现会负责把数据从内核拷贝到用户空间。
5.1.12. 五种IO模型
参考:https://www.jianshu.com/p/486b0965c296
同步阻塞 IO(blocking IO)、 同步非阻塞 IO(nonblocking IO)、IO 多路复用( IO multiplexing)、信号驱动式IO(signal-driven IO)、异步非阻塞 IO(asynchronous IO)
5.1.13. 哨兵机制
在一个典型的一主多从的系统中,slave在整个体系中起到了数据冗余备份和读写分离的作用。当master遇到异常终端后,需要从slave中选举一个新的master继续对外提供服务,这种机制在前面提到过N次,比如在zk中通过leader选举、kafka中可以基于zk的节点实现master选举。所以在redis中也需要一种机制去实现master的决策,redis并没有提供自动master选举功能,而是需要借助一个哨兵来进行监控。
5.1.14. 分布式锁
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下,进入下次循环中。
5.1.15. 消息队列
- 使用list
先介绍要使用的几个命令:
命令 | 描述 |
---|---|
LPUSH | 从左边插入数据 |
RPUSH | 从右边插入数据 |
LPOP | 从左边取出一个数据 |
RPOP | 从右边取出一个数据 |
BLPOP | 取数据时如果没有数据会等待指定时间 |
BRPOP | 取数据时如果没有数据会等待指定时间 |
- 发布/订阅模式
Redis除了对消息队列提供支持外,还提供了一组命令用于支持发布/订阅模式。
参考: https://blog.csdn.net/qq_34212276/article/details/78455004
命令 | 描述 |
---|---|
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 数据结构实现的。
5.2. Spring全家桶
5.2.1. Spring的启动流程
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,而是通过一些转换,具体可自行查看源码)的属性为属性
5.2.2. Spring如何解决循环依赖问题
Spring是通过递归的方式获取目标bean及其所依赖的bean的;Spring实例化一个bean的时候,是分两步进行的,首先实例化目标bean,然后为其注入属性。
结合这两点,也就是说,Spring在实例化一个bean的时候,是首先递归的实例化其所依赖的所有bean,直到某个bean没有依赖其他bean,此时就会将该实例返回,然后反递归的将获取到的bean设置为各个上层bean的属性的。
5.2.3. Spring事务传播行为
事务传播行为用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播。
5.2.4. Spring自动配置
参考:https://blog.csdn.net/u014745069/article/details/83820511
Spring Boot启动的时候会通过@EnableAutoConfiguration
注解找到META-INF/spring.factories
配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。
5.2.4.1. 流程分析
@SpringBootApplication是一个复合注解或派生注解,在@SpringBootApplication中有一个注解@EnableAutoConfiguration,翻译成人话就是开启自动配置,其定义如下:
@Import(AutoConfigurationImportSelector.class)
而这个注解也是一个派生注解,其中的关键功能由@Import提供,其导入的AutoConfigurationImportSelector的selectImports()方法通过SpringFactoriesLoader.loadFactoryNames()扫描所有具有META-INF/spring.factories
的jar包。spring-boot-autoconfigure-x.x.x.x.jar里就有一个这样的spring.factories文件。
这个spring.factories文件也是一组一组的key=value的形式,其中一个key是EnableAutoConfiguration类的全类名,而它的value是一个xxxxAutoConfiguration的类名的列表,这些类名以逗号分隔。
这个@EnableAutoConfiguration注解通过@SpringBootApplication被间接的标记在了Spring Boot的启动类上。在SpringApplication.run(…)的内部就会执行selectImports()方法,找到所有JavaConfig自动配置类的全限定名对应的class,然后将所有自动配置类加载到Spring容器中。
5.2.4.2. 自动配置生效
每一个XxxxAutoConfiguration自动配置类都是在某些条件之下才会生效的,这些条件的限制在Spring Boot中以注解的形式体现,常见的条件注解有如下几项:
@ConditionalOnBean:当容器里有指定的bean的条件下。
@ConditionalOnMissingBean:当容器里不存在指定bean的条件下。
@ConditionalOnClass:当类路径下有指定类的条件下。
@ConditionalOnMissingClass:当类路径下不存在指定类的条件下。
@ConditionalOnProperty:指定的属性是否有指定的值,比如@ConditionalOnProperties(prefix=”xxx.xxx”, value=”enable”, matchIfMissing=true),代表当xxx.xxx为enable时条件的布尔值为true,如果没有设置的情况下也为true。
5.2.4.3. 配置文件如何生效?
例如在ServletWebServerFactoryAutoConfiguration类上,有一个@EnableConfigurationProperties注解:开启配置属性,而它后面的参数是一个ServerProperties类,这就是习惯优于配置的最终落地点。
在这个类上,我们看到了一个非常熟悉的注解:@ConfigurationProperties,它的作用就是从配置文件中绑定属性到对应的bean上,而@EnableConfigurationProperties负责导入这个已经绑定了属性的bean到spring容器中(见上面截图)。那么所有其他的和这个类相关的属性都可以在全局配置文件中定义,也就是说,真正“限制”我们可以在全局配置文件中配置哪些属性的类就是这些XxxxProperties类,它与配置文件中定义的prefix关键字开头的一组属性是唯一对应的。
至此,我们大致可以了解。在全局配置的属性如:server.port等,通过@ConfigurationProperties注解,绑定到对应的XxxxProperties配置实体类上封装为一个bean,然后再通过@EnableConfigurationProperties注解导入到Spring容器中。
5.2.5. Spring cloud 基本使用
5.2.5.1. 服务的注册与发现(Eureka )
创建一个服务提供者 (eureka client),当client向server注册时,它会提供一些元数据,例如主机和端口,URL,主页等。Eureka server 从每个client实例接收心跳消息。 如果心跳超时,则通常将该实例从注册server中删除。
5.2.5.2. 负载均衡 (Ribbon)
Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。
5.2.5.3. 服务消费者(Feign)
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);
}
}
5.2.5.4. Hystrix断路器
在Spring Cloud可以用RestTemplate+Ribbon和Feign来调用。为了保证其高可用,单个服务通常会集群部署。由于网络原因或者自身的原因,服务并不能保证100%可用,如果单个服务出现问题,调用这个服务就会出现线程阻塞,此时若有大量的请求涌入,Servlet容器的线程资源会被消耗完毕,导致服务瘫痪。服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的“雪崩”效应。
5.3. mybatis
5.3.1. mybatis工作流程
- 创建SqlSessionFactoryBuilder对象,调用build(inputstream)方法读取并解析配置文件,返回SqlSessionFactory对象
- 由SqlSessionFactory创建SqlSession 对象,没有手动设置的话事务默认开启
- 调用SqlSession中的api,传入Statement Id和参数,内部进行复杂的处理,最后调用jdbc执行SQL语句,封装结果返回。其中MappedStatement是通过动态代理从mapper中生成的。
5.3.2. mybatis常用注解
@TableName
:映射数据库表名@TableFieId
:映射非主键字段@TableId
:表主键标识@Version
:更新时,实体对象的version属性必须有值,才会生成SQL
@Version
@TableField(value="version", fill = FieldFill.INSERT, update="%s+1")
protected Long version;
5.3.3. 常用标签
Mapper中一个提供了9个顶层标签,除了1个已经过期的我们不需要去了解,另外8个都是必须要掌握的,只要熟练掌握了标签的使用,使用MyBatis才能如鱼得水。
5.3.3.1. select
select用来映射查询语句,比如下面就是一个简单的select标签的使用:
<select id="listUserByUserName" parameterType="String" resultType="lwUser">
select user_id,user_name from lw_user where user_name=#{userName}
</select>
5.3.3.2. insert
<insert
id="insertLwUser"
parameterType="lwUser"
parameterMap="deprecated"
flushCache="true"
statementType="PREPARED"
keyProperty=""
keyColumn=""
useGeneratedKeys=""
timeout="20"
databaseId="mysql"
lang="">
5.3.3.3. update
update用来映射更新语句。
5.3.3.4. delete
delete用来映射删除语句。
5.3.3.5. sql
这个元素可以被用来定义可重用的 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>
5.3.3.6. cache
MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制。但是默认情况下只开启了一级缓存,即局部的session缓存,如果想要开启二级缓存。那么就需要使用到cache标签
<cache
type="com.lonelyWolf.xxx"
eviction="FIFO"
flushInterval="60000"
readOnly="true"
size="512"/>
5.4. 大数据
5.4.1. Kafka
5.4.1.1. 为什么快?
- 顺序读写
- Page Cache
Kafka利用了操作系统本身的Page Cache,就是利用操作系统自身的内存而不是JVM空间内存。这样做的好处有:
1、避免Object消耗:如果是使用 Java 堆,Java对象的内存消耗比较大,通常是所存储数据的两倍甚至更多。
2、避免GC问题:随着JVM中数据不断增多,垃圾回收将会变得复杂与缓慢,使用系统缓存就不会存在GC问题 - 零拷贝
linux操作系统 “零拷贝” 机制使用了sendfile方法, 允许操作系统将数据从Page Cache 直接发送到网络,只需要最后一步的copy操作将数据复制到 NIC 缓冲区, 这样避免重新复制数据 。 - 分区分段+索引
Kafka的message是按topic分类存储的,topic中的数据又是按照一个一个的partition即分区存储到不同broker节点。每个partition对应了操作系统上的一个文件夹,partition实际上又是按照segment分段存储的。这也非常符合分布式系统分区分桶的设计思想。 - 批量读写
Kafka数据读写也是批量的而不是单条的。
除了利用底层的技术外,Kafka还在应用程序层面提供了一些手段来提升性能。最明显的就是使用批次。在向Kafka写入数据时,可以启用批次写入,这样可以避免在网络上频繁传输单个消息带来的延迟和带宽开销。假设网络带宽为10MB/S,一次性传输10MB的消息比传输1KB的消息10000万次显然要快得多。 - 批量压缩
5.4.1.2. Topic 和 Partition
在 Kafka 中,Topic 是一个存储消息的逻辑概念,可以认为是一个消息集合。每条消息发送到 Kafka 集群的消息都有一个类别。物理上来说,不同的 Topic 的消息是分开存储的,每个 Topic 可以有多个生产者向它发送消息,也可以有多个消费者去消费其中的消息。
每个 Topic 可以划分多个分区(每个 Topic 至少有一个分区),同一 Topic 下的不同分区包含的消息是不同的。每个消息在被添加到分区时,都会被分配一个 offset,它是消息在此分区中的唯一编号,Kafka 通过 offset 保证消息在分区内的顺序,offset 的顺序不跨分区,即 Kafka 只保证在同一个分区内的消息是有序的。
5.4.1.3. 消息分发
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。
5.4.1.4. Kafka 分区分配策略
在 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 会均匀分布。
5.4.1.5. Reblance
当出现以下几种情况时,Kafka 会进行一次分区分配操作,也就是 Kafka Consumer 的 Rebalance
- 同一个 Consumer group内新增了消费者
- 消费者离开当前所属的 Consumer group,比如主动停机或者宕机
- Topic 新增了分区(也就是分区数量发生了变化)
Kafka Consuemr 的 Rebalance 机制规定了一个 Consumer group 下的所有 Consumer 如何达成一致来分配订阅 Topic 的每个分区。而具体如何执行分区策略,就是前面提到过的两种内置的分区策略。而 Kafka 对于分配策略这块,提供了可插拔的实现方式,也就是说,除了这两种之外,我们还可以创建自己的分配机制。
谁来执行 Rebalance 以及管理 Consumer 的 group 呢?
Consumer group 如何确定自己的 coordinator 是谁呢,消费者向 Kafka 集群中的任意一个 broker 发送一个 GroupCoord inatorRequest 请求,服务端会返回一个负载最小的 broker 节点的 id,并将该 broker 设置为 coordinator。
5.4.1.6. JoinGroup 的过程
在 Rebalance 之前,需要保证 coordinator 是已经确定好了的,整个 Rebalance 的过程分为两个步骤,Join和Syncjoin:表示加入到 Consumer group中,在这一步中,所有的成员都会向 coordinator 发送 joinGroup 的请求。一旦所有成员都发了 joinGroup请求,那么 coordinator 会选择一个 Consumer 担任 leader 角色,并把组成员信息和订阅信息发送给消费者
5.4.1.7. kafka中的ISR、AR又代表什么?
分区中的所有副本统称为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集合中的副本则没有机会(这个原则可以通过修改对应的参数配置来改变)
5.4.1.8. 序列化器、拦截器、分区器
KafkaProducer的消息在send()之后,可能会经过拦截器、序列化器、分区器之后才会真正到达Kafka-Broker中的Partition中
5.4.1.9. 高水位
在Kafka中,高水位的作用主要有两个
- 定义消息可见性,即用来标识分区下的哪些消息是可以被消费者消费的。
- 帮助Kafka完成副本同步
**Log End Offset(LEO)**表示副本写入下一条消息的位移值。注意,数字 15 所在的方框是虚线,这就说明,这个副本当前只有 15 条消息,位移值是从 0 到 14,下一条新消息的位移是 15。显然,介于高水位和 LEO 之间的消息就属于未提交消息。这也从侧面告诉了我们一个重要的事实,那就是:同一个副本对象,其高水位值不会大于 LEO 值。
高水位和 LEO 是副本对象的两个重要属性。Kafka 所有副本都有对应的高水位和 LEO 值,而不仅仅是 Leader 副本。只不过 Leader 副本比较特殊,Kafka 使用 Leader 副本的高水位来定义所在分区的高水位。换句话说,分区的高水位就是其 Leader 副本的高水位。
5.4.1.10. Kafka如何保证消息可靠性和一致性
生产者数据的不丢失
kafka 的 ack 机制:在 kafka 发送数据的时候,每次发送消息都会有一个确认反馈机制,确保消息正常的能够被收到。
消费者数据的不丢失
通过 offset commit 来保证数据的不丢失,kafka 自己记录了每次消费的 offset 数值,下次继续消费的时候,接着上次的 offset 进行消费即可。
保存的数据不丢失
kafka按照partition保存, 数据可以保存多个副本 , 副本中有一个副本是 Leader,其余的副本是 follower , follower会定期的同步leader的数据
5.4.1.11. Kafka事务
Kafka 的事务处理,主要是允许应用可以把消费和生产的 batch 处理(涉及多个 Partition)在一个原子单元内完成,操作要么全部完成、要么全部失败。为了实现这种机制,我们需要应用能提供一个唯一 id,即使故障恢复后也不会改变,这个 id 就是 TransactionnalId(也叫 txn.id,后面会详细讲述),txn.id 可以跟内部的 PID 1:1 分配,它们不同的是 txn.id 是用户提供的,而 PID 是 Producer 内部自动生成的(并且故障恢复后这个 PID 会变化),有了 txn.id 这个机制,就可以实现多 partition、跨会话的 EOS 语义。
当用户使用 Kafka 的事务性时,Kafka 可以做到的保证:
- 跨会话的幂等性写入:即使中间故障,恢复后依然可以保持幂等性;
- 跨会话的事务恢复:如果一个应用实例挂了,启动的下一个实例依然可以保证上一个事务完成(commit 或者 abort);
- 跨多个 Topic-Partition 的幂等性写入,Kafka 可以保证跨多个 Topic-Partition 的数据要么全部写入成功,要么全部失败,不会出现中间状态。
上面是从 Producer 的角度来看,那么如果从 Consumer 角度呢?Consumer 端很难保证一个已经 commit 的事务的所有 msg 都会被消费,有以下几个原因:
- 对于 compacted topic,在一个事务中写入的数据可能会被新的值覆盖;
- 一个事务内的数据,可能会跨多个 log segment,如果旧的 segmeng 数据由于过期而被清除,那么这个事务的一部分数据就无法被消费到了;
- Consumer 在消费时可以通过 seek 机制,随机从一个位置开始消费,这也会导致一个事务内的部分数据无法消费;
- Consumer 可能没有订阅这个事务涉及的全部 Partition。
因此为了解决上述问题,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的消息。
5.4.2. 选举算法
5.4.2.1. raft
包括三种角色: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。
5.4.2.2. paxos
两个阶段分别是准备(prepare)和提交(commit)。准备阶段解决大家对哪个提案进行投票的问题,提交阶段解决确认最终值的问题。
简单来说,提案者发出提案后,收到一些反馈,有两种结果,一种结果是自己的提案被大多数节点接受了,另外一种是没被接受,没被接受就过会再试试。
提案者收到来自大多数的接受反馈,也不能认为这就是最终确认。因为这些接收者并不知道自己刚反馈的提案就是全局的绝对大多数。
所以,引入新的一轮再确认阶段是必须的,提案者在判断这个提案可能被大多数接受的情况下,发起一轮新的确认提案。这就进入了提交阶段。
提交阶段的提案发送出去,其他阶段进行提案值比较,返回最大的,所以提案者收到返回消息不带新的提案,说明锁定成功,如果有新的提案内容,进行提案值最大比较,然后替换更大的值。如果没有收到足够多的回复,则需要再次发出请求。
一旦多数接受了共同的提案值,则形成决议,称为最终确认的提案。
5.5. Netty Nio
5.5.1. Netty介绍
Netty 对 JDK 自带的 NIO 的 API 进行封装,解决上述问题,主要特点有:
- 设计优雅,适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型 - 单线程,一个或多个线程池;真正的无连接数据报套接字支持(自 3.1 起)。
- 使用方便,详细记录的 Javadoc,用户指南和示例;没有其他依赖项,JDK 5(Netty 3.x)或 6(Netty 4.x)就足够了。
- 高性能,吞吐量更高,延迟更低;减少资源消耗;最小化不必要的内存复制。
- 安全,完整的 SSL/TLS 和 StartTLS 支持。
- 社区活跃,不断更新,社区活跃,版本迭代周期短,发现的 Bug 可以被及时修复,同时,更多的新功能会被加入。
5.5.2. BIO和NIO
阻塞IO:当阻塞IO调用inputStream.read()方法是阻塞的,一直等到数据到来时才返回,同样ServerScoket.accpt()方法时,也是阻塞,直到客户端连接才返回。
缺点:当客户端多时,才创建大量的处理线程,并且每一个线程分配一定的资源;阻塞可能打来频繁切换上下文,引用NIO。
NIO:jdk1.4引入。基于通过和缓存区的IO方式,NIO是一种非阻塞IO的模型,通过不断轮询IO事件是否就绪,非阻塞是在指线程在等待IO的时候,可以做其他的任务,同步的核心Selector,Selector代替线程本身的轮询IO事件。避免阻塞同时减少不想必要的线程消耗,非阻塞的核心是通道和缓存区,当IO事件的就绪是,可以缓冲区的数据写入通道。
5.5.3. IO多路复用
Netty 的 IO 线程 NioEventLoop 由于聚合了多路复用器 Selector,可以同时并发处理成百上千个客户端连接。
当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。
由于读写操作都是非阻塞的,这就可以充分提升 IO 线程的运行效率,避免由于频繁 I/O 阻塞导致的线程挂起。
一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
5.5.4. 基于 Buffer
传统的 I/O 是面向字节流或字符流的,以流式的方式顺序地从一个 Stream 中读取一个或多个字节, 因此也就不能随意改变读取指针的位置。
在 NIO 中,抛弃了传统的 I/O 流,而是引入了 Channel 和 Buffer 的概念。在 NIO 中,只能从 Channel 中读取数据到 Buffer 中或将数据从 Buffer 中写入到 Channel。
基于 Buffer 操作不像传统 IO 的顺序操作,NIO 中可以随意地读取任意位置的数据。
5.5.5. 事件驱动
主要包括 4 个基本组件:
事件队列(event queue):接收事件的入口,存储待处理事件。
分发器(event mediator):将不同的事件分发到不同的业务逻辑单元。
事件通道(event channel):分发器与处理器之间的联系渠道。
事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作。
可以看出,相对传统轮询模式,事件驱动有如下优点:
- 可扩展性好,分布式的异步架构,事件处理器之间高度解耦,可以方便扩展事件处理逻辑。
- 高性能,基于队列暂存事件,能方便并行异步处理事件。
5.5.6. Reactor 线程模型
Reactor 是反应堆的意思,Reactor 模型是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。
服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor 模式也叫 Dispatcher 模式,即 I/O 多了复用统一监听事件,收到事件后分发(Dispatch 给某进程),是编写高性能网络服务器的必备技术之一。
Reactor 模型中有 2 个关键组成:
Reactor,Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人。
Handlers,处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。
5.5.7. Netty 线程模型
Netty 主要基于主从 Reactors 多线程模型(如下图)做了一定的修改,其中主从 Reactor 多线程模型有多个 Reactor:
MainReactor 负责客户端的连接请求,并将请求转交给 SubReactor。
SubReactor 负责相应通道的 IO 读写请求。
非 IO 请求(具体逻辑处理)的任务则会直接写入队列,等待 worker threads 进行处理。
5.5.8. 异步处理
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。
Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture。
调用者并不能立刻获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果。
当 Future 对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态,注册监听函数来执行完成后的操作。
常见有如下操作:
通过 isDone 方法来判断当前操作是否完成。
通过 isSuccess 方法来判断已完成的当前操作是否成功。
通过 getCause 方法来获取已完成的当前操作失败的原因。
通过 isCancelled 方法来判断已完成的当前操作是否被取消。
通过 addListener 方法来注册监听器,当操作已完成(isDone 方法返回完成),将会通知指定的监听器;如果 Future 对象已完成,则理解通知指定的监听器。
5.6. Nginx
5.6.1. Nginx进程模型
Nginx在启动后,会有一个master进程和多个worker进程。
5.6.1.1. Master进程
master进程主要用来管理worker进程,包含:
- 接收来自外界的信号;
- 向各worker进程发送信号;
- 监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。
master进程充当整个进程组与用户的交互接口,同时对进程进行监护。它不需要处理网络事件,不负责业务的执行,只会通过管理worker进程来实现重启服务、平滑升级、更换日志文件、配置文件实时生效等功能。
我们要控制nginx,只需要通过kill向master进程发送信号就行了。比如kill -HUP pid,则是告诉nginx,从容地重启nginx,我们一般用这个信号来重启nginx,或重新加载配置,因为是从容地重启,因此服务是不中断的。
5.6.2. Worker进程
基本的网络事件,则是放在worker进程中来处理了。多个worker进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。
worker进程的个数是可以设置的,一般我们会设置与机器cpu核数一致,这里面的原因与nginx的进程模型以及事件处理模型是分不开的。
worker进程之间是平等的,每个进程,处理请求的机会也是一样的。当我们提供80端口的http服务时,一个连接请求过来,每个进程都有可能处理这个连接,怎么做到的呢?
首先,每个worker进程都是从master进程fork过来,在master进程里面,先建立好需要listen的socket(listenfd)之后,然后再fork出多个worker进程。所有worker进程的listenfd会在新连接到来时变得可读,为保证只有一个进程处理该连接,所有worker进程在注册listenfd读事件前抢accept_mutex,抢到互斥锁的那个进程注册listenfd读事件,在读事件里调用accept接受该连接。当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。我们可以看到,一个请求,完全由worker进程来处理,而且只在一个worker进程中处理。
5.6.3. Nginx处理HTTP请求流程
Http请求是典型的请求-响应类型的的网络协议。Http是文件协议,所以我们要分析请求行与请求头,以及输出响应行与响应头,这些往往是一行一行的进行处理。通常在一个连接建立好后,读取一行数据,分析出请求行中包含的method、uri、http_version信息;然后再一行一行处理请求头,并根据请求method与请求头的信息来决定是否有请求体以及请求体的长度,然后再去读取请求体;得到请求后,我们处理请求产生需要输出的数据,然后再生成响应行,响应头以及响应体。在将响应发送给客户端之后,一个完整的请求就处理完了。
6. OS类
6.1. 页面置换算法
地址映射过程中,若在页面中发现所要访问的页面不再内存中,则产生缺页中断。当发生缺页中断时操作系统必须在内存选择一个页面将其移出内存,以便为即将调入的页面让出空间。下面列出几种常见的置换算法。
6.1.1. 最佳置换算法(OPT)
每个页面都可以用在该页面首次被访问前所要执行的指令数进行标记。最佳页面置换算法规定:标记最大的页应该被置换。这个算法唯一的一个问题就是它无法实现。当缺页发生时,操作系统无法知道各个页面下一次是在什么时候被访问。虽然这个算法不可能实现,但是最佳页面置换算法可以用于对可实现算法的性能进行衡量比较。
6.1.2. 先进先出置换算法(FIFO)
这种算法的实质是,总是选择在主存中停留时间最长(即最老)的一页置换,即先进入内存的页,先退出内存。
6.1.3. 最近最久未使用(LRU)算法
当必须置换一个页面时,LRU算法选择过去一段时间里最久未被使用的页面。
因实现LRU算法必须有大量硬件支持,还需要一定的软件开销。所以实际实现的都是一种简单有效的LRU近似算法:
一种LRU近似算法是最近未使用算法(Not Recently Used,NUR)。它在存储分块表的每一表项中增加一个引用位,操作系统定期地将它们置为0。当某一页被访问时,由硬件将该位置1。过一段时间后,通过检查这些位可以确定哪些页使用过,哪些页自上次置0后还未使用过。就可把该位是0的页淘汰出去,因为在之前最近一段时间里它未被访问过。
6.1.4. 最少使用(LFU)置换算法
在采用最少使用置换算法时,应为在内存中的每个页面设置一个移位寄存器,用来记录该页面被访问的频率。该置换算法选择在之前时期使用最少的页面作为淘汰页。由于存储器具有较高的访问速度,例如100 ns,在1 ms时间内可能对某页面连续访 问成千上万次,因此,通常不能直接利用计数器来记录某页被访问的次数,而是采用移位寄存器方式。每次访问某页时,便将该移位寄存器的最高位置1,再每隔一定时间(例如100 ns)右移一次。这样,在最近一段时间使用最少的页面将是∑Ri最小的页。
6.2. 线程和进程
6.2.1. 区别
- 进程采用fork创建,线程采用clone创建
- 进程fork创建的子进程的逻辑流位置在fork返回的位置,线程clone创建的KSE的逻辑流位置在clone调用传入的方法位置,比如Java的Thread的run方法位置
- 进程拥有独立的虚拟内存地址空间和内核数据结构(页表,打开文件表等),当子进程修改了虚拟页之后,会通过写时拷贝创建真正的物理页。线程共享进程的虚拟地址空间和内核数据结构,共享同样的物理页
- 多个进程通信只能采用进程间通信的方式,比如信号,管道,而不能直接采用简单的共享内存方式,原因是每个进程维护独立的虚拟内存空间,所以每个进程的变量采用的虚拟地址是不同的。多个线程通信就很简单,直接采用共享内存的方式,因为不同线程共享一个虚拟内存地址空间,变量寻址采用同一个虚拟内存
- 进程上下文切换需要切换页表等重量级资源,线程上下文切换只需要切换寄存器等轻量级数据
- 进程的用户栈独享栈空间,线程的用户栈共享虚拟内存中的栈空间,没有进程高效
- 一个应用程序可以有多个进程,执行多个程序代码,多个线程只能执行一个程序代码,共享进程的代码段
- 进程采用父子结构,线程采用对等结构
6.2.2. 地址空间
在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中。这个沙盘就是虚拟地址空间(Virtual Address Space),在32位模式下它是一个4GB的内存地址块。虚拟地址通过页表(Page Table)映射到物理内存,页表由操作系统维护并被处理器引用。
名称 | 存储内容 |
---|---|
栈 | 局部变量、函数参数、返回地址等 |
堆 | 动态分配的内存 |
BSS段 | 未初始化或初值为0的全局变量和静态局部变量 |
数据段 | 已初始化且初值非0的全局变量和静态局部变量 |
代码段 | 可执行代码、字符串字面值、只读变量 |
在将应用程序加载到内存空间执行时,操作系统负责代码段、数据段和BSS段的加载,并在内存中为这些段分配空间。栈也由操作系统分配和管理;堆由程序员自己管理,即显式地申请和释放空间。
BSS段、数据段和代码段是可执行程序编译时的分段,运行时还需要栈和堆。
从 Linux 内核的角度来说,其实它并没有线程的概念。Linux 把所有线程都当做进程来实现,它将线程和进程不加区分的统一到了 task_struct 中。线程仅仅被视为一个与其他进程共享某些资源的进程,而是否共享地址空间几乎是进程和 Linux 中所谓线程的唯一区别。Linux中,进程和线程都被维护为一个task_struct
结构,线程和进程被同等对待来进行调度。
6.3. select,poll,epoll
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内部定义的等待队列)。这也能节省不少的开销。
6.4. 内核态与用户态的区别
内核态与用户态是操作系统的两种运行级别,当程序运行在3级特权级上时,就可以称之为运行在用户态。因为这是最低特权级,是普通的用户进程运行的特权级,大部分用户直接面对的程序都是运行在用户态;
当程序运行在0级特权级上时,就可以称之为运行在内核态。
运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态(比如操作硬件)。
这两种状态的主要差别是
- 处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理器是可被抢占的
- 处于内核态执行时,则能访问所有的内存空间和对象,且所占有的处理器是不允许被抢占的。
6.5. 用户级线程和内核级线程
用户级线程(user level thread):对于这类线程,有关线程管理的所有工作都由应用程序完成,内核意识不到线程的存在。在应用程序启动后,操作系统分配给该程序一个进程号,以及其对应的内存空间等资源。应用程序通常先在一个线程中运行,该线程被成为主线程。在其运行的某个时刻,可以通过调用线程库中的函数创建一个在相同进程中运行的新线程。用户级线程的好处是非常高效,不需要进入内核空间,但并发效率不高。
内核级线程(kernel level thread):对于这类线程,有关线程管理的所有工作由内核完成,应用程序没有进行线程管理的代码,只能调用内核线程的接口。内核维护进程及其内部的每个线程,调度也由内核基于线程架构完成。内核级线程的好处是,内核可以将不同线程更好地分配到不同的CPU,以实现真正的并行计算。
事实上,在现代操作系统中,往往使用组合方式实现多线程,即线程创建完全在用户空间中完成,并且一个应用程序中的多个用户级线程被映射到一些内核级线程上,相当于是一种折中方案。
6.6. 大内核和微内核有什么区别?
大内核,就是将操作系统的全部功能都放进内核里面,包括调度、文件系统、网络、设备驱动器、存储管理等等,组成一个紧密连接整体。大内核的优点就是效率高,但是很难定位bug,拓展性比较差,每次需要增加新的功能,都要将新的代码和原来的内核代码重新编译。
微内核与单体内核不同,微内核只是将操作中最核心的功能加入内核,包括IPC、地址空间分配和基本的调度,这些东西都在内核态运行,其他功能作为模块被内核调用,并且是在用户空间运行。微内核比较好维护和拓展,但是效率可能不高,因为需要频繁地在内核态和用户态之间切换。
6.7. 进程状态
在五状态模型里面,进程一共有5中状态,分别是创建、就绪、运行、终止、阻塞。
运行状态就是进程正在CPU上运行。在单处理机环境下,每一时刻最多只有一个进程处于运行状态。
就绪状态就是说进程已处于准备运行的状态,即进程获得了除CPU之外的一切所需资源,一旦得到CPU即可运行。
阻塞状态就是进程正在等待某一事件而暂停运行,比如等待某资源为可用或等待I/O完成。即使CPU空闲,该进程也不能运行。
运行态→阻塞态:往往是由于等待外设,等待主存等资源分配或等待人工干预而引起的。
阻塞态→就绪态:则是等待的条件已满足,只需分配到处理器后就能运行。
运行态→就绪态:不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,这时候就变成就绪态。例如时间片用完,或有更高优先级的进程来抢占处理器等。
就绪态→运行态:系统按某种策略选中就绪队列中的一个进程占用处理器,此时就变成了运行态。
6.8. 进程通信
进程通信,是指进程之间的信息交换(信息量少则一个状态或数值,多者则是成千上万个字节)。因此,对于用信号量进行的进程间的互斥和同步,由于其所交换的信息量少而被归结为低级通信。
高级通信机制可归结为三大类:
-
共享存储器系统(存储器中划分的共享存储区);实际操作中对应的是“剪贴板”(剪贴板实际上是系统维护管理的一块内存区域)的通信方式,比如举例如下:word进程按下ctrl+c,在ppt进程按下ctrl+v,即完成了word进程和ppt进程之间的通信,复制时将数据放入到剪贴板,粘贴时从剪贴板中取出数据,然后显示在ppt窗口上。
-
消息传递系统(进程间的数据交换以消息(message)为单位,当今最流行的微内核操作系统中,微内核与服务器之间的通信,无一例外地都采用了消息传递机制。应用举例:邮槽(MailSlot)是基于广播通信体系设计出来的,它采用无连接的不可靠的数据传输。邮槽是一种单向通信机制,创建邮槽的服务器进程读取数据,打开邮槽的客户机进程写入数据。
-
管道通信系统(管道即:连接读写进程以实现他们之间通信的共享文件(pipe文件,类似先进先出的队列,由一个进程写,另一进程读))。实际操作中,管道分为:匿名管道、命名管道。匿名管道是一个未命名的、单向管道,通过父进程和一个子进程之间传输数据。匿名管道只能实现本地机器上两个进程之间的通信,而不能实现跨网络的通信。命名管道不仅可以在本机上实现两个进程间的通信,还可以跨网络实现两个进程间的通信。
6.9. 死锁产生有哪些条件?
死锁产生的根本原因是多个进程竞争资源时,进程的推进顺序出现不正确。
互斥:每个资源要么已经分配给了一个进程,要么就是可用的。
占有和等待:已经得到了某个资源的进程可以再请求新的资源。
不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。
6.10. 死锁预防
死锁预防是指通过破坏死锁产生的四个必要条件中的一个或多个,以避免发生死锁。
破坏互斥:不让资源被一个进程独占,可通过假脱机技术允许多个进程同时访问资源;
破坏占有和等待:有两种方案,已拥有资源的进程不能再去请求其他资源。一种实现方法是要求进程在开始执行前请求需要的所有资源。要求进程请求资源时,先暂时释放其当前拥有的所有资源,再尝试一次获取所需的全部资源。
破坏不可抢占:有些资源可以通过虚拟化方式实现可抢占;
破坏循环等待:有两种方案:一种方法是保证每个进程在任何时刻只能占用一个资源,如果要请求另一个资源,必须先释放第一个资源;另一种方法是将所有资源进行统一编号,进程可以在任何时刻请求资源,但要求进程必须按照顺序请求资源。
6.11. 死锁检测和恢复
可以允许系统进入死锁状态,但会维护一个系统的资源分配图,定期调用死锁检测算法来检测途中是否存在死锁,检测到死锁发生后,采取死锁恢复算法进行恢复。
死锁检测方法如下:
在资源分配图中,找到不会阻塞又不独立的进程结点,使该进程获得其所需资源并运行,运行完毕后,再释放其所占有的全部资源。也就是消去该进程结点的请求边和分配边。
使用上面的算法进行一系列简化,若能消去所有边,则表示不会出现死锁,否则会出现死锁。检测到死锁后,就需要解决死锁。目前操作系统中主要采用如下几种方法:
取消所有死锁相关线程,简单粗暴,但也确实是最常用的把每个死锁线程回滚到某些检查点,然后重启连续取消死锁线程直到死锁解除,顺序基于特定最小代价原则连续抢占资源直到死锁解除。
6.12. 逻辑地址 Vs 物理地址 Vs 虚拟内存
所谓的逻辑地址,是指计算机用户(例如程序开发者),看到的地址。例如,当创建一个长度为100的整型数组时,操作系统返回一个逻辑上的连续空间:指针指向数组第一个元素的内存地址。由于整型元素的大小为4个字节,故第二个元素的地址时起始地址加4,以此类推。事实上,逻辑地址并不一定是元素存储的真实地址,即数组元素的物理地址(在内存条中所处的位置),并非是连续的,只是操作系统通过地址映射,将逻辑地址映射成连续的,这样更符合人们的直观思维。
另一个重要概念是虚拟内存。操作系统读写内存的速度可以比读写磁盘的速度快几个量级。但是,内存价格也相对较高,不能大规模扩展。于是,操作系统可以通过将部分不太常用的数据移出内存,“存放到价格相对较低的磁盘缓存,以实现内存扩展。操作系统还可以通过算法预测哪部分存储到磁盘缓存的数据需要进行读写,提前把这部分数据读回内存。虚拟内存空间相对磁盘而言要小很多,因此,即使搜索虚拟内存空间也比直接搜索磁盘要快。唯一慢于磁盘的可能是,内存、虚拟内存中都没有所需要的数据,最终还需要从硬盘中直接读取。这就是为什么内存和虚拟内存中需要存储会被重复读写的数据,否则就失去了缓存的意义。现代计算机中有一个专门的转译缓冲区(Translation Lookaside Buffer,TLB),用来实现虚拟地址到物理地址的快速转换。
与内存/虚拟内存相关的还有如下两个概念:
- Resident Set
当一个进程在运行的时候,操作系统不会一次性加载进程的所有数据到内存,只会加载一部分正在用,以及预期要用的数据。其他数据可能存储在虚拟内存,交换区和硬盘文件系统上。被加载到内存的部分就是resident set。
- Thrashing
由于resident set包含预期要用的数据,理想情况下,进程运行过程中用到的数据都会逐步加载进resident set。但事实往往并非如此:每当需要的内存页面(page)不在resident set中时,操作系统必须从虚拟内存或硬盘中读数据,这个过程被称为内存页面错误(page faults)。当操作系统需要花费大量时间去处理页面错误的情况就是thrashing。
6.13. 什么是分页系统?
分页就是说,将磁盘或者硬盘分为大小固定的数据块,叫做页,然后内存也分为同样大小的块,叫做页框。当进程执行的时候,会将磁盘的页载入内存的某些页框中,并且正在执行的进程如果发生缺页中断也会发生这个过程。页和页框都是由两个部分组成的,一个是页号或者页框号,一个是偏移量。分页一般是有硬件来完成的,每个页都对应一个页框,它们的对应关系存放在一个叫做页表的数据结构中,页号作为这个页表的索引,页框号作为页表的值。操作系统负责维护这个页表。
6.14. 什么是分段?
参考:分段内存管理
分页系统存在的一个无法容忍,同时也是分页系统无法解决的一个缺点就是:一个进程只能占有一个虚拟地址空间。在此种限制下,一个程序的大小至多只能和虚拟空间一样大,其所有内容都必须从这个共同的虚拟空间内分配。
分段管理就是将一个程序按照逻辑单元分成多个程序段,每一个段使用自己单独的虚拟地址空间。例如,对于编译器来说,我们可以给其5个段(如:符号表、代码段、词法树、调用栈、常数表),占用5个虚拟地址空间,并分配一段连续的地址空间(段内要求连续,段间不要求连续,因此整个作业的地址空间是二维的)。其逻辑地址由段号S与段内偏移量W两部分组成。
如此,一个段占用一个虚拟地址空间,不会发生空间增长时碰撞到另一个段的问题,从而避免因空间不够而造成编译失败的情况。如果某个数据结构对空间的需求超过整个虚拟之地所能够提供的空间,则编译仍将失败。不过出现这种可能的概率恐怕不会比太阳从西边出来的概率高出多少。
在页式系统中,逻辑地址的页号和页内偏移量对用户是透明的,但在段式系统中,段号和段内偏移量必须由用户显示提供,在髙级程序设计语言中,这个工作由编译程序完成。
6.15. 分页和分段有什区别?
首先说明二者之间的作用和联系。
纯粹分段: 就是为每个程序按段分配内存,使用基址极限来管理。
分页: 一个程序分为多个固定大小的页面,使用页表来管理。
逻辑分段: 程序按照逻辑分为多个段,使用一组基址极限来管理,基址极限放在段表里。
段页式: 程序按照逻辑分为多段,每一个段内又进行分页,通过段页表来管理。
因此二者的区别也呼之欲出。
- 分页对程序员是透明的,但是分段需要程序员显式划分每个段。
- 分页的地址空间是一维地址空间,分段是二维的。
- 页的大小不可变,段的大小可以动态改变。
- 分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。
6.16. Linux文件系统是怎么样的?
Linux文件系统里面有文件和目录,组成一个树状的结构,树的每一个叶子节点表示文件或者空目录。每个文件基本上都由两部分组成:
inode:一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 编号;
block:记录文件的内容,文件太大时,会占用多个 block。
除此之外还包括:
superblock:记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等;
block bitmap:记录 block 是否被使用的位图。
当要读取一个文件的内容时,先在 inode 中查找文件内容所在的所有 block,然后把所有 block 的内容读出来。
7. 算法
7.1. 二进制表示
在计算机中,负数以原码的补码形式表达。
原码:一个正数,按照绝对值大小转换成的二进制数;一个负数按照绝对值大小转换成的二进制数,然后最高位补1,称为原码。
反码:正数的反码与原码相同,负数的反码为对该数的原码除符号位外各位取反。
补码:正数的补码与原码相同,负数的补码为对该数的原码除符号位外各位取反,然后在最后一位加1.
7.2. Java中的数据结构
7.2.1. TreeMap
TreeMap基于红黑树实现。查看“键”或“键值对”时,它们会被排序(次序由Comparable或Comparator决定)。TreeMap的特点在于,所得到的结果是经过排序的。
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
NavigableMap接口扩展的SortedMap,具有了针对给定搜索目标返回最接近匹配项的导航方法。方法lowerEntry、floorEntry、ceilingEntry和higherEntry分别返回与小于、小于等于、大于等于、大于给定键的键关联的Map.Entry对象,如果不存在这样的键,则返回null。类似地,方法lowerKey、floorKey、ceilingKey和higherKey只返回关联的键。所有这些方法是为查找条目而不是遍历条目而设计的。
7.3. 红黑树与AVL
7.3.1. 红黑树
红黑树是一种含有红黑结点并能自平衡的二叉查找树。
性质,它必须满足下面性质:
- 每个节点要么是黑色,要么是红色。
- 根节点是黑色。
- 每个叶子节点(NIL)是黑色。
- 从根节点到叶子节点任何一条路径上 不能出现两个连续的红结点
- 从根节点到叶子节点任何一条路径上都包含数量相同的黑结点。
操作:
左旋:以某个结点作为支点(旋转结点),其右子结点变为旋转结点的父结点,右子结点的左子结点变为旋转结点的右子结点,左子结点保持不变。如图3。
右旋:以某个结点作为支点(旋转结点),其左子结点变为旋转结点的父结点,左子结点的右子结点变为旋转结点的左子结点,右子结点保持不变。如图4。
变色:结点的颜色由红变黑或由黑变红。
7.3.2. AVL树
AVL的平衡通过平衡因子(bf=左树深度-右树深度)和左单旋、右单旋、左右双旋、右左双旋四种操作保证。
7.3.3. 比较
删除旋转红黑树保证最多三次,而AVL树则可能是对数级别的。但是真实序列里影响不大。
7.4. 大数据下订单去重
- 用一个Hash函数 (f1) 对订单id进行运算, 比如简单的timer33就行。
- 将得到的hash结果数字再对内存区总长度取模。 会得到一个数字 n1.
- 我们把内存区 n1 的位置填为1.
- 然后我们再换几种hash实现, 分别对应的函数式f2, f3, f4, f5. 结果分别是n2, n3, n4, n5.
- 将这些n1, n2, n3, n4, n5位置都填充为1.
这个做法就是Bloom Filter, 也会被翻译为布隆过滤器。BloomFilter是一个概率游戏,判断不在, 则一定不在。 但是判断在, 则可能不在。
当数据存储几个月, 数据量太大导致准去率下降怎么办?
开30个BloomFilter, 每个订单来时对30个块均染色, 每天过期一个块, 新建一个块。
7.5. 找出一篇文章中,出现次数最多的单词
分治法
先做hash,然后求模映射为小文件,求出每个小文件中重复次数最多的一个,并记录重复次数。再与其他文件比较。