【今日面经】24/3/9 广州Java某小厂电话面经

面经来源:https://www.nowcoder.com/?type=818_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(控制反转)
        • 什么是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 中的主要区别如下:

  1. == 运算符:

    • 对于基本数据类型(如 int、char、boolean、float 等),== 比较的是它们存储的值是否相等。
    • 对于引用类型(如对象引用),== 比较的是两个对象引用是否指向内存中的同一块地址,也就是说,它检查的是对象的引用(内存地址)是否相同,而不是它们的内容是否相等。
  2. 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” 字符串在常量池中存在,所以在运行时常量池中 s1s2 实际上可能引用的是同一个对象,因此 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类方法:

  1. 获取字符串长度

    int length() // 返回字符串中的字符数。
    
  2. 访问特定位置的字符

    char charAt(int index) // 返回指定索引位置的字符。
    
  3. 字符串比较

    int compareTo(String anotherString) // 按照字典顺序比较两个字符串。
    boolean equals(Object anObject) // 检查此字符串与指定对象是否相等。
    boolean equalsIgnoreCase(String anotherString) // 忽略大小写检查字符串是否相等。
    int compareToIgnoreCase(String str) // 不区分大小写比较两个字符串。int hashCode() // 返回此字符串的哈希码,可用于集合中的快速查找。
    
  4. 查找子串或字符

    int indexOf(int ch) // 返回指定字符在此字符串中首次出现的位置。
    int indexOf(String str) // 返回指定子串在此字符串中首次出现的位置。
    int lastIndexOf(int ch) // 返回指定字符在此字符串中最后一次出现的位置。
    int lastIndexOf(String str) // 返回指定子串在此字符串中最后一次出现的位置。
    
  5. 子串操作

    String substring(int beginIndex) // 返回从指定索引开始到字符串结尾的子串。
    String substring(int beginIndex, int endIndex) // 返回从指定开始索引到指定结束索引之间的子串。
    
  6. 连接字符串

    String concat(String str) // 将指定字符串连接到此字符串的结尾。
    
  7. 字符串替换

    String replace(char oldChar, char newChar) // 替换所有指定字符为新字符。
    String replace(CharSequence target, CharSequence replacement) // 替换第一次出现的目标子串为新的子串。
    
  8. 字符串切割

    String[] split(String regex) // 根据匹配给定的正则表达式来拆分此字符串。
    
  9. 转换大小写

    String toLowerCase(Locale locale) // 使用给定的语言环境将字符串转换为小写。
    String toUpperCase(Locale locale) // 使用给定的语言环境将字符串转换为大写。
    
  10. 修剪空白

    String trim() // 移除字符串两端的空白字符。
    
  11. 字符串转换

    char[] toCharArray() // 将字符串转换为字符数组。
    byte[] getBytes(Charset charset) // 将此字符串编码为指定字符集的字节序列。
    
  12. 字符串是否以某个子串开头或结尾

    boolean startsWith(String prefix) // 检查此字符串是否以指定的前缀开始。
    boolean endsWith(String suffix) // 检查此字符串是否以指定的后缀结束。
    

这只是String类的部分方法,更多详细信息可以查阅Oracle官网的Java API文档。

4、抽象类和接口有什么区别?

抽象类(Abstract Class)和接口(Interface)在Java中都是为了实现抽象和多态而存在的,但它们之间有着明显的区别:

  1. 定义与抽象程度

    • 抽象类:可以包含抽象方法(没有实现的方法,由子类去实现)以及具体方法(已经实现了的方法)。抽象类可以有属性(变量)并且可以有构造方法。
    • 接口:只允许包含抽象方法(在Java 8及以后版本还可以包含默认方法和静态方法)和常量(默认为 public static final)。接口不允许定义构造方法和实例变量。
  2. 继承与实现

    • 抽象类:一个类只能继承一个抽象类(单一继承原则),通过关键字 extends 来继承。
    • 接口:一个类可以实现多个接口,通过关键字 implements 来实现。接口之间也可以通过 extends 关键字互相继承。
  3. 设计意图

    • 抽象类:主要用于定义一个类族的共同特征,体现的是“is-a”(是一个)的关系,侧重于描述类的内部属性和行为的一部分实现。
    • 接口:更侧重于定义一组行为规范,体现了“can-do”(能做什么)的关系,主要用于规定类所应遵循的某种契约或协议,关注的是外部行为而不涉及内部实现。
  4. 使用场景

    • 抽象类:适合于构建抽象层次结构,比如在组件的体系结构中,抽象类可以封装共享的功能和属性,子类可以进一步细化或增加额外的功能。
    • 接口:适用于实现多重继承、定义清晰的职责边界和约束条件,尤其在不同模块之间通信或对接时,通过接口实现解耦。
  5. 方法与变量修饰符

    • 抽象类中的方法可以是任意访问级别(public、protected、private),抽象方法默认为public,也可以显式声明为protected。
    • 接口中所有的方法默认都是public abstract的,Java 8之后可以有default方法和static方法;接口中的变量默认为public static final,即只能是常量。
  6. 静态方法和静态代码块

    • 抽象类可以包含静态方法和静态代码块。
    • 接口中在Java 8及以后版本才允许包含静态方法,但不能有静态代码块或实例变量。

总结来说,抽象类提供了一种继承层次上的抽象和实现部分,而接口则提供了一种纯粹的行为约定,更加灵活,有利于实现高度解耦的设计。

5、Java容器有哪些?

Java容器主要包括四大类别:

  1. List(列表)

    • ArrayList:基于动态数组实现,查询速度快,增删慢,非线程安全,可以通过 Collections.synchronizedList 包装实现线程安全。
    • LinkedList:基于双向链表实现,查询速度慢,增删快,同样是非线程安全的,也可通过同步包装实现线程安全。
    • Vector:类似于 ArrayList,但它是线程安全的,不过由于同步开销,性能通常不如 ArrayList。
  2. Set(集合)

    • HashSet:无序,不允许重复元素,基于哈希表实现,非线程安全。
    • LinkedHashSet:具有HashSet的特点,同时保留插入顺序。
    • TreeSet:有序,不允许重复元素,基于红黑树实现。
  3. Map(映射)

    • HashMap:键值对存储,无序,允许null键和值,基于哈希表实现,非线程安全。
    • LinkedHashMap:除了具有HashMap的特点外,还按插入顺序或最近最少使用(LRU)策略进行迭代。
    • TreeMap:键值对存储,有序,允许null键但不允许null值,基于红黑树实现。
    • Hashtable:类似于HashMap,线程安全,但性能较差,已被ConcurrentHashMap取代。
    • ConcurrentHashmap:线程安全的哈希映射,支持高效并发访问。
  4. 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集合框架中三种主要的容器类型,它们在数据存储和访问方式上有显著的不同:

  1. List(列表)

    • List接口表示有序的集合,其中的元素可以是重复的。
    • 元素在List中是按照插入的顺序存储的,可以通过索引访问,索引是从0开始的整数。
    • 常见的实现类有ArrayList、LinkedList和Vector等。
    • ArrayList基于动态数组实现,查询速度快,插入和删除中间元素较慢(需移动后续元素)。
    • LinkedList基于双向链表实现,插入和删除操作效率高,但随机访问性能相对较差。
  2. Set(集合)

    • Set接口表示一个不允许有重复元素的集合,元素无序。
    • Set不保证元素的插入顺序,主要关心的是唯一性,因此向Set中添加元素时,系统会自动检查是否存在重复。
    • 常见的实现类有HashSet、LinkedHashSet和TreeSet等。
    • HashSet基于哈希表实现,查找速度快,但不保证元素的顺序。
    • TreeSet基于红黑树实现,自动排序,元素按照自然排序或定制排序规则排列。
  3. Map(映射)

    • Map接口提供了一种存储键值对(key-value pair)的数据结构,键必须是唯一的,但值可以重复。
    • Map中的元素是成对出现的,每一对由一个键和对应的值组成,可以通过键来检索对应的值。
    • 常见的实现类有HashMap、LinkedHashMap、TreeMap、Hashtable以及ConcurrentHashMap等。
    • HashMap和Hashtable同样是基于哈希表实现的,但后者是线程安全的,而HashMap在多线程环境下如果不加控制可能会出现问题。
    • TreeMap基于红黑树实现,键值对是有序的。

总之,List适用于存储有序且可能重复的数据,Set适用于存储不重复的数据集,而Map则用于存储键值对关联数据。在选择使用哪种集合时,应考虑数据的特性和应用程序的需求。

7、线程创建的方式?

Java中创建线程主要有以下几种方式:

  1. 继承 Thread

    • 创建一个新的类,让它继承自 java.lang.Thread 类。
    • 重写 Thread 类的 run() 方法,该方法包含了线程需要执行的任务代码。
    • 创建 Thread 子类的实例,并调用 start() 方法来启动线程,而不是直接调用 run() 方法。

    示例代码:

    class MyThread extends Thread {@Overridepublic void run() {// 线程任务代码}
    }MyThread thread = new MyThread();
    thread.start();
    
  2. 实现 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();
    
  3. 使用 CallableFuture

    • 创建一个实现 java.util.concurrent.Callable 接口的类,并重写 call() 方法,call() 方法能够返回一个值,并且可以抛出异常。
    • 使用 FutureTask 类来包装 Callable 对象,FutureTaskRunnable 的实现,同时也持有 Callable 的结果。
    • 创建 Thread 并传入 FutureTask 实例,启动线程后,可通过 FutureTaskget() 方法获取线程执行结果。

    示例代码:

    class MyCallable implements Callable<String> {@Overridepublic 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();
    
  4. 通过 ExecutorService 和线程池

    • 使用 java.util.concurrent.ExecutorService(如 ThreadPoolExecutor)来管理和控制线程。
    • 提交 RunnableCallable 任务到线程池中,线程池会自动调度线程执行这些任务。

    示例代码(使用 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有什么区别?

RunnableCallable 都是用来定义任务以供线程执行的接口,但在Java并发编程中,它们有以下显著区别:

  1. 返回值

    • Runnable: 实现 Runnable 接口的任务没有返回值。它的 run() 方法没有返回类型,这意味着一旦线程执行完毕,无法通过 Runnable 直接获得执行结果。
    • Callable: 实现 Callable 接口的任务可以有返回值。Callablecall() 方法有一个泛型返回类型,执行完成后,可以返回一个结果给调用者。
  2. 异常处理

    • Runnable: run() 方法不能抛出受检异常(checked exception),如果需要抛出异常,必须在 run() 方法内部捕获并处理。
    • Callable: call() 方法允许抛出受检异常,调用者可以通过 Future 对象的 get() 方法捕获并处理异常。
  3. 使用方式

    • Runnable: 通常用于创建线程时指定任务,直接使用 Thread 类或通过 ExecutorService 执行。
    • Callable: 通常与 FutureFutureTask 结合使用,通过 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框架在设计和实现过程中广泛运用了多种设计模式,下面列出了一些关键的设计模式:

  1. 工厂模式

    • Spring通过BeanFactoryApplicationContext接口实现了工厂模式,这两个接口是Spring容器的基础,负责创建和管理对象(Bean)的生命周期。
  2. 单例模式

    • Spring容器默认将所有bean定义为单例,确保系统中只有一个共享的bean实例。通过控制bean的生命周期,Spring确保在系统中任何地方请求该bean时,总是返回相同的实例。
  3. 代理模式

    • Spring AOP(面向切面编程)大量使用了代理模式。Spring通过JDK动态代理或CGLIB代理为Bean创建代理对象,代理对象在方法调用前后执行通知(Advice)代码,实现诸如事务管理、日志记录等功能的横切关注点。
  4. 模板方法模式

    • Spring的JdbcTemplateJmsTemplate等类使用了模板方法模式,它们定义了执行数据库操作或消息发送的基本骨架,子类只需要关注具体的业务逻辑。
  5. 策略模式

    • 在Spring框架的多个组件中,如事务管理中定义的各种事务策略,都采用了策略模式,允许客户端根据不同的情况选择不同的策略(Strategy)。
  6. 适配器模式

    • Spring AOP中,Advice(通知)的实现就是一个适配器模式的应用,它允许将用户定义的业务逻辑(adaptee)包装成能够在join point处执行的切面。
  7. 装饰器模式

    • Spring在处理一些组件的时候,如处理web请求时,通过一系列过滤器和拦截器链,这些过滤器和拦截器可以看作是对原始请求处理器的装饰。
  8. 责任链模式

    • Spring框架中的过滤器链(Filter Chain)和拦截器链(Interceptor Chain)都体现了责任链模式,每个链上的过滤器或拦截器都可以决定是否继续执行下一个链上的处理单元。
  9. 依赖注入(DI)

    • 虽然不是严格意义上的设计模式,但依赖注入的思想在Spring框架中得到了广泛应用,它有助于实现松耦合,是控制反转(IoC)的一种具体实现方式。

Spring框架整合了许多优秀的设计模式,通过合理组合和使用这些模式,构建了一个强大且灵活的轻量级企业级应用框架。

12、Mybatis#()和$()有什么区别?

在MyBatis中,#{}${} 分别用于在SQL语句中处理动态参数,它们在处理参数时有不同的行为和目的:

  1. #{}

    • 用途:用于预编译参数占位符,它可以防止SQL注入,因为它会将参数值当做字符串进行处理,并在SQL执行前对参数进行预编译和类型检查。
    • 示例SELECT * FROM users WHERE id = #{userId}
    • 处理方式:MyBatis会将参数用PreparedStatement的参数占位符(如?)替换,并在执行SQL时通过jdbc驱动提供的API进行参数设置,确保了安全性。
  2. ${}

    • 用途:用于字符串替换,即将变量的值直接拼接到SQL语句中,不做任何特殊处理。
    • 示例SELECT * FROM ${tableName} WHERE column = 'value'
    • 处理方式:MyBatis会直接将变量值插入到SQL语句中,原样输出到最终执行的SQL中。
    • 风险:由于未做预编译处理,如果变量内容未经严格控制,容易导致SQL注入攻击。因此,除非必要(如动态表名或列名),一般情况下应尽量避免使用${}

总结来说,#{} 更安全,适用于大多数情况下的参数传递,而 ${} 仅在确实需要动态构造SQL结构时谨慎使用,并确保传入的值安全可靠。

13、Mysql的四个隔离级别以及默认隔离级别?

MySQL的四个事务隔离级别是基于SQL标准定义的,它们决定了事务之间如何相互影响彼此的数据读取和修改,旨在解决事务并发执行时可能出现的问题,如脏读(Dirty Reads)、不可重复读(Non-Repeatable Reads)、幻读(Phantom Reads)。

  1. 读未提交(Read Uncommitted)

    • 在这种级别下,一个事务可以读取到其他事务未提交的数据变更,可能导致脏读。
    • 这是最低的隔离级别,一般不推荐在生产环境中使用。
  2. 读已提交(Read Committed)

    • 在这个级别,一个事务只能读取到其他事务已经提交的数据,解决了脏读问题,但仍然可能存在不可重复读和幻读的情况。
    • 在这个级别下,每次查询都会获取最新的提交数据,所以两次相同的查询可能会得到不同的结果。
  3. 可重复读(Repeatable Read)

    • MySQL的默认事务隔离级别。
    • 在同一个事务内,多次执行相同的查询语句会得到相同的结果,即在同一事务内,其他事务对该事务可见的数据不会发生变化,解决了脏读和不可重复读的问题。
    • 不过,在可重复读隔离级别下,如果其他事务插入了新的记录(幻读),即使这些记录符合当前事务的查询条件,在本事务中也无法看到这些新插入的数据。
  4. 串行化(Serializable)

    • 这是最高的隔离级别,通过完全锁定事务涉及的所有数据行来防止任何并发问题,包括脏读、不可重复读和幻读。
    • 在这个级别下,事务执行效果如同按照顺序逐个执行,因此并发性能最低,但数据一致性最强。

需要注意的是,MySQL的InnoDB存储引擎在实现可重复读隔离级别时,通过多版本并发控制(MVCC)技术有效地避免了幻读问题,但是在其他数据库系统中,即使在可重复读隔离级别也可能存在幻读现象。

14、A事务未提交,B事务上查询到的是旧值还是新值?

这个问题的答案取决于事务B所处的隔离级别:

  1. 读未提交(Read Uncommitted)
    在这个级别,事务B可以看到事务A尚未提交的更改,即查询到的是A事务的新值,但也有可能是不一致的、随后可能被回滚的数据,即脏读。

  2. 读已提交(Read Committed)
    在这个级别,事务B只能看到事务A已经提交的更改。如果事务A尚未提交,事务B查询时将看不到事务A对数据所做的更改,只能看到旧值。

  3. 可重复读(Repeatable Read)
    这是MySQL的默认事务隔离级别。在该级别下,事务B在整个事务期间看到的数据是一致的,即事务开始时的旧值。即使事务A在事务B执行过程中提交了更新,事务B仍不会看到这些新提交的值,因此在这个隔离级别下,事务B看不到事务A未提交的新值。

  4. 串行化(Serializable)
    在这个级别下,为了防止幻读,数据库系统通常会对事务进行更为严格的锁定,事务B要么等待事务A完成并提交,要么由于锁定冲突而中止。在这种情况下,事务B同样在事务A提交之前看不到事务A对数据的更改,查询到的是旧值。

综上所述,除非是在读未提交隔离级别,否则在其他三个隔离级别下,事务B在事务A未提交的情况下,查询到的将是旧值,而不是事务A尚未提交的新值。

15、编写sql语句哪些情况导致索引失效?

编写SQL语句时,以下情况可能导致索引失效或不被使用:

  1. 索引列使用了计算操作

    • 如果在查询条件中对索引列进行了数学运算、函数运算或类型转换,可能导致索引失效。例如:
      SELECT * FROM table WHERE indexed_column + 1 = 10;
      
      上述语句中,由于对索引列进行了加1的操作,索引可能无法使用。
  2. LIKE查询以通配符开头

    • LIKE查询时,如果通配符 % 位于搜索词的开头,索引通常无法使用,例如:
      SELECT * FROM table WHERE indexed_column LIKE '%value';
      
      若要有效利用索引,搜索词应从非通配符开始,例如:
      SELECT * FROM table WHERE indexed_column LIKE 'value%';
      
  3. OR条件查询

    • 当SQL语句中含有多个条件通过OR连接,且这些条件中只有部分条件涉及索引时,可能导致索引失效。MySQL优化器可能会选择全表扫描而不是使用索引。
      SELECT * FROM table WHERE indexed_column = 'value1' OR non_indexed_column = 'value2';
      
  4. 隐式类型转换

    • 查询条件中索引列与其他类型的数据进行比较时,如果发生隐式类型转换,可能导致索引失效。例如:
      SELECT * FROM table WHERE indexed_string_column = 123; -- 字符串索引与数字比较
      
  5. 范围查询后紧跟不相关的条件

    • 当查询中包含范围查询(如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使用索引。
  6. 未使用索引覆盖

    • 如果查询返回的数据不仅包含索引列,还包括非索引列,即使查询条件使用了索引,但如果需要回表查询其他列,索引并不能达到最优的效果。
  7. 索引列使用了函数

    • 若在索引列上使用了数据库函数,如TRIMDATE_FORMAT等,索引通常不会被使用。
      SELECT * FROM table WHERE TRIM(indexed_column) = 'value';
      
  8. 联合索引未遵循最左前缀匹配原则

    • 对于复合索引(如index(column1, column2, column3)),如果查询条件不从索引的最左列开始,后面的列索引可能无法使用。
  9. NOT操作符

    • 使用NOT操作符否定索引列的条件可能导致索引失效,例如:
      SELECT * FROM table WHERE NOT indexed_column = 'value';
      
      相比之下,改写为 indexed_column <> 'value' 可能更利于索引的使用。

总之,为了确保索引能够得到有效利用,应尽量避免上述情况,并确保查询条件能够精确匹配索引列或者符合索引的使用原则。同时,根据实际情况和数据库优化器的特性调整SQL查询结构也很重要。

16、Redisson的底层原理?以及与SETNX的区别?

Redisson 是一个高级 Redis 客户端,提供了 Java 的数据结构和分布式服务功能,如分布式锁、信号量、闭锁、队列等多种分布式数据结构。Redisson 的底层原理主要是基于 Redis 的原生命令和 Lua 脚本来实现高效的分布式操作。

Redisson 的实现原理概要:

  1. 数据结构封装:Redisson 封装了 Redis 的常用数据结构如 String、List、Set、SortedSet、Map 等,为 Java 开发者提供了便捷的操作接口。

  2. 分布式锁

    • 基于 SETNX 和 DEL 命令:早期版本的 Redisson 实现分布式锁时,可能使用 SETNX 命令尝试获取锁,当 key 不存在时设置成功并返回 true,代表获取锁成功。
    • 基于 Lua 脚本:为了避免因网络中断等问题导致的锁丢失或死锁问题,Redisson 采用 Lua 脚本实现了一套更完善的分布式锁机制。在获取锁时,脚本会一次性完成检查锁存在与否、设置超时时间、设置锁标志等多个动作,确保操作的原子性。
  3. 自动续期:Redisson 的锁支持自动续期,即在持有锁的过程中定期延长锁的有效期,防止在长时间执行任务时因锁超时而意外释放。

  4. 监听器:Redisson 支持事件监听器,可以注册监听 Redis 中 key 的各种事件,方便开发者在数据变化时采取相应操作。

  5. 线程安全: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的具体实现细节可能有所不同,但大致原理如下:

  1. 事务版本与可见性:每个事务都有一个事务ID或版本号,读操作只会看到在它开始之前已经提交的事务修改过的数据版本。

  2. 版本记录:数据库系统维护一个数据版本链表或类似的数据结构,当数据被修改时,新的版本会被创建,旧版本会被标记为不可修改但仍可读。

  3. 读取视图:事务在开始时创建一个读取视图,决定能看到哪些数据版本。例如,在可重复读(Repeatable Read)隔离级别下,事务在整个执行过程中看到的都是同一版本的数据。

  4. 垃圾回收:旧版本数据在确定不再被任何活跃事务需要时,会被垃圾回收机制清理掉。

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)。

  1. RDB持久化

    • RDB 是 Redis 默认的持久化方式,它在指定的时间间隔内,通过生成数据集的快照(snapshot)来保存数据到磁盘。
    • RDB 的持久化操作是通过 Fork 子进程来完成的,子进程创建数据集副本后,将副本写入磁盘,避免了主线程的阻塞。
    • 用户可以通过配置 save 参数来指定触发快照的条件,例如在 N 秒内有 M 次数据修改时,自动触发一次快照保存。
    • RDB 文件通常是二进制格式的 .rdb 文件,它紧凑且易于备份和恢复。
  2. 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时,依然发生了售出商品的操作,导致实际售出的商品数量超过了库存总量,这是一种典型的并发控制问题。

原因

  1. 并发访问:在高并发环境下,多个购买请求几乎同时到达服务器,各自查询库存时发现还有剩余,于是都完成了购买操作,但实际上这些操作并未串行执行,导致库存扣减逻辑重复执行,库存变为负数。
  2. 事务控制不当:如果没有正确地使用事务管理库存扣减操作,当多个事务同时读取同一批库存并试图减少时,可能出现数据不一致的情况。
  3. 数据库锁机制缺失或不足:在没有恰当使用乐观锁或悲观锁的情况下,无法有效阻止并发修改库存的情况。

解决方案

  1. 使用数据库锁

    • 悲观锁:在查询库存前对库存记录加排它锁(如MySQL中的SELECT ... FOR UPDATE),确保在事务完成前其他事务无法修改库存。
    • 乐观锁:在库存表中添加一个版本号字段或时间戳字段,每次更新库存前检查版本号是否改变,如果改变则认为数据已经被其他事务修改,本次更新失败。
  2. 分布式锁

    • 在分布式系统中,可以使用Redis或其他分布式锁服务,在减库存操作前先获取分布式锁,确保同一时刻只有一个请求可以修改库存。
  3. 队列处理

    • 将下单请求放入队列,通过消费队列的方式来依次处理订单,从而避免高并发下库存被多次扣减。
  4. 库存预减

    • 在用户点击购买按钮时,先减少库存(如使用Redis进行预扣减),然后再进行后续的下单流程,确保库存不会被超卖。
  5. 事务控制

    • 确保整个下单流程在一个数据库事务中执行,包括查询库存、扣减库存和插入订单等操作,确保原子性。
  6. 使用队列+本地内存缓存

    • 使用内存缓存(如Redis)存储库存,当用户下单时,先在缓存中扣减库存,然后异步处理订单入库和真实数据库库存扣减,如果库存不足则回滚操作。

通过上述一种或多种方式结合使用,可以有效解决库存超卖问题,确保库存的准确性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/733717.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring MVC | Spring MVC 的“核心类” 和 “注解”

目录: Spring MVC 的“核心类” 和 “注解” &#xff1a;1.DispatcherServlet (前端控制器)2.Controller 注解3.RequestMapping 注解3.1 RequestMapping 注解的 “使用”标注在 “方法” 上标注在 “类” 上 3.2 RequestMapping 注解的 “属性” 4.组合注解4.1 请求处理方法的…

哪些狗粮比较适合幼年犬?

亲爱的朋友&#x1f44b;&#xff0c;你为家中的幼年犬挑选狗粮可真是个贴心的主人呢&#xff01;选择适合幼年犬的狗粮&#xff0c;确实是个需要仔细考虑的问题。幼年犬处于生长发育的关键期&#xff0c;所以狗粮的营养成分和口感都非常重要。 &#x1f436; 在选择狗粮时&…

排序算法全景:从基础到高级的Java实现

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

CentOS系统上安装Redis操作教程

&#x1f31f; 前言 欢迎来到我的技术小宇宙&#xff01;&#x1f30c; 这里不仅是我记录技术点滴的后花园&#xff0c;也是我分享学习心得和项目经验的乐园。&#x1f4da; 无论你是技术小白还是资深大牛&#xff0c;这里总有一些内容能触动你的好奇心。&#x1f50d; &#x…

【Python+Selenium学习系列4】Selenium常用的方法

目录 一、前言 二、基本方法 1、send_keys方法---模拟键盘输入 1.1 源代码 1.2 运行结果 2、text方法---用于获取文本值 2.1 源代码 2.2 运行结果 3、get_attribute()方法---用于获取属性值 ​3.1 源代码 3.2 运行结果 ​4、maximize_window()方法---实现浏览器窗口…

Pulsar 社区周报 | No.2024.03.08 Pulsar-Spark Connector 助力实时计算

关于 Apache Pulsar Apache Pulsar 是 Apache 软件基金会顶级项目&#xff0c;是下一代云原生分布式消息流平台&#xff0c;集消息、存储、轻量化函数式计算为一体&#xff0c;采用计算与存储分离架构设计&#xff0c;支持多租户、持久化存储、多机房跨区域数据复制&#xff0c…

每日一练:LeeCode-209、长度最小的子数组【滑动窗口+双指针】

每日一练&#xff1a;LeeCode-209、长度最小的子数组【滑动窗口双指针】 思路暴⼒解法滑动窗口 本文是力扣 每日一练&#xff1a;LeeCode-209、长度最小的子数组【滑动窗口双指针】 学习与理解过程&#xff0c;本文仅做学习之用&#xff0c;对本题感兴趣的小伙伴可以出门左拐 L…

开发指南002-前后端信息交互规范-返回类ResponseResult

返回类有两个&#xff0c;一般返回类ResponseResult和分页返回类PageResult&#xff0c;本篇介绍ResponseResult public class ResponseResult<T> implements Serializable{Schema(description "平台-返回结构类型 表明是千里马架构返回体")private String f…

基于php的用户登录实现(v2版)(持续迭代)

目录 版本说明 数据库连接 登录页面&#xff1a;login.html 登录处理实现&#xff1a;login.php 用户欢迎页面&#xff1a;welcome.php 密码修改页面&#xff1a;change_password.html 修改执行&#xff1a;change_password.php 用户注册页面&#xff1a;register.html …

远程连接Linux系统

图形化、命令行 对于操作系统的使用&#xff0c;有2种使用形式&#xff1a; 图形化页面使用操作系统 图形化&#xff1a;使用操作系统提供的图形化页面&#xff0c;以获得图形化反馈的形式去使用操作系统。 以命令的形式使用操作系统 命令行&#xff1a;使用操作系统提供的各…

React-路由导航

1.声明式路由导航 1.1概念 说明&#xff1a;声明式导航是指通过在模版中通过<Link/>组件描述出要跳转到哪里去&#xff0c;比如后台管理系统的左侧菜单通常使用这种方式进行。 import {Link} from "react-router-dom" const Login()>{return (<div>…

Mac(含M1) 使用 brew 安装nvm

目录 Mac 安装nvm 下载命令 配置环境变量 刷新 Mac(M1) 安装nvm 搜索 下载 为nvm创建文件夹 配置环境变量 刷新 Mac 安装nvm 下载命令 brew install nvm 配置环境变量 vi ~/.zshrc 内容如下&#xff1a; export NVM_DIR"$HOME/.nvm"[ -s "/usr/local…

CentOS 7升级openssh9.6p1

一、环境情况 [rootlocalhost ~]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core) [rootlocalhost ~]# ssh -V OpenSSH_7.4p1, OpenSSL 1.0.2k-fips 26 Jan 2017二、准备 1. 开启telnet 主要是在安装过程中&#xff0c;需要卸载老版本openssh&#xff0c;临…

物联网云原生云边协同

文章目录 一、物联网平台设计1.物联网平台设计2.物联网平台实现 二、部署环境1.节点配置2.版本信息 三、物联网平台部署1.部署 Kubernetes 集群2.部署 KubeEdge3.部署 ThingsBoard 集群4.部署 ThingsBoard Edge4.1.创建 Edge 实例4.2.部署 PostgreSQL4.3.创建数据库4.4.部署 Th…

在Blender中清理由Instant-NGP等几何学习技术生成的网格

使用布尔运算: 创建一个大的立方体或其他简单几何体包裹住全部网格。使用布尔修改器对两个网格进行“差集”运算。这将移除超出包裹体之外的多余网格部分。 手动选择并删除: 进入编辑模式&#xff08;按Tab键&#xff09;。按A键取消选择所有顶点。按B键并拖动以选择您想要删除…

11. 搭建较通用的GoWeb开发脚手架

文章目录 导言一、加载配置二、初始化日志三、初始化MySQL连接四、初始化Redis连接五、初始化gin框架内置的校验器使用的翻译器六、注册路由七、 启动服务八、测试运行九&#xff1a;注意事项 代码地址&#xff1a;https://gitee.com/lymgoforIT/bluebell 导言 有了前述知识的…

最简单的基于 FFmpeg 的内存读写的例子:内存转码器

最简单的基于 FFmpeg 的内存读写的例子&#xff1a;内存转码器 最简单的基于 FFmpeg 的内存读写的例子&#xff1a;内存转码器正文源程序结果工程文件下载参考链接 最简单的基于 FFmpeg 的内存读写的例子&#xff1a;内存转码器 参考雷霄骅博士的文章&#xff0c;链接&#xf…

Chrome中如何导出和导入书签

导出书签 如下图所示&#xff1a; 右上角三点->书签和清单->书签管理器->右上角三点->导出书签 然后你选择保存地址即可。打开后如下&#xff1a; 导入书签 如下图所示&#xff1a; 右上角三点->书签和清单->导入书签和设置->选择以前导出的书签&…

【Node.js】-闲聊:前端框架发展史

前端框架的发展史是一个不断演进和创新的过程&#xff0c;旨在提高开发效率、优化用户体验&#xff0c;并推动前端技术的不断发展。以下是前端框架发展的主要阶段和关键里程碑&#xff1a; 早期阶段&#xff1a; 在这个阶段&#xff0c;前端主要由HTML、CSS和JavaScript等基础技…

ceph 换盘扩容

调整时间 基础设施调整操作&#xff1a;工作日0点之后操作&#xff0c;或者非工作日 基础设施包括网络、主机系统、存储 / 备份系统、安全系统、以及机房动力环境等 调整规范 变更管理实现所有基础设施和应用系统的变更&#xff0c;变更管理应记录并对所有要求的变更进行分…