java基础
1、JDK java开发工具 是提供给开发人员来用的
Jre java运行时环境是提供给运行java程序的用户来使用的
JVM 是属于虚拟机,编译我们的class文件,让操作系统能够执行
JDK实际上是包含了JRE的,而在jre当中有两个核心文件夹一个bin(jvm)文件夹一个lib(类库)文件夹,jdk还包含我们的java工具javac,java,jconsole,所以总的来说呢就是jdk包含了jre,jre包含了jvm
2. ==和equals的区别
==比较的是栈中的值,基本数据类型是变量值,引用类型是堆中内存对象的地址
Equals是object中默认的==比较,通常会重写
-
对于基本类型,==比较的是值;
-
对于引用类型,==比较的是地址;
-
equals不能用于基本类型的比较;
-
如果没有重写equals,equals就相当于==;
-
如果重写了equals方法,equals比较的是对象的内容;
3. final的作用
Final是最终的嘛
主要的话有
修饰类 :表示当前的类是不可以被继承的
修饰方法 表示方法不能被子类覆盖,但是可以重载
修饰变量 表示变量一旦被赋值就不能改变他的值
修饰成员变量:
如果final修饰的类是变量,只能在静态初始化块中指定初始值或者声明该类变量时初始值
如果修饰的是成员变量的话,可以在非静态初始化块,声明变量或者构造器中执行初始值
修饰局部变量的话
局部变量是不会进行初始化的,必须我们手动显示初始化,所以使用final修饰局部变量的话,可以在定义时指定默认值
如果他修饰的是基本类型数据和引用类型数据的话
基本数据类型的话,数值一旦初始化就不能修改了
如果是引用类型变量的话,初始化之后就不能在指向另一个对象,但是引用的值是可以去改变的
4. String Stringbuffer Stringbuilder
String 是final修饰的,不可变,每次操作都会产生新的String对象
StringBuffer和String Builder都是在原对象上操作的
StringBuffer是线程安全的,StringBuilder是线程不安全的
StringBuffer方法是synchronized修饰的
性能:StringBuilder>StringBuffer>String
场景:经常需要改变字符串内容时使用后面两个
优先使用StringBuilder,多线程使用共享变量时使用StringBuffer
5. 重载和重写的区别
重载(Overloading)和重写(Overriding)是面向对象编程中常用的两个概念,它们有以下区别:
-
定义:
-
重载(Overloading):指在同一个类中,可以定义多个方法或构造函数,它们具有相同的名称但参数列表不同(参数类型、参数个数或参数顺序),编译器根据调用时传递的参数来决定调用哪个方法或构造函数。
-
重写(Overriding):指子类可以重写(覆盖)其父类中具有相同名称和参数的方法。重写后,当通过子类实例调用这个方法时,将执行子类中的方法实现,而不是父类中的。
-
-
适用对象:
-
重载通常应用于同一个类中的方法或构造函数之间,用于提供多种方法签名的选择。
-
重写通常发生在子类继承父类时,用于子类需要修改或扩展父类方法的行为时。
-
-
区分依据:
-
方法签名:重载依赖于方法的参数列表,不同的参数列表可以定义多个同名方法;重写依赖于方法的名称和参数列表,子类方法要和父类方法完全一致。
-
编译时行为:重载的方法调用在编译时确定调用哪个方法;重写的方法调用在运行时确定调用子类还是父类方法。
-
-
作用:
-
重载可以增强代码的灵活性和可读性,提供了一种方法的多种使用方式。
-
重写允许子类修改继承的方法行为,实现多态性(Polymorphism)。
-
总结来说,重载是在同一个类中方法的多态性表现,通过方法签名的不同实现不同的功能;而重写则是子类对父类方法的继承和修改,实现了继承的多态性。
6. 接口和抽象类的区别
抽象类可以存在普通成员函数,而接口呢只能存在public abstic方法
抽象类只能继承一个,而接口可以多实现
抽象类的成员变量可以是各种类型的,而接口的成员变量只能是public static final类型的
7. List和set的区别
List是有序的,按照对象进入的顺序保存对象,可以重复,允许有多个空元素对象,可以使用迭代器去取出所有元素,然后再进行逐一遍历,还可以使用get( int index)获取指定下标的元素
Set是无序,不可以重复的,最多允许有一个null元素对象,取元素的时候只能用迭代器取得所有接口,然后遍历
8. hashcode 和equals的区别
Hashcode实际上就是一个哈希吗,也叫散列码,在Java中实际上也就是为了获取hash吗,那么他这个方法是定义在jdkObject的类中的,是一个本地方法,他会返回一个int整数,这个hash吗的作用就是为了找到对象在hash表中索引位置,那么有了这个索引之后能,能很快的找到这个对象在堆中的位置,java中的任何类都是含有hashcode函数的,散列表中存放着也就是key value值 ,key就是我们的索引
hashCode和equals是Java中Object类的两个方法,用于对象比较和哈希表存储。它们的区别主要体现在以下几点:
-
功能不同:hashCode方法用于获取对象的哈希码,它的主要作用是确定对象在哈希表中的存储位置;equals方法用于判断对象是否相等,即比较两个对象的内容是否相同。
-
返回值不同:hashCode方法返回一个int类型的哈希码值;equals方法返回一个boolean类型的值,表示两个对象是否相等。
-
实现方式不同:hashCode的默认实现是返回对象的内存地址的整数表示,但是可以根据对象的特定属性来重新实现;equals方法的默认实现是比较两个对象的引用是否相等,即内存地址是否相同,但也可以根据对象的特定属性来重新实现。
-
相互依赖关系:根据Java规范,如果两个对象通过equals方法比较返回true,则它们的hashCode值必须相等;但是hashCode相等并不意味着equals一定返回true,因为哈希冲突可能导致不同对象具有相同的hashCode值。
总之,hashCode和equals方法在Java中通常需要同时重写,以确保对象在比较和存储时具有一致的行为。
9. arraylist和linkedlist的区别
ArrayList和LinkedList是Java集合框架下的两种常见的List实现类,它们之间有以下区别:
-
数据结构:ArrayList基于数组实现,它通过动态扩展和收缩内部数组来存储元素;LinkedList基于双向链表实现,它通过节点之间的链接来存储元素。
-
访问效率:ArrayList能够快速随机访问元素,因为可以根据索引直接访问数组元素,时间复杂度为O(1);LinkedList需要遍历节点来访问指定位置的元素,时间复杂度为O(n)。所以对于随机访问元素较多的场景,ArrayList的性能更好。
-
插入和删除效率:ArrayList的插入和删除操作涉及到元素的移动,当插入或删除元素在列表的中间位置时,需要大量的元素移动操作,时间复杂度为O(n);LinkedList利用节点的链接关系,插入和删除元素的时间复杂度为O(1),在列表中间位置的操作较快。
-
内存占用:ArrayList需要连续的内存空间来存储元素,因此相对来说会占用较多的内存空间;LinkedList因为需要存储节点的链接关系,会占用额外的内存空间。
-
遍历效率:ArrayList的遍历效率比LinkedList更高,因为可以直接根据索引访问元素,并且数组元素在内存中是连续存储的;LinkedList需要从头节点开始,依次遍历到指定位置的节点。
根据不同的需求和使用场景,选择合适的实现类可以提高程序的性能和效率。如果需要频繁随机访问元素,使用ArrayList,如果需要频繁进行插入和删除元素操作,特别是在列表的中间位置,使用LinkedList可能更合适。
10HashMap和HashTable有什么区别,其底层实现是什么
HashMap和Hashtable在功能上是相似的,它们都用于存储键值对,并提供了类似的方法来进行操作。然而,它们之间有以下几个区别:
-
线程安全性:Hashtable是线程安全的,即可以在多线程环境下使用,并保证线程安全;而HashMap不是线程安全的,如果在多线程环境中使用HashMap,需要手动进行同步处理。
-
允许null键或值:HashMap允许键和值都为null,而Hashtable不允许,如果尝试使用null作为键或值,则会抛出NullPointerException。
-
性能:由于Hashtable是线程安全的,它在多线程环境下添加了同步的开销,因此在性能方面相对较低。而HashMap在单线程环境下没有额外的同步开销,因此在性能上通常比Hashtable更好。
-
初始容量和扩展机制:Hashtable在创建时需要指定初始容量,并且在元素数量超过容量的75%时自动进行扩展;HashMap也可以指定初始容量,但是它的扩展机制是当元素数量超过容量的负载因子(默认为0.75)时进行扩展。
它们的底层实现都是基于哈希表(Hash Table)的数据结构。具体来说,HashMap和Hashtable底层都使用数组加链表或红黑树(JDK 8及以上版本)的方式来组织和存储数据。通过计算哈希值,确定元素在数组中的位置,并使用链表或红黑树来解决哈希冲突,实现高效的存储和查找操作。每个键值对都会根据其键的哈希值存储在对应的位置,这样在搜索、插入和删除元素时都能够以常数时间复杂度(O(1))进行。
11、ConcurrentHashMap原理
ConcurrentHashMap是Java并发编程中提供的线程安全的哈希表,它是对HashMap的改进,使用了特定的数据结构和算法来实现线程安全性和高并发性能。
ConcurrentHashMap的底层数据结构是分段锁(Segment),每个Segment类似于一个小的HashMap,它们相互独立且可以被多个线程同时访问,不同的Segment锁定的是不同的数据段。每个Segment内部包含一个数组(HashEntry[]),这个数组的元素是单向链表或红黑树(JDK 8及以上版本),用于解决哈希冲突。
ConcurrentHashMap首先根据键的哈希值,通过计算得到一个操作的Segment索引,然后在对应的Segment中进行操作。这样,多个线程可以同时访问不同的Segment,从而实现并发读写而无需进行全局锁定。因为每个Segment对应的是不同的数据段,所以在并发写入时不会相互冲突。
在进行插入、更新、删除等操作时,首先需要通过计算键的哈希值找到对应的Segment,然后再在Segment中进行链表或红黑树操作,保证线程安全。而在进行查询操作时,并不需要加锁,可以并发地进行。
ConcurrentHashMap在保证线程安全的同时,还提供了较高的并发性能。由于每个线程只需要锁定自己对应的Segment,而不是整个哈希表,多个线程可以并发地进行操作,从而提高了并发读写的性能。此外,在读多写少的场景下,ConcurrentHashMap可以选择合适的扩容策略来提高性能。
总之,ConcurrentHashMap通过分段锁和特定的数据结构实现了线程安全和高并发性能,并在Java并发编程中得到广泛应用。
Java当中线程安全的list有哪些
在 Java 中,线程安全的 List 主要有以下几种实现:
-
CopyOnWriteArrayList:这是一个线程安全的 List 实现类,通过在写操作(如添加和修改元素)时复制整个数组来实现线程安全。适合读操作频繁、写操作较少的场景。
-
Collections.synchronizedList:这是通过在每个方法上加锁来实现线程安全的 List,基于传入的普通 List 对象,使得每个方法都是同步的。适合简单的线程安全需求,但性能可能不如其他更专门的实现。
-
Vector:Vector 是 Java 中最早的线程安全的动态数组实现,所有方法都使用 synchronized 来保证线程安全。不过,它的性能通常比较低下,推荐使用更现代的实现如 CopyOnWriteArrayList。
-
ConcurrentLinkedList:这是 Java 并发包(java.util.concurrent)中提供的线程安全的链表实现,适合高并发环境下的操作,但它是基于链表结构的,不支持随机访问(get(index)操作效率较低)。
这些实现都具有不同的特点和适用场景,选择合适的线程安全 List 取决于具体的使用需求和性能考量。
12. 什么是字节码
字节码(Bytecode)是一种中间码,它是Java虚拟机(JVM)可以理解和执行的二进制指令集。在Java编程中,源代码首先被编译成字节码,然后由JVM解释执行或者即时编译成机器码。
字节码是一种与平台无关的中间表示形式,它具有以下特点:
-
基于栈:Java字节码是基于栈的指令集,在执行时使用操作数栈作为数据存储区。大部分的操作都是基于栈帧的,比如方法调用、参数传递、局部变量等。
-
适应多种虚拟机:字节码的设计考虑了Java虚拟机的多样性,使得同一个字节码文件可以在不同的虚拟机上执行。不同虚拟机可能有不同的指令集和执行引擎,但它们都能够通过解释或即时编译技术将字节码转换成具体的机器码。
-
安全性:字节码可以包含一些安全性检查指令,用于保证Java应用程序的安全性。例如,类型检查指令可以确保对象在操作前已经正确初始化,以避免空指针异常等问题。
-
可移植性:由于字节码与具体的硬件和操作系统无关,因此可以在任何支持Java虚拟机的平台上运行。这种可移植性是Java语言的重要特性之一。
通过将Java源代码编译成字节码,Java实现了一次编写,到处运行的特性,使得开发者可以在不同的平台上运行同一份字节码文件。这也是Java语言的优势之一,并为跨平台开发提供了便利。
13. java中的反射
简单说,反射机制值得是程序在运行时能够获取自身的信息。在java中,只要给定类的名字,那么就可以通过反射机制来获得类的所有信息。
*Java反射机制的作用* \1. 在运行时判断任意一个对象所属的类 \2. 在运行时构造任意一个类的对象 \3. 在运行时判断任意一个类所具有的成员变量和方法 \4. 在运行时调用任意一个对象的方法
*java反射机制提供了什么功能?* \1. 在运行时能够判断任意一个对象所属的类 \2. 在运行时构造任意一个类的对象 \3. 在运行时判断任意一个类所具有的成员变量和方法 \4. 在运行时调用任一对象的方法 \5. 在运行时创建新类对象
*哪里用到反射机制?*
\1. 各种框架用的最多的就是反射
\2. 加载驱动
\3. 读取配置文件
14.普通类和抽象类有哪些区别?
抽象类不能被实例化; 抽象类可以有抽象方法,只需申明,无须实现; 有抽象方法的类一定是抽象类; 抽象类的子类必须实现抽象类中的所有抽象方法,否则子类仍然是抽象类; 抽象方法不能声明为静态、不能被static、final修饰。
类的加载过程
类的加载过程(Class loading process)是指在Java虚拟机(JVM)中,当程序需要使用某个类时,JVM如何加载这个类的过程。以下是类加载的基本过程:
-
加载(Loading):
-
加载是指查找字节码(bytecode)并将其转换成JVM内部的数据结构。字节码可以来自文件、网络或者其他来源。在加载阶段,JVM会为每个类创建一个唯一的Class对象。
-
-
链接(Linking):
-
链接阶段分为三个步骤:
-
验证(Verification):确保被加载的类符合JVM规范,不会损害JVM自身的安全。
-
准备(Preparation):为类的静态变量分配内存空间,并设置默认初始值。
-
解析(Resolution):将类中的符号引用转换为直接引用,即确定各个类之间的实际引用关系(如方法调用、字段引用等)。
-
-
-
初始化(Initialization):
-
初始化阶段是类加载过程的最后一步,此阶段负责执行类构造器
<clinit>
方法的过程。该方法包含了类变量的赋值和静态代码块的执行,确保类在首次使用前已经完成了初始化。
-
类加载过程是Java虚拟机实现动态类加载和运行的重要机制,它保证了程序在运行时能够动态地加载和使用类。理解类加载过程对于理解Java程序的运行机制和调优非常重要。
Arraylist扩容机制
\1. arraylist每次扩容都是原来的1.5倍
\2. 数组进行扩容的时候,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是之前容量的1.5倍
\3. 但是代价是很高的,应该尽量避免数组的扩容,就是尽可能的去指定他的初始容量,避免数组的扩容List arraylist = new Arraylist(4);
HashMap扩容分为两步:
-
扩容:创建一个新的Entry空数组,长度是原数组的2倍。
-
ReHash:遍历原Entry数组,把所有的Entry重新Hash到新数组。
为什么要重新Hash呢,不直接复制过去呢?因为长度扩大以后,Hash的规则也随之改变。
Hash的公式---> index = HashCode(Key) & (Length - 1)
原来长度(Length)是8你位运算出来的值是2 ,新的长度是16你位运算出来的值明显不一样了,之前的所有数据的hash值得到的位置都需要变化。
为什么握手是三次,而不是两次或者四次?
答:两次不安全,四次没必要。tcp通信需要确保双方都具有数据收发的能力,得到ACK响应则认为对方具有数据收发的能力,因此双方都要发送SYN确保对方具有通信的能力。第一次握手是客户端发送SYN,服务端接收,服务端得出客户端的发送能力和服务端的接收能力都正常;第二次握手是服务端发送SYN+ACK,客户端接收,客户端得出客户端发送接收能力正常,服务端发送接收能力也都正常,但是此时服务器并不能确认客户端的接收能力是否正常;第三次握手客户端发送ACK,服务器接收,服务端才能得出客户端发送接收能力正常,服务端自己发送接收能力也都正常。
Git管理
一、我们的分支管理(研发)流程 1、Master对应生产环境版本,生产环境发布脚本只允许连接Master分支。 2、每隔三个星期左右,整理出近段时间的需求,排好开发计划,定义好产品版本。 3、根据产品版本创建对应的分支 比如一次创建三个分支V2021_08_11_V6.7.1 、V2021_08_11_V6.7.2、V2021_08_11_V6.7.3 如果有紧急需求或BUG会在这分支中间插入分支名为_fixbug之类。 4、各开发小组在对应的分支开发,提交代码。 5、分支2021_08_11_V6.7.1 开发完成,由开发人员在测试环境发布该分支 ,然后通知测试进行功能测试。 6、测试完成后通知我进行合并代码到Master,合并时我有时会Review代码,如果有不符合代码规范的,让开发人员在分支上重新修改提交。 7、测试对Master分支进行回归测试,如果有Bug,开发人员在原分支修改好,分支测试通过,然后我又合并代码到Master分支,测试重新回归,直至没有任何Bug。 8、生产环境Master分支发布后,各开发人员在后续的分支将Master合并过去,有代码冲突解决掉,继续开发。 9、生产环境发布后回归完成,测试会在文档上记录版本号和对应这次发布的功能点,然后将发布内容提交给业务方以及BOSS。 10、持续这样的迭代过程。 注:这种分支管理流程除了偶尔会造成有游离分支,没有发生代码被覆盖的情况(开始的时候少了从Master往各开发分支合并的步骤发生过代码丢失的情况),但是发生游离分支,折腾了几次都没有办法解决,一般是让对应的开发做好备份,然后我重新创建一个分支,开发人员将游离分支的代码迁移到新的分支,然后我再合并到Master,这样当代码改动分散在各模块中特别麻烦,后端Java还好,前端Vue页面和JS代码头都要大了,已经发生过多次了。
项目当中的设计模式
好的,让我以一个具体的业务场景来说明各个设计模式的应用:
假设我们正在开发一个电子商务平台,其中包含商品分类和推荐功能。
-
单例模式: 我们可以使用单例模式来实现商品分类管理器。该管理器负责加载、缓存和提供商品分类信息,确保整个系统只有一个实例,并且可以在任何地方方便地访问和使用。
-
工厂模式: 在电子商务平台中,我们可能需要根据商品的不同类型(例如电子产品、服装、食品等)创建不同的商品对象。这时可以使用工厂模式来封装对象的创建过程,根据不同的输入条件生成相应类型的商品对象。
-
观察者模式: 推荐功能可以基于用户的浏览行为和购买历史来生成个性化的推荐列表。我们可以使用观察者模,当用户浏览或购买了某个商品时,系统会通知观察者(推荐引擎),触发其重新计算并更新推荐列表。
-
装饰器模式: 电子商务平台可能需要记录用户行为日志,如用户登录日志、购买记录等。我们可以使用装饰器模式,为现有的用户管理组件和购物车组件添加日志记录的功能,而无需修改它们的代码。
-
适配器模式: 电子商务平台可能需要与第三方支付平台进行集成,以支持不同的支付方式。我们可以使用适配器模式来将支付平台的接口转换为系统内部统一的支付接口,使得系统能够适配不同的支付方式。例如将支付宝支付、微信支付等各种支付方式适配成统一的支付接口供系统使用。
以上是一些常见的设计模式在电子商务平台中的应用示例,它们帮助我们提高代码的可维护性、可扩展性和可重用性,使系统更加灵活和易于维护。
乐观锁 悲观锁
乐观锁和悲观锁是并发控制的两种不同策略,用于处理多个线程同时访问共享资源时可能出现的数据竞争问题。
-
悲观锁:
-
定义:悲观锁假设会发生并发冲突,因此在整个数据操作过程中会将数据锁住,直到操作完成,其他线程无法修改数据。
-
实现方式:通常通过数据库中的排他锁(如
SELECT ... FOR UPDATE
)或者 Java 中的 synchronized 关键字实现。例如,在数据库中,一个事务如果要更新某条数据,会先锁定该数据,其他事务则需要等待锁释放后才能进行操作。
-
-
乐观锁:
-
定义:乐观锁假设并发冲突很少发生,因此不对数据进行加锁,而是在更新时检查是否有其他线程同时修改了数据。
-
实现方式:通常通过版本号(Versioning)或时间戳(Timestamp)来实现。例如,每个数据记录增加一个版本号字段,更新时检查版本号是否仍然是原来的值,如果是则更新,否则认为操作失败需要重试。
-
适用场景:
-
悲观锁适合并发写入操作较频繁或者更新冲突概率较高的场景,可以确保数据的一致性和完整性,但可能会带来较大的性能开销和并发度降低。
-
乐观锁适合读操作远远超过写操作,并发冲突较少的场景,通过版本控制或时间戳检查可以减少锁的使用,提升系统的性能和并发度。
在实际应用中,选择合适的锁策略取决于具体的业务需求、数据访问模式和性能考量。
项目
sercurity
根据用户输入的用户名去数据库具体的用户对象,调取权限服务,来获取该用户所有的权限,首先查询该用户拥有哪些角色,根据角色再去查询权限,查询到角色对应的权限,存放到一个总的集合中。再次进行去重
HasAuthority 对于index.html来说,只有拥有“”多少号的权限的用户才能够访问
HasRole 对于index.html来说,属于“”号的角色用户才能够访问
自动登录 记住我
配置数据源,创建令牌持久化方案的对象,开启记住我的功能,持久化令牌的方案,设置令牌的有效期
Token的一次性
首先从前端传来的cookie解析出series和token
再根据series从数据库中查询remembertoker的实例
如果查出来的token和前端传来的token不相同时,说明账号可能被别人盗用,此时根据用户名移除相关的token
接下来校验token是否过期
构造新的remembertoken对象,并且更新数据库中的token
将新的令牌重新放回到cookie当中
秒杀业务
全剧唯一ID生成策略UUID Redis自增ID策略 雪花算法 数据库自增
\1. 首先设计订单全局唯一ID 使用时间戳,序列号,自增长的方式
设计的是普通优惠券和特价卷,特价卷需要抢购,这里就涉及到一个超卖的问题,前提还有秒杀时间是否开始和结束,第二点库存是否充足,不足则无法下单
在分布式多线程的情况下,会出现超卖的场景,多个线程在查询到库存扣减时,可能会出现同一时间内同时查到相同的库存,超卖的问题可以考虑加锁
乐观锁,版本号的方法,在扣减库存的同时,加入了修改版本号,如果版本号查询不一致是不能去扣减库存的
CAS方法:就是把版本号替换为之前的库存数,如果库存出现改动,其他的线程就不会扣减库存,会排队等待
悲观锁:添加同步锁,让线程串行执行
乐观锁:不加锁,在更新时是否有其他线程修改
一人一单的问题
查询当前用户是否有订单,是否存在
*中银企e贷申请步骤:*
1、企业网银提交申请、、、、、、、、、、、、、、、、、
企业登录中国银行企业网上银行,在“贷款服务”中,选择“中银企E贷”发起产品申请,对企业基本信息、账号信息及企业法定代表人等信息进行确认。确认无误后,企业在线签署中国银行相关协议文本后,发起对企业法定代表人的业务代表确认及企业相关数据使用授权。
2、法定代表人手机银行完成确认
企业通过网上银行提交申请后20个自然日内(含),企业法定代表人登录中国银行个人手机银行,发起“中银企E贷”产品申请,对企业及其本人基本信息、贷款申请等信息进行确认,在线签署相关协议文本后,同意接受企业业务代表,并发起个人相关数据使用授权,提交“中银企E贷”产品申请。
3、企业法定代表人手机银行签约
企业通过网上银行提交申请后20个自然日内(含),系统自动审批并给出授信额度后,企业法定代表人即可通过中国银行个人手机银行发起借款合同签订,借款企业与其法定代表人作为共同借款人与中国银行签订借款合同。
4、企业法定代表人手机银行提款
中银企e贷到期(不含到期日当天),企业法定代表人通过个人手机银行可发起中银企e贷在线提款,也可以通过企业网银发起“中银企E贷”在线提款,贷款资金转入合同约定的企业结算账户。
综上所述,中银企e贷的申请对象为符合中国银行信贷工厂客户范围的国标小微企业,企业及其法人需要有良好信用记录,满足以上几条即可登录中国银行官网申请。
附近网点排队系统优化方案
我们的系统基于以下步骤来实现对附近网点的排队管理:
-
数据录入与分类:
-
将所有附近网点信息录入数据库,包括网点类型和经纬度信息。
-
根据网点类型进行分类,将同类型的网点数据按照typeId存入Redis缓存,以便快速访问和查询。
-
-
查询优化:
-
根据用户提供的坐标判断是否需要进行附近网点的查询。
-
如果需要查询,计算出符合条件的网点列表,并进行分页处理。
-
-
排序与距离计算:
-
从Redis中获取符合条件的网点集合,并按照用户与网点之间的距离进行排序。
-
排序使用半径距离(distance)作为依据,确保最近的网点排在前面,以提供更好的用户体验。
-
-
数据检索与返回:
-
根据排序后的网点列表,解析出网点的唯一标识符(id)。
-
使用这些id从数据库中查询详细的网点信息(如店铺信息)。
-
将最终结果返回给用户,包括网点的位置、服务类型等信息。
-
通过以上优化方案,我们能够高效地管理和查询附近网点的信息,确保系统在高并发情况下仍能快速响应用户请求,并提供准确的网点排队和服务信息。
Springboot自动装配原理
1、Springboot的自动装配原理
自动配置简单来说呢,就是将第三方的组件自动装载到IOC容器里面,不需要开发人员再去编写相关的配置,在SpringBoot应用里面呢只需要加上@SpringBootApplication注解就可以实现自动配置,SpringBootApplication它是一个复合注解,真正实现自动装配的注解是@EnableAutoConfiguration注解。自动装配的实现呢主要依靠三个核心的关键技术:
1)引入Starter,启动依赖组件的时候,这个组件里面必须包括@Configuration配置类,然后我需要通过Bean注解去声明需要装配到IOC容器里面的Bean对象
2)这个配置类是放在第三方的jar包里面,然后通过Spring Boot中约定大于配置的理念,去把配置类的全路径放在件META_INF/Spring.factories文件里面,SpringBoot就可以知道第三方jar包里面配置类的位置,它主要是依靠Spring里面的SpringFactorierLoader来完成的
3)SpringBoot拿到所有第三方jar包声明的配置类之后,再通过ImportSelector这样一个接口来实现对这些配置类的动态加载,从而去完成自动装配这样的一个动作
分布式锁三种方式
分布式锁和我们java的syncroized的一样,其实都是为了将这一段代码在某一个时间点的时候,只能够单线程执行,也就是为了保证原子性,有线程执行的话,其他线程只能去排队等待,类似于加入一个第三方,就是获取锁的时候去第三方获取,这个时候就需要第三方来保证在这个分布式集群场景里面只有一个线程能够拉取到这个锁,这个第三方就是分布式锁去解决的。
数据库:一般db都是公用的,线程访问的db都是同一套数据库,可以利用主键冲突控制只有一个线程能够获取到锁,也就是在db中去建一张表,采用唯一约束,A和B线程在访问这个代码的时候,先往数据库中去插一条记录,然后把这个key放在唯一键上面,那么放进去了就是拿到锁了,放不进去就意味着锁拿不到,利用了主键的唯一性,唯一约束,这个也是最简单的实现分布式锁的方案。 数据库这种方式呢也是有缺点的,他是非阻塞的,比如说100个线程,一个拿到锁了,其他99个线程全部都是在等待的,就会进入阻塞态,但是用db这种方法呢他不会去等待,想要实现等待的话,需要自己去写阻塞逻辑,比如说写一个while循环,让他自旋,还有key过期的话需要写一个定时器去手动删除
ZooKeeper分布式锁:
这个分布式锁呢其实就是Zhode节点,在同一个目录下面我们的AB线程都去创建这个Zhode节点,这个zhode有点类似于我们windos当中的资源管理器,比如说我们在windos的文件夹下建名字相同的文件是建不成功的ZK也是一样,也是利用的这样的机制,A线程建zhode成功了,意味着拿到了这个锁,他不会有死锁的问题,在zk里面呢,一旦客户端获取到锁突然挂掉了,这个节点就会自动删除掉,他有这个监听机制,如果这个client失活了,他会自动删除,其他客户端自动获取锁
Redis分布式锁:
分布式场景下使用最多的一种,主要是通过setNX这个命令,单线程处理网络请求,天然的不需要去考虑并发安全问题,redis本身就是单线程,这也是他的一个天然优势。
Setnx呢,也就是他这个key不存在的时候才能够设置成功,类似于我们AB线程,A线程设置成功后这个key,B线程就不能去设置了,redis是一个中间键,集群中的每一个节点都有这个key值,所以说redis很方便的实现这个分布式锁,要比我们的数据库用法简单多了,就目前来说,大部分的分布式应用场景都会使用到分布式缓存,像redis这种一个命令就很方便了。
但是setnx最早起没有设置超时参数,需要单独设置(expire),存在死锁的问题,后来出了set(NX Timeout),就变成了一个原子操作了,但是又存在一个任务超时的问题,锁自动释放,导致任务超时,导致了并发问题,解决就在value的值里面设置一个线程的唯一id,那么再去释放锁的时候,先去对比一下id,是不是自己的id,是你自己的可以去释放,不是自己的不能去释放,这样也就解决了加锁和释放锁不是同一线程的问题。
还有锁续期的问题,可以通过redission来实现,通过看门狗的机制,相当于设置一个key,就会去监听,判断任务有没有执行完。以上的redis中的机制还存在一个问题,就是redis集群数据同步的问题。
Redlock,可以从多个节点申请锁,当一半以上的节点获取成功,锁才算获取成功,并且redission也有相对于的实现
Spring循环依赖的问题
@Lazy注解:解决构造方法造成的循环依赖问题,就是多个对象之间,存在循环的的引用关系,在初始化的过程当中,就会出现一个类似于先有鸡还是先有蛋的问题,相当于是初始化的时候,让一步。,@Lazy就是给了一个初始化为空的对象,需要有一个空参的构造函数,能够把这个空的对象先初始化出来
在单例池下面创建一个缓存,二级缓存里面存放的不是一个完整的对象,是没有属性值的对象,但他最终到单例池都是完整的
二级缓存对于对象之间普通引用,会保存new出来的不完整对象,这样当单例池中找到不依赖的属性时,就可以先从二级缓存中获取到不完整对象,完成对象创建,在后续的依赖注入过程中,将单例池中对象的引用关系调整完成
三级缓存:如果引用的对象配置了aop,那在单例池中最终就会需要注入动态代理对象,而不是原对象。而生成动态代理是要在对象完成之后去完成的,于是spring增加三级缓存,保存所有对象的动态代理配置信息,在发现有循环依赖时,将这个对象的动态代理信息获取出来,提前进行AOP,生成动态代理。
核心代码在DefaultSingletonBeanRegistery的getSingleton方法当中
解决循环依赖的方法通常依赖于具体的编程语言和架构设计,以下是一些常见的方法:
-
重构代码结构:尝试将循环依赖拆分成更小的模块或组件,使得它们之间的依赖关系更清晰。这可能需要重新设计代码的组织结构。
-
引入接口或抽象层:使用接口或抽象类来隔离具体实现,从而减少直接的依赖性。这可以通过依赖倒置原则来实现,让高层模块不依赖于低层模块的具体实现。
-
使用依赖注入(Dependency Injection):通过依赖注入容器(如Spring Framework中的Bean容器)来管理依赖关系,从而解耦各个组件之间的依赖。
-
延迟初始化:在必要时才初始化依赖关系,而不是在组件加载时立即初始化。这可以通过延迟加载或惰性加载的方式来减少循环依赖带来的问题。
-
模块化设计:将系统划分为更小的模块或服务,通过明确定义模块之间的边界和接口,避免直接的循环依赖。
-
依赖分析工具:使用工具来分析依赖关系,帮助识别和解决循环依赖问题。一些静态分析工具可以帮助检测代码中的复杂依赖关系。
具体选择哪种方法取决于项目的架构和具体情况。在设计和开发阶段,避免循环依赖是一种良好的实践,可以减少后续代码维护和扩展的复杂性。
SpringMvc的工作流程
用户发送请求到DispatcherServlet
DispatcherServlet收到请求调用HandlerMapping处理器映射器,处理器映射器就是维护url到一个handler的映射,HandlerMapping实际上是一个接口,可以有不同的实现类,我们也可以自定义这个实现类,然后再去遍历每一个url找到所对应的handler
处理器映射器找到具体的处理器(可以根据xml去配置、或者注解都可以),找到handler后会返回给DispatcherServlet
DispatcherServlet拿到接口之后会去调用HandlerAdapter处理器适配器,也是一个接口,有默认的实现,也可以自定义通过xml文件,DispatcherServlet会根据Handler调用sprot方法,找到他的一个适配器,再根据适配器去调一个handle的方法,然后Controller执行完成返回ModelAndView
HandlerAdapter将controller执行结果返回ModelAndView返回给DispatcherServlet,然后DispatcherServlet将ModelAndView传给ViewReslover视图解析器
ViewReslover解析后返回具体的View
DispatcherServlet根据view进行渲染视图,最后DispatcherServlet响应给用户
分布式事务
回答分布式事务的问题时, 可以参考以下结构:
-
定义:简单解释分布式事务是指多个数据库或者服务之间需要保持一致性的一系列操作,这些操作要么全部成功,要么全部失败,不存在部分成功或者部分失败的情况。
-
ACID 原则:解释传统的 ACID 原则(原子性、一致性、隔离性、持久性)在分布式环境中的挑战以及如何解决这些挑战。
-
两阶段提交(2PC):阐述 2PC 的基本原理,包括协调者和参与者的角色,以及两个阶段(准备阶段和提交阶段)的操作过程。同时弄清楚 2PC 的缺点,如同步阻塞、单点故障等问题。
-
三阶段提交(3PC):讲解 3PC 是对 2PC 的一种改进,以解决同步阻塞问题。介绍 3PC 的三个阶段(can-commit、pre-commit、do-commit)以及在第一阶段引入超时机制来避免阻塞问题。
-
TCC(Try-Confirm-Cancel):说明 TCC 是一种基于补偿的分布式事务解决方案,它通过使用三阶段(Try、Confirm、Cancel)来保证最终一致性。在 Try 阶段执行业务预检查和资源预留,在 Confirm 阶段进行业务确认,最后在 Cancel 阶段执行回滚操作。
-
BASE 理论:解释基于 BASE(基本可用、软状态、最终一致性)理论的分布式事务实现。相比严格的 ACID,BASE 允许短暂的不一致,并提供柔性的事务处理方式。
-
分布式事务框架:提及一些常见的分布式事务框架,如分布式事务协调器 Sagas、可靠消息最终一致性方案等。
-
应用选择:根据实际情况,解释何时使用分布式事务以及选择合适的方案,考虑到性能、一致性需求、容错能力等因素。
在回答问题时,尽量使用简明的语言,结合具体的案例或者应用场景进行说明,以展示对分布式事务的理解和实践经验。
对于分布式事务,公司一般按照以下几个方面去划分服务:
-
业务域:根据公司的业务领域和业务模型,将不同的业务功能划分为独立的服务。每个服务可以负责特定的业务逻辑和数据操作。
-
功能模块:将整个系统拆分为若干个独立的功能模块,每个模块负责特定的功能。每个功能模块可以部署为一个独立的服务,并通过事务控制保证数据的一致性。
-
可扩展性和性能考虑:根据系统的可扩展性和性能需求,将一些高并发、高负载的功能拆分为更小的服务单元,以便更好地进行横向扩展和负载均衡。
-
数据库划分:如果系统中涉及多个数据库,可以将数据库作为一个独立的服务进行划分,以便维护数据的一致性和事务的完整性。
-
系统稳定性和容错性:针对核心的、关键的功能模块,可以将其作为独立的服务进行划分,以确保系统的稳定性和容错性。这样可以做到故障隔离,某个服务出现故障时,不会影响整个系统的正常运行。
需要根据具体的业务需求和系统特点来进行服务划分,并在设计和实现中注意考虑分布式事务的一致性和并发控制。同时,选择适当的分布式事务解决方案和架构模式,如两阶段提交、补偿事务等,以确保分布式事务的可靠性和性能。
MQ
如何确保消息正确地发送至RabbitMQ?如何确保消息接收方消费了消息?
发送方确认模式
1.将信道设置成confirm模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的ID。
2.一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一 ID)。
3.如果 RabbitMQ发生内部错误从而导致消息丢失,会发送一条nack(notacknowledged,未确认)消息。发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者应用程序,生产者应用程序的回调方法就会被触发来处理确认消息。
接收方确认机制
接收方消息确认机制
消费者接收每一条消息后都必须进行确认(消息接收和消息确认是两个不同操作)。只有消费者确认了消息,RabbitMQ才能安全地把消息从队列中删除。这里并没有用到超时机制,RabbitMQ仅通过Consumer的连接中断来确认是否需要重新发送消息。也就是说,只要连接不中断,RabbitMQ给了Consumer足够长的时间来处理消息。保证数据的最终一致性;
下面罗列几种特殊情况
如果消费者接收到消息,在确认之前断开了连接或取消订阅,RabbitMQ会认为消息没有被分发,然后重新分发给下一个订阅的消费者。
(可能存在消息重复消费的隐患,需要去重)如果消费者接收到消息却没有确认消息,连接也未断开,则RabbitMQ认为该消费者繁忙,将不会给该消费者分发更多的消息。
MQ发送消息的过程
RabbitMQ发送消息的过程可以简要描述如下:
-
生产者发布消息:
-
生产者(Producer)创建一个连接(Connection)到RabbitMQ服务器,并创建一个通道(Channel)。
-
生产者通过通道声明一个交换机(Exchange),并指定消息的路由键(Routing Key)。
-
生产者将消息发布到指定的交换机上,同时指定消息的路由键。
-
-
交换机路由消息:
-
RabbitMQ根据交换机的类型和路由键,将消息路由到一个或多个队列(Queue)中。
-
如果交换机类型是直接交换机(Direct Exchange),则会根据消息的路由键精确匹配到队列。
-
如果交换机类型是扇形交换机(Fanout Exchange),则会将消息发送到绑定到该交换机上的所有队列。
-
如果交换机类型是主题交换机(Topic Exchange),则会根据路由键和队列绑定的模式进行匹配。
-
-
消息存入队列:
-
消息根据交换机的路由规则存入一个或多个队列中。
-
队列是消息的最终存储位置,等待消费者来消费消息。
-
-
消费者消费消息:
-
消费者(Consumer)通过连接到RabbitMQ服务器,并创建一个通道。
-
消费者订阅(或从)指定的队列,在通道上注册一个消费者回调函数(Consumer Callback)。
-
RabbitMQ将队列中的消息推送给消费者,消费者处理消息并确认(Acknowledge)消息接收。
-
在整个过程中,RabbitMQ通过交换机、队列和路由键的组合,实现了灵活的消息路由和传递机制,确保消息能够可靠地从生产者传递到消费者。
.如何避免消息重复投递或重复消费?
在消息生产时,MQ内部针对每条生产者发送的消息生成一个inner-msg-id,作为去重的依据(消息投递失败并重传),避免重复的消息进入队列
在消息消费时,要求消息体中必须要有一个 bizId(对于同一业务全局唯一,如支付ID、订单ID、帖子ID 等)作为去重的依据,避免同一条消息被重复消费。
mq在什么情况下会消息丢失
MQ(消息队列)在以下情况下可能会导致消息丢失:
-
未持久化的消息:如果消息被发送到了非持久化的队列或者使用了非持久化的方式发送消息,当消息队列服务重启或发生故障时,这些消息可能会丢失。
-
非事务性消息的发送:在一些消息队列系统中,如果发送方发送消息时选择了非事务性的方式,当消息还未完全发送完成就发生了异常,消息可能会丢失。
-
网络故障或消息传输失败:当消息在传输过程中发生网络故障或者传输失败时,有些消息队列可能没有内置的重试机制或者重试机制不足以保证消息不丢失。
-
消息超时:某些情况下,消息队列可能会设置消息的过期时间(TTL),如果消息在规定的时间内未被消费者消费,消息可能会被丢弃。
-
消费者消费能力不足:如果消息队列中的消息积压过多,或者消费者消费速度跟不上生产者的发送速度,部分消息可能会因为队列满了而被丢弃。
为了最大程度地避免消息丢失,可以采取以下措施:
-
使用持久化队列和持久化消息。
-
在发送消息时使用事务性的方式。
-
实现消息重试机制,确保在消息发送失败时可以重新尝试发送。
-
合理设置消息的过期时间,避免消息长时间积压。
-
监控和调整消息队列的性能,确保消费者的消费能力与消息的生产速度相匹配。
如何确保消息不丢失?
消息持久化,当然前提是队列必须持久化
RabbitMQ确保持久性消息能从服务器重启中恢复的方式是,将它们写入磁盘上的一个持久化日志文件,当发布一条持久性消息到持久交换器上时,Rabbit会在消息提交到日志文件后才发送响应。
一旦消费者从持久队列中消费了一条持久化消息,RabbitMQ会在持久化日志中把这条消息标记为等待垃圾收集。如果持久化消息在被消费之前RabbitMQ重启,那么Rabbit会自动重建交换器和队列(以及绑定),并重新发布持久化日志文件中的消息到合适的队列。
RabbitMQ的集群。
镜像集群模
你创建的queue,无论元数据还是queue里的消息都会存在于多个实例上,然后每次你写消息到queue的时候,都会自动把消息到多个实例的queue里进行消息同步。
好处在于,你任何一个机器宕机了,没事儿,别的机器都可以用。坏处在于,第一,这个性能开销也太大了吧,消息同步所有机器,导致网络带宽压力和消耗很重!第二,这么玩儿,就没有扩展性可言了,如果某个queue负载很重,你加机器,新增的机器也包含了这个queue的所有数据,并没有办法线性扩展你的queue。
mq的缺点
系统可用性降低
系统引入的外部依赖越多,越容易挂掉,本来你就是A系统调用BCD三个系统的接口就好了,人 ABCD四个系统好好的,没啥问题,你偏加个MQ进来,万一MQ挂了咋整?MQ挂了,整套系统崩溃了,你不就完了么。
系统复杂性提高硬生生加个MQ进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已。
一致性问题
A系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是BCD三个系统那里,BD两个系统写库成功了,结果C系统写库失败了,咋整?你这数据就不一致了。
所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,最好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了10倍。但是关键时刻,用,还是得用的。
如何保证RabbitMQ消息的顺序性?
拆分多个queue(消息队列),每个queue(消息队列) 一个consumer(消费者),就是多一些queue(消息队列)而已,确实是麻烦点;
或者就一个queue (消息队列)但是对应一个consumer(消费者),然后这个consumer(消费者)内部用内存队列做排队,然后分发给底层不同的worker来处理。
设计MQ思路。
比如说这个消息队列系统,我们从以下几个角度来考虑一下:
首先这个mq得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下kafka的设计理念,broker->topic->partition,每个partition放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给topic增加partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了?
其次你得考虑一下这个mq的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。
其次你考虑一下你的mq的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的kafka的高可用保障机制。多副本 -> leader&follower->broker挂了重新选举 leader 即可对外服务。能不能支持数据0丢失啊?可以呀,有点复杂的
如何保证高可用的?RabbitMQ的集群?
RabbitMQ是比较有代表性的,因为是基于主从(非分布式)做高可用性的,我们就以RabbitMQ为例子讲解第一种MQ的高可用性怎么实现。RabbitMQ有三种模式:单机模式、普通集群模式、镜像集群模式。
单机模式,就是Demo级别的,一般就是你本地启动了玩玩儿的?,没人生产用单机模式
普通集群模式:
意思就在多台机器上启动多个RabbitMQ实例,每个机器启动一个。
你创建的queue,只会放在一个RabbitMQ实例上,但是每个实例都同步queue的元数据(元数据可以认为是queue的一些配置信息,通过元数据,可以找到queue所在实例)。
你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从queue所在实例上拉取数据过来。这方案主要是提高吞吐量的,就是说让集群中多个节点来服务某个queue的读写操作。
镜像集群模式:
这种模式,才是所谓的RabbitMQ的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的queue,无论元数据还是 queue 里的消息都会存在于多个实例上,就是说,每个RabbitMQ节点都有这个queue的一个完整镜像,包含queue的全部数据的意思。然后每次你写消息到queue的时候,都会自动把消息同步到多个实例的queue上。
RabbitMQ有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建queue的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。
这样的好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个queue的完整数据,别的consumer都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!RabbitMQ一个queue的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个queue的完整数据。
2、RabbitMq的死信队列和延迟队列
RabbitMQ是一个功能强大的消息队列中间件,其中有两个特性与队列的延迟处理相关:死信队列和延迟队列。
-
死信队列(Dead Letter Queue):
-
死信队列是一种特殊的队列,用于接收其他队列中的“死信”消息。
-
当一个消息被标记为“死信”时,会被发送到死信队列,而不是直接被丢弃。
-
产生死信的几种情况:消息被拒绝、消息过期、队列满等。
-
死信队列可以被用于消息的二次处理、错误处理或日志记录等。
-
-
延迟队列(Delay Queue):
-
延迟队列是一种特殊的队列,用于在一定延迟后将消息传递给消费者。
-
通常,消息进入延迟队列后并不会立即发送给消费者,而是会被延迟处理。
-
延迟队列可以通过设置消息的过期时间或者通过配合死信队列来实现,具体实现方式取决于消息中间件的支持。
-
延迟队列可以应用于订单超时处理、定时任务、延迟通知等场景。
-
需要注意的是,RabbitMQ本身并不直接支持延迟队列,但可以通过结合其他特性来实现延迟队列的效果。一种常见的实现方式是使用死信队列和TTL(Time To Live)消息过期时间配合使用,根据消息的过期时间将消息路由到对应的延迟队列,然后再从延迟队列中取出消息进行处理。
总结来说,死信队列和延迟队列都是在消息处理过程中引入的机制,用于处理消息的延迟、错误或者重新处理。死信队列用于接收死信消息并进行后续处理,而延迟队列则用于实现消息的延迟处理,结合其他特性可以达到延迟队列的效果。
mysql
mysql有哪几种数据库存储引擎,有什么区别
Mysql中通过指令可以看到所有支持的数据引擎show engines,我们最常了解的也就是innodb,Myisam,像其他的一些blackhole,所有的数据消失了。
从指令中可以看到,有很多是只有innodb支持的
1. 存储文件,myisam每个表有两个文件MYD,MYISAM文件,数据文件和索引文件,而innodb每个表只有文件
2. Innodb是支持事务,支持行锁,外键
3. Innodb支持XA事务,开放性的事物标准,seta事务模式,XA事务级别 可以去手动开启事务
MySQL 中的索引主要可以分为以下几种类型:
-
B-tree 索引:最常见的索引类型,适用于各种数据类型,包括数字和字符串。B-tree 索引能够快速定位到具体的记录。
-
哈希索引:只能用于精确匹配查找,并且只适用于整数类型的列。哈希索引在等值比较中非常快,但是不支持范围查找和排序。
-
全文索引:用于全文搜索的索引类型,适合于对文本内容进行搜索。MySQL 使用全文索引来优化对大文本字段的搜索。
-
空间索引:用于空间数据类型(例如地理坐标点)的索引。空间索引可以加速空间数据的查询和分析。
-
组合索引:将多个列组合起来创建的索引,可以优化涉及多个列的查询。组合索引的顺序很重要,影响查询性能。
-
唯一索引:确保索引列的值唯一,可以用来实现表级别的唯一约束。
-
主键索引:一种特殊的唯一索引,用于唯一标识表中的每一行记录。主键索引通常是表的主键列。
-
外键索引:用于建立表与表之间的关系,外键索引通常是参照另一表的主键索引。
以上是 MySQL 中常见的索引类型,每种类型都有其适用的场景和优缺点,选择合适的索引类型可以有效提升数据库查询的性能。
InnoDB 的四大特性?
-
插入缓冲insert buffer)
-
二次写(double write)
-
自适应哈希索引(ahi)
-
预读(read ahead)
什么是 InnoDB 的页、区、段?
页(Page) 首先,InnoDB 将物理磁盘划分为页(page),每页的大小默认为 16 KB,页是最小的存储单位。页根据上层应用的需要,如索引、日志等,分为很多的格式。我们主要说数据页,也就是存储实际数据的页。
区(Extent) 如果只有页这一个层次的话,页的个数是非常多的,存储空间的分配和回收都会很麻烦,因为要维护这么多的页的状态是非常麻烦的。
所以,InnoDB 又引入了区(Extent) 的概念。一个区默认是 64 个连续的页组成的,也就是 1MB。通过 Extent 对存储空间的分配和回收就比较容易了。
段(Segment) 为什么要引入段呢,这要从索引说起。我们都知道索引的目的是为了加快查找速度,是一种典型的用空间换时间的方法。
B+ 树的叶子节点存放的是我们的具体数据,非叶子结点是索引页。所以 B+ 树将数据分为了两部分,叶子节点部分和非叶子节点部分,也就我们要介绍的段 Segment,也就是说 InnoBD 中每一个索引都会创建两个 Segment 来存放对应的两部分数据。
Segment 是一种逻辑上的组织,其层次结构从上到下一次为 Segment、Extent、Page。
Oracle和MySQL是两种常见的关系型数据库管理系统(RDBMS),它们在一些关键方面有以下区别:
-
厂商:
-
Oracle:由Oracle公司开发和维护,是一家全球领先的企业级数据库解决方案提供商。
-
MySQL:最初由瑞典公司MySQL AB开发,现在由Oracle公司拥有和维护,MySQL AB在被Sun Microsystems收购后又被Oracle收购。
-
-
开源性质:
-
Oracle:Oracle数据库是商业软件,有企业版和标准版等不同许可证,通常需要付费使用。
-
MySQL:MySQL数据库有两种版本,一种是开源的MySQL Community Server,另一种是商业版MySQL Enterprise Edition。MySQL Community Server可以免费使用,而MySQL Enterprise Edition需要付费许可证。
-
-
功能和复杂性:
-
Oracle:作为一种成熟的企业级数据库,Oracle提供了广泛的高级功能和管理工具,适合处理大型和复杂的数据管理需求。它支持更复杂的事务处理、高级的数据安全性和扩展性。
-
MySQL:MySQL在功能上相对轻量级,更适合用于中小型应用和网站开发,虽然也支持大部分SQL标准功能,但在一些高级特性上(如分区表、行级锁定、存储过程)与Oracle相比有所不同。
-
-
性能:
-
Oracle:由于其高级特性和复杂的架构,Oracle通常在处理复杂查询和大规模数据时表现良好,尤其是在大型企业环境中。
-
MySQL:MySQL以其简洁和高性能而闻名,尤其在处理简单查询和低至中等规模的数据时表现优秀。它被广泛用于Web应用和小型企业系统中。
-
-
适用场景:
-
Oracle:适合需要高度安全性、复杂查询、大数据处理和高可用性的企业级应用,如金融、电信等大型系统。
-
MySQL:适合用于Web开发、中小型企业的应用、轻量级数据处理和云环境下的部署,如电子商务平台、博客、小型网站等。
-
总体来说,选择Oracle还是MySQL取决于项目的具体需求、预算和复杂性。Oracle适合大型、高需求的企业级应用,而MySQL则更适合中小型应用和需要开源解决方案的场景。
sql优化
在项目中进行 SQL 优化可以提高数据库的性能和效率。以下是一些常见的 SQL 优化策略:
-
索引优化:使用合适的索引可以加快 SQL 查询速度。分析常用的查询语句,确定需要创建的索引以及索引的列,避免创建过多无效的索引。
-
查询优化:尽量避免将整个表作为查询结果,而是通过合理的条件和限制查询范围来减少数据库的负载。使用合适的 JOIN 语句,避免过多的 JOIN 操作。
-
避免不必要的数据传输:只选择需要的列,避免返回大量的无用数据。尽量将计算或过滤操作放在数据库层面完成,而不是在代码中处理。
-
合理使用缓存:将经常读取的数据缓存到内存或其他缓存系统中,减少对数据库的访问。
-
使用批量操作:尽量使用批量插入、更新、删除等操作,而不是逐条操作数据库,减少与数据库的交互次数。
-
SQL 语句的编写:编写高效的 SQL 语句,避免使用过于复杂的查询。尽量减少子查询的使用,优化 WHERE 子句的条件顺序。
-
定期进行数据库维护:索引重建、数据库统计信息更新、碎片整理等操作能够提升数据库性能。
-
服务器优化:合理配置数据库服务器的内存、CPU、磁盘空间等资源,确保服务器能够满足项目的需求。
总的来说,SQL 优化需要结合具体的项目和数据库引擎来进行,通过不断的测试、分析和调整来提升数据库的性能和效率。
索引失效的场景
-
高选择性列的索引实效:当某列的取值非常少,且不均匀分布时,索引的选择性很低,即索引的不同取值并不显著减少查询结果集的大小,此时使用索引可能不会带来明显的性能提升。
-
模糊查询的索引实效:对于模糊查询(如使用 LIKE 运算符),如果通配符(如%)位于搜索模式的开头,索引可能无法使用,因为通配符的存在使得索引无法准确匹配。
-
数据量较小的表索引实效:对于非常小的表,使用索引可能会引入额外的开销,因为数据库可能会选择直接扫描整个表而不是使用索引。
-
多列索引的顺序问题:对于多列索引,列的顺序非常重要。如果查询条件中只使用索引的后几列,那么索引可能会失效。此外,如果多列索引的列顺序与查询中的列顺序不一致,索引也可能无法生效。
-
频繁的更新和插入操作:对于频繁进行更新和插入操作的表,索引的维护成本可能会很高,因为每次更新都需要更新索引。在这种情况下,索引可能会降低性能,可以考虑在批量操作后重新创建索引。
-
不合理的表结构和查询策略:如果表结构设计不合理,或者查询语句的写法不规范,可能导致索引无法发挥作用。例如,使用不等于(!=)操作符或函数运算符(如UPPER)可能会使索引无效。
-
数据分布不均匀:如果数据分布不均匀,某些值的频率非常高,而其他值的频率非常低,那么在查询时使用索引可能不会带来明显的性能提升。
要解决索引实效的问题,需要进行详细的性能分析和优化。可以通过监控查询执行计划、优化查询语句、重新评估表结构和索引设计等方式来改进索引的使用和效果。
什么是脏读,幻读,不可重复读
这些都是mysql进行事务并发控制的时候遇到的问题
脏读:在事务进行过程中读到了其他事物没有提交的数据
不可重复读,在一个事务中,多次查询的结果不一样
幻读,在一个事务过程当中,用同样的操作查询数据,得到的记录数不相同,得到了类似幻想中的数据
处理的方式有很多,加锁,事务隔离,mvcc
加锁首先对脏读,在修改的时候,可以加一个排它锁,直到事务提交才释放,读取的时候,加一个共享锁。
不可重复度:就是读数据时,加一个共享锁,写数据时,加一个排它锁
幻读,不需要锁表,范围锁就行了
为何使用 B+ 树而非二叉查找树做索引?
在许多数据库系统中,B+树通常被用作索引结构,而不是二叉查找树(BST),有以下几个原因:
-
磁盘IO优化:B+树在设计上更加适合磁盘存储和访问。由于索引通常存储在磁盘上,B+树的节点可以容纳更多的键和指针,从而减少磁盘IO操作的次数。而BST则需要频繁地进行平衡操作,会导致频繁的节点分裂和合并,并且在磁盘上分散存储,增加了IO开销。
-
顺序访问:B+树的叶子节点是通过链表连接起来的,能够支持范围查询和顺序访问,这对于数据库的范围查询操作非常高效。而BST的叶子节点并没有顺序连接,无法直接支持范围查询,需要进行中序遍历等操作。
-
容量扩展性:B+树可以很容易地扩展到支持大量数据,通过增加新的节点并进行平衡操作即可。而BST在插入或删除操作后可能需要进行频繁的平衡操作,导致性能下降。
-
磁盘块的利用:B+树节点的大小通常与磁盘块大小相匹配,使得磁盘块的利用率得到优化。相比之下,BST的节点大小可能不适合磁盘块,导致在存储和访问时的额外开销。
综上所述,B+树具有更好的磁盘IO优化、顺序访问性能和扩展性,更适合作为索引结构。它对于数据库系统中大量数据的存储和查询操作具有很好的效率和性能表现。
什么是最左匹配原则
最左匹配原则(Leftmost Match Principle)是指在数据库查询优化中,索引的最左前缀将被优先利用来执行查询。具体来说,当一个查询条件包含联合索引的多个列时,数据库引擎会优先使用索引中最左边的列进行匹配。
最左匹配原则的应用流程如下:
-
索引使用:当查询中的条件能够与联合索引的最左前缀完全匹配时,数据库优化器会选择该索引进行查询,以减少检索的数据量。
-
范围查询:如果查询条件中的列无法匹配索引的最左前缀,但仍能够使用索引中的部分前缀进行匹配,那么索引可以加速范围查询。
-
不匹配索引:如果查询条件无法与索引的最左前缀匹配,那么数据库引擎将无法使用该索引,并采用其他策略进行查询,例如全表扫描。
最左匹配原则的好处是能够最大程度地利用索引,提高查询的效率。然而,需要注意以下几点:
-
索引列的顺序应与查询列的顺序相匹配,以便最左匹配生效。
-
联合索引中列的顺序需要根据查询频率和查询条件的选择性进行合理的考虑,以保证高效使用索引。
-
如果查询中包含了范围查询和不匹配索引的情况,性能可能会受到影响,需要评估和优化查询条件和索引设计。
总之,最左匹配原则是一种数据库查询优化的原则,通过利用索引的最左前缀来匹配查询条件,提高查询效率和性能。合理设计索引和查询条件能够更好地利用最左匹配原则,从而优化数据库查询。
事务的基本特性和隔离级别
*事务表示多个数据的操作组成的事务单元,事务内的所以数据要么同时成功,要么同时失败*
*事务的特性:ACID*
1、**原子性:要么完全成功,要么完全失败**
2、**一致性:无论成功还是失败, 都要保持事务的一致性,失败的时候,需要进行回滚,不管中途是否成功还是失败**
3、**隔离性:多个事务操作一个数据的时候,为了防止数据损坏,需要将每个事务进行隔离,互不干扰**
4、**持久性:事物开始就不会去中止,结果不受其他外在因素的影响**
事务的隔离级别
分布式
M\ysql隔离级别就是一个参数show variables like transaction
设置隔离级别 set transaction level xxx设置下次的隔离级别
MySQL支持多种事务隔离级别,每种级别对并发控制和数据一致性有不同的影响。以下是MySQL中的五种事务隔离级别:
-
读未提交(Read Uncommitted):
-
允许事务读取其他事务未提交的数据。
-
可能导致脏读(Dirty Read)、不可重复读(Non-repeatable Read)和幻读(Phantom Read)问题。
-
最低的隔离级别,性能较高,但数据一致性风险最大。
-
-
读已提交(Read Committed):
-
保证一个事务不会读取到其他事务未提交的数据。
-
可能导致不可重复读和幻读问题,但可以避免脏读。
-
-
可重复读(Repeatable Read):
-
保证同一事务多次读取同样的数据结果一致性。
-
可以避免脏读和不可重复读,但仍可能出现幻读问题。
-
-
可串行化(Serializable):
-
最高的隔离级别,确保事务之间的完全隔离。
-
通过对读取的数据集加锁来避免脏读、不可重复读和幻读问题,保证数据的严格一致性。
-
性能开销较大,通常会导致并发度降低,因为读取的数据集会被完全锁定。
-
-
未提交读(Read Uncommitted):
-
一种特殊的隔离级别,与读未提交类似,但在InnoDB存储引擎中,未提交读不会用到,仅用于兼容性。
-
在MySQL中,默认的事务隔离级别是**可重复读(Repeatable Read)。选择合适的隔离级别取决于应用的并发需求、数据访问模式和对数据一致性的要求。
*Mysql中的锁*
*从锁的粒度来划分*
1. *行锁 力度小,但是开销大*
*共享锁,读锁,多个事务可以对同一个数据共享一把锁,有锁的事务都可以访问*
*排它锁,写锁,只有一个事务能够获得排它锁,其他事物都不能获取该行的锁*
*自增锁,通常是针对mysql当中的自增字段,如果有事务回滚这种情况,数据会回滚,但自增序列不会回滚*
2. *表锁 粒度大,加锁资源开销比较小*
*表共享锁*
*表排他锁*
*意向锁*
3. *全局锁,加锁之后整个数据库都处于只读状态,所有的数据变更操作都会被挂起*
*记录锁*
*间隙锁*
Mysql的索引结构是怎么样的
M\ysql的索引结构就是一个树,树就是一个一个的节点,有关键的key的数字,那我们查找数据的时候,可以通过这个索引快速的查找,
二叉树- AV树 -红黑树 - b树 b+树
二叉树,每个节点最多有两个子节点,左边的子节点都比当前节点小,右边的子节点都比当前节点大,容易有不平衡的问题
*红黑树:1、每个节点都是红色或者黑色2、根节点是黑色3、每个叶子节点都是黑色的空节点4、红色节点的父子节点都必须是褐色5、从任一节点到其每个叶子节点的所有路径都包含相同的黑色节点*
*B-数1、B树的每个非叶子节点个数都不会超过D 2、所有的叶子结点都在同一层 3、所有节点关键字都是按照递增顺序排列*
*B+树 1、非叶子节点不存储数据,只进行数据索引 2、所有数据都存储在叶子节点当中 3.每个叶子节点都存在相邻叶子节点的指针4、叶子节点按照本身关键字从小到达排序*
*聚族索引就是索引和数据在一起的*
*Myisam使用的就是非聚簇索引,树的子节点上的data不是数据本身,而是数据存放的地址,Innodb采用的就是聚簇索引,树的叶子节点就是data数据本身*
lock的底层实现原理
*Lock 有三个实现类,一个是 ReentrantLock, 另两个是 ReentrantReadWriteLock 类中的两个静态内部类 ReadLock 和 WriteLock。*
LOCK 的实现类其实都是构建在 *AbstractQueuedSynchronizer* 上
可以看到Lock锁的*底层实现是AQS*
*==AQS 的核心思想*==
*如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 CLH 队列锁(CLH 锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性)实现的,即将暂时获取不到锁的线程加入到队列中。*
*AQS 是将每一条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node),来实现锁的分配。*
2、InnoDB 为什么设计 B+ 树索引?
两个考虑因素:
InnoDB 需要执行的场景和功能需要在特定查询上拥有较强的性能。
CPU 将磁盘上的数据加载到内存中需要花费大量时间。
为什么选择 B+ 树:
哈希索引虽然能提供O(1)复杂度查询,但对范围查询和排序却无法很好的支持,最终会导致全表扫描。
B 树能够在非叶子节点存储数据,但会导致在查询连续数据可能带来更多的随机 IO。
而 B+ 树的所有叶节点可以通过指针来相互连接,减少顺序遍历带来的随机 IO。
普通索引还是唯一索引?
由于唯一索引用不上 change buffer 的优化机制,因此如果业务可以接受,从性能角度出发建议你优先考虑非唯一索引。
Synchronized
(1)可重入性
synchronized的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁;
可重入的好处: 可以避免死锁; 可以让我们更好的封装代码; synchronized是可重入锁,每部锁对象会有一个计数器记录线程获取几次锁,在执行完同步代码块时,计数器的数量会-1,直到计数器的数量为0,就释放这个锁。
(2)不可中断性
一个线程获得锁后,另一个线程想要获得锁,必须处于阻塞或等待状态,如果第一个线程不释放锁,第二个线程会一直阻塞或等待,不可被中断; synchronized 属于不可被中断; Lock lock方法是不可中断的; Lock tryLock方法是可中断的;
synchronized关键字是Java里面最基本的同步手段,它经过编译之后,会在同步块的前后分别生成 monitorenter和 monitorexit字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解锁的对象;而直接使用 synchronized 关键字锁定方法时,生成的字节码指令里面并没有 monitorenter 和 monitorexit 这两个指令,而是为方法添加了一个flags: ACC_SYNCHRONIZED, 该标识指明了该方法是一个同步方法。
(1) synchronized在编译时会在同步块前后生成monitorenter和monitorexit字节码指令或者ACC_SYNCHRONIZED的标识; (2)monitorenter和monitorexit字节码指令需要一个引用类型的参数,基本类型不可以哦; (3)monitorenter和monitorexit字节码指令更底层是使用Java内存模型的lock和unlock指令; (4)synchronized是可重入锁; (5)synchronized是非公平锁; (6)synchronized可以同时保证原子性、可见性、有序性; (7)synchronized有三种状态:偏向锁、轻量级锁、重量级锁;
4、请谈谈 volatile 有什么特点,为什么它能保证变量对所有线程的可见性?
volatile只能作用于变量,保证了操作可见性和有序性,不保证原子性。
在Java的内存模型中分为主内存和工作内存,Java内存模型规定所有的变量存储在主内存中,每条线程都有自己的工作内存。
主内存和工作内存之间的交互分为8个原子操作:
lock
unlock
read
load
assign
use
store
write
volatile修饰的变量,只有对volatile进行assign操作,才可以load,只有load才可以use,,这样就保证了在工作内存操作volatile变量,都会同步到主内存中。
5、volatile 关键字的作用
对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens-before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。
从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。
volatile 常用于多线程环境下的单次操作(单次读或者单次写)。
6多线程中 synchronized 锁升级的原理是什么?
synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
7、synchronized 和 ReentrantLock 区别是什么?
synchronized 是和 if、else、for、while 一样的关键字,ReentrantLock 是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量
synchronized 早期的实现比较低效,对比 ReentrantLock,大多数场景性能都相差较大,但是在 Java 6 中对 synchronized 进行了非常多的改进。
相同点:两者都是可重入锁
两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。
主要区别如下:
ReentrantLock 使用起来比较灵活,但是必须有释放锁的配合动作;
ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动释放和开启锁;
ReentrantLock 只适用于代码块锁,而 synchronized 可以修饰类、方法、变量等。
二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的park 方法加锁,synchronized 操作的应该是对象头中 mark word
Java中每一个对象都可以作为锁,这是synchronized实现同步的基础:
普通同步方法,锁是当前实例对象
静态同步方法,锁是当前类的class对象
同步方法块,锁是括号里面的对象
什么是 PostgreSQL?
Postgres或在 SQL 世界中简称为Postgresql是广泛和流行的对象关系数据库管理系统之一,主要用于大型 Web 应用程序。它是功能强大的开源对象关系数据库系统之一。它通过结合四个基本概念来提供额外的强大功能,以便用户可以毫无问题地扩展系统。它扩展并使用结合各种功能的 SQL 语言来安全地扩展和存储复杂的数据工作负载。
\2) 列出 Postgresql 的一些特性?
以下是 Postgresql 的一些主要功能:
-
对象关系数据库
-
支持主流操作系统
-
支持 SQL 和复杂 SQL 查询的可扩展性
-
嵌套事务
-
灵活的 API 和数据库验证
-
多版本并发控制 (MVCC) 和过程语言
-
WAL 和客户端服务器
-
表继承和异步复制
mvcc
什么是 MVCC ? MVCC MVCC,全称 Multi-Version Concurrency Control ,即多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。 mvcc - @百度百科
MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读 MVCC 能解决什么问题,好处是? 数据库并发场景有三种,分别为:
读-读:不存在任何问题,也不需要并发控制 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失 MVCC 带来的好处是? 多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题
MVCC 的实现原理
MVCC 的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 , Read View 来实现的。所以我们先来看看这个三个 point 的概念
隐式字段 每行记录除了我们自定义的字段外,还有数据库隐式定义的 DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID 等字段
DB_TRX_ID 6 byte,最近修改(修改/插入)事务 ID:记录创建这条记录/最后一次修改该记录的事务 ID DB_ROLL_PTR 7 byte,回滚指针,指向这条记录的上一个版本(存储于 rollback segment 里) DB_ROW_ID 6 byte,隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以DB_ROW_ID产生一个聚簇索引 实际还有一个删除 flag 隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除 flag 变了
多线程
1、线程和进程的区别
进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
2、java中实现线程有哪几种方式
· 继承 Thread 类实现多线程
· 2)实现 Runnable 接口方式实现多线程
· 3)使用 ExecutorService、Callable、Future 实现有返回结果的多线程
3. *BIO、NIO、AIO 有什么区别?*
(1)同步阻塞BIO
一个连接一个线程。
JDK1.4之前,建立网络连接的时候采用BIO模式,先在启动服务端socket,然后启动客户端socket,对服务端通信,客户端发送请求后,先判断服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝请求,如果有的话会等待请求结束后才继续执行。
(2)同步非阻塞NIO
NIO主要是想解决BIO的大并发问题,BIO是每一个请求分配一个线程,当请求过多时,每个线程占用一定的内存空间,服务器瘫痪了。
JDK1.4开始支持NIO,适用于连接数目多且连接比较短的架构,比如聊天服务器,并发局限于应用中。
一个请求一个线程。
(3)异步非阻塞AIO
一个有效请求一个线程。
JDK1.7开始支持AIO,适用于连接数目多且连接比较长的结构,比如相册服务器,充分调用OS参与并发操作。
*NIO*
(New lO)也有人称之为java non-blocking lO是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java lO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,*NIO支持面向缓冲区的、基于通道的IO操作*。NIO将以更加高效的方式进行文件的读写操作。*NIO可以理解为非阻塞IO*,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。
· NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。
· NIO有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
· Java NlO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
· 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有1000个请求过来,根据实际情况,可以分配20或者80个线程来处理。不像之前的阻塞IO那样,非得分配1000个。
*4、线程的run()和start()有什么区别?*
每个线程都是通过某个特定Thread对象所对应的方法run()来完成其操作的,run()方法称为线程体。通过调用Thread类的start()方法来启动一个线程。
start() 方法用于启动线程,run() 方法用于执行线程的运行时代码。run() 可以重复调用,而 start() 只能调用一次。
start()方法来启动一个线程,真正实现了多线程运行。调用start()方法无需等待run方法体代码执行完毕,可以直接继续执行其他的代码; 此时线程是处于就绪状态,并没有运行。 然后通过此Thread类调用方法run()来完成其运行状态, run()方法运行结束, 此线程终止。然后CPU再调度其它线程。
run()方法是在本线程里的,只是线程里的一个函数,而不是多线程的。 如果直接调用run(),其实就相当于是调用了一个普通函数而已,直接待用run()方法必须等待run()方法执行完毕才能执行下面的代码,所以执行路径还是只有一条,根本就没有线程的特征,所以在多线程执行时要使用start()方法而不是run()方法。
redis
12/4
1、redis是单线程还是多线程
Redis6.0版本之前的单线程指的是其网络i/o和键值对读写由一个线程完成的
Redis6.0引入的多线程指的是网络请求过程采用了多线程,而键值对读写命令任然是单线程处理的,所以redis依然是并发安全的
也就是只有网络请求模块和数据操作模块是单线程的,而其他的持久化,集群数据同步等,其实是由额外的线程执行的。
1、redis单线程为什么这么快
\1. 命令执行基于内存操作,一条命令在内存里操作的时间是几十纳秒
\2. 命令执行是单线程操作,没有线程切换开销
\3. 基于io多路复用机制提升redis的i/o利用率
\4. 高效的数据存储结构:全局hash表以及多种高效数据结构:比如调表,压缩列表,链表
Hash列表 一维数组,二维链表
2、redis底层数据是如何用调表来进行存储的
将有序链表改造为支持近似“折半查找”算法,可以进行快速的插入,删除,查找操作
3、redis过期了为什么内存没释放
使用set命令时,如果刚开始就设置了过期时间,那么之后修改这个key,也务必加上过期时间的参数,避免过期时间丢失问题
Redis对于过期key的处理一般有惰性删除和定时删除两种策略
\1. 惰性删除:当读写一个过期的key时,会触发惰性删除策略,判断key是否过期,如果过期了直接删除这个key
\2. 定时删除:由于惰性删除策略无法保证冷数据即时被删除掉,所以reids会定期(100ms)主动淘汰一批已经过期的key,这里的一批只是部分过期key,所以可能会出现部分key已经过期,但还没有被删除的情况,导致内存没有被释放
4、reids没有设置key过期时间,为什么被redis主动删除了
12/5
1、redis淘汰key的算法LRU与LFU的区别
LRU算法 (最近最少使用)淘汰很久没被访问过的数据,以最近一次访问时间作为参考
LFU算法(最不经常使用)淘汰最近一段时间被访问次数最少的数据,以次数作为参考
绝大多数情况下我们都用LRU策略,当存在大量的热点缓存数据时,LFU可能会更好一点。
2、删除key的命令,会阻塞redis吗?
分情况回答,当删除单个字符串类型的key,时间复杂度为0(1),删除单个列表,集合,有序集合或哈希表类型的key,时间复杂度为0(m),m数据结构内的元素量
3、redis主从,哨兵,集群架构优缺点比较
4、
5、redis集群数据hash分片算法是怎么回事
redis集群将所有数据划分为16384个槽位,每个节点负责其中一个槽位,槽位的信息存储于每个节点中
当redis集群的客户端连接集群时,它也会集群的槽位配置信息并将其缓存在客户端本地。
这样客户端要查找某个key的时候,可以更=根据槽位定位算法找到目标节点
槽位定位算法
集群默认会对key值使用crc16算法进行hash得到一个整数值,然后用这个整数值对16384进行取模,来得到具体的槽位。再根据槽位置和redis节点的对应关系就可以定位到key具体是落在那个redis节点的
12/7
1、Redis执行命令竟然有死循环阻塞bug
2、一次线上事故,redis主从切换导致了缓存雪崩
Slave的机器时钟比master节点走的快很多,此时redis master里设置了过期时间的key,以slave角度来看,可能会有很多的master里没过期的数据已经过期了
如果此时操作主从切换,把slave提升为新的master
\1. 它成为主节点后,就会开始清理大量过期的key,主线程可能会发生阻塞,无法及时处理客户端请求
\2. Redis中大量数据过期,引起缓存雪崩
4、线上redis 持久化策略一般如何设置
如果对性能要求较高,在Master最好不要做持久化,可以在某个slave开启AOF数据备份策略,策略设置为每秒同步一次
6、redis线上数据如何备份
\1. 写crontab定时调度脚本,每小时都copy一份RDB或AOF文件到另一台机器当中去,保留最近48小时的备份
\2. 每天都保留一份当日的数据备份,到一个目录去,可以保留最近一个月的备份
\3. 每次copy备份的时候,都把太久的删除
7、redis主从复制风暴是怎么回事
如果redis主节点有很多从节点,在某一时刻如果所有的从节点都同时连接主节点,那么主节点会同时把内存RDB快照发给多个从节点,这样会导致主节点压力非常大,这就是所谓的主从复制风暴问题,可以采用树形复制架构解决。
2、redis集群为什么至少需要三个master节点
因为新的master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的
3、redis集群为什么推荐奇点数个节点
Redis 集群推荐奇数个节点的原因主要是为了确保在进行数据迁移、故障转移等操作时能够有足够的投票节点,以确保系统的高可用性和数据一致性。在 Redis 集群中,节点之间通过投票来决定故障转移的行为,奇数个节点可以避免出现投票平局的情况,从而确保故障转移的可靠性。
4、redis集群支持批量操作命令吗?
redis集群
要搭建 Redis 的集群,可以按照以下步骤进行操作:
-
下载和安装 Redis:在每个节点上下载和安装 Redis。你可以从 Redis 官方网站或通过包管理工具获取 Redis 的适当版本。
-
配置节点:在每个节点的 Redis 配置文件中进行必要的设置。确保设置节点的端口号、密码、集群配置模式等。
-
创建集群配置文件:在任意一个节点上创建集群配置文件。该文件用于定义集群中的节点信息和槽位分配。可以使用 Redis 提供的 redis-trib.rb 工具(Ruby 工具)来帮助创建该配置文件。
-
启动节点:在每个节点上启动 Redis。确保节点的配置文件和监听端口正确。
-
加入集群:使用 redis-trib.rb 工具将其他节点加入到集群中。这涉及到执行一些命令,如
redis-trib.rb create
命令来创建集群。 -
分配槽位:使用 redis-trib.rb 工具将槽位分配给不同的节点。Redis 集群将数据分布在不同的槽位中,每个节点负责一部分槽位。
-
验证集群:使用
redis-cli
来连接到集群并验证其是否正常工作。可以执行一些基本的读写操作,并确保数据在不同节点间正确地分布。 -
监控和维护:监控 Redis 集群的状态,包括节点的健康状况、负载均衡等。进行必要的维护操作,如数据备份、故障恢复等。
请注意,以上步骤提供了一个基本的指南,具体的操作可能因为你的环境和需求而有所不同。在搭建 Redis 集群之前,确保了解 Redis 集群的特性和要求,并根据实际情况进行相应的配置和调整。
1、**缓存击穿,缓存穿透,缓存雪崩是什么?**
缓存中存放的大多都是热点数据,目的就是防止请求可以直接从缓存中获取数据,而不用去访问mysql
*缓存雪崩*:如果缓存中某一时间大量热点数据同时过期,那么可能导致大量的数据访问到mysql,解决方法就是在过期时间上增加一点随机值,另外如果搭建一个高可用的集群也是防止缓存雪崩的有效方法。
*缓存击穿*:和缓存雪崩类似,缓存雪崩是大量热点数据实效,而缓存击穿是指某一个热点key突然实效,也导致了大量的数据直接访问mysql,解决的方案就是考虑这个热点key不设置过期时间
*缓存穿透:*假如某一时间访问redis的大量key都在redis中不存在,那么也会给数据造成压力,解决的方法就是使用布隆过滤器,它的作用就是他认为一个key不存在,那么这个key就肯定不存在,所以可以在缓存之前,加一层布隆过滤器来拦截不存在的key。
2、Redis和Mysql如何保证数据一致性
1.先更新mysql再更新redis,如果redis更新失败,可能任然不一致。
2.先删除redis缓存数据,再更新mysql,再次查询的时候将数据添加到缓存中,这种方案能解决1,但是在高并发下性能较低,可能任然会出现数据不一致的问题,比如线程一删除了redis缓存数据,正在更新mysql数据,此时另外一个查询再查询,那么就会把mysql中的老数据又查到redis中了
3.延时双删:先删除redis缓存数据,再更新mysql,延迟几百毫秒再去删除redis缓存数据,这样就算在更新mysql时,有其他线程读取了mysql,再把老数据存入redis中,那么也会被删除,从而保持了数据的一致性。
2、redis中的RDB和AOF机制
在 Redis 中,RDB(Redis DataBase)和 AOF(Append Only File)是两种不同的持久化机制,用于在服务重启时保留数据。
RDB(Redis DataBase)
-
工作原理:
-
RDB 是通过快照的方式进行持久化,即周期性地将内存中的数据保存到硬盘上的一个文件(默认名为
dump.rdb
)中。 -
在指定的时间间隔内,如果有设定数量的写操作(例如写入了多少个 key),Redis 将执行保存操作。
-
-
优点:
-
RDB 是一个紧凑的、全量备份,适合用于备份、恢复数据。
-
对于大数据集和高频率写入的环境,RDB 在性能上通常比 AOF 好。
-
-
缺点:
-
如果 Redis 因某种原因宕机,可能会丢失最后一次生成的 RDB 文件中的数据。
-
RDB 是基于周期性的保存机制,可能导致数据丢失的窗口期。
-
AOF(Append Only File)
-
工作原理:
-
AOF 通过记录每次写操作(包括增删改操作),以追加的方式将这些操作记录到一个日志文件中。
-
Redis 重启时通过重新执行这些写操作来恢复数据集的原始状态。
-
-
优点:
-
AOF 提供了更可靠的数据保护,可以通过配置来调整不同的 fsync 策略来平衡性能和持久性。
-
AOF 日志文件以易读的方式记录了写操作,便于理解和维护。
-
-
缺点:
-
AOF 文件通常比 RDB 文件大,因为它记录了每个写操作。
-
AOF 恢复数据的速度可能比 RDB 恢复慢,尤其是对于大数据集。
-
选择和配置建议
-
RDB vs AOF:通常情况下,可以同时启用 RDB 和 AOF 来结合它们各自的优点。如果要选择其中一个,AOF 更适合要求数据持久性更高的场景,而 RDB 则适合要求备份恢复速度更快的场景。
-
持久化策略:可以根据具体的业务需求和性能要求,配置 Redis 的持久化策略,例如调整 RDB 和 AOF 的保存频率,以及 fsync 策略来平衡数据持久性和性能的需求。
总之,RDB 和 AOF 是 Redis 中重要的持久化机制,开发者需要根据具体场景和需求来选择和配置合适的方式来保护数据。
在指定的时间间隔内,将内存中的数据集快照写入磁盘中,实际操作过程是fork一个子进程,先将数据集写入临时文件,在替换之前的文件,用二进制进行压缩 。
优点:
\1. 整个redis数据库只包含一个文件dump.rdb方便持久化
\2. 容灾性好,方便备份
\3. 性能最大化,frok子进程来完成写操作,让主程序继续命令,所以是io最大化,使用单独的子进程来进行持久化,主进程不会进行任何的io操作,保证了redis的高性能。
\4. 相对于数据集大时,比AOF启动效率更高。
缺点:
\1. 数据安全性低,RDB时间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失,所以这种方式更适合数据要求不严谨的时候
\2. 由于RDB是通过fork子进程来协助完成数据化持久工作的,因此,当数据集较大时,可能会导致整个服务器停止几百毫秒,甚至是一秒钟
AOF
redis在项目中的使用
:我们经常根据条件查询数据,如果每查询一次数据都经过数据库,随着数据量增多,对数据库性能都有一定的消耗,效率也会大大降低,这时,我们就可以用Redis,可以把经常查询的数据第一次存到Redis中,再此查询时直接通过Redis查询,不走数据库,这样大大提高了数据查询效率而且也降低了对数据库的压力。
Redis 在项目中的使用非常广泛,它是一种高性能的内存数据存储系统,主要用于缓存、会话管理、消息队列、实时统计等场景。以下是 Redis 在项目中常见的使用方式:
-
缓存(Caching): Redis 作为缓存存储,可以将频繁查询的结果缓存到内存中,避免重复的数据库查询操作,提高访问速度和性能。经常使用的模式是将热门数据或计算结果缓存在 Redis 中,并设置合适的过期时间。
-
会话管理: Redis 可以用作会话存储,将会话相关的数据(如用户登录信息、购物车等)存储在 Redis 中。这样可以减轻应用服务器的负担,实现快速的会话访问和共享。另外,对于分布式环境,可以使用 Redis 提供的分布式锁功能来实现会话的并发控制。
-
消息队列(Message Queue): Redis 提供了列表或发布/订阅等机制,可用作消息队列。应用程序可以将消息发布到 Redis 队列中,然后消费者通过订阅或轮询方式获取消息并进行处理。这种方式适用于异步任务处理、解耦系统组件、实时消息通信等场景。
-
实时统计: Redis 的高性能和数据结构的灵活性使其成为实时统计数据的理想选择。通过 Redis 的计数器、有序集合和 HyperLogLog 等功能,可以实时统计访问量、用户活跃度、热门内容等信息。
-
分布式锁(Distributed Locks): Redis 提供了分布式锁的功能,可以保证在分布式系统中的并发操作的原子性和互斥性。应用程序可以使用 Redis 的锁机制,通过获取和释放锁来实现对共享资源的控制,避免并发冲突。
需要注意的是,Redis 作为内存存储系统,在持久化方面可能不如磁盘数据库持久化可靠。因此,在使用 Redis 时,可能需要根据具体业务需求选择适当的持久化方式(如 RDB 或 AOF)来保证数据的持久性和可靠性。同时,由于 Redis 对内存的需求较高,需要合理配置内存管理和监控,以避免超出内存限制导致性能问题或服务异常。
spring
Spring事务的传播机制:
事务的传播,是指一个方法调用另一个方法并将事务传递给他,事务的传播机制主要是针对被调用者而言,控制他是否被传播或者被怎样传播,spring的事务传播机制有七种
PROPAGATION_REQUIRED: 默认的事务传播级别,若当前存在事务,就加入该事务,若不存在事务,就新建一个事务。
PROPAGATION_REQUIRE_NEW: 若当前没有事务 ,就新建一个事务,若当前存在事务,则新建一个事务,新老事务相互独立,外部事务抛出异常回滚不会影响内部事务的正常提交。
PROPAGATION_NESTED: 如果当前存在事务,就嵌套在事务中执行,如果当前没有事务,则新建一个事务,类似于PROPAGATION_REQUIRE_NEW
PROPAGATION_SUPPORTS:支持当前事务,若当前不存在事务,以非事务的方式执行
PROPAGATION_NOT_SUPPORTED: 以非事务的方式执行,若当前存在事务,则把当前事务挂起
PROPAGATION_MANDATORY: 强制事务执行,若不存在当前事务,则抛出异常。
PROPAGATION_NEVER: 以非事务的方式执行,如果当前存在事务,则抛出异常。
Spring中拦截器和过滤器的区别
在Spring中,拦截器(Interceptor)和过滤器(Filter)是两个不同的概念,虽然它们的作用有些类似,但是它们的使用场景和实现方式有所不同。
-
拦截器(Interceptor):
-
拦截器是Spring框架提供的一种机制,用于在请求处理的过程中进行预处理和后处理。它主要用于对处理器方法的调用进行拦截和处理,比如记录日志、权限检查、性能监控等。拦截器是Spring MVC框架中的一部分,由Spring容器管理。
-
拦截器是基于Java的反射机制实现的,可以直接调用Spring容器中的Bean,并对Handler执行预处理和后处理操作。
-
-
过滤器(Filter):
-
过滤器是Servlet规范中定义的一种组件,用于在请求进入Servlet之前或响应离开Servlet之后,对请求和响应进行预处理和后处理。过滤器是Servlet容器(如Tomcat、Jetty等)提供的一部分。
-
过滤器是基于Web服务器的Servlet规范实现的,可以用于处理URL请求、设置字符编码、检查用户会话状态等。
-
区别总结:
-
作用范围:
-
拦截器作用于Spring MVC的处理器方法,更关注于请求的处理链中的特定步骤。
-
过滤器作用于Servlet的请求和响应,更关注于Web应用程序的请求和响应的全局处理。
-
-
实现方式:
-
拦截器是Spring框架提供的接口,通过实现HandlerInterceptor接口来自定义拦截器。
-
过滤器是Servlet规范中的一部分,通过实现javax.servlet.Filter接口来自定义过滤器。
-
-
管理方式:
-
拦截器由Spring容器管理,因此可以直接依赖注入其他Spring管理的Bean。
-
过滤器由Servlet容器管理,无法直接依赖注入Spring的Bean,因此在处理时要注意管理上的差异。
-
在实际应用中,如果需要对特定的请求进行拦截处理,一般优先选择拦截器;如果需要对所有请求进行统一处理,例如字符编码设置、权限检查等,则使用过滤器更为合适。
Spring事务失效的场景
Spring 事务可以在某些场景下失效,导致事务不生效或不按预期执行。以下是一些常见的导致 Spring 事务失效的场景:
-
方法未被代理:Spring 事务依赖 AOP(面向切面编程)进行代理,如果目标方法没有被代理,那么事务将不会应用。确保目标方法被声明在 Spring 管理的 Bean 中,以便被代理。
-
异常未被捕获或处理:默认情况下,Spring 事务仅在遇到运行时异常或 unchecked 异常时回滚。如果发生受检异常或者异常没有被捕获和处理,事务可能不会回滚。可以使用
@Transactional
的rollbackFor
属性指定需要回滚的异常类型。 -
事务传播属性设置不正确:事务传播属性用于处理多个事务方法之间的交互。如果事务方法的传播属性设置错误,例如将一个具有
REQUIRES_NEW
属性的方法嵌套在没有事务的方法中,可能会导致事务失效。确保正确设置事务的传播属性。 -
未被 Spring 托管的方法:Spring 事务只会在被 Spring 托管的方法上生效。如果在与事务方法相同的类中调用另一个方法,但该方法没有被 Spring 托管,那么事务将不会应用到该方法。
-
数据库引擎不支持事务:某些数据库引擎不支持事务,或者事务设置不正确,例如数据库连接没有开启自动提交事务等。在使用 Spring 事务时,确保数据库引擎支持事务,并正确设置数据库连接。
-
方法未标注
@Transactional
:忘记在事务方法上添加@Transactional
注解,或者忘记在调用事务方法的方法上添加@Transactional
注解,会导致事务不生效。确保适当添加@Transactional
注解。 -
异步方法:Spring 事务默认不支持异步方法的事务管理。如果使用
@Async
注解标记方法为异步方法,事务不会在异步方法中生效。
以上是一些导致 Spring 事务失效的常见场景。在使用 Spring 事务时,需要仔细检查配置和代码,确保事务能够正常生效并按预期执行。
Spring注入依赖的方式
-
构造器注入(Constructor Injection):使用带有
@Autowired
注解的构造方法,Spring 会自动将匹配的依赖项注入到构造器中。 -
Setter 方法注入(Setter Injection):使用带有
@Autowired
注解的 setter 方法,Spring 会自动调用该方法并将匹配的依赖项注入。 -
字段注入(Field Injection):使用
@Autowired
注解直接标注在字段上,Spring 会自动将匹配的依赖项注入到字段中。
@autowired和@resource区别
当面试官问你关于 @Autowired
和 @Resource
注解的区别时,你可以简明扼要地回答如下:
@Autowired
和 @Resource
都是用于依赖注入的注解,但是有一些区别:
-
@Autowired
是 Spring 框架提供的注解,可以在 Spring 环境中使用。它通过类型进行依赖查找与注入,默认使用的是 byType 方式。 -
@Resource
是 JavaEE 标准提供的注解,可以在 JavaSE 或 JavaEE 环境中使用。它通过名称进行依赖查找与注入,默认使用的是 byName 方式。
此外,@Autowired
在功能上更强大一些,可以支持更多的依赖注入方式,并提供了更精确的依赖匹配。例如,可以使用 @Qualifier
进行限定符匹配,使用 @Primary
指定首选的 Bean。而 @Resource
主要用于字段注入和方法注入,并且使用一个 name
属性来指定 Bean 的名称。
根据具体的应用场景和框架选择,你可以进一步解释这些注解的使用方式和注意事项。记得在回答问题时语言简洁明了、不赘述,并且可以结合实际代码示例进行说明。如果面试官有进一步的追问,可以深入讨论。
Spring支持的几种bean的作用域
Singleton: 默认,每个容器中只有一个bean的实例,单例的模式由beanfactory自身来维护,该对象的生命周期与Spring Ioc容器一致的
Prototype:原型模式,作用域就不一样了,为每一个bean请求提供一个实例,也就是说每次get的时候都会提供一个实例的,每次注入都会创建一个新的对象。
Request: bena被定义在每个HTTP请求中创建一个单例对象,也就是说在单个请求中都会复用这个单例对象。请求一旦结束的话,就会回收。
Session:与request范围类似,确保每一个session中有一个bean的实例,在session过期后,bean会随之实效。
Applicatipn:bean被定义在ServletContext的生命周期中复用一个单例对象。
Websocket:bean被定义在websocket的生命周期中复用的一个单例对象
Gloal-session:全局作用域 global-session和protlet应用相关。
Spring的IoC理解:
(1)什么是IOC:
IOC,Inversion of Control,控制反转,指将对象的控制权转移给Spring框架,由 Spring 来负责控制对象的生命周期(比如创建、销毁)和对象间的依赖关系。 最直观的表达就是,以前创建对象的时机和主动权都是由自己把控的,如果在一个对象中使用另外的对象,就必须主动通过new指令去创建依赖对象,使用完后还需要销毁(比如Connection等),对象始终会和其他接口或类耦合起来。而 IOC 则是由专门的容器来帮忙创建对象,将所有的类都在 Spring 容器中登记,当需要某个对象时,不再需要自己主动去 new 了,只需告诉 Spring 容器,然后 Spring 就会在系统运行到适当的时机,把你想要的对象主动给你。也就是说,对于某个具体的对象而言,以前是由自己控制它所引用对象的生命周期,而在IOC中,所有的对象都被 Spring 控制,控制对象生命周期的不再是引用它的对象,而是Spring容器,由 Spring 容器帮我们创建、查找及注入依赖对象,而引用对象只是被动的接受依赖对象,所以这叫控制反转。
(2)什么是DI:
IoC 的一个重点就是在程序运行时,动态的向某个对象提供它所需要的其他对象,这一点是通过DI(Dependency Injection,依赖注入)来实现的,即应用程序在运行时依赖 IoC 容器来动态注入对象所需要的外部依赖。而 Spring 的 DI 具体就是通过反射实现注入的,反射允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性
(3)IoC的原理:
Spring 的 IoC 的实现原理就是工厂模式加反射机制,而在 Spring 容器中,Bean 对象如何注册到 IoC 容器,以及Bean对象的加载、实例化、初始化详细过程可以阅读这篇文章:Spring的Bean加载流程_张维鹏的博客-CSDN博客
Spring的AOP理解:
OOP面向对象,允许开发者定义纵向的关系,但并不适用于定义横向的关系,会导致大量代码的重复,而不利于各个模块的重用。
AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务处理。
AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
(1)AspectJ是静态代理,也称为编译时增强,AOP框架会在编译阶段生成AOP代理类,并将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。 Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理:
① JDK动态代理只提供接口的代理,不支持类的代理,要求被代理类实现接口。JDK动态代理的核心是InvocationHandler接口和Proxy类,在获取代理对象时,使用Proxy类来动态创建目标类的代理类(即最终真正的代理类,这个类继承自Proxy并实现了我们定义的接口),当代理对象调用真实对象的方法时, InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;
InvocationHandler 的 invoke(Object proxy,Method method,Object[] args):proxy是最终生成的代理对象; method 是被代理目标实例的某个具体方法; args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。
② 如果被代理类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
(3)静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。
IoC让相互协作的组件保持松散的耦合,而AOP编程允许你把遍布于应用各层的功能分离出来形成可重用的功能组件。
Spring框架中的Bean是线程安全的么?如果线程不安全,那么如何处理?
Spring容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但是具体情况还是要结合Bean的作用域来讨论。
(1)对于prototype作用域的Bean,每次都创建一个新对象,也就是线程之间不存在Bean共享,因此不会有线程安全问题。
(2)对于singleton作用域的Bean,所有的线程都共享一个单例实例的Bean,因此是存在线程安全问题的。但是如果单例Bean是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Controller类、Service类和Dao等,这些Bean大多是无状态的,只关注于方法本身。
有状态Bean(Stateful Bean) :就是有实例变量的对象,可以保存数据,是非线程安全的。
无状态Bean(Stateless Bean):就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。
对于有状态的bean(比如Model和View),就需要自行保证线程安全,最浅显的解决办法就是将有状态的bean的作用域由“singleton”改为“prototype”。
也可以采用ThreadLocal解决线程安全问题,为每个线程提供一个独立的变量副本,不同线程只操作自己线程的副本变量。
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。
Spring循环依赖的问题
@Lazy注解:解决构造方法造成的循环依赖问题,就是多个对象之间,存在循环的的引用关系,在初始化的过程当中,就会出现一个类似于先有鸡还是先有蛋的问题,相当于是初始化的时候,让一步。,@Lazy就是给了一个初始化为空的对象,需要有一个空参的构造函数,能够把这个空的对象先初始化出来
在单例池下面创建一个缓存,二级缓存里面存放的不是一个完整的对象,是没有属性值的对象,但他最终到单例池都是完整的
二级缓存对于对象之间普通引用,会保存new出来的不完整对象,这样当单例池中找到不依赖的属性时,就可以先从二级缓存中获取到不完整对象,完成对象创建,在后续的依赖注入过程中,将单例池中对象的引用关系调整完成
三级缓存:如果引用的对象配置了aop,那在单例池中最终就会需要注入动态代理对象,而不是原对象。而生成动态代理是要在对象完成之后去完成的,于是spring增加三级缓存,保存所有对象的动态代理配置信息,在发现有循环依赖时,将这个对象的动态代理信息获取出来,提前进行AOP,生成动态代理。
核心代码在DefaultSingletonBeanRegistery的getSingleton方法当中
Spring中的隔离级别:
① ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。
② ISOLATION_READ_UNCOMMITTED:读未提交,允许事务在执行过程中,读取其他事务未提交的数据。
③ ISOLATION_READ_COMMITTED:读已提交,允许事务在执行过程中,读取其他事务已经提交的数据。
④ ISOLATION_REPEATABLE_READ:可重复读,在同一个事务内,任意时刻的查询结果都是一致的。
⑤ ISOLATION_SERIALIZABLE:所有事务逐个依次执行。
Spring 框架中都用到了哪些设计模式?
Spring设计模式的详细使用案例可以阅读这篇文章:Spring中所使用的设计模式张维鹏的博客-CSDN博客spring使用的设计模式
(1)工厂模式:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象
(2)单例模式:Bean默认为单例模式
(3)策略模式:例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略
(4)代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术
(5)模板方法:可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中,用来解决代码重复的问题。比如RestTemplate, JmsTemplate, JpaTemplate
(6)适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring MVC中也是用到了适配器模式适配Controller
(7)观察者模式:Spring事件驱动模型就是观察者模式的一个经典应用。
(8)桥接模式:可以根据客户的需求能够动态切换不同的数据源。比如我们的项目需要连接多个数据库,客户在每次访问中根据需要会去访问不同的数据库
Spring Bean的生命周期?
简单来说,Spring Bean的生命周期只有四个阶段:实例化 Instantiation --> 属性赋值 Populate --> 初始化 Initialization --> 销毁 Destruction
Spring Bean 的生命周期包括以下几个阶段:
-
实例化(Instantiation): 在容器启动时,Spring 根据配置信息或者注解实例化 Bean 对象。
-
依赖注入(Dependency Injection): 实例化后,Spring 将依赖注入到 Bean 中,包括通过构造函数、Setter 方法或字段注入。
-
初始化方法调用(Initialization): 如果 Bean 实现了
InitializingBean
接口或者在配置文件中使用<init-method>
指定了初始化方法,则在依赖注入完成后调用该方法。 -
使用中(In Use): Bean 处于活动状态,可以被应用程序使用,执行业务逻辑。
-
销毁前处理(Pre Destruction): 如果 Bean 实现了
DisposableBean
接口或者在配置文件中使用<destroy-method>
指定了销毁方法,则在容器关闭之前调用该方法。 -
销毁(Destruction): 在容器关闭时,Spring 调用 Bean 的销毁方法,进行清理工作。
在整个生命周期中,Spring 容器负责管理 Bean 的创建、初始化、依赖注入和销毁过程,开发者可以通过配置文件或注解来影响和控制 Bean 的生命周期行为。
但具体来说,Spring Bean的生命周期包含下图的流程:
(1)实例化Bean:
对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。
对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。
(2)设置对象属性(依赖注入):实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以及 通过BeanWrapper提供的设置属性的接口完成属性设置与依赖注入。
(3)处理Aware接口:Spring会检测该对象是否实现了xxxAware接口,通过Aware类型的接口,可以让我们拿到Spring容器的一些资源:
①如果这个Bean实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,传入Bean的名字; ②如果这个Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。 ②如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。 ③如果这个Bean实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文; (4)BeanPostProcessor前置处理:如果想对Bean进行一些自定义的前置处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法。
(5)InitializingBean:如果Bean实现了InitializingBean接口,执行afeterPropertiesSet()方法。
(6)init-method:如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。
(7)BeanPostProcessor后置处理:如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术;
以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。
(8)DisposableBean:当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;
(9)destroy-method:最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。
BeanFactory和ApplicationContext有什么区别?
BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。
(1)BeanFactory是Spring里面最底层的接口,是IoC的核心,定义了IoC的基本功能,包含了各种Bean的定义、加载、实例化,依赖注入和生命周期管理。ApplicationContext接口作为BeanFactory的子类,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:
继承MessageSource,因此支持国际化。 资源文件访问,如URL和文件(ResourceLoader)。 载入多个(有继承关系)上下文(即同时加载多个配置文件) ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。 提供在监听器中注册bean的事件。 (2)①BeanFactroy采用的是延迟加载形式来注入Bean的,只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能提前发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。
②ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 ③ApplicationContext启动后预载入所有的单实例Bean,所以在运行的时候速度比较快,因为它们已经创建好了。相对于BeanFactory,ApplicationContext 唯一的不足是占用内存空间,当应用程序配置Bean较多时,程序启动较慢。
(3)BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。
(4)BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。