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