java知识点归纳(面试必备)
2019-10-24 10:19:00 1 举报
AI智能生成
java面试知识点归纳
作者其他创作
大纲/内容
MYSQL
索引
索引原理
B-Tree索引
所有的值都是按顺序存储,每一个叶子页到根的距离相同
Hash索引
只有精确匹配所有的列查询才会有效
如何查创建高性能索引
查询优化
缓存原理
四大特性
原子性
事务开始后所有操作,要么全部做完,要么全部不做
一致性
事务开始前和结束后,数据库的完整性约束没有被破坏
隔离性
同一时间,只允许一个事务请求同一数据,不同事务之间彼此没有任何干扰
持久性
事务完成后,事务对数据库的更新保存到数据库,不能回滚
事务隔离级别
读未提交
事务可以读取未提交的数据,产生脏读
不可重复读
同一事务的其他实例在该实例处理期间可能会有新的commit,同一select可能会返回不同结果
可重复读
mysql默认
产生幻读
串行化
JVM
垃圾回收算法
引用计数法
无法处理循环问题
每次操作时都伴随一个加法或减法操作,影响系统性能
标记清除法
标记阶段
通过根节点,标记从根节点可达的对象
清除阶段
清除所有未被标记的对象
会产生大量的空间碎片
复制算法
eden空间中存活的对象恢复复制到未使用的survivor空间中(假设是to)
正在使用的survivor中的年轻对象也会被复制到同to空间
此时eden空间和from空间中剩余的对象就是垃圾对象
适用于新生代
标记压缩算法
最终效果等同于用标记清除算法执行完成后,再进行一次内存碎片整理
分代算法
是一种思想
根据垃圾回收对象的特性选择合适的回收算法
分区算法
将整个堆空间划分成连续不同的小区间,每一个小区间独立使用,独立回收
对象引用
强引用
程序中使用的一般引用,可触及 不会被回收
软引用
当堆空间不足时就会被回收
使用java.lang.ref.SoftReference类实现
弱引用
发现即回收
由于垃圾回收器通常优先级很低,可以存在较长时间
软应用和弱引用非常适合保存可有可无的缓存数据
虚引用
可以跟踪对象的回收时间,可以将一些资源释放操作放置在虚引用中
垃圾回收器
串行收集器
-XX:+UseSerialGC
新生代和老年代都使用串行收集器
并行收集器
新生代ParNew
将串行回收器多线程化
配合老年代CMS工作
-XX:+UseConcMarkSweepGC -XX:UseParNewGC
新生代ParallelGC
关注系统吞吐量
-XX:MaxGCPauseMillis
设置最大垃圾停顿时间
-XX:+GCTimeRatio
设置吞吐量大小
老年代ParallelOldGC回收器
CMS
关注系统停顿时间基于标记清除算法
工作步骤
初始标记
并发标记
预清理
重新标记
并发清理
并发重置
参数设置
-XX:+UseConcMarkSweepGC
新生代使用ParNew回收器,老年代使用CMS收集器
-XX:ConcGCThreads或者-XX:parallelCMSThreads
并发线程数量
-XX:CMSInitationOccupancyFraction
老年代空间使用率达到阈值时实行CMS回收,默认68%
-XX:UseCMSCompactAtFullCollection
垃圾回收器在垃圾收集完成后进行一次内存碎片整理
-XX:CMSFullGCsBeforeCompaction
多少次CMS后进行一次内存压缩
G1
使用分区算法,兼顾吞吐量和停顿时间的GC实现
问题
region 大小和大对象很难保证一致,这会导致空间的浪费
工作步骤
初始标记
根区域扫描
并发标记
重新标记
独占清理
并发清理
参数设置
-XX:UseG1GC
-XX:MaxGCPauseMillis
目标最大停顿时间
-XX:ParallelGCThreads
GC工作线程数量
对象何时进入老年代
新创建的对象进入eden区
老年对象进入老年代
MaxTenuringThreshold(新生代最大年龄)默认15 超过则进入老年代
大对象直接进入老年代
PretenureSizeThreshold设置阈值
只对串行收集器、parNew有效,对ParallelGC无效,一切由运行情况决定
常用GC参数
-XX:UseSerialGC
新生代和老年代都使用串行收集器
-XX:SurvivorRatio
设置Eden区和survivior区大小比例
-XX:PretenureSizeThreadOld
大对象进入老年代的阈值
-XX:MaxTenuringThreshold
对象进入老年代年龄的最大值
常用jvm虚拟机参数
性能监控工具
linux下
top
显示系统整体资源使用情况
vmstat
统计cpu、内存的使用情况
pidstat工具
CPU使用率监控
I/O使用监控
内存监控
JDK性能监控工具
jps
查看java进程
jstat
查看虚拟机运行时信息
jinfo
查看虚拟机参数
jmap
导出堆文件
jstack
导出线程堆栈
堆内存分析
堆溢出
java heap space
-Xmx指定一个更大的堆外 使用MAT等工具分析找打大量占用堆空间的对象
直接内存溢出
java.lang.OutOfMemoryError
合理的进行FullGC
设定一个系统实际可达的-XX:MaxDirectMemorySize值
过多线程导致OOM
为支持更多的线程使用一个较小的堆空间
永久区溢出
PermGen space
增加MaxPermSize的值
减少类的数量
使用ClassLoader合理的加载各个类,并定期回收
GC效率低下引起OOM
GC overhead limit exceed
class文件结构
魔数
class文件的标识(4字节)
class文件版本
由那个本本的编译器编译产生
常量池
类的访问标记
......
calss装载系统
装载流程
加载
通过类的全名获取类的二进制数据流
解析类的二进制数据流为方法区内的数据结构
创建java.lang.Class类的实例,表示该类型
验证
格式验证
魔数、版本号、长度检查
语义验证
是否继承final、抽象方法是否有实现等
字节码验证
跳转指令是否指向正确的位置
符号引用验证
符号引用的直接引用是否存在
准备
分配相应的内存空间,并设置初始值
如果为final的常量会在此阶段赋值
解析
将类、接口、字段、方法的符号引用转为直接引用
初始化
主要执行类的初始化方法<clinit>
整合static赋值语句以及static语句块
ClassLoader
主要方法
loadClass
给定一个类名,加载一个类,返回这个类的实例
如果找不到会抛出ClassNotFoundException
findClass
重载classLoader的主要扩展点
定义查找类的逻辑
findLoadedClass
寻找已加载的类
分类
启动类加载器
由C代码实现,java中没有与之对应的对象
加载系统核心的类(如rt.jar)
扩展类加载器
lib/ext/*.jar
应用类加载器
用户程序的类
自定义类加载器
特殊的类,一般也是用户程序类
双亲委派模式
概念
进行类加载时如果已经被加载直接返回可用的类,负责会请求双亲处理,如果双亲处理失败,则由自己加载
弊端
检查类是否已经加载是单向的,顶层的ClassLoder无法访问底层ClassLoader加载的类
突破双亲模式
重载loadClass,改变类的加载次序
热替换的实现
重载findClass
调优
调优的目标
内存占用
延迟
吞吐量
思路
理解应用需求和问题,确定调优目标
通过JVM和GC状态,定位具体问题
通过Jstat等工具查看GC等相关状态,开启GC日志
选择的GC类型是否符合我们的应用特征
MinorGC过长还是MixedGC出现异常停顿
通过分析具体调整参数或者软硬件配置
如果达到目标则结束调优,否则重新完成分析、调整 验证
hadoop
MapReduce
执行过程
作业提交
客户端向资源管理器申请新应用ID
检查作业有没有指定输出目录,如果没有返回错误给MapReduce程序
计算作业的输入分片,如果指定的分片路径不存在则返回错误给Mapreduce程序
将运行做需要的资源(JAR、配置文件、计算所得的输入分片)复制到一个以ID命名的目录下的共享文件系统
作业初始化
资源管理器收到调用submitApplication消息后,将请求传递给Yarn的调度器
调度器分配一个容器启动application master进程
application master是一个java应用,主类是MRAppMaster,负责接收任务进度和状态
application master接收任务的进度和完成报告
接收共享文件系统在客户端计算的输入分片,并且为每个输入分片创建一个map任务
在运行之前application master调用setupJob设置最终的输出目录
如果任务很小就会选择和自己同一jvm上运行
少于10个mapper并且只有一个reduce输出大小小于一个HDFS数据块的属于小任务
任务分配
application master向资源管理器申请map任务运行的容器,有5%的map任务完成时为reduce任务的请求才会发出
map任务数据本地化,任务和分片驻留的统计阶段运行
请求会为任务指定内存需求和cpu数(可配置)
任务执行
application master直接和节点管理器通信启动容器
任务运行之前会将任务需要的资源(作业的配置、jar文件等)本地化
推测执行(可关闭)
一种优化措施
一个任务运行比预期慢的时候,启动另一个相同的任务作为备份
通过提交协议确保作业和任务完成成功或者失败
outputcommitter实现
成功调用commitJob方法,删除临时空间目录
失败调用abortJob,终止
进度和状态更新
map和reduce任务运行时,子进程和父application master通过umbilical通信
每隔3秒,发送自己的进度
application master会形成一个作业的汇聚视图
客户端每秒钟(可配置)轮询一次application master接收最新状态
完成
收到最后一个任务已完成通知把作业设置为成功
完成后application master和任务容器清理工作状态(中间结果被删除)
失败
任务运行失败
失败类型
用户代码抛出异常
jvm退出之前向父application master发送错误报告
application master尝试将此次任务标记为failed(失败),并释放容器资源
jvm突然退出
节点管理器注意到进程已退出,并通知application master
任务挂起
application master注意到一段时间没收到进度更新通知,将任务标记为失败
自动杀死jvm进程
失败处置
application master避免在以前失败过的节点管理器上重新运行任务
默认尝试次数为4(可配置),超过则整个作业失败
可配置失败最大百分比
有少数任务失败不影响整个作业
application master运行失败
默认重试2次
application master向资源管理器发送周期性心跳
资源管理器检测到该失败并在新的容器中开启新的master实例
使用作业历史恢复所运行任务状态,使其不必重新运行
客户端轮询进度失败就会向资源管理器重新询问master地址并缓存
节点管理器失败
节点管理器会停止向资源管理器发送心跳
资源管理器将其从自己的节点池中移出
在失败的节点上运行的所有任务或者application master如前面机制进行恢复
资源管理器运行失败
双机热备运行一对资源管理器
运行中的应用程序信息存储在高可用状态存储区中(ZooKeeper或HDFS备份)
新资源管理器启动后,从存储区读取应用程序信息并且为所有应用重启application master
使用Zookeeper的leader选举机制切换
shuffle和排序
map端
将输出数据写入缓冲区
达到阈值会溢出到磁盘
写磁盘之前根据最终传入的reduce将数据分区
每个分区中后台线程按键排序
有combiner函数就在排序后运行
将输出写入磁盘过程中压缩(默认未开启)
节约磁盘空间
减少传给reduce的数据量
输出文件位于运行map任务的本地磁盘
reduce端
复制阶段
每个map任务完成后 reduce任务就复制其输出
如果map输出相当小,会被复制到reduce任务jvm内存,否则输出到磁盘
合并阶段
后台线程将副本合并为更大,排好序的文件
reduce阶段
直接把数据输入reduce函数处理
对已排序输出中的每个键调用reduce函数,直接输出到文件系统
调优
mapreduce.task.io.sort.mb
排序map输出所使用的内存缓冲区大小
mapreduce.map.output.compress
是否压缩map输出
mapreduce.reduce.shuffle.parallelcopies
复制map输出到reduce的线程数
总的原则给shuffle过程尽量提供多的内存空间
类型格式
输入格式
输入分片
InputSplit
getLength
分片大小,用来排序优先处理大分片
getLocations
分片存储位置,将map任务放在分片附近执行
InputFormat
getSplits
客户端获取分片,将他们发送到application master,application master 使用其存储位置调度map处理这些任务
createRecordReader
获取迭代器,将数据传给map函数
FileInputFormat
InputFormat的子类
用于指定作业的输入文件位置
addInputPath等四个方法设置路径
使用setInputPathFilter设置过滤器
如果未设置过滤器也会使用默认的过滤器排除隐藏的文件
为输入文件生成代码片段
只切割超过HDFS块的文件
过多的小文件浪费namenode内存,还还增加寻址次数
避免切分
增加最小分片大小,设置为处理的最大文件大小
FileInputFormat子类重写isSplitable将返回值设置为false
文本输入
TextInputFormat
二进制输入
SequenceFileInputFormat
多个输入
MultipleInputs
数据库输入
DBInputFormat
输出格式
二进制输出
SequenceFileOutPutFormat
顺序文件,格式紧凑容易压缩
MapFileOutputFormat
把map文件作为输出
必须保证reduce输出的键已经是排好序的
文本输出
TextOutPutFormat
多个输出
MultipleOutputFormat
数据库输出
DBOutputFormat
高级特性
计数器
内置计数器
任务计数器
作业计数器
用户定义的java计数器
排序
部分排序
MapReduce根据输入的键对数据排序
排序顺序由RawComparator控制
全排序
使用一个分区
处理大型文件时效率极低
partitioner均匀划分多个分区前一个分区的数据大于后一个分区最后串联这些分区
如何均匀的划分分区?
写一个mapreduce作业计算落入分区的记录数
对键进行采样,获取近似分布
辅助排序
按值排序
定义包括自然键和自然值的组合键
根据组合键对记录进行排序(同时使用自然键和自然值进行排序)
针对组合键进行分区和分组时只考虑自然键
连接
map端
map的输入数据必须先分区并且以特定方式排序
各个输入数据集被划分成数量相同的分区且按照相同的键(连接键)排序
使用COmpositeInputFormat运行一个map端连接
reduce端
多输入
使用MultipleInputs
辅助排序
一个源的数据排列在另一个源的数据前是非常重要的
边数据
作业所需的额外只读数据
JobConf配置作业
加大mapreduce内存开销压力,不适合传输大数据
Hadoop分布式缓存
使用
使用-files选项指定待分发的文件
使用-archives向自己的任务中复制存档文件
工作机制
启动作业时会把由-files、-archives、-libjars等选项指定的文件复制到分布式文件系统
任务运行之前节点管理器将文件从分布式文件系统复制到本地磁盘
节点管理器为缓存中的文件维护一个计数器,当任务运行时+1,运行完毕后-1,当达到0时才有资格删除文件
HDSF
基础概念
数据块
默认128M,可以最小化寻址开销
文件所在的数据块可以存储在网络任意位置提高容错和可用性
namenode和datanode
namenode
维护文件系统树和整棵树内所有文件和目录
命名空间镜像文件和编辑日志文件保存整棵树内容
保存每个文件中各个数据块所在的节点信息
备份机制
通过原子操作同时写入多个文件(一般是NFS)
辅助namenode
定期合并编辑日志和命名空间镜像
把存储在NFS元数据复制到辅助namenode
datanode
定期向namenode发送存储的块列表
接收客户端和namenode调度
联邦HDFS
每个namenode维护一个命名空间卷,相互独立
一个namenode失效不会影响其他
高可用
活动-备用namenode
NFS
日志管理器(QJM)
保证每次编辑写入多个日志节点
使用Zookeeper实现namenode选取
文件读取原理
客户端Filesyste open()
远程RPC调用namenode
根据namenode获取最佳datanode地址
反复调用read读取内容,失败会重新选择并告知namenode失败节点
文件写入原理
客户端create()
PRC调用namenode(文件、权限检查)
创建记录并返回FSDataOutPutStream对象
分成数据包写入内部队列
DataStreamer挑选适合的数据副本组成管线(按顺序写入所有管线)
DFSDataOutPutStream收到管线中所有datanode确认消息后删除数据包
写入datanode异常处理
关闭管线
把数据包添加回数据队列的最前端
为正常的datanode数据块指定新的标识,并传递给namenode
基于正常的datanode构建新的管线
YARN
运行机制
资源管理器
客户端联系资源管理器运行application master进程
资源管理器找到节点管理器运行application master
简单运算将结果返回客户端
向资源管理器请求更多的容器运行分布式计算任务
本地限制(HDSF数据块所在的节点申请容器)
spark一开始启动固定数量的执行器
MapReduce最开始申请map任务容器,reduce后申请
节点管理器
运行在集群所有节点
启动和监控容器
应用生命周期
一个用户作业对应一个应用
MaperReduce默认
作业的每个工作流或者用户会话对应一个应用
spark默认
容器可以在作业之间重用,会缓存作业之间的结果数据
多个用户共享一个长期运行的应用
通常作为协调者角色运行
调度类型
FIFO调度器
将所有应用放到一个队列中,按照提交的顺序运行应用
优点
简单易懂、不需要任何配置
缺点
不适合共享集群
大应用一直占用集群所有资源,小作业会被阻塞
容量调度器
一个独立队列专门保证小作业运行
优点
小作业有专门的队列运行会比较快
缺点
牺牲整个集群的利用率
大作业的执行时间会比较长
队列放置
MapReduce中可以通过配置maperduce.job.queuename指定队列
公平调度器
在所有运行的应用之间动态平衡资源
第一个大作业启动时获得集群所有资源,第二个小作业启动时分配到一半的资源,小作业运行完成之后又得到所有资源
得到了较高的集群利用率,也保证了小作业能及时完成
队列放置
基于规则
hive
安装
下载直接解压
本地、伪分布式、分布式
配置
hivesite.xml
存放集群连接信息
指定文件系统和资源管理器
执行引擎(hive.execution.engine指定)
Mapreduce(默认)
spark
Tez
Metastore
元数据存放地
内嵌配置
以本地磁盘作为存储的Derby数据库实例
本地metaStore配置
metaStore和hive服务仍运行在同一个进程中,但连接的是另一个进程的数据库
一般使用mysql
远程metaStore配置
一个或多个metaStore服务器和Hive运行在不同的进程内
数据库层可以安全置于防火墙后
表
托管表
将数据放置在自己的仓库目录
执行drop 命令会删除元数据和表数据
外部表
创建表时使用external关键字
执行drop不会碰数据,只会删除元数据
分区和桶
分区
分区只是表目录下嵌套的子目录
创建表时使用partitioned by 定义
桶
使用clustered by 指定划分桶作用的列和要划分的桶的个数
获取更高的查询效率
使采样更加高效
优化
尽早过滤数据,减少每个阶段的数据量
只读取查询中所需要的列
减少不必要的分区
在join前过滤不需要的数据
减少job数
如果joib的key相同,不管多少表都会合并成一个MapReduce任务
在join前过滤不需要的数据
解决数据倾斜问题
大表和小表join时 使用mapJoin
使用left semi join替代in/exists
只能在on中设置过滤条件
select 和where中不能引用右边表的字段
spark
弹性分布式数据集
创建
来自内存中的对象集合
使用外部存储器(如HDFS)
对现有RDD转换
转换
从现有RDD生成新的RDD
map
flodBykey
aggregatebyKey
......
动作
collect
first
saveAsTextFile
foreach
......
共享变量
经过序列化后被发送到各个executor,然后缓存
类似Mapreduce的分布式缓存
运行机制
基本概念
Driver(驱动程序)
运行程序Main函数
创建SparkContext
准备spark运行环境
与Cluster Manager通信
资源申请
任务分配与监控
ClusterManager(资源管理器)
spark原生资源管理器
HadoopYarn
......
Executor(执行器)
工作节点上的一个进程,该进程负责运行task
将数据存储在内存或者磁盘上
每个应用都有独立的一批Executor
窄依赖
一个父RDD的分区对应于一个子RDD的分区
两个父RDD的分区对应于一个子RDD 的分区
map、filter、union、mapPartitions、mapValues
宽依赖
父RDD的每个分区都可能被多个子RDD分区所使用,子RDD分区通常对应所有的父RDD分区
groupByKey、partitionBy、reduceByKey
DAG(有向无环图)
反应Rdd之间的依赖关系
有向无环图调度器
基于DAG划分Stage 并以TaskSet的形势提交Stage给TaskScheduler
TaskScheduler(任务调度器)
将taskSet提交给Woker(集群)运行,并汇报结果
负责具体任务的实际调度
Stage(调度阶段)
每个Job会被拆分为多组Task,每组Task被称为stage
划分核心算法
从后往前回溯,遇到窄依赖加入本stage,遇见宽依赖进行Stage切分
TaskSet(任务集)
由一组关联的,但相互之间没有Shuffle依赖关系的任务所组成的任务集
运行流程
sparkContext提交作业到DAGScheduler
分析RDD构建DAG图
拆分Stage
生成作业
提交任务集TaskSet到任务调度器
任务调度器构建一个TaskSetManager的实例来管理这个任务集的生命周期
任务调度器得到计算资源后通过taskSetmanager调度具体的任务到对应的Executor节点上进行计算
RPC框架实现原理
建立通信(TCP)
服务寻址
网络传输
服务调用
使用到的技术
动态代理
我们可以使用JDK原生的动态代理机制,可以使用一些开源字节码工具框架 如:CgLib、Javassist等
序列化
可以使用Java原生的序列化机制,但是效率非常低,推荐使用一些开源的、成熟的序列化技术,例如:protobuf、Thrift、hessian、Kryo、Msgpack
NIO
使用Netty 作为底层通信框架
服务注册中心
Zookeeper
面试经典题
HashMap7和8的区别
插入顺序
1.7插入在链表的头部
不需要遍历整个链表,效率高
1.8插入链表尾部
链表大于8的时候需要树化,本身就需要计算长度
HashMap死循环的bug不会有了
只有平移,没有调换顺序
hash算法简化
1.7
JDK初始大小为16.加载因子0.75
用Hash值和需要扩容的二进制数进行&。只有2的n次幂的情况最后一位二进制数一定是1,这样能最大程度减少Hash碰撞
用9次扰动处理=4次位运算+6次异或
1.8
原始位置+扩容的大小值
2次扰动处理=1次位运算+1次异或
ConcurrentHashMap
早期实现
分离锁,内部进行分段 里面则是HashEntry的数组
HashEntry内部使用volatile的Value字段保证可见性
Segment本身基于ReenTrantLock扩展实现,并发修改时Segment被锁定
JAVA8后
内部仍有SegMent定义,仅仅是为了保证序列化的兼容性
初始化修改为lazy_load形式,避免初始开销
数据存储利用Volatile来保证可见性
使用CAS等操作在的定场景进行无锁并发操作
segment数量与桶数量一致
B树和B+树
区别
B+树只有叶子节点会带有指向记录的指针,B树所有的节点都带有,在内部出现的索引项不会出现在叶子节点中
B+树中所有叶子节点都是通过指针连接在一起而B树不会
子主题
什么情况下产生死锁,如何定位 以及如何避免
产生条件
多线程环境下,由于互相持有对方需要的锁。而永久处于阻塞状态
如何定位
jstack工具直接定位
类似JConsole等图形化工具
如何避免
避免使用多个锁,嵌套非常容易出问题
如果比较使用多个锁,尽量设置好锁的获取顺序,可以将锁和对象之前的关系用图形化的方式抽取出来
使用带超时的方法,为程序带来更多可控性
dubbo负载均衡策略
按权重随机调用
如果权重相同随机调用,如果权重不同,则按权重随机数调用
轮询
计算服务实例的最大最小权重,如果权重都一样,则直接取模轮询。如果权重不一样,每一轮调用,都计算出一个基础权重值,然后筛选出权重值大于基础权重值 取模随机调用
最少活跃次数
查找最少活跃次数的服务并统计权重和出现频次,如果最少活跃次数只出现一次,直接使用该服务里如果出现多次,如果出现多次并且权重不同,则按总权重随机调用
一致性hash
子主题
分布式ID生成策略
UUID
以连字号分为5段包含32个16进制数字
优点
性能非常高
本地生成,没有网络消耗
缺点
不易于存储
UUID太长12字节128位
信息不安全
基于mac地址生成的UUID可能造成MAC地址泄漏
不适合做BD主键
mysql 在InnoDB引擎下,UUID无序会造成页分裂
snowflake
以划分命名空间来生成ID
头部1位的正负标识位
41位时间戳
通常System.currentTimeMillis()
10位的WorkerID
5位数据中心
5位机器ID
12位的自增序列号
优点
毫秒数在高位,自增序列在低位,整个ID都是趋势递增的
不依赖第三方数据库,稳定性比较高,生成ID性能也很高
根据吱声业务特性分配bit位,非常灵活
缺点
强制依赖时钟,如果机器上时钟回拨,会导致拨号重复
数据库生成
mySqlID自增,每次使用时先读写mysql获取ID号
优点
非常简单,利用现有系统数据库功能,成本小,有DBA专门维护
ID单调递增
缺点
强制依赖DB,当DB不可用是整个系统不可用,致命问题
ID发号性能瓶颈限制在单台MySQL的读写性能
针对性能优化方案
多部署几台机器,没太机器设置不同的初始值,且补长和机器数相等
Leaf-segment
各个业务不同的发号需求用biz_tag字段
互不影响
压力大只需对biz_tag分库分表
优化
批量获取,做双buggfer优化
ID分号段加载,当号段消费到某个点是酒异步把下一个号段加载到内存中
Leaf-snowflake
使用Zookeeper持久顺序节点
启动服务,连接Zookeeper,在父节点下检查自己是否注册过(是否有顺序子节点)
如果注册过取回自己的workId,启动服务
没有注册过,在父节点下创建一个持久的顺序节点,创建成果后取回顺序号当做自己的workerID
解决时钟问题
每个一段时间(3s)上报自身系统时间,并写入Zookeeper子节点
分布式锁的实现方式
基于Zookeeper
指定一个作为锁的znode节点
客户端在该znode下创建一个顺序短暂znode
查询znode子节点并设置一个观察
如果步骤1中创建的znode是步骤2中返回的最小节点,那么获取锁,退出
等待步骤2中所设的观察通知,转到步骤2
redis
获取锁的时候使用setnx加锁,并使用expire为锁设置一个超时时间
获取锁的时候还需要设置一个超时时间,超过这个时间则放弃获取锁
释放锁的时候通过锁的value值判断是不是这个锁,如果是则delete
数据库
乐观锁
在表中添加版本号字段,每次更新前都查询出版本号数据,更新是where 带版本号,如果更新成功就表示获取锁,如果失败,则重试
悲观锁
select...for update(X锁)/select...lock in share mode(S锁)
秒杀系统设计
架构原则
数据要尽量少
用户的请求数据能少就少
依赖的数据能少就少
读取、保存的数据,调用会涉及到序列化和反序列化
路径尽量要短
依赖尽量要少
不要有单点
动静分离
如何缓存静态数据
把静态数据缓存到离用户最近的地方
浏览器
CDN
服务器Cache
直接缓存HTTP连接
http协议不用重新组装,请求头也不用解析
由谁来缓存
如何做动静分离的改造
URL唯一化
可以以URL做为key缓存整个http连接
分离浏览者相关因素
是否已登录,登录身份等可以单独拆出来,通过动态请求获取
分离时间因素
服务端输出的时间通过动态请求获取
异步化地域因素
地域相关因素可以做成异步方式获取
去掉Cookie
缓存的静态数据中不含Cookie
动态数据处理方案
ESI或SSI
在Web服务器上做动态内容请求,并将请求插入静态页面
对服务器性能有影响,单用户体验好
CSI
单独发起一个异步JavaScript请求
服务端性能更佳,用户端页面可能会有延迟
架构方案
实体机单机部署
将虚拟机改为实体机增大Cache容量
一致性Hash分组来实现命中率
优点
没有网络瓶颈,能够使用大内存
提高命中率
减少Cache失效压力
缺点
造成CPU浪费,单个java进程很难用完真个实体机的cpu
部署java又作为Cache,运维高度复杂
统一Cache层
将单机的Cache统一分离出来,形成单独的Cache集群
优点
减少多个应用接入时使用Cache成本,接入的应用只要维护自己的java系统就好
更于维护,可以配置自动化,加强监控
可以共享内存,最大化利用内存,不同系统之间可以动态切换
缺点
Cache内部交换网络会成为瓶颈
缓存服务器的网卡也会成为瓶颈
机器少风险大,挂掉一台会影响很大一部分缓存数据
上CDN
将Cache缓存到CDN上,离用户最近,效果会更好
处理热点数据
发现热点数据
静态热点数据
强制让卖家通过报名参加的商业手段
通过大数据计算
动态热点数据
构建一个异步系统,收集交易链路中各个环节的热点key,如nginx,缓存,RPC服务框架这些中间件
建立一个热点上报、订阅热点服务的下发规范,把上游颖发现的热点透传给下游系统,提前做好保护
将上游发现的任店数据发送到热点服务台,下游系统做热点保护
处理热点数据
优化
缓存热点数据,可以用一个队列缓存数秒中,由于队列长度有限,可以使用LRU淘汰算法替换
限制
对访问商品ID做一致性Hash,根据hash分桶,每个分桶设置一个处理队列,把热点商品限制在一个请求队列中
隔离
业务隔离
系统隔离
数据隔离
流量消峰
排队
把同步的直接调用转换成异步的间接推送
利用线程池加锁等待
先进先出、先进后出等内存片段算法
把请求序列化到文件,然后再顺序读文件
答题
增加购买的复杂度
防止秒杀器作弊
延缓请求
分层过滤
在不同的层次尽可能过滤掉无效请求,让露头最末端才是有效请求
将动态请求的读数据缓存在web端,过滤掉无效的数据读
对读数据不做强一直性校验,减少一致性校验产生的系统瓶颈
对写数据进行基于时间的合理分片,过滤掉过期的实现请求
对写数据做限流保护,将超出系统承载能力的请求过滤掉
对数据进行强一致性校验,只保留最后有效数据
提高系统性能
如何发现CPU瓶颈
Jprofiler和Yourkit,列出请求中每个函数的cpu执行时间
jstack 定时地打印调用栈,多的函数会多吃出现在系统调用栈
如何判断是否是CPU瓶颈
QPS当大极限时服务器CPU利用率是否超过95%
如何优化
减少编码
每个字符的编码需要查表,查表操非常消耗资源
网页输出可以直接用流输出,resp.getOutputStream写数据,把静态数据提前转化为字节,等到真正往外写的时候直接用outoutStream写,减少静态数据编码转化
减少序列化
关联性比较强的应用合并部署
java极致优化
对大流量的web系统做静态化改造,大部分请求和数据直接在Nginx服务器或者web代理服务器上直接返回,java层处理少量动态请求
直接使用servlet请求,避免使用MVC框架,可以绕过一大堆复杂且无用的处理逻辑
直接输出流数据
并发读优化
秒杀系统的单机上缓存商品相关数据
标题、描述等不变的数据,秒杀之前全量推到秒杀机器上并一直缓存到秒杀结束
减库存
减库存的方式
下单减库存
不会出现超卖,但是有些人下单可能不会付款
付款减库存
会出现买家下单之后付不了款的情况
预扣库存
下单后库存为其保留一定时间,超过则自动释放库存
秒杀系统如何减
抢到就是赚到,一般下单减库存更加合理
如果没有复杂的联动关系,完全可以将减库存放到缓存系统(redis)实现
在mysql中会有大量的线程去竞争InnoDB行锁,数据库吞吐量严重受影响
解决并发锁方案
应用层排队
按商品维度设置对垒顺序执行,减少同一台机器对数据库同一行记录的并发操作
数据库层做排队
应用层只能做单机排队,数据库补丁程序可以在数据库层上对单行记录做并发排队
兜底方案
高可用建设
架构阶段
考虑扩展和容错性
编码阶段
保证代码的健壮性
涉及远程调用是,设置合理的超时时间,防止被其他系统拖垮
调用的返回结果集有预期,防止超出程序处理范围
测试阶段
保证测试用例的覆盖度,保证最坏情况的发生
运行阶段
对系统的监控及时准确,发现问题能后准备报警
故障发生
及时止损
及时恢复服务、并定位原因解决问题
降级
当系统容量达到一定程度,限制或关闭系统非核心功能
设置降级开关
限流
支持方法以及URL方式限流 也要支持基于QPS和线程的限流
客户端限流
优点
限制请求的发出,减少无用请求从而减少系统的消耗
缺点
客户端比较分散,无法设置合理的阈值
服务端限流
优点
可以根据服务端性能设置合理的阈值
缺点
被限制的请求是无效请求,处理这些请求也会消耗服务器资源
拒绝服务
如果限流无法解决问题,直接拒绝服务
可以zaiNginx设置过载保护,当机器负载达到某个值时就直接拒绝http请求
缓存
缓存穿透
缓存和数据库都没有数据,而用户不断发起请求,导致数据库压力过大
解决方案
接口层增加校验,如用户鉴权校验,数据基础校验
将key-value改为key-null,缓存有效时间设置短点,如30秒
缓存击穿
缓存中没,数据库中用(一般是缓存时间到期),由于并发用户特别多,引起数据库压力瞬间增大
解决方案
设置热点数据永不过期
加互斥锁,缓存中没数据,获取锁从数据库读,读到之后更新缓存,其他等待线程等待100ms再去缓存中读,
缓存雪崩
缓存中数据大批量到过去时间,并且查询数据量巨大,引起数据库压力过大
解决方案
缓存数据过期时间设置随机,防止同一时间大量数据过期现象发生
如果缓存数据库是分布式部署,将任店数据均匀分布在不同缓存数据库中
设置热点数据永不过期
volatile
volatile变量修饰的共享变量进行写操作是会生成lock汇编指令
将当前处理器缓存的数据行写会内存
一个处理器缓存写会内存导致其他处理器缓存失效
原子操作类
原子更新基本类型
AtomicBoolean
原子更新布尔类型
AtomicInteger
原子更新整型
addAndGet
以原子的输入将输入的数值与实例中的数值相加
compareAndSet
如果输入的值等于预期值,则以原子的方式将该值设置为输入值
getAndIncrement
以原子方式将当前值加1
使用空循环,获取AtomicInteger中的值,将当期值加1,使用CAS更新
......
AtomicLong
原子更新长整型
原子更新数组
AtomicIntegerArray
原子更新整型数组中元素
AtomicLongArray
原子更新长整型整数数组元素
AtomicReferenceArray
原子更新应用类型数组元素
原子更新字段类
AtomicIntegerFieldUpdater
原子更新整型的字段更新器
AtomicLongFileUpdater
原子更新长整型字段的更新器
AtomicStampedReference
原子更新带版本号的应用类型
可以解决CAS的ABA问题
多线程
线程的优先级
操作系统分出一个个时间片,线程会分配到若干时间片,优先级高的分配到的时间片越多
构建线程的时候通过时间setPriority方法来修改优先级
偏重计算的线程设置较低的优先级,频繁阻塞的设置较高的优先级
线程的状态
NEW
初始状态
线程被构建,但未调用start
RUNNABLE
运行状态
将就绪和运行统称为运行中
BLOCKED
阻塞状态
表示线程阻塞与锁
进入synchronize
WAITING
等待状态
进入该状态表示当前线程需要等待其他线程做出一定动作(中断、唤醒)
Lock接口
Lock对于阻塞的实现使用了LockSupport类中方法
TIME_WAINTING
超时等待
可以在指定时间自行返回
TERMINATED
终止状态
Daemon线程
一种支持性线程,用于程序中后台调度以及支持性工作
Thread.setDaemon(true)将线程设置为Daemon线程
不能依靠finally中的内容确保关闭或者清理资源
与非守护线程区别
jvm监测到仅剩一个守护线程,而其他用户线程都退出时 jvm就会退出,因为没有守护的对象了
中断操作
表示一个运行中的线程是否被其他线程进行了中断操作
线程通过检查自身是否被中断来进行响应
isInterrupt判断是否被中断
Thread.interrupt对中断标识复位
线程间通信
volatile和synchronized关键字
等待、通知机制
notify
通知一个在对象上等待的线程,使其从wait返回
notifyAll
通知所有等待在该对象上的线程
wait
是线程进入等待状态,只有等待通知或被中断才会返回,会释放对象的锁
wait(long)
超时一段时间,如果没有通知就超时返回
Thread.join
当前线程A等待thread线程终止后才从thread.join返回
ThreadLocal
线程变量
是一个以threadLocal对象为键、任意对象为值的存储结构
实现原理
Thread的对象都有一个ThreadLocalMap,最终的变量是放在了当前线程的 ThreadLocalMap 中
ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,在下一次垃圾回收的时候必然会被清理掉
线程池
实现原理
如果当前运行的线程少于corePoolSize,则创建新线程来执行任务
如果运行的线程等于或多余corePoolSize,则将任务加入BLockingQueue
如果队列已满,则创建新的线程来处理(需要获取全局锁)
如果创建线程超过MaxImumPoolSize,任务将被拒绝,并调用拒绝策略执行
ThreadPoolExecutor
FixedThreadPool
可重用固定线程数的线程池
使用无界队列LinkedBlockingQuery作为线程池的工作队列
当线程池中的线程数到达CorePoolSize后,新任务将在无界队列中等待,线程数不会超过corePoolSize
maximumPoolSize 将是无效参数
keepAliveTime 将是一个无效参数
不会调用拒绝策略
SingleThreadExecutor
使用单个WorKer的线程池
corePoolSize和maximumPoolSize都被设置为1
实现时,定义了FinalizableDelegatedExecutorService包装类
实现了finalize 方法
进行垃圾回收的时候自动shutdown线程
CachedThreadPool
根据需要创建线程的线程池
corePoolSize设置为0,即为空,maximumPoolSzie被设置为Integer.MAX_VALUE,即无界,keepAliveTimes设置为60L,意味着corePoolSize中的空闲线程等待任务的最长时间为60秒,超过被终止
使用没有容量的SynchronousQueue作为线程池工作队列
ScheduledThreadExecutor
给定的延迟之后运行任务,或者定期执行任务
使用DelayQueue无界队列作为线程池的工作队列
TutureTask
实现了Future接口外还实现了Runnable接口
FutureTask可以交给Executor执行
ExecutorService.submit 返回FutureTask
实现
基于AQS
ReenTrantLock、semaphore、CountDownLatch都是基于AQS实现
FutureTask声明了一个私有的继承于AQS的子类Sync,对FutureTask的调用都委托给这个内部类
子主题
java中的锁
synchronized(JVM层面)
对于普通方法锁的是当前实例对象
对于静态同步方法,锁的是当前类的Class对象
对于同步方法块 锁的是Synchonized括号中配置的的对象
实现原理
JVM基于进入和退出Monitor对象实现方法同步和代码块同步
JAVA对象头
ssynchronized锁存在JAVA对象头
Mark Word中存储对象的HashCode、分代年龄、锁标记位
锁的升级
偏向锁
加锁
当线程访问代码块并获取锁时,会在对象头和栈帧的锁记录中存储偏向锁的线程ID,下次访问时只需简单测试对象头中的线程ID是当前线程ID,则直接获取锁
解锁
偏向锁不会主动撤销,只有等到竞争时才会撤销
检查对象头中的线程是否存活,如果没有存活那么锁对象被重置为无锁状态,其他线程可以竞争将其设置为偏向锁,如果存活,那么查看该线程的栈帧信息如果还是需要继续持有这个锁对象那么暂停当前线程1,撤销偏向锁,升级为轻量级锁,如果线程1 不再使用该锁对象,那么将锁对象状态设为无锁状态,重新偏向新的线程
轻量级锁
加锁
JVM在当前栈帧中创建存储锁记录的空间,并将对象头中的mark word复制到锁记录中
尝试使用CAS替换对象头中的mark word
如果成功则获取锁,如果失败尝试使用自旋获取锁
解锁
使用CAS将栈帧中的mark word替换回对象头,如果成功,则表示没有竞争发生,如果失败则膨胀为重量级锁
重量级锁
其他线程试图获取锁的操作都会被阻塞
Lock(java 类)
synchronized不具备的特性
尝试非阻塞获取锁
能够中断获取锁
超时获取锁
可以选择公平性
实现原理
队列同步器(AQS)
主要方法
基础方法
getState
获取当前同步状态
setState
设置当前同步状态
compareAndState
使用CAS设置同步状态
可重写方法
tryAcquire
独占式获取同步状态
查询当前状态,并判断是否符合预期,然后使用CAS判断同步状态
tryRelease
独占式释放同步状态
tryAcquireShared
共享式获取同步状态
tryReleaseShared
共享式释放同步状态
实现逻辑
同步队列
使用FIFO的双向队列完成线程同步
获取同步状态失败是会将当前线程以及等待信息组成Node加入其队列
加入队列过程使用CAS保证正线程安全
首节点是获取同步状态成功的节点,首节点的线程释放同步状态后会唤醒后继节点
独占式同步状态的获取与释放
通过调用同步器的acquire获取同步状态,对中断不敏感
acquire中通过调用自定义同步器实现的tryacquire
获取失败,则构造同步节点Node,通过addWaiter将改节点加入队列尾部
调用acquireQueued 使该节点以死循环的方式获取同步状态,如果获取不到则阻塞节点中的线程,通过前驱节点出队来唤醒
获取锁并且处理完相应逻辑之后调用同步器的release释放同步状态
唤醒后继节点(LockSupport.unpark)
共享式同步状态的获取与释放
通过调用同步去的acquireShare获取同步状态
acquireShare中通过调用自定义同步器tryAcquireShare方法获取同步状态
当tryAcquireShare大于等于0时获取同步状态
调用同步器的releaseShared释放同步状态
必须保证安全的释放一般使用循环和CAS保证
独占式超时获取同步状态
主要计算需要睡眠的时间间隔
nanosTimeOut = now -lastTime
如果nanosTimeOut>0则表示睡眠时间未到,需要继续睡眠,反之超时
now 为当前唤醒时间,lastTime为上次唤醒时间
ReentrantLock(重入锁)
判断当前线程是否为获得锁的线程,如果是,则将同步状态值增加 并返回true
释放锁时,只有将同步状态修改为0才算释放成功
公平锁
多个hasQueuedPredecessors判断
加入同步队列中的当前节点是否有前驱的判断
如果返回ture说明有线程比当前线程更早的获取锁,因此需要继续等待
ReentrantReadWriteLock(读写锁)
按位切割使用同步变量,搞16位表示读,低16位表示写
LockSupport工具
基础方法
park
阻塞当前线程
如果调用unpark方法或者当前线程被中断,才会返回
parkNanos(long nanos)
阻塞当前线程,最长不能超过nanos纳秒
parkUntil(long deadline)
阻塞当前线程,直到deadline时间
unpark
唤醒处于阻塞状态的线程
和wait区别
wait、notify必须保证wait方法比notify方法先执行,但是park、uppark可以先调用unpark释放一个许可证,后面线程调用park时发现许可证直接进入休眠
wait方法会释放持有的锁,park进入休眠后并不会释放持有的锁
park会响应中断,当外部线程对阻塞线程调用interrupt时,park阻塞线程会立即得到返回
Condition
定义了等待、通知两种类型的方法,与Lock配合使用
实现原理
是AQS的内部类
等待队列
FIFO的队列,每个节点都包含了一个在condition对象上等待的线程引用
只保存nextWaiter,condition拥有首节点和尾节点
condition.await
该线程释放锁
构造成节点加入等待队列并进入等待状态
从AQS角度,相当于从同步队列的首节点异动到Condition的等待队列
Condition.signal
唤醒等待队列中等待时间最长的节点,唤醒之前加入同步队列
锁在应用层的优化思路
减少锁持有时间
只有需要时进行同步
减少锁粒度
ConcurrentHashMap
锁分离
LinkedBlockingQueue实现
锁粗化
多次加锁合并为一次
CAS
并发容器
ConcurrentHashMap
结构
Segment数组
可重入锁
为了通过按位与的散列算法定位到Segment数组索引,必须保证Segment数组长度是2的N次方
HashEntry数组
用于存储键值对数据
操作
get
不用加锁
get方法中使用的变量都定义为volatile
如统计Segment大小的Count,用于存储值的HashEntry的value
通过一次再散列,通过散列运算定位到Segment,再通过散列算法定位到元素
put
操作共享变量时必须加锁
判断是否需要扩容
创建一个容量是原来两倍的数组,将原数组元素进行再散列后插入新的数组中,只对某个Segment
定位添加元素的位置,将其放入HashEntry数组
ConcurrentLinkedQueue
非阻塞的线程安全队列
结构
由head节点和tail节点组成(tail不一定是尾节点)
每个节点有节点元素和指向下一个节点的引用组成,节点与节点之间通过next关联,组成一张链表结构的队列
操作
入队列
将入队节点设置为当前队列尾节点的下一个节点
更新尾节点(CAS)
如果tail节点的next节点不为空,则将入队节点设置为tail节点
如果tail节点的next节点为空,将入队节点设置为tail的next节点
出队列
使用hops变量减少使用CAS更新head节点的消耗
阻塞队列
分类
ArrayBolckingQueue
由数组结果组成的有界阻塞队列
默认不保证公平
LinkedBlockingQueue
由链表组成的有界阻塞队列
默认和最大程度为Integer.VALUE 按照先进先出的原则访问元素
PriorityBlockingQueue
支持优先级排序的无界阻塞队列
默认情况元素才去自然顺序升序
DelayQueue
使用优先级队列实现的无界阻塞队列
SynchronousQueue
不存储元素的阻塞队列
LinkedTransferQueue
链表结果组成的无界阻塞队列
LinkedBlockingQueue
链表结果组成的双向阻塞队列
实现原理
使用通知模式实现
ArrayBlockingQueue使用Condition是实现
Frok/Join
设计
RecursiveAction
用于没有返回结果的任务
RecursiveTask
用于有返回结果的任务
实现原理(使用工作窃取算法)
fork
调用ForkJoinWorkerThread.pushTask异步执行
pushTask方法把当前任务放在ForkJoinTask数组队列中
调用FrokJoinPool的signalWork创建或唤醒一个工作线程来执行任务
join
通过调用doJoin方法得到当前任务状态来判断返回的结果
并发工具类
CountDownLatch
等待多线程完成
接入一个int类型的参数,如果想等待N个点完成,就传入N
调用countDown方法N就会减1
不可能重新初始化或者修改内部计数器的值,这也是和CyclicBarrier的主要区别
CyclicBarrier
同步屏障
让一个线程到达一个同步点时被阻塞,知道最后一个线程到达屏障时,所有被屏障拦截的线程才会继续执行
CountDownLatch与CyclicBarrier区别
CountDownLatch 计数器只能使用一次,CyclicBarrier可以使用reset重置,如果计算错误,可以重置计数器,重新执行
CyclicBarrier可以获取阻塞线程的数量
CyclicBarrier提供一个更高级的函数 用于线程到达屏障时,优先处理某业务
Semaphore(信号量)
控制并发线程数
可以用作流量控制,比如数据库连接
Exchanger
线程间交互数据
提供一个同步点,在这个同步点 两个线程可以交换彼此的数据
内存模型
指令重排
源代码
编译器优化重排序
指令级并行重排序
内存系统重排序
最终执行的指令序列
happens-before
程序监控规则
一个线程中的每个操作,happens-before于该线程中的任意后续操作
监视器锁规则
对一个锁的解锁,happens-before于随后对这个锁的加锁
Volatile变量规则
对一个volatile域的写,happens-before于任意后续对这个域的读
传递性
如果A happens-before B,且B happens-before C 那么 A happens-before C
volatile内存语义
可见性
对volatile的对,总是能看到任意线程对这个volatile最后的写入
原子性
对任意单个volatile的读写具有原子性
类似volatile++复合操作不具有原子性
volatile读-写内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中
当读一个volatile变量时,JMM会把该线程对应的本地地址设置为无效直接从主内存中读取
volatile内存语义实现
当第二个操作是volatile写是,第一个操作是什么都不能重排序
第一个操作是volatile读时,不管第二个操作是什么都不能重排序
第一个操作是volatile写,第二个操作是volatile都市,不能重排序
在指令序列中插入内存屏障禁止处理器重排序
volatile读之前,读之后,写之前、写之后都会插入内存屏障
锁的内存语义
锁的释放和获取内存语义
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中
当线程获取锁时,JMM会把该线程对应的本地内存设置为无效,从主内存中读取变量
锁的内存语义实现
ReentrantLock公平锁
加锁时首先读取volatile变量
释放锁的最后写volatile变量
根据volatile的happens-before规则,释放锁的线程写volatile之前可见共享变量,获取锁的线程读取同一个volatile变量后立即对获取锁的线程可见
ReentrantLock非公平锁
以原子的方式更新volatile变量,通过volatile语义,编译器不能对CAS和CAS前后的任意内存操作重排序
concurrent包实现
声明共享变量为volatile
使用CAS的原子条件更新实现线程间的同步
配合vloation读/写内存语义实现线程之间通信
HBase
概念
列族
行中的列被划分为列族
相同的成员具有相同的前缀
列族必须做为表定义的一部分预先给出,新的列祖成员可以随时按需要加入
物理上所有列族成员都一起存放在文件目录中
调优和存储都是在列族层次上进行的,最好使列族成员都有相同的访问模式
区域
自动把表水平划分成区域,每个区域由表中的行构成
随着表的增大,区域的个数也会变大
区域是HBase集群上分布的最小单位
一个大表会放大服务器集群上,每个节点负责管理表所有区域的一个子集
实现
集群模式
master
协调管理一个或者多个regionserver从属机
把区域分配给注册的regionserver,恢复regionserver故障
regionserver
负责0或多个区域的管理以及相应客户端的读写请求
吧区域划分并通知master有了新的区域
运行
内部保留hbase:meta的特殊目录表
维护当前集群所有区域的列表、状态和位置
使用区域名作为主键
表名+区域的起始行+区域创建时间+对整体MD5哈希值
区域变化时目录表会更新
读数据
客户端查找hbase:meta的位置
获取数据区域所在的节点及位置
查询区域内存
未找到从新到旧检查刷新文件
写数据
到达regionserver首先追加到"提交日志"
加入内存memstore
如果memstore已满输入文件系统
后台进程负责当刷新文件到达阈值压缩它们写入新的文件
Zookeeper
数据模型
znode
概念
Zookeeper维护树型层次,树种的节点称为znode
znode通过路径被引用,必须是绝对路径且规范的
分类
短暂znode
客户端会话结束时Zookeeper会删除短暂znode
不可以有子节点
持久znode
不受客户端影响,只有明确删除时才会被删除
顺序号
创建znode设置顺序标识,znode名称之后会附加一个由父节点维护的单调递增的计数器
顺序号可以被用于为所有的事件进行全局排序
实现共享锁
观察
当znode发生变化时,使用观察机制可以让客户端得到通知
操作
基本操作
create、delete、exists、getACL、setACL、getChildren、getData、setData、sync
集合更新
multi将多个基本操作合成一个原子操作
比如构建一个无向图
观察触发器
可以再exists、getChildren、getData这些读操作上设置观察
原理
实现
独立模式运行
只有一个ZooKeeper服务器
复制模式
只要集合体中超过半数以上的机器可用,它就可以提供服务
一个集合体中通常包含奇数台机器
ZAB协议
选票数据结构
logicClock
每个服务器都会维护一个自增的整数,表示该服务器发起第多少轮投票
state
当前服务器状态
self_id
当前服务器的myid
每个服务器都会在文件将下创建一个名为myid的文件
包含整个集群唯一的ID
self_zxid
当前服务器上所保存的数据的最大zxid
zkid单调递增、标识一次更新的事务唯一ID
vote_id
被推举的服务器的myid
vote_zxid
被推举的服务器上所保存的数据的最大zxid
投票流程
每个服务器在开始新一轮的投票时,会先对自己维护的logicClock自增
每个服务器最开始都是通过广播把票投给自己
接收外部投票
服务器会尝试从其它服务器获取投票,并记入自己的投票箱内
判断选举轮次
选票PK
统计选票
如果已经确定有过半服务器认可了自己的投票(可能是更新后的投票),则终止投票。否则继续接收其它服务器的投票
更新服务器状态
投票终止后,服务器开始更新自身状态。若过半的票投给了自己,则将自己的服务器状态更新为LEADING,否则将自己的状态更新为FOLLOWING
一致性
ZAB协议保证了在Leader选举的过程中,已经被Commit的数据不会丢失,未被Commit的数据对客户端不可见
未Commit过的消息对客户端不可见
写操作
客户端发送写请求
将请求转发给leader
leader将写请求发送给所有Follower并等待ACK
Follower收到写请求后返回ACK
leader收到过半ack后向所有follower提交commit
leader 将处理结果返回给客户端
读操作
leader、follower都可处理读请求
从本地内存读取数据并返回给客户端
应用
配置服务
使用观察机制
可复原的Zookeeper应用
InterruptedException
Zookeeper一个成功的取消操作将产生此异常
KeeperException
状态异常
当一个操作不能被应用于znode数而导致异常
可恢复的异常
应用能在同一Zookeeper会话中恢复的异常
分布式锁服务
基本实现过程
首先指定一个作为锁的znode
在锁znode下创建一个短暂顺序znode
查询znode的子节点并设计一个观察者
如果步骤1中创建的znode在步骤2返回子子节点中是最小的 则获取锁,然后退出
等待步骤2中观察者的通知,转到步骤2
问题以及优化
羊群效应
产生原因
所有客户端都会在锁znode上设置一个观察,维护的过程以及向客户端发送观察事件会产生峰值流量对Zookeeper服务器造成压力
解决方案
修改发送通知的条件
仅当前一个顺序号的子节点消失时才需要通知下一个客户端,而不是删除或创建任何一个节点时都都通知
可恢复异常
产生原因
创建顺序Znode是非幂等操作,重试后会产生很多孤儿znode
解决方案
使用客户端会话ID确定唯一性
lock-seesionId-sequenceNumber
重连后对锁的所有子节点进行检查
设计模式
Kafka
设计原理
broker(KafKa服务器)
持久化消息以及将消息队列中的消息从发送端传输到消费端
集群管理
使用ZooKeeper进行成员管理和服务发现
副本与ISR设计
基础概念
ISR
ISR动态维护的一组副本集合,每个topic分组都有自己的副本集合
起始位移
该副本所包含第一条消息的offset
高水位印(HW)
保存了该副本最新一条已提交的消息的位移
确定了consumer能够获取的消息上限
更新机制
follower更新HW
比较当前LEO值与请求响应中leader的HW值,两者取小作为新的HW
leader更新HW
比较自己的LEO和所有副本的LEO取最小值
日志末端位移(LED)
副本日志中下一条待写入消息的offect
更新机制
follower副本端的follower副本LED
leader将数据返回给follower副本,此时follower想底层log写数据,自动更新其LED值(+1)
leader副本端的follower副本LED
leader处理follower请求是从自己的log中读取相应的数据,在给follower返回数据之前更新follower的LED值
follower副本同步基本流程
broker1上的leader接收producer的消息,把自己的LED跟新为1
broker2和broker3的follower副本各自发送请求给broker1
broker1把消息推送给follower副本
follower副本接收到消息后各自更新自己的LEO为1
leader接收其他follower副本数据请求响应(Response)后更新HW值,此时位移为0的这条消息可以被consumer消费
备份原理
基于数据水印
leader接收消息
写入消息到底层日志,同时更新自己的LEO属性
尝试更新HW
比较自己的LEO和follower的LEO,取最小值
HW=0,leader的LEO=1follower的LEO都为0
follower发送请求后
读取底层log数据
更新follower的LEO为0
尝试更新HW:min(leader LEO,follower LEO) = 0
将当前数据和当前前HW(0)发送给follower
follower接收到数据后
写入本地log 同时更新LEO
更新follower的HW =min(本地LEO,leader的HW) =0
当前数据虽然被报存在log中,但HW未更新,发起第二轮请求时才会更新
基于数据水印的缺陷
数据丢失
前提是 min。insync.replicas = 1(消息被写入leader端,log被认为已提交)
备份数据不一致
ISR动态维护的一组副本集合,每个topic分组都有自己的副本集合
解决方案
引入leader epoch(其实是一对值(epoch,offset))
epoch表示leader的版本号,从0开始,当leader变更过一次,epoch就会加1
每个leader broker保存这个缓存,每次副本重新成为leader是回查询这部分缓存
ISR设计
follower副本落后leader副本的时间间隔超过10秒(默认),该副本就会被移出ISR
日志设计
每个分区日志都有若干组日志段文件+索引文件
日志段
一旦日志段被写满后,kafaka会自动创建一组新的日志段文件+索引文件
索引文件
.index(位移索引文件)
按位移顺序保存
.timeindex(时间戳索引文件)
按时间戳顺序保存
controller(控制器)设计
某个broke会被选举出来成为控制器
每个Kafka集群任意时刻都只能有一个controller
producer端设计
序列化+计算目标分区
追加写入消息缓冲区
Sender线程处理response
Consumer端设计
Storm
核心概念
拓扑(topology)
一个由元组、流、bolt、spout等组成的计算图
元组(tuple)
拓扑节点之间传输数据的形式
本身是一个有序的数值序列
流
一个流是一个无边界的元组序列
spout
一个spout是拓扑的流数据源头
从外部数据源读取数据并向拓扑中发射元组
bolt
主要对元组进行计算或转换,如过滤、组合、连接等
流分组
随机分组
确保每个bolt实例尽肯能收到数量相等的bolt
字段分组
字段相同的数据会被发送到同一个bolt
......
健壮的拓扑
消息处理保障
元组处理完成或失败
spout发射出一个元组,下游的bolt处理后可能会发射更多的元组,形成元组树(有向无环图)
如何跟踪元组树?
其输入元组已被锚定
bolt完成输入元组的处理后通知storm,应答(ack)
锚定、应答、容错
隐式
bolt继承BaseBasicBoolt
显式
BaseRichBolt
可靠性级别
最多一次处理
至少一次处理
仅一次处理
内核
数据如何在集群的spout和bolt之间流转?
基本概念
工作节点(物理机或虚拟机)
工作进程(JVM)
运行与工作节点之上
执行器(两个线程一个队列)
运行spout或bolt实例
主线程
运行用户自定义代码
发送线程
独立的队列
将元组从执行器发射出来
依赖第三方库LAMX Disruptor
同一JVM中两执行器之前传输
一条额外的队列处理输入的元组
主线程将元组从队列中提取出来
主线程将元组发送到bolt实例处理
主线程将发射的元组植入输出的分发队列
发送线程将队列中的元组发射出去
不同JVM中两执行器之间传输
发送线程将元组植入执行器的对外分发队列
并将其直接置入JVM的对外分发队列
JVM的发送和传输线程从对外分发队列中读取元组
将其发送到另一个jvm接收线程(TCP(netty))
另外一个JVM接收线程收到元组后,将其置入相应的执行器的输入分发队列中
Storm内部队列溢出
一个执行器的输入队列
一个执行器的输出队列
一个工作结点的对外队列
Storm处理方式
调整生产与消耗的比例
让元组的产生速度慢一点或者提高消耗速度是最佳的处理方案
提升所有拓扑缓冲区大小
不建议采用
提升指定拓扑缓冲区大小
提交拓扑时在config类中设置
设置spout的最大等待数
子主题
常用排序算法
elasticsearch
Spring
Redis
5中数据结构
string
list
set
hash
zset
收藏
0 条评论
下一页