Java招聘要求

Java原理

Java面向对象三大特征

继承、封装、多态

基本数据类型

整型(byte、short、int、long)、浮点型(float、double)、字符型(char)、布尔型(boolean)

  • byte 占用 1 个字节,short 占用 2 个字节,int 占用 4 个字节,long 占用 8 个字节

  • float 占用 4 个字节,double 占用 8 个字节

  • char 占用 2 个字节

  • boolean 占用 1 个字节。

== 和 equals

equals 和== 最大的区别是一个是方法一个是运算符

  • ==:如果比较的对象是基本数据类型,则比较的是数值是否相等;如果比较的是引用数据类型,则比较的是对象的地址值是否相等。
  • equals():用来比较方法两个对象的内容是否相等

hashcode和equals

equals()源自于java.lang.Object,该方法用来简单验证两个对象的相等性。

  • Object类中定义的默认实现只检查两个对象的对象引用,以验证它们的相等性。
  • 通过重写该方法,可以自定义验证对象相等新的规则,如果你使用ORM处理一些对象的话,你要确保在hashCode()和equals()对象中使用getter和setter而不是直接引用成员变量

hashCode()源自于java.lang.Object ,该方法用于获取给定对象的唯一的整数(散列码)。

  • 当这个对象需要存储在哈希表这样的数据结构时,这个整数用于确定桶的位置。
  • 默认情况下,对象的hashCode()方法返回对象所在内存地址的整数表示。
  • hashCode()是HashTable、HashMap和HashSet使用的。
  • 默认的,Object类的hashCode()方法返回这个对象存储的内存地址的编号。

Integer 存储机制

-128 ~ 127

JDK1.8新特性

lambda表达式

函数式接口

重写和重载的区别

方法的重载和重写都是实现多态的方式,区别在于重载实现的是编译时的多态性,而重写实现的是运行时的多态

  • 重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;
  • 重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求。

方法重载的规则:

1.方法名一致,参数列表中参数的顺序,类型,个数不同。

2.重载与方法的返回值无关,存在于父类和子类,同类中。

3.可以抛出不同的异常,可以有不同修饰符

方法重写的规则:

1.参数列表必须完全与被重写方法的一致,返回类型必须完全与被重写方法的返回类型一致。

2.构造方法不能被重写,声明为 final 的方法不能被重写,声明为 static 的方法不能被重写,但是能够被再次

声明。

3.访问权限不能比父类中被重写的方法的访问权限更低。

4.重写的方法能够抛出任何非强制异常(UncheckedException,也叫非运行时异常),无论被重写的方法是

否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则

可以。

接口和抽象类的区别

抽象类:自下从上

1.抽象类中可以定义构造器

2.可以有抽象方法和具体方法

3.接口中的成员全都是 public 的

4.抽象类中可以定义成员变量

5.有抽象方法的类必须被声明为抽象类,而抽象类未必要有抽象方法

6.抽象类中可以包含静态方法

7.一个类只能继承一个抽象类

接口:自上从下

1.接口中不能定义构造器

2.方法全部都是抽象方法

3.抽象类中的成员可以是 private、默认、protected、public

4.接口中定义的成员变量实际上都是常量

5.接口中不能有静态方法

6.一个类可以实现多个接口

相同点:

1.不能够实例化

2.可以将抽象类和接口类型作为引用类型

3.一个类如果继承了某个抽象类或者实现了某个接口都需要对其中的抽象方法全部进行实现,否则该类仍然需要

被声明为抽象类

怎样声明一个类不会被继承

如果一个类被final修饰,此类不可以有子类,不能被其它类继承,如果一个中的所有方法都没有重写的需要,当前类没有子类也罢,就可以使用final修饰类。

String、StringBuffer、StringBuilder

String 是只读字符串,也就意味着 String 引用的字符串内容是不能被改变的。

StringBuffer/StringBuilder 表示的字符串对象可以直接进行修改,buffer是线程安全的,builder的效率更高

Java代理

静态代理,只能静态的代理某些类或者某些方法,不推荐使用,功能比较弱,但是编码简单

动态代理,包含Proxy代理和CGLIB动态代理

Proxy代理

Proxy代理是JDK内置的动态代理

  • 特点:面向接口的,不需要导入三方依赖的动态代理,可以对多个不同的接口进行增强,通过反射读取注解时,只能读取到接口上的注解
  • 原理:面向接口,只能对实现类在实现接口中定义的方法进行增强

CGLIB代理

  • 特点:面向父类的动态代理,需要导入第三方依赖
  • 原理:面向父类,底层通过子类继承父类并重写方法的形式实现增强

JVM

内存模型

1.7 和 1.8的区别

  1. 1.8引入了新的内存模型,称为“内存屏障”。可以更好的支持多处理器系统
  2. 1.8引入新的原子操作,可以更好的支持多线程编程
  3. 1.8引入新的volatile关键字,更好的支持多线程编程
  4. 1.8支持更多的处理器架构,可以更好的支持多处理器系统

JVM常用参数

Xms :是指设定程序启动时占用内存大小。

Xmx :是指设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常

Xss :是指设定每个线程的堆栈大小。

**-Xmn、-XX:NewSize/-XX:MaxNewSize、-XX:NewRatio **

  1. 高优先级:-XX:NewSize/-XX:MaxNewSize
  2. 中优先级:-Xmn(默认等效 -Xmn=-XX:NewSize=-XX:MaxNewSize=?)
  3. 低优先级:-XX:NewRatio

如果想在日志中追踪类加载与类卸载的情况,可以使用启动参数 **-XX:TraceClassLoading -XX:TraceClassUnloading **

JVM性能调优

调优原则

  1. 多数的Java应用不需要在服务器上进行GC优化,虚拟机内部已有很多优化来保证应用的稳定运行,所以不要为了调优而调优,不当的调优可能适得其反
  2. 在应用上线之前,先考虑将机器的JVM参数设置到最优(适合)
  3. 在进行GC优化之前,需要确认项目的架构和代码等已经没有优化空间。我们不能指望一个系统架构有缺陷或者代码层次优化没有穷尽的应用,通过GC优化令其性能达到一个质的飞跃
  4. GC优化是一个系统而复杂的工作,没有万能的调优策略可以满足所有的性能指标。GC优化必须建立在我们深入理解各种垃圾回收器的基础上,才能有事半功倍的效果
  5. 处理吞吐量和延迟问题时,垃圾处理器能使用的内存越大,即java堆空间越大垃圾收集效果越好,应用运行也越流畅。这称之为GC内存最大化原则
  6. 在这三个属性(吞吐量、延迟、内存)中选择其中两个进行jvm调优,称之为GC调优3选2

什么情况下需要JVM调优

  • Heap内存(老年代)持续上涨达到设置的最大内存值
  • Full GC 次数频繁
  • GC 停顿(Stop World)时间过长(超过1秒,具体值按应用场景而定)
  • 应用出现OutOfMemory 等内存异常
  • 应用出现OutOfDirectMemoryError等内存异常( failed to allocate 16777216 byte(s) of direct memory (used: 1056964615, max: 1073741824))
  • 应用中有使用本地缓存且占用大量内存空间
  • 系统吞吐量与响应性能不高或下降
  • 应用的CPU占用过高不下或内存占用过高不下

JVM调优指标

吞吐量:用户代码时间 / (用户代码执行时间 + 垃圾回收时间)。

评价垃圾收集器能力的重要指标之一,是不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支撑应用程序达到的最高性能指标。吞吐量越高算法越好

低延迟: STW越短,响应时间越好。评价垃圾收集器能力的重要指标,度量标准是缩短由于垃圾收集引起的停顿时间或完全消除因垃圾收集所引起的停顿,避免应用程序运行时发生抖动。暂停时间越短算法越好

在设计(或使用)GC 算法时,我们必须确定我们的目标:一个 GC 算法只可能针对两个目标之一(即只专注于最大吞吐量或最小暂停时间),或尝试找到一个二者的折中

堆大小调整的着手点、分析点:

  1. 统计Minor GC 持续时间
  2. 统计Minor GC 的次数
  3. 统计Full GC的最长持续时间
  4. 统计最差情况下Full GC频率
  5. 统计GC持续时间和频率对优化堆的大小是主要着手点
  6. 我们按照业务系统对延迟和吞吐量的需求,在按照这些分析我们可以进行各个区大小的调整

线上调优

线上排查问题的一般流程

  • CPU占用过高排查流程
  1. 利用 top 命令可以查出占 CPU 最高的的进程pid ,如果pid为 9876
  2. 然后查看该进程下占用最高的线程id【top -Hp 9876】
  3. 假设占用率最高的线程 ID 为 6900,将其转换为 16 进制形式 (因为 java native 线程以 16 进制形式输出) 【printf ‘%x\n’ 6900】
  4. 利用 jstack 打印出 java 线程调用栈信息【jstack 9876 | grep ‘0x1af4’ -A 50 –color】,这样就可以更好定位问题
  • 内存占用过高排查流程
  1. 查找进程id: 【top -d 2 -c】
  2. 查看JVM堆内存分配情况:jmap -heap pid
  3. 查看占用内存比较多的对象 jmap -histo pid | head -n 100
  4. 查看占用内存比较多的存活对象 jmap -histo:live pid | head -n 100

什么情况下抛出OOM

  • JVM**98%**的时间都花费在内存回收
  • 每次回收的内存小于2%

满足这两个条件将触发OutOfMemoryException,这将会留给系统一个微小的间隙以做一些Down之前的操作,比如手动打印Heap Dump。并不是内存被耗空的时候才抛出

系统OOM预兆

  • 每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
  • FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
  • 老年代的内存越来越大并且每次FullGC后,老年代只有少量的内存被释放掉

堆Dump文件分析

通过指定启动参数

1
2
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/usr/app/data/dump/heapdump.hpro

在发生OOM的时候自动导出Dump文件

GC日志分析

指定启动参数 【**-Xloggc: app-gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps**】,方便详细地查看GC日志信息

  1. 使用 【jinfo pid】查看当前JVM堆的相关参数
  2. 继续使用 【jstat -gcutil 2315 1s 10】查看10s内当前堆的占用情况
  3. 也可以使用【jmap -heap pid】查看当前JVM堆的情况
  4. 我们可以继续使用 【jmap -F -histo pid | head -n 20】,查看前20行打印,即查看当前top20的大对象,一般从这里可以发现一些异常的大对象,如果没有,那么可以继续排名前50的大对象,分析
  5. 最后使用【jmap -F -dump:file=a.bin pid】,如果dump文件很大,可以压缩一下【tar -czvf a.tar.gz a.bin】
  6. 再之后,就是对dump文件进行分析了,使用MAT分析内存泄露

排查线上死锁

  1. jps 查找一个可能有问题的进程id
  2. 然后执行 【jstack -F 进程id
  3. 如果环境允许远程连接JVM,可以使用jconsole或者jvisualvm,图形化界面检测是否存在死锁

GC

线上YGC耗时过长优化方案

  1. 如果生命周期过长的对象越来越多(比如全局变量或者静态变量等),会导致标注和复制过程的耗时增加
  2. 对存活对象标注时间过长:比如重载了Object类的Finalize方法,导致标注Final Reference耗时过长;或者String.intern方法使用不当,导致YGC扫描StringTable时间过长。可以通过以下参数显示GC处理Reference的耗时-XX:+PrintReferenceGC
  3. 长周期对象积累过多:比如本地缓存使用不当,积累了太多存活对象;或者锁竞争严重导致线程阻塞,局部变量的生命周期变长

线上频繁FullGC优化方案

线上频繁FullGC一般会有这么几个特征:

  1. 线上多个线程的CPU都超过了100%,通过jstack命令可以看到这些线程主要是垃圾回收线程
  2. 通过jstat命令监控GC情况,可以看到Full GC次数非常多,并且次数在不断增加

排查流程:

  1. top找到cpu占用最高的一个 进程id
  2. 然后 【top -Hp 进程id】,找到cpu占用最高的 线程id
  3. 【printf “%x\n” 线程id 】,假设16进制结果为 a
  4. jstack 线程id | grep ‘0xa’ -A 50 –color
  5. 如果是正常的用户线程, 则通过该线程的堆栈信息查看其具体是在哪处用户代码处运行比较消耗CPU
  6. 如果该线程是 VMThread,则通过 jstat-gcutil命令监控当前系统的GC状况,然后通过 jmapdump:format=b,file=导出系统当前的内存数据。导出之后将内存情况放到eclipse的mat工具中进行分析即可得出内存中主要是什么对象比较消耗内存,进而可以处理相关代码;正常情况下会发现VM Thread指的就是垃圾回收的线程
  7. 再执行【jstat -gcutil **进程id】, **看到结果,如果FGC的数量很高,且在不断增长,那么可以定位是由于内存溢出导致FullGC频繁,系统缓慢
  8. 然后就可以Dump出内存日志,然后使用MAT的工具分析哪些对象占用内存较大,然后找到对象的创建位置,处理即可

线上堆外内存泄漏的分析

  1. JVM的堆外内存泄露的定位一直是个比较棘手的问题
  2. 对外内存的泄漏分析一般都是先从堆内内存分析的过程中衍生出来的。有可能我们分析堆内内存泄露过程中发现,我们计算出来的JVM堆内存竟然大于了整个JVM的Xmx的大小,那说明多出来的是堆外内存
  3. 如果使用了 Netty 堆外内存,那么可以自行监控堆外内存的使用情况,不需要借助第三方工具,我们是使用的“反射”拿到的堆外内存的情况
  4. 逐渐缩小范围,直到 Bug 被找到。当我们确认某个线程的执行带来 Bug 时,可单步执行,可二分执行,定位到某行代码之后,跟到这段代码,然后继续单步执行或者二分的方式来定位最终出 Bug 的代码。这个方法屡试不爽,最后总能找到想要的 Bug
  5. 熟练掌握 idea 的调试,让我们的“捉虫”速度快如闪电(“闪电侠”就是这么来的)。这里,最常见的调试方式是预执行表达式,以及通过线程调用栈,死盯某个对象,就能够掌握这个对象的定义、赋值之类
  6. 在使用直接内存的项目中,最好建议配置 -XX:MaxDirectMemorySize,设定一个系统实际可达的最大的直接内存的值,默认的最大直接内存大小等于 -Xmx的值
  7. 排查堆外泄露,建议指定启动参数: -XX:NativeMemoryTracking=summary - Dio.netty.leakDetection.targetRecords=100-Dio.netty.leakDetection.level=PARANOID,后面两个参数是Netty的相关内存泄露检测的级别与采样级别

内存溢出的原因有哪些,如何排查线上问题?

  1. java.lang.OutOfMemoryError: ……java heap space…..   堆栈溢出,代码问题的可能性极大
  2. java.lang.OutOfMemoryError: GC over head limit exceeded 系统处于高频的GC状态,而且回收的效果依然不佳的情况,就会开始报这个错误,这种情况一般是产生了很多不可以被释放的对象,有可能是引用使用不当导致,或申请大对象导致,但是java heap space的内存溢出有可能提前不会报这个错误,也就是可能内存就直接不够导致,而不是高频GC.
  3. java.lang.OutOfMemoryError: PermGen space jdk1.7之前才会出现的问题 ,原因是系统的代码非常多或引用的第三方包非常多、或代码中使用了大量的常量、或通过intern注入常量、或者通过动态代码加载等方法,导致常量池的膨胀
  4. java.lang.OutOfMemoryError: Direct buffer memory    直接内存不足,因为jvm垃圾回收不会回收掉直接内存这部分的内存,所以可能原因是直接或间接使用了ByteBuffer中的allocateDirect方法的时候,而没有做clear
  5. java.lang.StackOverflowError -     Xss设置的太小了
  6. java.lang.OutOfMemoryError: unable to create new native thread 堆外内存不足,无法为线程分配内存区域
  7. java.lang.OutOfMemoryError: request {} byte for {}out of swap 地址空间不够

元空间

JVM8为什么要增加元空间

  1. 字符串存在永久代中,容易出现性能问题和内存溢出。
  2. 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出。
  3. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低。

JVM8中元空间特点

  1. 每个加载器有专门的存储空间。
  2. 不会单独回收某个类。
  3. 元空间里的对象的位置是固定的。
  4. 如果发现某个加载器不再存货了,会把相关的空间整个回收

线上元空间内存泄露优化方案

  1. 需要注意的一点是 Java8以及Java8+的JVM已经将永久代废弃了,取而代之的是元空间,且元空间是不是在JVM堆中的,而属于堆外内存,受最大物理内存限制。最佳实践就是我们在启动参数中最好设置上 -XX:MetaspaceSize=1024m -XX:MaxMetaspaceSize=1024m。具体的值根据情况设置。为避免动态申请,可以直接都设置为最大值
  2. 元空间主要存放的是类元数据,而且metaspace判断类元数据是否可以回收,是根据加载这些类元数据的Classloader是否可以回收来判断的,只要Classloader不能回收,通过其加载的类元数据就不会被回收。所以线上有时候会出现一种问题,由于框架中,往往大量采用类似ASM、javassist等工具进行字节码增强,生成代理类。如果项目中由主线程频繁生成动态代理类,那么就会导致元空间迅速占满,无法回收

GC如何判断对象可以被回收

  • 引用计数法(已被淘汰的算法)
    • 每一个对象有一个引用属性,新增一个引用时加一,引用释放时减一,计数为0的时候可以回收。

但是这种计算方法,有一个致命的问题,无法解决循环引用的问题

  • 可达性分析算法(根引用)
  1. 从GcRoot开始向下搜索,搜索所走过的路径被称为引用链,当一个对象到GcRoot没有任何引用链相连时,则证明此对象是不可用的,那么虚拟机就可以判定回收。
  2. 那么GcRoot有哪些?
    1. 虚拟机栈中引用的对象
    2. 方法区中静态属性引用的对象。
    3. 方法区中常量引用的对象
    4. 本地方法栈中(即一般说的native方法)引用的对象

此外,不同的引用类型的回收机制是不一样的

  1. 强引用:通过关键字new的对象就是强引用对象,强引用指向的对象任何时候都不会被回收,宁愿OOM也不会回收。
  2. 软引用:如果一个对象持有软引用,那么当JVM堆空间不足时,会被回收。一个类的软引用可以通过java.lang.ref.SoftReference持有。
  3. 弱引用:如果一个对象持有弱引用,那么在GC时,只要发现弱引用对象,就会被回收。一个类的弱引用可以通过java.lang.ref.WeakReference持有。
  4. 虚引用:几乎和没有一样,随时可以被回收。通过PhantomReference持有。

GC回收算法

标记-清除(Mark-Sweep)算法

分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。

它的主要不足有两个:

  • 效率问题,标记和清除两个过程的效率都不高。
  • 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

为了解决效率问题,一种称为复制(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

复制算法的代价是将内存缩小为了原来的一半,减少了实际可用的内存。

标记-整理(Mark-Compact)算法

标记-整理(Mark-Compact)算法标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清理或者标记—整理算法来进行回收。

垃圾回收器

先解释几个名词:

  1. 并行(Parallel):多个垃圾收集线程并行工作,此时用户线程处于等待状态
  2. 并发(Concurrent):用户线程和垃圾收集线程同时执行
  3. 吞吐量:运行用户代码时间/(运行用户代码时间+垃圾回收时间)

新生代收集器(全部的都是复制算法):Serial、ParNew、Parallel Scavenge

  • Serial收集器单线程、简单高效(与其他收集器的单线程相比)适用于Client模式下的虚拟机
  • ParNew收集器是Serial收集器的多线程版本
  • Parallel Scavenge 收集器与吞吐量关系密切,故也称为吞吐量优先收集器,与ParNew收集器最重要的一个区别是 GC自适应调节策略

老年代收集器:CMS(标记-清理)、Serial Old(标记-整理)、Parallel Old(标记整理)

  • CMS收集器是一种以获取最短回收停顿时间为目标的收集器,适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如web程序、b/s服务。
  • Serial Old是Serial收集器的老年代版本,同样是单线程收集器
  • Parallel Old是Parallel Scavenge收集器的老年代版本,注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge+Parallel Old 收集器

整堆收集器: G1(一个Region中是标记-清除算法,2个Region之间是复制算法)

  • G1收集器一款面向服务端应用的垃圾收集器

垃圾回收器选择及调优

总体原则:减少STOP THE WORD时间,使用并发收集器(比如CMS+ParNew,G1)来减少暂停时间,加快响应时间,并使用并行收集器来增加多处理器硬件上的总体吞吐量。

除非应用程序有非常严格的暂停时间要求,否则请先运行应用程序并允许VM选择收集器(如果没有特别要求。使用VM提供给的默认GC就好)。
如有必要,调整堆大小以提高性能。 如果性能仍然不能满足目标,请使用以下准则作为选择收集器的起点:

  • 如果应用程序的数据集较小(最大约100 MB),则选择带有选项-XX:+ UseSerialGC的串行收集器。
  • 如果应用程序将在单个处理器上运行,并且没有暂停时间要求,则选择带有选项-XX:+ UseSerialGC的串行收集器。
  • 如果(a)峰值应用程序性能是第一要务,并且(b)没有暂停时间要求或可接受一秒或更长时间的暂停,则让VM选择收集器或使用-XX:+ UseParallelGC选择并行收集器 。
  • 如果响应时间比整体吞吐量更重要,并且垃圾收集暂停时间必须保持在大约一秒钟以内,则选择具有-XX:+ UseG1GC。(值得注意的是JDK9中CMS已经被Deprecated,不可使用!移除该选项)
  • 如果使用的是jdk8,并且堆内存达到了16G,那么推荐使用G1收集器,来控制每次垃圾收集的时间。
  • 如果响应时间是高优先级,或使用的堆非常大,请使用-XX:UseZGC选择完全并发的收集器。(值得注意的是JDK11开始可以启动ZGC,但是此时ZGC具有实验性质,在JDK15中[202009发布]才取消实验性质的标签,可以直接显示启用,但是JDK15默认GC仍然是G1)

这些准则仅提供选择收集器的起点,因为性能取决于堆的大小,应用程序维护的实时数据量以及可用处理器的数量和速度。
如果推荐的收集器没有达到所需的性能,则首先尝试调整堆和新生代大小以达到所需的目标。 如果性能仍然不足,再尝试使用其他收集器

Java类加载器

Bootstrap类加载器

启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,没有父类,是虚拟机自身的一部分,它负责将 /lib路径下的核心类库或**-Xbootclasspath参数指定的路径下的jar包**加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头

Extention 类加载器

扩展类加载器是指Sun公司实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,父类加载器为null,是Launcher的静态内部类,它负责加载**/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库**,开发者可以直接使用标准扩展类加载器
(https://blog.csdn.net/javazejian/article/details/73413292)

Application类加载器

又称应用程序加载器,是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。父类加载器为ExtClassLoader,它负责加载系统类路径java -classpath或**-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径**,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器

Custom自定义类加载器

应用程序可以自定义类加载器,父类加载器为AppClassLoader

image.png

类加载机制

Java虚拟机在运行时 加载、连接、初始化类 的过程

类加载机制主要由 类加载器、链接器 和 初始化器组成

  • 类加载器负责从指定位置加载类的二进制字节码
  • 链接器负责将字节码装换为虚拟机运行的机器码
  • 初始化器负责对类的静态变量和静态代码块进行初始化

双亲委派机制

双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载

双亲委派的好处

  • 每一个类都只会被加载一次,避免了重复加载
  • 每一个类都会被尽可能的加载(从引导类加载器往下,每个加载器都可能会根据优先次序尝试加载它)
  • 有效避免了某些恶意类的加载(比如自定义了Java.lang.Object类,一般而言在双亲委派模型下会加载系统的Object类而不是自定义的Object类)

如何破坏双亲委派模型

  1. 双亲委派模型的第一次“被破坏”是重写自定义加载器的loadClass(),jdk不推荐。一般都只是重写findClass(),这样可以保持双亲委派机制.而loadClass方法加载规则由自己定义,就可以随心所欲的加载类
  2. 双亲委派模型的第二次“被破坏”是ServiceLoader和Thread.setContextClassLoader()。即线程上下文类加载器(contextClassLoader)。双亲委派模型很好地解决了各个类加载器的基础类统一问题(越基础的类由越上层的加载器进行加载),基础类之所以被称为“基础”,是因为它们总是作为被调用代码调用的API。但是,如果基础类又要调用用户的代码,那该怎么办呢?线程上下文类加载器就出现了。
    1. SPI。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器。有了线程上下文类加载器,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等。
    2. 线程上下文类加载器默认情况下就是AppClassLoader,那为什么不直接通过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?其实是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为这些服务使用的线程上下文类加载器并非AppClassLoader,而是Java Web应用服自家的类加载器,类加载器不同。,所以我们应用该少用getSystemClassLoader()。总之不同的服务使用的可能默认ClassLoader是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader,从而避免不必要的问题
  3. 双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。

三级缓存

在 Spring 框架中,Bean 的创建涉及到 BeanDefinition 的解析、实例化、依赖注入等多个步骤,为了提高性能,Spring 使用了三级缓存来缓存 BeanDefinition 和 Bean 的相关信息。

具体而言,Spring 的三级缓存包括:

  1. singletonObjects 缓存:缓存已经创建完成的单例 Bean 实例。当 Bean 创建完成后,它会被缓存到 singletonObjects 中,以便后续使用。
  2. earlySingletonObjects 缓存:缓存正在创建过程中的单例 Bean 实例。当 Bean 创建过程中需要使用到自身实例时,它会被缓存到 earlySingletonObjects 中,以便后续使用。当 Bean 的创建完成后,它会从 earlySingletonObjects 中移除并放入 singletonObjects 缓存中。
  3. singletonFactories 缓存:缓存创建 Bean 实例的工厂对象。当 Bean 的创建过程中需要使用到自身实例时,它会先从 singletonFactories 缓存中获取对应的 Bean 工厂,然后使用该工厂创建 Bean 实例,并将实例缓存到 earlySingletonObjects 缓存中。

使用三级缓存可以提高 Bean 创建的效率和性能,避免重复创建和依赖注入,同时还可以支持循环依赖的处理。

IO

NIO

BIO

AIO

JWT

JWT 本质上就一段签名的 JSON 格式的数据。由于它是带有签名的,因此接收者便可以验证它的真实性。

json

web

token

流传输

Stream

JUC

juc参考文档

什么是JUC

JUC:指的是java.util三个并发编程工具包

  1. java.util.concurrent
  2. java.util.concurrent.atomic
  3. java.util.concurrent.locks

线程和进程

  • 进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程;

    • 进程——资源分配的最小单元
  • 线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。

    • 线程——程序执行的最小单元

并发和并行

并发: 同一时刻多个线程访问同一个资源(多线程共享资源)

例如:春运抢票、电商秒杀

并行: 多项工作一起执行,之后再汇总

例如:泡方便面,电水壶烧水的同时,拆开泡面调料倒入桶中

线程基本知识

线程创建方式

  1. 继承Thread类创建线程
  2. 实现Runnable接口创建线程
  3. 使用Callable和Future创建线程
  4. 使用线程池例如用Executor框架

继承Thread类创建线程

  1. 定义Thread类的子类,并重写该类的run()方法,该run()方法将作为线程执行体
  2. 创建Thread子类的实例,即创建线程对象
  3. 调用线程对象的start()方法启动线程

Runnable接口创建线程

  1. 定义Runnable接口的实现类,并实现该接口的run()方法,该run()方法将作为线程执行体
  2. 创建Runnable实现类的实例,并将其作为Thread的target来创建Thread对象,Thread对象即为线程对象
  3. 调用线程对象的start()方法启动线程

使用Callable和Future创建线程

  1. 定义Callable接口的实现类,并实现该接口的call()方法,该call()方法将作为线程执行体,且该call()方法有返回值
  2. 创建Callable实现类的实例
  3. 使用FutureTask类包装Callable对象,该FutureTask对象封装了该Callable对象的call()方法的返回值
  4. 使用FutureTask对象作为Thread对象的target,创建并启动线程
  5. 调用FutureTask对象的get()方法来获得子线程执行结果后的返回值

创建多线程方式的优缺点

  • Runnable接口、Callable接口
    • 优点:线程类还可以继承其他类,多线程可共享同一个target对象
    • 缺点:编码复杂,如果需要访问当前线程,必须使用 Thread.currentThread() 方法
  • Thread类
    • 优点:编码简单,如果需要访问当前线程,直接使用this即可获取当前线程
    • 缺点:线程类是 继承 Thread类,因此线程类不能再继承其他父类

线程的生命周期

未命名文件.jpg

1.创建
当程序使用new关键字创建了一个线程之后,该线程就处于一个新建状态(初始状态),此时它和其他Java对象一样,仅仅由Java虚拟机为其分配了内存,并初始化了其成员变量值。此时的线程对象没有表现出任何线程的动态特征,程序也不会执行线程的线程执行体。
2.就绪
当线程对象调用了Thread.start()方法之后,该线程处于就绪状态。Java虚拟机会为其创建方法调用栈和程序计数器,处于这个状态的线程并没有开始运行,它只是表示该线程可以运行了。从start()源码中看出,start后添加到了线程列表中,接着在native层添加到VM中,至于该线程何时开始运行,取决于JVM里线程调度器的调度(如果OS调度选中了,就会进入到运行状态)。
3.运行
当线程对象调用了Thread.start()方法之后,该线程处于就绪状态。添加到了线程列表中,如果OS调度选中了,就会进入到运行状态
4.阻塞
阻塞状态是线程因为某种原因放弃CPU使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况大概三种:

  • 1、等待阻塞:运行的线程执行wait()方法,JVM会把该线程放入等待池中。(wait会释放持有的锁)
  • 2、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则JVM会把该线程放入锁池中。
  • 3、其他阻塞:运行的线程执行sleep()或join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。(注意,sleep是不会释放持有的锁)。
  • 线程睡眠:Thread.sleep(long millis)方法,使线程转到阻塞状态。millis参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep()平台移植性好。
  • 线程等待:Object类中的wait()方法,导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 唤醒方法。这个两个唤醒方法也是Object类中的方法,行为等价于调用 wait(0) 一样。唤醒线程后,就转为就绪(Runnable)状态。
  • 线程让步:Thread.yield() 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。
  • 线程加入:join()方法,等待其他线程终止。在当前线程中调用另一个线程的join()方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。
  • 线程I/O:线程执行某些IO操作,因为等待相关的资源而进入了阻塞状态。比如说监听system.in,但是尚且没有收到键盘的输入,则进入阻塞状态。
  • 线程唤醒:Object类中的notify()方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程,选择是任意性的,并在对实现做出决定时发生。类似的方法还有一个notifyAll(),唤醒在此对象监视器上等待的所有线程。

5.死亡
线程会以以下三种方式之一结束,结束后就处于死亡状态:

  • run()方法执行完成,线程正常结束。
  • 线程抛出一个未捕获的Exception或Error。
  • 直接调用该线程的stop()方法来结束该线程——该方法容易导致死锁,通常不推荐使用

线程的sleep、wait、join、yield

  • sleep:让线程睡眠,期间会出让cpu,在同步代码块中,不会释放锁,是Thread的静态方法
  • wait(必须先获得对应的锁才能调用):让线程进入等待状态,释放当前线程持有的锁资源,线程只有在notify 或者notifyAll方法调用后才会被唤醒,然后去争夺锁,任何对象实例都能调用
  • join:线程之间协同方式,使用场景: 线程A必须等待线程B运行完毕后才可以执行,那么就可以在线程A的代码中加入ThreadB.join();
  • yield:让当前正在运行的线程回到可运行状态,以允许具有相同优先级的其他线程获得运行的机会。因此,使用yield()的目的是让具有相同优先级的线程之间能够适当的轮换执行。但是,实际中无法保证yield()达到让步的目的,因为,让步的线程可能被线程调度程序再次选中。

线程的run()和start()

  • run方法是线程执行体,代表了线程所需要完成的任务
  • start方法单纯是用来启动线程
  • 直接调用start方法启动线程时,系统会将run方法作为线程执行体进行处理
  • 直接调用线程对象的run方法,系统会将线程对象当做一个普通对象处理

守护线程

在Java中有两类线程:User Thread(用户线程)、Daemon Thread(守护线程)

任何一个守护线程都是整个JVM中所有非守护线程的保姆

只要当前JVM实例中尚存在任何一个非守护线程没有结束,守护线程就全部工作;只有当最后一个非守护线程结束时,守护线程随着JVM一同结束工作。

Daemon的作用是为其他线程的运行提供便利服务,守护线程最典型的应用就是 GC (垃圾回收器),它就是一个很称职的守护者。

注意事项:
(1) thread.setDaemon(true)必须在thread.start()之前设置,否则会出现一个IllegalThreadStateException异常。只能在线程未开始运行之前设置为守护线程。
(2) 在Daemon线程中产生的新线程也是Daemon的。
(3) 不要认为所有的应用都可以分配给Daemon来进行读写操作或者计算逻辑,因为这会可能回到数据不一致的状态。

ThreadLocal

ThreadLocal原理

Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类

在默认情况下,每个线程中的这两个变量都为null

ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的载体,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。

如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量,防止出现内存泄漏。

如何避免ThreadLocal内存泄露问题

每个Thread都有一个ThreadLocal,当ThreadLocal的key == null时,GC就会回收这部分空间,但是value却不一定能够被回收,因为他还与Current Thread存在一个强引用关系

由于存在这个强引用关系,会导致value无法回收。如果这个线程对象不会销毁那么这个强引用关系则会一直存在,就会出现内存泄漏情况

在ThreadLocalMap中的setEntry()、getEntry(),如果遇到key == null的情况,会对value设置为null。当然我们也可以显示调用ThreadLocal的remove()方法进行处理

ThreadLocal总结

  • ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制。这点至关重要
  • 每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本
  • ThreadLocal并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要木得视为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内部

线程池

为了减少创建和销毁线程的次数,让每个线程可以多次使用,可根据系统情况调整执行的线程数量,防止消耗过多内存,所以我们可以使用线程池.

线程池复用原理

一个通俗的理解:餐厅(线程池)& 服务员(线程)& 客人(任务)

  1. 当客人来到餐厅,服务员要将客人带到餐桌用餐
  2. 客人带到餐桌用完餐,服务员就可以去接另外的客人用餐
  3. 如果客人太多了,那就要进行两个操作:排队(队列)和 钟点工(临时线程)
  4. 等客人用餐高峰结束,常驻服务员可以胜任所有任务时,赶紧把钟点工炒了省工资

在哪都有万恶的资本。。。

如何实现线程同步

  • 同步方法:Java每个对象都有一个内置锁,内置锁会保护整个方法,使用synchronized关键字修饰方法,则必须获得内置锁才可调用方法,否则会处于阻塞状态

使用synchronized关键字也可以修饰静态方法,此时如果调用该静态方法,锁着的是整个类

  • 同步代码块:使用synchronized关键字修饰语句块,被修饰的语句块会自动加上内置锁

synchronized 和 Lock区别

区别类型 synchronized Lock
存在层次 Java的关键字,在jvm层面上 是JVM的一个接口
锁的获取 假设A线程获得锁,B线程等待。如果A线程阻塞,B线程会一直等待 情况而定,Lock有多个锁获取的方式,大致就是可以尝试获得锁,线程可以不用一直等待(可以通过tryLock判断有没有锁)
锁的释放 1、以获取锁的线程执行完同步代码,释放锁2、线程执行发生异常,jvm会让线程释放 在finally中必须释放锁,不然容易造成线程死锁
锁类型 锁可重入、不可中断、非公平 可重入、可判断 可公平(两者皆可)
性能 少量同步 适用于大量同步
支持锁的场景 1. 独占锁 1. 公平锁与非公平锁

1.synchronized 是内置的Java关键字,Lock是Java的一个接口

2.synchronized 无法判断获取锁的状态,Lock可以判断是否获取到了锁

3.synchronized 会自动释放锁,Lock必须手动释放锁!如果不释放锁,会造成死锁

4.synchronized 线程一在获得锁的情况下阻塞了,第二个线程就只能傻傻的等着;Lock就不一定会等待下去

5.synchronized 可重入锁,不可以中断,非公平;Lock,可重入锁,可以判断锁,非公平/公平(可以自己设置,默认非公平锁)

6.synchronized 适合锁少量的同步代码;Lock适合锁大量同步代码!

7.Lock可以提高多个线程进行读操作的效率。

在性能上来说,如果竞争资源不激烈,两者性能是差不多的,而当竞争资源非常激烈时(即大量线程同时竞争),此时Lock的性能要远远优于synchronized

ConcurrentHashMap底层原理

  • JDK1.7:内部主要是一个Segment数组,而数组的每一项又是一个HashEntry数组,元素都存在HashEntry数组里。因为每次锁定的是Segment对象,也就是整个HashEntry数组,所以又叫分段锁

1.7ConcurrentHashMap.png

  • JDK1.8:与HashMap一样采用:数组+链表+红黑树;底层原理则是采用锁链表或者红黑树头结点,相比于HashTable的方法锁,力度更细,是对数组(table)中的桶(链表或者红黑树)的头结点进行锁定,这样锁定,只会影响数组(table)当前下标的数据,不会影响其他下标节点的操作,可以提高读写效率。

putVal执行流程

  1. 判断存储的key、value是否为空,若为空,则抛出异常
  2. 计算key的hash值,随后死循环(该循环可以确保成功插入,当满足适当条件时,会主动终止),判断table表为空或者长度为0,则初始化table表
  3. 根据hash值获取table中该下标对应的节点,如果该节点为空,则根据参数生成新的节点,并以CAS的方式进行更新,并终止死循环。
  4. 如果该节点的hash值是MOVED(-1),表示正在扩容,则辅助对该节点进行转移。
  5. 对数组(table)中的节点,即桶的头结点进行锁定,如果该节点的hash大于等于0,表示此桶是链表,然后对该桶进行遍历(死循环),寻找链表中与put的key的hash值相等,并且key相等的元素,然后进行值的替换,如果到链表尾部都没有符合条件的,就新建一个node,然后插入到该桶的尾部,并终止该循环遍历。
  6. 如果该节点的hash小于0,并且节点类型是TreeBin,则走红黑树的插入方式。
  7. 判断是否达到转化红黑树的阈值,如果达到阈值,则链表转化为红黑树。

预防死锁

死锁的4个必要条件

  1. 互斥条件 同一时间只能有一个线程获取资源。
  2. 不可剥夺条件 一个线程已经占有的资源,在释放之前不会被其它线程抢占
  3. 请求和保持条件 线程等待过程中不会释放已占有的资源
  4. 循环等待条件 多个线程互相等待对方释放资源

预防死锁–破坏必要条件

  1. 由于资源互斥是资源使用的固有特性,无法改变,我们不讨论

  2. 破坏不可剥夺条件

    • 一个进程不能获得所需要的全部资源时便处于等待状态,等待期间他占有的资源将被隐式的释放重新加入到系统的资源列表中,可以被其他的进程使用,而等待的进程只有重新获得自己原有的资源以及新申请的资源才可以重新启动,执行
  3. 破坏请求与保持条件

    • 第一种方法静态分配即每个进程在开始执行时就申请他所需要的全部资源

    • 第二种是动态分配即每个进程在申请所需要的资源时他本身不占用系统资源

  4. 破坏循环等待条件

    • 采用资源有序分配,其基本思想是将系统中的所有资源顺序编号,将紧缺的,稀少的采用较大的编号,在申请资源时必须按照编号的顺序进行,一个进程只有获得较小编号的进程才能申请较大编号的进程。

ABA问题

  1. 有两个线程同时去修改一个变量的值,比如线程1、线程2,都更新变量值,将变量值从A更新成B。
  2. 首先线程1获取到CPU的时间片,线程2由于某些原因发生阻塞进行等待,此时线程1进行比较更新(CompareAndSwap),成功将变量的值从A更新成B。
  3. 更新完毕之后,恰好又有线程3进来想要把变量的值从B更新成A,线程3进行比较更新,成功将变量的值从B更新成A。
  4. 线程2获取到CPU的时间片,然后进行比较更新,发现值是预期的A,然后有更新成了B。但是线程1并不知道,该值已经有了A->B->A这个过程,这也就是我们常说的ABA问题。

线程安全活跃态问题

线程安全的活跃性问题可以分为 死锁、活锁、饥饿

  • 活锁:就是有时线程虽然没有发生阻塞,但是仍然会存在执行不下去的情况,活锁不会阻塞线程,线程会一直重复执行某个相同的操作,并且一直失败重试
  1. 我们开发中使用的异步消息队列就有可能造成活锁的问题,在消息队列的消费端如果没有正确的ack消息,并且执行过程中报错了,就会再次放回消息头,然后再拿出来执行,一直循环往复的失败。这个问题除了正确的ack之外,往往是通过将失败的消息放入到延时队列中,等到一定的延时再进行重试来解决。
  2. 解决活锁的方案很简单,尝试等待一个随机的时间就可以,会按时间轮去重试
  • 饥饿:线程因无法访问所需资源而无法执行下去的情况,分为两种情况:
  1. 一种是其他的线程在临界区做了无限循环或无限制等待资源的操作,让其他的线程一直不能拿到锁进入临界区,对其他线程来说,就进入了饥饿状态
  2. 另一种是因为线程优先级不合理的分配,导致部分线程始终无法获取到CPU资源而一直无法执行
  3. 解决饥饿的问题有几种方案:
    • 保证资源充足,很多场景下,资源的稀缺性无法解决
    • 公平分配资源,在并发编程里使用公平锁,例如FIFO策略,线程等待是有顺序的,排在等待队列前面的线程会优先获得资源
    • 避免持有锁的线程长时间执行,很多场景下,持有锁的线程的执行时间也很难缩短
  • 死锁 :线程在对同一把锁进行竞争的时候,未抢占到锁的线程会等待持有锁的线程释放锁后继续抢占,如果两个或两个以上的线程互相持有对方将要抢占的锁,互相等待对方先行释放锁就会进入到一个循环等待的过程,这个过程就叫做死锁

集合

集合类

**第一代线程安全集合类 **

Vector、Hashtable

是怎么保证线程安排的: 使用synchronized修饰方法*

缺点:效率低下

第二代线程非安全集合类

ArrayList、HashMap

线程不安全,但是性能好,用来替代Vector、Hashtable            

使用ArrayList、HashMap,需要线程安全怎么办呢?

使用 Collections.synchronizedList(list); Collections.synchronizedMap(m);

底层使用synchronized代码块锁 虽然也是锁住了所有的代码,但是锁在方法里边,并所在方法外边性能可以理解为稍有提高吧。毕竟进方法本身就要分配资源的

第三代线程安全集合类

在大量并发情况下如何提高集合的效率和安全呢?

java.util.concurrent.*

ConcurrentHashMap:

CopyOnWriteArrayList :

CopyOnWriteArraySet: 注意 不是CopyOnWriteHashSet*

底层大都采用Lock锁(1.8的ConcurrentHashMap不使用Lock锁),保证安全的同时,性能也很高。

ArrayList 和 LinkedList

ArrayList是基于索引的数据接口,它的底层是数组。它可以以O(1)时间复杂度对元素进行随机访问。

LinkedList是以元素列表的形式存储它的数据,每一个元素都和它的前一个和后一个元素链接在一起,在这种情况下,查找某个元素的时间复杂度是O(n)。

ArrayList vs. LinkedList

多查找用ArrayList,多增删用LinkedList

HashMap 构成

HashMap 非线程安全,想要安全用 第三代集合ConcurrentHashMap 或者 自行加锁 synchronizedMap

Java7及之前,数组+链表

Java8以后,数组+链表+红黑树

HashMap 扩容机制

1.7对元素进行rehash算法,计算原来每个元素在扩容之后的哈希表中的位置

1.8借助2倍扩容机制,元素不需要进行重新计算位置,通过高位运算(e.hash & oldCap)来确定元素是否需要移动

为什么hashmap扩容的时候是两倍

只有当n的值是2的N次幂的时候,进行&位运算的时候,才可以只看后几位,而不需要全部进行计算

HashMap和HashTable对比

  1. HashTable线程同步,HashMap非线程同步。

  2. HashTable不允许<键,值>有空值,HashMap允许<键,值>有空值。

  3. HashTable使用Enumeration,HashMap使用Iterator。

  4. HashTable中hash数组的默认大小是11,增加方式的old*2+1,HashMap中hash数组的默认大小是16,增长方式是2的指数倍。

  5. HashMap jdk1.8之前list + 链表; jdk1.8之后list + 链表,当链表长度到8时,转化为红黑树

  6. HashMap链表插入节点的方式 在Java1.7中,插入链表节点使用头插法。Java1.8中变成了尾插法

  7. Java1.8的hash()中,将hash值高位(前16位)参与到取模的运算中,使得计算结果的不确定性增强,降低发生哈希碰撞的概率

如何使 HashMap 线程安全

方法一:通过Collections.synchronizedMap()返回一个新的Map,这个新的map就是线程安全的. 这个要求大家习惯基于接口编程,因为返回的并不是HashMap,而是一个Map的实现.

  • 特点: 通过Collections.synchronizedMap()来封装所有不安全的HashMap的方法,就连toString, hashCode都进行了封装. 封装的关键点有2处:
  1. 使用了经典的synchronized来进行互斥
  2. 使用了代理模式new了一个新的类,这个类同样实现了Map接口
  • 在Hashmap上面,synchronized锁住的是对象,所以第一个申请的得到锁,其他线程将进入阻塞,等待唤醒.

  • 优点:代码实现十分简单,一看就懂.

  • 缺点:从锁的角度来看,方法一直接使用了锁住方法,基本上是锁住了尽可能大的代码块.性能会比较差.

方法二:重新改写了HashMap,具体的可以查看java.util.concurrent.ConcurrentHashMap. 这个方法比方法一有了很大的改进.

  • 特点:重新写了HashMap,比较大的改变有如下几点.使用了新的锁机制,把HashMap进行了拆分,拆分成了多个独立的块,这样在高并发的情况下减少了锁冲突的可能,使用的是NonfairSync. 这个特性调用CAS指令来确保原子性与互斥性.当如果多个线程恰好操作到同一个segment上面,那么只会有一个线程得到运行.

  • 优点:需要互斥的代码段比较少,性能会比较好. ConcurrentHashMap把整个Map切分成了多个块,发生锁碰撞的几率大大降低,性能会比较好.

  • 缺点:代码繁琐

框架

Spring

IOC

IOC(Inversion of Control),意思是控制反转,不是什么技术,而是一种设计思想,IOC意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。

如何实现一个IOC容器

在传统的应用程序中,我们是在对象中主动控制去直接获取依赖对象,这个是正转,反转是由容器来帮忙创建及注入依赖对象,在这个过程过程中,由容器帮我们查找级注入依赖对象,对象只是被动的接受依赖对象。

  1. 先准备一个基本的容器对象,包含一些map结构的集合,用来方便后续过程中存储具体的对象
  2. 进行配置文件的读取工作或者注解的解析工作,将需要创建的bean对象都封装成BeanDefinition对象存储在容器中
  3. 容器将封装好的BeanDefinition对象通过反射的方式进行实例化,完成对象的实例化工作
  4. 进行对象的初始化操作,也就是给类中的对应属性值进行设置,也就是进行依赖注入,完成整个对象的创建,变成一个完整的bean对象,存储在容器的某个map结构中
  5. 通过容器对象来获取对象,进行对象的获取和逻辑处理工作
  6. 提供销毁操作,当对象不用或者容器关闭的时候,将无用的对象进行销毁

AOP

公共代码逻辑抽象出来变成一个切面,然后注入到目标对象(具体业务)中去,AOP正是基于这样的一个思路实现的,通过动态代理的方式,将需要注入切面的对象进行代理,在进行调用的时候,将公共的逻辑直接添加进去,而不需要修改原有业务的逻辑代码,只需要在原来的业务逻辑基础之上做一些增强功能即可。

AOP支持允许将一些通用任务如安全、事务、日志等进行集中式处理,从而提供了更好的复用

Proxy和CGLIB是非常重要的代理模式,是springAOP底层实现的主要两种方式

Spring简化开发方式

基于POJO的轻量级和最小侵入性编程

通过依赖注入和面向接口实现松耦合

基于AOP切面和惯例进行声明式编程

通过AOP切面和模板减少样板式代码

设计模式及应用场景

  1. 工厂模式,在各种BeanFactory以及ApplicationContext创建中都用到了

  2. 模版模式,在各种BeanFactory以及ApplicationContext实现中也都用到了

  3. 代理模式,Spring AOP 利用了 AspectJ AOP实现的! AspectJ AOP 的底层用了动态代理

  4. 策略模式,加载资源文件的方式,使用了不同的方法,比如:ClassPathResourece,FileSystemResource,ServletContextResource,UrlResource但他们都有共同的借口Resource;在Aop的实现中,采用了两种不同的方式,JDK动态代理和CGLIB代理

  5. 单例模式,比如在创建bean的时候。

  6. 观察者模式,spring中的ApplicationEvent,ApplicationListener,ApplicationEventPublisher

  7. 适配器模式,MethodBeforeAdviceAdapter,ThrowsAdviceAdapter,AfterReturningAdapter

     8. 装饰者模式,源码中类型带Wrapper或者Decorator的都是
    

Spring常用注解

  • @Component
  • @Service
  • @Repository
  • @Controller
  • @Configuration
  • @Autowired
  • @Value
  • @RequestMapping

spring事务的实现方式原理

在使用Spring框架的时候,可以有两种事务的实现方式,一种是编程式事务,有用户自己通过代码来控制事务的处理逻辑,还有一种是声明式事务,通过**@Transactional**注解来实现

事务操作是AOP的一个核心体现,当一个方法添加@Transactional注解之后,spring会基于这个类生成一个代理对象,会将这个代理对象作为bean,当使用这个代理对象的方法的时候,如果有事务处理,那么会先把事务的自动提交给关系,然后去执行具体的业务逻辑,如果执行逻辑没有出现异常,那么代理逻辑就会直接提交,如果出现任何异常情况,那么直接进行回滚操作,当然用户可以控制对哪些异常进行回滚操作

事务的基本特性

事务四大特征:原子性,一致性,隔离性和持久性

  1. 原子性(Atomicity)
    一个原子事务要么完整执行,要么干脆不执行。这意味着,工作单元中的每项任务都必须正确执行。如果有任一任务执行失败,则整个工作单元或事务就会被终止。即此前对数据所作的任何修改都将被撤销。如果所有任务都被成功执行,事务就会被提交,即对数据所作的修改将会是永久性的。
  2. 一致性(Consistency)
    一致性代表了底层数据存储的完整性。它必须由事务系统和应用开发人员共同来保证。事务系统通过保证事务的原子性,隔离性和持久性来满足这一要求; 应用开发人员则需要保证数据库有适当的约束(主键,引用完整性等),并且工作单元中所实现的业务逻辑不会导致数据的不一致(即,数据预期所表达的现实业务情况不相一致)。例如,在一次转账过程中,从某一账户中扣除的金额必须与另一账户中存入的金额相等。支付宝账号100 你读到余额要取,有人向你转100 但是事物没提交(这时候你读到的余额应该是100,而不是200) 这种就是一致性
  3. 隔离性(Isolation)
    隔离性意味着事务必须在不干扰其他进程或事务的前提下独立执行。换言之,在事务或工作单元执行完毕之前,其所访问的数据不能受系统其他部分的影响。
  4. 持久性(Durability)
    持久性表示在某个事务的执行过程中,对数据所作的所有改动都必须在事务成功结束前保存至某种物理存储设备。这样可以保证,所作的修改在任何系统瘫痪时不至于丢失。

ACID是靠什么保证的

原子性由undolog日志来保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql

一致性是由其他三大特性保证,程序代码要保证业务上的一致性

隔离性是由MVCC来保证

持久性由redolog来保证,mysql修改数据的时候会在redolog中记录一份日志数据,就算数据没有保存成功,只要日志保存成功了,数据仍然不会丢失

Spring事务配置

  1. 配置事务管理器
  2. 配置事务属性
  3. 配置事务切面
  4. 配置事务拦截器
  5. 配置事务策略

spring事务什么时候会失效

​ 1、bean对象没有被spring容器管理

​ 2、方法的访问修饰符不是public

​ 3、自身调用问题

​ 4、数据源没有配置事务管理器

​ 5、数据库不支持事务

​ 6、异常被捕获

​ 7、异常类型错误或者配置错误

事务的隔离级别

spring中的事务隔离级别就是数据库的隔离级别,有以下几种:

  • read uncommitted
  • read committed
  • repeatable read
  • serializable

事务传播机制

REQUIRED:默认的传播特性,如果当前没有事务,则新建一个事务,如果当前存在事务,则加入这个事务

SUPPORTS:当前存在事务,则加入当前事务,如果当前没有事务,则以非事务的方式执行

MANDATORY:当前存在事务,则加入当前事务,如果当前事务不存在,则抛出异常

REQUIRED_NEW:创建一个新事务,如果存在当前事务,则挂起该事务

NOT_SUPPORTED:以非事务方式执行,如果存在当前事务,则挂起当前事务

NEVER:不使用事务,如果当前事务存在,则抛出异常

NESTED:如果当前事务存在,则在嵌套事务中执行,否则REQUIRED的操作一样

NESTEDREQUIRED_NEW的区别:

​ REQUIRED_NEW是新建一个事务并且新开始的这个事务与原有事务无关,而NESTED则是当前存在事务时会开启一个嵌套事务,在NESTED情况下,父事务回滚时,子事务也会回滚,而REQUIRED_NEW情况下,原有事务回滚,不会影响新开启的事务

NESTEDREQUIRED的区别:

​ REQUIRED情况下,调用方存在事务时,则被调用方和调用方使用同一个事务,那么被调用方出现异常时,由于共用一个事务,所以无论是否catch异常,事务都会回滚,而在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不会回滚。

Bean

BeanFactory和ApplicationContext

相同点:

  • Spring提供了两种不同的IOC 容器,一个是BeanFactory,另外一个是ApplicationContext,ApplicationContext继承于BeanFactory(ApplicationContext继承ListableBeanFactory,ListableBeanFactory继承BeanFactory)

  • BeanFactory 和 ApplicationContext 都提供了一种方式,使用getBean(“bean name”)获取bean

  • 它们都可以用来配置XML属性,也支持属性的自动注入

不同点:

  • 实例化:
    • 调用getBean()方法时,BeanFactory仅实例化bean,
    • ApplicationContext 在启动容器的时候实例化单例bean,不会等待调用getBean()方法时再实例化
  • BeanFactory不支持国际化,即i18n,但ApplicationContext提供了对它的支持
  • 核心实现:
    • BeanFactory 的一个核心实现是XMLBeanFactory
    • ApplicationContext 的一个核心实现是ClassPathXmlApplicationContext,Web容器的环境我们使用WebApplicationContext并且增加了getServletContext 方法
  • 自动注入:
    • BeanFactory需要使用API注册AutoWiredBeanPostProcessor
    • ApplicationContext可以使用XML进行配置

简而言之,BeanFactory提供基本的IOC和DI功能,而ApplicationContext提供高级功能,BeanFactory可用于测试和非生产使用,但ApplicationContext是功能更丰富的容器实现,应该优于BeanFactory

bean的生命周期

image-20230215213255581

1、实例化bean对象

​ 通过反射的方式进行对象的创建,此时的创建只是在堆空间中申请空间,属性都是默认值

2、设置对象属性

​ 给对象中的属性进行值的设置工作

3、检查Aware相关接口并设置相关依赖

​ 如果对象中需要引用容器内部的对象,那么需要调用aware接口的子类方法来进行统一的设置(容器对象赋值)

4、BeanPostProcessor的前置处理

​ 对生成的bean对象进行前置的处理工作

5、检查是否是InitializingBean的子类来决定是否调用afterPropertiesSet方法

​ 判断当前bean对象是否设置了InitializingBean接口,然后进行属性的设置等基本工作

6、检查是否配置有自定义的init-method方法

​ 如果当前bean对象定义了初始化方法,那么在此处调用初始化方法

7、BeanPostProcessor后置处理

​ 对生成的bean对象进行后置的处理工作

8、注册必要的Destruction相关回调接口

​ 为了方便对象的销毁,在此处调用注销的回调接口,方便对象进行销毁操作

9、获取并使用bean对象

​ 通过容器来获取对象并进行使用

10、是否实现DisposableBean接口

​ 判断是否实现了DisposableBean接口,并调用具体的方法来进行对象的销毁工作

11、是否配置有自定义的destory方法

​ 如果当前bean对象定义了销毁方法,那么在此处调用销毁方法

bean作用域

① singleton

使用该属性定义Bean时,IOC容器仅创建一个Bean实例,IOC容器每次返回的是同一个Bean实例。

② prototype

使用该属性定义Bean时,IOC容器可以创建多个Bean实例,每次返回的都是一个新的实例。

③ request

该属性仅对HTTP请求产生作用,使用该属性定义Bean时,每次HTTP请求都会创建一个新的Bean,适用于WebApplicationContext环境。

④ session

该属性仅用于HTTP Session,同一个Session共享一个Bean实例。不同Session使用不同的实例。

⑤ global-session

该属性仅用于HTTP Session,同session作用域不同的是,所有的Session共享一个Bean实例。

单例Bean是否线程安全

Spring中的Bean对象默认是单例的,框架并没有对bean进行多线程的封装处理

如果Bean是有状态的,那么就需要开发人员自己来保证线程安全,最简单的办法就是改变bean的作用域把singleton改成prototype,这样每次请求bean对象就相当于是创建新的对象来保证线程的安全。

有状态就是由数据存储的功能,无状态就是不会存储数据。

controller,service和dao本身并不是线程安全的,只是调用里面的方法,而且多线程调用一个实例的方法,会在内存中复制遍历,这是自己线程的工作内存,是最安全的。

因此在进行使用的时候,不要在bean中声明任何有状态的实例变量或者类变量,如果必须如此,也推荐大家使用ThreadLocal把变量变成线程私有,如果bean的实例变量或者类变量需要在多个线程之间共享,那么就只能使用synchronized,lock,cas等这些实现线程同步的方法了。

bean的自动装配

bean的自动装配指的是bean的属性值在进行注入的时候通过某种特定的规则和方式去容器中查找,并设置到具体的对象属性中,主要有五种方式:

  • no – 缺省情况下,自动配置是通过“ref”属性手动设定,在项目中最常用
  • byName – 根据属性名称自动装配。如果一个bean的名称和其他bean属性的名称是一样的,将会自装配它。
  • byType – 按数据类型自动装配,如果bean的数据类型是用其它bean属性的数据类型,兼容并自动装配它。
  • constructor – 在构造函数参数的byType方式。
  • autodetect – 如果找到默认的构造函数,使用“自动装配用构造”; 否则,使用“按类型自动装配”。

spring、springmvc、springboot

  1. spring是一个一站式的轻量级的java开发框架,核心是控制反转(IOC)和面向切面(AOP),针对于开发的WEB层(springMvc)、业务层(Ioc)、持久层(jdbcTemplate)等都提供了多种配置解决方案;

  2. springMvc是spring基础之上的一个MVC框架,主要处理web开发的路径映射和视图渲染,属于spring框架中WEB层开发的一部分;涵盖面包括前端视图开发、文件配置、后台接口逻辑开发等,XML、config等配置相对比较繁琐复杂;

  3. springBoot框架相对于springMvc框架来说,更专注于开发微服务后台接口,不开发前端视图,同时遵循默认优于配置,简化了插件配置流程,不需要配置xml,相对springmvc,大大简化了配置流程;

SpringMVC

springmvc工作流程

img

1、DispatcherServlet表示前置控制器,是整个SpringMVC的控制中心。用户发出请求,DispatcherServlet接收请求并拦截请求。
2、HandlerMapping为处理器映射。DispatcherServlet调用HandlerMapping,HandlerMapping根据请求url查找Handler。
3、返回处理器执行链,根据url查找控制器,并且将解析后的信息传递给DispatcherServlet
4、HandlerAdapter表示处理器适配器,其按照特定的规则去执行Handler。
5、执行handler找到具体的处理器
6、Controller将具体的执行信息返回给HandlerAdapter,如ModelAndView。
7、HandlerAdapter将视图逻辑名或模型传递给DispatcherServlet。
8、DispatcherServlet调用视图解析器(ViewResolver)来解析HandlerAdapter传递的逻辑视图名。
9、视图解析器将解析的逻辑视图名传给DispatcherServlet。
10、DispatcherServlet根据视图解析器解析的视图结果,调用具体的视图,进行试图渲染
11、将响应数据返回给客户端

springmvc的九大组件

  • HandlerMapping:根据request找到相应的处理器
  • HandlerAdapter:调用Handler适配器
  • HandlerExceptionResolver:异常处理
  • ViewResolver:将String类型的视图名和Locale解析为View类型的视图
  • RequestToViewNameTranslator:对没有设置返回类型的Handler(Controller),从request中获取viewName
  • LocaleResolver:解析出Locale
  • ThemeResolver:主题解析,如UI
  • MultipartResolver:处理上传请求,将普通的request封装成MultipartHttpServletRequest
  • FlashMapManager:管理FlashMap,FlashMap用于在redirect重定向中传递参数

MVC的注解

  • @Controller
  • @RequestMapping
  • @RequestParam
  • @PathVariable
  • @ModelAttribute
  • @SessionAttribute

SpringBoot

自动配置原理

启动类的@SpringBootApplication注解由

@SpringBootConfiguration

@EnableAutoConfiguration

@ComponentScan

三个注解组成,三个注解共同完成自动装配;

  • @SpringBootConfiguration 注解 标记启动类为配置类
  • @ComponentScan 注解 实现启动时扫描启动类所在的包以及子包下所有标记为bean的类,由IOC容器注册为bean
  • @EnableAutoConfiguration 通过 @Import 注解导入 AutoConfigurationImportSelector类,然后通过AutoConfigurationImportSelector 类的 selectImports 方法去读取需要被自动装配的组件依赖下的spring.factories文件配置的组件的类全名,并按照一定的规则过滤掉不符合要求的组件的类全名,将剩余读取到的各个组件的类全名集合返回给IOC容器并将这些组件注册为bean

简而言之,声明自己是启动类,找到bean包,再导入组件

springboot中的starter

starter就是一个jar包,写一个@Configuration的配置类,将这些bean定义在其中,然后再starter包的META-INF/spring.factories中写入配置类,那么springboot程序在启动的时候就会按照约定来加载该配置类

开发人员只需要将相应的starter包依赖进应用中,进行相关的属性配置,就可以进行代码开发,而不需要单独进行bean对象的配置

mybatis中#{}和${}

  • #{}是预编译处理(占位),${}是字符串替换。
  • Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用 PreparedStatement 的 set 方法来赋值;
  • Mybatis 在处理${}时,就是把${}替换成变量的值。
  • 使用#{}可以有效的防止 SQL 注入,提高系统安全性

SpringCloud

微服务设计原则

软件是为业务服务的,好的系统不是设计出来的,而是进化出来的

  1. 单一职责原则:让每个服务能独立,有界限的工作,每个服务只关注自己的业务。做到高内聚。
  2. 服务自治原则:每个服务要能做到独立开发、独立测试、独立构建、独立部署,独立运行。与其他服务进行解耦。
  3. 轻量级通信原则:让每个服务之间的调用是轻量级,并且能够跨平台、跨语言。比如采用RESTful风格,利用消息队列进行通信等。
  4. 粒度进化原则:对每个服务的粒度把控,其实没有统一的标准,这个得结合我们解决的具体业务问题。不要过度设计。服务的粒度随着业务和用户的发展而发展。

微服务架构原理

主要是面向SOA理念,更细小粒度服务的拆分,将功能分解到各个服务当中,从而降低系统的耦合性,并提供更加灵活的服务支持

分布式微服务项目如何设计

一般设计成两层:业务层和能力层(中台),业务层接受用户请求,然后通过调用能力层来完成业务逻辑

image-20210522172654370

springcloud核心组件

服务注册与发现——Netflix Eureka、Nacos、Zookeeper

客户端负载均衡——Netflix Ribbon、SpringCloud LoadBalancer

服务熔断器——Netflix Hystrix、Alibaba Sentinel、Resilience4J

服务网关——Netflix Zuul、SpringCloud Gateway

服务接口调用——Netflix Feign、 Resttemplate、Openfeign

链路追踪——Netflix Sleuth、Skywalking、Pinpoint

聚合Hystrix监控数据——Netflix Turbine

监控中心—- SpringBoot Admin

配置中心——Spring Cloud Config 、Apollo、nacos

注册中心原理

服务启动后向Eureka注册,Eureka Server会将注册信息向其他Eureka Server进行同步,当服务消费者要调用服务提供者,则向服务注册中心获取服务提供者地址,然后会将服务提供者地址缓存在本地,下次再调用时,则直接从本地缓存中取,完成一次调用

zookeeper和eureka做注册中心的区别

Zookeeper保证的是CP(一致性,容错性), 而Eureka则是AP(可用性,容错性)

配置中心原理

在服务运行之前,将所需的配置信息从配置仓库拉取到本地服务,达到统一化配置管理的目的

配置中心如何实现自动刷新

  1. 配置中心Server端承担起配置刷新的职责

  2. 提交配置触发post请求给server端的bus/refresh接口

  3. server端接收到请求并发送给Spring Cloud Bus总线

  4. Spring Cloud bus接到消息并通知给其它连接到总线的客户端

  5. 其它客户端接收到通知,请求Server端获取最新配置

  6. 全部客户端均获取到最新的配置

配置中心如何保证数据安全

  1. 保证容器文件访问的安全性,即保证所有的网络资源请求都需要登录

  2. 将配置中心里所有配置文件中的密码进行加密,保证其密文性

  3. 开发环境禁止拉取生产环境的配置文件

Spring Cloud和Dubbo的区别

  1. dubbo 是二进制传输,对象直接转成二进制,使用RPC通信。

  2. SpringCloud是http 传输,同时使用http协议一般会使用JSON报文,json再转二进制,消耗会更大。

Dubbo只是实现了服务治理,而Spring Cloud下面有几十个子项目分别覆盖了微服务架构下的方方面面,服务治理只是其中的一个方面

一定程度来说,Dubbo只是Spring Cloud Netflix中的一个子集

Ribbon负载均衡原理

  1. Ribbon通过ILoadBalancer接口对外提供统一的选择服务器(Server)的功能,此接口会根据不同的负载均衡策略(IRule)选择合适的Server返回给使用者。

  2. IRule是负载均衡策略的抽象,ILoadBalancer通过调用IRule的choose()方法返回Server

  3. IPing用来检测Server是否可用,ILoadBalancer的实现类维护一个Timer每隔10s检测一次Server的可用状态

  4. IClientConfig主要定义了用于初始化各种客户端和负载均衡器的配置信息,器实现类为DefaultClientConfigImpl

微服务熔断降级机制

微服务框架是许多服务互相调用的,要是不做任何保护的话,某一个服务挂了,就会引起连锁反应,导致别的服务也挂。

Hystrix 是隔离、熔断以及降级的一个框架。

如果调用某服务报错(或者挂了),就对该服务熔断,在 5 分钟内请求此服务直接就返回一个默认值,不需要每次都卡几秒,这个过程,就是所谓的熔断。

但是熔断了之后就会少调用一个服务,此时需要做下标记,标记本来需要做什么业务,但是因为服务挂了,暂时没有做,等该服务恢复了,就可以手工处理这些业务。这个过程,就是所谓的降级。

Hystrix实现原理

Hystrix是一个延迟和容错库,旨在隔离对远程系统、服务和第三方库的访问点,停止级联故障,并在 不可避免发生故障的复杂分布式系统中实现快速恢复。主要靠Spring的AOP实现

正常情况下,断路器关闭,服务消费者正常请求微服务

一段事件内,失败率达到一定阈值,断路器将断开,此时不再请求服务提供者,而是只是快速失败的方法(断路方法)

断路器打开一段时间,自动进入“半开”状态,此时,断路器可允许一个请求方法服务提供者,如果请求调用成功,则关闭断路器,否则继续保持断路器打开状态。

断路器hystrix是保证了局部发生的错误,不会扩展到整个系统,从而保证系统的即使出现局部问题也不会造成系统雪崩

注册中心挂了,或者服务挂了,应该如何处理

  • 注册中心挂了,可以读取本地持久化里的配置
  • 服务挂了 应该配有服务监控中心 感知到服务下线后可以通过配置的邮件通知相关人员排查问题。

RPC、RMI

  • RPC:远程过程调用协议,通过网络从远程计算机上请求调用某种服务。
  • RMI:远程方法调用 能够让在客户端Java虚拟机上的对象像调用本地对象一样调用服务端java 虚拟机中的对象上的方法

分布式事务

CAP

在一个分布式系统中,最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项。

  • C:一致性(Consistency),数据在多个副本中保持一致,可以理解成两个用户访问两个系统A和B,当A系统数据有变化时,及时同步给B系统,让两个用户看到的数据是一致的。

  • A:可用性(Availability),系统对外提供服务必须一直处于可用状态,在任何故障下,客户端都能在合理时间内获得服务端非错误的响应。

  • P:分区容错性(Partition tolerance),在分布式系统中遇到任何网络分区故障,系统仍然能对外提供服务。网络分区,可以这样理解,在分布式系统中,不同的节点分布在不同的子网络中,有可能子网络中只有一个节点,在所有网络正常的情况下,由于某些原因导致这些子节点之间的网络出现故障,导致整个节点环境被切分成了不同的独立区域,这就是网络分区

BASE

由于CAP中一致性C和可用性A无法兼得,eBay的架构师,提出了BASE理论,它是通过牺牲数据的强一致性,来获得可用性,简单来说就是最终一致性

  • Basically Available(基本可用):分布式系统在出现不可预知故障的时候,允许损失部分可用性,保证核心功能的可用。

  • Soft state(软状态):软状态也称为弱状态,和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。、

  • Eventually consistent(最终一致性):最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

2PC提交协议

算法思路

参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)

  • 准备阶段:事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交
  • 提交阶段:如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)
缺点
  1. 同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。

  2. 单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)

  3. 数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据部一致性的现象。

  4. 二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。

3PC提交协议

算法思路
  • CanCommit阶段:协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应
  • PreCommit阶段:协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能
    • 协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行
    • 有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断
  • doCommit阶段:该阶段进行真正的事务提交,也可以分为以下两种情况
    • 执行提交,完成事务
    • 中断事务,rollback回滚

2PC和3PC的区别

  1. 引入超时机制。同时在协调者和参与者中都引入超时机制。

  2. 三阶段在2PC的第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。

TCC解决方案

TCC(Try-Confirm-Cancel)是一种常用的分布式事务解决方案

  • T(Try):业务检查阶段,这阶段主要进行业务校验和检查或者资源预留;也可能是直接进行业务操作。

  • C(Confirm):业务确认阶段,这阶段对Try阶段校验过的业务或者预留的资源进行确认。

  • C(Cancel):业务回滚阶段,这阶段和上面的C(Confirm)是互斥的,用于释放Try阶段预留的资源或者业务

TCC空回滚

在没有调用TCC资源Try方法的情况下,调用了二阶段的Cancel方法。比如当Try请求由于网络延迟或故障等原因,没有执行,结果返回了异常,那么此时Cancel就不能正常执行,因为Try没有对数据进行修改,如果Cancel进行了对数据的修改,那就会导致数据不一致。

解决思路是关键就是要识别出这个空回滚。思路很简单就是需要知道Try阶段是否执行,如果执行了,那就是正常回滚;如果没执行,那就是空回滚

TCC幂等问题

为了保证TCC二阶段提交重试机制不会引发数据不一致,要求TCC的二阶段Confirm和Cancel接口保证幂等,这样不会重复使用或者释放资源

解决思路在上述 分支事务记录中增加执行状态,每次执行前都查询该状态

TCC悬挂问题

悬挂就是对于一个分布式事务,其二阶段Cancel接口比Try接口先执行

出现原因是在调用分支事务Try时,由于网络发生拥堵,造成了超时,TM就会通知RM回滚该分布式事务,可能回滚完成后,Try请求才到达参与者真正执行,而一个Try方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了

解决思路是如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,判断分支事务记录表中是否已经有二阶段事务记录,如果有则不执行Try

可靠性服务方案

可靠消息最终一致性方案指的是:当事务的发起方(事务参与者,消息发送者)执行完本地事务后,同时发出一条消息,事务参与方(事务参与者,消息的消费者)一定能够接受消息并可以成功处理自己的事务。

强调两点:

  1. 可靠消息:发起方一定得把消息传递到消费者。
  2. 最终一致性:最终发起方的业务处理和消费方的业务处理得完成,达成最终一致。

image-20210522125830646

最大努力通知方案的关键

  1. 有一定的消息重复通知机制。因为接收通知方(上图中的我方支付系统)可能没有接收到通知,此时要有一定的机制对消息重复通知。
  2. 消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。

分布式id生成方案

描述 优点 缺点
UUID UUID是通用唯一标识码的缩写,其目的是让分布式系统中的所有元素都有唯一的辨识信息,而不需要通过中央控制器来指定唯一标识。 1. 降低全局节点的压力,使得主键生成速度更快; 2. 生成的主键全局唯一; 3. 跨服务器合并数据方便。 1. UUID占用16个字符,空间占用较多; 2. 不是递增有序的数字,数据写入IO随机性很大,且索引效率下降
数据库主键自增 MySQL数据库设置主键且主键自动增长 1. INT和BIGINT类型占用空间较小; 2. 主键自动增长,IO写入连续性好; 3. 数字类型查询速度优于字符串 1. 并发性能不高,受限于数据库性能; 2. 分库分表,需要改造,复杂; 3. 自增:数据和数据量泄露
Redis自增 Redis计数器,原子性自增 使用内存,并发性能好 1. 数据丢失; 2. 自增:数据量泄露
雪花算法(snowflake) 大名鼎鼎的雪花算法,分布式ID的经典解决方案 1. 不依赖外部组件; 2. 性能好 时钟回拨

雪花算法生成的ID组成

  1. 符号位,占用1位。
  2. 时间戳,占用41位,可以支持69年的时间跨度。
  3. 机器ID,占用10位。
  4. 序列号,占用12位。一毫秒可以生成4095个ID。

image-20230216215909531

分布式锁

应用场景

  1. 系统是一个分布式系统,集群集群,java的锁已经锁不住了。
  2. 操作共享资源,比如库里唯一的用户数据。
  3. 同步访问,即多个进程同时操作共享资源

解决方案

  1. Reids的分布式锁,很多大公司会基于Reidis做扩展开发。setnx key value ex 10s,Redisson watch dog(看门狗).

  2. 基于Zookeeper。临时节点,顺序节点。

  3. 基于数据库,比如Mysql。主键或唯一索引的唯一性

Redis分布式锁

命令

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写

1
2
3
加锁:set key value nx ex 10s

释放锁:delete key
死锁解决

情况1:加锁,没有释放锁。需要加释放锁的操作。比如delete key。

情况2:加锁后,程序还没有执行释放锁,程序挂了。需要用的key的过期机制。

场景

假设有两个服务A、B都希望获得锁,执行过程大致如下:

Step1: 服务A为了获得锁,向Redis发起如下命令: SET productId:lock 0xx9p03001 NX EX 30000

其中,”productId”由自己定义,可以是与本次业务有关的id,”0xx9p03001”是一串随机值,必须保证全局唯一,“NX”指的是当且仅当key(也就是案例中的”productId:lock”)在Redis中不存在时,返回执行成功,否则执行失败。”EX 30000”指的是在30秒后,key将被自动删除。执行命令后返回成功,表明服务成功的获得了锁。

Step2: 服务B为了获得锁,向Redis发起同样的命令: SET productId:lock 0000111 NX EX 30000
由于Redis内已经存在同名key,且并未过期,因此命令执行失败,服务B未能获得锁。服务B进入循环请求状态,比如每隔1秒钟(自行设置)向Redis发送请求,直到执行成功并获得锁。

Step3: 服务A的业务代码执行时长超过了30秒,导致key超时,因此Redis自动删除了key。此时服务B再次发送命令执行成功,假设本次请求中设置的value值为0000222。此时需要在服务A中对key进行续期,watch dog。

Step4: 服务A执行完毕,为了释放锁,服务A会主动向Redis发起删除key的请求。

注意: 在删除key之前,一定要判断服务A持有的value与Redis内存储的value是否一致。

比如当前场景下,Redis中的锁早就不是服务A持有的那一把了,而是由服务2创建,如果贸然使用服务A持有的key来删除锁,则会误将服务2的锁释放掉。此外,由于删除锁时涉及到一系列判断逻辑,因此一般使用lua脚本,具体如下:

1
2
3
4
5
if redis.call("get", KEYS[1])==ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end

ZooKeeper 分布式锁

实现原理
  • 顺序节点特性:ZK集群会按照发起创建的顺序来创建节点,节点分别为/lock/0000000001、/lock/0000000002、/lock/0000000003,最后一位数是依次递增的,节点名由zk来完成。
  • 临时节点特性:临时节点由某个客户端创建,当客户端与ZK集群断开连接,则该节点自动被删除
基本逻辑
  1. 客户端1调用create()方法创建名为“/业务ID/lock-”的临时顺序节点。
  2. 客户端1调用getChildren(“业务ID”)方法来获取所有已经创建的子节点。
  3. 客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点是所有节点中序号最小的,就是看自己创建的序列号是否排第一,如果是第一,那么就认为这个客户端1获得了锁,在它前面没有别的客户端拿到锁。
  4. 如果创建的节点不是所有节点中需要最小的,那么则监视比自己创建节点的序列号小的最大的节点,进入等待。直到下次监视的子节点变更的时候,再进行子节点的获取,判断是否获取锁

ZooKeeper和Reids做分布式锁的区别

简而言之:

  1. Zookeeper每次进行锁操作前都要创建若干节点,完成后要释放节点,会浪费很多时间;
  2. 而Redis只是简单的数据操作,没有这个问题。

Reids:

  1. Redis只保证最终一致性,副本间的数据复制是异步进行(Set是写,Get是读,Reids集群一般是读写分离架构,存在主从同步延迟情况),主从切换之后可能有部分数据没有复制过去可能会 「丢失锁」 情况,故强一致性要求的业务不推荐使用Reids,推荐使用zk。
  2. Redis集群各方法的响应时间均为最低。随着并发量和业务数量的提升其响应时间会有明显上升(公网集群影响因素偏大),但是极限qps可以达到最大且基本无异常

ZooKeeper:

  1. 使用ZooKeeper集群,锁原理是使用ZooKeeper的临时顺序节点,临时顺序节点的生命周期在Client与集群的Session结束时结束。因此如果某个Client节点存在网络问题,与ZooKeeper集群断开连接,Session超时同样会导致锁被错误的释放(导致被其他线程错误地持有),因此ZooKeeper也无法保证完全一致。
  2. ZK具有较好的稳定性;响应时间抖动很小,没有出现异常。但是随着并发量和业务数量的提升其响应时间和qps会明显下降。

MySQL分布式锁

通过主键id 或者 唯一索性 的唯一性进行加锁

说白了就是加锁的形式是向一张表中插入一条数据,该条数据的id就是一把分布式锁

例如当一次请求插入了一条id为1的数据,其他想要进行插入数据的并发请求必须等第一次请求执行完成后删除这条id为1的数据才能继续插入,实现了分布式锁的功能

限流算法

计数器算法

计数器算法,是指在指定的时间周期内累加访问次数,达到设定的阈值时,触发限流策略。下一个时间周期进行访问时,访问次数清零

这个算法有一个临界问题,比如,在0:00到1:00内,只在0:50有60个请求,而在1:00到2:00之间,只在1:10有60个请求,虽然在两个一分钟的时间内,都没有超过100个请求,但是在0:50到1:10这20秒内,确有120个请求,虽然在每个周期内,都没超过阈值,但是在这20秒内,已经远远超过了我们原来设置的1分钟内100个请求的阈值

滑动时间窗口算法

为了解决计数器算法的临界值的问题,发明了滑动窗口算法

滑动时间窗口是将计数器算法中的实际周期切分成多个小的时间窗口,分别在每个小的时间窗口中记录访问次数,然后根据时间将窗口往前滑动并删除过期的小时间窗口。最终只需要统计滑动窗口范围内的小时间窗口的总的请求数即可

在滑动时间窗口算法中,我们的小窗口划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确

漏桶限流算法

漏桶算法的原理就像它的名字一样,我们维持一个漏斗,它有恒定的流出速度,不管水流流入的速度有多快,漏斗出水的速度始终保持不变,类似于消息中间件,不管消息的生产者请求量有多大,消息的处理能力取决于消费者

漏桶的容量=漏桶的流出速度*可接受的等待时长。在这个容量范围内的请求可以排队等待系统的处理,超过这个容量的请求,才会被抛弃

在漏桶限流算法中,存在下面几种情况:

  1. 当请求速度大于漏桶的流出速度时,也就是请求量大于当前服务所能处理的最大极限值时,触发限流策略。

  2. 请求速度小于或等于漏桶的流出速度时,也就是服务的处理能力大于或等于请求量时,正常执行。

漏桶算法缺点:当系统在短时间内有突发的大流量时,漏桶算法处理不了

令牌桶限流算法

增加一个大小固定的容器,也就是令牌桶,系统以恒定的速率向令牌桶中放入令牌,如果有客户端来请求,先需要从令牌桶中拿一个令牌,拿到令牌,才有资格访问系统,这时令牌桶中少一个令牌。当令牌桶满的时候,再向令牌桶生成令牌时,令牌会被抛弃

在令牌桶算法中,存在以下几种情况:

  1. 请求速度大于令牌的生成速度:那么令牌桶中的令牌会被取完,后续再进来的请求,由于拿不到令牌,会被限流。

  2. 请求速度等于令牌的生成速度:那么此时系统处于平稳状态。

  3. 请求速度小于令牌的生成速度:那么此时系统的访问量远远低于系统的并发能力,请求可以被正常处理。

令牌桶算法由于有一个桶的存在,可以处理短时间大流量的场景。这是令牌桶和漏桶的一个区别。

幂等

什么是分布式系统中的幂等

幂等(idempotent、idempotence)是一个数学与计算机学概念,常见于抽象代数中。

简而言之,幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的。

实现幂等的解决方案

  • 查询操作:查询一次和查询多次,在数据不变的情况下,查询结果是一样的【select 是天然的幂等操作】
  • 删除操作:删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回 0,删除的数据多条,返回结果多个
  • 唯一索引:为防止新增脏数据
  • token机制:防止页面重复提交
  • traceID:操作是唯一的
token机制实现幂等原理

业务要求:页面的数据只能被点击提交一次;

发生原因:由于重复点击或者网络重发,或者 nginx 重发等情况会导致数据被重复提交;

解决办法:

  • 集群环境:采用 token 加 redis(redis 单线程的,处理需要排队);
  • 单 JVM 环境:采用 token 加 redis 或 token 加 jvm 锁。

处理流程:

  1. 数据提交前要向服务的申请 token,token 放到 redis 或 jvm 内存,token 有效时间;
  2. 提交后后台校验 token,同时删除 token,生成新的 token 返回。

token 特点:要申请,一次有效性,可以限流。

注意:redis 要用删除操作来判断 token,删除成功代表 token 校验通过。

对外提供的API如何保证幂等

举例说明: 银联提供的付款接口–需要接入商户提交付款请求时附带:source 来源,seq 序列号

source+seq 在数据库里面做唯一索引,防止多次付款(并发时,只能处理一个请求)

重点:对外提供接口为了支持幂等调用,接口有两个字段必须传,一个是来源 source,一个是来源方序列号 seq,这个两个字段在提供方系统里面做联合唯一索引,这样当第三方调用时,先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。

为了幂等友好,一定要先查询一下,是否处理过该笔业务,不查询直接插入业务系统,会报错,但实际已经处理

双写一致性问题解决

  1. 先更新缓存,再更新数据库。(不可取,容易出现脏数据)
  2. 先更新数据库,再更新缓存。(同上,不可取)
  3. 先删除缓存,再更新数据库。(只能采用延时双删策略解决缓存为脏数据问题,但还是有可能存在脏数据(第二次删除缓存失败))
  4. 先更新数据库,再删除缓存。(可取,有问题待解决)

认证 (Authentication) 和授权 (Authorization)

  • Authentication(认证) 是验证您的身份的凭据(例如用户名/用户ID和密码),通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以,Authentication 被称为身份/用户验证。

  • Authorization(授权) 发生在 Authentication(认证) 之后。授权,它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如admin,有些对系统资源操作比如删除、添加、更新只能特定人才具有。

这两个一般在我们的系统中被结合在一起使用,目的就是为了保护我们系统的安全性。

  • Session 的主要作用就是通过服务端记录用户的状态。 典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了。

  • Cookie 数据保存在客户端(浏览器端),Session 数据保存在服务器端。相对来说 Session 安全性更高。如果使用 Cookie 的一些敏感信息不要写入 Cookie 中,最好能将 Cookie 信息加密然后使用到的时候再去服务器端解密

如何使用Session进行身份验证

很多时候我们都是通过 SessionID 来实现特定的用户,SessionID 一般会选择存放在 Redis 中。

举个例子:用户成功登陆系统,然后返回给客户端具有 SessionID 的 Cookie,当用户向后端发起请求的时候会把 SessionID 带上,这样后端就知道你的身份状态

  • 用户向服务器发送用户名和密码用于登陆系统。
  • 服务器验证通过后,服务器为用户创建一个 Session,并将 Session信息存储 起来。
  • 服务器向用户返回一个 SessionID,写入用户的 Cookie。
  • 当用户保持登录状态时,Cookie 将与每个后续请求一起被发送出去。
  • 服务器可以将存储在 Cookie 上的 Session ID 与存储在内存中或者数据库中的 Session 信息进行比较,以验证用户的身份,返回给用户客户端响应信息的时候会附带用户当前的状态

使用 Session 的时候需要注意下面几个点:

  1. 依赖Session的关键业务一定要确保客户端开启了Cookie
  2. 注意Session的过期时间

分布式架构下,Session 共享方案

  • 不要有session:在某些场景下,是可以没有session的,其实在很多接口类系统当中,都提倡【API无状态服务】;也就是每一次的接口访问,都不依赖于session、不依赖于前一次的接口访问;
  • 存入cookie:将session存储到cookie中,但是缺点很明显:例如每次请求都得带着session,数据存储在客户端本地,是有风险的;
  • session同步:对个服务器之间同步session,这样可以保证每个服务器上都有全部的session信息,不过当服务器数量比较多的时候,同步是会有延迟甚至同步失败
  • 使用Nginx(负载均衡软硬件)中的ip绑定策略:同一个ip只能在指定的同一个机器访问,但是这样做风险也比较大,而且也是去了负载均衡的意义;
  • 把session放到Redis中存储:虽然架构上变得复杂,并且需要多访问一次Redis;但优点也很明显:

为什么Cookie 无法防止CSRF攻击,而token可以

CSRF(Cross Site Request Forgery)一般被翻译为 跨站请求伪造 。那么什么是 跨站请求伪造 呢?说简单用你的身份去发送一些对你不友好的请求

举个简单的例子:小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子下面有一个链接写着“科学理财,年盈利率过万”,小壮好奇的点开了这个链接,结果发现自己的账户少了10000元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行发出请求。

其原理是:进行Session 认证的时候,我们一般使用 Cookie 来存储 SessionId,当我们登陆后后端生成一个SessionId放在Cookie中返回给客户端,服务端通过Redis或者其他存储工具记录保存着这个Sessionid,客户端登录以后每次请求都会带上这个SessionId,服务端通过这个SessionId来标示你这个人。如果别人通过 cookie拿到了 SessionId 后就可以代替你的身份访问系统

Session 认证中 Cookie 中的 SessionId是由浏览器发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。

但是,使用 token 的话不会存在这个问题:在我们登录成功获得 token 之后,一般会选择存放在 local storage 中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个 token,这样就不会出现 CSRF 漏洞的问题。因为,即使有个你点击了非法链接发送了请求到服务端,这个非法请求是不会携带 token 的,所以这个请求将是非法的。

Mybatis / Mybatis-Plus

中间件

Nacos

RabbitMQ

如何确保消息发送和消息接收

消息发送确认:

  • ConfirmCallback方法:ConfirmCallback 是一个回调接口,消息发送到 Broker 后触发回调,确认消息是否到达 Broker 服务器,也就是只确认是否正确到达 Exchange 中
  • ReturnCallback方法:通过实现 ReturnCallback 接口,启动消息失败返回,此接口是在交换器路由不到队列时触发回调,该方法可以不使用,因为交换器和队列是在代码里绑定的,如果消息成功投递到 Broker 后几乎不存在绑定队列失败,除非代码写错

消息接收确认:

  • RabbitMQ 消息确认机制(ACK)默认是自动确认的,自动确认会在消息发送给消费者后立即确认
  • 但存在丢失消息的可能,如果消费端消费逻辑抛出异常,假如你用回滚了也只是保证了数据的一致性,但是消息还是丢了,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。

消息确认模式

  • AcknowledgeMode.NONE:自动确认
  • AcknowledgeMode.AUTO:根据情况确认
  • AcknowledgeMode.MANUAL:手动确认

消费者收到消息后,手动调用 Basic.Ack 或 Basic.Nack 或 Basic.Reject 后,RabbitMQ 收到这些消息后,才认为本次投递完成。

  • Basic.Ack 命令:用于确认当前消息。
  • Basic.Nack 命令:用于否定当前消息(注意:这是AMQP 0-9-1的RabbitMQ扩展) 。
  • Basic.Reject 命令:用于拒绝当前消息。

Nack,Reject后都有能力要求是否requeue消息或者进入死信队列

事务消息原理

事务V.S确认

确认是对一件事的确认

事务是对批量的确认

增删改查中,事务是对于增删改的保证

发送方事务

开启事务,发送多条数据,事务提交或回滚是原子的,要么都提交,要么都回滚

消费方事务

消费方是读取行为,那么事务体现在哪里呢

rabbitmq的消费行为会触发queue中msg的是否删除、是否重新放回队列等行为,类增删改

所以,消费方的ack是要手动提交的,且最终确定以事务的提交和回滚决定

死信队列 和 延时队列

死信队列

DLX(Dead Letter Exchange),死信交换器

当队列中的消息被拒绝、或者过期会变成死信,死信可以被重新发布到另一个交换器,这个交换器就是DLX,与DLX绑定的队列称为死信队列。
造成死信的原因:

  • 信息被拒绝
  • 信息超时
  • 超过了队列的最大长度

过期消息

在 rabbitmq 中存在2种方可设置消息的过期时间,

第一种通过对队列进行设置,这种设置后,该队列中所有的消息都存在相同的过期时间,

第二种通过对消息本身进行设置,那么每条消息的过期时间都不一样。

如果同时使用这2种方法,那么以过期时间小的那个数值为准。当消息达到过期时间还没有被消费,那么那个消息就成为了一个 死信 消息。

  • 队列设置:在队列申明的时候使用 x-message-ttl 参数,单位为 毫秒

  • 单个消息设置:是设置消息属性的 expiration 参数的值,单位为 毫秒

延迟队列

延迟队列存储的是延迟消息

延迟消息指的是,当消息被发布出去之后,并不立即投递给消费者,而是在指定时间之后投递。如:

在订单系统中,订单有30秒的付款时间,在订单超时之后在投递给消费者处理超时订单。

rabbitMq没有直接支持延迟队列,可以通过死信队列实现。

在死信队列中,可以为普通交换器绑定多个消息队列,假设绑定过期时间为5分钟,10分钟和30分钟,3个消息队列,然后为每个消息队列设置DLX,为每个DLX关联一个死信队列。

当消息过期之后,被转存到对应的死信队列中,然后投递给指定的消费者消费。

Kafka

架构设计

KAFKA天生是分布式的,满足AKF的XYZ轴特点,扩展性,可靠性,高性能;而且,kafka具备自己的特色,比如动态ISR集合,是在强一致性,过半一致性之外的另一个实现手段

  • broker:Kafka 集群包含一个或多个服务器,服务器节点称为broker。

    • broker存储topic的数据。如果某topic有N个partition,集群有N个broker,那么每个broker存储该topic的一个partition。
    • 如果某topic有N个partition,集群有(N+M)个broker,那么其中有N个broker存储该topic的一个partition,剩下的M个broker不存储该topic的partition数据。
    • 如果某topic有N个partition,集群中broker数目少于N个,那么一个broker存储该topic的一个或多个partition。在实际生产环境中,尽量避免这种情况的发生,这种情况容易导致Kafka集群数据不均衡。
  • Topic

    • Topic 类似于数据库的表名,每条发布到Kafka集群的消息都有一个类别,这个类别就是Topic。(物理上不同Topic的消息分开存储,逻辑上一个Topic的消息虽然保存于一个或多个broker上但用户只需指定消息的Topic即可生产或消费数据而不必关心数据存于何处)
  • Partition

    • topic中的数据分割为一个或多个partition。
    • 每个topic至少有一个partition。每个partition中的数据使用多个segment文件存储。
    • partition中的数据是有序的,不同partition间的数据丢失了数据的顺序。如果topic有多个partition,消费数据时就不能保证数据的顺序。在需要严格保证消息的消费顺序的场景下,需要将partition数目设为1。
  • Producer

    • 生产者即数据的发布者,该角色将消息发布到Kafka的topic中。
    • broker接收到生产者发送的消息后,broker将该消息追加到当前用于追加数据的segment文件中。
    • 生产者发送的消息,存储到一个partition中,生产者也可以指定数据存储的partition。
  • Consumer

    • 消费者可以从broker中读取数据。消费者可以消费多个topic中的数据。
  • Consumer Group

    • 每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)
    • 这是kafka用来实现一个topic消息的广播(发给所有的consumer)和单播(发给任意一个consumer)的手段。
    • 一个topic可以有多个CG。topic的消息会复制给consumer。
      • 如果需要实现广播,只要每个consumer有一个独立的CG就可以了。
      • 要实现单播只要所有的consumer在同一个CG。
    • 用CG还可以将consumer进行自由的分组而不需要多次发送消息到不同的topic。
  • Leader

    • 每个partition有多个副本,其中有且仅有一个作为Leader,Leader是当前负责数据的读写的partition。
  • Follower

    • Follower跟随Leader,所有写请求都通过Leader路由,数据变更会广播给所有Follower,Follower与Leader保持数据同步。
    • 如果Leader失效,则从Follower中选举出一个新的Leader。
    • 当Follower与Leader挂掉、卡住或者同步太慢,leader会把这个follower从“in sync replicas”(ISR)列表中删除,重新创建一个Follower。
  • Offset

    • kafka的存储文件都是按照offset.kafka来命名,用offset做名字的好处是方便查找。例如你想找位于2049的位置,只要找到2048.kafka的文件即可。当然the first offset就是00000000000.kafka

消息丢失场景

  • 生产者在生产过程中的消息丢失
  • broker在故障后的消息丢失
  • 消费者在消费过程中的消息丢失

ACK机制

ack有3个可选值,分别是1,0,-1

  • ack=0:生产者在生产过程中的消息丢失

简单来说就是,producer发送一次就不再发送了,不管是否发送成功。

  • ack=1:broker在故障后的消息丢失

简单来说就是,producer只要收到一个分区副本成功写入的通知就认为推送消息成功了。

这里有一个地方需要注意,这个副本必须是leader副本。只有leader副本成功写入了,producer才会认为消息发送成功。

注意,ack的默认值就是1。这个默认值其实就是吞吐量与可靠性的一个折中方案。生产上我们可以根据实际情况进行调整,比如如果你要追求高吞吐量,那么就要放弃可靠性。

  • ack=-1:生产侧和存储侧不会丢失数据

简单来说就是,producer只有收到分区内所有副本的成功写入的通知才认为推送消息成功了。

Offset机制

kafka消费者的三种消费语义

  • at-most-once:最多一次,可能丢数据

  • at-least-once:最少一次,可能重复消费数据

  • exact-once message:精确一次

rebalance机制

消费者分区分配策略

  • Range 范围分区(默认的)

  • RoundRobin 轮询分区

  • Sticky策略

触发 Rebalance 的时机

Rebalance 的触发条件有3个。

  • 组成员个数发生变化。例如有新的 consumer 实例加入该消费组或者离开组。
  • 订阅的 Topic 个数发生变化。
  • 订阅 Topic 的分区数发生变化。

Coordinator协调过程

  • 消费者如何发现协调者

  • 消费者如何确定分配策略

  • 如果需要再均衡分配策略的影响

Kafka的pull模式

Kafka最初考虑的问题是,customer应该从brokes拉取消息 还是 brokers将消息推送到consumer,也就是pull还是push

最终Kafka还是选取了传统的pull模式:producer将消息推送到broker,consumer从broker拉取消息

PULL模式优点:consumer可以自主决定是否批量的从broker拉取数据,可以根据自己的消费能力去决定消费策略

PULL模式缺点:如果broker没有可供消费的消息,将导致consumer不断在循环中轮询,直到新消息到达

为了避免这个缺点,Kafka有个参数可以让consumer阻塞知道新消息到达(当然也可以阻塞知道消息的数量达到某个特定的量这样就可以批量发

Kafka中zk的作用

Zookeeper是分布式协调,不是数据库

  • kafka中使用了zookeeper的分布式锁和分布式配置及统一命名的分布式协调解决方案

  • 在kafka的broker集群中的controller的选择,是通过zk的临时节点争抢获得的

  • brokerID等如果自增的话也是通过zk的节点version实现的全局唯一

  • kafka中broker中的状态数据也是存储在zk中

不过这里要注意,zk不是数据库,所以存储的属于元数据

Kafka的高性能如何保障

  1. 性能的最大瓶颈依然是IO,这个是不能逾越的鸿沟

  2. broker在持久化数据的时候已经最大努力的使用了磁盘的顺序读写,更进一步的性能优化是零拷贝的使用,也就是从磁盘日志到消费者客户端的数据传递,因为kafka是mq,对于msg不具备加工处理,所以得以实现

  3. 做tradeoff,在 速度 与 可用性/可靠性 中挣扎—— ACK的0,1,-1级别就是在性能和可靠中权衡

Elesticsearch

Nginx

ZooKeeper

数据模型

ZooKeeper的数据模型,在结构上和标准文件系统的非常相似,拥有一个层次的命名空间,都是采用树形层次结构,ZooKeeper树中的每个节点被称为—Znode。

  • Znode兼具文件和目录两种特点。既像文件一样维护着数据、元信息、ACL、时间戳等数据结构,又像目录一样可以作为路径标识的一部分,并可以具有子Znode。用户对Znode具有增、删、改、查等操作(权限允许的情况下)

  • Znode具有原子性操作,读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据。另外,每一个节点都拥有自己的ACL(访问控制列表),这个列表规定了用户的权限,即限定了特定用户对目标节点可以执行的操作

  • Znode存储数据大小有限制。ZooKeeper虽然可以关联一些数据,但并没有被设计为常规的数据库或者大数据存储,相反的是,它用来管理调度数据,比如分布式应用中的配置文件信息、状态信息、汇集位置等等。这些数据的共同特性就是它们都是很小的数据,通常以KB为大小单位。ZooKeeper的服务器和客户端都被设计为严格检查并限制每个Znode的数据大小至多1M,当时常规使用中应该远小于此值

  • Znode通过路径引用,如同Unix中的文件路径。路径必须是绝对的,因此他们必须由斜杠字符来开头。除此以外,他们必须是唯一的,也就是说每一个路径只有一个表示,因此这些路径不能改变。在ZooKeeper中,路径由Unicode字符串组成,并且有一些限制。字符串”/zookeeper”用以保存管理信息,比如关键配额信息。

节点类型

Znode有两种,分别为临时节点和永久节点;节点的类型在创建时即被确定,并且不能改变

在ZooKeeper中,每个数据节点都是有生命周期的,其生命周期的长短取决于数据节点的节点类型

  • 临时节点:该节点的生命周期依赖于创建它们的会话。一旦会话结束,临时节点将被自动删除,当然可以也可以手动删除。临时节点不允许拥有子节点
  • 永久节点:该节点的生命周期不依赖于会话,并且只有在客户端显示执行删除操作的时候,他们才能被删除

Zookeeper状态

  • Disconnected:客户端是断开连接的状态,不能连接服务集合中的任意一个
  • SyncConnected:客户端是连接状态,连接其中的一个服务
  • AuthFailed:鉴权失败
  • ConnectedReadOnly:客户端连接只读的服务器
  • SaslAuthenticated:SASL认证
  • Expired:服务器已经过期了该客户端的Session

Zookeeper事件类型

  • None:无
  • NodeCreated:节点创建
  • NodeDeleted:节点删除
  • NodeDataChanged:节点数据改变
  • NodeChildrenChanged:子节点改变(添加/删除)

watch机制

ZooKeeper是用来协调(同步)分布式进程的服务,提供了一个简单高性能的协调内核,用户可以在此之上构建更多复杂的分布式协调功能。

多个分布式进程通过ZooKeeper提供的API来操作共享的ZooKeeper内存数据对象ZNode来达成某种一致的行为或结果,这种模式本质上是基于状态共享的并发模型,与Java的多线程并发模型一致,他们的线程或进程都是”共享式内存通信“。

Java没有直接提供某种响应式通知接口来监控某个对象状态的变化,只能要么浪费CPU时间毫无响应式的轮询重试,或基于Java提供的某种主动通知(Notif)机制(内置队列)来响应状态变化,但这种机制是需要循环阻塞调用。

而ZooKeeper实现这些分布式进程的状态(ZNode的Data、Children)共享时,基于性能的考虑采用了类似的异步非阻塞的主动通知模式即Watch机制,使得分布式进程之间的“共享状态通信”更加实时高效,其实这也是ZooKeeper的主要任务决定的—协调。Consul虽然也实现了Watch机制,但它是阻塞的长轮询。

Watch特性

  1. Watch是一次性的,每次都需要重新注册,并且客户端在会话异常结束时不会收到任何通知,而快速重连接时仍不影响接收通知。
  2. Watch的回调执行都是顺序执行的,并且客户端在没有收到关注数据的变化事件通知之前是不会看到最新的数据,另外需要注意不要在Watch回调逻辑中阻塞整个客户端的Watch回调
  3. Watch是轻量级的,WatchEvent是最小的通信单元,结构上只包含通知状态、事件类型和节点路径。ZooKeeper服务端只会通知客户端发生了什么,并不会告诉具体内容

Watcher使用的注意事项

  1. Watcher是一次触发器,假如需要持续监听数据变更,需要在每次获取时设置Watcher
  2. 会话过期:当客户端会话过期时,该客户端注册的Watcher会失效
  3. 事件丢失:在接收通知和注册监视点之间,可能会丢失事件,但Zookeeper的状态变更和数据变化,都会记录在状态元数据信息和ZK数据节点上,所以能够获取最终一致的ZK信息状态
  4. 避免Watcher过多:服务器会对每一个注册Watcher事件的客户端发送通知,通知通过Socket连接的方式发送,当Watcher过多时,会产生一个尖峰的通知

命名服务

  1. 通过使用有序节点的特性做到协调命名规则
  2. 通过zk的事务ID递增,做到有序行命名规则
  3. 通过使用自己点做map映射,做到1:N的命名映射,比如DNS
  4. 顺序关系、映射关系

配置管理

配置、元数据、状态等语义可以通过ZK的节点1MB存储,或者通过zk的节点目录结构特性存储,并且通过watch机制,满足配置变化的全局通知能力

集群管理

  1. 通过zk的排他性,有序性

  2. 满足分布式锁、分布式选主、队列锁

  3. 串行化回调调度

  4. 分布式调度等

xxljob

数据库

MySQL

主从复制

MySQL 主从复制是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。MySQL 默认采用异步复制方式,这样从节点不用一直访问主服务器来更新自己的数据,数据的更新可以在远程连接上进行,从节点可以复制主数据库中的所有数据库或者特定的数据库,或者特定的表。

主从复制原理

  • 从库生成两个线程,一个I/O线程,一个SQL线程;
  • I/O线程会去请求主库的binlog,并将得到的binlog写到本地的relay-log(中继日志)文件中;
  • 主库会生成一个log dump线程,用来给从库I/O线程传binlog;
  • SQL线程,会读取relay log文件中的日志,并解析成sql语句逐一执行

分库分表

索引

原理:通过不断地缩小想要获取数据的范围来筛选出最终想要的结果,同时把随机的事件变成顺序的事件

B+Tree是mysql使用最频繁的一个索引数据结构,是InnoDB和MyISAM存储引擎模式的索引类型。

聚簇和非聚簇索引

区分聚簇索引和非聚簇索引非常简单,只要判断数据跟索引是否存储在一起就可以了,跟数据绑定在一起的就是聚簇索引

索引结构

mysql中使用较多的索引有hash索引,B+树索引,innodb的索引实现为B+树,memory存储引擎为hash索引

B+树是一个平衡的多叉树,从根节点到每个叶子节点的高度差值不超过1,而且同层级的二节点间有指针相关连接,在B+树上的常规检索,从根节点到叶子节点的搜索效率基本相当,不会出现大幅波动,而且基于索引的顺序扫描时,也可以利用双向指针快速左右移动,效率非常高。因为,B+树索引被广泛应用于数据库、文件系统等场景。

哈希索引就是采用一定的哈希算法,把键值换算成新的哈希值,检索时不需要类似B+树那样从根节点到叶子节点逐级查找,只需一次哈希算法即可立刻定位到相应的位置,速度非常快。

索引的设计原则

1、适合索引的列是出现在where字句中的列,或者连接子句中指定的列

2、基数较小的表,索引效果差,没必要创建索引

3、在选择索引列的时候,越短越好,可以指定某些列的一部分,没必要用全部字段的值

4、不要给表中的每一个字段都创建索引,并不是索引越多越好

5、定义有外键的数据列一定要创建索引

6、更新频繁的字段不要有索引

7、创建索引的列不要过多,可以创建组合索引,但是组合索引的列的个数不建议太多

8、大文本、大对象不要创建索引

索引类型

  • 普通索引:允许被索引的数据列包含重复的值
  • 唯一索引:可以保证数据记录的唯一性
  • 主键索引:是一种特殊的唯一索引,在一张表中只能定义一个主键索引,主键用于唯一标识一条记录,使用关键字primary key来创建
  • 联合索引:索引可以覆盖多个数据列
  • 全文索引:通过建立倒排索引,可以极大的提升检索效率,解决判断字段是否包含的问题,是目前搜索引擎使用的一种关键技术

索引对数据库的影响

优点:

  1. 索引可以极大地提高数据的查询速度

  2. 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能

缺点:

  1. 会降低插入、删除、更新表的速度,因为在执行这些写操作的时候,还要操作索引文件

  2. 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要简历聚簇索引,那么需要的空间就会更大,如果非聚簇索引很多,一旦聚簇索引改变,那么所有非聚簇索引都会跟着变

如何确定索引被使用

  1. 查看慢查询日志,看是否有索引的查询
  2. 查看数据库统计信息
  3. 查看数据库优化器的计划
  4. 查看数据库系统表

mysql锁的类型

属性分类:

  • 共享锁:又称读锁,简称 S 锁,支持并发的读取数据,读取数据的时候不支持修改,避免出现重复读的问题
  • 排他锁:又称写锁,简称 X 锁,在数据修改时候,不允许其他人同时修改,也不允许其他人读取,避免了出现脏数据和脏读的问题

粒度分类:

  • 行级锁(innodb )
  • 表级锁( innodb 、myisam)
  • 页级锁( innodb引擎)
  • 记录锁:记录锁属于行锁的一种,只不过范围是表中的某一条记录,记录锁是说事务在加锁后锁住的只是表的某一条记录,加了记录锁之后数据可以避免数据在查询的时候被修改的重复读问题,也避免了在修改的事务未提交前被其他事务读取的脏读问题
  • 间隙锁:是行锁的一种,间隙锁是在事务加锁后其锁住的是表记录的某一个区间,当表的相邻ID之间出现空隙则会形成一个区间,遵循左开右闭原则。范围查询并且查询未命中记录,查询条件必须命中索引、间隙锁只会出现在REPEATABLE_READ(重复读)的事务级别中
  • 临键锁:行锁的一种,是INNODB的行锁默认算法,临键锁会把查询出来的记录锁住,同时也会把该范围查询内的所有间隙空间也会锁住,再之它会把相邻的下一个区间也会锁住

状态分类:

  • 意向共享锁
  • 意向排它锁

隔离级别

REPEATABLE READ 可重复读
MySQL数据库默认的隔离级别。该级别解决了READ UNCOMMITTED隔离级别导致的问题。它保证同一事务的多个实例在并发读取事务时,会“看到同样的”数据行。不过,这会导致另外一个棘手问题“幻读”。InnoDB和Falcon存储引擎通过多版本并发控制机制解决了幻读问题。
READ COMMITTED 读取提交内容
大多数数据库系统的默认隔离级别(但是不是MySQL的默认隔离级别),满足了隔离的早先简单定义:一个事务开始时,只能“看见”已经提交事务所做的改变,一个事务从开始到提交前,所做的任何数据改变都是不可见的,除非已经提交。这种隔离级别也支持所谓的“不可重复读”。这意味着用户运行同一个语句两次,看到的结果是不同的。
READ UNCOMMITTED 读取未提交内容
在这个隔离级别,所有事务都可以“看到”未提交事务的执行结果。在这种级别上,可能会产生很多问题,除非用户真的知道自己在做什么,并有很好的理由选择这样做。本隔离级别很少用于实际应用,因为它的性能也不必其他性能好多少,而别的级别还有其他更多的优点。读取未提交数据,也被称为“脏读”
SERIALIZABLE 可串行化
该级别是最高级别的隔离级。它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简而言之,SERIALIZABLE是在每个读的数据行上加锁。在这个级别,可能导致大量的超时Timeout和锁竞争Lock Contention现象,实际应用中很少使用到这个级别,但如果用户的应用为了数据的稳定性,需要强制减少并发的话,也可以选择这种隔离级。

数据处理问题

  • 脏读:脏读是指一个事务读取了未提交事务执行过程中的数据。(查到了未提交的数据)
    当一个事务的操作正在多次修改数据,而在事务还未提交的时候,另外一个并发事务来读取了数据,就会导致读取到的数据并非是最终持久化之后的数据,这个数据就是脏读的数据。

  • 不可重复读:不可重复读是指对于数据库中的某个数据,一个事务执行过程中多次查询返回不同查询结果,这就是在事务执行过程中,数据被其他事务提交修改了。(查的数据被多次修改,导致每次查都不一样)

不可重复读同脏读的区别在于,脏读是一个事务读取了另一未完成的事务执行过程中的数据,而不可重复读是一个事务执行过程中,另一事务提交并修改了当前事务正在读取的数据。

  • 虚读(幻读):幻读是事务非独立执行时发生的一种现象,例如事务T1批量对一个表中某一列列值为1的数据修改为2的变更,但是在这时,事务T2对这张表插入了一条列值为1的数据,并完成提交。此时,如果事务T1查看刚刚完成操作的数据,发现还有一条列值为1的数据没有进行修改,而这条数据其实是T2刚刚提交插入的,这就是幻读。

幻读和不可重复读都是读取了另一条已经提交的事务(这点同脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。

ACID是靠什么保证的

原子性由undolog日志来保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql

一致性是由其他三大特性保证,程序代码要保证业务上的一致性

隔离性是由MVCC来保证

持久性由redolog来保证,mysql修改数据的时候会在redolog中记录一份日志数据,就算数据没有保存成功,只要日志保存成功了,数据仍然不会丢失

MVCC

MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存

MVCC在MySQL InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读

MVCC实现原理

mvcc的实现原理主要依赖于记录中的 三个隐藏字段,undolog,read view来实现的

三个隐藏字段
  • DB_TRX_ID:6字节,最近修改事务id,记录创建这条记录或者最后一次修改该记录的事务id
  • DB_ROLL_PTR:7字节,回滚指针,指向这条记录的上一个版本,用于配合undolog,指向上一个旧版本
  • DB_ROW_JD:6字节,隐藏的主键,如果数据表没有主键,那么innodb会自动生成一个6字节的row_id

DB_ROW_ID是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID是当前操作该记录的事务ID,DB_ROLL_PTR是一个回滚指针,用于配合undo日志,指向上一个旧版本

undo log

undolog被称之为回滚日志,表示在进行insert,delete,update操作的时候产生的方便回滚的日志

不同事务或者相同事务的对同一记录的修改,会导致该记录的undolog生成一条记录版本线性表,即链表,undolog的链首就是最新的旧记录,链尾就是最早的旧记录

Read View

Read View的最大作用是用来做可见性判断,当某个事务在执行快照读的时候,对该记录创建一个Read View的视图,把它当作条件去判断当前事务能够看到哪个版本的数据,有可能读取到的是最新的数据,也有可能读取的是当前行记录的undolog中某个版本的数据

当前读、快照读、MVCC关系

  • 当前读:读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
  • 快照读:即不加锁的非阻塞读,快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;
  • MVCC多版本并发控制指的是维持一个数据的多个版本,使得读写操作没有冲突,快照读是MySQL为实现MVCC的一个非阻塞读功能。MVCC模块在MySQL中的具体实现是由三个隐式字段,undo日志、read view三个组件来实现的。

RC、RR级别下的InnoDB快照读有什么不同

因为Read View生成时机的不同,从而造成RC、RR级别下快照读的结果的不同

在RC隔离级别下,是每个快照读都会生成并获取最新的Read View

在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View,之后的快照读获取的都是同一个Read View.

MVCC解决什么问题

数据库并发场景有三种,分别为:

​ 1、读读:不存在任何问题,也不需要并发控制

​ 2、读写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读、幻读、不可重复读

​ 3、写写:有线程安全问题,可能存在更新丢失问题

​ MVCC是一种用来解决读写冲突的无锁并发控制,也就是为事务分配单项增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照,所以MVCC可以为数据库解决一下问题:

​ 1、在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能

​ 2、解决脏读、幻读、不可重复读等事务隔离问题,但是不能解决更新丢失问题

Myisam和Innodb的区别

InnoDB存储引擎:

主要面向OLTP(Online Transaction Processing,在线事务处理)方面的应用,是第一个完整支持ACID事务的存储引擎(BDB第一个支持事务的存储引擎,已经停止开发)。

  1. 支持行锁
  2. 支持外键
  3. 支持自动增加列AUTO_INCREMENT属性
  4. 支持事务
  5. 支持MVCC模式的读写
  6. 读的效率低于MYISAM
  7. 写的效率高优于MYISAM
  8. 适合频繁修改以及设计到安全性较高的应用
  9. 清空整个表的时候,Innodb是一行一行的删除

MyISAM存储引擎:

是MySQL官方提供的存储引擎,主要面向OLAP(Online Analytical Processing,在线分析处理)方面的应用。

  1. 独立于操作系统,当建立一个MyISAM存储引擎的表时,就会在本地磁盘建立三个文件,例如我建立tb_demo表,那么会生成以下三个文件tb_demo.frm,tb_demo.MYD,tb_demo.MYI
  2. 不支持事务,
  3. 支持表锁和全文索引
  4. MyISAM存储引擎表由MYD和MYI组成,MYD用来存放数据文件,MYI用来存放索引文件。MySQL数据库只缓存其索引文件,数据文件的缓存交给操作系统本身来完成;
  5. MySQL5.0版本开始,MyISAM默认支持256T的单表数据;
  6. 选择密集型的表:MYISAM存储引擎在筛选大量数据时非常迅速,这是他最突出的优点
  7. 读的效率优于InnoDB,写的效率低于InnoDB
  8. 适合查询以及插入为主的应用
  9. 清空整个表的时候,MYISAM则会新建表

redis

持久化

持久化指将Redis数据库中的数据保存到磁盘上,以便Redis数据库重启后恢复数据

AOF

将Redis数据库中每一条 写操作 记录到磁盘上

RDB

设置时间间隔或操作次数,将时间间隔内的操作打包成快照,存储到磁盘上

redis的过期删除策略

Redis淘汰过期的keys

Redis keys过期有两种方式:被动和主动方式

具体就是Redis每秒10次做的事情:

  1. 测试随机的20个keys进行相关过期检测。
  2. 删除所有已经过期的keys。
  3. 如果有多于25%的keys过期,重复步奏1.

这是一个平凡的概率算法,基本上的假设是,我们的样本是这个密钥控件,并且我们不断重复过期检测,直到过期的keys的百分比低于25%,这意味着,在任何给定的时刻,最多会清除1/4的过期keys

复制AOF文件时如何处理过期

为了获得正确的行为而不牺牲一致性,当一个key过期,DEL将会随着AOF文字一起合成到所有附加的slaves。

在master实例中,这种方法是集中的,并且不存在一致性错误的机会。

当slaves连接到master时,不会独立过期keys(会等到master执行DEL命令),他们任然会在数据集里面存在,所以当slave当选为master时淘汰keys会独立执行,然后成为master。

Redis线程模型,为什么单线程这么快

IO模型维度的特征

IO模型使用了多路复用器,在linux系统中使用的是EPOLL

类似netty的BOSS,WORKER使用一个EventLoopGroup(threads=1)

单线程的Reactor模型,每次循环取socket中的命令然后逐一操作,可以保证socket中的指令是按顺序的,不保证不同的socket也就是客户端的命令的顺序性

命令操作在单线程中顺序操作,没有多线程的困扰不需要锁的复杂度,在操作数据上相对来说是原子性质的

架构设计模型

自身的内存存储数据,读写操作不设计磁盘IO

redis除了提供了Value具备类型还为每种类型实现了一些操作命令

实现了计算向数据移动,而非数据想计算移动,这样在IO的成本上有一定的优势

且在数据结构类型上,丰富了一些统计类属性,读写操作中,写操作会O(1)负载度更新length类属性,使得读操作也是O(1)的

缓存雪崩、缓存穿透、缓存击穿实际处理

缓存穿透

缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

解决方案

有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

缓存击穿

对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。

缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

解决方案

缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。

这里分享一个简单方案就是将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存雪崩

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

解决方案

  • 使用互斥锁(mutex key)

业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。在redis2.6.1之前版本未实现setnx的过期时间

  • “提前”使用互斥锁(mutex key):

在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。

  • “永远不过期”:

这里的“永远不过期”包含两层意思:

  1. 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。

  2. 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期

从实战看,这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,但是对于一般的互联网功能来说这个还是可以忍受。

总结

穿透:缓存不存在,数据库不存在,高并发,少量key

击穿:缓存不存在,数据库存在,高并发,少量key

雪崩:缓存不存在,数据库存在,高并发,大量key

语义有些许差异,但是,都可以使用限流的互斥锁,保障数据库的稳定

Redis集群

常见集群分类

  • 主从复制集群

    • 主从复制集群,手动切换
    • 带有哨兵的HA的主从复制集群
  • 分片集群

    • 客户端实现路由索引的分片集群
    • 使用中间件代理层的分片集群
    • redis自身实现的cluster分片集群

Redis主从复制

主从复制机制

  • 当一个 master 实例和一个 slave 实例连接正常时, master 会发送一连串的命令流来保持对 slave 的更新,以便于将自身数据集的改变复制给 slave :包括客户端的写入、key 的过期或被逐出等等。

  • 当 master 和 slave 之间的连接断开之后,因为网络问题、或者是主从意识到连接超时, slave 重新连接上 master 并会尝试进行部分重同步:这意味着它会尝试只获取在断开连接期间内丢失的命令流。

  • 当无法进行部分重同步时, slave 会请求进行全量重同步。这会涉及到一个更复杂的过程,例如 master 需要创建所有数据的快照,将之发送给 slave ,之后在数据集更改时持续发送命令流到 slave

主从复制关注点

  • Redis 使用异步复制,slave 和 master 之间异步地确认处理的数据量
  • slave 可以接受其他 slave 的连接
  • Redis 复制在 master 侧是非阻塞的。这意味着 master 在一个或多个 slave 进行初次同步或者是部分重同步时,可以继续处理查询请求
  • 复制在 slave 侧大部分也是非阻塞的
  • 复制可以被用在可伸缩性,以便只读查询可以有多个 slave 进行(例如 O(N) 复杂度的慢操作可以被下放到 slave ),或者仅用于数据安全
  • 也可以避免 master 将全部数据集写入磁盘造成的开销
  • 任何时候数据安全性都是很重要的,所以如果 master 使用复制功能的同时未配置持久化,那么自动重启进程这项应该被禁用

分布式锁redisson

设计模式

GIT

版本控制

分支合并

项目部署上线

Linux

Docker

前端了解

VUE

React