模拟面试总结
抽象类和接口
概念
抽象类:一个类中,没有足够的信息来描绘一个具体的对象,这样的类就是抽象类。
抽象类是对整个事物的抽象
接口:接口,在JAVA编程语言中,是一个抽象类型,是抽象方法的集合。
是对事务局部行为的抽象,
区别
接口的方法默认是public,所有方法在接口中都不能有实现(Java 8 开始接口方法可以有默认实现),抽象类可以有非抽象方法。
接口中的实例变量默认是final 类型的,而抽象类中则不一定
一个类可以实现多个接口,但最多只能继承一个抽象类
一个类实现接口的话要实现接口的所有方法,而抽象类不一定
接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象,从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
在JDK8 中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的,如果同时实现两个接口,接口中定义了一样的默认方法,必须重写,不然会报错。
字符串常量池
JVM为了提高性能,和减少内存开销,在实例化字符串常量的时候,为字符串开辟了一个字符串常量池,类似于缓存区。
String name = "小明"; //指向常量池中的引用 只会在常量池中创建String s = new String("小明"); //指向的是堆空间中的引用 会保证堆 和 常量池中都有String intern = s.intern(); //System.out.println(s == name); // falseSystem.out.println(s == intern); // false 因为S指向的是堆空间的地址值 System.out.println(intern == name); //trueSystem.out.println("intern = " + intern); //intern = 小明
String name = “小新”;
String s = new String("小新");
String
类提供了一个intern()
方法,该方法检查传入的字符串是否已经存在于字符串常量池中。如果不存在,则将该字符串添加到池中,并返回池中字符串的引用;如果已经存在,则直接返回池中已有字符串的引用。
Object类的常见方法的总结
Object是所有类的父类,它主要提供了以下11个方法:
方法1:
public final native Class<?> getClass()
//native 方法 用于返回当前运行时对象的Class对象,使用了final关键字修饰,所以子类不可以重写。
方法2:
public native int hashCode()
//native方法 用于返回对象的哈希码,主要使用在哈希表之中,比如JDK中的HashMap。
方法3:
public boolean equals(Obinct obj)
用于比较两个对象的内存地址是否相等,String类对该方法进行了重写,用于比较字符串的内容是否相等。
方法4:
public native Object clone() throws CloneNotSupportedExpection
//native方法 用于创建并且返回当前对象的一份拷贝,一般情况之下,对于任何对象x ,表达式
x.clone() != x 为true x.clone().getClass == x.getClass() 为true 。 Object本身没有实现Cloneable接口,所以不重写clone方法兵器进行调用的话会发生CloneNotSupportedExpection异常。
方法5:
public String toString()
// 返回类的名字,@实例的哈希码的16进制的字符串,建议Object所有的子类都重写这个方法。
方法6、7:
public final native void notify() //唤醒多个在等待线程中的一个public final native void notifyAll() //唤醒多个在等待线程的所有线程
//native方法 被final所修饰,不能重写 。唤醒的是在对象监视器上等待的线程中的一个或者多个
方法8:
public final void wait(long timeout) throws InterruptedException
//native 方法 并且·被final修饰,不可以重写 暂停·线程的执行 注意sleep()方法没有释放锁,而wait()方法释放了锁 timeout是等待时间。
方法9:
public final void wait(long time,int nanos) throws InterruptedException
//相比方法8 多了int nanos 参数 这个参数表示额外时间(以毫微秒为单位,范围是0-999999).
所以超时的时间还要加上nanos毫秒。
方法10:
public final void wait() throws InterruptedException
和之前2个wait 方法一样 只不过该方法一直等待,没有超过时间这个钙奶你。
方法11:
protected void finalize() throws Throwable{}
实例被垃圾回收器回收的时候出发的操作。
mysql中date和datetime的区别
在MySQL中,DATE
和 DATETIME
都是用来存储日期和时间的数据类型,但它们有明显的区别:
- 存储内容:
DATE
类型只存储日期,格式为'YYYY-MM-DD'
,不包含时间信息。DATETIME
类型存储日期和时间,格式为'YYYY-MM-DD HH:MM:SS'
。
- 显示和存储格式:
DATE
的格式是'YYYY-MM-DD'
。DATETIME
的格式是'YYYY-MM-DD HH:MM:SS'
。
- 支持的范围:
DATE
支持的范围是'1000-01-01'
到'9999-12-31'
。DATETIME
支持的范围是'1000-01-01 00:00:00'
到'9999-12-31 23:59:59'
。虽然某些资料提到DATETIME
的范围始于1601-01-01
,但实际上MySQL文档声明的支持范围更广。
- 应用场景:
- 当你只需要存储日期而不关心时间时,使用
DATE
类型。 - 当你需要存储日期和具体时间(包括小时、分钟和秒)时,使用
DATETIME
类型。
- 当你只需要存储日期而不关心时间时,使用
- 默认值:
DATE
和DATETIME
类型都可以设置默认值,但DATETIME
类型还可以使用CURRENT_TIMESTAMP
作为默认值,自动填充当前的日期和时间。
- 性能和存储空间:
DATE
类型占用的存储空间比DATETIME
少,因为它不需要存储时间信息。- 对于只包含日期的查询,
DATE
类型可能比DATETIME
类型的查询效率更高,因为索引可以更小、更紧凑。
事务的隔离级别
隔离级别:事务之间隔离的程度,一个事务执行,是否能够看到(读取)另外一个事务已经提交的数据
- 读未提交:一个事务可以看到另一个事务未提交的数据
- 读已提交:一个事务可以看到另一个事务以提交的数据
- 可重复读: 在一个事务中多次读取同一行数据,数据是一致的 mysql 默认的隔离级别:是可重复
- 序列化读(串行化读): 在一个事务中按照同一条件,查询数据多次, 查询的数量(条数)一致
脏数据的程度
-
脏读:一个事务读取到另一个事务未提交的数据
-
不可重复读:在一个事务中多次读取同一行数据,数据不一样
-
幻读 在一个事务中按照同一条件查询多次, 查询到的条数 不一致
事务的传播行为:
传播行为,就是指在一个方法中开始事务,调用另外一个方法,事务是否可以传播到另外一个 方法上====》本质两个方法是否使用同一个事务(sqlSession)
- Required: 必须的,必须有事务
如果方法方法1开启了事务,方法2配置事务传播行为为 Required,方法2就是使用方法1的事务(sqlSession)如果方法方法1没有开启事务,方法2配置事务传播行为为 Required,方法2 自己开启新的事物(新的sqlSession)
- support: 支持的,
如果方法方法1开启了事务,方法2配置事务传播行为为 support,方法2就是使用方法1的事务(sqlSession)如果方法方法1没有开启事务,方法2配置事务传播行为为 support,方法2也不开启事务
- REQUIRES_NEW:必须有新的实物
无论方法1是否开启事务,方法2配置事务传播行为为REQUIRES_NEW,都要开启新的事物
final 、 finally 、 finalize
final: 用于修饰变量,方法和类
final修饰变量:final修饰的变量必须初始化, 被修饰的变量不可以改变。(不可变又分为引用不可变和对象不可变 ) final指的是引用不可变。
final修饰方法:被修饰的方法不允许任何子类重写,子类可以使用该方法。
final修饰类:被修饰的类不可以被继承,类中的所有方法都不可以被重写,
finally 作为异常处理的一部分
它只能在try/catch语句中,并且包含一个代码块,并且表示这段代码块最终一定会被执行。(常用于释放资源)(System。exit(0)可以阻断finally的执行)
finalize() 是在Object类中定义的方法
因为他是Object类中的方法,所以每一个对象都有这么一个方法。这个方法在 gc 启动,该对象被回收的时候调用。
将资源的释放和清洁放在finalize方法中,非常不好,非常影响性能,严重时会引起OOM(内存溢出)
Finalizer线程(守护线程)调用了finalize方法。
垃圾(一个对象没有任何引用时,就是垃圾!)
什么是类的加载、类加载过程
虚拟机把描述类的数据加载到内存里面,并对数据进行校验、解析和初始化,最终变成可以被虚拟机直接使用的class对象;
类的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。如图所示:
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)
类加载过程如下:
- 加载,加载分为三步: 1、通过类的全限定性类名获取该类的二进制流; 2、将该二进制流的静态存储结构转为方法区的运行时数据结构; 3、在堆中为该类生成一个class对象;
- 验证:验证该class文件中的字节流信息复合虚拟机的要求,不会威胁到jvm的安全;
- 准备:为class对象的静态变量分配内存,初始化其初始值;
- 解析:该阶段主要完成符号引用转化成直接引用;
- 初始化:到了初始化阶段,才开始执行类中定义的java代码;初始化阶段是调用类构造器的过程;
什么双亲委派模型?为什么要使用双亲委派模型?
类加载双亲委派模型(Parent Delegation Model)是Java 类加载机制的核心部分,它确保了 Java 平台的稳定性和安全性。
双亲委派模型是一种类加载器之间的协作机制,其中每个类加载器都有一个父类加载器,当一个类加载器收到类加载请求时,他首先不会尝试加载该类,而是将请求委托给父类加载器,只有当父类加载器无法加载该类时,子类加载器才会去尝试加载。
类加载器层次结构
Java中主要有三种内置的类加载器:
- Bootstrap ClassLoader(启动类加载器):这是最顶层的类加载器,负责加载Java 核心类库(例如:rt.jar)它没有父类加载器。
- Extension ClassLoader(扩展类加载器):他继承自Bootstrap ClassLoader ,负责加载/lib/ext 目录下的类库。
- Application ClassLoader(应用程序类加载器):这是默认的类加载器,用于加载用户应用程序的类路径(ClassPath)上的类。
(应用程序还可以定义自己的自定义加载器,这些自定义类加载器通常继承自 java.lang.ClassLoader)
工作流程
当应用程序尝试加载一个类时,类加载请求将按照以下顺序处理:
- 请求委派给父加载器:应用程序类加载器接收到加载类的请求时,它不会立即加载类,而是将请求传递给父类加载器——扩展类加载器。
- 父加载器继续向上委派:如果扩展类加载器也无法加载,它将继续委派给Bootstrap ClassLoader。
- 到达顶层或成功加载:如果Bootstrap ClassLoader也无法加载,或者类在某个级别被成功加载,加载过程停止。
- 子加载器尝试加载:如果类没有在父级加载器中找到,请求将返回到原始的子加载器,此时子加载器将尝试加载该类。
双亲委派模型的好处
- 安全性:确保了核心类库不会被随意覆盖,防止恶意代码篡改核心类库、核心API库。
- 稳定性:确保了类的一致性,避免了类加载的冲突、重复,确保了Java平台的稳定运行。
- 可扩展性:允许应用程序定义自定义类加载器,同时保持了类加载机制的统一性和安全性。
什么是内存溢出,内存泄漏?
**内存溢出:**就是内存满了,无法创建新的对象 此时报错 OutOfMemeory 异常 ====》程序停止
- 内存溢出通常指的是程序在运行过程中消耗的内存超过了系统所能提供的内存容量,导致无法继续分配新的内存。
**内存泄漏:**就是很多对象本来没有用了,需要被回收掉,但是被一些静态变量 静态常量持有无法被回收,此时就有内存泄漏 报的异常 leakofMemeory ======>一般来说程序不会停止,但是如果数据量非常大,也会造成内存溢出 如何避免内存泄漏,尽量避免静态资源持有很多对象
- 内存泄漏是指程序在运行过程中,由于未能适当释放不再使用的内存,导致这些内存被永久占用,随着时间的推移,可用内存逐渐减少。
- 内存泄漏不会导致立即的程序崩溃,但随着时间的推移,可能会导致程序运行缓慢,甚至最终耗尽所有可用内存。
- 在Java中,内存泄漏不会直接导致程序崩溃,但长期存在内存泄漏的程序最终可能会因为
OutOfMemoryError
而崩溃。
```
public static List studentList
get和post的区别
在HTTP协议中,GET和POST是两种最常见的请求方法,它们用于客户端与服务器之间的通信。以下是GET和POST方法的主要区别:
- 数据传输方式:
- GET:请求数据通过URL传递,数据附加在URL后面,形成查询字符串。
- POST:请求数据在请求体中传递,不会显示在URL中。
- 安全性:
- GET:由于数据暴露在URL中,因此不够安全,不适合传输敏感信息。
- POST:数据不会显示在URL中,相对更安全。
- 数据大小限制:
- GET:由于数据在URL中,受到URL长度限制,通常有大小限制。
- POST:数据在请求体中,通常没有大小限制,可以传输更大的数据。
- 缓存:
- GET:请求可以被缓存,适合用于获取数据。
- POST:请求不会被缓存,适合用于提交数据。
- 幂等性:
- GET:是幂等的,多次执行相同的GET请求,结果相同,不会改变服务器的状态。
- POST:不是幂等的,执行多次可能会导致服务器状态的改变。
- 用途:
- GET:通常用于请求服务器发送资源。
- POST:通常用于向服务器提交要被处理的数据。
- 历史记录:
- GET:请求会被保存在浏览器历史记录中。
- POST:请求不会被保存在浏览器历史记录中。
- 可读性:
- GET:请求的参数保留在浏览器历史记录中,可以被看到。
- POST:请求的参数不会保存在浏览器历史记录中,更难以被看到。
总结来说,GET方法适用于请求数据,而POST方法适用于提交数据。选择使用GET还是POST,取决于你想要实现的功能和对安全性、数据大小、缓存等的需求。
throw和throws的区别
throws 用在方法上,后面跟的是异常类,可以跟多个;而 throw 用在方法内,后面跟的是异常对象。
在Java中,throw
和throws
都与异常处理有关,但它们的作用和用法有所不同:
-
throw:
throw
关键字用于在代码中手动抛出一个异常对象。- 它只能在方法内部使用。
- 使用
throw
抛出的异常可以是任何类型的Throwable
对象,包括Error
和RuntimeException
。 throw
后面跟的是要抛出的异常对象。
示例代码:
public void checkValue(int value) {if (value < 0) {throw new IllegalArgumentException("Value cannot be negative");} }
-
throws:
throws
关键字用于在方法签名中声明该方法可能会抛出的异常。- 它用于方法的声明部分,用来说明方法在执行过程中可能会抛出的异常类型。
throws
后面跟的是异常类型列表,这些异常类型必须从Exception
类派生,不包括Error
和RuntimeException
。throws
允许方法的调用者知道可能需要处理哪些异常。
示例代码:
public void readFile(String fileName) throws IOException {// 假设这里有读取文件的代码,可能会抛出IOException }
总结来说,throw
用于实际抛出异常,而throws
用于方法声明,告知调用者该方法可能会抛出的异常类型。使用throws
时,方法的调用者必须处理这些异常,要么通过try-catch
块捕获它们,要么在调用者的方法签名中进一步声明throws
。而使用throw
时,异常是在方法内部抛出的,需要在方法的调用链中向上传递,直到被捕获或程序终止。
线程池的七个参数
①、corePoolSize
定义了线程池中的核心线程数量。即使这些线程处于空闲状态,它们也不会被回收。这是线程池保持在等待状态下的线程数。
②、maximumPoolSize
线程池允许的最大线程数量。当工作队列满了之后,线程池会创建新线程来处理任务,直到线程数达到这个最大值。
③、keepAliveTime
非核心线程的空闲存活时间。如果线程池中的线程数量超过了 corePoolSize,那么这些多余的线程在空闲时间超过 keepAliveTime 时会被终止。
④、unit
keepAliveTime 参数的时间单位:
- TimeUnit.DAYS; 天
- TimeUnit.HOURS; 小时
- TimeUnit.MINUTES; 分钟
- TimeUnit.SECONDS; 秒
- TimeUnit.MILLISECONDS; 毫秒
- TimeUnit.MICROSECONDS; 微秒
- TimeUnit.NANOSECONDS; 纳秒
⑤、workQueue
用于存放待处理任务的阻塞队列。当所有核心线程都忙时,新任务会被放在这个队列里等待执行。
⑥、threadFactory
一个创建新线程的工厂。它用于创建线程池中的线程。可以通过自定义 ThreadFactory 来给线程池中的线程设置有意义的名字,或设置优先级等。
⑦、handler
拒绝策略 RejectedExecutionHandler,定义了当线程池和工作队列都满了之后对新提交的任务的处理策略。常见的拒绝策略包括抛出异常、直接丢弃、丢弃队列中最老的任务、由提交任务的线程来直接执行任务等
在Java的ThreadPoolExecutor
中,当线程池中的任务太多,无法按照正常方式处理时,就会触发拒绝策略。以下是Java线程池中内置的一些常见拒绝策略:
- AbortPolicy:默认拒绝策略。当任务太多,无法处理时,会抛出
RejectedExecutionException
异常。 - CallerRunsPolicy:调用者运行策略。如果任务无法被线程池接受,那么调用
execute()
方法的线程(通常是主线程)将运行这个任务。如果调用线程本身就是线程池的一部分,则会抛出异常。 - DiscardPolicy:丢弃策略。当任务无法被线程池接受时,任务将被丢弃,且不会有任何异常抛出。
- DiscardOldestPolicy:丢弃最旧任务策略。当任务无法被线程池接受时,线程池会丢弃队列中最旧的任务,然后尝试再次提交当前任务。
- CallerRunsPolicyWithReport:调用者运行并报告策略。这是
CallerRunsPolicy
的变体,它在尝试提交任务时会抛出一个包含被丢弃任务数量的报告。 - Custom RejectedExecutionHandler:自定义拒绝策略。你可以实现自己的
RejectedExecutionHandler
接口来定义任务被拒绝时的处理方式。
选择合适的拒绝策略取决于你的应用程序需求和行为。例如,如果你的应用程序不能容忍任务丢失,可以选择CallerRunsPolicy
。如果你希望尽可能处理更多的任务,可以选择DiscardPolicy
或DiscardOldestPolicy
。自定义拒绝策略允许你根据特定需求来处理任务拒绝的情况。
在实际应用中,合理配置线程池参数和选择合适的拒绝策略,可以有效地提高应用程序的性能和稳定性。
线程死锁?如何避免?
死锁发生在多个线程相互等待对方释放锁资源,导致所有线程都无法继续执行。
死锁的条件
- 互斥条件:资源不能被多个线程共享,一次只能由一个线程使用。如果一个线程已经占用了一个资源,其他请求该资源的线程必须等待,直到资源被释放。
- 持有并等待条件:一个线程至少已经持有至少一个资源,且正在等待获取额外的资源,这些额外的资源被其他线程占有。
- 不可剥夺条件:资源不能被强制从一个线程中抢占过来,只能由持有资源的线程主动释放。
- 循环等待条件:存在一种线程资源的循环链,每个线程至少持有一个其他线程所需要的资源,然后又等待下一个线程所占有的资源。这形成了一个循环等待的环路。
理解产生死锁的这四个必要条件后,就可以采取相应的措施来避免死锁,换句话说,就是至少破坏死锁发生的一个条件。
- 破坏互斥条件:这通常不可行,因为加锁就是为了互斥。
- 破坏持有并等待条件:一种方法是要求线程在开始执行前一次性地申请所有需要的资源。
- 破坏非抢占条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:对所有资源类型进行排序,强制每个线程按顺序申请资源,这样可以避免循环等待的发生。
如何避免死锁?
银行家算法
线程池的类型
线程池是用来管理和重用线程的一种机制,它可以显著提升多线程应用程序的性能和稳定性。在Java中,常见的线程池类型包括以下几种:
-
FixedThreadPool(固定大小线程池):
- 固定大小的线程池有固定数量的线程工作。如果所有线程都处于活动状态,并且有更多的任务提交,那么它们会在队列中等待,直到有线程可用。适用于负载较重的服务器,可以控制线程的最大并发数,防止资源耗尽。
javaExecutorService executor = Executors.newFixedThreadPool(nThreads);
-
CachedThreadPool(缓存线程池):
- 缓存线程池可以根据需要创建新线程,并在可用时重用之前构造的线程。适用于执行很多短期异步任务的程序,线程在空闲一定时间后会被回收,适合处理大量的耗时较少的任务。
javaExecutorService executor = Executors.newCachedThreadPool();
-
SingleThreadExecutor(单线程线程池):
- 单线程线程池只有一个工作线程,任务按照提交顺序依次执行。适用于需要保证顺序执行各个任务,并且在任意时间点不会有多个线程活动的情况。
javaExecutorService executor = Executors.newSingleThreadExecutor();
-
ScheduledThreadPool(定时任务线程池):
- 定时任务线程池支持定时及周期性任务执行。它可以在给定的延迟后执行任务,或者定期执行任务。适用于需要定时执行任务的场景,如定时器任务、周期性统计等。
javaScheduledExecutorService executor = Executors.newScheduledThreadPool(corePoolSize);
-
WorkStealingPool(工作窃取线程池,Java 8新增):
- 工作窃取线程池是一种支持并行分解的线程池,每个线程都有一个自己的工作队列,如果一个线程完成了自己的工作,它可以从其他线程的队列中“窃取”任务来执行,这样可以减少线程间的竞争,提高执行效率。
javaExecutorService executor = Executors.newWorkStealingPool();
这些不同类型的线程池适合不同的应用场景,选择合适的线程池可以有效地管理线程资源,提高应用程序的性能和响应速度。
cookie的缺陷
Cookie作为一种客户端存储和跟踪技术,虽然在Web开发中非常普遍和有用,但它也有一些缺陷和限制:
-
大小限制:单个Cookie的数据大小通常限制在4KB左右。如果需要存储更多数据,可能需要使用其他存储解决方案。
-
数量限制:浏览器对每个域名下的Cookie数量有限制,通常是几十个到几百个不等。超过限制可能会导致旧的Cookie被自动删除。
-
隐私问题:Cookie可以存储用户的个人信息和浏览习惯,这可能引发隐私问题。用户可能不愿意自己的信息被网站跟踪和存储。
-
安全性问题:如果不正确地使用Cookie(例如,没有设置HttpOnly属性),它们可能会受到跨站脚本(XSS)攻击,攻击者可以窃取用户的Cookie信息。
-
跨域限制:由于安全原因,浏览器实施了同源策略,Cookie默认情况下不能跨域访问,这限制了Cookie在不同子域间的共享。
-
性能问题:每次HTTP请求都会携带Cookie,即使是无状态的请求。这可能会增加请求的大小,从而影响页面加载速度。
-
兼容性问题:一些用户的浏览器可能禁用了Cookie,或者用户可能主动删除了Cookie,这可能导致依赖于Cookie的网站功能出现问题。
-
存储限制:由于存储空间有限,Cookie不适合存储大量数据。
-
更新和删除的复杂性:更新或删除Cookie需要发送Set-Cookie头部,这可能导致额外的HTTP请求,增加了实现的复杂性。
-
CSRF攻击:如果Cookie中包含敏感信息,并且没有正确设置SameSite属性,可能会使网站容易受到跨站请求伪造(CSRF)攻击。
为了解决这些问题,Web开发者通常会采取一些措施,如使用更安全的存储选项(如HTTP-only和Secure标志的Cookie)、限制Cookie的使用范围、采用加密存储机制、利用现代的Web存储API(如localStorage和sessionStorage)等。此外,随着Web技术的发展,一些新的技术如HTML5的Web Storage(本地存储和会话存储)和IndexedDB提供了替代Cookie的解决方案。
mybaits为什么采用redis 二级缓存
MyBatis 采用 Redis 作为二级缓存的原因主要在于以下几个方面:
- 性能提升:Redis 是一个基于内存的数据结构存储系统,读写速度极快,远超传统的数据库查询速度。使用 Redis 作为二级缓存可以显著降低数据库的访问压力,提高应用程序的响应速度和处理能力。
- 分布式支持:在分布式系统中,MyBatis 自带的二级缓存(基于本地内存)无法在多个应用节点间共享,而 Redis 具备天然的分布式特性,可以作为集中式的缓存服务,让不同节点的应用共享缓存数据,保证数据的一致性和可用性。
- 数据淘汰策略:Redis 提供了多种数据淘汰策略(如 LRU、LFU 和 TTL 等),可以根据实际需求灵活选择,以自动管理缓存中的数据,确保热点数据得到优先保留。
- 大容量存储:相对于 JVM 内存限制的本地缓存,Redis 可以存储更多的数据,且可以通过集群模式进一步扩展存储能力。
- 数据不一致问题的缓解:虽然 MyBatis 自带的二级缓存也存在数据一致性问题,但通过 Redis 实现的二级缓存可以配合缓存更新策略(如先更新数据库再删除缓存的延时双删策略),在一定程度上减少数据不一致的风险。
- 灵活的数据结构:Redis 支持丰富的数据结构,如字符串、哈希、列表、集合、有序集合等,这为存储和检索不同类型的数据提供了便利。
- 易于监控和管理:Redis 提供了丰富的监控和管理工具,便于开发者查看缓存命中率、内存使用情况等,从而更好地优化缓存策略。
综上所述,MyBatis 结合 Redis 作为二级缓存,不仅可以提升系统的性能和扩展性,还能更好地适应分布式环境的需求,是提升应用程序数据访问效率的一个有效手段。
悲观锁和乐观锁
对于悲观锁来说,它总是认为每次访问共享资源时会发生冲突,所以必须对每次数据操作加上锁,以保证临界区的程序同一时间只能有一个线程在执行。
悲观锁的代表有 synchronized 关键字和 Lock 接口:
- synchronized:可以修饰方法或代码块,保证同一时刻只有一个线程执行该代码段。
- ReentrantLock:一种可重入的互斥锁,重入的意思是能够对共享资源重复加锁,即当前线程获取该锁后再次获取不会被阻塞。
乐观锁,顾名思义,它是乐观派。乐观锁总是假设对共享资源的访问没有冲突,线程可以不停地执行,无需加锁也无需等待。一旦多个线程发生冲突,乐观锁通常使用一种称为 CAS 的技术来保证线程执行的安全性。
由于乐观锁假想操作中没有锁的存在,因此不太可能出现死锁的情况,换句话说,乐观锁天生免疫死锁。
- 乐观锁多用于“读多写少“的环境,避免频繁加锁影响性能;
- 悲观锁多用于”写多读少“的环境,避免频繁失败和重试影响性能。
CAS
CAS操作包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。其执行过程可以简单描述为“比较并交换”:
- 比较:首先,CAS会比较内存位置V的当前值与预期原值A是否相等。
- 交换:如果相等,则将内存位置V的值更新为新值B;如果不相等,说明其他线程已经修改了该值,此时CAS操作失败,不会进行任何修改。
整个CAS操作是原子性的,这意味着在多线程环境下,从比较到交换这一系列动作是不可分割的,保证了操作的线程安全。
CAS有以下特点:
- 非阻塞:失败的线程不会被挂起或阻塞,而是被告知失败,可以立即再次尝试或者采取其他行动。
- 轻量级:相比于传统的锁机制,CAS避免了线程上下文切换和调度开销,提高了执行效率。
- 循环尝试:由于CAS操作可能失败,所以通常需要在一个循环中不断地重试,直到更新成功。这种模式被称为“自旋”。
但是,CAS也不是没有缺点:
- ABA问题:如果一个值从A变为B,又变回A,那么CAS操作可能会误认为值没有改变。解决ABA问题通常需要配合版本号或者使用AtomicStampedReference等机制。
- 吞吐量下降:在高竞争的情况下,大量的线程反复尝试更新同一个变量可能会导致CPU利用率上升,吞吐量下降。
- 只能保证一个共享变量的原子操作:对于需要同时更新多个共享变量的场景,需要配合其他同步机制来实现。
乐观锁通过CAS技术,在设计上假设数据一般不会发生冲突,从而减少了加锁解锁的开销,适用于多读少写的并发场景。在Java中,java.util.concurrent.atomic
包下的原子类(如AtomicInteger
)就广泛使用了CAS来实现线程安全。
公平锁和非公平锁
公平锁意味着在多个线程竞争锁时,获取锁的顺序与线程请求锁的顺序相同,即先来先服务(FIFO)。
虽然能保证锁的顺序,但实现起来比较复杂,因为需要额外维护一个有序队列。
非公平锁不保证线程获取锁的顺序,当锁被释放时,任何请求锁的线程都有机会获取锁,而不是按照请求的顺序。
怎么实现一个非公平锁呢怎么实现一个非公平锁呢?**
要实现一个非公平锁,只需要在创建 ReentrantLock 实例时,不传递任何参数或者传递 false 给它的构造方法就好了。
可重入锁和不可重入锁
重入锁(Reentrant Lock)和不可重入锁(Non-reentrant Lock)是两种不同类型的锁,它们的主要区别在于线程能否多次获取同一个锁。
1.重入锁(Reentrant Lock):
2.重入锁允许同一个线程在未释放之前多次获取同一个锁。这意味着线程可以进入任意数量的由同一个锁保护的代码块,而不会出现自己的锁获取造成的死锁。在Java中,java.util.concurrent.locks.ReentrantLock 就是一个典型的重入锁的实现。
重入锁的特点包括:
3.公平性:可以选择是否公平地获取锁。
4.可中断性:支持在等待锁的过程中可以响应中断。
5.条件变量支持:提供了条件变量来支持等待和通知机制。
ReentrantLock lock = new ReentrantLock();lock.lock();try {// 临界区代码} finally {lock.unlock();}
6.不可重入锁(Non-reentrant Lock):
7.不可重入锁是一种简单的锁,不支持同一个线程多次获取同一个锁。如果一个线程已经持有了该锁,再次尝试获取锁时会导致死锁或者直接返回失败。在Java中,synchronized 关键字就是一种不可重入锁的体现,因为它无法对自己的重复获取做出响应。
不可重入锁的特点:
8.不支持同一个线程重复获取锁。
9.在多线程环境下容易造成死锁,需要特别小心使用。
synchronized (lockObject) {// 临界区代码}
总结来说,重入锁允许同一个线程可以重复获取同一个锁,而不可重入锁不支持同一个线程重复获取锁。重入锁通常是更灵活和安全的选择,因为它允许在同一个线程内层层嵌套锁的使用,避免了死锁的发生。
java 线程安全的集合 和 不安全的集合 ?
线程安全的:
- Hashtable:比HashMap多了个线程安全。
- ConcurrentHashMap:是一种高效但是线程安全的集合。
- Vector:比ArrayList多了个同步化机制。
- Stack:栈,也是线程安全的,继承于Vector。
线性不安全的:
- HashMap
- ArrayList
- LinkedList
- HashSet
- TreeSet
- TreeMap
java中保证线程安全的方式 有哪些类?
保证线程安全的方式
- synchronized
- ReentrantLock
- ReentrantReadWriteLock
线程安全的类:**
- ThreadLocal 线程副本,为每一个线程,创建当前线程的副本
- AtomicInteger( 自增原子类) cas + volatile
- ConcurrentHashMap Concurrentxxxx
- Vector
- HashTable
- StringBuffer
synchronized和lock的区别
synchronized
和 Lock
都是Java中用于实现线程同步的机制,但它们之间存在一些关键差异:
- 实现方式:
synchronized
是Java的一个关键字,是JVM层面的原生支持,可以直接用于方法或代码块,实现简单。Lock
是一个接口(通常指的是java.util.concurrent.locks.Lock
),属于JUC包下的工具类,使用时需要手动获取和释放锁,提供了比synchronized
更多的灵活性。
- 锁的获取与释放:
synchronized
的锁是隐式的,由JVM自动管理,进入同步代码块或方法时自动加锁,退出时自动释放锁,包括正常执行结束和异常退出。Lock
需要显式地调用lock()
方法获取锁,使用完毕后必须调用unlock()
方法释放锁,这要求程序员手动管理锁的生命周期,增加了编程复杂度,但也提供了更细粒度的控制,如在finally
块中释放锁以确保异常时也能解锁。
- 中断支持:
synchronized
不支持中断,获取锁的线程如果阻塞,将一直等待下去,除非被同步代码块内的代码或其他线程中断其执行。Lock
支持中断,调用Lock
的lockInterruptibly()
方法获取锁时,线程可以响应中断,即在等待过程中可以通过Thread.interrupt()
方法中断该线程,线程将抛出InterruptedException
并停止等待。
- 公平性:
synchronized
默认采用非公平锁,即线程调度器可以随意选择等待的线程来获取锁,可能导致某些线程“饥饿”。Lock
提供了公平锁和非公平锁的选择,通过构造函数参数可以设置,公平锁会按照线程等待的顺序分配锁,减少了饥饿现象。
- 锁的状态检测:
synchronized
无法直接判断锁是否已经被获取。Lock
接口提供了如tryLock()
方法,可以尝试获取锁而不阻塞,并立即返回是否成功获取锁的信息。
- 条件变量(Condition):
synchronized
只有一个相关的监视器对象,所有线程竞争同一把锁,等待/唤醒机制基于wait()
,notify()
,notifyAll()
方法。Lock
接口中,每个Lock
实例可以绑定多个Condition
对象,允许更精细的线程间协调,提供更强大的线程同步功能。
综上所述,synchronized
更适用于简单的同步需求,利用其自动管理的便利性;而 Lock
提供了更多的控制和高级功能,适合复杂的多线程同步场景。
JVM 对Sychronized锁的优化
Java虚拟机(JVM)对synchronized
关键字的实现进行了大量的优化,以提高多线程环境下程序的性能。这些优化主要集中在减少锁的开销,提高锁的获取和释放速度,以及在适当的场景下采用更高效的锁定策略。以下是JVM对synchronized
锁的主要优化措施:
- 轻量级锁(Lightweight Locking): 轻量级锁是一种基于CAS(Compare and Swap)指令的锁实现。当一个线程试图获取一个
synchronized
锁时,JVM首先尝试使用轻量级锁。如果当前没有其他线程持有该锁,轻量级锁的获取将不会涉及内核态和用户态之间的切换,因此比重量级锁快得多。 - 偏向锁(Biased Locking): 偏向锁进一步优化了轻量级锁。当一个线程第一次访问一个
synchronized
块时,JVM会为该线程分配一个偏向锁。如果后续的访问仍然是同一个线程,那么可以直接进入同步代码块,而无需进行锁的获取和释放操作。偏向锁减少了频繁的CAS操作,提高了单线程访问的效率。 - 锁消除(Lock Elimination): 锁消除是指JVM在编译时分析代码,如果发现某些
synchronized
块实际上永远不会出现并发访问的情况,那么JVM会直接去除这些锁,从而避免了锁的开销。 - 锁粗化(Lock Coarsening): 锁粗化是指当JVM检测到一系列连续的
synchronized
块被同一个线程反复获取和释放时,会将这些synchronized
块合并成一个更大的锁区域,以减少锁的获取和释放次数,从而提高性能。 - 锁升级(Lock Promotion): 当多个线程竞争同一个
synchronized
锁时,JVM会将轻量级锁升级为重量级锁,以避免过多的CAS失败带来的性能损失。重量级锁涉及到操作系统层面的互斥锁,虽然开销较大,但在高并发场景下可以提供更好的性能。 - 适应性自旋锁(Adaptive Spin Locks): 适应性自旋锁允许线程在等待锁释放时进行自旋,而不是直接阻塞。JVM会根据锁的等待时间动态调整自旋的时间,如果锁很快就能被释放,那么自旋可以避免线程的上下文切换,提高性能。
通过这些优化措施,JVM大大提高了synchronized
关键字在多线程环境下的性能表现,使得Java的并发编程更加高效和灵活。然而,开发者在使用synchronized
时仍需谨慎,合理设计锁的使用范围和粒度,以避免不必要的性能瓶颈。
ArrayList的扩容机制
ArrayList 是基于数组的集./合,数组的容量是在定义的时候确定的,如果数组满了,再插入,就会数组溢出。所以在插入时候,会先检查是否需要扩容,如果当前容量+1 超过数组长度,就会进行扩容。
ArrayList 的扩容是创建一个1.5 倍的新数组,然后把原数组的值拷贝过去。
Redis的持久化机制
Redis 支持两种主要的持久化方式:RDB(Redis DataBase)持久化和 AOF(Append Only File)持久化。这两种方式可以单独使用,也可以同时使用。
RDB持久化机制 (redis database ):通过创建数据集的快照(snapshot)来工作,在指定的时间间隔内将 Redis 在某一时刻的数据状态保存到磁盘的一个 RDB 文件中。
可通过 save 和 bgsave 命令两个命令来手动触发 RDB 持久化操作:
AOF持久化机制通过记录每个写操作命令并将其追加到 AOF 文件中来工作,恢复时通过重新执行这些命令来重建数据集。
AOF 的主要作用是解决了数据持久化的实时性,目前已经是 Redis 持久化的主流方式。
RDB 和 AOF 各自有什么优缺点?
RDB 是一个非常紧凑的单文件(二进制文件 dump.rdb),代表了 Redis 在某个时间点上的数据快照。非常适合用于备份数据,比如在夜间进行备份,然后将 RDB 文件复制到远程服务器。但可能会丢失最后一次持久化后的数据。
AOF 的最大优点是灵活,实时性好,可以设置不同的 fsync 策略,如每秒同步一次,每次写入命令就同步,或者完全由操作系统来决定何时同步。但 AOF 文件往往比较大,恢复速度慢,因为它记录了每个写操作。
RDB 和 AOF 如何选择?
- 一般来说, 如果想达到足以媲美数据库的 数据安全性,应该 同时使用两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。
- 如果 可以接受数分钟以内的数据丢失,那么可以 只使用 RDB 持久化。
- 有很多用户都只使用 AOF 持久化,但并不推荐这种方式,因为定时生成 RDB 快照(snapshot)非常便于进行数据备份, 并且 RDB 恢复数据集的速度也要比 AOF 恢复的速度要快,除此之外,使用 RDB 还可以避免 AOF 程序的 bug。
- 如果只需要数据在服务器运行的时候存在,也可以不使用任何持久化方式
堆和栈的区别
在Java中,堆(Heap)和栈(Stack)是两种主要的内存区域,它们各自有不同的用途和特点:
-
堆(Heap):
- 用途: 堆是用于存储Java对象实例的地方,包括数组和对象。当使用
new
关键字创建对象时,对象将被分配到堆内存中。 - 内存管理: 堆内存由Java虚拟机(JVM)的垃圾回收器(Garbage Collector, GC)自动管理。GC负责回收不再使用的对象所占用的内存空间,以避免内存泄漏。
- 动态分配: 堆内存的大小可以在运行时动态地扩展或收缩,程序员无需预先声明对象所需的内存大小。
- 访问速度: 相较于栈,从堆中分配和访问内存的速度较慢,因为需要进行更多的寻址操作。
- 共享性: 堆是线程共享的区域,意味着一个线程创建的对象可以被其他线程访问。
- 用途: 堆是用于存储Java对象实例的地方,包括数组和对象。当使用
-
栈(Stack):
- 用途: 栈主要用于存储方法的调用信息,包括局部变量、方法参数、返回值以及操作指令的执行环境。它还保存了方法的返回地址,用于方法调用结束时恢复调用者的上下文。
- 内存管理: 栈内存的分配和释放由JVM自动处理,遵循“后进先出”(Last In, First Out, LIFO)原则,方法调用结束或局部变量不再使用时,相应的内存空间会被自动释放。
- 静态大小: 栈的大小相对固定,且通常比堆小,其大小可以在JVM启动时通过参数指定,但运行时通常不会改变。
- 访问速度: 栈内存的访问速度比堆快,因为栈数据遵循严格的访问规则和较小的寻址范围。
- 线程私有性: 每个线程都有自己独立的栈空间,栈内存中的数据不能被其他线程直接访问,保证了线程安全。
总结来说,堆主要用于存储复杂类型的数据结构(如对象)并支持动态内存管理,而栈则侧重于方法的执行上下文和基本类型的变量存储,提供快速的内存访问和自动的内存回收。
为什么重写equals时必须重写hashcode方法?
维护 equals()
和 hashCode()
之间的一致性是至关重要的,因为基于哈希的集合类(如 HashSet、HashMap、Hashtable 等)依赖于这一点来正确存储和检索对象。
具体地说,这些集合通过对象的哈希码将其存储在不同的“桶”中(底层数据结构是数组,哈希码用来确定下标),当查找对象时,它们使用哈希码确定在哪个桶中搜索,然后通过 equals()
方法在桶中找到正确的对象。
如果重写了 equals()
方法而没有重写 hashCode()
方法,那么被认为相等的对象可能会有不同的哈希码,从而导致无法在集合中正确处理这些对象。
重载和重写的区别
如果一个类有多个名字相同但参数个数不同的方法,我们通常称这些方法为方法重载(overload)。如果方法的功能是一样的,但参数不同,使用相同的名字可以提高程序的可读性。
如果子类具有和父类一样的方法(参数相同、返回类型相同、方法名相同,但方法体可能不同),我们称之为方法重写(override)。方法重写用于提供父类已经声明的方法的特殊实现,是实现多态的基础条件。
- 方法重载发生在同一个类中,同名的方法如果有不同的参数(参数类型不同、参数个数不同或者二者都不同)。
- 方法重写发生在子类与父类之间,要求子类与父类具有相同的返回类型,方法名和参数列表,并且不能比父类的方法声明更多的异常,遵守里氏代换原则
vue的八大指令
Vue.js 是一个用于构建用户界面的渐进式框架,它提供了一些指令来实现 DOM 的操作。Vue 2.x 中有 8 个指令,它们分别是:
-
v-bind:用于动态地绑定一个或多个属性,或一个组件 prop 到表达式。
- 简写:
:
- 示例:
<img v-bind:src="imageSrc">
或<img :src="imageSrc">
- 简写:
-
v-model:在表单输入和应用状态之间创建双向数据绑定。
- 示例:
<input v-model="username" type="text">
- 示例:
-
v-on:用于监听 DOM 事件,并通过事件处理方法引用代码。
- 简写:
@
- 示例:
<button v-on:click="sayHello">Click me</button>
或<button @click="sayHello">Click me</button>
- 简写:
-
v-if:条件性地渲染一块内容。
- 示例:
<p v-if="seen">Now you see me</p>
- 示例:
-
v-else:与
v-if
搭配使用,表示v-if
的“else”分支。- 示例:
<p v-if="seen">Now you see me</p> <p v-else>Now you don't</p>
- 示例:
-
v-else-if:与
v-if
搭配使用,表示多个条件分支。- 示例:
<p v-if="type === 'A'">A</p> <p v-else-if="type === 'B'">B</p>
- 示例:
-
v-for:基于一个数组来渲染一个列表。
- 示例:
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
- 示例:
-
v-show:简单地切换元素的 CSS 属性
display
。- 示例:
<p v-show="isVisible">Now you see me</p>
- 示例:
这些指令是 Vue 2.x 中提供的,而在 Vue 3.x 中,指令的使用方式基本保持不变,但 Vue 3 引入了 Composition API,提供了更多灵活的方式来组织和重用代码逻辑。
vue的生命周期,钩子函数
初始化 init
创建 create beaforeCreate created
绑定 mount beforeMount mounted
更新 update beforeUpdate updated 只要数据发生变化
销毁 destroy beforeDestroy destroyed
在mounted 中可以初始化界面参数
springmvc 请求流程
Spring MVC 是一个基于 Java 的 Web MVC 框架,它采用了经典的 MVC(Model-View-Controller)架构模式,用于构建灵活且可扩展的 Web 应用程序。下面是 Spring MVC 的基本流程:
1.请求到达 DispatcherServlet:
当客户端发送一个请求时,请求会首先到达 DispatcherServlet,它是 Spring MVC 的核心控制器。DispatcherServlet 充当请求的前端控制器,负责统一管理整个请求处理的流程。
2.HandlerMapping 映射处理器:
DispatcherServlet 通过 HandlerMapping 将请求映射到具体的处理器(Handler)上。HandlerMapping 将请求的 URL 映射到相应的 Controller 类和方法上。
3.HandlerAdapter 调用处理器:
一旦确定了处理器,DispatcherServlet 会调用 HandlerAdapter 来执行处理器。HandlerAdapter 是一个策略接口,用于执行处理器并返回 ModelAndView 对象。
4.执行处理器方法:
在这一步中,实际的 Controller 类的方法被调用来处理请求。这些方法执行业务逻辑,并返回一个 ModelAndView 对象,其中包含了处理结果以及对应的视图名称。
5.视图解析器解析视图:
DispatcherServlet 将 ModelAndView 中的视图名称交给视图解析器来解析。视图解析器根据视图名称解析出真正的视图对象。
6.渲染视图:
一旦确定了视图对象,DispatcherServlet 将模型数据传递给视图对象,并请求视图对象将模型数据渲染到响应中。视图对象生成 HTML 或其他类型的响应数据,并将其返回给客户端。
7.响应发送给客户端:
最后,DispatcherServlet 将生成的响应发送给客户端,完成整个请求-响应周期。
总的来说,Spring MVC 的流程是由 DispatcherServlet 控制的,它通过 HandlerMapping 找到合适的处理器,再通过 HandlerAdapter 调用处理器执行相应的业务逻辑,然后将处理结果交给视图解析器解析,并最终渲染到客户端。整个流程清晰地分为几个阶段,每个阶段都有明确的职责和功能,使得 Spring MVC 能够灵活、高效地处理 Web 请求。
请求转发和请求重定向的区别
**请求转发:**特点[背住]
- 请求转发: 地址栏不变
- 请求转发: 是服务器内部行为
- 当做域对象使用,即相当于容器,可以装载数据
- 两个servlet中请求域数据在一次请求转发中共享
请求转发:服务器内部处理,自己请求,获取数据后返回给客户端。
**重定向:**特点
- 重定向是响应重定向,是浏览器行为。
- 两次请求
- 地址栏会变化
- 请求域数据无法共享
重定向:服务器告诉客户端应该访问的地址,然后客户端访问正确的重定向后的服务器
正向代理和反向代理
正向代理
客户端向真实的服务器端发送请求,但是出于某种原因无法向真实的客户端发送请求,客户端就找到代理服务器,把请求发送给代理服务器,再由代理服务器把请求发送给真实的服务器,真实服务器并不知道具体访问的客户端是谁(真实服务器看到的访问自己的是代理,并不是真实的客户端)
反向代理
客户端向服务器端发送请求(服务器端是一个集群(4台服务器)),客户端并不知道具体访问哪一台服务器,客户端的请求就会被代理服务器所拦截,再由代理服务器把请求转交给集群中的某一个真实服务器,真实服务器最终把结果响应给代理服务器,代理服务器再把结果返回给客户端,客户端并不知道具体请求的服务器是真实服务器还是代理服务器
缓存击穿、缓存穿透、缓存雪崩
缓存穿透是指查询不存在的数据,由于缓存没有命中(因为数据根本就不存在),请求每次都会穿过缓存去查询数据库。如果这种查询非常频繁,就会给数据库造成很大的压力。
解决:
缓存雪崩是指在某一个时间点,由于大量的缓存数据同时过期或缓存服务器突然宕机了,导致所有的请求都落到了数据库上(比如 MySQL),从而对数据库造成巨大压力,甚至导致数据库崩溃的现象。
解决:避免大量的key同时失效。
缓存击穿是指某一个或少数几个数据被高频访问,当这些数据在缓存中过期的那一刻,大量请求就会直接到达数据库,导致数据库瞬间压力过大。
解决:
BeanFactory和ApplicationContext有什么区别?
BeanFactory 和 ApplicationContext 是 Spring 的两大核心接口,都可以
当做 Spring 的容器。其中 ApplicationContext 是 BeanFactory 的子接口。
(1)BeanFactory:是 Spring 里面最底层的接口,包含了各种 Bean 的定义,读取 bean 配置文档,管理 bean 的加载.实例化,控制 bean 的生命周期,维护 bean 之间的依赖关系。ApplicationContext 接口作为 BeanFactory 的派生,除了提供 BeanFactory 所具有的功能外,还提供了更完整的框架功能:
继承 MessageSource,因此支持国际化。
统一的资源文件访问方式。
提供在监听器中注册 bean 的事件。
同时加载多个配置文件。
载入多个(有继承关系)上下文 ,使得每一个上下文都专注于一个特定的层次,比如应用的 web 层。
(2)BeanFactroy 采用的是延迟加载形式来注入 Bean 的,即只有在使用到某个 Bean 时(调用 getBean()),才对该 Bean 进行加载实例化。这样,我们就不能发现一些存在的 Spring 的配置问题。如果 Bean 的某一个属性没有注入,BeanFacotry 加载后,直至第一次使用调用 getBean 方法才会抛出异常。
ApplicationContext,它是在容器启动时,一次性创建了所有的 Bean。这样,在容器启动时,我们就可以发现 Spring 中存在的配置错误,这样有利于检查所依赖属性是否注入。 ApplicationContext 启动后预载入所有的单实例Bean,通过预载入单实例 bean ,确保当你需要的时候,你就不用等待,因为它们已经创建好了。相对于基本的 BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置 Bean 较多时,程序启动较慢。
(3)BeanFactory 通常以编程的方式被创建,ApplicationContext 还能以声明的方式创建,如使用 ContextLoader。
(4)BeanFactory 和 ApplicationContext 都支持BeanPostProcessor.BeanFactoryPostProcessor 的使用,但两者之间的区别是:BeanFactory 需要手动注册,而 ApplicationContext 则是自动注册。
**共同点:**都可以作为容器,管理bean的生命周期,但是applicationcontext的功能更加强大
**不同点:**applicationContext 底层实现了beanfactory,是beanfactory的派生类 都有getbean()方法 但是applicationcontext 的getbean()方法是通过beanfactory来创建的。
spring中bean的生命周期
Spring 中 Bean 的生命周期大致分为五个阶段:实例化(Instantiation)、属性赋值(Populate)、初始化(Initialization)、使用、销毁(Destruction)。
1、Spring 容器根据配置中的 bean 定义中实例化 bean。
2、Spring 使用依赖注入填充所有属性,如 bean 中所定义的配置。
3、如果bean实现BeanNameAware 接口,则工厂通过传递bean的ID来调用setBeanName().
4、如果bean 实现BeanFactoryAware 接口,工厂通过传递自身的实例来调用setBeanFactoryO.
5、如果存在与bean关联的任何BeanPostProcessors,则调用preProcessBeforelnitialization()方法。
6、如果为bean 指定了init 方法(的init-method属性),那么将调用它。
7、最后,如果存在与bean关联的任何BeanPostProcessors,则将调用postProcessAfterlnitializationO)方法。
8、如果bean 实现DisposableBean 接口,当 spring容器关闭时,会调用destoryO。
9、如果为bean 指定了destroy方法(的destroy-method 属性),那么将调用它。
分为4大步:
实例化 : a:通过反射 去推断构造函数 去进行实例化
b:实例工厂 、静态工厂
属性赋值 : a: 解析自动装配 (byname bytype constractor none @Autowired ) DI 的体现
b:循环依赖
初始化 :a: 调用 XxxxxAware 接口 (BeanNameAware BeanClassLoaderAware BeanFactorAware) (bean实现接口就会调 用)
b: 调用初始化的生命周期的回调 (三种方式 : )
c:如果bean实现 aop 创建动态代理
销毁 : a :在spring容器 关闭的时候,进行调用
b: 调用销毁生命周期的回调
springboot的请求顺序
Spring Boot处理HTTP请求的基本顺序大致如下:
-
请求到达:请求首先到达Tomcat(或其他嵌入式Servlet容器)的
DispatcherServlet
。这是Spring MVC的核心组件,负责分发请求到对应的处理器。 -
请求分发:
DispatcherServlet
通过调用doDispatch
方法开始请求分发流程。在这个过程中,它会查询所有的HandlerMapping
,寻找能够处理当前请求的处理器(通常是Controller中的一个方法)。 -
处理器选择:
HandlerMapping
根据请求URL、HTTP方法等信息找到匹配的处理器执行链(包括Controller方法)。如果找不到匹配的处理器,将会抛出异常或者返回一个默认的错误响应。 -
前置处理:在调用Controller方法之前,如果有配置
HandlerInterceptor
(处理拦截器),则会按照配置顺序执行这些拦截器的preHandle
方法。这些拦截器可以用来进行权限验证、日志记录等操作。如果某个拦截器的preHandle
返回false
,则后续处理中断,直接跳到后置处理或完成处理阶段。 -
参数解析:Spring会根据方法签名,使用合适的
HandlerMethodArgumentResolver
解析请求中的参数,如从请求体、路径变量、查询参数中提取参数值,为Controller方法的参数赋值。 -
执行Controller方法:一旦参数准备就绪,Spring会通过反射机制动态调用目标Controller方法,并执行业务逻辑。
-
模型视图构建:Controller方法执行完毕后,可能会返回一个
ModelAndView
对象,或者直接返回数据(如ResponseEntity
),Spring会根据返回类型进行处理,准备响应数据。 -
后置处理:在处理完Controller方法后,如果配置了拦截器,会按照逆序执行拦截器的
postHandle
方法,这个阶段可以对响应数据进行修改。 -
返回值处理:接下来,Spring会选择合适的
HandlerMethodReturnValueHandler
(返回值处理器)来处理返回的数据,如使用HttpEntityMethodProcessor
处理ResponseEntity
类型的返回值。在此过程中,可能会应用ResponseBodyAdvice
进行响应体的进一步加工。 -
消息转换:通过
MessageConverter
(消息转换器),如MappingJackson2HttpMessageConverter
,将返回的对象转换成HTTP响应的适当格式(如JSON、XML)。 -
完成处理:最后,执行完所有后置处理逻辑后,拦截器的
afterCompletion
方法按逆序执行,进行最终的清理工作。 -
响应输出:转换后的响应内容被写回到HTTP响应中,由Servlet容器发送给客户端。
以上是Spring Boot处理HTTP请求的一般流程,具体步骤可能会根据应用配置有所不同。
Cookie和Session
Cookie 是 web 服务器发送给浏览器的一块信息,浏览器会在本地一个文件中给每个 web 服务器存储 cookie。以后浏览器再给特定的 web 服务器发送请求时,同时会发送所有为该服务器存储的 cookie。
Session 是存储在 web 服务器端的一块信息。
session 对象存储特定用户会话所需的属性及配置信息。当用户在应用程序的 Web 页之间跳转时,存储
在 Session 对象中的变量将不会丢失,而是在整个用户会话中一直存在下去。
Cookie 和 session 的不同点:
1.无论客户端做怎样的设置,session 都能够正常工作。当客户端禁用cookie 时将无法使用 cookie。
2.在存储的数据量方面:session 能够存储任意的 java 对象,cookie 只能存储 String 类型的对象
redis集群模式
主从模式
一台主节点redis负责读写,多个从节点负责只读
哨兵模式
哨兵模式一般都有2n+1(3)台机器组成,一台主节点负责读写,其余节点负责只读
去中心化模式
去中心化模式,就是集群中有多个主节点,每个主节点负责一部分的key的存储 一般是3主3从 4主4从
Java中的设计模式
Java中的设计模式是根据GoF(Gang of Four,四人组)在《设计模式:可复用面向对象软件的元素》一书中提出的23种经典设计模式的应用。这些模式分为三大类:创建型模式、结构型模式和行为型模式。下面是对这三类模式及其部分代表性的简单介绍:
创建型模式(Creational Patterns)
创建型模式关注对象的创建过程,旨在提供创建对象的最佳方式,以达到对象的创建与使用分离,提高系统的灵活性。
- 单例模式(Singleton):确保一个类只有一个实例,并提供一个全局访问点。
- 工厂方法模式(Factory Method):定义一个用于创建对象的接口,但让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
- 抽象工厂模式(Abstract Factory):为创建一组相关或依赖的对象提供一个接口,而无需指定它们具体的类。
- 建造者模式(Builder):将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。
- 原型模式(Prototype):通过复制现有的对象来创建新的对象,以避免创建对象时的性能开销。
- 懒汉模式**(Lazy Initialization)**,是一种创建型设计模式,主要用于推迟对象的创建,直到第一次真正需要使用该对象时才创建它。
结构型模式(Structural Patterns)
结构型模式关注如何组合类和对象以形成更大的结构,使得这些结构更易于使用。
- 适配器模式(Adapter):将一个类的接口转换成客户希望的另一个接口,使得原本不兼容的接口可以协同工作。
- 桥接模式(Bridge):将抽象部分与实现部分分离,使它们可以独立变化。
- 装饰器模式(Decorator):动态地给一个对象添加一些额外的职责,就增加功能来说,装饰器模式比生成子类更为灵活。
- 组合模式(Composite):允许你将对象组合成树形结构来表示“部分-整体”的层次结构,并可以对单个对象和组合对象进行一致的操作。
- 代理模式(Proxy):为其他对象提供一种代理以控制对这个对象的访问。
行为型模式(Behavioral Patterns)
行为型模式关注对象之间的交互以及职责分配,以达到更好的可维护性和扩展性。
- 观察者模式(Observer):定义对象之间的一对多依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知并自动更新。
- 策略模式(Strategy):定义一系列算法,把它们一个个封装起来,并使它们可以相互替换。让算法的变化独立于使用算法的客户。
- 模板方法模式(Template Method):定义一个操作中的算法骨架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
- 责任链模式(Chain of Responsibility):使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
- 命令模式(Command):将请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。
这些设计模式在Java开发中被广泛应用,以解决特定场景下的设计问题,提高代码的可维护性和可扩展性。学习和应用这些模式有助于提升软件开发的质量和效率。
简短点:
单例设计模式
工厂设计模式,抽象工厂设计模式
模板设计模式
代理设计模式
适配器设计模式
装饰器设计模式
拦截器和过滤器的区别
- 过滤器(Filter):当有一堆请求,只希望符合预期的请求进来。
- 拦截器(Interceptor):想要干涉预期的请求。
- 监听器(Listener):想要监听这些请求具体做了什么。
过滤器依赖于 Servlet 容器,而拦截器依赖于 Spring 的 IoC 容器,因此可以通过注入的方式获取容器当中的对象。
过滤器
- 过滤敏感词汇(防止sql注入)
- 设置字符编码
- URL级别的权限访问控制
- 压缩响应信息
过滤器的创建和销毁都由 Web 服务器负责,Web 应用程序启动的时候,创建过滤器对象,为后续的请求过滤做好准备。
拦截器
- 登录验证,判断用户是否登录
- 权限验证,判断用户是否有权限访问资源,如校验token
- 日志记录,记录请求操作日志(用户ip,访问时间等),以便统计请求访问量
- 处理cookie、本地化、国际化、主题等
- 性能监控,监控请求处理时长等
Rabbitmq
**消息队列的作用:**异步,削峰,解耦
模式:
**Hello-World:**也称为最简单模式
一个生产者,一个默认的交换机,一个队列,一个消费者
结构图
**work:**一个生产者,一个默认的交换机,一个队列,两个消费者
**Publish/Subscribe:**一个生产者,一个交换机,两个队列,两个消费者
发布订阅模式是将生产者所有的消息都会发布到不同的队列中(两个队列数据一致),每个队列都有一个消费者====》保证每一个消费者,都可以收到生产者的所有消息
**Routing:**一个生产者,一个交换机,两个队列,两个消费者
**Topic:**一个生产者,一个交换机,两个队列,两个消费者
Topic 本质上和路由模式没有太大区别,都是作用于消息的分流
RabbitMQ如何保证消息的高可靠
ack机制:保证消费者一定处理该消息
confirm机制:保证消息一定到达交换机
return机制:保证消息一定从交换机到达队列,否则返回给客户端
RabbitMQ消息被重复消费的问题
RabbitMQ消费者如果配置了手动ack机制,就有可能发生消息重复消费问题
解决法案:基于redis的setnx 完成 setnx(如果key不存在设置值,key存在什么也不做)
强引用,软引用,弱引用
强引用 (Strongly Reference)
最传统的引用,如 Object obj = new Object()
。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用 (Soft Reference)
用于描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出open in new window之前,会被列入回收范围内进行第二次回收,如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
弱引用 (Weak Reference)
用于描述那些非必须的对象,强度比软引用弱。被弱引用关联的对象只能生存到下一次垃圾收集发生时,无论当前内存是否足够,弱引用对象都会被回收。
什么是线程安全
多线程并发访问共享资源时,程序能够确保数据的完整性和一致性,不会出现因线程交错执行而导致的数据污染、竞态条件(race condition)或其他意外错误状态。具体来说,线程安全的代码或对象能够在多线程环境下正确地执行,并且其行为不受线程调度和执行顺序的影响。
NIO、BIO、AIO
BIO(Blocking I/O):采用阻塞式 I/O 模型,线程在执行 I/O 操作时被阻塞,无法处理其他任务,适用于连接数较少的场景。
NIO(New I/O 或 Non-blocking I/O):采用非阻塞 I/O 模型,线程在等待 I/O 时可执行其他任务,通过 Selector 监控多个 Channel 上的事件,适用于连接数多但连接时间短的场景。
AIO(Asynchronous I/O):使用异步 I/O 模型,线程发起 I/O 请求后立即返回,当 I/O 操作完成时通过回调函数通知线程,适用于连接数多且连接时间长的场景。
序列化和反序列化
序列化(Serialization)是指将对象转换为字节流的过程,以便能够将该对象保存到文件、数据库,或者进行网络传输。
实现Serializable
接口用于标记一个类可以被序列化
反序列化(Deserialization)就是将字节流转换回对象的过程,以便构建原始对象。
线程池的作用
线程池,简单来说,就是一个管理线程的池子。
①、快速响应用户请求
当用户发起一个实时请求,服务器需要快速响应,此时如果每次请求都直接创建一个线程,那么线程的创建和销毁会消耗大量的系统资源。
使用线程池,可以预先创建一定数量的线程,当用户请求到来时,直接从线程池中获取一个空闲线程,执行用户请求,执行完毕后,线程不销毁,而是继续保留在线程池中,等待下一个请求。
注意:这种场景下需要调高 corePoolSize (核心线程数)和 maxPoolSize(最大线程数),尽可能多创建线程,避免使用队列去缓存任务。
②、快速处理批量任务
这种场景也需要处理大量的任务,但可能不需要立即响应,这时候就应该设置队列去缓冲任务,corePoolSize (核心线程数)不需要设置得太高,避免线程上下文切换引起的频繁切换问题。
nginx负载均衡
负载均衡:就是将客户端请求的流量 通过nginx 按照规则 平均的分配给多个tomcat
负载均衡策略:
轮询:
轮询是upstream的默认分配方式,即每个请求按照时间顺序轮流分配到不同的后端服务器,如果某个后端服务器down掉后,能自动剔除。
weight (能者多劳,物尽其用)
轮询的加强版,即可以指定轮询比率,weight和访问几率成正比,主要应用于后端服务器异质的场景下。
ip_hash
每个请求按照访问ip(即Nginx的前置服务器或者客户端IP)的hash结果分配,这样每个访客会固定访问一个后端服务器,可以解决session一致问题。
backup(备用)
备用服务器:
备胎!平时不用,但是主服务器宕机的时候,备用服务器替代主服务器
一旦主服务器启动后,备用服务器就会退位
最少连接(Least Connections):
Nginx会选择当前活动连接数最少的服务器来处理新的请求。这种策略有利于更均衡地利用服务器资源,特别是在服务器性能差异较大或请求处理时间不一的情况下。
如何实现nginx高可用
使用keeplived 完成
1.监控nginx主节点,主节点挂掉,从节点升级为主节点
2.keeplived 生成虚拟ip 访问虚拟ip就是访问nginx的主节点
虚拟ip可以发生地址飘移
使用 F5硬件
设计模式的六大原则
设计模式的六大原则是面向对象设计和编程中的一组基本原则,它们指导开发者如何设计可复用、可维护和灵活的软件系统。以下是这六大原则的概述:
-
单一职责原则 (Single Responsibility Principle, SRP):
- 一个类或者模块应该有且仅有一个原因引起它变更。换句话说,一个类应该专注于做一件事情,这样当需求变更时,我们只需要修改相关的类,而不会影响到其他职责的代码。
-
开闭原则 (Open-Closed Principle, OCP):
- 软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。这意味着在不修改原有代码的基础上,可以容易地扩展功能。
-
里氏替换原则 (Liskov Substitution Principle, LSP):
- 子类应当可以替换父类并被使用者无感知地使用。也就是说,在使用基类的地方,可以透明地使用子类对象,而不会影响程序的正确性。
-
依赖倒置原则 (Dependence Inversion Principle, DIP):
- 高层模块不应依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。鼓励使用接口和抽象类,减少模块间的耦合。
-
接口隔离原则 (Interface Segregation Principle, ISP):
- 客户端不应该被迫依赖它不需要的接口。接口应该尽可能小,专注于单一功能,避免创建庞大臃肿的接口。
-
迪米特法则 (Law of Demeter, LoD) 或 最少知识原则:
- 一个对象应当对其他对象有最少的了解。一个类应该只和它的朋友(直接的朋友或通过朋友介绍的朋友)交流,避免与陌生人(在这个类的上下文中不直接关联的类)直接交互,以减少耦合度。
遵循这六大原则可以帮助开发者设计出更加健壮、易于理解和维护的软件系统。
TreeSet存储对象的要求
- 可排序性:
- 对象需要是可比较的,以便确定它们在集合中的位置。
- 如果存储的是基本数据类型的包装类(如
Integer
,Double
等),它们已经实现了Comparable
接口,可以直接使用。 - 自定义对象需要实现
Comparable
接口,并重写compareTo
方法来定义排序规则。
- 唯一性:
TreeSet
不允许存储重复元素。对于基本数据类型或其包装类,这是直接根据值来判断的。- 对于自定义对象,
compareTo
方法(或者提供的Comparator
)返回0
时认为两个对象相等(即它们是重复的)。这意味着你必须在compareTo
方法中定义何为“相等”,以确保逻辑上相同的对象不会被重复添加。
- 一致性:
- 如果使用
Comparable
接口,对象的比较逻辑在整个生命周期内应当保持一致。不一致可能导致集合的行为未定义。 - 使用
Comparator
时也应保持逻辑一致性,且该Comparator
应该在TreeSet
初始化时提供或通过sortedSet(comparator)
方法设置。
- 如果使用
- 非空性:
- 不能将
null
值添加到TreeSet
中,因为null
没有自然排序,且无法与任何对象进行比较。
- 不能将
- 自定义比较器:
- 当对象的自然排序不满足需求时,可以通过提供自定义的
Comparator
来决定排序逻辑。
- 当对象的自然排序不满足需求时,可以通过提供自定义的
总之,存储在 TreeSet
中的对象要么实现了 Comparable
接口,要么在创建 TreeSet
时指定了一个 Comparator
,以确保集合能够正确排序并维护唯一性。
hashset创建对象要求,实现什么方法
如果你要将自定义对象存储在HashSet中,确保你的自定义类正确地重写了hashCode()
和equals()
方法,以便HashSet能正确识别哪些对象是重复的。这两个方法的实现应该遵循以下原则:
hashCode()
:生成的对象哈希码应尽可能唯一,且对于相等的对象(根据equals()
方法判断),必须生成相同的哈希码。equals()
:用来确定两个对象是否相等,通常基于它们的关键属性进行比较。
注意事项
- 不保证顺序:HashSet不保证元素的添加顺序与遍历顺序一致。
- 唯一性:HashSet中的元素必须是唯一的,这是通过重写对象的
hashCode()
和equals()
方法来实现的。如果添加的元素违反了唯一性原则(即两个对象通过equals()
方法比较为相等,但它们的hashCode()
返回不同的值),则可能导致预期之外的行为。 - 允许
null
:HashSet允许插入一个null
元素,但只能有一个null
。 - 性能:由于底层使用了
HashMap
,HashSet提供了快速的添加、删除和查找操作,平均时间复杂度接近O(1)。
zookeeper的节点
znode 按照永久/临时2类 (临时节点,只要客户端断开,立即自动删除)
按照是否有序2类
此时znode就有4类
四种Znode
-
持久节点:永久的保存在你的Zookeeper
-
持久有序节点:永久的保存在你的Zookeeper,他会给节点添加一个有序的序号。 /xx -> /xx0000001
-
临时节点:当存储的客户端和Zookeeper服务断开连接时,这个临时节点自动删除
-
临时有序节点:当存储的客户端和Zookeeper服务断开连接时,这个临时节点自动删除,他会给节点添加一个有序的序号。 /xx -> /xx0000001
sql注入
插入恶意SQL语句:攻击者在输入框中输入类似1' AND 1=1; --
的字符串,尝试执行非法的SQL命令。
nginx的作用
Nginx作用:
-
反向代理 代理多个Tomcat
-
负载均衡 将流量动态均衡的分配各多个tomcat
-
动静分离:将静态资源缓存到nginx服务器,请求到达nginx,动态资源代理到tomcat,静态资源直接从nginx获取。
前段静态页面 图片 ,视频 放在nginx
动态内容(根据请求参数不同,返回数据不同) 去后台tomcat 查询
sql优化
SQL优化是数据库管理中的一个重要环节,目的是提高查询性能,减少资源消耗。以下是一些常见的SQL优化策略:
- 优化数据表结构:
- 确保表有适当的数据类型,避免冗余。
- 使用合适的索引来提高查询效率。
- 使用索引:
- 为经常查询的列创建索引,特别是WHERE子句和JOIN操作中使用的列。
- 避免对大型表的非主键列创建索引,因为维护成本较高。
- 优化查询语句:
- 减少子查询和复杂的JOIN操作。
- 使用合适的WHERE子句减少返回的数据量。
- 避免使用SELECT *,只选择需要的列。
- 使用查询缓存:
- 如果查询经常被执行且数据不经常变化,可以考虑使用数据库的查询缓存。
- 优化JOIN操作:
- 确保JOIN操作的表都有合适的索引。
- 考虑使用合适的JOIN类型(INNER JOIN, LEFT JOIN, RIGHT JOIN等)。
- 使用LIMIT和分页:
- 如果只需要查询结果的一部分,使用LIMIT来限制结果集大小。
- 对于大数据量的表,使用分页来逐步获取数据。
- 优化排序:
- 如果需要对结果进行排序,确保排序的列上有索引。
- 避免使用函数和计算:
- 避免在WHERE子句中使用函数或复杂的计算,这会影响索引的使用。
- 使用事务:
- 对于需要多个步骤的复杂操作,使用事务来保证数据的一致性和完整性。
- 检测死锁:
- 使用工具或编写代码来检测和解决死锁。
- 分析和解释执行计划:
- 使用EXPLAIN或类似命令分析查询的执行计划,找出性能瓶颈。
- 定期维护:
- 定期更新统计信息,重建索引,清理碎片。
- 考虑数据库分区:
- 对于非常大的表,考虑使用分区来提高查询和管理的效率。
- 避免锁竞争:
- 优化事务大小和持续时间,减少锁的竞争。
- 使用合适的隔离级别:
- 根据业务需求选择合适的事务隔离级别,以平衡性能和数据一致性。
- 监控和分析:
- 使用数据库监控工具来跟踪慢查询和性能问题。
SQL优化是一个持续的过程,需要根据实际的业务场景和数据模式来调整。在进行优化时,应该综合考虑查询的复杂性、数据量、系统资源和业务需求。
sql语句的执行顺序
SQL语句的执行顺序通常指的是查询语句(SELECT)中的各个子句的执行顺序。在一条典型的SQL查询语句中,子句的执行顺序如下:
- FROM:首先确定数据来源,即要从哪个表中读取数据。
- JOIN:接着处理连接操作,根据FROM子句指定的表和其他表进行连接。
- WHERE:然后应用过滤条件,根据WHERE子句的条件过滤数据。
- GROUP BY:如果存在分组需求,按照GROUP BY子句的列对结果进行分组。
- HAVING:对分组后的数据应用进一步的过滤条件,即HAVING子句。
- SELECT:选择需要的列,这通常发生在其他子句之后,以确保只处理所需的数据。
- DISTINCT:如果需要去除重复行,DISTINCT关键字在此步骤应用。
- ORDER BY:对结果集进行排序,根据ORDER BY子句的列和顺序。
- LIMIT:最后,如果需要限制结果集的大小,应用LIMIT子句。
- OFFSET:与LIMIT子句结合使用,跳过指定数量的行。
ThreadLocal
ThreadLocal 是 Java 中的一个线程封闭机制,它允许你在每个线程中存储和获取特定于该线程的数据。在多线程环境下,ThreadLocal 提供了一种线程安全的方式来存储每个线程独有的数据,而不需要进行同步操作。
下面是 ThreadLocal 的一些关键特点和使用场景:
1.线程隔离性:ThreadLocal 为每个线程提供了独立的变量副本,每个线程都可以访问自己的副本,而不会影响其他线程的数据。这种隔离性非常适合于需要在多线程环境中共享数据,但又需要保证线程安全的情况。
2.数据存储:ThreadLocal 内部维护了一个以线程为 key、变量副本为 value 的 Map 结构,通过线程对象作为 key,可以存储和获取与线程关联的数据。
3.线程封闭:ThreadLocal 提供了一种线程封闭的方式来管理数据,使得数据对于其他线程是不可见的。这种封闭性可以有效地减少线程间的竞争和冲突。
4.避免参数传递:使用 ThreadLocal 可以避免在方法之间频繁传递参数的情况,特别是在跨多层调用或异步调用的场景下,可以减少代码的复杂度和冗余。
5.适用场景:ThreadLocal 在一些特定的场景下非常有用,例如实现线程安全的数据库连接、会话管理、用户身份认证等。它可以确保每个线程都拥有自己的资源实例,而不需要进行显式的同步操作。
尽管 ThreadLocal 提供了一种方便的线程封闭机制,但过度地依赖 ThreadLocal 也可能导致内存泄漏和上下文切换等问题。因此,在使用 ThreadLocal 时需要注意及时清理线程持有的数据,避免过多占用内存资源。
如何实现高并发
1.引入redis
2.引入模板引擎 页面静态化
3.引入rabbitmq 进行削峰 异步
4.秒杀接口引入图片验证码机制
5.引入秒杀的动态接口(接口)
6.在redis二级缓存的基础上 引入jvm内存(作为三级缓存)
springboot的自动装配
自己的话:(spring boot的自动装配,简单来说就是自动去把第三方组件的Bean 装载到 IOC 容器中 不需要开发人员再去写 Bean 相关的一个配置 。在spring boot项目中 只需要在启动类上加上一个 spring boot application 注解 就可以实现spring boot的自动装配 这样一个注解是一个复合注解,真正实现spring boot 的自动装配的注解是 @ EnableAutoConfiguration
自动装配的实现主要依靠三个关键技术:
第一个: 引入starter 启动依赖组件的时候 这个组件里面必须要包含一个@Configuration 配置类 而在这个配置类之中 我们需要通过@Bean这个注解 去声明,需要装配到 IOC 容器里面 的Bean对象
第二个: 这个配置类 是放在第三方 jar 包里面的 然后通过spring boot 中约定大于配置 这样一个理念 去把这个配置类·的全路径 放在 classpath : /META-INF/spring.factories 文件里面 这样之后 spring boot 就可以知道 第三方 jar 包 里面这个配置类的位置 ,这个步骤主要是用到了 spring 里面 SpringFactoriesLoader 来完成的
**第三个 :**spring boot 拿到所有第三方 jar 包 里面 声明的配置类以后 再通过spring 提供的 ImportSelector 这样一个接口 来实现 对这些配置类的动态加载 从而去完成自动装配 这样一个动作
)
@SpringBootApplication
是Spring Boot框架中的一个核心注解,它被用在启动类上,用来简化Spring Boot应用的配置。这个注解实际上是一个组合注解,它结合了三个重要的Spring注解的功能:@SpringBootConfiguration
、@EnableAutoConfiguration
和@ComponentScan
。下面我们分别来看看这三个注解的作用:
@SpringBootConfiguration
:这是一个扩展了@Configuration
的注解,表明这是一个Spring配置类。@Configuration
注解是Spring框架的一部分,用于定义配置类,可以替代XML配置文件。在Spring Boot中,@SpringBootConfiguration
注解不仅包含了@Configuration
的功能,还添加了一些额外的处理,使其更加适合于Spring Boot应用的配置需求。@EnableAutoConfiguration
:这个注解是Spring Boot的核心功能之一,它开启了自动配置的功能。Spring Boot会根据你添加的依赖库自动配置Bean,而无需手动配置。例如,如果你在项目中添加了Spring Data JPA依赖,那么Spring Boot会自动配置数据源和实体管理器工厂等组件,而无需你编写任何配置代码。
SpringBoot自动配置最主要的注解就是@enableAutoConfiguration,这个注解会导入一个EnableAutoConfigurationlmportSelector的类,而这个类会去读取类路径下所有jar包里META-INF/spring.factories下key为EnableAutoConfiguration的对应值,找到相应得配置类,然后执行相应配置@ComponentScan
:这个注解用于扫描指定的包及其子包下所有带有@Component
、@Service
、@Repository
、@Controller
等注解的类,并将它们注册为Spring容器中的Bean。默认情况下,@ComponentScan
会扫描@SpringBootApplication
注解所在类的同级及子包。
综上所述,@SpringBootApplication
注解的作用可以概括为:
- 它标记的类是一个Spring配置类。
- 它启用了Spring Boot的自动配置功能,自动为你的应用配置所需的组件。
- 它自动扫描并注册了应用中所有基于注解的组件。
通过使用@SpringBootApplication
,开发者可以大大减少配置工作,使Spring Boot应用的启动变得简单快速。这也是Spring Boot能够实现“约定优于配置”的理念,帮助开发者专注于业务逻辑,而不是繁琐的配置细节。
Spring Boot的自动装配(Auto-Configuration)是其核心特性之一,它极大地简化了Spring应用的配置过程。自动装配机制允许Spring Boot在运行时根据项目中添加的依赖自动配置Spring应用上下文,从而使开发者无需手动配置即可快速启动和运行应用。下面是对Spring Boot自动装配机制的简要说明:
- 原理
Spring Boot的自动···装配基于以下几点原理实现:
-
条件注解:Spring Boot使用大量的
@ConditionalOn...
注解来判断是否需要进行特定的配置。这些条件注解检查类路径上的类、属性值、环境变量等,以决定是否应用某个配置类。 -
SpringFactoriesLoader:Spring Boot在启动时会加载
META-INF/spring.factories
文件中的配置信息。这个文件位于jar包中,由各个 Starter(启动器)提供,列出了自动配置类及其他组件的实现。 -
AutoConfigurationImportSelector:这是一个关键的类,负责读取
spring.factories
文件并决定哪些自动配置类需要被导入到Spring应用上下文中。
-
工作流程
-
启动Spring Boot应用:当应用启动时,Spring Boot会初始化Spring应用上下文。
-
检测starter和依赖:Spring Boot会扫描项目的类路径,识别出所有引入的starter(如spring-boot-starter-web)及其依赖的jar包。
-
加载自动配置信息:通过
SpringFactoriesLoader
加载所有spring.factories
文件,并读取其中列出的自动配置类。 -
条件匹配与配置:对于每个自动配置类,Spring会检查其上的条件注解,只有当条件满足时,才会实例化该配置类并将其加入到应用上下文中。这包括配置bean、设置属性值等。
-
应用自定义配置:尽管Spring Boot提供了自动配置,但开发者仍然可以通过自定义配置覆盖默认设置。这通常通过application.properties或application.yml文件完成。
-
自定义自动装配
如果需要,开发者也可以创建自己的自动配置类,只需遵循Spring Boot的约定,使用条件注解,并在自己的jar包中提供一个spring.factories
文件来声明这个配置类。
4. 禁用自动装配
有时可能需要禁用某些自动配置,可以通过设置spring.autoconfigure.exclude
属性或在application.properties中使用spring.autoconfigure.exclude=全类名,全类名
来排除特定的自动配置类。
总的来说,Spring Boot的自动装配机制通过智能化地分析项目依赖和配置,自动化地完成了大量配置工作,使得开发者能够更加专注于业务逻辑的开发。
就是将一个Bean 注入到其它的 Bean 的 Property 中,默认情况下,容器不会自动装配,需要我们手动设定。Spring 可以通过向BeanFactory中注入的方式来搞定bean之间的依赖关系,达到自动装配的目的。自动装配建议少用,如果要使用,建议使用ByName
springboot的启动流程
Spring Boot的启动流程是一个精心设计的序列,旨在简化应用的启动和配置。以下是启动流程的关键步骤:
- 启动类加载:
- 应用程序的入口通常是一个带有
@SpringBootApplication
注解的类,该注解是@SpringBootConfiguration
,@EnableAutoConfiguration
, 和@ComponentScan
三个注解的组合。 main
方法被调用,创建SpringApplication
实例。
- 应用程序的入口通常是一个带有
- 加载配置:
- 首先加载
bootstrap.*
配置文件(如boo tstrap.yml
或bootstrap.properties
),这些配置用于应用引导,如配置数据源或初始化云平台连接等。 - 然后加载
application.*
配置文件(如application.yml
或application.properties
),这些配置影响应用的主要行为。
- 首先加载
- Spring Environment准备:
- 初始化
SpringEnvironment
,它封装了所有配置属性,支持配置文件、命令行参数、系统环境变量等多种来源。
- 初始化
- 自动配置处理:
EnableAutoConfiguration
注解触发自动配置过程。Spring Boot根据类路径上发现的jar依赖和spring.factories
文件来决定哪些自动配置类需要被注册。- 每个自动配置类都可能包含条件注解,确保只在符合条件(如存在特定类或属性值)时才生效。
- 组件扫描:
@ComponentScan
注解触发组件扫描,自动发现并注册带有@Component
,@Service
,@Repository
,@Controller
等注解的类为Spring Beans。
- Bean定义与初始化:
- Spring根据配置和组件扫描的结果创建Bean定义,并按依赖顺序初始化它们。这包括配置类、用户自定义Bean以及自动配置产生的Bean。
- 执行初始化回调:
- Bean初始化期间可能会执行初始化回调方法,比如实现了
InitializingBean
接口的Bean或使用了@PostConstruct
注解的方法。
- Bean初始化期间可能会执行初始化回调方法,比如实现了
- 执行启动任务:
- 如果有实现
CommandLineRunner
或ApplicationRunner
接口的Bean,它们的run
方法会在所有其他Bean初始化之后执行,用于执行应用程序启动后的任务。
- 如果有实现
- Web服务器启动:
- 对于Web应用,Spring Boot会根据配置自动启动嵌入式的Web服务器(如Tomcat、Jetty或Undertow),并暴露HTTP端点。
- 应用启动完成:
- 至此,Spring Boot应用已经完全启动并准备好处理请求。
整个流程高度自动化,旨在减少手工配置,提高开发效率。开发者只需要关注业务代码,而大部分基础设施的配置和初始化工作由Spring Boot自动完成。
SpringBoot的执行流程
Spring Boot 应用程序的执行流程可以概括为以下几个主要步骤:
- 启动应用程序:
- Spring Boot 应用程序通过执行主类的
main()
方法来启动。这个主类通常标注了@SpringBootApplication
注解,它会触发 Spring Boot 自动配置和启动过程。
- Spring Boot 应用程序通过执行主类的
- 加载自动配置:
- Spring Boot 在启动过程中会自动加载大量的自动配置类(Auto-configuration)。这些自动配置类根据类路径中的依赖和配置,自动配置 Spring 应用程序的行为。例如,根据存在的数据库驱动自动配置数据源、根据依赖的模板引擎自动配置模板解析器等。
- 扫描组件和注册Bean:
- Spring Boot 应用程序会扫描主类所在包及其子包下的组件(Component Scanning),如带有
@Controller
、@Service
、@Repository
、@Component
等注解的类。同时,它还会注册一些特殊的 Bean,比如配置类、自动配置类、处理器映射等。
- Spring Boot 应用程序会扫描主类所在包及其子包下的组件(Component Scanning),如带有
- 启动内嵌的Servlet容器:
- Spring Boot 默认使用内嵌的 Servlet 容器(如Tomcat、Jetty或Undertow)。在启动过程中,Spring Boot 会自动配置并启动这些Servlet容器,将应用程序部署为一个独立的Web应用。
- 运行应用程序:
- 一旦Servlet容器启动成功,Spring Boot 应用程序就可以接收和处理客户端的请求了。请求由Servlet容器接收,然后通过Spring MVC框架的控制器处理,并生成响应返回给客户端。
- 关闭应用程序:
- 当应用程序关闭时,Spring Boot 会执行一些清理工作,比如释放资源、关闭数据库连接等。这部分工作通常在 Spring 容器销毁时通过
ApplicationContext
的关闭钩子来完成。
- 当应用程序关闭时,Spring Boot 会执行一些清理工作,比如释放资源、关闭数据库连接等。这部分工作通常在 Spring 容器销毁时通过
总结来说,Spring Boot 应用程序的执行流程包括自动配置加载、组件扫描和注册、内嵌Servlet容器的启动、应用程序的运行处理请求,以及应用程序的关闭。Spring Boot 的设计目标是尽可能减少配置,提供开箱即用的功能,使得开发者能够更专注于业务逻辑的实现而不是配置繁琐的框架。
spring事务的失效原因
1、方法为private 会失效
2、目标类没有配置Bean会失效
3、 自己捕获了异常。
4、内部调用也会失效
5、使用动态代理,但是@Transactional 声明在接口上面
mysql的存储引擎
MySQL的存储引擎是其架构中的重要组成部分,它们负责管理数据的存储方式、索引处理、事务支持等。不同的存储引擎适用于不同的应用场景,以下是几种常见的MySQL存储引擎:
- InnoDB:
- 默认引擎:自MySQL 5.5版开始,InnoDB成为默认存储引擎。
- 事务安全:支持ACID事务,保证数据的一致性和完整性。
- 行级锁定:减少锁定竞争,提高并发处理能力。
- 外键约束:支持外键,维护数据引用的完整性。
- 聚簇索引:数据行与主键索引存放在一起,优化某些查询类型。
- 崩溃恢复:具有自动灾难恢复能力。
- 全文索引:支持全文搜索(自5.6.4起)。
- MyISAM:
- 历史默认:在MySQL 5.5之前是默认存储引擎。
- 高速读取:优化了读取操作,适合大量SELECT查询。
- 不支持事务:没有事务处理能力,数据可能不一致。
- 表级锁定:在写操作时会锁定整个表,影响并发性能。
- 全文索引:较早支持全文索引。
- MEMORY:
- 内存存储:数据存储在内存中,适合临时表和高速读写操作。
- 易失性:重启服务后数据丢失,适合临时数据或缓存。
- 哈希或B-Tree索引:支持这两种类型的索引。
- ARCHIVE:
- 压缩存储:专为大量数据的存储而设计,数据被压缩存储,节省空间。
- 只读或插入优化:支持INSERT和SELECT操作,但不支持UPDATE和DELETE。
- 其他存储引擎:
- Merge:合并多个MyISAM表为一个虚拟表,适用于数据分片或日志归档。
- BLACKHOLE:数据会被丢弃,常用于日志或测试用途。
- CSV:数据存储为CSV文件格式,适合与其他CSV工具集成。
- Federated:访问远程MySQL服务器上的表,类似于数据库联邦。
- NDB (Cluster):专为高可用性和高扩展性的集群环境设计。
- TokuDB(虽然未在上述摘要中直接提及,但也是一个高性能存储引擎,特别适合大数据量的OLTP应用,支持高压缩率)。
选择存储引擎时,需要考虑数据的读写模式、事务需求、并发程度、空间效率、数据安全性等因素。
分布式事务解决方案
-
两阶段提交(2PC, Two-Phase Commit): 这是最经典的分布式事务协议。它分为两个阶段:准备阶段和提交阶段。在准备阶段,事务协调者询问所有参与者是否准备好提交事务;如果所有参与者都回复“准备好”,则进入第二阶段,协调者命令所有参与者提交事务;如果有任何参与者回复“不准备”或失败,则协调者命令所有参与者回滚事务。2PC保证了强一致性,但存在阻塞问题,且对网络和系统的稳定性要求较高。
-
三阶段提交(3PC, Three-Phase Commit): 为了解决2PC中的一些问题,提出了三阶段提交。相比2PC,3PC增加了一个预提交阶段,以减少阻塞并提高了容错性。三阶段分别是:CanCommit、PreCommit和DoCommit,增加了事务的灵活性,但仍存在一定的复杂性和潜在的不一致风险。
-
补偿事务(TCC, Try-Confirm-Cancel): TCC是一种业务级别的事务补偿方案。它将一个分布式事务分解为Try(预留资源)、Confirm(确认提交)和Cancel(取消释放资源)三个操作。在Try阶段预留资源,Confirm阶段正式提交事务,若提交失败则调用Cancel来回滚之前的操作。这种方式更灵活,但需要业务层面的支持和额外的补偿逻辑编写。
-
MQ分布式事务
RabbitMQ在发送消息时,confirm机制,可以保证消息发送到MQ服务中,消费者有手动ack机制,保证消费到MQ中的消息。
spring-cloud-alibaba的组件
nacos: 注册中心,配置中心,集群健康检测中心
Ribbon: 负载据衡器 1.负载均衡 2.在restemplate发起请求时 将url的服务名转化为实例对应ip 端口(根据负载均衡策略选择实例)
Feign:伪装 将远程的请求 伪装本地接口的请求
容错:当feign依赖的远程接口发生异常以后,feign通过容错策略依然可以返回数据
sentinel: 哨兵
以流量为切入点:容错 流量监控 熔断降级
sleuth :监控整个请求调用链(本地接口 发起远程请求)
1.收集 整个请求调用链 每个接口响应时间
2.收集整个调用链之间的 服务的依赖关系
zipkin:用来展示sleuth 收集到的数据
gateway: 网关:
路由
负载均衡
鉴权
过滤拦截
---------以上组将基本都可以再springcloudalibaba使用
还有一些组件再springcloud-netflix
eureka: 注册中心 配置中心 集群健康检测
hystrix: 等同于sentinel 熔断降级
zull 等同于gateway
快速失败(fail-fast)和安全失败(fail-safe)了解吗?
快速失败(fail—fast):快速失败是 Java 集合的一种错误检测机制
- 在用迭代器遍历一个集合对象时,如果线程 A 遍历过程中,线程 B 对集合对象的内容进行了修改(增加、删除、修改),则会抛出 Concurrent Modification Exception。
- 原理:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个
modCount
变量。集合在被遍历期间如果内容发生变化,就会改变modCount
的值。每当迭代器使用 hashNext()/next()遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。 - 注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改 modCount 值刚好又设置为了 expectedmodCount 值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的 bug。
- 场景:java.util 包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改),比如 ArrayList 类。
安全失败(fail—safe)
- 采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
- 原理:由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。
- 缺点:基于拷贝内容的优点是避免了 Concurrent Modification Exception,但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。
- 场景:java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改,比如 CopyOnWriteArrayList 类。
分布式锁有哪些?
zookeeper分布式锁
redis分布式锁
redission分布式锁
Redlock分布式锁
分布式锁详解、优缺点
这些都是常见的分布式锁实现方式或工具,让我们一一介绍一下:
1.Redis分布式锁:
2.Redis是一个高性能的内存数据库,提供了丰富的数据结构和功能,包括支持分布式锁。
3.Redis分布式锁通常使用SETNX(SET if Not eXists)命令来实现,通过尝试将某个值写入Redis中,来获得锁。
4.优点是简单易用,速度快,并且支持设置过期时间以防止死锁。
5.缺点是可能存在锁失效和死锁的问题,需要额外的容错机制来应对。
Redis分布式锁可能会因为多种原因导致失效和死锁,以下是一些常见的情况:
1.锁过期:Redis分布式锁通常会设置一个过期时间,在获取锁成功后,如果在一定时间内未能释放锁,锁将会过期。如果业务执行时间超过了锁的过期时间,锁就会失效,其他客户端可能会获取到相同的锁,导致多个客户端同时执行同一份业务逻辑,可能导致数据不一致或竞争条件。
2.业务执行时间过长:如果业务逻辑执行时间过长,在持有锁的情况下,其他客户端无法获取到锁,导致长时间阻塞。这可能会影响系统的性能和可用性。
3.锁释放错误:如果在释放锁时出现错误,例如网络中断或代码异常,可能会导致锁无法正确释放,其他客户端无法获取锁,从而造成死锁。
4.Redis节点故障:如果使用的是单节点Redis,那么如果Redis节点故障,锁也会失效。即使使用Redis的主从复制或集群,但在发生故障转移或者网络分区等情况下,也可能会导致锁的失效。
5.并发问题:在使用简单的SETNX命令来实现锁时,可能会出现多个客户端同时执行到了设置锁的命令,导致多个客户端都获取了锁。这种情况下,需要考虑使用额外的手段来确保锁的唯一性,如在设置锁时使用一个唯一的标识来区分不同的客户端。
为了避免失效和死锁的情况,可以采取一些预防措施:
6.设置适当的锁过期时间,避免长时间占用锁。
7.使用锁的续约机制,确保业务执行时间过长时能够延长锁的有效期。
8.在释放锁时使用原子性操作,确保锁能够正确释放。
9.考虑使用Redlock等分布式锁算法,以提高锁的可靠性和容错性。
在设计和使用分布式锁时,需要综合考虑系统的并发情况、业务逻辑复杂度以及所选用的锁实现方式等因素,以确保系统的稳定性和可靠性。
6.Redisson分布式锁:
7.Redisson是一个基于Redis的Java驻内存数据网格(In-Memory Data Grid),提供了丰富的分布式服务和功能,包括分布式锁。
8.Redisson分布式锁使用了更为复杂的机制,包括红黑树、异步监听等,以确保锁的正确性和可靠性。
9.优点是功能丰富,支持各种分布式场景和功能,并且具有较高的性能和可靠性。
10.缺点是相比简单的Redis分布式锁,Redisson可能需要更多的资源和学习成本。
11.ZooKeeper分布式锁:
12.ZooKeeper是一个开源的分布式协调服务,提供了分布式锁的实现机制。
13.ZooKeeper分布式锁通常使用临时顺序节点来实现,客户端尝试创建一个唯一的有序节点,并检查自己是否是最小节点,如果是则获取锁。
14.优点是具有较高的可靠性和一致性,并且在网络分区或节点故障时表现良好。
15.缺点是部署和维护成本较高,不适合小规模的应用。
16.Redlock分布式锁:
17.Redlock是一个由Redis官方提出的分布式锁算法,旨在解决Redis单节点可能存在的故障和并发问题。
18.Redlock通过在多个Redis实例上加锁,并使用大多数原则来确保锁的正确性。
19.优点是在Redis集群环境下具有较高的可靠性和一致性,并且相对于单节点锁具有更好的容错性。
20.缺点是相比于单节点锁,实现和维护成本较高,并且可能存在一些争议和问题,需要谨慎使用。
这些分布式锁实现方式或工具各有优缺点,选择合适的方式需要根据具体的需求和场景来进行评估和选择。
使用哪一种分布式锁
选择最适合的分布式锁取决于你的具体需求、系统架构以及对可用性、一致性和性能的要求。以下是一些常见的分布式锁,以及它们的一些特点:
1.Redis分布式锁:
2.常用程度:非常常用,因为Redis是一种轻量级、高性能的内存数据库,适用于许多场景。
3.优点:简单易用,速度快,支持设置过期时间,适用于大多数应用场景。
4.缺点:可能存在失效和死锁问题,对于一些高并发和对一致性要求很高的场景可能不够理想。
5.ZooKeeper分布式锁:
6.常用程度:在一些对一致性要求很高的场景中常用,如分布式协调服务、分布式配置管理等。
7.优点:具有较高的可靠性和一致性,能够在网络分区或节点故障时保持系统的稳定性。
8.缺点:部署和维护成本较高,性能可能不如Redis分布式锁。
9.Redisson分布式锁:
10.常用程度:在Java环境中比较常用,特别是在使用Redisson作为分布式对象和服务的情况下。
11.优点:功能丰富,支持各种分布式场景和功能,并且具有较高的性能和可靠性。
12.缺点:依赖于外部的Redis服务,可能存在单点故障问题,性能开销相对较大。
13.Redlock分布式锁:
14.常用程度:在一些对于高可用性和一致性要求较高的场景中使用。
15.优点:在Redis集群环境下具有较高的可靠性和一致性,并且相对于单节点锁具有更好的容错性。
16.缺点:实现和维护成本较高,可能存在一些争议和问题。
总的来说,没有一种分布式锁是“最好”的,而是需要根据具体的业务需求和系统特点来选择最合适的。对于大多数应用场景来说,Redis分布式锁是一个不错的选择,因为它简单易用、性能较好,并且能够满足大多数场景的需求。但对于一些对一致性和可靠性要求很高的场景,可能需要考虑使用ZooKeeper分布式锁或者Redisson分布式锁。
守护线程
守护线程(Daemon Thread)是Java中的一种特殊类型的线程,它的主要职责是为其他线程(通常称为用户线程)提供服务。守护线程的一个显著特点是,**当所有非守护线程都结束执行后,无论守护线程是否还在执行任务或者是否完成任务,它都会被自动终止。**这意味着守护线程不会阻止Java程序的结束——只要所有非守护线程执行完毕,程序就会结束,即使守护线程还在运行之中。
守护线程经常用于执行后台支持任务,比如垃圾回收(虽然Java的垃圾回收线程实际上是用更低层次的机制实现的)、监控内存使用情况、日志记录、连接检测等,这些任务不是程序的核心部分,但对程序的正常运行提供辅助。
守护线程与用户线程的主要区别在于其生命周期依赖于整个应用程序中是否存在非守护线程。用户线程代表了程序的主要功能,它们通常执行应用程序的主要逻辑,只有当这些线程结束时,程序才会自然结束。而守护线程则更像是幕后工作者,它们服务于用户线程,一旦没有用户线程需要服务,守护线程也就失去了存在的意义,随程序一同结束。
在Java中,可以通过调用线程对象的setDaemon(true)
方法将一个线程设置为守护线程,但必须在调用线程的start()
方法之前设置,否则会抛出IllegalThreadStateException
异常。需要注意的是,守护线程不适合执行需要确保完成的任务,因为它们可能会在任何时候被系统终止。
解释Spring支持的几种bean的作用域
当通过 Spring容器创建一个Bean 实例的时候,不仅可以完成bean 实例的实力化,还可以为bean指定作用域。Springbean 元素的支持以下5种作用域:
Singleton:单例模式,在整个 spring lOC 容器中,使用 singleton 定义的bean 将只有一个实例。
Prototype:多例模式,每次通过容器中的 getBean 方法获取 prototype 定义的beans 时,都会产生一个新的bean的实例。
Request:对于每次Http 请求,使用request定义的bean 都会产生一个新的实例,只有在web应用时候,该作用域才会有效。
Session:对于每次Http Session,使用 session 定义的Bean 都将产生一个新的实例。
Globalsession:每个全局的Http Sesisonn,使用 session 定义的本都将产生一个新的实例
可重入锁
可重入锁是一种允许同一线程多次获取同一把锁而不会造成死锁的锁。当一个线程已经持有了某个锁时,它可以再次获取该锁,而不会被阻塞,而其他线程在尝试获取该锁时会被阻塞,直到该线程释放锁。
可重入锁通常在以下情况下使用:
1.递归函数调用:当一个递归函数需要在多个递归层次上使用同一个锁时,可重入锁可以确保同一线程在递归调用期间不会被阻塞。
2.线程间的协作:在多线程编程中,可能需要在同一线程中多次获取同一个锁来保护共享资源的访问。可重入锁允许这样的操作,以确保线程在执行期间可以安全地访问共享资源。
3.避免死锁:可重入锁可以避免由于线程试图在持有锁时再次获取锁而导致的死锁情况。如果一个线程已经持有了某个锁,并且尝试再次获取该锁,可重入锁会允许这种操作而不会导致死锁。
总的来说,可重入锁提供了一种灵活且安全的锁机制,允许同一线程在多次获取同一把锁时不会被阻塞,从而提高了多线程程序的效率和安全性。
反射
反射是一种在运行时检查和修改类、方法、属性等程序结构的能力。在许多编程语言中,包括Java、C#、Python等,都提供了反射机制来动态地获取程序结构的信息并在运行时进行操作。
在Java中,反射主要通过java.lang.reflect包来实现。以下是反射的一些主要特性和用途:
1.获取类的信息:反射可以动态地获取类的信息,包括类的名称、方法、字段、构造方法等。
2.实例化对象:通过反射可以实例化对象,即在运行时创建类的实例,而不是在编译时确定。
3.调用方法:反射允许在运行时调用类的方法,包括公有方法、私有方法等。
4.操作字段:可以通过反射动态地访问和修改类的字段,包括公有字段、私有字段等。
5.动态代理:反射可以实现动态代理,即在运行时动态生成代理对象来实现某个接口或者继承某个类。
6.注解处理:反射可以用于处理注解,包括获取类、方法、字段上的注解信息,并根据注解信息做出相应的处理。
尽管反射提供了一种强大而灵活的机制来操作程序结构,但在实际应用中也存在一些注意事项和性能开销:
7.性能开销:由于反射涉及到在运行时动态地获取信息和执行操作,因此通常会比静态的编译时代码执行速度慢。
8.安全性问题:反射可以绕过访问控制权限,即使是私有的方法和字段也可以通过反射来访问。因此,在使用反射时需要格外小心,遵循安全性最小原则。
9.可维护性:由于反射使得代码更加动态,可能会导致代码的可读性和可维护性降低。因此,在使用反射时需要仔细考虑其对代码结构和可读性的影响。
总的来说,反射是一种强大的工具,可以在某些情况下简化程序的设计和实现,但在使用时需要谨慎考虑其影响,并权衡使用反射带来的好处和开销。
为什么选择spring来开发
选择Spring框架进行Java开发,主要是因为它带来了以下几个核心优势:
- 轻量级与非侵入式设计:Spring框架本身非常轻量,对应用的侵入性小。这意味着你可以在不修改代码的情况下轻易地添加或移除Spring框架,且应用的组件不需要直接依赖于Spring API。
- 控制反转(IoC)与依赖注入(DI):Spring通过IoC容器管理对象的生命周期和依赖关系,使得组件之间的耦合度大大降低。开发者不再需要手动创建和管理对象,而是由容器负责实例化、配置和管理对象之间的依赖关系,这促进了松耦合的设计。
- 面向切面编程(AOP):Spring提供了强大的AOP支持,允许将横切关注点(如日志记录、安全、事务管理)从业务逻辑中分离出来,从而实现更清晰、更易于维护的代码结构。
- 事务管理:Spring提供了对声明式事务管理的支持,使得开发者能够以更简洁的方式处理复杂的事务逻辑,无需在代码中手动控制事务边界。
- 集成能力:Spring框架能够很好地与其他Java EE技术栈集成,如Hibernate、MyBatis等ORM工具,以及各种Web框架(如Spring MVC),并且对JMS、JPA等也有良好的支持。此外,Spring Boot的出现进一步简化了这一过程,通过自动配置和最少的手动配置即可快速启动和运行应用。
- 测试友好:Spring框架设计时充分考虑了测试的需求,提供了Mock对象和测试上下文等功能,便于进行单元测试和集成测试。
- 社区与生态:Spring拥有庞大的开发者社区和丰富的生态系统,这意味着大量的文档、教程、第三方库和插件可用,能快速解决开发过程中遇到的问题。
- 持续更新与支持:Spring框架由Pivotal团队(现VMware)积极维护,不断更新以适应最新的技术趋势,确保其长期的稳定性和先进性。
综上所述,Spring不仅简化了企业级Java应用的开发,还提高了开发效率,降低了维护成本,因此成为了Java开发领域的首选框架之一
MySQL数据库索引有哪些
MySQL数据库支持多种类型的索引来适应不同的查询需求和数据结构,以下是一些主要的索引类型:
-
主键索引(Primary Key Index):
- 这是一种特殊的唯一索引,不允许有空值。
- 每个表只能有一个主键索引,用于唯一标识表中的每一行数据。
- 主键索引自动创建于定义为主键的列上。。
-
唯一索引(Unique Index):
- 确保索引列的值是唯一的,但允许有空值(NULL)。
- 可以用于非主键列,防止插入重复值。
- 可以创建多个唯一索引在一个表中。
-
普通索引(Regular or Single Column Index):
- 基础的索引类型,用于提高查询速度。
- 可以创建在单个列上,没有唯一性限制。
-
复合索引(Composite or Multi-Column Index):
- 也称作多列索引,可以在多个列上创建。
- 查询时只有当查询条件使用了复合索引中的第一个字段(及后续连续字段)时,索引才会生效,遵循最左前缀原则。
-
全文索引(Full-text Index):
- 用于全文搜索,可以高效地对文本内容进行关键词搜索。
- 适用于大文本字段,如文章内容、评论等。
- MySQL中的InnoDB存储引擎支持全文索引。
-
空间索引(Spatial Index):
- 针对空间数据类型(如GEOMETRY, POINT, LINESTRING, POLYGON)建立的索引。
- 用于高效地执行空间数据相关的查询操作,如位置查找、距离计算等。
-
InnoDB表的聚簇索引(Clustered Index):
- InnoDB存储引擎的表默认有一个聚簇索引,通常是主键索引。
- 聚簇索引决定了数据行在表中的物理存储顺序,叶子节点包含实际的数据行。
-
非聚簇索引(Secondary Index或Non-Clustered Index):
- 除了聚簇索引外的其他索引类型都可视为非聚簇索引。
- 非聚簇索引的叶子节点包含指向实际数据行的指针或行ID。
正确选择和使用索引对于优化数据库性能至关重要,但同时需要注意,过度或不当的索引设计会导致额外的存储开销、降低写入性能以及增加维护成本。因此,在设计索引时需要综合考虑数据的查询模式、表的大小、数据更新频率等因素。
Java的动态代理
程序为什么需要代理?代理长什么样?
对象如果嫌自己身上的事太多的话,可以通过代理来转移部分职责。
对象有什么方法想要被代理,代理就一定要有对应的方法。
代理就是帮你做一些增强
代理分为动态代理和静态代理
动态代理又分为 JDK动态代理(AOP) 和 CGLIB动态代理
JDK动态代理和CGLIB动态代理的区别
JDK动态代理
JDK 动态代理是Java提供的一种基于接口的代理方式。它的核心在于 java.lang.reflect.Proxy
类,通过动态创建代理类来实现对目标对象的代理。JDK 动态代理要求目标类必须实现一个或多个接口,然后通过 Proxy 类的 newProxyInstance
方法生成代理对象。代理对象实现了目标接口,当调用代理对象的方法时,实际上是调用了 InvocationHandler 接口的实现类的 invoke 方法。
特点:
- 基于接口的代理。
- 使用
java.lang.reflect.Proxy
类和InvocationHandler
接口。 - 在运行时动态生成代理类。
适用场景:
- 适用于代理实现了接口的类。
- 常用于AOP(面向切面编程)等需要基于接口进行代理的情况。
优点:
- 简单易用。
- 无需依赖第三方库,JDK自带支持。
- 生成的代理类性能较高。
缺点:
- 只能代理实现了接口的类。
- 如果目标类没有实现接口,则无法使用JDK动态代理。
CGLIB动态代理
CGLIB(Code Generation Library)是一个强大的,高性能的代码生成类库,用于在运行时扩展Java类与实现Java接口。与JDK动态代理不同,CGLIB动态代理不要求目标类实现接口,它通过创建目标类的子类来作为代理。CGLIB通过扩展目标类并重写其中的方法来实现代理功能。
特点:
- 基于子类的代理。
- 使用CGLIB库(如
net.sf.cglib.proxy
)。 - 通过生成目标类的子类来实现代理,使用字节码操作库如ASM动态生成字节码。
适用场景:
- 适用于代理没有实现接口的类。
- 常用于需要代理具体类的场景,例如需要拦截
final
类或方法的调用。
优点:
- 能够代理没有实现接口的类。
- 灵活性更高,可以代理任何普通类。
缺点:
- 需要依赖CGLIB库。
- 生成的代理类性能略低于JDK动态代理。
- 不能代理
final
类和final
方法。
总结区别
- 代理对象的生成方式:
- JDK动态代理基于接口,使用
java.lang.reflect.Proxy
。 - CGLIB动态代理基于子类,使用CGLIB库。
- JDK动态代理基于接口,使用
- 代理目标的限制:
- JDK动态代理只能代理实现了接口的类。
- CGLIB动态代理可以代理没有实现接口的类,但不能代理
final
类和方法。
- 依赖性:
- JDK动态代理不需要额外的库支持,JDK自带。
- CGLIB动态代理需要依赖CGLIB库。
- 性能:
- JDK动态代理在大多数情况下性能更好。
- CGLIB动态代理在一些情况下可能稍慢,但灵活性更高。
sql约束
SQL约束是在创建数据库表时用于强制执行数据规则的机制,这些规则确保了数据的准确性和一致性。以下是几种常见的SQL约束类型及其用途:
- 主键约束(PRIMARY KEY)
- 作用:用于唯一标识表中的每一行记录。主键列的值必须是唯一的,且不能为NULL。
- 示例:
CREATE TABLE Users (UserID INT PRIMARY KEY, ...);
- 外键约束(FOREIGN KEY)
- 作用:建立两个表之间的关联,确保一个表中的数据引用另一表中的有效数据。
- 示例:
CREATE TABLE Orders (OrderID INT, UserID INT, FOREIGN KEY (UserID) REFERENCES Users(UserID));
- 唯一约束(UNIQUE)
- 作用:确保某列或多列的组合值在表中是唯一的,但允许NULL值(除非是复合唯一约束的一部分)。
- 示例:
CREATE TABLE Products (ProductCode CHAR(5) UNIQUE, ...);
- 非空约束(NOT NULL)
- 作用:强制指定的列不允许有NULL值,必须在插入或更新时提供有效数据。
- 示例:
CREATE TABLE Employees (EmployeeID INT, Name VARCHAR(50) NOT NULL, ...);
- 默认约束(DEFAULT)
- 作用:为列指定一个默认值,当插入新记录时,如果没有为该列提供值,则自动使用默认值。
- 示例:
CREATE TABLE Orders (OrderID INT, OrderDate DATE DEFAULT CURRENT_DATE, ...);
- 检查约束(CHECK)
- 作用:限制列中的数据满足特定条件,比如范围、格式或基于其他列的条件。
- 示例:
CREATE TABLE Employees (Age INT CHECK (Age >= 18), ...);
这些约束可以在创建表时通过CREATE TABLE
语句定义,也可以在表创建后通过ALTER TABLE
语句追加。通过合理使用这些约束,可以有效维护数据库的完整性,减少数据错误,确保数据质量。
数据库索引失效的场景
数据库索引失效是指在本应利用索引来加速查询的情况下,数据库管理系统未有效利用索引,转而进行全表扫描或采取其他低效策略,导致查询性能下降。以下是一些常见的索引失效场景:
-
不使用索引列进行查询:如果查询条件没有涉及任何索引列,数据库将无法利用索引进行高效查找。
-
联合索引中断:对于复合索引(联合索引),如果查询条件没有遵循最左前缀原则,即没有从索引的第一个字段开始进行条件匹配,后续字段的索引将无法使用。
-
数据类型不匹配:查询条件中使用的数据类型与索引列的数据类型不一致,导致数据库无法正确应用索引。
-
查询条件使用函数或表达式:对索引列应用函数或表达式操作(如
SUBSTR(column)
或column + 1
)时,索引通常无法被直接利用。 -
低选择性列:如果索引列的值高度重复(选择性低),数据库可能认为全表扫描比使用索引更高效。
-
前模糊查询:在
LIKE
操作中,如果通配符%
出现在字符串的开始位置(如LIKE '%abc'
),索引通常无法发挥作用。 -
OR条件中的索引使用:在
OR
连接的条件中,如果两边的列都有索引,但数据库优化器可能无法有效利用这些索引。 -
索引列上的计算或比较运算符不支持:使用某些数据库不支持利用索引进行优化的运算符(如
NOT IN
、某些特定的BETWEEN
用法)。 -
隐式类型转换:当查询中的变量类型与索引列类型不匹配,导致数据库进行隐式类型转换时,索引可能无法使用。
-
索引字段参与了计算或比较:如果查询条件中索引列不是直接比较,而是参与了算术运算或函数调用,索引可能无法利用。
-
大范围扫描:当查询条件覆盖了索引列的大部分值,导致筛选效果不明显,数据库可能会放弃使用索引。
了解并避免这些场景有助于保持数据库查询的高性能。在设计查询和索引策略时,考虑这些因素是非常重要的。
zookeeper集群中节点的角色
Zookeeper就是一个文件系统 + 监听通知机制
- Leader:Master主节点
- Follower (默认的从节点):从节点,参与选举全新的Leader
参与投票,有机会成为leader
- Observer:从节点,不参与投票
不参与投票,也不可能成为leader
- Looking:正在找Leader节点
当主节点挂掉以后,从节点立即转变Looking,寻找新的主节点,当主节点找到以后,从节点也会变为Follower 或者 Observer
zookeeper集群
为什么要有Zookeeper集群?Zookeeper他是对整个大数据集群的管理,如果单个zk节点出现问题,那么这个集权也有可能瘫痪,所以必须对zk进行集群化。
好处:
1.解决单点故障问题
2.提高高可靠能力
3.增加并发能力(因为zk是分布式文件系统,需要强一致性满足的CP)
zookeeper的选举
当 Zookeeper 集群中的一台服务器出现以下两种情况之一时,需要进入 Leader 选举
。
- 情形1: 集群在启动的过程中,需要选举Leader
- 情形2: 集群正常启动后,leader因故障挂掉了,需要选举Leader
- 情形3: 集群中的Follower数量不足以通过半数检验,Leader会挂掉自己,选举新leader
- 情景4: 集群正常运行,新增的都是Follower
启动时期的 Leader 选举
假设一个 Zookeeper 集群中有5台服务器,id从1到5编号,并且它们都是最新启动的,没有历史数据。

集群刚启动选举过程
假设服务器依次启动,我们来分析一下选举过程:
(1)服务器1启动
发起一次选举,服务器1投自己一票,此时服务器1票数一票,不够半数以上(3票),选举无法完成。
投票结果:服务器1为1票。
服务器1状态保持为LOOKING
。
(2)服务器2启动
发起一次选举,服务器1和2分别投自己一票,此时服务器1发现服务器2的id比自己大,更改选票投给服务器2。
投票结果:服务器1为0票,服务器2为2票。 不满足半数原则
服务器1,2状态保持LOOKING
(3)服务器3启动
发起一次选举,服务器1、2、3先投自己一票,然后因为服务器3的id最大,两者更改选票投给为服务器3;
投票结果:服务器1为0票,服务器2为0票,服务器3为3票。此时服务器3的票数已经超过半数(3票),服务器3当选Leader
。
服务器1,2更改状态为FOLLOWING
,服务器3更改状态为LEADING
。
(4)服务器4启动
发起一次选举,此时服务器1,2,3已经不是LOOKING 状态,不会更改选票信息。交换选票信息结果:服务器3为3票,服务器4为1票。此时服务器4服从多数,更改选票信息为服务器3。
服务器4并更改状态为FOLLOWING
。
(5)服务器5启动
与服务器4一样投票给3,此时服务器3一共5票,服务器5为0票。
服务器5并更改状态为FOLLOWING
。
最终的结果:
服务器3是 Leader
,状态为 LEADING
;其余服务器是 Follower
,状态为 FOLLOWING
。
运行时期的Leader选举
在 Zookeeper运行期间 Leader
和 非 Leader
各司其职,当有非 Leader 服务器宕机或加入不会影响 Leader,但是一旦 Leader 服务器挂了,那么整个 Zookeeper 集群将暂停对外服务,会触发新一轮的选举。
初始状态下服务器3当选为Leader
,假设现在服务器3故障宕机了,此时每个服务器上zxid可能都不一样,server1为99,server2为102,server4为100,server5为101
集群 Leader 节点故障
运行期选举与初始状态投票过程基本类似,大致可以分为以下几个步骤:
(1)状态变更。Leader 故障后,余下的非 Observer
服务器都会将自己的服务器状态变更为LOOKING
,然后开始进入Leader选举过程
。
(2)每个Server会发出投票。
(3)接收来自各个服务器的投票,如果其他服务器的数据比自己的新会改投票。
(4)处理和统计投票,每一轮投票结束后都会统计投票,超过半数即可当选。
(5)改变服务器的状态,宣布当选。
话不多说先来一张图:
运行器 Leader 故障后选举流程
(1)第一次投票,每台机器都会将票投给自己。
(2)接着每台机器都会将自己的投票发给其他机器,如果发现其他机器的zxid比自己大,那么就需要改投票重新投一次。比如server1 收到了三张票,发现server2的xzid为102,pk一下发现自己输了,后面果断改投票选server2为老大。
选举机制中涉及到的核心概念
敲黑板了,这些概念是面试必考的。
(1)Server id(或sid):服务器ID
比如有三台服务器,编号分别是1,2,3。编号越大在选择算法中的权重越大,比如初始化启动时就是根据服务器ID进行比较。
(2)Zxid:事务ID
服务器中存放的数据的事务ID,值越大说明数据越新,在选举算法中数据越新权重越大。
(3)Epoch:逻辑时钟
也叫投票的次数,同一轮投票过程中的逻辑时钟值是相同的,每投完一次票这个数据就会增加。
(4)Server状态:选举状态
LOOKING
,竞选状态。
FOLLOWING
,随从状态,同步leader状态,参与投票。
OBSERVING
,观察状态,同步leader状态,不参与投票。
LEADING
,领导者状态。
总结
(1)Zookeeper 选举会发生在服务器初始状态和运行状态下。
(2)初始状态下会根据服务器sid的编号对比,编号越大权值越大,投票过半数即可选出Leader。
(3)Leader 故障会触发新一轮选举,zxid
代表数据越新,权值也就越大。
(4)在运行期选举还可能会遇到脑裂的情况,大家可以自行学习。
数据库连接池的原理
1、数据库连接是一件费时的操作,连接池可以使多个操作共享一个连接。
2、数据库连接池的基本思想就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。我们可以通过设定连接池最大连接数来防止系统无尽的与数据库连接。更为重要的是我们可以通过连接池的管理机制监视数据库的连接的数量、使用情况,为系统开发,测试及性能调整提供依据。
3、使用连接池是为了提高对数据库连接资源的管理
Mybaits的缓存策略
MyBatis是一个流行的持久层框架,它提供了多种缓存策略来优化数据库访问性能。以下是 MyBatis 支持的主要缓存策略:
-
Session 缓存(Local Cache):
- 默认情况下,MyBatis 的缓存策略是基于 SQLSession 的本地缓存。当同一个 SQLSession 执行相同的查询时,第一次查询的结果会被缓存到本地缓存中,后续的相同查询可以直接从本地缓存中获取结果,而不需要再次查询数据库。
-
Statement 缓存:
- 如果 SQLSession 的本地缓存不够用或者需要跨 SQLSession 共享缓存,可以开启 Statement 缓存。Statement 缓存是全局的,它缓存了 SQL 语句的查询结果和相应的参数,从而避免了每次查询都需要重新编译和执行 SQL 语句的开销。
-
二级缓存(Second Level Cache):
- 二级缓存是 MyBatis 提供的全局缓存机制,可以跨 SQLSession 和线程使用。当开启二级缓存时,查询的结果会被缓存到二级缓存中,下次查询相同的数据时,可以直接从二级缓存中获取,而不必再执行 SQL 查询。二级缓存是基于 namespace 级别的,每个 namespace 拥有独立的二级缓存。
-
缓存策略配置:
-
在 MyBatis 的配置文件中,可以通过
<cache>
元素来配置缓存策略。常见的配置属性包括:
eviction
:缓存淘汰策略,如 LRU(最近最少使用)、FIFO(先进先出)、或默认的 NONE(不淘汰)。flushInterval
:刷新缓存间隔,即多长时间刷新一次缓存。size
:缓存的最大条目数限制。readOnly
:是否只读缓存,用于提升并发性能。
-
-
缓存注解:
- MyBatis 也提供了注解方式来配置缓存,如
@CacheNamespace
和@CacheNamespaceRef
注解用于配置和引用二级缓存。
- MyBatis 也提供了注解方式来配置缓存,如
选择合适的缓存策略取决于应用的具体需求和访问模式。一般来说,Session 缓存适合对数据更新频繁的场景,而二级缓存适合对读取频繁、更新较少的场景。正确配置缓存可以显著提升 MyBatis 应用的性能和并发能力。
数据库的三范式
数据库的三范式(Third Normal Form,3NF)是关系数据库设计中的一种标准化方法,旨在消除数据中的冗余,并确保数据的结构清晰和有效。三范式建立在第二范式(2NF)的基础上,它的目标是进一步消除数据冗余。
三范式的要求包括:
- 第一范式(1NF):确保每个列都是原子性的,即每个列都不可再分。
- 第二范式(2NF):在1NF的基础上,消除非主键列对部分主键的依赖。这意味着每个非主键列必须完全依赖于主键,而不是仅依赖于主键的一部分。
- 第三范式(3NF):在2NF的基础上,消除非主键列之间的传递依赖。换句话说,任何非主键列都不能依赖于其他非主键列。
三范式的目标是通过逐步分解数据,将其规范化为更小、更简单的组成部分,以减少数据冗余并提高数据的一致性和有效性。然而,有时为了遵循三范式,可能需要对数据进行更多的分解,这可能会增加查询的复杂性和性能开销。因此,在设计数据库时,需要权衡规范化和性能之间的取舍。
nginx负载均衡·和springcloud的负载均衡
Nginx和Spring Cloud都提供了负载均衡的解决方案,但它们的实现方式和用途略有不同。
1. Nginx负载均衡:
Nginx是一款高性能的开源反向代理服务器和负载均衡器,它可以通过代理和转发来管理网络流量,并将请求分发到多个后端服务器上。
在Nginx中实现负载均衡通常使用其内置的负载均衡模块,如nginx_http_upstream_module。通过配置Nginx,可以定义多个后端服务器,以及负载均衡的策略,例如轮询、IP哈希、最少连接数等。
Nginx负载均衡通常用于前端应用的负载均衡、反向代理、静态资源的缓存和分发等场景,是一个高效、灵活的解决方案。
2. Spring Cloud的负载均衡:
Spring Cloud是一个基于Spring Boot的微服务框架,提供了丰富的分布式系统开发工具和解决方案。
在Spring Cloud中,负载均衡通常通过Netflix Ribbon来实现。Ribbon是一个客户端负载均衡器,它可以集成到Spring Cloud的服务消费者中,根据预定义的负载均衡策略,自动选择合适的服务实例。
Spring Cloud的负载均衡通常用于微服务架构中,服务间的调用和通信。通过Ribbon,可以实现负载均衡、故障转移、重试等功能,从而提高微服务系统的可靠性和可用性。
总的来说,Nginx 和Spring Cloud都提供了负载均衡的解决方案,但它们的使用场景略有不同。Nginx通常用于前端应用和反向代理的负载均衡,而Spring Cloud通常用于微服务架构中服务间通信的负载均衡。选择合适的负载均衡解决方案取决于具体的应用场景和需求。
线程池的执行流程
线程池的执行流程主要可以分为以下几个步骤:
-
初始化:线程池在创建时会初始化一些核心参数,比如线程池中核心线程数(corePoolSize)、最大线程数(maximumPoolSize)、线程空闲时间(keepAliveTime)、任务队列(WorkQueue)等。这些参数通常在构建线程池时通过参数设定。
-
任务提交:当有新任务通过
submit
或execute
方法提交给线程池时,线程池会执行以下逻辑:- 判断当前活动线程数:首先检查当前活跃的线程数是否小于核心线程数(corePoolSize)。如果小于,则直接创建一个新的工作线程来执行此任务。
- 任务队列:如果当前活跃线程数等于或大于核心线程数,线程池会尝试将任务添加到任务队列中。如果队列未满,则任务会被放入队列等待执行;如果队列已满,则继续下一步判断。
- 扩大线程池:如果队列已满,并且当前线程数小于最大线程数(maximumPoolSize),线程池会创建新的线程来执行任务,直到达到最大线程数。
- 拒绝策略:如果队列已满,且线程数已经达到最大值,线程池将触发其预设的拒绝策略(如
AbortPolicy
,CallerRunsPolicy
,DiscardPolicy
,DiscardOldestPolicy
等),决定如何处理这个超出能力的任务,比如抛出异常、直接在调用者线程中执行任务或丢弃任务等。
-
线程执行与回收:
- 任务执行:线程池中的线程会不断地从任务队列中取出任务并执行。
- 线程空闲:当线程池中的线程空闲(无任务可执行)时,如果当前线程数超过核心线程数,并且空闲时间达到了设定的keepAliveTime,那么这些额外的线程会被终止,直至线程数量降到核心线程数。
- 线程复用:一旦有新的任务提交,线程池会优先使用现有的空闲线程,而不是创建新的线程,以此达到资源复用的目的。
-
关闭线程池:线程池可以通过调用
shutdown
或shutdownNow
方法进行关闭。shutdown
会等待所有已提交的任务完成后再关闭线程池,而shutdownNow
则会尝试停止所有正在执行的任务,并立即返回待处理的任务列表。
线程池通过上述流程实现了任务的高效调度和线程资源的合理分配,有效避免了线程的频繁创建与销毁带来的性能开销,提升了系统的整体性能。
redis的淘汰策略
Redis的淘汰策略是指在内存不足时,决定要删除哪些键值对释放空间的策略。Redis目前支持以下几种淘汰策略:
- No Eviction (无淘汰策略):
- 如果设置了该策略,在内存不足时,Redis会返回错误给写操作(例如SET、LPUSH等),表示无法执行该操作,这通常是因为内存已经用完。
- All Keys Random (随机淘汰策略):
- 这是最简单的淘汰策略。Redis会从所有的键中随机选择一个进行淘汰以释放空间。虽然简单,但可能会导致有用数据被意外删除。
- Volatile LRU (基于LRU的过期键淘汰策略):
- Redis会在设置了过期时间的键中选择最近最少使用(LRU)的键进行淘汰。这种策略确保了只有过期的键才会被淘汰,而不是随机地删除键。
- Volatile TTL (基于TTL的过期键淘汰策略):
- Redis会优先淘汰那些剩余时间(TTL)较短的键,以确保尽快释放空间。这种策略对于内存管理更加高效,因为它确保了未来不久就会过期的键被优先处理。
- Volatile Random (基于过期键的随机淘汰策略):
- Redis会在设置了过期时间的键中随机选择一个进行淘汰。这种策略介于随机淘汰和基于TTL的淘汰之间,权衡了简单性和效率。
- Volatile LFU (基于LFU的过期键淘汰策略):
- 最不经常使用(LFU)策略会优先删除使用频率最低的过期键。这种策略适合那些被访问不频繁但又设置了过期时间的键。
在Redis中,可以通过配置文件或者在运行时使用命令来选择和设置这些淘汰策略。选择合适的淘汰策略取决于你的应用场景和对数据访问模式的理解,以确保在内存不足时能够最大化地保留重要数据。
CAP
在Java领域中提到CAP,通常是指分布式系统设计中的CAP定理(Brewer’s Theorem),适用于所有的分布式系统,而不仅仅局限于Java。这个定理阐述了在设计分布式系统时需要权衡的三个方面:
- 一致性(Consistency):所有节点在同一时间看到的数据是一致的。这意味着,一旦数据被更新,所有后续的访问都会返回最新的值。在强一致性模型中,系统的更新操作需要在全网中同步完成,以确保所有节点上的数据立即保持一致。
- 可用性(Availability):系统能够对每个请求都给出非错误响应,但不保证返回的是最新的数据。换句话说,即使部分系统出现故障,其他部分仍需能够继续响应查询。
- 分区容错性(Partition tolerance):尽管网络可能存在消息丢失或延迟等情况,系统仍能继续运行。这是分布式系统中不可避免的问题,因为网络分区是实际会发生的情况。
CAP定理表明,在一个分布式系统中,设计者不能同时实现这三项保证,只能选择其中的两项。例如,你可能选择牺牲一致性来保证高可用性和分区容错性,或者选择放弃可用性以确保强一致性和分区容错性。在Java中实现分布式应用时,开发者需要根据业务需求来决定如何在这三者之间做出取舍,并选择合适的中间件、数据库或算法来支持所选的设计策略。例如,某些NoSQL数据库如Cassandra倾向于AP(可用性和分区容错性),而传统关系型数据库往往追求CP(一致性与分区容错性)。
Http协议的状态码
200 OK客户端请求成功
301 Moved Permanently(永久移除),请求的URL已移走。Response 中应该包含一个LocationURL,说明资源现在所处的位置 302 found 重定向
400 Bad Request 客户端请求有语法错误,不能被服务器所理解
401 Unauthorized请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
403 Forbidden服务器收到请求,但是拒绝提供服务
404 NotFound请求资源不存在,eg:输入了错误的URL
500 InternalServerError服务器发生不可预期的错误
503 ServerUnavailable服务器当前不能处理客户端的请求,一段时间后可能恢复正常
servlet的生命周期
Servlet 的生命周期可以分为以下几个阶段:
- 加载和实例化:
- 当Servlet容器(如Tomcat)启动时,会加载并实例化Web应用程序中配置的所有Servlet。这通常发生在第一次请求到达Servlet时,或者在容器启动时预加载。
- 初始化(Initialization):
- 容器创建Servlet实例后,会调用Servlet的
init()
方法进行初始化。在初始化阶段,Servlet可以执行一些必要的初始化操作,如读取配置文件、建立数据库连接、加载资源等。init()
方法只会被调用一次,在Servlet的生命周期中,它负责初始化整个Servlet实例。
- 容器创建Servlet实例后,会调用Servlet的
- 请求处理:
- 一旦Servlet被初始化完成,它可以接收客户端的请求。每当有请求到达时,容器会调用Servlet的
service()
方法来处理请求。在service()
方法内部,根据请求类型(GET、POST等),会调用对应的处理方法(如doGet()
、doPost()
等)来处理请求并生成响应。
- 一旦Servlet被初始化完成,它可以接收客户端的请求。每当有请求到达时,容器会调用Servlet的
- 销毁(Destruction):
- 当Servlet容器决定将Servlet实例从服务中移除时(通常是因为Web应用被停止或重新部署),容器会调用Servlet的
destroy()
方法来进行销毁操作。在destroy()
方法内部,Servlet可以释放资源、关闭数据库连接、清理内存等操作。与init()
方法对应,destroy()
方法也只会被调用一次。
- 当Servlet容器决定将Servlet实例从服务中移除时(通常是因为Web应用被停止或重新部署),容器会调用Servlet的
在整个生命周期中,Servlet实例只有一个,但是service()
方法可以同时处理多个并发请求(每个请求会在不同的线程中处理)。这种设计使得Servlet能够高效地处理多个客户端请求,同时保证了线程安全(Servlet实现本身需要保证线程安全)。
总结来说,Servlet的生命周期包括加载和实例化、初始化、请求处理和销毁四个主要阶段,每个阶段都有特定的方法负责执行相应的任务,确保Servlet能够正确地接收和处理客户端的请求。
Elasticsearch的倒排索引
Elasticsearch(简称ES)是一个开源的分布式搜索引擎,它基于Apache Lucene库构建,特别适用于全文本搜索和分析。在Elasticsearch中,倒排索引(Inverted Index)是其核心数据结构之一,对于理解ES如何高效地执行搜索操作至关重要。
倒排索引的基本概念
倒排索引是一种将文档中的词汇项与包含这些词汇项的文档位置建立关联的数据结构。与传统的正向索引(文档到词汇的映射)不同,倒排索引是以词项为中心的索引方式,它的基本结构可以简化为一个映射表:单词 -> 文档ID列表
。这意味着,对于文档集合中的每一个唯一词项,倒排索引都会记录下所有包含该词项的文档的位置信息。
倒排索引的结构
倒排索引主要由两部分组成:
- 词项字典(Term Dictionary):这是一个有序的词汇列表,每个词汇项指向一个包含该词项出现的文档信息的posting list。通过使用前缀压缩和字典树等技术,词项字典能够高效地支持快速查找。
- Posting List(文档列表):对于词项字典中的每个词项,都有一个对应的posting list,它记录了所有包含该词项的文档ID以及词频、位置等信息。这个列表可以进一步优化,比如通过跳跃指针来加速对特定文档ID的搜索。
倒排索引的工作原理
当用户发起一个查询时,Elasticsearch首先会解析查询语句,将查询关键词转换成词项,然后利用词项字典快速定位到相应的posting list。通过对posting list的操作(如交集、并集),Elasticsearch能够快速确定哪些文档匹配了查询条件,并根据相关性评分(如TF-IDF)对这些文档进行排序,最终返回给用户最相关的搜索结果。
优势
- 高效搜索:倒排索引允许几乎实时地对大规模数据集进行全文搜索,因为它直接从关键词跳转到相关文档,避免了全表扫描。
- 支持复杂查询:通过操作posting list,ES能容易地实现AND、OR、NOT等布尔逻辑查询,以及短语搜索、同义词扩展等高级功能。
- 易于扩展和更新:新增或删除文档时,只需对相关词项的posting list进行修改,不影响整个索引结构。
总之,倒排索引是Elasticsearch实现高性能、高可扩展性和丰富搜索功能的基础,是其处理和检索大量文本数据的关键技术。
JVM内存划分
JVM 将内存划分为 6 个部分: PC寄存器(也叫程序计数器)、虚拟机栈,堆、方法区、运行时常量池,本地方法栈。
PC寄存器(程序计数器):
用于**记录当前线程运行时的位置,**每一个线程都有一个独立的程序计数器,线程的阻塞,恢复,挂起等一系列操作都需要程序计数器,因此必须是线程私有的。
Java虚拟机栈:
在创建线程时创建的,用来存储栈帧,因此也是线程私有的,java程序中的方法在执行时,会创建一个栈帧,用于存储方法运行时的临时数据和中间结果,包括局部变量表,操作数栈,动态链接,方法出口等信息。这些栈帧就存储在栈中,如果栈深度大于虚拟机允许的最大深度,则抛出StackOverError 异常(发生在程序运行时栈内存溢出时)
局部变量表:方法的局部变量表,在编译时就写入了class文件。
操作数栈:
int x = 1 ; 就需要将1 压入操作数栈,再将1 赋值给变量 x。
Java 堆:
Java 堆 被所有线程共享,堆的主要作用就是存储对象,如果堆空间不够,但扩展又不能申请到足够的内存时,则抛出OutOfMemoryError异常。
方法区:
方法区被各个线程共享,用于存储静态变量,运行时常量池等信息。
本地方法栈:
本地方法栈的主要作用就是支持native方法 ,比如在 Java 中调用C/C++
如何判断一个对象是不是垃圾?
引用计数法 可达性分析法
引用计数法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器加一,反之每当一个引用失效时,计数器减一,当计数器为 0 时吗,则表示对象不被引用。 (缺点:如果有两个垃圾对象互相引用的话,那么他们的引用计数不可能为0 所以不会被回收)
可达性分析法
设立若干个对象( GC Root ),每个对象 都是一个子节点,当一个对象找不到根式,就认为对象不可达。这意味着它从一个称为“GC Roots”的集合开始,递归地遍历所有从GC Roots可直接或间接访问的对象。所有不可达(即没有任何引用链可以与之相连)的对象被标记为垃圾,可以被回收。
注意:谁可以作为GC roots
怎么回收垃圾 ?
标记-清除算法
复制算法
标记-整理算法
分代算法
3.1 标记—清除算法
遍历所有的GC Root,分别标记处可达的对象和不可达对象,然后将不可达的对象回收。 就是先标记所有的垃圾 然后进行清除
缺点是:效率低,回收得到的空间不连续 使得内存的使用率变得越来越低。
3.2 复制算法
将内存分为两块,每次只使用一块,当这一块内存满了之后,就将还存活的对象复制到另一块上,并且严格按照内存地址排列,然后把已使用的那块内存同一回收。
优点: 能够得到连续的内存空间
缺点: 浪费了一半内存 新生代中的两块幸存者区就是为了实现这个算法。
3.3 标记—整理算法
标记过程仍然与“标记-清除” 算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有村后的对象都向一端移动,然后直接清理掉端边界以外的内存,“标记- 整理” 算法 的示意如下图所示。
缺点:不适合高频率的执行。
一般当老年代的空间不足时,会触发一次FULL GC 这是就会做碎片整理工作。
3.4 、 分代 算法
在java 中,把内存中的对象按生命长短分为:
新生代:活不了多久就go die 了 ,比如局部变量
老年代: 老不死的 活得久但也会die 比如一些生命周期长的对象
永久代: 千年王八,万年龟,不死,比如加载的class信息
有一点需要注意,新生代和老年代存储在Java 虚拟机堆上;永久代存储在方法区上
事务的传播解决了什么问题
事务的传播行为(Transaction Propagation)主要是为了解决在复杂业务场景下,不同服务或方法之间相互调用时,如何管理事务边界的问题。具体来说,它定义了当一个事务方法被另一个事务方法调用时,这两个方法之间的事务应该如何协同工作。这包括决定是否创建新的事务、加入现有的事务、挂起当前事务或是要求外部事务必须存在等情况。事务传播行为确保了事务处理的一致性和隔离性,同时也提高了系统的灵活性和可维护性。
在Spring框架中,使用@Transactional注解确实是一种非常便捷的方式来管理事务。但是,仅仅添加这个注解是不够的,还需要确保以下几点:
- 配置了事务管理器:你必须在你的Spring配置中定义一个事务管理器(如
DataSourceTransactionManager
或JpaTransactionManager
),并将其与你的数据源关联。 - 启用事务注解:在配置类中使用
@EnableTransactionManagement
或者在XML配置中使用<tx:annotation-driven>
来启用基于注解的事务管理。 - 正确地使用@Transactional注解:你应该理解@Transactional注解的语义和它如何影响方法的调用。例如,你可能需要考虑传播行为、隔离级别、回滚规则等。
- 事务边界:确保事务逻辑在一个适当的方法内执行。如果一个方法内部调用了另一个也带有@Transactional的方法,你可能需要调整传播行为以达到预期的效果。
- 异常处理:确保你的代码能够抛出适当的异常,以便事务可以被正确地回滚。通常,未检查异常(如RuntimeException)会导致事务回滚,而检查异常则不会,除非它们被标记为回滚事务。
因此,虽然添加@Transactional注解是一个开始,但要使其正常工作,还需要进行一些额外的配置和理解其背后的工作机制。
主要的事务传播行为包括但不限于:
- REQUIRED: 这是最常见的设置。如果当前存在事务,则方法加入到当前事务中;如果不存在事务,则新建一个事务。
- REQUIRES_NEW: 总是启动一个新的事务,并且在新事务内执行。如果当前存在事务,则将当前事务挂起。
- SUPPORTS: 如果当前存在事务,则加入到该事务中;如果不存在事务,则以非事务方式执行。
- NOT_SUPPORTED: 总是以非事务方式执行,如果当前存在事务,则将事务挂起。
- MANDATORY: 必须在事务中运行,如果当前没有事务,则抛出异常。
- NEVER: 绝对不能在事务中运行,如果当前存在事务,则抛出异常。
- NESTED: 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则其行为类似于REQUIRED。
通过合理配置事务的传播行为,开发者可以精确控制各个方法或服务调用时事务的开启与关闭,从而保证数据的完整性和一致性,同时也能提高系统的性能,避免不必要的事务开销。
volatile
在Java中,volatile
关键字主要用于多线程编程中,以确保变量在多个线程间的可见性和一定程度上的有序性。下面我将详细解释其用法以及在Java中的一些常见使用场景:
用法
- 变量修饰:
volatile
关键字用于声明变量,一般修饰成员变量,而不是局部变量,因为局部变量本身就具有线程安全性(每个线程有自己的副本)。 - 可见性保证: 当一个线程修改了一个
volatile
变量,其他线程能立即看到这个变化。这是因为volatile
变量的读写操作会直接与主内存交互,而不是线程的工作内存。 - 禁止指令重排序:
volatile
变量的读写操作前后会插入内存屏障,这会阻止编译器和处理器对这些操作进行重排序,从而确保操作的顺序。
使用场景
-
状态标志:
volatile
常用于表示一个状态标志,如一个线程是否应该继续运行的标志。例如,一个volatile boolean
变量可以用来作为线程的停止标志。Java
1public class WorkerThread { 2 private volatile boolean shouldRun = true; 3 4 public void run() { 5 while (shouldRun) { 6 // 执行任务 7 } 8 } 9 10 public void stop() { 11 shouldRun = false; 12 } 13}
-
双重检查锁定(Double-Checked Locking): 在实现线程安全的懒汉式单例模式时,
volatile
可以确保在实例创建过程中避免多次实例化,并且确保线程可见性。Java
1public class Singleton { 2 private static volatile Singleton instance; 3 4 private Singleton() {} 5 6 public static Singleton getInstance() { 7 if (instance == null) { 8 synchronized (Singleton.class) { 9 if (instance == null) { 10 instance = new Singleton(); 11 } 12 } 13 } 14 return instance; 15 } 16}
-
轻量级同步: 对于不需要复杂同步机制的简单状态标记,如信号量或标志位,
volatile
是一个较好的选择。它比synchronized
更轻量级,因为它不涉及线程的上下文切换和调度。 -
原子变量类的替代: 对于简单的操作,如读取和赋值,
volatile
可以提供线程安全性,但复合操作(如i++
)则需要使用AtomicInteger
等原子类来保证原子性。
虽然volatile
提供了可见性和一定程度的有序性,但它并不保证复合操作的原子性。如果需要复合操作的原子性,应该使用synchronized
关键字或Java并发库中的原子类,如AtomicInteger
、AtomicLong
等。
总之,volatile
关键字是Java中一种重要的多线程工具,它帮助开发者以较低的成本实现部分线程安全特性,但在使用时需要充分理解其限制。
volatile , CAS , synchronized
volatile
, CAS
(Compare and Swap), 和 synchronized
都是Java中用于实现多线程环境下原子操作和线程同步的关键字或机制。下面分别解释它们的用途和工作原理:
volatile
变量的可见性 禁止指令重排
volatile
关键字用于变量前,表示该变量的值可能会被不同线程修改,因此每次读取都应该从主内存中读取最新的值,而不是从线程的工作内存中读取可能过时的缓存副本。这保证了变量的可见性和一定程度的有序性(禁止指令重排序)。但是,volatile
不能保证原子性,即对复合操作的原子性不作保证。
ConcurrentHashmap使用到了volatile
一些原子类 例如AtomicLong,AtomicInteger使用了volatile
CAS (Compare and Swap)
CAS是一种无锁算法,用于在不需要锁的情况下实现多线程环境下的原子操作。CAS操作包含三个参数:内存位置V、预期原值A和新值B。当且仅当V的位置的值等于A时,CAS通过原子方式将V的位置的值更新为B,否则不做任何操作。这个操作由硬件层面支持,保证了原子性。
Java中的AtomicInteger
等原子类就是利用了CAS机制来实现线程安全的整型变量操作。
更新或删除节点时,ConcurrentHashMap
使用CAS来保证线程安全。
synchronized
synchronized
关键字用于创建互斥锁,它可以修饰方法或同步块,确保同一时刻只有一个线程可以访问被修饰的代码区域。对于方法而言,锁住的是当前对象或类;对于同步块而言,锁住的是指定的对象。synchronized
提供了比volatile
更高级别的同步,可以保证原子性、可见性和有序性,但它的性能开销通常比volatile
和CAS更高。
总结
- volatile 主要用于解决变量的可见性问题,但在复合操作上不能保证原子性。
- CAS 用于实现原子更新,避免锁的使用,提高并发性能,但在高并发下可能会有ABA问题(即在两次比较之间值被短暂改回原值,导致误判)。
- synchronized 提供了最全面的线程安全保证,但性能开销较大,适用于需要严格同步控制的场景。
选择哪种机制取决于具体的应用场景和对性能的要求。
原子类
Java中的原子类(Atomic Classes)是java.util.concurrent.atomic包中的一组类,用于在并发编程中提供对单一变量的线程安全操作。它们使用底层硬件的原子操作来保证变量的更新不会受到其他线程的干扰,从而避免了传统同步机制的开销。常见的原子类包括:
- AtomicBoolean: 用于对boolean值进行原子操作。
- AtomicInteger: 用于对int类型的值进行原子操作。
- AtomicLong: 用于对long类型的值进行原子操作。
- AtomicReference: 用于对引用类型的变量进行原子操作。
这些原子类提供了一组基本的原子操作方法,例如:
get()
: 获取当前值。set(value)
: 设置新值。lazySet(value)
: 最终设置新值,但是可能会有延迟。compareAndSet(expect, update)
: 如果当前值等于预期值,则以原子方式将其设置为更新值。getAndSet(newValue)
: 将当前值设置为新值,并返回旧值。
使用示例
以下是一个简单的AtomicInteger使用示例:
java复制代码import java.util.concurrent.atomic.AtomicInteger;public class AtomicExample {private static final AtomicInteger counter = new AtomicInteger(0);public static void main(String[] args) {// Increment counter and get the new valueint newValue = counter.incrementAndGet();System.out.println("New value: " + newValue);// Compare and setboolean updated = counter.compareAndSet(1, 2);System.out.println("Updated: " + updated);System.out.println("Current value: " + counter.get());}
}
优点
- 无锁并发: 原子类使用无锁的方式实现并发操作,从而提高了性能,尤其是在高并发场景下。
- 简单易用: 提供了简单的API,使得编程更加简洁。
缺点
- 限制性: 仅能对单一变量进行原子操作,无法替代所有的同步场景。
- 底层依赖: 底层依赖于CPU的原子指令,不同的硬件架构可能有不同的实现。
适用场景
-
计数器: 例如统计请求数、访问量等。
1. 计数器
场景
在高并发环境下统计访问次数或请求数,例如网站访问量、消息处理数量等。
解决方案
使用
AtomicInteger
或AtomicLong
可以确保计数操作的线程安全性,避免传统同步锁的性能开销。 -
状态管理: 例如在并发环境下管理共享状态。
场景
在多线程环境下管理共享状态,例如任务的执行状态、开关控制等。
解决方案
使用
AtomicBoolean
来管理布尔类型的状态,确保状态更新的原子性。 -
非阻塞算法: 实现CAS(Compare-And-Swap)算法,提高并发性能。
场景
实现高性能的非阻塞数据结构或算法,例如无锁队列、栈等。
解决方案
使用
AtomicReference
来实现无锁栈,确保在多线程环境下数据结构的原子性。
Redis缓存预热
Redis缓存预热是指在系统启动或某些特定时刻,将常用或重要的数据提前加载到缓存中,以便在系统运行过程中能够快速访问这些数据,从而提高系统性能和响应速度。缓存预热可以有效减少缓存未命中(Cache Miss)带来的延迟。
缓存预热的常见方法
- 启动时加载:
- 在系统启动时,批量加载一些常用的数据到缓存中。这些数据可以是通过数据库查询得到的。
- 这种方法适用于系统重启或服务重启时预热缓存。
- 定时任务:
- 通过定时任务定期更新缓存,将最新的数据加载到缓存中。
- 这种方法适用于需要定期更新的数据,确保缓存中的数据是最新的。
- 手动预热:
- 在系统发布或上线前,由运维人员或开发人员手动触发缓存预热操作。
- 这种方法适用于数据量较大且预热操作耗时较长的情况。
- 数据变更时加载:
- 在数据发生变更时(如数据库数据更新),触发缓存更新,将新的数据加载到缓存中。
- 这种方法适用于需要实时性较高的数据,确保缓存中的数据和数据库数据保持一致。
示例代码
以下是一个简单的缓存预热示例,使用Spring Boot和Redis:
java复制代码import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import java.util.List;@Component
public class CachePreheat {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowiredprivate DataService dataService;private static final String CACHE_KEY = "myData";// 系统启动时预热缓存@PostConstructpublic void preheatCacheOnStartup() {loadDataToCache();}// 定时任务定期预热缓存,每天凌晨2点执行@Scheduled(cron = "0 0 2 * * ?")public void preheatCacheScheduled() {loadDataToCache();}// 加载数据到缓存private void loadDataToCache() {List<MyData> dataList = dataService.getAllData();redisTemplate.opsForValue().set(CACHE_KEY, dataList);System.out.println("Cache preheated with " + dataList.size() + " items.");}
}
数据服务示例
java复制代码import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;@Service
public class DataService {// 模拟从数据库中获取数据public List<MyData> getAllData() {List<MyData> dataList = new ArrayList<>();// 假设从数据库中获取数据并填充到dataList中dataList.add(new MyData(1, "Data 1"));dataList.add(new MyData(2, "Data 2"));return dataList;}
}
数据模型示例
java复制代码public class MyData {private int id;private String name;public MyData(int id, String name) {this.id = id;this.name = name;}// getters and setters
}
注意事项
- 数据一致性:
- 确保缓存中的数据和数据库中的数据一致。可以通过缓存失效机制和数据变更时更新缓存来保证一致性。
- 缓存容量:
- 确保缓存的容量足够存储预热的数据。否则可能会导致缓存淘汰策略频繁触发,影响系统性能。
- 缓存失效策略:
- 根据业务需求设置合理的缓存失效策略,避免缓存中的数据长期不更新。
- 预热时机:
- 根据系统的特点和业务需求选择合适的预热时机,避免在高峰期进行缓存预热操作,影响系统性能。
缓存预热是提升系统性能的重要手段之一,通过提前加载常用数据,可以有效减少缓存未命中带来的性能开销,提高系统的响应速度和用户体验。
Linux常用命令
Linux系统提供了丰富的命令行工具,用于管理系统和执行各种任务。以下是一些常用的Linux命令及其详细解释和使用示例:
文件和目录管理
-
ls: 列出目录内容
ls # 列出当前目录下的文件和子目录 ls -l # 以长格式列出,包括文件的详细信息 ls -a # 列出所有文件,包括隐藏文件 ls -lh # 以人类可读的格式显示文件大小
-
cd: 更改目录
cd /path/to/directory # 切换到指定目录 cd .. # 返回上一级目录 cd ~ # 切换到用户主目录
-
pwd: 显示当前工作目录
pwd # 显示当前所在的目录路径
-
mkdir: 创建目录
mkdir new_directory # 创建新目录 mkdir -p /path/to/new_directory # 创建多级目录
-
rm: 删除文件或目录
rm file.txt # 删除文件 rm -r directory # 递归删除目录及其内容 rm -f file.txt # 强制删除文件,不提示确认 rm -rf directory # 递归强制删除目录及其内容
-
cp: 复制文件或目录
cp source_file destination_file # 复制文件 cp -r source_directory destination_directory # 递归复制目录
-
mv: 移动或重命名文件或目录
mv old_name new_name # 重命名文件或目录 mv file.txt /path/to/destination/ # 移动文件到指定目录
-
touch: 创建空文件或更新文件的时间戳
touch new_file.txt # 创建一个新文件
文件查看和编辑
-
cat: 查看文件内容
cat file.txt # 显示文件内容
-
less: 分页查看文件内容
less file.txt # 分页显示文件内容,使用q退出查看
-
head: 查看文件开头部分内容
head file.txt # 查看文件前10行 head -n 20 file.txt # 查看文件前20行
-
tail: 查看文件末尾部分内容
tail file.txt # 查看文件最后10行 tail -n 20 file.txt # 查看文件最后20行 tail -f file.txt # 实时跟踪文件末尾的新增内容,常用于日志文件
-
nano: 编辑文件的简单文本编辑器
nano file.txt # 打开文件进行编辑
-
vim: 强大的文本编辑器
vim file.txt # 打开文件进行编辑 # 在vim中按i进入插入模式进行编辑,按Esc退出插入模式,输入:wq保存并退出
系统管理
-
sudo: 以超级用户权限执行命令
sudo command # 以超级用户权限执行命令
-
apt-get: 包管理工具(适用于Debian及其衍生发行版)
sudo apt-get update # 更新包列表 sudo apt-get upgrade # 升级所有已安装的软件包 sudo apt-get install package_name # 安装软件包 sudo apt-get remove package_name # 移除软件包
-
yum: 包管理工具(适用于Red Hat及其衍生发行版)
sudo yum update # 更新所有包 sudo yum install package_name # 安装软件包 sudo yum remove package_name # 移除软件包
-
ps: 查看当前运行的进程
ps aux # 显示所有进程的详细信息 ps -ef # 另一种显示所有进程信息的格式
-
top: 动态显示系统进程
top # 实时显示系统进程信息,按q退出
-
kill: 终止进程
kill PID # 终止指定PID的进程 kill -9 PID # 强制终止指定PID的进程
-
df: 查看磁盘空间使用情况
df -h # 以人类可读的格式显示磁盘空间使用情况
-
du: 查看目录或文件的磁盘使用情况
du -sh /path/to/directory # 显示指定目录的总大小 du -h /path/to/directory # 递归显示目录及其子目录的大小
-
free: 查看内存使用情况
free -h # 以人类可读的格式显示内存使用情况
-
ifconfig: 查看或配置网络接口(需要安装net-tools包)
ifconfig # 显示网络接口信息
-
ip: 查看或配置网络接口(现代系统推荐使用)
ip addr show # 显示所有网络接口的信息 ip link set dev eth0 up # 启用网络接口 ip link set dev eth0 down # 禁用网络接口
-
ping: 测试网络连接
ping www.example.com # 向指定主机发送ICMP请求,测试网络连接
-
netstat: 查看网络连接、路由表、接口状态等(需要安装net-tools包)
netstat -tuln # 显示所有监听的端口
-
ss: 查看网络连接(现代系统推荐使用)
ss -tuln # 显示所有监听的端口
压缩与解压缩
-
tar: 处理tar归档文件
tar -cvf archive.tar file1 file2 # 创建tar归档文件 tar -xvf archive.tar # 解压tar归档文件 tar -czvf archive.tar.gz file1 file2 # 创建gz压缩的tar归档文件 tar -xzvf archive.tar.gz # 解压gz压缩的tar归档文件
-
zip/unzip: 处理zip文件
zip archive.zip file1 file2 # 创建zip文件 unzip archive.zip # 解压zip文件
搜索
-
find: 查找文件或目录
find /path/to/search -name "filename" # 按名称查找文件 find /path/to/search -type d -name "dirname" # 查找目录
-
grep: 在文件中搜索文本
grep "search_string" file.txt # 在文件中搜索字符串 grep -r "search_string" /path/to/search # 递归搜索目录中的字符串
这些命令是Linux系统中常用的一部分,掌握它们可以大大提高你的工作效率和系统管理能力。
top命令和top -c 命令的区别
top
和 top -c
是两个常用的命令,用于监控和显示Linux系统中当前运行的进程及其资源使用情况。它们之间的主要区别在于进程显示的详细程度。
top
命令
- 功能:默认情况下,
top
命令显示系统中运行的进程,并按 CPU 使用率排序。 - 显示:在进程列表中,
COMMAND
列仅显示可执行文件的名称,而不是完整的命令行。
示例输出:
yaml复制代码top - 10:24:43 up 10 days, 23:01, 2 users, load average: 0.00, 0.01, 0.05
Tasks: 123 total, 1 running, 122 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.3 us, 0.1 sy, 0.0 ni, 99.5 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 8174808 total, 1028944 free, 2734676 used, 4411188 buff/cache
KiB Swap: 4095996 total, 4095996 free, 0 used. 5099088 avail MemPID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND1923 root 20 0 1575076 210176 10444 S 1.3 2.6 53:13.65 Xorg2456 user 20 0 457664 35224 20248 S 0.7 0.4 2:17.75 gnome-shell1754 root 20 0 282912 70836 34508 S 0.3 0.9 10:33.67 upowerd2660 user 20 0 195692 12608 9496 S 0.3 0.2 1:04.95 gnome-terminal-
top -c
命令
- 功能:
top -c
命令与top
命令类似,但它在进程列表中显示的是完整的命令行(command line),而不仅仅是可执行文件的名称。 - 显示:在进程列表中,
COMMAND
列显示每个进程的完整命令行,包括执行该进程时使用的所有参数和选项。
示例输出:
yaml复制代码top - 10:24:43 up 10 days, 23:01, 2 users, load average: 0.00, 0.01, 0.05
Tasks: 123 total, 1 running, 122 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.3 us, 0.1 sy, 0.0 ni, 99.5 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 8174808 total, 1028944 free, 2734676 used, 4411188 buff/cache
KiB Swap: 4095996 total, 4095996 free, 0 used. 5099088 avail MemPID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND1923 root 20 0 1575076 210176 10444 S 1.3 2.6 53:13.65 /usr/lib/xorg/Xorg -core :0 -seat seat0 -auth /var/run/lightdm/root/:0 -nolis2456 user 20 0 457664 35224 20248 S 0.7 0.4 2:17.75 /usr/bin/gnome-shell1754 root 20 0 282912 70836 34508 S 0.3 0.9 10:33.67 /usr/lib/upower/upowerd2660 user 20 0 195692 12608 9496 S 0.3 0.2 1:04.95 /usr/lib/gnome-terminal-server
总结
top
:显示基本的进程信息,适合一般情况下快速查看系统资源使用情况。top -c
:显示详细的进程信息,包括完整的命令行,适合需要深入了解每个进程的运行参数和详细信息的场景。
小技巧: 我们可以输入 top 命令 然后按键盘 C 来在top 和 top -c 进行切换
yield
在Java中,yield
是Thread
类的一个静态方法,允许当前正在执行的线程让出CPU,以便其他等待线程能够获得执行的机会。yield
的作用是向线程调度器发出一个提示,表明当前线程愿意让出CPU时间片,但是不能保证一定会让出,也不能保证其他线程会立即获得执行。
使用场景
yield
方法主要用于调试和优化性能。在实际的生产环境中使用它的场景较少,因为它的行为依赖于线程调度器的实现,且不可预知。以下是一些可能的使用场景:
- 调试和测试:可以用来模拟多线程环境下的竞争情况,帮助发现并发问题。
- 减少资源竞争:在某些情况下,可以用来减少线程之间的资源竞争,优化性能。
- 临时让出CPU:在某些复杂计算中,临时让出CPU时间片,让其他优先级更高或重要的线程得以运行。
幂等性
一个幂等操作满足以下条件:
- 同一请求的重复执行:如果同一个操作请求被多次发送,对系统的状态只产生一次改变。
- 任意次序执行的结果相同:无论该操作请求被执行多少次,系统的状态都保持一致。
在设计API或者分布式系统时,考虑到幂等性是非常重要的,可以通过一些技术手段来实现,比如使用唯一请求标识符(request idempotency key)、状态检查和事务处理等方式来确保操作的幂等性。
-
客户端生成UUID:
- 每当客户端想要发起一个可能产生副作用的操作时(如创建资源或更新资源),它会生成一个唯一的UUID。
- 这个UUID会被包含在请求头或者请求体中发送给服务器。
-
服务器存储UUID:
- 服务器接收到带有UUID的请求后,首先检查这个UUID是否已经存在于某个存储中(如数据库或缓存)。
- 如果不存在,则继续处理请求,并将UUID与请求的结果相关联地存储起来。
- 如果存在,则表明这是一个重复的请求,直接返回之前处理的结果而不做任何额外处理。
-
响应处理:
- 无论请求是否被处理,服务器都会返回一个响应,该响应包含操作的结果以及UUID。
- 客户端可以通过检查响应来确定请求是否成功,以及是否为重复请求。
-
UUID:
UUID 的生成通常由编程语言提供的标准库支持。在不同的应用场景中,可以根据需要选择合适的 UUID 版本来生成标识符。例如,在需要强唯一性和时间排序的情况下可以选择 Version 1;而在需要随机性较高的情况下可以选择 Version 4。
- 时间戳 (Time Low):前 32 位,代表 UUID 创建的时间。
- 时间戳 (Time Mid):中间 16 位,继续表示时间戳。
- 时间戳 (Time High and Version):接下来 16 位中的高 12 位表示时间戳,低 4 位表示 UUID 的版本号。
- 时钟序列 (Clock Sequence High and Reserved):接下来 16 位中的高 14 位表示时钟序列,低 2 位保留未使用。
- 时钟序列 (Clock Sequence Low):接下来 8 位表示时钟序列的剩余部分。
- 节点 (Node):最后 48 位,通常用来表示生成 UUID 的物理机器的 MAC 地址,或者是随机生成的数字。
@Resource和
@Autowired 的区别
@Resource
和 @Autowired
都是在 Java 开发中用于依赖注入的注解,但它们有一些关键性的区别,主要涉及它们的来源、默认行为以及配置选项。
@Autowired
- 来源:
@Autowired
是由 Spring 框架提供的注解。 - 默认行为:它默认按照类型(byType)进行自动注入。这意味着 Spring 会寻找匹配类型的 Bean 并注入到标记了此注解的字段或方法中。
- 可选性:
@Autowired
支持可选注入,可以通过@Autowired(required=false)
指定。如果找不到匹配的 Bean,不会抛出异常而是注入null
。 - 解决歧义:当存在多个匹配类型的 Bean 时,可以使用
@Qualifier
注解进一步指定要注入哪一个 Bean。
@Resource
- 来源:
@Resource
是 J2EE 规范中的一部分,因此它不依赖于 Spring,只要 JDK 版本在 1.6 及以上即可使用。 - 默认行为:它默认按照名称(byName)进行自动注入。如果找不到匹配名称的 Bean,它会回退到类型匹配(byType)。这意味着,首先尝试使用变量名作为 Bean 的名称进行查找,如果找不到,则按类型查找。
- 配置选项:
@Resource
提供了更多的属性,如name
和type
,可以明确地指定要注入的 Bean 名称或类型。 - 必需性:
@Resource
在找不到匹配的 Bean 时,不会注入null
,而是抛出异常,除非你使用lookup
属性并指向一个可能返回null
的 JNDI 查找。
总结:
- 如果你希望根据类型注入并且希望在找不到 Bean 时有更灵活的处理方式,使用
@Autowired
更合适。 - 如果你想要根据 Bean 的名称进行注入,或者你的代码需要在非 Spring 环境下也能运行,
@Resource
可能是更好的选择。
在 Spring 中,两者通常可以互换使用,但在具体场景下选择其中一个可能更加适合。例如,如果你的项目完全基于 Spring,你可能会更倾向于使用 @Autowired
,因为它更紧密地集成了 Spring 的功能和特性。
分布式项目和微服务项目的区别
分布式项目和微服务项目虽然都涉及分布式系统的设计和实现,但它们之间存在一些关键的区别。
分布式项目
分布式项目是指那些跨越多个计算机节点运行的应用程序,这些节点通过网络相互通信。分布式项目的主要特点是它们能够处理大规模的数据和高并发的请求,同时提供高可用性和容错性。分布式项目的设计和实现通常需要考虑以下几个方面:
- 节点通信:节点之间需要通过网络进行通信,这可能涉及消息传递、RPC(远程过程调用)等机制。
- 数据一致性:在分布式环境中保持数据的一致性是一个挑战,可能需要使用共识算法(如Raft、Paxos)来实现。
- 容错机制:分布式系统需要能够处理节点故障、网络分区等故障,并提供相应的容错机制。
- 负载均衡:通过负载均衡器来分发请求到不同的节点,以提高系统的可用性和响应速度。
- 服务发现:在动态变化的环境中,节点可能加入或离开集群,服务发现机制可以帮助节点找到彼此。
微服务项目
微服务架构是一种设计模式,它将一个大型的应用程序分解成一系列小型、独立的服务,每个服务负责一个具体的业务功能。微服务项目的特点如下:
- 服务独立性:每个微服务都是一个独立的进程,有自己的数据库和配置。
- 可独立部署:微服务可以独立部署和扩展,这使得系统更具灵活性。
- 技术多样性:微服务可以使用不同的编程语言、数据存储和工具,这增加了技术栈的多样性。
- API 网关:通常会有一个 API 网关来作为前端应用程序与后端微服务之间的入口点,处理路由、认证、负载均衡等任务。
- 服务间通信:微服务之间通过轻量级的通信协议(如HTTP/REST、gRPC等)进行交互。
- 容错与弹性:每个微服务都应该具备自我恢复的能力,并且整个系统需要能够优雅地处理失败的服务。
的竞争情况,帮助发现并发问题。
2. 减少资源竞争:在某些情况下,可以用来减少线程之间的资源竞争,优化性能。
3. 临时让出CPU:在某些复杂计算中,临时让出CPU时间片,让其他优先级更高或重要的线程得以运行。
幂等性
一个幂等操作满足以下条件:
- 同一请求的重复执行:如果同一个操作请求被多次发送,对系统的状态只产生一次改变。
- 任意次序执行的结果相同:无论该操作请求被执行多少次,系统的状态都保持一致。
在设计API或者分布式系统时,考虑到幂等性是非常重要的,可以通过一些技术手段来实现,比如使用唯一请求标识符(request idempotency key)、状态检查和事务处理等方式来确保操作的幂等性。
-
客户端生成UUID:
- 每当客户端想要发起一个可能产生副作用的操作时(如创建资源或更新资源),它会生成一个唯一的UUID。
- 这个UUID会被包含在请求头或者请求体中发送给服务器。
-
服务器存储UUID:
- 服务器接收到带有UUID的请求后,首先检查这个UUID是否已经存在于某个存储中(如数据库或缓存)。
- 如果不存在,则继续处理请求,并将UUID与请求的结果相关联地存储起来。
- 如果存在,则表明这是一个重复的请求,直接返回之前处理的结果而不做任何额外处理。
-
响应处理:
- 无论请求是否被处理,服务器都会返回一个响应,该响应包含操作的结果以及UUID。
- 客户端可以通过检查响应来确定请求是否成功,以及是否为重复请求。
-
UUID:
UUID 的生成通常由编程语言提供的标准库支持。在不同的应用场景中,可以根据需要选择合适的 UUID 版本来生成标识符。例如,在需要强唯一性和时间排序的情况下可以选择 Version 1;而在需要随机性较高的情况下可以选择 Version 4。
- 时间戳 (Time Low):前 32 位,代表 UUID 创建的时间。
- 时间戳 (Time Mid):中间 16 位,继续表示时间戳。
- 时间戳 (Time High and Version):接下来 16 位中的高 12 位表示时间戳,低 4 位表示 UUID 的版本号。
- 时钟序列 (Clock Sequence High and Reserved):接下来 16 位中的高 14 位表示时钟序列,低 2 位保留未使用。
- 时钟序列 (Clock Sequence Low):接下来 8 位表示时钟序列的剩余部分。
- 节点 (Node):最后 48 位,通常用来表示生成 UUID 的物理机器的 MAC 地址,或者是随机生成的数字。
@Resource和
@Autowired 的区别
@Resource
和 @Autowired
都是在 Java 开发中用于依赖注入的注解,但它们有一些关键性的区别,主要涉及它们的来源、默认行为以及配置选项。
@Autowired
- 来源:
@Autowired
是由 Spring 框架提供的注解。 - 默认行为:它默认按照类型(byType)进行自动注入。这意味着 Spring 会寻找匹配类型的 Bean 并注入到标记了此注解的字段或方法中。
- 可选性:
@Autowired
支持可选注入,可以通过@Autowired(required=false)
指定。如果找不到匹配的 Bean,不会抛出异常而是注入null
。 - 解决歧义:当存在多个匹配类型的 Bean 时,可以使用
@Qualifier
注解进一步指定要注入哪一个 Bean。
@Resource
- 来源:
@Resource
是 J2EE 规范中的一部分,因此它不依赖于 Spring,只要 JDK 版本在 1.6 及以上即可使用。 - 默认行为:它默认按照名称(byName)进行自动注入。如果找不到匹配名称的 Bean,它会回退到类型匹配(byType)。这意味着,首先尝试使用变量名作为 Bean 的名称进行查找,如果找不到,则按类型查找。
- 配置选项:
@Resource
提供了更多的属性,如name
和type
,可以明确地指定要注入的 Bean 名称或类型。 - 必需性:
@Resource
在找不到匹配的 Bean 时,不会注入null
,而是抛出异常,除非你使用lookup
属性并指向一个可能返回null
的 JNDI 查找。
总结:
- 如果你希望根据类型注入并且希望在找不到 Bean 时有更灵活的处理方式,使用
@Autowired
更合适。 - 如果你想要根据 Bean 的名称进行注入,或者你的代码需要在非 Spring 环境下也能运行,
@Resource
可能是更好的选择。
在 Spring 中,两者通常可以互换使用,但在具体场景下选择其中一个可能更加适合。例如,如果你的项目完全基于 Spring,你可能会更倾向于使用 @Autowired
,因为它更紧密地集成了 Spring 的功能和特性。
分布式项目和微服务项目的区别
分布式项目和微服务项目虽然都涉及分布式系统的设计和实现,但它们之间存在一些关键的区别。
分布式项目
分布式项目是指那些跨越多个计算机节点运行的应用程序,这些节点通过网络相互通信。分布式项目的主要特点是它们能够处理大规模的数据和高并发的请求,同时提供高可用性和容错性。分布式项目的设计和实现通常需要考虑以下几个方面:
- 节点通信:节点之间需要通过网络进行通信,这可能涉及消息传递、RPC(远程过程调用)等机制。
- 数据一致性:在分布式环境中保持数据的一致性是一个挑战,可能需要使用共识算法(如Raft、Paxos)来实现。
- 容错机制:分布式系统需要能够处理节点故障、网络分区等故障,并提供相应的容错机制。
- 负载均衡:通过负载均衡器来分发请求到不同的节点,以提高系统的可用性和响应速度。
- 服务发现:在动态变化的环境中,节点可能加入或离开集群,服务发现机制可以帮助节点找到彼此。
微服务项目
微服务架构是一种设计模式,它将一个大型的应用程序分解成一系列小型、独立的服务,每个服务负责一个具体的业务功能。微服务项目的特点如下:
- 服务独立性:每个微服务都是一个独立的进程,有自己的数据库和配置。
- 可独立部署:微服务可以独立部署和扩展,这使得系统更具灵活性。
- 技术多样性:微服务可以使用不同的编程语言、数据存储和工具,这增加了技术栈的多样性。
- API 网关:通常会有一个 API 网关来作为前端应用程序与后端微服务之间的入口点,处理路由、认证、负载均衡等任务。
- 服务间通信:微服务之间通过轻量级的通信协议(如HTTP/REST、gRPC等)进行交互。
- 容错与弹性:每个微服务都应该具备自我恢复的能力,并且整个系统需要能够优雅地处理失败的服务。