当前位置:   article > 正文

个人面试总结_shardingsphere 缓存大小

shardingsphere 缓存大小

一、JVM

1、内存溢出和内存泄露

定义:

内存泄漏memory leak :是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。

内存溢出 out of memory :指程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错OOM,即所谓的内存溢出。

关系:

内存泄露 → 剩余内存不足 → 后续申请不到足够内存 →内存溢出。

内存泄露产生场景:

​ 1、静态集合类:集合内对象无法被释放。

​ 2、集合里的对象属性值被改变。

​ 3、监听器。

​ 4、各种连接。

​ 5、外部模块的引用。

​ 6、单例模式。

2、内存模型

栈: 栈的优势是,存取速度比堆要快,仅次于寄存器,栈数据可以共享。但缺点是,存在栈中的数据大小与生存期必须是确定的,缺乏灵活性。每个线程包含一个栈区,栈中存放着局部变量表(用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型)、操作数栈、动态链接(指向运行时常量池的方法引用)、方法返回地址(方法正常退出或者异常退出的定义)、附件信息,对象都存放在堆区中。 栈是线程独享(私有)的。

堆:堆是一个运行时数据区。可以动态地分配内存大小,生存期也不必事先告诉编译器,因为它是在运行时动态分配内存的。缺点是,由于要在运行时动态分配内存,存取速度较慢。jvm只有一个堆区(heap)被所有线程共享,堆中主要存放实例变量、数组等,每个对象都包含一个与之对应的class的信息(class信息存放在方法区)。

jdk7之前堆内存逻辑上分为三部分:新生区+养老区+永久区;jdk8及之后内存逻辑上分为三部分:新生区+养老区+元空间;

3、GC Root

除了堆空间外的一些结构,比如 虚拟机栈、本地方法栈、方法区、字符串常量池 等地方对堆空间进行引用的,都可以作为GC Roots

  • 虚拟机栈中引用的对象
    • 比如:各个线程被调用的方法中使用到的参数、局部变量等。
  • 本地方法栈内JNI(通常说的本地方法)引用的对象方法区中类静态属性引用的对象
    • 比如:Java类的引用类型静态变量
  • 方法区中常量引用的对象
    • 比如:字符串常量池(string Table)里的引用
  • 所有被同步锁synchronized持有的对象
  • Java虚拟机内部的引用。
    • 基本数据类型对应的Class对象,一些常驻的异常对象(如:Nu11PointerException、outofMemoryError),系统类加载器。
  • 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

4、方法区

方法区是可供各线程共享的运行时内存区域,属于一种规范。

  • JDK1.6及之前存储了类的元数据信息、静态变量、即时编译器编译后的代码以及运行时常量池。
  • JDK1.7及以后将运行时常量池从方法区中移了出来,在JVM堆开辟了一块区域存放常量池。
  • 在Hotspot虚拟机1.8之前,以永久代的形式实现了方法区。
  • HotSpot虚拟机在1.8之后已经取消了永久代,改为元空间,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中。元空间没有使用堆内存,而是与堆不相连的本地内存区域。所以,理论上系统可以使用的内存有多大,元空间就有多大,所以不会出现永久代存在时的内存溢出问题。

垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型

方法区内常量池之中主要存放的两大类常量:字面量和符号引用字面量指文本字符串、被声明为final的常量值等。符号引用指类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。

常量的回收:没有被任何地方引用即可回收。

类型的回收:该类所有的实例都已经被回收,即Java堆中不存在该类及其任何派生子类的实例。加载该类的类加载器已经被回收、该类对应的java.lang.C1ass对象没有在任何地方被引用、无法在任何地方通过反射访问该类的方法,Java虚拟机被允许对满足上述三个条件的无用类进行回收。

类加载器回收很难达成,除非是经过精心设计的可替换类加载器的场景,如大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及oSGi。

5、为什么移除永久代

  • 融合HotSpot JVM与JRockit VM,JRockit没有永久代
  • 简化垃圾回收
  • 永久代的大小指定比较困难,太小容易出现永久代溢出,太大则容易导致老年代溢出
  • 元空间不使用虚拟机内存,使用本地内存

6、垃圾收集器

(1)CMS收集器(Concurrent Mark Sweep)

CMS收集器是缩短暂停应用时间为目标而设计的,针对老年代的收集,是基于标记-清除算法实现,整个过程分为4个步骤,包括:

  • 初始标记(Initial Mark)
  • 并发标记(Concurrent Mark)
  • 重新标记(Remark)
  • 并发清除(Concurrent Sweep)

其中,初始标记、重新标记这两个步骤仍然需要暂停应用线程。初始标记只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段是标记可回收对象,从GC Roots的直接关联对象开始遍历整个对象图,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作导致标记产生变动的那一部分对象的标记记录,这个阶段暂停时间比初始标记阶段稍长一点,但远比并发标记时间短。由于整个过程中消耗最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,CMS收集器内存回收与用户一起并发执行的,大大减少了暂停时间。

特点:

  • 并发执行
  • 对CPU资源压力大
  • 采用的标记清除算法会导致大量碎片(可采用基于Mark-Compact算法的Serial old回收器作为补偿措施,当碎片过多,执行FullGC进行整理)
  • 无法处理浮动垃圾,在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收
  • 不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行

备注:新生代可选择串行垃圾回收器、并行垃圾回收器或综合使用G1。

为何不使用标记整理算法:当并发清除的时候,不会STW,要保证用户线程能继续执行,前提的它运行的资源不受影响,如果Compact整理内存的话,原来的用户线程使用的内存将无法使用。

(2)G1收集器(Garbage First)

G1收集器将堆内存划分多个大小相等的独立区域(Region),并且能预测暂停时间,能预测原因它能避免对整个堆进行全区收集。G1跟踪各个Region里的垃圾堆积价值大小(所获得空间大小以及回收所需时间),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,从而保证了再有限时间内获得更高的收集效率。

G1收集器工作工程分为4个步骤,包括:

  • 初始标记(Initial Mark)
  • 并发标记(Concurrent Mark)
  • 最终标记(Final Mark)
  • 筛选回收(Live Data Counting and Evacuation)

其中,初始标记、最终标记这两个步骤仍然需要暂停应用线程,但初始标记耗时短,最终标记可以并行。初始标记与CMS一样,标记一下GC Roots能直接关联到的对象。并发标记从GC Root开始标记存活对象,这个阶段耗时比较长,但也可以与应用线程并发执行。而最终标记也是为了修正在并发标记期间因用户程序继续运作而导致标记产生变化的那一部分标记记录。最后在筛选回收阶段对各个Region回收价值和成本进行排序,根据用户所期望的GC暂停时间来执行回收。此阶段非并发,会暂停相关用户线程。

特点:

  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短Stop-the-world停顿的时间,部分其他收集器原来需要停顿Java线程执行的GC操作,G1收集器仍然可以通过并发的方式让Java程序继续运行。
  • 逻辑的分代收集:整个内存分区不存在物理上的年轻代和老年代的区别,也不需要完全独立的Survivor(to space)堆做复制准备,每个分区都可能随G1的运行在不同代之间前后切换。
  • 空间整合:与CMS的标记-清除算法不同,G1从整体来看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于复制算法实现的。但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对于CMS的一个优势,降低停顿时间是G1和CMS共同的关注点。该收集器是把整个堆(新生代、老年代)划分成多个固定大小的区域,每次根据允许停顿的时间去收集垃圾最多的区域。

(3)为何STOP THE WORLD

使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。所以GC时需要停顿来枚举根节点时。

7、JVM Error

(1)StackoverFlowError

定义:堆栈溢出,某个线程的线程栈空间被耗尽,没有足够资源分配给新创建的栈帧。

原因:

  • 无限递归循环调用(最常见)。
  • 执行了大量方法,导致线程栈空间耗尽。
  • 方法内声明了海量的局部变量。
  • native 代码有栈上分配的逻辑,并且要求的内存还不小,比如 java.net.SocketInputStream.read0 会在栈上要求分配一个 64KB 的缓存(64位 Linux)。

(2)OutOfMemoryError

java heap space 分配的堆内存不足

原因:

  • 超出预期的访问量/数据量

  • 内存泄漏耗光堆内存

GC overhead limit exceeded 超过98%的时间用来做GC并且回收了不到2%的堆内存

原因:

  • 内存泄漏

Direct buffer memory 本地内存不足,但是堆内存充足的时候

原因:

NIO引起的,使用Native 函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。通过ByteBuffer.allocteDirect分配OS本地内存,不属于GC管辖范围,由于不需要内存的拷贝,所以速度相对较快。但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象就不会被回收,这时候堆内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError。

unable to create new native thread 创建线程的达到上限

原因:

  • 应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限

  • 服务器并不允许你的应用程序创建这么多线程,linux系统默认运行单个进程可以创建的线程为1024个,如果应用创建超过这个数量,就会报 java.lang.OutOfMemoryError:unable to create new native thread

Metaspace 元空间使用的本地内存不足

原因:

  • 加载大量的第三方的jar包
  • Tomcat部署的工程过多(30~50个)
  • 大量动态的生成反射类

8、类的加载

(1)流程

加载—链接[验证—准备—解析]—初始化

加载:通过一个类的全限定名获取定义此类的二进制字节流,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

**验证:**确保Class文件的字节流中包含信息符合当前虚拟机要求,保证被加载类的正确性,不会危害虚拟机自身安全。

**准备:**为类变量分配内存并且设置该类变量的默认初始值,即零值。

不包含用final修饰的static变量,final在编译的时候就会分配了,准备阶段会显式初始化。也不会为实例变量分配初始化,实例变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中

**解析:**将所有的类名,方法名,字段名这些符号引用替换为具体的内存地址或偏移量,也就是直接引用。

**初始化:**为标记为常量值的字段赋值的过程。换句话说,只对static修饰的变量或语句块进行初始化。如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。

(2)类加载器

启动类加载器(引导类加载器,Bootstrap ClassLoader)

  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
  • 它用来加载Java的核心库(JAVAHOME/jre/1ib/rt.jar、resources.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类
  • 并不继承自ava.lang.ClassLoader,没有父加载器。
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类

扩展类加载器(Extension ClassLoader)

  • java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • 派生于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载

应用程序类加载器(系统类加载器,AppClassLoader)

  • java语言编写,由sun.misc.LaunchersAppClassLoader实现
  • 派生于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
  • 该类加载是程序中默认的类加载器,一般来说,java应用的类都是由它来完成加载
  • 通过classLoader#getSystemclassLoader()方法可以获取到该类加载器

用户自定义类加载器

  • 开发人员可以通过继承抽象类ava.lang.ClassLoader类的方式,实现自己的类加载器
  • 用以隔离加载类、修改类加载的方式、扩展加载源、防止源码泄漏

(3)双亲委派

优点

  • 避免类的重复加载

  • 保护程序安全,防止核心API被随意篡改

为何破坏双亲委派机制

正常情况下,用户代码是依赖核心类库的,所以按照正常的双亲委派加载流程是没问题的,但是在加载核心类库时,如果需要使用用户代码,双亲委派流程就无法满足。

例如使用DriverManager.getConnection获取连接,DriverManager是由根类加载器Bootstrap加载的,在加载DriverManager时,会执行其静态方法,加载初始驱动程序,也就是Driver接口的实现类;但是这些实现类基本都是第三方厂商提供的,根据双亲委派原则,第三方的类不可能被根类加载器加载。

例如SPI机制*(Service Provider Interface,java 提供的一套用来被第三方实现的API,他可以用来启用框架扩展和替换组件,基于接口编程+策略模式+配置文件 组合实现的动态加载机制。)*,需要破坏双亲委派机制。

如何破坏:

实现关键,通过java.util.ServiceLoader#load(java.lang.Class<S>)中通过线程上下文件类加载器(Thread Context ClassLoader)拿到用户自定义的实现类。

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
  • 1
  • 2
  • 3
  • 4

举例:

java.sql.Driver是最为典型的 SPI 接口,java.sql.DriverManager通过扫包的方式拿到指定的实现类,完成 DriverManager的初始化。初始化时在其loadInitialDrivers方法中调用ServiceLoader#load

shardingsphere的SPI实现,在NewInstanceServiceLoader初始化接口和子类关系的集合,缓存了所有SPI接口和子类的关系。在其register(java.lang.Class<T>)方法调用ServiceLoader#load

9、逃逸分析

Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。在方法中创建对象之后,如果这个对象除了在方法体中还在其它地方被引用了,此时如果方法执行完毕,由于该对象有被引用,所以 GC 有可能是无法立即回收的。通过此方法判断哪些对象是可以存储在栈内存中而不用存储在堆内存中的,从而让其随着线程的消逝而消逝,进而减少了 GC 发生的频率。

逃逸状态:

  • 全局逃逸(GlobalEscape):即一个对象的作用范围逃出了当前方法或者当前线程。如静态变量或当前方法的返回值。
  • 参数逃逸(ArgEscape):即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的。

未逃逸时优化:

  • 同步消除(锁消除):移除该对象的同步锁,减少锁开销
  • 标量替换:针对未逃逸聚合量对象*(可进一步分解成基础类型和对象的引用)只会在栈或者寄存器上创建它用到的成员标量(基础类型和对象的引用)*,节省了内存空间,也提升了应用程序性能
  • 栈内存分配:将原本分配在堆内存上的对象转而分配在栈内存上,这样就可以减少堆内存的占用,从而减少 GC 的频次

二、JUC

1、volatile关键字

保证可见性;不保证原子性;禁止指令重排

**实现原理:**总线嗅探技术,每个处理器通过嗅探在总线上传播的数据,基于MESI协议来检查自己缓存值是否过期了,当处理器发现自己的缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操作的时候,会重新从内存中把数据读取到处理器缓存中。

**MESI协议:**对缓存行定义了四种状态,M: 被修改(Modified)、E: 独享的(Exclusive)、S: 共享的(Shared)、I: 无效的(Invalid)。当CPU写数据时,如果发现操作的变量是共享变量,即在其它CPU中也存在该变量的副本,会发出信号通知其它CPU将该内存变量的缓存行设置为无效,因此当其它CPU读取这个变量的时,发现自己缓存该变量的缓存行是无效的,那么它就会从内存中重新读取。

**存在隐患:**总线风暴,由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和CAS循环,无效的交互会导致总线带宽达到峰值。

2、可重入锁

可重入锁就是递归锁,可避免递归调用时死锁。

3、线程池

(1)常用线程池

Executors.newFixedThreadPool(int i) :创建一个拥有 i 个线程的线程池

  • 执行长期的任务,性能好很多
  • 创建一个定长线程池,可控制线程数最大并发数,超出的线程会在队列中等待

Executors.newSingleThreadExecutor:创建一个只有1个线程的 单线程池

  • 一个任务一个任务执行的场景
  • 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行

Executors.newCacheThreadPool(); 创建一个可扩容的线程池

  • 执行很多短期异步的小程序或者负载教轻的服务器
  • 创建一个可缓存线程池,如果线程长度超过处理需要,可灵活回收空闲线程,如无可回收,则新建新线程

(2)ThreadPoolExecutor中keepAliveTime作用

当线程数大于核心线程数时,在终止前,多余的空闲线程等待新任务的最长时间。

随着任务数量不断上升,线程池会不断的创建线程,直到到达核心线程数,就不创建线程了,这时多余的任务通过加入阻塞队列来运行,当超出(阻塞队列长度+核心线程数)时,这时不得不扩大线程个数来满足当前任务的运行,这时就需要创建新的线程了(最大线程数起作用),上限是最大线程数,那么,超出核心线程数并小于最大线程数的,可能新创建的这(最大线程数-核心线程数)个线程相当于是"借"的,如果这些线程空闲时间超过keepAliveTime,就会被退出。keepAliveTime=0时,"借来"的工作线程是不等待直接退出的。

三、消息队列

1、消息队列优点和缺点

优点:

  • 解耦:多系统之间涉及消息的广播和订阅,通过消息队列解耦,支持消费方灵活变更,生产方不关心消费者状态。
  • 异步:多系统之间同步调用延时较高,使用消息队列异步分发至各系统各自执行,生产者直接返回。(适用不关心同步处理结果的弱事务一致性场景)
  • 削峰:通过消息队列的存储能力缓存高峰期数据,逐步消费处理;通过自消费队列的方式将热点数据打散至不同机器并发处理,避免单机的处理压力。

缺点:

  • 依赖消息队列中间件,可用性降低。
  • 复杂性提高,需注意处理消息处理的幂等性,顺序性、可靠性、事务一致性。

2、RocketMQ

(1)事务消息

采用了2PC的方案来提交事务消息,确认 Producer 端的消息发送与本地事务执行的原子性。

Producer向Broker发送预处理消息,此时消息还未被投递出去,Consumer还不能消费。发送预处理消息成功后,开始执行本地事务;如果本地事务执行成功,发送提交请求提交事务消息,消息会投递给Consumer。如果本地事务执行失败,发送回滚请求回滚事务消息,消息不会投递给Consumer。

Producer实现RocketMQLocalTransactionListener,executeLocalTransaction方法执行本地事务方法,当executeLocalTransaction方法返回UNKNOW以后,RocketMQ会每隔一段时间调用一次checkLocalTransaction方法。

https://blog.csdn.net/yangbaggio/article/details/106450715

(2)消息可靠性

生产阶段:

1、采取同步发送的方式或异步发送加回调的方式。

2、可设置自动重试。默认是重试三次。

3、broker宕机。设置多个Master节点。

broker存储阶段:

1、broker未写入磁盘时异常宕机。调整刷盘策略为同步刷盘,性能降低。

2、消息写入磁盘后磁盘损害。集群部署,主从模式,高可用。设置Master和Slave都刷完盘后才通知成功。

消费阶段:

处理结果确认,消费失败自动重试

https://blog.csdn.net/weixin_42295690/article/details/118147617

(3)顺序消息

生产者通过MessageQueueSelector将顺序消息路由至同一队列,消费者也需保证此队列顺序消费。

**注意点:**需要解决热点数据问题,消费者顺序消费存在瓶颈问题,消费失败时无法跳过,需自己设计策略支持。

其他思路:

  • 生产者以批量的方式组装好顺序消息下发,保证有序,受限于报文大小。
  • 消费者先存储再排序后处理,需考虑执行时机(存在消息延迟问题,导致开始消费时,某条消息还未接收到被忽略导致乱序),也许考虑存储压力问题。
  • 生产者消费者约定消息序号,消费者按序号严格执行,需注意处理因序号失败卡单场景。

(4)架构

NameServe:

对Broker进行管理以及提供Broker的路由查询功能。

  • Broker管理

    NameServer会接收并保存Broker的注册信息。并且每10秒检测注册的Broker的心跳信息,如果一个Broker两分钟内都没有向NameServer发送心跳信息,那么Nameserver就认为这个Broker已经不可用,将其从路由信息中剔除。

  • Broker路由信息查询

    NameServer中会保存Broker集群里的每一个Broker的信息。NameServer中会保存Broker集群里的每一个Broker的信息。

Broker

Broker是RocketMQ中的核心角色,负责存储消息、转发消息。

  • 消息的存储

    Broker在内存中接收完消息,就会对消息进行持久化。分为同步刷盘或异步刷盘,采用顺序写的方式进行持久化

  • 消息的读取

    通过consumequeue索引文件读取消息。consumequeue存储消息在commitLog中的偏移量以及消息的Tag Hash,消费者根据offset去commitLog中读取真正的消息信息。

  • 消息的过滤

    Broker在接收到这条消息后会根据消息的Tag的hashcode进行过滤,发给对应的消费者队列MessageQueue。

Consumer/Produce:

与Name Server集群中的其中一个节点(随机)建立长连接,定期从Name Server拉取Topic路由信息,并向提供Topic服务的Master Broker、Slave Broker建立长连接,且定时向Master Broker、Slave Broker发送心跳。

3、Kafka

(1) 高性能保障

顺序写:

使用了磁盘顺序读写来提升的性能。Kafka的message是不断追加到本地磁盘文件末尾的,而不是随机的写入,这使得Kafka写入吞吐量得到了显著提升。Kafka是不会删除数据的,消费者基于Topic的offset判断读取到哪一条。

Page Cache:

利用了操作系统本身的Page Cache,更加简单可靠,基于内存的,读写速度得到了极大的提升。当数据的请求到达时,如果在 Cache 中存在该数据且是最新的,则直接将数据传递给用户程序,免除了对底层磁盘的操作,提高了性能。

首先,操作系统层面的缓存利用率会更高,因为存储的都是紧凑的字节结构而不是独立的对象。

其次,操作系统本身也对于Page Cache做了大量优化,提供了 write-behind、read-ahead以及flush等多种机制。

再者,即使服务进程重启,系统缓存依然不会消失,避免了in-process cache重建缓存的过程。

零拷贝:

使用了sendfile方法,允许操作系统将数据从Page Cache 直接发送到网络,只需要最后一步的copy操作将数据复制到 NIC 缓冲区, 这样避免重新复制数据 ,避免了数据在内核空间和用户空间之间穿梭。

分区分段+索引:

Kafka的message是按topic分类存储的,topic中的数据又是按照一个一个的partition即分区存储到不同broker节点。每个partition对应了操作系统上的一个文件夹,partition实际上又是按照segment分段存储的。Kafka又默认为分段后的数据文件建立了索引文件,就是文件系统上的.index文件。这种分区分段+索引的设计,不仅提升了数据读取的效率,同时也提高了数据操作的并行度。

批量读写和批量压缩:

(2)如何删除历史数据(日志)

Kafka删除数据有两种方式

  • 按照时间,超过一段时间后删除过期消息
  • 按照消息大小,消息数量超过一定大小后删除最旧的数据

Kafka删除数据的最小单位:segment。kafka清理时是不管该segment中的消息是否被消费过,仅仅依据时间和大小。

(3)消息可靠性

消费端:

关闭自动提交 offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。需注意做好消费幂等性。

kafka:

某个 broker的leader 机器宕机,然后重新选举 partition 的 leader,此时follower 刚好还有些数据没有同步,会丢失。

  • 给 topic 设置 replication.factor 参数,要求partition 必须有至少 2 个副本
  • Kafka 服务端设置 min.insync.replicas 参数,要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower
  • producer 端设置 acks=all:这个是要求每条数据,必须是写入所有 replica 之后,才能认为是写成功
  • producer 端设置 retries=MAX(很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里

生产者:

producer 端设置 acks=all,确保写入成功

四、分库分表场景事务

1、自定义分库分表框架

本地事务,支持因逻辑异常导致的跨库事务,基于Spring框架的DataSourceTransactionManager实现。

自定义事务管理器,Spring启动时加载,根据配置的多个数据源创建各自对应DataSourceTransactionManager,事务操作时,遍历获取所有事务状态。提交或回滚操作时,同样遍历所有事务管理器进行操作。

2、ShardingSphere

本地事务:弱XA事务,完全支持因逻辑异常导致的跨库事务。不支持因网络、硬件异常导致的跨库事务。

两阶段事务-XA:服务宕机重启后,提交/回滚中的事务可自动恢复。SPI机制可实现XATransactionManager。默认的XA事务管理器为Atomikos,在项目的logs目录中会生成xa_tx.log。
https://segmentfault.com/a/1190000038183881?utm_source=tag-newest

3、ShardingSphere框架禁止全库全表查询

方案1:强制进入自定义路由策略算法进行校验。关键实现ShardingRouteDecorator。

跟踪源码后发现ShardingSphere是先对sql进行解析检查是否存在指定路由字段,组织条件集合ShardingConditions,再执行路由引擎ShardingStandardRoutingEngine,根据ShardingConditions是否为空决定是否执行路由策略。

方案2:查看框架本身是否存在可配置项。

方案3:实现拦截器做统一拦截。

ShardingSphere框架无自定义拦截器。通过实现mybatisplus的InnerInterceptor.beforePrepare强制检查。

难点:sql解析,使用com.github.jsqlparser。

拓展:执行引擎连接模式。

1、内存限制模式:ShardingSphere对一次操作所耗费的数据库连接数量不做限制。对每张表创建一个新的数据库连接,并通过多线程的方式并发处理,以达成执行效率最大化。 并且在SQL满足条件情况下,优先选择流式归并,以防止出现内存溢出或避免频繁垃圾回收情况。 独立的数据库连接,能够持有查询结果集游标位置的引用,在需要获取相应数据时移动游标即可 。以结果集游标下移进行结果归并的方式,称之为流式归并,它无需将结果数据全数加载至内存,可以有效的节省内存资源,进而减少垃圾回收的频次。

2、连接限制模式:ShardingSphere严格控制对一次操作所耗费的数据库连接数量。每个库的每次操作只创建一个唯一的数据库连接,复用该数据库连接获取下一张分表的查询结果集之前,将当前的查询结果集全数加载至内存。

拓展:ShardingUpdateStatementValidator.validate shardingsphere校验sql方法

4、免迁移扩容

采用双倍扩容策略,避免数据迁移。扩容前每个节点的数据,有一半要迁移至一个新增节点中,对应关系比较简单。 具体操作如下(假设已有 2 个节点 A/B,要双倍扩容至 A/A2/B/B2 这 4 个节点):

  • 无需停止应用服务器;
  • 新增两个数据库 A2/B2 作为从库,设置主从同步关系为:A=>A2、B=>B2,直至主从数据同步完毕(早期数据可手工同步);
  • 调整分片规则并使之生效: 原 ID%2=0 => A 改为 ID%4=0 => A, ID%4=2 => A2; 原 ID%2=1 => B 改为 ID%4=1 => B, ID%4=3 => B2
  • 解除数据库实例的主从同步关系,并使之生效;
  • 此时,四个节点的数据都已完整,只是有冗余(多存了和自己配对的节点的那部分数据),择机清除即可(过后随时进行,不影响业务)。

五、MYSQL

1、mysql各事务隔离级别保证

READ UNCOMMITTED:

写操作会加X排他锁,事务结束之后释放。读不会加任何锁,因此读取不会被排他锁拒绝。

READ COMMITED:

写操作:行级排他锁(X lock),那么当其他事务访问另一个事务正在update (除select操作外其他操作本质上都是写操作)的同一条记录时,事务的读操作会被阻塞。

读操作:基于MVCC,根据事务ID读取副本(read view)快照数据, 只要当前语句执行前已经提交的数据都是可见的,总是读最新的一份快照数据。

REPEATABLE READ:

写操作:首先对索引记录加上行锁(Record Lock),再对索引记录(无索引时会锁全表)两边的间隙加上间隙锁(Gap Lock)。加上间隙锁之后,其他事务就不能在这个间隙修改或者插入记录。

读操作:基于MVCC,只要当前语句执行前已经提交的数据都是可见的,读事务开始时的行数据版本快照。

(幻读:在一个事务中,同一个范围内的记录被读取时,其他事务向这个范围添加了新的记录。本事务插入同样的数据却出现了错误。mysql在此级别下使用Next-Key锁,行锁+间隙锁,某种程度上解决幻读)

SERIALIZABLE:

每一个select请求下获得读锁(共享锁),在每一个update操作下尝试获得写锁。

2、主从复制

  • 半同步复制,用来解决主库数据丢失问题。指主库写入binlog日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的relay log之后,接着会返回一个ack给主库,主库接收到至少一个从库的ack之后才会认为写操作完成了。
  • 并行复制,用来解决主从同步延时问题。指从库开启多个线程,并行读取relay log中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。

七:Redis

1、生产配置

哨兵模式,11组实例,每组实例一主一从,单机2C8G。

2、架构模式

  • 单机模式。
  • 主从复制,数据的复制是单向的,只能由主节点到从节点,能力受单机限制。
  • 哨兵模式,通过哨兵节点监控整个redis数据节点,可实现主从自动切换,实现高可用。较难在线扩容。
  • 集群模式,通过数据分片的方式来进行数据共享问题,同时提供数据复制和故障转移功能。

https://www.cnblogs.com/zhonglongbo/p/13128955.html

3、常见问题

(1)缓存穿透

海量请求查询压根就不存在的数据,那么这些海量请求都会落到数据库中。

  • 缓存空数据

    存在数据一致性问题,该key对应数据写入时,需注意数据更新;

    恶意攻击查询的key往往各不相同而且数据量大,需要存储所有空数据的key,而这些恶意攻击的key往往各不相同,而且同一个key往往只请求一次。因此即使缓存了这些空数据的key,由于不再使用第二次,因此也起不了保护数据库的作用

  • BloomFilter检查数据库中是否存在

  • 使用互斥锁排队,阻塞减轻数据库压力?

(2)缓存雪崩

场景一:缓存因某种原因发生了宕机,那么原本被缓存抵挡的海量查询请求就会涌向数据库。此时数据库如果抵挡不了这巨大的压力,它就会崩溃。

  • 事前:redis高可用,主从+哨兵,redis cluster,避免全盘崩溃

  • 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL被打死

  • 事后:redis持久化,快速恢复缓存数据,一般重启,自动从磁盘上加载数据恢复内存中的数据。

场景二:缓存在同一时间内大量键过期(失效),接着来的一大波请求瞬间都落在了数据库中导致连接异常。

  • 查询数据库时使用互斥锁,阻塞其他的查询请求,等待数据被更新至缓存后从缓存中查询(系统的吞吐量将会下降)
  • 建立备份缓存,缓存A和缓存B,A设置超时时间,B不设值超时时间,先从A读缓存,A没有读B,并且更新A缓存和B缓存
  • 为缓存设置不同随机失效时间,避免大量缓存同时失效

(3)并发竞争

多个redis的client同时set key带来的更新时序性等问题。

  • 分布式锁+时间戳保证执行时序
  • 把Redis.set操作放在队列中使其串行化

(4)异步复制与脑裂

**异步复制:**因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了。这时Redis进行了主备切换,slave节点成了master节点,此时原master内存中的数据就丢失了。

**脑裂:**master节点出现了异常(比如网络异常),跟其他slave机器不能连接,但是实际上master还运行着,此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master。集群里就会有两个master,也就是所谓的脑裂。此时client还没来得及切换到新的master,还继续写向旧master写数据。当网络分区恢复正常后,旧master会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据。因此之前client写的数据就会丢失。

**解决方案:**通过配置要求至少有1个slave,数据复制和同步的延迟不能超过10秒。master如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求。减少存在的数据丢失范围。

4、持久化

(1)RDB

定义:

将Redis中的数据,每个一段时间,进行数据持久化,生成快照文件。

优点:

定期生成此时刻的完整数据快照,时候作为冷备份数据文件;

生成RDB文件时,主进程会fork()一个子进程来处理,对Redis读写影响小,保持Redis高性能;

数据恢复速度更快;

缺点:

由于存在时间间隔,Redis故障时可能丢失一段时间范围内数据;

如果数据文件特别大或备份间隔时间特别长,会对Redis服务产生影响;

(2)AOF

定义:

对每条写入命令作为日志,以append-only的模式写入一个日志文件,Redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集。

优点:

记录所有的写操作,数据完整性更好,相比RDB可以更好的保护数据不丢失;

AOF日志文件以append-only模式写入,所有没有任何磁盘寻址开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易快速修复;

适合做灾难性的误删除的紧急恢复,比如某人不小心用了 flushall命令,清空了整个Redis数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令删除了,然后再将该AOF文件放回去,就可以通过恢复机制,自动回复所有的数据;

缺点:

AOF日志文件比RDB日志文件更大;

支持写QPS会比RDB支持的写QPS低;

做数据恢复的时候,会比较慢,还有做冷备,定期的备份,不太方便,可能要自己手动写复杂的脚本去做;

5、单线程模型

通过IO多路复用机制监听了多个socket,队列中插入一个socket,文件事件分派器就是将队列中的socket取出来,分派到对应的处理器,然后通过ServerSocket创建一个与客户端一对一对应的socket,在处理器处理完成后,才会从队列中在取出一个。

原因

  • 使用单线程模型能带来更好的可维护性,方便开发和调试;
  • 使用单线程模型也能并发的处理客户端的请求;
  • Redis 服务中运行的绝大多数操作的性能瓶颈都不是 CPU;

效率高:

  • 绝大部分请求是纯粹的内存操作(非常快速)
  • 采用单线程,避免了不必要的上下文切换和竞争条件
  • 非阻塞IO多路复用机制

八、集合

1、LinkedHashMap

继承HashMap,增加一层双向链表数据结构,重写HashMap的afterNodeAccess/afterNodeInsertion/afterNodeRemoval方法,保证双向链表的更新。

**顺序性:**依赖HashMap基础上增加双向链表实现,按照插入的顺序从头部或者从尾部迭代。

**accessOrder:**默认false,按插入顺序排序;true,按访问顺序排序(也就是插入和访问都会将当前节点放置到尾部,尾部代表的是最近访问的数据)

**LRU实现:**重写removeEldestEntry,决定何时删除队首元素。

2、TreeMap

顺序性:基于底层红黑树的数据存储结构,按Key的自然顺序(如整数从小到大),也可以指定比较函数。但不是插入的顺序。

九、Elasticsearch

1、生产配置

生产集群我们部署了8台机器,3个Master节点,5个数据节点。集群总内存是237.3 GB。数据总量3.3TB。

十、RPC框架

1、定义

RPC是指远程过程调用,两台服务器A、B,A上的应用想要访问B上应用提供的函数方法,由于不在同一内存空间,不能直接调用,需要通过网络来表达调用语义和传达调用数据。完整的 RPC 实现一般会包含有 传输协议序列化协议 这两个。

需要解决问题:

  • 怎么告诉远程机器应用我们要调用的函数方法?远程调用函数唯一Call ID映射,服务端和客户端都维护此关系。
  • 怎么把请求和响应数据返回给对方?序列化和反序列化
  • 解决网络传输问题
// Client端 
// int l_times_r = Call(ServerAddr, Multiply, lvalue, rvalue)
1. 将这个调用映射为Call ID。这里假设用最简单的字符串当Call ID的方法
2. 将Call ID,lvalue和rvalue序列化。可以直接将它们的值以二进制形式打包
3. 把2中得到的数据包发送给ServerAddr,这需要使用网络传输层
4. 等待服务器返回结果
5. 如果服务器调用成功,那么就将结果反序列化,并赋给l_times_r

// Server端
1. 在本地维护一个Call ID到函数指针的映射call_id_map,可以用std::map<std::string, std::function<>>
2. 等待请求
3. 得到一个请求后,将其数据包反序列化,得到Call ID
4. 通过在call_id_map中查找,得到相应的函数指针
5. 将lvalue和rvalue反序列化后,在本地调用Multiply函数,得到结果
6. 将结果序列化后通过网络返回给Client
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

2、dubbo

(1)通信协议

dubbo协议:

默认协议,单一长连接,NIO异步传输,TCP协议,基于Hessian二进制序列化协议,适用于传输数据量很小(每次请求在100kb以内),但是并发量很高的场景。不适用于传输大文件或超大字符串。场景:常规远程服务方法调用。

为了要支持高并发场景,一般是服务提供者就几台机器,但是服务消费者有上百台,可能每天调用量达到上亿次!此时用长连接是最合适的,就是跟每个服务消费者维持一个长连接就可以,可能总共就100个连接。然后后面直接基于长连接NIO异步通信,可以支撑高并发请求。因为走的是单一长连接,所以传输数据量太大的话,会导致并发能力降低。所以一般建议是传输数据量很小,支撑高并发访问。

rmi协议:

多个短连接,同步传输,TCP协议,基于Java 标准二进制序列化,适合传入传出参数数据包大小混合,消费者和提供者数量差不多,适用于文件的传输。场景:常规远程服务方法调用,与原生RMI服务互操作

hessian协议:

多个短连接,同步传输,HTTP协议,基于Hessian二进制序列化,适用于传入传出参数数据包较大,提供者数量比消费者数量还多,提供者压力较大,适用于文件的传输。场景:页面传输,文件传输,或与原生hessian服务互操作。

http协议:

多个短连接,同步传输,HTTP协议,基于表单序列化,适用于传入传出参数数据包大小混合,提供者比消费者个数多,可用浏览器查看,可用表单或URL传入参数,暂不支持传文件。场景:需同时给应用程序和浏览器 JS 使用的服务。

webservice协议:

多个短连接,同步传输,HTTP协议,基于SOAP文本序列化。场景:系统集成,跨语言调用。

(2)负载策略

  • random loadbalance 随机调用实现负载均衡,dubbo默认策略。可以对provider不同实例设置不同的权重,会按照权重来负载均衡,权重越大分配流量越高。
  • roundrobin loadbalance 均匀地将流量打到各个机器上去,但是如果各个机器的性能不一样,容易导致性能差的机器负载过高,需要调整权重。
  • leastactive loadbalance 自动感知,如果某个机器性能越差,那么接收的请求越少,越不活跃,此时就会给不活跃的性能差的机器更少的请求。
  • consistanthash loadbalance 一致性Hash算法,相同参数的请求一定分发到一个provider上去,provider挂掉的时候,会基于虚拟节点均匀分配剩余的流量,抖动不会太大。如果你需要的不是随机负载均衡,是要一类请求都到一个节点,那就走这个一致性hash策略。

(3)集群容错策略

  • failover cluster模式 失败自动切换,自动重试其他机器,默认就是这个,常见于读操作

  • failfast cluster模式 一次调用失败就立即失败,常见于写操作

  • failsafe cluster模式 出现异常时忽略掉,常用于不重要的接口调用,比如记录日志

  • failbackc cluster模式 失败了后台自动记录请求,然后定时重发,比较适合于写消息队列这种

  • forking cluster 并行调用多个provider,只要一个成功就立即返回

  • broadcacst cluster 逐个调用所有的provider

十一、分布式SESSION

1、生产解决方案

  • 用户登录后通过后将用户ID和登录时间以非对称加密方式组合成登录密钥tokne放入cookie中,失效时间可设置较长,每次更新。
  • 登录后同时将用户相关信息存入redis中,用户ID作为key值,失效时间与登录超期时间保持一致。
  • 每次请求拦截校验,解析cookie中token是否合法,并校验登录时间是否超期。
  • 拦截通过后根据token中解析出的用户ID从redis中读取用户相关信息,放入请求request中或本地ThreadLocal(用户信息数据较多时)。
  • 退出登录时清空cookie中登录密钥token,redis中的用户信息,本地ThreadLocal该线程对应数据。

十二、分布式事务

思考点:

单应用内跨库事务保证一致性?

非强一致性要求应用,性能考虑常规使用本地事务,数据库宕机异常导致的事务不一致可依赖MySQL 自身XA事务日志回滚。或者在功能设计时允许重试补偿保证最终一致性。

事务方法尽量保证颗粒度低,集中。

多应用间事务保证一致性?

优先选择最终一致性方案,性能优势,需注意处理幂等性,失败重试及兜底方案。

事务实时性要求比较高选择TCC等事务方案,功能设计要求较高。需要主事务服务扮演协调者角色,次事务服务提供查询、提交、回滚接口。

适当考虑批量处理提升性能,减少依赖方。

十三、Mybatis

1、缓存

适用于缓存的数据:

  • 经常查询并且不经常被改变的数据。
  • 数据的正确与否对最终结果影响不大的数据。

一级缓存:

默认开启。Mybaits中SqlSession对象的缓存,依赖SqlSession对象,SqlSession中提供一块Map结构的区域,存放的是返回数据的对象。带Spring事务的方法时可以使用到一级缓存。

二级缓存:

默认关闭的,需在映射文件配置开启同时POJO类实现Serializable接口。Mybatis中SqlSessionFactory对象的缓存,由同一个SqlSessionFactory对象创建的SqlSession共享其二级缓存,存放的是数据,不是对象。一个SqlSessionFactory对象包括多个SqlSession对象。与事务无关。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/酷酷是懒虫/article/detail/757159
推荐阅读
相关标签
  

闽ICP备14008679号