2022校招面经总结
文章目录
- 前言
- 一、随机问题
- 二、JVM
- 三、计算机网络
- 四、Redis
- 五、Mybatis
- 六、消息队列
- 七、Docker
- 八、手撕代码
前言
这是2022届虾皮校招提前批面试题目及答案总结,后续会继续更新
其他方面的知识总结:java基础、java多线程、数据库
一、随机问题
1、linux系统查看cpu负载
ps -ef || ps axu || top
2、数组与链表的区别
- 存储位置上:数组逻辑上相邻的元素在物理存储位置上也相邻,而链表不一定
- 存储空间上:链表存放的内存空间可以试连续的,也可以是不连续的,数组则是连续的一段内存空间。一般情况下存放相同多的数据数组占用较小的内存,而链表还需要存放其前驱和后继的空间
- 长度的可变性:链表的长度是按实际需要可以伸缩的,而数组的长度在定义时就要指定,且不能变化
- 按序号查找时,数组可以随机访问,时间复杂度为1,而链表不支持随机访问,平均为O(n)
- 按值查找时,若数组无序,数组和链表时间复杂度均为O(1),但是当数组有序时,可以采用二分查找降为O(logn)
- 插入和删除时,数组平均需要移动n/2个元素,而链表只需要修改指针即可
- 空间分配方面:数组在静态存储分配情形下,存储元素数量受限制,动态存储分配情形下,虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且如果内存中没有更大块连续存储空间将导致分配失败; 即数组从栈中分配空间,,对于程序员方便快速,但自由度小。链表存储的节点空间只在需要的时候申请分配,只要内存中有空间就可以分配,操作比较灵活高效;即链表从堆中分配空间, 自由度大但申请管理比较麻烦。
3、从一组数据中选取100个最大的数据
采用小顶堆结构,将堆的大小设置为100,先放入100个数据,然后每次比较当前元素与堆顶的大小,小于直接下一个,大于则删除堆顶,将当前元素放入,更新堆顶。直到遍历完所有元素。时间复杂度为O(n)。
4、对象关系映射(ORM)
对象关系映射(Object Relational Mapping),是一种程序设计技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。从效果上说,它创建了一个可在编程语言里使用的“虚拟对象数据库”
(1)对象数据库
对象数据库是一种以对象形式表示信息的数据库。对象数据库管理系统(ODBMS)。
使用对象数据库的原因
- 关系型数据库在管理复杂数据时显得笨重
- 被应用软件操作的数据一般是用面向对象的编程语言写成,而那些用来转化数据表示和关系数据库元组的代码很冗繁,执行时非常耗时
5、常见的负载均衡算法
(1)轮询法(Round Robin)
轮询法基本上算是最简单的负载均衡算法了,他的思想就是不管啥情况,对所有的服务器节点全部按顺序来,将请求按照顺序轮流地分配到各个服务器上。这种算法会使每台服务器处理的请求是相同的,所以适合用于服务器硬件条件基本都相同的情况。
(2)加权轮询法(Weight Robin)
在轮询算法的基础上添加了权重的条件,刚才提到的轮询算法对所有服务器“一视同仁”,那么加权轮询算法无疑就是对各个服务器有了“高低贵贱之分”,没办法,服务器的吃力水平不同,只能让那些强悍的机器优先并多处理一些请求,比较弱的机器就让它稍稍压力小一点。
(3)随机法(Random)
随机算法也是一种使用场景比较多的负载均衡算法,这种算法基本思想也是很简单的,随机生成一个数字(或者随机挑一个IP地址)出来,然后挑到谁就是谁,当然,如果随机数是等概况生成的,那时间长了,基本上跟轮询算法没有什么区别,区别最主要的还是在顺序上,随机算法没有那么严格的顺序。
(4)加权随机法(Weight Random)
加权随机算法是在随机算法的基础上加了加权的条件,随机法时间长了,基本上跟一般轮询算法就没啥区别了,刚才也说到了,如果服务器的配置都差不多,可以分配差不多的任务,但是如果服务器吃力能力差异比较大,那水平高的和水平低的服务器都给那么多任务,对于高配置的服务器来说就是有点浪费了,对于低配置的服务器来说就有点吃不消,所以在这种配置差异性比较大的情况下,加权的工作还是必要的。
(5)最小连接法(Least Connections)
这种算法的思想也是非常简单的,顾名思义,那个服务器的连接数少,就分配给哪个服务器新的请求,合情合理,但是这种算法的缺点就是,当一个比较弱的服务器和一个比较彪悍的服务器,本来就是前者连接的要少,后者要大,如果非要谁的少新请求分配给谁的话,那就是弱服务器的连接要等于强服务器的连接,无疑这样会让弱服务器吃不消,或者造成强服务器的浪费,所以这里还可以使用加权的方法解决这样的问题------加权最小连接法。
(6)源地址哈希法(Hash)
Hash法对于大部分的程序员来说并不陌生,源地址哈希法可以把客户端的IP地址拿出来,然后计算出IP地址的hash值,hash值是一个很大的正整数
6、RSA非对称加密
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;
import org.apache.commons.codec.binary.Base64;
/**
* 非对称加密 唯一广泛接受并实现 数据加密&数字签名 公钥加密、私钥解密 私钥加密、公钥解密
*
* @author jjs
*
*/
public class RSADemo {
private static String src = "infcn";
private static RSAPublicKey rsaPublicKey;
private static RSAPrivateKey rsaPrivateKey;
static {
// 1、初始化密钥
KeyPairGenerator keyPairGenerator;
try {
keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(512);// 64的整倍数
KeyPair keyPair = keyPairGenerator.generateKeyPair();
rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
System.out.println("Public Key : " + Base64.encodeBase64String(rsaPublicKey.getEncoded()));
System.out.println("Private Key : " + Base64.encodeBase64String(rsaPrivateKey.getEncoded()));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
/**
* 公钥加密,私钥解密
*/
public static void pubEn2PriDe() {
//公钥加密
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(rsaPublicKey.getEncoded());
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] result = cipher.doFinal(src.getBytes());
System.out.println("公钥加密,私钥解密 --加密: " + Base64.encodeBase64String(result));
//私钥解密
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(rsaPrivateKey.getEncoded());
keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
result = cipher.doFinal(result);
System.out.println("公钥加密,私钥解密 --解密: " + new String(result));
}
/**
* 私钥加密,公钥解密
*/
public static void priEn2PubDe() {
//私钥加密
PKCS8EncodedKeySpec pkcs8EncodedKeySpec = new PKCS8EncodedKeySpec(rsaPrivateKey.getEncoded());
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec);
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] result = cipher.doFinal(src.getBytes());
System.out.println("私钥加密,公钥解密 --加密 : " + Base64.encodeBase64String(result));
//公钥解密
X509EncodedKeySpec x509EncodedKeySpec = new X509EncodedKeySpec(rsaPublicKey.getEncoded());
keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(x509EncodedKeySpec);
cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, publicKey);
result = cipher.doFinal(result);
System.out.println("私钥加密,公钥解密 --解密: " + new String(result));
}
public static void main(String[] args) {
pubEn2PriDe(); //公钥加密,私钥解密
priEn2PubDe(); //私钥加密,公钥解密
}
}
二、JVM
1、运行时数据区域
(1)程序计数器
程序计数器就是当前线程所执行字节码的行号指示器,线程私有
(2)Java虚拟机栈
线程私有,生命周期和线程相同,存储局部变量表、操作数栈、动态链接、方法出口等。
局部变量表存放了编译期可知的各种基本数据类型,对象引用和returnAddress类型
注:如果线程请求的栈的深度大于虚拟机所允许的深度,就会导致StackOverflowError;如果虚拟机栈是可以动态扩展的,当申请不到足够的内存的时候会导致 OutOfMemoryError
如果栈不支持动态扩展,定义大量变量就会导致栈溢出
(3)本地方法栈
为虚拟机使用到的本地方法服务,也会产生内存溢出和栈溢出。
(4) Java堆
Java堆是虚拟机所管理的内存中最大的一块,Java堆是被所有线程共享的一块内存区域,所有的对象实例以及数组都应该在堆上分配,Java堆是垃圾收集管理的内存区域
会产生内存溢出
通过不断的创建对象,会造成内存溢出
(5)方法区
是线程共享的内存区域,用于存储已被虚拟机加载的类型信息、常量、静态变量、及时编译器编译后的代码缓存等数据
如果无法满足新的内存分配需求,将跑出OutOfMemoryError。定义产生大量的类就会导致内存溢出
(6)运行时常量池
是方法区的一部分。存放各种字面量与符号引用,在类加载后存放到方法区中。当存入的常量数据过大或者过多时,就会产生内存溢出。
2、垃圾回收
触发时机
当内存不足时就会触发垃圾收集。
(1)堆回收:判断对象已死的算法
1、 引用计数算法
每当有一个地方引用了它,计数器加一;失效就减一,当计数器为零就回收
2、可达性分析算法
通过一系列称为“GC Root”的根对象作为起始节点,从这些节点开始向下搜索,如果某个对象到GC Roots间没有任何引用相连,则证明此对象是不可能在被使用的。
固定可作为GC Roots的对象包括
- 在虚拟机栈中引用的对象,譬如当前正在运行的方法所使用到的参数、局部变量、临时变量等
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量
- 在方法区中常量引用的对象,譬如字符串常量池里的引用
- 在本地方法栈中JIN引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象
- 所有被同步锁持有的对象
(2)回收方法区
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型
判断废弃的条件
- 该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例
- 加载该类的加载器已经被回收,通常是很难达成的
- 该类对应的Java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
(3)垃圾收集算法
1、分代收集理论
- 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
- 弱分代假说:绝大多数对象都是朝生夕灭的
- 跨代引用假说:跨代引用相对于同代引用来说仅占极少数:存在相互引用关系的两个对象,应该是倾向于同时生存或者同时消亡。
设计者一般至少会将Java堆中划分为新生代和老年代,每次垃圾收集的时候新生代中都会发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
2、标记清除算法
3、标记复制算法
将存活的对象复制到另一个区域,然后清空这个区域。好处就是分配内存就不需要考虑碎片空间的情况
4、标记整理算法
针对老年代中垃圾回收,将老年代中所有存活的对象都移动到内存的一端,然后将超过边界的对象全部清除。
(4)垃圾收集器
新生代收集器
- Serial
- ParNew
- Parallel Scavenge (吞吐量优先)
老年代收集器
- Serial Old
- Parallel Old
- CMS
- 初始标记 (stop)
- 并发标记
- 重新标记 (stop)
- 并发回收
混合收集器
- Garbage First:针对大内存的回收,可以做到部分回收,降低回收停顿时间
(5)名词解释
- Minor GC:新生代GC
- Major GC/Full GC:老年代GC
3、如何排查JVM问题
(1)还在正常运行的系统
- 使用jmap来查看给各个区域的使用情况
- 通过jstack来查看线程运行情况,看哪些线程阻塞,哪些线程死锁
- 可以通过jstat命令查看垃圾回收情况,特别是full gc,如果发生的比较频繁,就要进行调优了
- 可通过各个命令的结果,或者jvsualvm等工具来进行分析
- 找到占用cpu最多的线程,定位到具体的方法,优化这个方法的执行
首先,初步猜测频繁发生full gc的原因,如果频繁发生full gc但是有一只没有出现内存溢出,那表示full gc实际上回收了很多对象,所以这些对象最好能在Minor gc过程中就直接回收掉,避免这些对象进入老年代,对于这种情况就要考虑存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入了老年代,尝试加大年轻代的代销。
(2)已经发生OOM的系统
- 一般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件
- 可以利用jsisualvm等工具来分析dump文件
- 根据dump文件找到异常的实例对象,和异常的线程,定位到具体的代码
- 然后根据详细的分析和调试
(3)使用阿里调优工具arthas
4、什么时候会发生full gc
-
System.gc()方法的调用
此方法的调用是建议JVM进行Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加Full GC的频率,也即增加了间歇性停顿的次数。强烈影响系建议能不使用此方法就别使用,让虚拟机自己去管理它的内存,可通过通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。 -
老年代空间不足
老年代空间只有在新生代对象转入及创建大对象、大数组时才会出现不足的现象,当执行Full GC后空间仍然不足,则抛出如下错误:
java.lang.OutOfMemoryError: Java heap space
为避免以上两种状况引起的Full GC,调优时应尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。 -
永生区空间不足
JVM规范中运行时数据区域中的方法区,在HotSpot虚拟机中又被习惯称为永生代或者永生区,Permanet Generation中存放的为一些class的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:
java.lang.OutOfMemoryError: PermGen space
为避免Perm Gen占满造成Full GC现象,可采用的方法为增大Perm Gen空间或转为使用CMS GC。 -
CMS GC时出现promotion failed和concurrent mode failure
对于采用CMS进行老年代GC的程序而言,尤其要注意GC日志中是否有promotion failed和concurrent mode failure两种状况,当这两种状况出现时可能会触发Full GC。
具体原因和解决方案可以查看使用CMS垃圾收集器产生的问题和解决方案 -
HandlePromotionFailure
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
统计得到的Minor GC晋升到老年代的平均大小大于老年代的剩余空间,这是一个较为复杂的触发情况,例如程序第一次触发Minor GC后,有6MB的对象晋升到老年代,那么当下一次Minor GC发生时,首先检查老年代的剩余空间是否大于6MB,如果小于6MB,则执行Full GC。
当新生代采用PS GC时,方式稍有不同,PS GC是在Minor GC后也会检查,例如上面的例子中第一次Minor GC后,PS GC会检查此时旧生代的剩余空间是否大于6MB,如小于,则触发对旧生代的回收。
除了以上4种状况外,对于使用RMI来进行RPC或管理的Sun JDK应用而言,默认情况下会一小时执行一次Full GC。可通过在启动时通过 java -Dsun.rmi.dgc.client.gcInterval=3600000来设置Full GC执行的间隔时间或通过-XX:+ DisableExplicitGC来禁止RMI调用System.gc。 -
堆中分配很大的对象
所谓大对象,是指需要大量连续内存空间的java对象,例如很长的数组,此种对象会直接进入老年代,而老年代虽然有很大的剩余空间,但是无法找到足够大的连续空间来分配给当前对象,此种情况就会触发JVM进行Full GC。
为了解决这个问题,CMS垃圾收集器提供了一个可配置的参数,即-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但提顿时间不得不变长了,JVM设计者们还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置在执行多少次不压缩的Full GC后,跟着来一次带压缩的
能否对JVM调优,让其几乎不发生Full gc
调整JVM参数,让尽可能多的生命周期短的对象,在minor gc中回收
5、双亲委派模型
JVM中存在三个默认的类加载器
- BootstrapClassLoader
- ExtClassLoader
- AppClassLoader
AppClassLoader的父加载器是ExtClassLoader,ExtClassLoader的父加载器是BootstrapClassLoader
JVM在加载一个类时,会调用AppClassLoader的loadclass方法来加载这个类,在这个方法中,会先使用ExtClassLoader的方法来加载,同样ExtClassLoader会先试用BootstrapClassLoader来加载。如果父类加载不成功,子类才会尝试加载该类。
6、类加载的机制
(1)加载
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
(2)验证、准备、解析
- 验证:确保Class文件的字节流中包含的信息符合《java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。包括文件格式验证、元数据验证、字节码验证、符号引用验证。
- 准备:正式为类中定义的变量分配内存并设置类变量初始值的阶段。
- 解析:是java虚拟机将常量池内的符号引用替换为直接引用的过程。
(3)初始化
在此之前都是由JVM主导的加载过程。
以下场景会触发类的初始化
- 使用new关键字实例化对象的时候
- 读取或设置一个类型的静态字段的时候(被final修饰,已在编译期把结果放入常量池的静态字段除外)
- 调用一个类型的静态方法的时候
- 使用java.lang.reflect包的方法对类型进行反射调用的时候
- 初始化类时,父类还没有初始化的时候
- 当虚拟机启动时,用户指定的要执行的主类
(4)使用
(5)卸载
三、计算机网络
TCP/IP五层结构
- 应用层:提供应用层协议,如HTTP,FTP协议等,方便应用程序之间进行通信
- 传输层:提供进程之间的逻辑通信,传输层向高层用户屏蔽了下面网络层的核心细节,使应用程序开起来是在两个传输层实体之间有一条端到端的逻辑通信信道
- 网络层:将网络地址翻译成对应的物理地址,并决定如何将数据从发送方路由到接收方,通过路由选择算法为分组通过通信子网选择最佳路径。路由器
- 数据链路层:在不可靠的物理介质上提供可靠的传输,接收来自物理层的位流形式的数据,并封装成帧,传送到上一层;同样,也将来自上层的数据帧,拆装为位流形式的数据转发到物理层。这一层在物理层提供的比特流的基础上,通过差错控制、流量控制方法,使有差错的物理线路变为无差错的数据链路。提供物理地址寻址功能。交换机工作在这一层。
- 物理层:主要解决两台物理机之间的通信,通过二进制比特流的传输来实现,二进制数据表现为电流电压上的强弱,到达目的地再转化为二进制机器码。网卡、集线器工作在这一层。
IPV4和IPV6
IPv4采用32位地址长度,约有43亿地址,IPv6地址为128位长,但通常写作8组,每组为四个十六进制数的形式。
(1)ARP协议
ARP(Address Resolution Protocol)。主要功能是讲IP地址解析为物理地址。使用ARP协议可以根据网络层IP数据包包头中IP地址信息解析出目标硬件地址信息,以保证通信的顺利进行。
-
地址映射
- 静态映射:静态映射的意思是要手动创建一张ARP表,把逻辑(IP)地址和物理地址关联起来。这个ARP表储存在网络中的每一台机器上。例如,知道其机器的IP地址但不知道其物理地址的机器就可以通过查ARP表找出对应的物理地址。这样做有一定的局限性,因为物理地址可能发生变化
- 动态映射:动态映射时,每次只要机器知道另一台机器的逻辑(IP)地址,就可以使用协议找出相对应的物理地址。已经设计出的实现了动态映射协议的有ARP和RARP两种。ARP把逻辑(IP)地址映射为物理地址。RARP把物理地址映射为逻辑(IP)地址。
-
ARP原理
- ARP请求:任何时候,当主机需要找出这个网络中的另一个主机的物理地址时,它就可以发送一个ARP请求报文,这个报文包好了发送方的MAC地址和IP地址以及接收方的IP地址。因为发送方不知道接收方的物理地址,所以这个查询分组会在网络层中进行广播。
- ARP响应:局域网中的每一台主机都会接受并处理这个ARP请求报文,然后进行验证,查看接收方的IP地址是不是自己的地址,只有验证成功的主机才会返回一个ARP响应报文,这个响应报文包含接收方的IP地址和物理地址。这个报文利用收到的ARP请求报文中的请求方物理地址以单播的方式直接发送给ARP请求报文的请求方。
(2)RARP
RARP(reverse address resolution Protocol),逆地址解析协议。允许局域网的物理机器从网关服务器的ARP表或缓存上请求IP地址。
工作过程:
-
主机发送一个本地RARP广播,在广播包中,声明自己的MAC地址并且请求任何收到此请求的RARP服务器分配一个IP地址。
-
本地网段的RARP服务器收到此请求后,检查器RARP列表,查找该MAC地址对应的IP地址。
-
如果存在,RARP服务器就给源主机发送一个响应数据包,并将IP地址提供给对方主机使用。
-
如果不存在,RARP服务器对此不做任何响应。
-
源主机收到从RARP服务的响应信息,就利用得到的IP地址进行通信。如果一直没收到RARA服务器的响应信息,表示初始化失败。
1、TCP三次握手
2、四次挥手
3、TCP和UDP的区别
- 基于连接 tcp
- 可靠性和有序性 tcp
- 实时性 udp
- 协议手部大小 udp小
- 运行速度 tcp慢
- 拥塞机制 tcp
- 流模式 tcp面向字节 udp面向报文
应用层交下来的报文,UDP不合并不拆分,直接在其上面加上首部就交给了下面的网络层。无论应用层交给UDP多长的报文,它都一次性发送。TCP是面向字节流的,把上层交下来的数据看成无结构的字节流来发送,TCP有一个缓冲,当缓冲池满了之后,TCP就会将数据发送出去。 - 资源占用 tcp多
总结:TCP作为面向流的协议,提供可靠的、面向连接的运输服务,并且提供点对点通信 UDP作为面向报文的协议,不提供可靠交付,并且不需要连接,不仅仅对点对点,也支持多播和广播。
适用场景
- TCP:文件传输、接收邮件、远程登录
- UDP:QQ聊天、在线视频、网络语音通话、广播通信
TCP粘包问题
- TCP是面向字节流的传输协议,发送单位是字节流,因此会将多个小尺寸数据封装在一个tcp报文中发送出去。即存在客户端调用了两次send,服务端一次就全部接收了
- 解决办法:固定发送信息长度,或者在两个信息之间加入分隔符。
4、TCP如何保证可靠传输
(1)校验和
- 计算方式:在数据传输过程中,将发送的数据段都当做一个16位的整数。将这些整数加起来,并且前面的进位不能丢弃,补在后面,最后取反,等到校验和
- 发送方:在发送数据前计算校验和,并进行校验和的填充
- 接收方:在收到数据后,对数据以同样的方式进行计算,求出校验和,与发送方的进行对比
(2)确认应答与序列号
- 序列号:TCP传输时将每个字节的数据都进行了编号,这就是序列号
- 确认应答:TCP传输过程中,每次接收方收到数据后,都会对传输方进行确认应答。也就是发送ACK报文。这个报文中带有确认的序列号。告诉发送方,接收到了哪些数据
(3)超时重传
在发送方发送完数。据后的等待一个时间,时间到达没有接收到ACK报文,那么对刚才发送的数据进行重新发送。最大超时时间是动态计算的。
(4)连接管理
三次握手和四次挥手
(5)流量控制
滑动窗口控制发送信息数量
(6)拥塞控制
5、TCP四种拥塞控制算法
- 慢开始和拥塞避免:先将cwnd窗口设置为1,每次收到接收方发来的接收确认,将窗口大小增加至原来的2倍,当超过限定值sst之后改为拥塞避免,每次增加1。当发生拥塞时,窗口大小更改为1,且将sst设置为原来的一半
- 快速重传:使发送方尽快进行重传,而不是等超时重传计时器超时再重传。当发送方收到3个连续的重复确认,就将相应的报文段立即重传。
发送方发送1号数据报文段,接收方收到1号报文段后给发送方发回对1号报文段的确认,在1号报文段到达发送方之前,发送方还可以将发送窗口内的2号数据报文段发送出去,接收方收到2号报文段后给发送方发回对2号报文段的确认,在2号报文段到达发送方之前,发送方还可以将发送窗口内的3号数据报文段发送出去。
假设该报文丢失,发送方便不会发送针对该报文的确认报文给发送方,发送方还可以将发送窗口内的4号数据报文段发送出去,接收方收到后,发现这不是按序到达的报文段,因此给发送方发送针对2号报文段的重复确认,表明我现在希望收到的是3号报文段,但是我没有收到3号报文段,而收到了未按序到达的报文段,发送方还可以将发送窗口中的5号报文段发送出去,接收方收到后,发现这不是按序到达的报文段,因此给发送方发送针对2号报文段的重复确认,表明我现在希望收到的是3号报文段,但是我没有收到3号报文段,而收到了未按序到达的报文段,,发送方还可以将发送窗口内的最后一个数据段即6号数据报文段发送出去,接收方收到后,发现这不是按序到达的报文段,因此给发送方发送针对2号报文段的重复确认,表明我现在希望收到的是3号报文段,但是我没有收到3号报文段,而收到了未按序到达的报文段。
此时,发送方收到了累计3个连续的针对2号报文段的重复确认,立即重传3号报文段,接收方收到后,给发送方发回针对6号报文的确认,表明,序号到6为至的报文都收到了,这样就不会造成发送方对3号报文的超时重传,而是提早收到了重传。
6、全连接和半连接队列
TCP存在两个队列(全连接队列和半连接队列),第一次握手后TCP会产生的新项并先存放到半连接队列中。当完成三次握手之后项会移动到全连接队列里(全连接队列默认大小backlog值是50)。如果当全连接队列满了server则会根据tcp_abort_on_overflow 的值来做对应的处理,,值为0则丢弃当前客户端的ack,等隔一段时间重发第二步的syn+ack包,值为1则废弃当前握手过程与连接。
7、SYN攻击
SYN攻击即利用TCP协议缺陷,通过发送大量的半连接请求,占用半连接队列,耗费CPU和内存资源
(1)攻击方式
- 直接攻击:攻击者用他们自己的没有经过伪装的IP地址快速地发送SYN数据包,这就是所谓的直接攻击。一旦被检测到,这种攻击非常容易抵御,用一个简单的防火墙规则阻止带有攻击者IP地址的数据包就可以了。
- 欺骗式攻击:攻击者还必须能够用有效的IP和TCP报文头去替换和重新生成原始IP报文。如今,有很多代码库能够帮助攻击者替换和重新生成原始IP报文。要使攻击成功,位于伪装IP地址上的主机必须不能响应任何发送给它们的SYN-ACK包。攻击者可以用的一个非常简单的方法,就是仅需伪装一个源IP地址,而这个IP地址将不能响应SYN-ACK包。
如果一个源地址被重复地伪装,这个地址将很快被检测出来并被过滤掉。在大多数情况下运用许多不同源地址伪装将使防御变得更加困难。在这种情况下最好的防御方法就是尽可能地阻塞源地址相近的欺骗数据包。 - 分布式攻击:攻击者运用在网络中主机数量上的优势而发动的分布式SYN洪泛攻击将更加难以被阻止。
(2)防御方法
- 增加TCP backlog队列:不能实际的解决问题
- 减少SYN-RECEIVED的时间:可能会出错误,不推荐
- SYN缓存:在采用SYN缓存的主机中,一个带有被限制大小的HASH表空间被用于存放那些将被分配给TCB的数据的一个子集。如果当握手完成的ACK接收到了,这些数据将被复制到一个完整的TCB中,否则超出存活时间的HASH值将会在需要时被回收。在Lemon的FreeBSD中,对于一个半开连接的SYN缓存是160字节,而一个完整的TCB是736字节,并且支持15359个SYN缓存空间。
- SYN Cookies:因为构成连接状态的最基本数据都被编码压缩进SYN-ACK的序列号比特位里了。对于一个合法连接,服务器将收到一个带有序列号(其实序列号已经加1)的ACK报文段,然后基本的TCB数据将被重新生成,一个完整的TCB通过解压确认数据将被安全的实例化。
其不足之处就是,不是所有的TCB数据都能添加到32位的序列号段中,所以一些高性能要求的TCP选项将不能被编码。其另一个问题是这样的SYN-ACK报文段将不能被转发(因为转发需要完整的状态数据)。 - 防火墙与代理:一种是对连接发起人伪装SYN-ACK包,另一种是对服务器伪装ACK包。
8、HTTP
HTTP是一个客户端终端(用户)和服务器端(网站)请求和应答的标准(TCP)。通过使用网页浏览器、网络爬虫或者其它的工具,客户端发起一个HTTP请求到服务器上指定端口(默认端口为80)。我们称这个客户端为用户代理程序(user agent)。应答的服务器上存储着一些资源,比如HTML文件和图像。我们称这个应答服务器为源服务器(origin server)。在用户代理和源服务器中间可能存在多个“中间层”,比如代理服务器、网关或者隧道(tunnel)。
尽管TCP/IP协议是互联网上最流行的应用,HTTP协议中,并没有规定必须使用它或它支持的层。事实上,HTTP可以在任何互联网协议上,或其他网络上实现。HTTP假定其下层协议提供可靠的传输。因此,任何能够提供这种保证的协议都可以被其使用。因此也就是其在TCP/IP协议族使用TCP作为其传输层。
HTTP协议规定,请求从客户端发出,最后服务器端响应该请求并 返回。换句话说,肯定是先从客户端开始建立通信的,服务器端在没有 接收到请求之前不会发送响应
通常,由HTTP客户端发起一个请求,创建一个到服务器指定端口(默认是80端口)的TCP连接。HTTP服务器则在那个端口监听客户端的请求。一旦收到请求,服务器会向客户端返回一个状态,比如"HTTP/1.1 200 OK",以及返回的内容,如请求的文件、错误消息、或者其它信息。
(1)http请求头
请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号分割。请求头部通知服务器有关客户端请求的信息,典型的请求头有:
- User-Agent:产生请求的浏览器类型
- Accept:客户端可识别的内容类型列表
- Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机
- keep-alive:开启长连接。网页完成打开后,底层用于传输数据的TCP不会直接关闭,会根据服务器设置的时间保持连接,保持时间过后连接关闭。
(2)http响应消息
http响应由状态行、响应头、空行、响应正文组成
- 状态码和对应的信息:
- 1XX:接收的信息正在处理
- 2XX:请求正常处理完毕
- 3XX:重定向
- 4XX:客户端错误
- 5XX:服务端错误
301:永久重定向 302:临时重定向 304:资源没修改,用之前缓存就行 400:客户端请求的报文有错误 403:表示服务器禁止访问资源 404:表示请求的资源在服务器上不存在或未找到
(3)转发和重定向
- 转发是服务器行为、服务器直接想目标地址访问URL,将相应内容读取之后发给浏览器,用户浏览器地址栏URL不变,转发页面和转发到的页面可以共享request里面的数据。
- 重定向是利用服务器返回的状态码来实现的,如果服务器返回301或者302,浏览器收到新的消息后自动跳转到新的网址重新请求资源。用户的地址栏url会发生改变,而且不能共享数据。
(4)关于http请求get和post的区别
- get提交,请求数据会附在URL之后,以?分割,多个参数用&连接。post提交,把提交的数据放置在http包的包体中。因此get提交的数据会在地址栏显示,而post提交地址栏不变
- 传输数据大小:http协议没有对传输数大小进行限制,也没有对URL长度进行限制。get:特定浏览器和服务器对URL长度有限制,因此对于get提交时,传输数据就会受URL长度限制。post:不是通过URL传值,理论上数据不受限制,实际上WEB服务器会规定对post提交数据大小进行限制
- 安全性:post安全性比get安全性高。
- post会先将请求头发送给服务器进行确认,然后才真正的发送数据
(5)DNS协议
DNS协议是基于UDP的应用层协议,功能是根据用户输入的域名,解析出该域名对应的IP地址,从而给客户端进行访问。
(6)DNS解析过程
- 客户机发出查询请求,在本地计算机缓存查找,若没有找到,就会将请求发送给DNS服务器
- 本地DNS服务器会在自己的区域里面查找,找到根据此记录进行解析,若没有找到,就会在本地的缓存里查找
- 本地服务器没有找到客户机查询的信息,就会将此请求发送到根域名DNS服务器
- 根域名服务器解析客户机请求的根域部分,它包含的下一级DNS服务器的地址返回到客户机的DNS服务器地址
- 客户机的DNS服务器根据返回的信息接着访问下一级的DNS服务器
- 递归一级一级的查询目标,最后在有目标域名的服务器上得到相应的IP信息
- 客户机本地DNS服务器将查询结果返回给客户机
- 客户机根据ip信息访问目标主机
(7)DNS劫持
攻击者正是利用此点在范围内封锁正常DNS的IP地址,使用域名劫持技术,通过冒充原域名以E-MAIL方式修改公司的注册域名记录,或将域名转让到其他组织,通过修改注册信息后在所指定的DNS服务器加进该域名记录,让原域名指向另一IP的服务器,让多数网民无法正确访问,从而使得某些用户直接访问到了恶意用户所指定的域名地址
1)劫持过程
- 获取劫持域名注册信息:首先攻击者会访问域名查询站点,通过MAKE CHANGES功能,输入要查询的域名以取得该域名注册信息
- 控制该域名的E-MAIL帐号:此时攻击者会利用社会工程学或暴力破解学进行该E-MAIL密码破解,有能力的攻击者将直接对该E-MAIL进行入侵行为,以获取所需信息
- 修改注册信息:当攻击者破获了E-MAIL后,会利用相关的MAKE CHANGES功能修改该域名的注册信息,包括拥有者信息,DNS服务器信息等
- 使用E-MAIL收发确认函:此时的攻击者会在信件帐号的真正拥有者之前,截获网络公司回馈的网络确认注册信息更改件,并进行回件确认,随后网络公司将再次回馈成功修改信件,此时攻击者成功劫持域名
2) 可能的解决办法
- 在不同的网络上运行分离的域名服务器来取得冗余性。
- 将外部和内部域名服务器分开(物理上分开或运行BIND Views)并使用转发器(forwarders)。
- 限制动态DNS更新。
- 删除运行在DNS服务器上的不必要服务,如FTP、telnet和HTTP。
- 在网络外围和DNS服务器上使用防火墙服务。将访问限制在那些DNS功能需要的端口/服务上。
(8)网页访问过程
在浏览器地址栏键入URL,按下回车之后会经历以下流程:
- 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
- 解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立TCP连接;
- 浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
- 服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
- 释放 TCP连接;
- 浏览器将该 html 文本并显示内容;
(9)http同步和异步请求
- 同步:请求提交、等待服务器处理、处理完毕返回,这个期间客户端浏览器不能干任何事情(不同B/S模式)。不适合高并发以及处理大量数据的场景,适用于需要根据上一部的结果操作下一步的场景。
- 异步:请求通过事件触发、服务器处理(期间浏览器任然可以作其他的事情)、处理完毕(AJAX技术)
9、http与https
- http协议以明文方式发送内容,不提供任何方式的数据加密,因此http不适合传输一些敏感信息。
- https在http的基础上加入了SSL协议,SSL依靠证书来验证服务器的身份,并为浏览器与服务器之间到的通信加密。
- SSL全称为Secure socket layer即安全套接字层,其继任为TLSTransport layer Security传输层安全协议,用于在传输层为数据通讯提供安全支持。
HTTPS协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性。
(1)HTTPS和HTTP的区别
- https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
- http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
- http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
- http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
(2)客户端在使用HTTPS方式与Web服务器通信时有以下几个步骤
(1)客户使用https的URL访问Web服务器,要求与Web服务器建立SSL连接。
(2)Web服务器收到客户端请求后,会将网站的证书信息(证书中包含公钥)传送一份给客户端。
(3)客户端的浏览器与Web服务器开始协商SSL连接的安全等级,也就是信息加密的等级(会验证证书是否合法,不合法的话提示警告)。
(4)客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站。
(5)Web服务器利用自己的私钥解密出会话密钥。
(6)Web服务器利用会话密钥加密与客户端之间的通信。
(3)https的缺点
(1)HTTPS协议握手阶段比较费时,会使页面的加载时间延长近50%,增加10%到20%的耗电;
(2)HTTPS连接缓存不如HTTP高效,会增加数据开销和功耗,甚至已有的安全措施也会因此而受到影响;
(3)SSL证书需要钱,功能越强大的证书费用越高,个人网站、小网站没有必要一般不会用。
(4)SSL证书通常需要绑定IP,不能在同一IP上绑定多个域名,IPv4资源不可能支撑这个消耗。
(5)HTTPS协议的加密范围也比较有限,在黑客攻击、拒绝服务攻击、服务器劫持等方面几乎起不到什么作用。最关键的,SSL证书的信用链体系并不安全,特别是在某些国家可以控制CA根证书的情况下,中间人攻击一样可行。
(4)数据传输为什么用对称加密
- 非对称加密的效率非常低,而http的应用场景中通常端与端之间存在大量的交互,非对称加密的效率是无法接受的
- 在 HTTPS 的场景中只有服务端保存了私钥,一对公私钥只能实现单向的加解密,所以 HTTPS 中内容传输加密采取的是对称加密,而不是非对称加密。
(5)为什么需要CA认证机构颁发证书
当任何人都可以制作证书的时候,就可能会遭受中间人攻击。客户端的请求会被中间人拦截,然后用中间人发送的公钥加密私钥,于中间人建立会话连接。由于缺少对证书的验证,所以客户端虽然发起的是 HTTPS 请求,但客户端完全不知道自己的网络已被拦截,传输内容被中间人全部窃取。
(6)如何保证证书的合法性
证书包含什么信息
- 颁发机构信息
- 公钥
- 公司信息
- 域名
- 有效期
- 指纹
合法的依据
- 证书的可信性基于信任制,权威机构需要对其颁发的证书进行信用背书,只要是权威机构生成的证书,我们就认为是合法的。
如何验证合法性
- 验证域名、有效期等信息是否正确:证书上都有包含这些信息,比较容易完成验证;
- 判断证书来源是否合法:每份签发证书都可以根据验证链查找到对应的根证书,操作系统、浏览器会在本地存储权威机构的根证书,利用本地根证书可以对对应机构签发证书完成来源验证
- 判断证书是否被篡改:需要与 CA 服务器进行校验
- 判断证书是否已吊销:通过CRL(Certificate Revocation List 证书注销列表)和 OCSP(Online Certificate Status Protocol 在线证书状态协议)实现,其中 OCSP 可用于第3步中以减少与 CA 服务器的交互,提高验证效率。
(5)https的应用场景
- 需要输入密码的网站
- 需要提高网站安全性的网站
10、http如何保存用户状态
- Cookie:由服务端产生,再发给客户端保存,当客户端再次访问的时候,服务器可根据cookie辨识客户端是哪个,以此可以保存状态,免账号密码登录等。是否过期可以再cookie生成的时候设置。不是很安全,别人可以分析放在本地的cookie并进行cookie欺骗。单个cookie在客户端的限制是3k。
- Session:用与标记特定客户端信息,存在服务器的一个文件里。一般客户带cookie对服务器进行访问,可通过cookie中的session id从整个session中查询到服务器记录的关于客户端的信息。过期与否由服务端决定。当访问增多,会占用服务器性能。
11、http版本改进
- http-1.0:规定了请求头和请求尾,响应头和响应尾,每一个请求都是一个单独的连接,做不到连接复用
- http-1.1:默认开启长连接,在一个tcp连接上可以传送多个http请求和响应,改善了http1.0短连接造成的性能开销。支持管道网络传输,只要第一个请求发出去了,不必等其回来就可以发送第二个请求,可以减少整体响应时间。
- http-2.0:提出多路复用,a文件和b文件可以同时传输。引入二进制数据帧,帧对数据进行顺序标识,有了序列id,服务器就可以进行并行传输数据。
12、短连接和长连接的区别
- 短连接:客户端与服务器进行一次http连接操作,就进行一次TCP连接,连接结束TCP关闭连接
- 长连接:长连接网页完成打开后,底层用于传输数据的TCP连接不会直接关闭,会根据服务器设置的保持时间保持连接,保持时间过后连接关闭。
12、REST API
全称为表述性状态转移(Representational State Transfer ,REST),即利用http中get、post、put、delete以及其他的http方法构成rest中数据资源的增删改查操作。
13、跨域请求是什么?有什么问题?怎么解决?
跨域是指浏览器在发起网络请求时,会检查该请求所对应的协议、域名、端口和当前网页是否一直,如果不一致则浏览器会进行限制。之所以浏览器要做这层限制,是为了用户信息安全,因为在此过程中用户的cookie会泄漏
四、Redis
1、什么是redis
redis是一个开源高性能非关系型的键值对数据库。Redis的数据是存储在内存中的,所以读写速度非常快,因此广泛运用于高速缓存。也可以用来做分布式锁。支持事务、持久化、LUA脚本、LRU驱动事件、多种集群方案。
(1)非关系型数据库和关系型数据库
关系型数据库
关系型数据库是依据关系模型来创建的数据库,所谓关系模型就是“一对一、一对多、多对多”等关系模型,关系模型就是指二维表格模型,因而一个关系型数据库就是由二维表及其之间的联系组成的一个数据组织。关系模型包括数据结构(数据存储的问题,二维表)、操作指令集合(SQL语句)、完整性约束(表内数据约束、表与表之间的约束)
非关系型数据库
特点:
- 存储非结构化的数据,比如文本、图片、音频、视频
- 表与表之间没有关联,可扩展性强
- 支持海量数据的存储和高并发的高效读写
- 支持分布式,能够对数据进行分片存储,扩缩容简单
(2)为什么用redis
- 高性能:假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可
- 高并发:直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
(3)redis使用场景
- 计数器:可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
- 会话缓存:可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
- 全页缓存:除基本的会话token之外,Redis还提供很简便的FPC平台。以Magento为例,Magento提供一个插件来使用Redis作为全页缓存后端。此外,对WordPress的用户来说,Pantheon有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面
- 查找表:例如 DNS 记录就很适合使用 Redis 进行存储。查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。
- 消息队列:List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息
- 分布锁实现:在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现
2、redis优缺点
优点:
- 读写性能优异,读速度为110000次/s,写的速度为81000次/s
- 支持数据持久化,支持AOF和RDB两种持久化方式
- 支持事务,redis的所有操作都是原子性的,同时支持对几个操作合并后的原子性执行
- 数据结构丰富,支持String、hash、set、zset、list五种数据结构
- 支持主从复制,主机会自动将数据同步到从机,可以进行读写分裂,redis直接自己构建VM机制
缺点:
- 数据库容量收到物理内存的限制,不能用作海量数据的高性能读写,因此redis适合的场景主要局限在较小数据量的高性能操作和运算上
- redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复
- 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致问题
- redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须要确保有足够的空间,这对资源造成了很大的浪费。
(1)redis快的原因
- redis是基于内存的数据库,内存数据读取存储效率远高于硬盘型
- 数据结构简单,对数据操作也简单,redis中数据结构是专门设计的
- redis采用多路复用技术通过采用epoll的非阻塞IO,提升了效率
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多线程或者多进程切换消耗CPU,不存在加锁释放操作,没有因为可能出现死锁而导致的性能消耗
- 使用底层模型不同,它们之间底层实现方式以及客户端之间通信的应用协议不一样
3、redis主从复制
在主从复制中,有主库和从库节点两个角色,从节点服务启动会连接主库,并向主库发送SYNC命令。
主节点收到同步命令,启动持久化工作,工作执行完成后,主节点将传送整个数据库文件到从库,从节点收到的数据库文件数据之后将数据进行加载。此后,主节点继续将所有已经收到的修改命令,和新的修改命令依次传送给从节点,从节点依次执行,从而达到最终的数据同步。
依次实现读写分离
4、redis基本数据类型
- String:采用类似数组的形式存储。可以用来做最简单的数据缓存,可以缓存某个简单的字符串,也可以缓存某个json格式的字符串,redis分布式锁的实现就利用了这种数据结构,还包括可以实现计数器、session共享,分布式ID。
- list:采用双向链表进行具体实现。通过命令的组合,既可以当做栈,也可以当做队列来使用,可以用来缓存类似微信公众号、微博等消息流数据。
- hash:采用hashtable或者ziplist进行具体实现。可以用来存储一些键值对,更适合用来存储对象
- set:采用intset或者hashtable存储。可以存储多个元素,但是不能重复,集合可以进行交集、并集、差集等操作,从而可以实现类似我与某人共同关注的人、朋友圈点赞等功能。
- zset:采用ziplist或者skiplist+hashtable实现。可以实现排行榜功能。
5、数据结构
(1)简单动态字符串(simple dynamic string,SDS)
int len;//保存sds内字符串长度
int free;//记录未使用的字节数量
char buf[];//用于保存字符串
比C字符串有以下优点:
- 常熟复杂度获取字符串长度
- 杜绝缓冲区溢出
- 减少修改字符串长度所需的内存重新分配次数
- 二进制安全
- 兼容部分C字符串函数
(2) 链表
listNode *prev;
listNode *next;
void *value;
- 哈希表:采用链地址法解决键冲突。保存两个数组用于扩展或者收缩哈希表
typedef struct dictht{
dictEntry **table;//哈希表数组
unsigned long size;//哈希表大小
unsigned long sizemask;//哈希表大小掩码
unsigned long used;//已有节点数量
}
(3) 跳跃表(skiplist)
跳跃表是一种有序数据结构,通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
zskiplistNode用于保存跳跃表节点,zskiplist用于保存跳跃表节点的相关信息,比如节点数量,指向表头节点和表尾结点的指针。
- 每个跳跃表的节点的层高都是1-32之间的随机数
- 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的成员对象必须是唯一的
- 跳跃表中的节点按照分值大小进行排序,当分值相同时,节点按照成员对象的大小进行排序
//zskiplistNode
struct zskiplistNode *backward; //后退指针
double score; //分值
robj *obj;//成员对象
struct zskiplistLevel{ //层
struct zskiplistNode *forward; //前进指针
unsigned int span; //跨度
}level[]
层:跳跃表节点的level可以办函很多元素,每一个元素都包含一个执行其他节点的指针,程序可以通过这些层来加快访问其他节点的速度
前进指针:用于从表头向表尾访问节点
跨度:用于记录两个节点之间的距离(指向null的前进指针跨度都为0)。可以用于判断目标节点的排位
跳跃表的遍历过程
- 迭代程序首先访问跳跃表的第一个节点(表头),然后从第四层的前进指针移动到表中的第二个节点。
- 在第二个节点时,程序沿着前进指针移动到下一个节点
- 当程序碰到null时,就得知到达了表尾,结束这次遍历
(4)整数集合(intset)
整数集合是redis用于保存整数值的集合抽象数据结构,他可以保存类型为int16_t、int32_t、int64_t的整数值,并且保证集合中不会出现重复元素。
当一个集合只包含整数值元素,并且这个集合的元素数量不多时,redis就会使用整数集合作为集合键的底层实现。
typedef struct intset{
uint32_t encoding; //编码方式,决定contents里保存的数据类型
uint32_t length; //集合包含的元素数量
int8_t contents; //保存元素的数组
}intset;
当新元素类型比整数集合现有所有元素类型都要长时,整数集合就要进行升级。
- 根据新元素类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
- 将底层数组现有的所有元素都转换成新元素相同的类型
- 将新元素添加到底层数组里
特点:
- 整数集合的底层实现为数组,这个数组以有序、无重复的方式保存集合元素,在有需要时,程序会根据新添加元素的类型改变这个数组的类型
- 升级操作为整个整数集合带来了操作上的灵活性,并尽可能地节约了内存
- 整数集合只支持升级操作
(5)压缩列表(ziplist)
当一个列表键只包含少量列表项,并且每个列表项要么就是小数整数,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
压缩列表是为了集约内存而开发的,是由一系列特殊编码的连续内存块组成的书序型数据结构。一个压缩列表可以包含人一多节点,每个节点可以保存一个字节数组或者一个整数值。
-
压缩列表构成:
- zlbytes:记录整个压缩列表赵勇的内存字节数
- zltail:记录压缩列表表尾结点距离压缩列表的起始地址有多少字节
- zllen:记录压缩列表包含的节点数量
- entryX:列表节点
- zlend:标记末端 -
压缩列表节点:
- previous_entry_length:记录前一个节点长度(字节)
- encouding:记录节点的content属性所保存数据的类型和长度
- content:保存节点的值:整数值或者字节数组 -
字节数: 2 6 − 1 ; 2 14 − 1 ; 2 32 − 1 2^6-1;2^{14}-1;2^{32}-1 26−1;214−1;232−1
-
整数值:4位长无符号整数,1字节、3字节有符号,int16,int32,int64
6、持久化
(1)什么是持久化
持久化就是把内存的数据写到磁盘中去,防止服务器宕机导致内存数据丢失
(2)Redis持久化机制
redis提供两种持久化机制RDB(默认)和AOF两种
-
RDB:(Redis,DataBase),是redis默认的持久化方式。按照一定的时间将内存的数据以快照的形式保存到硬盘中。通过配置文件中的save参数来定义快照的周期。
优点:- 只生成一个dump.rdb文件,方便持久化
- 容灾性好,一个文件可以保存到安全的磁盘
- 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是IO最大化。使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能
- 相对于数据集大时,比AOF启动效率更高
缺点: - 数据安全低。rdb是间隔一段时间进行持久化,如果持久化之间redis发生故障,就会发生数据丢失
-
AOF:(Append Only File),是将redis执行的每次写命令记录到单独的日志文件中,当重启redis会重新持久化的日志文件中恢复数据。(优先加载aof)
优点:- 数据安全,可以配置每进行一次命令操作就记录一次
- 通过append模式写文件,即使中途服务器宕机,可以通过redis-check-aof工具解决一致性问题。
- aof机制的rewrite模式。aof文件没写入之前,可以删除其中的某些命令
缺点: - aof文件比RDB大,且恢复慢
- 数据集大的时候,启动效率比RDB低
(3)选择合适的持久化方式
- 如果达到很高的数据安全性,应同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。
- 如果可以承受数分钟以内的数据丢失,那么可以只使用RDB持久化
(4)redis持久化数据和缓存怎么扩容
- 如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容
- 如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样
7、简述Redis过期策略
- 定期删除:redis默认是每100ms就随机抽取一些设置了过期时间的key,并检查其是否过期,如果过期就删除。因此该删除策略不会删除所有过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果
- 惰性删除:在客户段需要获取某个key时,redis将首先进行检查,若该key设置了过期时间并已经过期就会删除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
- 定时删除:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量
过期时间和永久有效设置
expire和persist
8、MySQL里有2000w数据,redis中只存20w的数据,如何保证redis中的数据都是热点数据
redis内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。
9、redis内存淘汰策略
Redis的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。Redis的内存淘汰策略的选取并不会影响过期的key的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据;过期策略用于处理过期的缓存数据
全局的键空间选择性移除
-
noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
-
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。(这个是最常用的)
-
allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
设置过期时间的键空间选择性移除
-
volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。
-
volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
-
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
10、redis如何做内存优化
可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面
11、redis线程模型
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型
- 文件处理器采用IO多路复用机制
- 当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与 redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性
12、集群
(1)集群的概念?redis使用集群的必要性
所谓的集群就是通过添加服务器的数量,提供相同的服务,从而让服务器达到一个稳定、高效的状态
必要性:
- 单个redis存在不稳定性。当redis服务宕机了,就没有可用的服务了
- 单个redis的读写能力是有限的
(2)redis集群策略
- 主从模式:分为主数据库和从数据库,主库可以读写,并且会和从库进行数据同步,这种模式下,客户端直接连接主库或者某个从库,但是当主库或从库宕机后,客户端需要手动修改IP,另外,这种模式也较难进行扩容,整个集群所能存储的数据收到某台机器的内存容量,所以不可能支持特大数据量
- 哨兵模式:这种模式在处从的基础上增加了哨兵节点,当主库节点宕机后,哨兵会发现主库节点宕机,然后再从库中选取一个库为新的主库,另外哨兵也可以做集群,从而保证一个哨兵节点宕机后,还有其他哨兵节点可以继续工作,这种模式可以比较好的保证redis集群的高可用,但是还没解决容量上限的问题
- Cluster模式:Cluster模式是用的比较多的模式,它支持多主多从,这种模式会按照key进行槽位的分配,可以使得不同的key分散到不同的节点上,利用这种模式可以使得整个集群支持更大的数据容量,同时每个主节点可以拥有自己的多个从节点,如果该主机节点宕机,会从它的节点中选取一个新的主节点。
如果redis要存的数据量不大,可以选择哨兵模式,如果要持续扩容,则选择Cluster模式
五、Mybatis
1、Mybatis存在哪些优点和缺点
-
优点:
- 基于SQL语句编程,相当的灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL单独写,解除sql与程序代码的耦合,便于统一管理
- 与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接
- 很好的与各种数据库兼容(只要JDBC支持的数据库他都支持)
- 能够与Spring很好的继承
- 提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射变迁,支持对象关系组件维护。
-
缺点:
- SQL语句的编写工作量较大,尤其字段多、关联表多时,对开发人员编写SQL语句的功底有一定的要求
- SQL语句以来数据库,导致数据库移植性差,不能随意更换数据库。
六、消息队列
1、为什么使用消息队列
-
异步处理:如当用户注册的时候,需要将信息写入数据库,发送注册邮件,发送短信。用同步处理的方式会导致响应时间过长。用消息队列异步的方式,可以直接写入消息队列,然后异步读取消息队列中的信息,然后再给用户发邮件和短信
-
应用解耦:如当订单系统和库存系统写在一个应用里,当下完单后调用库存系统函数修改库存,是非常耗时的。可以把订单系统和库存系统分别抽取出来,下单之后信息写入消息队列里,库存系统可以通过订阅消息队列,通过消息队列收到的订单信息再对库存进行修改。
-
流量销峰:适用于秒杀场景
2、消息服务和目的地
当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地
主要有两种形式的目的地
- 队列:点对点消息通信。只有一个接受者,但是不是只能只有一个接收者
- 主题:发布/订阅消息通信
3、消息服务规范
- JMS(Java Message Service)Java消息服务,基于JVM消息代理的规范。ActiveMQ
- AMQP(Advance Message Queuing Protocol),高级消息队列协议,兼容JMS。RabbitMQ
七、Docker
docker是一个开源的应用容器引擎,可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何可以流行的linux机器上,也可以实现虚拟化
容器是完全使用沙箱机制,相互之间不会有任何接口,更重要的是容器性能开销极低。
docker支持将软件编译成一个镜像;然后在镜像中各种软件做好配置,将镜像发布出去,其他使用者可以直接使用这个镜像。运行中这个镜像称为容器,容器启动时非常快速的。
(1)镜像
docker镜像是用于创建docker容器的模板
(2)容器
容器是独立运行的一个或一组应用
(3)客户端
客户端通过命令行或者其他工具使用docker
(4)主机
一个物理或者虚拟的机器用于执行docker守护进程和容器
(5)仓库
docker仓库用来保存镜像,可以理解为代码控制中的代码仓库
八、手撕代码
class Solution {
//字符串转整型
public int strToInt(String str) {
char[] c = str.trim().toCharArray();
if(c.length == 0) return 0;
int res = 0, bndry = Integer.MAX_VALUE / 10;
int i = 1, sign = 1;
if(c[0] == '-') sign = -1;
else if(c[0] != '+') i = 0;
for(int j = i; j < c.length; j++) {
if(c[j] < '0' || c[j] > '9') break;
if(res > bndry || res == bndry && c[j] > '7') return sign == 1 ? Integer.MAX_VALUE : Integer.MIN_VALUE;
res = res * 10 + (c[j] - '0');
}
return sign * res;
}
}
蓄水池抽样
//蓄水池抽样算法,用于从数据流中随机抽取m个数据
int[] reservoir = new int[m];
// init
for (int i = 0; i < reservoir.length; i++)
{
reservoir[i] = dataStream[i];
}
for (int i = m; i < dataStream.length; i++)
{
// 随机获得一个[0, i]内的随机整数
int d = rand.nextInt(i + 1);
// 如果随机整数落在[0, m-1]范围内,则替换蓄水池中的元素
if (d < m)
{
reservoir[d] = dataStream[i];
}
}
布隆过滤器
BF是由一个长度为m比特的位数组(bit array)与k个哈希函数(hash function)组成的数据结构。位数组均初始化为0,所有哈希函数都可以分别把输入数据尽量均匀地散列。
当要插入一个元素时,将其数据分别输入k个哈希函数,产生k个哈希值。以哈希值作为位数组中的下标,将所有k个对应的比特置为1。
优点:
- 不需要存储数据本身,只用比特表示,因此空间占用相对于传统方式有巨大的优势,并且能够保密数据
- 时间效率也较高,插入和查询的时间复杂度均为O(k)
- 哈希函数之间相互独立,可以在硬件指令层面并行计算
缺点: - 存在假阳性的概率,不适用于任何要求100%准确率的情境
- 只能插入和查询元素,不能删除元素,这与产生假阳性的原因是相同的。我们可以简单地想到通过计数(即将一个比特扩展为计数值)来记录元素数,但仍然无法保证删除的元素一定在集合中
//用于从大量数据集中判断一个值是否存在
public class MyBloomFilter {
/**
* 一个长度为10 亿的比特位
*/
private static final int DEFAULT_SIZE = 256 << 22;
/**
* 为了降低错误率,使用加法hash算法,所以定义一个8个元素的质数数组
*/
private static final int[] seeds = {3, 5, 7, 11, 13, 31, 37, 61};
/**
* 相当于构建 8 个不同的hash算法
*/
private static HashFunction[] functions = new HashFunction[seeds.length];
/**
* 初始化布隆过滤器的 bitmap
*/
private static BitSet bitset = new BitSet(DEFAULT_SIZE);
/**
* 添加数据
*
* @param value 需要加入的值
*/
public static void add(String value) {
if (value != null) {
for (HashFunction f : functions) {
//计算 hash 值并修改 bitmap 中相应位置为 true
bitset.set(f.hash(value), true);
}
}
}
/**
* 判断相应元素是否存在
* @param value 需要判断的元素
* @return 结果
*/
public static boolean contains(String value) {
if (value == null) {
return false;
}
boolean ret = true;
for (HashFunction f : functions) {
ret = bitset.get(f.hash(value));
//一个 hash 函数返回 false 则跳出循环
if (!ret) {
break;
}
}
return ret;
}
/**
* 测试。。。
*/
public static void main(String[] args) {
for (int i = 0; i < seeds.length; i++) {
functions[i] = new HashFunction(DEFAULT_SIZE, seeds[i]);
}
// 添加1亿数据
for (int i = 0; i < 100000000; i++) {
add(String.valueOf(i));
}
String id = "123456789";
add(id);
System.out.println(contains(id)); // true
System.out.println("" + contains("234567890")); //false
}
}
class HashFunction {
private int size;
private int seed;
public HashFunction(int size, int seed) {
this.size = size;
this.seed = seed;
}
public int hash(String value) {
int result = 0;
int len = value.length();
for (int i = 0; i < len; i++) {
result = seed * result + value.charAt(i);
}
int r = (size - 1) & result;
return (size - 1) & result;
}
}