余声-个人博客


  • 首页

  • 分类

  • 归档

  • 标签

👌Spring事务失效场景?

发表于 2025-03-02 | 更新于 2025-09-14 | 分类于 Spring
字数统计 | 阅读时长

👌Spring事务失效场景?

题目详细答案

Spring事务失效的场景主要有以下几种。

非public方法使用@Transactional

场景描述:Spring事务管理是基于AOP实现的,而AOP对于JDK动态代理或CGLib动态代理只会代理public方法。如果事务方法的访问修饰符为非public,SpringAOP无法正确地代理该方法,从而导致事务失效。

示例代码:事务方法的访问修饰符被设置为private、default或protected。

解决方案:将需要事务管理的方法设置为public。

在同类中的非事务方法调用事务方法

场景描述:Spring的事务管理是通过动态代理实现的,只有通过代理对象调用的方法才能享受到Spring的事务管理。如果在同一个类中,一个没有标记为@Transactional的方法内部调用了一个标记为@Transactional的方法,那么事务是不会起作用的。

解决方案:尽量将事务方法放在不同的类中,或者使用Spring的AopContext.currentProxy()来获取当前类的代理对象,然后通过代理对象调用事务方法。

事务属性设置不当

场景描述:在Spring的事务管理中,如果在一个支持当前事务的方法(比如,已经被标记为@Transactional的方法)中调用了一个需要新事务的方法,如果后者方法抛出了异常,但异常并未被Spring识别为需要回滚事务的异常,那么后者的事务将不会回滚。

异常类型不匹配

场景描述:默认情况下,Spring只有在方法抛出运行时异常或者错误时才会回滚事务。对于检查性异常,即使你在方法中抛出了,Spring也不会回滚事务,除非你在@Transactional注解中显式地指定需要回滚哪些检查性异常。

解决方案:了解Spring事务管理对异常的处理,必要时在@Transactional注解中指定需要回滚的异常类型。

事务拦截器配置错误

场景描述:如果没有正确地配置事务拦截器,例如没有指定切入点或指定了错误的切入点,就会导致Spring事务失效。

事务超时配置错误

场景描述:如果事务超时时间设置得太短,就有可能在事务执行过程中出现超时,从而导致Spring事务失效。

👌Spring使用注解的进行装配的时候,需要什么注解

发表于 2025-03-02 | 更新于 2025-09-14 | 分类于 Spring
字数统计 | 阅读时长

👌Spring使用注解的进行装配的时候,需要什么注解

口语化答案

好的,面试官,其实这个问题的目的就是将 bean 让 spring 扫描到,并识别出要放到容器中。那就涉及到第一个概念,ComponentScan,自动扫描指定的包,配合basePackages 属性。指定包之后能,spring 就会将@Service、@Repository和@Controller 这些带了注解的类,作为 bean 放入到容器中。ComponentScan 也提供了一些比如 excludefilter 想排除某些包,或者加特定包的方式。以上。

题目解析

基础题必看,主要稍微会发散的一个点就是 lazyinit,其他的没有什么东西。正常背一下属性就可以了。

面试得分点

basepackage,includefilter,扫描注解

题目详细答案

@ComponentScan是用于自动扫描指定的包及其子包中的组件类,并将它们注册为 Spring 容器管理的 Bean。这个过程被称为组件扫描(Component Scanning)。

@ComponentScan注解

@ComponentScan注解通常与@Configuration注解一起使用,用于定义 Spring 应用的配置类。在这个配置类中,@ComponentScan指定了要扫描的包路径,Spring 会自动扫描这些包及其子包中的所有类,并将带有特定注解的类(如@Component、@Service、@Repository、@Controller等)注册为 Bean。

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
// 配置类内容
}

Spring 会扫描com.example包及其子包中的所有类,并将带有@Component、@Service、@Repository和@Controller注解的类注册为 Spring Bean。

详细说明

basePackages属性

basePackages属性用于指定要扫描的包。可以指定一个或多个包路径。

1
@ComponentScan(basePackages = {"com.example", "com.another"})

basePackageClasses属性

basePackageClasses属性用于指定一个或多个类,Spring 会扫描这些类所在的包。

1
@ComponentScan(basePackageClasses = {MyClass1.class, MyClass2.class})

includeFilters和excludeFilters属性

includeFilters和excludeFilters属性用于指定包含或排除的过滤器。可以根据注解、类型、正则表达式等进行过滤。

1
2
3
4
5
@ComponentScan(
basePackages = "com.example",
includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = CustomAnnotation.class),
excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com\\.example\\.exclude\\..*")
)

lazyInit属性

lazyInit属性用于指定是否延迟初始化扫描到的 Bean。默认值为false,表示立即初始化。

1
@ComponentScan(basePackages = "com.example", lazyInit = true)

示例

假设我们有以下包结构:

1
2
3
4
5
6
7
com.example
├── service
│ └── MyService.java
├── repository
│ └── MyRepository.java
└── controller
└── MyController.java

其中,MyService.java、MyRepository.java和MyController.java分别带有@Service、@Repository和@Controller注解。

定义配置类

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
// 配置类内容
}

启动 Spring 容器

1
2
3
4
5
6
7
8
9
10
11
12
public class Application {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

MyService myService = context.getBean(MyService.class);
MyRepository myRepository = context.getBean(MyRepository.class);
MyController myController = context.getBean(MyController.class);

// 使用 Bean
context.close();
}
}

Spring 会扫描com.example包及其子包中的所有类,并将带有@Service、@Repository和@Controller注解的类注册为 Spring Bean。

👌Spring 的依赖注入是什么?

发表于 2025-03-02 | 更新于 2025-09-14 | 分类于 Spring
字数统计 | 阅读时长

👌Spring 的依赖注入是什么?

口语化答案

好的,面试官,依赖注入(Dependency Injection,简称DI)是Spring框架实现控制反转(IoC)的主要手段。DI的核心思想是将对象的依赖关系从对象内部抽离出来,通过外部注入的方式提供给对象。这样,依赖对象的创建和管理由Spring容器负责,而不是由对象自身负责,使得代码更加模块化、松耦合和易于测试。以上。

题目解析

重点题,三大概念之一。di,ioc,aop 之一,大家一定要整明白。

面试得分点

构造函数,setter,注解

详细答案

在传统编程中,一个对象通常会自己创建它所依赖的其他对象。这种方式使得代码紧密耦合,不利于维护和测试。依赖注入通过将依赖关系从代码中移除,转而由外部容器(如Spring容器)来注入,从而实现了对象之间的松耦合。

依赖注入的类型

Spring框架主要提供了三种依赖注入的方式:

构造函数注入:通过构造函数将依赖对象传递给被依赖对象。

1
2
3
4
5
6
7
public class Service {
private final Repository repository;

public Service(Repository repository) {
this.repository = repository;
}
}

Setter方法注入:通过Setter方法将依赖对象注入到被依赖对象中。

1
2
3
4
5
6
7
public class Service {
private Repository repository;

public void setRepository(Repository repository) {
this.repository = repository;
}
}

字段注入:直接在字段上使用注解进行注入。

1
2
3
4
public class Service {
@Autowired
private Repository repository;
}

依赖注入的配置方式

XML配置:通过XML文件定义Bean及其依赖关系。

1
2
3
4
5
6
<beans>
<bean id="repository" class="com.example.Repository"/>
<bean id="service" class="com.example.Service">
<constructor-arg ref="repository"/>
</bean>
</beans>

Java配置:通过Java类和注解定义Bean及其依赖关系。

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class AppConfig {
@Bean
public Repository repository() {
return new Repository();
}

@Bean
public Service service() {
return new Service(repository());
}
}

注解配置:通过注解(如@Component,@Autowired)自动扫描和注入Bean。

1
2
3
4
5
6
7
8
9
@Component
public class Repository {
}

@Component
public class Service {
@Autowired
private Repository repository;
}

Spring 的属性注入方式有哪几种

发表于 2025-03-02 | 更新于 2025-09-14 | 分类于 Spring
字数统计 | 阅读时长

👌Spring 的属性注入方式有哪几种?

口语化答案

好的,面试官,属性注入的方式主要有几种,比如构造器注入,setter 注入,字段注入,value 注入等等。最常见的我们平时用的最多的就是,就是@Resource ,@Autowired 这种,像构造器和 setter 这种,经常在第三方依赖包的一些情况下,会进行使用,比如对接一些阿里云 oss,就体现的很明显。像@Value. 这种就常常用于读取配置文件的属性值,来使用。以上。

题目解析

常考题。也是年限低一点的小伙伴会问到,考察一下你对不同注入的情况,有没有一定的选择,以及了解当想注入一个属性的时候,都可以有什么方式的思路。

面试得分点

构造器,setter,@value。@resource @autowired。

题目详细答案

属性注入(Dependency Injection, DI)是将依赖对象注入到目标对象中的过程。Spring 支持多种属性注入方式,主要包括构造器注入、Setter 方法注入和字段注入。

构造器注入

构造器注入通过类的构造器来注入依赖对象。这种方式在对象创建时就完成了依赖注入,确保了依赖对象的不可变性。

1
2
3
4
5
6
7
public class MyBean {
private final Dependency dependency;

public MyBean(Dependency dependency) {
this.dependency = dependency;
}
}

XML 配置

1
2
3
4
<bean id="myBean" class="com.example.MyBean">
<constructor-arg ref="dependency"/>
</bean>
<bean id="dependency" class="com.example.Dependency"/>

注解配置

1
2
3
4
5
6
7
8
9
@Component
public class MyBean {
private final Dependency dependency;

@Autowired
public MyBean(Dependency dependency) {
this.dependency = dependency;
}
}

Setter 方法注入

Setter 方法注入通过类的 Setter 方法来注入依赖对象。这种方式允许在对象创建后进行依赖注入,并且可以在运行时更改依赖对象。

1
2
3
4
5
6
7
public class MyBean {
private Dependency dependency;

public void setDependency(Dependency dependency) {
this.dependency = dependency;
}
}

XML 配置

1
2
3
4
<bean id="myBean" class="com.example.MyBean">
<property name="dependency" ref="dependency"/>
</bean>
<bean id="dependency" class="com.example.Dependency"/>

注解配置

1
2
3
4
5
6
7
8
9
@Component
public class MyBean {
private Dependency dependency;

@Autowired
public void setDependency(Dependency dependency) {
this.dependency = dependency;
}
}

字段注入

字段注入(也称为属性注入)直接通过在类的字段上使用注解来注入依赖对象。这种方式不需要 Setter 方法,但不推荐使用,因为它违反了面向对象设计原则,使得依赖关系不够明确。

1
2
3
4
5
@Component
public class MyBean {
@Autowired
private Dependency dependency;
}

通过@Value注解注入基本类型和字符串

除了注入 Bean 对象,Spring 还支持通过@Value注解注入基本类型、字符串和其他值,如从配置文件中读取的值。

1
2
3
4
5
@Component
public class MyBean {
@Value("${my.property}")
private String myProperty;
}

通过环境变量或系统属性注入

1
2
3
4
5
@Component
public class MyBean {
@Value("#{systemProperties['user.name']}")
private String userName;
}

通过@Resource注解注入

1
2
3
4
5
@Component
public class MyBean {
@Resource(name = "dependency")
private Dependency dependency;
}

通过@Inject注解注入

1
2
3
4
5
@Component
public class MyBean {
@Inject
private Dependency dependency;
}

👌bean 的作用范围和生命周期?

发表于 2025-03-02 | 更新于 2025-09-14 | 分类于 Spring
字数统计 | 阅读时长

👌bean 的作用范围和生命周期?

口语化答案

好的,面试官。bean 的作用范围主要用 singleton,prototype,request,session,globalsession,application。常用的就是 singleton,singleton 是单例的,当 bean 是无状态的时候,singleton 是最好的使用方式,如果说 bean 里面涉及共享数据,singleton 就不够安全了,这个时候需要使用 prototype。bean 的生命周期从实例化创建 bean 开始,然后进行属性设置。再之后,调用 bean 的一些初始化方法,如果有则执行,这样处理完之后,bean 就可以被使用了。最终当 bean 要被销毁的时候,就会调用 destroy 方法进行 bean 的后置处理,以上。

题目解析

非常常考,一定要熟悉要 singleton 和 prototype 的区别。这个重点要的是状态问题。bean 的生命周期就比较好答,按照顺序来说就可以了。

面试得分点

singleton,prototype,无状态,实例化,属性设置,初始方法,销毁

题目详细答案

Bean的作用范围(Scope)和生命周期(Lifecycle)决定了Bean的创建、使用和销毁方式。

Bean的作用范围(Scope)

singleton:默认作用范围,整个Spring容器中只有一个实例,所有对该Bean的引用都指向同一个实例。适用于无状态的Bean。

1
<bean id="myBean" class="com.example.MyBean" scope="singleton"/>

prototype:每次请求该Bean时都会创建一个新的实例。适用于有状态的Bean。

1
<bean id="myBean" class="com.example.MyBean" scope="prototype"/>

request:每次HTTP请求都会创建一个新的实例,仅适用于Web应用。

1
<bean id="myBean" class="com.example.MyBean" scope="request"/>

session:每个HTTP会话都会创建一个新的实例,仅适用于Web应用。

1
<bean id="myBean" class="com.example.MyBean" scope="session"/>

globalSession:每个全局HTTP会话都会创建一个新的实例,仅适用于Portlet应用。

1
<bean id="myBean" class="com.example.MyBean" scope="globalSession"/>

application:每个ServletContext会创建一个新的实例,适用于Web应用。

1
<bean id="myBean" class="com.example.MyBean" scope="application"/>

Bean的生命周期

1724754790092-94de9a92-f85f-4b0f-851d-62174f065951.png

实例化(Instantiation)

Spring容器根据配置创建Bean实例。

属性设置(Property Population)

Spring容器进行依赖注入,设置Bean的属性。

初始化(Initialization)

如果Bean实现了InitializingBean接口,Spring会调用其afterPropertiesSet()方法。

如果在XML配置中指定了init-method属性,Spring会调用指定的初始化方法。

如果Bean使用了@PostConstruct注解,Spring会调用标注的方法。

使用(Usage)

Bean处于就绪状态,可以被应用程序使用。

销毁(Destruction)

如果Bean实现了DisposableBean接口,Spring会调用其destroy()方法。

如果在XML配置中指定了destroy-method属性,Spring会调用指定的销毁方法。

如果Bean使用了@PreDestroy注解,Spring会调用标注的方法。

生命周期回调接口和注解

InitializingBean接口

方法:afterPropertiesSet()

1
2
3
4
5
6
public class MyBean implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
// 初始化逻辑
}
}

DisposableBean接口

方法:destroy()

1
2
3
4
5
6
public class MyBean implements DisposableBean {
@Override
public void destroy() throws Exception {
// 销毁逻辑
}
}

@PostConstruct注解

用于标注初始化方法。

1
2
3
4
5
6
public class MyBean {
@PostConstruct
public void init() {
// 初始化逻辑
}
}

@PreDestroy注解

用于标注销毁方法。

1
2
3
4
5
6
public class MyBean {
@PreDestroy
public void cleanup() {
// 销毁逻辑
}
}

代码 Demo

1
<bean id="myBean" class="com.example.MyBean" scope="singleton" init-method="customInit" destroy-method="customDestroy"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class MyBean implements InitializingBean, DisposableBean {
@Override
public void afterPropertiesSet() throws Exception {
// 初始化逻辑
}

@Override
public void destroy() throws Exception {
// 销毁逻辑
}

@PostConstruct
public void init() {
// 初始化逻辑
}

@PreDestroy
public void cleanup() {
// 销毁逻辑
}

public void customInit() {
// 自定义初始化逻辑
}

public void customDestroy() {
// 自定义销毁逻辑
}
}

👌jdk动态代理如何实现

发表于 2025-03-02 | 更新于 2025-09-14 | 分类于 Spring
字数统计 | 阅读时长

👌jdk动态代理如何实现

口语化答案

好的,面试官。jdk 的动态代理主要是依赖Proxy 和InvocationHandler 接口。jdk 动态代理要求类必须有接口。在进行实现的时候,首先要定义接口,比如MyService,这个接口就是我们的正常功能的实现。但是希望在不更改MyService 的情况下增加,那么我们需要定义一个实现InvocationHandler 接口的实现类,同时在方法实现上面增加额外的逻辑。最后通过 Proxy 的 newProxyInstance 将二者结合到一起。就实现了动态代理。以上。

题目解析

大家不要觉得动态代理很难理解,按照这个步骤其实你发现很简单。记忆的过程和 cglib 对比着看,就很轻松,面试也是属于常考一点的题目。

面试得分点

InvocationHandler 增强,Proxy 创建代理

题目详细答案

JDK 动态代理主要依赖于java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口来实现。

实现步骤

定义接口:定义需要代理的接口。

实现接口:创建接口的实现类。

创建调用处理器:实现InvocationHandler接口,并在invoke方法中定义代理逻辑。

创建代理对象:通过Proxy.newProxyInstance方法创建代理对象。

代码 Demo

假设我们有一个简单的服务接口MyService和它的实现类MyServiceImpl,我们将通过 JDK 动态代理为MyService创建一个代理对象,并在方法调用前后添加日志。

1. 定义接口

1
2
3
public interface MyService {
void performTask();
}

2. 实现接口

1
2
3
4
5
6
public class MyServiceImpl implements MyService {
@Override
public void performTask() {
System.out.println("Performing task");
}
}

3. 创建调用处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class LoggingInvocationHandler implements InvocationHandler {
private final Object target;

public LoggingInvocationHandler(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Logging before method execution: " + method.getName());
Object result = method.invoke(target, args);
System.out.println("Logging after method execution: " + method.getName());
return result;
}
}

4. 创建代理对象并使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.lang.reflect.Proxy;

public class MainApp {
public static void main(String[] args) {
// 创建目标对象
MyService myService = new MyServiceImpl();

// 创建调用处理器
LoggingInvocationHandler handler = new LoggingInvocationHandler(myService);

// 创建代理对象
MyService proxyInstance = (MyService) Proxy.newProxyInstance(
myService.getClass().getClassLoader(),
myService.getClass().getInterfaces(),
handler
);

// 调用代理对象的方法
proxyInstance.performTask();
}
}

详细解释

1、 接口定义和实现:

MyService是一个简单的接口,定义了一个方法performTask。

MyServiceImpl是MyService的实现类,实现了performTask方法。

2、 调用处理器:

LoggingInvocationHandler实现了InvocationHandler接口。它的invoke方法在代理对象的方法调用时被调用。invoke方法接收三个参数:proxy:代理对象。method:被调用的方法。args:方法参数。在invoke方法中,我们在方法调用前后添加了日志打印。

3、 创建代理对象:

使用Proxy.newProxyInstance方法创建代理对象。

newProxyInstance方法接收三个参数:类加载器:通常使用目标对象的类加载器。接口数组:目标对象实现的所有接口。调用处理器:实现了InvocationHandler接口的实例。

4、 使用代理对象:

通过代理对象调用方法时,实际调用的是LoggingInvocationHandler的invoke方法。

在invoke方法中,首先打印日志,然后通过反射调用目标对象的方法,最后再打印日志。

👌什么是aop,aop的作用和优势

发表于 2025-03-02 | 更新于 2025-09-14 | 分类于 Spring
字数统计 | 阅读时长

👌什么是aop,aop的作用和优势?

口语化回答

好的,面试官。aop 是 spring 框架的一个核心。常见的可以做日志记录,事务管理等公共性的处理。当我们想要统一做一些水平统一的操作的时候,就常用 aop。aop 的核心概念就是面向切面变成。通过 aop 可以在范围内的类方法执行之前,执行之后,或者异常的时候,做统一的操作。aop 可以提高代码的可维护性,任何修改只需要在切面中进行,而不需要修改业务逻辑代码。基于动态代理机制,还可以在不修改源代码的情况下为现有代码添加功能。在 boot 应用使用中,只需要配合Aspect 注解,即可实现功能,以上。

题目解析

aop 是核心问题,spring 里面的比较好的一个特性,因为实际工作中非常常用,面试官就会问你,是否有用过这一块的东西。

面试得分点

面向切面,模块化,减少重复代码

题目详细答案

AOP(面向切面编程)是一种编程范式,它旨在通过分离横切关注点来提高代码的模块化。AOP 在传统的面向对象编程(OOP)基础上,提供了一种处理系统级关注点(如日志记录、事务管理、安全性等)的机制,而这些关注点通常会散布在多个模块中。

AOP 的基本概念

Aspect(切面):切面是模块化的横切关注点。它可以包含多个 advice(通知)和 pointcut(切入点)。

Advice(通知):通知是切面在特定的切入点执行的动作。通知有几种类型:

  • Before:在方法执行之前执行。
  • After:在方法执行之后执行。
  • AfterReturning:在方法成功执行之后执行。
  • AfterThrowing:在方法抛出异常后执行。
  • Around:包围一个方法的执行,能够控制方法的执行前后。

Pointcut(切入点):切入点定义了通知应该应用到哪些连接点上。连接点是程序执行的特定点,如方法调用或异常抛出。

Join Point(连接点):程序执行的特定点,例如方法调用或异常抛出。

Weaving(织入):将切面应用到目标对象创建代理对象的过程。织入可以在编译时、类加载时、运行时进行。

AOP 的作用和优势

模块化横切关注点:AOP 允许将横切关注点(如日志记录、事务管理、安全性等)从业务逻辑中分离出来,从而提高代码的模块化和可维护性。

减少重复代码:通过将通用功能提取到切面中,可以减少代码的重复,提高代码的可读性和可维护性。

提高代码的可维护性:由于横切关注点被模块化为切面,任何修改只需要在切面中进行,而不需要修改业务逻辑代码,从而提高了代码的可维护性。

动态代理:AOP 使用动态代理机制,可以在不修改源代码的情况下为现有代码添加功能。

增强代码的可测试性:由于横切关注点被分离成切面,业务逻辑代码变得更加简洁和专注,从而提高了代码的可测试性。

AOP 的应用场景

日志记录:在方法调用前后记录日志。

事务管理:在方法执行前开启事务,在方法执行后提交事务,在方法抛出异常时回滚事务。

安全性检查:在方法执行前进行权限检查。

性能监控:在方法执行前后记录执行时间。

缓存:在方法调用前检查缓存,在方法调用后更新缓存。

示例代码

定义切面

1
2
3
4
5
6
7
8
9
10
11
12
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore() {
System.out.println("Logging before method execution");
}
}

配置 Spring 应用

1
2
3
4
5
6
7
8
9
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan(basePackages = "com.example")
@EnableAspectJAutoProxy
public class AppConfig {
}

使用 AOP 的服务类

1
2
3
4
5
6
7
8
import org.springframework.stereotype.Service;

@Service
public class MyService {
public void performTask() {
System.out.println("Performing task");
}
}

主应用

1
2
3
4
5
6
7
8
9
10
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = context.getBean(MyService.class);
myService.performTask();
}
}

LoggingAspect切面会在每次调用MyService中的方法时记录日志,而无需修改MyService的源代码。 AOP 通过分离横切关注点来提高代码的模块化和可维护性。

👌说说Spring常用的注解

发表于 2025-03-02 | 更新于 2025-09-14 | 分类于 Spring
字数统计 | 阅读时长

👌说说Spring常用的注解

口语化回答

好的,面试官,比较常用的就是Component,将类放到容器管理,autowired,resource,来装配 bean。还有就是 configuration 和 bean 进行配合,装载 bean 进入容器。以及一些扩展的注解比如 aop 的 aspect 切面,事务相关的 transsactional。以上。

题目解析

基本不会考,这个没啥意义,不排除小公司或应届会问到这个问题。

面试得分点

随便搞下面的 3~4 个注解说出来即可。

题目详细答案

核心注解

@Component:将一个类标记为 Spring 组件类,表示这个类的实例将由 Spring 容器管理。

@Autowired:自动注入依赖对象。可以用于构造器、字段、Setter 方法或其他任意方法。

@Qualifier:在有多个候选 Bean 时,指定要注入的 Bean。通常与@Autowired一起使用。

1
2
3
4
5
6
@Component
public class MyComponent {
@Autowired
@Qualifier("specificBean")
private Dependency dependency;
}

@Value:注入外部化的值,例如配置文件中的属性值、系统属性或环境变量。

1
2
3
4
5
@Component
public class MyComponent {
@Value("${my.property}")
private String myProperty;
}

@Configuration:标记一个类为 Spring 配置类,该类可以包含一个或多个@Bean方法。

1
2
3
4
5
6
7
@Configuration
public class AppConfig {
@Bean
public MyComponent myComponent() {
return new MyComponent();
}
}

@Bean:定义一个方法,返回一个要注册为 Spring 容器管理的 Bean。

1
2
3
4
@Bean
public MyComponent myComponent() {
return new MyComponent();
}

@ComponentScan:指定要扫描的包,以查找带有@Component、@Service、@Repository和@Controller注解的类,并将它们注册为 Spring Bean。

1
2
3
4
5
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {
// 配置类内容
}

@Primary:标记一个 Bean 为首选 Bean,当有多个候选 Bean 时,Spring 会优先注入带有@Primary注解的 Bean。

1
2
3
4
5
@Component
@Primary
public class PrimaryBean implements Dependency {
// 实现
}

@Scope:指定 Bean 的作用域,例如单例(singleton)、原型(prototype)等。

1
2
3
4
5
@Component
@Scope("prototype")
public class PrototypeBean {
// 实现
}

特定用途注解

@Service:标记一个类为服务层组件,实际上是@Component的特殊化。

1
2
3
4
@Service
public class MyService {
// 服务逻辑
}

@Repository:标记一个类为数据访问层组件,实际上是@Component的特殊化,并且支持持久化异常转换。

1
2
3
4
@Repository
public class MyRepository {
// 数据访问逻辑
}

@Controller:标记一个类为 Spring MVC 控制器,实际上是@Component的特殊化

1
2
3
4
@Controller
public class MyController {
// 控制器逻辑
}

@RestController:标记一个类为 RESTful Web 服务的控制器,等同于@Controller和@ResponseBody的组合。

1
2
3
4
5
6
7
@RestController
public class MyRestController {
@GetMapping("/hello")
public String hello() {
return "Hello, World!";
}
}

AOP(面向切面编程)相关注解

@Aspect:标记一个类为切面类。

1
2
3
4
5
@Aspect
@Component
public class MyAspect {
// 切面逻辑
}

@Before:定义一个方法,在目标方法执行之前执行。

1
2
3
4
5
6
7
8
@Aspect
@Component
public class MyAspect {
@Before("execution(* com.example.service.*.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
// 前置逻辑
}
}

@After:定义一个方法,在目标方法执行之后执行。

1
2
3
4
5
6
7
8
@Aspect
@Component
public class MyAspect {
@After("execution(* com.example.service.*.*(..))")
public void afterMethod(JoinPoint joinPoint) {
// 后置逻辑
}
}

@Around:定义一个方法,包围目标方法的执行

1
2
3
4
5
6
7
8
9
@Aspect
@Component
public class MyAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
// 环绕逻辑
return joinPoint.proceed();
}
}

事务管理相关注解

@Transactional:标记一个方法或类,表示该方法或类中的所有方法都需要事务管理。

1
2
3
4
5
6
7
@Service
public class MyService {
@Transactional
public void performTransaction() {
// 事务逻辑
}
}

MyBatis拦截器实现分表

发表于 2025-02-22 | 更新于 2025-09-14 | 分类于 分表
字数统计 | 阅读时长

👌MyBatis拦截器实现分表

背景

所负责的业务中有一个 xx 系统,其中的 xx 核心表增长速度非常快,每日增长 200W 的数据量,业务特性主要是查询最近一周的数据,那么随着数据量越来越多,查询性能疯狂下降。这是典型的冷热数据场景,于是要开始进行冷热分离的操作。

常见的两种方案,一种是删除/归档旧数据,一种是数据分表。

方案选择

归档/删除旧数据

这种方式最常见的发难就是将冷数据移动到归档表或者直接进行删除,从而减少表的大小。

实现思路非常简单,一般写一个定时任务不断的跑就可以了。

缺点:

1、数据删除会影响数据库性能,引发慢sql,多张表并行删除,数据库压力会更大。

2、频繁删除数据,会产生数据库碎片,影响数据库性能,引发慢SQL。

有一定的风险,选择另一种分表的方案。

分表

业务特性是查最近的一周的数据,按日期的水平拆分是合理的。每周生成一张新表,同时此表只放本周的数据,这样表内数据量得到了控制,不会很大。

分表方案选型

分别常见的主要考虑2种分表方案:Sharding-JDBC、利用Mybatis自带的拦截器特性。

经过对比后,决定采用Mybatis拦截器来实现分表。

1、JAVA生态中很常用的分表框架是Sharding-JDBC,虽然功能强大,但需要一定的接入成本,并且很多功能暂时用不上。

2、系统本身已经在使用Mybatis了,只需要添加一个mybaits拦截器,把SQL表名替换为新的周期表就可以了,没有接入新框架的成本,开发成本也不高。

分表具体实现思路

1、通过拦截器获取表名

String tableName = TemplateMatchService.matchTableName(boundSql.getSql().trim());

2、根据数据日期进行计算后缀

3、替换原有 sql,最终执行。

String rebuildSql = boundSql.getSql().replace(shardingProperty.getTableName(), shardingTable);

metaStatementHandler.setValue(ORIGIN_BOUND_SQL, rebuildSql);

Linux中断机制是什么?

发表于 2025-02-20 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌Linux中断机制是什么?

Linux中断机制是操作系统处理硬件和软件事件的一种重要方式。中断可以使操作系统及时响应各种事件,如硬件设备的输入输出操作、定时器事件等。

中断的基本概念

1.中断类型

  • 硬件中断:由硬件设备(如键盘、网卡、硬盘等)发出的信号,引起CPU中断当前执行的程序,转而执行中断处理程序。
  • 软件中断:由软件指令(如系统调用、异常等)引发的中断。

2.中断向量表

中断向量表(Interrupt Vector Table, IVT)是一个包含中断服务程序地址的表。当中断发生时,CPU通过中断向量表找到相应的中断服务程序并执行。

中断处理流程

  1. 中断发生:硬件设备或软件事件发出中断信号。
  2. 中断响应:CPU停止当前执行的程序,保存当前的上下文(如程序计数器、寄存器等),并根据中断向量表找到对应的中断服务程序。
  3. 中断处理:执行中断服务程序,处理中断事件。
  4. 中断返回:中断处理完成后,恢复保存的上下文,继续执行被中断的程序。

中断处理的关键组件

1.中断控制器

中断控制器(如PIC、APIC)负责管理和分配中断请求。它可以屏蔽不需要的中断,并确定中断的优先级。

2.中断处理程序

中断处理程序(Interrupt Service Routine, ISR)是处理特定中断事件的函数。它通常分为顶半部(Top Half)和底半部(Bottom Half):

  • 顶半部:快速响应中断,执行紧急的处理任务。
  • 底半部:延迟执行较长时间的任务,以减少顶半部的执行时间,避免阻塞其他中断。

3.中断上下文

中断上下文是指中断发生时CPU的状态,包括程序计数器、寄存器等。保存和恢复中断上下文是中断处理的关键步骤。

中断处理的实现

1.注册中断处理程序

在Linux内核中,可以使用request_irq函数注册中断处理程序:

1
2
3
#include <linux/interrupt.h>

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);

2.中断处理程序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>

static irqreturn_t my_interrupt_handler(int irq, void *dev_id) {
printk(KERN_INFO "Interrupt occurred!\n");
// 中断处理逻辑
return IRQ_HANDLED;
}

static int __init my_module_init(void) {
int irq = 1; // 假设中断号为1
int result = request_irq(irq, my_interrupt_handler, IRQF_SHARED, "my_interrupt", NULL);
if (result) {
printk(KERN_ERR "Failed to request IRQ\n");
return result;
}
return 0;
}

static void __exit my_module_exit(void) {
int irq = 1; // 假设中断号为1
free_irq(irq, NULL);
}

module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("A simple interrupt handler module");
MODULE_AUTHOR("Your Name");

3.顶半部和底半部

  • 顶半部:快速处理中断的关键部分。
  • 底半部:使用任务队列、工作队列或软中断机制延迟处理较长时间的任务。

中断的优先级和屏蔽

中断控制器可以设置中断的优先级和屏蔽某些中断,以确保重要的中断优先得到处理。APIC(Advanced Programmable Interrupt Controller)提供了更高级的中断管理功能,如多级中断优先级和分布式中断处理。

中断的嵌套和重入

中断处理程序应尽量短小精悍,以减少中断嵌套的层数。中断处理程序应避免使用可能导致阻塞的操作,如长时间的I/O操作和锁定操作。

原文: https://www.yuque.com/jsyun/vwxwc6

Linux查看文件磁盘占用情况

发表于 2025-02-20 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌Linux查看文件磁盘占用情况?

1.du命令

du(disk usage)命令用于估算文件和目录的磁盘使用情况。

基本用法

1
du [选项] [文件或目录]

常用选项

  • -h:以人类可读的格式显示(例如,K、M、G)。
  • -s:显示总计(不显示子目录的详细信息)。
  • -a:包括所有文件和目录。
  • -c:显示总计。
  • --max-depth=N:限制显示的目录层级深度。

示例

  • 查看当前目录下每个文件和子目录的磁盘使用情况:
1
du -h
  • 查看指定目录的总磁盘使用情况:
1
du -sh /path/to/directory
  • 查看指定目录及其子目录的磁盘使用情况,限制深度为1:
1
du -h --max-depth=1 /path/to/directory

2.df命令

df(disk free)命令用于查看文件系统的磁盘使用情况。

基本用法

1
df [选项] [文件或目录]

常用选项

  • -h:以人类可读的格式显示(例如,K、M、G)。
  • -T:显示文件系统类型。
  • -i:显示inode使用情况。

示例

  • 查看所有挂载的文件系统的磁盘使用情况:
1
df -h
  • 查看特定目录所在的文件系统的磁盘使用情况:
1
df -h /path/to/directory

3.ncdu工具

ncdu(NCurses Disk Usage)是一个基于ncurses的磁盘使用分析工具,提供了交互式界面。

安装

在Debian/Ubuntu系统上:

1
sudo apt-get install ncdu

在CentOS/RHEL系统上:

1
sudo yum install ncdu

使用

1
ncdu /path/to/directory

4.ls命令

ls命令也可以显示文件的大小,但不如du详细。

示例

  • 以人类可读的格式显示文件大小:
1
ls -lh
  • 显示目录的总大小:
1
ls -lhS

5.find命令结合du

可以使用find命令查找特定条件的文件,然后结合du命令查看它们的磁盘使用情况。

示例

  • 查找大于100MB的文件并显示其磁盘使用情况:
1
find /path/to/directory -type f -size +100M -execdu -h {} +

Linux死锁是什么?

发表于 2025-02-20 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌Linux死锁是什么?

在Linux操作系统中,死锁(Deadlock)是指两个或多个进程在等待彼此释放资源,从而导致它们都无法继续执行的情况。这是一种常见的并发问题,尤其在多线程或多进程环境中。

死锁的四个必要条件

死锁的发生需要满足以下四个条件(也称为Coffman条件):

  1. 互斥条件(Mutual Exclusion):至少有一个资源是非共享的,即一次只能被一个进程使用。
  2. 占有并等待条件(Hold and Wait):一个进程已经持有了至少一个资源,并且正在等待获取其他被其他进程持有的资源。
  3. 不剥夺条件(No Preemption):资源不能被强制从进程中剥夺,只有持有资源的进程可以主动释放资源。
  4. 环路等待条件(Circular Wait):存在一个进程链,使得每个进程都在等待链中下一个进程所持有的资源。

死锁的检测和预防

为了处理死锁问题,可以采取以下几种策略:

1.预防死锁

预防死锁的方法包括破坏上述四个条件之一:

  • 破坏互斥条件:尽可能减少对非共享资源的使用。
  • 破坏占有并等待条件:在进程开始时一次性分配所有需要的资源,或者要求进程在请求资源前释放所有持有的资源。
  • 破坏不剥夺条件:允许操作系统强制剥夺某些资源。
  • 破坏环路等待条件:对资源进行排序,并要求进程按顺序请求资源。

2.避免死锁

避免死锁的方法主要是通过资源分配策略来确保系统永远不会进入死锁状态。常用的方法包括银行家算法(Banker’s Algorithm),它通过模拟资源分配情况来判断是否会导致死锁。

3.检测和恢复

如果无法预防或避免死锁,可以通过检测和恢复来处理死锁:

  • 死锁检测:定期检查系统中是否存在死锁。可以使用资源分配图(Resource Allocation Graph)来检测环路,从而识别死锁。
  • 死锁恢复:一旦检测到死锁,可以通过以下方式恢复:
    • 终止进程:强制终止一个或多个进程以打破死锁。
    • 资源剥夺:强制从某些进程中剥夺资源并重新分配。

linux 内存有问题怎么查看

发表于 2025-02-20 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌linux 内存有问题怎么查看?

如果怀疑内存有问题,可以通过以下步骤进行检查和诊断:

1. 使用free命令查看内存使用情况

free命令可以显示系统的内存使用情况,包括总内存、已使用内存、空闲内存和缓存等信息。

1
free -h

输出示例:

1
2
3
total        used        free      shared  buff/cache   available
Mem: 7.8G 3.2G 1.5G 200M 3.1G 4.1G
Swap: 2.0G 0.0K 2.0G

2. 使用top或htop命令查看内存使用情况

top和htop命令可以实时显示系统的资源使用情况,包括内存使用情况。

  • top命令:
1
top
  • htop命令(需要安装):
1
2
sudo apt-get install htop
htop

3. 使用vmstat命令查看内存和系统性能

vmstat命令可以报告虚拟内存、进程、CPU 活动等信息。

1
vmstat 1 5

输出示例:

1
2
3
4
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
1 0 0 1576828 72464 2192160 0 0 8 11 64 150 1 0 99 0 0
0 0 0 1576820 72464 2192160 0 0 0 0 59 144 0 0 100 0 0

4. 检查内存使用情况的日志

查看系统日志,如/var/log/syslog或/var/log/messages,看看是否有与内存相关的错误或警告。

1
2
sudo grep -i memory /var/log/syslog
sudo grep -i memory /var/log/messages

linux的中断机制用来干什么

发表于 2025-02-20 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌linux的中断机制用来干什么?

Linux的中断机制用于处理各种硬件和软件事件,使操作系统能够及时响应并处理这些事件。中断机制在操作系统中起着至关重要的作用。

1.硬件设备的事件处理

硬件设备通过中断机制通知操作系统它们的状态变化或需要处理的事件。例如:

  • 键盘输入:当键盘按键被按下时,键盘控制器会发出中断信号,通知CPU读取按键值。
  • 网络数据接收:网卡接收到数据包时,会触发中断,通知操作系统处理接收到的数据。
  • 硬盘I/O完成:硬盘完成读写操作后,会发出中断信号,通知操作系统该操作已完成,可以继续处理数据。

2.定时器和时钟管理

操作系统使用定时器中断来管理系统时钟和时间片轮转。例如:

  • 系统时钟更新:定时器会定期触发中断,操作系统通过这些中断更新系统时钟。
  • 时间片轮转:在多任务操作系统中,定时器中断用于实现时间片轮转调度,确保各个进程能够公平地获得CPU时间。

3.系统调用和异常处理

软件中断(如系统调用和异常)允许用户空间程序与内核进行交互,并处理异常情况。例如:

  • 系统调用:用户程序通过触发软件中断进入内核态,执行系统调用以请求操作系统服务(如文件操作、进程管理等)。
  • 异常处理:当程序发生异常(如除零错误、非法内存访问等)时,CPU会触发异常中断,操作系统捕获并处理这些异常。

4.中断驱动的I/O操作

中断机制使得设备驱动程序可以实现高效的I/O操作。例如:

  • 非阻塞I/O:通过中断通知,设备驱动程序可以在设备准备好时处理I/O操作,而无需进程持续轮询设备状态,从而提高系统效率。

5.电源管理

中断机制在电源管理中也扮演重要角色。例如:

  • 电池状态变化:当电池电量低或充电完成时,电源管理芯片会触发中断,通知操作系统采取相应的措施(如节能模式或提示用户)。

6.多核处理器的中断分配

在多核处理器系统中,中断控制器(如APIC)可以将中断分配到不同的CPU核,以实现负载均衡和提高并行处理能力。

原文: https://www.yuque.com/jsyun/vwxwc6

优惠券秒杀

发表于 2025-02-16 | 更新于 2025-09-14 | 分类于 场景题
字数统计 | 阅读时长

一、前置校验(redis的lua脚本实现)

1、检查库从是否充足
2、用户是否下单过该优惠券(使用Redis中的set类型来缓存下单该优惠券的用户id集合,并且要保证数据及时更新同步,即 在检验资格通过后需要向set中添加用户id)
Lua脚本需要的ARGV参数列表中有两个待定参数,分别是优惠券id 以及 用户id

二、处理业务

1、资格检验通过,则需要保证该有效订单被阻塞队列拿到,后续阻塞式执行成功,所以将“凭证”(封装好用户id、券id、订单id的订单实例)传入阻塞队列,等待异步线程阻塞式读取处理下单业务;

如果不让你用看门狗,你会用什么方案来解决重复支付问题?

发表于 2025-02-16 | 更新于 2025-09-14 | 分类于 场景题
字数统计 | 阅读时长

👌如果不让你用看门狗,你会用什么方案来解决重复支付问题?

1. 幂等性令牌(Idempotency Token)

原理:在每次支付请求中,客户端生成一个唯一的幂等性令牌,并将其发送给服务器。服务器在处理支付请求时,会检查这个令牌是否已经被使用过。如果令牌已经被使用,服务器会拒绝重复的支付请求。

实现步骤:

  1. 客户端生成一个唯一的幂等性令牌(如 UUID)。
  2. 客户端将令牌和支付请求一起发送给服务器。
  3. 服务器检查令牌是否已经存在于数据库中。
  4. 如果令牌不存在,服务器处理支付请求并将令牌存储在数据库中。
  5. 如果令牌已经存在,服务器拒绝支付请求或者返回之前的处理结果。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class PaymentService {

private PaymentRepository paymentRepository;

public PaymentService(PaymentRepository paymentRepository) {
this.paymentRepository = paymentRepository;
}

public void processPayment(PaymentRequest request) throws DuplicatePaymentException {
String antiReplayToken = request.getAntiReplayToken();

if (paymentRepository.existsByAntiReplayToken(antiReplayToken)) {
throw new DuplicatePaymentException("Duplicate payment detected for anti-replay token: " + antiReplayToken);
}

// 处理支付逻辑
Payment payment = new Payment();
payment.setAntiReplayToken(antiReplayToken);
payment.setAmount(request.getAmount());
payment.setStatus("processing");

paymentRepository.save(payment);

// 完成支付后更新状态
payment.setStatus("completed");
paymentRepository.save(payment);
}
}

2. 数据库唯一约束(不推荐)

原理:在数据库中为每个支付请求生成一个唯一的交易 ID,并在数据库中设置唯一约束(Unique Constraint)来防止重复记录。

实现步骤:

  1. 在支付请求中生成一个唯一的交易 ID。
  2. 在数据库中保存支付记录时,使用交易 ID 作为唯一键。
  3. 如果尝试插入重复的交易 ID,数据库会抛出唯一约束异常,从而防止重复支付。

示例代码(伪代码):

1
2
3
4
5
6
7
8
9
CREATE TABLE payments (
id SERIAL PRIMARY KEY,
transaction_id VARCHAR(255) UNIQUE NOT NULL,
amount DECIMAL(10, 2),
status VARCHAR(50)
);

-- 插入支付记录时
INSERT INTO payments (transaction_id, amount, status) VALUES ('unique_transaction_id', 100.00, 'pending');

3. 分布式锁

原理:使用分布式锁来确保同一时间只有一个支付请求能够被处理。可以使用 Redis、ZooKeeper 等工具实现分布式锁。

实现步骤:

  1. 在处理支付请求前,尝试获取分布式锁。
  2. 如果获取锁成功,处理支付请求。
  3. 如果获取锁失败,拒绝支付请求或等待重试。
  4. 支付处理完成后,释放分布式锁。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class PaymentService {

@Autowired
private RedissonClient redissonClient;

@Autowired
private PaymentRepository paymentRepository;

public void processPayment(PaymentRequest request) throws DuplicatePaymentException {
String transactionId = request.getTransactionId();
RLock lock = redissonClient.getLock("payment_lock_" + transactionId);

try {
if (lock.tryLock(10, 10, TimeUnit.SECONDS)) {
if (paymentRepository.existsByTransactionId(transactionId)) {
throw new DuplicatePaymentException("Duplicate payment detected for transaction ID: " + transactionId);
}

// 处理支付逻辑
Payment payment = new Payment();
payment.setTransactionId(transactionId);
payment.setAmount(request.getAmount());
payment.setStatus("processing");

paymentRepository.save(payment);

// 完成支付后更新状态
payment.setStatus("completed");
paymentRepository.save(payment);
} else {
throw new RuntimeException("Could not acquire lock for transaction ID: " + transactionId);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Lock acquisition interrupted", e);
} finally {
lock.unlock();
}
}
}

4. 事务和锁机制

原理:使用数据库事务和锁机制来确保支付操作的原子性和一致性。

实现步骤:

  1. 在数据库中为支付操作创建一个事务。
  2. 在事务中获取支付记录的锁,防止其他事务同时修改相同的记录。
  3. 执行支付操作并提交事务。
  4. 如果在支付操作中发生冲突,回滚事务并重试。

5. 状态机

原理:使用状态机来管理支付请求的状态,确保每个支付请求只能从一个状态转移到另一个状态,避免重复处理。

实现步骤:

  1. 定义支付请求的状态(如pending、processing、completed、failed)。
  2. 在处理支付请求时,根据当前状态进行状态转移。
  3. 使用数据库事务确保状态转移的原子性。

总结

以上方法都可以有效地防止重复支付,具体选择哪种方案取决于系统的架构和需求。幂等性令牌和数据库唯一约束是比较常见且易于实现的方法,而分布式锁和状态机适用于更复杂的分布式系统。无论选择哪种方案,都需要确保支付操作的幂等性和一致性。

原文: https://www.yuque.com/jingdianjichi/xyxdsi/cf41s0446a6gokgr

看门狗机制可以防止重复支付?

发表于 2025-02-16 | 更新于 2025-09-14 | 分类于 场景题
字数统计 | 阅读时长

👌看门狗机制可以防止重复支付?

看门狗机制(Watchdog Mechanism)并不是直接用于防止重复支付的常见技术,但它可以在一定程度上帮助监控和管理支付流程,确保支付操作的可靠性和安全性。

看门狗机制的基本原理

看门狗机制通常是一个定期检查系统状态或操作完成情况的过程。如果系统在规定时间内没有响应或完成特定操作,看门狗机制会触发预定义的恢复或补救措施。它类似于一种“守护进程”,确保系统在正常运行。

在支付系统中的应用

在支付系统中,看门狗机制可以用于监控支付请求的处理状态,确保支付请求在合理的时间内完成,并防止由于网络延迟或系统故障导致的重复支付问题。

实现步骤

  1. 支付请求的状态管理:
    • 每个支付请求都有一个唯一的标识符(如交易ID)。
    • 支付请求在数据库中有明确的状态(如pending、processing、completed、failed)。
  2. 看门狗机制的监控:
    • 看门狗进程定期检查支付请求的状态,确保所有支付请求在合理的时间内从pending转移到completed或failed状态。
    • 如果发现某个支付请求长时间停留在pending或processing状态,看门狗机制会触发相应的处理逻辑。
  3. 处理逻辑:
    • 如果支付请求长时间处于pending状态,看门狗机制可以重试支付请求或将其标记为failed,以防止重复支付。
    • 如果支付请求长时间处于processing状态,看门狗机制可以检查支付处理是否卡住,并尝试重启处理流程或进行人工干预。

通过看门狗机制,可以监控支付请求的处理状态,确保支付操作在合理时间内完成,防止由于网络延迟或系统故障导致的重复支付问题。尽管看门狗机制不是直接防止重复支付的工具,但它可以作为一种辅助措施,确保支付系统的可靠性和安全性。

原文: https://www.yuque.com/jingdianjichi/xyxdsi/getdtvffwg1agghx

锁库存如果不用redis 还能怎么实现?

发表于 2025-02-16 | 更新于 2025-09-14 | 分类于 场景题
字数统计 | 阅读时长

👌锁库存如果不用redis 还能怎么实现?

锁库存是电商系统中常见的需求,特别是在高并发场景下,需要确保库存的准确性和一致性。记住 Redis 是一种常见的解决方案,如果面试官非问这种,就答下面这些方案。

1. 数据库乐观锁

乐观锁是一种不使用数据库锁机制的锁定方式。它假设冲突很少发生,因此在更新数据时进行冲突检测。

实现方法:

  1. 添加版本号: 在库存表中添加一个version字段,用于记录数据的版本。
  2. 更新时检查版本号: 在更新库存时,通过version字段来确保数据没有被其他事务修改。

示例:

1
2
3
UPDATE inventory
SET stock = stock - 1, version = version + 1
WHERE product_id = ? AND version = ?;

如果更新成功,说明库存锁定成功;否则,说明有并发修改,需要重试。

2. 数据库悲观锁(不推荐)

悲观锁是一种锁定机制,确保在事务完成之前,其他事务无法访问被锁定的数据。

实现方法:

  1. 使用**SELECT ... FOR UPDATE**: 在事务中使用SELECT ... FOR UPDATE语句锁定行,直到事务提交或回滚。

示例 SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
BEGIN;

SELECT stock
FROM inventory
WHERE product_id = ?
FOR UPDATE;

-- 检查库存并更新
UPDATE inventory
SET stock = stock - 1
WHERE product_id = ?;

COMMIT;

悲观锁适用于冲突较多的场景,但可能会导致数据库锁争用,影响性能。

3. 分布式锁

分布式锁用于在分布式系统中确保资源的独占访问。可以使用 Zookeeper、Etcd 等工具实现分布式锁。

实现方法:

  1. 使用 Zookeeper: 利用 Zookeeper 的临时节点和顺序节点特性,实现分布式锁。

示例使用 Curator 框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CuratorFramework client = CuratorFrameworkFactory.newClient("zkServer:2181", new ExponentialBackoffRetry(1000, 3));
client.start();

InterProcessMutex lock = new InterProcessMutex(client, "/inventory_lock");
try {
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
// 处理库存
} finally {
lock.release();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
client.close();
}

4. 基于消息队列的异步处理(可用)

通过消息队列,将库存操作异步化,避免并发冲突。

实现方法:

  1. 消息队列: 使用 Kafka、RabbitMQ 等消息队列,将库存操作请求发送到队列中。
  2. 消费者处理: 消费者从队列中读取消息,顺序处理库存操作。

示例:

1
2
3
4
5
6
7
8
// 生产者
rabbitTemplate.convertAndSend("inventoryQueue", new InventoryMessage(productId, quantity));

// 消费者
@RabbitListener(queues = "inventoryQueue")
public void handleInventory(InventoryMessage message) {
// 处理库存
}

5. 数据库事务(不要用不要用)

通过数据库事务,确保库存操作的原子性和一致性。

实现方法:

  1. 事务管理: 在业务逻辑中使用数据库事务,确保库存操作的原子性。

示例代码(使用 Spring 事务管理):

1
2
3
4
5
6
7
8
9
10
11
@Transactional
public void updateInventory(Long productId, int quantity) {
// 检查库存
int stock = inventoryMapper.getStock(productId);
if (stock >= quantity) {
// 更新库存
inventoryMapper.updateStock(productId, stock - quantity);
} else {
throw new InsufficientStockException();
}
}

总结

锁库存的实现方式有很多种,选择合适的方案需要根据具体的业务需求和技术栈来决定。乐观锁和悲观锁适用于单体应用和简单的分布式场景;分布式锁适用于复杂的分布式系统;基于消息队列的异步处理适用于高并发、高吞吐量的场景;数据库事务适用于需要确保操作原子性的场景。每种方案都有其优缺点,需要综合考虑性能、复杂度和一致性要求。

原文: https://www.yuque.com/jingdianjichi/xyxdsi/og4as3nog7ss4mse

1000w条数据插入mysql如何设计?

发表于 2025-02-15 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌1000w条数据插入mysql如何设计?

1. 使用批量插入(Bulk Insert)

批量插入可以显著减少 SQL 语句的开销和网络传输的负担。可以使用INSERT INTO ... VALUES语句一次插入多条记录。

示例:

1
2
3
4
5
INSERT INTO your_table (column1, column2, column3) VALUES
('value1', 'value2', 'value3'),
('value4', 'value5', 'value6'),
...
('valueN1', 'valueN2', 'valueN3');

2. 禁用或延迟索引和约束(不到万不得已,别搞,不推荐)

在插入大量数据之前,可以暂时禁用或延迟索引和外键约束,以减少插入过程中的开销。插入完成后再重新启用索引和约束。

禁用外键检查:

1
SET foreign_key_checks =0;

禁用唯一检查:

1
SET unique_checks =0;

3. 调整 MySQL 配置

确保 MySQL 配置足够支持大批量数据插入。

增大缓冲区大小:

1
2
3
[mysqld]
innodb_buffer_pool_size = 1G # 根据你的内存大小调整
innodb_log_buffer_size = 64M

调整事务日志大小:

1
2
3
[mysqld]
innodb_log_file_size = 512M
innodb_log_files_in_group = 2

4. 使用事务

将大量插入操作分批放入事务中,可以减少每次插入的提交开销。如果数据量非常大,可以每 1000 或 10000 条记录提交一次。换成代码使用就是 transactiontemplate来手动控制事务提交

示例:

1
2
3
4
5
6
7
8
9
START TRANSACTION;

INSERT INTO your_table (column1, column2, column3) VALUES
('value1', 'value2', 'value3'),
('value4', 'value5', 'value6'),
...
('value1000', 'value1001', 'value1002');

COMMIT;

5. 使用分区表

如果数据量非常大且有分区需求,可以考虑使用 MySQL 的分区表功能,将数据按某个字段(如日期、ID 等)进行分区,提升查询和插入性能。

6. 数据导入工具

对于非常大的数据集,可以使用 MySQL 提供的工具如mysqlimport或者第三方工具如mydumper/myloader进行数据导入,这些工具通常针对大数据集做了优化。

redis滑动窗口限流怎么做的

发表于 2025-02-15 | 更新于 2025-09-14 | 分类于 面试
字数统计 | 阅读时长

👌Redis滑动窗口限流怎么做的说一下?

Redis的滑动窗口限流是一种基于时间窗口的限流策略,用于控制在特定时间范围内的请求数量。其基本思想是将时间划分为多个小的子窗口,通过记录每个子窗口内的请求数量来实现限流。与固定窗口限流相比,滑动窗口限流可以更平滑地限制请求速率,减少突发流量对系统的影响。

基本概念

  1. 时间窗口:滑动窗口限流将时间划分为多个小的子窗口。例如,要限制每秒最多10个请求,可以将1秒划分为10个子窗口,每个子窗口100毫秒。
  2. 记录请求时间戳:使用Redis的有序集合(Sorted Set)记录请求的时间戳。
  3. 计算当前窗口内的请求数量:每次有新请求到来时,移除过期的时间戳,计算当前窗口内的请求数量。
  4. 判断是否限流:如果当前窗口内的请求数量超过限流阈值,则拒绝请求;否则,允许请求并记录时间戳。

实现步骤

  1. 确定时间窗口和子窗口大小。
  2. 使用Redis有序集合记录请求时间戳。
  3. 计算当前窗口内的请求数量。
  4. 判断是否限流。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

import java.util.List;

public class SlidingWindowRateLimiter {
private Jedis jedis;
private String key;
private int windowSizeInMillis;
private int limit;

public SlidingWindowRateLimiter(Jedis jedis, String key, int windowSizeInMillis, int limit) {
this.jedis = jedis;
this.key = key;
this.windowSizeInMillis = windowSizeInMillis;
this.limit = limit;
}

public boolean isAllowed() {
long currentTimeMillis = System.currentTimeMillis();
long windowStart = currentTimeMillis - windowSizeInMillis;

Transaction transaction = jedis.multi();
transaction.zremrangeByScore(key, 0, windowStart); // 移除过期的时间戳
transaction.zcard(key); // 获取当前窗口内的请求数量
transaction.zadd(key, currentTimeMillis, String.valueOf(currentTimeMillis)); // 添加当前请求的时间戳
transaction.expire(key, windowSizeInMillis / 1000 * 2); // 设置过期时间
List<Object> results = transaction.exec();

long currentCount = (Long) results.get(1); // 获取当前窗口内的请求数量

return currentCount <= limit;
}

public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
SlidingWindowRateLimiter limiter = new SlidingWindowRateLimiter(jedis, "rate_limiter_key", 1000, 10);

if (limiter.isAllowed()) {
System.out.println("请求被允许");
} else {
System.out.println("请求被拒绝");
}

jedis.close();
}
}

详细解释

  1. 初始化:
    • SlidingWindowRateLimiter类的构造函数接受jedis(Jedis客户端实例)、key(Redis键)、windowSizeInMillis(时间窗口大小,以毫秒为单位)和limit(限流阈值)。
  2. isAllowed 方法:
    • 获取当前时间戳(毫秒)。
    • 计算窗口开始时间(当前时间减去窗口大小)。
    • 使用Redis事务(Transaction)执行以下操作:
      • zremrangeByScore:移除有序集合中所有时间戳小于窗口开始时间的元素(即过期的时间戳)。
      • zcard:获取当前窗口内的请求数量。
      • zadd:添加当前请求的时间戳到有序集合中。
      • expire:设置Redis键的过期时间,防止长期不活跃时占用Redis内存。
    • 执行事务并获取结果。
    • 检查当前窗口内的请求数量是否超过限流阈值,决定是否允许请求。
  3. main 方法:
    • 创建Jedis客户端实例并连接到Redis服务器。
    • 创建SlidingWindowRateLimiter实例并调用isAllowed方法来检查请求是否被允许。
    • 关闭Jedis客户端。

优点

  • 平滑限流:滑动窗口限流相比固定窗口限流更加平滑,能够更好地应对突发流量。
  • 精确控制:可以精确控制特定时间窗口内的请求数量。

缺点

  • 复杂度较高:实现相对复杂,需要精确管理时间戳和窗口。
  • 性能开销:频繁的Redis操作可能会带来一定的性能开销。
<i class="fa fa-angle-left"></i>1…678…12<i class="fa fa-angle-right"></i>

239 日志
22 分类
30 标签
GitHub
© 2025 javayun
由 Hexo 强力驱动
主题 - NexT.Gemini