电商网站开发面试,网站建设 图片上传,做行业网站投资多少,微信商城怎么找面经来源#xff1a;https://www.nowcoder.com/?type818_1 目录 1、 和equals()有什么区别#xff1f;2、String变量直接赋值和构造函数赋值比较相等吗#xff1f;3、String一些方法#xff1f;4、抽象类和接口有什么区别#xff1f;5、Java容器有哪些#xff1f;6、Lis… 面经来源https://www.nowcoder.com/?type818_1 目录 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控制反转什么是IOCSpring IOC容器依赖注入 Spring AOP面向切面编程什么是AOPSpring 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双端队列接口及其实现类如ArrayDequeLinkedList也实现了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 {Overridepublic void run() {// 线程任务代码}
}MyThread thread new MyThread();
thread.start();实现 Runnable 接口 创建一个新的类实现 java.lang.Runnable 接口。重写 Runnable 接口的 run() 方法。创建 Thread 类的一个实例将实现 Runnable 接口的对象作为构造函数的参数传入。调用 Thread 对象的 start() 方法来启动线程。 示例代码 class MyRunnable implements Runnable {Overridepublic 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 CallableString {Overridepublic String call() throws Exception {// 线程任务代码返回一个结果return Callable result;}
}MyCallable callable new MyCallable();
FutureTaskString 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 任务
FutureString 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框架的两大核心特性是IoCInversion of Control控制反转和AOPAspect-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;上述语句中由于对索引列进行了加1的操作索引可能无法使用。 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;此时即使indexed_column1和indexed_column2构成了复合索引MySQL也只能对indexed_column1使用索引。 未使用索引覆盖 如果查询返回的数据不仅包含索引列还包括非索引列即使查询条件使用了索引但如果需要回表查询其他列索引并不能达到最优的效果。 索引列使用了函数 若在索引列上使用了数据库函数如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以及事务IDRead View来确保并发事务间的隔离性和一致性。在InnoDB中通过Next-Key Locks和Record Locks结合MVCC来避免幻读的发生。
18、Redis的持久化方式
Redis 提供了两种主要的持久化方式RDBRedis Database和 AOFAppend-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宕机哪种恢复的比较快
RDBRedis Database和AOFAppend-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存储库存当用户下单时先在缓存中扣减库存然后异步处理订单入库和真实数据库库存扣减如果库存不足则回滚操作。
通过上述一种或多种方式结合使用可以有效解决库存超卖问题确保库存的准确性。