网络基础
TCP和UDP有什么区别
TCP/IP中有两个具有代表性的传输层协议
TCP是面向连接;TCP连接只能是点到点、一对一的;通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;TCP传输效率相对较低。
UDP是无连接的;UDP支持一对一,一对多,多对一和多对多的交互通信;UDP尽最大努力交付,即不保证可靠交付;UDP传输效率高。
TCP为什么是三次握手,而不是两次、四次
谢希仁版《计算机网络》中的例子是这样的,“已失效的连接请求报文段” 的产生在这样一种情况下:client 发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达 server。本来这是一个早已失效的报文段。但 server 收到此失效的连接请求报文段后,就误认为是 client 再次发出的一个新的连接请求。于是就向 client 发出确认报文段,同意建立连接。假设不采用 “三次握手”,那么只要 server 发出确认,新的连接就建立了。由于现在 client 并没有发出建立连接的请求,因此不会理睬 server 的确认,也不会向 server 发送数据。但 server 却以为新的运输连接已经建立,并一直等待 client 发来数据。这样,server 的很多资源就白白浪费掉了。采用 “三次握手” 的办法可以防止上述现象发生。例如刚才那种情况,client 不会向 server 的确认发出确认。server 由于收不到确认,就知道 client 并没有要求建立连接。”
如果你细读RFC793,也就是 TCP 的协议 RFC,你就会发现里面就讲到了为什么三次握手是必须的——TCP 需要 seq 序列号来做可靠重传或接收,而避免连接复用时无法分辨出 seq 是延迟或者是旧链接的 seq,因此需要三次握手来约定确定双方的 ISN(初始 seq 序列号)。
http和https的区别
http
http是互联网上应用最广泛的网络通信协议,基于tcp协议,可以使浏览器工作更为高效,减少网络传输。
https
https试试http的加强版,可以认为是http+ssl(secure socket layer)。在http的基础上增加二楼一系列的安全机制。一方面保证了数据传输安全,另一方面对访问者增加了验证机制。是目前现行架构下,最为安全的解决方案。
区别
- http的链接是简单无状态的,https的数据传输是经过证书加密的,安全性更高。
- http是免费的,而https需要申请证书,而一般是需要收费的。
- 两者的传输协议不一样,所以端口也不一样。
优缺点
https
- https的握手协议比较费时,所以会影响服务的响应速度以及吞吐量。
- https也并不是完全安全的。它的证书体系其实也并不是完全安全的。并且面对DDOS攻击时,几乎起不到任何作用。
- 证书需要收费,功能越强大越费钱
Java基础
Java有几种io模型?有什么区别
BIO (同步阻塞IO)
同步阻塞 IO 模型中,应用程序发起 read 调用后,会一直阻塞,直到在内核把数据拷贝到用户空间。
在客户端连接数量不高的情况下,是没问题的。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
NIO(异步阻塞IO)
Java 中的 NIO 可以看作是 I/O 多路复用模型。也有很多人认为,Java 中的 NIO 属于同步非阻塞 IO 模型。
同步非阻塞 IO 模型中,应用程序会一直发起 read 调用,等待数据从内核空间拷贝到用户空间的这段时间里,线程依然是阻塞的,直到在内核把数据拷贝到用户空间。
相比于同步阻塞 IO 模型,同步非阻塞 IO 模型确实有了很大改进。通过轮询操作,避免了一直阻塞。
但是,这种 IO 模型同样存在问题:应用程序不断进行 I/O 系统调用轮询数据是否已经准备好的过程是十分消耗 CPU 资源的。
IO 多路复用模型中,线程首先发起 select 调用,询问内核数据是否准备就绪,等内核把数据准备好了,用户线程再发起 read 调用。read 调用的过程(数据从内核空间->用户空间)还是阻塞的。
IO 多路复用模型,通过减少无效的系统调用,减少了对 CPU 资源的消耗。
Java 中的 NIO ,有一个非常重要的选择器 ( Selector ) 的概念,也可以被称为多路复用器。通过它,只需要一个线程便可以管理多个客户端连接。当客户端数据到了之后,才会为其服务。
AIO(异步非阻塞IO)
Java NIO的核心组件是什么?分别有什么作用
channel类似于stream,每个channel对应一个buffer缓冲区,channel会注册到selector。
selector会根据channel上发生的读写事件,交给某个空闲的线程处理。
selector对应一个或多个线程。
buffer和channel都是可读可写的
channel
Java NIO中的所有I/O操作都基于Channel对象,就像流操作都要基于Stream对象一样。一个Channel(通道)代表和某一实体的连接,这个实体可以是文件、网络套接字等。也就是说,通道是Java NIO提供的一座桥梁,用于我们的程序和操作系统底层I/O服务进行交互。
通道是一种很基本很抽象的描述,和不同的I/O服务交互,执行不同的I/O操作,实现不一样,因此具体的有FileChannel
、SocketChannel
等。
通道使用起来跟Stream比较像,可以读取数据到Buffer中,也可以把Buffer中的数据写入通道。
当然,也有区别,主要体现在如下两点:
- 一个通道,既可以读又可以写,而一个Stream是单向的(所以分 InputStream 和 OutputStream)
- 通道有非阻塞I/O模式
buffer
NIO中所使用的缓冲区不是一个简单的byte数组,而是封装过的Buffer类,通过它提供的API,我们可以灵活的操纵数据,下面细细道来。
与Java基本类型相对应,NIO提供了多种 Buffer 类型,如ByteBuffer、CharBuffer、IntBuffer等,区别就是读写缓冲区时的单位长度不一样(以对应类型的变量为单位进行读写)。
Buffer中有3个很重要的变量,它们是理解Buffer工作机制的关键,分别是
- capacity (总容量)
- position (指针当前位置)
- limit (读/写边界位置)
Buffer的工作方式跟C语言里的字符数组非常的像,类比一下,capacity就是数组的总长度,position就是我们读/写字符的下标变量,limit就是结束符的位置。Buffer初始时3个变量的情况如下图
在对Buffer进行读/写的过程中,position会往后移动,而 limit 就是 position 移动的边界。由此不难想象,在对Buffer进行写入操作时,limit应当设置为capacity的大小,而对Buffer进行读取操作时,limit应当设置为数据的实际结束位置。(注意:将Buffer数据 写入 通道是Buffer 读取 操作,从通道 读取 数据到Buffer是Buffer 写入 操作)
在对Buffer进行读/写操作前,我们可以调用Buffer类提供的一些辅助方法来正确设置 position 和 limit 的值,主要有如下几个
- flip(): 设置 limit 为 position 的值,然后 position 置为0。对Buffer进行读取操作前调用。
- rewind(): 仅仅将 position 置0。一般是在重新读取Buffer数据前调用,比如要读取同一个Buffer的数据写入多个通道时会用到。
- clear(): 回到初始状态,即 limit 等于 capacity,position 置0。重新对Buffer进行写入操作前调用。
- compact(): 将未读取完的数据(position 与 limit 之间的数据)移动到缓冲区开头,并将 position 设置为这段数据末尾的下一个位置。其实就等价于重新向缓冲区中写入了这么一段数据。
selector
Selector(选择器)是一个特殊的组件,用于采集各个通道的状态(或者说事件)。我们先将通道注册到选择器,并设置好关心的事件,然后就可以通过调用select()方法,静静地等待事件发生。
通道有如下4个事件可供我们监听:
- Accept:有可以接受的连接
- Connect:连接成功
- Read:有数据可读
- Write:可以写入数据了
为什么要用Selector
如果用阻塞I/O,需要多线程(浪费内存),如果用非阻塞I/O,需要不断重试(耗费CPU)。Selector的出现解决了这尴尬的问题,非阻塞模式下,通过Selector,我们的线程只为已就绪的通道工作,不用盲目的重试了。比如,当所有通道都没有数据到达时,也就没有Read事件发生,我们的线程会在select()方法处被挂起,从而让出了CPU资源。
select,poll和epoll有什么区别
他们是NIO实现多路复用的三种实现机制,是由Linux操作系统实现的
用户空间和内核空间:为了保护系统安全,将内核划分为两个部分,一个是用户空间,一个是内核空间。用户空间不能直接访问硬件底层,必须通过内核空间。
文件描述符 File Descriptor(FD):一个抽象的概念,形式上是一个整数,实际上是一个索引值,指向内核中为每个进程维护进程所打开的文件的记录表。当程序打开一个文件或者创建一个文件时,内核就会向进程返回一个文件描述符。一般只在Unix,Linux有。
selector机制
会维护一个FD的集合 fd_set。将fd_set从用户空间复制到内核空间,激活socket。本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。数组结构。
问题:
- 单个进程可监视的fd数量被限制,即能监听端口的大小有限。 一般来说这个数目和系统内存关系很大,具体数目可以
cat /proc/sys/fs/file-max
察看。32位机默认是1024个。64位机默认是2048. - 对socket进行扫描时是线性扫描,即采用遍历的方法,效率较低:当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。
- 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大
poll机制
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。
它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:
- 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
- poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
epoll机制
全称叫event poll,只将用户关心的FD事件存放到内核的一个事件表当中。这样可以减少用户空间与内核空间之间的需要拷贝的数据。红黑树
操作方式 | 底层实现 | 最大连接数 | IO效率 | |
select | 遍历 | 数组 | 受限于内核 32位机默认是1024个。64位机默认是2048. | 一般 |
poll | 遍历 | 链表 | 无上限 | 一般 |
epoll | 事件回调 | 红黑树 | 无上限 | 高 |
Java下是使用那种机制?
可以查看sun包下的DefaultSelectorProvider
源码。在window下是WindowsSelectorProvider
。在Linux下,根据内核版本,2.6版本以上使用EpollSelectorProvider
,否则就是PollSelectorProvider
。
List和Set的区别
List
有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出 所有元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素
Set
无序,不可重复,多允许有一个Null元素对象,取元素时只能用Iterator接口取得所有元 素,在逐一遍历各个元素
ArrayList和LinkedList区别
ArrayList
基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚至超过linkedList(需要创建大量的node对象)
LinkedList
基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询:需要逐一遍历遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需要对list重新进行遍历,性能消耗极大。另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexlOf对list进行了遍历,当结果为空时会遍历整个列表。
HashMap和HashTable有什么区别?其底层实现是什么?
区别
- HashMap方法没有synchronized修饰,线程非安全,HashTable线程安全;
- HashMap允许key和value为null,而HashTable不允许
底层实现
数组+链表实现
jdk8开始链表高度到8、数组长度超过64,链表转变为红黑树,元素以内部类Node节点存在计算key的hash值,二次hash然后对数组长度取模,对应到数组下标,如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组,如果产生hash冲突,先进行equal比较,相同则取代该元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表key为null,存在下标0的位置
数组扩容
ConcurrentHashMap原理,jdk7和jdk8版本的区别
jdk7
数据结构
ReentrantLock
+Segment
+HashEntry
,一个Segment
中包含一个HashEntry数组,每个
HashEntry又是一个链表结构
元素查询
二次hash,第一次Hash定位到Segment
,第二次Hash定位到元素所在的链表的头部
锁
Segment
分段锁 Segment
继承了ReentrantLock
,锁定操作的Segment
,其他的Segment
不受影响,并发度为segment
个数,可以通过构造函数指定,数组扩容不会影响其他的segment
。get方法无需加锁,volatile保证
jdk8
数据结构
synchronized+CAS+Node+红黑树,Node的val和next都用volatile修饰,保证可见性
查找,替换,赋值操作都使用CAS
锁
锁链表的head节点,不影响其他元素的读写,锁粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容
读操作无锁
Node的val和next使用volatile修饰,读写线程对该变量互相可见
数组用volatile修饰,保证扩容时被读线程感知
Java JUC
锁升级过程
在了解锁之前需要先了解一下对象头,我们都知道在Java中锁不是某一个具体的实物资源,而是对象上的某个标记,而这个标记就记录在对象头上。
Mark Word(对象头)是Java对象布局中的一个部分
无锁
对象头中有31bit的空间来存储对象的hashcode,4bit用于存放对象分代年龄,1bit来表示是否是偏向锁,2bit存放锁标志位,偏向锁位与锁标志位合起来“001”就代表无锁。无锁就是没有对任何资源进行锁定,所有线程都能访问并修改资源。
偏向锁
对象头中记录了获得偏向锁的线程ID,偏向锁与锁标志位合起来“101”就代表偏向锁。
有研究发现,在大多数情况下,锁很少被多个线程同时竞争,而且总是由同一个线程多次获得,因此只需要将获得锁的线程ID写入到锁对象Mark Word中,相当于告诉其他线程,这块资源已经被我占了。当线程访问资源结束后,不会主动释放偏向锁,当线程再次需要访问资源时,JVM就会通过Mark Word中记录的线程ID判断是否是当前线程:
- 如果是,则继续访问资源。所以,在没有其他线程参与竞争时,锁就一直偏向被当前线程持有,当前线程就可以一直占用资源或者执行代码。
- 如果不是,检查该线程是否存在(偏向锁不会主动释放锁)
- 如果不在,则设置线程ID为线程A的ID,此时依然是偏向锁。(偏向锁的批量再偏向(Bulk Rebias)机制)
- 引入一个概念 epoch, 其本质是一个时间戳 , 代表了偏向锁的有效性
- 从前文描述的对象头结构中可以看到, epoch 存储在可偏向对象的 MarkWord 中。
- 除了对象中的 epoch, 对象所属的类 class 信息中, 也会保存一个 epoch 值
- 每当遇到一个全局安全点时, 如果要对 class C 进行批量再偏向, 则首先对 class C 中保存的 epoch 进行增加操作, 得到一个新的 epoch_new
- 然后扫描所有持有 class C 实例的线程栈, 根据线程栈的信息判断出该线程是否锁定了该对象, 仅将 epoch_new 的值赋给被锁定的对象中。
- 退出安全点后, 当有线程需要尝试获取偏向锁时, 直接检查 class C 中存储的 epoch 值是否与目标对象中存储的 epoch 值相等, 如果不相等, 则说明该对象的偏向锁已经无效了, 可以尝试对此对象重新进行偏向操作。
- 如果再,则暂停线程A,同时将锁标志位设置为00即轻量级锁(将MarkWord复制到线程A的栈帧中并将MarkWord设置为栈帧中锁记录)。线程A自旋等待锁释放。
轻量级锁
轻量级锁考虑的是竞争锁对象的线程不多,而且线程持有锁的时间也不长的情景。因为阻塞线程需要CPU从用户态转到内核态,代价较大,如果刚刚阻塞不久这个锁就被释放了,那这个代价就有点得不偿失了,因此这个时候就干脆不阻塞这个线程,让它自旋这等待锁释放。
自旋锁
一旦有另外一个线程参与锁竞争,偏向锁就会升级为自旋锁。
此时撤销偏向锁,锁标志位变为“00”。竞争的两个线程都在各自的线程栈帧中生成一个Lock Record
空间,用于存储锁对象目前Mark Word的拷贝,用CAS操作将Mark Word设置为指向自己这个线程的LR(Lock Record)指针,设置成功者获得锁,其他参与竞争的线程如果未获取到锁,则会一直处于自旋等待的状态,直到竞争到锁。
重量级锁
长时间的自旋操作是很消耗CPU资源的,为了避免这种盲目的消耗,JVM会在有线程超过10次自旋,或者自旋次数超过CPU核数的一半(JDK1.6以后加入了自适应自旋-Adaptive Self Spinning,由JVM自己控制自旋次数)时,会升级到重量级锁。重量级锁底层是依赖操作系统的mutex互斥锁,也就是有操作系统来负责线程间的调度。重量级锁减少了自旋锁带来的CPU消耗,但是由于操作系统调度线程带来的线程阻塞会使程序响应速度变慢。
synchronized三种使用方式
普通同步方法
锁的是当前实例对象
静态同步方法
锁的是当前类的class对象
同步方法块(同步代码块)
锁的是括号里面的对象
- 如果括号里面是xxx.class,则锁的是class对象
- 如果括号里面是this,锁的是实例对象
AQS怎么实现锁
AQS即抽象同步队列器,由两大部分构成,标识符、FIFO队列。
标识符(state)
锁标识符,也叫同步器状态符,在AQS类源码中可以看到是一个int类型的成员变量,标识锁当前状态,ReentrantLock
用其标识锁是否被占用、Semaphore
用其标识剩余令牌数。
state配合CAS使用,并由volatile修饰,保证操作原子性。
FIFO队列
实质上是一个双向链表,节点Node类是AQS类中的内部类。
AQS中记录了队列的头部及尾部。
FIFO队列的作用是当锁被上锁后,新的请求线程请求到锁后加入到FIFO队列中进行等待并尝试获取锁对象。要注意的是前面的逻辑是AQS的实现类的大致逻辑,AQS只是提供了一个模板。根据实现方式不同,可以实现不同类型不同功能的锁。如公平锁、非公平锁,又或者独占锁、共享锁。
// Node 部分代码,在AQS内部中定义
static final class Node{
static final Node SHARED = new Node(); // 表示持有共享锁
static final Node EXCLUSIVE = null; // 持有独占锁
// waiter status,即节点所处的阻塞状态列表如下
static final int CANCELLED = 1; // 被取消,意味着放弃竞争锁资源,移出阻塞队列
static final int SIGNAL = -1; // 持有锁状态
static final int CONDITION = -2; // 线程处于条件等待队列中,也就是condition.await让线程挂起
static final int PROPAGATE = -3; // 释放锁并正在处于通知其他等待节点可以竞争锁资源的状态
volatile int waitStatus; // 当前节点的状态,初始化为0,不属于上述任何一种状态,属于非阻塞可竞争获取锁的状态
// 实现双端链表
volatile Node prev;
volatile Node next;
// 当前节点的线程
volatile Thread thread;
// 标志当前节点是共享锁还是独占锁,用节点指针引用指向对应的mode
Node nextWaiter;
// 独占锁:nextWaiter = null & thread = Thread.currentThread;
// 共享锁:nextWaiter = SHARED & thread = Thread.currentThread;
// 不具备上述条件属于正常对象,不持有锁状态
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
// 判断是否为共享锁的方法
boolean isShared(){
// ...
}
// 获取上一个节点Node
Node predecessor(){
// ...
}
}
AQS怎么实现可重入锁
state标识加锁的次数,0表示无锁,每加一次锁,state就加一,释放锁就减一。state是volitile的,保证线程可见性,加减操作的时候通过cas保证安全。
实现AQS只需要继承AQS类,实现父类接口方法,不用处理入队出队的操作,只需要维护state的值就可以。
什么是CAS
- CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。它是一条CPU并发原语。
- CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。
更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。这个过程是原子的。 - 原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致性的问题
CAS的缺点
CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized
了。
ABA问题
A线程在操作原子类的时候,需要把1改成2,在这个过程中如果线程B先一步把1改成了2,再很快的改回了1,这时候A是无法知道这个数字是否被修改过。
怎么解决CAS的ABA问题
对于POJO
使用AtomicReference包装这个POJO,使其操作原子化
本质是比较的是两个对象的地址是否相等。
User user1 = new User("Jack",25);
User user2 = new User("Luck",21);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user1);
System.out.println(atomicReference.compareAndSet(user1,user2)); // true
System.out.println(atomicReference.compareAndSet(user1,user2)); // false
ABA问题:版本号
使用AtomicStampedReference
类可以解决ABA问题。这个类维护了一个“版本号”Stamp,其实有点类似乐观锁的意思。
AtomicStampedReference.compareAndSet(expectedReference,newReference,oldStamp.newStamp);
A、B、C三个线程,如何保证三个线程同时执行?并发情况下保证三个线程依次执行?有序交错执行?
CountDownLatch(计数器)
使用计数器,采用倒计时的方式,将准备好的线程等待计时器倒计时到0,再一起执行
public static void main(String[] args) {
int i = 30;
CountDownLatch c = new CountDownLatch(1);
for (int j = 0; j < i; j++) {
new Thread(() -> {
try {
c.await();
log.info("{}|{}", Thread.currentThread().getName(), System.currentTimeMillis());
} catch (InterruptedException e) {
e.printStackTrace();
}
}, String.valueOf(j)).start();
}
try {
Thread.sleep(5000);
c.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
CylicBarrier(栅栏)
Semaphore(信号量)
如何对一个字符串快速进行排序
用
ForkJoinTask
拆分任务,再进行汇总
@Slf4j
public class Test {
private static int max = 100;
private static int ints[] = new int[max];
static {
Random r = new Random();
for (int i = 0; i < max; i++) {
ints[i] = r.nextInt(10000);
}
}
static class MyTask extends RecursiveTask<int[]> {
private int source[];
public MyTask(int source[]) {
this.source = source;
}
@Override
protected int[] compute() {
int sourceLen = source.length;
if (sourceLen > 2) {
int minIndex = sourceLen / 2;
MyTask task1 = new MyTask(Arrays.copyOf(source, minIndex));
task1.fork();
MyTask task2 = new MyTask(Arrays.copyOfRange(source, minIndex, sourceLen));
task2.fork();
int result1[] = task1.join();
int result2[] = task2.join();
int mer[] = joinInts(result1, result2);
return mer;
} else {
if (sourceLen == 1 || source[0] <= source[1]) {
return source;
} else {
int targetp[] = new int[sourceLen];
targetp[0] = source[1];
targetp[1] = source[0];
return targetp;
}
}
}
}
private static int[] joinInts(int[] a, int[] b) {
int l = a.length + b.length;
int[] temp = new int[l];
int i = 0, j = 0, h = 0;
// 这里必须用while,不能用if
while (i < a.length || j < b.length) {
if (i == a.length && j < b.length) {
temp[h++] = b[j++];
} else if (i < a.length && j == b.length) {
temp[h++] = a[i++];
} else if (a[i] <= b[j]) {
temp[h++] = a[i++];
} else if (a[i] > b[j]) {
temp[h++] = b[j++];
}
}
return temp;
}
public static void main(String[] args) {
long b = System.currentTimeMillis();
ForkJoinPool forkJoinPool = new ForkJoinPool();
MyTask task = new MyTask(ints);
ForkJoinTask<int[]> taskResult = forkJoinPool.submit(task);
try {
int[] ints = taskResult.get();
log.info(Arrays.toString(ints));
} catch (Exception e) {
e.printStackTrace();
}
long e = System.currentTimeMillis();
log.info("耗时:{}", e - b);
}
}
读写锁怎么实现
与传统锁不同的是读写锁的规则是可以共享读,但只能一个写,总结起来为:读读不互斥,读写互斥,写写互斥
,而一般的独占锁是:读读互斥,读写互斥,写写互斥
,而场景中往往读远远大于写,读写锁就是为了这种优化而创建出来的一种机制。注意是读远远大于写
,一般情况下独占锁的效率低来源于高并发下对临界区的激烈竞争导致线程上下文切换。因此当并发不是很高的情况下,读写锁由于需要额外维护读锁的状态,可能还不如独占锁的效率高。因此需要根据实际情况选择使用。
Java JVM
说一说JVM的内存模型
新生代:老年代=1:2
新生代中 eden : s1 : s2 = 8 : 1 : 1
memory (内存信息) | used | total | max | usage | GC | |
heap (堆) | 75M | 98M | 442M | 17.01% | gc.copy.count (垃圾回收次数) | 452 |
eden_space (伊甸园) | 12M | 27M | 122M | 10.37% | gc.ps_scavenge.time(ms) (垃圾回收消耗时间) | 5008 |
ps_survivor_space (幸存者区) | 3M | 3M | 15M | 22.13% | gc.ps_marksweep.count (标记-清除算法的次数) | 4 |
ps_old_gen (老年代) | 72M | 1365M | 1365M | 19.41% | gc.ps_marksweep.time(ms) (标记-清除算法的消耗时) | 1055 |
nonheap (非堆区) | 99M | 137M | -1 | 72.51% | ||
code_cache (代码缓存区) | 5M | 38M | 24M | 2.13% | ||
metaspace (元空间) | 84M | 87M | -1 | 95.91% |
Java类加载的全过程是怎么样的?
Java的类加载器:AppClassLoader -> ExtClassLoader -> Bootstrap ClassLoader(c++)
每种类加载器都有各自的加载类的目录。
每个类加载器都是有一个缓存,缓存了对应加载过的类
类加载的过程:加载 -> 连接 -> 初始化
加载
把Java的字节码数据加载到JVM内存当中,并映射成JVM认可的数据结构
连接
- 验证:检查加载到的字节码是否符合JVM安全规范,没有安全方面的问题。
- 准备:正式为类变量(static变量)分配内存并设置类变量初始值的阶段,这些内存将在方法区中进行分配(半初始化)。
- 解析:虚拟机常量池的符号引用替换为直接应用。
初始化
初始化阶段是执行类构造器()方法过程,类构造器()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块(static块)中语句合成的过程。
当初始化一个类的时候,如果发现其父类没有被初始化,则先初始化其父类。
虚拟机会保证一个类的()方法在多线程环境中被正确的加锁和同步。
当访问一个Java类的静态域时,只有真正声明这个域的类才会被初始化。
public class classLoader {
static {
System.out.println("静态初始化classLoader");
}
public static void main(String[] args)
{
System.out.println("main方法加载");
A a = new A();
System.out.println(A.width);
A a2 = new A(); //这里代表类加载初始化只有一次,然后调用的是对象的方法。
}
}
class A extends B{
public static int width=100;
static {
System.out.println("静态初始化A"); //这里静态方法静态变量会合并到一起
width = 300; //也就是width=300;
}
public A()
{
System.out.println("创建A的对象");
}
}
class B {
static {
System.out.println("静态初始化B");
}
}
什么是双亲委派机制?有什么作用?
双亲委派机制:向上委托查找,向下委托加载。
作用
- 防止重复加载同一个
.class
。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。 - 保证核心
.class
不能被篡改。通过委托方式,不会去篡改核心.class
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个Class
对象。这样保证了Class
执行安全。
一个对象从加载到JVM,再被GC的过程中,都经历了些什么过程?
- 用户创建对象,需要去方法区找对象的类型信息。
- JVM实例化对象,首先要在堆当中先创建一个对象。-> 半初始化
- 对象首先会分配在新生代的eden区,经过Minor GC,对象如果存活,就会进入S区。在后续每次GC中,如果一直存活,则在S0、S1来回复制转移(复制算法),年代+1(年龄最大15,因为对象头是4个bit记录年龄最大1111),年龄到达15之后会进入老年代,不再进行Minor GC。
- 当方法执行结束时,栈中的指针先移除。
- 堆中的对象,经过Full GC,就会被标记为垃圾,然后被GC线程清除。
GC如何判断对象可以被回收,什么是GC ROOT?
一、引用计数算法
当对象引用了则这个对象加计数加1.再次引用了则计数变为2,如果不再引用了则计数减1,知道这个计数变为0,则对对象回收。
问题:当两个对象相互引用时,虽然他们不会再被引用了,但他们的计数不能归为0,所以无法垃圾回收(如下图)。早期jdk使用方式
二、可达性分析算法
此算法的核心是:通过一系列的GC ROOT 作为起点,沿着引用链向下寻找,如果找不到,则表示该对象可以会被回收。那么什么对象可以 作为Root 起点呢?可以通过jmap命令结合 Eclipse Memory Analyzer(MAT)工具去查找他的根节点。
有四类可以作为根对象(GC ROOT)
- 虚拟机栈中引用的对象(栈帧中的本地变量表)
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(Native方法)引用的对象
- 可达性算法中的不可达对象并不是立即死亡的,对象拥有一次自我拯救的机会。对象被系统宣告死亡至
少要经历两次标记过程:第一次是经过可达性分析发现没有与GC Roots相连接的引用链,第二次是在由
虚拟机自动建立的Finalizer队列中判断是否需要执行finalize()方法。- 当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回
收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象
的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否
则,对象“复活”- 每个对象只能触发一次finalize()方法
- 由于finalize()方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,不推荐大家使用,建议遗忘它。
JVM有什么垃圾回收算法
markSweep 标记清除法
执行步骤:
- 标记:遍历内存区域,对需要回收的对象打上标记。
- 清除:再次遍历内存,对已经标记过的内存进行回收。
缺点:
- 效率问题;遍历了两次内存空间(第一次标记,第二次清除)。
- 空间问题:容易产生大量内存碎片,当再需要一块比较大的内存时,无法找到一块满足要求的,因而不得不再次出发GC。
copying 拷贝算法
执行步骤:
将内存划分为等大的两块,每次只使用其中的一块。当一块用完了,触发GC时,将该块中存活的对象复制到另一块区域,然后一次性清理掉这块没有用的内存。下次触发GC时将那块中存活的的又复制到这块,然后抹掉那块,循环往复。
优点:
- 相对于标记–清理算法解决了内存的碎片化问题。
- 效率更高(清理内存时,记住首尾地址,一次性抹掉)。
缺点:
- 内存利用率不高,每次只能使用一半内存。
改进:
研究表明,新生代中的对象大都是“朝生夕死”的,即生命周期非常短而且对象活得越久则越难被回收。在发生GC时,需要回收的对象特别多,存活的特别少,因此需要搬移到另一块内存的对象非常少,所以不需要1:1划分内存空间。而是将整个新生代按照8 : 1 : 1的比例划分为三块,最大的称为Eden(伊甸园)区,较小的两块分别称为To Survivor和From Survivor。首次GC时,只需要将Eden存活的对象复制到To。然后将Eden区整体回收。再次GC时,将Eden和To存活的复制到From,循环往复这个过程。这样每次新生代中可用的内存就占整个新生代的90%,大大提高了内存利用率。
但不能保证每次存活的对象就永远少于新生代整体的10%,此时复制过去是存不下的,因此这里会用到另一块内存,称为老年代,进行分配担保,将对象存储到老年代。若还不够,就会抛出OOM。
老年代:存放新生代中经过多次回收仍然存活的对象(默认15次)。
markCompack 标记压缩法
执行步骤:
- 标记:对需要回收的进行标记
- 整理:让存活的对象,向内存的一端移动,然后直接清理掉没有用的内存。
JVM有哪些垃圾回收器?什么是STW?
STW
Stop-The-World,是在垃圾回收算法执行过程当中,需要将JVM内存冻结的一种状态。在STW状态下Java的所有线程都是停止执行的(GC线程除外),native方法可以执行,但不能与JVM进行交互。GC各种算法优化的重点,就是减少STW情况,同时也是JVM调优的重点。
分代算法
一:Serial 收集器
Serial收集器是最基本的、发展历史最悠久的收集器。
**特点:**单线程、简单高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)。
应用场景:适用于Client模式下的虚拟机。
二:ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本。
除了使用多线程外其余行为均和Serial收集器一模一样(参数控制、收集算法、Stop The World、对象分配规则、回收策略等)。
特点:多线程、ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的环境中,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。和Serial收集器一样存在Stop The World问题
应用场景:ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,因为它是除了Serial收集器外,唯一一个能与CMS收集器配合工作的。
三:Parallel Scavenge 收集器
与吞吐量关系密切,故也称为吞吐量优先收集器。
特点:属于新生代收集器也是采用复制算法的收集器,又是并行的多线程收集器(与ParNew收集器类似)。
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与ParNew收集器最重要的一个区别)
GC自适应调节策略:Parallel Scavenge收集器可设置-XX:+UseAdptiveSizePolicy参数。当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRation)、晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为GC的自适应调节策略。
Parallel Scavenge收集器使用两个参数控制吞吐量:
- XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
- XX:GCRatio 直接设置吞吐量的大小。
四:Serial Old 收集器
Serial Old是Serial收集器的老年代版本。
特点:同样是单线程收集器,采用标记-整理算法。
应用场景:主要也是使用在Client模式下的虚拟机中。也可在Server模式下使用。
Server模式下主要的两大用途:
- 在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用。
- 作为CMS收集器的后备方案,在并发收集Concurent Mode Failure时使用。
五:Parallel Old 收集器
是Parallel Scavenge收集器的老年代版本。
特点:多线程,采用标记-整理算法。
应用场景:注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器。
六:CMS收集器
concurrent-mark-sweep,一种以获取最短回收停顿时间为目标的收集器。
特点:基于标记-清除算法实现。并发收集、低停顿。
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
CMS收集器的运行过程分为下列4步:
- 初始标记(STW):标记GC Roots能直接到的对象。速度很快但是仍存在Stop The World问题。
- 并发标记:进行GC Roots Tracing 的过程,找出存活对象,且用户线程可并发执行。
- 重新标记(STW):为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World问题。
- 并发清除:对标记的对象进行清除回收。(清除过程中用户又会不断产生新的垃圾,叫浮动垃圾,留到下一个GC清楚)
CMS收集器的内存回收过程是与用户线程一起并发执行的。
总结
回收器名称 | 算法分类 | 作用区域 | 是否多线程 | 类型 | 备注 |
Serial | 复制算法 | 新生代 | 单线程 | 串行 | 简单高效、不建议使用 client默认 |
ParNew | 复制算法 | 新生代 | 多线程 | 并行 | 唯一和CMS搭配使用的新生代垃圾回收器 |
Parallel Scavenge | 复制算法 | 新生代 | 多线程 | 并行 | 更关注吞吐量 |
Serial Old | 标记-整理 | 老年代 | 单线程 | 串行 | 能和所有的young gc搭配使用 |
Parallel Old | 标记-整理 | 老年代 | 多线程 | 并行 | 搭配Parallel Scavenger使用 |
CMS | 标记-清除 | 老年代 | 多线程 | 并发 | 追求最短的STW时间 |
不分代算法
七: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停顿时间来制定回收计划。(可并发执行)
什么是三色标记?如何解决三色标记错标和漏标的问题?
三色标记
三色标记是在cms和g1中使用的垃圾追踪算法
- 黑色
从GCRoots开始,已扫描过它全部引用的对象,标记为黑色 - 灰色
扫描过对象本身,还没完全扫描过它全部引用的对象,标记为灰色 - 白色
还没扫描过的对象,标记为白色
所以,从GCRoots开始,顺着一直向下扫描,用可达性分析算法,最后所有的白色对象,都是垃圾对象,可以回收
漏标问题
- 有至少一个黑色对象在自己被标记之后指向了这个白色对象
- 所有的灰色对象在自己引用扫描完成之前删除了对白色对象的引用
我们采用一个最简单的模型,只有三个对象
- 某个状态下,黑色->灰色->白色
- 如果一切顺利,不发生任何引用变化,gc线程顺着灰色的引用向下扫描,最后都变成黑色,都是存活对象
- 但是如果出现了这样一个状况,在扫描到灰色的时候,还没有扫描到这个白色对象,此时,黑色对象引用了这个白色对象,而灰色对象指向了别人,或者干脆指向了null,也就是取消了对白色对象的引用
- 那么我们会发现一个问题,根据三色标记规则,gc会认为,黑色对象是本身已经被扫描过,并且它所有指向的引用都已经被扫描过,所以不会再去扫描它有哪些引用指向了哪些对象。
然后,灰色对象因为取消了对白色对象的引用,所以后面gc开始扫描所有灰色对象的引用时候,也不会再扫描到白色对象。
最后结果就是,白色对象直到本次标记扫描结束,也是白色,根据三色标记规则,认为它是垃圾,被清理掉。
但是实际情况,它明显是被引用的对象,是绝对不能当做垃圾来清除的,因为漏标,最后被当垃圾清理掉了。
漏标问题处理
CMS:增量更新
增量更新破坏的是第一个条件,我们在这个黑色对象增加了对白色对象的引用之后,将它的这个引用,记录下来,在最后标记的时候,再以这个黑色对象为根,对它的引用进行重新扫描。
可以简单理解为,当一个黑色对象增加了对白色对象的引用,那么这个黑色对象就被变灰
这样有一个缺点,就是会重新扫描这个黑色对象的所有引用,比较浪费时间
G1:原始快照
原始快照破坏的是第二个条件,我们在这个灰色对象取消对白色对象的引用之前,将这个引用记录下来,在最后标记的时候,再以这个引用指向的白色对象为根,对它的引用进行扫描
可以简单理解为,当一个灰色对象取消了对白色对象的引用,那么这个白色对象被变灰
这样做的缺点就是,这个白色对象有可能并没有黑色对象去引用它,但是它还是被变灰了,就会导致它和它的引用,本来应该被垃圾回收掉,但是此次GC存活了下来,就是所谓的浮动垃圾.其实这样是比较可以忍受的,只是让它多存活了一次GC而已,浪费一点点空间,但是会比增量更新更省时间。
为什么要设计这么多垃圾回收器?
因为内存在不断变大,老垃圾回收器对CPU利用率不足,导致STW过长。
如何进行JVM调优
JVM调优主要是定制JVM运行参数来提高JAVA应用程序的运行速度
标准指令
-开头,这些是所有JDK版本都支持的参数,可用java -help打印查看
非标准指令
-X开头,通常是跟特定的JDK版本对应的,可以用java -X打印出来
不稳定参数
-XX开头,跟特定JDK版本对应,并且变化非常大。文档少。
指令 | 意义 |
java -XX:+PrintCommandLineFlags | 查看当前命令的不稳定指令 |
java -XX:+PrintFlagsInitial | 查看所有不稳定指令的默认值 |
java -XX:+PrintFlagsFinal | 查看所有不稳定指令最终生效的值 |
监控JVM工具
visual jvm
开启监听端口,用jdk bin下自带工具可以连接
arthas
启动arthas-boot.jar
dashboard
命令看到堆内存区和线程状态等信息
thread
可以查看线程情况
thread -b
可以查看死锁情况
Java 线程
ThreadLocal的原理和使用场景
每一个 Thread 对象均含有一个 ThreadLocalMap 类型的成员变量 threadLocals ,它存储本线程中所有ThreadLocal对象及其对应的值
ThreadLocalMap 由一个个 Entry 对象构成
Entry 继承自 WeakReference<ThreadLocal<?>>
,一个 Entry 由 ThreadLocal 对象和 Object 构成。由此可见, Entry 的key是ThreadLocal对象,并且是一个弱引用。
当没指向key的强引用后,该key就会被垃圾收集器回收当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。
get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。
由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。
使用场景:
- 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
- 线程间数据隔离
- 进行事务操作,用于存储线程事务信息。
- 数据库连接,Session会话管理。
Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的 connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离
ThreadLocal内存泄露原因,如何避免
强引用
使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的connection来执行数据库操作,实现了事务的隔离性。Spring框架里面就是用的ThreadLocal来实现这种隔离如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用
JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference
类来表示。可以在缓存中使用弱引用。
ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本
hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链(红色链条)
key 使用强引用
当hreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key 使用弱引用
当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal正确的使用方法
- 每次使用完ThreadLocal都调用它的remove()方法清除数据
- 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
sleep和wait的区别
类的不同
sleep来自thread,wait来自object
释放锁
sleep不释放锁,wait释放锁**,并把这个wait的线程加入到这个锁的等待队列中去**
用法不同
sleep时间到了会自动恢复
wait需要手动notify/notifyAll
为什么用线程池?
- 降低资源消耗;提高线程利用率,降低创建和销毁线程的消耗。
- 提高响应速度;任务来了,直接有线程可用可执行,而不是先创建线程,再执行。
- 提高线程的可管理性;线程是稀缺资源,使用线程池可以统一分配调优监控。
线程的启动用start还是run
start
start方法
当用start()开始一个线程后,线程就进入就绪状态,使线程所代表的虚拟处理机处于可运行状态,这意味着它可以由JVM调度并执行。这并不意味着线程就会立即运行。当cpu分配给它时间时,才开始执行run()方法(如果有的话)。start()是方法,它调用run()方法。而run()方法是你必须重写的。 run()方法中包含的是线程的主体。
run方法
run方法执行是在方法层面上执行的,依赖在当前线程,如果不是在多线程情况下执行run,则是main线程。
线程实现方式有几种
继承thread类
实现runnable接口
实现callable接口
通过FutureTask包装器来创建Thread线程
通过线程池创建线程
使用线程池接口ExecutorService结合Callable、future实现又返回结果的多线程
线程有几种状态
初始(NEW)
实现Runnable接口和继承Thread可以得到一个线程类,new一个实例出来,线程就进入了初始状态。但还没有调用start()方法。
运行(RUNNABLE)
Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。
- 线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。
- 就绪状态的线程在获得CPU时间片后变为运行中状态(running)。
阻塞(BLOCKED)
表示线程阻塞于锁,线程阻塞在进入synchronized关键字修饰的方法或代码块(获取锁)时的状态。
等待(WAITING)
进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。
处于这种状态的线程不会被分配CPU执行时间,它们要等待被显式地唤醒,否则会处于无限期等待的状态
超时等待(TIMED_WAITING)
该状态不同于WAITING,它可以在指定的时间后自行自动唤醒。
终止(TERMINATED)
- 当线程的run()方法完成时,或者主线程的main()方法完成时,我们就认为它终止了。这个线程对象也许是活的,但是它已经不是一个单独执行的线程。线程一旦终止了,就不能复生。
- 在一个终止的线程上调用start()方法,会抛出java.lang.IllegalThreadStateException异常。
线程池有几个构造参数
7个
1. corePoolSize:核心线程数
如果等于0,则任务执行完后,没有任务请求进入时销毁线程池中的线程。如果大于0,即使本地任务执行完毕,核心线程也不会被销毁。设置过大会浪费系统资源,设置过小导致线程频繁创建。
2. maximumPoo1size: 最大线程数
必须大于等于1,且大于等于corePoolSize.如果与corePoolSize相等,则线程池大小固定。如果大于corePoolSize, 则最多创建maximumPoolSize个线程执行任务
3. keepAliveTime :线程空闲时间
线程池中线程空闲时间达到keepAliveTime值时, 线程会被销毁,只到剩下corePoolSize个线程为止。默认情况下,线程池的最大线程数大于corePoolSize时,keepAliveTime才会起作用。如果allowCoreThreadTimeOut被设置为true,即使线程池的最大线程数等于corePoolSize,keepAliveTime也会起作用(回收超时的核心线程)。
4. unit : TimeUnit表示时间单位。
5. workQueue: 缓存队列
当请求线程数大于corePoolSize时, 线程进入BlockingQueue阻塞队列。
6. threadFactory: 线程工厂
用来生产一组相同任务的线程。主要用于设置生成的线程名词前缀、是否为守护线程以及优先级等。设置有意义的名称前缀有利于在进行虚拟机分析时,知道线程是由哪个线程工厂创建的。
7. handler: 执行拒绝策略对象
当达到任务缓存上限时(即超过workQueue参数能存储的任务数),执行拒接策略,可以看作简单的限流保护。
线程池的拒绝策略有几种
4种
AbortPolicy
默认,丢弃任务并抛出RejectedExecutionException异常。
DiscardPolicy
丢弃任务,但是不抛出异常(不推荐)。
DiscardOldestPolicy
抛弃队列中等待最久的任务,然后把当前任务加入队列中。
CallerRunsPolicy
调用任务的run()方法绕过线程池直接执行。
友好的拒绝策略:
- 保存到数据库进行削峰填谷,在空闲时间再提出来执行。
- 转向某个提示页面
- 打印日志
可以用Executors
工具类创建线程池吗
不可以/不建议
Executors.newCachedThreadPool
和Executors.newScheduledThreadPool
两个方法最大线程数为Integer.MAX_ _VALUE
,如果达到上限,没有任务服务器可以继续工作,肯定会抛出00M异常。
Executors.newSingleThreadExecutor
和Executors.newFixedThreadPool
两个方法的workQueue
参数为new LinkedBlockingQueue <Runnable>
(容量为Integer.MAX _VALUE
,如果瞬间请求非常大,会有O0M风险。
简述线程池处理流程
线程池中阻塞队列的作用?为什么是先添加列队而不是先创建最大线程?
- 一般的队列只能保证作为一个有限长度的缓冲区,如果超出了缓冲长度,就无法保留当前的任务了,阻塞队列通过阻塞可以保留住当前想要继续入队的任务。阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。阻塞队列自带阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活、不至于一直占用cpu资源
- 在创建新线程的时候,是要获取全局锁的,这个时候其它的就得阻塞,影响了整体效率。就好比一个企业里面有10个(core)正式工的名额,最多招10个正式工,要是任务超过正式工人数(task > core)的情况下,工厂领导(线程池)不是首先扩招工人,还是这10人,但是任务可以稍微积压一下,即先放到队列去(代价低)。10个正式工慢慢干,迟早会干完的,要是任务还在继续增加,超过正式工的加班忍耐极限了(队列满了),就的招外包帮忙了(注意是临时工)要是正式工加上外包还是不能完成任务,那新来的任务就会被领导拒绝了(线程池的拒绝策略)。
线程池中线程复用原理
线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。
在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式只使用固定的线程就将所有任务的 run 方法串联起来。
Spring
Spring中@Autowired注解与@Resource注解的区别
相同点
@Resource的作用相当于@Autowired,均可标注在字段或属性的setter方法上。
不同点
提供方
@Autowired
是由org.springframework.beans.factory.annotation.Autowired
提供, 换句话说就是由Spring提供; @Resource
是由javax.annotation.Resource
提供,即J2EE提供,需要JDK1.6及以 上。
注入方式
@Autowired
只按照byType 注入; @Resource
默认按byName自动注入,也提供按照byType注
入;
属性
@Autowired
按类型装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值, 可以设
置它required属性为false。如果我们想使用按名称装配,可以结合@Qualifier
注解一 起使用。@Resource
有两个
中重要的属性: name和type。name属性指定byName, 如果没有指定name属性,当注解标注在字段上,即默认
取字段的名称作为bean名称寻找依赖对象,当注解标注在属性的setter方法上,即默认取属性名作为bean名称寻
找依赖对象。需要注意的是,@Resource
如果没有指定name属性, 并且按照默认的名称仍然找不到依赖对象时,@Resource
注解会回退到按类型装配。但一旦指定了name属性,就只能按名称装配了。
Spring 如何解决循环依赖?
在Spring中,对象的实例化是通过反射实现的,而对象的属性则是在对象实例化之后通过一定的方式设置的。
这个过程可以按照如下方式进行理解:
Spring IoC和DI分别是什么东西
Ioc:Inversion of Control —— 控制反转
控制反转(loC)的定义
控制反转即IoC (Inversion of Control),它把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。所谓的“控制反转"概念就是对组件对象控制权的转移,从程序代码本身转移到了外部容器。
Spring I0C负责创建对象,管理对象(通过依赖注入(DI) ,装配对象,配置对象,并且管理这些对象的整个生
命周期。
控制反转(loC)有什么作用
- 管理对象的创建和依赖关系的维护。对象的创建并不是一件简单的事,在对象关系比较复杂时,如果依赖关系
需要程序猿来维护的话,那是相当头疼的 - 解耦,由容器去维护具体的对象
- 托管了类的产生过程,比如我们需要在类的产生过程中做一 些处理,最直接的例子就是代理,如果有容器程序
可以把这部分处理交给容器,应用程序则无需去关心类是如何完成代理的
控制反转(loC)的优点是什?
- loC或依赖注入把应用的代码量降到最低。
- 它使应用容易测试,单元测试不再需要单例和NDI查找机制。
- 最小的代价和最小的侵入性使松散耦合得以实现。
- loC容器支持加载服务时的饿汉式初始化和懒加载。
DI:Dependency Injection —— 依赖注入
什么是Spring的依赖注入?
控制反转loC是一个很大的概念,可以用不同的方式来实现。其主要实现方式有两种:依赖注入和依赖查找
依赖注入:相对于loC而言,依赖注入(DI)更加准确地描述了loC的设计理念。所谓依赖注入(Dependency Injection),即组件之间的依赖关系由容器在应用系统运行期来决定,也就是由容器动态地将某种依赖关系的目标对象实例注入到应用系统中的各个关联的组件之:中。组件不做定位查询,只提供普通的Java方法让容器去决定依赖关系。
依赖注入的基本原则
依赖注入的基本原则是:应用组件不应该负责查找资源或者其他依赖的协作对象。配置对象的工作应该由IoC容器负责,“查找资源"的逻辑应该从应用组件的代码中抽取出来,交给IoC容器负责。容器全权负责组件的装配,它会把符合依赖关系的对象通过属性(JavaBean中的setter) 或者是构造器传递给需要的对象。
依赖注入有什么优势
依赖注入之所以更流行是因为它是一种更可取的方式:让容器全权负责依赖查询,受管组件只需要暴露JavaBean的setter方法或者带参数的构造器或者接口,使容器可以在初始化时组装对象的依赖关系。其与依赖查找方式相
比,主要优势为:
- 查找定位操作与应用代码完全无关。
- 不依赖于容器的API,可以很容易地在任何容器以外使用应用对象。
- 不需要特殊的接口,绝大多数对象可以做到完全不必依赖容器。
什么是Spring AOP
当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。
日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。
在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP:将程序中的交叉业务逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象
(具体业务逻辑)中去。AOP可以对某个对象或某些对象的功能进行增强,比如对象中的方法进行增
强,可以在执行某个方法之前额外的做一些事情,在某个方法执行之后额外的做一些事情
Spring AOP你是怎么使用到的
- 日志打印
- 全局异常处理拦截
- 返回值统一处理
- 多数据源切换
- Authentication 权限
- Caching 缓存
- Context passing 内容传递
- Error handling 错误处理
- Lazy loading 懒加载
- Debugging 调试
- logging, tracing, profiling and monitoring 记录跟踪 优化 校准
- Performance optimization 性能优化
- Persistence 持久化
- Resource pooling 资源池
- Synchronization 同步
- Transactions 事务
AOP实现原理
Spring
的AOP
实现原理其实很简单,就是通过动态代理实现的。如果我们为Spring
的某个bean
配置了切面,那么Spring
在创建这个bean
的时候,实际上创建的是这个bean
的一个代理对象,我们后续对bean
中方法的调用,实际上调用的是代理类重写的代理方法。而Spring
的AOP
使用了两种动态代理,分别是JDK的动态代理,以及CGLib的动态代理。
(一)JDK动态代理
Spring默认使用JDK的动态代理实现AOP,类如果实现了接口,Spring就会使用这种方式实现动态代理。熟悉Java
语言的应该会对JDK
动态代理有所了解。JDK
实现动态代理需要两个组件,首先第一个就是InvocationHandler
接口。我们在使用JDK
的动态代理时,需要编写一个类,去实现这个接口,然后重写invoke
方法,这个方法其实就是我们提供的代理方法。然后JDK
动态代理需要使用的第二个组件就是Proxy
这个类,我们可以通过这个类的newProxyInstance
方法,返回一个代理对象。生成的代理类实现了原来那个类的所有接口,并对接口的方法进行了代理,我们通过代理对象调用这些方法时,底层将通过反射,调用我们实现的invoke
方法。
(二)CGLib动态代理
JDK
的动态代理存在限制,那就是被代理的类必须是一个实现了接口的类,代理类需要实现相同的接口,代理接口中声明的方法。若需要代理的类没有实现接口,此时JDK
的动态代理将没有办法使用,于是Spring
会使用CGLib
的动态代理来生成代理对象。CGLib
直接操作字节码,生成类的子类,重写类的方法完成代理。
JDK动态代理的实现原理
实现原理
JDK
的动态代理是基于反射实现。JDK
通过反射,生成一个代理类,这个代理类实现了原来那个类的全部接口,并对接口中定义的所有方法进行了代理。当我们通过代理对象执行原来那个类的方法时,代理类底层会通过反射机制,回调我们实现的InvocationHandler
接口的invoke
方法。并且这个代理类是Proxy类的子类。这就是JDK
动态代理大致的实现方式。
优点
-
JDK
动态代理是JDK
原生的,不需要任何依赖即可使用; - 通过反射机制生成代理类的速度要比
CGLib
操作字节码生成代理类的速度更快;
缺点
- 如果要使用
JDK
动态代理,被代理的类必须实现了接口,否则无法代理; -
JDK
动态代理无法为没有在接口中定义的方法实现代理,假设我们有一个实现了接口的类,我们为它的一个不属于接口中的方法配置了切面,Spring
仍然会使用JDK
的动态代理,但是由于配置了切面的方法不属于接口,为这个方法配置的切面将不会被织入。 -
JDK
动态代理执行代理方法时,需要通过反射机制进行回调,此时方法执行的效率比较低;
代码测试
public interface Human {
void display();
}
@Component
public class Student implements Human {
@Override
public void display() {
System.out.println("I am a student");
}
}
@Aspect
@Component
public class HumanAspect {
// 为Student这个类的所有方法,配置这个前置通知
@Before("execution(* cn.tewuyiang.pojo.Student.*(..))")
public void before() {
System.out.println("before student");
}
}
// 配置类
@Configuration
@ComponentScan(basePackages = "cn.tewuyiang")
@EnableAspectJAutoProxy
public class AOPConfig {
}
// 测试方法
@Test
public void testProxy() {
ApplicationContext context =
new AnnotationConfigApplicationContext(AOPConfig.class);
// 注意,这里只能通过Human.class获取,而无法通过Student.class,因为在Spirng容器中,
// 因为使用JDK动态代理,Ioc容器中,存储的是一个类型为Human的代理对象
Human human = context.getBean(Human.class);
human.display();
// 输出代理类的父类,以此判断是JDK还是CGLib
System.out.println(human.getClass().getSuperclass());
}
// 输出结果
// before student
// I am a student
// class java.lang.reflect.Proxy // 注意看,父类是Proxy
注意看上面代码中,最长的那一句注释。由于我们需要代理的类实现了接口,则Spring
会使用JDK
的动态代理,生成的代理类会实现相同的接口,然后创建一个代理对象存储在Spring
容器中。这也就是说,在Spring
容器中,这个代理bean
的类型不是Student
类型,而是Human
类型,所以我们不能通过Student.class
获取,只能通过Human.class
(或者通过它的名称获取)。这也证明了我们上面说过的另一个问题,JDK
动态代理无法代理没有定义在接口中的方法。假设Student
这个类有另外一个方法,它不是Human
接口定义的方法,此时就算我们为它配置了切面,也无法将切面织入。而且由于在Spring
容器中保存的代理对象并不是Student
类型,而是Human
类型,这就导致我们连那个不属于Human
的方法都无法调用。这也说明了JDK
动态代理的局限性。
我们前面说过,JDK
动态代理生成的代理类继承了Proxy
这个类,而CGLib
生成的代理类,则继承了需要进行代理的那个类,于是我们可以通过输出代理对象所属类的父类,来判断Spring
使用了何种代理。
CGLib动态代理实现原理
实现原理
CGLib
实现动态代理的原理是,底层采用了ASM
字节码生成框架,直接对需要代理的类的字节码进行操作,生成这个类的一个子类,并重写了类的所有可以重写的方法,在重写的过程中,将我们定义的额外的逻辑(简单理解为Spring
中的切面)织入到方法中,对方法进行了增强。而通过字节码操作生成的代理类,和我们自己编写并编译后的类没有太大区别。
优点
- 使用
CGLib
代理的类,不需要实现接口,因为CGLib
生成的代理类是直接继承自需要被代理的类; -
CGLib
生成的代理类是原来那个类的子类,这就意味着这个代理类可以为原来那个类中,所有能够被子类重写的方法进行代理; -
CGLib
生成的代理类,和我们自己编写并编译的类没有太大区别,对方法的调用和直接调用普通类的方式一致,所以CGLib
执行代理方法的效率要高于JDK
的动态代理;
缺点
- 由于
CGLib
的代理类使用的是继承,这也就意味着如果需要被代理的类是一个final
类,则无法使用CGLib
代理; - 由于
CGLib
实现代理方法的方式是重写父类的方法,所以无法对final
方法,或者private
方法进行代理,因为子类无法重写这些方法; -
CGLib
生成代理类的方式是通过操作字节码,这种方式生成代理类的速度要比JDK
通过反射生成代理类的速度更慢;
代码测试
@Component
public class Student {
// 加上final修饰符
public final void display() {
System.out.println("I am a student");
}
}
// 输出结果如下:
I am a student
class cn.tewuyiang.pojo.Student
可以看到,输出的父类仍然是Student
,也就是说Spring
依然使用了CGLib
生成代理。但是我们发现,我们为display
方法配置的前置通知并没有执行,也就是代理类并没有为display
方法进行代理。这也验证了我们之前的说法,CGLib
无法代理final
方法,因为子类无法重写父类的final
方法。下面我们可以试着为Student
类加上final
修饰符,让他无法被继承,此时看看结果。运行的结果会抛出异常,因为无法生成代理类,这里就不贴出来了,可以自己去试试。
为什么Java动态代理必须是接口?
动态代理实际上是程序在运行中,根据被代理的接口来动态生成代理类的class文件,并加载class文件运行的过程,通过反编译被生成的**$Proxy0.class**文件发现:
class类定义为:
public final class $Proxy0 extends Proxy implements Interface {
public $Proxy0(InvocationHandler paramInvocationHandler) {\
super(paramInvocationHandler);\
}\
...
...
// 该方法为被代理接口的业务方法,代理类都会自动生成相应的方法,里面去执行invocationHandler 的invoke方法。
public final void sayHello(String paramString) {
try {
this.h.invoke(this, m3, new Object[]{paramString});\
return;
} catch (Error | RuntimeException localError) {
throw localError;
} catch (Throwable localThrowable) {
throw new UndeclaredThrowableException(localThrowable);
}
}
}
由于Java的单继承,动态生成的代理类已经继承了Proxy类的,就不能再继承其他的类,所以只能靠实现被代理类的接口的形式,故JDK的动态代理必须有接口。
另外,为何调用代理类的方法就会自动进入InvocationHandler
的invoke()
方法呢?
其实是因为在动态代理类的定义中,构造函数是含参的构造,参数就是我们invocationHandler
实例,而每一个被代理接口的方法都会在代理类中生成一个对应的实现方法,并在实现方法中最终调用invocationHandler
的invoke
方法,这就解释了为何执行代理类的方法会自动进入到我们自定义的invocationHandler
的invoke
方法中,然后在我们的invoke
方法中再利用jdk反射的方式去调用真正的被代理类的业务方法,而且还可以在方法的前后去加一些我们自定义的逻辑。比如切面编程AOP等。
描述一下Spring Bean的生命周期?
- 解析类得到BeanDefinition
- 如果有多个构造方法,则要推断构造方法
- 确定好构造方法后,进行实例化得到一个对象
- 对对象中的加了@Autowired注解的属性进行属性填充
- 回调Aware方法,比如BeanNameAware,BeanFactoryAware
- 调用BeanPostProcessor的初始化前的方法
- 调用初始化方法
- 调用BeanPostProcessor的初始化后的方法,在这里会进行AOP
- 如果当前创建的bean是单例的则会把bean放入单例池
- 使用bean
- Spring容器关闭时调用DisposableBean中destory()方法
spring事务传播机制
- REQUIRED(Spring默认的事务传播类型):如果当前没有事务,则自己新建一个事务,如果当前存在事 务,则加入这个事务
- SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,就以非事务方法执行
- MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常。
- REQUIRES_NEW:创建一个新事务,如果存在当前事务,则挂起该事务。
- NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,则挂起当前事务
- NEVER:不使用事务,如果当前事务存在,则抛出异常
- NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样(开启一个事务)
- 和REQUIRES_NEW的区别 REQUIRES_NEW是新建一个事务并且新开启的这个事务与原有事务无关,而NESTED则是当前存在事务时(我 们把当前事务称之为父事务)会开启一个嵌套事务(称之为一个子事务)。 在NESTED情况下父事务回滚时, 子事务也会回滚,而在REQUIRES_NEW情况下,原有事务回滚,不会影响新开启的事务。
- 和REQUIRED的区别 REQUIRED情况下,调用方存在事务时,则被调用方和调用方使用同一事务,那么被调用方出现异常时,由于 共用一个事务,所以无论调用方是否catch其异常,事务都会回滚 而在NESTED情况下,被调用方发生异常 时,调用方可以catch其异常,这样只有子事务回滚,父事务不受影响
spring事务什么时候会失效?
spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了!常见情况有 如下几种
- 发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而 是UserService对象本身! 解决方法很简单,让那个this变成UserService的代理类即可!
- 方法不是public的
@Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。
- 数据库不支持事务
- 没有被spring管理
- 异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为
RuntimeException
)
Spring 框架中都用到了哪些设计模式?
简单工厂
由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类。
Spring中的BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得Bean对象,但是否是
在传入参数后创建还是传入参数前创建这个要根据具体情况来定。
工厂方法:
实现了FactoryBean接口的bean是一类叫做factory的bean。其特点是,spring会在使用getBean()调用获得该bean时,会自动调用该bean的getObject()方法,所以返回的不是factory这个bean,而是这个
bean.getOjbect()
方法的返回值。
单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
spring对单例的实现: spring中的单例模式完成了后半句话,即提供了全局的访问点BeanFactory。但没有从构造器级别去控制单例,这是因为spring管理的是任意的java对象。
适配器模式:
Spring定义了一个适配接口,使得每一种Controller有一种对应的适配器实现类,让适配器代替controller执行相应的方法。这样在扩展Controller时,只需要增加一个适配器类就完成了SpringMVC的扩展了。
装饰器模式
动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。
Spring中用到的包装器模式在类名上有两种表现:一种是类名中含有Wrapper,另一种是类名中含有Decorator。
动态代理
切面在应用运行的时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象创建动态的创建一个代理对象。SpringAOP就是以这种方式织入切面的。
织入:把切面应用到目标对象并创建新的代理对象的过程。
观察者模式
spring的事件驱动模型使用的是观察者模式 ,Spring中Observer模式常用的地方是listener的实现。
策略模式
Spring框架的资源访问Resource接口。该接口提供了更强的资源访问能力,Spring 框架本身大量使用了Resource 接口来访问底层资源。
模板方法
父类定义了骨架(调用哪些方法及顺序),某些特定方法由子类实现。
最大的好处:代码复用,减少重复代码。除了子类要实现的特定方法,其他方法及方法调用顺序都在父类中预先写好了。
refresh方法
SpringBoot
SpringBoot和SpringCloud的区别
- SpringBoot专注于快速方便的开发单个个体微服务。
- SpringCloud是关注全局的微服务协调整理治理框架, 它将SpringBoot开发的一个个单体微服务整合并管理起来。
- 为各个微服务之间提供,配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等集成服务。
- SpringBoot可以离开SpringCloud独立使用开发项目, 但是SpringCloud离不开SpringBoot,属于依赖的关系。
- SpringBoot专注于快速、 方便的开发单个微服务个体, SpringCloud关注全局的服务治理框架。
Spring Boot 自动配置原理
@Import + @Configuration + Spring spi
自动配置类由各个starter提供,使用@Configuration + @Bean定义配置类,放到METAINF/spring.factories下 使用Spring spi扫描META-INF/spring.factories下的配置类
使用@Import导入自动配置类
分布式
简述一下CAP理论
CAP 理论是针对分布式数据库而言的,它是指在一个分布式系统中,一致性(Consistency, C)、可用性(Availability, A)、分区容错性(Partition Tolerance, P)三者不可兼得。
一致性(C)
一致性是指“all nodes see the same data at the same time”,即更新操作成功后,所有节点在同一时间的数据完全一致。
一致性可以分为客户端和服务端两个不同的视角:
- 从客户端角度来看,一致性主要指多个用户并发访问时更新的数据如何被其他用户获取的问题;
- 从服务端来看,一致性则是用户进行数据更新时如何将数据复制到整个系统,以保证数据的一致。
一致性是在并发读写时才会出现的问题,因此在理解一致性的问题时,一定要注意结合考虑并发读写的场景。
可用性(A)
可用性是指“reads and writes always succeed”,即用户访问数据时,系统是否能在正常响应时间返回结果。
好的可用性主要是指系统能够很好地为用户服务,不出现用户操作失败或者访问超时等用户体验不好的情况。在通常情况下,可用性与分布式数据冗余、负载均衡等有着很大的关联。
分区容错性(P)
分区容错性是指“the system continues to operate despite arbitrary message loss or failure of part of the system”,即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。
简述Base理论
BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。BASE理论是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的总结, 是基于CAP定理逐步演化而来的。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
1、基本可用
基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性—-注意,这绝不等价于系统不可用。比如:
- 响应时间上的损失。正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒
- 系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面
2、软状态
软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时
3、最终一致性
最终一致性强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
总的来说,BASE理论面向的是大型高可用可扩展的分布式系统,和传统的事物ACID特性是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。但同时,在实际的分布式场景中,不同业务单元和组件对数据一致性的要求是不同的,因此在具体的分布式系统架构设计过程中,ACID特性和BASE理论往往又会结合在一起。
什么是Quorum机制和WARO协议
WARO协议
是一种简单的副本控制协议,当 Client 请求向某副本写数据时(更新数据),只有当所有的副本都更新成功之后,这次写操作才算成功,否则视为失败。这样的话,只需要读任何一个副本上的数据即可。但是WARO带来的影响是写服务的可用性较低,因为只要有一个副本更新失败,此次写操作就视为失败了。
Quorum机制
Quorum 的定义如下:假设有 N 个副本,更新操作 wi 在 W 个副本中更新成功之后,则认为此次更新操作 wi 成功,把这次成功提交的更新操作对应的数据叫做:“成功提交的数据”。对于读操作而言,至少需要读 R 个副本,其中,W+R>N ,即 W 和 R 有重叠,一般,W+R=N+1。
- N = 存储数据副本的数量
- W = 更新成功所需的副本
- R = 一次数据对象读取要访问的副本的数量
听起来有些抽象,举个例子:
假设我有5个副本,更新操作成功写入了3个,另外2个副本仍是旧数据,此时在读取的时候,只要确保读取副本的数量大于2,那么肯定就会读到最新的数据。至于如何确定哪份数据是最新的,我们可以通过引入数据版本号的方式判断(Quorum 机制的使用需要配合一个获取最新成功提交的版本号的 metadata 服务,这样可以确定最新已经成功提交的版本号,然后从已经读到的数据中就可以确认最新写入的数据。)
Zookeeper的选举机制是遵循了Quorum的,这也是为什么我们部署Zookeeper必须要求有奇数个Cluster可用的原因。这样一是能保证Leader选举时不会出现平票的情况,避免出现脑裂。二是Leader在向Follower同步数据的时候,必须要超过半数的Follower同步成功,才会认为数据写入成功。
Paxos算法
- 概念介绍
- Proposal提案,即分布式系统的修改请求,可以表示为**[提案编号N,提案内容value]**
- Client用户,类似社会民众,负责提出建议
- Propser议员,类似基层人大代表,负责帮Client上交提案
- Acceptor投票者,类似全国人大代表,负责为提案投票,不同意比自己以前接收过的提案编号要小的提案,其他提案都同意,例如A以前给N号提案表决过,那么再收到小于等于N号的提案时就直接拒绝了
- Learner提案接受者,类似记录被通过提案的记录员,负责记录提案
SpringCloud
你用的什么注册中心
- eureka
- alibaba nacos
熔断降级在你工作中怎么用的
服务降级
服务降级是从整个系统的负荷情况出发和考虑的,对某些负荷会比较高的情况,为了预防某些功能(业务场景)出现负荷过载或者响应慢的情况,在其内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback (退路)错误处理信息。这样,虽然提供的是一个有损的服务,但却保证了整个系统的稳定性和可用性。
熔断VS降级
相同点:
目标一致都是从可用性和可靠性出发,为了防止系统崩溃。
用户体验类似最终都让用户体验到的是某些功能暂时不可用。
不同点:
触发原因不同服务熔断-般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑。
什么是断路器
当一个服务调用另一个服务由于网络原因或自身原因出现问题,调用者就会等待被调用者的响应当更多的服务请
求到这些资源导致更多的请求等待,发生连锁效应(雪崩效应)
断路器有三种状态
- 打开状态:一段时间内达到一定的次数无法调用并且多次监测没有恢复的迹象断路器完全打开那么下次请求就不会请求到该服务
- 半开状态:短时间内有恢复迹象断路器会将部分请求发给该服务,正常调用时断路器关闭
- 关闭状态:当服务一直处于正常状态能正常调用.
hystrix除了做熔断降级还能做什么
限流
服务隔离
隔离当大多数人在使用Tomcat时,多个HTTP服务会共享-一个线程池,假设其中-一个HTTP服务访问的数据库响应非常慢,这将造成服务响应时间延迟增加,大多数线程阻塞等待数据响应返回,导致整个Tomcat线程池都被该服务占用,甚至拖垮整个Tomcat。因此,如果我们能把不同HTTP服务隔离到不同的线程池,则某个HTTP服务的线程池满了也不会对其他服务造成灾难性故障。这就需要线程隔离或者信号量隔离来实现了。使用线程隔离或信号隔离的目的是为不同的服务分配一-定的资源,当自己的资源用完,直接返回失败而不是占用别人的资源。
Hystrix实现服务隔离两种方案,分别为:线程池和信号量。
zuul和Spring Cloud Gateway的线程模型
Spring Cloud Gateway | zuul | |
基本介绍 | Spring Cloud Gateway是Spring官方基于Spring 5.0, SpringBoot 2.0和Project Reactor等技术开发的网关,旨在为微服务架构提供-种简单而有效的统一 -的API路由管理方式。 Spring 用其替代Netflix ZUUL,其不仅提供统一的路由方式, 并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。 | Zuul1是基于Servlet框架构建,采用的是阻塞和多线程方式,即一个线程处理一次连接请求,这种方式 |
性能 | WebFlux模块的名称是spring-webflux,名称中的Flux来源于Reactor中的类Flux。Spring webflux有一个 全新的非堵塞的函数式Reactive Web框架,可以用来构建异步的、非堵塞的、事件驱动的服务,在伸缩性方面表现非常好。使用非阻塞API。Websockets得到支持,并且由于它与Spring紧密集成。 | 本文的Zuul,指的是Zuul 1.x,是一个基于阻塞io的API Gateway。Zuul已经发布了Zuul 2.x,基于Netty,也是非阻塞的,支持长连接,但Spring Cloud暂时还没有整合计划。 |
源码维护组织 | spring-cloud-Gateway是spring旗下spring-cloud的一个子项目。还有一种说法是因为zuu12连续跳票和zuu11的性能表现不是很理想,所以催生了spring孵化Gateway项目。 | zuu7则是netf1ix公司的项目,只是spring将zuu1集成在springcloud中使用而已。关键目前spring不打算集成zuul2.x。 |
为什么需要使用网关服务?
请求接入
作为所有api接口服务请求的接入口
业务聚合
作为所有后端业务的聚合点
中介策略
实现安全、验证、路由、过滤、流控等策略
统一管理
对所有api服务和策略进行统一管理
sentinel底层限流算法
计数器算法
计数器算法指在一段时间内,进行计数,与阀值进行比较,如果超过了阀值则进行限流操作,到了时间临界点,将计数器清零进行重新计数,即单位时间段内可访问请求的次数进行控制。
计数器算法是一种比直观简单的限流算法,常用于应用服务对外提供的接口层面。
由于计数器算法存在时间临界点缺陷,因此在时间临界点左右的极短时间段内容易遭到攻击。
滑动窗口算法
滑动窗口算法是指把固定时间片进行划分,并且随着时间的流逝进行移动,通过这种方式可以巧妙的避开计数器的临界点的问题。也就是说这些固定数量的可以移动的格子,将会进行计数判断阀值,因此格子的数量影响着滑动窗口算法的精度。
滑动窗口算法可以有效的规避计数器算法中时间临界点的问题,但是仍然存在时间片段的概念。同时滑动窗口算法计数运算也相对计数器算法比较耗时。时间片段越精确,流量控制越精密,从而导致计算耗时越长。
漏桶算法
漏桶算法设计思路比较直观简单,即水(请求)以不确定的速率先进入到漏桶里,然后漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率。因此漏桶算法在这方面比滑动窗口而言,更加先进。
因此漏桶算法存在如下两个问题:漏桶的漏出速率是固定的参数,所以即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发到端口速率提高。因此漏桶算法对于存在突发特性的流量来说缺乏效率。存在所谓的溢出现象。
漏桶算法通过 RateLimiterController 来实现,在漏桶算法中,会记录上一个请求的到达时间,如果新到达的请求与上一次到达的请求之间的时间差小于限流配置所规定的最小时间,新到达的请求将会排队等待规定的最小间隔到达,或是直接失败。
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
if (acquireCount <= 0) {
return true;
}
if (count <= 0) {
return false;
}
long currentTime = TimeUtil.currentTimeMillis();
// 根据配置计算两次请求之间的最小时间
long costTime = Math.round(1.0 * (acquireCount) / count * 1000);
// 计算上一次请求之后,下一次允许通过的最小时间
long expectedTime = costTime + latestPassedTime.get();
if (expectedTime <= currentTime) {
// 如果当前时间大于计算的时间,那么可以直接放行
latestPassedTime.set(currentTime);
return true;
} else {
// 如果没有,则计算相应需要等待的时间
long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();
if (waitTime > maxQueueingTimeMs) {
return false;
} else {
long oldTime = latestPassedTime.addAndGet(costTime);
try {
waitTime = oldTime - TimeUtil.currentTimeMillis();
// 如果最大等待时间小于需要等待的时间,那么返回失败,当前请求被拒绝
if (waitTime > maxQueueingTimeMs) {
latestPassedTime.addAndGet(-costTime);
return false;
}
// 在并发条件下等待时间可能会小于等于0
if (waitTime > 0) {
Thread.sleep(waitTime);
}
return true;
} catch (InterruptedException e) {
}
}
}
return false;
}
令牌桶算法
令牌桶算法是和水桶算法效果一样但方向相反的算法。随着时间流逝,系统会按恒定时间间隔往桶里加入制定数量的水,如每100毫秒往桶里加入1000毫升的水,如果桶已经满了就不再加了(说明:令牌桶算法和漏桶算法相反,漏桶算法是按照客户请求的数量往漏桶中加水的,而令牌桶算法是服务器端控制往桶里加水的)。当有新请求来临时,会各自从桶里拿走一个令牌,如果没有令牌可拿了就阻塞或者拒绝服务。
令牌桶算法通过 WarmUpController 类实现。在这个情况下,当配置每秒能通过多少请求后,那么在这里 sentinel 也会每秒往桶内添加多少的令牌。当一个请求进入的时候,将会从中移除一个令牌。由此可以得出,桶内的令牌越多,也说明当前的系统利用率越低。因此,当桶内的令牌数量超过某个阈值后,那么当前的系统可以称之为处于饱和状态。
当系统处于 饱和状态的时候,当前允许的最大 qps 将会随着剩余的令牌数量减少而缓慢增加,达到为系统预热热身的目的。
this.count = count;
this.coldFactor = coldFactor;
warningToken = (int)(warmUpPeriodInSec * count) / (coldFactor - 1);
maxToken = warningToken + (int)(2 * warmUpPeriodInSec * count / (1.0 + coldFactor));
slope = (coldFactor - 1.0) / count / (maxToken - warningToken);
其中 count 是当前 qps 的阈值。coldFactor 则为冷却因子,warningToken 则为警戒的令牌数量,warningToken 的值为(热身时间长度 _ 每秒令牌的数量) / (冷却因子 - 1)。maxToken 则是最大令牌数量,具体的值为 warningToken 的值加上 (2 _ 热身时间长度 _ 每秒令牌数量) / (冷却因子 + 1)。当当前系统处于热身时间内,其允许通过的最大 qps 为 1 / (超过警戒数的令牌数 _ 斜率 slope + 1 / count),而斜率的值为(冷却因子 - 1) / count / (最大令牌数 - 警戒令牌数)。
举个例子: count = 3, coldFactor = 3,热身时间为 4 的时候,警戒令牌数为 6,最大令牌数为 12,当剩余令牌处于 6 和 12 之间的时候,其 slope 斜率为 1 / 9。 那么当剩余令牌数为 9 的时候的允许 qps 为 1.5。其 qps 将会随着剩余令牌数的不断减少而直到增加到 count 的值。
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
long passQps = (long) node.passQps();
long previousQps = (long) node.previousPassQps();
// 首先重新计算其桶内剩余的数量
syncToken(previousQps);
// 开始计算它的斜率
// 如果进入了警戒线,开始调整他的qps
long restToken = storedTokens.get();
if (restToken >= warningToken) {
long aboveToken = restToken - warningToken;
// 如果当前剩余的令牌数大于警戒数,那么需要根据准备的计算公式重新计算qps,这个qps小于设定的阈值
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
if (passQps + acquireCount <= warningQps) {
return true;
}
} else {
if (passQps + acquireCount <= count) {
return true;
}
}
return false;
}
protected void syncToken(long passQps) {
long currentTime = TimeUtil.currentTimeMillis();
currentTime = currentTime - currentTime % 1000;
long oldLastFillTime = lastFilledTime.get();
if (currentTime <= oldLastFillTime) {
return;
}
long oldValue = storedTokens.get();
long newValue = coolDownTokens(currentTime, passQps);
if (storedTokens.compareAndSet(oldValue, newValue)) {
// 从桶内移除相应数量的令牌,并更新最后更新时间
long currentValue = storedTokens.addAndGet(0 - passQps);
if (currentValue < 0) {
storedTokens.set(0L);
}
lastFilledTime.set(currentTime);
}
}
private long coolDownTokens(long currentTime, long passQps) {
long oldValue = storedTokens.get();
long newValue = oldValue;
// 当令牌的消耗程度远远低于警戒线的时候,将会补充令牌数
if (oldValue < warningToken) {
newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
} else if (oldValue > warningToken) {
if (passQps < (int)count / coldFactor) {
// qps小于阈值 / 冷却因子的时候,说明此时还不需要根据剩余令牌数调整qps的阈值,所以也会补充
newValue = (long)(oldValue + (currentTime - lastFilledTime.get()) * count / 1000);
}
}
return Math.min(newValue, maxToken);
}
令牌桶算法的优点如下:
服务器端可以根据实际服务性能和时间段改变生成令牌的速度和水桶的容量。 一旦需要提高速率,则按需提高放入桶中的令牌的速率。生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味着当面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情。
Redis
基本数据类型
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
redis的常用指令
String
指令 | 例子 | 备注 |
set | set name ls | |
get | get name | |
incr | incr num 1 | 如果num不存在,则创建一个num初始化为0的值,在进行增加操作。 |
decr | decr num 1 | 如果num不存在,同上 |
incrby | incrby num 10 | 指定num自增值为10 |
decrby | 同上 | 同上 |
append | append num abc | 追加操作,在num后面追加abc |
del | del num | 删除操作 |
HashSet
指令 | 例子 | 备注 |
hset | hset useInfo name ls | |
hget | hget userInfo name | |
hmset | hmset userInfo name zs age 12 phone 110 | |
hmget | hmget userInfo name age phone | |
hdel | hdel userInfo name |
List
指令 | 例子 | 备注 |
lpush | lpush list 3 2 1 | 依次装填 3 2 1,相当于子弹上膛; |
rpush | rpush list 4 5 | 中间一个旗杆,lpush从左边开始排队3 2 1,靠近旗杆依次为3 2 1;rpush旗杆 4 5,相当于从旗杆右边依次4 5,;最后效果为 1 2 3 旗杆 4 5; |
lrange | lrange list 0 -1 | 0表示第一;-1表示最后一个; |
lindex | lindex list 0 | 取第一个值 |
ltrim | ltrim list 1 -2 | 取第二个到倒数第二个区间内的值; |
lpop | lpop list | 左边开始移除list的值,并返回移除的值; |
rpop | 同上 | 同上 |
llen | llen list | 返回list集合元素的个数 |
redis如何进行持久化
RDB(快照)持久化
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
手动触发:
- save命令
- bgsave命令
自动触发以下场景下会触发:
- 使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。
- 如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点。
- 执行debug reload命令重新加载Redis时,也会自动触发save操作。
- 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave。
AOF持久化
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
步骤:
- 所有的写入命令会追加到aof_buf(缓冲区)中。
- AOF缓冲区根据对应的策略向硬盘做同步操作。
- 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。
- 当Redis服务器重启时,可以加载AOF文件进行数据恢复。
随着命令不断写入AOF,文件会越来越大。Redis引入AOF重写机制压缩文件体积。重写后AOF文件为什么变小
- 进程内已经超时数据不再写入文件
- 旧的AOF文件含有无效命令
- 多条命令合并
redis缓存雪崩、击穿、穿透
缓存雪崩
缓存雪崩是指,缓存层出现了错误或者同一时间内大量的数据过期失效,不能正常提供缓存服务了。于是所有的请求都会达到数据库存储层,数据库层的调用量会暴增,造成数据库层也会挂掉的情况。
解决方法
redis高可用
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
限流降级
这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
缓存击穿
是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
解决方法
互斥锁(mutex key)
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
使用双重检测锁
public List<UsersDO> getAllUserWithNoPage(){
try{
//序列化器,将key的值设置为字符串
RedisSerializer redisSerializer=new StringRedisSerializer();
redisTemplate.setKeySerializer(redisSerializer);
//查缓存
List<UsersDO> list=(List<UsersDO>)redisTemplate.opsForValue().get("allUsers");
if(null==list){
//双重检测 锁
synchronized (this) {
List<UsersDO> list1 = (List<UsersDO>) redisTemplate.opsForValue().get("allUsers");
if (null == list1) {
UsersQuery query=new UsersQuery();
list=usersDOMapper.selectByExample(query);
redisTemplate.opsForValue().set("allUsers", list);
System.out.println("从数据库中取数据");
}
else{
System.out.println("从缓存中取数据");
}
}
}
else{
System.out.println("从缓存中取数据");
}
return list;
}
catch (Exception e) {
logger.error("UserService.getAllUserWithNoPage error",e);
}
return null;
}
缓存穿透
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。场景:攻击
解决方法
布隆过滤器
布隆过滤器:一种数据结构,是由一串很长的二进制向量组成,可以将其看成一个二进制数组。既然是二进制,那么里面存放的不是0,就是1,但是初始默认值都是0。
如下所示:
①、添加数据
介绍概念的时候,我们说可以将布隆过滤器看成一个容器,那么如何向布隆过滤器中添加一个数据呢?
如下图所示:当要向布隆过滤器中添加一个元素key时,我们通过多个hash函数,算出一个值,然后将这个值所在的方格置为1。
比如,下图hash1(key)=1,那么在第2个格子将0变为1(数组是从0开始计数的),hash2(key)=7,那么将第8个格子置位1,依次类推。
②、判断数据是否存在?
知道了如何向布隆过滤器中添加一个数据,那么新来一个数据,我们如何判断其是否存在于这个布隆过滤器中呢?
很简单,我们只需要将这个新的数据通过上面自定义的几个哈希函数,分别算出各个值,然后看其对应的地方是否都是1,如果存在一个不是1的情况,那么我们可以说,该新数据一定不存在于这个布隆过滤器中。
反过来说,如果通过哈希函数算出来的值,对应的地方都是1,那么我们能够肯定的得出:这个数据一定存在于这个布隆过滤器中吗?
答案是否定的,因为多个不同的数据通过hash函数算出来的结果是会有重复的,所以会存在某个位置是别的数据通过hash函数置为的1。
我们可以得到一个结论:布隆过滤器可以判断某个数据一定不存在,但是无法判断一定存在。
③、布隆过滤器优缺点
优点:优点很明显,二进制组成的数组,占用内存极少,并且插入和查询速度都足够快。
缺点:随着数据的增加,误判率会增加;还有无法判断数据一定存在;另外还有一个重要缺点,无法删除数据。
缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;
但是这种方法会存在两个问题:
如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
redis的过期策略以及内存淘汰机制
redis采用的是定期删除+惰性删除策略。
定期删除
指的是redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意,这里可不是每隔100ms就遍历所有的设置过期时间的key,那样就是一场性能上的灾难。实际上redis是每隔100ms随机抽取一些key来检查和删除的。
惰性删除
在你获取某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。并不是key到时间就被删除掉,而是你查询这个key的时候,redis再懒惰的检查一下。
内存淘汰机制
在redis.conf
中有一行配置 maxmemory-policy volatile-lru
该配置就是配内存淘汰策略的
- volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据,新写入操作会报错
ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
Redis实现分布式锁
首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
- 互斥性。在任意时刻,只有一个客户端能持有锁。
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了,即不能误解锁。
- 具有容错性。只要大多数Redis节点正常运行,客户端就能够获取和释放锁。
错误的写法
正确写法
用 Redis 实现分布式锁的正确姿势(实现一)
通过 set key value px milliseconds nx
命令实现加锁, 通过Lua脚本实现解锁。核心实现命令如下:
//获取锁(unique_value可以是UUID等)
SET resource_name unique_value NX PX 30000
//释放锁(lua脚本中,一定要比较value,防止误解锁)
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
用 Redisson 实现分布式可重入锁(RedissonLock)(实现二)
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redisson 中使用 lua 脚本,我们需要注意的是,从 Redis 2.6.0后才支持 lua 脚本的执行。
使用 lua 脚本的好处:
- 原子操作:lua脚本是作为一个整体执行的,所以中间不会被其他命令插入。
- 减少网络开销:可以将多个请求通过脚本的形式一次发送,减少网络时延。
- 复用性:lua脚本可以常驻在redis内存中,所以在使用的时候,可以直接拿来复用,也减少了代码量。
Redis事务
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
Redis会将一个事务中的所有命令序列化,然后按顺序执行。
- redis 不支持回滚“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
- 如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
- 如果在一个事务中出现运行错误,那么正确的命令会被执行。
注:redis的discard只是结束本次事务,正确命令造成的影响仍然存在.
1)MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
2)EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。 当操作被打断时,返回空值 nil 。
3)通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
4)WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
Redis主从同步原理
全量同步
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
- 从服务器连接主服务器,发送SYNC命令;
- 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
- 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
- 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
- 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
- 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;
增量同步
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
MQ
MQ有什么用?MQ具体有什么使用场景?
MQ:message queue,消息队列、队列是一种FIFO先进先出的数据结构。消息由生产者发送到MQ进行排队,然后由消费者对消息进行处理。
MQ的作用主要有三点:
异步
可以同时给很多下游服务发送消息,下游服务可以同时开始处理该消息。非必要的业务逻辑以异步的方式运行,加快响应速度
例如:买东西下单,订单模块生成完订单数据,可以给支付服务、库存服务、物流服务等系统发送消息进行处理,不用等待整段流程执行完成可以直接返回下单成功的提示给用户。
提高了系统的响应速度和吞吐量
解耦
服务不再需要从上往下执行,而是可以切分成多级不同的服务模块相互通过MQ协调。需要消息的时候自己从消息队列中订阅,从而原系统不需要做任何修改。
减少服务之间的影响,提高系统的稳定性和可扩展性
削峰
原系统慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的。
MQ如何进行选型
产品 | 优点 | 缺点 | 场景 |
Kafka | 吞吐量大,性能非常好,集群高可用 | 会丢失数据,功能比较单一 | 日志分析,大数据采集 |
RabbitMQ | 消息可靠性高,功能全面 | 吞吐量比较低,消息累积会严重影响性能 erlang语言不好定制 | 小规模场景 |
RocketMQ | 高吞吐、高性能、高可用、功能非常全面 | 开源版功能不如云上商业版。 官方文档和周边生态还不够成熟。 客户端只支出Java | 几乎全场景 |
ActiveMQ | 成熟 | 大规模场景不适合 数据丢失 社区不活跃 | 小规模 |
如何保证消息不丢失?
哪些环节会造成消息丢失?
怎么去防止消息丢失?
生产者发送消息不丢失
- kafka:消息发送+回调
- RocketMQ:
- 消息发送+回调
- 事务消息
- RabbitMQ:
- 消息发送+回调
- 手动事务:channel.txSelect()开启事务,channel.txCommit()提交事务,channel.txRollback()回滚事务。这种方式对channel是会阻塞,造成吞吐量下降。
- 消息响应机制:生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会将消息写入磁盘之后发出,broker回传给生产者的确认消息中deliver-tag域包含了确认消息的序列号。
confirm的优势是,它是异步的,在生产者发送完一个消息之后,不必要等这个消息的返回,就可以继续处理另外一个消息,等待消息的ack返回之后,再去处理前面发过的消息,类似于多路复用的做法。rabbitmq在收到之后,会回复ack,如果因为rabbitmq自身的问题导致的,会回复nack消息。
对于生产者来说,为了方便确认消息有没有真正到达rabbitmq端,还需要在生产者端设置超时重发,毕竟网络里面是可能丢失消息的。
MQ主从消息同步不丢失
- RocketMQ:
- 普通集群:同步同步,异步同步
- Dledger集群:两阶段提交,选举主节点
- RabbitMQ:
- 普通集群:消息是分散存储的,节点不会主动进行同步,只有用到的时候才会去查找,是有可能丢失消息的
- 镜像集群:镜像集群会在节点之间进行同步,数据安全性得到提高
- kafka:
允许消息少量丢失
MQ消息存盘不丢失
- RocketMQ:
- 同步存入硬盘:效率低,安全性高
- 异步存入硬盘:效率高,容易丢消息
- RabbitMQ:
将队列配置成持久化队列。
新版本有Quorum类型的队列,会采用raft协议来进行消息同步。
MQ发送消息给消费者不丢失
如果保证消息的幂等性
MySQL
MySQL的锁有哪些
show status like 'innodb_row_lock&'; -- 查询锁的情况
行锁
行级锁是mysql中锁定粒度最细的一种锁。表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突,其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁和排他锁。
开销大,加锁慢,会出现死锁。发生锁冲突的概率最低,并发度也最高。
间隙锁
一般发生在范围查询,范围查询产生了间隙锁,比如update >1 and <9的值,这时候其他会话就不能插入或者更新到1-9之间。
表锁
表级锁是mysql中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分mysql引擎支持。最常使用的MyISAM与InnoDB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)
表锁的原因是,语句的使用的索引失效,由行锁升级到表锁,比如or连接条件
开销小,加锁快,不会出现死锁。发生锁冲突的概率最高,并发度也最低。
页锁
页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。因此,采取了折衷的页级锁,一次锁定相邻的一组记录。BDB 支持页级锁。
开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。
索引底层用什么数据结构存储数据的?
B+树数据结构存储数据的
MyISAM存储结构和Innodb存储结构有什么区别
- MyISAM:将表分成三个文件存储到本地,.frm文件用于保存表的结构、.MYD用于保存表的数据、.MYI用于保存表的索引;
- Innodb:将表分成两个文件存储到本地,.ibd文件用于保存表数据与表索引、.frm同上
为什么mysql页文件默认16K?
假设我们一行数据大小为1K,那么一页就能存16条数据,也就是一个叶子节点能存16条数据;再看非叶子节点,假设主键ID为bigint类型,那么长度为8B,指针大小在Innodb源码中为6B,一共就是14B, 那么一页里就可以存储16K/14=1170个(主键+指针)。
那么一颗高度为2的B+树能存储的数据为: 117016=18720条, - 颗高度为3的B+树能存储的数据为:11701170*16=21902400 (千万级条)
为什么mysql索引不选择使用B树、HASH、二叉树(红黑树)保存数据,而使用B+树?
先从数据结构的角度来看我们知道B-树和B+树最重要的一个区别就是B+树只有叶节点存放数据,其余节点用来索引,而B-树是每个索引节点都会有Data域。这就决定了B+树更适合用来存储外部数据,也就是所谓的磁盘数据。
从Mysql(Innodb)的角度来看,B+树是用来充当索引的,一般来说索引非常大,尤其是关系性数据库这种数据量大的索引能达到亿级别,所以为了减少内存的占用,索引也会被存储在磁盘上。那么Mysql如何衡量查询效率呢?磁盘IO次数,B-树(B类树)的特点就是每层(横向)节点数目非常多,层数很少,目的就是为了就少磁盘IO次数,当查询数据的时候,最好的情况就是很快找到目标索引,然后读取数据,使用B+树就能很好的完成这个目的,但是B-树的每个节点都有data域(指针),这无疑增大了节点大小,说白了增加了磁盘IO次数(磁盘IO一次读出的数据量大小是固定的,单个数据变大,每次读出的就少,IO次数增多,一次IO多耗时啊!),而B+树除了叶子节点其它节点并不存储数据,节点小,磁盘IO次数就少。这是优点之一。另一个优点是什么,B+树所有的Data域在叶子节点,一般来说都会进行一个优化,就是将所有的叶子节点用指针串起来。这样遍历叶子节点就能获得全部数据,这样就能进行区间访问啦。(数据库索引采用B+树的主要原因是 B树在提高了磁盘IO性能的同时并没有解决元素遍历的效率低下的问题。正是为了解决这个问题,B+树应运而生。B+树只要遍历叶子节点就可以实现整棵树的遍历。而且在数据库中基于范围的查询是非常频繁的,而B树不支持这样的操作(或者说效率太低)
至于MongoDB为什么使用B-树而不是B+树,可以从它的设计角度来考虑,它并不是传统的关系性数据库,而是以Json格式作为存储的nosql,目的就是高性能,高可用,易扩展。首先它摆脱了关系模型,上面所述的优点2需求就没那么强烈了,其次Mysql由于使用B+树,数据都在叶节点上,每次查询都需要访问到叶节点,而MongoDB使用B-树,所有节点都有Data域,只要找到指定索引就可以进行访问,无疑单次查询平均快于Mysql(但侧面来看Mysql至少平均查询耗时差不多)。
总体来说,Mysql选用B+树和MongoDB选用B-树还是以自己的需求来选择的。
B树相对于红黑树的区别
在大规模数据存储的时候,红黑树往往出现由于树的深度过大(右边,即大的一边倾斜)而造成磁盘IO读写过于频繁,进而导致效率低下的情况。为什么会出现这样的情况,我们知道要获取磁盘上数据,必须先通过磁盘移动臂移动到数据所在的柱面,然后找到指定盘面,接着旋转盘面找到数据所在的磁道,最后对数据进行读写。磁盘IO代价主要花费在查找所需的柱面上,树的深度过大会造成磁盘IO频繁读写。根据磁盘查找存取的次数往往由树的高度所决定,所以,只要我们通过某种较好的树结构减少树的结构尽量减少树的高度,B树可以有多个子女,从几十到上千,可以降低树的高度。
mysql为什么建议Innodb要设置整形的自增主键
如果InnoDB表的数据写入顺序能和B+树索引的叶子节点顺序一致的话,这时候存取效率是最高的,也就是下面这几种情况的存取效率最高:
- 使用自增列(INT/BIGINT类型)做主键,这时候写入顺序是自增的,和B+数叶子节点分裂顺序一致;
- 如果没有显式定义主键,则InnoDB会选择第一个不包含有NULL值的唯一索引作为主键索引、如果也没有这样的唯一索引,则InnoDB会选择内置6字节长的ROWID作为隐含的聚集索引(ROWID随着行记录的写入而主键递增,这个ROWID不像ORACLE的ROWID那样可引用,是隐含的),写入顺序和ROWID增长顺序一致;
除此以外,如果一个InnoDB表又没有显示主键,又有可以被选择为主键的唯一索引,但该唯一索引可能不是递增关系时(例如字符串、UUID、多字段联合唯一索引的情况),该表的存取效率就会比较差。
Mysql索引优化
1、最左前缀法则
如果建立的是复合索引,索引的顺序要按照建立时的顺序,即从左到右,如:a->b->c(和b+数的数据结构有关)
无效索引列举
- a->c:a有效,c无效
- b->c:b,c都无效
- c:c无效
不要对索引字段做如下处理
- 计算,如:+、 -、 *、 /、 !=、 <>、 is null、 is not null、or
- 函数,如sum()、round()等等
- 手动/自动类型转换,如:id=“1”,本来是数字,给写成了字符串
索引不要放在范围查询右边
比如复合索引: a->b->c,当where a="" and b>10 and c="",这时候只用到a和b的索引,c用不到索引,因为在范围之后索引都失效。(跟B+树结构有关)
减少select * 使用
尽量使用覆盖索引(查询的字段跟索引是一样的)
like模糊查询
使用左侧固定法
-- 失效的情况
like "%张三%";
like "%张三";
-- 正确使用索引的写法
like "张三%";
使用覆盖索引
select name from table where name like "%张三%";
order by
避免使用文件内排序,会在内存开辟一份空间用作排序。尽量不用mysql做排序。用后台去做。Camparable接口和Comparator接口,Java的内部排序和外部排序。
MySQL怎么保证隔离性的四个特性
原子性
主要依靠undo.log日志实现,即在事务失败时执行回滚。undo.log日志会记录事务执行的sql,当事务需要回滚时,通过反向补偿回滚数据库状态。
持久性
主要依靠redo.log日志实现。首先,mysql持久化通过缓存来提高效率,即在select时先查缓存,再查磁盘;在update时先更新缓冲,再定时把缓冲区的数据更新到磁盘。以减少磁盘io次数,提高效率。但由于缓存断电就没了,所以需要redo.log日志。在执行修改操作时,sql会先写入到redo.log日志(预写式日志),再写入缓存中。这样即使断电,也能保证数据不丢失,达到持久性。(缓存区的存储是随机io操作,redo log是文件尾部插入的顺序io操作,所以redo log比较快;缓存区是以数据页为单位的,MySQL一个页大小为16K,单页中的数据修改会导致缓存的整个页写入,redo log只需要尾部追加实际操作即可,无效io大大减少)
innodh_ lush_ loe o irx commit
命令
0:表示当提交事务时,并不将缓冲区的redo日志写入磁盘的日志文件,而是等待主线程每秒刷新。
1:在事务提交时将缓冲区的redo日志同步写入到磁盘,保证一定会写入成功。
2:在事务提交时将缓冲区的redo日志异步写入到磁盘,即不能完全保证commit时肯定会写入redo日志文件,只是有这个动作。
隔离性
多线程时多事务之间互相产生了影响,要避免这个影响,那就加锁。mysql的锁有表锁,行锁,间隙锁,全局锁。写写操作通过加锁实现隔离性,写读操作通过MVCC实现。
一致性
就是事务再执行的前和后数据库的状态都是正常的,表现为没有违反数据完整性,参照完整性和用户自定义完整性等等。而上面三种特性就是为了保证数据库的有一致性。
MySQL主从复制是怎么同步的?
- master更新数据时,会先将信息写入master服务器的二进制日志,然后根据二进制日志写入真实数据到数据库&表中。
- 然后通过dump thread线程通知slave服务器同步数据。
- slave收到消息后,触发已经启动的IO线程及sql线程,IO线程通过连接master线程,读取master-info文件获取位置点(pos),master服务器通过dump thread线程将位置点后的内容传给slave的IO线程。IO线程收到数据后,将数据写入到本地的relay-log中。
- 之后sql线程开始工作,先读取relay-log.info,获知上次在relay-log文件中执行的位置,然后在该位置继续向后执行。通过这种方式及可保持主从服务器上的数据完全一致。
MySQL主从同步延时问题
MySQL数据库主从同步延迟是怎么产生的?当主库的TPS并发较高时,产生的DDL数量超过slave一个sql线程所能承受的范围,那么延时就产生了,当然还有就是可能与slave的大型query语句产生了锁等待。首要原因:数据库在业务上读写压力太大,CPU计算负荷大,网卡负荷大,硬盘随机IO太高次要原因:读写binlog带来的性能影响,网络传输延迟。
架构方面
- 业务的持久化层的实现采用分库架构,mysql服务可平行扩展,分散压力。
- 单个库读写分离,一主多从,主写从读,分散压力。这样从库压力比主库高,保护主库。
- 服务的基础架构在业务和mysql之间加入redis的cache层。降低mysql的读压力。
- 不同业务的mysql物理上放在不同机器,分散压力。
- 使用比主库更好的硬件设备作为slave,mysql压力小,延迟自然会变小。
硬件方面
- 采用好服务器,比如4u比2u性能明显好,2u比1u性能明显好。
- 存储用ssd或者盘阵或者san,提升随机写的性能。
- 主从间保证处在同一个交换机下面,并且是万兆环境。
总结,硬件强劲,延迟自然会变小。一句话,缩小延迟的解决方案就是花钱和花时间。
配置方便
- mysql主从同步加速
- sync_binlog在slave端设置为0
- –logs-slave-updates 从服务器从主服务器接收到的更新不记入它的二进制日志。
- 直接禁用slave端的binlog
- slave端,如果使用的存储引擎是innodb,innodb_flush_log_at_trx_commit =2
从文件系统本身属性角度优化
master端修改linux、Unix文件系统中文件的etime属性, 由于每当读文件时OS都会将读取操作发生的时间回写到磁盘上,对于读操作频繁的数据库文件来说这是没必要的,只会增加磁盘系统的负担影响I/O性能。可以通过设置文件系统的mount属性,组织操作系统写atime信息,在linux上的操作为:打开/etc/fstab,加上noatime参数/dev/sdb1 /data reiserfs noatime 1 2然后重新mount文件系统#mount -oremount /data
同步参数调整
主库是写,对数据安全性较高,比如sync_binlog=1
,innodb_flush_log_at_trx_commit = 1
之类的设置是需要的而slave则不需要这么高的数据安全,完全可以讲sync_binlog设置为0或者关闭binlog,innodb_flushlog也可以设置为0来提高sql的执行效率。
- sync_binlog=1 oMySQL提供一个sync_binlog参数来控制数据库的binlog刷到磁盘上去。默认,sync_binlog=0,表示MySQL不控制binlog的刷新,由文件系统自己控制它的缓存的刷新。这时候的性能是最好的,但是风险也是最大的。一旦系统Crash,在binlog_cache中的所有binlog信息都会被丢失。
如果sync_binlog>0,表示每sync_binlog次事务提交,MySQL调用文件系统的刷新操作将缓存刷下去。最安全的就是sync_binlog=1了,表示每次事务提交,MySQL都会把binlog刷下去,是最安全但是性能损耗最大的设置。这样的话,在数据库所在的主机操作系统损坏或者突然掉电的情况下,系统才有可能丢失1个事务的数据。但是binlog虽然是顺序IO,但是设置sync_binlog=1,多个事务同时提交,同样很大的影响MySQL和IO性能。虽然可以通过group commit的补丁缓解,但是刷新的频率过高对IO的影响也非常大。
对于高并发事务的系统来说,“sync_binlog”设置为0和设置为1的系统写入性能差距可能高达5倍甚至更多。所以很多MySQL DBA设置的sync_binlog并不是最安全的1,而是2或者是0。这样牺牲一定的一致性,可以获得更高的并发和性能。默认情况下,并不是每次写入时都将binlog与硬盘同步。因此如果操作系统或机器(不仅仅是MySQL服务器)崩溃,有可能binlog中最后的语句丢失了。要想防止这种情况,你可以使用sync_binlog全局变量(1是最安全的值,但也是最慢的),使binlog在每N次binlog写入后与硬盘同步。即使sync_binlog设置为1,出现崩溃时,也有可能表内容和binlog内容之间存在不一致性。 - innodb_flush_log_at_trx_commit (这个很管用)抱怨Innodb比MyISAM慢 100倍?那么你大概是忘了调整这个值。默认值1的意思是每一次事务提交或事务外的指令都需要把日志写入(flush)硬盘,这是很费时的。特别是使用电池供电缓存(Battery backed up cache)时。设成2对于很多运用,特别是从MyISAM表转过来的是可以的,它的意思是不写入硬盘而是写入系统缓存。日志仍然会每秒flush到硬 盘,所以你一般不会丢失超过1-2秒的更新。设成0会更快一点,但安全方面比较差,即使MySQL挂了也可能会丢失事务的数据。而值2只会在整个操作系统 挂了时才可能丢数据。
- ls(1) 命令可用来列出文件的 atime、ctime 和 mtime。
atime 文件的access time 在读取文件或者执行文件时更改的ctime 文件的create time 在写入文件,更改所有者,权限或链接设置时随inode的内容更改而更改mtime 文件的modified time 在写入文件时随文件内容的更改而更改ls -lc filename 列出文件的 ctimels -lu filename 列出文件的 atimels -l filename 列出文件的 mtimestat filename 列出atime,mtime,ctimeatime不一定在访问文件之后被修改因为:使用ext3文件系统的时候,如果在mount的时候使用了noatime参数那么就不会更新atime信息。这三个time stamp都放在 inode 中.如果mtime,atime 修改,inode 就一定会改, 既然 inode 改了,那ctime也就跟着改了.之所以在 mount option 中使用 noatime, 就是不想file system 做太多的修改, 而改善读取效能
Elasticsearch
ES了解多少?
核心概念
- 索引 index:关系型数据库中的 table
- 文档 document:关系型数据库中的 row,它是一个json结构
- 字段 filed:数据库中的列,类型有text/keyword/byte
- 映射 mapping:数据库中的Schema,建表语句
- 查询方式:DSL,新版本也支持SQL
- 分片 sharding 和副本 replicas:index都是由sharding组成的,每个sharding都有一个备份。ES集群健康状态: