java面试
2021-10-12 16:55:12 3 举报
AI智能生成
Java后台 P7面试知识点大全,持续更新
作者其他创作
大纲/内容
java基础
String
StringBuffer
线程安全
StringBuilder
非线程安全
String.intern
如果字符串常量池中存在,则返回常量池中的对象,如果不存在则将字符串放入常量池,返回当前对象
使用场景
字符串有重复的时候,为了避免重复创建,可以指向常量池的同一个
集合
HashTable
HashMap
JDK1.7
数组+链表
操作
put
get
zise
JDK1.8
null元素
头结点
操作
put
1、对key进行hashcode
2、hash表是否为空,如果为空则初始化hash表
3、计算hashcode所在位置有没有数据,如果没有数据则直接放置
4、如果有数据,则判断key是否相等,如果相等则直接替换
5、如果不同则判断是否为红黑树
6、如果是红黑树,则直接插入树
7、如果是链表,则遍历链表,添加到最后
8、判断链表长度是否大于8,如果大于8则转为红黑树
9、如果元素个数大于阈值,则进行扩容
get
链表
红黑树
resize
1、将容量扩充为2倍
2、将原始hash表的元素重新hash放置在新的hash表中
原理
数组+链表+红黑树
参数
capitail
loadfactor
lambda
面向对象
封装
继承
多态
架构图
子主题
反射
反射创建对象
反射调用方法
反射获取属性值
原理
JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。
实现方式
Class clazz1 = Class.forName("全限定类名");
Class clazz2 = User.class;
Class clazz3 = p.getClass();
优缺点
在运行时获得类的各种内容,进行反编译,对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。
反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;
反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
finalize
描述
保证对象在回收前完成特定资源回收
Java9中以标记为过时
问题
使用不当会造成死锁和效率低的问题
替代方案
Cleaner替换
利用幻象引用和引用队列,我们可以保证对象被彻底销毁前做一些类似资源回收的工作,比如关闭文件描述符(操作系统有限的资源),它比 finalize 更加轻量、更加可靠
它有自己的运行线程,所以可以避免意外死锁等问题
并发编程
线程
线程状态
New
Runnable
Blocking
watting
Terminated
子主题
实现
new Thread
implement Runnable
implement Callable
操作
线程协作
fork
join
wait
notify
notifyall
线程切换
sleep
yield
线程互斥
synchornized
JVM提供
锁升级,性能一般情况下没有问题
不能实现公平锁
不可中断
reentrantLock
JDK实现
可以对多个Condition加锁
可以实现公平锁
可中断
线程安全
三大特性
原子性
可见性
有序性
一致性原理
MESI协议
E(exclusive)、M(modified)、S(shared)、I(invalid)
描述
主存与私有内存的通知与同步
常见实现
synchorize
锁状态
无锁
偏向锁
轻量级锁
重量级锁
优点
JDK的实现,比较简单
能保证原子性、可见性、有序性
缺点
不能自定义锁的状态
不支持公平锁
使用
对象
类
静态变量
原理
monitorEnter、monitorExit
对象结构
对象头
子主题
元素数据
对齐填充
valitile
特性
不完全原子性
原子操作能保证原子性
可见性
内存屏障
LoadLoad
StoreStore
StoreLoad
LoadStore
有序性
happens-before语义
as if seriazes
start
监视器规则
传递性
join规则
原理lock
final
禁止重排序,解决可见性问题
实现方式
互斥同步
synchronized、ReentrantLock
非阻塞同步
CAS、AtomicXXX
线程私有
ThreadLoal
ThreadLocalMap
原理
主要是用到了Thread对象中的一个ThreadLocalMap类型的变量threadLocals, 负责存储当前线程的关于XXX的对象, Key, 以新建的XXX对象为Value; 这样的话, 线程第一次读取的时候如果不存在就会调用ThreadLocal的initialValue方法创建一个XXX对象并且返回
问题
key为null时会出现内存泄漏
set 、remove
实现案例
CountDownLatch
CyclicBarrier
Semaphore
JUC
Lock
ReentrantLock
优点
可多个Condition条件
可中断
基于JDK实现的,操作更灵活
可实现公平锁
AQS
CountDownLatch
允许一个或多个线程等待某些操作完成
不可以重置的,所以无法重用
基本操作组合是 countDown/await
关注的是事件
CyclicBarrier
一种辅助性的同步结构,允许多个线程等待到达某个屏障
基本操作组合,则就是 await,当所有的伙伴(parties)都调用了 await,才会继续进行任务,并自动进行重置。
关注的是线程
Semaphore
信号量
,其基本逻辑基于 acquire/release
原理
将基础的同步相关操作抽象在 AbstractQueuedSynchronizer 中
private volatile int state;
一个先入先出(FIFO)的等待线程队列(使用Node的双向链表来实现),以实现多线程间竞争和等待
基于 CAS 的基础操作方法,以及各种期望具体同步结构去实现的 acquire/release 方法。
排他锁与共享锁
独占模式下有线程获取到许可的过程就是state值从0=>1的过程,只有获取到许可的线程进行,重入的操作state值会从1=>2,在共享模式下当调用countDown()方法时,就是一个线程获取到一个许可,state的值-1,只有当许可消耗完,也就是state=0时才会恢复队列中挂起的线程。
两个虚拟队列: 同步队列sync queue 和条件队列condition queue,
AQS内部维护了一个CLH队列来管理锁。线程会首先尝试获取锁,如果失败就将当前线程及等待状态等信息包装成一个node节点加入到同步队列sync queue里。 接着会不断的循环尝试获取锁,条件是当前节点为head的直接后继才会尝试。如果失败就会阻塞自己直到自己被唤醒。而当持有锁的线程释放锁的时候,会唤醒队列中的后继线程。
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
LockSupport
park
unpark
Collection
BlockingQueue
ConcurrentLinkedQueue
基于 lock-free(无锁),在常见的多线程访问场景,一般可以提供较高吞吐量
LinkedBlockingQueue
基于锁,并提供了 BlockingQueue 的等待性方法
改进了锁操作的粒度,头、尾操作使用不同的锁,所以在通用场景下,它的吞吐量相对要更好一些
ArrayBlockingQueue
源码分析
SynchronousQueue
这是一个非常奇葩的队列实现,每个删除操作都要等待插入操作,反之每个插入操作也都要等待删除动作。那么这个队列的容量是多少呢?是 1 吗?其实不是的,其内部容量是 0。
PriorityBlockingQueue
是无边界的优先队列,虽然严格意义上来讲,其大小总归是要受系统资源影响。
CurrentHashMap
原理
分离锁
内部使用volatile的value字段来保证可加性
使用CAS操作,保证无锁并发操作
put
如果key或者value为null,则抛出空指针异常;
如果table为null或者table的长度为0,则初始化table,调用initTable()方法。
计算当前键值的索引位置,如果Hash表中当前节点为null,则将元素直接插入。(注意,这里使用的就是前面锁说的CAS操作)
如果当前位置的节点元素的hash值为-1,说明这是一个ForwaringNodes节点,即正在进行扩容。那么当前线程加入扩容。
当前节点不为null,对当前节点加锁,将元素插入到当前节点。在Java8中,当节点长度大于8时,就将节点转为树的结构。
addCount
是否需要扩容
如果table为null或者table的长度为0,则初始化table,调用initTable()方法。
计算当前键值的索引位置,如果Hash表中当前节点为null,则将元素直接插入。(注意,这里使用的就是前面锁说的CAS操作)
如果当前位置的节点元素的hash值为-1,说明这是一个ForwaringNodes节点,即正在进行扩容。那么当前线程加入扩容。
当前节点不为null,对当前节点加锁,将元素插入到当前节点。在Java8中,当节点长度大于8时,就将节点转为树的结构。
addCount
是否需要扩容
get
如果该位置为 null,那么直接返回 null 就可以了
如果该位置处的节点刚好就是我们需要的,返回该节点的值即可
如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树,后面我们再介绍 find 方法
如果以上 3 条都不满足,那就是链表,进行遍历比对即可
如果该位置为 null,那么直接返回 null 就可以了
如果该位置处的节点刚好就是我们需要的,返回该节点的值即可
如果该位置节点的 hash 值小于 0,说明正在扩容,或者是红黑树,后面我们再介绍 find 方法
如果以上 3 条都不满足,那就是链表,进行遍历比对即可
size
private transient volatile long baseCount;
@sun.misc.Contended
static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
static final class CounterCell {
volatile long value;
CounterCell(long x) { value = x; }
}
在计算 size 的时候,会将 baseCount 和 CounterCell 数组中的元素的 value 累加,得到总的大小,但这个数字仍旧可能是不准确的。
ConcurrentSkipListMap
CopyOnWriteList
任何修改操作,如 add、set、remove,都会拷贝原数组,修改后替换原来的数组,通过这种防御性的方式,实现另类的线程安全。
比较适合读多写少的操作
Atomic
实现原理
CAS+volatile 依赖于 Unsafe 提供的一些底层能力
常见实现
原子更新基本类型
AtomicBoolean
AtomicInteger
AtomicLong
原子更新数组
AtomicIntegerArray
AtomicLongArray
AutomicReferenceArray
原子更新引用类型
AtomicReference
AtomicStampedReference
AtomicMarkableReferce
原子更新字段类型
AtomicIntegerFieldUpdater
AtomicLongFieldUpdater
AtomicStampedFieldUpdater
AtomicReferenceFieldUpdater
Executor
原理
workSet
存放线程
workQueue
存放任务
使用
优点
提高执行效率,不用每次创建和销毁
提高系统吞吐量
ThreadPoolExecutor
参数
corePoolSize
CPU密集型
CPU核数+1
IO密集型
CPU核数 × 目标CPU利用率 ×(1 + 平均等待时间/平均工作时间)
maximumPoolSize
keepAliveTime
unit
时分秒
BlockingQueue workQueue
ArrayBlockingQueue
SynchronousQueue
不存储元素的阻塞队列
LinkedBlockingQueue
基于链表的阻塞队列
PriorityBlockingQuene
有优先级的队列
RejectedExecutionHandler
直接丢弃
DiscardPolicy
丢弃老的
DiscardOldestPolicy
调用当前线程执行任务
CallerRunsPolicy
异常(默认)
AbortPolicy
Executor方式
newFixedThreadPool(固定大小)
LinkedBlockingQueue
基于数组的有界阻塞队列,Integer.MAX_VALUE
newSingleThreadExecutor(一个线程)
LinkedBlockingQueue
基于数组的有界阻塞队列,Integer.MAX_VALUE
newCachedThreadPool(Integer最大数的线程)
SynchronousQueue
ScheduleThreadPoolExecutor
为任务提供延迟或周期执行
原理
DelayedWorkQueue
ScheduledFutureTask
newWorkStealingPool
ForkJoinPool.defaultForkJoinWorkerThreadFactory
锁
种类
自旋锁
CAS
问题
ABA问题
AtomicStampedReference解决ABA问题
只能保证一个变量的原子操作
循环开销时间长
java中的实现
Unsafe
可重入锁、不可重入锁
可重复可递归调用的锁
ReentrantLock、Synchorized
公平锁、非公平锁
ReentrantLock
独占所、共享锁
ReentrantLock
ReentrantReadWriteLock
乐观锁、悲观锁
CAS
ReentrantLock、synchorized
分段锁
ConcurrentHashMap
偏向锁、轻量级锁、重量级锁
synchorized
读写锁、互斥锁
ReentrantReadWriteLock
JVM
结构图
子主题
java对象
对象头
运行时元数据(mark word)
hash地址
GC分代年龄
锁状态位
类型指针
指向元空间中对象所属的类型
实例数据
类中定义的成员变量
规则:先放父类的成员变量,在放子类的成员变量。相同宽度的变量被放在一起。如果compactFileds参数为true,子类的窄变量可能插入到父类变量的空隙
对其填充
缓存行是CUP与内存交互的最小工作单元,按块进行处理的,每块为64位处理器每次处理 8Byte(64bit)
JMM
内存模型
主内存
本地内存
重排序
缓存一致性原理
过程
加载
类加载器
AppClassLoader
jre/lib
ExtClassLoader
ext
BootstrapClassLoader
classpath
自定义类加载器
自定义类加载的方式
作用
加载非classpath下的文件
加载网络传输的class
加载需要加解密的class
实现热部署功能
加载机制
双亲委派机制
不会自己先加载,会交给父加载器加载
缓存机制
加载后的类会放入缓存,如果缓存中已有则不加载
父类委托机制
优先让父类进行加载,只有父类无法加载的时候,自己再加载
全盘负责
当一个类被加载是,该类依赖的其他类也由该类加载
如何打破双亲委派
重写loadClass
加载阶段完成后,虚拟机外部的 二进制字节流就按照虚拟机所需的格式存储在方法区之中,而且在Java堆中也创建一个java.lang.Class类的对象,这样便可以通过该对象访问方法区中的这些数据。
加载方式
从本地系统中直接加载
通过网络下载.class文件
从zip,jar等归档文件中加载.class文件
链接
验证
验证class文件的正确性
准备
为类的静态变量准备内存
解析
在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。
初始化
静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
普通原始类型静态变量和引用类型(即使是常量),是需要额外调用 putstatic 等 JVM 指令的,这些是在显式初始化阶段执行,而不是准备阶段调用;而原始类型常量,则不需要这样的步骤。
使用
卸载
结构
线程公用
堆
新生代
Eden
Survivor
from
to
Thread Local Allocation Buffer(TLAB)
多线程同时分配内存时,为避免操作同一地址,可能需要使用加锁等机制,进而影响分配速度
结构
start
top
end
Old
放置长生命周期的对象,通常都是从 Survivor 区域拷贝过来的对象。
我们知道普通的对象会被分配在 TLAB 上;如果对象较大,JVM 会试图直接分配在 Eden 其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM 就会直接分配到老年代。
老年代
用来放置 Java 对象实例,几乎所有创建的 Java 对象实例都是被直接分配在堆上
方法区
存储所谓的元(Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。
实现
1.8以前永久代
1.8以后元空间
线程私有
本地方法栈
本地方法的调用
在 Oracle Hotspot JVM 中,本地方法栈和 Java 虚拟机栈是在同一块儿区域,这完全取决于技术实现的决定,并未在规范中强制。
虚拟机栈
一次次的 Java 方法调用
结构
局部变量表
操作数(operand)栈
动态链接
方法正常退出或者异常退出的定义
程序计数器
存储当前线程正在执行的 Java 方法的 JVM 指令地址
元空间
堆外内存
Native Memory Tracking (NMT)分析
垃圾回收算法
原理
可达性分析
问题
循环引用
引用计数器
引用类型
强引用
不会被回收
软引用
内存不够时才会被回收
弱引用
一定会被回收
虚引用
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。
问题
并发情况下,需要保证同步,性能低
加锁(效率低)
Lock Free(复杂)
循环引用(导致垃圾泄漏)
引起连锁回收
对象引用的对象也会回收,导致效率低
GC Roots
虚拟机栈和本地方法栈中正在引用的对象、静态属性引用的对象和常量
算法基础
复制算法
原理
将存活的对象Copy到另一块内存
优点
无内存碎片
实现简单,通过指针后移即可
新生代效率高
问题
浪费一半内存空间
S0/S1,可配置 -XX:SurvivorRatio
会有停顿
实现方式
引用指针
Forwarding指针
图的DFS
BFS和DFS的区别是什么
CPU空间利用率
标记-清除算法
原理
使用链表管理所有的空闲区域,将不存活的对象所占用的内存交还给链表
freelist
特点
内存利用率高
可以用很小的代价实现并发标记和清除
问题
有内存碎片
标记-整理算法
标记-整理算法
标记 - 清除,但为避免内存碎片化,它会在清理过程中将对象移动,以确保移动后的对象占用连续的内存空间。
主要因素
分配效率
回收效率
是否产生内存分片
空间利用率
是否停顿
时间复杂度
实现的复杂度
调优
JVM参数
-Xms
-Xmx
-Xss
-XX:NewRatio
-XX:SurvivorRatio
工具
JConsole
JMap
Arthas
垃圾收集器
Serial 收集器
新生代
STW
一个线程串行
复制算法
ParNew 收集器
多个线程并行,减少STW时间
Parallel Scavenge 收集器
可控制吞吐量
Serial Old 收集器
标记整理算法
Parallel Old 收集器
目标:优先吞吐量
CMS 收集器
目标:减少停顿时间
实现:标记清除算法
初始标记
只标记根节点
不在堆上的引用
栈上的引用
全局变量等
虚拟机的引用
并发标记
漏标问题
CMS解决办法
解决办法:三色标记
往前进
黑引用白,直接将白标灰
会有垃圾
往后退
黑引用白,将黑变灰
重新标记
remark
把剩余的标记
如果年轻代太大,比较耗时,需要先进行一次yong gc
并发清除
问题
内存碎片,会增多FullGC
多CPU竞争,会与用户线程抢占CPU资源
G1 收集器
分块GC
目标
一种兼顾吞吐量和停顿时间
Region 之间是复制算法,但整体上实际可看作是标记 - 整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当 Java 堆非常大的时候,G1 的优势更加明显。
G1 的很多开销都是源自 Remembered Set,例如,它通常约占用 Heap 大小的 20% 或更高,这可是非常可观的比例。并且,我们进行对象复制的时候,因为需要扫描和更改 Card Table 的信息,这个速度影响了复制的速度,进而影响暂停时间。
分区类型
Eden
Surviver
Old
H
挑战
跨区引用
跨区引用
Point In(专属记录集)
问题:可能会变得很大
多
粗粒度表(整个region)
中
细粒度表(bitmap)
少
稀疏表(hash表)
Point Out(通用记录集)
记录集
Remember Set
用于记录和维护 region 之间对象的引用关系。垃圾回收的主力
合理选择回收区域
根据停顿时间,计算需要回收的块
优先回收垃圾多的,才能提高吞吐量
同时需要考虑分代
兼顾大对象分配
其他概念
Remember Set
用于记录和维护 region 之间对象的引用关系。垃圾回收的主力
card table
JVM将内存划分成了固定大小的Card。这里可以类比物理内存上page的概念。
主要因素
分配效率
回收效率
是否产生内存分片
空间利用率
是否停顿
时间复杂度
实现的复杂度
JVM调优思路
明确需求:是吞吐量还是延迟还是内存占用
找到具体的垃圾回收器
jstat
考虑硬件
重复完成分析、调整、验证这个过程。
开发框架
Spring
AOP
实现方式
Cglib动态代理(默认)
面向类,通过字节码增强实现
JDK动态代理
面向接口,通过JDK反射实现,性能差
IoC
控制反转,是一种思想,不用亲力亲为,用的时候去获取即可。
设计模式
单例模式
工厂模式
BeanFactory
ApplicationContext
XMLApplicationContext
ClasspathApplicationContext
代理模式
***Proxy
监听者模式
Observer
模板方法模式
JdbcTemplate
Springboot
原理
SpringBootApplicatin
SpringBootConfiguration
@Configuration
EnableAutoConfiguration
@AutoConfigurationPackage
Import
ImportSelect
Registar
spring.factories
Configuration
ComponentScan
优点
1、上手容易、开发快捷
2、开箱即用,远离繁琐的配置
3、提供了一系列大型项目通用的非业务性功能
4、避免大量的 Maven 导入和各种版本冲突
特性
EnableAutoConfiguration 自动装配
Starter 启动依赖依赖于自动装配的技术
Actuator 监控 , 提供了一些endpoint ,http、jmx形式去进行访问, health信息。 metrics 信息
Spring Boot CLI(命令行操作的功能, groovy脚本)客户端, groovy
MyBatis
Netty
高性能原因
更加优雅的 Reactor 模式实现、灵活的线程模型、利用 EventLoop 等创新性的机制,可以非常高效地管理成百上千的 Channel。
充分利用了 Java 的 Zero-Copy 机制
使用池化的 Direct Buffer 等技术,在提高 IO 性能的同时,减少了对象的创建和销毁;利用反射等技术直接操纵 SelectionKey,使用数组而不是 Java 容器等。
使用更多本地代码。
直接利用 JNI 调用 Open SSL 等方式,获得比 Java 内建 SSL 引擎更好的性能。
在通信协议、序列化等其他角度的优化。
定义
它是一个异步的、基于事件 Client/Server 的网络框架,目标是提供一种简单、快速构建网络应用的方式,同时保证高吞吐量、低延时、高可靠性。
中间件
MySQL
整体结构
结构图
接口层
服务层
存储层
事务
特性
ACID
事务隔离级别
读未提交
脏读、幻读、不可重复读
读已提交
脏读、不可重复读
可重复读
不可重复读(innodb没有)
串行化
无
存储引擎
InnoDB
结构
*.idb
特性
支持行级锁
支持事务
主键递增
ACID实现原理
A原子性
undo log
当事务对数据库进行修改时,InnoDB 会生成对应的 undo log
结构
row transation_id
data_roll_ptr
db_row_id
C一致性
事务追求的最终目标,一致性的实现既需要数据库层面的保障,也需要应用层面的保障
I隔离性
多个事务之间的隔离
写隔离
LBCC
意向锁、排它锁
读隔离
MVCC
read view
D持久性
redo log
产生的原因
InnoDB 作为 MySQL 的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘 IO,效率会很低。
为此,InnoDB 提供了缓存(Buffer Pool),Buffer Pool 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:
当从数据库读取数据时,会首先从 Buffer Pool 中读取,如果 Buffer Pool 中没有,则从磁盘读取后放入 Buffer Pool。当向数据库写入数据时,会首先写入 Buffer Pool,Buffer Pool 中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。Buffer Pool 的使用大大提高了读写数据的效率,但是也带来了新的问题:如果 MySQL 宕机,而此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。
于是,redo log 被引入来解决这个问题:当数据修改时,除了修改 Buffer Pool 中的数据,还会在 redo log 记录这次操作;当事务提交时,会调用 fsync 接口对 redo log 进行刷盘。
为此,InnoDB 提供了缓存(Buffer Pool),Buffer Pool 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:
当从数据库读取数据时,会首先从 Buffer Pool 中读取,如果 Buffer Pool 中没有,则从磁盘读取后放入 Buffer Pool。当向数据库写入数据时,会首先写入 Buffer Pool,Buffer Pool 中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。Buffer Pool 的使用大大提高了读写数据的效率,但是也带来了新的问题:如果 MySQL 宕机,而此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。
于是,redo log 被引入来解决这个问题:当数据修改时,除了修改 Buffer Pool 中的数据,还会在 redo log 记录这次操作;当事务提交时,会调用 fsync 接口对 redo log 进行刷盘。
利用顺序IO,避免写入数据的随机IO性能低的问题
索引
类型
聚簇索引
含有数据内容
非聚簇索引
不含有数据内容
原理
B+Tree
减少层级
效率平衡
性质
最左匹配原则
索引要尽量小,这样可以一次可以加载更多数据
MyISAM
问题
不支持sharding
CAT
ShardingJDBC
为什么要分库分表
一、数据库瓶颈
不管是IO瓶颈,还是CPU瓶颈,最终都会导致数据库的活跃连接数增加,
进而逼近甚至达到数据库可承载活跃连接数的阈值。
在业务Service来看就是,可用数据库连接少甚至无连接可用。
接下来就可以想象了吧(并发量、吞吐量、崩溃)
进而逼近甚至达到数据库可承载活跃连接数的阈值。
在业务Service来看就是,可用数据库连接少甚至无连接可用。
接下来就可以想象了吧(并发量、吞吐量、崩溃)
IO瓶颈
第一种:磁盘读IO瓶颈,热点数据太多,数据库缓存放不下,
每次查询时会产生大量的IO,降低查询速度 -> 分库和垂直分表
每次查询时会产生大量的IO,降低查询速度 -> 分库和垂直分表
第二种:网络IO瓶颈,请求的数据太多,网络带宽不够 -> 分库
CPU瓶颈
第一种:SQL问题,如SQL中包含join,group by,order by,
非索引字段条件查询等,增加CPU运算的操作 -> SQL优化,建立合适的索引,
在业务Service层进行业务计算
非索引字段条件查询等,增加CPU运算的操作 -> SQL优化,建立合适的索引,
在业务Service层进行业务计算
第二种:单表数据量太大,查询时扫描的行太多,SQL效率低,
CPU率先出现瓶颈 -> 水平分表
CPU率先出现瓶颈 -> 水平分表
二、分库分表
1、水平分库
概念
以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中
结果
每个库的结构都一样
每个库的数据都不一样,没有交集
所有库的并集是全量数据
场景
系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库
分析
库多了,io和cpu的压力自然可以成倍缓解
2、水平分表
概念
以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中
结果
每个表的结构都一样
每个表的数据都不一样,没有交集
所有表的并集是全量数据
场景
系统绝对并发量并没有上来,只是单表的数据量太多,影响了SQL效率,
加重了CPU负担,以至于成为瓶颈。推荐:一次SQL查询优化原理分析
加重了CPU负担,以至于成为瓶颈。推荐:一次SQL查询优化原理分析
分析
表的数据量少了,单次SQL执行效率高,自然减轻了CPU的负担
3、垂直分库
概念
以表为依据,按照业务归属不同,将不同的表拆分到不同的库中
结果
每个库的结构都不一样
每个库的数据也不一样,没有交集
所有库的并集是全量数据
场景
系统绝对并发量上来了,可以抽象出单独的业务模块
分析
到这一步,基本上就可以服务化了。例如,随着业务的发展一些公用的配置表、字典表等越来越多,
这时可以将这些表拆到单独的库中,甚至可以服务化。再有,随着业务的发展孵化出了一套业务模式,
这时可以将相关的表拆到单独的库中,甚至可以服务化
这时可以将这些表拆到单独的库中,甚至可以服务化。再有,随着业务的发展孵化出了一套业务模式,
这时可以将相关的表拆到单独的库中,甚至可以服务化
4、垂直分表
概念
以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中
结果
每个表的结构都不一样
每个表的数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据
所有表的并集是全量数据
场景
系统绝对并发量并没有上来,表的记录并不多,
但是字段多,并且热点数据和非热点数据在一起,
单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,
查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈
但是字段多,并且热点数据和非热点数据在一起,
单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,
查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈
分析
可以用列表页和详情页来帮助理解。
垂直分表的拆分原则是将热点数据(可能会冗余经常一起查询的数据)放在一起作为主表,
非热点数据放在一起作为扩展表。
这样更多的热点数据就能被缓存下来,进而减少了随机读IO。
拆了之后,要想获得全部数据就需要关联两个表来取数据。
但记住,千万别用join,因为join不仅会增加CPU负担并且会讲两个表耦合在一起(必须在一个数据库实例上)。
关联数据,应该在业务Service层做文章,分别获取主表和扩展表数据然后用关联字段关联得到全部数据
垂直分表的拆分原则是将热点数据(可能会冗余经常一起查询的数据)放在一起作为主表,
非热点数据放在一起作为扩展表。
这样更多的热点数据就能被缓存下来,进而减少了随机读IO。
拆了之后,要想获得全部数据就需要关联两个表来取数据。
但记住,千万别用join,因为join不仅会增加CPU负担并且会讲两个表耦合在一起(必须在一个数据库实例上)。
关联数据,应该在业务Service层做文章,分别获取主表和扩展表数据然后用关联字段关联得到全部数据
三、分库分表工具
sharding-sphere:jar,前身是sharding-jdbc
TDDL:jar,Taobao Distribute Data Layer
Mycat:中间件
四、分库分表问题
1、非partition key的查询问题
基于水平分库分表,拆分策略为常用的hash法
基于水平分库分表,拆分策略为常用的hash法
端上除了partition key只有一个非partition key作为条件查询
映射法
基因法
注:写入时,基因法生成user_id,如图。关于xbit基因,例如要分8张表,23=8,故x取3,即3bit基因。
根据user_id查询时可直接取模路由到对应的分库或分表。
根据user_name查询时,先通过user_name_code生成函数生成user_name_code再对其取模路由到对应的分库或分表。
id生成常用snowflake算法
根据user_id查询时可直接取模路由到对应的分库或分表。
根据user_name查询时,先通过user_name_code生成函数生成user_name_code再对其取模路由到对应的分库或分表。
id生成常用snowflake算法
端上除了partition key不止一个非partition key作为条件查询
映射法
冗余法
注:按照order_id或buyer_id查询时路由到db_o_buyer库中,按照seller_id查询时路由到db_o_seller库中。
感觉有点本末倒置!有其他好的办法吗?改变技术栈呢?
感觉有点本末倒置!有其他好的办法吗?改变技术栈呢?
后台除了partition key还有各种非partition key组合条件查询
NoSQL法
冗余法
2、非partition key跨库跨表分页查询问题
基于水平分库分表,拆分策略为常用的hash法
基于水平分库分表,拆分策略为常用的hash法
注:用NoSQL法解决(ES等)
3、扩容问题
基于水平分库分表,拆分策略为常用的hash法
基于水平分库分表,拆分策略为常用的hash法
水平扩容库(升级从库法)
注:扩容是成倍的
水平扩容表(双写迁移法)
第一步:(同步双写)修改应用配置和代码,加上双写,部署;
第二步:(同步双写)将老库中的老数据复制到新库中;
第三步:(同步双写)以老库为准校对新库中的老数据;
第四步:(同步双写)修改应用配置和代码,去掉双写,部署;
数据库优化思路
架构优化
主从、分库分表、添加缓存
SQL优化
SQL语句优化
只搜索需要的字段
优先使用ID查询
条件使用索引
索引优化
联合索引、索引的离散度分析
数据库优化
表设计优化
适当冗余
配置优化
连接数优化
Redis
数据结构
常用结构
String
数据结构
raw
int
embstr
适用场景
Hash
数据结构
ziplist
子主题
hashtable
适用场景
对象,更省空间
List
数据结构
quicklist
适用场景
消息队列
Set
数据结构
hashtable
intset
原理
适用场景
签到、点赞
ZSet
数据结构
ziplist
原理
只记录长度不记录内容
skiplist
实现简单
以空间换时间
高度是随机的
为什么使用红黑树
当我们插入或删除节点时,会移动节点进行平衡操作,这导致在并发场景中难以进行合理粒度的同步。
SkipList 结构则要相对简单很多,通过层次结构提高访问速度,虽然不够紧凑,空间使用有一定提高(O(nlogn)),但是在增删元素时线程安全的开销要好很多。
适用场景
排行榜
子主题
进阶结构
Geo
Bitmap
使用场景
布隆过滤器
Hayperloglog
使用场景
基数统计
每日访问用户
UV等
适用场景
分布式锁
分布式缓存
客户端
Redisson
封装了通用的数据结构
Jedis
命令齐全
单线程,线程安全问题,线程池
Lutuce
线程安全
集群
主从
主从同步
bgsave
主从数据同步
连接
同步
命令传播
Redis Sentinel
server
Sentinel
服务下线
Raft算法
故障转移
Redis Cluster
数据分片
16384个slot,每个节点管理一些slot。
一致性hash
虚拟节点,避免单点问题
亮点
高性能
ZeroCopy
事件驱动
单线程
数据结构简单
丰富的数据类型
持久化
AOF
优点
没1s记录一次日志
缺点
恢复慢
命令聚合
文件大
数据压缩
模式
always
append到缓冲区以后调用系统fsync操作同步到aof文件中,fsync操作完成后主线程返回;
no
不对aof文件做fsync同步,同步到硬盘操作由操作系统负责,通常同步周期最长30秒
everysec
表示命令写入aof缓冲区后调用操作系统write操作,write操作完成后主线程返回,由专门的线程每秒去进行fsync同步文件操作
优化
日志重写
日志压缩
RDB
优点
文件小
LZF压缩算法
恢复快
缺点
丢失数据大,不能做到秒级
每次调用bgsave都需要fork子进程,fork子进程属于重量级操作,频繁执行成本较高;
单线程
执行命令是单线程的,其他过程是多线程的
Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。
处理网络数据的读写和协议的解析,而执行命令依旧是单线程
特性
支持事务
MULTI
EXEC
DISCARD
WATCH
其他
内存淘汰策略
原理
LRU
redis原理
链表+HashMap实现
从所有的key中随机选择N个(N可以配置)要么从所有的设置了过期时间的key中选出N个键,然后再从这N个键中选出最久没有使用的一个key进行淘汰。所以准确度不高,但是执行效率高!
设置lru的值,不是获取的当前时间的时间戳,而是设置的为全局的server.lruclock的值。
这里值得注意的是全局时钟只有24位,按秒为单位来表示才能存储194天,所以可能会出现key的时钟大于全局时钟的情况,如果这种情况出现那么就两个相加而不是相减来求最久的key。
LFU
RANDOM
分类
allkeys-random
allkey-lru
volatile-ttl
将要过期的key
volatile-lru
volatile-random
volatile-ttl
noeviction
当内存使用超过配置的时候会返回错误,不会驱逐任何键
内存过期
主动过期
每个设置过期时间的key都会创建一个定时器,到期后立即清除。这个方案的问题是会有太多的定时器,比较消耗CPU。
惰性过期(使用)
只有当访问一个key的时候,才会判断这个key是否已经过期,过期则删除。这个方案的问题是如果有大量的过期key没有被访问,岂不是一直占用着内存空间。
被动过期(使用)
每隔一段时间扫描数据库中的expires字典中的key,清除掉已经过期的。可以看到这是一个折中方案,是在CPU和内存中取得了一个平衡。
缓存风险
缓存穿透
解决办法
缓存null
加锁,只能一个线程访问
缓存雪崩
解决办法
随机过期时间,不要同时过期
预热加载
缓存击穿
解决办法
加锁同步
缓存污染
解决办法
写操作:删缓存、修改数据、删缓存
线程
采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
MQ
Kafka
特性
高拓展
高性能
顺序写
批量写
ZeroCopy
异步处理
原理
Broker
Topic
partions
适用场景
亮点
集群
ISR
In-Sync Replicas 能够和 leader 保持同步的 follower + leader本身 组成的集合
OSR
Out-Sync Relipcas 不能和 leader 保持同步的 follower 集合
AR
使用场景
消息丢失
Producer
ack=all
Broker
Consumer
手动ack
消息顺序
写入到同一个Partition,一个Consumer消费
消息重复
Producer
开启ProducerId
Consumer
幂等支持
消息事务
引入TC,进行2PC提交
RocketMQ
原理
Broker
NameServer
Producer
Consumer
Topic
queue
MessageQueue
ConsumerQueue
commit.log
使用场景
有序消息
放到同一个queue
事务消息
延时消息
1 所有的延迟消息到达broker后,会存放到SCHEDULE_TOPIC_XXX的topic下(这个topic比较特殊,对客户端是不可见的,包括使用rocketmq-console,也查不到这个topic)
2 SCHEDULE_TOPIC_XXX这个topic下存在18个队列,每个队列中存放的消息都是同一个延迟级别消息
3 broker端启动了一个timer和timerTask的任务,定时从此topic下拉取数据,如果延迟时间到了,就会把此消息发送到指定的topic下,完成延迟消息。
2 SCHEDULE_TOPIC_XXX这个topic下存在18个队列,每个队列中存放的消息都是同一个延迟级别消息
3 broker端启动了一个timer和timerTask的任务,定时从此topic下拉取数据,如果延迟时间到了,就会把此消息发送到指定的topic下,完成延迟消息。
所有的延迟消息由producer发出之后,都会存放到同一个topic(SCHEDULE_TOPIC_XXXX)下,不同的延迟级别会对应不同的队列序号,当延迟时间到之后,由定时线程读取转换为普通的消息存的真实指定的topic下,此时对于consumer端此消息才可见,从而被consumer消费。
消息丢失
Broker
同步刷盘
异步刷盘
Producer
ack
Consumer
手动回执
问题
消息积压
Producer
限流
减少生产
Consumer
扩容
Consumer与ConsumerQueue一一对应,如果Queue不够多,则扩容也无解
丢弃
将一些日志消息丢弃
RabbitMQ
原理
消息类型
fanout
direct
topic
headers
使用场景
消息丢失
ES
子主题
一致性算法
PecificA
倒排索引
什么是倒排索引
正排索引根据ID查找内容,根据内容查找ID
实现
分词
索引
相关概念
MongoDB
非关系型数据库
无固定的结构、灵活拓展
基于文档
优点
支持全文搜索,地理位置索引
水平伸缩性
自动分片部署数据到机器集群的功能
数据压缩
自从MongoDB 3.0推出以后,MongoDB引入了一个高性能的存储引擎WiredTiger,并且它在数据压缩性能上得到了极大的提升,跟之前的MMAP引擎相比,压缩比至少可增加5倍以上,可以极大地改善磁盘空间使用率。
缺点
不支持事务
不支持join
Clickhouse
优点
真正的面向列的 DBMS
数据压缩
磁盘存储的数据
天然支持分布式
数据不仅按列存储,而且由矢量 - 列的部分进行处理,这使开发者能够实现高 CPU 性能
原理
缺点
没有完整的事务支持,
缺少完整的Update/Delete操作,缺少高频率、低延迟的修改或删除已存在数据的能力,仅能用于批量删
除或修改数据
除或修改数据
Hbase
特点
大:一个表可以有上亿行,上百万列。
面向列:面向列表(簇)的存储和权限控制,列(簇)独立检索。
稀疏:对于为空(NULL)的列,并不占用存储空间,因此,表可以设计的非常稀疏。
无模式:每一行都有一个可以排序的主键和任意多的列,列可以根据需要动态增加,同一张表中不同的行可以有截然不同的列。
数据多版本:每个单元中的数据可以有多个版本,默认情况下,版本号自动分配,版本号就是单元格插入时的时间戳。
数据类型单一:HBase中的数据都是字符串,没有类型。
promethus
架构
微服务
网关
原理
目标
轻量
安全
稳定
实现
Kong
Zuul2.0
SpringCloudGateway
服务发现
服务注册
zookeeper
nacos
服务发布
服务治理
服务容错
FailFast
服务限流
原理
令牌桶策略
漏斗策略
实现
单体实现
基于Guava实现
集群实现
基于Lua实现
成熟方案
sentinel
服务降级
服务监控
度量(Metrics)
Promethus
告警
Alter-Manager
日志(Logging)
Graylog
链路(Trace)
Pinpoint
其他
Skywalking
框架
dubbo
架构
分支主题
角色
Regisger
Provider
Consumer
Monitor
异步线程,定时存储
核心功能
服务提供、服务调用、连接处理、通信协议、序列化方式、服务发现、服务路由、日志输出
容错机制
failover
失败自动切换到其他服务器(默认)
failfast
快速失败,立即报错
failsafe
失败安全,直接忽略
failback
失败后记录下来,定时重试
forking
并行调用多个,一个成功就成功
thrift
负载均衡
计算机协议
四层协议
F5
七层协议
Nginx
LVS
HAProxy
算法
随机
轮训
加权轮训
最小连接数
hash
日志分析
应用日志输出
日志规范gelf规范
日志规范
命名规范
路径规范
滚动策略
日志格式
日志收集
Filebeat
缓冲
Kafka
加工聚合
Graylog
存储
ES
查询分析
Graylog
分布式
分布式理论
CAP
解释
一致性C
可用性A
分区容错性P
实现
CP
AP
BASE
解释
基本 B
可用 S
软状态 S
最终 E
表现形式
因果一致性
读己之所读
会话一致性
一致性理论
2PC
原理
准备阶段(Prepare)
提交阶段(commit)
缺点
协调者单点问题
数据不一致问题
阻塞问题
3PC
原理
准备阶段(Prepare)
尝试阶段(canCommit)
提交阶段(doCommit)
缺点
数据不一致问题
实现复杂
分布式一致性算法
Paxos
Basic Paxos
Multi Paxos
Raft
原理
使用案例
Redis Sentinel
Etcd
Console
ZAB
原理
使用案例
Zookeeper
Gossip
原理
使用案例
Redis Cluster
分布式事务
理论基础
XA
资源管理器RM
事务管理器TM
应用
实现
2PC
问题
协调者单点问题
同步阻塞问题
一致性问题
3PC
问题
单点问题
效率问题
实现方式
SEATA
描述
全局事务协调器
SAGA
描述
基于数据补偿替换回滚
优缺点
实现复杂
HalfMessage
描述
本地事务发起,发送一个MQ消息,本地事务提交则提交这个MQ消息,消费端可消费
优缺点
实现简单
最终一致,可能会出现不一致
依赖中间件
TCC
描述
Try,Commit,Cancel
优缺点
优点:
用户操作灵活
缺点:
代码耦合度高
分布式锁
实现方案
基于mysql
描述
基于MySQL的锁
缺点
单点问题
基于Redis
原理
基于redis的setnx expire
缺点
需要考虑超时情况
释放锁没有通知,需要客户端自旋
setnx与expire非原子操作,可能会出现没有过期的情况
基于Zookeeper
原理
基于ZK的临时节点,事件通知
缺点
每次要持久化数据,效率低
死锁的条件
解决办法
1、有序分配
2、银行家算法
条件
不可剥夺
循环等待
设置超时
互斥
请求和保持
无法获取就释放
分布式ID
要求
全局唯一
自动递增
高可用
实现
Redis
Zookeeper
mongodb
mysql
算法
Snowflake
计算机基础
IO模型
分类
阻塞
非阻塞
同步
异步
形式
BIO
NIO
实现
Channel
Buffer
Selector
多路复用模型
Reactor模式
AIO
ZeroCopy
概念
用户态
内核态
所谓0Copy是指减少从内核态到用户态的Copy
原理
mmap
减少一次copy,但是还有一次上下文切换
适用于小数据量读写
使用场景
RocketMQ
sendfile
减少一次上下文切换
适用于大文件传输
可用于DMA方式,减少CPU拷贝
使用场景
Kafka
IO多路复用
原理
实现
select
poll
epoll
(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
网络
HTTP
子主题
TCP/IP
操作系统
IO
随机IO
需要多次磁道寻址
顺序IO
SSD与机械硬盘
SSD使用电学信号,寻址更快
数据结构与算法
算法基础
时间复杂度
O(1)
O(n)
图的遍历
树的遍历
DFS/BFS
O(logn)
二分查找
nO(logn)
O(n2)
O(n3)
O(n!)
O(2^n)
空间复杂度
数组的长度
递归的深度
主定理
二分查找
O(logn)
二叉树
O(n)
矩阵
O(n)
归并排序
O(nlogn)
练习方法
1、5-10分钟阅读题目
2、写出实现思路
3、尝试去实现
4、阅读官方实践,对比实现方案
5、反复练习5次
数据结构
数组
数组(Array)
add
O(logn)
remove
O(logn)
get
O(1)
问题
连续内存
固定大小
越界
链表
链表(LinkedList)
类型
单向链表
next
双向链表
next
previous
循环链表
next
操作
add
O(1)
remove
O(1)
get
O(n)
问题
内存分散,不利于CPU缓存
容易造成内存碎片
使用场景
LRU
链表反转
二叉树
结构
遍历
前序
递归
public void preOrderTraverse1(TreeNode root) {
if (root != null) {
System.out.print(root.val + "->");
preOrderTraverse1(root.left);
preOrderTraverse1(root.right);
}
}
if (root != null) {
System.out.print(root.val + "->");
preOrderTraverse1(root.left);
preOrderTraverse1(root.right);
}
}
非递归
public void preOrderTraverse2(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
TreeNode node = root;
while (node != null || !stack.empty()) {
if (node != null) {
System.out.print(node.val + "->");
stack.push(node);
node = node.left;
} else {
TreeNode tem = stack.pop();
node = tem.right;
}
}
}
Stack<TreeNode> stack = new Stack<>();
TreeNode node = root;
while (node != null || !stack.empty()) {
if (node != null) {
System.out.print(node.val + "->");
stack.push(node);
node = node.left;
} else {
TreeNode tem = stack.pop();
node = tem.right;
}
}
}
中序
递归
public void inOrderTraverse(TreeNode root) {
if (root != null) {
inOrderTraverse(root.left);
System.out.print(root.val + "->");
inOrderTraverse(root.right);
}
}
if (root != null) {
inOrderTraverse(root.left);
System.out.print(root.val + "->");
inOrderTraverse(root.right);
}
}
非递归
public void inOrderTraverse(TreeNode root) {
Stack<TreeNode> stack = new Stack<>();
TreeNode node = root;
while (node != null || !stack.isEmpty()) {
if (node != null) {
stack.push(node);
node = node.left;
} else {
TreeNode tem = stack.pop();
System.out.print(tem.val + "->");
node = tem.right;
}
}
}
Stack<TreeNode> stack = new Stack<>();
TreeNode node = root;
while (node != null || !stack.isEmpty()) {
if (node != null) {
stack.push(node);
node = node.left;
} else {
TreeNode tem = stack.pop();
System.out.print(tem.val + "->");
node = tem.right;
}
}
}
后序
递归
public void postOrderTraverse(TreeNode root) {
if (root != null) {
postOrderTraverse(root.left);
postOrderTraverse(root.right);
System.out.print(root.val + "->");
}
}
if (root != null) {
postOrderTraverse(root.left);
postOrderTraverse(root.right);
System.out.print(root.val + "->");
}
}
非递归
public void postOrderTraverse(TreeNode root) {
TreeNode cur, pre = null;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.empty()) {
cur = stack.peek();
if ((cur.left == null && cur.right == null) || (pre != null && (pre == cur.left || pre == cur.right))) {
System.out.print(cur.val + "->");
stack.pop();
pre = cur;
} else {
if (cur.right != null)
stack.push(cur.right);
if (cur.left != null)
stack.push(cur.left);
}
}
}
TreeNode cur, pre = null;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.empty()) {
cur = stack.peek();
if ((cur.left == null && cur.right == null) || (pre != null && (pre == cur.left || pre == cur.right))) {
System.out.print(cur.val + "->");
stack.pop();
pre = cur;
} else {
if (cur.right != null)
stack.push(cur.right);
if (cur.left != null)
stack.push(cur.left);
}
}
}
AVL
红黑树
Trie
原理
使用场景
常用算法
散列表
算法
算法思想
贪心算法
原理
使用场景
分治算法
回溯算法
动态规划
排序
快速排序
计数排序
冒泡排序
// 冒泡排序,a表示数组,n表示数组大小
public void bubbleSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 0; i < n; ++i) {
// 提前退出冒泡循环的标志位
boolean flag = false;
for (int j = 0; j < n - i - 1; ++j) {
if (a[j] > a[j+1]) { // 交换
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true; // 表示有数据交换
}
}
if (!flag) break; // 没有数据交换,提前退出
}
}
// 冒泡排序,a表示数组,n表示数组大小
public void bubbleSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 0; i < n; ++i) {
// 提前退出冒泡循环的标志位
boolean flag = false;
for (int j = 0; j < n - i - 1; ++j) {
if (a[j] > a[j+1]) { // 交换
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true; // 表示有数据交换
}
}
if (!flag) break; // 没有数据交换,提前退出
}
}
特点
稳定
O(n2)
思想
后一个元素和前一个元素进行比较
图解
子主题
插入排序
实现方式
// 插入排序,a表示数组,n表示数组大小
public void insertionSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 1; i < n; ++i) {
int value = a[i];
int j = i - 1;
// 查找插入的位置
for (; j >= 0; --j) {
if (a[j] > value) {
a[j+1] = a[j]; // 数据移动
} else {
break;
}
}
a[j+1] = value; // 插入数据
}
}
// 插入排序,a表示数组,n表示数组大小
public void insertionSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 1; i < n; ++i) {
int value = a[i];
int j = i - 1;
// 查找插入的位置
for (; j >= 0; --j) {
if (a[j] > value) {
a[j+1] = a[j]; // 数据移动
} else {
break;
}
}
a[j+1] = value; // 插入数据
}
}
优化方式
希尔排序
实现方式
public static void shellSort(int[] arr) {
int length = arr.length;
int temp;
for (int step = length / 2; step >= 1; step /= 2) {
for (int i = step; i < length; i++) {
temp = arr[i];
int j = i - step;
while (j >= 0 && arr[j] > temp) {
arr[j + step] = arr[j];
j -= step;
}
arr[j + step] = temp;
}
}
}
int length = arr.length;
int temp;
for (int step = length / 2; step >= 1; step /= 2) {
for (int i = step; i < length; i++) {
temp = arr[i];
int j = i - step;
while (j >= 0 && arr[j] > temp) {
arr[j + step] = arr[j];
j -= step;
}
arr[j + step] = temp;
}
}
}
特点
稳定
O(n2)
思想
找到比指定元素小的元素位置进行替换
图解
子主题
选择排序
思想
找到最小的元素放在最前面
图解
子主题
特点
不稳定
O(n2)
算法
算法思想
贪心算法
原理
使用场景
分治算法
回溯算法
动态规划
排序
快速排序
计数排序
冒泡排序
// 冒泡排序,a表示数组,n表示数组大小
public void bubbleSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 0; i < n; ++i) {
// 提前退出冒泡循环的标志位
boolean flag = false;
for (int j = 0; j < n - i - 1; ++j) {
if (a[j] > a[j+1]) { // 交换
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true; // 表示有数据交换
}
}
if (!flag) break; // 没有数据交换,提前退出
}
}
// 冒泡排序,a表示数组,n表示数组大小
public void bubbleSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 0; i < n; ++i) {
// 提前退出冒泡循环的标志位
boolean flag = false;
for (int j = 0; j < n - i - 1; ++j) {
if (a[j] > a[j+1]) { // 交换
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true; // 表示有数据交换
}
}
if (!flag) break; // 没有数据交换,提前退出
}
}
特点
稳定
O(n2)
思想
后一个元素和前一个元素进行比较
图解
子主题
插入排序
实现方式
// 插入排序,a表示数组,n表示数组大小
public void insertionSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 1; i < n; ++i) {
int value = a[i];
int j = i - 1;
// 查找插入的位置
for (; j >= 0; --j) {
if (a[j] > value) {
a[j+1] = a[j]; // 数据移动
} else {
break;
}
}
a[j+1] = value; // 插入数据
}
}
// 插入排序,a表示数组,n表示数组大小
public void insertionSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 1; i < n; ++i) {
int value = a[i];
int j = i - 1;
// 查找插入的位置
for (; j >= 0; --j) {
if (a[j] > value) {
a[j+1] = a[j]; // 数据移动
} else {
break;
}
}
a[j+1] = value; // 插入数据
}
}
优化方式
希尔排序
实现方式
public static void shellSort(int[] arr) {
int length = arr.length;
int temp;
for (int step = length / 2; step >= 1; step /= 2) {
for (int i = step; i < length; i++) {
temp = arr[i];
int j = i - step;
while (j >= 0 && arr[j] > temp) {
arr[j + step] = arr[j];
j -= step;
}
arr[j + step] = temp;
}
}
}
int length = arr.length;
int temp;
for (int step = length / 2; step >= 1; step /= 2) {
for (int i = step; i < length; i++) {
temp = arr[i];
int j = i - step;
while (j >= 0 && arr[j] > temp) {
arr[j + step] = arr[j];
j -= step;
}
arr[j + step] = temp;
}
}
}
特点
稳定
O(n2)
思想
找到比指定元素小的元素位置进行替换
图解
子主题
选择排序
思想
找到最小的元素放在最前面
图解
子主题
特点
不稳定
O(n2)
堆排序
桶排序
堆排序
递归
迭代
广度优先
深度优先
项目
AB号项目
核心功能
号码绑定
号码调度
号码解绑
手动解绑
自动解绑
延迟消息
续绑
AXB续绑
先取消,然后再绑定
XB续绑
凌晨续绑
ElasticJob任务续绑
技术栈
MySQL数据存储
Redis缓存
绑定关系
RocketMQ
事件通知
延迟消息解绑
ElasticJob
数据状态
话单数据
续绑
背景
自如寓接听率底下,且无法知道中间状态,以及接听率底下的原因
需求:提升接听率,且将数据线上化
项目成果
1、接听率提升60%-90%
2、数据线上化、一键切换供应商,保证业务高可用,绑定不上用原号码兜底,且异常日志输出会第一时间知道具体原因
2、Saas平台化建设多个业务线使用
3、技术平台化建设,延迟消息平台
延迟消息原理
producer要将一个延迟消息发送到某个Topic中
Broker判断这是一个延迟消息后,将其通过临时存储进行暂存。
Broker内部通过一个延迟服务(delay service)检查消息是否到期,将到期的消息投递到目标Topic中。这个的延迟服务名字为delay service,不同消息中间件的延迟服务模块名称可能不同。
消费者消费目标topic中的延迟投递的消息
Broker判断这是一个延迟消息后,将其通过临时存储进行暂存。
Broker内部通过一个延迟服务(delay service)检查消息是否到期,将到期的消息投递到目标Topic中。这个的延迟服务名字为delay service,不同消息中间件的延迟服务模块名称可能不同。
消费者消费目标topic中的延迟投递的消息
业务大盘项目
核心功能
日志平台
收集阶段:日志规范SDK
日志格式:gelf
滚动策略
文件名称
采集阶段
Filebeat
http上报
处理阶段
流转阶段:kafka
一个业务线一个topic
存储阶段:ES
查询阶段:graylog
对整个日志做了管理,包括采集、流转和存储
FlinkSQL平台
日志分析
FlinkSQL平台
数据图表
技术栈
ES
Kafka
子主题
Flink
基本原理
state
MapState
ListState
BroadcastState
window
Tumbling Windows(不重复)
Sliding Windows(重复的)
Session Windows(会话窗口)
自定义窗口
time:乱序数据处理
处理时间 Processing Time
事件时间 Event Time
提取时间 Ingestion Time
实现watermark的机制
checkpoint
为了保证一致性
Exactly-Once
子主题
基本概念
Client
JobManager
TaskManager
Client 用来提交任务给 JobManager,JobManager 分发任务给 TaskManager 去执行,然后 TaskManager 会心跳的汇报任务状态。
功能开发
数据源拓展
自定义函数
clickhouse
优点
完备的DBMS功能
列式存储与数据压缩
按列存储与按行存储相比,前者可以有效减少查询时所需扫描的数据量
按列存储相比按行存储的另一个优势是对数据压缩的友好性
子主题
向量化执行引擎
向量化执行,可以简单地看作一项消除程序中循环的优化。
关系模型与SQL查询
关系模型相比文档和键值对等其他模型,拥有更好的描述能力,也能够更加清晰地表述实体间的关系。
多样化的表引擎
多主架构
多线程与分布式
数据分片与分布式查询
项目成果
日志平台
FlinkSQL平台
背景
日志查询分散,无统一的平台
日志接收有延迟,导致告警数据有延迟
数据实时分析需求
在线客服项目
项目主要功能
子主题
0 条评论
下一页