java八股文整理了1年多 非常全!
2024-07-08 19:50:24 0 举报
AI智能生成
《Java八股文大全》是一本涵盖Java核心内容的书籍,旨在帮助读者深入理解和掌握Java编程语言。这本书涵盖了Java的基础语法、面向对象编程、集合框架、泛型、多线程、JDBC、Servlet等关键领域。全书采用一问一答的形式,每个问题都配有详细的解答,帮助读者巩固知识,提高编程技能。无论是Java初学者还是有经验的开发人员,都能从这本书中找到有价值的参考信息。
作者其他创作
大纲/内容
JVM
GC
对象回收
引用技数法
每次被引用计数器+1
循环引用不会被判定为垃圾
可达性分析
被GCRoot引用不会被回收
GC Root
栈帧引用
类静态变量
常量池引用对象
本地方法栈引用对象(native)
Class对象
被同步锁引用的对象
jni指针(java和C的指向指针)
引用
强引用
new
只要引用在就不会被回收
软引用
SoftReference
内存溢出时回收
场景:大图片占地方的放缓存,如果满了就清掉
弱引用
WeakReference
无论内存是否足够都会被回收,即使还有引用
应用:ThreadLocal -- 配合强引用,外层有强引用拉着,内层有弱引用场景。 等外层强引用消失,GC会直接把Thread的map中的弱引用对象清除
GC删除了key,map的每一次get,put,remove都会把key为null的value进行惰性删除
GC删除了key,map的每一次get,put,remove都会把key为null的value进行惰性删除
虚引用
PhantomReference
这个对象被GC回收时会收到一个系统通知 引用队列 (类似监控作用,管理堆外内存)
当一个指向堆外内存的对象(挂了虚引用)被回收时,这个虚引用会将信息放在队列中
GC回收时发现队列有对象信息,就随之把对应的堆外内存一起清理
GC回收时发现队列有对象信息,就随之把对应的堆外内存一起清理
NIO,netty ,零拷贝应用
GC算法
标记法(Mark-Sweep)
标记需要回收的对象,直接删除
碎片化严重,大对象可能找不到内存空间
拷贝算法(Copying)
存活拷贝到新内存,老地方剩余对象清空
浪费内存空间,使用率减半
标记压缩(Mark-Compact)
同一空间,边整理排列,边清理
效率低,STW 移动存活对象需要时间
分代收集算法
新生代 需要回收的垃圾较多,所以用拷贝算法(需要拷贝的对象较少)
老年代 标记压缩算法(CMS除外)
GC收集器
Serial(新生代)
Serial Old (老年代)
Serial Old (老年代)
单线程 回收STW
效率低
最小内存适用
ParNew(新生代)
CMS(老年代)
CMS(老年代)
多线程回收 Serail的多线程版本
CMS(ConCurrent Mark Sweep)
GC线程和业务线程并发 1.5-7
三色标记算法
子节点跟踪(黑 灰 白)
并发扫描过程引用变化,导致漏标
阶段多,周期时间长,但stw时间短
低STW(只在初始和重新标记阶段)追求最短回收停顿时间
和其他老年代算法不同,采用的是标记清除算法,会有碎片产生-频繁触发FullGC
Parallel Scavenge(新生代)
Parallel Old(老年代)
Parallel Old(老年代)
PS+PO 1.8的默认回收器
最大吞吐量
特点:自适应调节策略:-XX:+UseAdptiveSizePolicy
G1
1.9以后的默认回收器 并发+并行
摒弃了分代模型,采用分区算法 部分回收(物理不分代,逻辑分代)
采用标记压缩算法,不产生碎片
根据每个区优先级不同,垃圾量不同,执行策略不同,保证了高吞吐,低延迟
ZGC
Oracl官方支持
不分代 速度快 (大中小页面)
java 21支持了分代分区,更效率
颜色指针 零停顿 100ms扫一遍
Shenandoal
OpenJDK12 开源
并发回收阶段
内存结构
私有线程
程序计数器
指向虚拟机字节码指令位置
若执行native方法,则为空
虚拟机栈
局部变量表
基本数据类型
对象引用
操作数栈
动态链接(指向元数据的指针)
方法出口
本地方法栈(C栈)
执行本地C语言方法
逻辑与虚拟机栈一致
共享线程
方法区(永久代)
存储已被JVM加载的类方法信息
JDK8已经把方法区的实现,从永久代转变为元空间
脱离JVM,存放在本地内存
之前版本永久代经常OOM
堆
新生代
eden
survivor(from和to)
8:1:1 eden正常回收率高
老年代
对象、成员变量
堆外内存(本地内存)
不受JVM管控(1.8之后)
元数据区(metaspace)
运行时常量
类常量池
类信息
内存模型 JMM
规定了所有变量都在主内存中,每条线程有自己的工作内存。 JMM作用于工作内存和主存之间的同步
原子性
synchronize保证
可见性
变量被修改,立即同步主内存,工作内存每次从主内存刷新
volatile和synchronize都可保证
有序性
volatile和synchronize都可保证
volatile禁止重排,synchronize加锁
模型类比CPU的缓存模型。CPU和主存之间的三级缓存,MESI协议保证了以上三点,缓存行的同步
类加载
加载器
启动类加载器(bootstrap class loader)
最上层,加载核心类库
负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,由C++实现
不允许直接获取引用
扩展类加载器(extensions class loader)
负责加载JRE的扩展目录
lib/ext或者由java.ext.dirs系统属性指定的目录中的JAR包的类
应用程序类加载器(application class loader)
加载用户路径上的类库
加载来自Java命令的-classpath选项、java.class.path系统属性,或者CLASSPATH换将变量所指定的JAR包和类路径
定制化加载器(Custom ClassLoader)
加载过程
加载(loading)
硬盘IO读取class文件,生成一个代表这个类的class对象 放到元空间(懒加载)
内存分配 指针碰撞
堆中生成class对象,作为访问入口
对象内存布局
对象头(markword)
锁信息
GC信息(分代年龄)
指针(class pointer)
实例数据
对齐(padding)
工具:JOL(java Object Layout)
连接(linking)
验证(verification)
确保class文件字节流包含的信息符合规范约束
准备(preparation)
对类中变量(静态变量,常量)分配内存和初始值(并非默认值),初始化阶段才赋默认值
若是final常量,则会在准备阶段赋真正的默认值
设置对象头
解析(resolution)
将常量池内的符号引用替换为直接引用
静态链接:把静态方法替换为指向数据所存内存的指针或句柄等(直接引用)。——类加载期间完成
动态链接:在程序运行期间完成的将符号引用替换为直接引用过程
初始化(initializing)
类变量的赋值和静态初始块的执行
执行 类构造器< clinit>() 方法的过程。
保证子类的Clint方法执行前,父类的已经执行完毕,所以第一个执行的一定是java.lang.Object
双亲委派
先询问父节点,是否已加载,若已存在就直接返回
若询问到最上层未加载,再逐步向下进行查找和加载
保证了唯一性和安全性(避免核心库被修改,如果存在会直接返回,不会加载相同全限定类名)
打破:我们常用数据库驱动Driver接口,Driver定义在jdk当中,当其实现却是各个数据库服务商,例如,mysql的MYSQL CONNECROR,所有这就有个问题,DriverManger要加载各个Driver接口实现类,然后进行管理,但是DriverManager是由启动类加载器进行加载的,而这个启动类加载器默认值加载JAVA_HOME下面的lib,但我们真正要加载的是各个实现类,需要有系统类加载器进行加载,这个时候就需要启动类加载器委托系统类加载器去加载Driver实现类,从而破坏了双亲委派。
打破方式:
1.自定义类加载,重写loadclass方法
因为双亲委派的机制都是通过这个方法实现的,这个方法可以指定类通过什么类加载器来进行加载,所有如果改写他的加载规则,相当于打破双亲委派机制
因为双亲委派的机制都是通过这个方法实现的,这个方法可以指定类通过什么类加载器来进行加载,所有如果改写他的加载规则,相当于打破双亲委派机制
2.使用线程上下文类
双亲委派模型的第二次“破坏”是由这个模型自身的缺陷所导致的,双亲委派很好的解决了各个类加载器的基础类统一问题,基础类之所以“基础”,是因为他们总被用户代码所调用,但是如果基础类又要重新调用用户代码,那咋办?
比如说JNDI是java的标准服务,它的代码是由启动类加载器进行加载的,但是jndi的作用就是进行资源的集中管理和查找,它需要调用由开发人员开发在classpath下的类代码,但是启动类加载器不会进行加载。
所以引入线程上下类加载器,通过java.lang.Thread类的setContextClassLoader()方法进行设置。如果创建线程是还未设置,它会从父线程继承一个,如果在应用程序全局范围内没有设置,那么这个线程上下类加载器就是应用程序类加载器。
那么这样JNDI服务使用这个线程上下类加载器去加载所需的spi代码,也就是父类加载器请求子类加载器去完成类加载的动作,这个实际是打通了双亲委派的逆向层次结构。
双亲委派模型的第二次“破坏”是由这个模型自身的缺陷所导致的,双亲委派很好的解决了各个类加载器的基础类统一问题,基础类之所以“基础”,是因为他们总被用户代码所调用,但是如果基础类又要重新调用用户代码,那咋办?
比如说JNDI是java的标准服务,它的代码是由启动类加载器进行加载的,但是jndi的作用就是进行资源的集中管理和查找,它需要调用由开发人员开发在classpath下的类代码,但是启动类加载器不会进行加载。
所以引入线程上下类加载器,通过java.lang.Thread类的setContextClassLoader()方法进行设置。如果创建线程是还未设置,它会从父线程继承一个,如果在应用程序全局范围内没有设置,那么这个线程上下类加载器就是应用程序类加载器。
那么这样JNDI服务使用这个线程上下类加载器去加载所需的spi代码,也就是父类加载器请求子类加载器去完成类加载的动作,这个实际是打通了双亲委派的逆向层次结构。
自定义加载器
继承ClassLoader
重写findClass
传入二进制至defineClass
生成calss对象(访问入口)
如果是重写loadClass() ,则打破了双亲委派,不会parent.loadClass了
SPI
通过配置文件目录 全限定类名加载
和Spring的注解自动加载 是两种方式,不可共存
JDBC为例,以前需要代码手动引入驱动,现在全类名文件写在了Jar包中,只需要热插拔就行了
打破双亲委派:调用方法和接口定义往往在核心类中,实现类由开发者实现,在app类加载器中,spi的serviceLoader通过线程上下文拿到实现类的classloader
应用:最小化服务提供者SDK/jar包,指定按需加载service,则用SPI模式指定加载类即可
缺点:遍历加载所有实现类,不能按需加载
执行模式
解释模式
不编译,启动快
逐条读入,执行,执行慢
适合小内存客户端
编译模式
将class节码文件编译成机器语言
启动慢,但执行快
混合模式
jdk8默认
起始阶段采用解释模式
热点代码会被JIT编译为hotspot本地代码,C执行
JIT会进行逃逸分析,如果发现一个对象不会被外界访问,通过标量替换代替这个对象
从而达到栈内存分配,避免了堆内存分配和垃圾回收
从而达到栈内存分配,避免了堆内存分配和垃圾回收
例如:new了某个对象,如果这个对象只在方法内部活跃,就可以在栈中new 而不是堆中
锁消除优化(堆中加锁消耗太大)
需要一定时间和调用频率才能触发 JIT 的分层机制
出现罕见陷阱(新的类继承关系),则机器语言逆优化为字节码文件重新编译
适合服务管理端,长时间优化编译
AOT模式(Ahead-of-Time)
jdk9之后推出(实验阶段)
将字节码直接编译成机器代码,避免了JIT预热等各方面的开销
运行前(build)全部编译完成,不占用运行时资源
CGLIB动态代理使用ASM技术,运行时直接在内存生成并加载修改后的字节码文件,AOT无法支持
无法跨平台,因为不是class文件,而是机器码
JVM优化
指标
吞吐量
堆内存大
能处理的QPS变大
单次GC时间长
导致后面排队的线程等待时间边长
低延迟
堆内存小
能处理的QPS变小
单次GC时间短
线程等待的延迟小
适用于交互性应用,尽量不要有明显的STW时间
GC方案
最小化内存
Serial(新)
Serial Old
Serial Old
最大化吞吐量
Parallel Scavenge(新)
Parallel Old
Parallel Old
可动态调节目标吞吐量和最高延迟时间
最低延迟
ParNew(新)
CMS
CMS
耗时的清理阶段可与业务线程并发,所以STW低
G1
并行+并发
分代收集
空间整合
可预测的停顿
策略
出现OOME时自动生成堆dump
HeapDumpOnOutOfMemoryError
HeapDumpOnOutOfMemoryError
xms和xmx 设置相同,防止内存抖动
SLB摘流执行,备份机执行,UAT压测执行
命令
Jps
列出Java程序进程ID和Main函数名称
jps -l:输出主函数的完整路径
Jstat(堆内存,gc)
查看Java程序运行时相关信息,可以通过它查看堆信息的相关情况
jstat -gc 7063 500 4 7063 是进程ID ,采样时间间隔为500ms,采样数为4
Jinfo
查看正在运行的java程序的扩展参数,甚至支持运行时修改部分参数
jinfo -flags 8384 查看所有参数
Jmap(进程内存)
查看堆内存使用状况,一般结合jhat(分析工具)使用
jmap -dump:format=b,file=2022xxxx.hprof 产生堆内存转储文件
jmap - histo 进程号 | head -20 对象占用比重
Jstack(线程,锁)
jstack用于生成java虚拟机当前时刻的线程快照
blocked ----waiting to lock
jstack -l pid > jstack_info.txt 将指定进程的当前堆栈情况记录到某个件中
jstack pid 线程内的调用栈信息
工具
java Visual VM
java文件夹里 bin自带的
MAT
eclipse
jhat
命令行的
GC easy
读日志的,关系不大
arthas 阿尔萨斯
dashbord命令 等于top-hp jinfo等命令组合
heapdump 替代jmap
thread 列出所有线程 thread-b 查找死锁
(重要)jad 反编译指定类,可排查线上代码版本是否符合预期
(重要)redefine(在线重新编译class,无需重启,救命用) 重新编辑vim xx. java ,并编译为class(javac XX.java),上传至远程再redfine。
(重要)trace 单机版的链路追踪
基础
反射
getConstructor
获取public构造器
getDeclaredConstructor
获取所有构造器
属性获取同上
反射是否破坏了对象封装性
为了动态性和拓展性,虽然可以破坏,但一般不会用于破坏private
注解
内置注解
类似于OverWrite
元注解
负责注解其他注解
内部类
可以隐藏细节和内部接口,封装性更好,让程序结构更加合理,当子部件
非静态内部类,只能通过外部类来创建
静态内部类无需依赖
匿名内部类
函数式接口,直接new接口自行实现,不需要专门写impl
泛型
静态泛型方法的占位符,与类的占位符无关
擦除模式 转换为字节码时,会擦除泛型
为了兼容JDK老版本的编码
因为泛型的实现需要改变Java的类文件格式。
比如反射去拿类信息 .add() 是不会受到泛型限制的
所有的类型转换为Object类型
只存在于编码编译阶段
泛型不存在继承和父子关系,需要强一致
通配符代替<?> 所有的父类
会被绕过类型限制
<? extends T> 上边界解决问题。指定子类关系
<? super T> 下边界解决问题。指定父类关系
序列化
对象转二进制字节流,保存到磁盘的对象会有一个序列化编号,故同一个对象,不会重复覆盖更新序列化
方式
继承Serializable接口,普通序列化
ObjectInputStream
ObjectOutputStream
serialVersionUID 为版本号,如果不指定,jvm 会按照自己的计算规则,版本不同代表 class 文件被更改,无法反序列化
继承Externalizable接口,强制自定义序列化
必须有无参构造函数(反序列化是通过反射)
必须重写 writeExternal 和 readExternal 自定义要序列化哪些属性
性能更好,但复杂度变高
应用
深浅拷贝
implements Cloneable
实现Cloneable接口 tt.clone() 完成深拷贝
也可重写clone 指定泛型 就不用强转了
不能完成对象类型的clone,需要逐步递归实现Cloneable
implements Serializable
所有引用类型的成员变量均实现Serializable
只需复制的类重写clone方法
将对象本身序列化到字节流
在将字节流反序列化得到对象副本
RPC 框架传输
存储到 DB 或 redis 也需要序列化
代理
静态代理
可拆卸功能变多,代理类一遍遍新增和套娃和重写,成本过高
动态代理
只写增强部分,不用创建代理类
在内存中生成代理对象
基于JDK(接口)
Proxy.newProxyInstance
内置反射技术,AOP适用
基于CGLAB(父类)
Enhancer.create
Spring默认支持两种代理方式
如果目标类实现了接口,自动选择jdk动态代理
CGLAB要求低,更通用
包装类
Byte,Short,Integer,Long 引入了-(128) - (127)的缓存数据,所以可以直接==判断,范围之外只能equls判断
String
jdk9 如果字符串包含的汉字没有超过latin-1范围,则用byte数组存储,只占8位1个字节,而char要2个字节
++ += 实际是用了StringBuilder的append ,但循环场景,会创建多个StringBuilder对象
常量折叠: 两个加法常量,编译器编译的时候会自动合并成一个,而变量的加法则会通过StringBuilder生成新的对象
try with resource
JDK1.7之后的语法糖,可以实现自动关闭资源,无需finally块。多个资源关系很方便,需要在try中声明资源,并且实现了autoCloseable
Throwable
Exception
Checked Exception
编译无法通过,需要catch货throws
Uncheck Exception
例如runtime 编译无法识别
Error
程序无法处理,JVM运行错误等,会终止线程
集合
List
ArrayList
数组
尾部add O1 插入删除 On-1 支持快速随机访问(RandomAccess接口标识)
列表结尾预留一定容量空间
扩容1.5倍
Vector
数组 线程安全
LinkedList
双向链表
头尾O1 插入删除On 不支持快速随机访问
每一个元素花费的空间比较多
Set
HashSet
hashMap实现
LinkedHashSet
LinkedHashMap实现
TreeSet
Map
hashMap
数组+链表+1.8红黑树
可存null
多线程弊端
2个元素hash碰撞,同时插入到链表第一个互相覆盖
current CAS解决
put的同时在扩容,链表逆转(1.8已修复)会导致闭环链,get的时候死循环
current 多线程协助扩容
强一致性的迭代器,一边迭代一边插入数据,会报错
current 弱一致性迭代器,多线程改变不会影响当前迭代
ConCurrentHashMap
发展
1.7、数组+Segment+分段锁
1.8、synchronized+CAS+HashEntry+红黑树
put
1、非空判断+是否初始化判断
2、计算key的hashcode,与数组长度-1 做与运算,定位索引位置
无元素->新增数组则CAS插入
有元素->判断是否扩容->协助扩容
有元素->synchronize加锁,DCL检验,判断链表插入/红黑树插入
无元素->新增数组则CAS插入
有元素->判断是否扩容->协助扩容
有元素->synchronize加锁,DCL检验,判断链表插入/红黑树插入
3、插入之后判断是否满足链表大于8,且数组长度大于64,满足则转换transfer,否则扩容(2*old)
扩容
put或者putAll触发tryPresize
转树之前判断<64,则tryPresize
保证每次是2的次幂,位运算的时候可以有效散列,且提高运算效率
对数组长度取模,防止hash过于松散,40亿的映射空间
hashTable
数组+链表
LinkedHashMap
增加了双向链表
TreeMap
红黑树(自平衡的排序二叉树)
Queue
PriorityQueue
数组 二叉堆
ArrayQueue
数组 双指针
Deque(双端队列)
ArrayDeque
可变数组的双指针实现
性能高于LinkedList
多线程
原理
进程
静态概念 -程序进入CPU内存,分配资源,占进程,独立的地址空间 (程序的实例)
若多进程处理问题,互相访问数据风险大
线程
为了解决进程间互相访问,出现了线程
动态概念-可执行的计算单元(指令),共享进程空间
CPU核数=计算单元=同一时间的线程数
超线程
1核装2套程序计数器存数据,可以跑2线程,但是只共享一个CPU算术逻辑单元ALU
协程
和主程序并行,一个线程可以N个协程,但函数之间是串行的
一个特殊的函数——可以在某个地方挂起,并且可以重新在挂起处继续运行
I/O阻塞时,利用协程来处理(切换效率比较高),但其不能利用多核(串行),必要的时候,还需要使用综合方案:多进程+协程
CPU级别并发控制
mesi缓存一致性
CPU ALU计算单元处理速度是读取内存的100倍,所以中间需要3层缓存增加读取速度
这也是为什么会有指令乱序的原因,CPU等待的时间,顺便把计算逻辑算了
多线程访问缓存行,若处在同一缓存行则需要相互通知失效变更
可以凑64字节,使得关键变量不会在多线程环境下处于相同缓存行
例如:Disruptor的环形队列,就在指针的前后声明了7个Long共56个字节,使得每个线程拿到的指针变量,一定不会出现同一行
@Contended注解 自动补全 java8
关中断
芯片级别关闭所有可能中断的命令
系统内存屏障
SS、LL、SL、LS 保证指令一致性
JVM层面的volatile 也可使CPU层加lock实现禁止重排
编译级别+指令级别
总线\缓存锁
Lock
linux内核提供各种锁的API,为了控制CPU层面锁的效果
锁
AQS
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 CLH 锁 (Craig, Landin, and Hagersten locks) 实现的。
阻塞队列通过node节点表示
AQS 是一个用来构建锁和同步器的框架
JUC
ThreadPoolExecutor
java util concurrent 并发包
线程池等
CAS
例:AtomicInteger
递增无需加锁
unsafe类 底层compare and swap C++实现
读取值,做递增,再读取一遍和之前读取的对比有没有变化
Atomic cmpxchg(非原子性) 内联方法 硬件支持cas汇编指令 如果是多个CPU前面还有lock硬件锁,优先锁cacheline ,其次北桥信号,用lock进行强锁总线
ABA问题(AtomicStampedReference解决)
重新读取的和之前读取的一样,认为没有发生变化
加version或者stamp来标识,是否真的和之前读取的一样。
stamp用户自行实现,在AtomicStampedReference的内部会将这两个变量封装成Pair对象
场景选择
单个任务执行效率快,就用CAS,自旋消耗CPU不高。
单个任务执行很慢,用synchronize,放入队列,不消耗CPU
jdk1.5之后,synchronize内部有锁升级过程,可以自己从偏向->自旋->重量级锁
CAS不会被阻塞,会在同一线程自旋,消耗CPU,但不会产生线程切换,synchronize会被阻塞/唤醒,频繁的线程切换
synchronize
过程
java代码:synchronize
字节码编译:monitorenter monitorexit (监视器)
执行过程中自动升级
底层汇编 lock comxchg
实现
实际是给对象头记录信息(markword)
1.6之后做了大量优化,如⾃旋锁、适应性
⾃旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。所以,你会发现⽬前的
话,不论是各种开源框架还是 JDK 源码都⼤量使⽤了 synchronized 关键字。
⾃旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。所以,你会发现⽬前的
话,不论是各种开源框架还是 JDK 源码都⼤量使⽤了 synchronized 关键字。
死锁
预防(破坏条件)-避免(银行家算法,阻止系统进入不安全状态)-检测-解决
锁粗化
两把/多把锁 合成为一把锁
解环
反转某一个元素达到目的(效率不高)
奇偶数反转(反转一半元素,效率高)
剥夺资源、终止进程
锁升级
无锁态
偏向锁(标签级)
没有争抢
jdk15之后 废除
轻量级锁(自旋/无锁)
较消耗cpu (自旋线程超过核数2分之1/自旋10次以上 升级)1.6之后,JVM自适应自旋,自己判断是否升级
重量级锁(无序队列 非公平)
切换用户态到内核态 做到硬件互斥
每一个重量级锁,后面跟着一个队列,不消耗CPU,转换为线程切换成本
分布式锁
redisson
集群主节点挂了,没有同步至从节点,则会重复获取锁
redLock方案,依次从多个独立的redis实例申请加锁,半数以上成功
实现复杂,性能差
问题汇总
死锁问题,如进程挂了没有释放
过期时间解决
实效性问题,过期时间不应该太长,太短没事,会续期,太长挂了会一直阻塞
主节点挂了,重复加锁。 redlock解决,但过于复杂,不实用
颗粒度过大,锁定资源太多,阻塞过多。
原理
加锁(可重入)
Redisson锁采用的是hash结构,Redis 的Key为锁名,也就是初始化时传入的name参数,hash的key为锁的id属性+线程id,hash的value为加锁次数
锁续期(默认30s)
超时时间的1/3 为间隔 去校验线程是否继续进行,然后重置锁的时间
解锁
锁次数减一,锁续期节点减一,为0则释放,取消锁续期任务
volatile
可见性
线程间有变化,下次用到重新从主内存 读进各个线程的工作内存
有序性 (禁止指令重排,加内存屏障)
volatile写(JVM层面)
前面会加StoreStroeBarrier;后面会加StroeLoadBarrier
保证了volatile写之前所有指令写完,volatile写之后完成,后面的指令才能读
volatile读(JVM层面)
读后面加了LL和LS内存屏障,volatile读完了 后面才能读和写
CPU层面 hostsport源码,用lock进行强锁总线,保证同一时间只有一个CPU工作进程访问内存,访问之后会让三级缓存进行更新
反例:new一个对象分三部:申请内存实例化--初始值--创建引用关联 ,若没有修饰volatile,后两步可能重排,其他线程可能拿到半成品对象
所以 DCL(double check lock)场景 需要加volatile关键字,防止多线程拿到半初始化对象
小问题
volatile 修饰变量,会告知编译器每次都从变量的地址读取数据,而在局部变量上,一般使用后即释放,会有很多函数的局部变量用同一地址的SRAM,如果中断函数改写了该数据,会导致执行异常。
ReentrantLock
与synchronize区别
可选公平或非公平(默认)
FairSync 公平锁是新线程进入同步队列尾部,直接调用acquire()
NonfairSync 非公平是尝试直接插队抢占,与对头线程参与锁竞争CAS,抢占失败才acquire()
构造函数 true = 公平
可中断
基于api实现,lock() unlock() , 必须手动释放锁,但 sync 不用
可以创建多个condition队列,可以精确signal唤醒
传统synchronize只有一个队列,由jvm随机选择唤醒
传统synchronize只有一个队列,由jvm随机选择唤醒
LockSupport
LockSupport.park() ----挂起
LockSupport.unpark() ----唤醒
JVM实现了互斥量,内部只是一个标记,可以实现 先叫醒再挂起
可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根揭底,LockSupport调用的是Unsafe中的native代码。
LockSupport 是AQS 底层阻塞线程的常用方式
CountDownLatch(1.8)
latch.await 挂起门栓
latch countdown 放开门栓
分N个子线程执行,state为N,每个子线程coutDown一次,state减1,所有执行完会unpark
多个线程并行执行,主线程await 即可等待所有的coutdown执行完毕再继续执行
Semaphore
设置资源可共享的数量,可设置公平/非公平
线程
线程数量因素:看每个线程对CPU计算的实际利用率(理论,不实际)、CPU核数、CPU每核期望的利用率
实际:压测看预期
实际:压测看预期
disruptor
forkjoin
ThreadLocal
同一线程的共享变量,源码体现:Thread类中 有一个ThreadLocalMap变量(Thread.threadLocals),k=ThreadLocal v= 放的value
set方法中,会new Entry(k,v),Entry继承了弱引用WeakReference,k则是弱引用的ThreadLocal
弱引用防止内存泄露: 若外层ThreadLocal的强引用消失了,map里面的key 弱引用也会跟着被GC,ThreadLocal才会真正被回收
因为value没有回收,必须外加remove方法,为了把Map中的那一行彻底清除
因为value没有回收,必须外加remove方法,为了把Map中的那一行彻底清除
应用: @Transactional 事务中,多个方法保证拿到同一个jdbc的connect
拓展
InheritableThreadlocal
可以线程传递,但如果是线程池管理,并不会线程初始化(复用),导致不会传递参数
TransmittableThreadLocal
解决了 Inheritable 无法线程池传递的问题,copy 方法进行任务执行时的传递
TransmittableThreadLocal本身增加一个静态的holderMap(可理解为中转站),里面保存了所有使用过的TransmittableThreadLocal作为key的引用,这样在复制TransmittableThreadLocal的值到线程本身的ThreadLocal时,就可以通过该holder遍历到所有的TransmittableThreadLocal。
TransferQueue
同步队列,容量为0
queue.transfer() 扔进元素,必须等queue.take()拿走元素,才会下一步执行
线程池7参数
corePoolSize(核心线程数)
cpu/1-阻塞因子
IO越密集、阻塞因子越大,线程数设置越多,这样 cpu 不会空等,有效利用 cpu
CPU 计算密集,线程数不能太多,因为太多也没用
根据压测来定
默认是任务到达启动,但可以用方法提前开启
maximumPoolSize(最大线程数)
如果你设置了keepAliveTime,空闲超时之后,会把超过 core 数量的部分回收
如果设置了executor.allowCoreThreadTimeOut(true),会把核心也回收
keepAliveTime
如果你设置了keepAliveTime,空闲超时之后,会把超过 core 数量的部分回收
TimeUnit
BlockWorkQueue
无限创建线程的消耗较高,所以需要队列
有界队列 要考虑拒绝策略
无界队列 要考虑提交任务过快内存溢出
阻塞队列主要是:有限的队列长度,队列满了防止溢出,可以阻塞保留当前任务;
队列为空,阻塞线程,保持核心线程不退出
队列为空,阻塞线程,保持核心线程不退出
PriorityBlockingQueue (优先级队列 )
底层是小顶堆形式的二叉堆
弊端 无界队列 防止溢出可以继承 重写 offer 方法,满足一定数量拒绝
构建的时候传入 Compare 规则
等待时间长可以重新入队 提高优先级
ThreadFactory
RejectHandel
拒绝策略
丢弃
处理代码为空,为模板代码,可重写
抛弃最久的一个任务
给主线程执行
报错
线程应用
执行顺序
先判断核心线程数满没
塞进队列
核心线程会while(getTask!=null) 一直阻塞从队列拿任务
在决定是否新增非核心队列
最大(有界队列的话)也满了,就拒绝策略
上为JUC 原生顺序,tomcat/Dubbo 等模型,为 IO 密集型,重写了队列的入队 offer 方法更改顺序
优先创建线程,避免阻塞时间过长,如果最大线程数也满了,才会放入队列
优先创建线程,避免阻塞时间过长,如果最大线程数也满了,才会放入队列
execute()
runnable
submit()
runnable和callable都可以 可以future 接收返回值
会被包装成 fetureTas() 执行任务
线程池状态流转 (通过ctl控制,高三位是状态,低29位是工作线程个数)
running
shutdown()/shutdownNow()
shutdown 不接受新任务,当前和队列会处理完
shutdownNow 中断当前任务,队列不处理,新任务不处理
tidying(自动)
过渡状态
terminated(自动)
只有tidying才可以过渡,彻底关闭,等待GC
遇到的问题
防止任务丢失
try catch 捕获异常
Feture get 获取结果
Exception Handle 拦截异常
重写 afterExecute 捕获异常
共享线程池
长任务占用,其他场景被阻塞
threadLocal 线程池场景会失效,用阿里的 ttl
Spring 容器关闭 容易丢数据
Spring 容器中如果用 JUC原生的线程池,容器关闭容易丢失数据,无法利用 Spring 的 Bean 生命周期管理
可以用Spring 提供的 ThreadPoolTaskExecutor,或者 DynamicTp 框架提供的 DtpExecutor 线程池实现。还可以进行业务隔离
可以用Spring 提供的 ThreadPoolTaskExecutor,或者 DynamicTp 框架提供的 DtpExecutor 线程池实现。还可以进行业务隔离
OOM
自己指定参数,用内存安全队列,入队之前判断是否达到内存阈值 比较合理
锁的应用
ThreadPoolExcute 用到了 ReentrantLock 类型锁 mainLock,保证了largestPoolSize、completedTaskCount的准确性。
(变量为 HashSet 不安全容器,所以要用锁,并且要实时拿到准确的,而不是等方法完成才拿到,所以没必要用 volatile)
(变量为 HashSet 不安全容器,所以要用锁,并且要实时拿到准确的,而不是等方法完成才拿到,所以没必要用 volatile)
Worker 继承 AQS 主要就是为了实现了一把非重入锁,维护线程的中断状态,保证不能中断运行中的线程(runWork 的时候会加锁,表示一个线程正在执行任务)。
lock() 方法会调用 tryAcquire() 方法,CAS 加锁
lock() 方法会调用 tryAcquire() 方法,CAS 加锁
监控,告警
ThreadPoolExcuter 提供了多种 set 和 get ,可用来监控指标和动态调整参数(活跃线程数、队列容量等)
通过钩子函数还可以算出任务执行时长告警等
通过钩子函数还可以算出任务执行时长告警等
线程池
newFixedThreadPool
无界队列的固定线程池
队列大量阻塞请求 OOM风险,队列容量是Integer.MAX_VALUE
newSingleThreadExecutor
单例线程池,无界队列,保证顺序执行
队列大量阻塞请求 OOM风险,队列容量是Integer.MAX_VALUE
newCachedThreadPool
核心=0,最大=MAXVALUE,适用于执行很多的短期异步任务,或者是负载较轻的服务器 队列没有容量
线程数容量是Integer.MAX_VALUE,OOM 风险
newScheduledThreadPool
创建一个以延迟或定时的方式来执行任务的线程池,工作队列为 DelayedWorkQueue。适用于需要多个后台线程执行周期任务。
线程数容量是Integer.MAX_VALUE,OOM 风险
newWorkStealingPool
JDK 1.8 新增,用于创建一个可以窃取的线程池,底层使用 ForkJoinPool 实现。
tomcat 进行了重写队列。为了满足 IO 密集,任务吞吐量大。 还可进行内存阈值告警
虚拟线程
概念
虚拟线程是没有绑定到特定操作系统线程的线程
平台线程是以传统方式实现的线程,作为围绕操作系统线程的简单包装。
阻碍
堆栈跟踪不提供可用的上下文
调试器不能单步执行请求处理逻辑
thread-per-request style
与 Java 平台的设计相协调,易于理解、易于编程、易于调试和配置
可用线程的数量是有限的,操作系统线程代价高昂,因此我们不能拥有太多线程
asynchronous style
这种细粒度的线程共享允许大量并发操作,而不需要消耗大量线程,提高可伸缩性
这种编程 style 与 Java 平台不一致,因为应用程序的并发单元(异步管道)不再是平台的并发单元
意义
Java 运行时实现 Java 线程的方式可以切断它们与操作系统线程之间的一一对应关系
Java 运行时也可以通过将大量虚拟线程映射到少量操作系统线程而给人一种线程充足的错觉
保留了thread-per-request style的思想一致性,又有大量廉价的线程可用
用途
并发任务的数量很多(超过几千个)
工作负载不受 CPU 限制(计算需要用到的核心线程数)
调度
JDK 的调度程序不直接将虚拟线程分配给处理器,而是将虚拟线程分配给平台线程。操作系统像往常一样调度平台线程。
JDK 的虚拟线程调度程序是一个在 FIFO 模式下运行的工作窃取(work-stealing) 的 ForkJoinPool
窃取:线程执行完自己队列的任务,会去其他队列窃取任务执行,从尾端拿。(双端队列)
Spring
Spring(框架、容器、生态基石)
简化开发
IOC
pojo轻量级,最小侵入
容器管理Bean
依赖注入 ,接口实现松耦合
DI为实现方式
AOP
基于切面声明式编程
事务自动管理
切面和模板减少样板编程
日志配置切面
IOC的一个扩展点而已(BeanPostProcessor)
advice、切面、切点完成代理过程
JDK/CGLAB生成代理对象
调用方法时,调用字节码文件中,DynamicAdvisoredIntercept的intercept方法
根据定义好的通知生成拦截器链
注解
注册bean
@Service
本质也是Component
@Component
基础的
@Configuration+@Bean
人为注册bean
获取bean
@Autowired
Spring提供
按类型装配,如果想使用名称装配可以结合@Qualifier注解进行使用。
@Resource
由J2EE提供
按名称装配
功能
@PostConstruct
在依赖注入完成后被自动调用
Constructor >> @Autowired >> @PostConstruct
实现InitializingBean,重写afterPropertiesSet也可实现
事务
隔离级别 (若与数据库配置不同,以spring配置为主)
read uncommitted
可以读到别的事务未提交的数据,并进行处理
存在问题:脏读+不可重复读+幻读
写锁的时候,允许其他读锁访问,所以可以读到未提交的
read committed
默认Isolation为read committed
可以读到别的事务已提交的数据 ,但一个事务范围内的两个相同的查询却可能返回了不同数据
存在问题:不可重复读+幻读
写锁的时候,其他读锁不能访问,但是写之前和写之后都能读
repeatable read
只能看到(当前)事务开始之前提交的数据;它从不看到未提交的数据或并发事务在(当前)事务执行期间提交的更改
存在问题:幻读(count范围变了,insert影响,读到的值,进行后续逻辑,其实DB中已经不是该值)
读锁的时候,其他写锁不能访问(行锁),所以数据在被读取的时候,禁止了其他事务对该行的更改
MVCC(分为快照读和当前读,当前读会有一套计算逻辑,算出可看版本)----多版本并发控制,锁定每一个事务的独立快照。(隐藏列,redo undo,记录上一事务id等方式实现)
serializable
串行化执行事务,最大限度隔离
表锁,进行范围锁定
spring实现原理
有@transaction 注解时,spring会将事务的自动提交关闭,并通过AOP动态代理来调用,若报错,会执行回滚
通过TransactionInterceptor 的invoke实现具体逻辑
用ThreadLocal保证了多方法拿到的同一个JDBC的connect
失效
Bean对象没有被spring容器管理
修饰符不是public
自调用没有经过AOP代理
事务方法的调用对象是原对象,是不会触发通知(事务)的
数据源没有配置事务管理器
异常被捕获/异常类型配置错误
回滚过程
准备工作,解析事务属性,判断是否开启事务
开启时,先获取数据库连接,关闭自动提交,开启事务
执行具体sql逻辑
失败:通过completeTransactionAfterThrowing完成事务回滚。具体逻辑通过doRollBack实现。也是先获取连接对象,然后con.rollBack
成功:commitTransactionAfterReturning 通过doCommit实现,也是先获取连接,再提交
清除事务相关信息 cleanupTransactionInfo
回滚规则
只会回滚运行时异常(RuntimeException)
可以将异常包装,或者rollbackFor=Exception.class 进行声明
Bean
IOC思想
一个容器,包含一些map结构方便存储对象
getBean检查是否存在(单例)
读取配置文件/注解 将需要创建的bean封装成beanDefiniton
将BD对象通过反射进行实例化
初始化,变成完整的bean,存储在map中
提供入口通过容器获取对象
提供销毁操作
Bean生命周期
实例化bean对象
通过反射创建,在堆空间申请内存,属性为默认值,放入beanFactory
设置bean属性
检查aware相关接口并设置相关依赖
容器自定义的属性
BeanPostProcessor的前置处理
检查是否实现initializingBean接口
实现则调用afterPropertiesSet
初始化方法调用 invokeInitMethod
BeanPostProcessor的后置处理
这里可以通过AOP生成代理
添加 Bean 对象到容器中
在完成 Bean 的初始化之后,Spring 容器把 Bean 对象生成一个唯一的 ID,并将该对象添加到 BeanFactory 中
注册必要的Destruction相关回调接口
方便对象进行销毁操作
作用域
singleton
单例
prototype
原型,多例,但是回收比较麻烦
单例Bean非安全,若使用类变量则改为@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
若无类变量,多线程调用同一个实例方法,会在内存中复制,自己线程的公共内存,所以安全
若无类变量,多线程调用同一个实例方法,会在内存中复制,自己线程的公共内存,所以安全
注意调用的外层(controller)注入的service,如果controller是单例,则只会注入一次service,多例的service则不生效
或者指定代理类型:proxyMode = ScopedProxyMode.TARGET_CLASS(cglab),每次注入调用都会重新创建新的对象
request
每次HTTP请求将会产生不同的Bean实例
session
每次HTTP Session,使用session定义的Bean都将产生一个新实例
global-session
每个全局的HTTP Session,使用session定义的Bean都将产生一个新实例
三级缓存
一级存储成品对象
初始化完成之后,删除二三级,同时addSingleton
二级存储半成品对象
从三级缓存确定是代理/原对象时,删除三级缓存 同时getSingleton放入二级
三级缓存lamda函数式接口
getEarlyBeanReference() 执行对象的覆盖过程(代理)
createBeanInstance之后:addSingletonFactory
BeanFactory/FactoryBean
BeanFactory 是用来获取 bean,管理 bean 生命周期的工厂
FactoryBean 是一个 bean,可以重写 getObject 来返回 bean,自定义 创建Bean 的规则,做一些增强,context.getBean("myFactoryBean")可以直接获取到 getObject 的值
经验
implements ApplicationContextInitializer<ConfigurableApplicationContext>
目的:动态查询,重写覆盖@Table @Field的值
难点:mybatisPlus会在bean生成之前,把类信息存成静态,无法覆盖
解决:在plus加载之前,找到切入点,应用上下文加载的时候,把类注解的值覆盖到类信息
设计模式
工厂设计模式
BeanFactory
懒加载
ApplicationContext
一次性创建所有的bean,拓展了BeanFactory
单例模式
内部通过CurrentHashMap 保证单里注册表的不冲突
代理模式(AOP)
jdk代理实现接口的对象
cglab代理没有实现接口的对象
模板方法
子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式
观察者模式
Spring事件驱动模型
继承ApplicationEvent 定义事件
实现 ApplicationListener 接口,重写 onApplicationEvent() 方法 定义事件监听者
ApplicationContext 的 publishEvent() 方法发布消息 使用事件发布者发布消息
适配器模式
DispatcherServlet 根据请求信息调用 HandlerMapping,解析请求对应的 Handler。解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由HandlerAdapter 适配器处理。HandlerAdapter 作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller 作为需要适配的类。
装饰者模式(子重写父思想)
spring体现
切换数据源场景--少修改原逻辑,拓展增强类功能
JDK体现
nputStream,InputStream 下边有 FileInputStream (读取文件)、BufferedInputStream等子类,都在不修改InputStream 代码的情况下扩展了各自的功能
mybatis体现
CachingExecutor增强Executor
SpringBoot
SpringCloud
网关
基于Spring WebFlux(Reactor库实现响应式变成模型,底层基于Netty实现异步IO)
统一路由方式,基于Filter链方式提供基础能力(安全,监控,限流)
分布式
定时任务
QuartZ
CRON 基于数据库锁,性能瓶颈,无内置UI
xxl-job
CRON 基于数据库锁,性能瓶颈,内置UI,开箱即用
Elastic-Job
依赖zookeeper注册中心,去中心化,由执行器自行触发
Powerjob
CEON、固定频率、固定延迟、openAPI,无锁化设计
雪花算法
时钟回拨问题解决: 报错,或者预留拓展位,遇到回拨拓展位+1
事务
2PC/3PC
2PC 1、准备提交 2、提交
3PC 1、询问状态 2、准备提交 3、提交
劣势:延迟高,容易阻塞,不适合高并发
TCC(rty confirm cancel)
业务自己提供三个TCC接口 事务协调者调CC ,应用调T(检测预留资源+锁)
优势:CC的幂等保证数据一致性,业务控制资源,锁粒度小,性能提升
劣势:业务耦合高
Saga
命令协调
协调器全权负责调用顺序,执行 等回复
优势:关系简洁易拓展和管理,降低参与者复杂度(只考虑执行和回复)
劣势:中央协调器复杂难维护,单点故障
事件编排
各个业务自身达成一定的调用顺序,不需要事务协调
优势:没有单点故障风险,步骤少
劣势:循环依赖风险,服务间关系混乱 难维护
劣势:事务不隔离,自行加锁控制并发,补偿麻烦
长事务分解成多个短事务
本地消息表/MQ
优势:解耦,保证消费
劣势:数据一致性延迟
zookeeper
满足CAP理论的CP,强一致性,导致写场景性能很差
单独集群,浪费资源,成为性能瓶颈
raft顶替
选举
ZAB选举: 互相推荐,节点竞争,选取最完整节点,单向通信多次,流程复杂
Raft选举:先发现leader挂了,先进行投票。也会参照日志完整性,任期等。但通讯消息数更少(双向通信),选举更快,每轮时间固定
日志
raft的leader日志一定是最新的,最新的日志才能当leader。单向数据协议
而zab经过一系列复杂的选举,当成leader,(需要将自己的日志先更新位最新的),再广播
写流程
Raft:创建log,广播log,半数成功,自己commit,反馈客户端。下一个心跳包,通知小弟commit
Zab:leader创建Proposal,半数成功,广播commit,自己也同时commit,运用到内存树,反馈客户端
服务治理
核心就是 微服务多系统互相调用更好的管理
模块
注册中心
配置中心
负载均衡
路由
熔断、降级、限流
链路追踪、监控
部署
分布式理论 CAP ACID BASE
网络
IO
阻塞IO模型(java BIO)
等待数据就绪和读取数据均阻塞
非阻塞IO模型(java NIO)
等待数据非阻塞,用户拿数据的时候,轮询访问直到数据就绪 (CPU空转)
多路复用则是select空转而已
多路复用模型
Java NIO(new IO)中的Selector和Linux中的epoll都是这种模型
用户态指定监听FD(File Descriptor)集合,内核任意数据就绪返回readable,用户进程进行遍历处理
select
系统轮询调用,每次调用都需要把全部描述符集合从用户态传输到内核态,并且需要判断每个描述符是否就绪
数据就绪之后,会把监听的FD集合都发过去,用户进程需要到FD遍历查找
fd_set大小固定为1024
pool
过程同上
pollfd内核中采用链表,理论无上限
epoll
epoll 是基于事件驱动的模型,通过callback函数只处理活跃的文件描述符,效率更高。
epoll是通过内核于用户空间mmap同一块内存实现的
IO多路复用机制是指一个线程处理多个IO流,多路是指网络连接,复用指的是同一个线程。
信号驱动IO
内核数据准备就绪时,给用户进程返回一个信号,通知应用进程数据准备好的可读状态
用户进程收到信号后,立刻调用recvfrom()进行系统调用内核数据并进行操作
仍然需要用户拷贝
AIO异步IO
内核将所有数据处理完,并拷贝到用户态中,才会传递信号告诉用户进程
无需用户拷贝
JavaNIO
面向缓冲区的、基于通道的 IO 操作(NIO面向块,BIO面向流)
也就是请求IO时不阻塞,但是select()方法其实是阻塞的。
Buffer
堆内存
直接内存(堆外)
本地内存更快,但申请消耗性能
适用于大数据且生命周期长,网络高并发场景,IO操作频繁
Channel
通道是支持读取或者写入缓冲区的,也可以异步地读写,但是是不能直接访问数据的
Selector(pool、epool、select)
检查一个或多个NIO通道,负责多个通道看哪个已经将数据准备就绪
方式
字节流
通过字节传输,例如视频文件,如果是中文utf-8 一个汉字3字节,字节截断容易出现乱码。而且需要提前知道编码规则
字符流
主要传输中文,按照一个字符读取,解决乱码问题,但效率较慢。字符流=字节流+编码
协议
负载均衡
随机
轮询
最小连接数
动态选取积压最小的一个服务器
加权
分成1-10代表份额, 每次访问到的 ip 都符合比例
哈希
ip 地址取模
nginx
cdn
CDN 就是将静态资源分发到多个不同的地⽅以实现就近访问,进⽽加快静态资源
的访问速度,减轻服务器以及带宽的负担。
的访问速度,减轻服务器以及带宽的负担。
主要针对静态资源加速
全站加速
腾讯云ECDN 、阿里云 DCDN
netty
零拷贝
一般来说,jvm处理堆外的数据,会拷贝一份数据,然后交给操作系统进行处理
零拷贝:JVM可以直接访问堆外内存进行操作
NIO,netty的实现方式 ,并用了虚引用
RPC
对于大型企业来说,内部子系统较多、接口非常多的情况下,RPC框架的好处就显示出来了,首先就是长链接,不必每次通信都要像http一样去3次握手什么的,减少了网络开销;其次就是RPC框架一般都有注册中心,有丰富的监控管理;发布、下线接口、动态扩展等,对调用方来说是无感知、统一化的操作。
RPC架构
Client
Server
Client Stub
存放服务器地址的消息,将请求打包成网络消息,通过网络远程发送给服务方
Server Sutb
接收客户端的请求消息,解包(解码,反序列化),并调用本地方法
RPC 调用过程
客户端调用本地方法(接口方式)
Client Stub收到调用后,将方法 参数消息体进行序列化二进制,通过 sockets 将消息发送到 Server sutb
server stub 收到消息 解码反序列化。 调用对应的本地服务
本地执行结果返回给 server stub,然后打包成序列化消息,通过 sockets 发送到客户端client stub
client stub 解码反序列化得到最终结果
RPC 框架
gRPC是Google公布的开源软件,基于最新的HTTP2.0协议,并支持常见的众多编程语言。 我们知道HTTP2.0是基于二进制的HTTP协议升级版本,目前各大浏览器都在快马加鞭的加以支持。 这个RPC框架是基于HTTP协议实现的,底层使用到了Netty框架的支持。
Dubbo是阿里集团开源的一个极为出名的RPC框架,在很多互联网公司和企业应用中广泛使用。协议和序列化框架都可以插拔是及其鲜明的特色。同样的远程接口是基于Java Interface,并且依托于spring框架方便开发。可以方便的打包成单一文件,独立进程运行,和现在的微服务概念一致
过程
服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小
消费者向注册中心获取服务提供者地址列表,并根据负载算法直接调用提供者
注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者
核心组件
provider
consumer
registry
monitor
container
效率
序列化和反序列化是效率的关键,消息体尽可能小最好
一般单连接是够用的,如果数据传输量不足以让单连接的缓冲区饱和,使用多连接也不会有提升,反倒增加连接管理开销
中间件
Redis
意义
内存寻址ns级别,而磁盘寻址ms级别 ( 10万倍)
磁盘-磁道-扇区 扇区-512Byte
索引成本变大
寻址和带宽 两个维度
二进制安全
无视编码,只接受二进制字节存取
Hash算法
普通的取模算法index = hash(key)%N 会根据服务器数量进行计算,若宕机一台会导致N变化,数据的 key 计算会访问其他的服务器获取不到数据
一致性哈希,把 N 变为2的32次方,虚拟结构为一个环,不会受服务器数量影响,数据落在环上顺势往下找机器节点
若机器挂了,也只会影响节点附近的数据,不会导致所有数据 key 偏移
负载均衡也可设计虚拟节点,平均分布在环上,做一个节点映射的策略
类型
String
SDS
定义了len属性,获取长度时间复杂度O1
buf[] 保存字符串, free记录buf中未使用字节的数量
空间预分配
使用free杜绝了缓冲区溢出如果free的长度不够值的长度则自动会开辟len长度的空间
减少连续执行字符串增长操作所需的内存重分配次数。
减少连续执行字符串增长操作所需的内存重分配次数。
惰性空间释放
对字符串进行缩短操作时,程序不立即使用内存重新分配来回收缩短后多余的字节,而是使用 free 属性将这些字节的数量记录下来,等待后续使用
二进制安全
以len长度判断是否结束,而不是空字符串标识(图片二进制文件可能包含空串,会有问题)
bitmaps(位图)
省空间,速度快
按位操作setbit、bitcount、bitpos、bitop
场景:1、统计用户登录天数,窗口随机
2、统计用户活跃度,窗口随机
2、统计用户活跃度,窗口随机
encoding
embstr 当存储的字符串长度小于44个字符时,此时使用embstr方式来存储
raw当存储的字符串长度大于44个字符时,此时使用raw方式来存储
int 当存储的字符串全是数字时,此时使用int方式来存储
好处:提前知道是否可执行操作,无需取出value
hashes
键值对存储计算等场景
单独覆盖某个属性值,无需大json整体出入
list
存储为双向链表结构,正负索引
同向命令-栈特征
反向命令-队列特征
阻塞 单播队列 blpop
sets
无序 去重
算交集/差集 sinterstroe 和sinter 前面的可以将交集结果直接放入新的key,减少IO
随机事件 srandmember/spop
count正数,随机取出去重结果集
count负数,随机取出可重复结果集
解决抽奖场景
sorted sets(Zset)
排序且去重,正逆向索引,物理从小到大左边排列,不随命令变化
给出元素和分值
场景: 排行榜、优先级队列
交集并集+权重
原理: 链表存储,skipList 跳跃表,每个层数随机(类平衡树) (牺牲空间 换取查询速度)
底层链表,上层多级索引链表,Node[] next 存储,方便向下跳级
持久化
RDB(快照)
父进程fork出一个子进程备份,无需其他IO
优势:适用数据集的备份(灾备),恢复更快
劣势:丢失数据较多,数据集过大fork消耗内存空间及性能
不同时间间隔策略不同,数据变更量越大,同步间隔越小
可以手动触发,也可以自动触发(config配置/重启/shutdown)
在有子进程执行RDB过程的时候,Redis主进程的读写不受影响,但是对于Redis的写操作不会同步到主进程的主内存中,而是会写到一个临时的内存区域作为一个副本,等到主进程接收到子进程完成RDB过程的消息后再将内存副本中的数据同步到主内存。
AOF(binlog)(Append Only File)
最大程度保证数据完整。但性能也会降低
频次策略:1、每秒一次,主线程IO写磁盘
2、每次变更都 IO,影响性能
3、系统决定
2、每次变更都 IO,影响性能
3、系统决定
重写策略优化:4.x以前是把数据集操作指令落地,新版是异步线程以rdb形式落地,重写压缩的同时,主线程记录重写过程中的增量,重写结束将增量拼进去,保证过程中的数据完整性
重写策略 ,fork 一个子线程进行 aof 重写, 会把一些重复的命令进行优化
劣势:文件体积大,恢复慢
执行流程
所有的写命令都会追加到aof_buf(缓冲区)中
可以使用不同的策略将AOF缓冲区中的命令写到AOF文件中
随着AOF文件的越来越大,会对AOF文件进行重写。
当服务器重启的时候,会加载AOF文件并执行AOF文件中的命令用于恢复数据
为什么快
存储模型层面
自身内存存储,无需磁盘IO
数据模型层面
每种类型有独特的计算方式,很多统计类方法,实现计算向数据移动,具有IO优势
redis底层提供了动态字符串(SDS)、跳表(skiplist)、压缩列表(ziplist)等数据结构来高效访问数据
6.0之后引入多线程IO 的多路复用(网络连接请求交互等,事件分发器用线程池传输命令),提高IO效率,包括持久化也是异步线程
基于内存命令执行,单机10w,执行线程为单线程足够了(避免线程切换,争抢资源的损耗)
过期策略
定期删除
activeExpireCycle
时长和频率随机检查过期,对数据一致性有风险
2.8之后可以自定义设置频率
惰性删除
expireIfNeeded
CPU友好,内存不友好
Redis 过期删除策略是采用惰性删除和定期删除这两种方式组合进行的,惰性删除能够保证过期的数据我们在获取时一定获取不到,而定期删除设置合适的频率,则可以保证无效的数据及时得到释放,而不会一直占用内存数据。
主从场景
master过期删除,将命令AOF记录,直接通知slave执行删除,达到数据一致,避免了绝对时间误差。
若slave升级为master,AOF还未同步命令,会重新执行淘汰key的策略,保证一致性
内存淘汰(回收策略)
noeviction:不移除任何 key,只是返回一个写错误 ,默认选项,一般不会选用。
volatile-ttl:移除即将过期的 key (minor TTL)
allkeys-random:无差别的随机移除
lun
volatile-lru:利用 LRU 算法移除设置过过期时间的 key (LRU:最近使用 Least Recently Used ),距离上次使用时间最长的被删除
allkeys-lru:利用 LRU 算法移除任何key (和上一个相比,删除的 key 包括设置过期时间和不设置过期时间的),通常使用该方式;
lfu相关
一段时间之内,使用次数最少(频率)
LRU 原理
标准的 LRU(非 redis) 是一个链表,根据实时访问,更新数据在链表的位置,需要淘汰则直接把链表尾部去除
redis 如果采用链表,数据结构会变更复杂,数据的移动开销增加,导致性能降低。故采用近似 LRU 的算法实现
每一个 KV 中的 V,都是一个 Redis Object,额外记录了访问的LRU时钟信息(精度1s),最近的一次时间戳
获取最新时间戳不是调用系统函数,而是访问全局的时钟信息lruclock
每次初始化参数的时候,会设置 lrulock 值更新全局时钟值,可通过配置决定多少 ms更新一次全局最新时钟
淘汰过程
每次被访问,最终都会调用 lookupKey 去更新当前 v 被访问的最新时钟信息
每处理一个命令,都会检查内存快超过配置的阈值了,触发流程
performEvictions 随机(maxmemory-samples 决定采样的 k)从 hash 表选择一些 kv(如果是 allkey 就去全局 hash 表采样)
采样的 k 计算空闲时间,组成待淘汰候选 kv 集合,并按照空闲时间排序
选择倒数最终淘汰的 k,若不满足空间释放大小,则重复12345
集群方案
主从集群
主从复制集群(手动切换)
带哨兵的HA的主从复制集群(故障自动切换)
解决性能问题,但存在数据一致性问题
分片集群
客户端实现路由索引
解决高并发,分摊压力
集群信息修改,SDK修改成本大
中间件代理层实现
客户端解耦
访问量依赖于中间件的吞吐量,中间件变成了瓶颈,违背了初衷
redis自身实现的cluster
客户端对节点的直接访问,无中心节点
自动将数据进行分片,每个master上放一部分数据,内部哈希算法自动重定向(分布式数据存储)
支持master横向扩展提供吞吐量,每个maste都可以挂载多个slave(高可用热备)
如果你要在slave 读取数据,那么需要先执行 readonly 执行,才能 读取对应master写入的数据
redis cluster 的slave默认是不支持数据的读和写的
因为读写分离是为了提高吞吐量,多master分片已经达到了这个目的
redis cluster 的slave默认是不支持数据的读和写的
因为读写分离是为了提高吞吐量,多master分片已经达到了这个目的
hashTag可能会导致热点数据倾斜,热点key随机hash可解决数据存储,访问负载均衡
redis事务
可通过multi开启事务,exec执行事务
redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令
redis事务遇到的命令正确而数据类型不符合时,其之前和之后的命令都会被正常执行。
redis事务遇到的命令错误,无论之前和之后的命令都不会执行
本身不支持事务回滚,因为代码是应该规避的
主从复制原理
全量复制
salve初始化阶段,master生成RDB文件,并同时开启缓存区记录增量
向所有Slava服务器发送RDB文件,并在发送期间继续在缓冲区内记录被执行的写命令
slave服务器收到RDB快照文件后,会将接收到的数据写入磁盘,然后清空所有旧数据
master服务器发送完RDB快照文件之后,便开始向slave服务器发送缓冲区中的写命令
salve继续执行缓存区命令(开启AOPF则进行重写)
增量复制
master产生一个命令,则发送一个命令
断点续传
2.8之后会进行断点续传,之前是全量复制
master在内存缓冲区中给每个slave都维护了一份同步备份日志,缓存最近一段时间的数据
master 和 slave 都维护了一个复制偏移量(replication offset)和 master线程ID(master run id),
每个同步都会携带master run id 和 最后一次同步的复制偏移量offset
通过offset可以知道主从之间的数据不一致的情况。
每个同步都会携带master run id 和 最后一次同步的复制偏移量offset
通过offset可以知道主从之间的数据不一致的情况。
无磁盘化复制
2.8以前:master会将数据保存在磁盘的rdb文件中然后发送给slave服务器
2.8之后:内存中创建RDB文件,再将rdb文件发送给slave服务器,不使用磁盘作为中间存储(省IO)
repl-diskless-sync :是否开启无磁盘复制
注意的点:
在关闭master服务器持久化机制并开始自动重启的情况下,会造成主从服务器数据被清空的情况。也就是master的持久化关闭,可能在master宕机重启的时候数据是空的(RDB和AOF都关闭了),此时就会将空数据复制到slave ,导致slave服务器的数据也丢了
若是哨兵模式,master重启较慢,则savle会当选为master,数据则不会清空。
若重启很快未发现宕机,salve立马重连,则有数据丢失风险
若重启很快未发现宕机,salve立马重连,则有数据丢失风险
延迟队列
消息延时队列(zset)score排序过期时间
顺序队列(list) 消息put 进来的顺序
目标队列(list)到期要消费的消息
流程
生产消息--delayedQueue.offer
生产消息的时候,会把消息放进 zset 和顺序队列
如果队列之前为空,则发送一个scheduleTask,执行延迟时间为过期时间
延迟任务 ScheduleTask 计时器
到时间执行 pushTask->pushTaskAsync
移出前两个 list,放到目标队列
延时任务执行放到进程里面实现
初始化 redissonClient.getDelayedQueue(blockingQueue)
pushTaskAsync 获取已过期的消息,插入目标队列,创建新的快过期的延迟任务
初始化是为了防止客户端挂了,消息堆积没有延迟任务处理
获取 blockingQueue.take()
阻塞获取目标队列消息
MQ
KafKa(订阅模型--分区实现)(pull)
2.8之后 引入了基于Raft,不再依赖zookeeper(用来注册Broker(实例),topic,然后实现负载均衡)
消息可靠性
生产阶段保证
生产者可以send之后配置回调,确保消息都成功发送,若失败则重试
消息确认机制(根据延迟和数据丢失场景做取舍)
ack =0(成功,不等待响应)
ack =1(默认,leader 成功即可)
ack =-1(all)
ISR in-sync replica(ISR)
与 leader 保持同步的集合,也是重新选举的备选集合
若某一次同步,follower 未响应,会被踢出
leader 挂了之后,zk 会优先从该集合中选
高水位线从 isr 中,最短的选举
存储阶段保证
同步刷盘,成功写入磁盘再返回成功,
多分区副本,也可以保证存储不丢
leader 副本的高水位的消息代表未同步/备份成功,不可被消费者消费
日志末端标识,指向下一条消息的位移
一个分区只能被一个消费者消费,所以分区数一定要大于消费者数量
原因: 如果kafka 允许1个partition的数据可以被同组中多个consumer消费,那么多个消费者必然要共同维护同一个 offset,因为这个offset影响数据消费的准确性(消息重复或漏消息),所以offset要做到强一致性。
1、subscribe 订阅模式,自动分配
2、assign 指定分区和key,可以保证同一类消息的顺序消费
2、assign 指定分区和key,可以保证同一类消息的顺序消费
消费策略
at most once 模式 最多一次
at least once 模式 至少一次(默认)
比较靠谱,但要做幂等性防止重复
exactly once 模式 精确传递一次
将 offset 作为唯一 id 与消息同时处理,并且保证处理的原子性。消息只会处理一次,不丢失也不会重复。但这种方式很难做到
配置
acks=all (所有副本收到消息,才认为leader接收成功)
replication.factor >= 3 保证每个分区至少有3个副本
min.insync.replicas > 1 至少写入成功2个副本才算成功
生产消息流程
1、生产者发送消息(可指定 topic、partition、key、value),生产者会把消息序列化为字节数组
2、分区器对消息进行分区
分区算法
轮询
默认
随机
哈希
指定 key 则按 key 取模算法,保证相同的 key 分到一个分区
3、分好区会把消息发送到 broker,broker 会进行 OS Cache的写入,并返回给生产者是否成功
优劣势
优势
吞吐量很高,4核8G 可以承载十几万的 QPS,性能很高
支持集群部署,部分机器宕机不影响使用
劣势
丢消息,因为是先写到磁盘缓冲区,再写到物理磁盘
功能单一,高级功能基本没有,使用场景受限
延迟高,因为是异步。
海量数据,日志分析收集,可接受丢数据
为什么快
磁盘
采用磁盘顺序读写(不需要寻道时间,只需很少的旋转时间)。仅仅将数据追加到文件的末尾,不是在文件的随机位置来修改数据,磁盘顺序写的性能基本上可以跟写内存的性能相差无几
写: Kafka 在写入磁盘文件的时候,可以直接写入这个 os cache 里,也就是仅仅写入内存中,接下来由操作系统自己决定什么时候把 os cache 里的数据真的刷入磁盘文件中
读: mmap 磁盘内存的映射 程序可以直接读内存,就像读磁盘一样
Kafka的零拷贝技术是通过利用Linux操作系统的sendfile系统调用和mmap系统调用实现的,避免了数据在不同缓存中的多次复制,从而提高了Kafka的性能和效率。
oscache 直接将数据发送到网卡
oscache 直接将数据发送到网卡
减少网络 io
批量写入
消息聚合,可设置批次大小,打包一起发送至 kafka 减少网络开销
也可设置延迟发送,接收多一些的消息 一起打包写入
异步发送,不用等待,回调接口确认
分区
1.topic进行分区 -->partition
2.partition为了方便超时删除等管理,又进一步划分segment
3.每个saement.又包括了index文件和 log 文件,可以二分查找-稀疏索引快速定位数据.
4.segment 数据只允许追加的形式.
5.offset是连续的支持预读和批量写
2.partition为了方便超时删除等管理,又进一步划分segment
3.每个saement.又包括了index文件和 log 文件,可以二分查找-稀疏索引快速定位数据.
4.segment 数据只允许追加的形式.
5.offset是连续的支持预读和批量写
rebalance
触发条件
consumer group 成员变更,增删
topic 数变更,有新匹配的 topic 创建时会触发
topic 的 partition 发生变更
三个分配策略
range 平均分配
round-robin topic按顺序排序,轮询依次分配
sticky 尽可能均匀,尽可能保持与上次相同,算法较复杂
过程
选择组协调器
每个 consumer 会选择一个 Broker(消息代理) 作为自己组的协调者,负责监控心跳等
加入消费组 (join group 协议)
消费者会向协调者发送加入请求,协调者会选举 leader 然后同步信息给各个消费者
同步阶段(sync group)
consumer leader会发送同步协议请求给协调者,告诉他分区方案,协调者下发给所有消费者
分区很多,rebalance 会很慢,轻易不要高峰时期触发 (通过 assign 指定了分区 则不会触发rebalance)
事务
RocketMQ(订阅模型--队列实现) 阿里套装
一个Topic可以维护多个队列(提高并发),一个消费者 只消费一个队列
支持优先级队列,延迟队列,死信队列,重试队列
优劣势
优势
吞吐量很高,十万+
保证消息不丢(异步刷盘、同步刷盘、定时刷盘)
支持大规模集群拓展,线性拓展方便(多个 Broker 节点横向拓展)
支持很多高级功能,如消息重试、死信队列
双11,可靠性+大并发,对交易可靠性有很高要求
RabbitMQ(订阅模型--Exchange实现)
采用 Erlang 语言实现 AMQP
优劣势
优势
保证消息不丢
支持集群部署,部分机器宕机不影响使用
支持很多高级功能,如消息重试、死信队列
实时性强
劣势
吞吐量低,每秒几万
集群线性拓展比较麻烦
源码改造困难
数据量小,实时性高,轻便
延迟队列
利用超时和死信队列做延时队列
如果一条消息在 TTL 设置的时间内没有被消费,那么它就会变成一条 Dead Letter(死信)。
ActiveMQ
不适合大消息量,无分片功能
DB
mysql
执行流程
内存缓存
索引原则
回文
索引覆盖
索引下推
最左匹配
ABC 联合索引 AC查询A 有效C失效。 BC查询 均失效
索引失效
like 左模糊匹配 破坏了最左原则
计算、函数、类型转换(自动或手动)导致索引失效
in not in != 范围小会走索引,范围过大会失效(20%)
表结构-性能
分库分表
好处:拆分大数据量,提高性能
坏处:逻辑设计复杂、很难join操作、事务问题、分布式id
ShardingSphere 首选
除了⽀持读写
分离和分库分表,还提供分布式事务、数据库治理等功能。
生态完善
分离和分库分表,还提供分布式事务、数据库治理等功能。
生态完善
读写分离
基于主从复制 依赖与binlog
canal也是依赖于binlog ,更方便
MVCC
多版本事务并发控制,增加了并发性能(一致性快照读)
原理 隐藏列
DATA_TRX_ID
记录最近更新这条记录的事务 ID
自增,用来控制当前事务可见性(<当前事务 id)
主键列
如果没有主键,会多一个隐藏主键列
ReadView
RR 隔离级别下,开始的第一个 select 会生成一个 readView,后续所有语句复用 (形成快照)
RC隔离级别下,每一个 select 都会生成一个 readView,刷新当前快照
B+树
单页16K
1-3层 约2千万数据
1-3次 IO 即可
叶子节点存放数据,非叶子存放指针,非叶子节点通过二分法确定数据在哪个叶
B树存储数据,B+树不存储数据,只存索引数据,所以可承载量大,查询IO次数少。
聚集索引,为主键或无主键的第一个唯一索引,会存数据
非聚集索引,会存索引数据和主键,然后通过主键回表到聚集索引
查询字段被联合索引包含,可减少回表次数
单页之间是链表,页与页时间是双向链表
B 树存放数据,每个叶子存放的量会变少,会增加树的高度,增加 IO
Innodb 特性
插入缓冲(insertbuffer)
二次写(doublewrite)
自适应哈希索引(Adaptive Hash index)
Innodb存储引擎会监控对表上二级索引的查找,如果发现某二级索引被频繁访问,二级索引成为热数据,建立哈希索引可以带来速度的提升
预读(read ahead)
支持事务 ,支持行级锁和表锁,myisam 不支持行锁
DB死锁
insert 场景
2个以上事务 并发插入相同数据
T1成功插入,T2T3紧接着插入,请求插入意向锁
因发生重复唯一键冲突,请求的排他记录锁转换为共享记录锁
T2T3 都需要争抢排他锁,但需要对方先释放共享锁,产生了死锁
防止一秒内多条重复插入。(按钮防抖 + 2s 内请求幂等防止 slb 重试)
保持事务简短并在一个批处理中
select for update 高并发场景也会死锁,两个线程都拿到间隙锁,获取插入意向锁要等待对方释放间隙锁
update 场景
innoDb 边扫描边加锁,如果 where 条件是非主键索引,会锁住索引,再去锁主键索引
假如线程A对a b c d e五条数据边扫描边加X锁,而同时线程B对 e f g h a五条数据也边扫描边加X锁
A 锁到 e 会等待 B 释放,B 锁到 a 会等待 A 释放 造成死锁
更新场景使用主键更新,只锁一行数据,不会死锁
常见场景
事务手动提交,update 之后挂了,没提交,会一直有行锁
ES
物理设计
主分片
一个索引划分多个主分片,分不到不同的节点,提高性能
官方默认一个实例节点,同时最多跑5个分片,每一个分片建议数据量30G
副本分片
和主分片保持同步,分散节点进行容错,读能力横向扩展
索引
不可变索引
当索引一旦被读入内存,它就可以在一直在那儿,有效利用内存
不用考虑多线程变更索引的情况,不需要锁
动态索引
新增时,直接生成新的索引,无需重建索引
查询时,每个索引都查询一次,重新聚合(对查询效率有影响)
文档写入过程
1、计算文档 id 的 hash 值,决定目标分片,若不在本分片(协调者)则转发
2、先通过分词器,转换成词条
3、为词条创建倒排索引
4、写入会先写进内存 buffer(不可查询),同时会写入事务日志文件
5、refresh: 1s 后刷新到 os cache,会生成 segment文件 (可查询)
多个段文件最终会压缩合并,写入磁盘
6、flush: translog 文件 会将 os cache 的段文件 写入磁盘,进行一次汇总提交,并清除自身
每次请求执行刷盘
最可靠写入,但影响性能
每隔5s 刷一次
超过阈值
每30min分片自动flush
查询效率
1、数据预热,提前查询,使得数据提前被存到 os cache中(系统内存分配越大,命中率就越高)
2、减少数据量,只存用于查找的索引
3、避免深度分页,因为要从每个分片查出相同的条数,进行重新排列
所以分片越少,查询效率越高
4、冷热数据
es 节点可以配置节点属性为 hot/cold
可以通过定时任务,定期把过期的数据索引属性改为 cold,节点会自己处理数据的迁移
热节点配置调高
秒杀设计
分析需求,读多还是写多
接入层
CDN分流 多城市部署
临时扩容机器
服务层
缓存预热,队列削峰刷盘
存储层
多活,读写分离,分库分表
ES
项目经验
低代码
数据库-业务类型 动态对应,字典等自动映射,类型版本控制,扩展增强
视图行列计算 函数,动态融合sql,与元数据隔离
前置后置触发器 与连接器的结合,实现数据库的触发机制,链路一体化
行列数据权限管控,颗粒度细到人和字段
跨环境一键部署--批导配置-execl载体
外部数据源-动态切换 表结构解析融合
社区
业务模型
表结构设计
帖子放冗余计数 提高响应速率
回复平铺设计--帖子 id 索引,主评论 id 索引,满足多种查询场景
热搜打分设计
未读已读设计
业务敏感性
异步敏感词校验+NLP 的语言识别
隐藏限流 + 报警
数据模型
关键场景的承接 如ES,redis
各大经典场景的对比和方案确定
ES>=5.0 支持 search after
各大场景的压测,降级的承压
DB 承压--缓存计数- 异步入库
咳咳
kafka 保证消费正常 zookeeper
消息确认机制
提供手动/自动提交偏移量的机制,防止重复消费。
分区负载均衡
每个主题N个分区,每个分区负责一部分数据请求,每个消费者加入到消费组中,自动分配分区实现均衡
多副本机制
每个分区有多副本,支持自动选举新的领导者
消息持久化
用文件系统保存消息,防止宕机消息丢失
zookeeper作用
元数据存储:维护kafka的各种信息,如生产消费者,分区等
分区的leader宕机后,通过zookeeper同步信息到service和client,进行选举
broker的注册 相当于kafka的实例注册中心
消费组协调:管理消费者订阅、查看其他主题订阅情况,来进行负载均衡
spring如何处理请求
用户向服务器发送请求,请求被SpringMVC 前端控制器 DispatcherServlet捕获
DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI),判断请求URI对应的映射
根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及 Handler对象对应的拦截器),最后以HandlerExecutionChain执行链对象的形式返回。
DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter
如果成功获得HandlerAdapter,此时将开始执行拦截器的preHandler(…)方法 等拦截器
提取Request中的模型数据,填充Handler入参,开始执行Handler
Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象
此时将开始执行拦截器的postHandle(...)方法【逆向】 后置拦截器
选择一个适合的ViewResolver进行视图解析,根据Model 和View,来渲染视图。
bean创建过程
mysql索引类型
B树索引
hash索引
计算索引值的hash进行快速匹配
计算过程,构建索引过程较慢
等值比较,不支持排序
适用于量小,等值匹配场景
refrange 执行计划
1、const:查询索引字段,并且表中最多只有一行匹配(好像只有主键查询只匹配一行才会是const,有些情况唯一索引匹配一行会是ref)
2、eq_ref 主键或者唯一索引
3、ref 非唯一索引(主键也是唯一索引)
4、range 索引的范围查询
5、index (type=index extra = using index 代表索引覆盖,即不需要回表)
6、all 全表扫描(通常没有建索引的列)
2、eq_ref 主键或者唯一索引
3、ref 非唯一索引(主键也是唯一索引)
4、range 索引的范围查询
5、index (type=index extra = using index 代表索引覆盖,即不需要回表)
6、all 全表扫描(通常没有建索引的列)
1、using temporary(组合查询返回的数据量太大需要建立一个临时表存储数据,出现这个sql应该优化)
2、using where (where查询条件)
3、using index(判断是否仅使用索引查询,使用索引树并且不需要回表查询)
4、using filesort(order by 太占内存,使用文件排序)
2、using where (where查询条件)
3、using index(判断是否仅使用索引查询,使用索引树并且不需要回表查询)
4、using filesort(order by 太占内存,使用文件排序)
group by having 原理
创建内存临时表
全表扫描记录,并记录到临时表的次数
再根据字段排序
B+树,为什么使用B+树
脏读,可重复读
GC特点
内存模型
方法区存的东西
线程如何创建,线程池的使用
继承Thread
实现Runnable
实现CallAble
hashmap 实现细节
threadLocal
redis string 原理
分布式锁集群实现原理
set nx实现, 但有主从同步风险,导致锁丢失,重复加锁,或者线程中断,死锁,加了过期时间 ex和nx又不是原子性,也有死锁问题
一般用redisson的分布式锁,watchdog自动续期
redLock
大多数节点获取成功,则成功
保证了健壮性,但性能会降低。0容忍业务适用
zookeeper分布式锁,一样保证健壮性
tcp 3次4次握手
三次握手为 tcp传输层 建联的过程
客户端发送一个带有SYN(synchronize)标志的数据包给服务端;
服务端接收成功后,回传一个带有SYN/ACK标志的数据包传递确认信息,表示我收到了;
客户端再回传一个带有ACK标志的数据包,表示我知道了,握手结束。
四次挥手 断联的过程
客户端发送一个FIN,用来关闭客户端到服务端的数据传送,客户端进入FIN_WAIT_1状态;
服务端收到FIN后,发送一个ACK给客户端,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),服务端进入CLOSE_WAIT状态
服务端发送一个FIN,用来关闭服务端到客户端的数据传送,服务端进入LAST_ACK状态
客户端收到FIN后,客户端t进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,服务端进入CLOSED状态,完成四次挥手
分布式保存用户登录信息
redis 存token
satoken 轮子
根据ip限流
ip加密为key ,value做increment增量
为什么方法中的变量 不需要volatile
volatile 修饰变量,会告知编译器每次都从变量的地址读取数据,而在局部变量上,一般使用后即释放,会有很多函数的局部变量用同一地址的SRAM,如果中断函数改写了该数据,会导致执行异常。
为什么方法中变量,只有当前线程可见
dubbo原理
rpc框架
Provider服务提供者启动时,需要将自身暴露出去让远程服务器可以发现,同时向Registry注册中心注册自己提供的服务
Consumer服务消费者启动时,向Registry注册中心订阅所需要的服务
Registry注册中心返回服务提供者列表给消费者,同时如果发生变更,注册中心将基于长连接推送实时数据给消费者
服务消费者需要调用远程服务时,会从提供者的地址列表中,基于负载均衡算法选出一台提供者服务器进行调用,如果调用失败,会基于集群容错策略进行调用重试
服务消费者与提供者会在内存中统计调用次数和调用时间,然后通过定时任务将数据发送给Monitor监控中心
服务消费者与提供者会在内存中统计调用次数和调用时间,然后通过定时任务将数据发送给Monitor监控中心
关键技术
动态代理 Client Stub(客户端存根)和Server Stub(服务端存根)
序列化/反序列化
netty NIO 通信
注册中心 zookeeper
rpc && http
性能:RPC通过thrift二进制传输,http json序列化更消耗性能。
传输协议:RPC基于tcp也可以基于http,http只能是http。
负载均衡:RPC自带负载均衡的。http 需要自己搞(nginx)
传输效率:可以自定义tcp协议报文相对较小。http有很多无用的东西(很多头部信息,keepalivetime reffer)
协议
dubbo
缺省协议,采用单一长连接和 NIO 异步通讯,Hessian2 序列化,适合于小数据量大并发的服务调用。
不适合传送大数据量的服务。
不适合传送大数据量的服务。
RMI
RMI协议采用阻塞式(同步)短连接, JDK 标准序列化方式,Serializable-OutStream。
适用数据包大小混合,消费者与提供者个数差不多,可传文件。
适用数据包大小混合,消费者与提供者个数差不多,可传文件。
Hessian
底层采用Http通讯(同步),hessian 序列化,采用Servlet暴露服务。
适用于传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件。
适用于传入传出参数数据包较大,提供者比消费者个数多,提供者压力较大,可传文件。
Http
JSON 序列化
PS
自己实现 SPI 按需加载
消费者要比服务提供者多1:20,否则长链接网卡占不满 行程浪费
传输大报文,占用带宽,网络瓶颈
传输大报文,占用带宽,网络瓶颈
redis数据结构和应用
字符串 单词维度颠倒
Docker
命令
docker images (查看镜像)
docker ps (查看容器)
docker exec -it demo /bin/bash (进容器)
docker logs demo (查看容器日志)
docker rmi <image_id> (删除镜像)
部署 mysql
pull mysql 镜像
创建 mysql 容器(外挂)
docker run -d -p 3180:3306 -v /usr/local/mysql:/var/lib/mysql -e MYSQL_ROOT_PASSWORD="654321" --restart always --name mysqlTest mysql
部署 jar
docker File
FROM openjdk:8-jdk-alpine
VOLUME /tmp
ADD backend-0.0.1-SNAPSHOT.jar app.jar
CMD ["java","-jar","app.jar"]
VOLUME /tmp
ADD backend-0.0.1-SNAPSHOT.jar app.jar
CMD ["java","-jar","app.jar"]
docker build -f ./DockerFile -t aaa:2.0.0 . (构建镜像)
创建运行 jar 容器 docker run -d --restart=always --name demo -p 8081:8081 aa51950cdefb
部署 vue
部署 nginx 容器
docker pull nginx
docker cp ng:/etc/nginx/conf.d/default.conf /root/nginx/conf/default.conf 复制文件
docker run --name ng -d -p 4030:80 \
-v /root/nginx/logs:/var/log/nginx \
-v /root/nginx/html:/usr/share/nginx/html \
-v /root/nginx/conf/nginx.conf:/etc/nginx/nginx.conf \
-v /root/nginx/conf/default.conf:/etc/nginx/conf.d/default.conf \
nginx
-v /root/nginx/logs:/var/log/nginx \
-v /root/nginx/html:/usr/share/nginx/html \
-v /root/nginx/conf/nginx.conf:/etc/nginx/nginx.conf \
-v /root/nginx/conf/default.conf:/etc/nginx/conf.d/default.conf \
nginx
docker exec -it ng /bin/bash (进容器)
部署 vue
上传 dockerFile :
FROM nginx
MAINTAINER shentong
COPY dist/ /usr/share/nginx/html/
FROM nginx
MAINTAINER shentong
COPY dist/ /usr/share/nginx/html/
上传 dist (target 文件)
构建镜像 :docker build -f ./DockerFile -t vv .
部署 docker run --name=myvv -d -p 9020:80 vv
部署 minio
docker run \
--name minio \
-p 9000:9000 \
-p 9090:9090 \
-d --restart=always \
-e "MINIO_ROOT_USER=minio" \
-e "MINIO_ROOT_PASSWORD=minio123" \
-v /root/minio/data:/data \
-v /root/minio/config:/root/.minio \
minio/minio server /data --console-address ":9090" --address ":9000"
--name minio \
-p 9000:9000 \
-p 9090:9090 \
-d --restart=always \
-e "MINIO_ROOT_USER=minio" \
-e "MINIO_ROOT_PASSWORD=minio123" \
-v /root/minio/data:/data \
-v /root/minio/config:/root/.minio \
minio/minio server /data --console-address ":9090" --address ":9000"
0 条评论
下一页