架构师技术栈
2024-04-02 20:38:24 1 举报
AI智能生成
架构
作者其他创作
大纲/内容
java基础
集合
Collection
List
List集合基础
实现了Collection接口
List接口特性∶是有序的,元素是可重复的
允许元素为rull
常用的子类
Vector
底层结构是数组,初始容量为10,每次增长2倍
它是线程同步的,已被ArrayList替代
LinkedList
底层结构是双向链表
实现了Deque接口,因此我们可以像操作栈和队列一样操作它
线程非同步
ArrayList
底层结构是数组,初始容量为10,每次增长1.5倍
在增删时候,需要数组的拷贝复制(navite方法由C/C++实现),性能还是不差的!
线程非同步
Set
Set集合基础
实现了Collection接口
set接口特性︰无序的,元素不可重复
底层大多数是Map结构的实现
常用的三个子类都是非同步的
常用子类
HashSet
底层数据结构是哈希表(是一个元素为链表的数组)+红黑树
实际上就是封装了HashMap
元素无序,可以为null
LinkedHashSet
底层数据吉构由哈希表(是一个元素为链表的数组和双向链表组成。
父类是HashSet
实际上就是LinkHashMap
元素可以为null
TreeSet
底层实际上是一个TreeMap实例(红黑树)
可以实现排序的功能
元素不能为null
Map
基础知识
存储的结构是key-value键值对,不像Collection是单列集合
阅读Map前最好知道什么是散列表和红黑树
子类
HashMap
底层是散列表+红黑树。初始容量为16,装载因子为0.75,每次扩容2倍
允许为null,存储无序
非同步
散列表容量大于64且链表大于8时,转成红黑树
Key的哈希值会与该值的高16位做异或操作,进一步增加随机性
当散列表的元素大于容量*装载因子时,会再散列,每次扩容2倍
如果hashCode相同,key不同则替换元素,否则就是散列冲突
LinkedHashMap
底层是散列表+红黑树+双向链表,父类是HashMap
允许为null,插入有序
非同步
提供插入顺序和访问顺序两种,访问顺序是符合LRU算法的,一般用于扩展(默认是插入顺序)
迭代与初始容量无关(迭代的是维护的双向链表)
大多使用HashMap的API,只不过在内部重写了某些方法,维护了双向链表
TreeMap
底层是红黑树,保证了时间复杂度为log(n)
可以对其进行排序,使用Comparator或者Comparable
只要compare或者CompareTo认定该元素相等,那就相等
非同步
自然排序(手动排序),元素不能为null
ConcurrentHashMap
底层是散列表+红黑树,支持高并发操作
key和value都不能为null
线程是安全的,利用CAS算法和部分操作上锁实现
get方法是非阻塞,无锁的。重写Node类,通过volatile修饰next来实现每次获取都是最新设置的值
在高并发环境下,统计数据(计算size...等等)其实是无意义的,因为在下一时刻size值就变化了。
jvm
类加载
过程
加载
链接
验证
准备
解析
初始化
类加载条件
创建实例
静态方法
静态字段
反射
初始化子类
main方法
双亲委派
先找父类
父类没有,向下查找
ClassNotFoundException
ClassLoader主要方法
loadClass
给定一个类名,加载一个雷,返回代表这个类的Class 实例,如果找不到类,则返回异常。
defineClass
根据给定的字节码流b定义一个类,off表示位置,len表示长度。该方法只有子类可以使用,
findclass
查找一个类,也是只能子类使用,这是重载ClassLoader时,最重要的系统扩展点。这个方法会被loadClass 调用,用于自定义查找类的逻辑,如果不需要修改类加载默认机制,只是想改变类加载的形式,就可以重载该方法。
findLoadedClass
同样的,这个方法也只有子类能够使用,他会去寻找已经加载的类,这个方法是final方法,无法被修改。
哪些类加载器
Bootstrap
负责加载<JAVAHOME>/lib目录中的,或者别-Xbootclasspath参数指定的路径。并且是被虚拟机识别的,如rtjar,名字不符合的类库即使放在ib目录中也不会加载。
sun.misc.Launcher$ExtClassLoader
负责加载<JAVA_HOME>/lib/ext目录中的。或者被java.ext.dirs系统变量所指定的路径中的所有类库。
sun.misc.LauncherSAppClassLoader
由于这个类是ClassLoader 中的getsystemClassLoader方法的返回值,也称为系统类加载器,负或加载用户类路径( ClassPath )上所指定的类库,开发者可以直接使用这个类加载器。一般情况下,这个就是程序中默认的类加载器。
自定义类加载器
│自定义类加载器用于加载一些特殊途径的类,一段也是用户程序类。
类加载器的缺陷和补充
SPI
在Java平台中,把核心类(rtjar)中提供外部服务,可由应用层自行实现的接口,通常可以称为Service Provider Interface,即 SPl
在rtjar中的抽象类需要加载继承他们的在应用层的子类实现,但是以目前的双亲机制是无法实现的。
上下文类加载器
也就是讲类加载放在线程上下文变量中。通过Thread.getContextClassLoader(), ThreadsetContextClassLoader(ClassLoader)这两个方法获取和没ClassLoader,这样,rt.,jar中的代码就可以获取到底层的类加载了。
重写类加载器逻钼
双亲模式是虚拟机的默认行为,但并非必须这么做,通过重觌ClassLoader可以修改该行为。事实上,很多框架和软件都修改了,比如Tomcat ,OSG1。具体实现则是通过重写loadClass 方法,改变类的加载次序。比如先使用自定义类加载器加载,如果加载不到,则交给双亲加载。
光加载死锁
jstack看不到死锁信息
内存布局
堆
虚拟机栈
本地方法栈
方法区(永久代)
常量池(运行时常量池)
程序计数器
直接内存
GC
算法
标记清除算法
最基础的算法
碎片问题
效率问题
复制算法
基于标记清除的优化算法,使用2块内存,解决内存碎片
缺点是内存缩小了到了原来的一半
现代虚拟机大都采用这种算法
通常做法
Eden
From
To
8 : 1 : 1
分配担保
标记整理算法
解决对象存活率较高的问题
解决额外空间担保问题
老年代的GC使用该算法
在标记清除的基础上整理碎片
分代收集算法
新生代
老年代
目的︰根据各个年代的特点采用最适当的收集算法
GC任务
哪些内存需要回收
引用计数
可达性分析
GC Roots
虚拟机栈〔栈帧中的本地变量表)中引用的对象
方法区中静态属性引用的对象
方法区常量引用的对象
本地方法栈中JNl引用的对象
什么时候回收
finalize方法
如果—个对象重写了finalize方法,那么这个方法最多只会被执行一次
如非必要,不要重写该方法
如何回收(大问题)
垃圾处理器
Serial串行收集器〔只适用于堆内存256m 以下的JVM)
单线程
独占式
系统会停顿很长时间
复制算法
-XX:UseSerialGc
Client 默认收集器
CMS的备用处理器
ParNew并行收集器(Serial 收集器的多线程版本)
-×X:ParallelGCThreadl指定线程数
一般和CPU数量相当
默认
当CPU小于8,线程数是CPU 数量
当大于8,公式:3+( ( 5* CPU ) /8 )
Parallel Scavenge 〔PS 收集器,该收集器以吞吐量为主要目的)
和ParNew类似,但关注吞吐量
吞吐量设置
-XX:MaxGCPauseMillis设置最大垃圾收集停顿时间,是一个大于0的整数,PS处理器会将停顿时间控制在参数内,如果设置的很小,PS 收集器会将堆设置的很小,导致垃圾回收频繁,从而降低吞吐量
-XX:GCTimeRatio设置吞吐量大小,0-100,公式为1/(1+n),如果n 是19,则垃圾收集的时间不超过5%,默认是1,即垃圾收集时间是1%
-XX:UseAdaptiveSizePolicy打开自适应策酪,新生代的大小,eden和Survivor的比例,晋升老年代的年龄阈值将会自动调整,直到达到吞吐呈和停顿时间的平衡点。
JDK8的默认垃圾收集器
手工调优困难的场合,可以使用该收集器
GCTimeRatio和 MaxGCPauseMillis是冲突的,即停顿时间和吞吐量是冲突的,停顿时间短,则吞吐量会下降,吞吐量大,停顿时间会变长
Parallel Old
老年代的Parallel Old 收集器,工作在Old 区,和PS一起使用。标记清除算法
CMS收集器〔全称Concurrent Mark Sweep,关注最短停顿时间)
与PS相反,特点
并发标记清除
内存碎片
多线程回收
实现复杂
执行过程
初始标记(STW标记根对象〕
仅仅是标记一下GC Roots能直接关联到的对象,速度很快
并发标记(并发标记所有对象)
进行GC Roots的跟踪过程
清理前准备以及控制停顿时间
由于并发标记阶段和程序是并发执行的,因此会产生大量的新对象指向老年代的对象,引用关系发生变化.如果不处理,remark阶段〔独占式)持非常耗时。
因此,在这个阶段,CMS会尽量处理那些变化的对象,特别是新生代中的对象。默认5秒之内,其实这5秒,是在等待一次YGC,希望YGC能够把那些新生的对象消除,避免后面的remark阶段扫描导致长时间的暂停。
不过这个功能可以通过 -XX-CMSPrecleaningenabled关闭。也可以通过CMSScavengeBeforeRemark强制在此阶段发生YGC。注意:虚拟机还会预估下次的YGC发生时间,尽量不让remark阶段和下一次YGC阶段重叠,防止停顿时间过长
重新标记( STW修正并发标记数据(时间数长))—
为了修正并发标记期间产生变动的那β一部分对象,这个阶段的巴停时间一酸会比初始标记时间长一些,但远比并发标记的时间短
并发清理垃圾
重置
3个注意点
1.CMS对CPU资源敏感,由于是并发执行的,所有会抢夺CPU资源,默认相册数(CPU + 3 ) / 4,所以需要妥善设置好ParallelGCThread参数
2由于并发清理阶段,会产生大量的对象,如果内存不够,将会出现Concurrent Mode Failure同时 Full GC,并使用备用收集器Serail,停顿时间将非常的长,当出现这种哦情况的时候,使用-KX ∶CMSInitiatingOccupancyFraction的值来设定老年代的空间使用百分率来吃触发CMS,如果Old 区增长很快,则设置的低一些,防止Full GC,反之,则可以设置的高一些,尽量减少Old Gc
3.由于CMS基于标记清理算法,肯定会有内存碎片,因此虚拟机提供了-XXc+UseCMSCompactAtFullCollection开关参数(默认开启),用于在CMS顶不住要进行FGC 的最后进行碎片整理,但停顿时间会变长,因此,虚拟机还提供了一个参数-XX : CMSFulGCBeforeCompatcion,这个参数是用于设置执行了多少次不压缩的FGC后,跟着来一次整理的(默认是0,也就是每次都整理)。
G1收集器
特点
1.并行性(多个GC线程同时工作)
2.并发性(和应用程序并发执行)
3.分代 GC,G1最大的的区别就是他既工作在年轻代也工作在老年代,和之前的GC收集器完全不同
4.空间整理,CMS有内存碎片,G1基于复制算法,没有suip
5.可预测的停顿,由于分区的原因,G1可以只选取部分区域进行内存回收,缩小了范围,相应的减少了系统停顿。
设计— Region
将Java堆分成了多个大小相等的独立区域(Region ),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都是—部分Region的组合
G1之所有能够预测停顿时间,是因为它不再像别的收集器那样收集整个新生代或者老年代,而是回收一部分Region
G1跟踪各个Region里面的垃圾的价值大小(回收所获得空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region (这也是Grabage-First的由来)。这种根据垃圾价值来回收Region的方式保证了G1在有限的时间里回收更多的内存。
在VM启动是不需要立即指定哪些Region属于年轻代,哪些属于老年代,因为无论是年轻代还是老年代,他们都不需要一大块连续的内存,只是由一系列Region组成而已。随着时间的流逝,Region有时属于新生代,有时属于老年代,来回变动。
注意:Region不可能是孤立的,一个对象分配在某个Region中,他并非只能被本Region 中的其他对象引用,而是可以与整个java址任意的对象发生引用关系。在做可达性判断的时候,不可能3描整个堆。也就是:如果回收新生代的时候同时也扫描老年代,那么YGC的效率将会大打折扣。
解决办法是:Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用,JVM都是使用Remembered Set来避免全堆扫描的,G1中每个Region都有一个与之对应的Remembered Set。当Region 中的引用发生变化的时候,G1将会把这些信息记录到一个Cardtable数掘结构中并存到被引用对象所尾的 Region的RSet 中。当进行内存回收时,在GC根节点的枚举范围中加入RSet即可保证不对全堆扫描也不会有漏洞。一般来说,RSet的大小占整个Java堆空间的1% =20%,
G1 YGC的过程和之前的YGC相同:当Eden区占满,YGC就会启动,YGC只处理Eden和Survivor 区,回收后,所有的Eden区将会被清空
收集过程〔4步骤)
新生代GC
G1 YGC的过程和之前的YGC相同:当Eden区占满,YGC就会启动,YGC只处理Eden和Survivor 区,回收后,所有的Eden区将会被清空。而Survivor区会被收集一部分数据,但是应用至少仍然存在一个Survivor区。另外,老年代的Region会增多,因为通常YGC后会有大量的对象晋升到老年代。
并发标记周期
当老年代的使用率达到了一定的讯值,则会触发并发标记,而并发标记的主要目的则是为了标记处那些垃圾比例比较高的Region,为后面的混合收集服务,即收集整个新生代和部分老年代。而并发标记的过程和CMS类似。可参看CMS的过程
混合收集
在之前的并发标记过程中,已经标记出来垃圾比例绞高的Region,此时轮到混合回收出场了,而这也是G1的由来,有限回收垃圾比例较高的Region。之所以叫混合回收,是因为既执行正常的年轻代GC,又会选取一些被标记的老年代Region进行回收。被清理的区域中的存活对象被拷贝到其他区城域,消除了CMS 产生的内存碎片。混合GC会执行多次,直到回收的足够多的内存空间,然后,他会触发一次YGC,YGC后,有可能会发生一次并发周期的处理,最后,又会引起混合GC的执行,循环反复。
如果需要,将进行FGC
如果内存增长的很快,而混合GC的速度又跟不上,老年代被填满,则进行一次 FGC。而G1和FGV算法是单线程的Serial GC,因此会造成长时间的停顿,所以,一定要避兔FGC出现。
GC组合
l. Serial + Serial Old
2.Serial + CMS
3.ParNew + CMS
4.ParNew + Serial Old
5.Parallel Scavenge + Serial old
6.Parallel Scavenge + Parallel Old
GC种类
YGc ( minor GC,年轻代GC)
只收集eden区的GC一
过程
当eden不够放入新创建的对象时
将存活对象放入to区,如果to区放不下,进入老年代
如果能放下,放入to区,清理无用对象。
第二次GC时,扫描eden和to 区,将存货对象放入from区。将 to区清空
如果from区放不下,进入老年代
old GC(老年代GC,只有CMS才会单独回收Old 区 )
通常等同于Full Gc
老年代无法放下YGC晋升的对象
Full GC(又称major Gc )
这个堆和方法区进行GC
System.gc()的调用
heap dump带GC
永久代(方法区)空间不够
当准备触发YGC时,发现之前YGC后晋升对象的大小比目前Old 区的剩余空间大,则不会触发YGC,转而直接触发FGC
晋升
YGC后,幸存的对象会放入到Survivor 区,如果一个对象在多次YGC后仍然存活,则进入老年代,这个过程叫做晋升。每次YGC后,对象年龄加一( Mark Work )
Mixed GC(混合GC,G1收集器独有)
GC常用参数
1. Serial 收集器参数
串行收集器,client 的默认GC,分为年轻代 Serial和老年代Serial Old
1.-XX : +UseSerialGc
指定新生代和老年代都使用SerialGc,+开启,-关闭
2.-XX:+ UseParNewGc
新生代使用ParNew,老年代使用Serial Old
3.-XX:+UseParallelGc
新生代使用Parallel Scavenge Gc,老年代使用串行收集器
日志标记
Serial 收集器出现的日志为DefNew
2.ParNew参数
Serial的多线程版本
1.-XX:+UseParNewGc
新生代使用ParNew,老年代使用Serial
2.-XX:+UseConcMarkSweepGc
新生代使用ParNew ,老年代使用CMS
3.-XX:ParallelGCThreads={value}
指定GC线程数量
公式:默认CPU小于8,线程数等于CPU核心数,大于8∶3+( ( 5*CPU ) /8)
日志标记——
ParNew
3.PS收集器参数
java 8默认垃圾收集器,关注吞吐量,自动调优
1.-XX:MaxGCPauseMillis
最大垃圾收集停顿时间
PS会调整堆大小和其他的一些参数,尽可能的将停顿时间控制在参数范围内
如果停顿值设管的很小,那么堆相应的就会小,吞吐量就会下降
2.-xX:GCTimeRatio
设置吞吐虽大小
0-100的整数,公式为1/(1+n),当n为99,吞吐量为99%,垃圾收集的最大时间为1%
3.-XX:+ UseParallelGc
新生代使用PS处理器,老年代使用Serial Old
4.-XX:+ UseParalleloldGc
新生代PS,老年代 Ps old
5.-XX:UseAdaptiveSizePolicy
打开自适应策略。在这种模式下,新生代的大小, eden和Survivor 的比例,晋升老年代的对象年龄等参数会被自动调整。以达到堆大小,吞吐量,停顿时间的平衡点
注意点∶
1和2是矛盾的,吞吐量和停顿时间就是矛盾的。所以,要根据应用的特性来进行设置,以达到最优水平
线程数量
-XX:ParallelGCThreads
日志标记
PSYoungGen
4.CMS收集器参数
1.-XX:-CMSPrecleaningEnabled
不进行预清理,CMS并发标记和重新标记的这段时间内,会有一个预清理的工作,而这个通常会尝试5秒之内等待来一次YGC,以免在后面remark节点耗费大量时间来标记新生代的对象
2.-XX:+ UseConcMarkSweepGc
开启CMS,默认新生代是 ParNew,也可以设置为Serial。该参数等价于-Xconcgc
3.-XX:ParallelGCThreads
指定线程数
4.-XX:ConcGcThreads
指定线程数
5.-XX:CMSInitatingOccupancyFraction
由于CMS回收器不是独占式的,在垃圾回收的时候应用程序仍在工作,所以需要留出足够的内存给应用程序,否则会触发FGC。而什么时候运行
CMS GC呢 ?通过该参数即可设置,该参数表示的是老年代的内存使用百分比。当达到这个阈值就会执行CMS,默认是68,如果老年代内存增长很快,建议降低阈值,避免FGC,如果增长慢,则可以加大阈值,减少CMS GC次数。提高吞吐量。
6.-XX:+UseCMscompactAtFullCollection
由于CMS使用标记清理算法,内存碎片无法避免。该参数指定每次CMS后进行一次碎片整理。
7.-XX:CMSFullGCsBeforeCompaction
由于每次进行碎片整理将会影响性能,你可以使用该参数设定多少次CMS后才进行一次碎片整理,也就是内存压缩。
8.XX:+CMSClassUnloadingEnabled
允许对类元数据进行回收。
9.-XX:CMSInitiatingPermoccupancyFraction
当永久区占用率达到这一百分比时,启动CMS回收(前提是-XX:+CMSClassUnloadingEnabled激活了)
10.-XX:UseCMSInitiatingOccupancyOnly
表示只在到达阈值的时候才进行CMS回收。
11.-XX:CMSwaitDuration=200o
由于CMS GC条件比较简单,VM有一个线程定时扫描Old区,时间间隔可以通过该参数指定(毫秒单位),默认是2s。
日志格式
CMS
5.G1收集器参数
Java 9的默认垃圾收集器
1.-XX:+ UseG1Gc——开启G1
2.-xx:MaxGCPauseMillis
用于指定最大停顿时间,如果任何一次停顿超过这个设置值时,G1就会尝试调整新生代和老年代的比例,调整堆大小,调整晋升年龄的手段,试图达到目标。和PS一样,停顿时间小了,对应的吞吐量也会变小。这点值得注意。4.-xX:GCPauseIntervalMillis
3.-xX:InitiatingHeapOccupancyPercent
该参数可以指定当整个堆使用率达到多少时,触发并发标记周期的执行。财认值时45,即当堆的使用率达到45%,执行并发标记周期,该值一旦设置,始终都不会被G1修改,也就是说,G1就算为了满足MaxGCPauseMilis也不会修改此值,如果该值设置的很大,导致并发闾期迟迟得不到启动,那么引起FGC的几率将会变大。如果过小,则会频繁标记,GC线程抢占应用程序CPU资源,性能将会下降,
4.-xX:GCPauseIntervalMillis
设置停顿时间间隔
6.通用参致
1 .XC-+Disable&xplicitGc
禁用System.gc() ,由于液方法默认会触发FC,并且忽略参数中的UseG1GC 和UseConcMarkSweepGC,因此必要时可以禁用改方法,
2-XX:+ExplicitGCInwokesConcurrent
该参数可以改变上面的行为,也就是说,System.gc()后不使用FGC,而是使用配置的并发收集闟进行并发收货。注意:使用此选项荔不要使用上面的选项.
3.-XX:-ScavengeBeforeFullGcH
由于大部分FGC之前都会YGC ,经了FCC的压力,纳短了FGC的停顿时间,但也可能你不利要这个特性,那么你可以使用这个参数关闭,默认是ture开启。
、4.-XX:Max TenuringThreshold =fvalue}
新生代 to区的对象在经过多次GC后,如果还没有死亡,则认为他是一个老对象,则可以当升到老年代,而这个年龄(GC次数)是可以设置的,有就是这个参敏。默认值时15。超过15则认为是无限大(囚为Bge变量时4个 bit,超过15无法表达)。但该参数不是唯一决定对象晋升的条件。当to区不够或者改对象年龄已经法到了平均巴升值或者大对象等等条件。
5.-XX:TargetSurvivorRatio=value}
决定对何时晋升的不仅只有XXMaxTenuringThreshold 参数,如果在Survivor-空间中相同年龄所有对象大小的总和大于Survivor空间的一半(默认50%》,年抬大于或等于该年饴的对象就可以益接进入老年代。无就在乎XXMBx TenuringThreshold参数。因此, Ma*TenuringThreshold 只是对象普升的最大年龄。如果将TargetSurvivorRatio没查的很小,对象将巴升的很快。
6.-XX.TargetsurvivorRatio={value]
除了年歇外,对象的体积也是影响誉升的一个关键,也就是大对象。如果一个对象新生代放不下,只能直接通过分配担保机制进入老年代。该参数是没置对象直接晋升到老年代的觉值,单位是字节,只要对象的大小大于此战值,就会吉接烧过新生代,直接进入老年代。意∶这个参数只对5erial和ParNew有效,ParallelGC无效,默认情况下该值为0,也就是不指定最大的普升大小,一切有运行情况决定。"
7.-XX:-UseTLAB
禁用线程本地分配缓存。TLAB的全称是Thread LocalAllocation Buffer,即线程本地线程分配缓存,是一个线粒私有的内存区域。该设计是为了加讨i这对象分代运度。由于对象一份都是分配在件上,而对是线程共享的。囚此肯定有锁,虽然使用CAS的操作,但性能仍有优化空间。通过为每一个线程分配一个TLAB的空问(在eden 区),可以消除多个线程同步的开销。狱认开启。
TLAB过程
8. -XX;TLABSize H
指定LAB的大小。
9._-xX:+PrintTLAB
跟踪TLAB的使用情况。用以确定是用多大的TLABSize。
10.-XX:+ReslizeTLAB
自动调整TLAB大小。
7.GC日志参数
-XX:+ PrintGCDateStamps
打印GC日志时间戳。
-XX:+PrintGCDetails
打印GC详情。
-XX:+ PrintGCTimeStamps
打印此次垃圾回收距离jvm开始运行的所耗时间。
-Xloggc:<filename>
将垃圾回收信息输出到指定文件
-verbose:gc-
打印GC日志
-XX:+PrintGCApplicationStopedTime
查看gc造成的应用暂停时间
XX:+PrintTenuringDistribution
对象晋升的日志
-XX:+HeapDumpOnOutOfMemoryError
内存溢出时输出dump文件。
GC问题排查
工具
Jvisualvm
Jprofile
MAT
jinfo
jmap
注意:可能触发FGC
jcmd
jps
jstat
经验
YGC优化
频率
如果YGC超过5秒一次,甚至更长,说明系统内存过大,应该缩小容量,如果频率很高,说明Eden区过小,可以将Eden区增大,但整个新生代的容量应该在堆的30%-40%之间,eden , from和to的比例应该在8∶1∶1左右,这个比例可根据对象晋升的大小进行调整。
时长
YGC有3个过程,一个是扫描,一个是复制,再就是清除,通常扫描速度很快,复制速度相比而言要慢一些,如果每次都有大量对象要复制,就会将STW时间延长,还有一个情况就是StringTable,这个数据结构中存储着String.intern方法返回的常连池的引用,YGC每次都会扫描这个数据结构( HashTable ),如果这个数据结构很大,且没有经过FGC,那么也会拉长STW时长,还有一种情况就是操作系统的虚拟内存,当GC时正巧操作系统正在交换内存,也会拉长STW时长。SystemDictionary主要记录了加载的类,也有可能是这里导致变慢
FGC无法优化时长,只能优化频率
原因
1是Old区内存不够
2是元数据区内存不够
3是System.gc()
4是jmap或者jcmd
5是CMS Promotion failed 或者concurrent mode failure
6 JVM基于悲观策略认为这次YGC后Old 区无法容纳晋升的对象,因此取消YGC,提前FGC。
优化点
old区内存不够导致FGC。如果FGC后还有大量对象,说明Old 区过小,应该扩大Old 区,如果FGC后效果很好,说明Old区存在了大量短命的对象,优化的点应该是让这些对象在新生代就被YGC掉,通常的做法是增大新生代,如果有大而短命的对象,通过参数设置对象的大小,不要让这些对象进入Old区,还需要检查晋升年龄是否过小。如果YGC后,有大量对象因为无法进入Survivor区从而提前晋升,这时应该增大Survivor 区,但不宜太大。
一定带上GC日志
并发
并发基础
1. 并发编程的优缺点
1. 为什么要用到并发(优点)
充分利用多核CPU的运算能力
方便业务拆分
2.并发编程的缺点
频繁的上下文切换
线程安全(常见的避免死锁的方式)
3. 易混淆的概念
同步 VS 异步
并发 VS 并行
阻塞 VS 非阻塞
临界区 VS 临界资源
2. 线程的状态和基本操作
1. 如何新建线程
继承Thread类
实现Runnable接口
实现Callable或Runnable接口,FutureTask
2. 线程状态的转换
NEW
RUNNABLE
WAITING
TIMED_WAITING
TERMINATED
BLOCKED
3. 线程的基本操作
interrupt
抛出InterruptedException异常时,会清除中断标志位
interrupt(), interrupted(), isInterrupt()
sleep
sleep与wait的区别
join
等待多个线程都执行完再继续执行
yield
暂停当前线程,执行其他同优先级的线程,而sleep没有这个要求
4. 守护线程Daemon
并发理论(JMM)
1.JMM内存模型
1.哪些是共享数据
实例域
静态域
数组
2.抽象结构
线程将数据拷贝到工作内存,再刷新到主存。各个线程通过主存中的数据来完成隐式通信
2. 重排序
1.什么是重排序
为了提高执行性能,编译器和处理器会对指令进行重排序
针对编译器重排序,编译器重排序规则会禁止一些特定类型的编译器重排序
针对处理器重排序,编译器会在生成指令的时候插入内存屏障来禁止特定类型的处理器重排序
2. 数据依赖性
3. as-if-serial
遵守as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的
3.happens-before规则
1. 定义
如果A happens-before B,则A操作的结果对操作B可见,且A操作在操作B之前执行
如果指令重排序之后的结果,与按照happens-before关系执行的结果一致,则指令可以重排序
2.理解
站在程序员角度:为编程人员提供了一个类似强内存的内存结构,方便编程
站在编译器和处理器厂商:在不影响正确结果的前提下,可以让编译器和处理器厂商尽情优化
3.具体规则
程序顺序规则
volatile变量规则
监视器锁规则
传递性
start规则
join规则
线程中断规则
对象finnalize规则
并发关键字
1.synchronized
1. 如何使用?
实例方法(锁的是实例对象)
静态方法(锁的是类对象)
代码块根据配置,锁的是实例对象也可以是类对象
2. moniter机制
字节码中会添加monitorenter和monitorexit指令
锁的重入性:同一个锁程,不需要再次申请获取锁
3. synchronized的happens-before关系
4. synchronized的内存语义
共享变量会刷新到主存中,线程每次会从主存中读取最新的值到自身的工作内存中
5.锁优化
锁状态
无锁状态
偏向锁
轻量级锁
重量级锁
CAS操作
是一种乐观锁策略
利用现代处理器的CMPXCHG
存在ABA的问题;自旋时间可能过长的问题
JAVA对象头
对象的hashcode
对象的分代年龄
是否是偏向锁的标志位
锁标志位
6. 锁升级策略
轻量级锁
加锁:在对象头和栈帧的锁记录中,添加自身的线程ID
锁撤销:在全局安全点上进行
轻量级锁
加锁:Displace mark word,对象头mark word通过CAS指向栈中锁记录
锁撤销:如果CAS替换回对象头失败,则升级成重量级锁
重量级锁
各种锁的比较
2. volatile
1.实现原理
写volatile变量在编译时添加Lock指令
缓存一致性:每个处理器会通过总线嗅探出自己的工作内存中数据是否发生变化(MESI缓存一致性协议 )
2.happens-before关系的推导
3.内存语义
写volatile变量会重新刷新到主存中,其他线程读volatile变量会重新从主存中读取最新值
4.内存语义的实现
通过在特定位置处插入内存屏障来防止重排序
3.final
1.如何使用
变量
基本类型
类变量(static变量):只能在声明时赋值或者在静态代码块中赋值
实例变量:声明时赋值,构造器以及非静态代码块中赋值
局部变量:有且仅有一次赋值机会
引用类型
final修饰的引用类型只保证引用的对象地址不变,其对象的属性是可以改变的
方法
被final修饰的方法不能被子类重写,但是可以被重载
类
被final修饰的类,不能被子类继承
2. final的重排序规则
final域为基本类型
禁止对final于的写重排序到构造函数之外
禁止读对象的引用和读该对象包含的final域重排序
final域为引用类型
对一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量,这两个操作是不能被重排序的
3. final实现原理
插入StoreStore和LoadLoad内存屏障
4. finla引用不能从构造函数函数中“溢出”(this逃逸)
4.三大性质
原子性
synchronzied
可见性
synchronized
volatile
有序性
synchronized
volatile
Lock体系
1. Lock与synchronized的比较
Lock提供了基于API的可操作性,提供能可响应中断式获取锁,超时获取锁以及非阻塞式获取锁的特性
synchronized执行完同步块以及遇到异常会自动释放锁,而Lock要显示的调用unlock方法释放锁
2. AQS
1. 设计意图(模板方法设计模式)
AQS提供给同步组件实现者,为其屏蔽了同步状态的管理,线程排队等底层操作实现者只需要通过AQS提供的模板方法实现同步组件的语义即可
lock(同步组件)是面向使用者的,定义了接口,隐藏了实现细节
2. 如何使用AQS实现自定义同步组件
重写protected方法,告诉AQS如何判断当前同步状态获取是否成功或者失败
同步组件调用AQS的模板方法,实现同步语义。而提供的模板方法又会调用被重写的方法
实现自定义同步组件时,推荐采用继承AQS的静态内部类
3. 可重写的方法
4. AQS提供的模板方法
3. AQS源码解析
1. AQS同步队列的数据结构
带头结点的双向链表实现的队列
2. 独占式锁
同步状态获取成功则退出;失败则通过addWaiter方法将当前线程封装成节点加入同步队列,acquireQueued方法使得当前线程等待获取同步状态
如果获取同步状态并且是同步队列中的头结点,则表明获取锁成功,并唤醒后继结点
可响应中断式获取锁以及超时获取锁特性的实现原理
3.共享式锁
锁获取原理
锁释放原理
可响应中断式获取锁以及可超时获取锁特性的实现原理
4.ReentrantLock
1.重入锁的实现原理
2.公平锁的实现原理
3.非公平锁的实现原理
4.公平锁和非公平锁的比较
5.ReentrantReadWriteLock
1.如何表示读写状态的
低16位用来表示写状态,高16位用来表示读状态
2.WriteLock的获取和释放
当ReadLock已经被其他线程获取或者WriteLock被其他线程获取,当前线程获取WriteLock失败;否则获取成功。支持重入性
WriteLock释放时将写状态通过CAS操作减一
3.ReadLock的获取和释放
当WriteLock已经被其他线程获取的话,ReadLock获取失败;否则获取成功。支持重入性
通过CAS操作将读状态减一
4.锁降级策略
按照WriteLock.lock()-->ReadLock.lock()-->WriteLock.unlock()的顺序,WriteLock会降级为ReadLock
5.生成Condition等待队列
WriteLock可以通过newCondition方法生成Condition等待队列,而ReadLock无法生成Conditon等待队列
6.应用场景
适用于读多写少的应用场景,比如缓存设计上
6. Condition机制
1.与Object的wait/notify机制相比具有的特性
Condition能够支持不响应中断,而Object不支持
Lock能够支持多个Condition等待队列,而Object只能支持一个
Condition能够支持设置超时时间的await,而Object不能
2.与Object的wait/notify相对应的方法
针对Object的wait方法:await, awaitNanos,...
针对Object的notify/notifyAll方法:signal,signalAll方法
3.底层数据结构
复用AQS的Node类,由不带头结点的链表实现的队列
4. awiat实现原理
将调用await方法的线程封装成Node,尾插入到同步队列中,并通过LockSupport.park方法将当前线程置于WAITING状态,直至其他线程通过signal/signalAll方法将其移入到同步队列中,使其有机会在同步队列中通过自旋获取到Lock,从而当前线程才能从await方法处退出
5.signal/signalAll实现原理
将等待队列的队头结点移入到同步队列中
6. await和signal/signalAll的结合使用
7.LockSupport
1. 主要功能
可阻塞线程以及唤醒线程,功能实现依赖于Unsafe类
2. 与synchronized阻塞唤醒相比具有的特色
LockSupport通过LockSupport.unpark(thread)可以指定哪个线程被唤醒,而synchronized不能
并发容器
1. concurrentHashMap
1. 关键属性
table:元素为Node类的哈希桶数组
nextTable:扩容时的新数组
sizeCtl:控制数组的大小
Unsafe u:提供对哈希桶数组元素的CAS操作
2. 重要内部类
Node:实现Map.entry接口,存放key,value
TreeNode:继承Node,会被封装成TreeBin
TreeBin:进一步封装TreeNode,链表过长时转换成红黑树时使用
ForwardingNode:扩容时出现的特殊结点
3. 涉及到的CAS操作
tabAt:查询哈希桶数组的元素
casTabAt:设置哈希桶数组中索引为i的元素
setTabAt:设置哈希桶数组中索引为i的元素
4. 构造方法
数组长度总是会保证为2的幂次方
5. put执行流程
1.如果当前数组还未初始化,先进行初始化
2.spread方法重哈希(高16位和低16位异或操作),将哈希值与数组长度与运算,确定待插入结点的索引为i
3.当前哈希桶中i处为null,直接插入
4. i处结点不为null的话并且结点hash>0,说明i处为链表头结点。遍历链表,遇到与key相同的结点则覆盖其value,如果遍历完没有找到,则尾插入新结点
5. i处结点不为null的话并且结点状态为MOVED,则说明在扩容,帮助扩容
6.i处结点不为null的话并且结点位TreeBin,则使用红黑树的方式插入结点
7. 插入新结点后,检查链表长度是否大于8,若大于,则转换成红黑树
8. 检测数组长度,若超过临界值,则扩容
6. get执行流程
7. 扩容机制
8. 用于统计size的方法的执行流程
9. 1.8版本的ConcurrentHashMap与之前版本的比较
减小锁粒度
采用了synchronized而不是lock,大量使用CAS操作
2. CopyOnWriteArrayList
1. 实现原理
利用了读写分离的思想;当写线程写入数据的时候会复制新建一个新容器,当数据更新完成后,再将旧容器引用指向新容器。读线程感知数据更新是延时的,也就是说COW是牺牲了数据实时性而保证数据最终一致性
由于写线程写数据是在新容器写入的,因此读线程不会被阻塞
2. COW和ReentrantReadWriteLock的区别
相同点
1. 两者都采用了读写分离的思想,并且读和读线程之间都不会被阻塞
不同点
1. 当写线程在写数据时,ReadWriteLock会阻塞读线程,而由于COW采用了延时更新的策略,COW并不会阻塞读线程
ReadWriteLock保证了数据实时性而COW保证数据最终一致性
3. 应用场景
适用于读多写少的场景,比如系统的黑名单,白名单设置
4. 为什么具有弱一致性
COW的实现是采用数组,而数组的引用是volatile修饰,但是数组的元素并不是volatile的。因此数据更新只有当volatile引用指向新数组时才会生效
5. COW的缺点
由于在写数据时,会复制,因此可能会出现内存使用瞬间增加,导致minor GC和major GC
只具有数据最终一致性,对数据实时性要求高的场景不合适
3. ThreadLocal
1. 实现思想
采用“空间换时间的”思想,每个线程拥有变量副本,达到隔离线程的目的,线程间不受影响解决线程安全的问题
操作系统
进程,线程,协程
定义
进程:是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的基本单位。每个进程都有自己独立的内存空间,由于进程比较重量,占据独立的内存,所以上下文进程间切换开销比较大
线程:线程是进程的一个实体,是CPU调度和分派的基本单位,是比进程更小的能独立运行的基本单位线程自己基本不拥有系统资源,只拥有一点在运行中比不可少的资源(如程序计数器,寄存器和栈)
协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器和栈。协程调度切换时,将寄存器和栈保存在其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,基本没有内核切换的开销,可以不加锁,所以上下文切换非常快
比较
进程和线程的比较:
1. 进程是系统资源分派和调度的基本单位,而线程是CPU调度和分派的基本单位;
2. 进程拥有自己独立的资源,而线程是进程的一个实体,共享进程的资源,自己只拥有极少的资源;
线程和协程的区别:
协程是用户态轻量级线程,能由用户控制切换,切换开销较小,而线程的切换不能由用户控制并且切换开销较大
2. set方法原理
数据存放在由当前线程Thread维护的ThreadLocalMap中,数据结构为当前ThreadLocal实例为key,值为value的键值对
3. get方法原理
以当前ThreadLocal为键,从当前线程Thread维护的ThreadLocalMap中获取value
4. remove方法原理
从当前线程Thread维护的ThreadLocalMap中删除以当前ThreadLocal实例为键的键值对
5. ThreadLocalMap
底层数据结构
键为ThreadLocal实例,值为value的Entry数组
数组大小为2的幂次方
键ThreadLocal为弱引用
set方法原理
1. 计算ThreadLocal的hashcode
总是加上0x61c88647,这是“Fibonacci Hashing”
2. 计算待插入的索引为i
采用与运算
3.如何解决hash冲突
当索引为i处有Entry的话(hash冲突),就采用线性探测,进行环形搜素
4. 加载因子
ThreadLocalMap初始大小为16,加载因子为2/3
5. 扩容resize
容量为原数组大小的两倍
getEntry方法原理
根据ThreadLocal的hashcode进行定位,如果所定位的Entry的key与所查找的key相同则直接返回,否则,环形向后继续探测。
remove原理
先找到对应的entry,然后让它的Key为null,之后再对其进行清理
6. ThreadLocal内存泄漏
造成内存泄露的原因
由于ThreadLocal在Entry中是弱引用,当外部ThreadLocal实例被置为null后,根据可达性分析,堆中ThreadLocal不可达,会被GC掉,因此就存在key为null的entry。无法通过key为null去访问entry,因此,就会存在threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory引用链造成valueMemory不会被GC掉,造成内存泄漏
怎样来解决内存泄漏
关键方法cleanSomeSlots,expungeStaleEntry,replaceStaleEntry
在ThreadLocal的set,getEntry以及remove方法中都利用以上三个关键方法对潜在的内存泄漏进行处理
为什么要使用弱引用
如果使用强引用的话,即使显示对ThreadLocal的实例置为null的话,由于Thread,ThreadLocal以及ThreadLocalMap引用链关系,ThreadLocal也不会被GC掉,反而会程序员带来困扰;
使用弱引用,尽管存在ThreadLocal内存泄漏的危险,但实际上已经对其进行了处理
7. ThreadLocal的最佳实践
使用完ThreadLocal后要remove掉
8. 应用场景
hibernate管理seesion,每个线程维护其自身的session,彼此不干扰
用于解决对象不能被多个线程共享的问题
4.BlockingQueue
1.BlockingQueue的基本操作
2. 常用的BlockingQueue
ArrayBlockingQueue:由数组实现的有界阻塞队列
LinkedBlockingQueue:由链表实现的有界阻塞队列,可指定长度,如果没有指定则为Integer.MAX_VALUE
PriorityBlockingQueue:支持优先级的无界阻塞队列
SynchronousQueue:不存储任何元素阻塞队列
LinkedTransferQueue:由链表实现的无界阻塞队列
LinkedBlockingDeque:由链表实现的无界阻塞队列
DelayQueue:存在实现了Delayed接口的数据的无界阻塞队列
3. ArrayBlockingQueue与LinkedBlockingQueue
实现原理
会有notFull和notEmpty两个等待队列,分别存放被阻塞的插入数据线程以及被阻塞的消费数据的线程
ArrayBlockingQueue只有一个lock,而LinkedBlockingQueue有两个lock,因此LinkedBlockingQueue的并发度更高,吞吐量更大
通过put和take方法了解生产者-消费者的正确写法
5.ConcurrentLinkedQueue
1. 实现原理
主要采用CAS操作以保证线程安全,并且采用了延时更新的策略,提高吞吐量
2. 数据结构
由Node构成的链式队列
3. 核心方法
offer方法实现原理
poll方法实现原理
4. HOPS延迟更新的设计意图
线程池(Executor体系)
1. ThreadPoolExecutor
1. 为什么要使用线程池
降低资源损耗
提升系统响应速度
提高线程的可管理性
2. 执行流程
核心线程corePool,阻塞队列workQueue以及最大线程池maxPool三级缓存的工作方式
3. 构造器各个参数的意义
coolPoolSize:核心线程池的大小
maximumPoolSize:线程池最大容量
keepAliveTime:空闲线程可存活时间
unit:keepAliveTime的时间单位
workQueue:存放任务的阻塞队列
threadFactory:生产线程的工厂类
handler:饱和丢弃策略。共四种:
AbortPolicy,CallerRunsPolicy,DiscardPolicy,DiscardOldestPolicy
AbortPolicy,CallerRunsPolicy,DiscardPolicy,DiscardOldestPolicy
4. 如何关闭线程池
shutdown:正在执行任务的线程,将任务执行完。空闲线程以中断的方式关闭
shutdownNow:停止所有线程,包括正在执行任务的线程。返回未执行的任务列表
showdown将线程池状态设置为SHUTDOWN,而shutdownNow将线程池状态设置为STOP
isTerminated来检查线程池是否已经关闭
5. 如何配置线程池
CPU密集型:Ncpu+1
IO密集型:2Ncpu
任务按照IO密集型和CPU密集型进行拆分
2. ScheduledThreadPoolExecutor
1. UML(类结构)
继承了ThreadPoolExecutor,并实现了ScheduledExecutorService
2. 常用方法
可 延时执行任务:schedule(.....)
可周期执行任务:scheduledAtFixedRate(...)和scheduledWithFixedDelay
scheduledAtFixedRate和scheduledWithFixedDelay的区别:...AtFixedRate不要求任务结束了才开始统计延时时间,而....WithFixedDelay要求从任务结束开始统计延时时间
3. ScheduledFutureTask
可周期执行的异步任务,每一次执行完后会重新设置任务下一次执行的任务,并且会添加到阻塞队列中
4. DelayedWorkQueue
按优先级排序的有界阻塞队列,底层数据结构是堆
3. FutureTask
1. FutureTask的几种状态
未启动(还未执行run方法),已启动(已执行run方法),已结束(正常结束,被取消,出现异常)
2. get()
未启动和已启动状态,get方法会阻塞当前线程直到异步任务执行结束
3. cancel()
未启动状态时,调用cancel方法后该异步任务永远不会再执行
已启动状态,调用cancel方法后根据参数是否中断当前执行任务的线程
已结束状态,调用cancel方法时会返回false
4. 应用场景
当一个线程需要等到另一个任务执行结束后才能继续进行时,可以使用futureTask
5. 实现了Runnable接口
futureTask同样可以交由executor执行,获取直接调用run方法执行
原子操作类
1. 实现原理
借住与Unsafe类的CAS操作,达到并发安全的目的
2. 原子更新基本类型
AtomicInteger, AtomicLong,AtomicBoolean
3. 原子更新数组类型
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
4. 原子更新引用类型
AtomicReference,AtomicReferenceFieldUpdater,AtomicMarkableReference
5. 原子更新字段类型
AtomicIntegerFieldUpdater,AtomicLongUpdater,AtomicStampedReference
JUC工具
1. 倒计时器CountDownLatch
当CountDownLatch维护的计数器减至为0的时候,调用await方法的线程才会继续往下执行,否则会阻塞等待
适用于一个线程需要等待其他多个线程执行结果的应用场景
2. 循环栅栏CyclicBarrier
当一组线程都达到了“临界点”时,所有的线程才能继续往前执行,否则阻塞等待
3. CountDownLatch和CyclicBarrier的比较
1. CyclicBarrier能够复用,而CountDownLatch维护的倒计数器不能复用
2. CyclicBarrier会在await处阻塞等待,而CountDownLatch在await出不会阻塞等待
3. CyclicBarrier提供了例如isBroken,getNumerWaiting等方法能够查询当前状态,而CountDownLatch提供的方法较少
4. 资源访问控制Semaphore
适用于对特定资源需要控制能够并发访问资源的线程个数。需要先执行acquire方法获取许可证,如果获取成功后线程才能往下继续执行,否则只能阻塞等待;使用完后需要用release方法归还许可
5. 数据交换Exchanger
为两个线程提供了一个同步点,当两个线程都达到了同步点之后就可以使用exchange方法,互相交换数据;如果一个线程先达到了同步点,会在同步点阻塞等待直到另外一个线程也到达同步点
6.CompletableFuture组合式异步编程
比较耗时的操作可以通过多线程+异步的方式提高效率:CompletableFuture类(实现了Future接口)
CompletableFuture的异常管理:completeExceptionally(Exception)来将异常返回
工厂方法supplyAsync创建CompletableFuture:接受一个生产者(Supplier),返回一个CompletableFuture对象
等待所有的异步操作结束:CompletableFuture::join
异步操作和同步操作:CompletableFuture.thenApply;
两个依赖异步操作的流水线:CompletableFuture.thenCompose;
两个非依赖异步操作的流水线:CompletableFuture.thenCombine;
你可以决定什么时候结束程序的运行,等待由CompletableFuture对象构成的列表中所有的对象都执行完毕(allOf().jion()),或者其中任何一个首先完成就中止(anyOf())
并发实践
生产者-消费者问题
1.使用Object的wait/notifyAll方式实现
使用Object的消息通知机制可能存在的问题
notify过早,wait线程无法再获取到通知以至于一直阻塞等待。解决办法:添加状态标志
wait条件变化。解决方法:使用while进行wait条件的判断,而不是在if中进行判断
“假死”状态:使用notifyAll而不是notify
标准范式
永远在while中对wait条件进行判断,而不是在if中进行判断
使用notifyAll进行通知,而不要使用notify进行通知
2.使用lock的condition的await/signalAll方式实现
3. 使用blockingQueue方式实现
由于BlockingQueue有可阻塞的插入和删除数据的put和take方法,因此,在实现上比使用Object和lock的方式更加简洁
数据结构/设计模式
数据结构
含义
什么是数据结构
非数值型程序设计中数据的组织方式及其处理的算法
4种基本逻辑结构
集合
数据元素除了“属于同一集合”的关系外,没有其他关系。
线性结构
数据元素之间存在一对一的关系。如:线性表,栈,队列
层次结构
数据元素之间存在一对多的关系。如:树
网状结构
数据元素之间存在若干个多对多关系。如:图
4种基本存储结构:
顺序存储
将数据结构中各元素按照其逻辑顺序存放于一片连续的存储空间中。如C语言的一维数组。
优点:随机访问快(可以直接计算数据的地址)
缺点:插入、删除效率低,不利于动态增长
链式存储
数据结构中各元素可以存放到存储器的不同地方,数据元素之间以指针(地址)链接。
缺点:随机访问慢
优点:便于插入、删除和动态增长
索引存储
在存储数据的同时,建立一个附加的索引表,即索引存储结构 = 数据 + 索引表。索引表存放各数据元素的地址
索引表的组织方式:
顺序存储:索引表占用连续空间
链式存储
索引存储:多级索引(多重索引)
例如,二级索引:将一个大文件的所有索引表(二级索引)的地址放在另一个索引表(一级索引)中
此外,还有三级索引等。
散列存储
根据数据元素的特殊字段(称为关键字key),计算数据元素的存放地址,然后数据元素按地址存放,所得到的存储结构为散列存储结构(或Hash结构)
数组
类型相同的数据
通过下标访问,顺序存储
内存连续性
缺点
大小固定
写入需要移动元素,效率慢
链表
单链表
双向链表
循环链表
跳表
特性
(1). 跳跃表的每一层都是一条有序的链表.
(2). 跳跃表的查找次数近似于层数,时间复杂度为O(logn),插入、删除也为 O(logn)。
(3). 最底层的链表包含所有元素。
(4). 跳跃表是一种随机化的数据结构(通过抛硬币来决定层数)。
(5). 跳跃表的空间复杂度为 O(n)。
查找
由于元素的有序的,我们是可以通过增加一些路径来加快查找速度的。例如
通过这种方法,我们只需要遍历5次就可以找到元素9了(红色的线为查找路径)。
还能继续加快查找速度吗?
答是可以的,再增加一层就行了,这样只需要4次就能找到了,这就如同我们搭地铁的时候,去某个站点时,有快线和慢线几种路线,通过快线 + 慢线的搭配,我们可以更快着到达某个站点。
答是可以的,再增加一层就行了,这样只需要4次就能找到了,这就如同我们搭地铁的时候,去某个站点时,有快线和慢线几种路线,通过快线 + 慢线的搭配,我们可以更快着到达某个站点。
插入
例如,我们要插入结点 3,4,通过抛硬币知道3,4跨越的层数分别为 0,2 (层数从0开始算),则插入的过程如下:
插入 3,跨越0层。
插入 4,跨越2层。
删除
解决了插入之后,我们来看看删除,删除就比较简单了,例如我们要删除4,那我们直接把4及其所跨越的层数删除就行了。
树
无序树:树中任意节点的子结点之间没有顺序关系,这种树称为无序树,也称为自由树;
有序树:树中任意节点的子结点之间有顺序关系,这种树称为有序树;
二叉树:每个节点最多含有两个子树的树称为二叉树;
满二叉树:叶节点除外的所有节点均含有两个子树的树被称为满二叉树;
完全二叉树:除最后一层外,所有层都是满节点,且最后一层缺右边连续节点的二叉树称为完全二叉树;
哈夫曼树(最优二叉树):带权路径最短的二叉树称为哈夫曼树或最优二叉树。
栈
先进后出
队列
先进先出
堆
堆通常是一个可以被看做一棵树的数组对象。堆的具体实现一般不通过指针域,而是通过构建一个一维数组与二叉树的父子结点进行对应,因此堆总是一颗完全二叉树。
hash散列表
图
设计模式
六大原则
1、开闭原则(Open Close Principle)
开闭原则的意思是:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。
2、里氏代换原则(Liskov Substitution Principle)
里氏代换原则是面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。LSP 是继承复用的基石,只有当派生类可以替换掉基类,且软件单位的功能不受到影响时,基类才能真正被复用,而派生类也能够在基类的基础上增加新的行为。里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化,而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
3、依赖倒转原则(Dependence Inversion Principle)
这个原则是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。
4、接口隔离原则(Interface Segregation Principle)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。它还有另外一个意思是:降低类之间的耦合度。由此可见,其实设计模式就是从大型软件架构出发、便于升级和维护的软件设计思想,它强调降低依赖,降低耦合。
5、迪米特法则,又称最少知道原则(Demeter Principle)
最少知道原则是指:一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
6、合成复用原则(Composite Reuse Principle)
合成复用原则是指:尽量使用合成/聚合的方式,而不是使用继承。
创造型
工厂模式(Factory Pattern)
意图:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
主要解决:主要解决接口选择的问题。
何时使用:我们明确地计划不同条件下创建不同实例时。
如何解决:让其子类实现工厂接口,返回的也是一个抽象的产品。
关键代码:创建过程在其子类执行。
应用实例:
1、您需要一辆汽车,可以直接从工厂里面提货,而不用去管这辆汽车是怎么做出来的,以及这个汽车里面的具体实现。
2、Hibernate 换数据库只需换方言和驱动就可以。
优点:
1、一个调用者想创建一个对象,只要知道其名称就可以了。
2、扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
3、屏蔽产品的具体实现,调用者只关心产品的接口。
缺点:每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
使用场景:
1、日志记录器:记录可能记录到本地硬盘、系统事件、远程服务器等,用户可以选择记录日志到什么地方。
2、数据库访问,当用户不知道最后系统采用哪一类数据库,以及数据库可能有变化时。
3、设计一个连接服务器的框架,需要三个协议,"POP3"、"IMAP"、"HTTP",可以把这三个作为产品类,共同实现一个接口。
注意事项:作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。
抽象工厂模式(Abstract Factory Pattern)
意图:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。
主要解决:主要解决接口选择的问题。
何时使用:系统的产品有多于一个的产品族,而系统只消费其中某一族的产品。
如何解决:在一个产品族里面,定义多个产品。
关键代码:在一个工厂里聚合多个同类产品。
应用实例:工作了,为了参加一些聚会,肯定有两套或多套衣服吧,比如说有商务装(成套,一系列具体产品)、时尚装(成套,一系列具体产品),甚至对于一个家庭来说,可能有商务女装、商务男装、时尚女装、时尚男装,这些也都是成套的,即一系列具体产品。假设一种情况(现实中是不存在的,要不然,没法进入共产主义了,但有利于说明抽象工厂模式),在您的家中,某一个衣柜(具体工厂)只能存放某一种这样的衣服(成套,一系列具体产品),每次拿这种成套的衣服时也自然要从这个衣柜中取出了。用 OO 的思想去理解,所有的衣柜(具体工厂)都是衣柜类的(抽象工厂)某一个,而每一件成套的衣服又包括具体的上衣(某一具体产品),裤子(某一具体产品),这些具体的上衣其实也都是上衣(抽象产品),具体的裤子也都是裤子(另一个抽象产品)。
优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
缺点:产品族扩展非常困难,要增加一个系列的某一产品,既要在抽象的 Creator 里加代码,又要在具体的里面加代码。
使用场景:
1、QQ 换皮肤,一整套一起换。
2、生成不同操作系统的程序。
注意事项:产品族难扩展,产品等级易扩展。
单例模式(Singleton Pattern)
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
应用实例:
1、一个党只能有一个书记。
2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。
优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
2、避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
使用场景:
1、要求生产唯一序列号。
2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。
注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。
建造者模式(Builder Pattern)
意图:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。
主要解决:主要解决在软件系统中,有时候面临着"一个复杂对象"的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
何时使用:一些基本部件不会变,而其组合经常变化的时候。
如何解决:将变与不变分离开。
关键代码:建造者:创建和提供实例,导演:管理建造出来的实例的依赖关系。
应用实例:
1、去肯德基,汉堡、可乐、薯条、炸鸡翅等是不变的,而其组合是经常变化的,生成出所谓的"套餐"。
2、JAVA 中的 StringBuilder。
优点:
1、建造者独立,易扩展。
2、便于控制细节风险。
缺点:
1、产品必须有共同点,范围有限制。
2、如内部变化复杂,会有很多的建造类。
使用场景:
1、需要生成的对象具有复杂的内部结构。
2、需要生成的对象内部属性本身相互依赖。
注意事项:与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。
原型模式(Prototype Pattern)
意图:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
主要解决:在运行期建立和删除原型。
何时使用:
1、当一个系统应该独立于它的产品创建,构成和表示时。
2、当要实例化的类是在运行时刻指定时,例如,通过动态装载。
3、为了避免创建一个与产品类层次平行的工厂类层次时。
4、当一个类的实例只能有几个不同状态组合中的一种时。建立相应数目的原型并克隆它们可能比每次用合适的状态手工实例化该类更方便一些。
如何解决:利用已有的一个原型对象,快速地生成和原型对象一样的实例。
关键代码:
1、实现克隆操作,在 JAVA 继承 Cloneable,重写 clone(),在 .NET 中可以使用 Object 类的 MemberwiseClone() 方法来实现对象的浅拷贝或通过序列化的方式来实现深拷贝。
2、原型模式同样用于隔离类对象的使用者和具体类型(易变类)之间的耦合关系,它同样要求这些"易变类"拥有稳定的接口。
应用实例:
1、细胞分裂。
2、JAVA 中的 Object clone() 方法。
优点:
1、性能提高。
2、逃避构造函数的约束。
缺点:
1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
2、必须实现 Cloneable 接口。
使用场景:
1、资源优化场景。
2、类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
3、性能和安全要求的场景。
4、通过 new 产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
5、一个对象多个修改者的场景。
6、一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。
7、在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone 的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,大家可以随手拿来使用。
注意事项:与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable,重写,深拷贝是通过实现 Serializable 读取二进制流。
结构型
适配器模式(Adapter Pattern)
意图:将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。
主要解决:主要解决在软件系统中,常常要将一些"现存的对象"放到新的环境中,而新环境要求的接口是现对象不能满足的。
何时使用:
1、系统需要使用现有的类,而此类的接口不符合系统的需要。
2、想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。
3、通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。)
如何解决:继承或依赖(推荐)。
关键代码:适配器继承或依赖已有的对象,实现想要的目标接口。
应用实例:
1、美国电器 110V,中国 220V,就要有一个适配器将 110V 转化为 220V。
2、JAVA JDK 1.1 提供了 Enumeration 接口,而在 1.2 中提供了 Iterator 接口,想要使用 1.2 的 JDK,则要将以前系统的 Enumeration 接口转化为 Iterator 接口,这时就需要适配器模式。
3、在 LINUX 上运行 WINDOWS 程序。
4、JAVA 中的 jdbc。
优点:
1、可以让任何两个没有关联的类一起运行。
2、提高了类的复用。
3、增加了类的透明度。
4、灵活性好。
缺点:
1、过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器,而是直接对系统进行重构。
2.由于 JAVA 至多继承一个类,所以至多只能适配一个适配者类,而且目标类必须是抽象类。
使用场景:有动机地修改一个正常运行的系统的接口,这时应该考虑使用适配器模式。
注意事项:适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。
桥接模式(Bridge Pattern)
意图:将抽象部分与实现部分分离,使它们都可以独立的变化。
主要解决:在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。
何时使用:实现系统可能有多个角度分类,每一种角度都可能变化。
如何解决:把这种多角度分类分离出来,让它们独立变化,减少它们之间耦合。
关键代码:抽象类依赖实现类。
应用实例:
1、猪八戒从天蓬元帅转世投胎到猪,转世投胎的机制将尘世划分为两个等级,即:灵魂和肉体,前者相当于抽象化,后者相当于实现化。生灵通过功能的委派,调用肉体对象的功能,使得生灵可以动态地选择。
2、墙上的开关,可以看到的开关是抽象的,不用管里面具体怎么实现的。
优点:
1、抽象和实现的分离。
2、优秀的扩展能力。
3、实现细节对客户透明。
缺点:桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
使用场景:
1、如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
2、对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
3、一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
注意事项:对于两个独立变化的维度,使用桥接模式再适合不过了。
过滤器模式(Filter、Criteria Pattern)
组合模式(Composite Pattern)
意图:将对象组合成树形结构以表示"部分-整体"的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
主要解决:它在我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。
何时使用:
1、您想表示对象的部分-整体层次结构(树形结构)。
2、您希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
如何解决:树枝和叶子实现统一接口,树枝内部组合该接口。
关键代码:树枝内部组合该接口,并且含有内部属性 List,里面放 Component。
应用实例:
1、算术表达式包括操作数、操作符和另一个操作数,其中,另一个操作符也可以是操作数、操作符和另一个操作数。
2、在 JAVA AWT 和 SWING 中,对于 Button 和 Checkbox 是树叶,Container 是树枝。
优点:
1、高层模块调用简单。
2、节点自由增加。
缺点:在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则。
使用场景:部分、整体场景,如树形菜单,文件、文件夹的管理。
注意事项:定义时为具体类。
装饰器模式(Decorator Pattern)
意图:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
主要解决:一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。
何时使用:在不想增加很多子类的情况下扩展类。
如何解决:将具体功能职责划分,同时继承装饰者模式。
关键代码:
1、Component 类充当抽象角色,不应该具体实现。
2、修饰类引用和继承 Component 类,具体扩展类重写父类方法。
应用实例:
1、孙悟空有 72 变,当他变成"庙宇"后,他的根本还是一只猴子,但是他又有了庙宇的功能。
2、不论一幅画有没有画框都可以挂在墙上,但是通常都是有画框的,并且实际上是画框被挂在墙上。在挂在墙上之前,画可以被蒙上玻璃,装到框子里;这时画、玻璃和画框形成了一个物体。
优点:装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
缺点:多层装饰比较复杂。
使用场景:
1、扩展一个类的功能。
2、动态增加功能,动态撤销。
注意事项:可代替继承。
外观模式(Facade Pattern)
意图:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
主要解决:降低访问复杂系统的内部子系统时的复杂度,简化客户端与之的接口。
何时使用:
1、客户端不需要知道系统内部的复杂联系,整个系统只需提供一个"接待员"即可。
2、定义系统的入口。
如何解决:客户端不与系统耦合,外观类与系统耦合。
关键代码:在客户端和复杂系统之间再加一层,这一层将调用顺序、依赖关系等处理好。
应用实例:
1、去医院看病,可能要去挂号、门诊、划价、取药,让患者或患者家属觉得很复杂,如果有提供接待人员,只让接待人员来处理,就很方便。
2、JAVA 的三层开发模式。
优点:
1、减少系统相互依赖。
2、提高灵活性。
3、提高了安全性。
缺点:不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
使用场景:
1、为复杂的模块或子系统提供外界访问的模块。
2、子系统相对独立。
3、预防低水平人员带来的风险。
注意事项:在层次化结构中,可以使用外观模式定义系统中每一层的入口。
享元模式(Flyweight Pattern)
意图:运用共享技术有效地支持大量细粒度的对象。
主要解决:在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。
何时使用:
1、系统中有大量对象。
2、这些对象消耗大量内存。
3、这些对象的状态大部分可以外部化。
4、这些对象可以按照内蕴状态分为很多组,当把外蕴对象从对象中剔除出来时,每一组对象都可以用一个对象来代替。
5、系统不依赖于这些对象身份,这些对象是不可分辨的。
如何解决:用唯一标识码判断,如果在内存中有,则返回这个唯一标识码所标识的对象。
关键代码:用 HashMap 存储这些对象。
应用实例:
1、JAVA 中的 String,如果有则返回,如果没有则创建一个字符串保存在字符串缓存池里面。
2、数据库的数据池。
优点:大大减少对象的创建,降低系统的内存,使效率提高。
缺点:提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。
使用场景:
1、系统有大量相似对象。
2、需要缓冲池的场景。
注意事项:
1、注意划分外部状态和内部状态,否则可能会引起线程安全问题。
2、这些类必须有一个工厂对象加以控制。
代理模式(Proxy Pattern)
意图:为其他对象提供一种代理以控制对这个对象的访问。
主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。
何时使用:想在访问一个类时做一些控制。
如何解决:增加中间层。
关键代码:实现与被代理类组合。
应用实例:
1、Windows 里面的快捷方式。
2、猪八戒去找高翠兰结果是孙悟空变的,可以这样理解:把高翠兰的外貌抽象出来,高翠兰本人和孙悟空都实现了这个接口,猪八戒访问高翠兰的时候看不出来这个是孙悟空,所以说孙悟空是高翠兰代理类。
3、买火车票不一定在火车站买,也可以去代售点。
4、一张支票或银行存单是账户中资金的代理。支票在市场交易中用来代替现金,并提供对签发人账号上资金的控制。
5、spring aop。
优点: 1、职责清晰。 2、高扩展性。 3、智能化。
缺点:
1、由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
2、实现代理模式需要额外的工作,有些代理模式的实现非常复杂。
使用场景:按职责来划分,通常有以下使用场景: 1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理。
注意事项:
1、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
2、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
行为型
责任链模式(Chain of Responsibility Pattern)
意图:避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
主要解决:职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。
何时使用:在处理消息的时候以过滤很多道。
如何解决:拦截的类都实现统一接口。
关键代码:Handler 里面聚合它自己,在 HanleRequest 里判断是否合适,如果没达到条件则向下传递,向谁传递之前 set 进去。
应用实例:
1、红楼梦中的"击鼓传花"。
2、JS 中的事件冒泡。
3、JAVA WEB 中 Apache Tomcat 对 Encoding 的处理,Struts2 的拦截器,jsp servlet 的 Filter。
优点:
1、降低耦合度。它将请求的发送者和接收者解耦。
2、简化了对象。使得对象不需要知道链的结构。
3、增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。
4、增加新的请求处理类很方便。
缺点:
1、不能保证请求一定被接收。
分支主题
2、系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用。
3、可能不容易观察运行时的特征,有碍于除错。
使用场景:
1、有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。
2、在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
3、可动态指定一组对象处理请求。
注意事项:在 JAVA WEB 中遇到很多应用。
命令模式(Command Pattern)
意图:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。
主要解决:在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。
何时使用:在某些场合,比如要对行为进行"记录、撤销/重做、事务"等处理,这种无法抵御变化的紧耦合是不合适的。在这种情况下,如何将"行为请求者"与"行为实现者"解耦?将一组行为抽象为对象,可以实现二者之间的松耦合。
如何解决:通过调用者调用接受者执行命令,顺序:调用者→接受者→命令。
关键代码:定义三个角色:1、received 真正的命令执行对象 2、Command 3、invoker 使用命令对象的入口
应用实例:struts 1 中的 action 核心控制器 ActionServlet 只有一个,相当于 Invoker,而模型层的类会随着不同的应用有不同的模型类,相当于具体的 Command。
优点:
1、降低了系统耦合度。
2、新的命令可以很容易添加到系统中去。
缺点:使用命令模式可能会导致某些系统有过多的具体命令类。
使用场景:认为是命令的地方都可以使用命令模式,比如: 1、GUI 中每一个按钮都是一条命令。 2、模拟 CMD。
注意事项:系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作,也可以考虑使用命令模式,见命令模式的扩展。
解释器模式(Interpreter Pattern)
意图:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
主要解决:对于一些固定文法构建一个解释句子的解释器。
何时使用:如果一种特定类型的问题发生的频率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题。
如何解决:构件语法树,定义终结符与非终结符。
关键代码:构件环境类,包含解释器之外的一些全局信息,一般是 HashMap。
应用实例:编译器、运算表达式计算。
优点:
1、可扩展性比较好,灵活。
2、增加了新的解释表达式的方式。
3、易于实现简单文法。
缺点:
1、可利用场景比较少。
2、对于复杂的文法比较难维护。
3、解释器模式会引起类膨胀。
4、解释器模式采用递归调用方法。
使用场景:
1、可以将一个需要解释执行的语言中的句子表示为一个抽象语法树。
2、一些重复出现的问题可以用一种简单的语言来进行表达。
3、一个简单语法需要解释的场景。
注意事项:可利用场景比较少,JAVA 中如果碰到可以用 expression4J 代替。
迭代器模式(Iterator Pattern)
意图:提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。
主要解决:不同的方式来遍历整个整合对象。
何时使用:遍历一个聚合对象。
如何解决:把在元素之间游走的责任交给迭代器,而不是聚合对象。
关键代码:定义接口:hasNext, next。
应用实例:JAVA 中的 iterator。
优点:
1、它支持以不同的方式遍历一个聚合对象。
2、迭代器简化了聚合类。
3、在同一个聚合上可以有多个遍历。
4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。
缺点:由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
使用场景:
1、访问一个聚合对象的内容而无须暴露它的内部表示。
2、需要为聚合对象提供多种遍历方式。
3、为遍历不同的聚合结构提供一个统一的接口。
注意事项:迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。
中介者模式(Mediator Pattern)
意图:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
主要解决:对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。
何时使用:多个类相互耦合,形成了网状结构。
如何解决:将上述网状结构分离为星型结构。
关键代码:对象 Colleague 之间的通信封装到一个类中单独处理。
应用实例: 1、中国加入 WTO 之前是各个国家相互贸易,结构复杂,现在是各个国家通过 WTO 来互相贸易。 2、机场调度系统。 3、MVC 框架,其中C(控制器)就是 M(模型)和 V(视图)的中介者。
优点: 1、降低了类的复杂度,将一对多转化成了一对一。 2、各个类之间的解耦。 3、符合迪米特原则。
缺点:中介者会庞大,变得复杂难以维护。
使用场景: 1、系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象。 2、想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。
注意事项:不应当在职责混乱的时候使用。
备忘录模式(Memento Pattern)
意图:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
主要解决:所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。
何时使用:很多时候我们总是需要记录一个对象的内部状态,这样做的目的就是为了允许用户取消不确定或者错误的操作,能够恢复到他原先的状态,使得他有"后悔药"可吃。
如何解决:通过一个备忘录类专门存储对象状态。
关键代码:客户不与备忘录类耦合,与备忘录管理类耦合。
应用实例:
1、后悔药。
2、打游戏时的存档。
3、Windows 里的 ctri + z。
4、IE 中的后退。
5、数据库的事务管理。
优点:
1、给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。
2、实现了信息的封装,使得用户不需要关心状态的保存细节。
缺点:消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
使用场景: 1、需要保存/恢复数据的相关状态场景。 2、提供一个可回滚的操作。
注意事项:
1、为了符合迪米特原则,还要增加一个管理备忘录的类。
2、为了节约内存,可使用原型模式+备忘录模式。
观察者模式(Observer Pattern)
意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
如何解决:使用面向对象技术,可以将这种依赖关系弱化。
关键代码:在抽象类里有一个 ArrayList 存放观察者们。
应用实例:
1、拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价。
2、西游记里面悟空请求菩萨降服红孩儿,菩萨洒了一地水招来一个老乌龟,这个乌龟就是观察者,他观察菩萨洒水这个动作。
优点: 1、观察者和被观察者是抽象耦合的。 2、建立一套触发机制。
缺点: 1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。 2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。 3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
使用场景:
一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
一个对象必须通知其他对象,而并不知道这些对象是谁。
需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。
注意事项: 1、JAVA 中已经有了对观察者模式的支持类。 2、避免循环引用。 3、如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。
状态模式(State Pattern)
意图:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
主要解决:对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。
何时使用:代码中包含大量与对象状态有关的条件语句。
如何解决:将各种具体的状态类抽象出来。
关键代码:通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if...else 等条件选择语句。
应用实例: 1、打篮球的时候运动员可以有正常状态、不正常状态和超常状态。 2、曾侯乙编钟中,'钟是抽象接口','钟A'等是具体状态,'曾侯乙编钟'是具体环境(Context)。
优点: 1、封装了转换规则。 2、枚举可能的状态,在枚举状态之前需要确定状态种类。 3、将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。 4、允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。 5、可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
缺点: 1、状态模式的使用必然会增加系统类和对象的个数。 2、状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。 3、状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
使用场景: 1、行为随状态改变而改变的场景。 2、条件、分支语句的代替者。
注意事项:在行为受状态约束的时候使用状态模式,而且状态不超过 5 个。
空对象模式(Null Object Pattern)
策略模式(Strategy Pattern)
意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。
如何解决:将这些算法封装成一个一个的类,任意地替换。
关键代码:实现同一个接口。
应用实例: 1、诸葛亮的锦囊妙计,每一个锦囊就是一个策略。 2、旅行的出游方式,选择骑自行车、坐汽车,每一种旅行方式都是一个策略。 3、JAVA AWT 中的 LayoutManager。
优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。
缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。
使用场景: 1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。 2、一个系统需要动态地在几种算法中选择一种。 3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
模板模式(Template Pattern)
意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
主要解决:一些方法通用,却在每一个子类都重新写了这一方法。
何时使用:有一些通用的方法。
如何解决:将这些通用算法抽象出来。
关键代码:在抽象类实现,其他步骤在子类实现。
应用实例: 1、在造房子的时候,地基、走线、水管都一样,只有在建筑的后期才有加壁橱加栅栏等差异。 2、西游记里面菩萨定好的 81 难,这就是一个顶层的逻辑骨架。 3、spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务、获取 Session、关闭 Session 等,程序员不重复写那些已经规范好的代码,直接丢一个实体就可以保存。
优点: 1、封装不变部分,扩展可变部分。 2、提取公共代码,便于维护。 3、行为由父类控制,子类实现。
缺点:每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
使用场景: 1、有多个子类共有的方法,且逻辑相同。 2、重要的、复杂的方法,可以考虑作为模板方法。
注意事项:为防止恶意操作,一般模板方法都加上 final 关键词。
访问者模式(Visitor Pattern)
意图:主要将数据结构与数据操作分离。
主要解决:稳定的数据结构和易变的操作耦合问题。
何时使用:需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。
如何解决:在被访问的类里面加一个对外提供接待访问者的接口。
关键代码:在数据基础类里面有一个方法接受访问者,将自身引用传入访问者。
应用实例:您在朋友家做客,您是访问者,朋友接受您的访问,您通过朋友的描述,然后对朋友的描述做出一个判断,这就是访问者模式。
优点: 1、符合单一职责原则。 2、优秀的扩展性。 3、灵活性。
缺点: 1、具体元素对访问者公布细节,违反了迪米特原则。 2、具体元素变更比较困难。 3、违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
使用场景: 1、对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。 2、需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。
注意事项:访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。
J2EE 模式
MVC 模式(MVC Pattern)
业务代表模式(Business Delegate Pattern)
组合实体模式(Composite Entity Pattern)
数据访问对象模式(Data Access Object Pattern)
前端控制器模式(Front Controller Pattern)
拦截过滤器模式(Intercepting Filter Pattern)
服务定位器模式(Service Locator Pattern)
传输对象模式(Transfer Object Pattern)
分布式
分布式应用
分布式锁
redis分布式锁
基本
直接setnx
直接利用setnx,执行完业务逻辑后调用del释放锁,简单粗暴
缺点
如果setnx成功,还没来得及释放,服务挂了,那么这个key永远都不会被获取到
setnx设置一个过期时间
为了改正第一个方法的缺陷,我们用setnx获取锁,然后用expire对其设置一个过期时间,如果服务挂了,过期时间一到自动释放
缺点
setnx和expire是两个方法,不能保证原子性,如果在setnx之后,还没来得及expire,服务挂了,还是会出现锁不释放的问题
set nx ex
扩展参数nx和ex,保证了setnx+expire的原子性,使用方法: set key value ex 5 nx
缺点
如果在过期时间内,事务还没有执行完,锁提前被自动释放,其他的线程还是可以拿到锁
上面所说的那个缺点还会导致当前的线程释放其他线程占有的锁
set nx ex 加一个事务id
上面所说的第一个缺点,没有特别好的解决方法,只能把过期时间尽量设置的长一点,并且最好不要执行耗时任务
第二个缺点,可以理解为当前线程有可能会释放其他线程的锁,那么问题就转换为保证线程只能释放当前线程持有的锁,即setnx的时候将value设为任务的唯一id,释放的时候先get key比较一下value是否与当前的id相同,是则释放,否则抛异常回滚,其实也是变相地解决了第一个问题
缺点
get key和将value与id比较是两个步骤,不能保证原子性
set nx px + 事务id + lua
可以用lua来写一个getkey并比较的脚本,jedis/luttce/redisson对lua脚本都有很好的支持
缺点
集群环境下,对master节点申请了分布式锁,由于redis的主从同步是异步进行的,master在内存中写入了nx之后直接返回,客户端获取锁成功,此时master节点挂了,并且数据还没来得及同步,另一个节点被升级为master,这样其他的线程依然可以获取锁
redlock
假设集群中所有的n个master节点完全独立,并且没有主从同步,此时对所有的节点都去setnx,并且设置一个请求过期时间re和锁的过期时间le,同时re必须小于le(可以理解,不然请求3秒才拿到锁,而锁的过期时间只有1秒),此时如果有n / 2 + 1个节点成功拿到锁,此次分布式锁就算申请成功
缺点
可靠性还没有被广泛验证,并且严重依赖时间,好的分布式系统应该是异步的,并不能以时间为担保,程序暂停、系统延迟等都可能会导致时间错误
缺陷
客户端长时间阻塞导致锁失效问题
客户端1得到了锁,因为网络问题或者GC等原因导致长时间阻塞,然后业务程序还没执行完锁就过期了,这时候客户端2也能正常拿到锁,可能会导致线程安全的问题
启动另外一个线程去检查的问题,这个key是否超时,在某个时间还没释放的话如果业务没有处理完对比value值延长锁的时间
Redisson 中,internalLockLeaseTime 是 30s,也就是每隔 10s 续期一次,每次 30s
redis服务器时钟漂移问题
如果redis服务器的机器时钟发生了向前跳跃,就会导致这个key过早超时失效,比如说客户端1拿到锁后,key的过期时间是12:02分,但redis服务器本身的时钟比客户端快了2分钟,导致key在12:00的时候就失效了,这时候,如果客户端1还没有释放锁的话,就可能导致多个客户端同时持有同一把锁的问题
系统时钟漂移原因
1.系统的时钟和NTP服务器不同步。这个目前没有特别好的解决方案,只能相信运维同学了。
2.clock realtime被人为修改。在实现分布式锁时,不要使用clock realtime。不过很可惜,redis使用的就是这个时间,我看了下Redis 5.0源码,使用的还是clock realtime。Antirez说过改成clock monotonic的,不过大佬还没有改。也就是说,人为修改redis服务器的时间,就能让redis出问题了
单点实例安全问题
如果redis是单master模式的,当这台机宕机的时候,那么所有的客户端都获取不到锁了,为了提高可用性,可能就会给这个master加一个slave,但是因为redis的主从同步是异步进行的,可能会出现客户端1设置完锁后,master挂掉,slave提升为master,因为异步复制的特性,客户端1设置的锁丢失了,这时候客户端2设置锁也能够成功,导致客户端1和客户端2同时拥有锁
redlock
获取当前时间戳(ms)
先设定key的有效时长(TTL),超出这个时间就会自动释放,然后client(客户端)尝试使用相同的key和value对所有redis实例进行设置,每次链接redis实例时设置一个比TTL短很多的超时时间,这是为了不要过长时间等待已经关闭的redis服务。并且试着获取下一个redis实例
client通过获取所有能获取的锁后的时间减去第一步的时间,还有redis服务器的时钟漂移误差,然后这个时间差要小于TTL时间并且成功设置锁的实例数>= N/2 + 1(N为Redis实例的数量),那么加锁成功
如果客户端由于某些原因获取锁失败,便会开始解锁所有redis实例
问题
如果有节点发生崩溃重启的话,还是有可能出现多个客户端同时获取锁的情况
假设一共有5个Redis节点:A、B、C、D、E,客户端1和2分别加锁
客户端1成功锁住了A,B,C,获取锁成功(但D和E没有锁住)
节点C的master挂了,然后锁还没同步到slave,slave升级为master后丢失了客户端1加的锁
客户端2这个时候获取锁,锁住了C,D,E,获取锁成功
RedLock并没有完全解决Redis单点故障存在的隐患,也没有解决时钟漂移以及客户端长时间阻塞而导致的锁超时失效存在的问题,锁的安全性隐患依然存在
zk分布式锁
实现
临时顺序节点
优点
zk通过临时节点,解决掉了死锁的问题,一旦客户端获取到锁之后突然挂掉(Session连接断开),那么这个临时节点就会自动删除掉,其他客户端自动获取锁
zk通过节点排队监听的机制,也实现了阻塞的原理,其实就是个递归在那无限等待最小节点释放的过程
可重入,创建临时节点时带上线程信息
高可用的,只要半数以上的或者,就可以对外提供服务了
缺点
性能上可能并没有缓存服务那么高
网络抖动,客户端和ZK集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了
总结
基于数据库分布式锁实现
优点
直接使用数据库,实现方式简单
缺点
db操作性能较差,并且有锁表的风险
非阻塞操作失败后,需要轮询,占用cpu资源;
长时间不commit或者长时间轮询,可能会占用较多连接资源
基于jdk的并发工具自己实现的锁
优点
不需要引入中间件,架构简单
缺点
编写一个可靠、高可用、高效率的分布式锁服务,难度较大
基于redis缓存
redis set px nx + 唯一id + lua脚本
优点
redis本身的读写性能很高,因此基于redis的分布式锁效率比较高
缺点
依赖中间件,分布式环境下可能会有节点数据同步问题,可靠性有一定的影响,如果发生则需要人工介入
基于redis的redlock
优点
可以解决redis集群的同步可用性问题
不一定
A、B、C、D、E,客户端2锁A,B,C然后C宕机,客户端2又能成功锁C,D,E
缺点
依赖中间件,并没有被广泛验证,维护成本高,需要多个独立的master节点;需要同时对多个节点申请锁,降低了一些效率
锁删除失败 过期时间不好控制
非阻塞,操作失败后,需要轮询,占用cpu资源
基于zookeeper的分布式锁
优点
不存在redis的超时、数据同步(zookeeper是同步完以后才返回)、主从切换(zookeeper主从切换的过程中服务是不可用的)的问题,可靠性很高
缺点
依赖中间件,保证了可靠性的同时牺牲了一部分效率(但是依然很高)。性能不如redis
分布式事务
基础知识
数据库事务
ACID(更多指数据库事务层面)
Atomicity(原子性)
事务内的操作要么全部成功,要么全部失败,不会在中间的某个环节结束
Consistency(一致性)
数据库在一个事务执行之前和执行之后,都必须处于一致性状态,事务提交或执行失败,看到的结果一致
Isolation(隔离性)
并发环境中,不同事务同时修改相同数据时,一个未完成事务不会影响另外一个未完成事务
Durability(持久性)
事务一旦提交,其修改的数据将永久保存到数据库,改变是永久的
分布式事务理论
CAP理论
Consistency(一致性)
所有节点每次读操作都能保证获取最新数据,且一致。
Availability(可用性)
在集群中一部分节点故障后,集群整体还能响应客户端的读写请求,保证服务仍然可用
Partition tolerance(分区容错性,是分布式的基础)
网络节点之间无法通信的情况下, 节点被隔离,产生了网络分区, 整个系统仍然是可以工作的。简单理解,被分区的节点可用正常对外提供服务
BASE理论
Basically Available(基本可用)
Soft state(软状态)
Eventually consistent(最终一致性)
解决方案
两阶段提交(2PC)
阶段一(准备阶段)
1、协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待答复。
2、各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。
3、如参与者执行成功,给协调者反馈 yes,否则反馈 no。
阶段二(提交阶段)
如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息。
存在的问题
性能问题:所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
可靠性问题:如果协调者存在单点故障问题,或出现故障,提供者将一直处于锁定状态。
数据一致性问题:在阶段 2 中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一致。
优缺点
优点
尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能100%保证强一致)
缺点
实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。
流程图
子主题
子主题
三阶段提交(3PC)
阶段一
1、协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。
2、参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。
阶段二
协调者根据参与者响应情况,所有参与者均反馈 yes,协调者预执行事务。
只要有一个参与者反馈 no,或者等待超时后协调者尚无法收到所有提供者的反馈,即中断事务
只要有一个参与者反馈 no,或者等待超时后协调者尚无法收到所有提供者的反馈,即中断事务
阶段三
进行真正的事务提交
优缺点
优点
相比二阶段提交,三阶段提交降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题。阶段 3 中协调者出现问题时,参与者会继续提交事务
缺点
数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commite 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。
流程图
子主题
补偿事务(TCC)
条件
需要实现确认和补偿逻辑,支持幂等
处理流程
1、Try 阶段主要是对业务系统做检测及资源预留。完成所有业务检查( 一致性 ) 。预留必须业务资源( 准隔离性 ) 。Try 尝试执行业务。
2、Confirm 阶段主要是对业务系统做确认提交。Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
3、Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
优缺点
优点
1、性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
2、数据最终一致性:基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。
3、可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。
缺点
TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本
异常处理
幂等处理
通过悲观锁与乐观锁保证数据的唯一性,确保幂等性
悲观锁
传统的关系型数据库使用这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
Java 里面的同步 synchronized 关键字的实现。悲观锁主要分为共享锁(读锁)和排他锁(写锁)
Java 里面的同步 synchronized 关键字的实现。悲观锁主要分为共享锁(读锁)和排他锁(写锁)
乐观锁
CAS 加版本号version,先查询再更新根据版本。使用条件限制实现乐观锁
空回滚
资源悬挂
流程图
子主题
本地消息表
本地消息表其实就是利用了 各系统本地的事务来实现分布式事务。
本地消息表顾名思义就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候 将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。
然后再去调用下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改成已成功。
如果调用失败也没事,会有 后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。
这时候有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。
可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。
本地消息表顾名思义就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候 将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。
然后再去调用下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改成已成功。
如果调用失败也没事,会有 后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。
这时候有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。
可以看到本地消息表其实实现的是最终一致性,容忍了数据暂时不一致的情况。
子主题
可靠事件模式(消息队列)
子主题
Sagas事务模型(最终一致性)
拆分分布式事务为多个本地事务,由saga引擎负责协调,如果过程中出现部门失败,调用补偿操作。恢复策略:向前恢复和向后恢复,向前恢复对失败的节点采取最大努力不断重试,保证数据库的操作最终一定保证数据一致性,如果最终多次重试失败则根据相关日志主动通知开发人员手工介入。向后恢复对之前的成功节点执行回滚的事务操作,保证数据一致性。Saga比TCC少了Try操作,因此会直接提交数据库,然后出现失败进行补偿。
最大努力通知方案
分布式任务调度
xxl-job
分布式ID
要求
全局唯一
高性能
高可用
方便接入
趋势递增
生成方式
UUID(不推荐)
使用
优点
生成简单,本地生成无网络消耗,具有唯一性
缺点
无序的字符串,不具备自增特性
没有具体的业务含义
长度过长,16字节,128位。MySQL对于36位长度的字符串,存储以及查询对数据库的性能消耗较大,MySQL明确建议主键越短越好,作为数据库主键,UUID的无序性会导致数据位置频繁变动,严重影响性能。
数据库自增ID
使用
优点
实现简单,ID单调递增,数值类型查询速度快
缺点
并发高,访问量激增时MySQL本身就是系统的瓶颈
DB单点存在宕机风险,无法扛住高并发场景
数据库多主模式
使用
优点
解决DB单点问题
缺点
不利于后续扩容,在其他集群机器起始值和自增步长确定好之后,新增机器会比较麻烦
单个数据库自身压力还是大,高并发的情况下对数据库自身压力还是大
Redis
使用
优点
使用集群,解决数据库生成ID的性能问题
缺点
持久化问题。RDB方式,挂掉重启后会出现ID重复的情况。
AOF方式,重启数据恢复时间过长。
redis集群之后水平扩张会麻烦
号段模式
使用
优点
不强依赖数据库,不会频繁访问数据库,对数据库压力比较小
缺点
雪花算法
时间戳 + 工作机器id + 序列号
介绍
使用
优点
缺点
滴滴-TinyID
图解
介绍
原理
接入方式
http
tinyid-client
建议
优点
缺点
百度-Uidgenerator
介绍
组成
使用
建议
优点
可以自定义机器id生成策略
能支持较高的吞吐量,基本维持在 600w/s
缺点
需要额外使用数据库,生成机器id用
美团-Leaf
介绍
使用
号段模式
snowflake
优点
缺点
基础
RPC
定义
一般用来实现部署在不同机器上的系统之间的方法调用,使得程序能够像访问本地系统资源一样,通过网络传输去访问远端系统资源
组成部分
Client Code
客户端调用方代码实现,负责发起RPC调用,为调用方用户提供使用API
Serialization/Deserialization
负责RPC调用通过网络传输的内容进行序列化与反序列化,不同的RPC框架有不同的实现机制
Stub Proxy
屏蔽RPC调用过程中复杂的网络处理逻辑,使得RPC调用透明化,能够保持与本地调用一样的代码风格
Transport
作为RPC框架底层的通信传输模块,一般通过Socket在客户端与服务器端之间传递请求与应答消息
Server Code
服务端服务业务逻辑具体的实现
Java RMI
是一种基于Java的远程方法调用技术,是java特有的一种RPC实现
特性
支持真正的面向对象的多态性
Java独有,不支持其他语言
使用java原生的序列化机制,所有序列化对象必须实现java.io.Serializable接口
底层通信基于BIO(同步阻塞I/O)实现Socket完成
缺点
由于使用了java原生序列化机制与BIO通信机制(因为存在性能问题),所以导致RMI的性能较差
WebService
是一种跨平台的RPC技术协议
实现
CXF
是一个开源的WebService RPC框架,它包含一个范围广泛、功能齐全的集合
特点
支持Web Service标准,包括SOAP规范,WSI Basic Profile、WSDL、WS-Addressing等
支持JSR相关规范和标准,包括JAX-WS、JAX-RS、SAAJ
支持多种传输洗衣和协议绑定、数据绑定
Axis2
是Axis的后续版本,是新一代的SOAP引擎,是CXF之外另外一个非常流行的Web Service/SOAP/WSDL实现
特点
高性能,Axis2具有自己的轻量级对象模型AXIOM,并且采用stAx技术,具有更加优秀的性能表现,比前一代的内存消耗更
热部署,Axis2配备了再系统启动和运行时部署Web服务和处理程序的功能
异步服务支持,支持使用非阻塞客户端的传输的异步,以及Web服务和异步Web服务调用
WSDL支持,支持Web服务描述语言版本1.1和2.0,它允许轻松构建存根以访问远程服务,还可以自动导出来自Axis2的已部署服务的机器可读描述
Thrift
是跨越不同的平台和语言,协助构建可伸缩的分布式系统的一种RPC实现,开源,具备广泛的语言支持以及高性能
Trrifts有着明显的性能优势,在于它是采用二进制编码协议、使用TCP/IP传输协议的一种RPC实现,而XML-RPC/JSON/-RPC/SOAP与WSDL协议栈采用文本协议,WSDL的实现WebService采用HTTP作为传输协议。对于网络数据传输,TCP/IP协议的性能要高于HTTP协议
gRPC
是Google的一个高性能、开源和通用的RPC框架,面向移动和HTTP/2设计
在个RPC里客户端应用可以像调用本地对象一样直接调用另一台不同机器上服务端应用的方法,能够更容易地创建分布式应用和服务
HTTP Client
序列化实现
定义
序列化
是将对象的状态信息转换成为可存储或传输的形势过程
反序列化
是序列化的逆过程,将字节数组反序列化为对象,把字节序列恢复为对象的过程
作用
通过将对象序列化为字节数组,使得不共享内存通过网络连接的系统之间能够进行对象的传输
通过将对象序列化为字节数组,能够将对象永久存储到存储设备
解决远程接口调用JVM之间内存无法共享的问题
衡量指标
序列化后码流的大小
序列化本身的速度及系统资源开销大小
实现
java默认的序列化(Serializable)
优点
java语言自带,无须额外引入第三方依赖
与java语言有天然的最好的易用性与亲和性
缺点
只支持java语言,不支持跨语言
java默认序列化性能欠佳,序列化后产生的码流过大,对于引用过深的对象序列化易发生内存溢出异常
XML序列化框架
优点
可读性好,利于调试
由于XML具有语言无关性,可用于异构系统之间的数据交换协议(WebService)
缺点
由于使用标签对来表示数据,导致序列化后码流大,而且效率不高。(适用于对性能要求不高,且QPS较低的企业级内部系统之间的数据交换的场景)
JSON序列化框架
是一种轻量级的数据交换格式
相比XML,它的码流更小,而且保留了XML可读性好的优势
开源工具
Jackson
fastjson
GSON
jackson与fastjson比GSON性能好,但是Jackson与GSON相对fastjson稳定性更好
Hessian序列化框架
优点
支持跨语言传输的二进制序列化协议
相对于java默认的序列化机制,Hessian具有更好的性能与易用性
protobuf序列化框架
优点
是一个纯粹的展示层协议,可以和各种传输层协议一起使用
文档非常完善
空间开销小及高解析性能,序列化后数据量相对少,也适合应用层对象的持久化场景
非常适合公司内部对性能要求高德RPC调用
缺点
由于需要编写.proto IDL文件,使用起来工作量稍大
需要额外学习proto IDL特有的语法,增加了额外的学习成本
protostuff序列化架构
优点
实现了在代码执行时实现编译功能,而不必像protobuf那样通过它提供的编译器生成对应于各种语言的代码
protostuff-runtime实现了无须预编译对java bean进行protobuf序列化/反序列化的能力
具有高性能,同时免去了编写.proto文件的麻烦
thrift序列化框架
优点
支持多种序列化协议,常用的有TBinaryProtocol、TCompactProtocol和TJSONProtocol。其中TBinaryProtocol为二进制序列化协议,TCompactProtocol可以看做是TBinaryProtocol的升级版,采用了字节压缩算法,进一步减少了序列化后的码流,TJSONProtocol是一种JSON数据格式序列化协议
缺点
需要编写以.thrift结尾的IDL文件,再使用thrift提供的编译器编译生成对应的代码
Avro序列化框架
特点
动态类型
Avro无须生成代码。数据总是伴以模式定义,这样就可以在不生成代码、静态数据类型的情况下对数据进行所有处理,有利于构建通用的数据处理系统和语言
无标记数据
由于在读取数据时有模式定义,这就大大减少了数据编辑所需的类型信息,从而减少序列化空间开销
不用手动分配的字段ID
当数据模式发生变化,处理数据时总是同时提供新旧模式,差异就可以用字段名来做符号化的分析
优点
性能高、基本代码少和产出数据量精简
特性
丰富的数据结构类型
快速可压缩得二进制数据形式
存储持久数据的文件容器
远程过程调用RPC
简单的动态语言结合功能,Avro和动态语言结合后,读取数据文件和使用RPC协议都不需要生成代码,而代码生成作为一种可选的优化只值得在静态类型语言中实现
两种序列化编码方式
二进制
性能更高,序列化后产生的码流更小
JSON编码
编码后的可读性好,适合在开发调试阶段使用
JBoss Marshalling序列化框架
优点
是一个java对象序列化包,兼容java原生的序列化机制,对其做了优化,在性能上有很大的提升
保持跟java.io.Serializable接口兼容的同时增加了一些可调的参数和附加特性,这些参数和附加的特性可通过工厂类进行配置,对原生java序列化是一个很好的替代
序列化框架的选型
技术层面
序列化空间开销,即序列化结果产生的码流大小,码流过大会对带宽、存储空间造成较大的压力
序列化时间开销,即徐丽华过程消耗的时长,序列化消耗时间过长会拖慢整个服务的响应时间
序列化协议是否支持跨平台、跨语言,公司内部存在异构系统通信需求时,往往要求RPC架构采用的序列化协议支持跨平台、跨语言
可扩展性/兼容性
成熟度及支持的数据结构的丰富性也是一个需要重点考量的方面
其他层面
技术的流行程度,背后是否有大公司技术支撑,是否是一个长期发展的持续进化的技术,是否已经得到业界的充分验证
学习难度和易用性
选型建议
对于公司间的系统调用,性能要求在100ms以上的服务,基于XML的SOAP洗衣是一个值得考虑的方案
基于Web Browser的Ajax,以及Mobile APP与服务器之间的通信,JSON协议是首选。对于性能要求不太高,或者动态类型语言为主,或者传输数据载荷很小的运用场景,JSON也是一个非常不错的选择
对于调试环境比较恶劣的场景,采用JSON或XML能够极大的提高调试效率,降低系统开发成本
对于性能和简洁性有极高要求的场景,Hessian,protobuf,Thrift,Avro之间具有一定的竞争关系。其中Hessian是在性能和稳定性同时考虑下最优的序列化协议
对于T级别的数据持久化应用场景,protobuf和Avro是首要选择。如果持久化后的数据存在Hadoop子项目里,Avro会是更好的选择
由于Avro的设计理念偏向于动态类型语言,对于以动态语言为主的应用场景,Avro是更好的选择
对于持久层非Hadoop项目,以静态类型语言为主的应用场景,protobuf会更符合静态类型语言工程师的开发习惯
对需要提供一个完成的RPC解决方案,Thrift是一个好的选择
对于序列化后需要支持不同的传输层协议,或者需要跨防火墙访问的高性能场景,Protobuf可以优先考虑
注册中心
服务注册中心
分布式服务框架部署在多台不同的机器上,集群就是一个典型的例子,不同的集群之间要进行通信
实时存储更新服务提供者信息及该服务的实时调用信息。所有的服务调用者和服务提供者都得让服务注册中心知道
流程
服务启动的时候,将服务提供者信息主动上报到服务注册中心进行服务注册
服务调用者启动的时候,将服务提供者信息从注册中心下拉到读物调用者机器本地缓存,服务调用者从本地缓存的服务提供者地址列表中,基于某种服务负载均衡策略选择一台服务提供者发起远程调用
服务注册中心能够感知服务提供者集群中某一台机器下线,将该机器服务提供者信息从服务注册中心删除,并主动通知服务调用者集群中的每一台机器,使得服务调用者不在调用该机器
优点
软负载及透明化服务路由:服务提供者和服务调用者之间相互解耦,服务调用者不需要硬编码服务提供者地址
服务动态发现及可伸缩能力:服务提供者机器增减能被服务调用者通过注册中心动态感知,而且通过增减机器可以实现服务的弹性伸缩
通过注册中心可以动态地监控服务运行质量及服务依赖,为服务提供服务治理能力
Zookeeper
提供了统一命名服务、配置管理、分布式锁等分布式基础服务,基于这些服务,可以实现集群管理、软负载、发布/订阅、分布式锁,分布式队列、命名服务等
实现服务注册中心
服务注册中心还可以用来收集服务消费者信息,以达到实现部分服务治理功能的目的。在服务消费端,通过将服务消费者信息写入ZooKeeper临时节点,一旦消费者机器下线,断开与ZooKeeper的连接,该临时节点将被自动删除,达到通过ZooKeeper自动收集消息者信息的目的
实现流程
作用及应用
服务提供端将服务接口注册到服务中心ZooKeeper中,如user.api,然后消费端先将ZooKeeper中心的所有机器上的所有服务同步到本地,然后本地动态地组合访问地址,如http://127.0.0.1:8080/user.httpInvoker。然后通过rmi,httpInvoker或hessian访问服务提供端的服务。这样就是一个完整的消费端访问服务端的服务的过程
这样的实际应用就是服务消费端不用固定的配置服务的地址(ip,port等),当一台服务提供端机器挂掉的时候,我们可以访问其他的机器,同时我们在对服务提供端的机器可以动态地添加或删除。更加动态化
底层通信
I/O模型
阻塞
调用方发起调用请求,在没有返回结果之前,调用方线程被挂起,处于一直等待状态
非阻塞
非阻塞和阻塞的概念相对应,调用方发起请求,当线程不会等待挂起,而会立刻返回。后续可以通过轮询等手段来获取调用结果状态
同步
在发出一个功能调用时,在没有得到结果之前,该调用就会返回
异步
异步和同步的概率相对。当一个异步过程调用发出后,调用者不会立刻得到结果,通过回调等措施来处理这个调用
java I/O
Streams字节流
所有的输出流都继承抽象类OutputStream,所有的输入流都继承抽象InputStream。注意output和Input是针对内存,写进内存是input,从内存写出到其他地方是output
ByteArrayOutputStream/ByteArrayInputStream
ByteArrayOutputStream可以将数据写入字节数组,随着数据的写入能够自动扩容。无须调用close()方法进行关闭。ByteArrayInputStream可以将字节数组转换成输入流
FileOutputStream/FileInputStream
FileOutputStream用于将数据写入文件。一般用来写入二进制字节流。如图像文件的数据。FileInputStream用来从文件读取字节流数据
专门用于文件的I/O操作。适合于图片等二进制文件操作
FilterOutputStream/FilterInputStream
是所有过滤输入/输出流实现的超类。
BufferedOutputStream/BufferedInputStream
先将字节数据写入该缓冲类,再一次性输出。将单字节操作转变为批量操作字节数组,避免了做个字节处理操作,提高了I/O处理性能
DataOutputStream/DataInputStream
DataOutputStream提供了直接写入原生Java数据类型数据的能力,后续可以使用DataInputStream将数据读取到程序中并转换成对应Java数据类型
常用于网络数据传输过程中的写入与读取
PrintStream
字节打印流,功能很强大的一个装饰流,作为FilterInputStream的一个子类,在OutputStream基础上做了增强,可以方便地打印各种类型的数据。它可以自动刷新,当我们在构造PrintStream时指定它自动刷新,则每次调用它的print或println方法之后都会及时得地将数据写入底层字节输出流中,而不用手动调用flush去刷新。还有一个特性就是它从不抛出IOException
常用于日志输出组件的实现
ObjectOutputStream/ObjectInputStream
Java对象字节输入/输出流,一般用来实现java的序列化功能
常用于Java对象的反序列化/序列化或者网络数据的写入与读取
PipedOutputStream/PipedInputStream
通过管道读写字节流
Writer/Reader字符流
所有的输出流都继承抽象类Writer,所有的输入流都继承抽象Reader。注意writer和reader是针对内存,写进内存是writer,从内存写出到其他地方是reader
BufferedWriter/BufferedReader
先将字符数据写入或者读取到缓冲区,再一次性处理,相对于逐个字符处理,提高了I/O处理性能
使用了装饰模式,增加了对字符流操作缓存能力,使其能够批量读写字符流,提高了I/O操作的效率与性能
CharArrayWriter/CharArrayReader
CharArrayWriter提供了一个字符类型数据缓存,当写入数据的时候,缓冲区自动增长。CharArrayReader提供了字符输入流的缓存数组
能够将字符串或者字符数组转换为字符流
应用
某个第三方API使用字符流对外输出数据,writer可以将获得的字节流展示保存在内存,不必存到磁盘
OutputStreamWriter/InputStreamReader
InputStreamReader是字节输入流通想字符流的桥梁。OutputStreamWriter是字符输出流通向字节流的桥梁
FileWriter/FileReader
提供一字符为单位读写文件的能力。但是没有stream的性能好
一般用来操作文本文件
应用
第三方接口返回的数据时字节流形式的文本,为了提高操作性能及操作便捷性,可以用这个将字节流转换为字符流
StringWriter/StringReader
将字符串String类型的数据适配到Writer与Reader操作
PipedWriter/PipedReader
通过管道读写字符流
PrintWriter
除了提供PrintStream中的所有print方法,还提供了格式化输出字符串的能力。其方法不会抛出I/O异常
字节流/字符流对比
因为一个字节8bit,而一个字符是16bit,字符串由字符组成,字符串类型天然处理的是字符而不是字节。更重要的是,字节流无法知道字符集及其字符编码
NIO
缓冲区(Buffer)
缓冲区实质上是一个数组。抽象类是Buffer
所有缓冲区的属性
容量
缓冲区能够容纳的数据元素的最大数量。这一容量在缓冲区创建时被设定,并且永远不能被修改
上界
缓冲区的第一个不能被读或写的元素。或者说是缓冲区中现存元素的计数
位置
下一个要被读或写的元素的索引。回执会自动由相应的get()和put()函数更新
标记
一个备忘位置。调用mark()来设定mark=postion。调用reset()设定position=mark。标记在设定前是未定义的
属性之间总是遵循的关系
0 <= mark <= position <= limit <= capacity
通道(Channel)
Stream VS Channel
Stream 是单向的,通过OutputStream实现输出流,InputStream实现输入流
Channel是全双工通道,可以通过Channel实现同时读取和写入
Selector选择器
Channel在Selector上注册,Selector通过不断轮询注册在骑上的Channel,能够感知到Channel可读或者可写事件。通过这种机制,可以使用一个或者少数几个线程管理大量的网络连接。用较少的线程处理大量的网络连接有很大的好处,可以减少线程之间的切换开销,而且线程本身也需要占用系统资源。
应用
Netty
负载实现
软负载的实现原理
目的
将请求按照某种策略分布到多台机器上,使得系统能够实现横向扩展,是应用实现可伸缩性的关键技术
定义
分布式服务架构中实现负载均衡是通过软件算法来实现的,有别于基于硬件设备实现负载均衡
原理
分布式服务框架中,负载均衡是在服务消费端实现的
服务消费端在应用启动之初从服务注册中心获取服务提供者列表,缓存到服务调用端本地缓存
服务消费端发起服务调用之前,先通过某种策略或者短发从服务提供者列表本地缓存中选择本地调用的目标机器,再发起服务调用,从而完成负载均衡的功能
负载均衡算法
随机
获取服务列表大小范围内的随机数,将该随机数作为列表索引,从服务提供列表中获取服务提供者
加权随机
在随机算法的基础上针对权重做了处理。首先根据加权数放大服务提供者列表,比如服务提供者A加权数为3,放大之后变为A,A,A,存放在新的服务提供者列表,然后对新的服务提供者列表应用随机算法
轮询
定义
将服务调用请求按顺序轮流分配到服务提供者后端服务器上,均衡对待每一台服务提供者机器
原理
依次按顺序获取服务提供者列表中的数据,并使用计数器记录使用过得数据索引,若数据索引到最后一个数据,则计数器归零,重新开始新的循环
加权轮询
首先根据加权数放大服务提供者列表,再在放大后的服务提供者基础上使用轮询算法获取服务提供者
源地址hash
定义
利用请求来源的IP的hashcode对服务提供者列表大小取模,得到服务提供者列表索引,从而获取服务提供者
原理
使用调用方ip地址的hash值,将服务列表大小取模后的值作为服务列表索引,根据该索引取值
服务治理
服务治理内容
服务注册与发现
软负载
服务质量监控与服务指标数据采集
服务分组路由
服务依赖关系分析
服务降级
服务权重调整
服务调用链路跟踪
记录负责人
实现
服务分组路由实现原理
由于注册中心都是保存的路径,所以在路劲中加入分组名。注册中心加入服务分组名路径之后,指定消费摸个服务组的消费端将该服务组下的服务提供者列表获取到本地缓存,消费端服务调用的时候,将按照指定的软负载算法从本地缓存中选取一个服务调用者发起调用
简单服务依赖关系分析实现
服务提供者信息与对应的服务消费者信息在注册中心已经存在了,所需要做的,不过是提供获取服务提供者信息与消费者信息列表的接口方法,从注册中心查找对应的信息
服务调用链路跟踪实现原理
在服务调用发起方生成标识本次调用的唯一ID,传递到服务提供方,然后将该ID使用ThreadLocal保存起来,在应用的业务代码里面使用拦截器统一从ThreadLocal中获取出来。
微服务
子主题
//todo
框架源码
spring
基础
模块
Spring的主要jar包
常用注解
第三方框架集成
启动流程
(1)初始化Spring容器,注册内置的BeanPostProcessor的BeanDefinition到容器中
① 实例化BeanFactory【DefaultListableBeanFactory】工厂,用于生成Bean对象
② 实例化BeanDefinitionReader注解配置读取器,用于对特定注解(如@Service、@Repository)的类进行读取转化成 BeanDefinition 对象,(BeanDefinition 是 Spring 中极其重要的一个概念,它存储了 bean 对象的所有特征信息,如是否单例,是否懒加载,factoryBeanName 等)
③ 实例化ClassPathBeanDefinitionScanner路径扫描器,用于对指定的包目录进行扫描查找 bean 对象
(2)将配置类的BeanDefinition注册到容器中
(3)调用refresh()方法刷新容器
① prepareRefresh()刷新前的预处理
② obtainFreshBeanFactory():获取在容器初始化时创建的BeanFactory
③ prepareBeanFactory(beanFactory):BeanFactory的预处理工作,向容器中添加一些组件
④ postProcessBeanFactory(beanFactory):子类重写该方法,可以实现在BeanFactory创建并预处理完成以后做进一步的设置
⑤ invokeBeanFactoryPostProcessors(beanFactory):在BeanFactory标准初始化之后执行BeanFactoryPostProcessor的方法,即BeanFactory的后置处理器
⑥ registerBeanPostProcessors(beanFactory):向容器中注册Bean的后置处理器BeanPostProcessor,它的主要作用是干预Spring初始化bean的流程,从而完成代理、自动注入、循环依赖等功能
⑦ initMessageSource():初始化MessageSource组件,主要用于做国际化功能,消息绑定与消息解析:
⑧ initApplicationEventMulticaster():初始化事件派发器,在注册监听器时会用到
⑨ onRefresh():留给子容器、子类重写这个方法,在容器刷新的时候可以自定义逻辑
⑩ registerListeners():注册监听器:将容器中所有的ApplicationListener注册到事件派发器中,并派发之前步骤产生的事件
⑪ finishBeanFactoryInitialization(beanFactory):初始化所有剩下的单实例bean,核心方法是preInstantiateSingletons(),会调用getBean()方法创建对象
⑫ finishRefresh():发布BeanFactory容器刷新完成事件
bean生命周期
四个阶段
实例化 Instantiation
属性赋值 Populate
初始化 Initialization
销毁 Destruction
(1)实例化Bean
对于BeanFactory容器,
当客户向容器请求一个尚未初始化的bean时,
或初始化bean的时候需要注入另一个尚未初始化的依赖时,
容器就会调用createBean进行实例化
(2)设置对象属性(依赖注入)
实例化后的对象被封装在BeanWrapper对象中,
紧接着,Spring根据BeanDefinition中的信息
以及 通过BeanWrapper提供的设置属性的接口完成属性设置与依赖注入
(3)处理Aware接口
Spring会检测该对象是否实现了xxxAware接口,通过Aware类型的接口,可以让我们拿到Spring容器的一些资源
①如果这个Bean实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,传入Bean的名字
②如果这个Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例
3如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身
4如果这个Bean实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文
(4)BeanPostProcessor前置处理
如果想对Bean进行一些自定义的前置处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法
(5)InitializingBean
如果Bean实现了InitializingBean接口,执行afeterPropertiesSet()方法
(6)init-method
如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法
(7)BeanPostProcessor后置处理
如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术
(8)DisposableBean
当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法
(9)destroy-method
最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法
Spring框架中的设计模式
(1)工厂模式
Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象
(2)单例模式
Bean默认为单例模式
(3)策略模式
例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略
(4)代理模式
Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术
(5)模板方法
可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中,用来解决代码重复的问题。比如RestTemplate, JmsTemplate, JpaTemplate
(6)适配器模式
Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring MVC中也是用到了适配器模式适配Controller
(7)观察者模式
Spring事件驱动模型就是观察者模式的一个经典应用
(8)桥接模式
可以根据客户的需求能够动态切换不同的数据源。比如我们的项目需要连接多个数据库,客户在每次访问中根据需要会去访问不同的数据库
Spring中的隔离级别
① ISOLATION_DEFAULT
这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别
② ISOLATION_READ_UNCOMMITTED
读未提交,允许事务在执行过程中,读取其他事务未提交的数据
会脏读
③ ISOLATION_READ_COMMITTED
读已提交,允许事务在执行过程中,读取其他事务已经提交的数据
会产生不可重复读
④ ISOLATION_REPEATABLE_READ
可重复读,在同一个事务内,任意时刻的查询结果都是一致的
会产生幻读
⑤ ISOLATION_SERIALIZABLE
所有事务逐个依次执行
Spring的事务传播机制
① PROPAGATION_REQUIRED
默认传播行为:如果当前没有事务,就创建一个新事务;如果当前存在事务,就加入该事务
② PROPAGATION_REQUIRES_NEW
无论当前存不存在事务,都创建新事务进行执行
③ PROPAGATION_SUPPORTS
如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行
④ PROPAGATION_NOT_SUPPORTED
以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
⑤ PROPAGATION_NESTED
如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUIRED属性执行
⑥ PROPAGATION_MANDATORY
如果当前存在事务,就加入该事务;如果当前不存在事务,就抛出异常
⑦ PROPAGATION_NEVER
以非事务方式执行,如果当前存在事务,则抛出异常
Spring中Bean的作用域
1)singleton
默认作用域,单例bean,每个容器中只有一个bean的实例
(2)prototype
为每一个bean请求创建一个实例
(3)request
为每一个request请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收
(4)session
与request范围类似,同一个session会话共享一个实例,不同会话使用不同的实例
(5)global-session
全局作用域,所有会话共享一个实例。如果想要声明让所有会话共享的存储变量的话,那么这全局变量需要存储在global-session中
Spring IOC扩展
BeanPostProcessor
BeanPostProcessor是Spring IOC容器给我们提供的一个扩展接口。接口声明如下:
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
BeanFactoryPostProcessor
BeanFactoryPostProcessor接口和BeanPostProcessor原理一致,Spring提供了对BeanFactory进行操作的处理器BeanFactoryProcessor,简单来说就是获取容器BeanFactory,这样就可以在真正初始化bean之前对bean做一些处理操作。bean工厂的bean属性处理容器,说通俗一些就是可以管理我们的bean工厂内所有的beandefinition(未实例化)数据,可以随心所欲的修改属性。
简单来说就是在工厂里所有的bean被加载进来后但是还没初始化前,对所有bean的属性进行修改也可以add属性值。
简单来说就是在工厂里所有的bean被加载进来后但是还没初始化前,对所有bean的属性进行修改也可以add属性值。
@FunctionalInterface
public interface BeanFactoryPostProcessor {
void postProcessBeanFactory(ConfigurableListableBeanFactory var1) throws BeansException;
}
public interface BeanFactoryPostProcessor {
void postProcessBeanFactory(ConfigurableListableBeanFactory var1) throws BeansException;
}
InitializingBean
InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候都会执行该方法。
DisposableBean
在Bean生命周期结束前调用destory()方法做一些收尾工作
public interface DisposableBean {
void destroy() throws Exception;
}
void destroy() throws Exception;
}
spring mvc
原理
1、 用户发送请求至前端控制器DispatcherServlet。
2、 DispatcherServlet收到请求调用HandlerMapping处理器映射器。
3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet。
4、 DispatcherServlet调用HandlerAdapter处理器适配器。
5、 HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)。
6、 Controller执行完成返回ModelAndView。
7、 HandlerAdapter将controller执行结果ModelAndView返回给DispatcherServlet。
8、 DispatcherServlet将ModelAndView传给ViewReslover视图解析器。
9、 ViewReslover解析后返回具体View。
10、DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
11、 DispatcherServlet响应用户。
mybatis
dubbo
dubbo工作原理
第一层:service层,接口层,给服务提供者和消费者来实现的
第二层:config层,配置层,主要是对dubbo进行各种配置的
第三层:proxy层,服务代理层,透明生成客户端的stub和服务单的skeleton
第四层:registry层,服务注册层,负责服务的注册与发现
第五层:cluster层,集群层,封装多个服务提供者的路由以及负载均衡,将多个实例组合成一个服务
第六层:monitor层,监控层,对rpc接口的调用次数和调用时间进行监控
第七层:protocol层,远程调用层,封装rpc调用
第八层:exchange层,信息交换层,封装请求响应模式,同步转异步
第九层:transport层,网络传输层,抽象mina和netty为统一接口
第十层:serialize层,数据序列化层
工作流程:
1)第一步,provider向注册中心去注册
2)第二步,consumer从注册中心订阅服务,注册中心会通知consumer注册好的服务
3)第三步,consumer调用provider
4)第四步,consumer和provider都异步的通知监控中心
nacos
高可用
shardingsphere
原理
分片算法
精确分片算法
对应PreciseShardingAlgorithm,用于处理使用单一键作为分片键的=与IN进行分片的场景。需要配合StandardShardingStrategy使用。
范围分片算法
对应RangeShardingAlgorithm,用于处理使用单一键作为分片键的BETWEEN AND、>、<、>=、<=进行分片的场景。需要配合StandardShardingStrategy使用。
复合分片算法
对应ComplexKeysShardingAlgorithm,用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。需要配合ComplexShardingStrategy使用。
Hint分片算法
对应HintShardingAlgorithm,用于处理使用Hint行分片的场景。需要配合HintShardingStrategy使用。
分页原理
先查出来所有的数据, 再做归并排序
例如查询第2页时
原sql是:
select * from ORDER_00 where create_tm >= ? and create_tm <= ? limit 10 ,10 ;
select * from ORDER_01 where create_tm >= ? and create_tm <= ? limit 10 ,10 ;
select * from ORDER_02 where create_tm >= ? and create_tm <= ? limit 10 ,10 ;
会被改写成:
select * from ORDER_00 where create_tm >= ? and create_tm <= ? limit 0 ,20 ;
select * from ORDER_01 where create_tm >= ? and create_tm <= ? limit 0 ,20 ;
select * from ORDER_02 where create_tm >= ? and create_tm <= ? limit 0 ,20 ;
select * from ORDER_00 where create_tm >= ? and create_tm <= ? limit 10 ,10 ;
select * from ORDER_01 where create_tm >= ? and create_tm <= ? limit 10 ,10 ;
select * from ORDER_02 where create_tm >= ? and create_tm <= ? limit 10 ,10 ;
会被改写成:
select * from ORDER_00 where create_tm >= ? and create_tm <= ? limit 0 ,20 ;
select * from ORDER_01 where create_tm >= ? and create_tm <= ? limit 0 ,20 ;
select * from ORDER_02 where create_tm >= ? and create_tm <= ? limit 0 ,20 ;
查询第3页时
原sql是:
select * from ORDER_00 where create_tm >= ? and create_tm <= ? limit 20 ,10 ;
select * from ORDER_01 where create_tm >= ? and create_tm <= ? limit 20 ,10 ;
select * from ORDER_02 where create_tm >= ? and create_tm <= ? limit 20 ,10 ;
会被改写成:
select * from ORDER_00 where create_tm >= ? and create_tm <= ? limit 0 ,30 ;
select * from ORDER_01 where create_tm >= ? and create_tm <= ? limit 0 ,30 ;
select * from ORDER_02 where create_tm >= ? and create_tm <= ? limit 0 ,30 ;
select * from ORDER_00 where create_tm >= ? and create_tm <= ? limit 20 ,10 ;
select * from ORDER_01 where create_tm >= ? and create_tm <= ? limit 20 ,10 ;
select * from ORDER_02 where create_tm >= ? and create_tm <= ? limit 20 ,10 ;
会被改写成:
select * from ORDER_00 where create_tm >= ? and create_tm <= ? limit 0 ,30 ;
select * from ORDER_01 where create_tm >= ? and create_tm <= ? limit 0 ,30 ;
select * from ORDER_02 where create_tm >= ? and create_tm <= ? limit 0 ,30 ;
seata
核心组件:
事务协调器 TC
维护全局和分支事务的状态,指示全局提交或者回滚。
事务管理者 TM
开启、提交或者回滚一个全局事务。
资源管理者 RM
管理执行分支事务的那些资源,向TC注册分支事务、上报分支事务状态、控制分支事务的提交或者回滚。
TM 请求 TC,开始一个新的全局事务,TC 会为这个全局事务生成一个 XID。
XID 通过微服务的调用链传递到其他微服务。
RM 把本地事务作为这个XID的分支事务注册到TC。
TM 请求 TC 对这个 XID 进行提交或回滚。
TC 指挥这个 XID 下面的所有分支事务进行提交、回滚。
XID 通过微服务的调用链传递到其他微服务。
RM 把本地事务作为这个XID的分支事务注册到TC。
TM 请求 TC 对这个 XID 进行提交或回滚。
TC 指挥这个 XID 下面的所有分支事务进行提交、回滚。
重要机制
(1)全局事务的回滚是如何实现的呢?
Seata 有一个重要的机制:回滚日志。
每个分支事务对应的数据库中都需要有一个回滚日志表 UNDO_LOG,在真正修改数据库记录之前,都会先记录修改前的记录值,以便之后回滚。
在收到回滚请求后,就会根据 UNDO_LOG 生成回滚操作的 SQL 语句来执行。
如果收到的是提交请求,就把 UNDO_LOG 中的相应记录删除掉。
(2)RM 是怎么自动和 TC 交互的?
是通过监控拦截JDBC实现的,例如监控到开启本地事务了,就会自动向 TC 注册、生成回滚日志、向 TC 汇报执行结果。
(3)二阶段回滚失败怎么办?
例如 TC 命令各个 RM 回滚的时候,有一个微服务挂掉了,那么所有正常的微服务也都不会执行回滚,当这个微服务重新正常运行后,TC 会重新执行全局回滚。
springboot
原理
Spring Boot 核心功能
1)独立运行的 Spring 项目
Spring Boot 可以以 jar 包的形式独立运行,运行一个 Spring Boot 项目只需通过 java–jar xx.jar 来运行。
2)内嵌 Servlet 容器
Spring Boot 可选择内嵌 Tomcat、Jetty 或者 Undertow,这样我们无须以 war 包形式部署项目。
3)提供 starter 简化 Maven 配置
Spring 提供了一系列的 starter pom 来简化 Maven 的依赖加载,例如,当你使用了spring-boot-starter-web 时,会自动加入如图 1 所示的依赖包。
4)自动配置 Spring
Spring Boot 会根据在类路径中的 jar 包、类,为 jar 包里的类自动配置 Bean,这样会极大地减少我们要使用的配置。当然,Spring Boot 只是考虑了大多数的开发场景,并不是所有的场景,若在实际开发中我们需要自动配置 Bean,而 Spring Boot 没有提供支持,则可以自定义自动配置。
5)准生产的应用监控
Spring Boot 提供基于 http、ssh、telnet 对运行时的项目进行监控。
6)无代码生成和 xml 配置
Spring Boot 的神奇的不是借助于代码生成来实现的,而是通过条件注解来实现的,这是 Spring 4.x 提供的新特性。Spring 4.x 提倡使用 Java 配置和注解配置组合,而 Spring Boot 不需要任何 xml 配置即可实现 Spring 的所有配置。
starter
1、新建一个工程
2、pom依赖
3、定义一个实体类映射配置信息
4.定义一个Service
5,定义一个配置类
6.新建META-INF文件夹,然后创建spring.factories文件,
#-------starter自动装配---------
2 org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.demo.starter.config.DemoConfig
springcloud
服务注册发现组件Eureka工作原理
服务网关组件Zuul工作原理
断路器组件Hystrix工作原理
子主题
互联网工程
git
GIT打补丁
两个相同的repo:repo1和repo2,有修改合并
一是用git diff生成的UNIX标准补丁.diff文件
.diff文件只是记录文件改变的内容,不带有commit记录信息,多个commit可以合并成一个diff文件。
git diff 的使用方法:
创建:
git diff 【commit sha1 id】 【commit sha1 id】 > 【diff文件名】
打入:
git apply 【diff文件名】
创建:
git diff 【commit sha1 id】 【commit sha1 id】 > 【diff文件名】
打入:
git apply 【diff文件名】
二是git format-patch生成的Git专用.patch 文件。
.patch文件带有记录文件改变的内容,也带有commit记录信息,每个commit对应一个patch文件。
创建:
git format-patch HEAD^ // 最后一次提交补丁
git format-patch HEAD^^ // 最后两次提交补丁
git format-patch -1 // 最后一次提交补丁
git format-patch -2 // 最后两次提交补丁
打入:
git am 【diff文件名】
git format-patch HEAD^ // 最后一次提交补丁
git format-patch HEAD^^ // 最后两次提交补丁
git format-patch -1 // 最后一次提交补丁
git format-patch -2 // 最后两次提交补丁
打入:
git am 【diff文件名】
maven
idea
jekins
sonarQube
Nginx
Nginx是一个 轻量级/高性能的反向代理Web服务器,他实现非常高效的反向代理、负载平衡,他可以处理2-3万并发连接数,官方监测能支持5万并发,现在中国使用nginx网站用户有很多,例如:新浪、网易、 腾讯等。
Nginx性能这么高?
异步非阻塞事件处理机制:运用了epoll模型,提供了一个队列,排队解决
Nginx应用场景?
http服务器。Nginx是一个http服务可以独立提供http服务。可以做网页静态服务器。
虚拟主机。可以实现在一台服务器虚拟出多个网站,例如个人网站使用的虚拟机。
反向代理,负载均衡。当网站的访问量达到一定程度后,单台服务器不能满足用户的请求时,需要用多台服务器集群可以使用nginx做反向代理。并且多台服务器可以平均分担负载,不会应为某台服务器负载高宕机而某台服务器闲置的情况。
nginz 中也可以配置安全管理、比如可以使用Nginx搭建API接口网关,对每个接口服务进行拦截。
Nginx负载均衡的算法怎么实现的?策略有哪些?
1 轮询(默认)
每个请求按时间顺序逐一分配到不同的后端服务器,如果后端某个服务器宕机,能自动剔除故障系统。
upstream backserver {
server 192.168.0.12;
server 192.168.0.13;
}
2 权重 weight
weight的值越大分配
到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下。其次是为在主从的情况下设置不同的权值,达到合理有效的地利用主机资源。
upstream backserver {
server 192.168.0.12 weight=2;
server 192.168.0.13 weight=8;
}
到的访问概率越高,主要用于后端每台服务器性能不均衡的情况下。其次是为在主从的情况下设置不同的权值,达到合理有效的地利用主机资源。
upstream backserver {
server 192.168.0.12 weight=2;
server 192.168.0.13 weight=8;
}
3 ip_hash( IP绑定)
每个请求按访问IP的哈希结果分配,使来自同一个IP的访客固定访问一台后端服务器,并且可以有效解决动态网页存在的session共享问题
upstream backserver {
ip_hash;
server 192.168.0.12:88;
server 192.168.0.13:80;
}
upstream backserver {
ip_hash;
server 192.168.0.12:88;
server 192.168.0.13:80;
}
4 fair(第三方插件)
必须安装upstream_fair模块。
对比 weight、ip_hash更加智能的负载均衡算法,fair算法可以根据页面大小和加载时间长短智能地进行负载均衡,响应时间短的优先分配。
upstream backserver {
server server1;
server server2;
fair;
}
对比 weight、ip_hash更加智能的负载均衡算法,fair算法可以根据页面大小和加载时间长短智能地进行负载均衡,响应时间短的优先分配。
upstream backserver {
server server1;
server server2;
fair;
}
5、url_hash(第三方插件)
必须安装Nginx的hash软件包
按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。
upstream backserver {
server squid1:3128;
server squid2:3128;
hash $request_uri;
hash_method crc32;
}
按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,可以进一步提高后端缓存服务器的效率。
upstream backserver {
server squid1:3128;
server squid2:3128;
hash $request_uri;
hash_method crc32;
}
Zookeeper
高性能
寻找性能瓶颈
YSlow
yahoo的前端分析浏览器插件
firebug
firefox的插件
btrace
动态跟踪工具,能够快速定位和发现耗时方法
GC日志分析
JVM启动增加参数:-verbose:gc -Xloggc:/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps
PrintGCDetails表示输出GC详情
PrintGCDateStamps表示输出GC时间戳
数据库慢查询
show variables like 'log_slow_queries'
查看是否启用慢日志
show variables like 'long_query_time'
查看慢于多少秒的SQL会记录到日志
my.cnf
log_slow_queries = /var/log/mysql/mysql-slow.log
日志地址
long_query_time=1
慢于多少秒记录
性能测试工具
ab:ApacheBench
ab [options] [http[s]://]localhost[:port]/path
-n
请求次数
-c
并发数
JMeter
使用JMeter进行压测的时候,可以使用jconsole、VisualVM等工具查看CPU和内存使用情况
LoadRunner
HP
商业未开源
反向代理引流
nginx调整负载权重
upstream
TCPCopy
请求复制工具,将在线请求复制到测试机器,模拟真实环境
TCPCopy Client
运行在线上,用来捕获在线请求数据包
TCPCopy Server
运行在线下测试机,用来截获响应包,并将响应包的头部信息传递给TCPCopy Client,以完成TCP交互
WEB前端
浏览器访问优化
浏览器缓存
设置http头中的Cache-Control
设置http头中的Expires
页面压缩
合理布局
CSS放在页面最上面
js放在最下边
减少cookie传输
减少数据量
静态资源独立域名
减少Http请求
合并css
合并js
合并图片
CDN加速
缓存静态资源,如图片、文件等
反向代理
动静分离
js、css等文件独立部署,使用专门的域名
图片服务
用户上传,独立部署
反向代理
提供页面缓存
DNS
DNS负载均衡
应用服务器
异步操作
削峰
加快响应速度
使用集群
代码优化
线程池
Future模式
新建线程异步获取数据,执行完一段逻辑后,使用线程对象的get()方法可获取线程内的执行结果,如未获取到,则线程阻塞
NIO
Selector 非阻塞IO机制
资源复用
单例模式
对象池
减少上下文切换
当可运行的线程大于CPU数量,则正在运行的某个线程可能就会被挂起,增加调度开销
锁竞争激烈也会导致上下文切换频繁
尽可能的缩短锁持有的时间
减少锁的粒度
将使用单独锁保护多个变量变为采用独立的锁分别进行保护
放弃使用独占锁,使用读写锁
ReentrantReadWriteLock
JVM
JVM内存结构
堆heap
存储对象的内存空间,对象的创建和释放、垃圾回收都在这里进行
是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。
分类
年轻代
Eden空间
From Survivor空间
To Survivor空间
默认比例为 8:1:1
老年代
方法区(Method Area)/永久代(PermGen)
与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
程序计数器(Program Counter Register)
是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器。
栈
JVM栈(JVM Stacks)
存储线程上下文信息,如方法参数、局部变量等
与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
本地方法栈(Native Method Stacks)
与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。
总结:方法区和堆是所有线程共享的内存区域;而Java栈、本地方法栈和程序计数器是运行是线程私有的内存区域。
总结:方法区和堆是所有线程共享的内存区域;而Java栈、本地方法栈和程序计数器是运行是线程私有的内存区域。
对象分配规则
对象优先分配在Eden区,如果Eden区没有足够的空间时,虚拟机执行一次Minor GC。
大对象直接进入老年代(大对象是指需要大量连续内存空间的对象)。这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝(新生代采用复制算法收集内存)
长期存活的对象进入老年代。虚拟机为每个对象定义了一个年龄计数器,如果对象经过了1次Minor GC那么对象会进入Survivor区,之后每经过一次Minor GC那么对象的年龄加1,知道达到阀值对象进入老年区。
动态判断对象的年龄。如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代
空间分配担保。每次进行Minor GC时,JVM会计算Survivor区移至老年区的对象的平均大小,如果这个值大于老年区的剩余值大小则进行一次Full GC,如果小于检查HandlePromotionFailure设置,如果true则只进行Monitor GC,如果false则进行Full GC。
JVM垃圾回收
新生代
Eden Space
新建对象总是在该区创建,当空间满,触发Young GC,将还未使用的独享复制到From区,Eden区则清空
From
当Eden区再次用完,再触发Young GC,将Eden和From区未使用的对象复制到To区
To
当Eden区再次用完,触发Young GC,将Eden和To区未使用的对象复制到From区,如此反复
老年代
经过多次Young GC,某些对象会在From和To多次复制,如果超过某个阈值对象还未释放,则将该对象复制到老年区。如果老年区空间用完,就会触发Full GC
垃圾回收器
Serial收集器
串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长的停顿,只使用一个线程去回收。
-XX:+UseSerialGC 串行收集器
ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,新生代并行,老年代串行;新生代复制算法、老年代标记-压缩
-XX:+UseParNewGC ParNew收集器
-XX:ParallelGCThreads 限制线程数量
Parallel收集器
Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量
-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行(并行收集器)
Parallel Old 收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法
-XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行(并行老年代收集器)
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器
优点:并发收集、低停顿
缺点:产生大量空间碎片、并发阶段会降低吞吐量
-XX:+UseConcMarkSweepGC 使用CMS收集器(并发收集器)
-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)
G1收集器
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征
特点
空间整合,G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
GC优化
堆设置
-Xmn:年轻代大小
-XX:NewRatio=n:设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4
-XX:SurvivorRatio=n:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5
子主题
垃圾回收统计信息
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:filename
tomcat线程优化
maxThreads
Tomcat使用线程来处理接收的每个请求。这个值表示Tomcat可创建的最大的线程数。默认值150
acceptCount
指定当所有可以使用的处理请求的线程数都被使用时,可以放到处理队列中的请求数,超过这个数的请求将不予处理。默认值10
minSpareThreads
Tomcat初始化时创建的线程数。默认值25
maxSpareThreads
一旦创建的线程超过这个值,Tomcat就会关闭不再需要的socket线程。默认值75
enableLookups
是否反查域名,默认值为true。为了提高处理能力,应设置为false
connnectionTimeout
网络连接超时,默认值60000,单位:毫秒。设置为0表示永不超时,这样设置有隐患的。通常可设置为30000毫秒
maxKeepAliveRequests
保持请求数量,默认值100。 bufferSize: 输入流缓冲大小,默认值2048 bytes
compression
压缩传输,取值on/off/force,默认值off。 其中和最大连接数相关的参数为maxThreads和acceptCount。如果要加大并发连接数,应同时加大这两个参数
32G 内存配置示例:
```
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000" maxThreads="1000" minSpareThreads="60" maxSpareThreads="600" acceptCount="120"
redirectPort="8443" URIEncoding="utf-8"/>
```
存储性能优化
RAID
RAID0
将磁盘数量分成N份,数据同时并发写入N块磁盘,速度快,缺点是没有备份
RAID1
一份数据同时写入两块磁盘,有备份
RAID10
RAID0与RAID1的结合,缺点是磁盘利用率低
RAID3
数据分成N-1份,并发写入N-1块磁盘,在第N块磁盘记录校验数据,缺点是第N块磁盘在频繁修改情况下容易损坏,实践中很少使用
RAID5
与RAID3不同的是,将第N块磁盘记录校验的数据写入所有的磁盘,避免同时写坏一个磁盘
RAID6
类似RAID5,但是数据只写入到N-1块磁盘,螺旋式的将校验数据写入在两块磁盘中
存储算法
B+树(传统关系型数据库)
LSM树(NOSQL)
HDFS分布式文件系统
NameNode,只有一个实例,负责数据分区的分配
DataNode,真正存储数据的存储节点
关系型数据库
读写分离
分库
问题
join操作问题
事物问题
成本问题
单台数据库服务器一般来说能支撑10万用户量级的业务
分表
垂直分表
将表中某些不常用且占了大量空间的列拆分出去
水平分表
参考:单表超过五千万条建议分表
路由算法
范围路由
选取有序的数据列作为路由条件,不同分段分散到不同的数据库表中
复杂点在于分段大小的选取上,建议分段大小在100万到2000万之间
优点:可随数据的增加平滑地扩充新的表
缺点:分布不均匀。新扩充的表数据量一开始很少
Hash路由
复杂点在于初始表数量的选取上
优点:表分布比较均匀
缺点:扩充新表很麻烦,数据需要重新分布
配置路由
新建一张单独的表记录路由信息
优点:设计简单,扩充表时只需要迁移指定数据,修改路由表
缺点:必须多查询一次路由表,影响整体性能
问题
join操作
进行多个表的join查询,将结果合并
count()操作
count相加
对每个表进行count,最后结果相加
缺点:性能低
记录数表
新建表,包含table_name,row_count两个字段
每次插入或删除成功后,更新记录数表,缺点是增加性能开销
不要求精确的业务,可通过定时任务更新
order by操作
分别查询每个子表中的数据,然后汇总进行排序
实现方式
程序代码封装
TDDL(淘宝)
ShardingJDBC(当当)
中间件
mycat
mysql router
atlas(奇虎360)
mysql
存储引擎
myisam(B树)
表锁策略
innodb(B+树)
行锁
性能优化
合理使用索引
explain
分析执行语句
条件左侧避免使用表达式
like关键字需要进行全表扫描
“最左前缀”原则,即组合索引,如果前面的索引没有命中,后面的索引即无效
反范式设计
冗余很少变化的关联字段
使用查询缓存
select @@query_cache_type;
使用搜索引擎
针对于分表分库的查询
使用key-value数据库
NoSQL
Redis(K-V存储)
MongoDB(文档数据库)
HBase(列式数据库)
Elasticsearch(全文搜索引擎)
分布式缓存
场景
结果缓存
需要经过复杂运算得到的数据
读多写少,很少变化
不适合缓存
频繁修改的数据
没有热点的数据
设置失效时间
缓存雪崩
更新锁
分布式环境下,对缓存更新操作进行加锁保护,保证只有一个线程能够进行操作
后台更新
缓存有效期设置为永久,后台定时更新
通过定时读取缓存,判断缓存是否存在
业务线程发现缓存丢失后,通过消息队列通知后台
缓存预热
系统上线时将热点数据加载好
缓存穿透
存储数据不存在:造成数据库压力,应对策略是将不存在的数据缓存起来
数据生成耗费大量时间和资源
缓存热点
很多业务请求都命中同一份缓存
解决方案是复制多份缓存,将请求分散到多台服务器
文件存储
小文件
HBase
Hadoop
Hypertable
FastDFS
未开源:TFS(淘宝)、JFS(京东)、Haystack(Facebook)
大文件
Hadoop
HBase
Storm
Hive
安全性
XSS跨站脚本攻击
防御
消毒:特殊字符进行转译
HttpOnly:对于存放敏感信息的cookie,可通过对该Cookie添加HttpOnly属性
分类
反射型:发布带攻击的链接
持久型:将攻击信息存到服务器数据库中
注入攻击
分类
SQL注入
OS注入
防御
消毒:特殊字符转译
参数绑定:sql预编译和参数绑定
CSRF跨站点请求伪造
核心:利用了浏览器的Cookie或服务器Session策略
防御
表单Token
验证码
Referer check
将cookie设置为HttpOnly
DDoS
SYN Flood
基于TCP的三次握手,攻击者伪造大量IP地址给服务器发送SYN报文,导致服务器接收不到客户端的ACK,服务器需要分配资源来维护本次握手,并不断重试。当等待队列占满后,服务器不再接收新的SYN请求。
DNS Query Flood
向被攻击的服务器发送海量的域名解析请求
CC
基于HTTP协议发起,通过控制大量肉鸡和互联网上大量的代理,模拟正常用户给网站发起请求,直到网站拒绝服务。
从应用层发起,与网站的业务紧密相连,使防守方进行过滤的时候进行大量的误杀,真正业务无法处理。
其他攻击手段
Error Code
程序内部错误,浏览器打印堆栈信息
防御:配置Web服务器参数,跳转到500的专用错误页面
Html注释
注释显示在客户端,给黑客攻击造成便利
防御:代码Review,删除注释
文件上传
利用文件上传功能上传可执行程序,进而控制服务器
防御:设置上传文件白名单,只允许上传可靠文件类型,判断文件上传类型,使用“魔数”
文件单独存储
路径遍历
在请求的URL中使用相对路径,遍历未开放的目录及文件
防御:静态资源独立部署,其他资源不使用静态URL访问,动态参数不包含文件路径信息
防火墙
ModSecurity,开源
SiteShell
深信服
加解密
单向散列加密(摘要算法)
MD5
SHA-1
对称加密
加密与解密使用同一个密匙,远程通信的密匙交换是难题
DES
56位
3DES
AES
AES-128
AES-192
AES-256
RC
非对称加密
算法RSA
位数越大,加解密速度越慢
信息安全传输:公钥加密,私钥解密
数字签名(不可抵赖):私钥加密,公钥解密
发送发:将内容生成摘要,再用私钥将摘要进行加密生成数字签名,最后将数字签名和原文内容传输给接收方。
接收方:将原文内容采用相同的摘要算法生成摘要,再用公钥将数字签名解密成发送方的摘要,最后将两个摘要对比即可确认真伪。
算法
MD5withRSA
SHA1withRSA
数字证书
证书管理
keytool
Java的数字证书管理工具
OpenSSL
进行证书的签发与证书链的管理
故障排查
命令
jps
输出JVM虚拟机进程的一些信息,可以列出虚拟机当前执行的进程,并显示其主类和进程的ID
jstat
对虚拟机各种运行状态进行监控的工具,可以查看虚拟机的类加载和卸载情况,管理内存的使用和垃圾收集等信息
jinfo
查看应用程序的配置参数,以及打印运行JVM时所指定的JVM参数
jstack
生成虚拟机当前快照信息,线程快照是当前虚拟机每一个线程正在执行的方法堆栈的集合
jmap
用来查看等待回收对象的队列,查看堆的概要信息,包括采用的哪种GC收集器,堆空间的使用情况,以及通过JVM参数指定的各个内存空间的大小。
可以dump当前堆快照,并使用Memory Analyzer分析
BTrace
开源的Java程序动态跟踪工具,动态查看程序运行的细节,方便对程序进行调试
JConsole
JDK内置的图形化性能分析工具,对运行的java程序的性能及资源消耗情况进行分析和监控,提供可视化的图表对相关数据进行展现
Memory Analyzer(MAT)
堆分析工具,能够快速找到占用堆内存空间最大的对象
Eclipse插件,亦可独立客户端运行
VisualVM
功能十分强大,通过插件组装,可完成:内存监控、GC监控、应用程序分析、线程分析、堆dump分析、CPU,以及内存抽样、BTrace跟踪等。
重点推荐使用
远程配置方式
vi catalina.sh
找到如下内容“#—–Execute The Requested Command”,在其上添加以下配置,后重启tomcat
```
CATALINA_OPTS="$CATALINA_OPTS
-Dcom.sun.management.jmxremote
-Djava.rmi.server.hostname=192.168.23.1
-Dcom.sun.management.jmxremote.port=9999
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
```
数据分析
方案一
日志收集
inotify机制
异步传输
ActiveMQ-CPP
实时处理
Storm
实时分布式流处理系统
存储
MySql
Hbase
高可用,高性能,可伸缩的列存储系统,支持数据表自动分区
Memcahe
HDFS
无需提供实时访问,通过如下工具进行分析与挖掘
MapReduce
Hive SQL
方案二
Chukwa
Agent
节点上采集原始数据,发送给Collector
Collector
ETL
数据解析与归档
PigLatin,MapReduce(任务)
数据分析
HICC
页面展示
hadoop
HDFS
分布式文件系统
MapReduce
大规模数据处理的编程模型
map
reduce
zookeeper
分布式协作服务
Hbase
分布式数据库,支持大表结构化存储
Hive
数据仓库平台,提供类sql查询
HiveQL
元数据库
存储在关系型数据库中,一般包含Hive的表属性,桶信息和分区信息
可以将日志从HDFS导入到Hive表中,然后通过SQL语句进行统计,比MapReduce方便很多。
Chukwa
分布式系统的数据收集系统
Pig
海量数据并行计算的编程语言和执行框架
Mahout
可扩展的机器学习和数据挖掘库
Storm
流式数据分析,实时处理任务
Topology
spout数据流输入
bolt对数据做解析
Sqoop
离线数据同步,支持将关系型数据库导入到HDFS,也支持将HDFS数据导入到关系型数据库
使用MapReduce来执行数据导入导出任务
实时数据同步方案
Binary log parser将自己伪装成MySQL的Slave,像Master发送sump请求,Master收到请求后,会将Binary log发送给parser,通过解析还原出变更对象,将其发送到ActiveMQ的topic上,即可实现数据实时传输
高可用
高可用应用
负载均衡进行故障转移
集群的session管理
同IP会话黏滞
session服务器
redis共享session
高可用服务
分级管理
核心服务优先使用更好的硬件
不同级别的服务需要隔离,避免故障连锁反应
超时设置
超时后,通信框架抛出异常,程序选择继续重试或失效转移
异步调用
消息队列
防止关联的服务互相影响
服务降级
拒绝服务
拒绝低优先级应用的调用,减少服务调用并发数
随机拒绝
关闭功能
关闭部分不重要的服务
关闭服务内部部分不重要的功能
实施方式
基于Java的信号量机制:Semaphore
调用超时次数超过阈值,自动降级,后续来的流量直接拒绝,等超过休眠时间点,再次对服务进行重试
幂等性设计
重复调用和调用一次必须保证结果相同
异地多活机房
保证核心业务异地多活
例:注册、登录、用户信息,只有登录才是核心业务
核心数据最终一致性
尽量减少数据同步,只同步核心业务数据
保证最终一致性,不保证实时一致性
采用多种手段同步数据
消息队列
二次读取
第一次读取本地失败后,根据路由规则去另外一个机房读取
存储系统同步方式
Mysql同步
回源读取
session id不同步,本地机房没有该id的情况下,根据路由去另外一个机房获取
重新生成数据
session id在另外一个机房未获取到,则让用户重新登录
保证大部分用户的异地多活
高可用数据
数据失效转移机制
失效确认
访问转移
数据恢复
CAP
一个提供数据服务的存储系统无法同时满足数据一致性、数据可用性、分区容忍性,通常会牺牲数据一致性
一致性(Consistency)
对指定的某个客户端来说,读操作保证能够返回最新的写操作结果
可用性(Availability)
非故障的节点在合理的时间内返回合理的响应(非错误和超时的响应)
分区容忍性(Partition Tolerance)
当出现网络分区(网络故障)后,系统能够继续“履行职责”
ACID
数据库约束
Atomicity原则性
Consistency一致性
Isolation隔离性
Durability持久性
BASE
Basically Available基本可用
分布式系统在出现故障时,允许损失部分可用性,即保证核心可用
Soft State软状态
Eventual Consistency最终一致性
是CAP理论中AP方案的延伸,即使无法做到强一致性,但应用可以采取适合的方式达到最终一致性
高可用存储
数据备份
冷备
热备
异步热备
同步热备
主备复制
主从复制
主机负责读写,从机只负责读
主主复制
对数据的设计有严格的要求,一般适合临时、可丢失、可覆盖的数据场景
数据集群
数据集中集群
一主多备
一主多从
数据分散集群
Hadoop的HDFS存储系统
独立的服务器负责数据分区的分配(Namenode)
Elasticsearch
选举一台服务器做数据分区的分配(master node)
分布式事务算法
2PC,强一致性算法
Commit请求阶段
Commit提交阶段
3PC
提交判断阶段
准备提交阶段
提交执行阶段
分布式一致性算法
Paxos
纯理论算法,特别复杂,是其他算法的鼻祖
Raft
为工程实践而设计,Paxos算法的不完整版
ZAB
自动化测试
工具:selenium
接口性能工具:Jmeter,loadrunner
监控
数据采集
用户行为
服务器性能
系统load
内存占用
磁盘IO
网络IO
运行数据
缓存命中率
平均响应延迟时间
每分钟发送邮件数目
待处理任务总数
监控管理
系统报警
失效转移
自动优雅降级
Linux命令
日志查看
cat
cat -n access.log 显示行数
more
Enter:下一行,空格:下一页,F:下一屏,B:上一屏
less
sort
对内容进行排序
-n
数字从小到大排列
-r
数字从大到小排列
-k
指定排序的列
-t
指定分隔符
wc
统计指定文件的字符数、字数、行数
-l
统计行数
uniq
查看重复出现的行
sort uniqfile | uniq -c
-c:每一行前面加上该行出现的次数
常用脚本
页面访问量前十的f1列
cat access.log | cut -f1 -d " " | sort |uniq -c | sort -k 1 -n -r |head -10
查看最耗时的页面
cat access.log | sort -k 2 -n -r | head -10
系统监控
uptime
查看系统的load
一般来说,load不大于3,表示负载正常,load大于5,负载压力过高
top | grep Cpu
st
us
表示CPU执行用户进程所占的时间,越高越好
sy
在内核态所花费的时间,越低越好
ni
系统在调整进程优先级花费的时间
id
系统空闲时间
wa
CPU在等待IO操作所花费的时间,越低越好
hi
系统处理硬件中断所占用的时间
si
系统处理软件中断所占用的时间
top -p 进程号
查看指定进程
du-d 1 -h
查看磁盘剩余空间,-d 1表示递归深度
sar -n DEV 1 1
查看系统的网络状况
iostat -d -k
查看系统的IO
free -m
查看内存使用情况
swap过高,表示物理内存不够用
vmstat
查看当前swap的IO情况
扩展性
核心思想:模块化
降低模块间的耦合性
提高模块的复用性
分层和分割,以消息传递和依赖调用聚合成完整系统
分布式队列
分布式服务
伸缩性
网站架构伸缩
不同功能物理分离
相同功能集群伸缩
负载均衡器
DNS域名解析负载均衡,缺点是失效转移存在一定的时效性
反向代理(应用层负载均衡)
NGINX,七层负载,5万/每秒
数据链路层负载均衡
LVS,四层负载,80万/每秒
负载均衡算法
轮询
加权轮询
负载最低优先
LVS可以以连接数来判断
Nginx可以以HTTP请求数来判断
性能最优
响应最快,性能最优
原地址散列(哈希)
分布式缓存集群
一致性性Hash算法
数据存储服务器集群
关系型数据库集群
读写分离
分库
不同业务部署在不同的集群
分片
单表拆分,存在多个库中
支持数据分片的中间件:Cobar,缺点是只能在单一数据库实例上处理查询请求,无法执行跨库的Join操作
NoSql
HBase
以HRegion为单位进行管理
HDFS分布式文件系统
高并发
常见指标
响应时间(Response Time)
吞吐量(Throughput)
单位时间内处理的请求数量
每秒查询率QPS(Query Per Second)
每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显
并发用户数
同时承载正常使用系统功能的用户数量
如何提升并发
垂直扩展,提升单机处理能力
增强单机硬件性能
提升单机架构性能,例如:使用Cache来减少IO次数,使用异步来增加单服务吞吐量,使用无锁数据结构来减少响应时间
水平扩展(Scale Out)
增加服务器数量,就能线性扩充系统性能
反向代理层的水平扩展,是通过“DNS轮询”实现的
站点层的水平扩展,是通过“nginx”实现的
服务层的水平扩展,是通过“服务连接池”实现的(注册中心)
数据库水平拆分方式
按照范围水平拆分
优点
服务层的水平扩展,是通过“服务连接池”实现的
数据均衡性较好
比较容易扩展,可以随时加一个uid[2kw,3kw]的数据服务
缺点
请求的负载不一定均衡,一般来说,新注册的用户会比老用户更活跃,大range的服务请求压力会更大
按照哈希水平拆分
优点
规则简单,service只需对uid进行hash能路由到对应的存储服务
数据均衡性较好
请求均匀性较好
缺点
不容易扩展,扩展一个数据服务,hash方法改变时候,可能需要进行数据迁移
负载均衡
LVS
位于传输层的四层负载
方式
修改IP地址(NAT)
NAT(Network Address Translation)是一种外网和内网地址映射的技术
源地址修改 SNAT(请求返回)
目标地址修改 DNAT(请求进入)
修改目标 MAC(DR 模式)
请求由LVS接收,由真实服务器直接返回给用户
LVS转发过程中,只修改mac地址,不修改ip地址
DR 模式具有较好的性能,也是目前大型网站使用最广泛的一种负载均衡手段
优点
抗负载能力强、是工作在传输层上仅作分发之用,没有流量的产生,这个特点也决定了它在负载均衡软件里的性能最强的,对内存和 cpu 资源消耗比较低。
配置性比较低,这是一个缺点也是一个优点,因为没有可太多配置的东西,所以并不需要太多接触,大大减少了人为出错的几率。
工作稳定,因为其本身抗负载能力很强,自身有完整的双机热备方案,如 LVS + Keepalived。
无流量,LVS 只分发请求,而流量并不从它本身出去,这点保证了均衡器 IO 的性能不会受到大流量的影响。
应用范围比较广,因为 LVS 工作在传输层,所以它几乎可以对所有应用做负载均衡,包括 http、数据库、在线聊天室等等。
缺点
软件本身不支持正则表达式处理,不能做动静分离;而现在许多网站在这方面都有较强的需求,这个是 Nginx、HAProxy + Keepalived 的优势所在
如果是网站应用比较庞大的话,LVS/DR + Keepalived 实施起来就比较复杂了,相对而言,Nginx / HAProxy + Keepalived 就简单多了
Nginx
以反向代理的方式进行负载均衡的,通过upstream来实现
方式
轮询(默认):每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器 down 掉,能自动剔除。
weight:指定轮询几率,weight 和访问比率成正比,用于后端服务器性能不均的情况。
ip_hash:每个请求按访问 ip 的 hash 结果分配,这样每个访客固定访问一个后端服务器,可以解决 session 的问题。
fair(第三方):按后端服务器的响应时间来分配请求,响应时间短的优先分配。
url_hash(第三方):按访问 url 的 hash 结果来分配请求,使每个 url 定向到同一个后端服务器,后端服务器为缓存时比较有效。
优点
跨平台:Nginx 可以在大多数 Unix like OS编译运行,而且也有 Windows 的移植版本
配置异常简单:非常容易上手。配置风格跟程序开发一样,神一般的配置
非阻塞、高并发连接:官方测试能够支撑5万并发连接,在实际生产环境中跑到2~3万并发连接数
事件驱动:通信机制采用 epoll 模型,支持更大的并发连接
Master/Worker 结构:一个 master 进程,生成一个或多个 worker 进程
内存消耗小:处理大并发的请求内存消耗非常小。在3万并发连接下,开启的10个 Nginx 进程才消耗150M 内存(15M*10=150M)
内置的健康检查功能:如果 Nginx 代理的后端的某台 Web 服务器宕机了,不会影响前端访问
节省带宽:支持 GZIP 压缩,可以添加浏览器本地缓存的 Header 头
稳定性高:用于反向代理,宕机的概率微乎其微
缺点
Nginx 仅能支 持http、https 和 Email 协议,这样就在适用范围上面小些
对后端服务器的健康检查,只支持通过端口来检测,不支持通过 ur l来检测。不支持 Session 的直接保持,但能通过 ip_hash 来解决
HAProxy
对后端服务器的健康检查,只支持通过端口来检测,不支持通过 ur l来检测。不支持 Session 的直接保持,但能通过 ip_hash 来解决
HAProxy 的优点能够补充 Nginx 的一些缺点,比如支持 Session 的保持,Cookie 的引导;同时支持通过获取指定的 url 来检测后端服务器的状态
HAProxy 跟 LVS 类似,本身就只是一款负载均衡软件;单纯从效率上来讲 HAProxy 会比 Nginx 有更出色的负载均衡速度,在并发处理上也是优于 Nginx 的
HAProxy 支持 TCP 协议的负载均衡转发,可以对 MySQL 读进行负载均衡,对后端的 MySQL 节点进行检测和负载均衡,大家可以用 LVS+Keepalived 对 MySQL 主从做负载均衡
HAProxy 负载均衡策略非常多:Round-robin(轮循)、Weight-round-robin(带权轮循)、source(原地址保持)、RI(请求URL)、rdp-cookie(根据cookie)
方法论
方案设计
分析问题
方案设计的方法论
本质论
矛盾论
系统论
演进论
总结
具体的案例验证
1 高并发技术方案
2 异步处理技术方案
复杂业务
复杂业务代码要怎么写:即自上而下的结构化分解+自下而上的面向对象分析
上下结合
所谓上下结合,是指我们要结合自上而下的过程分解和自下而上的对象建模,螺旋式的构建我们的应用系统。这是一个动态的过程,两个步骤可以交替进行、也可以同时进行。
这两个步骤是相辅相成的,上面的分析可以帮助我们更好的理清模型之间的关系,而下面的模型表达可以提升我们代码的复用度和业务语义表达能力。
能力下沉
一般来说实践DDD有两个过程:
1. 套概念阶段
了解了一些DDD的概念,然后在代码中“使用”Aggregation Root,Bonded Context,Repository等等这些概念。更进一步,也会使用一定的分层策略。然而这种做法一般对复杂度的治理并没有多大作用。
2. 融会贯通阶段
所谓的能力下沉,是指我们不强求一次就能设计出Domain的能力,也不需要强制要求把所有的业务功能都放到Domain层,而是采用实用主义的态度,即只对那些需要在多个场景中需要被复用的能力进行抽象下沉,而不需要复用的,就暂时放在App层的Use Case里就好了。
注:Use Case是《架构整洁之道》里面的术语,简单理解就是响应一个Request的处理过程
通过实践,我发现这种循序渐进的能力下沉策略,应该是一种更符合实际、更敏捷的方法。因为我们承认模型不是一次性设计出来的,而是迭代演化出来的。
下沉的过程如下图所示,假设两个use case中,我们发现uc1的step3和uc2的step1有类似的功能,我们就可以考虑让其下沉到Domain层,从而增加代码的复用性。
注:Use Case是《架构整洁之道》里面的术语,简单理解就是响应一个Request的处理过程
通过实践,我发现这种循序渐进的能力下沉策略,应该是一种更符合实际、更敏捷的方法。因为我们承认模型不是一次性设计出来的,而是迭代演化出来的。
下沉的过程如下图所示,假设两个use case中,我们发现uc1的step3和uc2的step1有类似的功能,我们就可以考虑让其下沉到Domain层,从而增加代码的复用性。
复杂业务:深入理解
业务代码思考与实现
自上而下的结构化分解
自下而上的面向对象分析
业务理解
矩阵思维(多维度思考)
多态扩展
利用面向对象的多态特性,实现代码的复用和扩展
继承方式
组合方式
把需要扩展的部分封装、抽象成需要被组合的对象,然后对其进行扩展,比如星环的能力扩展点就是这种方式
代码分离
对不同的场景,使用不同的流程代码实现。这样很清晰,但是可维护性不好。
问题来了,我们什么时候要用多态来处理差异,什么时候要用代码分离来处理差异呢?
弄一个矩阵,纵列代表业务场景,横列代表业务动作,里面的内容代表在这个业务场景下的业务动作的详细业务流程。
分支主题
我们不难看出普通品和组合品可以复用同一套流程编排代码,而赠品和出清品的业务相对简单,更适合有一套独立的编排代码,这样的代码结构会更容易理解。
如何分析
波士顿矩阵
矩阵表
分支主题
RFM模型
分支主题
复杂业务治理总结
业务理解
找到业务的核心要素,理解核心概念,梳理业务流程
领域建模
在软件设计中,模型是指实体,以及实体之间的联系,这里需要我们具备良好的抽象能力。能够透过庞杂的表象,找到事务的本质核心。
分支主题
流程分解
流程分解就是对业务过程进行详细的分解,使用结构化的方法论(先演绎、后归纳),最后形成一个金字塔结构。
分支主题
矩阵分析
业务的复杂性主要体现在流程的复杂性和多维度要素相互关联、依赖关系上,结构化思维可以帮我们梳理流程,而矩阵思维可以帮忙我们梳理、呈现多维度关联、依赖关系。二者结合,可以更加全面的展现复杂业务的全貌。从而让我们的治理可以有的放矢、有章可循。
既然是方法论,在这里,我会尝试给出一个矩阵分析的框架。试想下,如果我们的业务很简单,只有一个业务场景,没有分支流程。我们的系统不会太复杂。之所以复杂,是因为各种业务场景互相叠加、依赖、影响。
因此,我们在做矩阵分析的时候,纵轴可以选择使用业务场景,横轴是备选维度,可以是受场景影响的业务流程(如文章中的商品流程矩阵图),也可以是受场景影响的业务属性(如文章中的订单组成要素矩阵图),或者任何其它不同性质的“东西”。
分支主题
后续
分支主题
心力是指不将就的匠心,不妥协的好奇心,不放弃的恒心。
脑力是指那些必要的思维能力、学习能力、思考能力、思辨能力。
之所以说“业务理解-->领域建模-->流程分解-->矩阵分析”是体力,是因为实现它们就像是在做填空题,只要你愿意花时间,再复杂的业务都可以按部就班的清晰起来。
软技能
不要做宅男;
和面试官成为好朋友后再去面试(结果你懂的);
如何成为自由职业者;
假装自己能成功;
打造自身品牌:坚持写博客;
写博客
社交媒体
演讲、培训别人
写书
有效管理时间以提升效率;
学会理财:要善于炒股炒房(炒股在中国可能不算理财算赌博);
不要刷爆信用卡(这个问题可能美国人比较严重);
少看电视多运动,争取练成肌肉男。
如何和产品/测试互怼
代码重构
什么是重构
Refactoring是对软件内部结构的一种调整,目的是在不改变外部行为的前提下,提高其可理解性,降低其修改成本。
为什么重构
改进软件的设计
程序员对代码所做的为了满足短期利益代码改动,或再没有完全清楚整个架构下的改动,都很容易是代码失去它的清晰结构,偏离需求或设计。而这些改动的积累很容易使代码偏离它原先设计的初衷而变得不可理解和无法维护。
Refactoring则帮助重新组织代码,重新清晰的体现结构和进一步改进设计。
提高代码质量,更易被理解
容易理解的代码可以很容易的维护和做进一步的开发。即使对写这些代码的程序员本身,容易理解代码也可以帮助容易地做修改。程序代码也是文档。而代码首先是写给人看的,然后才是给计算机看的。
Refactoring帮助尽早的发现错(Bugs)
Refactoring是一个code review和反馈的过程。在另一个时段重新审视自己或别人代码,可以更容易的发现问题和加深对代码的理解。
Refactoring是一个良好的软件开发习惯。
Refactoring可以提高开发速度
Refactoring对设计和代码的改进,都可以有效的提高开发速度。好的设计和代码质量是提高开发速度的关键。在一个有缺陷的设计和混乱代码基础上的开发,即使表面上进度较快,但本质是延后了对设计缺陷的发现和对错误的修改,也就是延后了开发风险,最终要在开发的后期付出更多的时间和代价。
项目的维护成本远高于开发成本。
何时重构?
添加新功能时一并重构
为了增加一个新的功能,程序员需要首先读懂现有的代码。
修补错误时一并重构
为了修复一个Bug,程序员需要读懂现有的代码。
Code Review时一并重构
何时不该重构?
代码太混乱,设计完全错误。与其Refactor,不如重写。
明天是DeadLine
永远不要做Last-Minute-Change。推迟Refactoring,但不可以忽略,即使已经正式发布的代码都正确的运行。
Refactoring的工作量显著的影响最后期限
一个Task的计划是3天,如果为了Refactoring,需要更多的时间( 2天或更多)。推迟Refactoring,同步可以忽略。可以把这个Refactoring作为一个新的Task,或者安排在二次Refactoring中完成。
两顶帽子
[重构]与[添加新功能]
添加新功能时,你不应该修改既有代码,只管添加新功能。
重构时你就不能再添加功能,只管改进程序结构。此外你不应该添加任何测试(除非发现有先前遗漏的东西)
两顶"帽子"可同时进行,一会重构,一会添加新功能。
重构与设计
重构可以从很大程度上去辅助设计,通常情况下我们的设计不是能贯穿我们软件开发的全过程的,在这个过程中,我们的需求变更的可能性非常大,当需求变了,设计也得变,但是我们已有的实现怎么办?全部废除?显然不能!这时候就要依靠重构来解决这种设计的矛盾
重构与性能
关于重构,有一个常被提出的问题:它对程序的性能将造成怎样的影响?为了让软件易于理解,你常会作出一些使程序运行变慢的修改。这是个重要的问题。我并不赞成为了提高设计的纯洁性或把希望寄托于更快的硬件身上,而忽略了程序性能。已经有很多软件因为速度太慢而被用户拒绝,日益提高的机器速度亦只不过略微放宽了速度方面的限制而已。但是,换个角度说,虽然重构必然会使软件运行更慢,但它也使软件的性能优化更易进行。关键在于自己的理解,当你拥有了重构的经验,你也就有能力在重构的基础上来改进程序的性能。
重构与模式
那么真正要实现重构时,我们有哪些具体的方法呢?可以这样说,重构的准则由很多条,见《重构》这本书。但它不是最终的标准,因为你要是完全按照它的标准来执行,那你也就等于不会重构,重构是一本武功秘籍,而真正的武林高手是不用武功秘籍的,一般是“无招胜有招”。只有根据实际的需要,凭借一定的思想,才能实现符合实际的重构,我们不能被一些固定的模式套牢了,这样你的程序会很僵化。究竟如何把握这个度,需要大家去总结。
重构与思想
要想实现一个好的重构,不是重构本身,而是我们在写代码的时候,思想当中时刻有它的位置存在!非常重要!如果你本身就没想着要去重构,那么就是有再好的模式供你调用又怎么样?就是有了好的模式,你不能根据实际的需要去融会贯通,那你做出来的重构有意义么?
流程
Refactoring的流程
流程1
读懂代码(包括测试例子代码)
Refactoring
运行所有的Unit Tests
流程2
读懂代码
应用重构工具进行重构(如Eclipse)
糟糕的代码——22 种代码的坏味道
重复的代码(Duplicated Code)
过长的函数(Long Method)
过大类(Large Class)
过长的参数列(Long Parameter List)
发散式变化(Divergent Change)
霰弹式修改(Shotgun Surgery)
依恋情结(Feature Envy)
数据泥团(Data Clumps)
基本型别偏执(Primitive Obsession)
Switch语句(Swtich Statements)
平行继承体系(Parallel Inheritance Hierarchies)
冗赘类(Lazy Class)
夸夸其谈未来性(Speculative Generality)
令人迷惑的暂时值域(Temporary Field)
过度遇合的消息链(Message Chains)
中间转手人(Middle Man)
狎昵关系(Inappropriate Intimacy)
异曲同工的类(Alternative Classes with Different Interfaces)
不完善的程序库类(Incomplete Library Class)
纯粹的数据类(Data Class)
被拒绝的遗赠(Refused Bequest)
过多的注释(Comments)
重构技巧
重新组织你的函数
在对象之间搬移特性
搬移函数(Move Method)
搬移值域(Move Field)
提炼类(Extract Class)
将类内联化(Inline Class)
隐藏[委托关系](Hide Delegate)
移除中间人(Remove Middle Man)
引入外加函数(Introduce Foreign Method)
引入本地扩展(Introduce Local Extension)
重新组织数据
简化条件表达式
简化函数调用
处理概括关系
0 条评论
下一页