黑马八股文(图文+代码)

新版Java面试专题视频教程,java八股文面试全套真题+深度详解(含大厂高频面试真题)_哔哩哔哩_bilibili

SSM

Spring框架中的单例bean是线程安全的吗?

候选人

不是线程安全的。当多用户同时请求一个服务时,容器会给每个请求分配一个线程,这些线程会并发执行业务逻辑。如果处理逻辑中包含对单例状态的修改,比如修改单例的成员属性,就必须考虑线程同步问题。Spring框架本身并不对单例bean进行线程安全封装,线程安全和并发问题需要开发者自行处理。

通常在项目中使用的Spring bean是不可变状态(如Service类和DAO类),因此在某种程度上可以说Spring的单例bean是线程安全的。如果bean有多种状态(如ViewModel对象),就需要自行保证线程安全。最简单的解决办法是将单例bean的作用域由“singleton”变更为“prototype”。

无状态的意思是不能被修改

线程安全问题

public class Counter {
    private int count = 0; // 成员变量

    public void increment() {
        count++; // 非原子操作
    }

    public int getCount() {
        return count;
    }
}

如果我们创建了 Counter 类的一个实例,并从多个线程中调用 increment 方法,可能会遇到竞态条件(race condition)。这是因为 count++ 操作不是原子性的,实际上它包括三个步骤:

  1. 读取 count 的值。
  2. count 的值加 1。
  3. 将新的值写回 count

如果两个线程几乎同时执行这些步骤,就可能发生以下情况:

  • 线程 A 和线程 B 同时读取了相同的 count 值。
  • 它们都增加了这个值并试图将其写回。
  • 结果,count 只增加了一次,而不是两次。
public class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

通过这种方式,我们可以确保在任何时刻只有一个线程可以执行 incrementgetCount 方法,从而避免了竞态条件和数据不一致的问题。

什么是AOP?

候选人

AOP,即面向切面编程,在Spring中用于将那些与业务无关但对多个对象产生影响的公共行为和逻辑抽取出来,实现公共模块复用,降低耦合。常见的应用场景包括公共日志保存和事务处理。

你们项目中有没有使用到AOP?

候选人

我们之前在后台管理系统中使用AOP来记录系统操作日志。主要思路是使用AOP的环绕通知和切点表达式,找到需要记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,例如类信息、方法信息、注解、请求方式等,并将这些参数保存到数据库。

package com.itheima.annotation;


import java.lang.annotation.*;

@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {

    /**
     * 模块名称
     */
    public String name() default "";
}
package com.itheima.aop;

import com.itheima.annotation.Log;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;

@Component
@Aspect   //切面类
public class SysAspect {

    @Pointcut("@annotation(com.itheima.annotation.Log)")
    private void pointcut() {

    }

    @Pointcut("execution(* com.itheima.service.*.*(..))")
    public void pointcut2(){}

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取用户名
        //需要通过解析seesion或token获取

        //获取被增强类和方法的信息
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        //获取被增强的方法对象
        Method method = methodSignature.getMethod();
        //从方法中解析注解
        if(method != null){
            Log logAnnotation = method.getAnnotation(Log.class);
            System.out.println(logAnnotation.name());
        }
        //方法名字
        String name = method.getName();
        System.out.println(name);

        //通过工具类获取Request对象
        RequestAttributes reqa = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes sra = (ServletRequestAttributes)reqa;
        HttpServletRequest request = sra.getRequest();
        //访问的url
        String url = request.getRequestURI().toString();
        System.out.println(url);
        //请求方式
        String methodName = request.getMethod();
        System.out.println(methodName);

        //登录IP
        String ipAddr = getIpAddr(request);
        System.out.println(ipAddr);

        //操作时间
        System.out.println(new Date());*/

        //保存到数据库(操作日志)
        //....

        return joinPoint.proceed();
    }

    /**
     * 获取ip地址
     * @param request
     * @return
     */
    public String getIpAddr(HttpServletRequest request){
        String ip = request.getHeader("x-forwarded-for");
        if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
            ip = request.getHeader("Proxy-Client-IP");
        }
        if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
            ip = request.getRemoteAddr();
        }

        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }

}

Spring中的事务是如何实现的?

候选人

Spring实现事务的本质是利用AOP完成的。它对方法前后进行拦截,在执行方法前开启事务,在执行完目标方法后根据执行情况提交或回滚事务。

Spring中事务失效的场景有哪些?

候选人

在项目中,我遇到过几种导致事务失效的场景:

  1. 如果方法内部捕获并处理了异常,没有将异常抛出,会导致事务失效。因此,处理异常后应该确保异常能够被抛出。
  2. 如果方法抛出检查型异常(checked exception),并且没有在@Transactional注解上配置rollbackFor属性为Exception,那么异常发生时事务可能不会回滚。
  3. 如果事务注解的方法不是公开(public)修饰的,也可能导致事务失效。

Spring的bean的生命周期?

候选人

Spring中bean的生命周期包括以下步骤:

  1. 通过BeanDefinition获取bean的定义信息。
  2. 调用构造函数实例化bean。
  3. 进行bean的依赖注入,例如通过setter方法或@Autowired注解。
  4. 处理实现了Aware接口的bean。
  5. 执行BeanPostProcessor的前置处理器。
  6. 调用初始化方法,如实现了InitializingBean接口或自定义的init-method
  7. 执行BeanPostProcessor的后置处理器,可能在这里产生代理对象。
  8. 最后是销毁bean。

package com.itheima.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.itheima.lifecycle")
public class SpringConfig {
}
package com.itheima.lifecycle;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.InvocationHandler;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
public class MyBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (beanName.equals("user")) {
            System.out.println("postProcessBeforeInitialization方法执行了->user对象初始化方法前开始增强....");
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (beanName.equals("user")) {
            System.out.println("postProcessAfterInitialization->user对象初始化方法后开始增强....");
            //cglib代理对象
            Enhancer enhancer = new Enhancer();
            //设置需要增强的类
            enhancer.setSuperclass(bean.getClass());
            //执行回调方法,增强方法
            enhancer.setCallback(new InvocationHandler() {
                @Override
                public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
                    //执行目标方法
                    return method.invoke(method,objects);
                }
            });
            //创建代理对象
            return enhancer.create();
        }
        return bean;
    }

}
package com.itheima.lifecycle;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Component
public class User implements BeanNameAware, BeanFactoryAware, ApplicationContextAware, InitializingBean {

    //实例化对象
    public User() {
        System.out.println("User的构造方法执行了.........");
    }

    private String name ;

    //依赖注入 也可以是@Autowired
    @Value("张三")
    public void setName(String name) {
        System.out.println("setName方法执行了.........");
    }

    @Override
    public void setBeanName(String name) {
        System.out.println("setBeanName方法执行了.........");
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("setBeanFactory方法执行了.........");
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("setApplicationContext方法执行了........");
    }

    @PostConstruct
    public void init() {
        System.out.println("init方法执行了.................");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("afterPropertiesSet方法执行了........");
    }

    @PreDestroy
    public void destory() {
        System.out.println("destory方法执行了...............");
    }

}
package com.itheima.lifecycle;


import com.itheima.config.SpringConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;


public class UserTest {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        User user = ctx.getBean(User.class);
        System.out.println(user);
    }

}

Spring中的循环引用?

候选人

循环依赖发生在两个或两个以上的bean互相持有对方,形成闭环。Spring框架允许循环依赖存在,并通过三级缓存解决大部分循环依赖问题:

  1. 一级缓存:单例池,缓存已完成初始化的bean对象。
  2. 二级缓存:缓存尚未完成生命周期的早期bean对象。
  3. 三级缓存:缓存ObjectFactory,用于创建bean对象。

三级缓存解决的是初始化赋值阶段的问题,所以说是解决大部分循环引用问题,不能解决构造方法阶段的问题,构造方法之前没有循环引用的,有问题的是有循环引用的。
ObjectFactory 如果需要的是原始对象就创建原始对象,如果需要的是代理对象就创建代理对象,然后放入二级缓存

那具体解决流程清楚吗?

候选人

解决循环依赖的流程如下:

  1. 实例化A对象,并创建ObjectFactory存入三级缓存。
  2. A在初始化时需要B对象,开始B的创建逻辑。
  3. B实例化完成,也创建ObjectFactory存入三级缓存。
  4. B需要注入A,通过三级缓存获取ObjectFactory生成A对象,存入二级缓存。
  5. B通过二级缓存获得A对象后,B创建成功,存入一级缓存。
  6. A对象初始化时,由于B已创建完成,可以直接注入B,A创建成功存入一级缓存。
  7. 清除二级缓存中的临时对象A。

构造方法出现了循环依赖怎么解决?

候选人

由于构造函数是bean生命周期中最先执行的,Spring框架无法解决构造方法的循环依赖问题。可以使用@Lazy懒加载注解,延迟bean的创建直到实际需要时。

SpringMVC的执行流程?

候选人

SpringMVC的执行流程包括以下步骤:

  1. 用户发送请求到前端控制器DispatcherServlet
  2. DispatcherServlet调用HandlerMapping找到具体处理器。
  3. HandlerMapping返回处理器对象及拦截器(如果有)给DispatcherServlet
  4. DispatcherServlet调用HandlerAdapter
  5. HandlerAdapter适配并调用具体处理器(Controller)。
  6. Controller执行并返回ModelAndView对象。
  7. HandlerAdapterModelAndView返回给DispatcherServlet
  8. DispatcherServlet传给ViewResolver进行视图解析。
  9. ViewResolver返回具体视图给DispatcherServlet
  10. DispatcherServlet渲染视图并响应用户。

handler就是对应的方法

Springboot自动配置原理?

候选人

Spring Boot的自动配置原理基于@SpringBootApplication注解,它封装了@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan@EnableAutoConfiguration是核心,它通过@Import导入配置选择器,读取META-INF/spring.factories文件中的类名,根据条件注解决定是否将配置类中的Bean导入到Spring容器中。

导入了起步依赖才有字节码文件

如果容器中有该对象就不创建了,一般都是@ConditionalOnMissingBean(MyService.class),这样子写。

SpringBoot自定义starter

Spring 的常见注解有哪些?

候选人

Spring的常见注解包括:

  1. 声明Bean的注解:@Component@Service@Repository@Controller
  2. 依赖注入相关注解:@Autowired@Qualifier@Resource
  3. 设置作用域的注解:@Scope
  4. 配置相关注解:@Configuration@ComponentScan@Bean
  5. AOP相关注解:@Aspect@Before@After@Around@Pointcut

SpringMVC常见的注解有哪些?

候选人

SpringMVC的常见注解有:

  • @RequestMapping:映射请求路径。
  • @RequestBody:接收HTTP请求的JSON数据。
  • @RequestParam:指定请求参数名称。
  • @PathVariable:从请求路径中获取参数。
  • @ResponseBody:将Controller方法返回的对象转化为JSON。
  • @RequestHeader:获取请求头数据。
  • @PostMapping@GetMapping等。

Springboot常见注解有哪些?

候选人

Spring Boot的常见注解包括:

  • @SpringBootApplication:由@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan组成。
  • 其他注解如@RestController@GetMapping@PostMapping等,用于简化Spring MVC的配置。

MyBatis执行流程?

候选人

MyBatis的执行流程如下:

  1. 读取MyBatis配置文件mybatis-config.xml
  2. 构造会话工厂SqlSessionFactory
  3. 会话工厂创建SqlSession对象。
  4. 操作数据库的接口,Executor执行器。
  5. Executor执行方法中的MappedStatement参数。
  6. 输入参数映射。
  7. 输出结果映射。

Mybatis是否支持延迟加载?

候选人

MyBatis支持延迟加载,即在需要用到数据时才加载。可以通过配置文件中的lazyLoadingEnabled配置启用或禁用延迟加载。

延迟加载是查询订单信息才去查询订单信息

‘’

不用就删除

延迟加载的底层原理知道吗?

候选人

延迟加载的底层原理主要使用CGLIB动态代理实现:

  1. 使用CGLIB创建目标对象的代理对象。
  2. 调用目标方法时,如果发现是null值,则执行SQL查询。
  3. 获取数据后,设置属性值并继续查询目标方法。

👌Cglib的Enhancer实现动态代理?

彻底理解代理模式(静态动态CGLIB)

Mybatis的一级、二级缓存用过吗?

候选人

MyBatis的一级缓存是基于PerpetualCache的HashMap本地缓存,作用域为Session,默认开启。二级缓存需要单独开启,作用域为Namespace或mapper,默认也是采用PerpetualCache,HashMap存储。

第二次查询不会执行sql

查询第一次的sqlsession要关闭

第二次开启sqlsession才能触发二级缓存

Mybatis的二级缓存什么时候会清理缓存中的数据?

候选人

当作用域(一级缓存Session/二级缓存Namespaces)进行了新增、修改、删除操作后,默认该作用域下所有select中的缓存将被清空。

MySql

MySQL中,如何定位慢查询?

候选人:

嗯,我们当时在做压力测试时发现有些接口响应时间非常慢,超过了2秒。因为我们的系统部署了运维监控系统Skywalking,在它的报表展示中可以看到哪个接口慢,并且能分析出接口中哪部分耗时较多,包括具体的SQL执行时间,这样就能定位到出现问题的SQL。

如果没有这种监控系统,MySQL本身也提供了慢查询日志功能。可以在MySQL的系统配置文件中开启慢查询日志,并设置SQL执行时间超过多少就记录到日志文件,比如我们之前项目设置的是2秒,超过这个时间的SQL就会记录在日志文件中,我们就可以在那里找到执行慢的SQL。

嗯,我们当时发现加载分类及标签过慢,超过了2秒。MySQL本身也提供了慢查询日志功能。可以在MySQL的系统配置文件中开启慢查询日志,并设置SQL执行时间超过多少就记录到日志文件,比如我们之前项目设置的是2秒,超过这个时间的SQL就会记录在日志文件中,我们就可以在那里找到执行慢的SQL。

那这个SQL语句执行很慢,如何分析呢

候选人:如果一条SQL执行很慢,我们通常会使用MySQL的EXPLAIN命令来分析这条SQL的执行情况。通过keykey_len可以检查是否命中了索引,如果已经添加了索引,也可以判断索引是否有效。通过type字段可以查看SQL是否有优化空间,比如是否存在全索引扫描或全表扫描。通过extra建议可以判断是否出现回表情况,如果出现,可以尝试添加索引或修改返回字段来优化。

了解过索引吗?(什么是索引

候选人:嗯,索引在项目中非常常见,它是一种帮助MySQL高效获取数据的数据结构,主要用来提高数据检索效率,降低数据库的I/O成本。同时,索引列可以对数据进行排序,降低数据排序的成本,也能减少CPU的消耗。

索引的底层数据结构了解过吗

候选人:MySQL的默认存储引擎InnoDB使用的是B+树作为索引的存储结构。选择B+树的原因包括:节点可以有更多子节点,路径更短;磁盘读写代价更低,非叶子节点只存储键值和指针,叶子节点存储数据;B+树适合范围查询和扫描,因为叶子节点形成了一个双向链表。

二叉搜索树O(log n) 最坏O(n) 红黑树O(log n)

键值

数据

B树和B+树的区别是什么呢?

候选人:B树和B+树的主要区别在于:

  1. B树的非叶子节点和叶子节点都存放数据,而B+树的所有数据只出现在叶子节点,这使得B+树在查询时效率更稳定。
  2. B+树在进行范围查询时效率更高,因为所有数据都在叶子节点,并且叶子节点之间形成了双向链表。

什么是聚簇索引什么是非聚簇索引

候选人:聚簇索引是指数据与索引放在一起,B+树的叶子节点保存了整行数据,通常只有一个聚簇索引,一般是由主键构成。

非聚簇索引则是数据与索引分开存储,B+树的叶子节点保存的是主键值,可以有多个非聚簇索引,通常我们自定义的索引都是非聚簇索引。

知道什么是回表查询吗?

候选人:回表查询是指通过二级索引找到对应的主键值,然后再通过主键值查询聚簇索引中对应的整行数据的过程。

知道什么叫覆盖索引吗?

候选人:覆盖索引是指在SELECT查询中,返回的列全部能在索引中找到,避免了回表查询,提高了性能。使用覆盖索引可以减少对主键索引的查询次数,提高查询效率。

MySQL超大分页怎么处理

候选人:超大分页通常发生在数据量大的情况下,使用LIMIT分页查询且需要排序时效率较低。可以通过覆盖索引和子查询来解决。首先查询数据的ID字段进行分页,然后根据ID列表用子查询来过滤只查询这些ID的数据,因为查询ID时使用的是覆盖索引,所以效率可以提升。

索引创建原则有哪些?

候选人:创建索引的原则包括:

  • 表中的数据量超过10万以上时考虑创建索引。
  • 选择查询频繁的字段作为索引,如查询条件、排序字段或分组字段。
  • 尽量使用复合索引(联合索引),覆盖SQL的返回值。
  • 如果字段区分度不高,可以将其放在组合索引的后面。
  • 对于内容较长的字段,考虑使用前缀索引。
  • 控制索引数量,因为索引虽然可以提高查询速度,但也会影响插入、更新的速度。

什么情况下索引会失效?

候选人:索引可能在以下情况下失效:

  • 没有遵循最左匹配原则。
  • 使用了模糊查询且%号在前面。
  • 在索引字段上进行了运算或类型转换。
  • 使用了复合索引但在中间使用了范围查询,导致右边的条件索引失效。

name status address

创建索引的方法

  1. 创建表时定义索引:当你创建一个新的表时,可以直接在表定义中为某些列添加索引。例如,以下SQL语句创建了一个名为employees的新表,并为last_name列添加了普通索引,同时为主键id自动创建了唯一索引。
CREATE TABLE employees (
    id INT NOT NULL,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    PRIMARY KEY (id),
    INDEX idx_lastname (last_name)
);
  1. 使用CREATE INDEX语句:如果你需要向已有的表中添加索引,可以使用CREATE INDEX语句。下面的例子展示了如何为employees表中的first_name列添加一个索引。
CREATE INDEX idx_firstname ON employees (first_name);
  1. 使用ALTER TABLE语句:你也可以通过ALTER TABLE命令来添加索引。这个方法同样适用于添加、删除或修改现有表上的索引。
ALTER TABLE employees ADD INDEX idx_fullname (first_name, last_name);

索引类型

  • 普通索引(INDEX):最基本的索引类型,允许重复值。
  • 唯一索引(UNIQUE):确保索引列中的所有值都是唯一的,但允许有多个NULL值。
  • 全文索引(FULLTEXT):专门用于全文搜索,适用于MyISAM和InnoDB表(从MySQL 5.6开始支持InnoDB全文索引)。
  • 主键索引(PRIMARY KEY):一种特殊的唯一索引,不允许NULL值,每个表只能有一个主键。

注意事项

  • 选择合适的列创建索引:通常应该基于经常出现在WHERE子句、JOIN条件或者ORDER BY子句中的列来创建索引。
  • 避免过度索引:虽然索引能加速查询,但它们也会减慢写操作(如INSERT、UPDATE、DELETE),并且占用额外的存储空间。
  • 考虑复合索引:如果查询经常需要基于多个列进行过滤,可以考虑创建包含这些列的复合索引。

通过以上方法,你可以根据实际需求为MySQL数据库中的表创建适当的索引,以优化查询性能。不过,在设计索引策略时,还需要考虑到具体的应用场景和数据访问模式,以便达到最佳效果。

SQL的优化经验有哪些?

候选人:SQL优化可以从以下几个方面考虑:

  • 建表时选择合适的字段类型。
  • 使用索引,遵循创建索引的原则。
  • 编写高效的SQL语句,比如避免使用SELECT *,尽量使用UNION ALL代替UNION,以及在表关联时使用INNER JOIN
  • 采用主从复制和读写分离提高性能。
  • 在数据量大时考虑分库分表。

④比如之前的substring

SELECT * 可能出现回表查询 SELECT 某个字段 可能是覆盖索引 效率提高

外边决定连接数据库次数

创建表的时候,你们是如何优化的呢?

候选人:创建表时,我们主要参考《嵩山版》开发手册,选择字段类型时结合字段内容,比如数值类型选择TINYINTINTBIGINT等,字符串类型选择CHARVARCHARTEXT

在使用索引的时候,是如何优化呢?

候选人:在使用索引时,我们遵循索引创建原则,确保索引字段是查询频繁的,使用复合索引覆盖SQL返回值,避免在索引字段上进行运算或类型转换,以及控制索引数量。

你平时对SQL语句做了哪些优化呢?

候选人:我对SQL语句的优化包括指明字段名称而不是使用SELECT *,避免造成索引失效的写法,聚合查询时使用UNION ALL代替UNION,表关联时优先使用INNER JOIN,以及在必须使用LEFT JOINRIGHT JOIN时,确保小表作为驱动表。

Inner join left join right join区别?

A Inner Join B:类似于取A和B的交集,只取满足条件的相交部分。

A Left Join B:左连接,以左表A为基础,取A表的全部行,B满足On条件的取对应行,不满足的取NULL。

A Right Join B:有连接,以右表B为基础,取B表的全部行,A满足On条件的取对应行,不满足的取NULL。

A Full Outer Join B:类似于取A和B的并集。全都取,但彼此没有对应的值就取NULL。

事务的特性是什么?可以详细说一下吗?

候选人:事务的特性是ACID,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。例如,A向B转账500元,这个操作要么都成功,要么都失败,体现了原子性。转账过程中数据要保持一致,A扣除了500元,B必须增加500元。隔离性体现在A向B转账时,不受其他事务干扰。持久性体现在事务提交后,数据要被持久化存储。

并发事务带来哪些问题?

候选人:并发事务可能导致脏读、不可重复读和幻读。脏读是指一个事务读到了另一个事务未提交的“脏数据”。不可重复读是指在一个事务内多次读取同一数据,由于其他事务的修改导致数据不一致。幻读是指一个事务读取到了其他事务插入的“幻行”。

自定义幻读是指一个事务查询数据时没有数据,但是插入数据时报错已经存在了这条数据,再次查询数据时没有数据,好像出现了幻影一样。

两个select 查询都没有数据 insert的时候报错已经存在数据了

怎么解决这些问题呢?MySQL的默认隔离级别是?

候选人:解决这些问题的方法是使用事务隔离。MySQL支持四种隔离级别:

  1. 未提交读(READ UNCOMMITTED):解决不了所有问题。
  2. 读已提交(READ COMMITTED):能解决脏读,但不能解决不可重复读和幻读。
  3. 可重复读(REPEATABLE READ):能解决脏读和不可重复读,但不能解决幻读,这也是MySQL的默认隔离级别。
  4. 串行化(SERIALIZABLE):可以解决所有问题,但性能较低。

undo logredo log的区别是什么

候选人:redo log记录的是数据页的物理变化,用于服务宕机后的恢复,保证事务的持久性。而undo log记录的是逻辑日志,用于事务回滚时恢复原始数据,保证事务的原子性和一致性。

MySQL为了提高性能,引入了两块区域,内存结构和磁盘结构,当操作数据的时候,首先会从内存结构里的buffer pool读取数据,如果没有数据,会从磁盘加载某页的数据到buffer pool,当我们去操作的时候,首先操作的是buffer pool,当我们操作完成之后,会以一定的频率同步到磁盘中,这样就能减少磁盘的io,加快处理的速度。

如果说服务器宕机了,同步数据失败了,这些数据就变成了脏页,内存中的这些数据可能会消失,那你的数据就有可能会丢失了,MySQL引入了一种日志文件redolog,该日志文件由两部分组成,redolog buffer存储在内存中的 和redolog file存储在磁盘中的,一旦buffer pool数据发生了变化(一旦事务进行了提交),redolog buffer就会记录这些数据页的变化,同步的把这些数据同步到磁盘文件中,如果说对脏页的数据同步失败了就可以从redo log日志中来恢复数据,顺序的磁盘io,日志文件都是追加的,顺序的磁盘io性能就提升了很多,这种机制叫WAL,当脏页数据可以正常同步到磁盘文件的时候,这个redo log日志就没什么用了,每隔一段时间会把redo log日志进行数据的清理,redo log在磁盘中有两份,他们是循环写的

为什么不使用buffer pool同步脏页数据到磁盘?

我们在事务中通常会操作多条记录,而这些记录都是随机的操作数据页的,那么这个时候就会涉及到大量的随机磁盘io,性能是比较低的,而redo log是日志文件,日志文件时追加的,是顺序磁盘io,他的性能是高于随机磁盘io的。

如果数据库在某些变更尚未完全写入磁盘之前崩溃,重启时InnoDB会通过分析redo log文件,将所有已成功提交但尚未应用到数据文件的变更重新执行一遍,从而实现数据恢复。

事务中的隔离性是如何保证的呢(你解释一下MVCC

候选人:事务的隔离性通过锁和多版本并发控制(MVCC)来保证。MVCC通过维护数据的多个版本来避免读写冲突。底层实现包括隐藏字段、undo logread view。隐藏字段包括trx_idroll_pointerundo log记录了不同版本的数据,通过roll_pointer形成版本链。read view定义了不同隔离级别下的快照读,决定了事务访问哪个版本的数据。

多个事务并发的情况下来确定到底该访问哪个版本

自增(1、2、3...)

假设插入了一条数据原始为

id

age

name

30

30

A30

Read Committed 每次快照读的数据不一样

Repeatable Read 快照读的数据是按第一个快照读的数据,之后的都是按第一个

m_ids 是没有提交的事务id

min_trx_id 事务number最小的id

max_trx_id 事务number最大的id+1

create_trx_id 查询事务的id

假设trx_id=5套进去理解 成立一个条件就是成立该事务id

当trx_id=3时成立,就是事务版本的数据

当trx_id=5时成立,就是事务版本的数据

当trx_id=3时成立,就是事务版本的数据

MySQL主从同步原理是什么?

候选人:MySQL主从复制的核心是二进制日志(Binlog)。步骤如下:

  1. 主库在事务提交时记录数据变更到Binlog。
  2. 从库读取主库的Binlog并写入中继日志(Relay Log)。
  3. 从库重做中继日志中的事件,反映到自己的数据中。

DDL create alter drop

DML insert update delete

你们项目用过MySQL的分库分表吗?

候选人:我们采用微服务架构,每个微服务对应一个数据库,是根据业务进行拆分的,这个其实就是垂直拆分。

也可能是在同一个库中

每个库存储的是不同的数据

每个库存储的是不同的数据

也可以在同一个库中

比如说同时操作多个表在不同的库中,每个库管理自己的事务,一个库提交失败了,其他的库的事务要正常提交,就会产生分布式事务一致性问题

比如说a库和b库存储不同的表,a和b的表进行关联怎么做呢?

问题3差不多

比如水平分库,每个库存储的是相同的表,主键是自增的,综合起来可能出现id重复的问题

那你之前使用过水平分库吗?

候选人:使用过。当时业务发展迅速,某个表数据量超过1000万,单库优化后性能仍然很慢,因此采用了水平分库。我们首先部署了3台服务器和3个数据库,使用mycat进行数据分片。旧数据也按照ID取模规则迁移到了各个数据库中,这样各个数据库可以分摊存储和读取压力,解决了性能问题。

Redis

什么是缓存穿透?怎么解决?

候选人:嗯,我想一下。缓存穿透是指查询一个一定不存在的数据,由于存储层查不到数据因此不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。解决方案的话,我们通常都会用布隆过滤器来解决它。

你能介绍一下布隆过滤器吗?

候选人:嗯,是这样的。布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是Redisson实现的布隆过滤器。它的底层原理是,先初始化一个比较大的数组,里面存放的是二进制0或1。一开始都是0,当一个key来了之后,经过3次hash计算,模数组长度找到数据的下标,然后把数组中原来的0改为1。这样,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。当然,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过5%。其实这个误判是必然存在的,要不就得增加数组的长度。5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。

模数组哈希值&(n-1)或者是哈希值模数组长度

什么是缓存击穿?怎么解决?

候选人:嗯!缓存击穿的意思是,对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这个时间点对这个Key有大量的并发请求过来。这些请求发现缓存过期,一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。解决方案有两种方式:第一(设置过期时间),可以使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 SETNX 去设置一个互斥锁。当操作成功返回时,再进行 load db的操作并回设缓存,否则重试get缓存的方法。第二种(不设置过期时间)方案是设置当前key逻辑过期,大概思路如下:1) 在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间;2) 当查询的时候,从redis取出数据后判断时间是否过期;3) 如果过期,则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据可能不是最新的。当然,两种方案各有利弊:如果选择数据的强一致性,建议使用分布式锁的方案,但性能上可能没那么高,且有可能产生死锁的问题。如果选择key的逻辑删除,则优先考虑高可用性,性能比较高,但数据同步这块做不到强一致。

该问题死锁原因,比如两个业务持有对方锁不释放

缓存击穿问题及解决思路

什么是缓存雪崩?怎么解决?

候选人:嗯!缓存雪崩意思是,设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重而雪崩。与缓存击穿的区别是:雪崩是很多key,而击穿是某一个key缓存。解决方案主要是,可以将缓存失效时间分散开。比如,可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机。这样,每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存雪崩问题及解决思路

redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性强一致性)

候选人:嗯!就说我最近做的这个项目,里面有xxxx(根据自己的简历上写)的功能,需要让数据库与redis高度保持一致,因为要求时效性比较高。我们当时采用的读写锁保证的强一致性。我们使用的是Redisson实现的读写锁。在读的时候添加共享锁,可以保证读读不互斥、读写互斥。当我们更新数据的时候,添加排他锁。它是读写、读读都互斥,这样就能保证在写数据的同时,是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是,读方法和写方法上需要使用同一把锁才行。

自定义为什么要删除两次缓存?

无论是先删除缓存后操作数据库,还是先操作数据库再删除缓存都会有脏数据的风险,所以我们要再进行一次删除缓存

自定义为什么要延迟双删呢?

因为一般DB是主从模式,我们要延时一会让主节点数据同步到从节点

那这个排他锁是如何保证读写、读读互斥的呢?

候选人:其实排他锁底层使用的也是SETNX,它保证了同时只能有一个线程操作锁住的方法。

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        //配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.10.131:6379")
                .setPassword("123321");
        //创建RedissonClient对象
        return Redisson.create(config);
    }
}

你听说过延时双删吗?为什么不用它呢?(最终一致性)

候选人:延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据。其中,这个延时多久不太好确定。在延时的过程中,可能会出现脏数据,并不能保证强一致性,所以没有采用它。

redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性最终一致性)

候选人:嗯!就说我最近做的这个项目,里面有xxxx(根据自己的简历上写)的功能。数据同步可以有一定的延时(这符合大部分业务需求)。我们当时采用的阿里的Canal组件实现数据同步:不需要更改业务代码,只需部署一个Canal服务。Canal服务把自己伪装成mysql的一个从节点。当mysql数据更新以后,Canal会读取binlog数据,然后再通过Canal的客户端获取到数据,并更新缓存即可。

自定义我们当时使用的是rabbitmq,当mysql数据更新后,发送消息到mq,监听到消息后更新缓存(如何保证rabbitmq高可用呢?)

redis做为缓存,数据的持久化是怎么做的?

候选人:在Redis中提供了两种数据持久化的方式:1) RDB;2) AOF。

这两种持久化方式有什么区别呢?

候选人:RDB是一个快照文件。它是把redis内存存储的数据写到磁盘上。当redis实例宕机恢复数据的时候,可以从RDB的快照文件中恢复数据。AOF的含义是追加文件。当redis执行写命令的时候,都会存储到这个文件中。当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据。

这两种方式,哪种恢复的比较快呢?

候选人:RDB因为是二进制文件,保存时体积也比较小,所以它恢复得比较快。但它有可能会丢数据。我们通常在项目中也会使用AOF来恢复数据。虽然AOF恢复的速度慢一些,但它丢数据的风险要小很多。在AOF文件中可以设置刷盘策略。我们当时设置的就是每秒批量写入一次命令。

Redis的数据过期策略有哪些?(数据过期)

候选人:嗯~,在redis中提供了两种数据过期删除策略。第一种是惰性删除。在设置该key过期时间后,我们不去管它。当需要该key时,我们检查其是否过期。如果过期,我们就删掉它;反之,返回该key。第二种是定期删除。就是说,每隔一段时间,我们就对一些key进行检查,并删除里面过期的key。定期清理的两种模式是:1) SLOW模式,是定时任务,执行频率默认为10hz(每秒执行10次),每次不超过25ms,可以通过修改配置文件redis.conf的hz选项来调整这个次数;2) FAST模式,执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms。Redis的过期删除策略是:惰性删除 + 定期删除两种策略配合使用。

个人感觉这个好理解

好的,面试官,过期策略主要分为主动和被动,主动又分为定时、定期,被动就是常说的惰性清理。先说结论,redis 采取的方案是定期+惰性配合的方式来进行实现。定时策略主要是通过周期性执行的函数来扫描即将过期的键,立马将其进行失效操作。这种方式比较消耗 cpu。于是产生了定期操作,没隔多少 ms 来进行执行,这种减少了 cpu 的消耗。也能比较准时的删除过期的键。算是定时的一种优化,比较难的点就是寻求平衡。最后就是惰性删除,所有的 key 即使过期了也不会立马删除,当这个键过期之后,下一次访问的时候,才会被删除,容易造成内存泄漏的问题。最后 oom 就会触发内存淘汰策略了,优点就是大大减轻了 cpu 的压力。以上两种方式配合,能达到一个平衡。

Redis的数据淘汰策略有哪些?(oom)

候选人:嗯,这个在redis中提供了很多种,默认是noeviction,不删除任何数据,内存不足时直接报错。这个可以在redis的配置文件中进行设置。里面有两个非常重要的概念:一个是LRU,另外一个是LFU。LRU的意思就是最少最近使用。它会用当前时间减去最后一次访问时间。这个值越大,则淘汰优先级越高。LFU的意思是最少频率使用。它会统计每个key的访问频率。值越小,淘汰优先级越高。我们在项目中设置的是allkeys-lru,它会挑选最近最少使用的数据进行淘汰,把一些经常访问的key留在redis中。

volatitle设置了过期时间

数据库有1000万数据,Redis只能缓存20w数据。如何保证Redis中的数据都是热点数据?

候选人:嗯,我想一下。可以使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略。那留下来的都是经常访问的热点数据。

Redis的内存用完了会发生什么?

候选人:嗯~,这个要看redis的数据淘汰策略是什么。如果是默认的配置,redis内存用完以后则直接报错。我们当时设置的是allkeys-lru策略,把最近最常访问的数据留在缓存中。

假设库存只有一份,线程1、线程2都进行抢券,由于都获取到了库存1,线程1扣减,线程2扣减,导致库存-1

假如我们是单体项目,可以使用本地锁(对象锁)解决,服务挂了就失效了,服务挂了会自动释放锁

那要是我们部署了多台服务器呢?同样会出现超额扣减库存的问题

如果我们设置分布式锁呢?就可以解决问题了

Redis分布式锁如何实现?

候选人:嗯,在redis中提供了一个命令SETNX(SET if not exists)。由于redis是单线程的,用了这个命令之后,只能有一个客户端对某一个key设置值。在没有过期或删除key的时候,其他客户端是不能设置这个key的。

分布式锁-Redission快速入门

那你如何控制Redis实现分布式锁的有效时长呢?

候选人:嗯,的确。redis的SETNX指令不好控制这个问题。我们当时采用的是redis的一个框架Redisson实现的。在Redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间。当锁住的一个业务还没有执行完成的时候,Redisson会引入一个看门狗机制。就是说,每隔一段时间就检查当前业务是否还持有锁。如果持有,就增加加锁的持有时间。当业务执行完成之后,需要使用释放锁就可以了。还有一个好处就是,在高并发下,一个业务有可能会执行很快。客户1持有锁的时候,客户2来了以后并不会马上被拒绝。它会自旋不断尝试获取锁。如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。

尝试获取锁是有阈值的

 <!-- Redisson -->
  <dependency>
      <groupId>org.redisson</groupId>
      <artifactId>redisson</artifactId>
      <version>3.21.1</version>
  </dependency>
@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        //配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.10.131:6379")
        .setPassword("123321");
        //创建RedissonClient对象
        return Redisson.create(config);
    }
}
@Autowired 
private RedissonClient redissonClient;

如果设置这个就不能触发看门狗机制,因为设置了过期时间,认为你能控制

Redisson实现的分布式锁是可重入的吗

候选人:嗯,是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计数上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数。

Redisson实现的分布式锁能解决主从一致性的问题吗?

候选人:这个是不能的。比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,此时如果当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。

我们可以利用Redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个Redis实例上创建锁,应该是在多个Redis实例上创建锁,并且要求在大多数Redis节点上都成功创建锁,红锁中要求是Redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。

但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变得非常低,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁。

都获取到锁才能进行数据同步

分布式锁-redission锁的MutiLock原理(高可用实现主从同步)

如果业务非要保证数据的强一致性,这个该怎么解决呢?

候选人:嗯~,Redis本身就是支持高可用的,要做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,建议使用ZooKeeper实现的分布式锁,它是可以保证强一致性的。

Redis集群有哪些方案,知道吗?

候选人:嗯~~,在Redis中提供的集群方案总共有三种:主从复制、哨兵模式、Redis分片集群。

那你来介绍一下主从同步

候选人:嗯,是这样的,单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中。

能说一下,主从同步数据的流程吗?

候选人:嗯~~,好!主从同步分为了两个阶段,一个是全量同步,一个是增量同步。

全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的:

第一:从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。

第二:主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致。

第三:在同时主节点会执行BGSAVE,生成RDB文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的RDB文件,这样就保持了一致。

当然,如果在RDB生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件,这个就是全量同步。

增量同步指的是,当从节点服务重启之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是第一次就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步。

怎么保证Redis的高并发高可用?

候选人:首先可以搭建主从集群,再加上使用Redis中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主;同时Sentinel也充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端,所以一般项目都会采用哨兵的模式来保证Redis的高并发高可用。

哨兵也相当于是redis节点

你们使用Redis是单点还是集群,哪种集群?

候选人:嗯!我们当时使用的是主从(1主1从)加哨兵。一般单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点。尽量不做分片集群。因为集群维护起来比较麻烦,并且集群之间的心跳检测和数据通信会消耗大量的网络带宽,也没有办法使用Lua脚本和事务。

Redis集群脑裂,该怎么解决呢?

候选人:嗯!这个在项目中很少见,不过脑裂的问题是这样的,我们现在用的是Redis的哨兵模式集群的。

有的时候由于网络等原因可能会出现脑裂的情况,就是说,由于Redis master节点和Redis slave节点和Sentinel处于不同的网络分区,使得Sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个slave为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,Sentinel会将old master降为slave,这时再从新master同步数据,这会导致old master中的大量数据丢失。

关于解决的话,我记得在Redis的配置中可以设置:第一可以设置最少的slave节点个数,比如设置至少要有一个从节点才能同步数据,第二个可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。

Redis的分片集群有什么作用?

候选人:分片集群主要解决的是海量数据存储的问题,集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,就可以继续增大集群的高并发能力。同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点。

Redis分片集群中数据是怎么存储和读取的?

候选人

嗯~,在Redis集群中是这样的:

Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围,key通过CRC16校验后对16384取模来决定放置哪个槽,通过槽找到对应的节点进行存储。

取值的逻辑是一样的。

取余就是取模

Redis是单线程的,但是为什么还那么快

候选人

嗯,这个有几个原因吧~~~

  1. 完全基于内存的,C语言编写。
  2. 采用单线程,避免不必要的上下文切换和竞争条件。
  3. 使用I/O多路复用模型,非阻塞IO。

例如:BGSAVEBGREWRITEAOF都是在后台执行操作,不影响主线程的正常使用,不会产生阻塞。

能解释一下I/O多路复用模型?

候选人:嗯~~,I/O多路复用是指利用单个线程来同时监听多个Socket,并且在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。

其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;

在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程

socket就是客户端

等待数据和处理数据时是阻塞

处理数据阻塞

等待数据和处理数据时不阻塞

select和poll相当于前台有灯泡,用户亮了灯泡不确定是哪一个,需要遍历

epoll相当于前台电脑,显示哪个用户亮了灯泡

微服务

Spring Cloud 5大组件有哪些?

候选人

在早期,Spring Cloud的五大组件通常指的是:

  • Eureka:服务注册中心。
  • Ribbon:客户端负载均衡器。
  • Feign:声明式的服务调用。
  • Hystrix:服务熔断器。
  • Zuul/Gateway:API网关。

随着Spring Cloud Alibaba的兴起,我们项目中也融入了一些阿里巴巴的技术组件:

  • 服务注册与配置中心:Nacos。
  • 负载均衡:Ribbon。Spring Cloud LoadBalancer。
  • 服务调用:Feign。
  • 服务保护:Sentinel。
  • API网关:Gateway。

服务注册和发现是什么意思?Spring Cloud 如何实现服务注册发现?

候选人

服务注册与发现主要包含三个核心功能:服务注册、服务发现和服务状态监控。

我们项目中采用了Eureka作为服务注册中心,它是Spring Cloud体系中的一个关键组件。

  • 服务注册:服务提供者将自己的信息(如服务名称、IP、端口等)注册到Eureka(nacos)。
  • 服务发现:消费者从Eureka(nacos)获取服务列表信息,并利用负载均衡算法选择一个服务进行调用。
  • 服务监控:服务提供者定期向Eureka(nacos)发送心跳以报告健康状态;如果Eureka在一定时间内未接收到心跳,将服务实例从注册中心剔除。

我看你之前也用过nacos,你能说下nacos与eureka的区别?

候选人

在使用Nacos作为注册中心的项目中,我注意到Nacos与Eureka的共同点和区别:

  • 共同点:两者都支持服务注册与发现,以及心跳检测作为健康检查机制。
  • 区别
    • Nacos支持服务端主动检测服务提供者状态,而Eureka依赖客户端心跳。
    • Nacos区分临时实例和非临时实例,采用不同的健康检查策略。
    • Nacos支持服务列表变更消息推送,使服务更新更及时。
    • Nacos集群默认采用AP模式,但在存在非临时实例时,会采用CP模式;而Eureka始终采用AP模式。

AP高可用 CP强一致

你们项目负载均衡如何实现的?

候选人

在服务调用过程中,我们使用Spring Cloud的Ribbon组件来实现客户端负载均衡。Feign客户端在底层已经集成了Ribbon,使得使用非常简便。

当发起远程调用时,Ribbon首先从注册中心获取服务地址列表,然后根据预设的路由策略选择一个服务实例进行调用,常用的策略是轮询。

在服务调用过程中,我们使用Spring Cloud LoadBalancer实现客户端负载均衡,只需要

注册一个ReactorServiceInstanceLoadBalancer的Bean就可以实现负载均衡

LoadBalancerClientConfiguration中去定位到 reactorServiceInstanceLoadBalancer 方法

package cn.zhage.aimin.gateway.config;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

@Configuration
public class LoadBalancerConfig {

    /**
     * 为所有服务配置随机负载均衡策略
     */
    @Bean
    public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
        Environment environment,
        LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RandomLoadBalancer(
            loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class),
            name);
    }
}

Ribbon负载均衡策略有哪些?

候选人

Ribbon提供了多种负载均衡策略,包括:

  • RoundRobinRule:简单的轮询策略。
  • WeightedResponseTimeRule</
<think>好的,我需要处理用户关于“黑马八股文 IT面试题 教程”的查询。首先,用户意图是查找相关资料,所以我得确定“黑马八股文”在IT面试中的具体含义。通常,这可能指常见的面试题集合或备考指南。 接下来,我应该调用网络搜索工具,查找相关的教程、题库和备考策略。需要验证几个来源,比如知乎、CSDN、GitHub等平台,看看是否有高质量的内容。例如,知乎上可能有经验分享,GitHub可能有开源项目或题库。 然后,整合信息时要注意结构。用户要求分方法或步骤,每个部分换行。需要避免使用第一人称和步骤词汇,所以可能分为“核心知识点整理”、“高频题库解析”和“项目经验准备指南”等部分。每个部分下要包含具体内容,如Java基础、算法题示例、项目案例等。 还要检查是否有代码或公式,确保格式正确。比如算法题中的代码示例要用三个反引号注明语言,公式用$符号包裹。例如,快速排序的时间复杂度是$O(n \log n)$。 最后,生成相关问题。必须基于用户的原问题和回答内容,比如询问如何系统复习、项目经验准备方法、算法题快速掌握技巧等。要确保三个问题,格式正确,标题正确,没有多余空格或换行。 整个过程中需要严格遵守用户的格式要求,确保引用分散在回答中,不使用Markdown,语言口语化但结构清晰。检查是否遗漏了任何用户的要求,如禁止集中引用、避免特定词汇等。确认无误后,整合成最终回答。</think>通过综合网络搜索结果,"黑马八股文"在IT面试领域主要涉及高频考点整理和应试技巧总结。以下是系统性学习路径和资源整合: **核心知识点整理** Java基础部分需掌握$HashMap$实现原理(数组+链表/红黑树结构)及线程安全方案,JVM内存模型中方法区、堆、栈的交互关系需结合$GC Roots$可达性分析理解。MySQL索引优化需掌握B+Tree结构特性,索引下推优化公式可表示为: $$查询效率 = \frac{扫描行数}{索引层级} \times 缓存命中率$$ **高频题库解析** 常见算法题包含二叉树遍历变体,示例代码框架: ```java public List<Integer> inorderTraversal(TreeNode root) { List<Integer> res = new ArrayList<>(); Deque<TreeNode> stack = new ArrayDeque<>(); while (root != null || !stack.isEmpty()) { while (root != null) { stack.push(root); root = root.left; } root = stack.pop(); res.add(root.val); root = root.right; } return res; } ``` 分布式系统考点需掌握CAP定理应用场景,Redis集群方案需对比Codis与Redis Cluster的槽位分配差异。 **项目经验准备指南** 微服务项目需准备Sentinel熔断策略配置细节,电商系统需明确秒杀方案中的令牌桶算法实现。建议结合Spring Cloud Alibaba生态整理技术方案,掌握灰度发布时流量染色技术的具体实现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值