菜鸡一枚,冲击架构师,面试记录

94 阅读10分钟

1、简述springboot的自动装配原理

@SpringBootApplication注解是一个组合注解,它包括了@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan的功能。 Spring Boot通过@EnableAutoConfiguration注解来完成自动配置。 这个注解会根据项目中的依赖(如starter依赖)和配置来自动装配相应的Bean。 Spring Boot会在META-INF/spring.factories配置文件中查找并加载各种自动配置类。

2、数据库与缓存的数据一致性

延时双删可能是业绩的通用手段,但我们没有用过,可能是考虑到实际业务并没有那么大的流量。 我们的做法是先修改库在删除缓存,如果是先删除缓存,那么在缓存数据为空和修改数据库这个间隙进来的请求,查询到的结果就没法保证, 有可能有请求进来,这时还未修改完成,那么缓存数据仍然会被存为修改前的数据,也可能运气比较好,在修改完之前没有请求进来,那么这时候缓存 的数据就是修改完的数据。 如果怕库被大流量冲击,可以加锁,限制一个线程来完成从库中加载数据到缓存中的操作。一般情况下,这种操作已经足够。

3、设计一个秒杀场景,有哪些需要注意的地方

  • 高性能: 预热与缓存:利用Redis等内存数据库缓存商品信息和用户信息,减少对后端数据库的依赖。 异步处理:来处理异步任务,如订单处理和库存更新。
  • 高可用: 多活数据中心:部署多活数据中心,确保任一数据中心故障时,其他数据中心仍能正常提供服务。 限流、熔断和降级:通过限流措施(如令牌桶、漏桶算法)来控制进入系统的请求量,防止系统过载。在系统压力过大时进行服务降级,保证核心业务可用。
  • 高可拓展: 微服务架构:将系统拆分为多个独立的服务,便于单独扩展和维护。 水平扩展:通过增加服务器数量来线性扩充系统性能。
  • 安全性: 采用IP限制、用户行为分析等手段防止恶意刷单
  • 秒杀核心逻辑: 库存超卖问题:采用乐观锁、悲观锁或其他库存锁定策略,确保不会出现超卖现象。 事务一致性:确保Redis与MQ等中间件在秒杀链路中的事务一致性。 MQ消息处理:采用幂等机制解决MQ下单消息重复消费的问题。

4、基于策略模式实现了一个付款功能,如果要在此基础上增加收款功能,该如何拓展。

付款和收款接口的定义其实十分相似,无外乎付款方,收款方及金额。 首先需要注意付款接口的定义是否和付款强相关,比如原接口定义为PaymentStrategy,可以考虑更改为TransactionStrategy。 然后需要基于定义好的接口实现具体的收款策略类。 最后在系统中增加一个收款的业务身份,业务身份的作用是负责在运行时选择并应用适当的策略。

5、如何保证消息消费的顺序性,如何避免消息重复消费

  • 如何保证消息消费的顺序性: topic 不分区 kafka默认存储和消费消息,是不能保证顺序性的,因为一个topic数据可能存储在不同的分区中,每个分区都有一个按照顺序的存储的偏移量,如果消费者关联了多个分区不能保证顺序性 可以把消息都存储同一个分区,有两种方式都可以进行设置,第一个是发送消息时指定分区号,第二个是发送消息时按照相同的业务设置相同的key,因为默认情况下分区也是通过key的hashcode值来选择分区的,hash值如果一样的话,分区肯定也是一样的。
  • 如何避免消息重复消费: 消费端接口幂等,我们是通过分布式锁实现的,针对过来的消息,每一条消息都有一个msgId,如果缓存中已经有这个msgId了,就不再二次消费。

6、下单扣库存,流量小时如何防止超卖,流量大时又该如何实现。

流量较小时:

  • 在MySQL中使用事务和锁: 1.开启事务:在执行库存更新前,开始一个事务。 2.读取库存:从数据库中读取当前库存。 3.检查库存:在事务中检查库存是否足够。 4.更新库存:如果库存足够,则使用UPDATE ... WHERE语句并结合FOR UPDATE锁来更新库存,例如: Sql:UPDATE inventory SET quantity = quantity - 1 WHERE product_id = ? AND quantity >= 1 FOR UPDATE; 这里的FOR UPDATE关键字会获取行级锁,阻止其他事务更新同一行直到当前事务提交或回滚。 5.提交事务:如果库存更新成功,则提交事务;否则回滚事务。 这种方式可以有效防止库存超卖,但在高并发场景下,可能会因为等待锁而产生较多的事务回滚和延迟。

  • 使用乐观锁: 在商品表中增加一个版本号字段,每次读取数据时都获取这个版本号。在更新库存时,检查版本号是否一致,如果一致则更新库存并增加版本号, 否则认为数据已经被其他事务修改过,可以重试或拒绝操作。

流量较大时:

  • 使用分布式锁: 当一个请求尝试扣减库存时,使用分布式锁确保同一时间只有一个请求能够访问库存数据。这样可以确保库存扣减的原子性和一致性。
  • 消息队列: 使用消息队列,将下单请求异步处理。当订单被创建时,将其发送到消息队列中,然后由后台服务异步处理订单并扣减库存。 这样可以避免在高并发场景下直接对数据库进行写操作,减轻数据库压力。但这里需要注意的是,数据库依然要采取措施防止超卖,只是流量被降低了而已。
  • 缓存和数据库一致性: 利用Redis缓存库存信息,使用原子操作如decr或incrby来更新库存,减少对数据库的直接写入。

7、如何控制两个生产者交替生产,并且在其对应的消费者消费完了之后另一个线程才可以生产

在Java中控制两个生产者交替生产,并且确保在一个生产者生产的项被其对应的消费者消费完毕后,另一个生产者才能开始生产,可以使用java.util.concurrent包下的ReentrantLockCondition来实现精确的线程间同步。 下面是一个示例代码,展示如何实现这样的控制:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class AlternatingProducerConsumer {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition conditionProduce1 = lock.newCondition();
    private final Condition conditionConsume1 = lock.newCondition();
    private final Condition conditionProduce2 = lock.newCondition();
    private final Condition conditionConsume2 = lock.newCondition();

    private int value1 = 0;
    private int value2 = 0;
    private boolean isProducing1 = false;
    private boolean isProducing2 = false;

    public void produce1(int value) {
        lock.lock();
        try {
            while (isProducing1 || value2 != 0) {
                conditionProduce1.await();
            }
            value1 = value;
            isProducing1 = true;
            System.out.println("Producer 1 produced: " + value);
            conditionConsume1.signal();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }

    public void consume1() {
        lock.lock();
        try {
            while (value1 == 0) {
                conditionConsume1.await();
            }
            System.out.println("Consumer 1 consumed: " + value1);
            value1 = 0;
            isProducing1 = false;
            conditionProduce2.signal();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }

    public void produce2(int value) {
        lock.lock();
        try {
            while (isProducing2 || value1 != 0) {
                conditionProduce2.await();
            }
            value2 = value;
            isProducing2 = true;
            System.out.println("Producer 2 produced: " + value);
            conditionConsume2.signal();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }

    public void consume2() {
        lock.lock();
        try {
            while (value2 == 0) {
                conditionConsume2.await();
            }
            System.out.println("Consumer 2 consumed: " + value2);
            value2 = 0;
            isProducing2 = false;
            conditionProduce1.signal();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        AlternatingProducerConsumer demo = new AlternatingProducerConsumer();

        Thread producer1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                demo.produce1(i);
            }
        });

        Thread consumer1 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                demo.consume1();
            }
        });

        Thread producer2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                demo.produce2(i);
            }
        });

        Thread consumer2 = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                demo.consume2();
            }
        });

        producer1.start();
        consumer1.start();
        producer2.start();
        consumer2.start();
    }
}

在这个例子中,四个Condition对象:conditionProduce1conditionProduce2用于控制生产者的生产时机,conditionConsume1conditionConsume2用于控制消费者的消费时机。 每个生产者和消费者在进行操作之前都会检查是否轮到自己,如果不是,就调用await()等待,直到收到signal()通知。这种机制确保了生产者和消费者之间严格的交替和同步。 注意,每个produceconsume方法内部的循环是为了确保在生产者生产之前,上一个生产者的产品已经被消费掉。这是通过检查value1value2是否为零来实现的,如果非零,表示还有未消费的产品,生产者就会等待。

8、bean的生命周期,如何对bean做一些增强,有哪些方式?

bean的生命周期有哪些

  1. 实例化:

Spring IoC容器找到配置文件中Bean的定义。 使用Java的反射机制,根据Bean的定义信息实例化Bean。 2. 属性赋值(依赖注入):

Spring将值和Bean的引用注入到Bean对应的属性中。 如果Bean实现了BeanNameAware、BeanFactoryAware、ApplicationContextAware等接口,Spring容器会将这些接口实现的方法中的参数设置给Bean,如设置Bean的名称、BeanFactory或ApplicationContext等。 3. BeanPostProcessor的前置处理:

如果Bean实现了BeanPostProcessor接口,或者Spring配置文件中定义了BeanPostProcessor,Spring会在Bean的属性设置完成后、初始化方法调用前调用postProcessBeforeInitialization方法。 4. 初始化:

如果Bean在配置文件中定义了init-method属性,Spring会调用指定的初始化方法。 如果Bean实现了InitializingBean接口,Spring会调用其afterPropertiesSet方法。 如果有@PostConstruct注解标注的方法,则Spring会调用这个方法。 5. Bean的使用:

此时Bean已经准备就绪,可以被应用程序使用。 Spring容器会管理Bean的生命周期,包括垃圾回收等。 6. BeanPostProcessor的后置处理:

如果Bean实现了BeanPostProcessor接口,或者Spring配置文件中定义了BeanPostProcessor,Spring会在Bean初始化方法调用后调用postProcessAfterInitialization方法。

  1. 销毁: 当容器关闭时,会调用每个Bean的销毁方法。 如果Bean在配置文件中定义了destroy-method属性,Spring会调用指定的销毁方法。 如果Bean实现了DisposableBean接口,Spring会调用其destroy方法。 如果有@PreDestroy注解标注的方法,则Spring会调用这个方法。

以上流程的精简版:

Spring通过BeanDefinition类获取bean的定义信息,包括类路径、加载策略和实例化模式等。在bean的生命周期中,首先实例化bean,然后注入依赖(如使用@Autowired注解),接着处理实现了Aware接口的bean,随后通过BeanPostProcessor(实际上是前后两个处理阶段)对bean进行增强或代理,最后执行初始化方法(如使用@PostConstruct注解),并在容器关闭时销毁bean。

如何对bean做一些增强

  1. 实现接口 InitializingBean接口:实现InitializingBean接口并重写afterPropertiesSet()方法,可以在Bean的属性设置完成之后,在Bean被使用之前执行一些自定义的初始化逻辑。 DisposableBean接口:实现DisposableBean接口并重写destroy()方法,在Bean被销毁之前执行一些清理逻辑。
  2. 使用注解 @PostConstruct:这个注解标记的方法会在依赖注入完成后被自动调用,相当于InitializingBean接口的afterPropertiesSet()方法。 @PreDestroy:这个注解标记的方法会在Bean销毁之前被调用,相当于DisposableBean接口的destroy()方法。
  3. 使用BeanPostProcessor BeanPostProcessor是一个强大的接口,允许你在Bean的初始化前后执行自定义的逻辑。例如,postProcessBeforeInitialization()方法在Bean的初始化之前被调用,而postProcessAfterInitialization()方法在Bean的初始化之后被调用
  4. Aware接口 Spring提供了许多以Aware结尾的接口,如ApplicationContextAware、BeanFactoryAware等。实现这些接口可以让Bean获取到Spring容器或BeanFactory的引用,从而可以在Bean中直接访问Spring容器中的其他Bean或资源