源码.java —>(javac编辑器)—>字节码.class —> 类加载器(JVM)—>运行时数据区(JVM)—>执行引擎(JVM) —>机器码 —>机器识别处理
类加载过程了包括五个阶段:加载、验证、准备、解析、初始化
- 加载
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将二进制字节流代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表该类的java.lang.Class对象,作为这个类在方法区中各种数据的访问入口
- 验证
- 目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危及虚拟机本身的安全。
- 验证阶段的四个步骤:
文件格式检验:这一阶段主要是为了验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理
元数据检验:是对字节码描述的信息进行语义分析,以保证其描述的信息符合 java 语言规范的要求。
字节码检验:通过数据流和控制流分析,确定程序语义是合法、符合逻辑的。保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
符号引用检验:这个阶段的校验是发生在虚拟机将符号引用(间接引用)转化为直接引用的时候,但是这个转化的动作是发生在解析阶段
- 准备
- 该阶段正式为类变量分配内存并设置类变量初始值。这些变量所使用的内存将在方法区中进行分配。此时进行内存分配的仅包括类变量,而不包括实例变量(实例变量将会在对象实例化时随着对象一起分配在Java堆中)。
- 在这里分配的静态类变量是将其值定义为默认值而非声明值。因为在该阶段并未执行任何Java方法,正确的赋值将在初始化阶段执行。
- 解析
- 该阶段虚拟机会将常量池内的符号引用(间接引用)替换为直接引用的过程。
符号引用:代码中声明的变量名称,与虚拟机中内存的布局无关
直接引用:可以是直接指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。与虚拟机中内存的布局有关。
- 初始化
- 这是类加载的最后一步,真正执行类中定义的字节码,也就是.class文件。 初始化阶段是执行类构造器方法的过程,以及真正初始化类变量和其他资源的过程。
有的书籍也分为加载、连接【验证、准备、解析】和初始化三个阶段。
对于实现了RandomAccess 接口的List 是可以实现随机访问(RandomAccess 类型作为判断是否可以随机访问的标识)
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
总结:实现了RandomAccess 接口的list,优先选择普通for循环,其次选择foreach,未实现RandomAccess 接口的list,优先选择Iterator遍历,foreach 底层也是使用Iterator 实现,大数据量禁忌使用for遍历。
双向队列
// 一般计算hashcode方式:
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i]; // 以31为权,每一位为字符的ASCII值进行运算,用自然溢出来等效取模
// 因为计算机对乘法和除法的计算性能低下,一般使用移位法代替乘/除法计算提升性能。使用31可以得到更好的性能: 31 * i == (i << 5) - i
}
hash = h;
}
return h;
}
// HashMap 中的扰动函数hash:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// 采用无符号右移16位和异或运算是为了降低哈希碰撞
}
HashMap 默认初始化容量是16,加载因子0.75,即16 * 0.75 = 12 当前容量已使用12 个的时候会触发扩容机制,因为取模寻位的公式:hashcode % length == (length-1) & hashcode,公式成立的前提是length为2的幂次方。计算机采用二进制按位与(&)操作相对于做除法取模(%)能够提升性能,所以每次扩容都是原来的两倍。
HashTable是较为远古的使用Hash算法的容器结构了,现在基本已被淘汰,单线程转为使用HashMap,多线程使用ConcurrentHashMap。
HashTable
HashTable底层数组长度可以为任意值,这就造成了hash算法散射不均匀,容易造成hash冲突,默认为11;
Hash映射:HashMap的hash算法通过非常规设计,将底层table长度设计为2的幂,使用位与运算代替取模运算,减少运算消耗;而HashTable的hash算法首先使得hash值小于整型数最大值,再通过取模进行散射运算;
HashTable 内部方法基本上都用synchronized 同步锁修饰,所以线程安全。HashMap 没有使用同步锁,所以线程不安全
HashTable使用了同步锁的数据结构在线程并发的情况下需要线程等待,所以效率低下。HashMap 是为了追求高效率,牺牲安全性。key/value要求:HashMap 允许key 只能有一个null,value 可以有一个或多个。HashTable 中若是添加了一个key = null,就会报NullPointerException
HashMap 扩容见上。HashTable 在没有指定初始容量的时候,默认容量为11,加载因子0.75。每次扩容都是2 * n + 1(n为原来容量)
HashMap 见上。HashTable 使用数组 + 链表的方式,不会自动转化为红黑树的机制
因为HashMap 线程不安全,而线程安全的HashTable 又效率低下,所以ConcurrentHashMap 就是为了解决HashMap 线程不安全性和HashTable 效率低下问题。
HashTable 使用synchronized 修饰操作方法,这种方式是全表锁(在任意时刻同步锁只能被一个线程获取,其他线程等待其释放锁) ConcurrentHashMap 并不是直接使用synchronized 锁住方法
使用分段锁的方式达到线程安全,把整个数据集拆分成多个segment(每个segment 中存在一个链表节点元素的HashEntry[]),数据结构segment 继承了ReentrantLock 可重用锁来实现并发控制。
使用synchronized + CAS(乐观锁的一种,采用版本号的方式实现并发控制) 的方式控制线程安全,直接对数组元素/链表首节点/红黑树根结点上锁。这样只要不发生hash冲突,有并发现象,效率得到提高。
CAS(Compare And Swap)比较和交换机制。java平台对这种操作机制做了封装,在Unsafe 类下:如,unsafe.compareAndSwapInt(this, valueOffset, expect, update);这其中有三个重要的参数:valueOffset——内存位置,expect——旧预期值,update——新交换值
它的思想源自乐观锁的一种,采用版本号的方式实现并发控制。每个线程操作共享数据的时候都维持一个自己的版本号,关键就是获取版本号操作时必须要求原子性,否则没办法保证并发控制。
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1; // 获取当前版本号+1,并返回
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2); // 采用volatile 关键字保证每次获得的旧预期值都是最新的
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); // 重试机制
return var5;
}
CAS操作时,先用valueOffset 读取内存中的版本号,再和自身的旧预期值进行比较。true:把内存位置的值换成新交换值,false:不作任何处理
public final boolean weakCompareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
由上面的代码可知为了保证原子性java的原子类中使用了一个死循环进行CAS操作。成功了就跳出循环体返回,失败了就重新从内存中读取旧预期值和重新设计更新值直到成功为止。
CAS 保证了比较和交换的原子性。但是从读取到开始比较这段期间,其他核心仍然是可以修改这个值的。如果核心将 A 修改为 B,CAS 可以判断出来。但是如果核心将 A 修改为 B 再修改回 A。那么 CAS 会认为这个值并没有被改变,从而继续操作。这是和实际情况不符的。解决方案是加一个版本号。
对象头、实例数据、对齐填充
ClassMetadataAddress——类型指针(jvm就是通过这个查询该对象属于哪个类型的实例)、MarkWord——标记字段(标记哈希码、锁状态、GC年龄代等)
它是有三部分组成owner、EntryList、WaitSet
EntryList中竞争胜利者,记录当前锁拥有者
等待队列,存放多线程并发锁竞争中的失败者,等待下一次获得锁
挂起队列,存放owner 中执行了wait() 方法后进入等待队列
多线程并发竞争锁时,这些线程都会进入到EntryList 等待队列中。只有一个竞争胜利者可以进入owner(获得锁)执行,执行完毕走出owner(释放锁)。EntryList 中剩下的线程就会再次竞争选出以为胜利者进入owner 。当在owner 中的线程执行了wait() 方法该线程就会进入WaitSet 挂起队列。等待被通知(执行notify())后再次进入EntryList。
编译器会把synchronized 翻译成monitorenter(获取锁) 和 monitorexit(释放锁) 两个指令,分别放于代码块的开始和结尾的位置。
编译器会为方法生成一个ACC_SYNCHRONIZED 标志,jvm 根据这个标志来判断是否需要同步。
指在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能调用它的任意一个方法.这种动态获取信息,以及动态调用对象方法的功能叫java语言的反射机制.
在运行时获得类的各种内容,进行反编译,对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。
.class–>.java
通过反射机制访问java对象的属性,方法,构造方法等
反射最重要的用途就是开发各种通用框架。比如很多框架(Spring)都是配置化的(比如通过XML文件配置Bean),为了保证框架的通用性,他们可能需要根据配置文件加载不同的类或者对象,调用不同的方法,这个时候就必须使用到反射了,运行时动态加载需要的加载的对象。
Class.forName("com.mysql.jdbc.Driver"); // 动态加载mysql驱动
public static int key = 10;
public static void main(String[] args) {
try {
// 通过反射加载类信息
Class<?> aClass = Class.forName("cn.chaoyou.interview.Reflect");
// 根据指定属性名称初始化一个属性对象
Field field = aClass.getField("key");
// 从属性对象中获取属性值
int fieldValue = field.getInt(null);
System.out.println(fieldValue);
// 从属性对象中设置属性值
field.set(null, 5);
fieldValue = field.getInt(null);
System.out.println(fieldValue);
} catch (Exception e){
e.printStackTrace();
}
}
手动执行System.gc()方法会触发Full GC(非常不建议)
一种是被static修饰的变量;
一种是没有被static修饰的变量;
static int = arraySize = 100;
static final String CODE = “ABCD”;
/**
* Created by admin on 2018/4/26.
* 业务异常.
*/
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
public BusinessException() {
}
}
类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的实现代码块称之为“类加载器”
主要负责加载核心的类库(java.lang.*等),构造ExtClassLoader和APPClassLoader。
主要负责加载jre/lib/ext目录下的一些扩展的jar。
主要负责加载应用程序的主函数类
打开“java.lang”包下的ClassLoader类。然后将代码翻到loadClass方法:
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
// -----??-----
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 首先,检查是否已经被当前类加载器加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
// 存在父类加载器,递归的交由父类加载器
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 直到最上面的Bootstrap类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
其实这段代码已经很好的解释了双亲委派机制,为了大家更容易理解,我做了一张图来描述一下上面这段代码的流程:
这种设计有个好处是防止一个类被多个类加载器加载而造成在系统中出现多个不同的类,到后面应用程序都不知道到底使用哪个类比较合适。双亲委派机制存在时,不管在哪个类加载器收到类加载请求,都会首先委派父类加载器进行类加载。最终类加载请求会到达BootstrapClassLoader(启动类加载器)执行,由于都是在同一个加载器执行操作,加载之前先判断先前是否已经加载过了,所以基本不会出现一个类被加载出多个不同的类。
java development kit(java 开发工具包),主要包含了:jre、java源码的编译器javac、监控工具jconsole、分析工具jvisualvm
java runable environment(java 运行环境),包含了java虚拟机,java基础类库
就是申请了内存,无法释放已申请的内存空间,导致内存空间浪费。通俗说法就是有人占着茅坑不拉屎。
就是申请内存时,JVM没有足够的内存空间。通俗说法就是去蹲坑发现坑位满了。
两者是不同的概念,大量的内存泄漏可能会引起内存溢出(内存泄漏积累到堆无法为新执行的程序分配内存时,就会出现内存溢出)
Java内存泄露根本原因是什么呢?长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄露,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是java中内存泄露的发生场景。
像HashMap、Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,它们容器中所有的对象Object也不能被释放,因为他们也将一直被Vector等引用着。
集合对象用完之后要及时清空集合中元素,并且把集合对象设置为null。
当一个对象被存储进HashSet集合中以后,就不能修改这个对象中的那些参与计算哈希值的字段,否则对象修改后的哈希值与最初存储进HashSet集合中时的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为参数去HashSet集合中检索对象,也将返回找不到对象的结果,这也会导致无法从HashSet集合中删除当前对象,造成内存泄露。
在java 编程中,我们都需要和监听器打交道,通常一个应用当中会用到很多监听器。我们会调用一个控件的诸如addXXXListener()等方法来增加监听器,但往往在释放对象时忘记去删除这些监听器,从而增加了内存泄漏的机会。
监听器用完要及时关闭
比如数据库连接(dataSourse.getConnection()),网络连接(socket)和io连接,除非其显式的调用了其close()方法将其连接关闭,否则是不会自动被GC 回收的。对于Resultset 和Statement 对象可以不进行显式回收,但Connection 一定要显式回收,因为Connection 在任何时候都无法自动回收,而Connection一旦回收,Resultset 和Statement 对象就会立即为NULL。但是如果使用连接池,情况就不一样了,除了要显式地关闭连接,还必须显式地关闭Resultset Statement 对象(关闭其中一个,另外一个也会关闭),否则就会造成大量的Statement 对象无法释放,从而引起内存泄漏。这种情况下一般都会在try里面去的连接,在finally里面释放连接。
public static void main(String[] args) throws SQLException {
Connection connection = null;
try {
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "admin", "123456");
Statement statement = connection.createStatement();
ResultSet rs = statement.executeQuery("select * from student;");
while (rs.next()){
String username = rs.getString("username");
String password = rs.getString("password");
}
} catch (Exception e){
e.printStackTrace();
} finally {
if (null != connection){
connection.close();
}
}
}
不正确使用单例模式是引起内存泄露的一个常见问题,单例对象在被初始化后将在JVM的整个生命周期中存在(以静态变量的方式),如果单例对象持有外部对象的引用,那么这个外部对象将不能被jvm正常回收,导致内存泄露,
public static void main(String[] args) throws IOException {
InputStream input = null;
try {
input = new FileInputStream("file absolute path");
} catch (Exception e){
e.printStackTrace();
} finally {
if (null != input){
input.close();
}
}
}
public static void main(String[] args) throws IOException {
ExecutorService executorService = null;
try {
executorService = Executors.newFixedThreadPool(10);
} catch (Exception e){
executorService.execute(new Runnable() {
@java.lang.Override
public void run() {
System.out.println("测试多线程!!");
}
});
} finally {
if (null == executorService){
executorService.shutdown();
}
}
}
某个业务系统在一段时间突然变慢,我们怀疑是因为出现内存泄露问题导致的,于是踏上排查之路。
ps aux | grep 进程名字
# 意思是每1000毫秒查询一次,一直查。gcutil的意思是已使用空间站总空间的百分比
jstat -gcutil PID 1000
结果如下:S0(survivor0)、S1(survivor1)、E(Eden)、O(老年代)、M(方法区)、CCS(类空间)、YGC(年轻代gc次数)、YGCT(年轻代gc耗时)、FGC(老年代gc次数)、FGCT(老年代gc耗时)、GCT(堆总gc耗时)
# 主要是找出对象的引用出现了未被垃圾回收收集,通知开发人员优化相关代码。
jmap -histo:live PID | head -7
3. 如果上面一步还无法定位到关键信息,那么需要拿到heap dump,生成离线文件。
jmap -dump:live,format=b,file=~/heap.hprof PID
4.4. profiler
// HashMap 源码中的扰动函数hash:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// 采用无符号右移16位和异或运算是为了降低哈希碰撞
}
如图所示,通过异或运算计算出来的hash比较均匀,不容易出现冲突,但是总有一些例外,一旦出现了冲突现象怎么解决呢?
在数据结构中,处理hash冲常用的办法有:开发定址法、再哈希法、链地址法、建立公共溢出区。而HashMap处理hash冲突的方法就是链地址法。
链地址法:基本思想是将所有哈希地址冲突的元素构成一条冲突链(单链表),并依次将hash冲突的节点链到冲突链尾结点的后继指针,因而查找、插入和删除主要在冲突链中进行,链地址法适用与经常进行插入和删除的场景。
jdk1.8后优化:存储的元素越来越多,冲突链越来越长,当查找一个元素时效率不仅没有提高,反而下降了,于是就把链表换成了一个适合查找的树形结构——红黑树。原来链表的优点是增删操作效率高,现在查找的效率也大大提高了。
注意:只有在链表的长度大于8且数组长度小于64的时候才会将链表转成红黑树。
为什么使用红黑树?
7.1. 红黑树是一个自平衡的二叉查找树,在每个节点增加一个存储位表示节点的颜色,红色或者黑色。通过任意一条从根到子叶的路径上各个节点颜色的限制,红黑树确保没有一条路径会比其他路径长出两倍,因此红黑树是一种弱平衡二叉树。查询效率非常高。
为什么非要等到链表长度大于等于8的时候才转变为红黑树,而不是直接变为红黑树?
8.1. 因为构造红黑树要比构造链表复杂,另外在链表的节点不多的时候,数组+链表+红黑树的结构不一定比数组+链表的结构性能高。
8.2. HashMap扩容的时候,会造成红黑树不断的进行拆分重组,这是非常耗时的。所以,在链表长度比较长的时候才转变为红黑树,这样才会提高效率。
继承Thread类,重写run()方法,创建Thread对象调用start()方法启动线程。
public class ThreadDemo extends Thread {
@Override
public void run() {
int t = 1;
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + (t++));
}
}
public static void main(String[] args) {
ThreadDemo td1 = new ThreadDemo();
ThreadDemo td2 = new ThreadDemo();
td1.setName("Thread1");
td2.setName("Thread2");
td1.start();
td2.start();
}
}
实现Runnable接口,实现run()方法,接口的实现类的实例作为Thread的target传入带参的Thread构造函数,调用start()方法启动线程。
public class RunnableDemo implements Runnable {
@Override
public void run() {
int t = 1;
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + (t++));
}
}
public static void main(String[] args) {
RunnableDemo rd = new RunnableDemo();
Thread tr1 = new Thread(rd);
Thread tr2 = new Thread(rd);
tr1.setName("Thread1");
tr2.setName("Thread2");
tr1.start();
tr2.start();
}
}
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class CallableFutureTaskDemo implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int t = 1;
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + (t++));
}
return t;
}
public static void main(String[] args) {
Callable<Integer> cftd1 = new CallableFutureTaskDemo();
Callable<Integer> cftd2 = new CallableFutureTaskDemo();
FutureTask<Integer> ft1 = new FutureTask<>(cftd1);
FutureTask<Integer> ft2 = new FutureTask<>(cftd2);
Thread t1 = new Thread(ft1);
Thread t2 = new Thread(ft2);
t1.setName("Thread1");
t2.setName("Thread2");
t1.start();
t2.start();
try {
System.out.println(ft1.get());
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
定长线程池,每提交一个任务都会新建一个线程,直到线程池容量限制为止。可控制线程最大并发数,超出的线程会在队列中等待
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorDemo implements Runnable {
private static int task_num = 2; //任务数量
@Override
public void run() {
int t = 1;
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + " " + (t++));
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i <task_num; i++) {
ExecutorDemo ed = new ExecutorDemo();
executorService.execute(ed);
}
executorService.shutdown();
}
}
缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则灵活新建线程,不会对线程池的容量有任何限制。
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int index = i;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
cachedThreadPool.execute(new Runnable() {
public void run() {
System.out.println(index);
}
});
}
}
定长线程池,支持定时及周期性任务执行。
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
for (int i = 0; i < 10; i++) {
scheduledThreadPool.schedule(new Runnable() {
public void run() {
System.out.println("delay 3 seconds");
}
}, 3, TimeUnit.SECONDS);
}
}
单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
public static void main(String[] args) {
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
public void run() {
try {
System.out.println(index);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
提供互斥访问,同一时刻只能有一个线程对数据进行操作,(atomic, synchronized)
基于 Atomic 的数据类型的读写操作都是具有原子性的。例如,AtomicInteger 的实现源码如下:
/**
* 以原子方式将当前值递增 1。
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
/**
* 并发相关。主要提供低级别同步原语,如CAS、线程调度、volatile、内存屏障等
*
* @param var1 变量的内存地址,V
* @param var2 旧的预期值,O
* @param var4 增量,N
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
// 自旋锁
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
一个线程对主内存的修改可以及时地被其他线程看到,(synchronized,volatile, final)
使用 synchronized 关键字修饰的代码块中的变量,因为 synchronized 在编译的时候会产生 lock 和 unlock 过程,在 unlock 之前必须完成变量值同步回主内存中,其他线程只能在unlock之后才有机会读取变量
使用 final 关键字修饰的变量,因为被 final 修饰的变量无法被修改,所以所有线程拿到的值都是一样的
volatile 修饰的变量在被线程使用之前还会到主内存中刷新一遍变量值(即执行一次 read + load 操作),并且修改后会立即同步到主内存中(store + write)
典型的场景有访问共享对象的属性,访问 static 静态变量,访问共享的缓存等等。因为这些信息不仅会被一个线程访问到,还有可能被多个线程同时访问,那么就有可能在并发读写的情况下发生线程安全问题
// 共享资源
private static Integer num = 10;
// 竞争共享资源的方法
public static void computeData() {
for (int i = 0; i < 100; i++) {
num ++;
}
}
public static void main(String[] args) {
try {
Runnable runnable = new Runnable() {
@java.lang.Override
public void run() {
computeData();
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(num);
} catch (Exception e){
e.printStackTrace();
}
}
如果我们操作的正确性是依赖时序的,而在多线程的情况下又不能保障执行的顺序和我们预想的一致,这个时候就会发生线程安全问题
// “检查与执行”并非原子性操作
if (map.containsKey(key)) {
map.remove(obj)
}
线程池的初始化涉及到一个非常重要的类ThreadPoolExecutor。这个类的构造方法设置了线程池的必要参数。我们从源码入手,看一下ThreadPoolExecutor类的构造参数有哪些必要参数。
/**
* 使用给定的初始参数创建一个新的 {@code ThreadPoolExecutor}.
*
* @param corePoolSize 要保留在池中的线程数,即使它们处于空闲状态,除非设置了 {@code allowCoreThreadTimeOut}
* @param maximumPoolSize 池中允许的最大线程数
* @param keepAliveTime 当线程数大于核心时,这是多余的空闲线程在终止前等待新任务的最长时间。
* @param unit {@code keepAliveTime} 参数的时间单位
* @param workQueue 用于在执行任务之前保存任务的队列。 该队列将仅保存由 {@code execute} 方法提交的 {@code Runnable} 任务。
* @param threadFactory 执行程序创建新线程时使用的工厂
* @param handler 在执行被阻塞时使用的处理程序,因为达到了线程边界和队列容量
*/
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
线程池中保持空闲线程的基本数量
线程池的最大容量,表示可同时活动线程数量的上限
当线程池中空闲线程数量超过了基本数量时,超过部分的线程的存活时间,线程空置时间超过这个值就会被执行回收操作
keepAliveTime的单位。
用于保存等待执行任务的队列。ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排列方法有3种:
线程工厂,在ThreadFactory中只定义了一个方法newThread,每当线程池需要创建一个新线程时都会调用该方法。
饱和策略。主要有4种:
JDK对线程池提供了有效的支持。我们可以通过Executors类中的静态工厂方法来创建线程池,下图展示了常用的四种静态工厂方法,我们将一一对其进行说明。
定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
定长线程池,支持定时及周期性任务执行。
单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
为了解决执行服务的生命周期问题,ExecutorService扩展了Executor接口
源码分析:了解线程池的参数之后,我们来看看ThreadPoolExecutor类的核心部分源码。
public class ThreadPoolExecutor extends AbstractExecutorService {
/**
* 执行
*/
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 当前任务数小于线程池的基本大小
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 线程池状态isRunning并且工作队列可以加入
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 通过addWorker(command, false)新建线程执行任务,若失败则拒绝。
else if (!addWorker(command, false))
reject(command);
}
/**
* 平缓关闭
*/
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 状态为SHUTDOWN
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown();
} finally {
mainLock.unlock();
}
tryTerminate();
}
/**
* 暴力关闭
*/
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
// 状态为STOP
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
/**
* 已终止
*/
protected void finalize() {
shutdown();
}
}
ExecutorService在创建之后处于运行状态
调用shutdown方法进入关闭状态(不再接受新任务,但已接受的任务包括未开始执行的任务会继续执行);调用shutdownNow方法进入关闭状态(尝试取消所有运行中和尚未执行的任务)
ExecutorService关闭之后,在所有任务完成之后,进入终止状态
-- 查询mysql服务器的连接次数
SHOW STATUS LIKE 'Connections';
-- 查询mysql服务器的慢查询次数。慢查询次数参数可以结合慢查询日志,找出慢查询语句,然后针对慢查询语句进行表结构优化或者查询语句优化。
SHOW STATUS LIKE 'Slow_queries';
id:表示查询中执行 select 子句或操作表的顺序
select_type:查询类型:simple、primary、subquery、derived、union、union result
table:显示这一行数据关于哪一张表的
type:访问排序类型
possible_keys:显示可能应用在这张表中的索引列表
key:实际使用的索引,如果为 NULL,则没有使用索引
key_len:表示索引中使用到的字节数,可通过该列计算查询中的索引长度,在不损失查询条件精确性的情况下,长度越短越好
ref:显示索引的哪一列被使用了,如果可能的话,是一个常量,哪些列或常量被用于查询索引列上的值
rows:根据表统计信息和索引选用情况,大致估算出找到目标的记录需要读取的行数
Extra:包含一些不合适在其他列中显示,但又十分重要的额外信息
当添加一条数据到表中的时候,首先会对主键进行hash,然后将这条数据存在的地址和hash值建立一个映射关系,当我们根据主键查找这条数据的时候,只需要将主键进行hash,得到hash值,最后根据hash值就可以直接定位到这条数据。所以hash算法只需要进行一次磁盘IO,查询速度是非常快的。
B-树又称为多路平衡查找树,它在平衡二叉树的基础之上,划分出来多个叉。正是因为每个节点有多个子节点,所以有效地降低了树的高度,提升了查找效率。
B-树的几大特性:
B+树其实是B树的一个变种,它在B树的基础之上做了一些改善,将索引节点所关联的数据记录全部移到叶子节点上了,目的是为了可以存储更多的索引节点,但是却增加了索引节点的冗余,因为叶子节点包含了所有的索引节点。
B+树具有以下几个特性:
mysql的b+ tree优化了什么?
1.增加了一个双向的指针
2.首尾节点也通过指针进行关联起来
主要目的是为了更加友好的支持索引内部的范围查找。如果不加双向链表指针,我们每次查找的时候,都要回到根节点查找,增加了磁盘IO,增加查询时间。
一个或一组sql 语句组成一个执行单元(原子性),这个执行单元要么全部执行,要么全部不执行。事务是由单独单元的一个或多个SQL 语句组成,在这个单元中,每个MySQL 语句是互相依赖的,而整个单独单元作为一个不可分割的整体,如果单元中的某条SQL 语句一旦执行失败或产生错误,整个单元将会进行回滚操作,所有收到影响的数据将会返回到事务开启前的一个状态;如果单元中的所有SQL 语句均执行成功,则事务被顺利执行。
使用的的一种叫MVCC的控制方式 ,即Mutil-Version Concurrency Control,多版本并发控制,类似于乐观锁的一种实现方式
DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="dbUser.properties">properties>
<typeAliases>
<package name="cn.zdxh.lcy.domain" />
typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">transactionManager>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver">property>
<property name="url" value="jdbc:mysql:///travel">property>
<property name="username" value="root">property>
<property name="password" value="123456">property>
dataSource>
environment>
environments>
<mappers>
<mapper resource="cn/zdxh/lcy/mapper/registMapper.xml" />
.....
mappers>
configuration>
称Mybatis是半自动ORM映射工具,是因为在查询关联对象或关联集合对象时,需要手动编写sql来完成。不像Hibernate这种全自动ORM映射工具,Hibernate查询关联对象或者关联集合对象时,可以根据对象关系模型直接获取。
<resultMap id="BaseResultMap" type="cn.zdxh.lcy.domain.Register">
<result column="regist_username" property="regist_username" jdbcType="VARCHAR" />
<result column="regist_password" property="regist_password" jdbcType="VARCHAR" />
<result column="regist_phone" property="regist_phone" jdbcType="VARCHAR" />
<result column="regist_email" property="regist_email" jdbcType="VARCHAR" />
<result column="regist_time" property="regist_time" jdbcType="VARCHAR" />
resultMap>
<select id="selectUserById" parameterType="Integer" resultType="cn.zdxh.lcy.domain.Register" resultMap="BaseResultMap">
select * from register where regist_id = #{regist_id}
select>
3.2. 注解
@Results(id = "registerMap", value = {
@Result(column="regist_id", property="regist_id", jdbcType = JdbcType.BIGINT),
@Result(column="regist_username", property="regist_username", jdbcType = JdbcType.VARCHAR),
@Result(column="regist_password", property="regist_password", jdbcType = JdbcType.VARCHAR),
@Result(column="regist_phone", property="regist_phone", jdbcType = JdbcType.VARCHAR),
@Result(column="regist_email", property="regist_email", jdbcType = JdbcType.VARCHAR),
@Result(column="regist_time", property="regist_time", jdbcType = JdbcType.VARCHAR)
})
@ResultMap("registerMap")
@Select("select * from register where regist_id=#{regist_id}")
Register findRegisterById(String regist_id);
不能
在投鞭断流时,Mybatis使用package+Mapper+method全限名作为key,去xml内寻找唯一sql来执行的。类似:key=x.y.UserMapper.getUserById,那么,重载方法时将导致矛盾。对于Mapper接口,Mybatis禁止方法重载(overLoad)。
/**
* 映射器代理,代理模式
*
*/
public class MapperProxy<T> implements InvocationHandler, Serializable {
private static final long serialVersionUID = -6424540398559729838L;
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
private final Map<Method, MapperMethod> methodCache;
public MapperProxy(SqlSession sqlSession, Class<T> mapperInterface, Map<Method, MapperMethod> methodCache) {
this.sqlSession = sqlSession;
this.mapperInterface = mapperInterface;
this.methodCache = methodCache;
}
/**
* 代理以后,所有Mapper的方法调用时,都会调用这个invoke方法
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
//并不是任何一个方法都需要执行调用代理对象进行执行,如果这个方法是Object中通用的方法(toString、hashCode等)无需执行
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 业务方法,会优先从缓存中获取
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
/**
* 获取业务方法对象 MapperMethod,会优先从缓存中获取
*/
private MapperMethod cachedMapperMethod(Method method) {
// 从缓存中获取
MapperMethod mapperMethod = methodCache.get(method);
if (mapperMethod == null) {
// 缓存中没有,则自行初始化对象
mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
// 把对象存储到缓存中
methodCache.put(method, mapperMethod);
}
return mapperMethod;
}
/**
* Object中通用的方法
*/
@UsesJava7
private Object invokeDefaultMethod(Object proxy, Method method, Object[] args)
throws Throwable {
final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class
.getDeclaredConstructor(Class.class, int.class);
if (!constructor.isAccessible()) {
constructor.setAccessible(true);
}
final Class<?> declaringClass = method.getDeclaringClass();
return constructor
.newInstance(declaringClass,
MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
| MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC)
.unreflectSpecial(method, declaringClass).bindTo(proxy).invokeWithArguments(args);
}
/**
* Backport of java.lang.reflect.Method#isDefault()
*/
private boolean isDefaultMethod(Method method) {
return ((method.getModifiers()
& (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC)
&& method.getDeclaringClass().isInterface();
}
}
Dao接口即Mapper接口。接口的全限定名,就是映射文件中的namespace的值;
接口的方法名,就是映射文件中Mapper的Statement的id值;
接口方法内的参数**#{regist_username}**,就是传递给sql的参数。
<mapper namespace="cn.zdxh.lcy.mapper.registMapper">
<select id="selectUserById" parameterType="String" resultType="cn.zdxh.lcy.domain.Register">
select * from register where regist_username = #{regist_username}
select>
mapper>
当调用接口方法时,接口全限定名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中,每一个SQL标签,比如 insert、update、select、delete 等标签,都会被解析为一个MapperStatement对象。
/**
* 解析语句(select|insert|update|delete)
*
*
*/
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
//如果databaseId不匹配,退出
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
//暗示驱动程序每次批量返回的结果行数
Integer fetchSize = context.getIntAttribute("fetchSize");
//超时时间
Integer timeout = context.getIntAttribute("timeout");
//引用外部 parameterMap,已废弃
String parameterMap = context.getStringAttribute("parameterMap");
//参数类型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
//引用外部的 resultMap(高级功能)
String resultMap = context.getStringAttribute("resultMap");
//结果类型
String resultType = context.getStringAttribute("resultType");
//脚本语言,mybatis3.2的新功能
String lang = context.getStringAttribute("lang");
//得到语言驱动
LanguageDriver langDriver = getLanguageDriver(lang);
Class<?> resultTypeClass = resolveClass(resultType);
//结果集类型,FORWARD_ONLY|SCROLL_SENSITIVE|SCROLL_INSENSITIVE 中的一种
String resultSetType = context.getStringAttribute("resultSetType");
//语句类型, STATEMENT|PREPARED|CALLABLE 的一种
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
//获取命令类型(select|insert|update|delete)
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
//是否要缓存select结果
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
//仅针对嵌套结果 select 语句适用:如果为 true,就是假设包含了嵌套结果集或是分组了,这样的话当返回一个主结果行的时候,就不会发生有对前面结果集的引用的情况。
//这就使得在获取嵌套的结果集的时候不至于导致内存不够用。默认值:false。
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
//解析之前先解析SQL片段
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// Parse selectKey after includes and remove them.
//解析之前先解析
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: and were parsed and removed)
//解析成SqlSource,一般是DynamicSqlSource
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
//(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值
String keyProperty = context.getStringAttribute("keyProperty");
//(仅对 insert 有用) 标记一个属性, MyBatis 会通过 getGeneratedKeys 或者通过 insert 语句的 selectKey 子元素设置它的值
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? new Jdbc3KeyGenerator() : new NoKeyGenerator();
}
//又去调助手类
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
/**
* org.apache.ibatis.builder.MapperBuilderAssistant:把 MappedStaement 存储到 Configuration 类的 Map mappedStatements 容器中
*/
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
// 调用 configuration 类
configuration.addMappedStatement(statement);
return statement;
}
/**
* org.apache.ibatis.session.Configuration:容器存储元素的方法
*/
public void addMappedStatement(MappedStatement ms) {
// ms.getId() 为 sql 语句中的 id 变量值(方法名)
mappedStatements.put(ms.getId(), ms);
}
举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面 id 为 findStudentById 的 MapperStatement。
原因:namespace + id 是作为Map的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。
DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="dbUser.properties">properties>
<typeAliases>
<package name="cn.zdxh.lcy.domain" />
typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">transactionManager>
<dataSource type="POOLED">
.....
dataSource>
environment>
environments>
<settings>
<setting name="lazyLoadingEnabled" value="true" />
<setting name="aggressiveLazyLoading" value="false" />
settings>
<mappers>
......
mappers>
configuration>
1.2. mapper接口中的业务方法使用赖加载:***Mapper.xml
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace = "shfq.lazy_load.vo.Blog">
<select id="selectBlog" parameterType="int" resultMap="blogResultMap">
SELECT * from blog where id=#{id}
select>
<resultMap id="blogResultMap" type="shfq.lazy_load.vo.Blog">
<id column="id" property="id">id>
<result column="content" property="content">result>
<association property="author" column="author_id" select="selectAuthor" fetchType="lazy"/>
resultMap>
<select id="selectAuthor" parameterType="int" resultType="shfq.lazy_load.vo.Author">
SELECT * from author where id=#{id}
select>
mapper>
/**
* mybatis 源码:使用cglib创建代理对象
* @param type 原生的对象类型
* @param callback 回调方法,就是实现了MethodInterceptor接口的子类,会有accept方法
* @param constructorArgTypes 构造函数的参数类型
* @param constructorArgs 构造函数的参数值
* @return
*/
private static Object crateProxy(Class<?> type, Callback callback, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
Enhancer enhancer = new Enhancer();
//回调方法,就是实现了MethodInterceptor接口的子类,会有accept方法
enhancer.setCallback(callback);
//要生成代理对象的原生类
enhancer.setSuperclass(type);
try {
//获取目标类型的 writeReplace 方法,如果没有,异常中代理类设置enhancer.setInterfaces(new Class[]{WriteReplaceInterface.class});
type.getDeclaredMethod(WRITE_REPLACE_METHOD);
// ObjectOutputStream will call writeReplace of objects returned by writeReplace
log.debug(WRITE_REPLACE_METHOD + " method was found on bean " + type + ", make sure it returns this");
} catch (NoSuchMethodException e) {
enhancer.setInterfaces(new Class[]{WriteReplaceInterface.class});
} catch (SecurityException e) {
// nothing to do here
}
Object enhanced = null;
//如果构造函数没有参数,创建代理对象
if (constructorArgTypes.isEmpty()) {
enhanced = enhancer.create();
} else {
//否则,初始化带有参数的构造函数
Class<?>[] typesArray = constructorArgTypes.toArray(new Class[constructorArgTypes.size()]);
Object[] valuesArray = constructorArgs.toArray(new Object[constructorArgs.size()]);
//创建带有参数的代理对象
enhanced = enhancer.create(typesArray, valuesArray);
}
return enhanced;
}
一级缓存 Mybatis的一级缓存是指SQLSession,一级缓存的作用域是SQlSession, Mabits默认开启一级缓存。 在同一个SqlSession中,执行相同的SQL查询时;第一次会去查询数据库,并写在缓存中,第二次会直接从缓存中取。 当执行SQL时候两次查询中间发生了增删改的操作,则SQLSession的缓存会被清空。 每次查询会先去缓存中找,如果找不到,再去数据库查询,然后把结果写到缓存中。 Mybatis的内部缓存使用一个HashMap,key为hashcode+statementId+sql语句。Value为查询出来的结果集映射成的java对象。 SqlSession执行insert、update、delete等操作commit后会清空该SQLSession缓存。
对外提供了用户和数据库之间交互需要的所有方法,隐藏了底层的细节。默认实现类是DefaultSqlSession。
public class DefaultSqlSession implements SqlSession {
private final Configuration configuration;
// 数据库操作有关的职责都会委托给Executor
private final Executor executor;
private final boolean autoCommit;
private boolean dirty;
private List<Cursor<?>> cursorList;
}
public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory.getLog(BaseExecutor.class);
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
// 缓存容器
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
protected int queryStack;
private boolean closed;
}
BaseExecutor成员变量之一的PerpetualCache,是对Cache接口最基本的实现,其实现非常简单,内部持有HashMap,对一级缓存的操作实则是对HashMap的操作。
public class PerpetualCache implements Cache {
private final String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
}
二级缓存是mapper级别的,Mybatis默认是没有开启二级缓存的。 第一次调用mapper下的SQL去查询用户的信息,查询到的信息会存放代该mapper对应的二级缓存区域。 第二次调用namespace下的mapper映射文件中,相同的sql去查询用户信息,会去对应的二级缓存内取结果。 如果调用相同namespace下的mapepr映射文件中增删改sql,并执行了commit操作,此时要刷新缓存(flushCache=“true”),否则会出现脏读情况。
<insert id="insertUser" parameterType="cn.itcast.mybatis.po.User" flushCache="true">
现有这样一个场景,有两个表,部门表dept(deptNo,dname,loc)和 部门数量表deptNum(id,name,num),其中部门表的名称和部门数量表的名称相同,通过名称能够联查两个表可以知道其坐标(loc)和数量(num),现在我要对部门数量表的 num 进行更新,然后我再次关联dept 和 deptNum 进行查询,你认为这个 SQL 语句能够查询到的 num 的数量是多少?来看一下代码探究一下
public class DeptNum {
private int id;
private String name;
private int num;
get and set...
}
public class DeptVo {
private Integer deptNo;
private String dname;
private String loc;
private Integer num;
public DeptVo(Integer deptNo, String dname, String loc, Integer num) {
this.deptNo = deptNo;
this.dname = dname;
this.loc = loc;
this.num = num;
}
public DeptVo(String dname, Integer num) {
this.dname = dname;
this.num = num;
}
get and set
@Override
public String toString() {
return "DeptVo{" +
"deptNo=" + deptNo +
", dname='" + dname + '\'' +
", loc='" + loc + '\'' +
", num=" + num +
'}';
}
}
public interface DeptDao {
...
DeptVo selectByDeptVo(String name);
DeptVo selectByDeptVoName(String name);
int updateDeptVoNum(DeptVo deptVo);
}
<select id="selectByDeptVo" resultType="com.mybatis.beans.DeptVo">
select d.deptno,d.dname,d.loc,dn.num from dept d,deptNum dn where dn.name = d.dname
and d.dname = #{name}
</select>
<select id="selectByDeptVoName" resultType="com.mybatis.beans.DeptVo">
select * from deptNum where name = #{name}
</select>
<update id="updateDeptVoNum" parameterType="com.mybatis.beans.DeptVo">
update deptNum set num = #{num} where name = #{dname}
</update>
/**
* 探究多表操作对二级缓存的影响
*/
@Test
public void testOtherMapper(){
// 第一个mapper 先执行联查操作
SqlSession sqlSession = factory.openSession();
DeptDao deptDao = sqlSession.getMapper(DeptDao.class);
DeptVo deptVo = deptDao.selectByDeptVo("ali");
System.out.println("deptVo = " + deptVo);
// 第二个mapper 执行更新操作 并提交
SqlSession sqlSession2 = factory.openSession();
DeptDao deptDao2 = sqlSession2.getMapper(DeptDao.class);
deptDao2.updateDeptVoNum(new DeptVo("ali",1000));
sqlSession2.commit();
sqlSession2.close();
// 第一个mapper 再次进行查询,观察查询结果
deptVo = deptDao.selectByDeptVo("ali");
System.out.println("deptVo = " + deptVo);
}
在对DeptNum 表执行了一次更新后,再次进行联查,发现数据库中查询出的还是 num 为 1050 的值,也就是说,实际上 1050 -> 1000 ,最后一次联查实际上查询的是第一次查询结果的缓存,而不是从数据库中查询得到的值,这样就读到了脏数据。
如果是两个mapper命名空间的话,可以使用
来把一个命名空间指向另外一个命名空间,从而消除上述的影响,再次执行,就可以查询到正确的数据
当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear 掉并重新更新,如果开启了二级缓存,则只根据配置判断是否刷新。
根据表达式的值完成逻辑判断 并动态拼接sql的功能。
<trim>trim>
<where>where>
<set>set>
<foreach collection="">foreach>
<if test="">if>
<choose>
<when test="">when>
<otherwise>otherwise>
choose>
<bind name="" value=""/>
resultMap、parameterMap、sql、include、selectKey,加上动态sql的9个标签 trim | where | set | foreach | if | choose | when | otherwise | bind 等,其中 为sql片段标签,通过标签引入sql片段,为不支持自增的主键生成策略标签。
DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.zdxh.lcy.dao.RegisteDao">
<resultMap id="BaseResultMap" type="cn.zdxh.lcy.domain.Register">
<result column="regist_username" property="regist_username" jdbcType="VARCHAR" />
<result column="regist_password" property="regist_password" jdbcType="VARCHAR" />
<result column="regist_phone" property="regist_phone" jdbcType="VARCHAR" />
<result column="regist_email" property="regist_email" jdbcType="VARCHAR" />
<result column="regist_time" property="regist_time" jdbcType="VARCHAR" />
resultMap>
<select id="selectUserById" parameterType="Integer" resultType="cn.zdxh.lcy.domain.Register" resultMap="BaseResultMap">
select * from register where regist_id = #{regist_ide}
select>
<insert id="addUser" parameterType="cn.zdxh.lcy.domain.Register">
insert into register(regist_username, regist_password, regist_phone, regist_email, regist_time, header_url) values(#{regist_username}, #{regist_password}, #{regist_phone}, #{regist_email}, #{regist_time}, #{header_url})
insert>
<delete id="deleteUser" parameterType="cn.zdxh.lcy.domain.Register">
delete from register where regist_username = #{regist_username} and regist_password = #{regist_password}
delete>
<update id="updateUser" parameterType="cn.zdxh.lcy.domain.Register">
update register set regist_password = #{regist_password}, regist_phone = #{regist_phone}, regist_email = #{regist_email} where regist_id = #{regist_id}
update>
mapper>
package cn.zdxh.lcy.dao;
import cn.zdxh.lcy.domain.Register;
public interface RegisteDao {
Register selectUserById();
int addUser(Register register);
int deleteUser(String regist_username, String regist_password);
int updateUser(Register register);
}
<select id=”selectlike”>
select * from foo where bar like #{value}
select>
String wildcardname = “%smi%”;
List<name> names = mapper.selectlike(wildcardname);
<select id=”selectlike”>
select * from foo where bar like "%"${value}"%"
select>
String wildcardname = “smi”;
List<name> names = mapper.selectlike(wildcardname);
<select id=”selectorder” parametertype=”int” resultetype=”me.gacl.domain.order”>
select order_id id, order_no orderno ,order_price price form orders where order_id=#{id};
select>
<select id="getOrder" parameterType="int" resultMap="orderresultmap">
select * from orders where order_id=#{id}
select>
<resultMap type=”me.gacl.domain.order” id=”orderresultmap”>
<!–用id属性来映射主键字段–>
<id property=”id” column=”order_id”>
<!–用result属性来映射非主键字段,property为实体类属性名,column为数据表中的属性–>
<result property = “orderno” column =”order_no”/>
<result property=”price” column=”order_price” />
reslutMap>
<insert id=”insertName” usegeneratedkeys=”true” keyproperty=”id”>
insert into names (name) values (#{name})
insert>
Name name = new Name();
name.setName("fred");
int rows = mapper.insertName(name);
// 完成后,id已经被设置到对象中
System.out.println("rows inserted = " + rows);
System.out.println("generated key value = " + name.getid());
(1)第一种:
//DAO层的函数
public UserselectUser(String name,String area);
//对应的xml,#{0}代表接收的是dao层中的第一个参数,#{1}代表dao层中第二参数,更多参数一致往后加即可。
<select id="selectUser"resultMap="BaseResultMap">
select * fromuser_user_t whereuser_name = #{0} anduser_area=#{1}
</select>
(2)第二种: 使用 @param 注解:
public interface usermapper {
User selectuser(@Param("username") String username, @Param("hashedpassword") String hashedpassword);
}
然后,就可以在xml像下面这样使用(推荐封装为一个map,作为单个参数传递给mapper):
<select id="selectuser" resulttype="user">
select id, username, hashedpassword
from some_table
where
username = #{username} and
hashedpassword = #{hashedpassword}
</select>
(3)第三种:多个参数封装成map
try{
// 映射文件的命名空间.SQL片段的ID(StudentID),就可以调用对应的映射文件中的SQL
// 由于我们的参数超过了两个,而方法中只有一个Object参数收集,因此我们使用Map集合来装载我们的参数
Map<String, Object> map = new HashMap();
map.put("start", start);
map.put("end", end);
return sqlSession.selectList("StudentID.pagination", map);
}catch(Exception e){
e.printStackTrace();
sqlSession.rollback();
}
finally{
MybatisUtil.closeSqlSession();
}
<mapper namespace="com.lcb.mapping.userMapper">
<select id="getClass" parameterType="int" resultMap="ClassesResultMap">
select
*
from class c, teacher t
where
c.teacher_id = t.t_id and
c.c_id = #{id}
select>
<resultMap type="com.lcb.user.Classes" id="ClassesResultMap">
<id property="id" column="c_id"/>
<result property="name" column="c_name"/>
<association property="teacher" javaType="com.lcb.user.Teacher">
<id property="id" column="t_id"/>
<result property="name" column="t_name"/>
association>
resultMap>
<select id="getClass2" parameterType="int" resultMap="ClassesResultMap2">
select
*
from class c, teacher t, student s
where
c.teacher_id = t.t_id and
c.c_id = s.class_id and
c.c_id = #{id}
select>
<resultMap type="com.lcb.user.Classes" id="ClassesResultMap2">
<id property="id" column="c_id"/>
<result property="name" column="c_name"/>
<association property="teacher" javaType="com.lcb.user.Teacher">
<id property="id" column="t_id"/>
<result property="name" column="t_name"/>
association>
<collection property="student" ofType="com.lcb.user.Student">
<id property="id" column="s_id"/>
<result property="name" column="s_name"/>
collection>
resultMap>
mapper>
<mappers>
<mapper resource="mapper.xml 文件的地址" />
<mapper resource="mapper.xml 文件的地址" />
mappers>
<insert id="addUser" parameterType="cn.zdxh.lcy.domain.Register">
insert into register(regist_username, regist_password, regist_phone, regist_email, regist_time, header_url) values(#{regist_username}, #{regist_password}, #{regist_phone}, #{regist_email}, #{regist_time}, #{header_url})
insert>
<bean id="对象ID" class="mapper 接口的实现">
<property name="sqlSessionFactory" ref="sqlSessionFactory">property>
bean>
<mappers>
<mapper resource="mapper.xml 文件的地址" />
<mapper resource="mapper.xml 文件的地址" />
mappers>
<bean id="" class="org.mybatis.spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="mapper 接口地址" />
<property name="sqlSessionFactory" ref="sqlSessionFactory" />
bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="mapper接口包地址" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
bean>
在MyBatis中任意定义接口,然后把接口里面的方法和SQL语句绑定, 我们直接调用接口方法就可以,这样比起原来了SqlSession提供的方法我们可以有更加灵活的选择和设置。
@Select("select * from student where username = #{username}")
Student findStudentByUsername(String username);
<resultMap id="studentResultMap" type="cn.zdxh.lcy.domain.Student">
<result column="username" property="username" jdbcType="VARCHAR" />
<result column="password" property="password" jdbcType="VARCHAR" />
<result column="phone" property="phone" jdbcType="VARCHAR" />
<result column="email" property="email" jdbcType="VARCHAR" />
resultMap>
<select id="findStudentByUsername" parameterType="String" resultType="cn.zdxh.lcy.domain.Student" resultMap="studentResultMap">
select
*
from student
where
username = #{username}
select>
public class MyBatisUtil{
public static void main(String[] args) {
// 获取mybatis-config配置文件的资源信息
String resource = "mybatis-config.xml";
InputStream is = MyBatisUtil.class.getClassLoader().getResourceAsStream(resource);
// 创建数据源工厂
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
// 通过SQLSessionFactory创建SQLSession
SqlSession sqlSession = factory.openSession();
// 通过SQLSession执行数据库操作
UserMapper cim = sqlSession.getMapper(UserMapper.class);
// 调用mapper接口中的业务方法
int ci = cim.deleteUser("123");
// 调用session.commit()提交事物
sqlSession.commit();
// 调用session.close()关闭会话
sqlSession.close();
}
}
properties(属性)
settings(配置)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境集合属性对象)
environment(环境子属性对象)
transactionManager(事务管理)
dataSource(数据源)
mappers(映射器)
@Insert : 插入sql , 和xml insert sql语法完全一样
@Select : 查询sql, 和xml select sql语法完全一样
@Update : 更新sql, 和xml update sql语法完全一样
@Delete : 删除sql, 和xml delete sql语法完全一样
@Param : 入参
@Results :结果集合
@Result : 结果
@ResultMap:结果集名称
Spring MVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller层次结构分离。将web层进行职责解耦,把复杂的web应用分成逻辑清晰的几部分。简化开发,减少出错,方便组内开发人员之间的配合。
前端控制器 DispatcherServlet:接收请求、响应结果,相当于转发器,有了DispatcherServlet 就减少了其它组件之间的耦合度。
处理器映射器 HandlerMapping:根据请求的URL来查找Handler
处理器适配器 HandlerAdapter:负责执行Handler
处理器 Handler:处理器,需要程序员开发
视图解析器 ViewResolver:进行视图的解析,根据视图逻辑名将ModelAndView解析成真正的视图(view)
视图View:View是一个接口, 它的实现类支持不同的视图类型,如jsp,freemarker,pdf等等
// 处理器方法返回 ModelAndView 转发到视图
@RequestMapping("user.do")
public ModelAndView login(){
ModelAndView view = new ModelAndView();
// 设置参数
view.addObject("username", "李四");
view.addObject("password", "123456");
// 初始化转发策略
view.setViewName ("forward:user.do?name=method4");
return view;
}
// 处理器方法返回 ModelAndView 重定向到百度
@RequestMapping("user.do")
public ModelAndView login(){
ModelAndView view = new ModelAndView();
// 设置参数
view.addObject("username", "李四");
view.addObject("password", "123456");
// 初始化转发策略
view.setViewName("redirect:http://www.baidu.com");
return view;
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
/**
* The value may indicate a suggestion for a logical component name,
* to be turned into a Spring bean in case of an autodetected component.
* @return the suggested component name, if any
* @since 4.0.1
*/
String value() default "";
}
@RequestMapping("/user.do/{id}")
public Integer login(@PathVariable("id") Integer id){
System.out.println(id);
return id;
}
/**
* http 请求:http://localhost:8080/login/user.do?id=10
*/
// handler 接口
@RequestMapping("user.do")
public Integer login(@RequestParam("id") Integer id){
System.out.println(id);
return id;
}
答:一般用 @Controller 注解,也可以使用 @RestController。@RestController 注解相当于 @ResponseBody + @Controller。表示是表现层,除此之外,一般不用别的注解代替。
<filter>
<filter-name>CharacterEncodingFilterfilter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilterfilter-class>
<init-param>
<param-name>encodingparam-name>
<param-value>utf-8param-value>
init-param>
filter>
<filter-mapping>
<filter-name>CharacterEncodingFilterfilter-name>
<url-pattern>/*url-pattern>
filter-mapping>
// ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码。
String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8");
有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承HandlerInterceptorAdapter适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可:
<mvc:interceptors>
<bean id="myInterceptor" class="com.zwp.action.MyHandlerInterceptor">bean>
<mvc:interceptor>
<mvc:mapping path="/modelMap.do" />
<bean class="com.zwp.action.MyHandlerInterceptorAdapter" />
mvc:interceptor>
mvc:interceptors>
注解本质是一个继承了Annotation的特殊接口,其具体实现类是 JDK 动态代理生成的代理类。我们通过反射获取注解时,返回的也是Java运行时生成的动态代理对象。通过代理对象调用自定义注解的方法,会最终调用 AnnotationInvocationHandler 的 invoke 方法,该方法会从memberValues这个Map中查询出对应的值,而memberValues的来源是Java常量池。
用于指明被修饰的注解最终可以作用的目标是谁,也就是指明,你的注解到底是用来修饰方法的?修饰类的?还是用来修饰字段属性的。
package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* 返回可以应用注释类型的元素种类的数组。
* @return 可以应用注释类型的元素种类的数组
*/
ElementType[] value();
}
我们可以通过以下的方式来为这个 value 传值:
@Target(value = {ElementType.FIELD})
被这个 @Target 注解修饰的注解将根据ElementType枚举字段规定使用范围。其中,ElementType 是一个枚举类型,有以下一些值:
注解指定了被修饰的注解的生命周期,一种是只能在编译期可见,编译后会被丢弃,一种会被编译器编译进 class 文件中,无论是类或是方法,乃至字段,他们都是有属性表的,而 JAVA 虚拟机也定义了几种注解属性表用于存储注解信息,但是这种可见性不能带到方法区,类加载时会予以丢弃,最后一种则是永久存在的可见性。
用于指明当前注解的生命周期
package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
我们可以通过以下的方式来为这个 value 传值:
@Retention(value = RetentionPolicy.RUNTIME)
这里的 RetentionPolicy 依然是一个枚举类型,它有以下几个枚举值可取:
@Documented 注解修饰的注解,当我们执行 JavaDoc 文档打包时会被保存进 doc 文档,反之将在打包时丢弃。
@Inherited 注解修饰的注解是具有可继承性的,也就说我们的注解修饰了一个类,而该类的子类将自动继承父类的该注解。
除了上述四种元注解外,JDK 还为我们预定义了另外三种注解,它们是:
它没有任何的属性,所以并不能存储任何其他信息。它只能作用于方法之上,编译结束后将被丢弃。所以你看,它就是一种典型的『标记式注解』,仅被编译器可知,编译器在对 java 文件进行编译成字节码的过程中,一旦检测到某个方法上被修饰了该注解,就会去匹对父类中是否具有一个同样方法签名的函数,如果不是,自然不能通过编译。
package java.lang.annotation;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
依然是一种『标记式注解』,永久存在,可以修饰所有的类型,
package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
package java.lang.annotation;
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
/**
* 编译器要在带注释的元素中抑制的警告集。 允许重复名称。 名称的第二次和连续出现将被忽略。
* 出现无法识别的警告名称不是一个错误:编译器必须忽略它们无法识别的任何警告名称。
* 但是,如果注释包含无法识别的警告名称,它们可以自由发出警告。
*
* 字符串 {@code "unchecked"} 用于抑制未经检查的警告。
* 编译器供应商应结合此注释类型记录他们支持的其他警告名称。
* 鼓励他们合作以确保相同的名称在多个编译器中工作。 @return 要抑制的警告集
*/
String[] value();
}
通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。具体步骤如下 :
- 如果自定义异常类,考虑加上ResponseStatus注解;
- 对于没有ResponseStatus注解的异常,可以通过使用ExceptionHandler+ControllerAdvice注解,或者通过配置> - SimpleMappingExceptionResolver,来为整个Web应用提供统一的异常处理。
- 如果应用中有些异常处理方式,只针对特定的Controller使用,那么在这个Controller中使用ExceptionHandler注解。
- 不要使用过多的异常处理方式,不然的话,维护起来会很苦恼,因为异常的处理分散在很多不同的地方。
答:是单例模式,在多线程访问的时候有线程安全问题
不要在控制器里面定义可变状态的变量(成员变量)
可以使用ThreadLocal机制解决,为每个线程单独生成一份变量副本,独立操作,互不影响。
答:可以在@RequestMapping注解里面加上method=RequestMethod.GET。
@RequestMapping(value = "/test", method = RequestMethod.GET)
public ModelAndView test(){
// nothing to do
return new ModelAndView();
}
答:直接在@RequestParam中的params参数设置拦截字符串“type=test”
@RequestMapping(value = "/test", params = "type=test")
public ModelAndView test(){
// nothing to do
return new ModelAndView();
}
答:直接在方法的形参中声明request,SpringMvc就自动把request对象传入。
public void getSessionAction(HttpServletRequest request){
HttpSession session = request.getSession();
}
答:直接在形参里面声明这个参数就可以,但必须名字和传过来的参数一样。
答:直接在方法中声明这个对象,SpringMvc就自动会把属性赋值到这个对象里面。
性能上Struts1>SpringMvc>Struts2 开发速度上SpringMvc和Struts2差不多,比Struts1要高
答:返回值可以有很多类型,有String,ModelAndView。ModelAndView类把视图和数据都合并的一起的,但一般用String比较好。
答:通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前端就可以通过el表达式拿到。
答:可以在类上面加上@SessionAttributes注解,里面包含的字符串就是要放入session里面的key。
SpringMVC根据配置文件中InternalResourceViewResolver的前缀和后缀,用前缀+返回值+后缀组成完整的返回值
@RequestMapping(value = "/test", method = RequestMethod.POST)
public ModelAndView test(@RequestParam("ids") Long[] ids){
// nothing to do
System.out.println(ids);
return new ModelAndView();
}
Spring是一个轻量级的IOC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。主要包括以下七个模块:
IoC 的一个重点就是在程序运行时,动态的向某个对象提供它所引用的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。即应用程序在运行时依赖 IoC 容器来动态注入对象所引用的外部依赖。而 Spring 的 DI 具体就是通过反射实现注入的,反射允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性。
Spring 的 IoC 的实现原理就是工厂模式 + 反射机制。在 Spring 容器中,Bean 对象如何注册到 IoC 容器,以及Bean对象的加载、实例化、初始化详细过程可以阅读这篇文章:Spring的Bean加载流程_张维鹏的博客-CSDN博客
AspectJ是静态代理,也称为编译时增强,AOP框架会在编译阶段生成AOP代理类,并将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
public interface MyInterface {
public void database();
public void log();
}
class MyImplement implements MyInterface {
@Override
public void database() {
System.out.println("测试连接数据库操作");
}
@Override
public void log() {
System.out.println("测试编写系统运行日志");
}
}
public class DynamicProxyDemo {
public void database() {
System.out.println("测试连接数据库操作");
}
public void log() {
System.out.println("测试编写系统运行日志");
}
}
JDK动态代理只提供接口的代理,不支持类的代理,要求被代理类实现接口。JDK动态代理的核心是InvocationHandler接口和Proxy类,在获取代理对象时,使用Proxy类来动态创建目标类的代理类(即最终真正的代理类,这个类继承自Proxy并实现了我们定义的接口),当代理对象调用真实对象的方法时, InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;
InvocationHandler 的 invoke(Object proxy,Method method,Object[] args):proxy是最终生成的代理对象; method 是被代理目标实例的某个具体方法; args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。
/**
* Author: chaoyou
* CSDN:https://blog.csdn.net/qq_41910568
* Date: 2020/5/9 0009 0:17
* Content:jdk 动态代理是利用反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用 InvocationHandler 来处理
*/
public class JdkProxy implements InvocationHandler {
/**
* 声明一个代理的目标对象
*/
private Object target;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Jdk动态代理,监听开始!");
// 执行代理方法
Object invoke = method.invoke(target, args);
System.out.println("Jdk动态代理,监听结束!");
return invoke;
}
/**
* 为代理目标类创建一个临时类
*/
public Object getJdkProxy(Object target) {
this.target = target;
/**
* 为代理接口生成一个匿名实现类,并匿名实现类会把代理接口的实现类的方法全部克隆到匿名类中
*
* 由newProxyInstance 方法中的target.getClass().getInterfaces() 参数可知,代理目标对象必须要实现接口
*/
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
}
public static void main(String[] args){
JdkProxy jdkProxy = new JdkProxy();
// MyImplement 类实现了 MyInterface 接口
MyInterface myInterface = (MyInterface) jdkProxy.getJdkProxy(new MyImplement());
myInterface.database();
// DynamicProxyDemo 类型没有实现任何接口
DynamicProxyDemo dynamicProxyDemo = (DynamicProxyDemo) jdkProxy.getJdkProxy(new DynamicProxyDemo());
dynamicProxyDemo.log();
}
}
结果:没有实现接口的DynamicProxyDemo类报错了
如果被代理类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
/**
* Author: chaoyou
* CSDN:https://blog.csdn.net/qq_41910568
* Date: 2020/5/9 0009 0:27
* Content:cglib动态代理是利用asm 开源包,对代理对象类的class 文件加载进来,通过修改其字节码生成子类来处理。
*/
public class CglibProxy implements MethodInterceptor {
/**
* 声明一个代理的目标对象
*/
private Object target;
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("Cglib动态代理,监听开始!");
Object invoke = method.invoke(target, objects);
System.out.println("Cglib动态代理,监听结束!");
return invoke;
}
public Object getCglibProxy(Object target){
// 初始化代码目标对象
this.target = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(target.getClass());
enhancer.setCallback(this);
Object object = enhancer.create();
return object;
}
public static void main(String[] args){
CglibProxy jdkProxy = new CglibProxy();
// MyImplement 类实现了 MyInterface 接口
MyInterface myInterface = (MyInterface) jdkProxy.getCglibProxy(new MyImplement());
myInterface.database();
// DynamicProxyDemo 类型没有实现任何接口
DynamicProxyDemo dynamicProxyDemo = (DynamicProxyDemo) jdkProxy.getCglibProxy(new DynamicProxyDemo());
dynamicProxyDemo.log();
}
}
静态代理与动态代理区别在于生成AOP代理对象的时机不同(前者编译器,后者运行时),相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。
IoC让相互协作的组件保持松耦合,而AOP编程允许你把遍布于应用各层的通用功能抽离出来形成可重用的功能组件。
在Spring AOP中,切面可以在类上使用 @AspectJ 注解来实现。
切点分为execution方式和annotation方式。execution方式可以用路径表达式指定对哪些方法拦截,比如指定拦截add*、search*。annotation方式可以指定被哪些注解修饰的代码进行拦截,如下的JoinPoint是自定义注解。
package com.example.mongodb.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* Author: chaoyou
* Content:设置一个日志处理切面类
*/
@Aspect
@Component
public class LogAdvice {
// 定义一个切点:所有被 JoinPoint 注解修饰的方法会织入advice
@Pointcut("@annotation(com.example.mongodb.util.annotation.JoinPoint)")
private void logAdvicePointcut1() {}
/**
* 定义一个切面,拦截 com.example.mongodb.controller 包和子包下的所有方法
*
* 表达式解释:* com.example.mongodb.controller..*.*(..)
*
* 第一个 * 号的位置:表示返回值类型,* 表示所有类型。
* 包名:表示需要拦截的包名
* 两个点: 第一个点表当前包,第二个点表示子包
* 第二个 * 号的位置:表示类名,* 表示所有类。
* 第三个 * 号的位置:表示方法名,* 表示所有方法
* (..):表示方法有任意个参数(可以是int,String等)。
*/
@Pointcut("execution(* com.example.mongodb.controller..*.*(..))")
private void logAdvicePointcut2() {}
}
同一个Aspect,不同advice的执行顺序:
1、没有异常情况下的执行顺序:
around before advice
before advice
target method 执行
around after advice
after advice
afterReturning
2、有异常情况下的执行顺序:
around before advice
before advice
target method 执行
around after advice
after advice
afterThrowing
java.lang.RuntimeException: 异常发生
详细内容可以阅读这篇文章:Spring容器的启动流程_张维鹏的博客-CSDN博客
初始化Spring容器,注册内置的BeanPostProcessor的BeanDefinition到容器中
1.1. 实例化BeanFactory【DefaultListableBeanFactory】工厂,用于生成Bean对象
1.2. 实例化BeanDefinitionReader注解配置读取器,用于对特定注解(如@Service、@Repository)的类进行读取转化成 BeanDefinition 对象,(BeanDefinition 是 Spring 中极其重要的一个概念,它存储了 bean 对象的所有特征信息,如是否单例,是否懒加载,factoryBeanName 等)
1.3. 实例化ClassPathBeanDefinitionScanner路径扫描器,用于对指定的包目录进行扫描查找 bean 对象
将配置类的BeanDefinition注册到容器中
调用refresh()方法刷新容器
3.1. prepareRefresh()刷新前的预处理:
3.2. obtainFreshBeanFactory():获取在容器初始化时创建的BeanFactory:
3.3. prepareBeanFactory(beanFactory):BeanFactory的预处理工作,向容器中添加一些组件:
3.4. postProcessBeanFactory(beanFactory):子类重写该方法,可以实现在BeanFactory创建并预处理完成以后做进一步的设置
3.5. invokeBeanFactoryPostProcessors(beanFactory):在BeanFactory标准初始化之后执行BeanFactoryPostProcessor的方法,即BeanFactory的后置处理器:
3.6. registerBeanPostProcessors(beanFactory):向容器中注册Bean的后置处理器BeanPostProcessor,它的主要作用是干预Spring初始化bean的流程,从而完成代理、自动注入、循环依赖等功能
3.7. initMessageSource():初始化MessageSource组件,主要用于做国际化功能,消息绑定与消息解析:
3.8. initApplicationEventMulticaster():初始化事件派发器,在注册监听器时会用到:
3.9. onRefresh():留给子容器、子类重写这个方法,在容器刷新的时候可以自定义逻辑
3.10. registerListeners():注册监听器:将容器中所有的ApplicationListener注册到事件派发器中,并派发之前步骤产生的事件:
3.11. finishBeanFactoryInitialization(beanFactory):初始化所有剩下的单实例bean,核心方法是preInstantiateSingletons(),会调用getBean()方法创建对象;
3.12. finishRefresh():发布BeanFactory容器刷新完成事件:
ApplicationContext 继承 MessageSource,因此支持国际化。
资源文件访问,如URL和文件(ResourceLoader)。
载入多个(有继承关系)上下文(即同时加载多个配置文件) ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。
提供在监听器中注册bean的事件。
①BeanFactroy采用的是延迟加载形式来注入Bean的,只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能提前发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。
②ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。
③ApplicationContext启动后预载入所有的单实例Bean,所以在运行的时候速度比较快,因为它们已经创建好了。相对于BeanFactory,ApplicationContext 唯一的不足是占用内存空间,当应用程序配置Bean较多时,程序启动较慢。
简单来说,Spring Bean的生命周期只有四个阶段:实例化 Instantiation --> 属性赋值 Populate --> 初始化 Initialization --> 销毁 Destruction
但具体来说,Spring Bean的生命周期包含下图的流程:
实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以及 通过BeanWrapper提供的设置属性的接口完成属性设置与依赖注入。
Spring会检测该对象是否实现了xxxAware接口,通过Aware类型的接口,可以让我们拿到Spring容器的一些资源:
①如果这个Bean实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,传入Bean的名字;
②如果这个Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。
③如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。
④如果这个Bean实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文;
如果想对Bean进行一些自定义的前置处理,那么可以让Bean实现BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法。
如果Bean实现了InitializingBean接口,执行afeterPropertiesSet()方法。
如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的自定义初始化方法。
如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术;
以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。
当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;
最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的自定义销毁方法。
如果对bean详细加载流程的感兴趣的读者,可以阅读这篇文章:Spring的Bean加载流程_张维鹏的博客-CSDN博客
默认作用域,单例bean,每个容器中只有一个bean的实例。
因为所有对象都是共享这一个bean实例,所以spring中作用域为singleton下的bean不能保证安全性
每一个bean请求都会创建一个新的实例。
为每一个request请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。
与request范围类似,同一个session会话共享一个实例,不同会话使用不同的实例。
全局作用域,所有会话共享一个实例。如果想要声明让所有会话共享的存储变量的话,那么这全局变量需要存储在global-session中。
Spring容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但是具体情况还是要结合Bean的作用域来讨论。
每次都创建一个新对象,也就是线程之间不存在Bean共享,因此不会有线程安全问题。
所有的线程都共享一个单例实例的Bean,因此是存在线程安全问题的。但是如果单例Bean是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Controller类、Service类和Dao等,这些Bean大多是无状态的,只关注于方法本身。
有状态Bean(Stateful Bean) :就是有实例变量的对象,可以保存数据,是非线程安全的。
无状态Bean(Stateless Bean):就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。
案例代码:
package cn.zdxh.lcy.learn;
public class School {
private String schoolName;
public School(String schoolName) {
this.schoolName = schoolName;
}
public void setSchoolName(String schoolName) {
this.schoolName = schoolName;
}
public String getSchoolName() {
return schoolName;
}
}
// 需求:student 对象中注入 school 依赖的方式
public class Student {
private School school;
}
package cn.zdxh.lcy.learn;
public class Student {
private School school;
public void setSchool(School school) {
this.school = school;
}
}
<bean name="school" class="cn.zdxh.lcy.learn.School">bean>
<bean name="student" class="cn.zdxh.lcy.learn.Student">
<property name="school" ref="school">property>
bean>
package cn.zdxh.lcy.learn;
public class Student {
private School school;
private String username;
public Student(School school, String username) {
this.school = school;
this.username = username;
}
}
<bean name="school" class="cn.zdxh.lcy.learn.School">bean>
<bean name="student" class="cn.zdxh.lcy.learn.Student">
<constructor-arg index="0" ref="school">constructor-arg>
<constructor-arg index="1" type="java.lang.String" ref="username">constructor-arg>
bean>
package cn.zdxh.lcy.learn;
public class School {
public static final School initSchool() {
return new School();
}
}
public class Student {
private School school = School.initSchool();
}
<bean name="student" class="cn.zdxh.lcy.learn.Student" >
<property name="school" ref="getSchool">property>
bean>
<bean name="getSchool" class="cn.zdxh.lcy.learn.School" factory-method="initSchool">bean>
package cn.zdxh.lcy.learn;
public class School {
public School initSchool() {
return new School();
}
}
public class Student {
private School school;
}
<bean name="student" class="cn.zdxh.lcy.learn.Student">
<property name="school" ref="getSchool">property>
bean>
<bean name="school" class="cn.zdxh.lcy.learn.School">bean>
<bean name="getSchool" factory-bean="school" factory-method="initSchool">bean>
详细内容请参考这篇文章:Spring中bean的注入方式
详细内容强烈建议参考这篇文章:Spring如何解决循环依赖问题
public class ClassA {
private ClassB classB;
public ClassB getClassB() {
return classB;
}
public void setClassB(ClassB classB) {
this.classB = classB;
}
}
public class ClassB {
private ClassA classA;
public ClassA getClassA() {
return classA;
}
public void setClassA(ClassA classA) {
this.classA = classA;
}
}
循环依赖问题在Spring中主要有三种情况:
在Spring中,只有第(3)种方式的循环依赖问题被解决了,其他两种方式在遇到循环依赖问题时都会产生异常。其实也很好解释:
Spring解决的单例模式下的setter方法依赖注入引起的循环依赖问题,主要是通过两个缓存来解决的,请看下图:
这类循环依赖问题可以通过把bean改成单例的解决。
在spring中,使用autowire来配置自动装载模式,对象无需自己查找或创建与其关联的其他对象,由容器负责把需要相互协作的对象引用赋予各个对象。
package cn.zdxh.lcy.learn;
public class School {
protected void schoolName(){
System.out.println("这里是 Spring 自学体系院校");
}
}
public class Student {
private School school;
public void setSchool(School school) {
this.school = school;
}
public void mySchool(){
school.schoolName();
}
}
<beans>
<bean name="school" class="cn.zdxh.lcy.learn.School">bean>
<bean name="student" class="cn.zdxh.lcy.learn.Student">
<property name="school" ref="school" autowire="no">property>
bean>
beans>
public class TestDemo {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("resource/spring-config.xml");
Student student = (Student)context.getBean("student");
student.mySchool();
}
}
<beans>
<bean id="student" class="cn.suancioud.lcy.learn.Student" autowire="byName">bean>
<bean id="school" class="cn.suancioud.lcy.learn.School">bean>
beans>
<beans>
<bean id="student" class="cn.suancioud.lcy.learn.Student" autowire="byType">bean>
<bean id="school111" class="cn.suancioud.lcy.learn.School">bean>
beans>
package cn.zdxh.lcy.learn;
public class School {
protected void schoolName(){
System.out.println("这里是 Spring 自学体系院校");
}
}
public class Student {
private School school;
public Student(School school) {
this.school = school;
}
public void mySchool(){
school.schoolName();
}
}
<beans>
<bean id="school" class="cn.suancioud.lcy.learn.School">bean>
<bean id="student" class="cn.suancioud.lcy.learn.Student" autowire="constructor">bean>
beans>
使用@Autowired、@Resource注解来自动装配指定的bean。在使用@Autowired注解之前需要在Spring配置文件进行配置,。在启动spring IoC时,容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器,当容器扫描到@Autowied、@Resource或@Inject时,就会在IoC容器自动查找需要的bean,并装配给该对象的属性。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<bean id="school" class="cn.suancioud.lcy.learn.School">
<property name="disc" value="这里是 Spring 自学体系院校" />
bean>
<bean id="school1" class="cn.suancioud.lcy.learn.School">
<property name="disc" value="这里是 mybatis 自学体系院校" />
bean>
<bean id="student" class="cn.suancioud.lcy.learn.Student">bean>
<context:annotation-config />
beans>
public class School {
private String disc;
protected void schoolName(){
System.out.println(disc);
}
public void setDisc(String disc) {
this.disc = disc;
}
}
在使用@Autowired时,首先在容器中按类型查询对应的bean:
如果查询结果刚好为一个,就将该bean装配给@Autowired指定的数据;
如果查询的结果不止一个,那么@Autowired会根据名称来查找;
如果上述查找的结果为空,那么会抛出异常。解决方法时,使用required=false。
应用场景
@Autowired可用于:构造函数、成员变量、Setter方法
public class Student {
@Autowired
private School school;
public void mySchool(){
school.schoolName();
}
}
这个注解是java中自带的一个注解。它相当于@Qualifier与@AutoWired两者的结合,beanid可以不与set后面相同,当有多个相同class的bean时在后面加上一个参数(name = “beanid”)就可以装配指定id的bean。
注:@Autowired和@Resource之间的区别:
@Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。
@Resource默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。
public class Student {
@Resource(name = "school")
private School school;
public void mySchool(){
school.schoolName();
}
}
这个注解是为了解决一个类在bean容器中有多个bean的情况,因为用前面两种注解无法精确到哪个bean名称
public class Student {
// Qualifier + Autowired == Resource,必须搭配一起使用
@Autowired
@Qualifier("school1")
private School school;
public void mySchool(){
school.schoolName();
}
}
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。Spring只提供统一事务管理接口,具体实现都是由各数据库自己实现,数据库事务的提交和回滚是通过binlog或者undo log实现的。Spring会在事务开始时,根据当前环境中设置的隔离级别,调整数据库隔离级别,由此保持一致。
spring支持编程式事务管理和声明式事务管理两种方式
编程式事务管理使用TransactionTemplate。
声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。
spring事务的传播机制说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。事务传播机制实际上是使用简单的ThreadLocal实现的,所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。
PROPAGATION_REQUIRED(必需的):(默认传播行为)如果当前没有事务,就创建一个新事务;如果当前存在事务,就加入该事务。
PROPAGATION_REQUIRES_NEW(总是新的):无论当前存不存在事务,都创建新事务进行执行。
PROPAGATION_SUPPORTS(支持):如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行。‘
PROPAGATION_NOT_SUPPORTED(不支持):以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NESTED(嵌套):如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUIRED属性执行。
PROPAGATION_MANDATORY(强制性):如果当前存在事务,就加入该事务;如果当前不存在事务,就抛出异常。
PROPAGATION_NEVER(决不):以非事务方式执行,如果当前存在事务,则抛出异常。
ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。
ISOLATION_READ_UNCOMMITTED:读未提交,允许事务在执行过程中,读取其他事务未提交的数据。
ISOLATION_READ_COMMITTED:读已提交,允许事务在执行过程中,读取其他事务已经提交的数据。
ISOLATION_REPEATABLE_READ:可重复读,在同一个事务内,任意时刻的查询结果都是一致的。
ISOLATION_SERIALIZABLE:所有事务逐个依次执行。
Spring事务 的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:
获取连接 Connection con = DriverManager.getConnection(); ①
开启事务con.setAutoCommit(true/false); ②
执行CRUD ③
提交事务/回滚事务 con.commit() / con.rollback(); ④
关闭连接 conn.close(); ⑤
使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由Spirng 自动完成。 那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。下面简单地介绍下,注解方式为例子
配置文件开启注解驱动
在相关的类和方法上通过注解@Transactional标识
spring 在启动的时候会去解析生成相关的bean,这时候会查看拥有@Transactional注解的类和方法。
根据@Transaction的相关参数进行相关配置注入, 利用代理的方式在类和方法的前置(开启事务)、后继(提交/回滚事务)。
真正的数据库层的事务提交和回滚是通过binlog或者redo log实现的。
Spring设计模式的详细使用案例可以阅读这篇文章:Spring中所使用的设计模式_张维鹏的博客-CSDN博客_spring使用的设计模式
工厂模式:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象
单例模式:Bean默认为单例模式
策略模式:例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略
代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术
模板方法:可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中,用来解决代码重复的问题。比如RestTemplate, JmsTemplate, JpaTemplate
适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring MVC中也是用到了适配器模式适配Controller
观察者模式:Spring事件驱动模型就是观察者模式的一个经典应用。
桥接模式:可以根据客户的需求能够动态切换不同的数据源。比如我们的项目需要连接多个数据库,客户在每次访问中根据需要会去访问不同的数据库
Spring 提供了以下5种标准的事件:
上下文更新事件(ContextRefreshedEvent):在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。
上下文开始事件(ContextStartedEvent):当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。
上下文停止事件(ContextStoppedEvent):当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。
上下文关闭事件(ContextClosedEvent):当ApplicationContext被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。
请求处理事件(RequestHandledEvent):在Web应用中,当一个http请求(request)结束触发该事件。
如果一个bean实现了ApplicationListener接口,当一个ApplicationEvent 被发布以后,bean会自动被通知。
标识一个类是Spring MVC controller处理器,用来创建处理http请求的对象。
Spring4之后加入的注解,原来在@Controller中返回json需要@ResponseBody来配合,如果直接用@RestController替代@Controller就不需要再配置@ResponseBody,默认返回json格式。
用于标注业务层组件,说白了就是用注解的方式把这个类注入到spring配置中。
用来装配bean,可以写在字段上,或者方法上。
默认情况下必须要求依赖对象必须存在,如果要允许null值,可以设置它的required属性为false,例如:@Autowired(required=false)
@Resource的作用相当于@Autowired
只不过@Autowired按byType自动注入,而@Resource默认按 byName自动注入罢了。
用于将请求参数区数据映射到功能处理方法的参数上。
将请求参数绑定在url地址后面。
@RequestMapping(value="/happy/{dayid}",method=RequestMethod.GET)
public String findPet(@PathVariable String dayid, Model mode) {
//使用@PathVariable注解绑定 {dayid} 到String dayid
}
当标记在一个方法上时表示该方法是支持缓存的,
当标记在一个类上时则表示该类所有的方法都是支持缓存的。
用于标注数据访问组件,即DAO组件。
泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。
用来配置 spring bean 的作用域,它标识 bean 的作用域。
默认情况下Spring MVC将模型中的数据存储到request域中。当一个请求结束后,数据就失效了。如果要跨页面使用。那么需要使用到session。而@SessionAttributes注解就可以使得模型中的数据存储一份到session域中。
当你创建多个具有相同类型的 bean 时,并且想要用一个属性只为它们其中的一个进行装配,在这种情况下,你可以使用 @Qualifier 注释和 @Autowired 注释通过指定哪一个真正的 bean 将会被装配来消除混乱。
使用@Configuration 来注解类表示类可以被 Spring 的 IoC 容器所使用,作为 bean 定义的资源。
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
这个注解用于将url映射到整个处理类或者特定的处理请求的方法。
事务的注解,可以添加在方法或者类上。一般注解在业务层。
SpringBoot来简化Spring应用开发,约定大于配置,去繁化简
Spring Boot 而且内嵌了各种 servlet 容器,Tomcat、Jetty 等,现在不再需要打成war 包部署到容器中,Spring Boot 只要打成一个可执行的 jar 包就能独立运行,所有的依赖包都在一个 jar 包内。
spring-boot-starter-web 启动器自动依赖其他组件,简少了 maven 的配置。
Spring Boot 能根据当前类路径下的类、jar 包来自动配置 bean,如添加一个 springboot-starter-web 启动器就能拥有 web 的功能,无需其他配置。
SpringBoot 配置过程中无代码生成,也无需 XML 配置文件就能完成所有配置工作,这一切都是借助于条件注解完成的,这也是 Spring4.x 的核心功能之一。
SpringBoot 提供一系列端点可以监控服务及应用,做健康检测。
Spring Boot 虽然上手很容易,但如果你不了解其核心技术及流程,所以一旦遇到问题就很棘手,而且现在的解决方案也不是很多,需要一个完善的过程。
Spring最重要的特征是依赖注入。所有Spring Modules不是依赖注入就是IOC控制反转。
当我们恰当的使用DI或者是IOC的时候,可以开发松耦合应用。
Spring MVC提供了一种分离式的方法来开发Web应用。通过运用像DispatcherServelet,MoudlAndView 和 ViewResolver 等一些简单的概念,开发 Web 应用将会变的非常简单。
Spring和Spring MVC的问题在于需要配置大量的参数。
SpringBoot通过一个自动配置和启动的项来解决这个问题。
启动器是一套方便的依赖描述符,它可以放在自己的程序中。可以一站式的获取你所需要的Spring和相关技术,而不需要依赖描述符的通过示例代码搜索和复制粘贴的负载。
例如,如果想使用Spring和JPA访问数据库,只需要项目中包含spring-boot-starter-data-jpa 依赖项,你就可以正产是用。
spring-boot-maven-plugin提供了一些像jar一样打包或者运行应用程序的命令。
spring-boot:run 运行SpringBoot应用程序;
spring-boot:repackage 重新打包你的jar包或者是war包使其可执行
spring-boot:start和spring-boot:stop管理Spring Boot应用程序的生命周期
spring-boot:build-info生成执行器可以使用的构造信息
YAML是一种人类可读的数据序列化语言。它通常用于配置文件。
与属性文件相比,如果我们想要在配置文件中添加复杂的属性,YAML文件就更加结构化,而且更少混淆。可以看出YAML具有分层配置数据。
在Spring程序main方法的主类上,添加@SpringBootApplication或者@EnableAutoConfiguration会自动去maven中读取每个starter中的spring.gfactories文件,该文件里配置了所有需要被加载到Spring容器中的bean
我们知道,新建一个SpringBoot项目,默认都是有parent的,这个parent就是spring-boot-starter-parent,spring-boot-starter-parent主要有如下作用:
定义了Java编译版本为1.8
使用UTF-8格式编码
继承自spring-boor-dependencies,这里面定义了依赖的版本,也正是因为继承了这个依赖,所以我们在写依赖时才不需要写版本号
执行打包操作的配置
自动化的资源过滤
自动化的插件配置
针对application.peoperties和application.yuml的资源过滤,包括通过profile定义的不同环境的配置文件,例如application-dev.properties和application-dev.yuml。
Spring Boot 项目最终打包成的 jar 是可执行 jar ,这种 jar 可以直接通过java -jar xxx.jar命令来运行,这种 jar 不可以作为普通的 jar 被其他项目依赖,即使依赖了也无法使用其中的类。
Spring Boot 的 jar 无法被其他项目依赖,主要还是他和普通 jar 的结构不同。普通的 jar 包,解压后直接就是包名,包里就是我们的代码,而 Spring Boot 打包成的可执行 jar 解压后,在 \BOOT-INF\classes目录下才是我们的代码,因此无法被直接引用。如果非要引用,可以在 pom.xml 文件中增加配置,将 Spring Boot 项目打包成两个 jar ,一个可执行,一个可引用。
Spring Data 是 Spring 的一个子项目。用于简化数据库访问,支持NoSQL 和 关系数据存储。其主要目标是使数据库的访问变得方便快捷。Spring Data 具有如下特点:
Spring Data Jpa 致力于减少数据访问层 (DAO) 的开发量. 开发者唯一要做的,就是声明持久层的接口,其他都交给 Spring Data JPA 来帮你完成!Spring Data JPA 通过规范方法的名字,根据符合规范的名字来确定方法需要实现什么样的逻辑。
Swagger 广泛用于可视化 API,使用 Swagger UI 为前端开发人员提供在线沙箱。Swagger 是用于生成 RESTful Web 服务的可视化表示的工具,规范和完整框架实现。它使文档能够以与服务器相同的速度更新。当通过 Swagger 正确定义时,消费者可以使用最少量的实现逻辑来理解远程服务并与其进行交互。因此,Swagger消除了调用服务时的猜测。
前后端分离开发日益流行,大部分情况下,我们都是通过 Spring Boot 做前后端分离开发,前后端分离一定会有接口文档,不然会前后端会深深陷入到扯皮中。一个比较笨的方法就是使用 word 或者 md 来维护接口文档,但是效率太低,接口一变,所有人手上的文档都得变。在 Spring Boot 中,这个问题常见的解决方案是 Swagger ,使用 Swagger 我们可以快速生成一个接口文档网站,接口一旦发生变化,文档就会自动更新,所有开发工程师访问这一个在线网站就可以获取到最新的接口文档,非常方便。
Spring提供了一种使用ControllerAdvice处理异常的非常有用的方法。通过实现一个ControlerAdvice类,来处理控制类抛出的所有异常。
FreeMarker 是一个基于 Java 的模板引擎,最初专注于使用 MVC 软件架构进行动态网页生成。使用 Freemarker 的主要优点是表示层和业务层的完全分离。程序员可以处理应用程序代码,而设计人员可以处理 html 页面设计。最后使用freemarker 可以将这些结合起来,给出最终的输出页面。
为了实现Spring Boot的安全性,使用spring-boot-starter-security依赖项,并且必须添加安全配置。它只需要很少代码。配置类将必须扩展WebSecurityConfigurerAdapter并覆盖其方法。
由于Spring Boot官方提供了大量的非常方便的开箱即用的Starter,包括Spring Security的Starter,使得在SpringBoot中使用Spring Security变得更加容易,甚至只需要添加一个一来就可以保护所有接口,所以如果是SpringBoot项目,一般选择Spring Security。当然这只是一个建议的组合,单纯从技术上来说,无论怎么组合,都是没有问题的。
跨域可以在前端通过JSONP来解决,但是JSONP只可以发送GET请求,无法发送其他类型的请求,在RESTful风格的应用中,就显得非常鸡肋,因此推荐在后端通过(CORS,Cross-origin resource sharing)来解决跨域问题。这种解决方案并非Spring Boot特有的,在传统的SSM框架中,就可以通过CORS来解决跨域问题,只不过之前我们是在XML文件中配置CORS,现在可以通过实现WebMvcConfigurer接口然后重写addCorsMappings方法解决跨域问题。
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.maxAge(3600);
}
}
项目中前后端分离部署,所以需要解决跨域的问题。
我们使用cookie存放用户登录的信息,在spring拦截器进行权限控制,当权限不符合时,直接返回给用户固定的json结果。
当用户登录以后,正常使用;当用户退出登录状态时或者token过期时,由于拦截器和跨域的顺序有问题,出现了跨域的现象。
我们知道一个http请求,先走filter,到达servlet后才进行拦截器的处理,如果我们把cors放在filter里,就可以优先于权限拦截器执行。
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}
CSRF 代表跨站请求伪造。这是一种攻击,迫使最终用户在当前通过身份验证的Web 应用程序上执行不需要的操作。CSRF 攻击专门针对状态改变请求,而不是数据窃取,因为攻击者无法查看对伪造请求的响应。
启动类上面的注解是@SpringBootApplication,他也是SpringBoot的核心注解,主要组合包含了以下3个注解:
SpringBoot的核心配置文件是application和bootstrap配置文件。
使用Spring Cloud Config配置中心时,这时需要在bootstrap配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;
一些固定的不能被覆盖的属性;
一些加密/解密的场景
Spring Boot 可 以 通 过 @PropertySource,@Value,@Environment, @ConfigurationProperties 来绑定变量。
Spring Boot 支持 Java Util Logging, Log4j2, Lockback 作为日志框架,如果你使用Starters 启动器,Spring Boot 将使用 Logback 作为默认日志框架。
在生产中使用HTTPS
使用Snyk检查你的依赖关系
升级到最新版本
启用CSRF保护
使用内容安全策略防止XSS攻击
配置变更
JDK版本升级
第三方类库升级
响应式Spring编程支持
HTTP/2支持
配置属性绑定
更多改进与加强
每种MQ没有绝对的好坏,主要依据使用场景,扬长避短,利用其优势,规避其劣势。
- 不考虑 rocketmq 的原因是,rocketmq是阿里出品,如果阿里放弃维护rocketmq,中小型公司一般抽不出人来进行rocketmq的定制化开发,因此不推荐。
- 不考虑 kafka 的原因是:中小型软件公司不如互联网公司,数据量没那么大,选消息中间件应首选功能比较完备的,所以kafka排除
RabbitMQ 是 AMQP 协议的一个开源实现,所以其内部实际上也是 AMQP 中的基本概念:
生产者Publisher:生产消息,就是投递消息的一方。消息一般包含两个部分:消息体(payload)和标签(Label)
消费者Consumer:消费消息,也就是接收消息的一方。消费者连接到RabbitMQ服务器,并订阅到队列上。消费消息时只消费消息体,丢弃标签。
Broker服务节点:表示消息队列服务器实体。一般情况下一个Broker可以看做一个RabbitMQ服务器。
Queue:消息队列,用来存放消息。一个消息可投入一个或多个队列,多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。
Exchange:交换器,接受生产者发送的消息,根据路由键将消息路由到绑定的队列上。
Routing Key: 路由关键字,用于指定这个消息的路由规则,需要与交换器类型和绑定键(Binding Key)联合使用才能最终生效。
Binding:绑定,通过绑定将交换器和队列关联起来,一般会指定一个BindingKey,通过BindingKey,交换器就知道将消息路由给哪个队列了。
Connection:网络连接,比如一个TCP连接,用于连接到具体broker
Channel: 信道,AMQP 命令都是在信道中进行的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为建立和销毁 TCP 都是非常昂贵的开销,所以引入了信道的概念,以复用一条 TCP 连接,一个TCP连接可以用多个信道。客户端可以建立多个channel,每个channel表示一个会话任务。
Message:消息,由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
Virtual host:虚拟主机,用于逻辑隔离,表示一批独立的交换器、消息队列和相关对象。一个Virtual host可以有若干个Exchange和Queue,同一个Virtual host不能有同名的Exchange或Queue。最重要的是,其拥有独立的权限系统,可以做到 vhost 范围的用户控制。当然,从 RabbitMQ 的全局角度,vhost 可以作为不同权限隔离的手段
Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers
匹配规则:
- RoutingKey 和 BindingKey 为一个 点号 ‘.’ 分隔的字符串。 比如: java.xiaoka.show
- BindingKey可使用 * 和 # 用于做模糊匹配:匹配一个单词,#匹配多个或者0个单词。如:order.
- Producer 先连接到 Broker,建立连接 Connection,开启一个信道 channel
- Producer 声明一个交换器并设置好相关属性
- Producer 声明一个队列并设置好相关属性
- Producer 通过绑定键将交换器和队列绑定起来
- Producer 发送消息到 Broker,其中包含路由键、交换器等信息
- 交换器根据接收到的路由键查找匹配的队列
- 如果找到,将消息存入对应的队列,如果没有找到,会根据生产者的配置策略丢弃或者退回给生产者。
- 关闭信道
- Producer 先连接到 Broker,建立连接 Connection,开启一个信道 channel
- 向 Broker 请求消费相应队列中消息,可能会设置响应的回调函数。
- 等待 Broker 回应并投递相应队列中的消息,接收消息。
- 消费者确认收到的消息,并响应ack(消费确认)。
- RabbitMQ从队列中删除已经确定的消息。
- 关闭信道
正常情况下,Consumer在消费消息后会对Queue响应一个确认(ack),Queue接收后就知道消息已经被成功消费了,然后就从队列中删除该消息,也就不会将该消息再发送给其他消费者了。不同消息队列发出的确认消息形式不同,RabbitMQ是通过发送一个ACK确认消息。但是因为网络故障,Consumer发出的确认并没有传到Queue,导致Queue不知道该消息已经被消费,然后再次将消息发送给了其他Consumer,从而造成重复消费的情况。
重复消费问题的解决思路是:保证消息的唯一性,即使多次传输,也不让消息的多次消费带来影响,也就是保证消息等幂性;幂等性指一个操作执行任意多次所产生的影响均与一次执行的影响相同。具体解决方案如下:
- 通过数据库:比如处理订单时,记录订单ID,在消费前,去数据库中进行查询该记录是否存在,如果存在则直接返回。
- 使用全局唯一ID,再配合第三组主键做消费记录。比如,利用redis的set结构处理,生产者发送消息时给消息分配一个全局ID。消费者在每次拿到消息后,并且在消费前,先去redis中查询这个ID是否存在,如果存在则放弃这条消息的消费操作,如果不存在则对这条信息进行后续的消费操作,消费完之后,就将这个ID以k-v的形式存入redis中(过期时间根据具体情况设置)。
对于消息的可靠性传输,每种MQ都要从三个角度来分析:生产者丢数据、消息队列丢数据、消费者丢数据。以RabbitMQ为例:
RabbitMQ提供事务机制(transaction)和确认机制(confirm)两种模式来确保生产者不丢消息。
发送消息前,开启事务(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事务就会回滚(channel.txRollback()),如果发送成功则提交事务(channel.txCommit())
该方式的缺点是生产者发送消息会同步阻塞等待发送结果是成功还是失败,导致生产者发送消息的吞吐量降下降。
// 开启事务
channel.txSelect
try {
// 发送消息
} catch(Exception e){
// 回滚事务
channel.txRollback;
//再次重试发送这条消息
....
}
//提交事务
channel.txCommit;
生产环境常用的是confirm模式。生产者将信道 channel 设置成 confirm 模式,一旦 channel 进入 confirm 模式,所有在该信道上发布的消息都将会被指派一个唯一的ID,一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个确认给生产者(包含消息的唯一ID),这样生产者就知道消息已经正确到达目的队列了。如果rabbitMQ没能处理该消息,也会发送一个Nack消息给你,这时就可以进行重试操作。
Confirm模式最大的好处在于它是异步的,一旦发布消息,生产者就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者便可以通过回调方法来处理该确认消息。
处理Ack的代码如下所示:
@Bean
public RabbitTemplate rabbitTemplate(){
Logger logger = LoggerFactory.getLogger(MyAMQPConfig.class);
// 设置为 true 后 消费者在消息没有被路由到合适队列情况下会被return监听,而不会自动删除
rabbitTemplate.setMandatory(true);
// 消息返回,需要配置 spring.rabbitmq.publisher-returns=true
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
String correlationId = message.getMessageProperties().getCorrelationId();
logger.debug("消息 : {} 发送失败,应答码 : {},原因 : {},交换机 : {},路由键 : {}", correlationId, replyCode, replyText, exchange, routingKey);
});
// 开启消息消费确认机制,需要配置:spring.rabbitmq.publisher-confirms=true
rabbitTemplate.setConfirmCallback(((correlationData, ack, cause) -> {
if (ack){
// logger.debug("消息发送到 exchange 成功,id : {}", correlationData.getId());
} else {
logger.debug("消息发送到 exchange 失败, : {}", correlationData.getId());
}
}));
return rabbitTemplate;
}
处理消息队列丢数据的情况,一般是开启持久化磁盘。持久化配置可以和生产者的 confirm 机制配合使用,在消息持久化磁盘后,再给生产者发送一个Ack信号。这样的话,如果消息持久化磁盘之前,即使 RabbitMQ 挂掉了,生产者也会因为收不到Ack信号而再次重发消息。
持久化设置如下(必须同时设置以下 2 个配置):
- 创建queue的时候,将queue的持久化标志durable在设置为true,代表是一个持久的队列,这样就可以保证 rabbitmq 持久化 queue 的元数据,但是不会持久化queue里的数据;
- 发送消息的时候将 deliveryMode 设置为 2,将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。
这样设置以后,RabbitMQ 就算挂了,重启后也能恢复数据。在消息还没有持久化到硬盘时,可能服务已经死掉,这种情况可以通过引入镜像队列,但也不能保证消息百分百不丢失(整个集群都挂掉)
引入镜像队列(Mirror Queue)的机制,可以将队列镜像到集群中的其他 Broker 节点之上,如果集群中的一个节点失效了,队列能自动地切换到镜像中的另一个节点上以保证服务的可用性。
消费者丢失数据一般是因为采用了自动确认消息模式。该模式下,虽然消息还在处理中,但是消费中者会自动发送一个确认,通知 RabbitMQ 已经收到消息了,这时 RabbitMQ 就会立即将消息删除。这种情况下,如果消费者出现异常而未能处理消息,那就会丢失该消息。
解决方案就是采用手动确认消息,设置 autoAck = False,等到消息被真正消费之后,再手动发送一个确认信号,即使中途消息没处理完,但是服务器宕机了,那 RabbitMQ 就收不到发的ack,然后 RabbitMQ 就会将这条消息重新分配给其他的消费者去处理。
但是 RabbitMQ 并没有使用超时机制,RabbitMQ 仅通过与消费者的连接来确认是否需要重新发送消息,也就是说,只要连接不中断,RabbitMQ 会给消费者足够长的时间来处理消息。另外,采用手动确认消息的方式,我们也需要考虑一下几种特殊情况:
需要注意的点:
- 消息可靠性增强了,性能就下降了,因为写磁盘比写 RAM 慢的多,两者的吞吐量可能有 10 倍的差距。所以,是否要对消息进行持久化,需要综合考虑业务场景、性能需要,以及可能遇到的问题。若想达到单RabbitMQ服务器 10W 条/秒以上的消息吞吐量,则要么使用其他的方式来确保消息的可靠传输,要么使用非常快速的存储系统以支持全持久化,例如使用 SSD。或者仅对关键消息作持久化处理,且应该保证关键消息的量不会导致性能瓶颈。
- 当设置 autoAck = False 时,如果忘记手动 ack,那么将会导致大量任务都处于 Unacked 状态,造成队列堆积,直至消费者断开才会重新回到队列。解决方法是及时 ack,确保异常时 ack 或者拒绝消息。
- 启用消息拒绝或者发送 nack 后导致死循环的问题:如果在消息处理异常时,直接拒绝消息,消息会重新进入队列。这时候如果消息再次被处理时又被拒绝 。这样就会形成死循环。
针对保证消息有序性的问题,解决方法就是保证生产者入队的顺序是有序的,出队后的顺序消费则交给消费者去保证。
拆分queue,使得一个queue只对应一个消费者。由于MQ一般都能保证内部队列是先进先出的,所以把需要保持先后顺序的一组消息使用某种算法都分配到同一个消息队列中。然后只用一个消费者单线程去消费该队列,这样就能保证消费者是按照顺序进行消费的了。但是消费者的吞吐量会出现瓶颈。如果多个消费者同时消费一个队列,还是可能会出现顺序错乱的情况,这就相当于是多线程消费了
对于多线程的消费同一个队列的情况,可以使用重试机制:比如有一个微博业务场景的操作,发微博、写评论、删除微博,这三个异步操作。如果一个消费者先执行了写评论的操作,但是这时微博都还没发,写评论一定是失败的,等一段时间。等另一个消费者,先执行发微博的操作后,再执行,就可以成功。
场景题:几千万条数据在MQ里积压了七八个小时。
消息堆积往往是生产者的生产速度与消费者的消费速度不匹配导致的。有可能就是消费者消费能力弱,渐渐地消息就积压了,也有可能是因为消息消费失败反复复重试造成的,也有可能是消费端出了问题,导致不消费了或者消费极其慢。比如,消费端每次消费之后要写mysql,结果mysql挂了,消费端hang住了不动了,或者消费者本地依赖的一个东西挂了,导致消费者挂了。
所以如果是 bug 则处理 bug;如果是因为本身消费能力较弱,则优化消费逻辑,比如优化前是一条一条消息消费处理的,那么就可以批量处理进行优化。
这种做法相当于临时将 queue 资源和 consumer 资源扩大 N 倍,以正常 N 倍速度消费。
如果使用的是 rabbitMQ,并且设置了过期时间,消息在 queue 里积压超过一定的时间会被 rabbitmq 清理掉,导致数据丢失。这种情况下,实际上队列中没有什么消息挤压,而是丢了大量的消息。所以就不能说增加 consumer 消费积压的数据了,这种情况可以采取 “批量重导” 的方案来进行解决。在流量低峰期,写一个程序,手动去查询丢失的那部分数据,然后将消息重新发送到mq里面,把丢失的数据重新补回来。
如果消息积压在MQ里,并且长时间都没处理掉,导致MQ都快写满了,这种情况肯定是临时扩容方案执行太慢,这种时候只好采用 “丢弃+批量重导” 的方式来解决了。首先,临时写个程序,连接到mq里面消费数据,消费一个丢弃一个,快速消费掉积压的消息,降低MQ的压力,然后在流量低峰期时去手动查询重导丢失的这部分数据。
RabbitMQ 是基于主从(非分布式)做高可用性的,RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式
一般没人生产用单机模式
就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。我们创建的 queue,只会放在其中一个 RabbitMQ 实例上,但是每个实例都同步 queue 的元数据(元数据是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。消费的时候,如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。
普通集群模式主要用于提高系统的吞吐量,可以通过添加更加的节点来线性的扩展消息队列的吞吐量,就是说让集群中多个节点来服务某个 queue 的读写操作
无高可用性,queue所在的节点宕机了,其他实例就无法从那个实例拉取数据;RabbitMQ 内部也会产生大量的数据传输。
RabbitMQ 真正的高可用模式。镜像集群模式下,队列的元数据和消息会存在于多个实例上,每次写消息到 queue 时,会自动将消息同步到各个实例的 queue ,也就是说每个 RabbitMQ 节点都有这个 queue 的完整镜像,包含 queue 的全部数据。任何一个机器宕机了,其它机器节点还包含了这个 queue 的完整数据,其他 consumer 都可以到其它节点上去消费数据。
配置镜像队列的集群都包含一个主节点master和若干个从节点slave,slave会准确地按照master执行命令的顺序进行动作,故slave与master上维护的状态应该是相同的。如果master由于某种原因失效,那么按照slave加入的时间排序,"资历最老"的slave会被提升为新的master。
除发送消息外的所有动作都只会向master发送,然后再由master将命令执行的结果广播给各个slave。如果消费者与slave建立连接并进行订阅消费,其实质上都是从master上获取消息,只不过看似是从slave上消费而已。比如消费者与slave建立了TCP连接之后执行一个Basic.Get的操作,那么首先是由slave将Basic.Get请求发往master,再由master准备好数据返回给slave,最后由slave投递给消费者。
有效解决了普通集群的高可用性缺点。因为有了选举机制,不会因为master的宕机而造成整个集群的瘫痪。
在RabbitMQ 的管理控制台Admin页面下,新增一个镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
- 消息被拒绝(basic.reject/basic.nack)同时 requeue=false(不重回队列)
- TTL 过期
- 队列达到最大长度,无法再添加
以下介绍消息队列在实际应用中常用的使用场景。异步处理,应用解耦,流量削锋和消息通讯四个场景,详情请看
场景说明:用户注册后,需要发注册邮件和注册短信。传统的做法有两种 1.串行的方式;2.并行方式
串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端
并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间
假设三个业务节点每个使用50毫秒钟,不考虑网络等其他开销,则串行方式的时间是150毫秒,并行的时间可能是100毫秒。
因为CPU在单位时间内处理的请求数是一定的,假设CPU1秒内吞吐量是100次。则串行方式1秒内CPU可处理的请求量是7次(1000/150)。并行方式处理的请求量是10次(1000/100)
小结:如以上案例描述,传统的方式系统的性能(并发量,吞吐量,响应时间)会有瓶颈。如何解决这个问题呢?
引入消息队列,将不是必须的业务逻辑,异步处理。改造后的架构如下:
按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是50毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是50毫秒。因此架构改变后,系统的吞吐量提高到每秒20 QPS。比串行提高了3倍,比并行提高了两倍
用户下单后,订单系统需要通知库存系统。传统的做法是,订单系统调用库存系统的接口。如下图
流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛
秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。
日志处理是指将消息队列用在日志处理中,比如Kafka的应用,解决大量日志传输的问题。架构简化如下
消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等
客户端A,客户端B,客户端N订阅同一主题,进行消息发布和接收。实现类似聊天室效果。
以上实际是消息队列的两种消息模式,点对点或发布订阅模式。模型为示意图,供参考。
集群由三个节点组成(由于节点数少所以使用公用版ES集群设置,三个节点均可参与选举master、作为数据节点、作为搜索节点)。每个索引拆分为三个分片(shard)、索引副本数是2,即每个索引有6个分片(3主 + 6副),所以每个节点分配到两个分片(一主一副,不能为同一份数据)。这样的集群可以保证在一个节点宕机之后还能正常运行,容灾能力只有65%,这样将大大提高es集群的高可用性。
把同一个索引的主分片(shard)和副分片(replica)分配到不同的节点上,可以在elasticsearch.yml配置文件中设置参数
{
"order": 0,
"index_patterns": [
"classify_two*" # 索引名称格式( * 代表后面一个或多个字符)
],
"settings": {
"index": {
"max_result_window": "100000000", # 索引的最大检索范围
"refresh_interval": "1s", # 刷新时限
"number_of_shards": "3", # 分片数
"number_of_replicas": "1", # 每个分片的副本数
"analysis": { # 分词器配置
"analyzer": {
"default": {
"type": "ik_smart"
}
}
}
}
},
"mappings": {
"data": {
"properties": { # 索引字段设置
"FIELD": {
"type": "keyword"
},
}
}
},
"aliases": {
"classify_two": {} # 索引别名设置
}
}
解答:索引数据的规划,应在做好前期规划,正所谓“设计先行,编码在后”,这样才能有效的避免突如其来的数据激增导致集群处理能力不足引发的线上客户检索或者其他业务受到影响。
设计模式是一套经过反复使用的代码设计经验,目的是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 设计模式于己于人于系统都是多赢的,它使得代码编写真正工程化,它是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。总体来说,设计模式分为三大类:
- 创建型模式:共5种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
- 结构型模式:共7种:适配器模式、装饰器模式、代理模式、桥接模式、外观模式、组合模式、享元模式
- 行为型模式:共11种:策略模式、模板方法模式、观察者模式、责任链模式、访问者模式、中介者模式、迭代器模式、命令模式、状态模式、备忘录模式、解释器模式
开闭原则指的是对扩展开放,对修改关闭。在对程序进行扩展的时候,不能去修改原有的代码,想要达到这样的效果,我们就需要使用接口或者抽象类
依赖倒置原则是开闭原则的基础,指的是针对接口编程,依赖于抽象而不依赖于具体
使用多个隔离的接口,比使用单个接口要好,降低接口之间的耦合度与依赖,方便升级和维护方便
迪米特原则,也叫最少知道原则,指的是一个类应当尽量减少与其他实体进行相互作用,使得系统功能模块相对独立,降低耦合关系。该原则的初衷是降低类的耦合,虽然可以避免与非直接的类通信,但是要通信,就必然会通过一个“中介”来发生关系,过分的使用迪米特原则,会产生大量的中介和传递类,导致系统复杂度变大,所以采用迪米特法则时要反复权衡,既要做到结构清晰,又要高内聚低耦合。
尽量使用组合/聚合的方式,而不是使用继承。
接下来我们详细介绍Java中23种设计模式的概念,应用场景等情况,并结合他们的特点及设计模式的原则进行分析
工厂方法模式分为三种:简单工厂模式、工厂方法模式、静态工厂模式。
建立一个工厂类,并定义一个接口对实现了同一接口的产品类进行创建。首先看下关系图:
工厂方法模式是对简单工厂模式的改进,简单工厂的缺陷在于不符合“开闭原则”,每次添加新产品类就需要修改工厂类,不利于系统的扩展维护。而工厂方法将工厂抽象化,并定义一个创建对象的接口。每增加新产品,只需增加该产品以及对应的具体实现工厂类,由具体工厂类决定要实例化的产品是哪个,将对象的创建与实例化延迟到子类,这样工厂的设计就符合“开闭原则”了,扩展时不必去修改原来的代码。UML关系图如下:
静态工厂模式是将工厂方法模式里的方法置为静态的,不需要创建实例,直接调用即可。
抽象工厂模式主要用于创建相关对象的家族。当一个产品族(电池族、屏幕族)中需要被设计在一起工作时,通过抽象工厂模式,能够保证客户端始终只使用同一个产品族中的对象;并且通过隔离具体类的生成,使得客户端不需要明确指定具体生成类;所有的具体工厂都实现了抽象工厂中定义的公共接口,因此只需要改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为。
但该模式的缺点在于添加新的行为时比较麻烦,如果需要添加一个新产品族对象时,需要更改接口及其下所有子类,这必然会带来很大的麻烦。
- 抽象工厂 BaseFactory:定义了一个接口,这个接口包含了一组方法用来生产产品,所有的具体工厂都必须实现此接口。
- 具体工厂 ScreenFactory:用于生产屏幕产品族,要创建一个产品,用户只需使用其中一个工厂进行获取,完全不需要实例化任何产品对象。
- 抽象产品 BaseProduct:这是一个产品家族,每一个具体工厂都能够生产一整组产品。
- 具体产品 ScreenProduct:这是一个具体的屏幕产品对象
- 抽象厂商 BaseVentor:定义一个接口,这个接口包含:生产屏幕方法(productScreen)、生产电池方法(productBattery)
建造者模式将复杂产品的创建步骤分解在在不同的方法中,使得创建过程更加清晰,从而更精确控制复杂对象的产生过程;通过隔离复杂对象的构建与使用,也就是将产品的创建与产品本身分离开来,使得同样的构建过程可以创建不同的对象;并且每个具体建造者都相互独立,因此可以很方便地替换具体建造者或增加新的具体建造者,用户使用不同的具体建造者即可得到不同的产品对象。UML结构图如下:
- 抽象建造者 BaseBuilder:相当于建筑蓝图,声明了创建 Phone 对象的各个部件指定的抽象接口。
- 具体建造者 XiaomiBuilder:实现BaseBuilder抽象接口,构建和装配各个部件,定义并明确它所创建的表示,并提供一个检索产品的接口。
- 指挥者 PhoneDirector:构建一个使用 Builder 接口的对象。主要有两个作用,一是隔离用户与对象的生产过程,二是负责控制产品对象的生产过程。
- 产品角色 Phone:被构造的复杂对象。XiaomiBuilder创建该产品的内部表示并定义它的装配过程,包含定义组成部件的类,包括将这些部件装配成最终产品的接口。
单例模式可以确保系统中某个类只有一个实例,该类自行实例化并向整个系统提供这个实例的公共访问点,除了该公共访问点,不能通过其他途径访问该实例。单例模式的优点在于:
懒汉式,顾名思义就是指只有在使用到的时候才会进行初始化对象。
public class BaseBuilder {
private BaseBuilder() {}
private volatile static Phone phone = null;
/**
* 使用双重检查锁保证懒汉式线程安全
*/
public static Phone getInstance() {
if (null == phone){
synchronized (BaseBuilder.class){
if (null == phone){
phone = new Phone();
}
}
}
return phone;
}
}
饿汉式,是指类在刚初始化或者对象刚初始化的时就跟着一起创建。
public class BaseBuilder {
private BaseBuilder() {}
private static final Phone phone = new Phone();
/**
* 使用静态变量的方式实现类对象
*/
public static Phone getInstance() {
return phone;
}
}
饿汉式和懒汉式区别:
- 初始化时机与首次调用:
- 饿汉式是在类加载时,就将单例初始化完成,保证获取实例的时候,单例是已经存在的了。所以在第一次调用时速度也会更快,因为其资源已经初始化完成。
- 懒汉式会延迟加载,只有在首次调用时才会实例化单例,如果初始化所需要的工作比较多,那么首次访问性能上会有些延迟,不过之后就和饿汉式一样了。
- 线程安全方面:饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,懒汉式本身是非线程安全的,需要通过额外的机制保证线程安全
类似Spring里面的方法,将类名注册到一个容器中,下次从里面直接获取。
public class BaseBuilder {
// 存储容器
private static Map<Strnig, Object> containerMap = new HashMap();
//保护的默认构造子
private BaseBuilder() {}
//静态工厂方法,返还此类惟一的实例
public static Phone getInstance(String name) {
if(null == name) {
name = Phone.class.getName();
}
Phone phone = containerMap.get(name);
if(null == phone) {
try {
Phone phone = (Phone)Class.forName(name).newInstance();
map.put(name, phone);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
return phone;
}
}
原型模式也是用于对象的创建,通过将一个对象作为原型,对其进行复制克隆,产生一个与源对象类似的新对象。UML类图如下:
在 Java 中,原型模式的核心是就是原型类 Prototype,Prototype 类需要具备以下两个条件:
- 实现 Cloneable 接口:
- 重写 Object 类中的 clone() 方法,用于返回对象的拷贝;
public abstract class Prototype implements Cloneable {
/**
* 对象的克隆方法
* @return
* @throws CloneNotSupportedException
*/
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Department extends Prototype {
private String departCode;
private String departName;
@Override
public Department clone(){
Department department = null;
try {
department = (Department)super.clone();
}
catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return department;
}
}
public class Employee extends Prototype {
private String username;
private Department department;
@Override
public Employee clone(){
Employee employee = null;
try {
employee = (Employee)super.clone();
employee.department = this.department.clone();
}
catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return employee;
}
}
Object 类中的 clone() 方法默认是浅拷贝,如果想要深拷贝对象,则需要在 clone() 方法中自定义自己的复制逻辑。
- 浅复制:将一个对象复制后,基本数据类型的变量会重新创建,而引用类型指向的还是原对象所指向的内存地址。
- 深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。
/**
* 浅复制
*/
@Override
public Employee clone(){
Employee employee = null;
try {
employee = (Employee)super.clone();
}
catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return employee;
}
/**
* 深复制
*/
@Override
public Employee clone(){
Employee employee = null;
try {
employee = (Employee)super.clone();
// 深度 copy 对象属性
employee.department = this.department.clone();
}
catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return employee;
}
使用原型模式进行创建对象不仅简化对象的创建步骤,还比 new 方式创建对象的性能要好的多,因为 Object 类的 clone() 方法是一个本地方法,直接操作内存中的二进制流,特别是复制大对象时,性能的差别非常明显;
利用序列化来做深复制,把对象写到流里的过程是序列化(Serilization)过程,而把对象从流中读出来的过程则叫做反序列化(Deserialization)过程。应当指出的是,写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。利用这个特性,可以做深拷贝 。并且该新对象与母对象之间并不存在引用共享的问题,真正实现对象的深拷贝。
public class CloneUtils {
/**
* 使用字节流的方式实现对象的深度拷贝
*
* @param
* @param obj 要克隆的对象
* @return
*/
public static <T extends Serializable> T clone(Object obj){
T cloneObj = null;
ByteArrayOutputStream out = null;
ByteArrayInputStream ios = null;
try {
// 写入字节流,将该对象序列化成流,因为写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。所以利用这个特性可以实现对象的深拷贝
out = new ByteArrayOutputStream();
ObjectOutputStream obs = new ObjectOutputStream(out);
obs.writeObject(obj);
obs.close();
//分配内存,写入原始对象,生成新对象
ios = new ByteArrayInputStream(out.toByteArray());
ObjectInputStream ois = new ObjectInputStream(ios);
//返回生成的新对象
cloneObj = (T) ois.readObject();
ois.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (null != out){
out.close();
}
if (null != ios){
ios.close();
}
} catch (IOException e){
e.printStackTrace();
}
}
return cloneObj;
}
}
使用该工具类的对象必须要实现Serializable接口,否则是没有办法实现克隆的。
public class Person implements Serializable{
private static final long serialVersionUID = 2631590509760908280L;
..................
//去除clone()方法
}
上面我们介绍了5种创建型模式,下面我们就开始介绍下7种结构型模式:适配器模式、装饰模式、代理模式、外观模式、桥接模式、组合模式、享元模式。其中对象的适配器模式是各种模式的起源,如下图:
适配器模式主要用于将一个类或者接口转化成客户端希望的格式,使得原本不兼容的类可以在一起工作,将目标类和适配者类解耦;同时也符合“开闭原则”,可以在不修改原代码的基础上增加新的适配器类;将具体的实现封装在适配者类中,对于客户端类来说是透明的,而且提高了适配者的复用性,但是缺点在于更换适配器的实现过程比较复杂。
所以,适配器模式比较适合以下场景:
- 系统需要使用现有的类,而这些类的接口不符合系统的接口。
- 使用第三方组件,组件接口定义和自己定义的不同,不希望修改自己的接口,但是要使用第三方组件接口的功能。
下面有个非常形象的例子很好地说明了什么是适配器模式:
适配器模式的主要实现有三种:类的适配器模式、对象的适配器模式、接口的适配器模式。三者的使用场景如下:
- 类的适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。
- 对象的适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个Wrapper类,持有原类的一个实例,在Wrapper>- 类的方法中,调用实例的方法就行。
- 接口的适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类Wrapper,实现所有方法,我们写别的类的时候,继承抽象类即可。
- 目标接口(Target):客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
- 需要适配者(Adaptee):需要适配的类或适配者类或接口。
- 适配器(Adapter):通过包装一个需要适配的对象,把原接口转换成目标接口。
/**
* Content:原有功能,需要适配的类或适配者
*/
public interface Adaptee {
String screen = "屏幕";
String productScreen();
}
/**
* Content:目标功能,客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
*/
public interface Target {
String battery = "电池";
String productBattery();
}
/**
* Content:适配器,兼容 Adaptee 和 Target 两者功能
*/
public interface Adapter extends Target, Adaptee {
}
/**
* Content:适配器的基础抽象类
*/
public abstract class BaseProduct implements Adapter {
protected Phone phone;
@Override
public String productScreen() {
return "生产【" + this.phone.getBrand() + "】手机的【" + screen + "】设备";
}
@Override
public String productBattery() {
return "生产【" + this.phone.getBrand() + "】手机的【" + battery + "】设备";
}
}
/**
* Content:适配器的具体实现类(小米厂商)
*/
public class XiaomiVendor extends BaseProduct {
public XiaomiVendor(Phone phone) {
this.phone = phone;
}
}
/**
* Content:需要适配的类或适配者类
*/
public class Adaptee {
private final String screen = "屏幕";
private Phone phone;
public String getScreen() {
return screen;
}
public Phone getPhone() {
return phone;
}
public void setPhone(Phone phone) {
this.phone = phone;
}
public String productScreen(){
return "生产【" + this.phone.getBrand() + "】手机的【" + screen + "】设备";
}
}
/**
* Content:客户所期待的接口。目标可以是具体的或抽象的类,也可以是接口。
*/
public interface Target {
String battery = "电池";
String productBattery();
}
/**
* Content:通过包装一个需要适配的对象,把原接口转换成目标接口
*/
public abstract class Adapter implements Target {
private Adaptee adaptee;
protected Phone phone;
public Adapter(Adaptee adaptee) {
this.adaptee = adaptee;
}
public String productScreen() {
return adaptee.productScreen();
}
}
/**
* Content:适配器的基础抽象类
*/
public abstract class BaseProduct extends Adapter {
public BaseProduct(Adaptee adaptee) {
super(adaptee);
}
@Override
public String productBattery() {
return "生产【" + this.phone.getBrand() + "】手机的【" + battery + "】设备";
}
}
/**
* Content:适配器的具体实现类
*/
public class XiaomiVendor extends BaseProduct {
public XiaomiVendor(Phone phone, Adaptee adaptee) {
super(adaptee);
adaptee.setPhone(phone);
this.phone = phone;
}
}
public interface Product {
String screen = "屏幕";
String battery = "电池";
String productScreen();
String productBattery();
}
public abstract class ProductWrapper implements Product {
protected Phone phone;
public String productScreen(){return null;};
public String productBattery(){return null;};
}
public class ScreenProduct extends ProductWrapper {
public ScreenProduct(Phone phone) {
this.phone = phone;
}
@Override
public String productScreen(){
return "生产【" + this.phone.getBrand() + "】手机的【" + screen + "】设备";
}
}
public class BatteryProduct extends ProductWrapper {
public BatteryProduct(Phone phone) {
this.phone = phone;
}
@Override
public String productBattery(){
return "生产【" + this.phone.getBrand() + "】手机的【" + battery + "】设备";
}
}
/**
* Content:适配器的具体实现类
*/
public class XiaomiVendor {
private ProductWrapper wrapper;
private Phone phone;
public String getScreenProduct() {
wrapper = new ScreenProduct(this.phone);
return wrapper.productScreen();
}
public String getBatteryProduct() {
wrapper = new BatteryProduct(this.phone);
return wrapper.productBattery();
}
public XiaomiVendor(Phone phone) {
this.phone = phone;
}
}
装饰器模式可以动态给对象添加一些额外的职责从而实现功能的拓展,在运行时选择不同的装饰器,从而实现不同的行为;比使用继承更加灵活,通过对不同的装饰类进行排列组合,创造出很多不同行为,得到功能更为强大的对象;符合“开闭原则”,被装饰类与装饰类独立变化,用户可以根据需要增加新的装饰类和被装饰类,在使用时再对其进行组合,原有代码无须改变。装饰器模式的UML结构图如下:
TCP/IP 与 OSI 都是为了使网络中的两台计算机能够互相连接并实现通信与回应,但他们最大的不同在于,OSI 是一个理论上的网络通信模型,而 TCP/IP 则是实际上的网络通信标准。
实现计算机节点之间比特流的透明传输,规定传输媒体接口的标准,屏蔽掉具体传输介质和物理设备的差异,使数据链路层不必关心网络的具体传输介质,按照物理层规定的标准传输数据就行。
通过差错控制、流量控制等方法,使有差错的物理线路变为无差错的数据链路。
数据链路层的几个基本方法:数据封装成桢、透明传输、差错控制、流量控制。
- 封装成桢:把网络层数据报加头和尾,封装成帧,帧头中包括源MAC地址和目的MAC地址。
- 透明传输:零比特填充、转义字符。
- 差错控制:接收者检测错误,如果发现差错,丢弃该帧,差错控制方法有 CRC 循环冗余码。
- 流量控制:控制发送的传输速度,使得接收方来得及接收。传输层TCP也有流量控制功能,但TCP是端到端的流量控制,链路层是点到点(比如一个路由器到下一个路由器)。
实现网络地址与物理地址的转换,并通过路由选择算法为分组通过通信子网选择最适当的路径。
网络层最重要的一个功能就是:路由选择。路由一般包括路由表和路由算法两个方面。每个路由器都必须建立和维护自身的路由表。
- 静态维护,也就是人工设置,适用于小型网络。
- 动态维护,是在运行过程中根据网络情况自动地动态维护路由表。
提供源端与目的端之间提供可靠的透明数据传输,传输层协议为不同主机上运行的进程提供逻辑通信。
- 网络层协议负责的是提供主机间的逻辑通信;
- 传输层协议负责的是提供进程间的逻辑通信。
是用户应用程序和网络之间的接口,负责在网络中的两节点之间建立、维持、终止通信。
处理用户数据的表示问题,如数据的编码、格式转换、加密和解密、压缩和解压缩。
为用户的应用进程提供网络通信服务,完成和实现用户请求的各种服务。
TCP/IP协议模型(Transmission Control Protocol/Internet Protocol),包含了一系列构成互联网基础的网络协议,是Internet的核心协议。TCP/IP协议族按照层次由上到下,层层包装。
上图表示了TCP/IP协议中每个层的作用,而TCP/IP协议通信的过程其实就对应着数据入栈与出栈的过程。入栈的过程,数据发送方每层不断地封装首部与尾部,添加一些传输的信息,确保能传输到目的地。出栈的过程,数据接收方每层不断地拆除首部与尾部,得到最终传输的数据。
实现网络地址与物理地址的转换,并通过路由选择算法为分组通过通信子网选择最适当的路径。
物理地址是数据链路层和物理层使用的MAC地址,IP地址是网络层和以上各层使用的地址,是一种逻辑地址,其中ARP协议将IP地址转换成物理地址。
ARP 是根据 IP 地址获取 MAC 地址的一种协议,核心原理就是广播发送ARP请求,单播发送ARP响应。
- 每个主机都在自己的ARP缓冲区中建立一个ARP列表,以表示 IP 地址和 MAC 地址之间的对应关系。
- 当源主机要发送数据时,先检查ARP列表中是否有该 IP 地址对应的 MAC 地址,如果有,则直接发送数据;如果没有,就向本网段的所有主机广播ARP数据包,用于查询目的主机的MAC地址,该数据包包括的内容有:源主机IP地址,源主机MAC地址,目的主机的IP。
- 当本网络的所有主机收到该ARP数据包时,首先检查数据包中的IP地址是否是自己的IP地址,如果不是,则忽略该数据包,如果是,则首先从数据包中取出源主机的IP和MAC地址写入到ARP列表中,如果已经存在,则覆盖,然后将自己的MAC地址写入ARP响应包中,告诉源主机自己是它想要找的MAC地址。
- 源主机收到 ARP 响应包后,将目的主机的 IP 和 MAC 地址写入ARP列表,并利用此信息发送数据。如果源主机一直没有收到ARP响应数据包,表示ARP查询失败。
RARP是逆地址解析协议,作用是完成硬件地址到IP地址的映射,主要用于无盘工作站,因为给无盘工作站配置的IP地址不能保存。工作流程:在网络中配置一台RARP服务器,里面保存着 MAC 地址和 IP 地址的映射关系,当无盘工作站启动后,就封装一个RARP数据包,里面有其MAC地址,然后广播到网络上去,当服务器收到请求包后,就查找对应的MAC地址的IP地址装入响应报文中发回给请求者。因为需要广播请求报文,因此RARP只能用于具有广播能力的网络。
动态主机配置协议,对 IP地址进行集中管理和分配,提升地址的使用率,通过DHCP协议,可以使客户机自动获得服务器分配的lP地址和子网掩码
因特网控制报文协议,用于在IP主机、路由器之间传递控制消息(控制消息是指网络通不通、主机是否可达、路由器是否可用等网络本身的消息),确认 IP 包是否成功到达目标地址。因为 IP 协议并不是一个可靠的协议,它不保证数据被送达,当传送IP数据包发生错误,比如主机不可达、路由不可达等等,ICMP协议将会把错误信息封包,然后传送回给主机,给主机一个处理错误的机会。
ICMP报文有两种:差错报告报文和询问报文。以下是4种常见的ICMP差错报告报文
- 工作所处的OSI层次不一样,交换机工作在OSI第二层数据链路层,路由器工作在OSI第三层网络层;
- 寻址方式不同:交换机根据MAC地址寻址,路由器根据IP地址寻址;
- 转发速不同:交换机的转发速度快,路由器转发速度相对较慢。
- RIP(Routing Information Protocol):是一种动态路由选择协议,基于距离矢量算法,使用“跳数”来衡量到达目标地址的路由距离,并且只与自己相邻的路由器交换信息,范围限制在15跳之内。
- OSPF:开放最短路径优先协议,使用Dijskra算法计算出到达每一网络的最短路径,并在检测到链路的情况发生变化时(如链路失效),就执行该算法快速收敛到新的无环路拓扑。
BGP:边界网关协议,BGP 是力求寻找一条能够到达目的网络 且 较好的路由,而并非要寻找一条最佳路由。BGP采用路径向量路由选择协议。
传输层主要提供不同主机上进程间 逻辑通信 + 可靠传输 或者 不可靠传输的功能。
- 面向字节流:应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序看成是一连串的无结构的字节流。TCP有一个缓冲,当应用程序传送的数据块太长,TCP就可以把它划分短一些再传送。
- 面向报文:面向报文的传输方式是应用层交给UDP多长的报文,UDP就照样发送。因此,应用程序必须选择合适大小的报文。
TCP对应的协议:
- FTP:文件传输协议,使用21端口
- Telnet:远程终端接入,使用23端口,用户可以以自己的身份远程连接到计算机上,可提供基于DOS模式下的通信服务。
- SMTP:邮件传送协议,用于发送邮件,使用25端口
- POP3:邮件传送协议,P用于接收邮件。使用110端口
- HTTP:万维网超文本传输协议,是从Web服务器传输超文本到本地浏览器的传送协议
UDP对应的协议:
- DNS:域名解析服务,将域名地址转换为IP地址,使用53号端口;
- SNMP:网络管理协议,用来管理网络设备,使用161号端口;
- TFTP:简单文件传输协议,提供不复杂、开销不大的文件传输服务,使用 69 端口;
- NFS:远程文件服务器
- RIP:路由信息协议
- DHCP:动态主机配置协议
- IGMP:网际组管理协议
例如,一报文段的序号是 101,共有 100 字节的数据。这就表明:本报文段的数据的第一个字节的序号是 101,最后一个字节的序号是 200。显然,下一个报文段的数据序号应当从 201 开始,即下一个报文段的序号字段值应为 201。
- 紧急位URG:当 URG = 1 时,表明此报文段中有紧急数据,是高优先级的数据,应尽快发送,不用在缓存中排队。
- 确认ACK:仅当 ACK = 1 时确认号字段才有效,当 ACK = 0 时确认号无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置为 1。
- 推送PSH:接收方收到 PSH = 1 的报文段时,就直接发送给应用进程,而不用等到整个缓冲区都填满了后再向上传送。
- 复位RST:当 RST = 1 时,表明 TCP 连接中出现了严重错误(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立传输连接。
- 同步SYN:SYN = 1 表示这是一个连接请求或连接接受报文。当 SYN = 1 而 ACK = 0 时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使 SYN = 1 且 ACK = 1。
- 终止FIN:用来释放一个连接。当 FIN = 1时,表明此报文段的发送发的数据已发送完毕,并要求释放运输连接。
三次握手的一个重要功能是客户端和服务端交换 ISN,以便让对方知道接下来接收数据时如何按序列号组装数据。
ISN 是动态生成的,并非固定,因此每个连接都将具有不同的 ISN。如果 ISN 是固定的,攻击者很容易猜出后续的确认号。
此时 SYN 控制位变为 0,表示这不是建立连接的请求了,要正式发数据了。
客户端发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达Server。本来这是一个早已失效的报文段,但Server收到此失效的连接请求报文段后:
- 假设不采用“三次握手”,那么只要Sever发出确认,新的连接就建立了。但由于现在Client并没有发出建立连接的请求,因此不会理睬Server的确认,也不会向Server发送数据。而Server却以为新的连接已经建立,并一直等待Client发来数据,这样,Server的很多资源就白白浪费掉了
- 而采用“三次握手”协议,只要Server收不到来自Client的确认,就知道Client并没有要求建立请求,就不会建立连接了。
- 服务端进入CLOSE_WAIT状态,此时TCP连接处于半关闭状态,即客户端不能向服务端发送报文,只能接收,但服务端仍然可以向客户端发送数据。
- 客户端收到服务端的确认后,进入 FIN_WAIT2 状态,等待服务端发出的连接释放报文段。
TIME_WAIT 状态持续 2MSL(最大报文存活时间),约4分钟才转换成CLOSE状态。由于TIME_WAIT 的时间会非常长,因此服务端应尽量减少主动关闭连接,TIME_WAIT 的主要作用有:
由于网络等原因,无法保证最后一次挥手的 ACK 报文一定能传送给对方,如果 ACK 丢失,对方会超时重传 FIN,主动关闭端会再次响应ACK过去;如果没有 TIME_WAIT 状态,直接关闭,对方重传的FIN报文则被响应一个RST报文,此RST会被动关闭端被解析成错误。同时,服务器就因为接收不到客户端的信息而无法正常关闭。
如果存在两个连接,第一个连接正常关闭,第二个相同的连接紧接着建立;如果第一个连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达,则会干扰第二连接,等待 2MSL 可以让上次连接的报文数据消逝在网络中。
TCP 是全双工模式,并且支持半关闭特性,提供了连接的一端在结束发送后还能接收来自另一端数据的能力。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后就完全关闭了 TCP 连接。
通俗的来说,两次握手就可以释放一端到另一端的 TCP 连接,完全释放连接一共需要四次握手。
SYN 洪泛是指利用 TCP 需要三次握手的特性,攻击者伪造 SYN 报文向服务器发起连接,服务器在收到报文后用 ACK 应答,但之后攻击者不再对该响应进行应答,造成一个半连接。假设攻击者发送大量这样的报文,那么被攻击主机就会造成大量的半连接,耗尽其资源,导致正常的 SYN 请求因为队列满而被丢弃,使得正常用户无法访问。
半连接队列:服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD 状态,此时双方还没有完全建立其连接,服务器会把这种状态下的请求连接放在一个队列里,我们把这种队列称之为半连接队列。当然还有一个全连接队列,完成三次握手后建立起的连接就会放在全连接队列中。
第三次握手时是可以携带数据的,但第一二次握手时不可以携带数据。
- 假如第一次握手可以携带数据的话,那么会放大 SYN 洪泛。如果有人要恶意攻击服务器,每次都在第一次握手中的 SYN 报文中放入大量的数据,然后疯狂重复发送 SYN 报文的话,就会让服务器开辟大量的缓存来接收这些报文,内存会更快容易耗尽,从而拒绝服务。
- 第三次握手时客户端已经处于 ESTABLISHED 状态,对于客户端来说,它已经建立起连接了,并且已经知道服务器的接收和发送能力是正常的,所以也就可以携带数据了。
拆包粘包在数据链路层、网络层以及传输层都可能存在。而在传输层中,由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。TCP是个“流”协议,所谓流,就是没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
如果用链路层以太网,MSS的值往往为1460。而 Internet 上标准的 MTU(最小的 MTU,链路层网络为x2.5时)为576,那么如果不设置,则MSS的默认值就为536个字节。很多时候,MSS的值最好取512的倍数。
在建立TCP连接之前,客户端和服务端会进行三次的握手操作,确保两端的信息传递的可靠性。
TCP接收端收到发送端的数据时,它将发送一个确认响应。当TCP发送端发出一个报文段后,它会启动一个定时器,等待接收端的确认报文段,如果不能及时收到一个确认响应,发送端会认为接收端没有收到数据,将重发这个报文段。
TCP会检测数据在传输过程中的任何变化,若校验出包有错,则丢弃报文段并且不给出响应,这时TCP会超时重发数据;对于重复数据,则进行丢弃;
既然TCP报文段作为IP数据报来传输,而IP数据报的到达可能会失序,因此TCP报文段的到达也可能会失序。TCP将对失序数据进行重新排序,然后才交给应用层;
TCP 连接的每一方都有固定大小的缓冲空间。TCP 规定发送端发送的数据不能超过接收端的缓冲空间大小,否则会造成接收端的缓冲区溢出。TCP使用的流量控制协议是可变大小的滑动窗口协议。
网络拥塞时,减少数据的发送。
所谓流量控制就是让发送方的发送速率不要太快,让接收方来得及接收。因为如果发送方把数据发送得过快,接收方可能会来不及接收,这就会造成数据丢失。TCP的流量控制是通过大小可变的滑动窗口来实现的。接收端将自己可以接收的缓冲区大小放入TCP首部中的“窗口大小”字段,通过ACK报文来通知发送端,滑动窗口是接收端用来控制发送端发送数据的大小,从而达到流量控制。
其实发送方的窗口上限,是取值拥塞窗口和滑动窗口两者的最小值。当滑动窗口为 0 时,发送方一般不能再发送数据包,但有两种情况除外:
- 一种情况是可以发送紧急数据,例如,允许用户终止在远端机上的运行进程。
- 一种情况是发送方可以发送一个 1 字节的数据报来通知接收方重新声明它希望接收的下一字节及发送方的滑动窗口大小。
设A向B发送数据。在连接建立时,B告诉了A:“我的接收窗口是 rwnd = 400 ”(这里的 rwnd 表示 receiver window) 。因此,发送方的发送数据不能超过接收方给出的接收窗口的数值。假设每一个报文段为100字节长,而数据报文段序号的初始值设为1。
从图中可以看出,B进行了三次流量控制。第一次把窗口减少到 rwnd = 300 ,第二次又减到了 rwnd = 100 ,最后减到 rwnd = 0 ,即不允许发送方再发送数据了。这种使发送方暂停发送的状态将持续到主机B重新发出一个新的窗口值为止。B向A发送的三个报文段都设置了 ACK = 1 ,只有在 ACK=1 时确认号字段才有意义。
拥塞控制就是防止在同一时间段内过多的数据注入到网络中,使网络中的路由器或链路不致过载。发送方维持一个拥塞窗口cwnd 的状态变量。拥塞窗口的大小动态变化,取决于网络的拥塞程度,发送方让自己的发送窗口等于拥塞窗口。只要网络没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数。 拥塞控制的方法主要有以下几种:慢启动、拥塞避免、快重传和快恢复。
是试探一下网络的拥塞情况,由小到大逐渐增大发送窗口。在开始发送报文段时先设置cwnd=1,使得发送方在开始时只发送一个报文段,然后每经过一个传输轮次RTT,拥塞窗口 cwnd 就加倍。另外,为了防止拥塞窗口cwnd增长过大引起网络拥塞,还需要设置一个慢开始门限 ssthresh 状态变量。
让拥塞窗口cwnd缓慢地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1。这样拥塞窗口cwnd按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。
无论在慢开始阶段还是在拥塞避免阶段,只要网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的拥塞窗口值的一半(但不能小于2)。然后把拥塞窗口cwnd 设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的数据量,使得发生拥塞的路由器有足够时间把队列中积压的数据处理完毕。过程图如下:
快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(使发送方及早知道有报文段没有到达对方)而不必等到自己发送数据时捎带确认。发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。
接收方收到了M1和M2后都分别发出了确认。现在假定接收方没有收到M3但接着收到了M4。显然,接收方不能确认M4,因为M4是收到的失序报文段。根据可靠传输原理,接收方可以什么都不做,也可以在适当时机发送一次对M2的确认。但按照快重传算法的规定,接收方应及时发送对M2的重复确认,这样做可以让发送方及早知道报文段M3没有到达接收方。发送方接着发送了M5和M6。接收方收到这两个报文后,也还要再次发出对M2的重复确认。这样,发送方共收到了 接收方的四个对M2的确认,其中后三个都是重复确认。
与快重传配合使用的还有快恢复算法,当发送方连续收到三个重复确认时,就执行“乘法减少”算法,把ssthresh门限设置为拥塞窗口cwnd的一半,但是接下去并不执行慢开始算法,而是将cwnd设置为ssthresh的大小,然后执行拥塞避免算法:因为如果网络出现拥塞的话,就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞,所以此时并不执行慢开始算法,而是执行拥塞避免算法。
拥塞控制和流量控制的相同点都是控制丢包现象,实现机制都是让发送方发得慢一点。
应用层主要提供应用进程间的网络通信服务,完成用户请求的各种服务。
http协议即超文本传输协议,基于TCP协议,用于从Web服务器传输超文本到本地浏览器的传送协议。http协议是无状态协议,自身不对请求和响应直接的通信状态进行保存,但有些场景下我们需要保存用户的登陆信息,所以引入了cookie 和 session 来管理状态。
无状态协议是指比如客户获得一张网页之后关闭浏览器,然后再一次启动浏览器,再登录该网站,但是服务器并不知道客户关闭了一次浏览器。
cookie保存在客户端,session保存在服务端,所以在安全性上面,cookie存在安全隐患,可以通过拦截或本地文件找到cookie后进行攻击,而session相对更加安全。因此,可以将登陆信息等重要信息存放为session中;其他信息如果需要保留,可以放在cookie中。
单个cookie最大只允许4KB,一个站点最多保存20个Cookie;session没有大小限制,个数只跟服务器的内存大小有关。
cookie可长期有效存在;session依赖于cookie,过期时间默认为-1,只需关闭窗口该 session 就会失效。每个客户端对应一个session ,客户端之间的 session 相互独立;
cookie:cookie是一小段的文本信息,当客户端请求服务器时,如果服务器需要记录该用户状态,就在响应头中向客户端浏览器颁发一个cookie,而客户端浏览器会把cookie 保存 起来。当再次请求该网站时,浏览器把cookie放入请求头中提交给服务器,服务器会检查该cookie,以此来辨认用户状态。
session:当客户端请求服务器时,都会带上cookie,cookie里面一般都会有一个JSESSIONID,服务器就按照 JSESSIONID 来找到对应的 session;如果客户端请求不包含 JSESSIONID,则为此客户端 创建 session并生成相关联的JSESSIONID,并将这个JSESSIONID在本次响应中返回给客户端保存。客户端保存这个 JSESSIONID 的方式可以使用cookie机制。若浏览器禁用cookie的话,可以通过 URL重写机制 将JSESSIONID传回服务器。
DNS服务器大致分为三种类型:根DNS服务器、顶级域DNS服务器 和 权威DNS服务器,其中: 顶级域DNS服务器主要负责诸如com、org、net、edu、gov 等顶级域名。
根DNS服务器存储了所有 顶级域DNS服务器的 IP 地址,可以通过根服务器找到顶级域服务器(例如:www.baidu.com,根服务器会返回所有维护 com 这个顶级域服务器的 IP 地址)。然后你任选其中一个顶级域服务器发送请求,该顶级域服务器拿到域名后能够给出负责当前域的权威服务器地址(以 baidu为例的话,顶级域服务器将返回所有负责 baidu 这个域的权威服务器地址)。接着任选其中一个权威服务器地址查询 「www.baidu.com」 的具体 IP 地址,最终权威服务器会返回给你具体的 IP 地址。此外,本地 DNS 服务器是具有缓存功能的,通常两天内的记录都会被缓存。
所以,通过DNS系统查询域名对应的 IP 的具体步骤可以总结为:
- 操作系统先查本地 hosts文件 中是否有记录,如果有,则直接返回相对应映射的IP地址。
- 如果本地hosts文件中没有配置,则主机向自己的本地 DNS 服务器 发送查询报文,如果本地DNS服务器缓存中有,将直接返回结果
- 如果本地服务器缓存中没有,则从内置在内部的根服务器列表(全球13台,固定的IP地址)中选一个发送查询报文
- 根服务器解析域名中的后缀名,告诉本地服务器负责该后缀名的所有顶级服务器列表
- 本地服务器选择其中一个顶级域服务器发送查询请求,顶级域服务器拿到域名后继续解析,返回对应域的所有权威服务器列表
- 本地服务器再向返回的权威服务器发送查询报文,最终会从某一个权威服务器上得到具体的 IP 地址
- 主机返回结果IP
- 浏览器首次加载资源成功时,服务器返回200,此时浏览器不仅将资源下载下来,而且把response的header(里面的date属性非常重要,用来计算第二次相同资源时当前时间和date的时间差)一并缓存;
- 下一次加载资源时,首先要经过强缓存的处理,cache-control的优先级最高,比如cache-control:no-cache,就直接进入到协商缓存的步骤了,如果cache-control:max-age=xxx,就会先比较当前时间和上一次返回200时的时间差,如果没有超过max-age,命中强缓存,不发请求直接从本地缓存读取该文件(这里需要注意,如果没有cache-control,会取expires的值,来对比是否过期),过期的话会进入下一个阶段,协商缓存
- 协商缓存阶段,则向服务器发送header带有If-None-Match和If-Modified-Since的请求,服务器会比较Etag,如果相同,命中协商缓存,返回304;如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200;
- 协商缓存第二个重要的字段是,If-Modified-Since,如果客户端发送的If-Modified-Since的值跟服务器端获取的文件最近改动的时间,一致则命中协商缓存,返回304;不一致则返回新的last-modified和文件并返回200;
- 构建DOM树(DOM tree):从上到下解析HTML文档生成DOM节点树(DOM tree),也叫内容树(content tree);
- 构建CSSOM(CSS Object Model)树:加载解析样式生成CSSOM树;
- 执行JavaScript:加载并执行JavaScript代码(包括内联代码或外联JavaScript文件);
- 构建渲染树(render tree):根据DOM树和CSSOM树,生成渲染树(render tree);
- 渲染树:按顺序展示在屏幕上的一系列矩形,这些矩形带有字体,颜色和尺寸等视觉属性。
- 布局(layout):根据渲染树将节点树的每一个节点布局在屏幕上的正确位置;
- 绘制(painting):遍历渲染树绘制所有节点,为每一个节点适用对应的样式,这一过程是通过UI后端模块完成;
http的长连接和短连接本质上是TCP长连接和短连接。从http1.1开始就默认使用长连接。
- 短链接:是指客户端与服务端每进行一次请求操作,就建立一次TCP连接,收到服务器响应后,就断开连接。
- 长连接:是指客户端和服务建立TCP连接后,它们之间的连接会持续存在,不会因为一次HTTP请求后关闭,后续的请求也是用这个连接进行通信,使用长连接的HTTP协议,会在响应头有加入:Connection:keep-alive。长连接可以省去每次TCP建立和关闭的握手和挥手操作,节约时间提高效率。但在长连接下,客户端一般不会主动关闭连接,如果客户端和服务端之间的连接一直不关闭的话,随着连接数越来越多,会对服务端造成压力。
所以长连接多用于频繁请求资源,而且连接数不能太多的情况,例如数据库的连接用长连接。而像Web网站这种并发量大,但是每个用户无需频繁操作的场景,一般都使用短连接,因为长连接对服务端来说会耗费一定的资源。
HTTP请求头有个Range字段;我们下载文件的时候如果遇到网络中断,如果重头开始下载会浪费时间,所以我们可以从上一次中断处继续开始下载;具体的操作:
Range: bytes=5001-10000
或者指定5001以后的所有数据
Range: bytes=5001-
- 通信使用明文不加密,通信内容可能被窃听;
- 无法验证报文的完整性,数据内容可能被篡改
- 不验证通信方身份、可能遭到伪装,无法保证数据发送到正确的机器上;
为了解决上述几个问题,那么就引入了https协议。
https 是基于tcp协议,在http的基础上加入了SSL/TLS,可看成是添加了加密和认证机制的http,使用对称加密、非对称加密、证书等技术进行进行客户端与服务端的数据加密传输,最终达到保证整个通信的安全性。
对称加密指加密和解密都使用同一个密钥的方式,这种方式存在如何安全地将密钥发送对方的问题;非对称加密使用两个密钥,公钥加密则需要私钥解密,私钥加密则需要公钥解密。不能私钥加密,私钥解密。非对称加密不需要发送用来解密的私钥,所以可以保证安全性,但是和对称加密比起来,速度非常的慢,所以我们还是要用对称加密来传送消息,但对称加密所使用的密钥我们可以通过非对称加密的方式发送出去。
- https是基于tcp协议的,首先客户端会和服务端发起链接建立
- 服务端返回它的证书给客户端,证书中包含了服务端公钥S.pub、颁发机构和有效期等信息
- 客户端通过浏览器内置的根证书(内部包含CA机构的公钥C.pub)验证证书的合法性
- 客户端生成随机的对称加密密钥Z,然后通过服务端的公钥S.pub加密发送给服务端
- 客户端和服务端之后就通过对称加密密钥Z加密数据来进行http通信
- 服务器会预先生成非对称加密密钥,私钥S.pri自己保留,而公钥S.pub则发送给CA进行签名认证
- CA机构也会预先生成非对称加密密钥,其私钥C.pri用来对服务器的公钥S.pub进行签名,生成CA证书
- CA机构将签名生成的CA证书返回给服务器,也就是前面服务端给客户端那个证书
- 因为CA机构比较权威,所以很多浏览器会内置包含它公钥C.pub的证书,称之为根证书,然后可以使用根证书来验证其颁发证书的合法性了
在整个过程中,一共涉及2对公私密钥对,一对由服务器产生,主要用于加密,一对由CA产生,主要用于签名。
CA证书是为了确保服务端的公钥是准确无误、没有被修改过的。虽然https是加密的,但是请求还是可以被拦截的,假设没有CA证书,如果服务器返回的包含公钥的包被攻击者截取,然后攻击者也生成一对公私钥,他将自己的公钥发给客户端。攻击者得到客户端数据后进行解密,然后再通过服务器的公钥加密发给服务器,这样数据就被攻击者获取到了。
有了CA证书后,客户端根据内置的CA根证书,很容易识别出攻击者的公钥不合法,或者说攻击者的证书不合法。
证书通常包含这些内容:
- 服务端的公钥;
- 证书发行者(CA)对证书的数字签名;
- 证书所用的签名算法;
- 证书发布机构、有效期、所有者的信息等其他信息
- get:向服务端获取资源,所以查询操作一般用get
- post:向服务端提交请求字段,创建操作使用 post,该操作不是幂等的,多次执行会导致多条数据被创建
- put:修改指定URL的资源,如果资源不存在,则进行创建,修改操作一般使用 put,在http中,put 被定义成幂等的,多次操作会导致前面的数据被覆盖
- patch:局部修改URL所在资源的数据,是对put的补充
- delete:删除指定URL的资源。
- head:获取响应报文的首部,即获得URL资源的头部
- options:询问服务器支持哪些方法,响应头中返回 Allow: GET、POST、HEAD
- trace:追踪路径,主要用于测试或诊断;在请求头中在Max-Forwards字段设置数字,每经过一个服务器该数字就减一,当到0的时候就直接返回,一般通过该方法检查请求发送出去是否被篡改
- 功能:get一般用来从服务器上面获取资源,post一般用来更新服务器上面的资源。
- 幂等性:get 是幂等的,post 为非幂等的
- 安全性:get 请求的参数会明文附加在URL之后,而 post 请求提交的数据则被封装到请求体中,相对更安全。
- 传输数据量的大小:get请求允许发送的数据量比较小,大多数浏览器都会限制请求的url长度在2048个字节,而大多数服务器最多处理64K大小的url;而post请求提交的数据量则是没有大小限制的。
- 参数的数据类型:GET只接受ASCII字符,而POST没有限制。
- GET在浏览器回退时是无害的,而POST会再次提交请求。
- get请求可以被缓存,可以被保留在浏览器的历史记录中;post请求不会被缓存,不会被保留在浏览器的历史记录中。
请求报文包含三部分:
- 请求行:包含请求方法、URI、HTTP版本信息
- 请求首部字段
- 请求内容实体
响应报文包含三部分:
- 状态行:包含HTTP版本、状态码、状态码的原因短语
- 响应首部字段
- 响应内容实体
- 方法(method):客户端希望服务器对资源执行的动作,是一个单独的词,比如:get 或者 post
- 请求URL(request-URL):请求URL是资源的绝对路径,服务器可以假定自己是URL的主机/端口
- 版本(version):报文所使用的Http版本,其格式:HTTP/<主要版本号>.<次要版本号>
- 状态码(status-code):标识请求过程中所发生的情况
- 原因短语(reason-phrase):数字状态码的可读版本,包含行终止序列之前的所有文本。
- 请求头部(header):可以有零个或多个头部,每个首部都包含一个名字,后面跟着一个冒号(,然后是一个可选的空格,接着是一个值,>- 最后是一个CRLF首部是由一个空行(CRLF)结束的,表示了头部列表的结束和实体主体部分的开始
- 实体的主体部分(entity-body):实体的主体部分包含一个由任意数据组成的数据块,并不是所有的报文都包含实体的主体部分,有时,报文只是以一个CRLF结束。
- Connection:允许客户端和服务器指定与请求/响应连接有关的选项,http1.1之后默认是 keep-alive
- Date:日期和时间标志,说明报文是什么时间创建的
- Transfer-Encoding:告知接收端为了保证报文的可靠传输,对报文采用了什么编码方式
- Cache-Control:用于随报文传送缓存指示
- Host:给出了接收请求的服务器的主机名和端口号
- Referer:提供了包含当前请求URI的文档的URL
- User-Agent:将发起请求的应用程序名称告知服务器
- Accept:告诉服务器能够发送哪些媒体类型
- Accept-Encoding:告诉服务器能够发送哪些编码方式
- Accept-Language:告诉服务器能够发送哪些语言
- Range:如果服务器支持范围请求,就请求资源的指定范围
- If-Range:允许对文档的某个范围进行条件请求
- Authorization:包含了客户端提供给服务器,以便对其自身进行认证的数据
- Cookie:客户端用它向服务器传送数据
- Age:(从最初创建开始)响应持续时间
- Server:服务器应用程序软件的名称和版本
- Accept-Ranges:对此资源来说,服务器可接受的范围类型
- Set-Cookie:在客户端设置数据,以便服务器对客户端进行标识
- Allow:列出了可以对此实体执行的请求方法
- Location:告知客户端实体实际上位于何处,用于将接收端定向到资源的位置(URL)上去
- Content-Base:解析主体中的相对URL时使用的基础URL
- Content-Encoding:对主体执行的任意编码方式
- Content-Language:理解主体时最适宜使用的自然语言
- Content-Length:主体的长度
- Content-Type:这个主体的对象类型
- ETag:与此实体相关的实体标记
- Last-Modified:这个实体最后一次被修改的日期和时间
1xx:请求处理中,请求已被接受,正在处理。
2xx:请求成功,请求被成功处理。
- 200 :OK,客户端请求成功;
- 204(请求处理成功,但是没有资源返回)
3xx:重定向,要完成请求必须进一步处理。
- 301:永久性转移,请求的资源已经被分配到了新的地址
- 302:暂时重定向
- 304:已缓存。
4xx:客户端错误,请求不合法。
- 400:客户端请求报文出现错误,通常是参数错误
- 401:客户端未认证授权
- 403:没有权限访问该资源
- 404:未找到请求的资源
- 405:不支持该请求方法,如果服务器支持GET,客户端用POST请求就会出现这个错误码
5xx:服务端错误,服务端不能处理合法请求。
- 500:服务器内部错误。
- 503:服务不可用,一段时间后可能恢复正常。
DNS底层既使用TCP又使用UDP协议:
- 域名解析时使用UDP协议:客户端向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可,不用经过TCP三次握手,这样DNS服务器负载更低,响应更快。
- 区域传送时使用TCP,主要有一下两点考虑:
①辅域名服务器会定时(一般时3小时)向主域名服务器进行查询以便了解数据是否有变动。如有变动,则会执行一次区域传送,进行数据同步。区域传送将使用TCP而不是UDP,因为数据同步传送的数据量比一个请求和应答的数据量要多得多。
②TCP是一种可靠的连接,保证了数据的准确性。
- bit:位
- byte:字节
- 1 byte= 8 bit
- int 类型为 4 byte,共32位bit,unsigned int也是
- 2^32 byte = 4G
- 1G= 2^30 =10.7亿
所谓海量数据处理,就是指数据量太大,无法在较短时间内迅速解决,或者无法一次性装入内存。而解决方案就是:针对时间,可以采用巧妙的算法搭配合适的数据结构,如 Bloom filter/Hashmap/bit-map/堆/数据库/倒排索引/trie树;针对空间,大而化小,分而治之(hash映射),把规模大化为规模小的,各个击破。所以,海量数据处理的基本方法总结起来分为以下几种:
- 分而治之/hash映射 + hash统计 + 堆/快速/归并排序;
- Trie树/Bloom filter/Bitmap
- 数据库/倒排索引;
- 双层桶划分;
- 外排序;
- 分布式处理之Hadoop/Mapreduce。
这种方法是典型的“分而治之”的策略,是解决空间限制最常用的方法,即海量数据不能一次性读入内存,而我们需要对海量数据进行的计数、排序等操作。基本思路如下图所示:先借助哈希算法,计算每一条数据的 hash 值,按照 hash 值将海量数据分布存储到多个桶中。根据 hash 函数的唯一性,相同的数据一定在同一个桶中。如此,我们再依次处理这些小文件,最后做合并运算即可。
问题1:海量日志数据,统计出某日访问百度次数最多的那个IP
解决方式:IP地址最多有 2^32 = 4G 种取值情况,所以不能完全加载到内存中进行处理,采用 hash分解+ 分而治之 + 归并 方式:
- 按照 IP 地址的 Hash(IP)%1024 值,把海量IP日志分别存储到1024个小文件中。这样,每个小文件最多包含4MB个IP地址;
- 对于每一个小文件,构建一个IP为key,出现次数为value的Hash map,同时记录当前出现次数最多的那个IP地址
- 然后再在这1024组最大的IP中,找出那个频率最大的IP
问题2:有一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
解决思想: hash分解+ 分而治之 + 归并
- 顺序读文件中,对于每个词x,按照 hash(x)/(1024*4) 存到4096个小文件中。这样每个文件大概是250k左右。如果其中的有的文件超过了1M大小,还可以按照hash继续往下分,直到分解得到的小文件的大小都不超过1M。
- 对每个小文件,可以采用 trie树/hashmap 统计每个文件中出现的词以及相应的频率,并使用 100个节点的小顶堆取出出现频率最大的100个词,并把100个词及相应的频率存入文件。这样又得到了4096个文件。
- 下一步就是把这4096个文件进行归并的过程了
问题3:有a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?
解决方案1:如果内存中想要存入所有的 url,共需要 50亿 * 64= 320G大小空间,所以采用 hash 分解+ 分而治之 + 归并 的方式:
- 遍历文件a,对每个 url 根据某种hash规则,求取hash(url)/1024,然后根据所取得的值将 url 分别存储到1024个小文件(a0a1023)中。这样每个小文件的大约为300M。如果hash结果很集中使得某个文件ai过大,可以在对ai进行二级hash(ai0ai1024),这样 url 就被hash到 1024 个不同级别的文件中。
- 分别比较文件,a0 VS b0,…… ,a1023 VS b1023,求每对小文件中相同的url时:把其中一个小文件的 url 存储到 hashmap 中,然后遍历另一个小文件的每个url,看其是否在刚才构建的 hashmap 中,如果是,那么就是共同的url,存到文件中。
- 把1024个文件中的相同 url 合并起来
解决方案2:Bloom filter
- 如果允许有一定的错误率,可以使用 Bloom filter,4G内存大概可以表示 340 亿bit,n = 50亿,如果按照出错率0.01算需要的大概是650亿个bit,现在可用的是340亿,相差并不多,这样可能会使出错率上升些,将其中一个文件中的 url 使用 Bloom filter 映射为这340亿bit,然后挨个读取另外一个文件的url,检查是否与Bloom filter,如果是,那么该url应该是共同的url(注意会有一定的错误率)
问题4:有10个文件,每个文件1G,每个文件的每一行存放的都是用户的 query,每个文件的query都可能重复。要求你按照query的频度排序。
解决方案1:hash分解+ 分而治之 +归并
- 顺序读取10个文件 a0~a9,按照 hash(query)%10 的结果将 query 写入到另外10个文件(记为 b0~b9)中,这样新生成的文件每个的大小大约也1G
- 找一台内存2G左右的机器,依次使用 hashmap(query, query_count) 来统计每个 query 出现的次数。利用 快速/堆/归并排序 按照出现次数进行排序。将排序好的query和对应的query_cout输出到文件中。这样得到了10个排好序的文件c0~c9。
- 对这10个文件 c0~c9 进行归并排序(内排序与外排序相结合)。每次取 c0~c9 文件的 m 个数据放到内存中,进行 10m 个数据的归并,即使把归并好的数据存到 d结果文件中。如果 ci 对应的m个数据全归并完了,再从 ci 余下的数据中取m个数据重新加载到内存中。直到所有ci文件的所有数据全部归并完成。
解决方案2:Trie树
- 如果query的总量是有限的,只是重复的次数比较多而已,可能对于所有的query,一次性就可以加入到内存了。在这种情况下,可以采用 trie树/hashmap 等直接来统计每个query出现的次数,然后按出现次数做快速/堆/归并排序就可以了。
问题5:海量数据分布在100台电脑中,请高效统计出这批数据的TOP10
解决思想: 分而治之 + 归并
- 在每台电脑上求出TOP10,采用包含10个元素的堆完成(TOP10小,用最大堆,TOP10大,用最小堆)
- 求出每台电脑上的TOP10后,把这100台电脑上的 TOP10 合并之后,共1000个数据,在采用堆排序或者快排方式 求出 top10
(注意:该题的 TOP10 是取最大值或最小值,如果取频率TOP10,就应该先hash分解,将相同的数据移动到同一台电脑中,再使用hashmap分别统计出现的频率)
问题6:在 2.5 亿个整数中找出不重复的整数,内存不足以容纳这2.5亿个整数
解决方案1:hash 分解+ 分而治之 + 归并
- 2.5亿个 int 类型 hash 到1024个小文件中 a0~a1023,如果某个小文件大小还大于内存,进行多级hash
- 将每个小文件读进内存,找出只出现一次的数据,输出到b0~b1023
- 最后数据合并即可
解决方案2 : 2-Bitmap
- 如果内存够1GB的话,采用 2-Bitmap 进行统计,共需内存 2^32 * 2bit = 1GB内存。2-bitmap 中,每个数分配 2bit(00表示不存在,01表示出现一次,10表示多次,11无意义),然后扫描这 2.5 亿个整数,查看Bitmap中相对应位,如果是00,则将其置为01;如果是01,将其置为10;如果是10,则保持不变。所描完成后,查看bitmap,把对应位是01的整数输出即可。(如果是找出重复的数据,可以用1-bitmap。第一次bit位由0变1,第二次查询到相应bit位为1说明是重复数据,输出即可)
Trie树、红黑树 和 hashmap 可以认为是第一部分中分而治之算法的具体实现方法之一。
其中,Trie树适合处理海量字符串数据,尤其是大量的字符串数据中存在前缀时。Trie树在字典的存储,字符串的查找,求取海量字符串的公共前缀,以及字符串统计等方面发挥着重要的作用。
问题1:上千万或上亿数据(有重复),统计其中出现次数最多的前N个数据。
解决方案: hashmap/红黑树 + 堆排序
- 如果是上千万或上亿的 int 数据,现在的机器4G内存能存下。所以考虑采用 hashmap/搜索二叉树/红黑树 等来进行统计重复次数
- 然后使用包含 N 个元素的小顶堆找出频率最大的N个数据
问题2:一个文本文件,大约有一万行,每行一个词,要求统计出其中最频繁出现的前10个词,并给出时间复杂度
解决思路: trie树 + 堆排序
- 用 trie树 统计每个词出现的次数,时间复杂度是O(n*len)(len表示单词的平均长度)。
- 然后使用小顶堆找出出现最频繁的前10个词,时间复杂度是O(n*lg10)。
- 总的时间复杂度,是O(nle)与O(nlg10)中较大的那一个
问题3:有一千万个字符串记录(这些字符串的重复率比较高,虽然总数是1千万,但是如果去除重复和,不超过3百万个),每个查询串的长度为1-255字节。请你统计最热门的10个查询串(重复度越高,说明越热门),要求使用的内存不能超过1G。
解决方案:
- 内存不能超过 1G,每条记录是 255byte,1000W 条记录需要要占据2.375G内存,这个条件就不满足要求了,但是去重后只有 300W 条记录,最多占用0.75G内存,因此可以将它们都存进内存中去。使用 trie树(或者使用hashmap),关键字域存该查询串出现的次数。最后用10个元素的最小堆来对出现频率进行排序。总的时间复杂度,是O(nle)与O(nlg10)中较大的那一个。
问题4:1000万字符串,其中有些是重复的,需要把重复的全部去掉,保留没有重复的字符串。
解决方案:trie树
问题1:已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数。
解决思路:
- 8位最多99 999 999,需要 100M个bit 位,不到12M的内存空间。我们把 0-99 999 999的每个数字映射到一个Bit位上,这样,就用了小小的12M左右的内存表示了所有的8位数的电话
问题2:2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数。
解决方案:使用 2-bitmap,详情见上文
问题3:给40亿个不重复的 unsigned int 的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中
解决方案:使用 Bitmap,申请 512M 的内存,一个bit位代表一个 unsigned int 值。读入40亿个数,设置相应的bit位,读入要查询的数,查看相应bit位是否为1,为1表示存在,为0表示不存在。
问题4:现有两个各有20亿行的文件,每一行都只有一个数字,求这两个文件的交集。
解决方案:采用 bitmap 进行问题解决,因为 int 的最大数是 2^32 = 4G,用一个二进制的下标来表示一个 int 值,大概需要4G个bit位,即约4G/8 = 512M的内存,就可以解决问题了。
- 首先遍历文件,将每个文件按照数字的正数,负数标记到2个 bitmap 上,为:正数 bitmapA_positive,负数 bitmapA_negative
- 遍历另为一个文件,生成正数:bitmapB_positive,bitmapB_negative
- 取 bitmapA_positive and bitmapB_positive 得到2个文件的正数的交集,同理得到负数的交集。
- 合并,问题解决
- 这里一次只能解决全正数,或全负数,所以要分两次
问题5:与上面的问题4类似,只不过现在不是A和B两个大文件,而是A, B, C, D….多个大文件,求集合的交集
解决方案:
- 依次遍历每个大文件中的每条数据,遍历每条数据时,都将它插入 Bloom Filter;
- 如果已经存在,则在另外的集合(记为S)中记录下来;
- 如果不存在,则插入Bloom Filter;
- 最后,得到的S即为所有这些大文件中元素的交集
多层划分本质上还是分而治之的思想,重在“分”的技巧上!因为元素范围很大,需要通过多次划分,逐步确定范围,然后最后在一个可以接受的范围内进行。适用用于:第k大,中位数,不重复或重复的数字
问题1:求取海量整数的中位数
解决方案:
- 依次遍历整数,按照其大小将他们分拣到n个桶中。如果有的桶数据量很小,有的则数据量很大,大到内存放不下了;对于那些太大的桶,再分割成更小的桶;
- 之后根据桶数量的统计结果就可以判断中位数落到哪个桶中,如果该桶中还有子桶,就判断在其哪个子桶中,直到最后找出目标。
问题2:一共有N个机器,每个机器上有N个数,每个机器最多存 N 个数,如何找到 N^2 个数中的中数?
解决方案1: hash分解 + 排序
- 按照升序顺序把这些数字,hash划分为N个范围段。假设数据范围是2^32 的unsigned int 类型。理论上第一台机器应该存的范围为0(2^32)/N,第i台机器存的范围是(2^32)*(i-1)/N(2^32)*i/N。hash过程可以扫描每个机器上的N个数,把属于第一个区段的数放到第一个机器上,属于第二个区段的数放到第二个机器上,…,属于第N个区段的数放到第N个机器上。注意这个过程每个机器上存储的数应该是O(N)的。
- 然后我们依次统计每个机器上数的个数,依次累加,直到找到第k个机器,在该机器上累加的数大于或等于(N2)/2,而在第k-1个机器上的累加数小于(N2)/2,并把这个数记为x。那么我们要找的中位数在第k个机器中,排在第(N2)/2-x位。然后我们对第k个机器的数排序,并找出第(N2)/2-x个数,即为所求的中位数的复杂度是O(N^2)的。
解决方案2: 分而治之 + 归并
- 先对每台机器上的数进行排序。排好序后,我们采用归并排序的思想,将这N个机器上的数归并起来得到最终的排序。找到第(N2)/2个便是所求。复杂度是O(N2 * lgN^2)的