Java面经整理
数据结构
几种常见的数据结构
树、图、链表、优先队列(堆)、跳表(链表+多级索引)
跳表(redis的zset使用跳表):
跳表的查询的时间复杂度为 O(logn),因为找到位置之后插入和删除的时间复杂度很低,为 O(1),所以最终插入和删除的时间复杂度也为 O(logn) 删除操作: * 如果这个结点在索引中也有出现,我们除了要删除原始链表中的结点,还要删除索引中的。 * 同时我们删除节点时需要获得前驱节点(双向链表除外) 插入操作 * 插入元素过多,可能导致两个索引间节点过多,效率降低。我们需要维护索引与原始链表的大小平衡。。 * 跳表是通过一个随机函数来维护这个平衡的,当我们向跳表中插入数据时,我们可以选择同时把这个数据插入到索引里,那我们插入到哪一级的索引呢,这就需要随机函数,来决定我们插入到哪一级的索引中。这样可以很有效的防止跳表退化,而造成效率变低。
优先队列实现:
// 清楚大顶堆、小顶堆定义 // Java PriorityQueue 通过数组实现 // 插入元素 从末尾开始找 private void siftUpComparable(int k, E x) { // k代表当前元素个数、x为需要插入的元素 Comparable<? super E> key = (Comparable<? super E>) x; while (k > 0) { // 找到父节点下标 int parent = (k - 1) >>> 1; Object e = queue[parent]; // 如果是小顶堆 则判断 x >= queue[parent] 如果符合说明找到了需要插入的位置 break // 进行插入该节点,并将该parent节点放入index为k的位置 if (key.compareTo((E) e) >= 0) break; // 否则 将该节点下降 并令k为parent继续循环 queue[k] = e; k = parent; } // 插入到找到的位置 queue[k] = key; } // poll方法 移除最小元素 // 传入index为size的元素 此时根节点为空 k从0开始 // 即index为k的元素被取走后,拿出数组最后的元素x 不断寻找位置进行插入 如果不满足 会将空节点的子节点放入空节点 然后对其子节点进行继续操作直到找到能插入的位置或者循环结束 private void siftDownComparable(int k, E x) { // x为堆中最后的元素 Comparable<? super E> key = (Comparable<? super E>)x; int half = size >>> 1; // loop while a non-leaf while (k < half) { // x 从根节点开始 每次和 index为2k+1和2k+2中更小的元素进行比较 int child = (k << 1) + 1; // assume left child is least Object c = queue[child]; int right = child + 1; if (right < size && ((Comparable<? super E>) c).compareTo((E) queue[right]) > 0) // 小顶堆:c为2k+1和2k+2中较小的一个 也就是k对应节点的左右节点中小的那个 c = queue[child = right]; // 小顶堆:如果x比较c大,我们就和c交换位置 否则我们的数就应该在k位置上,k从0开始。 if (key.compareTo((E) c) <= 0) break; // 交换位置 queue[k] = c; k = child; } // 找到了位置 queue[k] = key; } // remove方法 移除指定元素 E removeAt(int i) { // i为指定元素的index 是通过循环遍历得到的 // assert i >= 0 && i < size; modCount++; int s = --size; // s为最后一个元素的下标 if (s == i) // removed last element queue[i] = null; else { // 将s对应的节点也就是最后一个节点拿到 需要进行树的修复 E moved = (E) queue[s]; // 将其置为null queue[s] = null; // 以queue[i]为根节点的树(堆) 进行树的修复 // 小顶堆:将moved元素作为上文的x元素 和上文的poll方法类似 // 只是此处不是k=0开始,而是k=i开始 因为移除的是index=i的元素 siftDown(i, moved); // 此时以queue[i]为根节点的树(堆)已经修复完成了 if (queue[i] == moved) { // 如果moved在i位置上 说明moved元素较小 还可以继续向上调整 siftUp(i, moved); // 向上调整 ???? if (queue[i] != moved) return moved; } } return null; }
判断链表是否存在环
快慢指针判断是否重合(如果需要找到入环点,在第一次相遇后一指针指向头部,然都保持慢指针速度继续前进,再次相遇的地方即为入环点)
二叉搜索树、平衡二叉树、红黑树
搜索二叉树:左节点 < 根节点 < 右节点,且左子树所有节点 < 根节点 < 右子树所有节点
平衡二叉树:它是一 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。对于插入节点和删除节点,如果破坏了平衡,那么都需要进行旋转以重新平衡。AVL树的查找、插入、删除操作在平均和最坏的情况下都是O(logn),代码可参考下方:
// 只会存在4种旋转方式:左旋、右旋、先左再右、先右再左。 // 递归进行判断即可 class Node { int val; Node left, right; // 构造方法... /* 左旋,右旋省略 */ public void leftRotate() { Node newNode = new Node(this.val); newNode.left = this.left; newNode.right = this.right.left; this.val = this.right.val; this.left = newNode; this.right = this.right.right; } /* 添加节点 */ public void add(Node node) { // 首先进行节点添加 if (node.val < this.lval) { if (this.left == null) this.left = node; else this.left.add(node); } else if (node.val > this.lval) { if (this.right == null) this.right = node; else this.right.add(node); } // 判断是否需要旋转 if (this.getLeftHeight() - this.getRightHeight() > 1) { // 获得子树高度的get方法 略 // 需要右旋 但需要先判断左子树是否需要先左旋 if (this.left != null && this.left.getLeftHeight() < this.left.getRightHeight()) { this.left.leftRotate(); } this.rightRotate(); } // 左旋同理... // 删除节点 /* 1、查找到对应节点 2、判断该节点是否为叶节点,如果为叶节点,则直接删除 如果不是叶节点,则判断该节点是否存在左右子树 如果只有左子树 则获得左子树的最大值leftMax 并将需要删除的节点的值置为leftMax 然后递归删除左子树中的该节点 如果只有右子树 则获得右子树的最小值rigthMin 并将需要删除的节点的值置为rigthMin 然后递归删除右子树中的该节点 如果左右子树均存在,也一样获得右子树的最小值rigthMin 并将需要删除的节点的值置为rigthMin 然后递归删除右子树中的该节点 3、删除完成后 判断是否平衡 即和上面add方法的后半部分类似 然后进行旋转平衡接即可 */ } }
红黑树:
- 任何一个节点都有颜色,黑色或者红色
- 根节点是黑色的
- 空节点被认为是黑色的(即NIL结点)
- 每个红色节点必须有两个黑色的子节点。(从每个叶子到根的所有路径上不能有两个连续的红色节点。)
- 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。(这也叫做完美黑色平衡,与2-3树的完美平衡有些不同)
只有黑色节点的红黑树其实就是平衡二叉树。我们将红色点想成是填充节点。即每次插入都以红色插入,且将黑色想成主要节点。红色节点做填充。红色节点的作用就是使得树的高度更加灵活,不至于像平衡二叉树那样每次插入都需要 做平衡操作(减少了需要平衡的概率)。**(红色节点的子节点是黑色节点)**条件就使得高度受到限制,极限情况就是一个红色一个黑色串联。最多的查找次数 不会超过2倍的 最少的查找数。
红黑树其实就是 二叉查找树和平衡二叉树 两者优缺点的一种折中。可以防止出现二叉查找树那种极差的情况,也可以减少插入时平衡的次数。
// 旋转操作和平衡树保持一致 /* 插入操作有三种情况是不会破坏性质的 且插入的节点一定是红色 1、插入节点作为根节点 2、插入节点作为根节点的子节点 3、插入节点的父结点是黑色的 如果会破坏性质 则说明不满足以上情况 也就是说明 新节点必定存在祖父节点、新节点父节点为红色 1、叔节点为红色——重绘,父节点和叔节点绘制为黑色,祖父节点绘制为红色 2、叔节点为黑色,祖-父-子节点在同侧——将父绘黑,祖绘红,然后以祖为枢纽进行向相反侧的方向旋转 3、叔节点为黑色,祖-父-子节点不在同侧——先根据子父所在的那一侧的方向,以父为枢向相反方向旋转,这样就实现了将子父祖异侧转换为了父子祖同侧,第二步是发现它符合上一种情况了,那就将现在的父(原来的子)绘黑,祖(原来的祖)绘红,然后再次旋转。 注意:需要修复时,还需要递归进行,即在一次修复后将指针指向此次操作的祖父节点 然后继续判断是否需要修复 最后仍然需要将根节点绘制为红色。 */
线段树、树状数组
树状数组是一种特殊的线段树
B树、B+树
每个节点不止一个数据
B树:每个节点上都存了key-data
B+树:只有叶子节点存了data,其他节点可存更多key,且所有叶子节点之间存在顺序指针关系。
红黑树和B+树
在数据量较小时,不需要磁盘io,红黑树的性能高于b+树
数据插入时,红黑树性能更好。
OS
开机过程
BIOS自检->加载bootloader到内存->bootloader加载OS到内存->从OS起始位置开始执行指令
BIOS位于RAM;bootstrap位于磁盘第一个主引导扇区;OS位于磁盘
以x86为例,bootloader一般会被加载到于内存0x7c00处
内存管理
段页式存储、虚拟内存、系统抖动。
进程和线程区别,什么是协程
进程是资源分配的基本单位。线程是CPU调度的基本单位。一个进程中可包含多个线程。
协程是轻量级的用户线程,一个线程可包含多个协程,只在用户态运行,开销小。
进程调度的方式(CFS?)
Linux的调度器类主要实现两类进程调度算法:实时调度算法和完全公平调度算法*(CFS)*,实时调度算法SCHED_FIFO和SCHED_RR,按优先级执行,一般不会被抢占。直到实时进程执行完,才会执行普通进程。而大多数的普通进程,用的就是CFS算法。
CFS 调度程序并不采用严格规则来为一个优先级分配某个长度的时间片,而是为每个任务分配一定比例的 CPU 处理时间。每个任务分配的具体比例是根据友好值来计算的。友好值的范围从 -20 到 +19,数值较低的友好值表示较高的相对优先级。具有较低友好值的任务,与具有较高友好值的任务相比,会得到更高比例的处理器处理时间。默认友好值为 0。
友好一词源自如下想法:当一个任务增加了它的友好值,如从 0 至 +10,该任务通过降低优先级,进而对其他任务更加友好。
CFS 没有使用离散的时间片,而是采用目标延迟,这是每个可运行任务应当运行一次的时间间隔。根据目标延迟,按比例分配 CPU 时间。除了默认值和最小值外,随着系统内的活动任务数量超过了一定阈值,目标延迟可以增加。
CFS 调度程序没有直接分配优先级。相反,它通过每个任务的变量 vruntime 以便维护虚拟运行时间,进而记录每个任务运行多久。虚拟运行时间与基于任务优先级的衰减因子有关,更低优先级的任务比更高优先级的任务具有更高衰减速率。对于正常优先级的任务(友好值为 0),虚拟运行时间与实际物理运行时间是相同的。
因此,如果一个默认优先级的任务运行 200ms,则它的虚拟运行时间也为 200ms。然而,如果一个较低优先级的任务运行 200ms,则它的虚拟运行时间将大于 200ms。同样,如果一个更高优先级的任务运行 200ms,则它的虚拟运行时间将小于 200ms。当决定下步运行哪个任务时,调度程序只需选择具有最小虚拟运行时间的任务。此外,一个更高优先级的任务如成为可运行,就会抢占低优先级任务。
下面分析一下 CFS 调度程序是如何工作的。假设有两个任务,它们具有相同的友好值。一个任务是 I/O 密集型而另一个为 CPU 密集型。通常,I/O 密集型任务在运行很短时间后就会阻塞以便等待更多的 I/O;而 CPU 密集型任务只要有在处理器上运行的机会,就会用完它的时间片。
因此,I/O 密集型任务的虚拟运行时间最终将会小于 CPU 密集型任务的,从而使得 I/O 密集型任务具有更高的优先级。这时,如果 CPU 密集型任务在运行,而 I/O 密集型任务变得有资格可以运行(如该任务所等待的 I/O 已成为可用),那么 I/O 密集型任务就会抢占 CPU 密集型任务。
死锁
两个线程互相持有对方所需要的资源,互相等待,谁也无法继续执行下去。
条件:互斥、持有并等待、不可抢占、循环等待
处理方法:死锁预防(破坏四个条件中的一个)、死锁避免(银行家)、死锁检测与消除
死锁预防: 互斥-使得共享资源其他进程也能访问。但会导致系统无法正常运行,无法确定执行 占用并等待-进程要么拥有所有资源要么不拿资源,但会导致资源利用率低且易导致饥饿 不抢占-进程可以抢占其他进程的资源,但和互斥条件冲突,只能将其他进程kill 循环等待-对资源进行排序,要求每个进程按照资源的顺序进行申请
进程、线程、协程的区别(详细)
进程、线程,都是有内核进行调度,有 CPU 时间片的概念,进行 抢占式调度。
协程(用户级线程)完全由用户自己的程序进行调度(协作式调度),需要协程自己主动把控制权转让出去之后,其他协程才能被执行到。
协程,是在应用层模拟的线程,他避免了上下文切换的额外耗费,兼顾了多线程的优点。简化了高并发程序的复杂度。协程还是通过共享内存通讯.
目前的协程框架一般都是设计成 1:N 模式。 所谓 1:N 就是一个线程作为一个容器里面放置多个协程。 那么谁来适时的切换这些协程?答案是有协程自己主动让出CPU, 也就是每个协程池里面有一个调度器,这个调度器是被动调度的。 意思就是他不会主动调度。 而且当一个协程发现自己执行不下去了(比如异步等待网络的数据回来,但是当前还没有数据到), 这个时候就可以由这个协程通知调度器, 这个时候执行到调度器的代码,调度器根据事先设计好的调度算法找到当前最需要CPU的协程。 切换这个协程的CPU上下文把CPU的运行权交个这个协程,直到这个协程出现执行不下去需要等等的情况, 或者它调用主动让出CPU的API之类,触发下一次调度。对的没错就是类似于 领导人模式那么这个实现有没有问题? 其实是有问题的,假设这个线程中有一个协程是CPU密集型的他没有IO操作,也就是自己不会主动触发调度器调度的过程, 那么就会出现其他协程得不到执行的情况,所以这种情况下需要程序员自己避免。
深拷贝、浅拷贝(java)
浅拷贝:只是增加了一个指针指向原内存。
深拷贝:深拷贝是增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存。
写时复制
系统调用 fork() 创建了父进程的一个复制,以作为子进程。传统上,fork() 为子进程创建一个父进程地址空间的副本,复制属于父进程的页面。然而,考虑到许多子进程在创建之后立即调用系统调用 exec(),父进程地址空间的复制可能没有必要。
因此,可以采用一种称为写时复制的技术,它通过允许父进程和子进程最初共享相同的页面来工作。这些共享页面标记为写时复制,这意味着如果任何一个进程写入共享页面,那么就创建共享页面的副本。
当使用写时复制技术时,仅复制任何一进程修改的页面,所有未修改的页面可以由父进程和子进程共享。
注意:采用 vfork(),父进程被挂起,子进程使用父进程的地址空间。因为 vfork() 不采用写时复制,如果子进程修改父地址空间的任何页面,那么这些修改过的页面对于恢复的父进程是可见的。因此,应谨慎使用 vfork(),以确保子进程不会修改父进程的地址空间。
mmap内存映射
内存映射,简而言之就是将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,同样,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间<---->用户空间两者之间需要大量数据传输等操作的话效率是非常高的。
缓冲区溢出
例如栈溢出导致返回值或者方法返回地址被修改,进而导致被攻击。
为何需要用户态、内核态
应用程序不能直接访问外设,需要内核在其中充当被信任的第三方,只有内核才能执行特权指令。同时也是方便应用程 序通过内核提供的接口来更方便编写操作外设的程序。即不用关注和外设具体打交道的细节,其应该由操作系统来完成。
read ahead文件预读
所谓预读,是指文件系统为应用程序一次读出比预期更多的文件内容并缓存在page cache中,这样下一次读请求到来时部分页面直接从page cache读取即可。当然,这个细节对应用程序透明,应用程序可能的感觉就是下次读的速度会更快
x86是大端还是小端,为什么
小端
select、poll、epoll
select:bitmap、遍历bitmap获取数据、bitmap不可重用
poll:结构体数组(三个字段存在状态位)、遍历判断状态位获取数据、结构体数组可重用
epoll:类似poll结构体(两个字段 不存在状态位)、由内核对其进行排序并返回数量,直接遍历数量即可获得socket
管道底层实现
无名管道:内存中实现(内核中的缓存),如|,有名管道:磁盘实现,如mkfifo。只有父子进程之间可以通过匿名管道。
管道采用半双工通信,使用一个管道一般的规则是读管道数据的进程关闭管道写入端,而写管道进程关
闭其读出端。管道传输的数据是无格式的且大小受限。
父子进程之间的匿名管道,因为子进程复制父进程创建的文件描述符,所以各自拥有两个fd[0]、fd[1],这样就实现了进程间通信。
系统调用的具体过程,如何实现
应用程序主动向操作系统发起服务请求,应用程序请求操作系统提供服务,切换到内核态,内核态响应服务然后完成后返回。
select、poll、epoll机制
select采用轮询机制,耗时较大,且监听的socket有限(可以改变)
select和poll只支持LT工作模式,epoll的默认的工作模式是LT模式,还支持ET(边缘触发)模式。
水平触发:①对于读操作,只要缓冲内容不为空,LT模式返回读就绪。②对于写操作,只要缓冲区还不满,LT模式会返回写就绪。
边缘触发:在ET模式下, 缓冲区从不可读变成可读,会唤醒应用进程,缓冲区数据变少的情况,则不会再唤醒应用进程
举例1: 读缓冲区刚开始是空的 读缓冲区写入2KB数据 水平触发和边缘触发模式此时都会发出可读信号 收到信号通知后,读取了1KB的数据,读缓冲区还剩余1KB数据 水平触发会再次进行通知,而边缘触发不会再进行通知 举例2:(以脉冲的高低电平为例) 水平触发:0为无数据,1为有数据。缓冲区有数据则一直为1,则一直触发。 边缘触发发:0为无数据,1为有数据,只要在0变到1的上升沿才触发。
epoll红黑树
epoll和poll的一个很大的区别在于,poll每次调用时都会存在一个将pollfd结构体数组中的每个结构体元素从用户态向内核态中的一个链表节点拷贝的过程,而内核中的这个链表并不会一直保存,当poll运行一次就会重新执行一次上述的拷贝过程,这说明一个问题:poll并不会在内核中为要监听的文件描述符长久的维护一个数据结构来存放他们,而epoll内核中维护了一个内核事件表,它是将所有的文件描述符全部都存放在内核中,系统去检测有事件发生的时候触发回调,当你要添加新的文件描述符的时候也是调用epoll_ctl函数使用EPOLL_CTL_ADD宏来插入,epoll_wait也不是每次调用时都会重新拷贝一遍所有的文件描述符到内核态。当我现在要在内核中长久的维护一个数据结构来存放文件描述符,并且时常会有插入,查找和删除的操作发生,这对内核的效率会产生不小的影响,因此需要一种插入,查找和删除效率都不错的数据结构来存放这些文件描述符,那么红黑树当然是不二的人选。
epoll与select、poll的对比
1. 用户态将文件描述符传入内核的方式
select:创建3个文件描述符集并拷贝到内核中,分别监听读、写、异常动作。这里受到单个进程可以打开的fd数量限制,默认是1024。poll:将传入的struct pollfd结构体数组拷贝到内核中进行监听。epoll:执行epoll_create会在内核的高速cache区中建立一颗红黑树以及就绪链表(该链表存储已经就绪的文件描述符)。接着用户执行的epoll_ctl函数添加文件描述符会在红黑树上增加相应的结点。
2. 内核态检测文件描述符读写状态的方式
select:采用轮询方式,遍历所有fd,最后返回一个描述符读写操作是否就绪的mask掩码,根据这个掩码给fd_set赋值。poll:同样采用轮询方式,查询每个fd的状态,如果就绪则在等待队列中加入一项并继续遍历。epoll:采用回调机制。在执行epoll_ctl的add操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将文件描述符放在就绪链表中。
3. 找到就绪的文件描述符并传递给用户态的方式
select:将之前传入的fd_set拷贝传出到用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。poll:将之前传入的fd数组拷贝传出用户态并返回就绪的文件描述符总数。用户态并不知道是哪些文件描述符处于就绪态,需要遍历来判断。epoll:epoll_wait只用观察就绪链表中有无数据即可,最后将链表的数据返回给数组并返回就绪的数量。内核将就绪的文件描述符放在传入的数组中,所以只用遍历依次处理即可。这里返回的文件描述符是通过mmap让内核和用户空间共享同一块内存实现传递的,减少了不必要的拷贝。
4. 重复监听的处理方式
select:将新的监听文件描述符集合拷贝传入内核中,继续以上步骤。poll:将新的struct pollfd结构体数组拷贝传入内核中,继续以上步骤。epoll:无需重新构建红黑树,直接沿用已存在的即可。
epoll更高效的原因
select和poll的动作基本一致,只是poll采用链表来进行文件描述符的存储,而select采用fd标注位来存放,所以select会受到最大连接数的限制,而poll不会。select、poll、epoll虽然都会返回就绪的文件描述符数量。但是select和poll并不会明确指出是哪些文件描述符就绪,而epoll会。造成的区别就是,系统调用返回后,调用select和poll的程序需要遍历监听的整个文件描述符找到是谁处于就绪,而epoll则直接处理即可。select、poll都需要将有关文件描述符的数据结构拷贝进内核,最后再拷贝出来。而epoll创建的有关文件描述符的数据结构本身就存于内核态中,系统调用返回时利用mmap()文件映射内存加速与内核空间的消息传递:即epoll使用mmap减少复制开销。select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制。造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大影响,除非活跃的socket很多。epoll的边缘触发模式效率高,系统不会充斥大量不关心的就绪文件描述符虽然epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
计网
TCP
特点:可靠、面向连接、点对点通信、全双工通信、拥塞控制、流量控制。(ARQ)
close_wait:防止被动关闭方仍然存在数据没有发送完。
time_wait为何等待两个MSL:①保证自己发送的ACK能够到达被动方,2MSL保证能够超时重传;②保证本次连接中产生的所有报文都已经在网络中消失,不会影响下一个连接。
time_wait太多:调整内核参数。即:①重用处于time_wait的socket;②快速回收处于time_wait的socket;③降低socket处于time_wait的时间④降低系统默认设置的time_wait的socket最大数量;⑤扩大可用于socket的端口范围
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭; --用于防御半连接攻击 net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME_WAIT sockets重新用于新的TCP连接,默认为0,表示关闭; net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME_WAIT sockets的快速回收,默认为0,表示关闭。 net.ipv4.tcp_fin_timeout 修改系统默认的 TIMEOUT 时间
流量控制:发送窗口不能大于接收窗口。
拥塞控制:慢开始(小于门限,指数增长)、拥塞避免(大于门限、线性增长)、快重传(收到三个重复确认时,直接重传丢失报文而非等待超时)、快恢复(门限值减半,拥塞窗口减半,直接开始拥塞避免)。对于超时的情况,门限减半,拥塞窗口直接从0开始执行慢开始算法。
查看time_wait连接数
netstat -ae | grep “TIME_WAIT” | wc -l
拥塞控制为何是3个ACK才快重传
TCP segment乱序有2/5= 40%的概率会造成A收到三次duplicated ACK(N);而如果N丢了,则会100%概率A会收到三次duplicated ACK(N);
UDP
不可靠、无连接、RIP协议使用、实现可靠传输需要上层应用层帮助(RUDPS、RTP、UDT模仿TCP)。不对数据包做任何操作,直接将其发送,也不考虑接收方。
丢包问题一般发生在哪一层
各层均可能
ICMP协议作用
类型:目标不可到达、源抑制和超时报文
是ip报文的组成部分。
电脑联网失败,发生在哪一层
各层均可能
TCP和UDP的报头长度
udp8字节,tcp20字节
怎么构建一个请求超时?
后端接收到请求不处理也不拒绝
SCTP了解过吗, 介绍一下
流控制传输协议(SCTP,Stream Control Transmission Protocol)是一种在网络连接两端之间同时传输多个数据流的协议。
SCTP是面向消息的(message-oriented)。它提供各个记录的按序递送服务。与UDP一样,由发送端写入的每一条记录的长度随数据一道传递给接收端应用。
SCTP能给在所连接的端点之间提供多个流,每个流各自可靠地按序递送消息。一个流上某个消息的丢失不会阻塞同一关联其他流上消息的投递。这种做法与TCP正好相反,就TCP而言,在单一字节流中任何位置的字节丢失都将在阻塞该连接上其后所有数据的递送,直到该丢失被修复为止。
TELNET
Telnet协议是TCP/IP协议家族中的一员,是Internet远程登陆服务的标准协议和主要方式。它为用户提供了在本地计算机上完成远程主机工作的能力。在终端使用者的电脑上使用telnet程序,用它连接到服务器。终端使用者可以在telnet程序中输入命令,这些命令会在服务器上运行,就像直接在服务器的控制台上输入一样。可以在本地就能控制服务器。要开始一个telnet会话,必须输入用户名和密码来登录服务器。Telnet是常用的远程控制Web服务器的方法。
需要连接的机器开启telnet服务。
半连接攻击(SYN_FLOOD)
半连接就是通过不断地构造客户端的SYN连接数据包发向服务端,等到服务端的半连接队列满的时候,后续的正常用户的连接请求将会被丢弃,从而无法连接到服务端。此为半连接攻击方式。
可通过开启SYN Cookies来解决,即通过发送方的信息(端口、ip)和接收方的信息计算出一个cookie,并将其作为序列号进行回复,然后cookie对应一个时间范围,在时间范围内的ack都是合法的,不进入半连接队列,直接完成三次握手。
全连接攻击
全连接攻击:是通过消费服务端进程数和连接数,只连接而不进行发送数据的一种攻击方式。当客户端连接到服务端,仅仅只是连接,此时服务端会为每一个连接创建一个进程来处理客户端发送的数据。但是客户端只是连接而不发送数据,此时服务端会一直阻塞在recv或者read的状态,如此一来,多个连接,服务端的每个连接都是出于阻塞状态从而导致服务端的崩溃。
可通过设置超时时间来解决。
为何需要三次握手而不是两次?四次挥手而不是三次?
个人理解:三次握手分别保证客户端发送数据的能力、服务端发送数据和接收数据的能力以及客户端接收数据的能力。如果没有第三次握手,就无法保证客户端有接收数据的能力。同时三次握手保证了序列号的一致性以及双方连接建立的完整性(两次握手可能存在服务端的ack延迟到达客户端,此时客户端已经放弃连接了而服务端却以为建立好了连接)。而四次挥手则是因为被动关闭方可能还存在数据未发送完全,需要等待被动方发送完数据并主动发出FIN才能保证数据发送完毕。
http:
状态码:1xx信息、2xx成功、3xx重定向、4xx客户端错误、5xx服务端错误
100——继续、200——成功、301永久重定向、302临时重定向、400 Bad Request、404找不到页面、401未授权、403拒绝请求、500服务器内部错误。
301 302区别:301表示旧地址A的资源已经被永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址;302表示旧地址A的资源还在(仍然可以访问),这个重定向只是临时地从旧地址A跳转到地址B,搜索引擎会抓取新的内容而保存旧的网址。
400——1、语义有误,当前请求无法被服务器理解。除非进行修改,否则客户端不应该重复提交这个请求。2、请求参数有误。
403——服务器理解了请求但是拒绝执行
http头部
通用头部: cache-control:请求和响应遵循的缓存机制 pragma:Pragma头域用来包含实现特定的指令,最常用的是Pragma:no-cache connection:是否保持长连接 keep-alive等等 date:时间 请求头: accept:接收什么类型 text/html等等 accept-encoding、accept-language referer:从何而来 如何跳转过来的 user-agent:浏览器表明自己身份 响应头: age:当代理服务器用自己缓存的实体去响应请求时,用该头部表明该实体从产生到现在经过多长时间了。 server:表明自身是什么软件 Server:Apache/2.0.61 (Unix) Accept-Ranges:bytes表示接受、none表示不接受 实体头: allow:支持哪些方法 content-length:响应对象长度 content-type:响应对象的类型 last-modified:对象的最后修改时间
http各版本
1)HTTP 1.0:
- 请求与响应支持 HTTP 头,响应含状态行,增加了状态码,
- 支持 HEAD,POST 方法
- 支持传输 HTML 文件以外其他类型的内容
HTTP1.0 使用的是非持久连接,主要缺点是客户端必须为每一个待请求的对象建立并维护一个新的连接,即每请求一个文档就要有两倍RTT的开销。因为同一个页面可能存在多个对象,所以非持久连接可能使一个页面的下载变得十分缓慢,而且这种短连接增加了网络传输的负担。(RTT(Round Trip Time):一个连接的往返时间,即数据发送时刻到接收到确认的时刻的差值)
2)HTTP 1.1:
- 支持长连接。
- 在HTTP1.0的基础上引入了更多的缓存控制策略。
- 引入了请求范围设置,优化了带宽。
- 在错误通知管理中新增了错误状态响应码。
- 增加了Host头处理,可以传递主机名(hostname)。
**缺点:**传输内容是明文,不够安全
3)HTTPS
- HTTPS运行在安全套接字协议(Secure Sockets Layer,SSL )或传输层安全协议(Transport Layer Security,TLS)之上,所有在TCP中传输的内容都需要经过加密。
- 连接方式不同,HTTP的端口是80,HTTPS的端口是443。
- HTTPS可以有效防止运营商劫持。
注:SSL运行在TCP之上
4)HTTP 1.x优化(SPDY)
SPDY 并不是新的一种协议,而是在 HTTP 之前做了一层会话层。为了达到减少页面加载时间的目标,SPDY 引入了一个新的二进制分帧数据层,以实现优先次序、最小化及消除不必要的网络延迟,目的是更有效地利用底层 TCP 连接。
- 多路复用,为多路复用设立了请求优先级。
- 对header部分进行了压缩。
- 引入了HTTPS加密传输。
- 客户端可以在缓存中取到之前请求的内容。
5)HTTP2.0(SPDY的升级版):
- HTTP2.0支持明文传输,而HTTP 1.X强制使用SSL/TLS加密传输。
- 和HTTP 1.x使用的header压缩方法不同。
- HTTP2.0 基于二进制格式进行解析,而HTTP 1.x基于文本格式进行解析。
- 多路复用,HTTP1.1是多个请求串行化单线程处理,HTTP 2.0是并行执行,一个请求超时并不会影响其他请求。
HTTP2.0的多路复用提升了网页性能:
- 在 HTTP1 中浏览器限制了同一个域名下的请求数量(Chrome下一般是六个),当在请求很多资源的时候,由于队头阻塞,当浏览器达到最大请求数量时,剩余的资源需等待当前的六个请求完成后才能发起请求。
- HTTP2 中引入了多路复用的技术,这个技术可以只通过一个TCP连接就可以传输所有的请求数据。多路复用可以绕过浏览器限制同一个域名下的请求数量的问题,进而提高了网页的性能。
注意:
- 主流浏览器只支持基于TLS部署的HTTP 2.0协议,所以要将网站升级为HTTP 2.0,就需要先升级为HTTPS。
- HTTP 2.0完全兼容HTTP 1.x,所以对于部署了HTTP 2.0的网站可以自动向下兼容HTTP 1.X。
6) HTTP 3.0 (QUIC):
QUIC (Quick UDP Internet Connections),快速 UDP 互联网连接。QUIC是基于UDP协议的。
两个主要特性:
(1)线头阻塞(HOL)问题的解决更为彻底:
基于TCP的HTTP/2,尽管从逻辑上来说,不同的流之间相互独立,不会相互影响,但在实际传输方面,数据还是要一帧一帧的发送和接收,一旦某一个流的数据有丢包,则同样会阻塞在它之后传输的流数据传输。而基于UDP的QUIC协议则可以更为彻底地解决这样的问题,让不同的流之间真正的实现相互独立传输,互不干扰。
(2)切换网络时的连接保持:
当前移动端的应用环境,用户的网络可能会经常切换,比如从办公室或家里出门,WiFi断开,网络切换为3G或4G。基于TCP的协议,由于切换网络之后,IP会改变,因而之前的连接不可能继续保持。而基于UDP的QUIC协议,则可以内建与TCP中不同的连接标识方法,从而在网络完成切换之后,恢复之前与服务器的连接。
线头阻塞(HOL)
TCP协议中,序号为1、3的数据包接收到后,不能直接传递给上层,需要等待到序号为2的数据包到达,这种等待的情况称为线头阻塞。
https身份认证
身份认证(CA数字证书):
https协议中身份认证的部分是由数字证书来完成的,证书由公钥、证书主题、数字签名等内容组成,在客户端发起SSL请求后,服务端会将数字证书发给客户端,客户端对证书进行验证,并获取用于秘钥交换的非对称秘钥
数字证书作用:
- 身份授权 确保浏览器访问的网站是经过CA验证的可信任网站
- 分发公钥 每个数字证书都包含了注册者生成的公钥。在SSL握手时通过certificate消息传输给客户端
数字证书验证:
申请者拿到CA的证书并部署在网站服务器端,浏览器发起握手接收到证书后,如何确认这个证书就是CA签发的呢?怎样避免第三方伪造这个证书?答案就是数字签名(digital signature)。数字签名是证书的防伪标签,目前使用最广泛的是SHA-RSA(SHA用于哈希算法,RSA用于非对称加密算法)数字签名
浏览器如何验证CA证书
首先,浏览器通过URL网址去请求服务端,服务端接收到请求后,就会给浏览器发送一个自己的CA数字证书 然后,浏览器接收到证书以后,开始验证。首先从证书中得知证书的颁发机构,然后从浏览器系统中去寻找此颁发机构的根证书(世界上权威CA机构的根证书都是预先嵌入到浏览器中的),如果在浏览器系中没有找到对应的根证书,就代表此机构不是受信任的,那么就会警告无法确认证书的真假,比如以前打开12360网站就会提示。 之后,如果找到了证书颁发机构的根证书,那么就从根证书中取得那个根公钥,用根公钥去解密此证书的数字签名,成功解密的话就得到证书的指纹和指纹算法,指纹是证书内容通过指纹算法计算得到的一个hash值,这里我们称之为h1,h1代表证书的原始内容;然后用指纹算法对当前接收到的证书内容再进行一次hash计算得到另一个值h2,h2则代表当前证书的内容,如果此时h1和h2是相等的,就代表证书没有被修改过。如果证书被篡改过,h2和h1是不可能相同的。 假如证书上的指纹是不法分子伪造的,伪造是没有用的,因为你伪造的指纹不可能用CA机构的根私钥去加密(根私钥是CA机构绝对保密的),伪造者只能拿自己的秘钥去加密这个伪造的指纹,但当我们拿机构的根公钥去解密伪造指纹的时候是不可能成功的(加密内容只能由一对公钥私钥解密) 在证书没有被修改过的基础上,再检查证书上的使用者的URL(比如csdn.net)和我们请求的URL是否相等,如果相等,那么就可以证明当前浏览器链接的网址也是正确的,而不是一些钓鱼网之类的。 但如果浏览器的连接被某个中间人截取了,中间人也可以发一个由权威的CA机构颁发的证书给浏览器,然后也可以通过证书没有被篡改的验证,但是在证书没有被篡改的前提下,通过对比证书上的URL和我们请求的URL是否相同,我们还是可以判断当前证书是不是服务器发的证书。可以这么理解,因为URL具有唯一性,所以中间人的证书的上的URL和我们的证书的URL是不可能相同的,如果中间人修改了自己证书上的URL,那么就通过不了证书没有被篡改的验证,所以中间人的证书也是欺骗不了我们的。 然后生成对称密钥即可。
https密钥协商机制
1)非对称加密:
客户端发送 ClientHello(包含支持的协议版本、加密算法和 随机数A (Client random))到服务端 服务端返回 ServerHello、公钥、证书、随机数B (Server random) 到客户端 客户端使用CA证书验证返回证书无误后。生成 随机数C (Premaster secret),用公钥对其加密,发送到服务端 服务端用 私钥 解密得到 随机数C (Premaster secret),随后根据已经得到的 随机数ABC生成对称密钥(hello的时候确定的加密算法),并对需要发送的数据进行对称加密发送 客户端使用对称密钥(客户端也用随机数ABC生成对称密钥)对数据进行解密。 双方手持对称密钥 使用对称加密算法通讯
2)DH密钥协商:可以做到——“通讯双方在完全没有对方任何预先信息的条件下通过不安全信道创建起一个密钥”
但无法防止中间人篡改。需要和RSA配合签名机制使用。
1. 客户端先连上服务端 2. 服务端生成一个随机数 s 作为自己的私钥,然后根据算法参数计算出公钥 S(算法参数通常是固定的) 3. 服务端使用某种签名算法把“算法参数(模数p,基数g)和服务端公钥S”作为一个整体进行签名 4. 服务端把“算法参数(模数p,基数g)、服务端公钥S、签名”发送给客户端 5. 客户端收到后验证签名是否有效 6. 客户端生成一个随机数 c 作为自己的私钥,然后根据算法参数计算出公钥 C 7. 客户端把 C 发送给服务端 8. 客户端和服务端(根据上述 DH 算法)各自计算出 k 作为会话密钥
如何防范偷窥(嗅探):
嗅探者可以通过监视网络传输,得到算法参数(模数p,基数g)以及双方的公钥,但是【无法】推算出双方的私钥,也【无法】推算出会话密钥(这是由 DH 算法在数学上保证的)
如何防范篡改(假冒身份)
攻击方式1:攻击者可以第4步篡改数据(修改算法参数或服务端公钥)。但因为这些信息已经进行过数字签名。篡改之后会被客户端发现。
攻击方式2:攻击者可以在第7步篡改客户端公钥。这步没有签名,服务端收到数据后不会发现被篡改。但是,攻击者篡改之后会导致“服务端与客户端生成的会话密钥【不一致】”。在后续的通讯步骤中会发现这点,并导致通讯终止。
TLS
整个TLS传输的过程如下:
(1)TCP三次握手
(2)SSL的ClientHello和ServerHello和对应的秘钥交换KeyExchange
(3)Client和Server互相ChangeCipherSpec通知进入加密模式,此时可以进入数据传输状态
(4)应用数据传输过程
(5)应用数据传输完成,TCP两次挥手
抛开TCP连接和数据包文传输的部分,TLS握手部分将使用2个RTT。
前向安全性
指的是长期使用的主密钥泄漏不会导致过去的会话密钥泄漏
http一些参数
Content-Length:指明响应体的数据大小
content-type:数据格式。
- application/json:JSON数据格式
- text/html:HTML格式
- text/xml:XML格式
Connection: keep-alive:保持长连接
http——chunk
当客户端向服务器请求一个静态页面或者一张图片时,服务器可以很清楚的知道内容大小,然后通过Content-Length消息首部字段告诉客户端需要接收多少数据。但是如果是动态页面等时,服务器是不可能预先知道内容大小,这时就可以使用Transfer-Encoding:chunk模式来传输数据了。即如果要一边产生数据,一边发给客户端,服务器就需要使用"Transfer-Encoding: chunked"这样的方式来代替Content-Length。
在进行chunked编码传输时,在回复消息的头部有Transfer-Encoding: chunked
编码使用若干个chunk组成,由一个标明长度为0的chunk结束。每个chunk有两部分组成,第一部分是该chunk的长度,第二部分就是指定长度的内容,每个部分用CRLF隔开。在最后一个长度为0的chunk中的内容是称为footer的内容,是一些没有写的头部内容。
chunk编码格式如下:
[chunk size][\r\n][chunk data][\r\n][chunk size][\r\n][chunk data][\r\n][chunk size = 0][\r\n][\r\n]
chunk size是以十六进制的ASCII码表示,比如:头部是3134这两个字节,表示的是1和4这两个ascii字符,被http协议解释为十六进制数14,也就是十进制的20,后面紧跟[\r\n](0d 0a),再接着是连续的20个字节的chunk正文。chunk数据以0长度的chunk块结束,也就是(30 0d 0a 0d 0a)。
原理
HTTP 1.1引入分块传输编码提供了以下几点好处:
HTTP分块传输编码允许服务器为动态生成的内容维持HTTP持久链接。通常,持久链接需要服务器在开始发送消息体前发送Content-Length消息头字段,但是对于动态生成的内容来说,在内容创建完之前是不可知的。
分块传输编码允许服务器在最后发送消息头字段。对于那些头字段值在内容被生成之前无法知道的情形非常重要,例如消息的内容要使用散列进行签名,散列的结果通过HTTP消息头字段进行传输。没有分块传输编码时,服务器必须缓冲内容直到完成后计算头字段的值并在发送内容前发送这些头字段的值。
DNS
浏览器输入一个地址,发生了什么?
根据域名查找ip地址(浏览器缓存——本机host缓存——DNS系统调用——本地DNS服务器缓存——递归查询直到获得ip地址——可能因为负载均衡每次获得不同的ip地址),然后向该ip发送http请求,服务器响应回复html文档,浏览器解析html并根据content-type判断如何处理(显示、下载等等),浏览器获取html文档内嵌的图片、音频、js等等,最后浏览器还可以发送ajax异步请求。
DNS区域传输的时候使用TCP协议:辅域名服务器会定时(一般3小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,会执行一次区域传送,进行数据同步。区域传送使用TCP而不是UDP,因为数据同步传送的数据量比一个请求应答的数据量要多得多。
域名解析时使用UDP协议
DNS over TLS / HTTPS
加密的DNS协议。但是延时也很高,需要耗费4 RTT来保证安全。
cookie和session、token
cookie:客户端会话技术,存储数据在客户端浏览器,默认浏览器关闭后清除,能存放的数据有限且安全性较低。存放sessionID,之后的请求默认携带。
session:服务端会话技术,存储在服务端,用来保存状态,依赖于cookie,存放的数据无限制且安全性高,但需要单独存储,耗费空间。
token:无状态的令牌。采用签名的方式来验证(私钥签名、公钥验证),每次传输过来的数据再次进行签名以对比。再次请求需要手动添加token。
当用户首次与Web服务器建立连接的时候,服务器会给用户分发一个 SessionID作为标识。SessionID是一个由24个字符组成的随机字符串。用户每次提交页面,浏览器都会把这个SessionID包含在 HTTP头中提交给Web服务器,这样Web服务器就能区分当前请求页面的是哪一个客户端。这个SessionID就是保存在客户端的,属于客户端Session。 其实客户端Session默认是以cookie的形式来存储的,所以当用户禁用了cookie的话,服务器端就得不到SessionID。
http和rpc区别
RPC:可以基于TCP协议,也可以基于HTTP协议 HTTP:基于HTTP协议 RPC:可以基于thrift实现高效的二进制传输 HTTP:大部分是通过json来实现的,字节大小和序列化耗时都比thrift要更消耗性能 RPC:基本都自带了负载均衡策略 HTTP:需要配置Nginx,HAProxy来实现 RPC主要用于公司内部的服务调用,性能消耗低,传输效率高,服务治理方便。 HTTP主要用于对外的异构环境,浏览器接口调用,APP接口调用,第三方接口调用等。
nginx
工作流程: 1、用户通过域名发出访问Web服务器的请求,该域名被DNS服务器解析为反向代理服务器的IP地址; 2、反向代理服务器接受用户的请求; 3、反向代理服务器在本地缓存中查找请求的内容,找到后直接把内容发送给用户; 4、如果本地缓存里没有用户所请求的信息内容,反向代理服务器会代替用户向源服务器请求同样的信息内容,并把信息内容发给用户,如果信息内容是非缓存的还会把它保存到缓存中。 -------- Nginx模块: Nginx有五大优点:模块化、事件驱动、异步、非阻塞、多进程单线程。由内核和模块组成的,其中内核完成的工作比较简单,仅仅通过查找配置文件将客户端请求映射到一个location block,然后又将这个location block中所配置的每个指令将会启动不同的模块去完成相应的工作。 -------- 作用: * 保护了真实的web服务器,保证了web服务器的资源安全 * 节约了有限的IP地址资源 * 减少WEB服务器压力,提高响应速度 - 反向代理就是通常所说的web服务器加速,它是一种通过在繁忙的web服务器和外部网络之间增加一个高速的web缓冲服务器来降低实际的web服务器的负载的一种技术。反向代理是针对web服务器提高加速功能,作为代理缓存,它并不是针对浏览器用户,而针对一台或多台特定的web服务器,它可以代理外部网络对内部网络的访问请求。 - 反向代理服务器会强制将外部网络对要代理的服务器的访问经过它,这样反向代理服务器负责接收客户端的请求,然后到源服务器上获取内容,把内容返回给用户,并把内容保存到本地,以便日后再收到同样的信息请求时,它会把本地缓存里的内容直接发给用户,以减少后端web服务器的压力,提高响应速度。因此Nginx还具有缓存功能。 * 请求的统一控制,包括设置权限、过滤规则等; * 区分动态和静态可缓存内容; * 实现负载均衡,内部可以采用多台服务器来组成服务器集群,外部还是可以采用一个地址访问; * 解决Ajax跨域问题; * 作为真实服务器的缓冲,解决瞬间负载量大的问题;
接口幂等性
接口幂等用于表示任意多次请求执行的结果均与一次请求执行的结果相同
实现幂等性的关键步骤分为以下三个:
(1)每个请求操作必须有唯一的 ID,而这个 ID 就是用来表示此业务是否被执行过的关键凭证,例如,订单支付业务的请求,就要使用订单的 ID 作为幂等性验证的 Key;
(2)每次执行业务之前必须要先判断此业务是否已经被处理过;
(3)第一次业务处理完成之后,要把此业务处理的状态进行保存,比如存储到 Redis 中或者是数据库中,这样才能防止业务被重复处理
get请求为幂等、post则不是
理解restful(Representational State Transfer)
(1)每一个URI代表一种资源;
(2)客户端和服务器之间,传递这种资源的某种表现层;
(3)客户端通过四个HTTP动词,对服务器端资源进行操作,实现"表现层状态转化"。
RESTful 架构的核心规范与约束:统一接口 分为四个子约束: 1.每个资源都拥有一个资源标识,每个资源的资源标识可以用来唯一地标明该资源 2.消息的自描述性 3.资源的自描述性。 4.HATEOAS Hypermedia As The Engine Of Application State(超媒体作为应用状态引擎) 即客户只可以通过服务端所返回各结果中所包含的信息来得到下一步操作所需要的信息,如到底是向哪个URL发送请求等。也就是说,一个典型的REST服务不需要额外的文档标示通过哪些URL访问特定类型的资源,而是通过服务端返回的响应来标示到底能在该资源上执行什么样的操作
使用标准的状态码
GET 安全且幂等、获取表示、变更时获取表示(缓存) 200(OK) - 表示已在响应中发出 204(无内容) - 资源有空表示 301(Moved Permanently) - 资源的URI已被更新 303(See Other) - 其他(如,负载均衡) 304(not modified)- 资源未更改(缓存) 400 (bad request)- 指代坏请求(如,参数错误) 404 (not found)- 资源不存在 406 (not acceptable)- 服务端不支持所需表示 500 (internal server error)- 通用错误响应 503 (Service Unavailable)- 服务端当前无法处理请求 POST 不安全且不幂等 使用服务端管理的(自动产生)的实例号创建资源 创建子资源 部分更新资源 如果没有被修改,则不过更新资源(乐观锁) 200(OK)- 如果现有资源已被更改 201(created)- 如果新资源被创建 202(accepted)- 已接受处理请求但尚未完成(异步处理) 301(Moved Permanently)- 资源的URI被更新 303(See Other)- 其他(如,负载均衡) 400(bad request)- 指代坏请求 404 (not found)- 资源不存在 406 (not acceptable)- 服务端不支持所需表示 409 (conflict)- 通用冲突 412 (Precondition Failed)- 前置条件失败(如执行条件更新时的冲突) 415 (unsupported media type)- 接受到的表示不受支持 500 (internal server error)- 通用错误响应 503 (Service Unavailable)- 服务当前无法处理请求 PUT 不安全但幂等 用客户端管理的实例号创建一个资源 通过替换的方式更新资源 如果未被修改,则更新资源(乐观锁) 200 (OK)- 如果已存在资源被更改 201 (created)- 如果新资源被创建 301(Moved Permanently)- 资源的URI已更改 303 (See Other)- 其他(如,负载均衡) 400 (bad request)- 指代坏请求 404 (not found)- 资源不存在 406 (not acceptable)- 服务端不支持所需表示 409 (conflict)- 通用冲突 412 (Precondition Failed)- 前置条件失败(如执行条件更新时的冲突) 415 (unsupported media type)- 接受到的表示不受支持 500 (internal server error)- 通用错误响应 503 (Service Unavailable)- 服务当前无法处理请求 DELETE 不安全但幂等 删除资源 200 (OK)- 资源已被删除 301 (Moved Permanently)- 资源的URI已更改 303 (See Other)- 其他,如负载均衡 400 (bad request)- 指代坏请求 404 (not found)- 资源不存在 409 (conflict)- 通用冲突 500 (internal server error)- 通用错误响应 503 (Service Unavailable)- 服务端当前无法处理请求
路由器和交换机具体实现了什么功能,路由选择如何实现
路由器属于网络层,使用ip地址通信,连接局域网和外网。
交换机属于数据链路层,使用mac地址通信,工作在局域网内部。
介绍下IPV6
128位,16字节,16进制
jwt(JSON Web Token)
JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:
- Header、Payload、Signature
header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。例如:
{ 'alg': "HS256", 'typ': "JWT" }
用Base64对这个JSON编码就得到JWT的第一部分
payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private。
Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。
Public claims : 可以随意定义。
Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。 下面是一个例子:
{ "sub": '1234567890', "name": 'john', "admin":true }
对payload进行Base64编码就得到JWT的第二部分
注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的。
Signature
为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可。例如:
HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)
签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。
CSRF跨站点请求伪造(Cross—Site Request Forgery)
攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。
反爬策略
- 限制IP地址单位时间的访问次数
- 用户登录才能访问网站内容, 若识别为爬虫账号,封禁IP
- header, User-Agent检查用户所用客户端的种类和版本, 在请求头中加入CSRF_token识别用户请求(参考form表单验证)
- Referer, 检查请求由哪里来,通常可以做图片的盗链判断
- Cookies,检测Cookie中session_id 的使⽤用次数,如果超过限制,就触发反爬策略略
- 动态加载,网站使用ajax动态加载内容
- 对前端请求的API的参数进行加密
- 对网站JS进行混淆加密(适用于对API参数加密的情况,对用于加密的JS进行混淆)
- 在用户登录时,进行验证码验证(图片验证码或滑动验证码或短信验证码等)
- 对网页数据展示的总页数进行限制,比如用户只能浏览200页
java基础
java和python 开发web的区别
①架构:java更像是一种行业规定的十分清晰的架构;而python则更加自由,有优势也有缺点
②生态:java的生态十分强大,而python则相对小众
③库:python库十分丰富且方便调用,java则是通过jar包进行发布,相对较麻烦
④稳定性:python2和python3的区别导致了稳定性方面的不足,java则更加统一。从虚拟机角度也是如此。
⑤其他:java更商业化、python的脚本等等也非常流行。
包装类自动拆箱、自动装箱
c == a + b c.equals(a + b)
注解
* 作用分类 * 编写文档:生成文档【doc文档】 * 代码分析:通过注解对代码进行分析【使用反射】 * 编译检查:让编辑器能实现基本的编译检查【@Override】 * JDK中预定义的一些注解 * @Override:检测被该注解标注的方法是否是继承自父类(接口)的 * @Deprecated:将该注解标注的内容,表示已过时 * @SuppressWarnings:压制警告 * 一般传递参数all @SuppressWarnings("all") * 自定义注解 * 格式: * 元注解 * public @interface 注解名称() { 属性列表; } * 本质:注解本质上就是一个接口,该接口默认继承Annotation * public interface 注解名称 extends java.lang.annotation.Annotation {} * 属性:接口中的抽象方法 * 要求: 1. 属性的返回值类型有下列取值: * 基本数据类型 * String * 枚举 * 注解 * 以上类型的数组 2. 定义了属性,在使用时需要给属性赋值 1. 如果定义属性时,使用default关键字给属性默认初始值,则使用注解时可以不进行属性的赋值 2. 如果只有一个属性需要赋值,并且属性的名称为value,则value可以省略,直接定义值即可【@SuppressWarnings】 3. 数组赋值时,值使用{}包裹,如果数组中只有一个值,则{}可以省略 * 元注解:用于描述注解的注解 * @Target:描述注解能够作用的位置 * ElementType取值: * TYPE:可以作用于类上 * METHOD:可以作用于方法上 * FIELD:可以作用于成员变量上 * @Retention:描述注解被保留的一个阶段 * @Rentention(RententionPolicy.RUNTIME):当前被描述的注解,会保留到class字节码文件中,并被JVM读取到 * @Documented:描述注解是否被抽取到api文档中 * @Inherited:描述注解是否被子类继承 * 在程序中使用(解析)注解:获取注解中定义的属性值 1. 获取注解定义的位置的对象 【Class,Method,Field】 2. 获取指定的注解 * getAnnotation(Class) * public class ProImpl implements Pro{ public String className(){ return "day01.annotation.Demo1"; } public String methodName(){ return "show"; } } 3. 调用注解中的抽象方法,获取配置的属性值
- RPC(Remote Procedure Call)远程过程调用,简单的理解是一个节点请求另一个节点提供的服务
继承和多态
继承:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
多态:允许将子类类型的指针赋值给父类类型的指针。
重写和重载
重写:子类重写父类方法,返回值和形参都不能改变
重载:方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
TCP UDP对应的socket编程的API
socket():创建socket
bind():绑定socket到本地地址和端口,通常由服务端调用
listen():TCP专用,开启监听模式
accept():TCP专用,服务器等待客户端连接,一般是阻塞态
connect():TCP专用,客户端主动连接服务器
send():TCP专用,发送数据
recv():TCP专用,接收数据
sendto():UDP专用,发送数据到指定的IP地址和端口
recvfrom():UDP专用,接收数据,返回数据远端的IP地址和端口
closesocket():关闭socket
函数式编程
示例:
Function<Integer, Integer> f = x -> x + 1;
和匿名内部类的区别:
①this指向不同,lambda指向当前类,匿名内部类指向其本身
②lambda表达式并没有生成.class文件,匿名内部类则生成了
RBAC模型
权限控制?
transient
不进行序列化
NIO
BytebBuffer——HeapByteBuffer:在堆中创建的缓冲区。MappedByteBuffer:直接缓冲区。
allocate方法创建的直接缓冲区是创建的DirectByteBuffer实例。
java集合
hash冲突的解决方案
链地址法、开放地址法
开放地址法:hash冲突时对索引进行改变再放入。如hash得到的hashcode已经冲突,则将hashcode++再判断,直到有空位。这也就是线性探测,还可以进行平方的步长的增加,加快效率。或者再hash。
两次hash,加盐。等等
HashMap源码、实现原理、转红黑树的时机(为何是7)、多线程优化、多线程失败的场景
jdk7,数组加链表,hash冲突插入首部;jdk8,数组+链表+红黑树。hash冲突插入尾部,在链表长度大于7且数组长度大于64时转换为红黑树,否则先扩容。为何是7呢,因为链表中节点数是8的概率已经接近千分之一,而且此时链表的性能已经很差了。所以在这种比较罕见和极端的情况下,才会把链表转变为红黑树。而同时树节点的占用空间约为链表的两倍,占用空间较大。且在红黑树节点数为6时退化为链表。
容量:默认容量16,可自定义容量,如果不是2的幂次系统会默认设置为2的幂次,如果容量超过initial * loadFactor(默认0.75)会进行扩容(两倍)。(hash & (len - 1) —— 10101001 00001111)
为何线程不安全:
jdk7中由于头插法,会存在resize后链表中形成环的情况。 线程1阻塞前链表情况:a->b->null 线程2此时执行,会采用头插法将该链表放入新的table,放入后变成b->a->null 线程1此时返回,同样采用头插法插入该线程自己的新的table中,两次循环后变为b->a,但是由于线程2执行时的后果会导致b的next不是null而是a,所以循环多执行一次,e此时变为a会导致a.next = newTable[i](也就是b)然后发生循环。 jdk8中采用尾插法会导致数据的覆盖。 假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第13行代码后(hash碰撞判断,此时判断是没有碰撞的但还没有进行插入)由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。 除此之外还存在一个++size的线程安全问题。 jdk8解决成环问题: 采用两组指针loHead、loTail、hiHead、hiTail 这两组指针将链表分成了两部分,高位指针指向哪些扩容后下标变为(旧下标+扩容大小),低位指针指向哪些扩容后下标还保持不变的节点。分成两条链表今次那个迁移,迁移后节点的前后顺序保持不变,不会出现环的情况。(扩容后链表节点的情况只有两种下标不变 or 旧下标+扩容的大小) 红黑树的拆分和链表的逻辑基本一致,不同的地方在于,重新映射后,会将红黑树拆分成两条链表,根据链表的长度,判断需不需要把链表重新进行树化。
多线程优化:和读写锁配合。
ConcurrentHashMap原理
jdk7,Segment数组 + HashEntry数组 + ReentrantLock(对每个Segment上锁);jdk8,Node数组 + CAS + Synchronized(只对每个node上锁)
具体:
jdk7:保证segment数组为2的幂次、会再散列来获取下标 初始化有三个参数: initialCapacity:初始容量大小 ,默认16。 loadFactor, 扩容因子,默认0.75,当一个Segment存储的元素数量大于initialCapacity* loadFactor时,该Segment会进行一次扩容。 concurrencyLevel:并发度,默认16。Segment[]的数组长度。如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大,原本位于同一个Segment内的访问会扩散到不同的Segment中,CPU cache命中率会下降,从而引起程序性能下降。 segment不扩容,扩容的是hashentry数组 扩容时的get操作访问的是旧链表,对于put以及其他更新操作会阻塞直到扩容完成。
get不需要加锁,除非读到的是null,原理是get方法中的变量都使用volatile关键字修饰。且对volatile字段的写入操作先于读取操作,所以即使两个线程同时修改和获取volatile变量,get操作也能保证拿到最新的值。
ArrayList和LinkedList插入效率比较
插入到最后,效率相当。插入到中间,LinkedList效率高。数据量过大,ArrayList动态扩容,LinkedList效率更高。
ArrayList
懒加载(但在jdk7及以前会直接初始化一个容量为10的数组),需要扩容时,会首先扩容置原容量的1.5倍左右(
new = old + old >> 1
),然后如果new满足需求,则会直接用new作为新容量,否则会将当前所需容量作为新容量。
iterator
核心方法:next()、hashNext()、remove()、forEachRemaining()
remove()方法将会删除上次调用next()方法时返回的元素
Java最顶层集合有哪些
Collection、Map
抽象类和接口的区别:
抽象类更多是对事物的抽象,如人,是一种模板的设计;而接口则是对行为的抽象,如:运动,是一种行为的规范。
抽象类可以有静态方法、成员变量可以为任意类型。
java并发
synchronized和lock区别以及底层原理
区别:
1.首先synchronized是java内置关键字,在jvm层面,Lock是个java类; 2.synchronized无法判断是否获取锁的状态,Lock可以判断是否获取到锁; 3.synchronized会自动释放锁(a 线程执行完同步代码会释放锁 ;b 线程执行过程中发生异常会释放锁),Lock需在finally中手工释放锁(unlock()方法释放锁),否则容易造成线程死锁; 4.用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了; 5.synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可公平(两者皆可) 6.Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
底层原理:
synchronized: 原子性:保证语句块内是原子的 可见性:通过在unlock前需要将变量同步回主存,其他线程需要重新获取 有序性:一个变量在同一时刻只允许一条线程对其操作 方法级的同步是通过方法调用和返回中实现的,方法常量池的ACC_SYNCHRONIZED标志是否为同步方法,如果方法是同步方法,则执行线程需要先持有monitor然后执行方法,返回后释放。 代码块的同步是通过monitorenter和monitorexit实现的。遇到monitorenter试图获取monitor对象,如果未加锁或者已经被自己持有,则锁计数器+1,执行,遇到monitorexit则锁计数-1.计数器为0代表锁释放。如果获取monitor对象失败会进入阻塞。且是可重入的。 1.6前慢的原因:对象内部的监视器锁是通过底层OS的Mutex实现的,存在用户态到内核态的切换,成本极高 1.6后的优化:四种锁状态 无锁——偏向锁——轻量级锁——重量级锁。(不可降级) -- 偏向锁:无实际竞争,且将来只有第一个申请锁的线程会使用锁。只有一次CAS 锁对象第一次被获取时,jvm将对象头锁标志位设为01偏向模式,然后通过CAS将线程id记录到对象的markword中。如果成功,该线程在以后每次进入该同步块时,jvm不进行任何操作。如果不是第一次获取锁,则判断偏向线程id是否为当前线程,是的话就进入同步块。否则则根据当前偏向的线程是否存活,未存活则取消锁到无锁状态,存活则升级为轻量级锁。 -- 轻量级锁:无实际竞争,多个线程交替使用锁;允许短时间的锁竞争。申请和释放需要CAS 轻量级锁是相对于重量级锁而言的。使用轻量级锁时,不需要申请互斥量,而是在当前线程栈帧中开辟空间Lock Record用来记录当前对象markword的拷贝。然后将Mark Word中的部分字节CAS更新指向线程栈中的Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为00轻量级锁;否则,说明已经有线程获得了轻量级锁,如果指向的是当前线程的栈帧,则重入代码块,否则出现了竞争,会尝试几次CAS,如果不行,升级为重量级锁,标志位11,markword中指针指向重量级锁。 -- 重量级锁:有实际竞争,且锁竞争时间长。monitor实现。 Lock: 有三个实现类,ReentrantLock, ReentrantReadWriteLock类中的两个静态内部类ReadLock和WriteLock。 底层实现为AQS。 AQS:CLH锁队列(双向链表)+state状态变量,线程通过CAS去改变状态,成功则获取锁成功,失败则进入等待队列,等待被唤醒。 lock的存储结构:一个int类型状态值(用于锁的状态变更),一个双向链表(用于存储等待中的线程) lock获取锁的过程:本质上是通过 CAS 来获取状态值修改,如果当场没获取到,会将该线程放在线程等待链表中。 lock释放锁的过程:修改状态值,调整等待链表。 lock()-acquire()-tryAcquire()-未成功获取锁-addwaiter()-acquireQueued() acquireQueued的主要作用是把已经追加到队列的线程节点进行阻塞,但阻塞前又通过tryAccquire重试是否能获得锁,如果重试成功能则无需阻塞,直接返回。
monitor底层
C++实现的。通过一个管程和FIFO队列,保证临界区同一个时间只有一个线程能够进入。
Monitor的基本结构是什么?
1.Owner字段:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL
2.EntryQ字段:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程
3.RcThis字段:表示blocked或waiting在该monitor record上的所有线程的个数
4.Nest字段:用来实现重入锁的计数
5.HashCode字段:保存从对象头拷贝过来的HashCode值(可能还包含GC age)
6.Candidate字段:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降;Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁
锁升级时什么时候可能出现内存溢出
轻量级锁的lock record?
ReebtrantLock
可重入的互斥锁。可公平可非公平(公平锁会判断当前线程前是否有其他等待线程,有的话就进入等待队列,没有的话才会尝试获取锁,而非公平锁则是直接尝试获取锁)
ReebtrantReadWriteLock
支持公平和非公平、可重入、锁降级(获得写锁—获得读锁—释放写锁),不支持锁升级。
状态变量:高16位标识读,低16位表示写。通过位运算来获得读写的状态。
BlockingQueue原理
ArrayBlockingxxx, LinkedBlockingxxx, PriorityBlockingxxx, Delayxxx(延时获取元素), Synchronousxxx(不存储元素), LinkedTransferxxx(无界), LinkedBlockingDeque
处理方式:抛出异常(add)、返回特殊值(offer)、一直阻塞(put、超时退出(offer(e, time, unit))
使用Condition + LockSupport实现。
CopyOnWriteArrayList
写时复制:就是当有多个调用者同时去请求一个资源时(可以是内存中的一个数据),当其中一个调用者要对资源进行修改,系统会copy一个副本给该调用者,让其进行修改;而其他调用者所拥有资源并不会由于该调用者对资源的改动而发生改变。这就是写时复制思想;
当我们往CopyOnWrite容器中添加元素时,不直接操作当前容器,而是先将容器进行Copy,然后对Copy出的新容器进行修改,修改后,再将原容器的引用指向新的容器,即完成了整个修改操作。
存储数据的array数组通过volatile修饰,保证线程间的可见性。还使用ReentrantLock锁来实现线程安全
add方法为何既加锁又复制数组:首先加锁保证数组操作的安全性和原子性,其次数组的复制,保证旧数组所在内存区域的数据未发生变化,这样在修改数组时不会对get方法产生影响。
CountDownLatch
类似join方法,作用为使一个线程等待其他线程各自执行完毕后再执行。
构造方法传入int型整数,表示计数器,计数器的初始值是线程的数量。每当一个线程执行完毕后,调用countDown方法计数器的值就-1,当计数器的值为0时,表示所有线程都执行完毕,然后在await()闭锁上等待的线程就可以恢复工作了。
线程池
Executors的几个静态方法:
newFixedThreadPool() 阻塞队列无限,会OOM newSingleThreadExecutor() 阻塞队列无限,会OOM newCachedThreadPool() 最大线程数为Integer.MAX 也会OOM
自定义线程池参数的设置(cpu密集 or io密集)
cpu使用率较高(也就是一些复杂运算,逻辑处理),所以线程数一般只需要cpu核数的线程就可以了,减少上下文切换 cpu使用率较低,程序中会存在大量I/O操作占据时间,导致线程空余时间出来,所以通常就需要开cpu核数的两倍的线程, 当线程进行I/O操作cpu空暇时启用其他线程继续使用cpu,提高cpu使用率
核心线程数、最大线程数、存活时间、时间单位、阻塞队列、创建线程的工厂、拒绝策略
拒绝策略:
AbortPolicy:默认的策略,直接抛出 RejectedExecutionException 异常,阻⽌系统正常运⾏。 CallerRunsPolicy:既不会抛出异常,也不会终⽌任务,⽽是将任务返回给调⽤者,从⽽降低新任务的流量。 DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加⼊队列中尝试再次提交任务。 DiscardPolicy:该策略默默地丢弃⽆法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的⼀种策略。
读写锁的实现类
ReentrantReadWriteLock、ReadWriteLockView in StampedLock
公平锁和非公平锁
公平锁保证了FIFO原则但是耗费大量资源在上下文切换,而非公平锁则保证了极大的吞吐量和效率,但可能造成饥饿。
CAS
compareAndSet,自旋保证原子操作,和volatile关键字配合使用。
ABA问题。
重排序
volatile,内存屏障。final
AQS
acquire方法中:tryAcquire()—失败—addWaiter()加入等待队列—addQueued()再尝试一次失败就阻塞。
只有前节点唤醒或者中断才会继续执行。
volatile
内存屏障保证有序性,总线嗅探机制保证可见性。
伪共享:volatile修饰变量需要更新时,其他和volatile修饰变量在同一缓存行的变量也需要重新获取,性能降低。通过添加一些long的变量来填充缓存行。
创建线程的方式
①继承自Thread重写run方法②实现Runnable接口并实现run方法③实现Callerable接口,实现call方法,可有返回值
TLAB
缺省情况下仅占有整个Eden空间的1%
LockSupport
park()、unpark(Thread t)、parkNaos、parkUtil
Condition接口
await方法原理:将当前获得锁的线程从同步队列移到等待队列最后,并且释放同步状态。
signal:将当前condition对应的等待队列的首节点放入到同步队列最后,然后通过unpark唤醒线程,进而线程通过acquireQueued方法尝试获取同步状态。
乐观锁、悲观锁:
容易发生冲突且冲突量大时使用悲观锁,否则乐观锁(多读少写)。
数据库等适合悲观锁。
concurrent包下有哪些:
java.util.concurrent包下包含:tools、locks、collections、executor、atomic
其中tools包含CountDownLatch、Semaphore、Executors、Exchanger等等
locks则是包含了Lock、Condition、LockSupport、ReadWriteLock
collections则是一些支持并发的集合:阻塞队列、ConcurrentHashMap、ConcurrentSkipList等等
executors则是线程池,atomic为原子类。
Java线程的通信方式
volatile
等待/通知机制
join方式
threadLocal
ThreadLocal
首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
①实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
② 为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;
③ 在进行get之前,必须先set,否则会报空指针异常;
如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。
ThreadLoacl 类、内存泄漏(key是弱引用 ,value是强引用) 每次使用后remove。
由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。
但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()【源码保证】的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
解决方案:
- 每次使用完ThreadLocal都调用它的remove()方法清除数据
- 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
jdk源码
ArrayList
底层数组,非线程安全
扩容:懒加载(但在jdk7及以前会直接初始化一个容量为10的数组),扩容时先扩容1.5倍,如果无法满足,则将需要满足的容量作为新容量。
modCount作用:记录了集合的操作次数。在使用迭代器迭代的过程中,每次都要查一下modCount是否变化,如果变化了,迭代器在处理时可能出现不可预知的情况。因此只要这个值变化了,迭代器就抛出异常,所以modCount的作用是迭代器在遍历时做线程安全检查的。
add:将指定位置后面的数组进行后移一位(System.arrayCopy),然后插入新元素
remove:判断是否为最后一位,不为的话就指定位置后的数组前移一位,然后去除最后一个元素
RandomAccess接口,表示支持随机访问,无定义方法。
AQS
CLH队列 + volatile修饰的state变量
LockSupport + CMS完成
可重入
两种资源共享方式:独占(ReentrantLock)和共享(Semaphore/CountDownLatch)
accquire —— tryAccquire(尝试获取锁) —— addWaiter(cms添加到等待队列最后) —— accquireQueued(还会尝试一次获取锁)
共享模式下:doAcquireShared —— setHeadAndPropagate(将head指向自己,还有剩余资源可以再唤醒之后的线程)
由前序节点 或者 中断 唤醒等待中的线程
这篇博客讲的很好
PriorityQueue
堆排序,底层数组
见数据结构
TreeMap
红黑树
HashMap
JVM
JVM内存结构
class文件——类加载子系统——运行时数据区(方法区、堆、虚拟机栈、本地方法栈、PC寄存器)——执行引擎——本地方法库
垃圾回收算法
标记:引用计数和可达性分析
标记-清除、标记-整理、复制算法
垃圾回收器:CMS、G1、ParNew、Serial Old、Parellel Old
类加载过程
加载(已经存在Class对象)——链接(验证、准备、解析,准备阶段对类变量赋默认值)——初始化(clinit方法)
类加载机制
- 隐式加载 new 创建类的实例,
- 显式加载:loaderClass,forName等
- 访问类的静态变量,或者为静态变量赋值
- 调用类的静态方法
- 使用反射方式创建某个类或者接口对象的Class对象。
- 初始化某个类的子类
- 直接使用
java.exe
命令来运行某个主类
双亲委派模型
- 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类豳加载器去执行;
- 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器
- 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
破坏双亲委派机制
在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。
线程上下文类加载器(contextClassLoader)是从 JDK 1.2 开始引入的,我们可以通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源
沙箱安全机制
假如自定义java.lang.String类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class),报错信息说没有main方法,就是因为加载的是rt.jar包中的String类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。
ClassLoader的方法
getParent()获取父加载器、loadClass(String name)加载指定name的类并返回Class对象、findClass(String name)查找指定name的类并返回Class对象、 findLoadedClass(String name)查找已经加载的类并返回、defineClass(String name, byte[] b, int off, int len)将字节数组中的内容转换为一个类、resolveClass(Class<?> c)链接指定的一个类。
遵循双亲委派——重写findClass方法即可、打破双亲委派——重写loadClass方法
OOM如何分析,一些分析工具,常用命令
jvisualVM、jprofiler
jmap、jflag、jinfo、jstate
堆和栈的区别
堆中存在OOM和GC,栈中只存在OOM。堆是存储的单位,栈是运行时的单位。
对象头
MarkWord(hashcode、锁标志位、分代年龄、是否偏向锁、偏向线程id等等)
元数据指针(指向方法区的类型数据信息)
数组长度(如果是数组的话)
对象实例化过程
①查看对应的类信息是否加载②计算所需内存并分配空间③并发问题(CAS)分配TLAB④默认初始化⑤设置对象头⑥显示初始化
NIO
直接内存,DirectByteBuffer操作本地内存,没有中间状态。IO多路复用
String不可变原理:
final修饰char数组(jdk9后采用byte数组)。
原因:①字符串常量池②String缓存了自身的hashcode,如果可变但hash没变,散列会存在问题③String会作为参数
StringBuffer线程安全,synchronized修饰,StringBuilder性能更高。
String str1=“a”;String str2=“a”+“bc”;
都在字符串常量池中创建一个对象。
String a = new String(“11”)+new String(“22”);创建了几个对象?
new String(“xx”)都会创建两个对象,然后如果+两边存在变量,那么都会存在一个StringBuilder对象,StirngBuilder还会通过toString方法再创建一个对象。
ClassNotFoundException场景
1、调用class的forName方法时,找不到指定的类
2、ClassLoader 中的 findSystemClass() 方法时,找不到指定的类
3、ClassLoader 中的 loadClass() 方法时,找不到指定的类
GC ROOTS
在Java语言中,GC Roots包括以下几类元素:
①虚拟机栈中引用的对象。比如:各个线程被调用的方法中使用到的参数、局部变量等。
②本地方法栈内JNI(通常说的本地方法)引用的对象
③方法区中类静态属性引用的对象。比如: Java类的引用类型静态变量
④方法区中常量引用的对象。比如:字符串常量池(string Table)里的引用
⑤所有被同步锁synchronized持有的对象
⑥Java虚拟机内部的引用。基本数据类型对应的class对象,一些常驻的异常对象(如:NullPointerException、OutOfMemoryError)、系统类加载器
⑦反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
⑧在某些特殊情况下,还可能存在一些对象临时加入到Root中的情况。(如:在分代垃圾收集时,如果只回收新生代的对象,那么一些老年代的对象也可以作为Root)
跨代引用问题
常用jvm参数
-Xms, -Xmx
-XX:MetaspaceSize; -XX:MaxMetaspaceSize / -XX:PermSize
-XX:+PrintGCDetails
-XX:NewRatio: 配置新生代与老年代在堆结构的占比
-XX:SurvivorRatio: 设置新生代中Eden和s0/s1空间的比例
OOM排查
1、先查看应用进程号pid: ps -ef | grep 应用名 2、查看pid垃圾回收情况: jstat -gc pid 5000(时间间隔) 3、开启OOM快照: -XX:+HeapDumpOnOutOfMemoryError(开启堆快照) -XX:HeapDumpPath=C:/m.hprof(保存文件到哪个目录) 4、dump 查看方法栈信息: jstack -l pid > /home/test/jstack.txt 5、dump 查看JVM内存分配以及使用情况 jmap -heap pid > /home/test/jmapHeap.txt 6、dump jvm二进制的内存详细使用情况 jmap -dump:format=b,file=/home/test/oom.hprof pid
Spring
java连接数据库 jdbc原生
public static void conn() { String URL = "jdbc:mysql://127.0.0.1:3306/Supermarket?characterEncoding=utf-8"; String USER = "root"; String PASSWORD = "123"; // 1.加载驱动程序 try { Class.forName("com.mysql.jdbc.Driver"); // 2.获得数据库链接 Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); // 3.通过数据库的连接操作数据库,实现增删改查(使用Statement类) String name="张三"; //预编译 String sql="select * from userinfo where UserName=?"; PreparedStatement statement = conn.prepareStatement(sql); statement.setString(1, name); ResultSet rs = statement.executeQuery(); // String sql="select * from userinfo where UserName='"+name+"'"; // Statement statement = conn.createStatement(); // ResultSet rs = statement.executeQuery(sql); // 4.处理数据库的返回结果(使用ResultSet类) while (rs.next()) { System.out.println(rs.getString("UserName") + " " + rs.getString("Password")); } // 关闭资源 conn.close(); rs.close(); statement.close(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } }
IOC AOP
IOC: Inversion of Control,控制反转 DI: Dependency Injection,依赖注入 关系:IOC是一种面向编程设计思想,DI是IOC思想的实现方式,即:DI实现IOC这一思想 A需要B,A不直接控制B,A给IOC容器需要的信息,然后IOC容器为A创建B。 主动获取反转为被动获取,解耦。 AOP: jdk or cglib JDK动态代理主要涉及java.lang.reflect包下边的两个类:Proxy和InvocationHandler。其中,InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态地将横切逻辑和业务逻辑贬值在一起。 所以使用JDK动态代理的话,他有一个限制,就是它只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,如何创建动态代理实例呢?答案就是CGLib。 CGLib采用底层的字节码技术,全称是:Code Generation Library,CGLib可以为一个类创建一个子类,在子类中采用方法拦截的技术拦截所有父类方法的调用并顺势织入横切逻辑。 在spring中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。 ----------------------------------------------------------------------- JDK的动态代理 final Advice advice = new Advice(); // 获得增强对象 final Target target = new Target(); // 返回值就是生成的动态代理对象 TargetInterface proxy = (TargetInterface) Proxy.newProxyInstance( target.getClass().getClassLoader(), // 目标对象的类加载器 target.getClass().getInterfaces(), // 目标对象相同的接口字节码对象数组 new InvocationHandler() { // 调用代理对象的任何方法,实质执行的为invoke方法 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { advice.before();// 前置增强 method.invoke(target, args);// 执行目标方法 advice.after(); return null; // 该返回值对于方法本身而言 无意义 } } ); proxy.save(); // 调用代理对象的方法 cglib的动态代理 final Advice advice = new Advice(); // 获得增强对象 final Target target = new Target(); // 返回值就是生成的动态代理对象 基于cglib // 1.创建增强器 Enhancer enhancer = new Enhancer(); // 2.设置父类 (目标) enhancer.setSuperclass(Target.class); // 3.设置回调 enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { advice.before(); // 执行前置 Object invoke = method.invoke(target, args); advice.after(); // 执行后置 return invoke; } }); // 4.生成代理对象 Target proxy = (Target) enhancer.create(); proxy.save();
Spring加载bean的流程
class对象——实例化得到原始bean对象——属性注入——初始化——放入单例池——销毁
三级缓存与循环依赖
class对象——实例化——放入三级缓存——属性注入——需要的属性先查找一级缓存、然后查找二级缓存、然后查找三级缓存,放入二级缓存、删除三级缓存。
三级缓存内存存的是函数式接口。
构造器的循环依赖和prototype的循环依赖无法解决。(?)
Bean生命周期
实例化Bean --> 属性注入 --> [Aware(xxxXxxAware接口) -->] 初始化 --> 销毁
大的流程大概为四步,如上,后续补充:
1、InstantiationAwareBeanPostProcessor接口(postProcessBeforeInstantiation、postProcessAfterInstantiation)
- 该两接口作用于实例化Bean的前后
2、BeanPostProcessor接口(postProcessBeforeInitialization、postProcessAfterInitialization)
- 该两接口作用于初始化的前后
- 所有的Aware接口注入也就是在postProcessBeforeInitialization中完成的
3、InitializingBean和DisposableBean对应初始化和销毁阶段
- InitializingBean方法中可以使用在Aware阶段所获得的资源。
4、需要AOP时:
实例化Bean --> 属性注入 --> [Aware(xxxXxxAware接口) -->] 初始化 --> AOP --> 单例池(ConcurrentHashMap<BeanName, 对象>)
5、广义:
class --> BeanDefinition --> 实例化Bean --> 属性注入 --> [Aware(xxxXxxAware接口) -->] 初始化 --> AOP --> 单例池(ConcurrentHashMap<BeanName, 对象>)
6、BeanDefinition
而BeanDefinition实际上是通过解析最初提供的class得到的,其中包含了bean的scope等等信息,同时我们可以通过实现BeanFactoryPostProcessor接口并实现postProcessBeanFactory方法对其中的信息进行修改。(例如我们可以将BeanDefinition中的beanClass改变,这样创建bean实例时创建的也就是改变后的class对应的对象实例了)
而BeanFactoryPostProcessor是在BeanFactory组建完成后才会调用的。而BeanFactory组建完成是指所有类对应的BeanDefinition都创建完成并且存入对应的ConcurrentMap后。
而BeanFactory创建bean的过程也就是从BeanDefinitionMap中拿到对应的BeanDefinition然后进行实例化bean,并放入单例池。
注:BeanDefinition可参考下图。
7、FactoryBean
FactoryBean是一个接口,当在IOC容器中的Bean实现了FactoryBean后,通过getBean(String BeanName)获取到的Bean对象并不是FactoryBean的实现类对象,而是这个实现类中的实现的getObject()方法返回的对象,如下图的MapperFactoryBean。而如果要想获取FactoryBean的实现类,就要getBean(&BeanName),在BeanName之前加上&。
8、ImportBeanDefinitionRegistrar
ImportBeanDefinitionRegistrar是一个接口,实现该接口的类需要实现registerBeanDefinitions方法,该方法可以将Spring中没有通过@Component修饰的接口通过@Import注解的方式插入到registerBeanDefinitions方法的参数中,然后通过该方法生成BeanDefinition,并通过方法的registry参数注册到BeanDefinitionMap中也就是BeanFactory中,然后进行后面Bean的实例化。即不是通过ComponentScan扫描并自动生成BD的。
解决循环依赖(三级缓存)
Spring创建bean的流程如下:
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 一级缓存 单例池
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16); // 二级缓存 存放代理对象
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16); // 三级缓存 存放lambda
大致流程:
- 先从一级缓存singletonObjects中去获取。(如果获取到就直接return)
- 如果获取不到或者对象正在创建中(isSingletonCurrentlyInCreation()),那就再从二级缓存earlySingletonObjects中获取。(如果获取到就直接return)
- 如果还是获取不到,且允许singletonFactories(allowEarlyReference=true)通过getObject()获取。就从三级缓存singletonFactory.getObject()获取。(如果获取到了就将其从三级缓存中移除,并且放进二级缓存earlySingletonObjects。)
Spring、MyBatis整合
通过注解对mapper接口进行扫描并获得对应包名和接口,然后进行BD的注册,注册完后根据接口类型等信息对BD进行处理,即改变BD生成bean的方式(通过实现BeanFactory的方式来生成),通过MapperBeanFactory进行后续的bean的生成。
BeanFactory和FactoryBean
FactoryBean是一个接口,当在IOC容器中的Bean实现了FactoryBean后,通过getBean(String BeanName)获取到的Bean对象并不是FactoryBean的实现类对象,而是这个实现类中的实现的getObject()方法返回的对象,如下图的MapperFactoryBean。而如果要想获取FactoryBean的实现类,就要getBean(&BeanName),在BeanName之前加上&。
BeanFactory是一个接口,Spring内部实现了很多类,存放很多东西,如definitionmap、singletonmap等等。BeanFactory是个Factory,也就是IOC容器或对象工厂。在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的
@Autowired和@Resource区别
@Autowired//默认按type注入
@Qualifier(“cusInfoService”)//一般作为@Autowired()的修饰用
@Resource(name=“cusInfoService”)//默认按name注入,可以通过name和type属性进行选择性注入
@Inject和@Autowired
@Inject是Java EE 6(JSR-299)中引入的Java CDI(上下文和依赖项注入)标准的一部分
@Autowired则是Spring框架的一部分。
前者自动装配的bean范围是singleton,后者则是原型
前者和@Named一起使用,后者和@Qualifier一起使用。
全局异常处理
继承HandlerExceptionResolver接口,并将实现类作为bean进行注册到Spring,在resolveException中实现处理逻辑。
SpringBoot怎么在服务端接收到HTTP请求后,再转发到控制层?
filter->dispatchServlet(doService->doDispath)->intercepter(prehandle拦截器)->RequestMappingHandler(查找映射)->ServletInvocableHandlerMethod: invokeAndHandle(参数处理)->通过java 反射机制动态调用目标API方法进入相对应具体的controller->执行 handleReturnValue 方法, 首先会判断是否需要对response entity 进行二次处理(ResponseBodyAdvice: beforeBodyWrite), 处理完成后, 调用注册进来的MessageConvertor 对返回信息进行转换处理, 比如使用faster Jackson 把对象类型转成 json 字符串,并以application/json 的形式返回 http response -> 拦截器 postHandle 方法, 最后执行拦截器的afterCompletion 方法。
按照 Servlet 规范,所有请求都会被tomcat容器交到 dispatchServlet 的 doService 方法中去处理。跟到这个方法中去,我们发现其中设置了变量进 request 对象,然后执行了 doDispatch 方法,这个方法才是真正实现请求处理的核心。
该doDispatch方法中调用 getHandler 找到 url 匹配的 handler 方法(示例代码中的 hello 方法)。然后调用 ha.handle() 来获得处理结果。
对于getHandler 方法,是通过 HandlerMapping 接口对象的集合对象来操作的。HandlerMapping 接口要求实现类实现从请求到处理对象的映射的方法。以 RequestMappingHandlerMapping 实现为例,它底层注册了一个 url -> handler方法的 map,每当请求过来,就会根据请求的url 去 map 中匹配,匹配到对应的handler 方法。
Spring中的设计模式
简单工厂模式——BeanFactory
工厂方法模式——FactoryBean
单例模式——单例池,bean(singleton)
适配器模式——AOP
包装器模式——Wrapper、Decorator
代理模式——AOP动态代理
观察者模式——
策略模式——
SpringMVC
什么是MVC
Controller(控制器)、Model(业务模型)、View(用户视图)实现代码分离,Controller用于同步Model和View
SpringMVC流程
(1)用户发送请求至前端控制器DispatcherServlet;
(2) DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handle;
(3)处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet;
(4)DispatcherServlet 调用 HandlerAdapter处理器适配器;
(5)HandlerAdapter 经过适配调用 具体处理器(Handler,也叫后端控制器);
(6)Handler执行完成返回ModelAndView;
(7)HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet;
(8)DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行解析;
(9)ViewResolver解析后返回具体View;
(10)DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)
(11)DispatcherServlet响应用户。
SpringMVC实现返回json
通过一些json框架如(Jackson),并在方法前加上@ResponseBody即可
解决post、get乱码问题
post:在web.xml中配置一个CharacterEncodingFilter过滤器,设置成utf-8;
get:①修改tomcat配置文件添加编码与工程编码一致;②对传过来的参数进行重新编码
SpringMVC异常处理
可以将异常抛给Spring框架,由Spring框架来处理;我们只需要配置简单的异常处理器,在异常处理器中添视图页面即可。
SpringMVC控制器
是单例的,多线程存在线程安全问题,不使用同步,会影响性能,在控制器中不写字段来保证线程安全。
SpringMVC常用注解
@RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。
@RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
@ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。@Controller、@RestController(@Controller + @ResponseBody)
如何在方法中得到session、request对象
直接在方法中声明这个对象,SpringMvc就自动会把属性赋值到这个对象里面。
SpringMvc用什么对象从后台向前台传递数据的?
通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前台就可以通过el表达式拿到。
怎么样把ModelMap里面的数据放入Session里面?
可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。
SpringMvc里面拦截器是怎么写的?
有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMVC的配置文件中配置拦截器即可
注解原理
注解本质是一个继承了
Annotation
的特殊接口,其具体实现类是Java
运行时生成的动态代理类。我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用AnnotationInvocationHandler
的invoke
方法。该方法会从memberValues
这个Map
中索引出对应的值。而memberValues
的来源是Java
常量池。
Controller局部异常处理
①在某个方法上方使用@ExceptionHandler()注解,并给出想要处理的异常类型,然后该方法就会作为该Controller的异常处理方法
②定义一个异常处理类,并使用@ControllerAdvice()注解修饰,并给出想要处理的Controller,可以传入一个接口class对象,表示实现了该接口的Controller的异常都由该异常类处理,内部的exceptionHandler和方法①一致。
SpringBoot
配置文件装载顺序
①application.properties优先级大于application.yml
②先去项目根目录找config文件夹下找配置文件件;再去根目录下找配置文件;去resources下找cofnig文件夹下找配置文件;去resources下找配置文件
③如果高优先级的配置文件和低优先级的配置文件中属性不冲突,则可以实现互补配置。
④外部配置:如cmd命令,或者系统属性System.getProperties();同样可以形成互补配置
Redis
单线程原因:
- 单线程编程容易并且更容易维护;
- Redis 的性能瓶颈不再 CPU ,主要在内存和网络;
- 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。
缓存淘汰策略
lru(最近最少未使用)、ttl(时间)、lfu(使用频率)、不驱逐
布隆过滤器原理
bitmap + 多次hash
对于hash结果不正确的,一定不存在。反之则不是,可能存在也可能不存在。
一致性hash
hash环(对2^32而不是一个固定值进行hash),保证即使hash结果数要求发生变化,只用改变hash环上的分割即可。可使用虚拟节点进行hash环分配不均的改良。
虚拟节点即将单个节点虚拟为多个节点,使得数据能够平均分布在各个节点上。
内存模型
k-v结构,本质内存模型就是为底层的字典结构。参考下方
redis和java字典的区别
(1)Redis中有缩容,Java中没有
(2)Redis中的扩容不是一次完成的,可以分多次,是渐进式地,而Java的是一次完成的
持久化机制
AOF、RDB
RDB其实就是把数据以快照的形式保存在磁盘上。什么是快照呢,你可以理解成把当前时刻的数据拍成一张照片保存下来。Redis调forks。同时拥有父进程和子进程。子进程将数据集写入到一个临时RDB文件中。当子进程完成对新RDB文件的写入时,Redis用新RDB文件替换原来的RDB文件,并删除旧的RDB文件。 RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘。也是默认的持久化方式,这种方式是就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。 三种触发机制save、bgsave、自动化。 save: 阻塞redis,用新的RDB替换旧的并直到过程完成才继续 bgsave: Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用bgsave命令。 自动化:更改配置文件,它在“N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动进行数据集保存操作。 RDB是一个非常紧凑的文件,方便传输 快照持久化期间修改的数据不会被保存,可能丢失数据。 ---------------------------------------------------------------------------------------------------- AOF:redis会将每一个收到的写命令都通过write函数追加到文件中。通俗的理解就是日志记录。每当Redis执行一个改变数据集的命令时(比如 SET), 这个命令就会被追加到AOF文件的末尾。这样的话, 当Redis重新启时, 程序就可以通过重新执行AOF文件中的命令来达到重建数据集的目的 为了压缩aof的持久化文件。redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。 每次修改同步always:同步持久化,每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好 每秒同步everysec:异步操作,每秒记录 如果一秒内宕机,有数据丢失 不同no:从不 fsync :将数据交给操作系统来处理,由操作系统来决定什么时候同步数据。更快,也更不安全的选择。 AOF重写:因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。举个例子, 如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录(entry)。然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。 为了处理这种情况, Redis 支持一种有趣的特性: 可以在不打断服务客户端的情况下, 对 AOF 文件进行重建(rebuild)。执行 bgrewriteaof 命令, Redis 将生成一个新的 AOF 文件, 这个文件包含重建当前数据集所需的最少命令。
Redis 持久化 之 AOF 和 RDB 同时开启,Redis听谁的?
AOF
缓存雪崩、缓存击穿、缓存穿透
缓存雪崩是指缓存同一时间大面积的失效(也可能为redis重启),所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。(一般出现于被攻击或者电商中高并发的场景)
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
底层数据结构
字符串:SDS(char数组buf + len + 未使用的空间大小free),可以动态扩容(预分配),惰性空间释放(改变free的值而非回收内存)。
链表:双向链表,表头前置为null,表尾后置为null
字典:新增时,先根据键值对的键计算出哈希值,然后根据 sizemask 属性和哈希值,计算索引值——即落入数组中的哪个位置。之后如果有一个位置多个键值对要存入时,组成单向链表即可。
这里和 HashMap 的不同之处在于,链表添加时总是添加在表头位置。因为 dictEntry 节点组成的链表没有指向链表表尾的指针,为了速度考虑,总是将新节点加在链表的表头位置。(为什么要这样,而不是遍历完整个链表后加在链表尾部,不遍历出现重复键怎么办?)
rehash: rehash 也可以参考 Java 中 HashMap 的原理。 负载因子 = 哈希表中已保存的节点数量 / 哈希表数组大小。 当哈希表中存放的键值对不断增多或减少,为了让负载因子在一个合理的范围内,需要对大小进行扩展或者收缩。(这里类似 HashMap 中的重新散列方法) 1. 字典的 ht[1] 分配空间,空间的大小由 ht[0] 已经使用的键值对数量以及执行的扩张和收缩来决定。 - 扩展操作,那么 ht[1] 分配的空间大小应是比当前 ht[0].used 值的二倍大的第一个 2 的整数幂。(比如当前使用空间 14,那么找 28 的下一个 2 的整数幂,为 32) - 收缩操作,取 ht[0].used 的第一个大于等于的 2 的整数幂。(比如 14,那么就是 16) 2. 将 ht[0] 中的所有键值对,rehash 到 ht[1] 上面:根据新的大小来重新计算所有键的哈希和索引,映射到新数组的指定位置上。 3. ht[0] 的所有键值对都迁移到 ht[1] 之后,释放 ht[0] ,然后将 ht[1] 设置为 ht[0] ,然后在 ht[1] 处新创建空白哈希表,为下一次 rehash 做准备。 扩展的条件 服务器没有执行 BGSAVE 或者 BGREWRITEAOF 命令,并且哈希表的负载因子大于等于 1 。 服务器正在执行 BGSAVE 或者 BGREWRITEAOP 命令,并且哈希表的负载因子大于等于 5 。 这两种情况根据是否有后台命令执行来区分,是因为在执行 BGSAVE 或者 BGREWRITEAOF 的过程中,Redis 需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率。所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,尽可能避免在子进程存在期间进行哈希表的扩展操作,来避免不必要的内存写入操作,最大限度的节省内存。 收缩的条件 当哈希表的负载因子小于 0.1 时,自动开始对哈希表进行收缩操作。 渐进式rehash: 如果键值对量巨大时,一次性全部 rehash 必然造成一段时间的停止服务。所以要分多次、渐进式的将键值对从 ht[0] 慢慢的 rehash 到 ht[1] 中。 具体过程: 1. 为 ht[1] 分配空间,同时有 ht[0] 和 ht[1] 两个哈希表。 2. 在字典中维持一个索引计数器变量 rehashindex ,并将其置为 0 ,表示 rehash 正式开始。 3. 在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作之外,还会顺便将 ht[0] 哈希表在 rehashindex 索引上的所有键值对 rehash 到 ht[1] 上,当 rehash 工作完成之后,程序将 rehashindex 的值加一。 4. 随着字典操作的不断进行,最终在某个时间点,ht[0] 的所有键值对都被 rehash 到 ht[1] ,这时程序将 rehashindex 的值置为 -1 ,表示 rehash 工作完成。 渐进式 rehash 的过程中,更新删除查找等都会在两个哈希表上进行,比如查找,先在 ht[0] 中查找,如果没找到,就去 ht[1] 中查找。而新增操作,直接新增在 ht[1] 中,ht[0] 不会进行任何的新增操作。保证 ht[0] 的数量只减不增,最终变为空表。
跳表:跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。Redis 使用跳跃表作为有序集合键的底层实现之一。
跳跃表在 Redis 中,只有两个地方用到:一个是实现有序集合对象,另一个是在集群节点中用作内部数据结构。head:指向跳跃表的表头节点。 tail:指向跳跃表的表尾节点。 level:记录当前跳跃表中,层数最高的节点的层数(表头节点的层数不计算)。 length:记录跳跃表的长度,即包含节点的数量。 level:每一层都有前进指针和跨度,从头到尾遍历时,访问会沿着层的前进指针进行。 BW:后退指针,指向前一个节点,从尾到头遍历时使用。 score:分值,跳跃表中的分值按从小到大排列。 obj:成员对象,各个节点保存有各个成员对象。
整数集合:整数集合是集合键的底层实现之一。当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis 就会使用整数集合作为集合键的底层实现。
整数集合是 Redis 保存整数值的集合的抽象数据结构,可以保存 int16_t ,int32_t ,int64_t 的整数值,并且集合中不会出现重复元素。
底层由数组实现,整数集合的每个元素都是数组的一个数组项,各个项在数组中按从小到大排列。length 属性记录了包含的元素数量,即数组的长度。升级: 当一个新元素添加到整数集合中时,如果新元素类型比整数集合现有的所有元素的类型都要长时,整数集合要先进行升级,然后才能将新元素添加到整数集合中。 1. 根据新元素类型,扩展整数集合底层数组的大小,并为新元素分配空间。 2. 将底层数组现有的所有元素都转换成新元素相同的类型,并将类型转换后的元素放置到正确位置上,而且放置过程中需要维持底层数组的有序。 3. 将新元素添加到底层数组中。 因为引发升级的新元素的长度肯定比现有所有元素都大,才会出现升级的情况,所以这个值要么大于所有元素,放置的位置就对应新数组的末尾;要么小于所有元素,放置的位置在数组的开头。 升级可以提高灵活性,不用担心类型错误,可以随意添加不同类型的元素。另外,可以节约内存,只在有需要的时候进行升级。 另外,整数集合不支持降级操作。
压缩列表
压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项并且每个都是小整数值或者长度比较短的字符串时,Redis 就采用压缩列表做底层实现。当一个哈希键只包含少量键值对,并且每个键值对的键和值也是小整数值或者长度比较短的字符串时,Redis 就采用压缩列表做底层实现。
压缩列表是 Redis 为了节约内存而实现的,是一系列特殊编码的连续内存块组成的顺序型数据结构。
zlbytes :4 字节。记录整个压缩列表占用的内存字节数,在内存重分配或者计算 zlend 的位置时使用。 zltail :4 字节。记录压缩列表表尾节点记录压缩列表的起始地址有多少个字节,可以通过该属性直接确定表尾节点的地址,无需遍历。 zllen :2 字节。记录了压缩列表包含的节点数量,由于只有 2 字节大小,那么小于 65535 时,表示节点数量。等于 65535 时,需要遍历得到总数。 entry :列表节点,长度不定,由内容决定。 zlend :1 字节,特殊值 0xFF ,用于标记压缩列表的结束。
压缩列表节点保存一个字节数组或者一个整数值。
字节数组可以是下列值:
- 长度小于等于 2^6-1 字节的字节数组
- 长度小于等于 2^14-1 字节的字节数组
- 长度小于等于 2^32-1 字节的字节数组
整数可以是六种长度:
- 4 位长,介于 0 到 12 之间的无符号整数
- 1 字节长的有符号整数
- 3 字节长的有符号整数
- int16_t 类型整数
- int32_t 类型整数
- int64_t 类型整数
每个压缩列表节点的结构如图:
previous_entry_length 属性以字节为单位,记录了压缩列表中前一个节点的长度。该属性的长度可以是 1 字节或者 5 字节。如果前一个节点的长度小于 254 字节,那么该属性长度为 1 字节,保存小于 254 的值。如果前一节点的长度大于等于 254 字节,那么长度需要为 5 字节,属性的第一字节会被设置为 0xFE (254) 之后的 4 个字节保存其长度。 压缩列表的从表尾到表头遍历: 1. 首先,有指向压缩列表表尾节点起始地址的指针 p1 (指向表尾节点的指针可以通过指向压缩列表起始地址的指针加上 zltail 属性的值得出); 2. 通过用 p1 减去节点的 previous_entry_length 属性,得到前一个节点的起始地址的指针。 3. 如此循环,最终从表尾遍历到表头节点。 encoding 属性记录了节点的 content 属性所保存的数据的类型和长度: - 一字节、两字节或五字节长,值的最高位为 00、01 或者 10 的是字节数组编码,字节数组的长度由编码除去最高两位之后的其他位记录; - 一字节长,值的最高位以 11 开头的是整数编码,这种编码表示保存是整数值,整数值的类型和长度由其他位记录。 出现新增或删除节点导致 previous_entry_length 1 字节或者 5 字节的长度变化,是连锁更新的问题,但出现几率比较小,而且数量不多的情况下不会对性能造成影响。
数据结构底层实现
字符串:①整数——int;②长字符串(大于44【64 - 19(头部) - 1(’/0’)】字节)——raw;③短字符串(小于44字节)——embstr
列表:①满足列表对象所有字符串元素长度都小于64个字节且元素数量小于512——ziplist;②其他使用双向链表
hash:①满足元素数量小于512且所有元素长度小于64字节——ziplist;②哈希表
set:①所有元素都是整数,元素数量小于512——整数列表;②哈希表
zset:①所有元素都是整数,元素数量小于512——整数列表;②跳表
Docker
Docker是一个容器化平台,它以容器的形式将您的应用程序及其所有依赖项打包在一起,以确保您的应用程序在任何环境中无缝运行。
Docker不是虚拟化方法。docker四种状态:运行、已暂停、重新启动、已退出。
DockerFile:FROM—指定基础镜像;LABEL—功能是为镜像指定标签;RUN—运行指定的命令;CMD—容器启动时要运行的命令。
虽然ADD并且COPY在功能上类似,但是首选COPY。
那是因为它比ADD更易懂。COPY仅支持将本地文件复制到容器中,而ADD具有一些功能(如仅限本地的tar提取和远程URL支持),这些功能并不是很明显。因此,ADD的最佳用途是将本地tar文件自动提取到镜像中
Docker镜像是Docker容器的源代码,类和对象实例的关系。
docker镜像本质:
分层文件系统。
centos的镜像很小,复用了os的bootfs,只有其他层。tomcat镜像很大,因为依赖于其他的镜像。
RocketMQ
选型
MQ 描述 RabbitMQ erlang开发,对消息堆积的支持并不好,当大量消息积压的时候,会导致 RabbitMQ 的性能急剧下降。每秒钟可以处理几万到十几万条消息。 RocketMQ java开发,面向互联网集群化功能丰富,对在线业务的响应时延做了很多的优化,大多数情况下可以做到毫秒级的响应,每秒钟大概能处理几十万条消息。 Kafka Scala开发,面向日志功能丰富,性能最高。当你的业务场景中,每秒钟消息数量没有那么多的时候,Kafka 的时延反而会比较高。所以,Kafka 不太适合在线业务场景。 ActiveMQ java开发,简单,稳定,性能不如前面三个。小型系统用也ok,但是不推荐。推荐用互联网主流的。
底层实现
netty
文件上传
断点传输、文件秒传(已经上传过的不再上传):hash
文件传输粘包问题
为何粘包:
A. TCP协议为了提高传输效率,发送方往往需要收集定量的数据才会封装给底层并发送,若出现连续send(data),TCP会把该数据进行整合(直到装满数据缓冲区),这样就造成了粘包数据;
B. 接收方接收方的粘包是由于接收用户相关进程不及时接收数据,从而导致粘包问题,这是因为接收方先把接收到的数据放在系统接受缓冲区,用户进程从该缓冲区取定量的数据,但若下一包数据到达前,缓冲区的数据没有及时的被用户进程取走,则下一包数据与前一包部分数据在系统缓冲区,就可能导致用户设定的进程缓冲区从系统缓冲区取走两个包的部分数据,从而导致粘包解决方案:
A 发送方在send()之前,先向接收方发送数据总量大小,并通过双端确认,server端发送数据包,然后接收方通过按数据量大小循环设立缓冲区接收数据;;
B: TCP提供了PUSH(强制数据立即传送)操作,但影响性能;
传输文件的方式
ftp、sftp
事务消息:
问题: 1、账号服务扣款成功了,通知积分系统也成功了,但是积分增加的时候失败了,数据不一致了。 2、账号服务扣款成功了,但是通知积分系统失败了,所以积分不会增加,数据不一致了。 rocketmq的解决方案: 针对问题1:如果消费失败了,是会自动重试的,如果重试几次后还是消费失败,那么这种情况就需要人工解决了,比如放到死信队列里然后手动查原因进行处理等。 针对问题2:RocketMQ针对第二个问题解决方案是:如果你扣款成功了,但是往mq写消息的时候失败了,那么RocketMQ会进行回滚消息的操作,这时候我们也能回滚我们扣款的操作。(通过半消息实现)
顺序消息:
方案1:发送消息到一个queue中来保证顺序消费(MessageQueueSelector)。
方案2:线程数设置为1,且通过消息体判断到哪个queue进行消费。
消息持久化
Broker端拿到消息后先将消息、topic、queue等内容存到ByteBuffer里,然后去持久化到commitlog文件中。commitlog文件大小为1G,超出大小会新创建commitlog文件来存储,采取的nio方式。
如何保证消息不丢失
消息消费的流程: 生产阶段:Producer通过网络将消息发送给Broker,这个发送可能会发生丢失,比如网络延迟不可达等。 存储阶段:Broker肯定是先把消息放到内存的,然后根据刷盘策略持久化到硬盘中,刚收到Producer的消息,在内存中了,但是异常宕机了,导致消息丢失。(持久化位置为commitlog) 消费阶段:消费失败了其实也是消息丢失的一种变体吧。
解决方案: 生产阶段: 1、通过同步发送来保证消息的成功送达。 2、如果发送失败会重试,默认为三次,可通过api调整。producer.setRetryTimesWhenSendFailed(10); 3、如果broker宕机,producer会重试发送到另一台broker 同步发送+自动重试机制+多个Master节点 存储阶段: 1、同步刷盘来保证消息的不丢失,同样会损失一部分性能。(默认为异步) 2、集群部署保证高可用。等Master和Slave都刷完盘后才去通知Producer说消息ok了。brokerRole=SYNC_MASTER 消费阶段: 1、手动ack 2、自动重试15次,进入死信队列
发消息的时候选择queue的算法
random、hash、自定义
为什么同一个消费组设置不同tag会出现奇怪现象
两个相同组的消费者c1,c2相同topic,订阅tag1,tag2。此时往这个topic的两个tag分别发送10条消息。会发现c1没有收到消息,c2只收到了不到10条消息。消息能够正常发送。
原因:broker的问题。
Consumer端发心跳给Broker,Broker收到后存到consumerTable里(就是个Map),key是GroupName,value是ConsumerGroupInfo。
ConsumerGroupInfo里面是包含topic等信息的,但是问题就出在上一步骤,key是groupName,你同GroupName的话Broker心跳最后收到的Consumer会覆盖前者的。所以c1的tag1被覆盖,无法接收到消息。为何c2没有收到10条消息呢,因为是集群模式消费,所以会有负载均衡,有一部分消息到达了c1但tag为tag2,无法消费,所以c2只收到了几条消息而非10条。如果换为广播模式,则c2能接收到10条消息。
注意:一个consumer可以订阅多个topic(存在一个map以topic为键)
消费者负载均衡策略
- queue个数大于Consumer个数,且queue个数能整除Consumer个数的话, 那么Consumer会平均分配queue。(比如上面表格的Consumer有2个 可以整除部分)
- queue个数大于Consumer个数,且queue个数不能整除Consumer个数的话, 那么会有一个Consumer多消费1个queue,其余Consumer平均分配。(比如上面表格的Consumer有3个 不可整除部分)
- queue个数小于Consumer个数,那么会有Consumer闲置,就是浪费掉了,其余Consumer平均分配到queue上。(比如上面表格的Consumer有5个 无法都分配部分)
nginx
负载均衡有哪些算法
轮询法、随机法、源地址hash法、加权轮询、加权随机、最小连接数法(动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求)
红黑树管理timer:
单服务器抗压
1.对socket方面的优化 1)操作系统(linux)的设置: 增大socket的最大连接数 echo 50000 > /proc/sys/net/core/somaxconn (系统默认的值是128,现在改成50000) 加快系统的tcp回收机制 (系统默认tcp在断开后还会存活一段时间) 方法如下 echo 1 > /proc/sys/net/ipv4/tcp_tw_recycle (系统默认是0,修改为1) 允许空的tcp回收利用 方法如下 echo 1 >/proc/sys/net/ipv4/tcp_tw_reuse (系统默认为0,修改为1) 让系统不做洪水抵御保护,(当系统检测到80端口在大量的请求时,会自动给返回信息中增加 cookie ,还验证客户端身份,从而避免受到攻击,但这时只是高并发,并不是攻击,所以要把这个抵御机制给关闭) 方法如下 echo 0 >/proc/sys/net/ipv4/tcp_syncookie (系统默认为1,修改为0) 2)nginx的设置: 增大子进程打开的连接 ,在event段中 worker_connections 1024; nginx默认能打开1024个连接 修改worker_connections 10000; 修改为可以打开10000个socket连接 2.对文件系统方面的优化 1)操作系统方面: 让操作系统允许打开更多的文件 ulimit -n(设置一个比较大的值) ulimit -n 10240; (把操作系统允许打开文件的最大值设为10240,原本的默认值是1024) 2)nginx 配置子进程可以打开的文件个数 在nginx全局的配置中 worker_processes 1;下面加上worker_limit_nofile 10240; work_limit_nofile 10240 ; (nginx的子进程可以打开10240个文件)
进程模型
nginx模型有两种进程,master进程和worker进程。master进程主要用来管理worker进程,管理包含:接收来自外界的信号,向各worker进程发送信号,监控worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。
而基本的网络事件,则是放在worker进程中来处理了。多个worker进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。worker进程的个数是可以设置的,一般我们会设置与机器cpu核数一致.
启动方式
Nginx的启动方式有两种:
单进程启动:此时系统中只有一个进程,这个进程既是master进程,也是worker进程。
多进程启动:此时系统中有且仅有一个master进程,有多个worker进程,master进程主要是用来管理worker进程的。
如何处理请求
worker进程之间是平等的,每个进程,处理请求的机会也是一样的。
当我们提供80端口的http服务时,一个连接请求过来,每个进程都有可能处理这个连接。首先,每个worker进程都是从master进程fork过来,在master进程里面,先建立好需要listen的socket之后,然后再fork出多个worker进程,这样每个worker进程都可以去accept这个socket。
一般来说,当一个连接进来后,所有在accept在这个socket上面的进程,都会收到通知,而只有一个进程可以accept这个连接,其它的则accept失败,这是所谓的惊群现象(惊群现象(thundering herd)就是当多个进程和线程在同时阻塞等待同一个事件时,如果这个事件发生,会唤醒所有的进程,但最终只可能有一个进程/线程对该事件进行处理,其他进程/线程会在失败后重新休眠,这种性能浪费就是惊群。)
当然,nginx也不会视而不见,所以nginx提供了一个accept_mutex这个东西,从名字上,我们可以看这是一个加在accept上的一把共享锁。有了这把锁之后,同一时刻,就只会有一个进程在accpet连接,这样就不会有惊群问题了。accept_mutex是一个可控选项,我们可以显示地关掉,默认是打开的。
当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。我们可以看到,一个请求,完全由worker进程来处理,而且只在一个worker进程中处理。
通信
linux与nginx之间通过信号进行通信。
master进程与worker进程通过sockpair(全双工通信)进行通信(channel)
worker进程间则是通过比较快速的共享内存进行通信。(mmap内存映射、通过文件、通过system v)
数据库MySQL
脏读、不可重复读、幻读
脏读:脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个被修改的数据,然后使用了这个数据。事务读取到了其他事务修改并且未提交的数据。
不可重复读:一个事务多次读取同一数据,该数据记录会改变。(其他事务进行了数据的修改)
幻读:多次读取,后面读取时数据记录数量发生改变。(其他事务进行了数据的插入、删除操作)
四种隔离级别:
读未提交、读已提交、可重复读、串行化。
读已提交解决脏读;可重复读解决脏读和不可重复读;串行化解决所有问题
delete、truncate、drop区别
1) DELETE语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作。会触发触发器。TRUNCATE TABLE 则一次性地从表中删除所有的数据并不把单独的删除操作记录记入日志保存,删除行是不能恢复的。并且在删除的过程中不会激活与表有关的删除触发器。执行速度快。 2) 释放空间问题 当表被TRUNCATE 后,这个表和索引所占用的空间会恢复到初始大小, DELETE操作不会减少表或索引所占用的空间。 drop语句将表所占用的空间全释放掉。 一般而言,drop > truncate > delete 3) TRUNCATE 只能对TABLE; DELETE可以是table和view TRUNCATE 和 DELETE 只删除数据, DROP则删除整个表(结构和数据)。 TRUNCATE 无法使用 WHERE 子句 eg. delete from table_test where ... / truncate table table_test
索引、索引类型(Hash、B+、全文索引)
innodb、MyIsam——数据节点存储数据 or 存储数据地址(指针)
MyISAM索引文件和数据文件分开——非聚集索引、InnoDB则是索引文件和数据文件在一起——聚集索引。
主键索引、辅助索引。
最左前缀匹配。
索引类型:
1.普通索引;2.唯一索引;3.主键索引;4.组合索引;5.全文索引(innodb不支持)
创建联合索引:
create index xxx on 表名(字段,字段,xxx)
增加索引:
ALTER TABLE 'table_name' ADD INDEX index_name('col1','col2','col3')
hash索引
对单行记录查询非常快,但不支持范围查询。要想支持范围查询,需要和其他数据结构进行结合。
如:hash + 跳表
聚簇索引和非聚簇索引
聚簇索引:索引和数据一起存放(innodb),非聚簇索引:索引的叶节点是指针指向对应的数据,分开存放(myisam)
插入数据时,聚簇索引需要排序,非聚簇索引则需要维护索引到数据的指针。非聚集索引会存在索引和数据的两次io,更耗时。
innodb四大特性
插入缓存、两次写、自适应hash、提前读click here or there
innodb底层详解
InnoDB的内存架构主要分为三大块,缓冲池(Buffer Pool)、重做缓冲池(Redo Log Buffer)和额外内存池
缓冲池采用了LRU算法,但可能导致缓冲池污染。
mysql在写入记录前,会先记录到redo log,用于刷盘。
插入缓存:等数据达到某个阈值(例如50条)才批量的写入磁盘,降低io。
两次写:插入缓冲提高了MySQL的性能,而两次写则在此基础上提高了数据的可靠性。我们知道,当数据还在缓冲池中的时候,当机器宕机了,发生了写失效,有Redo Log来进行恢复。但是如果是在从缓冲池中将数据刷回磁盘的时候宕机了呢?这种情况叫做部分写失效,此时重做日志就无法解决问题。
在刷脏页时,并不是直接刷入磁盘,而是copy到内存中的Doublewrite Buffer中,然后再拷贝至磁盘共享表空间(你可以就理解为磁盘)中,每次写入1M,等copy完成后,再将Doublewrite Buffer中的页写入磁盘文件。有了两次写机制,即使在刷脏页时宕机了,在实例恢复的时候也可以从共享表空间中找到Doublewrite Buffer的页副本,直接将其覆盖原来的数据页即可。
自适应哈希索引:参考jit热点代码,对热点索引进行hash。
提前读:innodb中将64个页划分为一个extent,当一个extent中的页,被顺序读超过了多少个,比如50个,这个值是可以通过nnodb_read_ahead_threshold设置的,那么就会认为顺序读到下一个extent的可能性很大,会提前将下一个extent中的所有页都加载到buffer pool中,这叫线性预读
间隙锁
Innodb在可重复读提交下为了解决幻读问题时引入的锁机制。
针对范围查询,例如查询id为1-9之间的所有数据(前提,数据库中没有id为2 4 6的数据),但是范围锁就会将1-9的所有都锁上,如果此时想要插入一条id为2的数据,是无法插入的,会导致阻塞。
间隙锁可能导致死锁,间隙锁之间并不是互斥的。
解决mysql读写效率
sql优化、索引、缓存、主从复制+读写分离、垂直拆分(分布式)、水平拆分(解决主键问题)、分区
MVCC原理
多版本并发控制技术。保存数据的历史版本。可以通过比较版本号决定数据是否显示出来。读取数据的时候不需要加锁可以保证事务的隔离效果。
innodb:更新前建立undo log,根据各种策略读取时非阻塞就是MVCC,undo log中的行就是MVCC中的多版本。即:事务更新某记录时,先用排他锁锁定,然后copy一份记录到undo log然后让roll_ptr指向undo log,然后进行更新并填写事务编号。
解决读写之间阻塞的问题,通过 MVCC 可以让读写互相不阻塞,读不相互阻塞,写不阻塞读,这样可以提升数据并发处理能力。 降低了死锁的概率,这个是因为 MVCC 采用了乐观锁的方式,读取数据时,不需要加锁,写操作,只需要锁定必要的行。 解决了一致性读的问题,当我们朝向某个数据库在时间点的快照是,只能看到这个时间点之前事务提交更新的结果,不能看到时间点之后事务提交的更新结果。
InnoDB 的 MVCC 是如何实现的?
InnoDB 是如何存储记录多个版本的?这些数据是 事务版本号,行记录中的隐藏列(row_id, tx_id, roll_ptr)和Undo Log。
事务查询行记录: 当前事务的 creator_trx_id 想要读取某个行记录,这个行记录ID的trx_id ,这样会有以下的情况: 如果 trx_id < 活跃的最小事务ID(up_limit_id),也就是说这个行记录在这些活跃的事务创建前就已经提交了,那么这个行记录对当前事务是可见的。 如果trx_id > 活跃的最大事务ID(low_limit_id),这个说明行记录在这些活跃的事务之后才创建,说明这个行记录对当前事务是不可见的。 如果 up_limit_id < trx_id <low_limit_id,说明该记录需要在 trx_ids 集合中,可能还处于活跃状态,因此我们需要在 trx_ids 集合中遍历,如果trx_id 存在于 trx_ids 集合中,证明这个事务 trx_id 还处于活跃状态,不可见,否则 ,trx_id 不存在于 trx_ids 集合中,说明事务trx_id 已经提交了,这行记录是可见的。 如何查询一条记录 1、获取事务自己的版本号,即事务ID 2、获取 Read View 3、查询得到的数据,然后 Read View 中的事务版本号进行比较。 4、如果不符合 ReadView 规则, 那么就需要 UndoLog 中历史快照; 5、最后返回符合规则的数据 InnoDB 实现多版本控制 (MVCC)是通过 ReadView + UndoLog 实现的,UndoLog 保存了历史快照,ReadView 规则帮助判断当前版本的数据是否可见。
innodb如何保证崩溃恢复能力
两阶段日志提交
MVCC下的一些操作
增删查改 在InnoDB中,给每行增加两个隐藏字段来实现MVCC,一个用来记录数据行的创建时间,另一个用来记录行的过期时间(删除时间)。在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。 于是乎,默认的隔离级别(REPEATABLE READ)下,增删查改变成了这样: SELECT:读取创建版本小于或等于当前事务版本号,并且删除版本为空或大于当前事务版本号的记录。这样可以保证在读取之前记录是存在的。 INSERT:将当前事务的版本号保存至行的创建版本号 UPDATE:新插入一行,并以当前事务的版本号作为新行的创建版本号,同时将原记录行的删除版本号设置为当前事务版本号 DELETE:将当前事务的版本号保存至行的删除版本号 快照读和当前读 快照读:读取的是快照版本,也就是历史版本 当前读:读取的是最新版本 普通的SELECT就是快照读,而UPDATE、DELETE、INSERT、SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE是当前读。
read-view(一致性视图)
未提交的事务id数组以及当前已经创建(不论是否提交)的最大事务id。(【未提交事务id】,max_id)
当执行查询sql时会生成一致性视图read-view,它由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id (max_id)组成,查询的数据结果需要跟read-view做比对从而得到快照结果 版本链比对规则: 1.如果落在绿色部分( trx_id < min_id ),表示这个版本是已提交的事务生成的,这个数据是可见的; 2.如果落在红色部分( trx id > max_id ),表示这个版本是由将来启动的事务生成的,是肯定不可见的; 3.如果落在黄色部分(min_ id <= trx_id <= max_id),那就包括两种情况 a.若row的trx_id在数组中,表示这个版本是由还没提交的事务生成的,不可见,当前自己的事务是可见的; b.若row的trx_id不在数组中,表示这个版本是已经提交了的事务生成的,可见。 对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的trx_id,同时在该条记录的头信(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经被删除,在查间时按照上面的规则查到对应的记录如果delete flag标记位为true,意味着记录已被删除,则不返回数据。
如果是可重复读的隔离级别,则MVCC中每个select记录使用的是之前select的read-view,即进行延用而非重新创建。(当一个session发起第一个查询时,此刻对应的全表的read-view已经确定了,之后的查询都会使用该read-view)
而如果是读已提交的隔离界别,则MVCC中每个select记录都会重新生成read-view。
mvcc不同实现方式
第一种实现方式是将数据记录的多个版本保存在数据库中,当这些不同版本数据不再需要时,垃圾收集器回收这些记录。
这个方式被PostgreSQL和Firebird/Interbase采用,SQL Server使用的类似机制,所不同的是旧版本数据不是保存在数据库中,而保存在不同于主数据库的另外一个数据库tempdb中/
第二种实现方式只在数据库保存最新版本的数据,但是会在使用undo时动态重构旧版本数据。
这种方式被Oracle和MySQL/InnoDB使用。
mysql的mvcc的缺陷
The snapshot of the database state applies to SELECT statements within a transaction, not necessarily to DML statements. If you insert or modify some rows and then commit that transaction, a DELETE or UPDATE statement issued from another concurrent REPEATABLE READ transaction could affect those just-committed rows, even though the session could not query them. If a transaction does update or delete rows committed by a different transaction, those changes do become visible to the current transaction.
一个不同的事务通过update或者delete语句进行修改时,会导致被修改的记录对于之前的事务的select语句可见,而本身是不应该的。根本原因在于 MySQL 在 update/delete/insert/select for update/select lock in share mode 时进行的是 current read(selectlocktype != LOCK_NONE) 而非 consistent read。
select所见非update所用啊。
行锁和表锁(具体)
行锁:锁定一行或者多行记录,行锁是基于索引加载的,可能死锁。
表锁:没有触发索引,则会锁表。表锁响应的是非索引字段,即全表扫描,不会死锁。
如何实现的隔离级别
读写锁 or MVCC。
事务看到的是自己查询时候的快照ReadView。
回表查询
第一遍定位主键值,再定位行记录,它的性能较扫一遍索引树更低。
索引覆盖
查询时不需要回表查询,直接通过索引就可以获取查询的结果数据,extra:using index代表索引覆盖
mysql,数据查询慢怎么办
慢查询抓取sql语句,然后用explain执行计划判断,建索引,B+树,为什么可以增快速度。
show profiles查看sql语句性能。
explain
id、select_type、table、partitions、type、possible_keys、key、key_len、ref、row、filtered、Extra
* id: SELECT查询的标识符.每个SELECT都会自动分配一个唯一的标识符. * select_type: SELECT查询的类型. * table: 查询的是哪个表 * partitions: 匹配的分区 * type: join类型 - 从好到坏 最少应使用range - system、const、eq_ref、ref、fulltext、ref_or_null、unique_subquery、index_subquery、[range]、index_merge、index、ALL * possible_keys: 此次查询中可能选用的索引 * key: 此次查询中确切使用到的索引. * ref: 哪个字段或常数与key一起被使用 * rows: 显示此查询一共扫描了多少行.这个是一个估计值. * filtered: 表示此查询条件所过滤的数据的百分比 * extra: 额外的信息
分库、分表、分区
就搞一个主库,挂多个从库,然后我们就单单只是写主库,然后主库会自动把数据给同步到从库上去。读从库。主从复制,binlog。
分库:垂直按功能模块切分、水平减少数据库压力、数据隔离、功能切分。
分表:数据冗余问题、热点数据等等。
分区:分区并不是生成新的数据表,而是将表的数据均衡分摊到不同的硬盘,系统或是不同服务器存储介子中,实际上还是一张表。另外,分区可以做到将表的数据均衡到不同的地方,提高数据检索的效率,降低数据库的频繁IO压力值。
分区就是把一张表的数据分成N个区块,在逻辑上看最终只是一张表,但底层是由N个物理区块组成的
垂直拆分是把不同的表拆到不同的数据库中,而水平拆分是把同一个表拆到不同的数据库中。
垂直拆分和水平拆分
首选垂直拆分
垂直切分是指按照业务将表进行分类,分布到不同的数据库上面,这样也就将数据或者说压力分担到不同的库上面 。
水平拆分的典型场景就是大家熟知的分库分表。
垂直拆分后遇到单机瓶颈,可以使用水平拆分。相对于垂直拆分的区别是:垂直拆分是把不同的表拆到不同的数据库中,而水平拆分是把同一个表拆到不同的数据库中。
mysql主从复制原理
①为何需要主从复制?
并发量大时,需要读写分离,主库负责写,从库负责读,即使需要锁表,对读的性能也没有影响。
数据的预热。
降低单机IO压力。②主从复制原理(bin log)
原理: 1)、在master机器上的操作: 当master上的数据发生变化时,该事件变化会按照顺序写入bin-log中。当slave链接到master的时候,master机器会为slave开启binlog dump线程。当master的binlog发生变化的时候,bin-log dump线程会通知slave,并将相应的binlog内容发送给slave。 2)、在slave机器上操作: 当主从同步开启的时候,slave上会创建两个线程:I\O线程。该线程连接到master机器,master机器上的binlog dump 线程会将binlog的内容发送给该I\O线程。该I/O线程接收到binlog内容后,再将内容写入到本地的relay log;sql线程。该线程读取到I/O线程写入的ralay log。并且根据relay log。并且根据relay log 的内容对slave数据库做相应的操作。 也就是说: - 从库会生成两个线程,一个I/O线程,一个SQL线程; - I/O线程会去请求主库的binlog,并将得到的binlog写到本地的relay-log(中继日志)文件中; - 主库会生成一个log dump线程,用来给从库I/O线程传binlog; - SQL线程,会读取relay log文件中的日志,并解析成sql语句逐一执行;
③三种主要实现粒度
详细的主从同步主要有三种形式:statement、row、mixed
1)、statement: 会将对数据库操作的sql语句写道binlog中
2)、row: 会将每一条数据的变化写道binlog中。
3)、mixed: statement与row的混合。Mysql决定何时写statement格式的binlog, 何时写row格式的binlog。④主从复制延迟问题
原因:
1)、MySQL数据库主从同步延迟原理mysql主从同步原理:主库针对写操作,顺序写binlog,从库单线程去主库顺序读”写操作的binlog”,从库取到binlog在本地原样执行(随机写),来保证主从数据逻辑上一致。mysql的主从复制都是单线程的操作,主库对所有DDL和DML产生binlog,binlog是顺序写,所以效率很高,slave的Slave_IO_Running线程到主库取日志,效率比较高,下一步,问题来了,slave的Slave_SQL_Running线程将主库的DDL和DML操作在slave实施。DML和DDL的IO操作是随即的,不是顺序的,成本高很多,还可能可slave上的其他查询产生lock争用,由于Slave_SQL_Running也是单线程的,所以一个DDL卡主了,需要执行10分钟,那么所有之后的DDL会等待这个DDL执行完才会继续执行,这就导致了延时。有朋友会问:“主库上那个相同的DDL也需要执行10分,为什么slave会延时?”,答案是master可以并发,Slave_SQL_Running线程却不可以。 2)、MySQL数据库主从同步延迟是怎么产生的?当主库的TPS并发较高时,产生的DDL数量超过slave一个sql线程所能承受的范围,那么延时就产生了,当然还有就是可能与slave的大型query语句产生了锁等待。首要原因:数据库在业务上读写压力太大,CPU计算负荷大,网卡负荷大,硬盘随机IO太高次要原因:读写binlog带来的性能影响,网络传输延迟。
解决方案:
①硬件方面;②禁用从库binlog;③分担压力等等
事务如何实现隔离性
读写锁 or MVCC
三大范式
1NF:字段不可分
2NF:属性完全依赖于主键
3NF:属性不依赖于其它非主属性 属性直接依赖于主键
BCNF:无传递依赖
事务特性
ACID,原子性、一致性、隔离性、持久性
一致性:数据前后的完整性。
sql很慢优化:
索引、拆分表、大字段、减少函数运算、内存、cpu
自增id用完
id边界值使用后,越过此边界值插入数据会失败。主键冲突、无法插入。使用big int or 自己的主键
mysql和redis数据一致性保证
索引下推
即如果下图中
name
也有索引,则会将其过滤也放在底层进行过滤,进而增大效率。在不使用ICP的情况下,在使用非主键索引(又叫普通索引或者二级索引)进行查询时,存储引擎通过索引检索到数据,然后返回给MySQL服务器,服务器然后判断数据是否符合条件 。 在使用ICP的情况下,如果存在某些被索引的列的判断条件时,MySQL服务器将这一部分判断条件传递给存储引擎,然后由存储引擎通过判断索引是否符合MySQL服务器传递的条件,只有当索引符合条件时才会将数据检索出来返回给MySQL服务器 。 索引条件下推优化可以减少存储引擎查询基础表的次数,也可以减少MySQL服务器从存储引擎接收数据的次数。
join和union区别
join是通过on上面的条件进行的
union则是将两个结果集进行合并
窗口函数(用于组内排名)
# 基本语法 <窗口函数> over (partition by <用于分组的列名> order by <用于排序的列名>) # rank, dense_rank, row_number # rank排名会间隔 如 1 2 2 4 5... # 而dense_rank则不会间隔 如 1 2 2 3 4 5 # row_number则不考虑并列的情况
<窗口函数>的位置,可以放以下两种函数:
1) 专用窗口函数,包括后面要讲到的rank, dense_rank, row_number等专用窗口函数。
2) 聚合函数,如sum. avg, count, max, min等
因为窗口函数是对where或者group by子句处理后的结果进行操作,所以窗口函数原则上只能写在select子句中。
partition by用来对表分组,order by子句的功能是对分组后的结果进行排序(默认asc)。
partition by不会改变表的行数,而group by则会改变。
对于聚合函数,则会在分组内,按顺序进行。例如:
select *, sum(成绩) over (order by 学号) as current_sum, avg(成绩) over (order by 学号) as current_avg, count(成绩) over (order by 学号) as current_count, max(成绩) over (order by 学号) as current_max, min(成绩) over (order by 学号) as current_min from 班级表
数据库分库分表主键处理
①使用自增主键,适用于数据量大并发较小需要分表的情况。
②设置数据库 sequence 或者表自增字段步长。比如说,现在有 8 个服务节点,每个服务节点使用一个 sequence 功能来产生 ID,每个 sequence 的起始 ID 不同,并且依次递增,步长都是 8。方案简单,但是如果需要增加服务节点时,就较为麻烦。
③UUID,好处就是本地生成,不用基于数据库来了;不好之处就是,UUID 太长了、占用空间大,作为主键性能太差了,会导致索引效率低下
④snowflake 算法,把一个 64 位的 long 型的 id,1 个 bit 是不用的,用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。
设计模式
工厂模式、单例模式、原型模式、装饰器模式、代理模式。
工厂:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。1、一个调用者想创建一个对象,只要知道其名称就可以了。 2、扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。 3、屏蔽产品的具体实现,调用者只关心产品的接口。
单例:懒汉、饿汉、双重检验锁懒汉
volatile
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
原型模式:创建对象代价太大 采用实现Cloneable接口和重写clone方法
装饰器模式:装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
代理模式:jdk代理和cglib代理
分布式系统
rpc框架
dubbo、springcloud、thrift
分布式与集群的区别
集群是指将多台服务器集中在一起提供同一种服务,在逻辑上可以看做是一台服务器对外进行服务,这些服务器组合就是集群。
从概念上就可以看出两者最主要的区别就是分布式是将一种业务拆分成多个子业务部署在多台服务器上,进而对外提供服务;而集群就是将多台服务器组合在一起提供同一种服务;
集群强调在多台服务器位置集中,并且容易统一管理;而分布式没有具体要求,不论放置在哪个位置,只要通过网络连接起来就行;
集群是一种物理形态,即多台服务器在一起提供一种服务;而分布式是一种工作方式,即一个程序或业务分解到多台服务器分别完成;
从单机结构到集群结构,你的代码基本无需要作任何修改,你要做的仅仅是多部署几台服务器,每台服务器上运行相同的代码就行了。但是,当你要从集群结构演进到微服务结构的时候,之前的那套代码就需要发生较大的改动了。所以对于新系统我们建议,系统设计之初就采用微服务架构,这样后期运维的成本更低。但如果一套老系统需要升级成微服务结构的话,那就得对代码大动干戈了。所以,对于老系统而言,究竟是继续保持集群模式,还是升级成微服务架构,这需要你们的架构师深思熟虑、权衡投入产出比。 分布式结构就是将一个完整的系统,按照业务功能,拆分成一个个独立的子系统,在分布式结构中,每个子系统就被称为“服务”。这些子系统能够独立运行在web容器中,它们之间通过RPC方式通信。举个例子,假设需要开发一个在线商城。按照微服务的思想,我们需要按照功能模块拆分成多个独立的服务,如:用户服务、产品服务、订单服务、后台管理服务、数据分析服务等等。这一个个服务都是一个个独立的项目,可以独立运行。如果服务之间有依赖关系,那么通过RPC方式调用。
微服务如何拆分服务
①服务拆分最多三层,两次调用 ②仅仅单向调用,严禁循环调用 ③将串行调用改为并行调用,或者异步化 ④接口应该实现幂等
为何需要幂等性
- 前端重复提交:提交订单,用户快速重复点击多次,造成后端生成多个内容重复的订单。
- 接口超时重试:对于给第三方调用的接口,为了防止网络抖动或其他原因造成请求丢失,这样的接口一般都会设计成超时重试多次。
- 消息重复消费:MQ消息中间件,消息重复消费。
如何实现接口幂等性
数据库上的幂等性相关:select和delete一定是幂等性,而insert如果带唯一索引则是幂等性,不带唯一索引则不是;update对于计算式的更新不是幂等性的,非计算式则是幂等性的。
只有不带唯一索引插入(insert)和计算式的更新(update)会引起幂等性问题的
单机幂等性:代码逻辑判断、数据库约束、token(在内存中存储正在处理的用户id来实现幂等性)
分布式系统幂等性:
①token+redis保证多次请求被拦截,
②数据库通过唯一索引特性保证唯一性
③redis将唯一序列号作为key进行校验,需要设置过期时间。
④状态机,每个状态存在前置状态,无法回退。
前后端分离?
前后端通过http请求通信,后端只负责提高api接口和api文档。页面跳转交给前端完成。
微服务链式调用异常
避免调用链过长,通过消息队列解耦
其他
CAP
- 一致性(Consistency):数据在多个副本之间能否保持一致的特性
- 可用性(Availability):提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。这里的重点是"有限时间内"和**“返回结果”**。
- 分区容忍性(Partition tolerance):分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。(基本需求
BASE
- Basically Availble --基本可用:基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。
- Soft-state --软状态/柔性事务:即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
“Soft state” 可以理解为"无连接"的, 而 “Hard state” 是"面向连接"的
- Eventual Consistency --最终一致性:所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。
对CAP理论中一致性和可用性的权衡。
最终一致性(分布式事务)
2PL,两阶段提交。
消息重试、补偿交易、事务消息、接口支持重入
什么是可重入
重入函数也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果确实需要访问全局变量(包括static),一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要,但是一般要为访问全局变量付出一些性能代价。
项目
基于区块链技术的数据共享平台
去中心化的数据共享 + 计算
客户 主要为高校科研团队、交通委
数据提供方则为 路网中心、高德这类
共享数据 给激励 用其他人的数据就需要花钱
类似于kaggle的模式
rocketmq收发消息 广播模式直接无法绑定
消息组相同 tag不同的情况
金刚石文档项目
markdown文档编辑器
团队协作
幂等性问题
Linux
wc
wc [-clw] 文件*
计算文件的行数 单词数 字节数
-c或–bytes或–chars 只显示Bytes数。-l或–lines 显示行数。-w或–words 只显示字数。
可以跟多个文件,则会都显示并进行汇总
Linux查看内存, cpu的占有率的命令
top
netstat、find、cat、cp、mv、su、ftp(ftp+ip)
netstat 命令用于显示网络状态。-a显示所有套接字和端口。例子:TCP 192.168.9.52:1299 60.210.8.160:https CLOSE_WAIT
find命令格式:find path -xxx expression。将当前目录及其子目录下所有文件后缀为 .c 的文件列出来:find . -name “*.c”
把 textfile1 的文档内容加上行号后输入 textfile2 这个文档里:cat -n textfile1 > textfile2 (-b和-n相似只是-b不会把空白行输出)
ps
-a/-e 所有
-f 输出完整的
根据时间查找文件
---(+n)----------|----------(n)----------|----------(-n)--- (n+1)*24H前| (n+1)*24H~n*24H间 |n*24H内 mtime 文件内容上次修改时间 atime 文件被读取或访问的时间 ctime 文件状态变化时间 eg. 查找./本目录下十天前修改过的目录(层级为1,即当前目录下),并删除 find ./ -mtime +10 -type d -maxdepth 1 | xargs rm -rf eg. 当前时间24小时 - 当前时间(昨天-今天) find . -mtime 0
chmod
chmod [owner group others]
RWX——777chmod 777 file
telnet
telnet + ip登陆远程主机
Linux 中主要有哪几种内核锁
mutex互斥锁、semaphore信号量、Spanlock自旋锁、seqlock顺序锁、rwlock读写锁(读不阻塞,写阻塞)
linux申请内存
kmalloc() 申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因为存在较简单的转换关系,所以对申请的内存大小有限制,不能超过128KB。kzalloc()将申请到的内存清0。
vmalloc() 函数则会在虚拟内存空间给出一块连续的内存区,但这片连续的虚拟内存在物理内存中并不一定连续。由于 vmalloc() 没有保证申请到的是连续的物理内存,因此对申请的内存大小没有限制,如果需要申请较大的内存空间就需要用此函数了。
只有开机时,申请到大块内存的概率较大。
算法
反转链表:递归、迭代
删除字符串中的空格,时间O(n),空间O(1)
写一个程序判断机器是大端法还是小端法(什么是大端、什么是小端)
小端法:低地址存放低位,高地址存放高位
int i = 0x11223344; char *c = (char*)(&i); printf("%x", *c); // 0x44-小端 0x11-大端
很长的英文论文,统计出现最高频的k个单词。(hashmap + 堆)
一组无序的数,找出中位数。(快速选择,O(n))
熟悉的排序算法(归并、快排、冒泡、插入、选择) 从小到大
插入排序:从第二个数开始,和前面的数进行比较,如果比前面的数小,则交换,直到没有比该数大的数。继续遍历下一个数。
选择排序:从当前遍历到的数开始遍历到结尾,找到最小的数,和当前数交换,直到遍历完数组。
冒泡排序:每一趟排序,将相邻的两个数进行比较,小的放在前面。重复len - 1趟,且每一趟都能确定len - 1 - i个数。
归并排序:分治 + 双指针。
快速排序:分治 + 选取枢纽。
稳定:插入、冒泡、归并
不稳定:选择、快排、堆排序
10亿个数排序后打印,内存有限(拆分成小文件并排好序分别放入小文件,然后每个文件进行读取首数据然后关闭然后读取下一个文件,可以利用堆排序实现大文件的有序)
从100万个数中找出最大的前100个数(快速选择 or 维护最大堆)
java操作有500万数据的大表如何操作(多线程?分表?limit分页?)
两个线程交替打印1-100
分布锁?
下层层序序列的完全二叉树 打印前序遍历结果
找出一个字符串有多少回文字串,输出最大的
Leetcode 1438
最长上升子序列(需要输出子序列)
64匹马 8条赛道 找出跑的最快的4匹 最少需要几场比赛(11)
lru
高并发设计:
拆分为服务、缓存、mq、分库分表、读写分离、分布式
如果不用红黑树,怎么把hash后桶上的链表存入到磁盘空间内,要怎么设计 磁盘内的存储方式?
桶上的链表再进行一次hash。
——————————
——————
————
———
—
👆这样划分磁盘的存储空间,使记录hash后的值时先在最大的那块记录,有冲突就往第二大的快记录。使最大的磁盘块能直接返回值,防止hash冲突。
位运算实现加法
0 + 0 = 0 不进位 1 + 0 = 1 不进位 0 + 1 = 1 不进位 1 + 1 = 0 进位1 -------------- 加法本身:a ^ b 进位:(a & b) << 1 -------------- 结论:设a,b为两个二进制数,则a+b = a^b + (a&b)<<1。 证明:a^b是不考虑进位时加法结果。当二进制位同时为1时,才有进位,因此 (a&b)<<1是进位产生的值,称为进位补偿。将两者相加便是完整加法结果。 后续的加法通过递归即可 public int add(int a, int b) { if (b == 0) return a; // 进位为0时退出递归 return add(a ^ b, (a & b) << 1); }
4亿个unsigned int型的数让你保存,然后给你一个数,判断它是否存在已经保存的数中。
bitmap
高考成绩查询高并发
省份、成绩区间分库分表
redis预热数据
前端页面提前缓存CDN
nginx负载均衡
中间件
三数之和
lc 678
lc 316(单调栈 + 贪心)
场景题:两堆大数,100亿个数和10亿个数,找交集
场景题:直播房间,一个大V发了一条消息,如何让上千万的粉丝收到这条消息,如果只是纯粹的广播会很耗资源
自己拉取 ?
转载:https://blog.csdn.net/qq_43780964/article/details/115622749