目录
- 1、== 和equals()有什么区别?
- 2、String变量直接赋值和构造函数赋值==比较相等吗?
- 3、String一些方法?
- 4、抽象类和接口有什么区别?
- 5、Java容器有哪些?
- 6、List、Set还有Map的区别?
- 7、线程创建的方式?
- 8、Runable和Callable有什么区别?
- 9、启动一个线程是run()还是start()?
- 10、介绍Spring IOC和Spring Aop?
- Spring IOC(控制反转)
- 什么是IOC
- Spring IOC容器
- 依赖注入
- Spring AOP(面向切面编程)
- 什么是AOP
- Spring AOP实现方式
- 切面(Aspect)
- 11、Spring框架使用到的设计模式?
- 12、Mybatis#()和$()有什么区别?
- 13、Mysql的四个隔离级别以及默认隔离级别?
- 14、A事务未提交,B事务上查询到的是旧值还是新值?
- 15、编写sql语句哪些情况导致索引失效?
- 16、Redisson的底层原理?以及与SETNX的区别?
- 17、了解的MVCC模式?
- 18、Redis的持久化方式?
- 19、RDB和AOF的区别?Redis宕机哪种恢复的比较快?
- 20、乐观锁和悲观锁?
- 悲观锁(Pessimistic Locking)
- 乐观锁(Optimistic Locking)
- 21、库存的超卖问题的原因和解决方案?
1、== 和equals()有什么区别?
==
和 equals()
在 Java 中的主要区别如下:
- == 运算符:
- 对于基本数据类型(如 int、char、boolean、float 等),
==
比较的是它们存储的值是否相等。 - 对于引用类型(如对象引用),
==
比较的是两个对象引用是否指向内存中的同一块地址,也就是说,它检查的是对象的引用(内存地址)是否相同,而不是它们的内容是否相等。
- equals() 方法:
-
equals()
是 Object 类的一个方法,所有 Java 类都继承自 Object 类,因此所有对象都有equals()
方法。 - 在 Object 类中,默认的
equals()
实现也是比较对象的引用是否相等,就像==
进行的对象引用比较一样。 - 但是,许多类(如 String、Integer、Date 等)都重写了
equals()
方法,以便按照类的业务逻辑来比较对象的内容是否相等,而不是比较引用。 - 当你在自定义类中编写代码时,如果你希望
equals()
能够基于对象的状态(属性值)来进行相等性判断,你需要重写equals()
方法,并且通常建议同时重写hashCode()
方法以保持一致性。
总结来说,当你需要比较两个基本类型的值是否相等时,使用 ==
就足够了;而在比较两个对象是否相等时,尤其是当比较对象内容而非引用时,应该使用 equals()
方法,并确保你清楚该类是否以及如何重写了 equals()
方法。
2、String变量直接赋值和构造函数赋值==比较相等吗?
在Java中,对于String变量直接赋值和通过构造函数赋值,只要内容相同,使用==
进行比较的结果也是相等的,但这并不是因为赋值方式不同,而是因为Java中String的特性决定的。
Java的String类被设计为不可变(immutable),无论是通过字面量赋值(直接赋值)还是通过构造函数赋值,只要字符串内容一致,那么在内存中就会复用同一个字符串对象。这得益于Java的字符串常量池机制。
例如:
String s1 = "Hello";
String s2 = new String("Hello");
System.out.println(s1 == s2); // 在某些版本的Java中,输出可能是true
上述例子中,虽然 s1
是通过字面量赋值的,而 s2
是通过构造函数创建的,但由于 “Hello” 字符串在常量池中存在,所以在运行时常量池中 s1
和 s2
实际上可能引用的是同一个对象,因此 s1 == s2
可能会返回 true
。
然而,如果字符串不是常量池中的内容,或者明确进行了字符串对象的创建,那么即使内容相同,也会生成不同的对象,这时 ==
比较的结果就是 false
:
String s3 = new String("World") + "!";
String s4 = "World!";
System.out.println(s3 == s4); // 输出一定是false,因为这是两个不同的对象
另外需要注意的是,为了比较字符串的内容是否相等,应当始终使用 equals()
方法或者 Objects.equals()
方法,而不是 ==
,因为 equals()
会比较字符串的实际内容,不受对象引用的影响。例如:
String s5 = new String("test");
String s6 = new String("test");
System.out.println(s5.equals(s6)); // 输出true,因为内容相同
3、String一些方法?
Java中的String
类提供了众多用于处理和操作字符串的方法,以下是一些常见的String
类方法:
- 获取字符串长度:
int length() // 返回字符串中的字符数。
- 访问特定位置的字符:
char charAt(int index) // 返回指定索引位置的字符。
- 字符串比较:
int compareTo(String anotherString) // 按照字典顺序比较两个字符串。
boolean equals(Object anObject) // 检查此字符串与指定对象是否相等。
boolean equalsIgnoreCase(String anotherString) // 忽略大小写检查字符串是否相等。
int compareToIgnoreCase(String str) // 不区分大小写比较两个字符串。
int hashCode() // 返回此字符串的哈希码,可用于集合中的快速查找。
- 查找子串或字符:
int indexOf(int ch) // 返回指定字符在此字符串中首次出现的位置。
int indexOf(String str) // 返回指定子串在此字符串中首次出现的位置。
int lastIndexOf(int ch) // 返回指定字符在此字符串中最后一次出现的位置。
int lastIndexOf(String str) // 返回指定子串在此字符串中最后一次出现的位置。
- 子串操作:
String substring(int beginIndex) // 返回从指定索引开始到字符串结尾的子串。
String substring(int beginIndex, int endIndex) // 返回从指定开始索引到指定结束索引之间的子串。
- 连接字符串:
String concat(String str) // 将指定字符串连接到此字符串的结尾。
- 字符串替换:
String replace(char oldChar, char newChar) // 替换所有指定字符为新字符。
String replace(CharSequence target, CharSequence replacement) // 替换第一次出现的目标子串为新的子串。
- 字符串切割:
String[] split(String regex) // 根据匹配给定的正则表达式来拆分此字符串。
- 转换大小写:
String toLowerCase(Locale locale) // 使用给定的语言环境将字符串转换为小写。
String toUpperCase(Locale locale) // 使用给定的语言环境将字符串转换为大写。
- 修剪空白:
String trim() // 移除字符串两端的空白字符。
- 字符串转换:
char[] toCharArray() // 将字符串转换为字符数组。
byte[] getBytes(Charset charset) // 将此字符串编码为指定字符集的字节序列。
- 字符串是否以某个子串开头或结尾:
boolean startsWith(String prefix) // 检查此字符串是否以指定的前缀开始。
boolean endsWith(String suffix) // 检查此字符串是否以指定的后缀结束。
这只是String
类的部分方法,更多详细信息可以查阅Oracle官网的Java API文档。
4、抽象类和接口有什么区别?
抽象类(Abstract Class)和接口(Interface)在Java中都是为了实现抽象和多态而存在的,但它们之间有着明显的区别:
- 定义与抽象程度:
- 抽象类:可以包含抽象方法(没有实现的方法,由子类去实现)以及具体方法(已经实现了的方法)。抽象类可以有属性(变量)并且可以有构造方法。
- 接口:只允许包含抽象方法(在Java 8及以后版本还可以包含默认方法和静态方法)和常量(默认为 public static final)。接口不允许定义构造方法和实例变量。
- 继承与实现:
- 抽象类:一个类只能继承一个抽象类(单一继承原则),通过关键字
extends
来继承。 - 接口:一个类可以实现多个接口,通过关键字
implements
来实现。接口之间也可以通过extends
关键字互相继承。
- 设计意图:
- 抽象类:主要用于定义一个类族的共同特征,体现的是“is-a”(是一个)的关系,侧重于描述类的内部属性和行为的一部分实现。
- 接口:更侧重于定义一组行为规范,体现了“can-do”(能做什么)的关系,主要用于规定类所应遵循的某种契约或协议,关注的是外部行为而不涉及内部实现。
- 使用场景:
- 抽象类:适合于构建抽象层次结构,比如在组件的体系结构中,抽象类可以封装共享的功能和属性,子类可以进一步细化或增加额外的功能。
- 接口:适用于实现多重继承、定义清晰的职责边界和约束条件,尤其在不同模块之间通信或对接时,通过接口实现解耦。
- 方法与变量修饰符:
- 抽象类中的方法可以是任意访问级别(public、protected、private),抽象方法默认为public,也可以显式声明为protected。
- 接口中所有的方法默认都是public abstract的,Java 8之后可以有default方法和static方法;接口中的变量默认为public static final,即只能是常量。
- 静态方法和静态代码块:
- 抽象类可以包含静态方法和静态代码块。
- 接口中在Java 8及以后版本才允许包含静态方法,但不能有静态代码块或实例变量。
总结来说,抽象类提供了一种继承层次上的抽象和实现部分,而接口则提供了一种纯粹的行为约定,更加灵活,有利于实现高度解耦的设计。
5、Java容器有哪些?
Java容器主要包括四大类别:
- List(列表):
- ArrayList:基于动态数组实现,查询速度快,增删慢,非线程安全,可以通过
Collections.synchronizedList
包装实现线程安全。 - LinkedList:基于双向链表实现,查询速度慢,增删快,同样是非线程安全的,也可通过同步包装实现线程安全。
- Vector:类似于 ArrayList,但它是线程安全的,不过由于同步开销,性能通常不如 ArrayList。
- Set(集合):
- HashSet:无序,不允许重复元素,基于哈希表实现,非线程安全。
- LinkedHashSet:具有HashSet的特点,同时保留插入顺序。
- TreeSet:有序,不允许重复元素,基于红黑树实现。
- Map(映射):
- HashMap:键值对存储,无序,允许null键和值,基于哈希表实现,非线程安全。
- LinkedHashMap:除了具有HashMap的特点外,还按插入顺序或最近最少使用(LRU)策略进行迭代。
- TreeMap:键值对存储,有序,允许null键但不允许null值,基于红黑树实现。
- Hashtable:类似于HashMap,线程安全,但性能较差,已被ConcurrentHashMap取代。
- ConcurrentHashmap:线程安全的哈希映射,支持高效并发访问。
- Queue(队列):
- PriorityQueue:优先级队列,基于堆实现,非线程安全。
- ArrayBlockingQueue:有界的阻塞队列,线程安全,基于数组实现。
- LinkedBlockingQueue:阻塞队列,基于链表实现,可选择有界或无界。
- Deque(双端队列)接口及其实现类,如ArrayDeque,LinkedList也实现了Deque接口。
除此之外,还有工具类,如Iterator(迭代器)、Enumeration(枚举类,旧版集合接口中使用较多)、Arrays和Collections,以及并发编程中使用的各种并发容器,如CopyOnWriteArrayList、CopyOnWriteArraySet等。
以上容器类都在java.util
包中,部分并发容器位于java.util.concurrent
包内。
6、List、Set还有Map的区别?
List、Set、Map是Java集合框架中三种主要的容器类型,它们在数据存储和访问方式上有显著的不同:
- List(列表):
- List接口表示有序的集合,其中的元素可以是重复的。
- 元素在List中是按照插入的顺序存储的,可以通过索引访问,索引是从0开始的整数。
- 常见的实现类有ArrayList、LinkedList和Vector等。
- ArrayList基于动态数组实现,查询速度快,插入和删除中间元素较慢(需移动后续元素)。
- LinkedList基于双向链表实现,插入和删除操作效率高,但随机访问性能相对较差。
- Set(集合):
- Set接口表示一个不允许有重复元素的集合,元素无序。
- Set不保证元素的插入顺序,主要关心的是唯一性,因此向Set中添加元素时,系统会自动检查是否存在重复。
- 常见的实现类有HashSet、LinkedHashSet和TreeSet等。
- HashSet基于哈希表实现,查找速度快,但不保证元素的顺序。
- TreeSet基于红黑树实现,自动排序,元素按照自然排序或定制排序规则排列。
- Map(映射):
- Map接口提供了一种存储键值对(key-value pair)的数据结构,键必须是唯一的,但值可以重复。
- Map中的元素是成对出现的,每一对由一个键和对应的值组成,可以通过键来检索对应的值。
- 常见的实现类有HashMap、LinkedHashMap、TreeMap、Hashtable以及ConcurrentHashMap等。
- HashMap和Hashtable同样是基于哈希表实现的,但后者是线程安全的,而HashMap在多线程环境下如果不加控制可能会出现问题。
- TreeMap基于红黑树实现,键值对是有序的。
总之,List适用于存储有序且可能重复的数据,Set适用于存储不重复的数据集,而Map则用于存储键值对关联数据。在选择使用哪种集合时,应考虑数据的特性和应用程序的需求。
7、线程创建的方式?
Java中创建线程主要有以下几种方式:
- 继承
Thread
类:
- 创建一个新的类,让它继承自
java.lang.Thread
类。 - 重写
Thread
类的run()
方法,该方法包含了线程需要执行的任务代码。 - 创建
Thread
子类的实例,并调用start()
方法来启动线程,而不是直接调用run()
方法。
示例代码:
class MyThread extends Thread {
@Override
public void run() {
// 线程任务代码
}
}
MyThread thread = new MyThread();
thread.start();
- 实现
Runnable
接口:
- 创建一个新的类,实现
java.lang.Runnable
接口。 - 重写
Runnable
接口的run()
方法。 - 创建
Thread
类的一个实例,将实现Runnable
接口的对象作为构造函数的参数传入。 - 调用
Thread
对象的start()
方法来启动线程。
示例代码:
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程任务代码
}
}
MyRunnable task = new MyRunnable();
Thread thread = new Thread(task);
thread.start();
- 使用
Callable
和Future
:
- 创建一个实现
java.util.concurrent.Callable
接口的类,并重写call()
方法,call()
方法能够返回一个值,并且可以抛出异常。 - 使用
FutureTask
类来包装Callable
对象,FutureTask
是Runnable
的实现,同时也持有Callable
的结果。 - 创建
Thread
并传入FutureTask
实例,启动线程后,可通过FutureTask
的get()
方法获取线程执行结果。
示例代码:
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
// 线程任务代码,返回一个结果
return "Callable result";
}
}
MyCallable callable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(callable);
Thread thread = new Thread(futureTask);
thread.start();
// 获取线程执行结果(会阻塞直到结果可用)
String result = futureTask.get();
- 通过
ExecutorService
和线程池:
- 使用
java.util.concurrent.ExecutorService
(如ThreadPoolExecutor
)来管理和控制线程。 - 提交
Runnable
或Callable
任务到线程池中,线程池会自动调度线程执行这些任务。
示例代码(使用 Executors
工具类创建线程池):
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
Runnable worker = () -> {
// 任务代码
};
executor.execute(worker);
}
// 或者提交 Callable 任务
Future<String> future = executor.submit(new MyCallable());
String result = future.get();
// 最后记得关闭线程池
executor.shutdown();
通过以上方式,可以根据应用场景选择合适的创建线程的方法,以满足程序需求。
8、Runable和Callable有什么区别?
Runnable
和 Callable
都是用来定义任务以供线程执行的接口,但在Java并发编程中,它们有以下显著区别:
- 返回值:
-
Runnable
: 实现Runnable
接口的任务没有返回值。它的run()
方法没有返回类型,这意味着一旦线程执行完毕,无法通过Runnable
直接获得执行结果。 -
Callable
: 实现Callable
接口的任务可以有返回值。Callable
的call()
方法有一个泛型返回类型,执行完成后,可以返回一个结果给调用者。
- 异常处理:
-
Runnable
:run()
方法不能抛出受检异常(checked exception),如果需要抛出异常,必须在run()
方法内部捕获并处理。 -
Callable
:call()
方法允许抛出受检异常,调用者可以通过Future
对象的get()
方法捕获并处理异常。
- 使用方式:
-
Runnable
: 通常用于创建线程时指定任务,直接使用Thread
类或通过ExecutorService
执行。 -
Callable
: 通常与Future
和FutureTask
结合使用,通过ExecutorService
提交任务并获取异步执行结果。当调用submit(Callable)
方法时,返回一个Future
对象,可以用来获取任务执行后的结果或状态。
总结来说,如果你只需要一个简单的任务不需要返回结果,可以选择使用 Runnable
;如果需要异步执行并获取执行结果,或者任务有可能抛出受检异常,那么应选择使用 Callable
。
9、启动一个线程是run()还是start()?
启动一个线程是使用 start()
方法,而不是 run()
方法。
在Java中,如果你想要启动一个新的线程执行任务,你应该创建一个 Thread
对象或实现 Runnable
接口,并将其传递给 Thread
构造函数,然后调用 Thread
对象的 start()
方法。start()
方法负责安排线程在Java虚拟机中并发地执行,当 start()
方法被调用后,它会调用线程的 run()
方法,但是重要的是,直接调用 run()
方法并不会启动新的线程,它将在当前线程上下文中同步执行。
10、介绍Spring IOC和Spring Aop?
Spring框架的两大核心特性是IoC(Inversion of Control,控制反转)和AOP(Aspect-Oriented Programming,面向切面编程)。
Spring IOC(控制反转)
什么是IOC
控制反转(IoC)是一种设计原则,它提倡将对象的创建、组装和依赖关系管理的责任从应用代码中移出,转交给一个专门的容器(在Spring中被称为IoC容器)。通过这种方式,对象不再自行创建或查找依赖的对象,而是由容器在运行时自动提供所需的依赖。
Spring IOC容器
Spring的IoC容器负责管理对象的整个生命周期,包括创建、初始化、装配(dependency injection,依赖注入)以及销毁对象。在Spring配置文件中,我们可以声明Bean的定义,容器根据这些定义来创建和管理对象。依赖注入允许我们在运行时将对象所需的服务或资源动态绑定到对象中,减少了代码间的耦合度。
依赖注入
依赖注入(DI)是IoC的实现手段,主要有以下几种形式:
- 构造器注入:通过构造器参数将依赖注入到对象中。
- Setter方法注入:通过setter方法设置对象依赖项。
- 注解注入:使用如@Autowired注解实现依赖注入。
Spring AOP(面向切面编程)
什么是AOP
面向切面编程是一种编程范式,它把横切关注点(如日志记录、事务管理、权限验证等)从主业务逻辑中抽离出来,通过声明式的方式统一管理,增强了程序的模块化和可维护性。AOP使得开发者可以集中在一个地方管理分散在多个方法或类中的交叉关注点。
Spring AOP实现方式
Spring AOP主要通过代理(Proxying)机制来实现。有两种代理方式:
- 动态代理(JDK Proxy或CGLIB代理):在运行时动态地创建代理对象,代理对象在方法调用前后执行切面逻辑。
- 编译时织入(例如借助AspectJ框架):在编译时就已经将切面逻辑织入到了目标类中。
切面(Aspect)
AOP中的切面定义了一系列的通知(Advice),这些通知在特定的连接点(Join Point)上执行,如方法调用、异常抛出等。通知类型包括前置通知(Before Advice)、后置通知(After Advice)、环绕通知(Around Advice)、最终通知(Finally Advice,无论方法正常结束还是异常退出都会执行)和引介通知(Introduction Advice,为类添加新的接口或方法)。
通过Spring AOP,开发者可以将横切关注点独立配置并在全局范围内应用,从而提高代码的复用性和系统的灵活性。
11、Spring框架使用到的设计模式?
Spring框架在设计和实现过程中广泛运用了多种设计模式,下面列出了一些关键的设计模式:
- 工厂模式:
- Spring通过
BeanFactory
和ApplicationContext
接口实现了工厂模式,这两个接口是Spring容器的基础,负责创建和管理对象(Bean)的生命周期。
- 单例模式:
- Spring容器默认将所有bean定义为单例,确保系统中只有一个共享的bean实例。通过控制bean的生命周期,Spring确保在系统中任何地方请求该bean时,总是返回相同的实例。
- 代理模式:
- Spring AOP(面向切面编程)大量使用了代理模式。Spring通过JDK动态代理或CGLIB代理为Bean创建代理对象,代理对象在方法调用前后执行通知(Advice)代码,实现诸如事务管理、日志记录等功能的横切关注点。
- 模板方法模式:
- Spring的
JdbcTemplate
、JmsTemplate
等类使用了模板方法模式,它们定义了执行数据库操作或消息发送的基本骨架,子类只需要关注具体的业务逻辑。
- 策略模式:
- 在Spring框架的多个组件中,如事务管理中定义的各种事务策略,都采用了策略模式,允许客户端根据不同的情况选择不同的策略(Strategy)。
- 适配器模式:
- Spring AOP中,Advice(通知)的实现就是一个适配器模式的应用,它允许将用户定义的业务逻辑(adaptee)包装成能够在join point处执行的切面。
- 装饰器模式:
- Spring在处理一些组件的时候,如处理web请求时,通过一系列过滤器和拦截器链,这些过滤器和拦截器可以看作是对原始请求处理器的装饰。
- 责任链模式:
- Spring框架中的过滤器链(Filter Chain)和拦截器链(Interceptor Chain)都体现了责任链模式,每个链上的过滤器或拦截器都可以决定是否继续执行下一个链上的处理单元。
- 依赖注入(DI):
- 虽然不是严格意义上的设计模式,但依赖注入的思想在Spring框架中得到了广泛应用,它有助于实现松耦合,是控制反转(IoC)的一种具体实现方式。
Spring框架整合了许多优秀的设计模式,通过合理组合和使用这些模式,构建了一个强大且灵活的轻量级企业级应用框架。
12、Mybatis#()和$()有什么区别?
在MyBatis中,#{}
和 ${}
分别用于在SQL语句中处理动态参数,它们在处理参数时有不同的行为和目的:
#{}
- 用途:用于预编译参数占位符,它可以防止SQL注入,因为它会将参数值当做字符串进行处理,并在SQL执行前对参数进行预编译和类型检查。
- 示例:
SELECT * FROM users WHERE id = #{userId}
- 处理方式:MyBatis会将参数用PreparedStatement的参数占位符(如
?
)替换,并在执行SQL时通过jdbc驱动提供的API进行参数设置,确保了安全性。
${}
- 用途:用于字符串替换,即将变量的值直接拼接到SQL语句中,不做任何特殊处理。
- 示例:
SELECT * FROM ${tableName} WHERE column = 'value'
- 处理方式:MyBatis会直接将变量值插入到SQL语句中,原样输出到最终执行的SQL中。
- 风险:由于未做预编译处理,如果变量内容未经严格控制,容易导致SQL注入攻击。因此,除非必要(如动态表名或列名),一般情况下应尽量避免使用
${}
。
总结来说,#{}
更安全,适用于大多数情况下的参数传递,而 ${}
仅在确实需要动态构造SQL结构时谨慎使用,并确保传入的值安全可靠。
13、Mysql的四个隔离级别以及默认隔离级别?
MySQL的四个事务隔离级别是基于SQL标准定义的,它们决定了事务之间如何相互影响彼此的数据读取和修改,旨在解决事务并发执行时可能出现的问题,如脏读(Dirty Reads)、不可重复读(Non-Repeatable Reads)、幻读(Phantom Reads)。
- 读未提交(Read Uncommitted)
- 在这种级别下,一个事务可以读取到其他事务未提交的数据变更,可能导致脏读。
- 这是最低的隔离级别,一般不推荐在生产环境中使用。
- 读已提交(Read Committed)
- 在这个级别,一个事务只能读取到其他事务已经提交的数据,解决了脏读问题,但仍然可能存在不可重复读和幻读的情况。
- 在这个级别下,每次查询都会获取最新的提交数据,所以两次相同的查询可能会得到不同的结果。
- 可重复读(Repeatable Read)
- MySQL的默认事务隔离级别。
- 在同一个事务内,多次执行相同的查询语句会得到相同的结果,即在同一事务内,其他事务对该事务可见的数据不会发生变化,解决了脏读和不可重复读的问题。
- 不过,在可重复读隔离级别下,如果其他事务插入了新的记录(幻读),即使这些记录符合当前事务的查询条件,在本事务中也无法看到这些新插入的数据。
- 串行化(Serializable)
- 这是最高的隔离级别,通过完全锁定事务涉及的所有数据行来防止任何并发问题,包括脏读、不可重复读和幻读。
- 在这个级别下,事务执行效果如同按照顺序逐个执行,因此并发性能最低,但数据一致性最强。
需要注意的是,MySQL的InnoDB存储引擎在实现可重复读隔离级别时,通过多版本并发控制(MVCC)技术有效地避免了幻读问题,但是在其他数据库系统中,即使在可重复读隔离级别也可能存在幻读现象。
14、A事务未提交,B事务上查询到的是旧值还是新值?
这个问题的答案取决于事务B所处的隔离级别:
- 读未提交(Read Uncommitted):
在这个级别,事务B可以看到事务A尚未提交的更改,即查询到的是A事务的新值,但也有可能是不一致的、随后可能被回滚的数据,即脏读。 - 读已提交(Read Committed):
在这个级别,事务B只能看到事务A已经提交的更改。如果事务A尚未提交,事务B查询时将看不到事务A对数据所做的更改,只能看到旧值。 - 可重复读(Repeatable Read):
这是MySQL的默认事务隔离级别。在该级别下,事务B在整个事务期间看到的数据是一致的,即事务开始时的旧值。即使事务A在事务B执行过程中提交了更新,事务B仍不会看到这些新提交的值,因此在这个隔离级别下,事务B看不到事务A未提交的新值。 - 串行化(Serializable):
在这个级别下,为了防止幻读,数据库系统通常会对事务进行更为严格的锁定,事务B要么等待事务A完成并提交,要么由于锁定冲突而中止。在这种情况下,事务B同样在事务A提交之前看不到事务A对数据的更改,查询到的是旧值。
综上所述,除非是在读未提交隔离级别,否则在其他三个隔离级别下,事务B在事务A未提交的情况下,查询到的将是旧值,而不是事务A尚未提交的新值。
15、编写sql语句哪些情况导致索引失效?
编写SQL语句时,以下情况可能导致索引失效或不被使用:
- 索引列使用了计算操作:
- 如果在查询条件中对索引列进行了数学运算、函数运算或类型转换,可能导致索引失效。例如:
SELECT * FROM table WHERE indexed_column + 1 = 10;
- LIKE查询以通配符开头:
- LIKE查询时,如果通配符
%
位于搜索词的开头,索引通常无法使用,例如:
SELECT * FROM table WHERE indexed_column LIKE '%value';
- 若要有效利用索引,搜索词应从非通配符开始,例如:
SELECT * FROM table WHERE indexed_column LIKE 'value%';
- OR条件查询:
- 当SQL语句中含有多个条件通过
OR
连接,且这些条件中只有部分条件涉及索引时,可能导致索引失效。MySQL优化器可能会选择全表扫描而不是使用索引。
SELECT * FROM table WHERE indexed_column = 'value1' OR non_indexed_column = 'value2';
- 隐式类型转换:
- 查询条件中索引列与其他类型的数据进行比较时,如果发生隐式类型转换,可能导致索引失效。例如:
SELECT * FROM table WHERE indexed_string_column = 123; -- 字符串索引与数字比较
- 范围查询后紧跟不相关的条件:
- 当查询中包含范围查询(如
BETWEEN
、>
、<
、>=
、<=
)时,对于复合索引,MySQL只能使用索引的左边部分,右边的列索引将会失效。例如:
SELECT * FROM table WHERE indexed_column1 = 'value' AND indexed_column2 > 100 AND non_indexed_column = 'some_value';
- 未使用索引覆盖:
- 如果查询返回的数据不仅包含索引列,还包括非索引列,即使查询条件使用了索引,但如果需要回表查询其他列,索引并不能达到最优的效果。
- 索引列使用了函数:
- 若在索引列上使用了数据库函数,如
TRIM
、DATE_FORMAT
等,索引通常不会被使用。
SELECT * FROM table WHERE TRIM(indexed_column) = 'value';
- 联合索引未遵循最左前缀匹配原则:
- 对于复合索引(如
index(column1, column2, column3)
),如果查询条件不从索引的最左列开始,后面的列索引可能无法使用。
- NOT操作符:
- 使用
NOT
操作符否定索引列的条件可能导致索引失效,例如:
SELECT * FROM table WHERE NOT indexed_column = 'value';
- 相比之下,改写为
indexed_column <> 'value'
可能更利于索引的使用。
总之,为了确保索引能够得到有效利用,应尽量避免上述情况,并确保查询条件能够精确匹配索引列或者符合索引的使用原则。同时,根据实际情况和数据库优化器的特性调整SQL查询结构也很重要。
16、Redisson的底层原理?以及与SETNX的区别?
Redisson 是一个高级 Redis 客户端,提供了 Java 的数据结构和分布式服务功能,如分布式锁、信号量、闭锁、队列等多种分布式数据结构。Redisson 的底层原理主要是基于 Redis 的原生命令和 Lua 脚本来实现高效的分布式操作。
Redisson 的实现原理概要:
- 数据结构封装:Redisson 封装了 Redis 的常用数据结构如 String、List、Set、SortedSet、Map 等,为 Java 开发者提供了便捷的操作接口。
- 分布式锁:
- 基于 SETNX 和 DEL 命令:早期版本的 Redisson 实现分布式锁时,可能使用 SETNX 命令尝试获取锁,当 key 不存在时设置成功并返回 true,代表获取锁成功。
- 基于 Lua 脚本:为了避免因网络中断等问题导致的锁丢失或死锁问题,Redisson 采用 Lua 脚本实现了一套更完善的分布式锁机制。在获取锁时,脚本会一次性完成检查锁存在与否、设置超时时间、设置锁标志等多个动作,确保操作的原子性。
- 自动续期:Redisson 的锁支持自动续期,即在持有锁的过程中定期延长锁的有效期,防止在长时间执行任务时因锁超时而意外释放。
- 监听器:Redisson 支持事件监听器,可以注册监听 Redis 中 key 的各种事件,方便开发者在数据变化时采取相应操作。
- 线程安全:Redisson 在多线程环境下的操作是线程安全的,确保了并发环境下对 Redis 资源的安全访问。
Redisson 与 SETNX 的区别:
- 基础操作:SETNX 是 Redis 的一个原生命令,仅用于设置一个键值对,当键不存在时才设置成功,常用于实现简单的分布式锁,但不具备自动续期、公平锁、锁超时清理等功能。
- 复杂性:Redisson 是一套完整的客户端库,除了提供分布式锁之外,还有一系列针对 Redis 的高级封装和分布式数据结构的支持。
- 可靠性增强:Redisson 的分布式锁基于 Lua 脚本实现,相比于直接使用 SETNX 提供了更高的可靠性保障,如公平锁机制、锁自动续期以及解锁失败时的自动清理机制等。
- 易用性:Redisson 提供了更丰富的 API 和更高级别的抽象,简化了开发者直接使用 Redis 命令进行分布式编程的复杂性。同时,Redisson 的分布式锁具备更全面的异常处理和错误恢复机制。
17、了解的MVCC模式?
多版本并发控制(MVCC, Multi-Version Concurrency Control)是一种在数据库管理系统中用于提高并发性能和数据一致性的技术。在MVCC模式下,系统允许多个事务同时查看数据库的某个历史版本,而不是强制等待其他事务结束才能读取数据,这样可以避免传统的锁机制带来的并发访问瓶颈。
在MVCC中,每一次数据更新都会产生一个新的版本,旧版本的数据并不会立即删除,而是保留一段时间直至不再需要。不同事务看到的数据版本可能是不同的,事务看到的数据版本取决于事务的开始时间。
在数据库中,MVCC的具体实现细节可能有所不同,但大致原理如下:
- 事务版本与可见性:每个事务都有一个事务ID或版本号,读操作只会看到在它开始之前已经提交的事务修改过的数据版本。
- 版本记录:数据库系统维护一个数据版本链表或类似的数据结构,当数据被修改时,新的版本会被创建,旧版本会被标记为不可修改但仍可读。
- 读取视图:事务在开始时创建一个读取视图,决定能看到哪些数据版本。例如,在可重复读(Repeatable Read)隔离级别下,事务在整个执行过程中看到的都是同一版本的数据。
- 垃圾回收:旧版本数据在确定不再被任何活跃事务需要时,会被垃圾回收机制清理掉。
MySQL的InnoDB存储引擎使用了MVCC来实现事务的并发控制,通过记录每个行版本的创建和删除时间戳(Undo Logs),以及事务ID(Read View)来确保并发事务间的隔离性和一致性。在InnoDB中,通过Next-Key Locks和Record Locks结合MVCC来避免幻读的发生。
18、Redis的持久化方式?
Redis 提供了两种主要的持久化方式:RDB(Redis Database)和 AOF(Append-only File)。
- RDB持久化:
- RDB 是 Redis 默认的持久化方式,它在指定的时间间隔内,通过生成数据集的快照(snapshot)来保存数据到磁盘。
- RDB 的持久化操作是通过 Fork 子进程来完成的,子进程创建数据集副本后,将副本写入磁盘,避免了主线程的阻塞。
- 用户可以通过配置
save
参数来指定触发快照的条件,例如在 N 秒内有 M 次数据修改时,自动触发一次快照保存。 - RDB 文件通常是二进制格式的
.rdb
文件,它紧凑且易于备份和恢复。
- AOF持久化:
- AOF 持久化则是通过记录服务器执行的所有写命令,在命令执行完后,将命令追加到 AOF 文件中,以此来记录数据的变更。
- AOF 持久化可以配置不同的同步策略,包括
always
(每次写命令都同步到磁盘)、everysec
(每秒同步一次,最多丢失一秒数据)和no
(不实时同步,操作系统控制何时同步)。 - AOF 文件随着时间的增长可能会变得很大,Redis 提供了 AOF 重写功能,可以定期压缩 AOF 文件,去掉无效命令,只保留重建当前数据集所需的最小命令集。
- AOF 持久化相比于 RDB 更加健壮,能够提供更好的数据完整性,但是写入性能和恢复速度理论上不如 RDB。
在实际应用中,Redis 可以同时开启 RDB 和 AOF 两种持久化方式,以达到最佳的数据安全性和性能平衡。在 Redis 重启时,如果同时配置了两种持久化方式,Redis 会优先使用 AOF 文件来恢复数据,因为 AOF 文件通常包含更完整、最新的数据。
19、RDB和AOF的区别?Redis宕机哪种恢复的比较快?
RDB(Redis Database)和AOF(Append-Only File)是Redis的两种持久化机制,它们的主要区别在于:
- RDB持久化:
- 优点:
- 文件体积较小,适合做冷备。
- 恢复时,由于是加载完整的数据集 Snapshot,速度快。
- 缺点:
- 如果Redis意外宕机,最后一次生成快照以来的数据可能会丢失,取决于最后生成RDB文件的时间点。
- 需要定时触发或满足一定条件才会生成RDB文件,因此数据安全性相对较低。
- AOF持久化:
- 优点:
- 数据安全性高,即使Redis宕机,仅丢失最近一次fsync之后的少量数据。
- 可以配置不同的同步策略,如每秒、每次写入后等时机进行同步,数据丢失风险低。
- 文件内容是操作日志,可通过回放操作来恢复数据,支持数据一致性检查和修复。
- 缺点:
- 文件体积随着写操作增多而逐渐增大(可通过AOF重写优化)。
- 因为AOF文件包含所有操作命令,所以在Redis重启时,解析并执行AOF文件来恢复数据通常比加载RDB文件慢。
关于Redis宕机后的恢复速度:
- 如果从恢复速度的角度考虑,RDB通常会更快,因为它直接加载整个数据集的Snapshot,不需要逐条执行命令。
- 而AOF则需要读取并执行所有累积的命令才能恢复到最后一个状态,这在大量写操作的情况下,尤其是AOF文件非常大的时候,恢复时间可能较长。
然而,考虑到数据安全性,很多用户会根据业务需求选择同时开启AOF和RDB,这样即使RDB恢复过程中出现问题,还可以依赖AOF来恢复尽可能多的数据。当然,具体的选择应当根据应用场景对于数据丢失容忍度、恢复速度以及存储空间等因素综合权衡。
20、乐观锁和悲观锁?
乐观锁和悲观锁是并发控制中两种常见的策略,它们分别针对不同的并发场景设计,用于解决多线程或多进程环境下对共享资源的访问冲突问题。以下是这两种锁的主要区别:
悲观锁(Pessimistic Locking)
- 原理:悲观锁假定在并发环境中,会发生频繁的数据冲突,因此在事务开始处理数据时,就立即对数据进行加锁,阻止其他事务对该数据进行访问和修改,直到当前事务完成并释放锁。
- 操作:当一个事务请求获得悲观锁时,它会一直持有该锁直到事务结束,期间其他事务请求相同的锁时会进入等待状态。
- 特点:悲观锁确保了在事务执行期间数据的一致性,但可能导致并发性能下降,因为多个事务可能因等待锁而形成阻塞队列。
- 适用场景:适用于写操作较多、并发冲突概率较高、数据完整性要求严格的场景。
乐观锁(Optimistic Locking)
- 原理:乐观锁假设大多数情况下不会有并发冲突,因此不主动加锁,而是每个事务在提交时检查数据在这段时间内是否被其他事务修改过。
- 操作:乐观锁通常通过版本号或时间戳等机制实现,事务在更新数据时会验证数据的版本是否与自己读取时一致,若一致则提交更新,否则拒绝更新(或者重试)。
- 特点:乐观锁减少了锁的使用,提高了系统的并发性能,尤其在读多写少的情况下效果显著。但是,如果并发修改的频率较高,可能出现大量的更新失败和重试。
- 实现方式:例如在数据库层面,可以使用行级版本控制(如SQL Server的
timestamp
类型或MySQL的innodb_version
),或者应用程序层面自行维护一个版本号字段。 - 适用场景:适用于读操作远多于写操作,且并发冲突较少的场景,或者是能够接受偶尔因冲突而回滚事务的场景。
总结来说,悲观锁采取预先锁定的方式来避免并发冲突,代价是可能会降低并发性能;而乐观锁则是尽量避免锁定,但在更新时增加了一步验证步骤,牺牲了一定的一致性保证来换取更高的并发性能。在实际应用中,需要根据项目需求和预期的并发模式来合理选择锁的策略。
21、库存的超卖问题的原因和解决方案?
库存超卖问题是指在高并发场景下,商品的库存数量小于等于0时,依然发生了售出商品的操作,导致实际售出的商品数量超过了库存总量,这是一种典型的并发控制问题。
原因:
- 并发访问:在高并发环境下,多个购买请求几乎同时到达服务器,各自查询库存时发现还有剩余,于是都完成了购买操作,但实际上这些操作并未串行执行,导致库存扣减逻辑重复执行,库存变为负数。
- 事务控制不当:如果没有正确地使用事务管理库存扣减操作,当多个事务同时读取同一批库存并试图减少时,可能出现数据不一致的情况。
- 数据库锁机制缺失或不足:在没有恰当使用乐观锁或悲观锁的情况下,无法有效阻止并发修改库存的情况。
解决方案:
- 使用数据库锁:
- 悲观锁:在查询库存前对库存记录加排它锁(如MySQL中的
SELECT ... FOR UPDATE
),确保在事务完成前其他事务无法修改库存。 - 乐观锁:在库存表中添加一个版本号字段或时间戳字段,每次更新库存前检查版本号是否改变,如果改变则认为数据已经被其他事务修改,本次更新失败。
- 分布式锁:
- 在分布式系统中,可以使用Redis或其他分布式锁服务,在减库存操作前先获取分布式锁,确保同一时刻只有一个请求可以修改库存。
- 队列处理:
- 将下单请求放入队列,通过消费队列的方式来依次处理订单,从而避免高并发下库存被多次扣减。
- 库存预减:
- 在用户点击购买按钮时,先减少库存(如使用Redis进行预扣减),然后再进行后续的下单流程,确保库存不会被超卖。
- 事务控制:
- 确保整个下单流程在一个数据库事务中执行,包括查询库存、扣减库存和插入订单等操作,确保原子性。
- 使用队列+本地内存缓存:
- 使用内存缓存(如Redis)存储库存,当用户下单时,先在缓存中扣减库存,然后异步处理订单入库和真实数据库库存扣减,如果库存不足则回滚操作。
通过上述一种或多种方式结合使用,可以有效解决库存超卖问题,确保库存的准确性。