赞
踩
java中.java文件在执行编译后,产生.class文件,.class文件是在JVM虚拟机中运行的,而JVM虚拟机可以安装在各个主流系统上,所以编译后的.class文件可以运行在各个系统。
在java中int是中基本类型的其中一种,占4字节,32位,定义后初始化数据为0。
Integer为int类型的包装类,里面定义了许多关于int类型的方法,同时也可以达到存储int类型数据的目的,拥有自动装箱,自动拆箱功能,定义后初始化数据为null,当存储数据在-128到127时,Integer对象是在IntegerCache.cache产生,会复用已有对象,此时使用进行判断,两个是同一个对象。当不在这个区间内,则会重新new一个Integer对象,使用判断后结果为false。
实际上equals方法的底层是==去实现的,当比较的数据类型为基本类型时则不能使用基本类型.equals,需要使用==才能进行比较。
使用equals或==对类型为String的数据进行判断时比较的是字符串内容,当使用equals或==对引用数据进行判断时,比较的是地址在内存中的地址值,当重写equals后可以自己指定比较内容,同时也可以使用hashcode作为比较值。当需要比较大量数据时,使用重写equals方法并结合hashcode进行查找效率最高。
抽象: 将一类对象的共同特征总结出来构造类的过程,抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
封装: 把一个对象的属性私有化,但是同时也提供一些可以被外界访问的属性的方法,如果不希望外界访问,则也不必提供方法给外界。
继承: 使用已存在的类的定义作为基础,建立新类的技术,继承父类的子类可以增加自己独有的属性和方法,同时也可以使用父类的属性和功能,但不能选择性的继承父类,通过继承可以减少代码冗余,增加代码复用。要注意的是java是单继承多实现。
子类拥有父类非private的属性和方法。
子类可以拥有自己的属性和方法,可以在父类的基础上进行扩展。
子类可以用自己的方式实现父类的方法,也就是方法重写。
多态: 指程序中定义的引用变量所指向的具体类型在编程时不确定,而是在运行期间才确定,即一个引用变量到底指向哪个实例对象,该引用变量发出的方法调用到底是那个类中的实现方法,必须由程序运行期间才能决定。
抽象类是定义要被继承的子类的通用特性的,接口只能定义方法。
抽象类是对类的抽象,是一种模板,接口是对行为的抽象,是对行为的规范。
相同点:
接口和抽象类都不能实例化,只能被继承或实现。
都包含抽象方法,并且子类必须重写这些方法。(1.8以后接口引入默认方法和静态方法,默认方法不用被强制实现)
不同点:
声明关键字不同。
实现关键字不同。
抽象类可以有构造器,接口没有。
抽象类的方法可以有各种访问修饰符,接口访问修饰符必须是public
一个类最多继承一个抽象类,但是可以实现多个接口
字符串常量池位于堆内存,专门存放字符串常量,可以提高内存使用率,避免多块空间存储相同字符串。在初始化字符串时会在内存中检查字符串常量池,如果常量池已存在该字符串,则返回它的引用,如果不存在,则实例化一个字符串放入池中,并返回其引用。
字符串是不可变的,java中String类是用final修饰的一个常量char数组,所以不可修改。表面看上去的可以修改实际上是在字符串常量池中新建了一个字符串,并返回了该字符串的引用。
String底层是被final修饰过的所以是不可变的,StringBuffer和StringBuilder虽然也是用char数组实现,但是没有用final修饰,所以是可变的,并且拥有一些独有的字符串处理方法
StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以它是线程安全的,而StringBuilder没有加锁,所以它不是安全的。
StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并引用,相同情况下使用StringBuilder要比StringBuffer效率要高。
修饰符 | 当前类 | 同包 | 子类 | 其他包 |
---|---|---|---|---|
public | ✔ | ✔ | ✔ | ✔ |
producted | ✔ | ✔ | ✔ | ✖ |
default | ✔ | ✔ | ✖ | ✖ |
private | ✔ | ✖ | ✖ | ✖ |
&和&&都是逻辑运算符,从某种意义上来讲,两个效果是相同的,但是&&拥有短路特性,当有多个逻辑表达式进行运算时,如果第一个逻辑表达式就是false,那么不管后面是否正确,都不在执行。同理 | 和 || 也是一样的,只要第一个为true那么后面都不在执行了。
不可以,String类是被final修饰过的,并且继承String本身就是一种错误的行为。
是值传递,因为java中不存在引用传递,当一个对象实例被传入方法中,参数的值是该对象的引用,对象的属性可能会被改变,但是对象引用的改变是不会影响到调用者的。
重载是相同的方法名,参数列表的类型、数量、顺序不同,并且与返回值无关,可以有不同修饰符。
重写是将继承的父类方法重写覆盖掉父类方法,所以要求,方法名,参数列表都与父类方法名和参数列表相同
注:构造方法不能被重写,被final修饰的方法不能被重写,声明为static的方法不能被重写,但是可以被再次声明
可以,因为java使用的默认编码为Unicode,直接使用字符在字符集的编号,一个charl类型占两字节,所以放一个中文汉字是没问题的。
都不可以
首先抽象方法是要被子类重写的,而static方法无法被重写,这是相互矛盾的。
native是有本地方法实现的,而抽象方法是没有实现的,所以也不可以。
synchronized和实现的细节有关,而抽象方法不设计实现细节,因此也是矛盾的。
多态是父类或接口定义的引用变量可以指向子类实现,而程序调用的方法在执行期间才会动态绑定,就是引用变量所指向的具体实例对象的方法,也就是对象内存中正在运行的那个对象,而不是引用变量的类型中定义的那个方法。
静态变量和实例变量的区别:
静态变量属于类,实例变量属于对象;
静态变量只有一份,被所有对象共享,实例变量每个对象都有一份;
静态变量可以直接通过类名访问,实例变量需要通过对象名访问;
静态方法和实例方法的区别:
静态方法属于类,实例方法属于对象;
静态方法可以直接通过类名调用,实例方法需要通过对象名调用;
静态方法中不能使用 this 关键字,因为 this 表示当前对象,而静态方法没有对象;
静态对象和实例对象在存储方式上也是不同的
静态变量存储在方法区(Method Area)中,也称为永久代(PermGen),即在类加载时就已经被分配了内存空间,所有该类的对象共享同一份静态变量的内存空间。
而实例变量则存储在 Java 堆(Java Heap)中,即每个对象都有自己的实例变量的内存空间,当对象被创建时,实例变量也随之被分配内存空间。
需要注意的是,从 JDK 8 开始,永久代被移除了,取而代之的是元空间(Metaspace),静态变量的存储方式也变成了存储在元空间中。但是,与永久代不同,元空间并不是虚拟机运行时数据区域的一部分,而是使用本地内存来实现的。
对象的序列化(Serialization)是指将对象转换为字节流的过程,而反序列化(Deserialization)则是指将字节流转换为对象的过程。通过序列化和反序列化,可以实现对象的持久化存储、网络传输等功能。
实现 Serializable 接口:要使一个类可序列化,需要让该类实现 Serializable 接口。Serializable 接口是一个标记接口,没有定义任何方法,只是作为标记告诉编译器这个类可以被序列化。
序列化:使用 ObjectOutputStream 将对象序列化为字节流,并将字节流写入文件或发送给网络。
进行反序列化:使用 ObjectInputStream 从字节流中读取数据,并将其反序列化为对象。
java异常分为运行时异常和编译时异常
编译时异常是可以被手动处理掉的,大部分是可以预见性的异常,如果是提前知道怎么处理异常,则可以使用try…cache捕获并处理异常,如果不知道如何处理,则定义该方法是声明时抛出该异常。
运行时异常则是只有在代码运行时才发出的异常,比如类转换异常,数组下标越界异常、除数为0数学异常等,这种异常在出现时会被系统自动捕获,可以手动try…cache进行处理,或者直接交给程序自动处理。
try-catch 块:try-catch 块用于捕获和处理可能抛出异常的代码块。try 块中编写可能引发异常的代码,catch 块用于捕获并处理异常。
throws 关键字:throws 关键字用于声明方法可能抛出的异常,让调用该方法的代码去处理异常。
finally 块:finally 块用于定义无论是否发生异常都会执行的代码,通常用于释放资源或清理工作。
throw 关键字:throw 关键字用于在代码中手动抛出异常。
error和exception都继承于throwable类
error一般是虚拟机相关的问题,如系统崩溃,内存空间不足,方法调用栈溢出等,这种问题一旦出现,就代表这无法修复的错误,是非常严重的。
exception表示程序可处理的异常,遇见这种问题,应该尽可能的解决异常,使程序恢复运行。
exception又分为运行时异常和编译时异常,编译时异常表示语法都没用办法通过,运行时异常表示的是只有在程序运行时,才会出现的异常,比如数组下标越界,没找到类异常等。遇见异常时,尽量使用try…cache进行异常捕获并处理,保证程序的正常运行。
泛型(Generic)是一种在编译时期约束集合类接受的元素类型的机制。通过泛型,可以使代码更加通用、可重用,并提高代码的类型安全性。
使用泛型可以带来以下好处:
代码复用性:通过泛型,可以编写更通用的类和方法,适用于不同类型的数据,避免了重复编写相似的代码。
类型安全性:使用泛型可以在编译时发现类型错误,避免在运行时出现类型转换异常或其他类型相关的错误。
减少强制类型转换:使用泛型可以避免频繁进行类型转换操作,使代码更加简洁清晰。
在 Java 中,泛型主要应用于以下几个方面:
泛型类(Generic Class):定义一个泛型类可以接受任意类型的数据,例如 class MyGenericClass<T>
。
泛型方法(Generic Method):定义一个泛型方法可以接受任意类型的参数,例如 public <T> void myGenericMethod(T t)
。
泛型接口(Generic Interface):定义一个泛型接口可以让实现类指定具体的类型,例如 interface MyGenericInterface<T>
。
泛型通配符(Generic Wildcards):使用通配符 ? 可以表示未知类型,在一些情况下可以灵活地处理不同类型的数据。### 反射
在程序运行状态时,都能够知道任何一个类的所有属性和方法,对于任意一个对象,都能调用它的任意一个方法和属性,这种动态获取对象类或对象信息,并动态调用对象的方法被称为反射。
优点:可以在运行期间就对类或对象进行判断,动态加载,提高代码灵活度。
缺点:相当于手动操作JVM进行操作,不如JVM自动操作效率高。
在平时的项目开发过程中,很少直接使用反射,但是在框架设计、模块化开发等都是通过反射调用相对应的字节码,在spring的动态代理中就使用到了反射机制,spring就是通过读取配置文件后,通过全限定类名和反射获得对象实例。
.class
方法.getClass()
方法获取clazz.newInstance()
获取实例clazz.getConstructor()
,通过constroctor.newInstance()
创建实例对象首先通过反射获取class对象
通过class对象获取属性 getDeclaredField(filedName)
给属性设置可以访问 field.setAccessible(true);
Collection接口 和 Map接口继承于Iterator接口
set接口 和 List接口继承于Collection接口
set接口下有HashSet、TreeSet和LinkedHashSet
- HashSet:无序,集合内元素不可重复,线程不安全
- TreeSet:指定排序算法后可以进行排序,线程不安全
- LinkedHashSet:有序并且保证排序,线程不安全
List接口下有ArrayList、LinkedList
- ArrayList:查、改快,线程不安全,初始长度没有指定默认长度的时候,长度为0,并在第一次添加元素时初始化,初始长度为10,再次扩容会先copy数组,并扩容为原数组的1.5倍。
- LinkedList:增、删快,线程不安全,插入元素是将元素置于链表末尾,属于尾插法。
Map接口下有HashMap、TreeMap、LinkedHashMap和HashTable
- HashMap:无序双列集合,线程不安全
- TreeMap:可排序双列集合,线程不安全
- LinkedHashMap:有序并且保证排序的双列集合,线程不安全
- HashTable:无序双列集合,线程安全
HashMap和HashTable都继承于Map接口,都是双列集合,两者的区别在于:
两个类都继承了List接口,都属于单列有序集合。
Vector是Java出现的时候就存在的类,而ArrayList是后面更新后才出现的。java也推荐我们优先使用ArrayList,但是在使用前要考虑线程安全问题,因为ArrayList是线程不安全的,而Vector是线程安全的,如果没有多线程问题则选择ArrayList,这样效率高于Vector。
ArrayList和Vector在创建时都有一个初始容量大小,当存储的数据超过初始容量时,会自动对集合进行扩容,Vector默认每次增长为原来的2倍,ArrayList是增长为原来的1.5倍,但是Vector可以手动设置增长的空间大小,ArrayList不能手动设置增长空间大小。
Array大小是固定的,ArrayList大小是动态变化的。
ArrayList处理固定大小的基本数据类型时,这种方式效率较慢。
在实际应用场景中,如果提前知道需要存储的数量,并且后期不会在改变大小的时候可以使用数组,如果后期对存储容量有动态变化的时候则使用ArrayList。
在jdk1.7之前HashMap的底层原理是由数组+链表实现的,当创建出来HashMap时,是一个数组,数组中的每一个元素是一个单向链表的头指针,指向一个entry键值对,当一个键值对放入hashMap时会根据键的hashcode选择放入哪一个数组元素,也就是选择放入哪一个单向链表中,如果出现两个entry的hash值一样,这样就产生了hash冲突,那么新放入的entry键值对则使用头插法,插入在表头。
jdk1.8之后采用数组+链表+红黑树实现,在基础思想上添加了红黑树进行优化,当链表长度大于等于阈值(8)时,链表转化为红黑树,利用红黑树的自平衡在查找性能上得到提升。当链表长度小于于等于阈值(6)时,红黑树转化为链表。HashMap的初始长度是16,每次自动扩展或手动扩展时,长度必须是2的幂,1.8之后遇到hash冲突后是尾插法。
set底层先使用hashcode值和将要加入的hashcode值进行比较,如果相同则继续使用equals方法进行比较。
HashSet底层是有HashMap实现的,在HashSet的构造方法中初始化了一个HashMap,利用HashMap的键值不唯一,使用HashMap的键来存储值,因此当存储的值重复时会返回false。
TreeSet是有树形结构,基于TreeMap实现的,所以存储是有序的,但是同样是线程不安全的。
LinkedHashMap也是基于HashMap实现的,只是它额外定义了一个Entry header,这个header是独立出来的一个链表头指针。每个entry添加了两个属性,before和after和header结合起来形成了一个双向链表,由此就实现了插入顺序或访问顺序排序。默认排序即插入的顺序。
两者都是线程安全的,但是底层对于线程安全实现方式不同,Hashtable是对表结构进行上锁,其他操作需要等待执行完毕后才能访问,1.8之前ConcurrentHashMap是采用分离锁形式,没有对整表进行锁定,而是对局部进行锁定,不影响其他线程对表的其他地方操作。1.8之后ConcurrentHashMap采用CAS算法进行安全实现线程安全。
进程是包含多个线程的集合,每一个程序在执行时都是一个进程。
线程是进程中最小的数据单位,每个指令都是一个线程。
进程可以没有线程,但是线程必须要存在于进程,即线程依赖于进程。
守护线程是为了服务用户线程的存在,比如jvm虚拟机会等待用户线程执行完成后关闭,但是不会等待GC线程执行完再关闭。
start()方法用来创建线程,底层也是调用了run()方法,这和直接调用run()方法不一样,当你调用run()方法时,是在当前线程调用,使用start()方法是启动一个新线程。
导致线程阻塞的原因大体上来说是因为需要的资源没有就绪,所以会陷入阻塞状态,需要等待资源就绪后才会继续执行。常见的阻塞原因有以下几种
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享,是一种实现线程安全的方式
被volatile修饰的共享变量保证了不同线程对该变量操作的内存可见性,禁止指令重排序。
就是当你写一个 volatile 变量之前,会插入一个写屏障,读一个 volatile 变量之前,会插入一个读屏障。在你写一个 volatile 变量时,能保证任何线程都能看到你写的值,在写之前,也能保证任何数值的更新对所有线程是可见的
创建线程池的常见方法包括使用 Executors 工厂类和直接使用 ThreadPoolExecutor 类。
根据阿里巴巴的《Java 开发手册》,在企业开发中,推荐使用 ThreadPoolExecutor 类直接创建线程池,而不建议使用 Executors 工厂类来创建线程池。这是因为 Executors 工厂类虽然提供了一些便捷的方法来创建线程池,但在某些情况下可能会引发一些意想不到的问题。
具体来说,Executors 工厂类创建的线程池存在一些问题:
FixedThreadPool 和 CachedThreadPool 的风险:Executors.newFixedThreadPool() 和 Executors.newCachedThreadPool() 使用的是无界队列,如果任务提交速度过快,可能导致内存溢出。
SingleThreadExecutor 的风险:Executors.newSingleThreadExecutor() 使用的也是无界队列,如果任务提交速度过快,也可能导致内存溢出。
相比之下,使用 ThreadPoolExecutor 类可以更加灵活地配置线程池的参数,包括核心线程数、最大线程数、工作队列类型等,从而更好地控制线程池的行为,避免出现意外情况。因此,在阿里巴巴企业中,建议使用 ThreadPoolExecutor 类来创建线程池,以确保线程池的稳定性和可靠性。
创建一个线程池时,可以通过设置一些核心参数来配置线程池的行为。以下是线程池的一些核心参数:
corePoolSize(核心线程数):
线程池中同时执行的核心线程数量。
核心线程会一直存活,即使没有任务需要执行。
当有新任务提交时,如果核心线程数还没有达到限制,将创建新的核心线程来执行任务。
maximumPoolSize(最大线程数):
线程池中允许的最大线程数量。
当任务提交数量超过核心线程数时,线程池可以创建新的线程来执行任务,直到达到最大线程数。
若任务继续增加,超出最大线程数的任务会被拒绝执行。
keepAliveTime(线程空闲时间):
当线程池中的线程数量超过核心线程数,并且空闲时间达到 keepAliveTime,多余的线程会被销毁。
设置时间单位,如 TimeUnit.MILLISECONDS。
workQueue(任务队列):
用于存储待执行任务的阻塞队列。
不同的队列类型可选择,如 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue 等。
当任务提交数量超过核心线程数时,新任务会被添加到队列中等待执行。
threadFactory(线程工厂):
用于创建新的线程。
可以自定义线程的命名、优先级等属性。
handler(拒绝策略):
当线程池已达到最大线程数并且队列也已满时,新任务无法被提交时的处理策略。
常见的处理策略有 AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy 等。
除了上面提到的几个核心参数外,线程池还有其他一些可选参数可以根据实际情况进行配置。以下是一些常用的可选参数:
allowCoreThreadTimeOut(允许核心线程超时):
若为 true,则核心线程也会在 keepAliveTime 时间内超时并被回收。
默认为 false。
rejectedExecutionHandler(拒绝策略):
当任务无法被提交时的处理策略。
ThreadPoolExecutor 中提供了 4 种拒绝策略,分别是 AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy。
在使用自定义拒绝策略时,需要实现 RejectedExecutionHandler 接口,并重写 rejectedExecution() 方法。
keepAliveTime 和 TimeUnit 的组合设置:
keepAliveTime 参数的单位可以通过 TimeUnit 枚举值进行设置,包括 NANOSECONDS、MICROSECONDS、MILLISECONDS、SECONDS、MINUTES、HOURS 和 DAYS。
不同的 TimeUnit 设置对应的 keepAliveTime 值不同,如 TimeUnit.SECONDS 对应的 keepAliveTime 值为秒数。
threadFactory 和 rejectedExecutionHandler 的默认实现:
ThreadPoolExecutor 提供了默认的线程工厂和拒绝策略实现,如果不需要自定义,可以直接使用默认实现。
setMaximumPoolSize() 方法:
在运行过程中,可以通过 setMaximumPoolSize() 方法动态地修改最大线程数。这个方法可能会影响到线程池的并发性能,需要慎重使用。
ArrayBlockingQueue、LinkedBlockingQueue 和 SynchronousQueue 是 Java 中常用的三种阻塞队列实现,它们有以下区别:
ArrayBlockingQueue:
ArrayBlockingQueue 是一个基于数组的有界阻塞队列。
它在构造时需要指定容量,即队列中可以存储的元素数量。
如果队列已满,则插入操作将被阻塞,直到队列中有空闲位置。
如果队列为空,则移除操作将被阻塞,直到队列中有元素可供移除。
ArrayBlockingQueue 是线程安全的,适用于固定大小的线程池。
LinkedBlockingQueue:
LinkedBlockingQueue 是一个基于链表的可选界阻塞队列。
在构造时,可以选择不指定容量,或者指定一个上限。如果不指定,则容量默认为 Integer.MAX_VALUE,即无界队列。
插入操作将一直成功,除非队列已满。
移除操作将一直成功,除非队列为空。
LinkedBlockingQueue 是线程安全的,适用于无限制大小或大容量的线程池。
SynchronousQueue:
SynchronousQueue 是一个不存储元素的阻塞队列。
每个插入操作必须等待一个对应的移除操作,反之亦然。
插入和移除操作是成对的,无法独立进行。
SynchronousQueue 是线程安全的,适用于线程池中的任务移交。
总结:
ArrayBlockingQueue 是一个有界阻塞队列,固定大小,适用于固定大小的线程池。
LinkedBlockingQueue 是一个可选界阻塞队列,默认情况下无界,适用于无限制大小或大容量的线程池。
SynchronousQueue 是一个不存储元素的阻塞队列,仅用于线程之间直接传输任务。
AbortPolicy(默认策略):
当线程池无法执行新任务时,会抛出 RejectedExecutionException 异常。
这是默认的拒绝策略。
CallerRunsPolicy:
当线程池无法执行新任务时,会使用调用线程来执行该任务。
这可能会降低整体的处理速度,但可以保证不会丢失任务。
DiscardPolicy:
当线程池无法执行新任务时,会丢弃被拒绝的任务,不做任何处理。
使用这个策略可能会导致任务丢失,不建议在需要保证任务不丢失的情况下使用。
DiscardOldestPolicy:
当线程池无法执行新任务时,会丢弃队列中最旧的任务,然后尝试重新提交新任务。
这样可以腾出空间给新任务,但可能会丢失一些等待时间较长的任务。
自定义拒绝策略:
可以通过实现 RejectedExecutionHandler 接口来自定义拒绝策略,重写 rejectedExecution() 方法来定义具体的处理逻辑。
http是超文本传输协议,它规定通过浏览器访问服务器时要遵循请求发送的规则,要求请求的格式要有请求行,请求头,请求体。get方法没有请求体
get和post则是发送请求的两种不同的方法
get请求可以被缓存,可以保存在历史浏览记录中,可以被收藏为书签,响应速度比post快,但是缺乏安全性,请求的数据会在地址栏中显示,请求长度限制最大4k。
post请求不可以被缓存,不会保留在历史浏览记录中,不能被收藏为书签,请求长度没有限制,请求数据不会显示在地址栏上,请求头比get更大。
servlet包含四个生命周期
jsp是servlet的技术加强,所有的jsp文件都会被解析成一个继成HttpServlet的类,这个类可以对外访问,并且可以动态的生成HTML,XML,或其他格式的web文档返回给用户。
servlet是应用在java文件中,处理用户请求,以HTML,XML,或其他格式返回给用户。
jsp多侧重于页面展示,servlet侧重处理业务逻辑。
jsp拥有9个内置对象,4个作用域
内置对象
作用域
两者同是追踪回话的一种方式,最直观的区别就在于Session是在服务器端存储用户的登录信息,Cookie是在客户端浏览器存储用户的登录信息。
Seesion的机制决定了用户只能获取自己的session,其他用户的seesion不可见,各客户的session相互独立不可见。seesion的使用比cookie要方便,但是会对服务器产生一定的压力。
MVC是目前B/S架构中最常见的设计思想,利用分层思想将程序的整个运行流程分为三层,方便开发中的逻辑处理,实现每一层处理不同的业务。
M: 用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据。
V: view代表向用户展示的页面数据。
C: 是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。
Java类的生命周期为:加载、验证、准备、解析、初始化、使用、卸载七个生命周期,其中,加载、验证、准备、解析、初始化可以称之为类的加载过程。
首先将class文件转为二进制流,接着验证是否为class文件和文件的正确性,验证完毕后在内存中生成该类对象,接着对类中的属性进行预定义,在局部变量表中进行分配存储。解析主要完成符号引用到直接引用的转换动作,有可能在初始化之后开始解析。初始化是对类中的变量进行初始化,加载构造方法,剩余的都又jvm进行初始,之后才开始执行类中定义的java程序代码。开始使用,最后卸载。
当运行指定程序时JVM会按照一定规则编译并加载class文件,组织成为一个完整的java应用程序,这个过程由类加载器完成。一般有隐式加载(使用new的方式创建对象)和显示加载(利用class.forName()创建对象)两种方式。
类的加载是动态的,它不会一次性将所有的类加载完后再执行,而是保证基础类的加载,至于其他类则在需要时加载。
java内存分为以下5个区域:
GC是JVM虚拟机中的垃圾回收器,可以自动监测堆内存中是否存在垃圾对象,从而达到自动回收内存的目的,尽量避免内存溢出。Java 语言没有提供释放已分配内存的显示操作方法。
在 Java 中垃圾回收由虚拟机自行执行。JVM 有一个垃圾回收线程,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描垃圾对象,将他们回收。
判断一个对象是否存活有两种方法:
引用计数法:给每一个对象设置一个引用计数器,当有一个地方引用这个对象时,计数器加一,引用失效时,计数器减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收。
引用计数法有一个缺陷就是无法解决循环引用问题,所以主流的虚拟机都没有采用这种算法。
可达性算法:从一个被称为 GC Roots 的对象开始向下搜索,如果一个对象到 GC Roots 没有任何引用链相连时,则说明此对象不可用。当一个对象不可达 GC Root 时,这个对象并不会立马被回收,若要被真正的回收需要经历两次标记。当经历两次标记后该对象将被移除” 即将回收”集合,等待回收。
在程序开发过程中,最让人头疼的就是内存回收,但是java引入了垃圾回收机制,使此类为题迎刃而解,使开发人员在开发中不用再考虑内存管理。
垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。不可预知的情况下对内存中已经死亡的或者长时间没有使用的对象进行清除和回收,开发者不能手动的调用垃圾回收器对某个对象进行垃圾回收。
回收机制有分代复制垃圾回收和标记垃圾回收,增量垃圾回收。
当程序员创建对象时,GC 就开始监控这个对象的地址、大小以及使用情况。GC 采用有向图的方式记录和管理堆(heap)中的所有对象。通过这种方式判断是不是不可引用的垃圾对象,当确定是垃圾对象后GC会回收这些内存空间。而程序员虽然可以手动执行System.gc()但是java不能保证一定会销毁对象。
内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中。Java 中有垃圾回收机制,它可以保证一个对象不再被引用的时候,对象将被垃圾回收器从内存中清除。
而java中使用有向图进行垃圾回收管理,可以消除引用循环的问题。
当然java也存在内存泄漏的可能,比如创建了一个对象,以后一直不再使用这个对象,这个对象却一直被引用,即这个对象无用但是却无法被垃圾回收器回收的,这就是 java中可能出现内存泄露的情况。
深拷贝就是更改copy后的对象数据,原对象数据不变。浅拷贝就是更改copy后的对象数据原对象数据也跟着改变。简单来说就是深拷贝复制对象的值,浅拷贝是复制对象的地址。
如果对象的引用被置为null,只是断开了当前线程栈帧中对该对象的引用关系,而垃圾收集器是运行在后台的线程,只有当用户线程运行到安全点或者安全区域才会扫描对象引用关系,扫描到对象没有被引用则会标记对象,这时候仍然不会立即释放该对象内存,因为有些对象是可恢复的(在 finalize方法中恢复引用 )。只有确定了对象无法恢复引用的时候才会清除对象内存。
当对象没有变量引用的时候,这个对象就可以被回收了。
• 对象优先在堆的 Eden 区分配
• 大对象直接进入老年代
• 长期存活的对象将直接进入老年代
当 Eden 区没有足够的空间进行分配时,虚拟机会执行一次Minor GC 。Eden 区的对象生存期短,所以可能会频繁触发MinorGC,触发MinorGC后会将未被释放掉的对象放入S0或S1内存,如果要放入对象大于S0或S1内存的50%,则跳过S1或S0直接放入老年代。
垃圾回收不会发生在永久代,如果永久代满了或者是超过了临界值,会触发Full GC
注:Java 8 中已经移除了永久代,新加了一个叫做元数据区的native内存区。
类加载器是一个用来加载类文件的类。Java源代码通过javac编译器编译成类文件。然后JVM执行加载后的字节码。类加载器负责加载文件系统、网络或其他来源的类文件。
加载器 | 说明 |
---|---|
启动类加载器(BootstrapClassLoader) | 用来加载 Java 核心类库,无法被 Java 程序直接引用。 |
扩展类加载器(ExtensionsClassLoader) | 用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。 |
系统类加载器(SystemClassLoader) | 根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java应用的类都是由它来完成加载的。 |
用户自定义类加载器 | 通过继承 java.lang.ClassLoader 类的方式实现。 |
HTTP(Hypertext Transfer Protocol)是一种用于在Web浏览器和Web服务器之间传输数据的协议。它是基于客户端-服务器模型的,使用TCP作为传输协议,通过URL来定位资源,并使用请求-响应的方式进行通信。
HTTPS(Hypertext Transfer Protocol Secure)是在HTTP的基础上添加了安全性支持的协议。它通过使用SSL(Secure Sockets Layer)或TLS(Transport Layer Security)协议对通信进行加密,从而确保传输过程中的数据安全性。
主要区别如下:
安全性:HTTP是明文协议,数据传输过程中的内容是不加密的,易受到窃听和篡改的风险。而HTTPS通过使用SSL/TLS协议对数据进行加密,确保传输过程中数据的机密性和完整性,提供更高的安全性。
端口号:HTTP默认使用端口号80进行通信,而HTTPS默认使用端口号443。这样可以使得网络设备能够根据端口号来区分HTTP和HTTPS流量,从而进行相应的处理。
证书:在使用HTTPS时,服务器需要拥有一个有效的数字证书。该证书由受信任的第三方机构颁发,用于验证服务器的身份。这样可以防止中间人攻击,确保通信的安全性。
HTTPS相比于HTTP提供了更高的安全性,适用于需要保护用户隐私和敏感数据的场景,如登录、支付等。而HTTP则适用于不涉及敏感信息传输的普通网页浏览等场景。为了确保数据的安全性,使用HTTPS仍然需要注意其他方面的安全措施,如防止跨站脚本攻击(XSS)、点击劫持等。
连接性:TCP是面向连接的协议,通信前需要建立连接、传输数据、然后释放连接;而UDP是无连接的协议,通信时不需要建立连接,直接发送数据包。
可靠性:TCP提供可靠的数据传输,通过确认和重传机制来确保数据的完整性和顺序性;UDP不提供数据传输的可靠性,数据包可能丢失或乱序,并不关心对方是否收到数据包的情况。
面向字节流和面向报文:TCP是面向字节流的协议,将数据视为字节流进行传输;UDP是面向报文的协议,每个数据包都是一个完整的报文。
拥塞控制:TCP具有拥塞控制机制,可以根据网络情况动态调整传输速率;UDP没有拥塞控制,数据包只能以发送者设定的速率传输。
TCP常用于需要可靠传输的场景,如网页浏览、电子邮件传输、文件下载等。
UDP常用于对实时性要求较高、容忍少量数据丢失的场景,如直播的音视频流媒体传输、在线游戏、VoIP通话等。
物理层(Physical Layer):负责传输比特流,主要涉及物理介质、数据传输速率、电压等物理特性。
数据链路层(Data Link Layer):负责在相邻节点之间传输数据帧,提供可靠的数据传输服务。包括物理寻址、错误检测与纠正等功能。
网络层(Network Layer):负责在不同网络之间传输数据包,实现数据的路由和转发。包括逻辑寻址、路由选择、拥塞控制等功能。
传输层(Transport Layer):负责端到端的数据传输,提供可靠的数据传输服务。包括数据分段、流量控制、错误恢复等功能。常见的协议有TCP和UDP。
会话层(Session Layer):负责建立、管理和终止会话连接,确保数据的顺序传输和同步。处理会话层的协议如SIP、NetBIOS等。
表示层(Presentation Layer):负责数据格式转换、加密解密、数据压缩等操作,确保数据在不同系统之间的兼容性和可靠性。
应用层(Application Layer):提供用户接口和网络应用服务,包括HTTP、FTP、SMTP等协议,实现用户与网络的交互。
IP地址(Internet Protocol address)是用来标识网络上设备的数字地址,它是计算机在网络上的唯一标识,类似于现实世界中的门牌号码。IP地址可以分为IPv4和IPv6两种版本。
IPv4是目前广泛使用的IP地址协议版本,它由32位二进制数表示,通常以每8位二进制数为一组,用十进制数表示
IPv6是下一代IP地址协议版本,它由128位二进制数表示,通常以每16位二进制数为一组,用十六进制数表示
IPv4和IPv6主要的不同点在于地址长度和表示方式上的差异,IPv6拥有更大的地址空间和更多的功能特性,能够更好地支持互联网的持续发展和扩张
DNS(Domain Name System)是互联网中用于将域名解析为对应IP地址的分布式数据库系统,它提供了域名和IP地址之间的映射关系,使得用户可以通过使用易记的域名来访问互联网上的各种服务和资源。
DNS的主要作用包括以下几个方面:
域名解析:将用户输入的域名转换成对应的IP地址,以便能够在网络中定位到特定的主机或服务器。
逆向解析:将IP地址反向解析成对应的域名,用于确定某个IP地址对应的主机名或域名。
路由器(Router)是一种网络设备,用于在不同网络之间传输数据包,并根据目标地址选择合适的路径进行转发。路由器在计算机网络中扮演着非常重要的角色,它主要用于实现不同网络之间的互联互通,确保数据能够在网络中准确、快速地传输。
路由器在计算机网络中的主要角色包括:
数据包转发:路由器接收到数据包后,会根据数据包中的目标IP地址,查找路由表确定最佳路径,然后将数据包转发到相应的下一跳路由器或目标主机。
网络分割:路由器可以将一个大的网络划分为多个子网,实现对不同子网的管理和控制,提高网络性能和安全性。
网络连接:路由器可以连接不同类型的网络,如LAN(局域网)与WAN(广域网),实现不同网络之间的通信交换。
数据包过滤:路由器可以根据配置的访问控制列表(ACL)等规则,对数据包进行过滤和检查,保护网络安全。
负载均衡:路由器可以根据负载情况,动态调整数据包的传输路径,实现负载均衡,提高网络性能和可靠性。
网络拓扑结构是指计算机网络中各个节点之间连接的方式和布局方式。不同的网络拓扑结构对于数据传输、网络容错性和成本等方面有不同的影响。
常见的网络拓扑结构包括:
星型拓扑结构:
优点:易于管理和维护,故障定位简单,适合小型网络。
缺点:中心节点故障会导致整个网络瘫痪,扩展性受限。
总线型拓扑结构:
优点:易于布线,成本较低,适合小型网络。
缺点:如果主干线路出现问题,整个网络将受到影响,扩展性受限。
环型拓扑结构:
优点:对称性强,适合小型网络。
缺点:当某个节点或线路出现问题时,整个环型结构会受到影响。
网状型拓扑结构:
优点:具有高度的冗余性和容错性,适合大型网络。
缺点:成本较高,管理和维护较为复杂。
树型拓扑结构:
优点:结构清晰,扩展性较好,适合中等规模的网络。
缺点:如果根节点出现问题,整个网络将受到影响。
ARP(地址解析协议)是一种用于在IPv4网络中将IP地址解析为对应的物理MAC地址的协议。
ARP的作用是通过查询局域网内的设备,获取目标设备的MAC地址,从而实现数据包的传输。当主机需要发送数据包给同一局域网内的另一个主机时,它会首先检查目标主机的IP地址是否存在于其本地的ARP缓存表中。如果存在,则直接使用对应的MAC地址进行通信;如果不存在,则需要发送ARP请求广播,询问目标主机的MAC地址。
TCP连接的建立过程(三次握手):
客户端向服务器发送一个带有SYN标志的数据包,表示请求建立连接。
服务器接收到客户端的SYN请求后,会回复一个带有ACK和SYN标志的数据包,表示同意建立连接,并确认客户端的SYN。
客户端收到服务器的确认后,也会发送一个带有ACK标志的数据包,表示连接建立成功。
TCP连接的断开过程(四次挥手):
客户端向服务器发送一个带有FIN标志的数据包,表示请求断开连接。
服务器收到客户端的FIN后,会回复一个带有ACK标志的数据包,表示确认收到客户端的请求。
服务器在完成当前发送的数据传输后,也会向客户端发送一个带有FIN标志的数据包,表示自己也准备断开连接。
客户端收到服务器的FIN后,会回复一个带有ACK标志的数据包,表示确认收到服务器的请求。此时客户端和服务器的连接就断开了。
SQL注入攻击是一种利用Web应用程序对用户输入数据的处理不当,(如:用户输入未经验证或过滤、动态构建SQL语句等),从而向后端数据库中插入恶意SQL语句的安全漏洞。通过SQL注入攻击,黑客可以执行未经授权的数据库操作,如删除数据、修改数据、获取敏感信息等。
防范SQL注入攻击的方法包括:
XSS攻击)是一种利用Web应用程序对用户输入数据的处理不当,将恶意脚本注入到网页中,从而在用户的浏览器上执行恶意脚本的安全漏洞。通过XSS攻击,攻击者可以窃取用户的会话信息、篡改网页内容、重定向用户浏览器等,造成一系列安全问题。
防范XSS攻击的方法包括:
DDoS攻击是一种分布式拒绝服务攻击,它通过大量的合法或非法的请求来淹没目标服务器或网络,导致目标系统无法正常工作或服务中断。攻击者通常会使用大量的计算机、服务器或物联网设备等构成一个“僵尸网络”,利用这些设备来发起攻击。
应对DDoS攻击的方法包括:
加密算法是一种通过对数据进行转换和处理,使其在未授权的情况下无法被读取或理解的算法。加密算法可以用于保护数据的机密性、完整性和可用性,确保数据在传输和存储过程中不被窃取或篡改。
以下是几种常见的加密算法以及它们的特点和适用场景:
对称加密算法:
DES(Data Encryption Standard):DES是一种对称加密算法,使用56位密钥对数据进行加密和解密。DES已经被认为安全性较低,通常用于保护低级别的敏感数据。
AES(Advanced Encryption Standard):AES是目前最常用的对称加密算法之一,支持128位、192位和256位三种密钥长度,安全性高,适用于保护各种类型的数据,如金融数据、个人信息等。
非对称加密算法:
RSA(Rivest-Shamir-Adleman):RSA是一种非对称加密算法,使用公钥和私钥进行加密和解密。RSA广泛应用于数字签名、数据加密等场景,适用于保护通信数据的安全性。
ECC(Elliptic Curve Cryptography):ECC是一种基于椭圆曲线数学原理的非对称加密算法,相比RSA在相同安全级别下具有更小的密钥长度和更高的性能,适用于移动设备、物联网设备等资源受限的场景。
哈希算法:
MD5(Message Digest Algorithm 5):MD5是一种哈希算法,用于将任意长度的数据映射为固定长度的哈希值。由于MD5存在碰撞攻击风险,已不推荐用于安全性要求高的场景。
SHA-256(Secure Hash Algorithm 256-bit):SHA-256是一种安全性更高的哈希算法,生成256位的哈希值,广泛应用于数字签名、数据完整性验证等场景。
公钥基础设施(PKI)是一种由多个组件和协议组成的框架,用于管理和分发数字证书、公钥和密钥对等安全凭证,以确保通信双方的身份验证和数据加密。
PKI包括以下四个主要组件:
数字证书:数字证书是一个电子文档,其中包含了用户或实体的公钥、身份信息以及数字签名等信息,由认证机构(CA)颁发并验证真实性和合法性。
公钥:公钥是由数字证书中提取出来的一个加密算法所需的公钥,用于加密数据以保证数据的机密性和完整性。
私钥:私钥是与公钥配对的一种加密算法所需的秘密密钥,用于解密数据和签名数据以保证数据的真实性和完整性。
认证机构(CA):认证机构是负责数字证书颁发和管理的机构,负责验证用户或实体身份信息,颁发数字证书,并在数字证书过期或被吊销时撤销数字证书。
PKI在网络安全中的作用主要有以下三个方面:
身份验证:PKI通过数字证书和公钥的方式,验证通信双方的身份信息,确保通信双方是合法的、授权的实体,并减少身份欺骗的风险。
数据保护:PKI通过公钥加密和私钥解密的方式,确保通信数据在传输和存储过程中的机密性和完整性,防止数据泄露和篡改等风险。
数字签名:PKI通过私钥签名和公钥验证的方式,确保通信数据的真实性和完整性,防止数据被篡改和伪造,保证通信数据的可信性。
进行网络漏洞扫描和评估是保障网络安全的重要步骤,主要包括以下几个步骤:
确定扫描范围:首先确定需要扫描的网络和系统范围,包括内部和外部网络设备、应用程序、操作系统等。
选择合适的漏洞扫描工具:根据实际需求选择适合的漏洞扫描工具进行扫描,可以是开源工具或商业工具,确保工具能够覆盖到目标系统的各种漏洞类型。
进行漏洞扫描:运行选定的漏洞扫描工具对目标系统进行扫描,识别系统中存在的漏洞、弱点和安全风险。
分析扫描结果:对扫描结果进行分析和评估,确定哪些漏洞是真实的、危险程度如何,以及如何修复这些漏洞。
制定漏洞修复计划:根据漏洞扫描结果,制定漏洞修复计划,按照严重性级别和紧急性进行漏洞修复。
常见的漏洞扫描工具包括但不限于:
Nessus:Nessus是一款商业漏洞扫描工具,功能强大,支持广泛的漏洞检测和报告功能。
OpenVAS:OpenVAS是一款开源的漏洞扫描工具,提供漏洞检测、资产管理和报告功能,适合中小型组织使用。
Nmap:Nmap是一款开源的网络扫描工具,可以用于发现主机、服务和漏洞,并提供灵活的扫描选项和脚本功能。
Acunetix:Acunetix是一款专注于Web应用程序漏洞扫描的商业工具,用于检测Web应用程序中的漏洞和安全问题。
QualysGuard:QualysGuard是一款云端的漏洞扫描工具,提供自动化的漏洞扫描和管理解决方案,适用于企业级网络安全评估。
网络嗅探(sniffing)攻击是指黑客利用网络嗅探工具监视和拦截网络传输的数据包,以获取敏感信息(如账号、密码、信用卡信息等)的一种攻击方式。黑客可以通过网络嗅探工具截取经过网络的数据包,并分析其中的信息,从而获取目标系统或用户的机密信息。
为了避免成为网络嗅探者的目标,可以采取以下几点防范措施:
加密通信数据:使用加密通信协议(如HTTPS、SSH等)保护数据在传输过程中的安全性,避免数据被窃取和篡改。
使用虚拟专用网络(VPN):通过使用VPN可以建立加密的隧道通信,保护数据在公共网络上的传输安全,降低被网络嗅探者截取数据的风险。
定期更新安全补丁:及时更新操作系统、应用程序和网络设备的安全补丁,修复潜在漏洞,减少黑客利用漏洞进行网络嗅探攻击的可能性。
禁用不必要的网络服务:关闭或禁用不必要的网络服务和端口,减少黑客进行嗅探攻击的入口,提高网络安全性。
使用网络入侵检测系统(IDS):部署网络入侵检测系统来监控网络流量,及时发现异常活动和潜在的嗅探攻击行为,加强网络安全防护。
加强访问控制:设置强密码、多因素认证等访问控制措施,限制未授权用户的访问权限,避免敏感信息被窃取。
端口扫描(Port scanning)是指黑客或安全测试人员用来识别目标主机开放的网络端口和服务的过程。通过端口扫描,可以确定目标主机上哪些端口处于监听状态,并根据端口开放情况做进一步的漏洞分析、攻击或安全评估。
以下是几种常见的端口扫描技术和工具:
TCP 扫描:使用 TCP 协议的端口扫描技术,发送 TCP SYN、ACK 或 RST 包到目标主机的端口,根据返回的响应来确定端口的开放状态。常用工具包括 Nmap、Hping等。
UDP 扫描:使用 UDP 协议的端口扫描技术,发送 UDP 数据包到目标主机的端口,根据返回的响应来确定端口的开放状态。常用工具包括 Nmap、NetScanTools等。
SYN 扫描:也称为半开放扫描,通过发送 TCP SYN 包到目标主机的端口,根据返回的响应来确定端口的开放状态。常用工具包括 Nmap、Masscan等。
FIN 扫描:通过发送 TCP FIN 包到目标主机的端口,根据返回的响应来确定端口的开放状态。FIN 扫描通常用于绕过防火墙和入侵检测系统。常用工具包括 Nmap、NetScanTools等。
XMAS 扫描:通过发送 TCP FIN、URG、PSH 标志位都置为 1 的数据包到目标主机的端口,根据返回的响应来确定端口的开放状态。XMAS 扫描通常用于检测主机是否处于活跃状态。常用工具包括 Nmap、NetScanTools等。
spring框架是一个轻量级,一站式的开源java框架,它提供了综合广泛的基础性支持平台。拥有简单性,可测试性,松耦合性,任何java程序都能从中收益。
spring中已经集成了20多个模块,比如常用的web,jdbc,aop,tx等。
spring框架提供了IoC(控制翻转)和DI(依赖注入)实现了松耦合的依赖关系
spring框架提供了AOP切面编程,对指定功能进行增强,减少冗余代码,提高代码复用性。
spring框架提供了简单的声明式事务,使开发中能够专注逻辑代码而不用花费过多心思管理事务。
spring框架是按照模块的形式来进行组装的,使用的时候一目了然,直接导入使用的模块即可。
IoC(控制翻转)将对象交给spring进行创建、初始化、管理、销毁,并存放在IoC容器中。
DI(依赖注入)在程序使用IoC容器中的某个对象时,从容器中获取对象。有三种注入方式:set方法注入,构造方法注入,工厂方法注入,静态方法注入,接口注入。
spring框架没有对单例Bean进行任何的线程封装处理,关于单例Bean的线程安全问题需要开发者自己解决。但是在实际开发中,大部分bean都没有可变状态,所以从某种情况上考虑,spring的单例是安全的,如果bean有多种状态的话,就需要自己保证线程安全。最常用的办法就是将bean的作用域由singleton变更为prototype。
spring中用到了大量的设计模式具体如下:
作用域 | 说明 |
---|---|
singleton | bean在每个spring ioc容器中只有一个实例,配置文件配置后默认是单例 |
prototype | 一个bean的定义可以有多个实例 |
request | 每次http请求都会创建一个bean,该作用域仅在基于web的Spring ApplicationContext情形下有效 |
session | 在一个HTTP Session中,一个bean定义对应一个实例,该作用域仅在基于web的Spring ApplicationContext情形下有效 |
global-session | 在一个HTTP Session中,一个bean定义对应一个实例,该作用域仅在基于web的Spring ApplicationContext情形下有效,缺省的Spring bean的作用域是Singleton |
AOP是面向切面编程,是一种编程技术,允许对功能做横向切割,对目标方法做增强,可以有效的增加代码复用性,减少冗余代码。
AOP底层是以jdk动态代理和cglib动态代理技术实现的,jdk的动态代理是对接口方法进行增强,所以要求切入点至少要实现一个接口,否则spring会使用另一个cglib动态代理进行增强,而cglib是在程序运行期间找到继承的父类,并生成代理后的子类。
切面是我们要织入的功能,可以是一个类中的一个方法,也可以是一个类中的多个方法。
切入点是我们要对哪个功能进行增强,在开发中,我们通常理解为一个切入点就是一个方法。
增强的方式也有前置增强,返回前增强,返回后增强,异常增强,环绕增强,总共5中方式,具体使用哪种方法还要参考具体业务逻辑需求。
spring有自己的一套事务管理机制,一般使用TransactionMananger进行管理,可以通过配置文件进行注入来完成此功能,通过配置可以实现对事务级别和传播性的控制。而底层也是使用到了spring的AOP思想。
ApplicationContext是BeanFactory的子类,两者都有getBean
这个核心方法
Beanfactory只用基本只有跟Bean相关的功能,而ApplicationContext则在此基础上有增加了很多bean的细节,且进行了一定程度的封装。
用户请求发送给DispatcherServlet,DispatcherServlet调用HandlerMapping处理器映射器;
HandlerMapping根据xml或注解找到对应的处理器,生成处理器对象返回给DispatcherServlet;
DispatcherServlet会调用相应的HandlerAdapter;
HandlerAdapter经过适配调用具体的处理器去处理请求,生成ModelAndView返回给DispatcherServlet
DispatcherServlet将ModelAndView传给ViewReslover解析生成View返回给DispatcherServlet;
DispatcherServlet根据View进行渲染视图;
Spring的MVC框架是围绕DispatcherServlet来设计的,它用来处理所有的HTTP请求和响应。
是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方案是在控制器里面不能写字段。
通过Jackson框架就可以把Java里面的对象直接转化成Js可以识别的Json对象。
这样就可以直接返回一个json字符串形式的json对象,但是要注意方法上要加上@ResponseBody注解,或者类上加@RestController注解。
是对象关系映射,是一种为了解决关系型数据库与java对象的映射技术,ORM是通过使用描述对象和数据库之间的映射关系,将程序中的对象自动持久化到关系型数据库中。
mybatis支持一对一、一对多的延迟加载
当调用方法进行查询时,进入拦截器方法,当判断到关联查询的值为null时,会去查找相对应的sql语句进行二次查询,然后在赋予值。
'%${question}%'
可能引起SQL注入,不推荐"%"#{question}"%"
CONCAT(’%’,#{question},’%’)
dao接口工作原理是JDK动态代理,在运行时会通过动态代理产生代理对象,代理对象会拦截接口方法,执行所对应的sql语句,随后返回执行结果。
dao接口方法是不能重载的,因为要靠全限定名和方法名去进行拦截处理
首先判断返回结果是resultType还是resultMap,如果是ResultType会根据全限定类名查找实体类通过反射创建对象,调用set方法给对象赋值。如果是ResultMap则会直接根据配置的名称和数据库查询后的别名进行映射处理。
Mybatis动态sql可以让我们在Xml映射文件内,以标签的形式编写动态sql,完成逻辑判断和动态拼接sql的功能
比如<foreach>
、<if>
、<where>
mybatis拥有一级和二级缓存
简化配置:Spring Boot 提供了自动化配置(Auto-configuration)功能,大大减少了开发人员对应用程序的配置工作量。通过约定大于配置的原则,开发者可以快速搭建和启动应用程序。
内嵌容器:Spring Boot 支持内嵌容器(Embedded Container),如 Tomcat、Jetty 等,使得应用程序可以独立运行,无需外部应用服务器的支持。
依赖管理:Spring Boot 提供了 Starter 依赖,简化了项目中各种依赖库的管理,开发者只需添加相关 Starter 依赖即可快速集成所需功能模块。
集成测试支持:Spring Boot 提供了良好的测试支持,开发者可以轻松编写单元测试和集成测试,保证应用程序的质量。
监控与管理:Spring Boot Actuator 模块提供了丰富的监控和管理功能,如健康检查、性能指标、应用信息等,方便开发者监控应用程序的运行状态。
生态系统:Spring Boot 集成了大量的第三方库和插件,支持各种应用场景的开发,如数据库访问、安全认证、缓存等,使得开发更加高效。
微服务支持:Spring Boot 非常适合构建微服务架构,支持 RESTful Web 服务开发,同时提供了微服务治理的解决方案。
当应用程序启动时,Spring Boot 会扫描项目中的类路径,并加载所有的配置类、组件和依赖,通过 @EnableAutoConfiguration 注解,Spring Boot 启用自动配置功能。这个注解通常放在主配置类(如 SpringApplication 的入口类)上。Spring Boot 会加载所有在类路径下的 META-INF/spring.factories 文件中声明的 EnableAutoConfiguration 实现类。这些实现类包含了自动配置的逻辑和条件。Spring Boot 根据条件化配置的规则(@Conditional 注解)来判断是否需要自动配置某些组件或功能。这些条件可以基于环境变量、系统属性、类路径上的资源等。如果项目中引入了 Starter 依赖,Spring Boot 会根据 Starter 中所包含的自动配置来决定需要加载哪些组件。Starter 依赖通常包含了一组相关的库和自动配置,以便快速集成常用功能。根据条件化配置和 Starter 依赖,Spring Boot 开始自动装配各种组件和功能。它会根据约定大于配置的原则,尝试自动配置数据库连接、消息队列、Web 服务器等常见的组件,以及其他必要的配置。自动装配过程中,Spring Boot 将创建并注册各种 Bean 到 Spring 上下文中。这些 Bean 可能是各种服务、控制器、数据库连接池等组件,开发者无需手动编写配置文件或代码。自动配置的顺序也是有优先级的,高优先级的配置会覆盖低优先级的配置。这样,开发者可以通过自定义配置来覆盖默认的自动配置。
Spring Boot Starter 的作用包括:
简化依赖管理:Starter 依赖将相关的库和配置打包在一起,开发者只需引入一个 Starter 依赖,就能够快速集成所需的功能,而不必手动管理多个依赖项。
自动配置:Starter 依赖中通常包含了自动配置类,可以根据约定自动配置应用程序所需的组件和功能,简化了配置过程。
约定大于配置:Spring Boot Starter 遵循约定大于配置的原则,提供了一种标准化的方式来组织和使用依赖,降低了集成和配置的复杂度。
自定义的 Spring Boot Starter,可以按照以下步骤进行:
编写自动配置类:编写一个自动配置类,使用 @Configuration 注解标记,并通过条件化注解(如 @ConditionalOnClass)来指定在何种情况下启用自动配置。在自动配置类中,你可以通过 @Bean 注解来定义需要被 Spring 容器管理的 Bean。
创建 spring.factories 文件:创建一个名为 META-INF/spring.factories 的资源文件,在该文件中声明你的自动配置类
提供默认配置:为 Starter 提供默认的配置文件,如 application.properties 或 application.yml,定义一些默认属性值。
编写 Starter 类:创建一个类,继承自 Spring Boot 的 org.springframework.boot.autoconfigure.EnableAutoConfiguration 类,用于启用自动配置。
常用的日志框架(如 Log4j、Logback)提供了丰富的功能和配置选项,包括日志级别控制、日志输出格式定制、日志滚动策略等。它们具有较高的灵活性和可扩展性,适用于各种日志需求。
Slf4j 是一个日志框架的抽象层,它只定义了一组统一的日志接口,不提供具体的日志实现。Slf4j 的主要目的是在应用程序中使用统一的日志接口,并支持根据需要切换底层的日志实现框架。
常用的日志框架提供了丰富的功能和灵活性,适用于各种日志需求。而 Slf4j 则是一个日志框架的抽象层,提供统一的日志接口,并允许根据需要切换底层的日志实现框架。使用 Slf4j 可以减少对具体日志框架的直接依赖,提高代码的灵活性和可维护性
使用 @CrossOrigin 注解:
在控制器类或方法上使用 @CrossOrigin 注解可以实现对特定请求处理跨域访问。例如,在控制器类上添加 @CrossOrigin 注解,可以允许该控制器下所有方法处理跨域请求
全局配置:
另一种方式是在 Spring Boot 应用的配置类中进行全局配置,以允许所有请求处理跨域。可以通过 WebMvcConfigurer 接口的 addCorsMappings 方法来配置全局跨域支持
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true);
}
}
实现异常处理可以通过使用@ControllerAdvice
注解定义全局异常处理器,或者在@Controller
内部使用@ExceptionHandler
注解处理局部异常。常用全局异常处理的方式:
全局异常处理器(Global Exception Handler):
创建一个全局异常处理器类,使用@ControllerAdvice注解标记,然后在类中定义异常处理方法,并使用@ExceptionHandler注解指定处理的异常类型。这样可以统一处理应用中抛出的各种异常。
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred: " + e.getMessage());
}
}
创建一个类,类上增加@SpringBootTest
注解和@RunWith(SpringRunner.class)
,编写方法,在方法上增加@Test
注解
最新版本的 JUnit5 中,不再需要@RunWith(SpringRunner.class)
注解,
在 Spring Boot 中实现定时任务(Scheduled Task)可以通过@Scheduled
注解来实现。
@EnableScheduling 注解
,以启用定时任务支持@Scheduled)
注解,还可以通过fixedRate
、fixedDelay
、initialDelay
、cron
来控制不同的用法 @Scheduled(fixedRate = 5000) // 每隔5秒执行一次
@Scheduled(fixedDelay = 3000) // 上一次任务完成后延迟3秒再执行下一次
@Scheduled(initialDelay = 2000, fixedRate = 5000) // 延迟2秒后首次执行,然后每隔5秒执行一次
@Scheduled(cron = "0 0 12 * * ?") // 每天中午12点执行
File file = new File("src/main/resources/file.txt");
InputStream inputStream = new FileInputStream(file);
需要注意的是,使用该方法需要提供完整的文件路径,因此需要知道文件所在的绝对路径。这种方式在idea上可以运行,但打成jar包是会报文件找不到异常。
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("File.txt");
注意,该方法返回的资源文件路径是相对于类加载器的根路径。因此,对于 resources 目录下的文件,需要在文件名前加上 “ClassPath:” 前缀。例如: “classpath:file.txt”。
InputStream inputStream = getClass().getResourceAsStream("/file.txt");
该方法返回的资源文件路径是相对于当前类的路径。因此,对于 resources 目录下的文件,需要在文件名前加上 “/” 前缀。例如: “/file.txt”。
@Autowired
private ResourceLoader resourceLoader;
Resource resource = resourceLoader.getResource("classpath:file.txt");
InputStream inputStream = resource.getInputStream();
File file = ResourceUtils.getFile("classpath:file.txt");
需要注意的是,该方法只适用于本地文件系统和 JAR 文件。对于 WAR 文件或者其他类型的文件,该方法可能无法正常工作。
@Autowired
private ApplicationContext applicationContext;
public void readResourceFile() throws IOException {
Resource resource = applicationContext.getResource("classpath:file.txt");
InputStream inputStream = resource.getInputStream();
}
@Autowired
private ServletContext servletContext;
public void readResourceFile() throws IOException {
InputStream inputStream = servletContext.getResourceAsStream("/WEB-INF/classes/file.txt");
}
可以使用 Java NIO 中的 Paths 和 Files 类来读取资源文件。该方法需要提供完整的文件路径。
Path path = Paths.get("src/main/resources/file.txt");
InputStream inputStream = Files.newInputStream(path);
需要注意的是,使用该方法需要提供完整的文件路径,因此需要知道文件所在的绝对路径。
可以使用 Spring 提供的 ClassPathResource 类来读取资源文件。该方法需要提供资源文件的相对路径。
ClassPathResource resource = new ClassPathResource("file.txt");
InputStream inputStream = resource.getInputStream();
需要注意的是,ClassPathResource 会在类路径下查找资源文件,因此不需要提供完整的文件路径。
@EnableTransactionManagement
注解,以启用事务管理功能。@Transactional
注解可以应用在方法级别或类级别上,用于标识希望启用事务管理的方法或类。默认情况下,@Transactional
注解只会对受检查异常(RuntimeException 的子类)进行回滚操作,如果需要对所有异常进行回滚,可以使用 rollbackFor 属性。
微服务架构是一种将应用程序拆分为多个小型、独立部署的服务的架构风格。每个微服务都专注于完成特定的业务功能,并通过轻量级的通信机制(比如 RESTful API)进行交互。这些微服务可以被独立地开发、部署和扩展,从而提高系统的灵活性、可维护性和可伸缩性。
与传统的单体架构相比,微服务架构具有以下几个显著的区别:
模块化:
单体架构通常是将整个应用程序作为一个单独的模块进行开发、部署和维护,所有功能都集中在同一个代码库中。
微服务架构将应用程序拆分为多个小型的服务,每个服务负责一个特定的业务功能,这样可以更好地实现模块化开发和团队自治。
独立部署:
在单体架构中,所有功能模块共享同一个部署单元,一次部署会影响整个应用程序。
微服务架构中,每个微服务都是独立部署的,可以独立更新和扩展,降低了对整个系统的影响范围。
技术多样性:
在单体架构中,通常会使用相同的技术栈和开发框架,不容易引入新的技术或语言。
微服务架构允许每个微服务选择适合自身需求的最佳技术栈,从而更好地解决特定问题。
弹性伸缩:
微服务架构可以根据需求对每个微服务进行独立的水平扩展,提高系统的弹性和可伸缩性。
在单体架构中,只能对整个应用程序进行水平扩展,无法针对特定功能进行精确调整。
复杂性:
微服务架构引入了服务间通信、服务发现、负载均衡等新的挑战,增加了系统的复杂性和运维管理成本。
单体架构相对来说更加简单直观,适用于小型项目或固定功能的应用。
Spring Cloud是基于Spring Boot的微服务架构开发工具,它提供了一系列开发工具和库,用于快速构建分布式系统中的微服务架构。Spring Cloud为开发人员提供了一套简单易用的组件,帮助他们解决微服务架构中常见的问题。
服务注册与发现:
Spring Cloud集成了Netflix Eureka、Consul等服务注册中心,可以帮助微服务在动态环境下实现自动化的服务注册与发现,使得服务之间可以更加方便地通信。
负载均衡:
通过集成Netflix Ribbon等负载均衡组件,Spring Cloud可以实现对服务请求的负载均衡,提高系统的性能和可靠性。
断路器:
通过集成Netflix Hystrix等断路器组件,Spring Cloud可以实现服务之间的故障隔离和容错处理,防止因为某个服务故障导致整个系统的瘫痪。
网关:
Spring Cloud提供了Netflix Zuul等API网关组件,可以帮助开发人员统一管理和路由微服务的请求,提高系统的安全性和稳定性。
服务间调用:
Spring Cloud提供了Feign、OpenFeign实现服务间的通信和调用,简化了开发过程,提高了系统的可维护性和可扩展性。
配置管理:
Spring Cloud Config可以帮助开发人员集中管理应用程序的配置信息,实现配置的集中化管理和动态刷新,避免了修改代码重新部署的麻烦。
分布式跟踪:
通过集成Zipkin等分布式跟踪组件,Spring Cloud可以帮助开发人员跟踪和监控微服务之间的调用链路,帮助排查和解决分布式系统中的问题。
规模和粒度:
SOA强调的是面向服务的架构,服务可以是较为庞大的、粗粒度的服务,通常是基于企业的业务功能划分而来的。
微服务则更强调将应用系统划分为多个小型、细粒度的服务,每个微服务关注一个特定的业务领域,并且可以独立部署和扩展。
通信方式:
在SOA中,服务之间的通信通常采用SOAP(Simple Object Access Protocol)或者基于消息队列的方式进行。
微服务架构更倾向于采用基于HTTP的轻量级通信机制,比如RESTful API。
数据一致性:
在SOA中,由于服务粒度较大,可能会存在多个服务共享同一数据库,因此需要注意数据一致性的问题。
微服务架构鼓励每个微服务拥有自己的数据库,从而避免了数据一致性的问题。
部署和管理:
SOA服务通常被部署在统一的应用服务器上,并由统一的管理框架进行管理。
微服务则更加强调独立部署和自治性,每个微服务都可以独立部署并运行,具有更高的灵活性和可替换性。
CAP 理论是分布式系统领域的一个重要理论概念。CAP 理论指出,一个分布式系统不可能同时满足以下三个特性:一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)。
一致性(Consistency):所有节点在同一时间点看到的数据是相同的,即在进行写操作后,所有节点的数据应该保持一致。
可用性(Availability):系统能够保证每个请求都能够得到响应,即系统对外提供正常的服务并能够处理客户端的请求。
分区容忍性(Partition Tolerance):系统能够在网络发生分区的情况下仍然能够继续工作,即系统能够处理因网络故障而导致的节点之间通信失败的情况。
分布式系统在面临网络分区时,必须在一致性(Consistency)和可用性(Availability)之间做出权衡选择,即要么保证数据的一致性但牺牲一定的可用性,要么保证可用性但可能出现数据的不一致性。分区容忍性(Partition Tolerance)是必须要满足的基本条件,因为网络分区在分布式系统中是常见且不可避免的。
在实际设计分布式系统时,根据具体的业务需求和系统特点,需要权衡考虑如何在一致性、可用性和分区容忍性之间进行取舍,以满足系统的实际需求。 CAP 理论帮助开发人员更好地理解分布式系统设计中的抉择,并指导其进行合理的系统设计和架构选择。
Eureka是Netflix开源的一种服务注册与发现组件,用于在基于微服务架构的应用中实现服务的注册、发现和故障转移。它允许微服务在动态环境下注册自身,并能够发现其他服务的位置。
Eureka的核心组件包括
Eureka Server:
Eureka Server是服务注册中心的组件,负责管理服务的注册和发现。当一个微服务启动时,它会向Eureka Server注册自己的信息(比如服务名、IP地址、端口号等),并周期性地向Eureka Server发送心跳以表明自己的健康状态。同时,其他微服务可以通过Eureka Server查询到注册的服务信息,从而实现服务之间的通信。
Eureka Client:
Eureka Client是服务提供者和消费者的组件,用于注册自身并发现其他服务。当一个微服务作为客户端运行时,它会向Eureka Server注册自己,并且从Eureka Server获取其他服务的信息来进行调用。同时,Eureka Client会定期从Eureka Server获取注册表信息,并缓存在本地,以便在Eureka Server不可用时仍然能够提供服务。
服务注册:
当一个微服务启动时,它会向Eureka Server发送注册请求,将自身的信息(比如服务名、IP地址、端口号等)注册到Eureka Server上。
心跳与健康检查:
注册完成后,微服务会定期向Eureka Server发送心跳,以表明自己的健康状态。如果Eureka Server在一定时间内没有收到某个微服务的心跳,就会将该服务实例从注册表中移除,认为该服务不再可用。
服务发现:
其他微服务可以通过Eureka Server查询到已注册的服务信息,包括服务名、IP地址、端口号等。这样,微服务之间就可以相互发现并调用对方提供的服务。
负载均衡:
Eureka Server可以根据注册表中的服务实例信息,实现负载均衡功能,将请求分发到多个具体的服务实例上。
高可用性:
Eureka Server支持构建多节点的集群,在多个Eureka Server节点之间相互注册以实现高可用性,即使其中一个节点出现故障,其他节点仍然可以提供服务注册与发现的功能。
设计目的:
Eureka 是专门为基于微服务架构设计的服务注册与发现组件,着重于服务的动态注册、发现和负载均衡。
ZooKeeper 是一个分布式协调服务,提供的功能包括服务注册、协调、配置管理等多方面,适用于更广泛的分布式系统场景。
一致性协议:
Eureka 使用 eventual consistency(最终一致性)的策略,即服务注册信息可能存在一定时间的不一致,但最终会达到一致状态。
ZooKeeper 使用 ZAB(ZooKeeper Atomic Broadcast)协议,保证了强一致性,即数据变更会被所有节点同步,读操作能够获得最新的数据。
数据存储方式:
Eureka 使用内存存储注册信息,并且默认情况下不支持持久化,适用于相对较小规模的微服务架构。
ZooKeeper 使用磁盘存储数据,支持持久化存储,并且能够存储更复杂的数据结构,适用于更复杂的分布式系统场景。
生态系统支持:
Eureka 是 Netflix 提供的开源项目,与 Netflix 的其他组件(如 Ribbon、Hystrix 等)集成良好。
ZooKeeper 是 Apache 的顶级项目,有较大的社区支持,同时也被许多公司广泛应用于生产环境。
Eureka 内部实现高可用性的关键在于其自身的集群化和故障处理机制。
Eureka Server 集群:
Eureka 通过搭建多个 Eureka Server 实例组成集群的方式来实现高可用性。这些实例之间互为对等,彼此相互注册,共享服务注册信息。
客户端应用可以向任何一个 Eureka Server 实例注册服务或查询服务,这样就可以实现负载均衡和故障转移。
心跳和健康检查:
Eureka Server 会周期性地向集群中的其他节点发送心跳消息,以确保节点之间的存活状态。
同时,Eureka Server 会对注册在其上的服务进行健康检查,及时剔除不健康的服务实例。
自我保护机制:
Eureka Server 在面临网络分区或异常情况下,会进入自我保护模式,不轻易剔除健康的服务实例,以免造成更大的系统压力。
自我保护机制可以防止因网络问题导致整个集群中的实例被错误剔除,确保系统的稳定性。
数据同步和复制:
Eureka Server 可以配置跨区域或多数据中心的数据同步和复制机制,确保不同地域的 Eureka Server 数据一致性,提高整体的可用性。
负载均衡:
Eureka Server 集群可以通过负载均衡器进行流量分发,保证请求能够平衡地分布到各个节点,避免单点故障。
ZooKeeper 是一个开源的分布式协调服务,提供高性能、高可用、有序的数据存储和访问服务。它主要用于解决分布式系统中的一致性问题,为分布式应用提供可靠的协调机制。
ZooKeeper 的主要作用包括:
它的数据模型类似于一个分层的文件系统,但是它实际上是一种基于内存的树形数据结构。ZooKeeper 中的数据被组织成类似目录结构的节点(node),每个节点可以存储少量的数据。
具体来说,ZooKeeper 的数据结构是一颗树,每个节点都可以存储一个小于 1MB 大小的数据。每个节点都有一个路径(path),类似于文件系统中的路径,以 “/” 开头。节点之间的关系是父子关系,类似于文件系统中文件与文件夹的关系。
ZooKeeper 如何维护这些数据呢?ZooKeeper 使用 ZAB(ZooKeeper Atomic Broadcast)协议来保证数据的一致性和可靠性。当客户端向 ZooKeeper 发送写请求时,ZooKeeper 集群会将该请求转发给一个 Leader 节点,Leader 节点负责协调集群中的其他节点进行数据写入操作。只有当多数节点都写入成功后,写操作才会被提交,这样可以确保数据的一致性。
此外,ZooKeeper 还会在内存中维护一个数据快照(snapshot),用于快速恢复数据。ZooKeeper 会定期将内存中的数据快照持久化到磁盘,以防止数据丢失。当 ZooKeeper 重启时,它可以通过加载最近的数据快照来恢复数据。
是 ZooKeeper 中用于实现一致性和可靠性的关键协议。ZAB 协议主要用于保证分布式系统中的数据一致性,并确保在发生故障或节点重启时能够恢复数据的正确性。
主要包含以下几个功能
Leader 选举:ZAB 协议会在 ZooKeeper 集群中选择一个 Leader 节点来负责处理客户端的写请求。Leader 负责接收客户端的写请求,并将这些请求广播给其他节点。在初始启动时,ZooKeeper 集群中没有 Leader,因此需要进行 Leader 选举。
消息广播:一旦选出了 Leader,它可以接收来自客户端的写请求,并将这些请求作为提案(proposal)广播给集群中的其他节点。其他节点会接收并处理这些提案,然后向 Leader 发送确认信息。
多数派提交:ZAB 协议要求大部分节点都要确认某个提案,即超过半数的节点都要确认某个写操作后,Leader 才会将该提案确定为已提交。这样可以确保在网络分区或节点故障的情况下仍能保持数据的一致性。
事务日志:一旦 Leader 确认了一个提案,它会将该提案写入事务日志(transaction log)。这个事务日志会被持久化到磁盘上,以便在节点重启时能够恢复数据。
快照和状态同步:ZAB 协议还包括了快照机制,用于定期将内存中的数据快照持久化到磁盘,以便在节点重启时快速恢复数据。同时,ZAB 协议还包括了状态同步机制,用于在节点重启后与 Leader 进行状态同步,以确保数据的正确性。
ZooKeeper 中 Leader 选举的基本过程:
初始化阶段:在初始启动时,所有的节点都处于初始状态,没有 Leader。此时,每个节点都会尝试成为 Leader。
投票选举:每个节点开始向其他节点发送选举通知,并宣布自己的候选身份。其他节点收到通知后会进行投票,根据一定的规则比较各个候选者的 zxid(事务 ID)来决定是否接受该节点的领导权。
选票比较:如果某个节点的 zxid 最大,并且得到了超过半数节点的赞成票,那么该节点就会成为 Leader。这样就确保了在网络分区或节点故障的情况下,集群可以快速选出新的 Leader,并继续处理客户端的请求。
Leader 宣告:一旦某个节点成为 Leader,它会向其他节点发送消息,宣称自己已成为 Leader,并开始处理客户端的写请求,同时将写请求广播给其他节点。
ZooKeeper 的 CAP 理论指的是分布式系统中的三个基本属性:一致性(Consistency)、可用性(Availability)和分区容忍性(Partition Tolerance)。根据 CAP 理论,一个分布式系统不可能同时满足这三个属性,最多只能同时满足其中两个。
ZooKeeper 在设计上选择保证一致性和分区容忍性,因此在面对网络分区时会牺牲一定的可用性。这种设计使得 ZooKeeper 能够成为一个强一致性的分布式协调服务,适用于多种分布式系统场景。
ZooKeeper 实现分布式锁的基本原理:
创建临时顺序节点:当一个客户端需要获取锁时,它在 ZooKeeper 中创建一个临时顺序节点(ephemeral sequential node),节点的路径包含一个有序的序号。这样可以确保每个客户端都会创建一个唯一的节点,并且节点会按照创建的顺序排列。
获取锁:客户端创建完临时顺序节点后,它会获取当前 ZooKeeper 中所有临时顺序节点的列表,并判断自己创建的节点是否是列表中序号最小的节点。如果是最小节点,则表示该客户端成功获取了锁;否则,客户端会监听自己前一个节点的变化(Watch),并等待前一个节点被删除或释放锁。
释放锁:当客户端不再需要锁时,它会删除自己创建的临时顺序节点。这会触发 ZooKeeper 发送通知给等待的客户端,让它们重新检查是否可以获取锁。
ZooKeeper 的 Watcher 机制是一种事件通知机制,允许客户端在数据变化或状态变化时得到通知。客户端可以注册 Watcher 监听指定的节点,当节点被创建、删除或数据被更新时,ZooKeeper 会向客户端发送通知,客户端收到通知后可以做出相应的处理。
Watcher 机制的作用包括:
实时通知:客户端可以通过 Watcher 机制实时获取数据变化的通知,而不需要轮询检查节点的状态。
简化开发:Watcher 可以简化分布式系统中节点间的协调和同步,避免手动实现复杂的通知机制。
减少网络开销:相比频繁的轮询检查,Watcher 可以减少网络开销,提高系统性能。
支持分布式协同:Watcher 可以帮助客户端实现分布式锁、选举等机制,保证分布式系统的一致性和可靠性。
在 ZooKeeper 中,主要有以下几种节点类型和数据节点的特点:
持久节点(Persistent Nodes):
持久节点在创建后会一直存在,直到显式删除。
当客户端与 ZooKeeper 断开连接后,持久节点不会被自动删除。
临时节点(Ephemeral Nodes):
临时节点在创建它的客户端会话失效或断开连接时被自动删除。
临时节点通常用于标识临时性状态或连接,例如临时性的服务注册信息。
顺序节点(Sequential Nodes):
顺序节点在节点路径后会附加一个递增的序号,确保节点在创建时有序排列。
顺序节点的序号是 ZooKeeper 服务器自动分配的,客户端无法指定。
临时顺序节点(Ephemeral Sequential Nodes):
临时顺序节点结合了临时节点和顺序节点的特点,即在客户端会话失效时会被自动删除,并且节点路径后会附加递增的序号。
数据节点的特点包括:
轻量级数据存储:ZooKeeper 用于存储小量的关键元数据和配置信息,每个数据节点的大小通常受到限制,适合存储少量的状态信息。
高可用性:ZooKeeper 数据存储是基于内存的,并通过多副本复制实现高可用性,可以提供可靠的数据访问和一致性保证。
数据监听机制:客户端可以通过 Watcher 机制监听数据节点的变化,一旦数据节点发生变化,客户端会收到通知,实现实时的数据同步和通知。
层级命名空间:ZooKeeper 的数据节点是以层级命名空间的方式组织的,类似于文件系统的目录结构,方便管理和查找数据节点。
原子性(Atomicity):
ZooKeeper使用事务(Transaction)机制来确保操作的原子性,在一个事务中,一组操作要么全部成功,要么全部失败,不会存在部分成功的情况。如果在一个事务中的某个操作失败,整个事务将被回滚,以保持数据的一致性。
例如,创建节点、更新节点数据等操作都是原子性的,要么操作成功,要么失败,不会出现中间状态。
一致性(Consistency):
ZooKeeper 提供了强一致性,即在任意时刻,所有客户端看到的数据视图都是一致的。
当一个客户端对数据进行修改后,其他客户端会立即看到最新的数据状态,确保了数据的一致性。
隔离性(Isolation):
ZooKeeper 使用递增的版本号(zxid)和序列号等机制来实现客户端之间的操作隔离,即多个客户端并发操作时,它们彼此之间互不影响。
每个数据节点都有一个版本号,客户端在更新数据时需要指定该节点的当前版本号,如果版本号不匹配,则更新操作会失败,避免了并发操作的冲突。
持久性(Durability):
ZooKeeper 确保数据的持久性,即一旦数据被写入成功,数据将被持久化存储,不会因服务器故障或重启而丢失。
数据节点的持久性保证了系统的可靠性和数据的稳定性。
ZooKeeper 使用多副本复制(Replication)机制来处理节点的故障恢复,确保系统的可用性和数据的持久性。具体来说,ZooKeeper 采取以下措施来处理节点的故障恢复:
Leader 选举:
在 ZooKeeper 集群中,通过选举机制选出一个节点作为 Leader,负责处理客户端的请求和协调集群中的其他节点。
如果当前的 Leader 节点发生故障,ZooKeeper 会自动进行 Leader 选举,选出新的 Leader 节点来接管工作,确保系统的连续性。
多副本复制:
ZooKeeper 将数据存储在多个节点上,并使用多副本复制机制来保证数据的可靠性和持久性。通常情况下,每个数据节点都会有多个副本分布在不同的服务器上。
当某个节点发生故障时,ZooKeeper 会从其他副本中选择一个新的 Leader,并通过数据同步机制将数据恢复到故障节点上,确保数据的完整性和可用性。
数据恢复:
当一个节点因为故障而下线时,ZooKeeper 会自动触发数据恢复机制,尝试将该节点上的数据同步到其他正常运行的节点上,以确保数据的一致性和完整性。
一旦节点恢复正常,ZooKeeper 会将其重新加入到集群中,并进行数据同步,使其恢复到正常的工作状态。
会话超时是指客户端与 ZooKeeper 服务器之间的会话在一定时间内没有收到心跳信号而被认定为超时失效
会话超时具有以下作用:
心跳检测:
客户端与 ZooKeeper 服务器之间会周期性地进行心跳通信,以维持会话的活跃状态。
如果服务器在一定时间内没有收到客户端的心跳信号,就会认为客户端可能出现了故障或断连,将其会话标记为超时失效。
故障检测和恢复:
当客户端的会话超时被触发后,ZooKeeper 服务器会将该客户端标记为已断开连接,进而触发相应的故障检测和恢复机制。
断开连接的客户端可能是因为网络故障、客户端崩溃等原因,ZooKeeper 会通过重新选举 Leader 和数据同步等操作来处理这些故障情况,确保系统的可用性。
保持数据的一致性:
会话超时对于保持数据的一致性非常重要。当一个客户端的会话超时失效后,ZooKeeper 会将其之前创建的临时节点和所持有的锁等资源删除或释放,避免了因为客户端故障导致资源未被正确清理的问题。
这样可以确保分布式系统中的数据和资源状态始终保持一致,避免悬挂的临时节点和死锁等问题。
Consul 是一种开源的服务网格和服务发现工具。它提供了一种简单、高效的方式来实现服务发现、健康检查、动态配置和分布式一致性等功能,帮助构建可靠的分布式系统。
Consul 的主要功能包括:
服务发现:Consul 允许服务注册和发现,使得各个微服务能够动态地找到彼此,从而实现服务之间的通信和协作。
健康检查:Consul 提供健康检查机制,定期检查服务的健康状态,并及时发现并处理不健康的服务实例,确保系统的稳定性。
KV 存储:Consul 提供 Key-Value 存储功能,可以用来存储配置信息、控制参数等,实现动态配置管理。
分布式锁:Consul 支持分布式锁的实现,用于协调分布式系统中的并发访问控制,避免竞争条件和数据冲突。
多数据中心支持:Consul 能够跨多个数据中心进行服务发现和通信,支持构建全球性的分布式系统。
Consul Connect:Consul 提供了 Consul Connect 功能,用于安全地管理服务之间的通信流量,实现服务之间的加密、认证和授权。
Consul Server(Consul 服务器):
Consul Server 用于存储集群的状态信息、执行一致性协议,并处理来自客户端的查询请求。
Consul 集群通常包含多个 Consul Server,通过 Raft 协议实现一致性,选举出 Leader 节点负责处理写操作。
Consul Client(Consul 客户端):
Consul 客户端是与 Consul Server 进行通信的代理,用于注册服务、进行健康检查、查询服务信息等操作。
客户端会定期同步服务信息和健康状态,并将查询请求转发给 Consul Server 处理。
Service Catalog(服务目录):
服务目录用于存储已注册的服务信息,包括服务名称、IP 地址、端口号等。
通过服务目录,Consul 客户端可以轻松地发现和访问其他服务。
Health Checking(健康检查):
Consul 提供了健康检查功能,用于定期检查服务的健康状态,包括 HTTP、TCP、TLS 等各种类型的健康检查方式。
健康检查结果会影响服务的可用性,不健康的服务实例会被标记并从服务发现中移除。
Key-Value Store(键值存储):
Consul 提供了 Key-Value 存储功能,用于存储配置信息、控制参数等。
应用程序可以通过 Key-Value 存储动态获取配置,实现动态配置管理。
Consul Connect:
Consul Connect 是 Consul 的一部分,用于安全地管理服务之间的通信流量,提供加密、认证和授权功能。
通过 Consul Connect,服务之间的通信可以更加安全可靠。
Raft 协议是一种分布式一致性协议,用于在分布式系统中实现状态机复制。Raft 协议通过领导者(Leader)选举、日志复制和安全性机制来确保分布式系统中各个节点之间的一致性。下面是 Raft 协议的一些核心概念:
Leader 选举:Raft 将节点划分为 Leader、Follower 和 Candidate 三种角色。在初始状态下,所有节点都是 Follower。如果 Follower 超时没有收到 Leader 的心跳,则会转变为 Candidate,并开始一轮选举过程。Candidate 会向其他节点发送投票请求,获得多数票的节点将成为新的 Leader。
日志复制:Leader 负责接收客户端请求并将操作追加到日志中,然后将日志条目发送给 Followers 进行复制。一旦大多数节点确认已复制该日志条目,Leader 就可以提交该操作并告知 Followers 执行。
安全性:Raft 通过定期选举、日志复制和持久化等机制来确保系统的安全性和一致性。每个节点都会定期向其他节点发送心跳消息以维持连接,确保系统正常运行。
服务注册:
当一个服务启动时,它会向 Consul 注册自己的服务实例信息,包括服务名称、IP 地址、端口号等。服务注册可以由 Consul 客户端或者服务网格代理执行。
注册后的服务信息会被存储在 Consul 的服务目录中,用于服务发现和健康检查。
服务发现:
其他服务想要调用某个服务时,它们可以向 Consul 发送查询请求,请求特定服务名称的服务实例信息。
Consul 会返回与请求匹配的服务实例信息,包括 IP 地址和端口号,使得客户端能够直接访问所需的服务。
健康检查:
Consul 客户端会定期执行健康检查,以确保注册的服务实例的健康状态。健康检查可以包括 HTTP、TCP、TLS 等多种方式,用于检测服务是否可用。
如果服务实例被标记为不健康,Consul 将不再返回该实例的信息,避免客户端访问到不可用的服务。
故障转移:
如果一个服务实例变得不可用,Consul 会自动将其从服务目录中移除,从而避免客户端继续向其发送请求。
同时,Consul 还支持在服务实例故障时自动触发故障转移,将流量转移到其他健康的服务实例上,确保系统的高可用性。
DNS 服务发现:
Consul 提供了 DNS 接口,通过域名解析的方式来进行服务发现。客户端可以直接使用域名来访问服务,而无需显式地与 Consul API 进行交互。
通过 DNS 服务发现,可以使得现有的应用程序更容易地迁移到基于 Consul 的服务发现机制上。
HTTP API
Consul 提供了丰富的 HTTP API,允许开发者使用 RESTful 接口来查询和管理服务实例信息。通过调用这些 API,可以实现自动化的服务发现和管理。
RPC 接口:
Consul 还提供了支持多种编程语言的 RPC 接口,如 gRPC、Thrift、Protocol Buffers 等。这些接口可以与现有的 RPC 框架结合,使得服务发现过程更加方便和透明。
Consul Template:
Consul Template 是 Consul 的一个附加工具,它可以监视 Consul 中的服务信息变化,并根据模板生成对应的配置文件。这样可以实现动态更新应用程序的配置信息,从而无缝地适应服务实例的变化。
Consul 中的 Key-Value 存储功能是指 Consul 提供的一种轻量级的分布式键值存储服务,允许用户在集群中存储和检索简单的键值对数据。Key-Value 存储在 Consul 中被广泛用于配置管理、动态参数设置、特性开关等场景,为分布式系统提供了便捷的配置管理工具。Consul提供了RESTful的HTTP API接口,你可以使用类似cURL、Postman这样的工具或者编程语言的HTTP客户端来进行访问。
配置管理:
开发人员可以将应用程序的配置信息存储在 Consul 的 Key-Value 存储中,如数据库连接信息、日志级别、调试标志等。这些配置可被动态更新,无需重启应用程序即可生效。
动态参数设置:
Key-Value 存储可以用于存储系统的参数设置,如超时时间、重试次数等。通过 Consul API 或者 Consul Template,可以实现参数的动态调整和更新。
特性开关:
通过在 Key-Value 存储中设置特性开关的值,可以实现系统中特定功能的开启或关闭。这种方式可以帮助开发人员进行 A/B 测试、灰度发布等功能控制。
服务注册元数据:
除了配置信息,Key-Value 存储还可以存储与服务注册相关的元数据,如服务版本、环境信息等。这些元数据可以帮助实现更灵活的服务发现和路由策略。
监控和告警配置:
将监控和告警配置信息存储在 Consul 的 Key-Value 存储中,可以实现监控系统的配置自动化管理,方便对监控报警规则进行调整。
Consul实现分布式锁的机制通常使用基于Key-Value存储和Session的方式来实现。以下是Consul如何实现分布式锁的一般步骤:
创建锁:
通过向Consul的Key-Value存储中写入一个特定的键值对来创建一个锁。这个键通常表示一个资源或者一个任务,而值可以是任意值。
尝试获取锁:
当一个客户端(服务)想要获取这个锁时,它会尝试在Consul中创建一个与该键关联的Session。这个Session由Consul管理,可以设置过期时间。
竞争获取锁:
多个客户端同时尝试获取锁时,只有一个客户端的Session能够成功创建,并且这个客户端获得了锁。其它客户端的Session创建可能会失败,它们将不拥有锁。
持有锁:
客户端成功获取锁后,在Consul中的Session将保持活动状态,确保锁的有效性。只有持有锁的客户端才能对资源进行操作。
释放锁:
当客户端完成任务或者释放资源时,它会主动释放锁,即删除相应的键值对或者Session。这样其他客户端就有机会尝试获取锁了。
Consul Connect 是 Consul 提供的一种服务网格解决方案,用于管理和安全地连接服务之间的通信。它有以下主要功能和优势:
安全的服务通信:
Consul Connect 提供了基于TLS的加密通信机制,确保服务之间的通信是安全可靠的。它通过自动化证书颁发和轮换,简化了证书管理的复杂性,同时可以有效地防止窃听和篡改。
服务授权:
Consul Connect 支持细粒度的服务授权,可以根据服务的身份和属性对通信进行灵活的访问控制。这样可以确保只有经过授权的服务才能相互通信,提高了系统的安全性。
流量控制:
通过 Consul Connect,你可以轻松地实现流量控制和负载均衡,以确保服务之间的通信是稳定和可靠的。它支持多种代理模式,可以根据需要进行灵活配置。
动态服务发现:
Consul Connect 集成了 Consul 的服务发现功能,可以自动发现并连接服务,并提供了统一的服务注册表和命名解析。这样可以降低服务之间的耦合度,提高了系统的可扩展性和灵活性。
易于部署和管理:
Consul Connect 提供了丰富的命令行工具和 API,可以方便地部署和管理服务网格。同时,它与 Consul 的其他功能如健康检查、故障恢复等紧密集成,使得整个系统更加稳定和可靠。
要在 Consul 中实现跨数据中心的服务发现和通信,可以使用 Consul 的 WAN 功能(Wide Area Network)。
启用 WAN 功能:
首先,在 Consul 的配置文件中启用 WAN 功能。确保每个数据中心的 Consul agent 都配置了正确的 WAN 地址,并且能够相互通信。
确保不同数据中心的 Consul 集群能够互相通信。这通常涉及到配置网络规则和防火墙规则,以允许数据中心之间的通信。
在数据中心之间配置服务同义词(Service Aliases),使得不同数据中心的服务可以通过相同的名称进行访问。这样可以简化跨数据中心服务发现的配置和管理。
为了确保跨数据中心通信的安全性,可以配置 Consul 的 WAN 加密功能,使用 TLS 加密数据传输。这样可以有效地保护数据中心之间的通信安全。
在各个数据中心的服务中注册服务信息,并使用 Consul 的服务发现功能来发现跨数据中心的服务。通过 Consul 的 DNS 接口或 HTTP API,可以轻松地在不同数据中心中查找和访问服务。
确保在跨数据中心通信时配置了适当的健康检查机制,以及故障恢复策略。这样可以及时发现并处理服务不可用的情况,保证系统的稳定性和可靠性。
服务发现与注册:
Nacos 提供了服务注册和发现功能,服务提供者可以向 Nacos 注册自己的服务,并且消费者可以通过 Nacos 发现并调用这些服务,实现服务之间的动态发现和通信。
动态配置管理:
Nacos 可以集中管理应用程序的配置信息,支持动态更新配置,应用程序可以实时获取最新的配置信息,无需重启服务即可生效。
服务健康检查:
Nacos 提供了健康检查机制,可以定期检查服务的健康状态,并及时发现故障节点,确保服务的稳定性和可靠性。
动态 DNS:
Nacos 支持动态 DNS 解析,可以根据服务名称动态解析到具体的服务实例地址,简化服务调用过程。
流量管理:
Nacos 提供了流量管理功能,可以实现流量的动态调度、限流和熔断,保护系统免受突发流量影响。
多环境支持:
Nacos 支持多环境的管理,可以根据不同的环境(如开发、测试、生产)管理不同的配置信息,便于应用程序在不同环境中部署和运行。
Nacos 注册中心具有以下主要功能:
服务注册:
服务提供方可以通过 Nacos 注册中心将自己提供的服务注册到注册中心,包括服务名称、IP地址、端口号等信息。
服务发现:
服务消费方可以通过 Nacos 注册中心查询注册的服务列表,以便发现可用的服务实例,从而实现服务之间的通信与调用。
健康检查:
Nacos 注册中心会定期检查注册的服务实例的健康状态,如果某个服务实例出现故障或不可用,注册中心会及时发现并标记为不健康,确保服务的可靠性。
负载均衡:
Nacos 注册中心可以根据服务实例的健康状态和负载情况进行负载均衡,确保请求被均匀地分发到各个健康的服务实例上。
集群管理:
Nacos 注册中心支持集群部署,可以横向扩展以应对高并发和大规模的服务注册与发现需求,提高系统的可伸缩性和稳定性。
动态配置:
Nacos 注册中心还提供了动态配置管理功能,使得服务的配置信息可以动态更新,无需重启服务即可生效,方便应用程序灵活调整配置。
Nacos 配置中心具有以下主要功能:
动态配置管理:
Nacos配置中心允许用户集中管理应用程序的配置信息,包括数据库连接信息、日志级别、缓存配置等,这些配置信息可以在配置中心中进行统一管理。
配置分组:
Nacos配置中心支持将配置信息进行分组管理,可以按照不同的环境(如开发、测试、生产)或不同的应用程序进行分组,方便进行管理和区分。
动态更新配置:
配置中心允许用户实时更新配置信息,应用程序可以动态获取最新的配置信息,无需重启服务即可生效,提高了配置的灵活性和响应速度。
配置监听:
Nacos配置中心支持配置监听功能,应用程序可以注册配置监听器,一旦配置信息发生变化,配置中心会通知应用程序,从而实现配置的动态加载。
版本管理:
配置中心支持配置信息的版本管理,用户可以查看历史配置信息,并进行回滚操作,确保配置信息的安全性和可追溯性。
权限控制:
Nacos配置中心提供了权限控制机制,可以对不同用户或角色设置不同的权限,保障配置信息的安全性和隐私性。
在 Nacos 中,命名空间(Namespace)是用来隔离和管理配置信息的逻辑空间。每个命名空间都可以包含一组配置信息,并且可以单独进行管理和控制。命名空间为用户提供了一个独立的配置环境,可以在不同的命名空间中管理不同的配置信息,避免配置信息混乱和冲突。
配置隔离:
不同的应用程序或不同的环境可以使用不同的命名空间,通过命名空间的隔离,确保配置信息之间相互独立,避免配置信息的混乱和冲突。
权限控制:
命名空间可以设置不同的权限控制,可以对不同用户或角色进行授权,限制其对配置信息的访问和修改权限,保障配置信息的安全性。
版本管理:
每个命名空间都可以独立管理配置信息的版本,用户可以查看历史配置信息,并进行回滚操作,确保配置信息的可追溯性和安全性。
环境切换:
使用命名空间可以方便地进行不同环境(如开发、测试、生产)之间的配置切换,使得应用程序在不同环境中能够快速部署并加载相应的配置信息。
服务注册:
Nacos 的注册中心实现服务注册和发现的关键在于其内部的数据存储机制和通信协议。注册中心通过高效的数据存储和管理,实现了快速的服务注册和发现功能;同时通过与服务实例的心跳检测,保证了注册中心与服务实例之间的实时性和可靠性。
Nacos 使用分布式存储技术来存储服务注册信息、配置信息等数据。这使得 Nacos 具有良好的横向扩展性和高可用性,能够应对大规模数据存储和高并发访问的需求。同时Nacos 基于一致性协议(例如 Paxos 或 Raft)来保证数据的一致性和可靠性。这确保了注册中心存储的数据在各个节点之间的同步和一致,避免了数据不一致性和丢失的问题。
Nacos 会定期向服务实例发送心跳检测请求,以确保服务实例的健康状态。如果某个服务实例长时间未响应心跳检测,注册中心会将其标记为不可用,从而避免服务调用者调用到不健康的实例。同时Nacos 可以通过故障转移和负载均衡等机制,确保服务实例的健康状态和负载均衡。当某个实例出现故障时,注册中心会自动将请求转发给其他健康的实例,保证了系统的稳定性和可靠性。
当服务启动时,服务会向 Nacos 注册中心订阅所需的配置信息,Nacos 注册中心会将对应的配置信息发送给服务。同时,服务可以注册监听器,实时监测配置的变化情况。这样,一旦配置发生变化,Nacos 注册中心就会通知相应的服务,并更新配置信息,而服务无需重启即可应用新的配置。
Nacos 提供了 Web 控制台和 API 接口,用户可以通过这些界面来管理配置信息。用户可以在控制台上添加、修改、删除配置,并设置配置的数据类型、格式等,也可以通过 API 接口进行批量操作和自动化管理。
Nacos 还支持配置的分组和命名空间管理,用户可以对配置进行分类和归档,方便管理和查找。同时,Nacos 提供了配置的版本管理功能,用户可以查看历史配置记录,进行回滚操作,确保配置的可追溯性和稳定性。
nacos支持以下几种方式的存储
Nacos 支持将数据持久化到文件系统中,这种方式简单直接,适合小规模部署和测试环境使用。
Nacos 支持将数据持久化到关系型数据库(如 MySQL、Oracle 等)中是一种常见且成熟的持久化方式。
Nacos 也支持将数据持久化到 NoSQL 数据库中,比如使用 Redis、Elasticsearch 等作为后端存储。
这些不同的数据持久化存储方式之间的区别主要体现在以下几个方面:
性能:NoSQL 数据库通常具有更高的性能和横向扩展能力,适合处理大规模数据和高并发访问;而关系型数据库在事务处理和一致性方面表现更为出色。
一致性:关系型数据库通常提供强一致性的特性,而 NoSQL 数据库在某些情况下可能会提供最终一致性或者柔性事务的特性。
可靠性:关系型数据库通常具有成熟的备份恢复机制,能够保证数据的可靠性和持久化;NoSQL 数据库的可靠性取决于具体的实现和配置方式。
横向扩展:NoSQL 数据库通常更容易实现横向扩展,可以方便地应对大规模数据和高并发访问的需求;关系型数据库在横向扩展方面相对较为复杂。
Nacos 的配置推送原理主要基于长连接和监听器机制。当客户端向 Nacos 注册配置监听器后,Nacos 会在配置发生变化时通知客户端,从而实现配置的即时更新。
Nacos 的配置推送原理包括以下几个关键步骤:
注册配置监听器:
客户端通过 Nacos 提供的 API 向 Nacos 注册配置监听器,指定需要监听的配置信息的 Group、DataId 和 Namespace 等参数。客户端可以通过 SDK 或者 Nacos 的 REST API 来进行注册。
长连接:
Nacos 客户端与 Nacos 服务器之间建立长连接,以便及时接收配置变更的通知。这种长连接通常基于 WebSocket 或者长轮询等技术实现,保持客户端和服务器的实时通信。
配置变更通知:
当指定的配置信息发生变化时,Nacos 服务器会向注册了监听器的客户端发送配置变更通知。这可以是针对单个客户端的通知,也可以是广播给所有订阅了相同配置的客户端。
即时更新:
客户端收到配置变更通知后,立即执行相应的更新逻辑,将最新的配置应用到应用程序中。这样就实现了配置的即时更新,无需重启应用程序或者手动刷新配置。
服务提供者(Provider):服务提供者负责将具体的服务实现发布为Dubbo服务,以供消费者调用。它将自己的服务注册到注册中心,并对外提供接口。
服务消费者(Consumer):服务消费者通过Dubbo框架调用远程的服务提供者,获取需要的业务功能。它通过注册中心获取可用的服务提供者列表,并通过负载均衡策略选择一个合适的服务提供者进行调用。
注册中心(Registry):注册中心作为服务的管理和协调中心,负责服务的注册与发现。服务提供者在启动时将自己的服务信息注册到注册中心,服务消费者通过注册中心获取可用的服务列表。Dubbo支持多种注册中心,如ZooKeeper、Consul等,dubbo从2.7.0版本后支持nacos做注册中心。
监控中心(Monitor):监控中心用于监控系统的运行状态和性能指标,提供可视化的监控界面。它收集服务提供者和消费者的调用统计数据,并进行展示和报警。
当Dubbo应用启动时,服务提供者会将自己的服务信息注册到注册中心,并监听指定的端口等待消费者的调用。服务提供者需要配置服务接口的实现类、协议、端口号等参数。服务消费者在调用服务之前,首先需要从注册中心获取可用的服务提供者列表。消费者可以通过配置或注解指定要引用的服务接口、注册中心地址、负载均衡策略等参数。Dubbo内部会维护一套服务元数据,包括服务接口、版本、协议、负载均衡策略等信息。Dubbo会根据这些元数据生成相应的代理类,用于在消费者端与服务提供者进行通信。Dubbo的远程通信主要依赖于通信协议。Dubbo默认使用Dubbo协议,该协议基于Netty框架实现,使用NIO方式进行网络通信。Dubbo协议采用自定义的二进制编码和解码方式,以提高性能和减少网络传输的数据量。
在Dubbo中,可以通过在服务引用的配置中指定loadbalance属性来选择不同的负载均衡策略
例如:
<dubbo:reference id="userService" interface="com.example.UserService" loadbalance="random"/>
或者
dubbo:
provider:
loadbalance: random
consumer:
loadbalance: random
dubbo支持以下几种策略
Dubbo的服务治理是指通过一系列的机制和工具来管理和控制Dubbo服务架构中的各种组件,以确保服务的可靠性、稳定性、性能和可伸缩性。Dubbo的服务治理涵盖了多个方面,主要包括以下几个方面:
服务注册与发现:Dubbo通过服务注册中心实现服务的注册和发现,服务提供者将自己的服务注册到注册中心,服务消费者从注册中心获取可用的服务提供者列表。这样可以动态管理服务的上下线、扩容和缩容。
负载均衡:Dubbo提供了多种负载均衡策略,如随机、轮询、最少活跃调用等,用于在多个服务提供者之间分配请求负载,以提高系统的性能和稳定性。
服务容错:Dubbo支持多种容错机制,如失败自动切换、快速失败、失败重试等,用于处理服务调用中可能出现的异常情况,保障服务的可靠性。
并发控制:Dubbo提供了并发控制机制,可以限制服务提供者的并发访问量,防止系统被过多的请求压垮。
隔离机制:Dubbo支持服务隔离,可以将不同的服务按照不同的隔离策略进行隔离,避免由于某个服务出现问题而影响整个系统的稳定性。
限流与熔断:Dubbo支持限流和熔断功能,可以根据系统负载情况对服务进行限流,同时在服务异常情况下进行熔断,保护系统免受雪崩效应的影响。
监控与报警:Dubbo提供了监控和报警机制,可以实时监控服务的运行状态、调用次数、响应时间等指标,并及时报警处理异常情况。
Dubbo框架提供了多种服务容错机制,用于处理在分布式系统中可能出现的各种异常情况,确保系统的稳定性和可靠性。以下是Dubbo常用的几种服务容错策略:
配置容错的话一般通过xml、注解、yaml配置来做
xml
<dubbo:service interface="com.example.DemoService" ref="demoService" cluster="failover"/>
<dubbo:reference id="demoService" interface="com.example.DemoService" cluster="failover"/>
java
/** *提供者 */ @Service(interfaceClass = DemoService.class, cluster = "failover") public class DemoServiceImpl implements DemoService { // 服务实现代码 } /** *消费者 */ public class SomeConsumer { @Reference(interfaceClass = DemoService.class, cluster = "failover") private DemoService demoService; // 使用 demoService 的代码 }
yaml
dubbo:
# 配置中心容错
config-center:
cluster: failover
# 注册中心容错
registry:
cluster: failover
# 消费者容错
consumer:
cluster: failover
# 提供者容错
provider:
cluster: failover
Dubbo 支持多种注册中心,每种注册中心都有其特点和适用场景。以下是 Dubbo 支持的几种常见注册中心及它们的区别:
Zookeeper:
Zookeeper 是 Dubbo 最常用的注册中心之一,具有高可用、一致性和良好的性能特点。它适合用于生产环境,并且在大型分布式系统中表现优秀。
Nacos:
Nacos 是一个新一代的动态服务发现、配置管理和服务管理平台,具有服务注册、发现、配置管理等功能。与传统的注册中心相比,Nacos 具有更丰富的功能和更灵活的扩展性。
Consul:
Consul 是一个由 HashiCorp 公司开发的开源工具,提供了服务发现、健康检查、KV 存储等功能。它的特点是易于部署和维护,并且提供了丰富的 HTTP API 接口。
Etcd:
Etcd 是一个分布式的 Key-Value 存储系统,可以用于构建分布式系统中的注册中心。它具有高可用、一致性和可靠性等特点。
当一个服务接口有新的实现或者接口定义发生变化时,您可以通过为该服务接口指定不同的版本号来进行版本管理。这样,在服务提供者和服务消费者之间就可以根据版本号来区分不同的服务实现,从而实现平滑的服务升级和版本切换。
在 Dubbo 的服务提供者和消费者配置中,您可以通过 @Service 或 @Reference 注解、XML 配置文件或者 Spring 配置文件中的 version 属性来指定服务的版本号。
// 服务提供者
@Service(version = "1.0.0")
public class SomeServiceImpl implements SomeService {
// 实现代码
}
// 服务消费者
@Reference(version = "1.0.0")
private SomeService someService;
在Dubbo中,SPI配置文件位于META-INF/dubbo
目录下,使用接口名称加上后缀Adaptive的文件来定义扩展点的实现类。例如,如果有一个名为com.example.SomeInterface的接口需要扩展,可以创建一个名为com.example.SomeInterfaceAdaptive的配置文件,并在文件中指定具体的扩展实现类。
Dubbo允许通过@Adaptive
注解标记接口或方法,实现自适应扩展。这意味着Dubbo根据运行时条件选择合适的扩展实现类。例如,当某个接口方法被标记为@Adaptive
时,Dubbo会根据调用时的参数来选择合适的实现类。
Dubbo的SPI机制支持一个接口有多个实现类。通过在配置文件中指定不同的实现类,可以实现不同的功能扩展。Dubbo会根据实际情况选择合适的实现类。
Dubbo的SPI机制支持动态加载扩展实现类。这意味着无需重新编译和部署应用程序,可以通过更改配置文件来添加或替换扩展实现类。这提供了更大的灵活性和可维护性。
在加载扩展实现类时,Dubbo的SPI机制会根据配置文件中定义的加载顺序进行加载。如果某个实现类加载失败(例如找不到类或初始化失败),Dubbo会使用默认的实现类或者抛出异常。这种容错机制确保即使有些扩展实现类无法加载,系统仍能正常工作。
在 Dubbo 中,可以通过实现 Dubbo 提供的 Filter 接口来定义自己的过滤器功能。
需要实现 invoke 方法,该方法接收两个参数:Invoker<?> invoker 和 Invocation invocation,并返回一个 Result 对象。
在 invoke 方法中,您可以编写自己的逻辑来对调用进行过滤、增强或者其他操作,并最终返回处理结果。通过实现这个接口,您可以实现各种自定义的过滤器功能,比如权限控制、日志记录、异常处理等。
一旦实现了自定义的过滤器类,需要将其配置到 Dubbo 的配置文件中,以便 Dubbo 在启动时能够加载和应用这些过滤器。在配置文件中指定提供者和消费者的过滤器链,Dubbo 将按照配置的顺序依次调用这些过滤器。
Direct Exchange(直连交换机):
功能:将消息通过 Routing Key 直接路由到与之完全匹配的 Queue 中。
路由规则:Exchange 会根据消息的 Routing Key 将消息发送到对应的 Queue 中,只有当消息的 Routing Key 与 Queue 绑定时指定的 Routing Key 完全匹配时,消息才会被投递到该 Queue。
Fanout Exchange(扇形交换机):
功能:将消息广播到所有绑定的 Queue 中,忽略 Routing Key。
路由规则:Exchange 接收到消息后,会将消息发送到所有与之绑定的 Queue 中,无需考虑消息的 Routing Key。
Topic Exchange(主题交换机):
功能:根据通配符匹配将消息路由到一个或多个 Queue 中。
路由规则:Exchange 会根据消息的 Routing Key 和 Queue 绑定时指定的通配符规则进行匹配,将消息发送到符合条件的 Queue 中。其中,通配符符号 * 匹配一个单词,# 可以匹配零个或多个单词。
Headers Exchange(标题交换机):
功能:根据消息头信息进行匹配,而非 Routing Key。
路由规则:Exchange 会根据消息的 Headers 属性进行匹配,并根据匹配结果将消息发送到对应的 Queue 中。在绑定时需要指定一组键值对,只有当消息的 Headers 属性与这些键值对完全匹配时,消息才会被发送到对应的 Queue 中。
RabbitMQ 使用信道的方式来传输数据。信道是建立在真实的 TCP 连接内的虚拟连接,且每条 TCP 连接上的信道数量没有限制
消息确认机制可以保证消息不会因为网络故障、Broker 故障等原因而丢失,从而提高消息传递的可靠性。在 RabbitMQ 中,有两种消息确认模式:Publisher Confirm
模式和 Consumer Acknowledge
模式。
Publisher Confirm 模式(生产者确认):
在生产者发送消息到 Exchange 后,可以通过确认机制来确保消息已经被 Broker 成功接收。
生产者在发送消息时,可以将信道设置为 Confirm 模式,当消息成功到达 Broker 时,Broker 会返回一个确认信息给生产者。
生产者可以通过监听 Confirm 信号来获取消息是否成功发送的确认信息,以此来决定是否进行下一步操作。
这种确认模式适用于需要确保消息不丢失的场景,如重要业务消息的发送。
Consumer Acknowledge 模式(消费者确认):
在消费者接收并处理消息后,可以向 Broker 发送确认信息,告知 Broker 消息已经成功处理。
消费者可以使用手动确认(manual acknowledgment)或自动确认(automatic acknowledgment)两种方式来确认消息的处理情况。
手动确认需要消费者在完成消息处理后显式地发送确认信息,而自动确认则由 RabbitMQ 自动确认消息。
手动确认模式适用于消费者需要在处理消息后执行一些复杂操作,确保消息处理的完整性和准确性。
RabbitMQ 的死信队列(Dead Letter Exchange,DLX)是一种特殊的队列,用于存储那些无法被消费者成功消费并处理的消息。当消息满足一定条件而被标记为“死信”时,这些消息会被重新路由到死信队列中,以便进行后续处理。
下面是关于 RabbitMQ 死信队列的一些重要信息和作用:
触发条件:
消息成为死信的触发条件可以通过设置队列的 TTL(Time-To-Live)、消息过期时间、消息被拒绝等多种方式来定义。
当消息满足这些条件之一时,RabbitMQ 将会将该消息标记为死信,并将其路由到指定的死信交换机(Dead Letter Exchange)。
作用:
死信队列可以帮助处理那些无法被正常消费和处理的消息,从而避免这些消息一直占据队列资源或丢失。
通过使用死信队列,可以对处理失败的消息进行分类、记录和后续处理,有助于排查和解决消息处理失败的原因。
死信队列还可以用于实现延迟消息队列、消息重试机制等应用场景,提高消息处理的灵活性和可靠性。
除了使用死信队列实现延迟消息队列,还可以通过私信队列实现延迟队列。具体步骤如下:
如何实现
创建一个普通队列,并设置其属性,包括消息的 TTL、队列绑定的交换机及路由键等信息。
在队列绑定的交换机上创建一个私信队列(Private Queue),并将其绑定到一个专门用于处理延迟消息的交换机上。
当消息达到其 TTL 时,RabbitMQ 将会将该消息路由到私信队列中,而不是死信队列中。
私信队列再将延迟消息发送到专门用于处理延迟消息的交换机中,以便进行后续处理。
数据库分为关系型数据库和非关系型数据库
关系型数据库:
Mysql,Oracle,sqlServer,DB2等
因为关系型数据库的原因,各表之间可以有关联,方便对用户请求数据的分模块、分功能管理,可以使用简单的sql语句就可以做非常复杂的数据查询。
非关系型数据库:
NoSql,Redis,MongoDB
非关系型数据库利用键值对的方式存储数据,不便于存储用户的大量数据,也正是因为这个机制,非关系型数据库经常被用做关系型数据库的缓存,以此减少关系型数据库的压力和资源消耗。
第一范式: 数据表中的每个字段都具有不可拆分性。也是数据设计中最基本的要求。
第二范式: 在满足第一范式的基础上,消除了部分依赖,解决了删除异常,插入异常和修改异常。
第三范式: 在满足第二范式的基础上,消除了传递依赖。
mysql的最大值是16384,默认值是100,最大连接数同时也根据当前系统的内存、tcp连接数相关,但是一般不建议设置为最大值,这样很容易导致数据库崩溃,一般情况下我们会设置一个最大连接数,用来限制数据库的最大连接,防止访问量过大导致数据库崩溃。
mysql
-- 0代表开始查找的索引值,5代表查找数量。
select *
from your_table
limit 0,5;
Oracle
-- 1代表开始查找的索引值,5代表查找数量。
SELECT *
FROM (
SELECT your_columns, ROWNUM AS rn
FROM your_table
WHERE your_conditions
ORDER BY your_order_column
)
WHERE ROWNUM BETWEEN 1 AND 5;
JDBC(Java Database Connectivity)是 Java 语言访问数据库的标准 API。通过 JDBC,Java 程序可以与各种关系型数据库进行连接、查询和更新数据。
包括以下功能:
连接数据库:使用 JDBC,Java 应用程序可以通过驱动程序连接到各种数据库管理系统(如Oracle、MySQL、SQL Server 等)。
执行 SQL 语句:通过 JDBC,可以发送 SQL 查询和更新语句到数据库,并获取执行结果。
处理结果集:JDBC 可以帮助开发人员处理从数据库返回的结果集,包括读取查询结果、处理数据并在应用程序中进行展示。
事务管理:JDBC 支持事务管理,可以通过提交或回滚事务来确保数据的完整性和一致性。
异常处理:JDBC 提供了异常处理机制,可以捕获和处理与数据库连接、查询或更新相关的异常情况。
元数据访问:JDBC 允许开发人员访问数据库的元数据信息,如表结构、列属性等。
连接池极大的提高了数据库的性能,极大的节省了数据库资源。
提高获取数据库连接的速度,避免了频繁创建和销毁连接。
方便对数据库连接的管理,方便对数据库连接的调优。
Mysql中的索引类别有以下几种:
B-Tree索引:B-Tree索引是MySQL中最常用的索引类型,它可以用于几乎所有的数据类型。B-Tree索引是一种平衡树结构,它能够快速定位到某个key值对应的记录。
哈希索引:哈希索引适用于等值比较查询,例如使用“=”或“IN”条件进行查询。哈希索引使用哈希算法将key值转换为哈希码,然后使用哈希码来进行查询。相对于B-Tree索引,哈希索引查询速度更快,但不支持排序和范围查询。
全文索引:全文索引适用于文本类型的列,例如VARCHAR和TEXT类型的列。全文索引可以实现针对文本内容的关键字搜索,支持自然语言查询和布尔查询等功能。
空间索引:空间索引适用于存储地理位置信息、二维坐标点等类型的列,例如GEOMETRY类型的列。空间索引可以进行距离计算、区域查询等功能。
前缀索引:前缀索引可以只对索引列的前n个字符进行索引,从而减少索引的存储空间和提高查询性能。但是由于只索引了部分字符,可能存在重复值较多的情况,因此需要权衡存储空间和查询效率。
联合索引(Composite Index):联合索引是指将多个列组合起来创建的索引,可以同时对多个列进行索引,从而加快多列条件查询的速度。当查询条件涉及到联合索引的所有列或部分列时,数据库可以有效地利用联合索引来提高查询性能。在创建联合索引时,需要注意列的顺序,通常应该将最常用于查询的列放在联合索引的前面。
主键索引(Primary Key Index):主键索引是用来唯一标识每条记录的索引,它要求所有记录都有唯一的主键值,通常是表中的某个列或列组合。主键索引不允许有重复值或NULL值,并且自动创建主键索引也会自动创建主键约束,确保数据的完整性和唯一性。主键索引可以帮助数据库快速定位到指定记录,是表中最重要的索引之一。
唯一索引(Unique Index):唯一索引要求索引列的值是唯一的,可以用来保证数据的唯一性约束。与主键索引不同的是,唯一索引允许存在NULL值(只能有一个NULL值),并且可以有多个唯一索引。
首先B树上,内容节点可以存放键和值,但是B+树的内容节点都是键,所有的值都在叶子节点上
其次B树的每个叶子节点相互独立,B+树的叶子节点有一条链相连
B树可以将经常查询的热点数据放入内容节点中,有利于缩小查找范围,方便快速查找。
B+树因为在叶子节点上有一条链相连,所以查找时会根据链进行顺序查找,也更适合于范围查找。
当写操作影响数据库查询效率时,这时候就要用到读写分离。
读写分离的基本的原理是让主数据库处理事务性增、改、删操作,而从数据库处理查询操作。我们可以通过主从复制实现数据同步,通过读写分离提升并发负载能力。
首先在主数据库执行完事务操作后,将执行操作写入binlog日志文件,随后通知从数据库进行同步。从数据库会建立一个I/O线程,随后从主数据库中读取事件放入中继日志,如果已经将事件读取完毕,则陷入阻塞状态,等待新的事件到来。随后从数据库从中继日志读取事件进行数据的同步更新。
当读操作频繁时,我们可以增加从数据库进行读写分离来降低数据库压力,但是当从数据库也变多的时候,我们就要考虑分库。同样当写操作压力很大时,我们也要进行分库操作。
垂直分库:
我们可以按照业务进行拆分,将不同业务的表放在不同的库中,这样就可以减轻单库的操作压力。但是随着数据的增大同样会让数据库的能力达到瓶颈,服务器磁盘、内存也会受到影响,这时候就要将其拆分到多个服务器上。
垂直分表:
一般就是将大表拆成小表,将不常用的字段拆分出来放到扩展表中,避免查询的时候数据量太大造成跨页问题。
水平分表
类似于垂直分库,只是将表的内容做了一定限制,可以减少单表数据量,达到查询效率的提升。
水平分库分表
在水平分表的基础上将数据切片部署在多个服务器中,每个服务器具有相对应的库和表,但是表的数据不同,这样做可以有效缓解数据库的大部分压力。
策略 | 介绍 |
---|---|
哈希取模 | 根据用户的id进行取模,按照取模后的结果放入不同的数据库 |
范围分片 | 比如根据id进行划分,1-10000划入表1,10001-20000划入表2 |
地理位置分片 | 按照地理位置进行分片,华东区一个表,华北区一个表 |
时间分片 | 按照时间进行分片,比如按年或者月进行分片 |
mysql总共有4种日志类型
日志类型 | 介绍 |
---|---|
Error Log(错误日志) | 用于几率sql错误信息 |
General Query Log(一般查询日志) | 记录目前sql正在做的事情 |
Binary Log(二进制日志) | 记录数据库的建表,改动等,可以通过该日志进行数据回滚和主从复制的数据同步 |
Slow Query Log(慢查询日志) | 记录查询时间超过阈值的sql语句,常用于数据库调优 |
mysql拥有4种事务隔离界别从低到高分别是
读未提交、读已提交(解决脏读)、可重复读(解决不可重复读)、序列化(解决幻读)
游标是系统为用户开设的一个数据缓冲区,存放SQL语句的执行结果,每个游标区都有一个名字。用户可以通过游标逐一获取记录并赋给主变量,交由主语言进一步处理
存储过程是一个预编译的SQL语句,允许模块化的设计,只需要创建一次,以后在该程序中就可以调用多次。如果某次操作需要执行多次SQL,使用存储过程比单纯SQL语句执行要快。
触发器是用户定义在关系表上的一类由事件驱动的特殊的存储过程。触发器是指一段代码,当触发某个事件时,自动执行这些代码。
Hash索引是通过hash函数计算出hash值在表中查找对应的数据,所以在精确查找上hash索引要快
BTree索引底层使用B+树实现,要多次使用折半查找来找到对应的数据,所有数据都存放在叶子节点上,并且每个叶子节点之间都有链相连,所以BTree索引支持范围查找,模糊查询。
缓存雪崩(Cache Avalanche):指缓存中大量的key在同一时间失效,导致大量请求同时落在数据库上,引起数据库的崩溃。解决办法主要有两种:一是使用高可用架构(如Redis集群)来保证系统的可用性;二是对缓存的过期时间进行随机化(Jitter)来避免缓存同时失效。
缓存穿透(Cache Penetration):指恶意攻击者故意查询不存在于缓存和数据库中的数据,导致每次查询都需要访问数据库,从而引起数据库压力过大而崩溃。解决办法主要有两种:一是在代码中对输入参数进行校验,拒绝不合法的请求;二是使用布隆过滤器(Bloom Filter)等技术,过滤掉一部分不存在于数据库中的请求。
缓存击穿(Cache Miss):指一个不存在于缓存中的key,被大量并发请求访问,导致这些请求都落在了数据库上,从而引起数据库的压力过大,导致系统崩溃。解决办法一般是使用互斥锁(如Redis的setnx命令)或者在缓存中设置一个空对象(Null Object Pattern)来避免缓存穿透。
全量同步
增量同步
4. 如果出现网络问题导致的数据丢失情况,从节点自身已经保存了已复制的偏移量和主节点id
5. 主节点根据偏移量将缓存区的数据发送给从节点,保证主从复制状态的正常。
有RDB和AOF两种方式做持久化,RDB做快照持久,AOF做增量持久
RDB根据快照保存当前数据库中的信息,随后开启一个线程创建RDB文件,但是RDB机制依旧会存在一定问题,因为它是每隔一段时间进行快照,这种方式会造成上次快照时间到这一秒中的数据丢失。
AOF是遵循redis协议对执行的命令进行持久化,redis会记录下所有变更数据库状态的命令,redis在载入AOF文件的时候,把AOF中每一条命令都执行一遍,最终还原回数据库的状态,它的载入也是自动的。在RDB和AOF文件都有的情况下,redis会优先载入AOF文件
全局的键空间选择删除策略:
设置过期时间的键空间选择性的删除:
使用SETNX
命令,SETNX
(SET if Not eXists)是 Redis 提供的一个原子性命令,用于设置指定键的值,仅当该键不存在时才设置成功。因此,可以利用 SETNX
命令来实现分布式锁。具体实现方式是将某个唯一标识作为锁的值,以锁的名字作为键,通过 SETNX 命令尝试获取锁。如果 SETNX
返回 1,表示成功获取到锁;否则,表示锁已经被其他客户端持有。
springboot环境中可以使用RedisTemplate来进行实现
/** * 获取锁 */ public boolean acquireLock(String lockKey, String uniqueValue, long expireTime) { ValueOperations<String, String> ops = redisTemplate.opsForValue(); Boolean result = ops.setIfAbsent(lockKey, uniqueValue); if (result != null && result) { redisTemplate.expire(lockKey, expireTime, TimeUnit.MILLISECONDS); return true; } return false; } /** * 释放锁 */ public void releaseLock(String lockKey, String uniqueValue) { String value = redisTemplate.opsForValue().get(lockKey); if (uniqueValue.equals(value)) { redisTemplate.delete(lockKey); } }
布隆过滤器(Bloom Filter)是一种空间效率高、快速判断元素是否存在的概率型数据结构。它通过使用一个很长的二进制向量和一系列的哈希函数来表示集合,并可以高效地进行插入和查询操作。
布隆过滤器的原理如下:
初始化:布隆过滤器是一个长度为m的位数组(通常用二进制表示),初始时所有位都被置为0。
插入操作:当要将一个元素插入布隆过滤器时,使用k个不同的哈希函数对该元素进行哈希计算,得到k个哈希值。然后将位数组中对应这k个哈希值的位置置为1。
查询操作:当要查询一个元素是否存在于布隆过滤器时,同样使用k个哈希函数对该元素进行哈希计算,得到k个哈希值。然后检查位数组中对应这k个哈希值的位置是否都为1,若有任何一个位置为0,则说明该元素不存在;若所有位置都为1,则该元素可能存在,但可能存在误判的概率。
由于布隆过滤器采用了多个哈希函数进行计算,并且将结果映射到位数组中,因此可以有效地减少冲突的概率。但是布隆过滤器存在一定的误判率,即有时会判断某个元素存在于布隆过滤器中,但实际上并不存在。这是因为多个元素哈希到了位数组中的同一位置,导致冲突。为了降低误判率,可以适当增加位数组的长度和哈希函数的数量。
通过两层循环遍历数组,每次比较相邻的两个元素,如果顺序不对则交换它们,直到整个数组排序完成
public static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换 arr[j] 和 arr[j+1]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
通过两层循环遍历数组,在每一轮中找到未排序部分的最小元素的索引,然后将其与当前位置的元素进行交换
public static void selectionSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < n; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
// 交换 arr[i] 和 arr[minIndex]
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
从数组的第二个元素开始,将当前元素插入到已经排序好的部分的合适位置。在插入过程中,需要将大于当前元素的元素依次向右移动。
public static void insertionSort(int[] arr) {
int n = arr.length;
for (int i = 1; i < n; i++) {
int key = arr[i];
int j = i - 1;
// 将大于 key 的元素都向右移动
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}
希尔排序是插入排序的一种改进,它通过使用不同的步长来对数组进行排序,从而减少逆序对的数量,提高了效率。
public static void shellSort(int[] arr) { int n = arr.length; // 初始步长设定为数组长度的一半,然后逐步缩小步长直至 1 for (int gap = n / 2; gap > 0; gap /= 2) { // 类似于插入排序,但是步长为 gap for (int i = gap; i < n; i++) { int temp = arr[i]; int j; for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) { arr[j] = arr[j - gap]; } arr[j] = temp; } } }
归并排序采用分治策略,将数组不断拆分为更小的部分,然后合并已排序的部分,最终得到完全有序的数组。
public static void mergeSort(int[] arr, int left, int right) { if (left < right) { int mid = left + (right - left) / 2; mergeSort(arr, left, mid); mergeSort(arr, mid + 1, right); merge(arr, left, mid, right); } } public static void merge(int[] arr, int left, int mid, int right) { int n1 = mid - left + 1; int n2 = right - mid; int[] L = new int[n1]; int[] R = new int[n2]; for (int i = 0; i < n1; i++) { L[i] = arr[left + i]; } for (int j = 0; j < n2; j++) { R[j] = arr[mid + 1 + j]; } int i = 0, j = 0; int k = left; while (i < n1 && j < n2) { if (L[i] <= R[j]) { arr[k] = L[i]; i++; } else { arr[k] = R[j]; j++; } k++; } while (i < n1) { arr[k] = L[i]; i++; k++; } while (j < n2) { arr[k] = R[j]; j++; k++; } }
快速排序通过选取一个基准元素,将数组分割为比基准元素小和比基准元素大的两部分,然后分别对这两部分递归进行排序,最终得到完全有序的数组。
public static void quickSort(int[] arr, int low, int high) { if (low < high) { int pi = partition(arr, low, high); quickSort(arr, low, pi - 1); quickSort(arr, pi + 1, high); } } public static int partition(int[] arr, int low, int high) { int pivot = arr[high]; int i = low - 1; for (int j = low; j < high; j++) { if (arr[j] < pivot) { i++; int temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } } int temp = arr[i + 1]; arr[i + 1] = arr[high]; arr[high] = temp; return i + 1; }
堆排序首先将数组构建成一个最大堆,然后逐步将堆顶元素(最大元素)与最后一个元素交换并调整堆,直到整个数组有序。
public void heapSort(int[] arr) { int n = arr.length; // Build heap (rearrange array) for (int i = n / 2 - 1; i >= 0; i--) { heapify(arr, n, i); } // One by one extract an element from heap for (int i = n - 1; i > 0; i--) { // Move current root to end int temp = arr[0]; arr[0] = arr[i]; arr[i] = temp; // Call max heapify on the reduced heap heapify(arr, i, 0); } } void heapify(int[] arr, int n, int i) { int largest = i; // Initialize largest as root int left = 2 * i + 1; int right = 2 * i + 2; // If left child is larger than root if (left < n && arr[left] > arr[largest]) { largest = left; } // If right child is larger than largest so far if (right < n && arr[right] > arr[largest]) { largest = right; } // If largest is not root if (largest != i) { int swap = arr[i]; arr[i] = arr[largest]; arr[largest] = swap; // Recursively heapify the affected sub-tree heapify(arr, n, largest); } }
public void countingSort(int[] arr) { int n = arr.length; // Find the maximum element in the array int max = arr[0]; for (int i = 1; i < n; i++) { if (arr[i] > max) { max = arr[i]; } } // Create a count array to store the count of each unique element int[] count = new int[max + 1]; // Store the count of each element in the count array for (int i = 0; i < n; i++) { count[arr[i]]++; } // Modify the count array to store the actual position of each element in the output array for (int i = 1; i <= max; i++) { count[i] += count[i - 1]; } // Create an output array and fill it with the sorted elements int[] output = new int[n]; for (int i = n - 1; i >= 0; i--) { output[count[arr[i]] - 1] = arr[i]; count[arr[i]]--; } // Copy the sorted elements back to the original array for (int i = 0; i < n; i++) { arr[i] = output[i]; } }
桶排序将元素分配到不同的桶中,然后对每个桶中的元素进行排序,最后将所有桶中的元素按顺序合并起来,从而完成排序。
public void bucketSort(float[] arr) { int n = arr.length; @SuppressWarnings("unchecked") ArrayList<Float>[] buckets = new ArrayList[n]; // Create empty buckets for (int i = 0; i < n; i++) { buckets[i] = new ArrayList<>(); } // Add elements into the buckets for (int i = 0; i < n; i++) { int bucketIndex = (int) (n * arr[i]); buckets[bucketIndex].add(arr[i]); } // Sort the elements in each bucket for (int i = 0; i < n; i++) { Collections.sort(buckets[i]); } // Concatenate the sorted buckets int index = 0; for (int i = 0; i < n; i++) { for (int j = 0; j < buckets[i].size(); j++) { arr[index++] = buckets[i].get(j); } } }
基数排序从最低位到最高位依次对每个位进行计数排序,直到完成排序
public void radixSort(int[] arr) { // Find the maximum element in the array int max = Arrays.stream(arr).max().getAsInt(); // Perform counting sort for every digit for (int exp = 1; max / exp > 0; exp *= 10) { countingSort(arr, exp); } } private void countingSort(int[] arr, int exp) { int n = arr.length; int[] output = new int[n]; int[] count = new int[10]; // Initialize count array Arrays.fill(count, 0); // Store count of occurrences in count[] for (int i = 0; i < n; i++) { count[(arr[i] / exp) % 10]++; } // Change count[i] so that count[i] contains actual position of this digit in output[] for (int i = 1; i < 10; i++) { count[i] += count[i - 1]; } // Build the output array for (int i = n - 1; i >= 0; i--) { output[count[(arr[i] / exp) % 10] - 1] = arr[i]; count[(arr[i] / exp) % 10]--; } // Copy the output array to arr[] System.arraycopy(output, 0, arr, 0, n); }
睡眠排序(Sleep Sort)是一个有趣且不太实用的“排序算法”,它基于每个元素的大小来安排线程的睡眠时间,从而实现排序。这种排序方法仅适用于对正整数进行排序,并且对于大量数据来说效率非常低,不适合实际使用。
鸡尾酒排序(Cocktail Shaker Sort),也称作双向冒泡排序(Bidirectional Bubble Sort)或定向冒泡排序(Shuttle Sort),是冒泡排序的一种变体。它与传统的冒泡排序的区别在于,鸡尾酒排序是从左到右和从右到左交替进行的,而不是单向地从左到右进行。
鸡尾酒排序的基本思想是通过多次往返扫描列表来实现排序,每次扫描都会在两个方向上依次比较相邻的元素,并根据需要交换它们的位置。这样可以在一定程度上提高排序的效率,特别是对于某些接近排序好的列表。
public static int linearSearch(int[] arr, int target) {
for (int i = 0; i < arr.length; i++) {
if (arr[i] == target) {
return i;
}
}
return -1;
}
二分查找(Binary Search),也称为折半查找,是一种高效的查找算法,要求数据集合必须是有序的。它通过将目标值与数据集合的中间值进行比较,以确定目标值在左侧还是右侧,并逐步缩小查找范围,直到找到目标值或确定不存在。
public static int binarySearch(int[] arr, int target) { int left = 0; int right = arr.length - 1; while (left <= right) { int mid = left + (right - left) / 2; if (arr[mid] == target) { return mid; } else if (arr[mid] > target) { right = mid - 1; } else { left = mid + 1; } } return -1; }
插值查找(Interpolation Search)是一种改进的二分查找算法,特别适用于数据量较大且分布均匀的有序数据集合。与二分查找不同,插值查找通过插值公式来预测目标值可能在数据集中的位置,从而更快地缩小查找范围。
public static int interpolationSearch(int[] arr, int target) { int low = 0; int high = arr.length - 1; while (low <= high && target >= arr[low] && target <= arr[high]) { int mid = low + ((target - arr[low]) * (high - low) / (arr[high] - arr[low])); if (arr[mid] == target) { return mid; } else if (arr[mid] < target) { low = mid + 1; } else { high = mid - 1; } } return -1; }
哈希查找(Hash Search)是一种基于哈希表进行查找的查找算法。在哈希查找中,通过将数据存储在哈希表中,可以实现常数时间复杂度的查找操作(O(1))。哈希查找适用于需要快速查找元素的场景,但要求内存空间较大。
java中可以使用HashMap来进行实现
二叉查找树(Binary Search Tree,BST)是一种基于二叉树的数据结构,它具有以下性质:
对于任意节点,其左子树中的所有节点的值都小于该节点的值。
对于任意节点,其右子树中的所有节点的值都大于该节点的值。
左子树和右子树也分别是二叉查找树。
从根节点开始,将要查找的值与当前节点的值进行比较。如果相等,则找到了目标节点;如果小于当前节点的值,则在左子树中继续查找;如果大于当前节点的值,则在右子树中继续查找。直到找到目标节点或遍历到叶子节点为止。
平衡二叉查找树(Balanced Binary Search Tree),也称为自平衡二叉查找树,是一种在插入和删除操作后能自动保持平衡的二叉查找树。平衡二叉查找树的目的是为了提高插入、删除和查找等操作的效率。
常见的平衡二叉查找树有红黑树(Red-Black Tree)、AVL树以及B树等。
B树是一种多路搜索树,常被用于数据库和文件系统中,其具有良好的平衡性和高效的查找特性。B树的每个节点可以包含多个子节点,相比于二叉树,B树可以更好地利用磁盘块的特性,减少磁盘 I/O 操作,从而提高数据的读取速度。
B树的基本特点如下:
跳表(Skip List)是一种支持快速查找的数据结构,它在有序链表的基础上增加了多层索引。跳表允许快速地进行插入、删除和查找操作,其时间复杂度为O(log n),与平衡二叉查找树类似。
跳表的基本思想是通过建立多级索引来加速搜索。跳表中的节点按照层级划分,最底层是原始链表,每个节点都有一个指向下一个节点的指针。除了原始链表外,还会建立其他层级的索引链表,其中每个节点只保留一个指向下一个节点的指针,该节点可能是下一层级的同一位置的节点,也可能是下一层级的前一个节点。
通过使用多级索引,跳表可以跳过部分节点,从而减少搜索的时间复杂度。在跳表中,每个节点的上方节点都是下方节点的快速访问入口点。索引层数越高,跨越的节点数量就越多,查询的速度也就越快。
斐波那契查找(Fibonacci Search)是一种利用斐波那契数列来进行搜索的算法,它在有序数组中进行查找操作。与二分查找不同,斐波那契查找使用黄金分割比例来确定要搜索的位置,以提高搜索效率。
斐波那契查找的基本思想如下:
首先,需要找到一个斐波那契数列中大于或等于数组长度的最小数。这个数被称为斐波那契数列的索引。
根据斐波那契数列中的两个相邻数相除的比例来划分数组,将数组分成两部分:前半部分的长度为前一个斐波那契数,后半部分的长度为前两个斐波那契数。
比较要查找的元素与当前位置的元素大小:
如果相等,返回当前位置;
如果要查找的元素大于当前位置的元素,说明在后半部分继续查找;
如果要查找的元素小于当前位置的元素,说明在前半部分继续查找。
重复以上步骤,直到找到要查找的元素或者遍历完整个数组。
斐波那契查找的时间复杂度为O(log n),比起传统的二分查找,在某些情况下具有更好的性能表现。
块查找(Block Search)是一种在静态有序表中进行查找的算法,它将表分成若干块,每个块中的元素也是有序的。块查找的基本思想是先确定待查找元素所在的块,然后在该块内进行顺序查找。
块查找的步骤如下:
块查找的优点是减少了比较次数,特别适用于对静态表进行查找操作。然而,在动态表中,由于元素的插入和删除可能导致块的重新划分,块查找的效率会降低。
Trie树(也称为字典树或前缀树)是一种用于高效存储和查找字符串的数据结构。它通过将字符串拆分为字符,并将每个字符作为节点存储在树中,从而实现快速的字符串查找操作。
Trie树的基本思想是将字符串按照字符的顺序构建成一棵树。树的根节点表示空字符串,每个节点包含一个字符和指向子节点的指针。从根节点到叶子节点的路径表示一个完整的字符串。
Trie树的查找操作根据待查找的字符串逐个字符地在树中进行遍历,直到遍历完所有字符或者找不到匹配的节点为止。具体的查找过程如下:
Trie树的时间复杂度取决于待查找字符串的长度,即为O(m),其中m是待查找字符串的长度。因此,Trie树在处理大量字符串查找时具有较高的效率。
后缀树(Suffix Tree)是一种数据结构,用于存储一个字符串的所有后缀,并支持高效的字符串匹配和查找操作。后缀树可以在O(m)的时间复杂度内完成字符串匹配,其中m是待匹配字符串的长度。
后缀树的构建过程会将原始字符串的所有后缀添加到树中,形成一个压缩的树结构,其中每条从根节点到叶子节点的路径表示一个后缀。通过这种方式,后缀树可以快速地进行子串匹配和查找操作。
后缀树的基本思想是将所有后缀构建成一棵树,其中包含如下特点:
后缀树的构建可以使用不同的算法,如Ukkonen算法或McCreight算法。在构建完后缀树后,可以利用后缀树进行各种字符串匹配和查找操作,例如查找某个模式串是否在原始字符串中出现。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。