java
依据个人认知总结,如有技术性问题烦请指正!
java基础
数据类型
基本数据类型
基本数据类型的存储原理: 所有的简单数据类型不存在“引用”的概念,基本数据类型都是直接存储在内存中的内存栈上的,数据本身的值就是存储在栈空间里面,而Java语言里面八种基本数据类型就是这种存储模型
基本类型是按值传递
一个字节等于8位
- byte:Java中最小的数据类型,在内存中占8位(bit),即1个字节,取值范围-128~127,默认值0
- short:短整型,在内存中占16位,即2个字节,取值范围-32768~32717,默认值0
- int:整型,在内存中占32位,即4个字节,取值围-2147483648~2147483647,默认值0
- long:长整型,在内存中占64位,即8个字节,取值范围-263~263-1,默认值0L
- float:单精度浮点型,在内存中占32位,即4个字节,用于存储带小数点的数字
(与double的区别在于float类型有效小数点只有6~7位),默认值0.0f - double:双精度浮点型,用于存储带有小数点的数字,在内存中占64位,即8个字节,默认值0.0d
- char:字符型,用于存储单个字符,在内存中占16位,即2个字节,取值范围0~65535,默认值是\u0000 即空值
- boolean:布尔类型,在内存中占8位,即1个字节,用于判断真或假,默认值false
练习题
short s1 = 1; s1 = s1 + 1;有错吗?short s1 = 1; s1 += 1;有错吗?
- 第一个有错,1是int类型,不能转为short,第二个对,因为s1+= 1;相当于s1 = (short)(s1 + 1);其中有隐含的强制类型转换。
- 表达式的数据类型自动提升, 关于类型的自动提升,注意下面的规则。
①所有的byte,short,char型的值将被提升为int型;
②如果有一个操作数是long型,计算结果是long型;
③如果有一个操作数是float型,计算结果是float型;
④如果有一个操作数是double型,计算结果是double型;
而声明为final的变量会被JVM优化
引用类型
首先我们要知道,引用类型的出现是为了节省内存,当我们使用引用类型时,一定要给定一个空间,
即需要new一个对象。(1) 引用是一种数据类型(保存在栈中),保存了对象在内存(堆)中的地址,这种类型即不是我们平时所说的基本数据类型也不是类实例(对象);
(2) 不同的引用可能指向同一个对象,换句话说,一个对象可以有多个引用,即该类类型的变量。
Integer
-
integer的128陷阱
-
算法 IP地址和int的双向转换
- 192.168.1.1如何转换成int类型?int类型如何转换成IP地址?用位运算解决
关键字
static-静态属性
static关键字并不会改变变量和方法的访问权限
static是不允许用来修饰局部变量
所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)
静态变量
静态变量(带有static关键字的字段)是属于类的,所有该类的对象共用该字段;
非静态变量(普通字段)是属于类的对象的,每一个该类的对象都有自己的非静态字段,他们互不影响。
静态方法
静态方法与普通方法的区别,与静态字段与普通字段的区别类似
静态方法是不在对象上执行的方法,在调用静态方法时,不需要实例化该类而调用普通方法必须实例化该类。
abstract-抽象
抽象类
抽象类除了不能实例化对象之外,类的其它功能依然存在,成员变量、成员方法和构造方法的访问方式和普通类一样。
由于抽象类不能实例化对象,所以抽象类必须被继承,才能被使用
抽象类表示的是一种继承关系,一个类只能继承一个抽象类,而一个类却可以实现多个接口
抽象方法
抽象方法只包含一个方法名,而没有方法体
声明抽象方法会造成以下两个结果:
- 如果一个类包含抽象方法,那么该类必须是抽象类。
- 任何子类必须重写父类的抽象方法,或者声明自身为抽象类。
抽象类总结规定
- 抽象类不能被实例化(初学者很容易犯的错),如果被实例化,就会报错,编译无法通过。只有抽象类的非抽象子类可以创建对象。
- 抽象类中不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
- 抽象类中的抽象方法只是声明,不包含方法体,就是不给出方法的具体实现也就是方法的具体功能。
- 构造方法,类方法(用 static 修饰的方法)不能声明为抽象方法。
- 抽象类的子类必须给出抽象类中的抽象方法的具体实现,除非该子类也是抽象类。
为什么需要抽象类?
抽象方法和抽象类看上去是多余的,对于抽象方法,不知道如何实现,定义一个空方法体不就行了吗,而抽象类不让创建对象,看上去只是增加了一个不必要的限制。
引入抽象方法和抽象类,是Java提供的一种语法工具,对于一些类和方法,引导使用者正确使用它们,减少被误用。
使用抽象方法,而非空方法体,子类就知道他必须要实现该方法,而不可能忽略。
使用抽象类,类的使用者创建对象的时候,就知道他必须要使用某个具体子类,而不可能误用不完整的父类。
无论是写程序,还是平时做任何别的事情的时候,每个人都可能会犯错,减少错误不能只依赖人的优秀素质,还需要一些机制,使得一个普通人都容易把事情做对,而难以把事情做错。抽象类就是Java提供的这样一种机制。
extends-继承
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为
继承的特性
- 子类拥有父类非 private 的属性、方法。
- 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
- Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 B 类继承 A 类,C 类继承 B 类,所以按照关系就是 B 类是 C 类的父类,A 类是 B 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。
- 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)
多态
多态是同一个行为具有多个不同表现形式或形态的能力。
多态就是同一个接口,使用不同的实例而执行不同操作
多态的优点
- 消除类型之间的耦合关系
- 可替换性
- 可扩充性
- 接口性
- 灵活性
- 简化性
多态存在的三个必要条件
- 继承
- 重写
- 父类引用指向子类对象:Parent p = new Child();
多态的实现方式
方式一:重写:
重写(Override):子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变
重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。
重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常
注意区分重载(重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。
每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
最常用的地方就是构造器的重载。)
方式二:接口
- 1.生活中的接口最具代表性的就是插座,例如一个三接头的插头都能接在三孔插座中,因为这个是每个国家都有各自规定的接口规则,有可能到国外就不行,那是因为国外自己定义的接口类型。
- 2.java中的接口类似于生活中的接口,就是一些方法特征的集合,但没有方法的实现。具体可以看 java接口 这一章节的内容。
方式三:抽象类和抽象方法
interface-接口
接口,在JAVA中是一个抽象类型,是抽象方法的集合。一个类通过继承接口的方式,从而来继承接口的抽象方法。
接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。
除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。
接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象。
Reflection-反射
Java的反射是指程序在运行期可以拿到一个对象的所有信息,是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P3In4YVW-1657867343290)(D:\资料留存\面试\图片知识点\反射API.jpg)]
final
final 用于修饰变量、方法和类。
final 变量:被修饰的变量不可变,不可变分为引用不可变和对象不可变,final 指的是引用不可变,final 修饰的变量必须初始化,通常称被修饰的变量为常量。
final 方法:被修饰的方法不允许任何子类重写,子类可以使用该方法。
final 类:被修饰的类不能被继承,所有方法不能被重写。
finally
finally 作为异常处理的一部分,它只能在
try/catch
语句中,并且附带一个语句块表示这段语句最终一定被执行(无论是否抛出异常),经常被用在需要释放资源的情况下,System.exit (0)
可以阻断 finally 执行。
finalize
是在 java.lang.Object 里定义的方法,也就是说每一个对象都有这么个方法,这个方法在 gc 启动,该对象被回收的时候被调用。
一个对象的 finalize 方法只会被调用一次,finalize 被调用不一定会立即回收该对象,所以有可能调用 finalize 后,该对象又不需要被回收了,然后到了真正要被回收的时候,因为前面调用过一次,所以不会再次调用 finalize 了,进而产生问题,因此不推荐使用 finalize 方法。
implements
类使用implements关键字实现接口
super
super
关键字是一个引用变量,用于引用直接父类对象。每当创建子类的实例时,父类的实例被隐式创建,由
super
关键字引用变量引用。调用的位置只能在构造器的第一行
super
关键字的用法如下:
super
可以用来引用直接父类的实例变量。super
可以用来调用直接父类方法。super()
可以用于调用直接父类构造函数。
使用super和this应注意:
1)调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。
2)super()和this()类似,区别是,super从子类中调用父类的构造方法,this()在同一类内调用其它方法。
3)super()和this()均需放在构造方法内第一行。
4)尽管可以用this调用一个构造器,但却不能调用两个。
5)this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
6)this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。
7)从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。
访问修饰符
private | 私有的 | 被其修饰的属性以及方法只能被该类的对象 访问,其子类不能访问,更不能允许跨包访问 |
---|---|---|
protected | 受保护访问 | 被其修饰的属性以及方法只能被类本身的方法及子类访问,即使子类在不同的包中也可以访问 |
public | 公开的 | 被其修饰的类、属性以及方法不仅可以跨类访问,而且允许跨包访问 |
default(不加任何访问修饰符) | 默认访问 | 只允许在同一个包中进行访问 |
类内部成员初始化顺序
方法调用
- 有静态方法才能被类名调用
- 一般方法,只能被实例对象调用
- final修饰方法,只是说明本方法不能被重写
- abstract修饰方法,表示本方法为抽象方法,没有方法体,且抽象方法必须在抽象类中,但是抽象类中可以没有抽象方法
final语义:
在接口里面的变量默认都是public static final 的,它们是公共的,静态的,最终的常量.相当于全局常量,可以直接省略修饰符。
实现类可以直接访问接口中的变量
变量被static修饰则该变量为类变量,类变量存储在方法区,不属于每个实例的私有,该类的所有对象操作的都是同一个变量
String、StringBuilder、StringBuffer
执行效率(相对情况):StringBuilder > StringBuffer > String
String:适用于少量的字符串操作的情况,对象不可变(内部结构用的是private final char[],字符串拼接是生成新的字符串)
StringBuilder:线程不安全,性能较好,适用于单线程下在字符缓冲区进行大量操作的情况
StringBuffer:线程安全(StringBuffer中的方法基本被synchronized修饰),性能较差,适用多线程下在字符缓冲区进行大量操作的情况,对象可变(字符串拼接是在原字符串后链接)
问题1、String, StringBuilder, StringBuffer的区别、使用场景
- String数据内部结构用的是private final char[],是不可变的
- 其实被private final修饰的是引用对象,只要引用不变,char[]数组中的内容是可以变的,但是String没有提供修改方法,所以String是不可变的。但是,可以用反射把value的访问权限valueField.setAccessible(true),那么外部就可以对value进行修改了。
- 对String进行 “+=”操作 时,会生成一个新的String对象,让引用指向新的对象。如果经常改变String内容的场景,不要用String类型,内存中无引用的对象多了,容易造成内存泄漏,GC就开始工作,性能会降低。
- 使用String类的concat与replace方法时,不会对原来的对象产生影响,他们会返回一个全新的对象
StringBuilder, StringBuffer的实现原理:
• 用char[]存储数据,当char[]盛不下时,进行扩容,2倍扩容,避免总是扩容,默认数组的长度是16,如果能够预测char[]的长度的话,如果长度小于16,那么可以不设置,如果比16长的话,应该尽量设置长度,不然的话,会进行扩容,导致性能降低。
java异常处理机制
异常处理一般格式:
捕获异常:
try{
//代码块
}catch(异常类型,例如:Exception e){
//需要抛出的异常,例如:e.printStackTrace();
}catch(异常类型){
//需要抛出的异常
}finally{
//必定执行的代码块
}
所以说在一个异常处理中catch语句块是可以多个的,也就是可以抛出多个异常!
JVM
jvm类加载机制
类加载机制:jvm把数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
类的生命周期:
- 加载:查找并加载类的二进制数据
- 验证:确保被加载的类的正确性(文件格式、元数据、字节码、符号引用等验证)
- 准备:为类的静态变量分配内存,并将其初始化为默认值
- 解析:把常量池中的符号引用转换为直接引用
- 初始化:对类的静态变量,静态代码块执行初始化操作
- 使用:类访问方法区内的数据结构的接口, 对象是Heap区的数据。
- 卸载:java虚拟机将结束生命周期的几种情况
- 执行了System.exit()方法
- 程序正常执行结束
- 程序在执行过程中遇到了异常或错误而异常终止
- 由于操作系统出现错误而导致Java虚拟机进程终止
类加载的三种方式:
- 命令行启动应用时候由JVM初始化加载
- 通过Class.forName()方法动态加载
- 通过ClassLoader.loadClass()方法动态加载
jvm类加载机制:
-
全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
-
父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
-
缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。
-
双亲委派机制:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
判断2个类是否相同:
首先看他们的类加载器是不是一样的,如果不一样,那么肯定不是同一个类
jvm内存模型
jvm局部变量表
局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量
虚拟机栈由栈帧组成,栈帧由局部变量表、操作数栈、动态链接和方法返回四部分组成,有的虚拟机还有一些附加信息
内存模式在jdk1.8的改动
元数据区取代了永久代。元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元数据空间并不在虚拟机中,而是使用本地内存
废弃永久代原因:官方解释是为了融合其他vm,实际使用中永久代内存经常不够用或发生内存泄露
jvm中重要的内存区域
方法区:专门用来存放已经加载的类信息,常量,静态变量以及方法代码的内存区域
常量池:是方法区的一部分,主要用来存放常量和类中的符号引用等信息;
堆区:存放类的对象实例
栈区:也叫Java虚拟机栈,由一个个的栈帧组成的后进先出的栈式结构,存放方法运行时产生的局部变量,方法出口等信息。当调用一个方法时,虚拟机栈就会创建一个栈帧存放这些数据,当方法调用完成时,栈帧消失,如果方法调用了其他方法,则继续在栈顶创建新的栈帧。
-
堆、栈、方法区会出现的异常:
- 内存溢出
-
方法区 PermGen space:Perm被占满,无法为新的class分配存储空间而引发的异常
- 主要原因:就是大量动态反射生成的类不断被加载
- 解决方法:-XX:MaxPermSize=16m,用完时,进行GC
-
栈溢出(只有栈):递归没返回,或者循环调用造成
-
内存泄漏:解决办法一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。
-
怎么让方法区溢出 方法区存的是 类加载信息、静态变量、常量、常量池
- 静态块中写while循环,调用String的intern。intern()方法设计的初衷:重用String对象,以节省内存消耗,使用intern的话,时间会比不适用intern的时间稍微长点,但是如果不适用intern的话,GC花费的时间更长。intern()方法是把String对象的value放入常量池中
- 动态发射会加载很多类信息,也会造成方法区溢出
内存分配和垃圾收集gc算法
内存分配策略
- 对象优先分配在新生代(Eden区),MinorGC(新生代垃圾回收)后存活对象进入Survivor区,一般Eden:Survivor:Survivor= 8:1:1
- 大对象直接进入老年代
- 大年龄的对象进入老年代(默认15岁提升入老年代)
- 动态年龄分配:并不是永远要求对象年龄必须达到「阈值」才能提升至老年代。在有的垃圾收集器实现中,如果Survivor空间中相同年龄的对象占用空间>Survivor总空间的一半,则此年龄的所有对象就可以提前进入老年代,而不是必须达到阈值。
- 空间分配担保机制:在MinorGC之前,JVM会首先检查老年代最大可用的连续内存空间是否 > 青年代所有对象总空间,并以其作为MajorGC执行的「担保」。如果大于则MinorGC可以正常执行。否则JVM会查看HandlePromotionFailure设置值是否允许担保失败,如果允许,则继续执行MinorGC,否则则执行MajorGC(老年代垃圾回收)用来回收足够的内存空间。
Full GC (新生代+老年代垃圾回收)
Full GC可以理解为Major GC+Minor GC组合后进行的一整个过程,是清理JVM整个堆空间(年轻代和老年代空间)。
Full GC触发条件
- 调用System.gc()方法时,可通过-XX:+ DisableExplicitGC 参数来禁止调用System.gc()。
- 当方法区空间不足时。
- Minor GC后存活的对象大小超过了老年代剩余空间。
- Minor GC时中Survivor幸存区空间不足时,判断是否允许担保失败,不允许则触发Full GC。允许,并且每次晋升到老年代的对象平均大小>老。年代最大可用连续内存空间,也会触发Full GC。
- CMS GC异常,CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,会触发Full GC。
垃圾回收算法
- 标记-清除:碎片,所以尽量不要-(适用老年代)
- 标记-整理-(适用老年代)
- 复制算法-(适用新生代)
- 分代收集算法
垃圾收集器
-
Serial收集器:(适用新生代+单线程“串行”)它在进行垃圾回收的工作时,必须暂停JVM中的其他工作线程
-
Serial Old收集器:(适用老年代+单线程“串行”)是Serial收集器在老年代上的版本,同样是采用复制算法的单线程收集器
-
Parnew收集器:(适用新生代+多线程“并行”)其实就是Serial收集器的多线程版本
-
Parellel Scavenge收集器:(适用新生代+多线程“并行”)采用复制算法的收集器,主要关注点在于达到一个可控制的吞吐量
-
Parellel Old收集器:(适用老年代+多线程“并行”)是Parellel Scavenge的老年代版本,采用多线程和标记-压缩算法。
-
CMS收集器:(标记-清除并发收集器+多线程)
收集过程分为4步:
-
初始标记:标记GC Roots能直接关联到的对象,速度很快
-
并发标记:进行GC Roots追踪的过程
-
重新标记:修正并发标记期间由于用户程序继续执行可能产生变动的那部分对象的标记记录,此阶段会比初始标记长一些,但远小于并发标记的时间。
-
并发清除
-
优点:
- GC收集间隔时间短,多线程并发。
-
缺点:
- 1.并发时对CPU资源占用多,不适合CPU核心数较少的情况。
- 2.且由于采用标记清除算法,所以会产生内存碎片。
- 3.无法处理浮动垃圾。(CMS并发清除阶段用户线程还可以继续执行,可能产生新垃圾)
-
-
G1收集器
并行与并发:G1能充分利用多CPU下的优势来缩短Stop The World的时间,同时在其他部分收集器需要停止Java线程来执行GC动作时,G1收集器仍然可以通过并发来让Java线程同步执行。
分代收集:与其他收集器一样,分代的概念在G1中任然被保留。可以不需要配合其他的垃圾收集器,就独立管理整个Java堆内存的所有分代区域,且采用不同的方式来获得更好的垃圾收集效果。
空间整合:G1从整体来看,使用的是标记-压缩算法实现的,从局部两个Region来看,采用的是复制算法实现的,对内存空间的利用非常高效,不会像CMS一样产生内存碎片。
可以预测的停顿:除了追求低停顿以外,G1的停顿时间可以被指定在一个时间范围内。
如果不计算维护Remenbered Set的操作,G1收集器的工作阶段大致区分如下:- 初始标记
- 并发标记
- 最终标记
- 筛选回收
-
垃圾回收器分类: JDK1.7和1.8(默认parallel scavenge,parallel old),1.9(默认G1)
-
1、串行处理器(单线程):serial,serial old
-
2、并行处理器(多线程):ParNew,parallel scavenge,parallel old 适用情况:“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算。 缺点:垃圾收集过程中应用响应时间可能加长
-
3、并发处理器(多线程):cms,G1;特点:对响应时间有高要求(低停顿)
★★★★★ 重点说下G1的垃圾回收过程:初始标记;并发标记;最终标记;选择回收(CMS是并发回收,G1相对于CMS的优点)
问题4:JVM调优?或者做过关于性能提高的工作吗
曾经遇到过性能调优的一个情况,起因是一个POD容器频繁宕机,通过检查发现相比其他容器,GC比较频繁,便考虑到可能是内存分配较小,调大了内存后,GC不频繁,宕机情况也解决
判断对象可以被回收:基于GC Roots的可达性分析算法,如果对象不在任意一个以 GC Root 为起点的引用链中,则这些对象会被判断为垃圾对象,会被 GC 回收。
JVM调优
- 选择合适的垃圾回收器
- CPU单核选择Serial垃圾收集器(新生代)
- CPU多核
- 关注吞吐量 ,那么选择PS+PO组合,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
- 关注用户停顿时间
- JDK版本1.6或者1.7,那么选择CMS垃圾收集器(老年代)
- JDK1.8及以上,JVM可用内存6G以上,那么选择G1垃圾收集器
- 调整内存大小(现象:垃圾收集频率非常频繁。若GC频繁但回收对象少,可能是内存泄露导致对象无法回收)
- 设置符合预期的停顿时间(现象:程序间接性的卡顿)
- 调整内存区域大小比例(现象:某一个区域的GC频繁,其他都正常。也有可能是内存泄露情况)
- 调整对象升老年代的年龄(现象:老年代频繁GC,每次回收的对象很多)
- 调整大对象的标准(现象:老年代频繁GC,每次回收的对象很多,而且单个对象的体积都比较大)
- 调整GC的触发时机(现象:CMS,G1 经常 Full GC,程序卡顿严重)
- 调整JVM本地内存大小(现象:GC的次数、时间和回收的对象都正常,堆内存空间充足,但是报OOM)
jvm使用过的参数
参数 | 含义 |
---|---|
-XX:+UseConcMarkSweepGC | 使用CMS垃圾回收器 |
-XX:+UseCMSCompactAtFullCollection | 在使用ConcurrentGC(并发GC)的情况下,防止内存碎片,对存活对象进行整理,使碎片减少 |
-XX:+CMSClassUnloadingEnabled | 老年代启用CMS,但默认是不会回收永久代的。此处对永久代启用类回收,防止内存满 |
-XX:+UseCMSInitiatingOccupancyOnly | 只有在老年代使用了初始化的比例后ConcurrentCollector(并发收集器)启动收集 |
-XX:+PrintGCDetails | 打印GC日志 |
-XX:+PrintGCDateStamps | 打印GC日志对应的时间戳 |
-XX:-OmitStackTraceInFastThrow | 省略异常栈信息从而快速抛出 |
-XX:+HeapDumpOnOutOfMemoryError | 可以让JVM在出现内存溢出时候Dump出当前的内存快照dump文件 |
-XX:MetaspaceSize=2g | 初始化的Metaspace大小,该值越大触发Metaspace GC的时机就越晚。 |
-XX:MaxMetaspaceSize=512m | 限制Metaspace增长的上限,防止因为某些情况导致Metaspace无限的使用本地内存,影响到其他程序。默认无上限 |
-XX:MaxDirectMemorySize=1g | 指定本地直接内存大小,如果不指定,则默认与Java堆的最大值-Xmx指定一样 |
-Xmx4g -Xms4g | jvm内存最大最小值 |
-Xmn2g | 设置年轻代大小为2G |
-XXSurvivorRatio=3 | 代表Eden:Survivor:Survivor = 3:1:1 |
dump
HeapDump文件是指定时刻的Java堆栈的快照,是一种镜像文件。
- jmap 命令是JDK提供的用于生成堆内存信息的工具,切换到JDK_HOME/bin目录下后,执行dump命令
- ***(./jmap -dump:live,format=b,file=heap.hprof )***其中pid是JVM进程的id,heap.hprof是生成的heap dump文件,在执行命令的目录下面
- 使用dump文件分析工具分析展示
- jhat 是JDK自带的用于分析JVM Heap Dump文件的工具
- 命令 jhat heap-dump-file 是文件的路径和文件名
- 访问 http://localhost:7000/ 即可以看到结果。
java制造OOM
jvm的几个运行时区域都有发生 OutOfMemoryError 异常的可能
锁
锁是多线程环境的一种同步机制,对线程访问资源权限进行控制,实现并发策略
volatile
是轻量级的synchronized,保证共享变量的“可见性”,只能对变量作用,如果使用恰当,比synchronized的使用和执行成本更低(不会引起线程上下文切换和调度)
只保证多线程操作的可见性(将当前处理器缓存行的数据写回到系统内存,同时这个写回内存操作会使其他CPU里缓存了该内存地址的数据无效),不保证原子性
synchronize-互斥锁
只能作用在方法上,互斥锁
Syncronized是重量级锁,加了 syncronized 关键字的方法、代码块中,一次只允许一个线程进入特定代码段,从而避免多线程同时修改同一数据
应用Sychronized注意:
- 一把锁只能同时被一个线程获取,没有获得锁的线程只能等待;
- 每个实例都对应有自己的一把锁(this),不同实例之间互不影响;例外:锁对象是*.class以及synchronized修饰的是static方法的时候,所有对象公用同一把锁
- synchronized修饰的方法,无论方法正常执行完毕还是抛出异常,都会释放锁
synchronized锁的实现
有两种形式上锁,一个是对方法上锁,一个是构造同步代码块。他们的底层实现其实都一样,在进入同步代码之前先获取锁,获取到锁之后锁的计数器+1,同步代码执行完锁的计数器-1,如果获取失败就阻塞式等待锁的释放。只是他们在同步块识别方式上有所不一样,从class字节码文件可以表现出来,一个是通过方法flags标志,一个是monitorenter和monitorexit指令操作。
synchronized锁底层实现
理解锁实现原理之前要先了解Java的对象头和Monitor,在JVM中,对象是分成三部分存在的:对象头、实例数据(存放类的属性数据信息)、对其填充(仅是字节对齐作用)。
对象头是synchronized实现锁的基础,因为synchronized申请锁、上锁、释放锁都与对象头有关。对象头主要结构是由Mark Word
和 Class Metadata Address
组成,其中Mark Word
存储对象的hashCode、锁信息或分代年龄或GC标志等信息,Class Metadata Address
是类型指针指向对象的类元数据,JVM通过该指针确定该对象是哪个类的实例。
锁的类型和状态在对象头Mark Word
中都有记录,在申请锁、锁升级等过程中JVM都需要读取对象的Mark Word
数据。
每一个锁都对应一个monitor对象,当线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。
锁的升级
锁的状态总共有四种,级别由低到高依次为:无锁、偏向锁、轻量级锁、重量级锁,在 JDK 1.6之前,synchronized 还是一个重量级锁,是一个效率比较低下的锁,但是在JDK 1.6后,Jvm为了提高锁的获取与释放效率对(synchronized )进行了优化,引入了 偏向锁 和 轻量级锁,并且四种状态会随着竞争的情况逐渐升级,而且是不可逆的过程,即不可降级,这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。
升级过程:
无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功。
偏向锁:初次执行到synchronized代码块的时候,锁对象变成偏向锁,执行完同步代码块后,线程并不会主动释放偏向锁。
轻量级锁:当锁是偏向锁的时候,却被另外的线程访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。
重量级锁:如果锁竞争情况严重,某个达到最大自旋次数(默认10次)的线程,会将轻量级锁升级为重量级锁。当后续线程尝试获取锁时,发现被占用的锁是重量级锁,则直接将自己挂起(而不是忙等),等待被唤醒。
公平锁:先申请的先得到锁
非公平锁:先申请的不一定先得到锁。一般来说,使用非公平锁可以获得较大的吞吐量,所以推荐优先使用非公平锁。
悲观锁:在读数据的时候总认为其他线程会对数据进行修改,所以采取加锁的形式,一旦本线程要读取数据时,就加锁,其他线程被阻塞,等待锁的释放。悲观锁总结为悲观加锁阻塞线程
乐观锁:在读数据时总认为其他线程不会对数据做修改,在更新数据时会判断其他线程有没有更新数据,如果有更新,则重新读取,再次尝试更新,循环上述步骤直到更新成功。这样来看乐观锁实际上是没有锁的,只是通过一种比较交换的方法来保证数据同步,总结为乐观无锁回滚重试。
CAS(比较和交换)
可重入锁:允许多个线程多次获取同一把锁,那从锁本身的角度来看,就是可以重新进入该锁。比如有一个递归函数里面有加锁操作,如果这个锁不阻塞自己,就是可重入锁,故也称递归锁 。
JDK中Lock锁接口
Lock相较于Synchronized优势如下:
- 可中断获取锁:使用synchronized关键字获取锁的时候,如果线程没有获取到被阻塞了,那么这个时候该线程是不响应中断(interrupt)的,而使用Lock.lockInterruptibly()获取锁时被中断,线程将抛出中断异常。
- 可非阻塞获取锁:使用synchronized关键字获取锁时,如果没有成功获取,只有被阻塞,而使用Lock.tryLock()获取锁时,如果没有获取成功也不会阻塞而是直接返回false。
- 可限定获取锁的超时时间:使用Lock.tryLock(long time, TimeUnit unit)。
- 同一个所对象上可以有多个等待队列(Conditin,类似于Object.wait(),支持公平锁模式)。
CopyOnWriteArrayList适用于写少读多的并发场景
ReadWriteLock即为读写锁,他要求写与写之间互斥,读与写之间互斥,读与读之间可以并发执行。在读多写少的情况下可以提高效率
AQS
链接:https://pdai.tech/md/java/thread/java-thread-x-lock-AbstractQueuedSynchronizer.html
AbstractQueuedSynchronizer的分析,最核心的就是sync queue的分析。
- 每一个结点都是由前一个结点唤醒
- 当结点发现前驱结点是head并且尝试获取成功,则会轮到该线程运行。
- condition queue中的结点向sync queue中转移是通过signal操作完成的。
- 当结点的状态为SIGNAL时,表示后面的结点需要运行
多线程及线程池
线程生命周期
-
新建状态:
使用 new 关键字和 Thread 类或其子类建立一个线程对象后,该线程对象就处于新建状态。它保持这个状态直到程序 start() 这个线程。
-
就绪状态:
当线程对象调用了start()方法之后,该线程就进入就绪状态。就绪状态的线程处于就绪队列中,要等待JVM里线程调度器的调度。
-
运行状态:
如果就绪状态的线程获取 CPU 资源,就可以执行 run(),此时线程便处于运行状态。处于运行状态的线程最为复杂,它可以变为阻塞状态、就绪状态和死亡状态。
-
阻塞状态:
如果一个线程执行了sleep(睡眠)、suspend(挂起)等方法,失去所占用资源之后,该线程就从运行状态进入阻塞状态。在睡眠时间已到或获得设备资源后可以重新进入就绪状态。可以分为三种:
- 等待阻塞:运行状态中的线程执行 wait() 方法,使线程进入到等待阻塞状态。
- 同步阻塞:线程在获取 synchronized 同步锁失败(因为同步锁被其他线程占用)。
- 其他阻塞:通过调用线程的 sleep() 或 join() 发出了 I/O 请求时,线程就会进入到阻塞状态。当sleep() 状态超时,join() 等待线程终止或超时,或者 I/O 处理完毕,线程重新转入就绪状态。
-
死亡状态:
一个运行状态的线程完成任务或者其他终止条件发生时,该线程就切换到终止状态。
Java 提供了三种创建线程的方法:
- 通过实现 Runnable 接口;
- 通过继承 Thread 类本身;
- 通过 Callable 和 Future 创建线程。
创建线程的三种方式的对比
-
采用实现Runnable、Callable接口的方式创见多线程时,
- 优势是:线程类只是实现了Runnable接口或Callable接口,还可以继承其他类。在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码和数据分开,形成清晰的模型,较好地体现了面向对象的思想。
- 劣势是:编程稍微复杂,如果要访问当前线程,则必须使用Thread.currentThread()方法。
-
使用继承Thread类的方式创建多线程时
- 优势是:编写简单,如果需要访问当前线程,则无需使用Thread.currentThread()方法,直接使用this即可获得当前线程。
- 劣势是:线程类已经继承了Thread类,所以不能再继承其他父类。
Object类的线程方法。
**notify() **:通知一个在对象上等待的线程,使其从wait()返回,而返回的前提是该线程获取到了对象的锁。
notifyAll(): 通知所有等待在该对象上的线程。
wait():调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意,调用wait()方法后,会释放对象的锁。
wait(long) :超时等待一段时间(参数是毫秒),如果没有通知就超时返回。
wait(long, int) : 对于超时时间更细粒度的控制,可以达到毫秒。
线程池
https://blog.csdn.net/fanrenxiang/article/details/79855992
https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
线程池核心参数
-
corePoolSize 线程池核心线程大小:这些线程处理空闲状态,他们也不会被销毁,除非设置了allowCoreThreadTimeOut
-
maximumPoolSize 线程池最大线程数量:当前线程数达到corePoolSize后,如果继续有任务被提交到线程池,会将任务缓存到队列中。如果队列也满,则会创建新线程处理新提交的任务。
-
keepAliveTime 空闲线程存活时间:一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,空闲线程会被销毁
-
unit 空闲线程存活时间单位
-
workQueue 工作队列:新任务被提交后,会先进入到此工作队列中,任务调度时再从队列中取出任务。jdk中提供了四种工作队列
-
ArrayBlockingQueue
基于数组的有界阻塞队列,按FIFO排序。新任务进来后,会放到该队列的队尾,有界的数组可以防止资源耗尽问题。当线程池中线程数量达到corePoolSize后,再有新任务进来,则会将任务放入该队列的队尾,等待被调度。如果队列已经是满的,则创建一个新线程,如果线程数量已经达到maxPoolSize,则会执行拒绝策略。
-
LinkedBlockingQuene
基于链表的无界阻塞队列(其实最大容量为Interger.MAX),按照FIFO排序。由于该队列的近似无界性,当线程池中线程数量达到corePoolSize后,再有新任务进来,会一直存入该队列,而基本不会去创建新线程直到maxPoolSize(很难达到Interger.MAX这个数),因此使用该工作队列时,参数maxPoolSize其实是不起作用的。
-
SynchronousQuene
一个不缓存任务的阻塞队列,生产者放入一个任务必须等到消费者取出这个任务。也就是说新任务进来时,不会缓存,而是直接被调度执行该任务,如果没有可用线程,则创建新线程,如果线程数量达到maxPoolSize,则执行拒绝策略。
-
PriorityBlockingQueue
具有优先级的无界阻塞队列,优先级通过参数Comparator实现。
-
-
threadFactory 线程工厂:创建一个新线程时使用的工厂,可以用来设定线程名、是否为daemon(守护)线程等等
-
handler 拒绝策略:当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,就需要拒绝策略,jdk中提供了4种拒绝策略
- CallerRunsPolicy:在调用者线程中直接执行被拒绝任务的run方法,除非线程池已经shutdown,则直接抛弃任务
- AbortPolicy:直接丢弃任务,并抛出RejectedExecutionException异常。
- DiscardPolicy:直接丢弃任务,什么都不做。
- DiscardOldestPolicy:抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列
• 定时线程池同步数据
• 多数据库源问题的解决
数据结构
set、list、map区别:https://blog.csdn.net/qq_39241239/article/details/82116734、 https://www.jb51.net/article/112985.htm
java集合:主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。
java容器-集合
https://blog.csdn.net/dengpeng0419/article/details/47983033
排序
叫车服务,有500量符合要求的车,怎么快速选出前10量?
根据车的远近、司机的评分等因素,进行堆排序,时间复杂度500log10
堆排序是稳定的吗?不是,稳定的有哪些?冒泡、插入、归并
HashMap底层原理
介绍:HashMap是最常用的存储键值对的集合,继承了AbstractMap类,实现了Map等接口,内部原理是基于散列函数计算出元素存储的位置,查询的时候也是根据散列函数继续计算出存储的位置去获取该位置上存储的元素,非并发安全。
底层原理:1.7版本中,底层数据结构是数组+链表,也就是一个数组用来存储value
,因为散列函数是很有可能出现哈希碰撞的,也就是两个不同的key
计算得出同一个哈希值,结果就存到同一个数组索引上了,那不能覆盖掉前面的值呀,所以数组中存的是链表,如果有冲突了,同一个索引上就使用链表来存储多个值。
1.8版本中,因为链表的查询时间复杂度是O(n),所以冲突很严重,一个索引上的链表非常长,效率就很低了,所以在1.8版本的时候做了优化,当一个链表的长度超过8的时候就转换数据结构,不再使用链表存储,而是使用红黑树,红黑树是一个保证大致平衡的平衡树,所以性能相较AVL树这样的高度平衡树来将性能会更好。
ConcurconrentHashMap底层原理
-
1.7版本ConcurconrentHashMap底层采用的是分段锁,具体来说是一个 Segment 数组(默认长度为16),每个 Segment 又包含了一个 HashEntry 数组,所以可以看做一个 HashMap, Segment 通过继承 ReentrantLock 来进行加锁,所以每次需要加锁的操作锁住的是一个 Segment,这样只要保证每个 Segment 是线程安全的,也就实现了全局的线程安全。
-
1.8版本取而代之的是 Node 数组 + CAS + synchronized + volatile 的新设计方式,只调用synchronized锁住首节点的方式,使得锁更加灵活,同时锁粒度也更加小
-
不仅数据结构变得更简单了(与JDK 1.8 的HashMap类似),锁的粒度也更小了,锁的单位从 Segment 变成了 Node 数组中的桶(科普:桶就是指数组中某个下标位置上的数据集合,这里可能是链表,也可能是红黑树)。说到红黑树,必须提一下,在JDK 1.8 的 HashMap 和ConcurrentHashMap 中,如果某个数组位置上的链表长度过长(大于等于8),就会转化为红黑树以提高查询效率
-
get 操作过程
可以发现源码中完全没有加锁的操作,因为使用 volatile 关键字已经足以保证线程在读取数据时不会读取到脏数据,所以没有加锁的必要。
- 首先计算hash值,定位到该table索引位置,如果是首节点符合就返回
- 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
- 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null
-
put 操作过程
- 第一次 put 元素会初始化 Node 数组 (initTable)
- put 操作又分为 key (hash 碰撞) 存在时的插入和 key 不存在时的插入
- put 操作可能会引发数组扩容 (tryPresize) 和链表转红黑树 (treeifyBin)
- 扩容会使用到数据迁移方法 (transfer)
-
-
-
CAS 操作简要介绍
CAS 操作是新版本 ConcurrentHashMap 线程安全实现原理的精华所在,如果说其共享变量的读取全靠 volatile 实现线程安全的话,那么存储和修改过程除了使用少量的 synchronized 关键字外,主要是靠 CAS 操作实现线程安全的。
ConcurrentHashMap的底层结构和HashMap一样,都是数组+链表+红黑树
ConcurrentHashMap是同步的HashMap,读写都加锁
缓存
Redis
Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。 键的类型只能为字符串,值支持的五种类型数据类型为:字符串(String)、列表(List)、集合(Set)、有序集合(Zset)、散列表(Hash)。 Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。
数据结构:字典和跳跃表
- 跳跃表是有序集合的底层实现之一。 跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。
- 跳跃表与红黑树等平衡树相比,跳跃表具有以下优点
- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性;
- 更容易实现;
- 支持无锁操作。
- 跳跃表与红黑树等平衡树相比,跳跃表具有以下优点
使用场景
计数器 :可以对 String 进行自增自减运算,从而实现计数器功能。 Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
缓存: 将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
查找表: 例如 DNS 记录就很适合使用 Redis 进行存储。 查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因 为缓存不作为可靠的数据来源。
消息队列: List 是一个双向链表,可以通过 lpop 和 lpush 写入和读取消息。 不过最好使用 Kafka、RabbitMQ 等消息中间件。
会话缓存: 在分布式场景下具有多个应用服务器,可以使用 Redis 来统一存储这些应用服务器的会话信息。 当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器。
分布式锁实现: 在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。 可以使用 Reids 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。
其它: Set 可以实现交集、并集等操作,从而实现共同好友等功能。 ZSet 可以实现有序性操作,从而实现排行榜等功能。
Redis与Memcache
两者都是非关系型内存键值数据库,主要有以下不同:
数据类型: Memcached 仅支持字符串类型,而 Redis 支持五种不同的数据类型,可以更灵活地解决问题。
数据持久化: Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。
分布式: Memcached 不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需 要先在客户端计算一次数据所在的节点。 Redis Cluster 实现了分布式的支持。
内存管理机制: 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘,而 Memcached 的数据则会一直在内存中。 Memcached 将内存分割成特定长度的块来存储数据,来解决内存碎片的问题,但是这种方式会使内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
键的过期时间
Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。
对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间
数据淘汰策略
Redis可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。
Reids 具体有 6 种淘汰策略:
- 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
- 从已设置过期时间的数据集中挑选将要过期的数据淘汰
- 从已设置过期时间的数据集中任意选择数据淘汰
- 从所有数据集中挑选最近最少使用的数据淘汰
- 从所有数据集中任意选择数据进行淘汰
- 禁止驱逐数据
作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小 部分并且从中选出被淘汰的 key。
使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,启用 最近最少使用的数据淘汰策略。
Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。
持久化
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
-
RDB 持久化: 将某个时间点的所有数据都存放到硬盘上。 可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。 如果系统发生故障,将会丢失最后一次创建快照之后的数据。 如果数据量很大,保存快照的时间会很长。
-
优点
- 快照保存数据极快,还原数据极快
- 适用于灾难备份
缺点
- 小内存及其不适合使用
- 符合快照条件才会进行快照,意外宕机会丢失最后一次快照后的所有修改
-
-
AOF 持久化: 将写命令添加到 AOF 文件(Append Only File)的末尾。 使用 AOF 持久化需要设置同步选项,从而确保写命令什么时候会同步到磁盘文件上。这是因为对文件进行写入并 不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。同步选项:每个写命令都同步、每秒同步一次 、no 让操作系统来决定何时同步
-
优点
- 持久化比RDB更好,不会丢失任何的修改
缺点
- 持久化文件会变的越来越大
- 重复命令很多
-
Redis事务:Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。
缓存击穿、雪崩
缓存击穿(不存在的数据,可以在redis中存null)
数据库水平切分,或者读写分离
分布式锁,读数据库,然后把数据加载到缓存
熔断、降级
布隆过滤器
缓存雪崩
过期时间加上一个随机数
备份缓存:缓存a有超时,缓存b没有超时
分布式锁:每次只能使用限个数据库连接
缓存并发
分布式的话:用分布式锁
单个服务器的话:用synchronize,lock等保证线程安全
redis集群
redis集群有三种模式:主从模式、哨兵模式、Redis cluster(redis集群)
三种模式主从复制原理基本一致,主从模式不支持高可用(机器故障需要手动操作);哨兵模式解决高可用问题,但扩容需求无法满足;
主从模式
集群介绍
1)主从模式里一个redis实例作为主机(master),其余多个实例作为备份机(slave);
2)master支持数据的写入和读取操作,而slave支持读取及master的数据同步;
3)在整个架构里,master和slave实例里的数据完全一致;
主从复制原理
- 全量同步
- 当从节点启动时,会向主节点发送SYNC(同步)命令;
- 主节点接收到SYNC命令后,在后台执行保存快照的命令生成RDB文件,并使用缓冲区记录此后执行的所有写命令;
- 主节点快照完成后,将快照文件和所有缓存命令发送给集群内的从节点,并在发送期间继续记录被执行的写命令;
- 主节点快照发送完毕后开始向从节点发送缓冲区中的写命令;
- 从节点载入快照文件后,开始接收命令请求,执行接收到的主节点缓冲区的写命令。
- 增量同步
主从复制中因网络等原因造成数据丢失场景,当从节点再次连上主节点。如果条件允许,主节点会补发丢失数据给从节点。因为补发的数据远远小于全量数据,可以有效避免全量复制的过高开销。
主节点故障处理方式
主从模式中,每个客户端连接redis实例时都指定了ip和端口号。如果所连接的redis实例因为故障下线了,则无法通知客户端连接其他客户端地址,因此只能进行手动操作。
不支持高可用
主从模式很好地解决了数据备份的问题,但是主节点因为故障下线后,需要手动更改客户端配置重新连接,这种模式并不能保证服务的高可用。
哨兵模式
集群介绍
哨兵模式中增加了独立进程(即哨兵)来监控集群。客户端在连接集群时,首先连接哨兵,通过哨兵查询主节点的地址,然后再去连接主节点进行数据交互。
如果master异常,则会进行master-slave切换,将最优的一个slave切换为主节点。同时,哨兵持续监控挂掉的主节点,待其恢复后,作为新的从节点加入集群中。
主节点故障处理方式/哨兵工作方式
- 每个哨兵每秒向集群中的master、slave以及其他哨兵发送一个PING命令;
- 如果某个实例距离最后一次有效回复ping命令的时间超过一定值,则会被标记为主观下线;
- 如果master被标记为主观下线,那么其他正在监视master的哨兵以每秒的频率确认其确实进入主观下线状态,且数量达到一定值时,master会被标记为下线,然后通知其他的从服务器,修改配置文件,让它们切换主机;
- 客户端在master节点发生故障时会重向哨兵要地址,此时会获得最新的master节点地址。
扩容问题
哨兵模式的出现虽然解决了主从模式中master节点宕机不能自主切换(即高可用)的问题。但是,随着业务的逐渐增长,不可避免需要对当前业务进行扩容。
常见的扩容方式有垂直和水平扩容两种方式:
- 垂直扩容:通过增加master内存来增加容量;
- 水平扩容:通过增加节点来进行扩容,即在当前基础上再增加一个master节点。
虽然垂直扩容方式很便捷,不需要添加多余的节点,但是机器的容量是有限的,最终还是需要通过水平扩容方式来解决。而水平扩容涉及到数据的迁移,且迁移过程中又要保证服务的可用性。因此,数据能不迁移就尽量不要迁移。
Redis cluster 模式
集群介绍
- **redis cluster模式采用了无中心节点的方式来实现,每个主节点都会与其它主节点保持连接。**节点间通过gossip协议交换彼此的信息,同时每个主节点又有一个或多个从节点;
- 客户端连接集群时,直接与redis集群的每个主节点连接,根据hash算法取模将key存储在不同的哈希槽上;
- 在集群中采用数据分片的方式,将redis集群分为16384个哈希槽。
- 每个节点会保存一份数据分布表,节点会将自己的slot信息发送给其他节点,节点间不停的传递数据分布表;
- 客户端连接集群时,通过集群中某个节点地址进行连接。客户端尝试向这个节点执行命令时,比如获取某个key值,如果key所在的slot刚好在该节点上,则能够直接执行成功。如果slot不在该节点,则节点会返回MOVED错误,同时把该slot对应的节点告诉客户端,客户端可以去该节点执行命令。
主节点故障处理方式
redis cluster中主节点故障处理方式与哨兵模式比较相似,当约定时间内某节点无法与集群中的另一个节点顺利完成ping消息通信时,则将该节点标记为主观下线状态,同时将这个信息向整个集群广播。
如果一个节点收到某个节点失联的数量达到了集群的大多数时,那么将该节点标记为客观下线状态,并向集群广播下线节点的fail消息。然后立即对该故障节点进行主从切换。等到原来的主节点恢复后,会自动成为新主节点的从节点。如果主节点没有从节点,那么当它发生故障时,集群就将处于不可用状态。
扩容问题
- 当集群中加入新节点时,会与集群中的某个节点进行握手,该节点会把集群内的其它节点信息通过gossip协议发送给新节点,新节点与这些节点完成握手后加入到集群中,然后集群中的节点会各取一部分哈希槽分配给新节点
- 当集群中要删除节点时,只需要将节点中的所有哈希槽移动到其它节点,然后再移除空白(不包含任何哈希槽)的节点就可以了
数据库
复杂sql语句,遇到的复杂SQL语句,group by、行转列(小表涉及)等
mysql
mysql引擎(myisam与innodb)区别以及应用
1、innodb支持事务ACID,myisam不支持
2、innodb支持行级锁,myisam支持表级锁
3、innodb支持mvcc(多版本并发控制),myisam不支持
4、MyIASM支持全文类型索引,而InnoDB不支持全文索引
5、MyIASM相对简单,效率上要优于InnoDB,小型应用可以考虑使用MyIASM
6、MyIASM表保存成文件形式,跨平台使用更加方便
应用:1、MyIASM管理非事务表,提供高速存储和检索以及全文搜索能力,如果再应用中执行大量select操作,应该选择MyIASM
2、InnoDB用于事务处理,具有ACID(原子性、一致性、隔离性、持久性)事务支持等特性,如果在应用中执行大量insert和update操作,应该选择InnoDB
数据库事务
数据库事务可以保证多个对数据库的操作(也就是 SQL 语句)构成一个逻辑上的整体。构成这个逻辑的数据库操作遵循:要么全部执行成功,要么全部不执行,关系型数据库具有ACID特性
- 原子性(
Atomicity
) : 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; - 一致性(
Consistency
): 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的; - 隔离性(
Isolation
): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; - 持久性(
Durabilily
): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
mysql四个事务隔离级别
这四个级别可以逐个解决脏读 、不可重复读 、幻读 这几类问题,讨论隔离级别的场景,主要是在多个事务并发 的情况下
-
读未提交-Read uncommitted ----最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
- **举例:**公司发工资了,领导把5000元打到A的账号上,此时该事务并未提交,而A正好查看账户,发现工资已经到账,是5000元整。领导发现发给A的工资金额不对,应该是2000元,于是迅速回滚了事务,修改金额后,将事务提交,最后A实际的工资只有 2000元,A空欢喜一场。
- 出现上述情况,即我们所说的脏读 ,两个并发的事务,“事务A:领导给A发工资”、“事务B:A查询工资账户”,事务B读取了事务A尚未提交的数据。
-
读提交-Repeatable read ---- 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。(大多数数据库的默认级别就是读提交,比如Sql Server , Oracle)
- **举例:**A去消费,系统读取到卡里有2000元,而此时她的老婆也正好在转账,把A工资卡的2000元转到另一账户,并在 A之前提交了事务,当A扣款消费时,系统检查到A的工资卡已经没有钱
- 出现上述情况,即我们所说的不可重复读 ,两个并发的事务,“事务A:A员工消费”、“事务B:A的老婆网上转账”,事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。
-
可重复读-Read committed ---- 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。(Mysql的默认隔离级别是可重复读)
- **举例:**当A拿着工资卡去消费时,一旦系统开始读取工资卡信息(即事务开始),A的老婆就不可能对该记录进行修改,也就是A的老婆不能在此时转账。
-
序列化- Serializable ----最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,该级别可以防止脏读、不可重复读以及幻读。
- 同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行
### 脏读(读取未提交数据)
### 不可重复读(前后多次读取,数据内容不一致)
### 幻读(前后多次读取,数据总量不一致)
不可重复读和幻读到底有什么区别呢?
(1)不可重复读是读取了其他事务更改的数据,针对insert与update操作
解决:使用行级锁,锁定该行,事务A多次读取操作完成后才释放该锁,这个时候才允许其他事务更改刚才的数据。
(2)幻读是读取了其他事务新增的数据,针对insert与delete操作
解决:使用表级锁,锁定整张表,事务A多次读取数据总量之后才释放该锁,这个时候才允许其他事务新增数据。
MySQL 的隔离级别是基于锁实现的吗?
MySQL 的隔离级别基于锁和 MVCC (多版本并发控制)机制共同实现的。
序列化隔离级别,是通过锁来实现的。除了序列化隔离级别,其他的隔离级别都是基于 MVCC 实现。
不过, 序列化之外的其他隔离级别可能也需要用到锁机制,就比如 读提交 在当前读情况下需要使用加锁读来保证不会出现幻读
Mysql锁
表级锁和行级锁
MyISAM 仅仅支持表级锁,锁整张表,在并发写的情况下性能非常差。
InnoDB 不光支持表级锁,还支持行级锁,默认为行级锁。行级锁的粒度更小,仅对相关的记录上锁即可,所以对于并发写入操作来说, InnoDB 的性能更高。
表级锁和行级锁对比 :
- 表级锁: MySQL 中锁定粒度最大的一种锁,是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM 和 InnoDB 引擎都支持表级锁。
- 行级锁: MySQL 中锁定粒度最小的一种锁,是针对索引字段加的锁,只针对当前操作的记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁
行级锁的使用注意事项:InnoDB 的行锁是针对索引字段加的锁,表级锁是针对非索引字段加的锁。当我们执行 更新或删除语句时,如果 WHERE
条件中字段没有命中唯一索引或者索引失效的话,就会导致扫描全表对表中的所有记录进行加锁。
共享锁和排他锁
不论是表级锁还是行级锁,都存在共享锁和排他锁这两类:
- 共享锁(S 锁) :又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
- 排他锁(X 锁) :又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁(锁不兼容)。
排他锁与任何的锁都不兼容,共享锁仅和共享锁兼容。
B+树索引
是MySQL 存储引擎的默认索引类型,便于查找两个值之间的多个元素
B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。
与B树的区别:
- 每个叶子结点都存有相邻叶子结点的指针
- 父节点存有右孩子的第一个元素的索引。
- 内部结点中的key都按照从小到大的顺序排列,对于内部结点中的一个key,左树中的所有key都小于它,右子树中的key都大于等于它。叶子结点中的记录也按照key的大小排列。
- B+树有两种类型的节点:内部结点(也称索引结点)和叶子结点。内部节点就是非叶子节点,内部节点不存储数据,只存储索引,数据都存储在叶子节点。
B+树与红黑树的比较:
红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B+ Tree 作为索引结构,主要有以下两个原因:
(一)更少的查找次数(红黑树的出度为 2,而 B+ Tree 的出度一般都非常大,所以红黑树的树高 h 很明显比 B+ Tree 大非常多,检索的次数也就更多)
(二)利用计算机预读特性(预读过程中,磁盘进行顺序读取,数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,相邻的节点也能够被预先载入)
大量数据排序的方法
先进行适当分批次,然后在批次内进行堆排序
数据库中查询的时间复杂度
使用二叉搜索树(BST),只需 log(N) 次运算,查找特定值好用
定义:若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 它的左、右子树也分别为二叉排序树
一条sql语句执行到结果经历了什么?
SQL优化方法
- 在实现功能的基础上,尽量减少对数据库的访问次数;
- 检查是否使用了索引
- 需要使用多个列作为条件查询时,使用多列索引比使用多个单列索引性能更好
- 尽量把使用的索引放在选择的首列,让选择性最强的索引列放在前面;
- 在查询时,不要过多地使用如SELECT * 语句;
- 尽量减少子查询,使用关联查询(left join,right join,inner join)替代
- 减少使用IN或者NOT IN ,使用exists,not exists或者关联查询语句替代,因为要对子查询的表进行全表扫描。
- 通过搜索参数,尽量减少对表的访问行数,最小化结果集,减轻网络负担;
- 能够分开的操作尽量分开处理,提高每次的响应速度;
- 算法的结构尽量简单;
- 在可能的情况下尽量限制结果集行数如:SELECT TOP 300 COL1,COL2,COL3 FROM T1,因为某些情况下用户是不需要那么多的数据的。
- or 的查询尽量用 union或者union all 代替,索引列上的or操作会造成全表扫描。union具有去重的操作,增加了计算时间。union all不需要去重,但会包含相同记录。
- 合理的增加冗余的字段(减少表的联接查询)
- 增加中间表进行优化(这个主要是在统计报表的场景,后台开定时任务将数据先统计好,尽量不要在查询的时候去统计)
- 建表的时候能使用数字类型的字段就使用数字类型(type,status…),数字类型的字段作为条件查询比字符串的快
索引失效情况
sort() 函数的工作原理
是归并排序,把问题拆分为小问题,通过解决小问题来解决最初的问题
mysql语句
- 创建数据库:create database 数据库名;
- 创建数据库时设置字符编码:create database 数据库名 character set utf8;默认是latin1 (单字节编码)
- 查看数据库信息:show create database 数据库名;
关系型数据库和非关系型数据库
关系型数据库:采用了关系模型来组织数据的数据库,其以行和列的形式存储数据,以便于用户理解,关系型数据库这一系列的行和列被称为表,一组表组成了数据库。用户通过查询来检索数据库中的数据,而查询是一个用于限定数据库中某些区域的执行代码。关系模型可以简单理解为二维表格模型,而一个关系型数据库就是由二维表及其之间的关系组成的一个数据组织。
常见的关系型数据库:mysql,oracle,SQL Server
存储方式:行存储,一个表里每一个对象的记录存储一行,一行里包括了该记录的所有特征
关系型优缺点以及应用场景:
优点:
1)复杂查询可以用SQL语句方便的在一个表以及多个表之间做非常复杂的数据查询。
2)事务支持使得对于安全性能很高的数据访问要求得以实现。
缺点:
1)不擅长大量数据的写入处理
2)不擅长为有数据更新的表做索引或表结构(schema)变更
3) 字段不固定时应用不方便
4)不擅长对简单查询需要快速返回结果的处理
使用场景:
1)需要做复杂处理的数据;
2)数据量不是特别大的数据;
3)对安全性要求高的数据;
4)数据格式单一的数据;
非关系型数据库
NoSQL,泛指非关系型的数据库。NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,尤其是大数据应用。
常见的非关系型数据库:
(1)键值对存储(key-value):Redis键值对存储,优势:快速查询,缺点:存储数据缺少结构化。
(2)列存储:Hbase,优势:快速查询,扩展性强。缺点:功能相对于局限。
(3)文档数据库存储:MongoDB,早起应用多。优势:要求不特别的严格。缺点:查询性不高,缺少统一查询语法。
(4)图形数据库存储:应用于社交网络,优势:利用图结构相关算法。缺点:需要整个图计算才得出结果,不容易做分布式集群方案。
存储方式:
以列为单位进行数据的存储,一列作为一个记录,每个对象的记录会存储多行,各行相对独立;
非关系优缺点以及使用场景
优点:
1)nosql数据库简单易部署,基本都是开源软件,不需要像使用oracle那样花费大量成本购买使用,相比关系型数据库价格便宜。
2)nosql数据库将数据存储于缓存之中,关系型数据库将数据存储在硬盘中,自然查询速度远不及nosql数据库。
3)nosql的存储格式是key,value形式、文档形式、图片形式等等,所以可以存储基础类型以及对象或者是集合等各种格式,而数据库则只支持基础类型。
4)不支持Join处理,各个数据都是独立设计的,很容易把数据分散在多个服务器上,故减少了每个服务器上的数据量,即使要处理大量数据的写入,也变得更加容易,数据的读入操作当然也同样容易。
缺点:
1)无法对表进行复杂的计算,不支持join等功能。
使用场景:
1)海量数据存储;
2)多格式的数据存储;
3)对查询速度要求快的数据存储;
https://blog.csdn.net/weixin_46043015/article/details/107896330
Tomcat
Spring
spring帮我们做了什么,spring mvc做了什么,分发,静态资源配置,aop,IOC,设计模式用了什么
轻量级框:Spring是轻量级框架,基本的版本大约2M
Spring Bean 的生命周期:实例化 -> 属性赋值 -> 初始化 -> 销毁
控制反转IOC
- 依赖:类之间的方法调用
- 依赖倒置:把调用类改为调用这个类的接口
- 依赖注入DI:就是如何获得请求的对象的,这个对象是怎样的注入到你的类的
- 控制反转:我想要用筷子(向容器发出请求),接着筷子就会”注入“到的手上,而在这个过程当中,你不再是控制方,反而演变成一名请求者(虽然本身还是调用者),依赖于容器给予你资源,控制权坐落到了容器身上
- Autowired和Resource是用来修饰字段,构造函数,或者设置方法,并做注入的,自动从spring的上下文找到合适的bean来注入。而Service,Controller,Repository,Component则是用来修饰类,标记这些类要生成bean。
面相切面的编程 AOP(动态代理)@Aspect
Spring支持面相切面的编程,并且把应用业务逻辑和系统分开,可以很方便的实现对程序进行权限拦截和运行监控等功能
- spring aop中@Around @Before @After三个注解的区别@Before是在所拦截方法执行之前执行一段逻辑。@After 是在所拦截方法执行之后执行一段逻辑。@Around是可以同时在所拦截方法的前后执行一段逻辑。
spring实现注解
是框架会扫描注解,通过反射方法读出注解,然后执行对应的方法
SpringMVC
为什么用SpringMVC?
很多应用程序的问题在于处理业务数据的对象和显示业务数据的视图之间存在紧密耦合,通常,更新业务对象的命令都是从视图本身发起的,使视图对任何业务对象更改都有高度敏感性。而且,当多个视图依赖同一个业务对象时是没有灵活性的。
SpringMVC基于java,实现了web MVC设计模式,请求驱动类型的轻量级web框架,使用了MVC架构模式思想,将web层进行职责解耦。基于请求驱动指的是使用请求-响应模型。
SpringMVC运行原理
- 客户端请求提交到DispatcherServlet(调度程序)
- 由DispatcherServlet控制器查询一个或多个HandlerMapping
- DispatcherServlet将请求提交到Controller
- Controller调用业务逻辑处理后,返回ModelAndView
- DispatcherServlet查询一个或多个ViewResoler视图解析器,找到ModelAndView指定的视图
- 视图负责将结果显示到客户端
SpringMVC缺点
依赖的jar需要一个个配置,非常繁琐
Spring Boot
Spring Boot:基于spring,为了解决使用spring框架时配置繁多、部署流程复杂、开发效率低等问题,可以创建独立的应用程序,嵌入tomcat、jetty等,可以直接启动应用程序而不需要外部的容器。同时,spring boot可以自动配置spring应用
springboot中典型的设计模式
- 工厂模式
- 最典型的就是BeanFactory,通过bean名称我们可以获取bean实例
- 代理模式
- AOP的实现本质即是代理模式,通过JDK或CGLIB新建一个类代理原来的类,在保证原来类的功能的基础上进行增强
网络
- tcp/ip协议
- http和https区别,优缺点,保证内容不被篡改,传输的都是密文,不担心被看到,80,https慢一些,有加密解密过程,端口号443,配置https
TCP和UDP的区别,使用场景?
• TCP面向连接,可靠传输,相对于UDP效率低,http的get,post等方法使用的就是TCP连接传输数据
• UDP没有连接,不可靠,但是传输速度快,用于视频会议等场合
TCP拥塞控制算法
TCP拥塞控制有四种算法:慢开始,拥塞避免,快重传,快恢复。
- 慢开始:以拥塞窗口cwnd=1为初始值,如果发送的包没有超时重传,则cwnd每次翻倍,直到cwnd>ssthresh(慢开始门限值初始值为ssthresh=16),慢开始结束,进入拥塞避免阶段。
- 拥塞避免:当cwnd>ssthresh时,采用拥塞避免算法控制。拥塞避免的过程为:cwnd=16,发送一个数据包,没有超时重传,cwnd=cwnd+1=16+1=17,再次发送数据包,依旧没有超时,cwnd=cwnd+1,以此类推,假设在cwnd=24时,发送的数据包出现超时重传,说明了网络出现拥塞,拥塞避免的算法为:把cwnd设置为1,ssthresh设置为cwnd/2=24/2=12,这样又以慢开始传输,当拥塞窗口大小超过慢开始门限,又采用拥塞避免控制。
- 快重传:如果接收方收不到数据包是因为数据包丢失,而不是网络拥塞,如果把cwnd设置为1,ssthresh设置为原来cwnd的一半,那网络传输的效率就会低很多。针对这一问题的解决方法是采用快重传算法,其核心是:如果接收方连续三次发送相同的确认包,发送方收到三次确认包后就认为该包丢失,于是重新发送丢失的包给接收端。
- 快恢复:当发送方连续收到三个重复确认时,把慢开始门限设置为cwnd/2(24/2 拥塞窗口值的一半),拥塞窗口的值设置为慢开始门先减半后的值,即cwnd=12,然后开始执行拥塞避免算法。
长连接&短连接
长连接:连接->传输数据->保持连接 -> 传输数据-> …->直到一方关闭连接,客户端关闭连接。
长连接指建立SOCKET连接后不管是否使用都保持连接,但安全性较差。
短连接:连接->传输数据->关闭连接。
比如HTTP是无状态的的短链接,浏览器和服务器每进行一次HTTP操作,就建立一次连接,但任务结束就中断连接。因为连接后接收了数据就断开了,所以每次数据接受处理不会有联系。这也是HTTP协议无状态的原因之一。
应用场景:
-
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。每个TCP都需要三次握手,处理速度会降低很多,所以每个操作完后都不断开,处理时直接发送数据包就OK了,不用建立TCP连接。
- 例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。
-
短连接:像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,资源情况和系统压力难以想象。
所以并发量大,但每个用户无需频繁操作情况下需用短连好。
TCP长&短链接
-
短链接:我们模拟一下TCP短连接的情况,client向server发起连接请求,server接到请求,然后双方建立连接。client向server发送消息,server回应client,然后一次读写就完成了,这时候双方任何一个都可以发起close操作,不过一般都是client先发起close操作。
- 短连接一般只会在client/server间传递一次读写操作
- 短连接的优点是:管理起来比较简单,存在的连接都是有用的连接,不需要额外的控制手段。
-
长链接:模拟一下长连接的情况,client向server发起连接,server接受client连接,双方建立连接。Client与server完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。
- 在长连接的应用场景下,client端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可以避免一些恶意连接导致server端服务受损;如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,这样可以完全避免某个蛋疼的客户端连累后端服务。
粘包拆包
粘包拆包场景
因为TCP是面向字节流的操作,没有边界,操作系统在发送TCP数据时,会通过缓冲区进行优化
粘包:如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送。
拆包:如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送
为什么UDP没有粘包?
粘包拆包问题在数据链路层、网络层以及传输层都有可能发生。日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。
常见的解决方案
- 发送端将每个包都封装成固定的长度,比如100字节大小。如果不足100字节可通过补0或空等进行填充到指定长度;
- 发送端在每个包的末尾使用固定的分隔符,例如\r\n。如果发生拆包需等待多个包发送过来之后再找到其中的\r\n进行合并;例如,FTP协议;
- 将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息;
- 通过自定义协议进行粘包和拆包的处理。
Netty对粘包和拆包的处理
Netty对解决粘包和拆包的方案做了抽象,提供了一些解码器(Decoder)来解决粘包和拆包的问题。如:
- LineBasedFrameDecoder:以行为单位进行数据包的解码;
- DelimiterBasedFrameDecoder:以特殊的符号作为分隔来进行数据包的解码;
- FixedLengthFrameDecoder:以固定长度进行数据包的解码;
- LenghtFieldBasedFrameDecode:适用于消息头包含消息长度的协议(最常用,适用高并发大流量);
- 1xx-信息提示
- 2xx-成功标志
- 3xx-重定向
- 4xx-客户端错误
- 5xx服务端错误
请求头Header中怎么清除信息?
可以扩展HttpServletRequestWrapper并覆盖getHeaderNames();,其中您可以返回另一个枚举,只添加了必需的标题。
设计模式
单例
单例怎么避免反射构造对象:增加一个标识位,如通过增加一个布尔类型的 ideal 标识,保证只会执行一次,更安全的做法,可以进行加密处理,保证其安全性
Linux
linux基本命令,加分项是线上确定日志确定问题在哪里,项目日志文件里面的,vim的命令,查找关键信息
grep “” *.log
tail -200 *.log
tail -200f *.log
tail -200 *.log | grep -a3 -b3 “”
ps -ef | grep “java”
架构
log4j2,配置日志
扩展
服务器降级
降级的目的是为了保证核心服务可用
降级可以有几个层面的分类:自动降级,人工降级;按照功能可以分为:读服务降级和写服务降级;
1.对一些非核心服务进行人工降级,在大促之前通过降级开关关闭那些推荐内容,评价等对主流程序没有影响的功能
2.故障降级,比如调用的远程服务挂了,网络故障,或者RPC服务返回异常。那么可以直接降级,降级的方案比如设置默认值,采用兜底数据(系统推荐的行为广告挂了,可以提前准备静态页面做返回)等等
3.限流降级,在秒杀这种流量比较集中并且流量特别大的情况下,因为突发访问量特别大可能导致系统支撑不了。这个时候可以采用限流来限制访问量。当达到阈值时,后续的请求被降级,比如进入排队页面,比如跳转到错误页面(活动火爆,请稍后重试)
秒杀思路
-
秒杀系统与原有电商系统分开部署
-
商品页面 进行静态化缓存,只访问服务器中商品的动态数据
- 读写分离是用来解决数据库的读性能瓶颈的(主从集群)
- 但是解决读性能问题首选缓存,因为读写分离存在(主从一致性;如何实现故障自动转移)问题
- 商品列表页 进行缓存
-
下订单时,查看是否重复下单,查看redis,key为userId,value为用户下单的商品id集合
- 如果已经买过,就不让在买
- 如果没有买过,检查redis缓存的商品库存是否为0
- 如果库存不为0,则在redis中预减库存,然后把购买请求加入到rabbitmq中,把并发请求串行化
- 如果库存为0,则告诉用户卖完了
依据个人认知总结,如有技术性问题烦请指正!