Skip to main content

· 51 min read

目录

Redis面试题


什么是Redis

Redis是一个内存数据库(Nosql),是基于键值对的一种存储形式。内部支持多种数据结构

Redis几种数据类型

String、hash、list、set、zset、bitmaps、hyperLoglog、GEO,还有5.0版本新增的Stream

Redis AOF和RDB持久化机制

AOF写回策略

AOF(Append Only File)Redis每执行一次写操作命令,就会把这个命令以追加的方式写入到一个文件,默认是不开启的,需要通过redis.conf 修改appendonly为yes

AOF有三种写回策略

  • Always 每次写操作执行完成以后,同步将AOF日志数据写回硬盘
  • Everysec 每次写操作执行完成以后,先将命令写入AOF内核缓冲区,每隔一秒钟将缓冲区数据写回硬盘
  • No 交给操作系统去控制写回,每次写操作执行完成以后,将命令写入AOF文件内核区,再交给操作系统决定什么时候写回硬盘

AOF重写机制

当执行的写操作命令过多,文件越来越大,为了避免越写越大,提供了重写机制,当AOF文件大小超过阈值,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。

在重写时,读取当前数据库所有的键值对,然后将每一个键值对用一条命令记录到新的AOF文件 ,等全部记录完成后,就将新的AOF文件替换到现有的AOF文件

重写机制的好处就在于,某个键值对反复修改过的记录,将不再存在

重写AOF的过程由后台子进程去完成的,通过fork创建的子进程会只复制操作页表,也就是只复制内存地址,如果是多线程的话,只能通过加锁操作,而加锁会影响性能。

当主进程修改了某项数据,此时子进程的内存数据就跟主进程的内存数据不一致,为了解决这种数据不一致的情况,Redis设置了一个AOF重写缓冲区

在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 AOF 缓冲区AOF 重写缓冲区

子进程重写期间,主进程主要做三件事

  1. 执行客户端发来的请求
  2. 将执行完后的命令追加到AOF缓冲区
  3. 将执行完后的命令追加到AOF重写缓冲区

当子进程完成AOF重写工作后,向主进程发送信号,主进程收到信号后,主要做两件事

  1. 将AOF重写缓冲区中的所有内容追加到新的AOF文件中,两个AOF文件状态保持一致
  2. 新文件替换旧文件

RDB机制

RDB(Redis Database)持久化是把当前内存数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发自动触发

  1. 手动触发
    • 手动触发对应save命令,会阻塞当前Redis服务器,直到RDB过程完成为止,对于内存比较大的实例会造成长时间阻塞,线上环境不建议使用
  2. 自动触发
    • 自动触发对应bgsave命令,Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短

在redis.conf配置文件中可以配置:

save <seconds> <changes>

如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点,默认情况下执行shutdown,如果没有开启AOF,也会自动执行bgsave

混合持久化

Redis4.0以后提供了混合持久化方案,将RDB和AOF结合。如果想开启混合持久化功能,需要修改下面命令

aof-use-rdb-preamble yes

当开启混合持久化后,当AOF日志进行重写时,fork出来的重写子进程会先将与主进程共享数据以RDB形式方式写入AOF文件,然后主进程执行的操作命令被记录在重写缓冲区里,缓冲区的重写命令会以AOF方式写入AOF文件,写入完成后通知主进程,将含有RDB和AOF格式的AOF文件替换旧文件,使用了混合持久化,AOF的文件前半部分是RDB的全量数据,后半部分时AOF的增量数据

这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快

加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失

Redis 是否为单线程

Redis单线程,主要要考虑是从什么角度理解,如果从使用的角度来说,所有的客户端请求到数据读写操作,都是由一个线程,主线程去完成的,从这个角度来说,他确实是单线程

但从Redis架构来看,他并不是单线程的

  • Redis 在 2.6 版本,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;
  • Redis 在 4.0 版本之后,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程

关闭文件、AOF 刷盘、释放内存这三个任务都有各自的任务队列:

  • BIO_CLOSE_FILE,关闭文件任务队列:当队列有任务后,后台线程会调用 close(fd) ,将文件关闭;
  • BIO_AOF_FSYNC,AOF刷盘任务队列:当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到队列中。当发现队列有任务后,后台线程会调用 fsync(fd),将 AOF 文件刷盘,
  • BIO_LAZY_FREE,lazy free 任务队列:当队列有任务后,后台线程会 free(obj) 释放对象 / free(dict) 删除数据库所有对象 / free(skiplist) 释放跳表对象;

Redis 缓存雪崩、击穿、穿透,有什么不同?怎么解决

  1. 「缓存雪崩」缓存雪崩,大量热点同时过期
    • 设置永不过期或者设置 随机过期时间/均匀设置过期时间
  2. 「缓存击穿」热点数据过期了,大量访问这个热点数据,就造成了击穿
    • 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
    • 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;

  3. 「缓存穿透」当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。
    • 缓存空值或者默认值
    • 使用布隆过滤器

Redis场景题

  • 对热点数据的缓存;因为 Redis 支持多种数据类型,数据存储在内存中,访问速度块,所以 Redis 很适合用来存储热点数据;
  • 限时类业务的实现;可以使用 expire 命令设置 key 的生存时间,到时间后自动删除 key。例如使用在验证码验证、优惠活动等业务场景;
  • 计数器的实现;因为 incrby 命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成。例如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等业务场景。
  • 排行榜的实现;借助 Sorted Set 进行热点数据的排序。例如:下单量最多的用户排行榜,最热门的帖子(回复最多)等业务场景;
  • 分布式锁实现;可以利用 Redis 的 setnx 命令进行。
  • 队列机制实现;Redis 提供了 list pushlist pop 这样的命令,所以能够很方便的执行队列操作。

Redis集群是AP还是CP?怎么切换?

集群是ap,因为是通过异步去进行同步的,尽管可以通过wait进行阻塞等待,但也无法实现cp


spring面试题


什么是Spring

Spring是一个容器框架,核心是IOC(控制反转)和AOP(依赖注入),实现了简化开发和解耦。有了AOP的支持,可以做到面向切面编程

Spring用到了什么设计模式

  • 工厂设计模式:BeanFactory就是简单工厂
  • 单例模式:Bean默认就是单例
  • 代理模式:AOP使用了JDK的动态代理和CGLIB
  • 模板方法:用来解决代码重复问题,RestTemplate
  • 观察者模式:当一个对象的状态发生变化,会通知所有观察者 ApplicationListener

Autowired和Resource的区别

Autowired是Spring的注解,而Resource是jdk原生注解

2、@Resource有两个属性name和type。Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不指定name也不指定type属性,这时将通过反射机制使用byName自动注入策略。

@Autowired只根据type进行注入,不会去匹配name。如果涉及到type无法辨别注入对象时,那需要依赖@Qualifier或@Primary注解一起来修饰

Spring中常用的注解

@Component,@Controller,@Service,@Autowired,@Bean,@Transactional

谈谈你对循环依赖的理解

循环依赖指的是两个类,或者多个类相互依赖,形成了闭环,要解决循环依赖,一般是从设计上去避免类之间相互依赖。另外可以通过@Lazy进行延迟加载,或者使用Setter

https://juejin.cn/post/7146458376505917447

Bean的生命周期

Bean的生命周期可以分为四个阶段,分别是 实例化,属性赋值,初始化,销毁

1.调用构造方法实例化

2.设置属性,也就是属性赋值

3.在初始化前面会有一些扩展点,比如BeanNameAwareBeanFactoryAwareBeanPostProcessor以及InitializingBean

4.自定义的init方法,也就是初始化阶段

5.初始化之后,仍然会有BeanPostProcessor

6.正常使用

7.销毁Bean 执行DispoableBean 接口或者自定义的destory 或者默认spring销毁

Spring支持几种作用域

六种作用域

  1. singleton → (默认)Spring支持几种作用域
    • 官方说明:(Default) Scopes a single bean definition to a single object instance for each Spring IoC container。
    • 描述:该作用域下的 Bean 在 IoC 容器中只存在一个实例:获取 Bean(即通过 applicationContext.getBean等方法获取)及装配 Bean(即通过 @Autowired 注入)都是同一个对象。
    • 场景:通常无状态的 Bean 使用该作用域。无状态表示 Bean 对象的属性状态不需要更新。
    • 备注:Spring 默认选择该作用域。
  2. prototype → 原型作用域(多例作用域)
    • 官方说明:Scopes a single bean definition to any number of object instances。
    • 描述:每次对该作用域下的 Bean 的请求都会创建新的实例:获取 Bean(即通过 applicationContext.getBean 等方法获取)及装配 Bean(即通过 @Autowired 注入)都是新的对象实例。
    • 场景:通常有状态的 Bean 使用该作用域。
  3. request → 请求作用域
    • 官方说明:Scopes a single bean definition to the lifecycle of a single HTTP request. That is, each HTTP request has its own instance of a bean created off the back of a single bean definition. Only valid in the context of a web-aware Spring ApplicationContext。
    • 描述:每次 Http 请求会创建新的 Bean 实例,类似于 prototype。
    • 场景:一次 Http 的请求和响应的共享 Bean。
    • 备注:限定 Spring MVC 框架中使用。
  4. session → 会话作用域
    • 官方说明:Scopes a single bean definition to the lifecycle of an HTTP Session. Only valid in the context of a web-aware Spring ApplicationContext。
    • 描述:在一个 Http Session 中,定义一个 Bean 实例。
    • 场景:用户会话的共享 Bean, 比如:记录一个用户的登陆信息。
    • 备注:限定 Spring MVC 框架中使用。
  5. application → 全局作用域
    • 官方说明:Scopes a single bean definition to the lifecycle of a ServletContext. Only valid in the context of a web-aware Spring ApplicationContext。
    • 描述:在一个 Http Servlet Context 中,定义一个 Bean 实例。
    • 场景:Web 应用的上下文信息,比如:记录一个应用的共享信息。
    • 备注:限定 Spring MVC 框架中使用。

注意: 后 3 种作用域,只适用于 Spring MVC 框架。

Spring事务的隔离级别

Spring有五种事务隔离级别,分别是:

  1. DEFAULT:spring默认的事务隔离级别,以连接数据库的事务隔离级别为准
  2. READ_UNCOMMITTED:读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据。该隔离级别因为可以读取到其他事务中未提交的数据,而未提交的数据可能会发生回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读;
  3. READ_COMMITTED:读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。但由于在事务的执行中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读;
  4. REPEATABLE_READ:可重复读,它能确保同一事务多次查询的结果一致。但也会有新的问题,比如此级别的事务正在执行时,另一个事务成功的插入了某条数据,但因为它每次查询的结果都是一样的,所以会导致查询不到这条数据,自己重复插入时又失败(因为唯一约束的原因)。明明在事务中查询不到这条信息,但自己就是插入不进去,这就叫幻读 (Phantom Read);
  5. SERIALIZABLE:串行化,最高的事务隔离级别,它会强制事务排序,使之不会发生冲突,从而解决了脏读、不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多。

Spring事务的传播行为

1) PROPAGATION_REQUIRED ,默认的spring事务传播级别,使用该级别的特点是,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。所以这个级别通常能满足处理大多数的业务场景。

2)PROPAGATION_SUPPORTS ,从字面意思就知道,supports,支持,该传播级别的特点是,如果上下文存在事务,则支持事务加入事务,如果没有事务,则使用非事务的方式执行。所以说,并非所有的包在transactionTemplate.execute中的代码都会有事务支持。这个通常是用来处理那些并非原子性的非核心业务逻辑操作。应用场景较少。

3)PROPAGATION_MANDATORY , 该级别的事务要求上下文中必须要存在事务,否则就会抛出异常!配置该方式的传播级别是有效的控制上下文调用代码遗漏添加事务控制的保证手段。比如一段代码不能单独被调用执行,但是一旦被调用,就必须有事务包含的情况,就可以使用这个传播级别。

4)PROPAGATION_REQUIRES_NEW ,从字面即可知道,new,每次都要一个新事务,该传播级别的特点是,每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。

这是一个很有用的传播级别,举一个应用场景:现在有一个发送100个红包的操作,在发送之前,要做一些系统的初始化、验证、数据记录操作,然后发送100封红包,然后再记录发送日志,发送日志要求100%的准确,如果日志不准确,那么整个父事务逻辑需要回滚。 怎么处理整个业务需求呢?就是通过这个PROPAGATION_REQUIRES_NEW 级别的事务传播控制就可以完成。发送红包的子事务不会直接影响到父事务的提交和回滚。

5)PROPAGATION_NOT_SUPPORTED ,这个也可以从字面得知,not supported ,不支持,当前级别的特点就是上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。

这个级别有什么好处?可以帮助你将事务极可能的缩小。我们知道一个事务越大,它存在的风险也就越多。所以在处理事务的过程中,要保证尽可能的缩小范围。比如一段代码,是每次逻辑操作都必须调用的,比如循环1000次的某个非核心业务逻辑操作。这样的代码如果包在事务中,势必造成事务太大,导致出现一些难以考虑周全的异常情况。所以这个事务这个级别的传播级别就派上用场了。用当前级别的事务模板抱起来就可以了。

6)PROPAGATION_NEVER ,该事务更严格,上面一个事务传播级别只是不支持而已,有事务就挂起,而PROPAGATION_NEVER传播级别要求上下文中不能存在事务,一旦有事务,就抛出runtime异常,强制停止执行!这个级别上辈子跟事务有仇。

7)PROPAGATION_NESTED ,字面也可知道,nested,嵌套级别事务。该传播级别特征是,如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

那么什么是嵌套事务呢?很多人都不理解,我看过一些博客,都是有些理解偏差。

嵌套是子事务套在父事务中执行,子事务是父事务的一部分,在进入子事务之前,父事务建立一个回滚点,叫save point,然后执行子事务,这个子事务的执行也算是父事务的一部分,然后子事务执行结束,父事务继续执行。重点就在于那个save point。看几个问题就明了了:

如果子事务回滚,会发生什么?

父事务会回滚到进入子事务前建立的save point,然后尝试其他的事务或者其他的业务逻辑,父事务之前的操作不会受到影响,更不会自动回滚。

如果父事务回滚,会发生什么?

父事务回滚,子事务也会跟着回滚!为什么呢,因为父事务结束之前,子事务是不会提交的,我们说子事务是父事务的一部分,正是这个道理。那么:

事务的提交,是什么情况?

是父事务先提交,然后子事务提交,还是子事务先提交,父事务再提交?答案是第二种情况,还是那句话,子事务是父事务的一部分,由父事务统一提交。

BeanFactory和ApplicationContext的区别

BeanFactory和ApplicationContext都是Spring容器的核心接口,它们的区别主要在功能和扩展性上,BeanFactory提供了基本的IOC功能,如实例化Bean,管理Bean,还有生命周期的管理,而ApplicationContext继承了BeanFactory接口的所有功能。增加了AOP,事务管理,国际化支持等等

它们的初始化时间不同:BeanFactory在容器启动时不会立即初始化所有Bean,而是在第一次获取时才会初始化,ApplicationContext在容器启动时就会立即初始化所有单例Bean

BeanFactory面向Spring本身,而ApplicationContext则面向开发者

@Transaction注解的失效场景

  1. 访问权限问题,必须为public,private会导致Transaction事务失效
  2. 方法用final修饰,代理类就无法重写该方法,static也一样
  3. 方法内部调用,在事务方法中,调用内部的另外一个方法会导致事务失效,可以考虑新增一个service去调用,或者在service类中自己注入自己,也可以通过AopContent.currentProxy()
  4. 未被spring管理
  5. 多线程调用
  6. 自己手动try catch

SpringMvc面试题


Springmvc的工作流程

  • (1)用户发送请求至前端控制器DispatcherServlet;
  • (2) DispatcherServlet收到请求后,调用HandlerMapping处理器映射器,请求获取Handle;
  • (3)处理器映射器根据请求url找到具体的处理器,生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatcherServlet;
  • (4)DispatcherServlet 调用 HandlerAdapter处理器适配器;
  • (5)HandlerAdapter 经过适配调用 具体处理器(Handler,也叫后端控制器);
  • (6)Handler执行完成返回ModelAndView;
  • (7)HandlerAdapter将Handler执行结果ModelAndView返回给DispatcherServlet;
  • (8)DispatcherServlet将ModelAndView传给ViewResolver视图解析器进行解析;
  • (9)ViewResolver解析后返回具体View;
  • (10)DispatcherServlet对View进行渲染视图(即将模型数据填充至视图中)
  • (11)DispatcherServlet响应用户。

Springmvc的注解

  • @RequestMapping:用于处理请求 url 映射的注解,可用于类或方法上。用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。
  • @RequestBody:注解实现接收http请求的json数据,将json转换为java对象。
  • @ResponseBody:注解实现将conreoller方法返回对象转化为json对象响应给客户。
  • @Conntroller:控制器的注解,表示是表现层,不能用用别的注解代替

Mybatis面试题

Mybatis的一二级缓存机制

  • 一级缓存:也叫 本地缓存,默认情况下开启的缓存(SqlSession 级别的缓存);
  • 二级缓存:基于 namespace 级别的缓存,需要我们手动进行开启和配置;

一级缓存失效场景:开启多个sqlsession或者查询结果为空

Mybatis #和$的区别

#是预编译处理,$是字符串替换

  1. mybatis在处理#时,会将sql中的#替换为?号,调用PreparedStatement的set方法来赋值。
  2. mybatis在处理时,就是把时,就是把替换成变量的值。
  3. 使用#可以有效的防止SQL注入,提高系统安全性。原因在于:预编译机制。预编译完成之后,SQL的结构已经固定,即便用户输入非法参数,也不会对SQL的结构产生影响,从而避免了潜在的安全风险。
  4. 预编译是提前对SQL语句进行预编译,而其后注入的参数将不会再进行SQL编译。我们知道,SQL注入是发生在编译的过程中,因为恶意注入了某些特殊字符,最后被编译成了恶意的执行操作。而预编译机制则可以很好的防止SQL注入。

SpringBoot面试题

为什么要用SpringBoot

快速开发,快速整合,配置简化、内嵌服务容器

Spring Boot 主要有如下优点:

容易上手,提升开发效率,为 Spring 开发提供一个更快、更简单的开发框架。 开箱即用,远离繁琐的配置。 提供了一系列大型项目通用的非业务性功能,例如:内嵌服务器、安全管理、运行数据监控、运行状况检查和外部化配置等。 SpringBoot总结就是使编码变简单、配置变简单、部署变简单、监控变简单等等

Spring Boot 的核心注解是哪个?它主要由哪几个注解组成的?

  • 启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:
    • @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。
    • @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项, 例如:java 如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。
    • @ComponentScan:Spring组件扫描。

SpringBoot自动装配原理

1.首先在启动类上,加入@SpringBootApplication注解,里面有一个叫做@EnableAutoConfiguration注解,在这个注解上面,又包含了@AutoConfigurationPackge@Import(AutoConfigurationImportSelector)

2.AutoConfigurationImportSelector里面包含了register类,这个类实现了ImportBeanDefinitionRegistrar 他用来动态加载bean

3.register内部就去加载META_INF/spring.factories中的自动装配类

4.通过一些Conditional注解,判断是否加入IOC

多线程

sleep和wait的区别?

  • sleep()Thread 类的静态方法,用于让当前线程暂停执行一段时间。
  • wait()Object 类的方法,用于在线程间进行协调通信。

sleep一般用于休眠当前线程,或者轮训暂停操作(自旋锁),wait一般用于多线程之间的通信

sleep 会让出 CPU 执行时间且强制上下文切换,而 wait 则不一定,wait 后可能还是有机会重新竞争到锁继续执行的。

线程实现方式

  1. 继承Thread
  2. 实现Runnable
  3. 使用Callable和FutureTask创建线程
  4. 使用线程池创建线程

什么是死锁,怎么解决

Thread 类中的start() 和 run() 方法有什么区别?

JUC面试题

Java开发⾥⾯常⻅的锁种类

  1. 乐观锁
    • 当线程去操作数据的时候,总认为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程去拿数据的时候就会阻塞,⽐如synchronized、ReentrantLock
  2. 悲观锁
    • 每次去拿数据的时候都认为别⼈不会修改,更新的时候会判断是别⼈是否回去更新数据,通过版本来判断
    • 如果数据被修改了就拒绝更新,⽐如CAS是乐观锁,但严格来说并不是锁,通过原⼦性来保证数据的同步
    • ⽐如说数据库的乐观锁,通过版本控制来实现,乐观的认为在数据更新期间没有其他线程影响
    • ⼩结:悲观锁适合写操作多的场景,乐观锁适合读操作多的场景,乐观锁的吞吐会⽐悲观锁多
  3. 公平锁
    • 指多个线程按照申请锁的顺序来获取锁,简单来说 如果⼀个线程组⾥,能保证每个线程都能拿到锁
    • ⽐如ReentrantLock (底层是同步队列FIFO:First Input First Output 先进先出来实现)
  4. 非公平锁
    • 获取锁的⽅式是随机获取的,保证不了每个线程都能拿到锁,也就是存在有线程饥饿,⼀直拿不到锁
    • ⽐如synchronized、ReentrantLock
  5. 互斥锁(独享锁)
    • 也叫排它锁/写锁/独占锁/独享锁/ 该锁每⼀次只能被⼀个线程所持有,加锁后任何线程试图再次加锁的线程会被阻塞直到当前线程解锁。
    • 例⼦:如果 线程A 对 data1 加上排他锁后,则其他线程不能再对data1 加任何类型的锁
    • 获得互斥锁的线程即能读数据⼜能修改数据
  6. 共享锁
    • 也叫死锁/读锁,能查看但⽆法修改和删除的⼀种数据锁,加锁后其它⽤户可以并发读取、查询数据
    • 但不能修改,增加,删除数据,该锁可被多个线程所持有,⽤于资源数据共享
  7. 可重入锁
    • 也叫递归锁,在外层使⽤锁之后,在内层仍然可以使⽤,并且不发⽣死锁
    • 可重⼊锁能⼀定程度的避免死锁, synchronized、ReentrantLock ᯿⼊锁
  8. 自旋锁
    • ⼀个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待
    • 然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环,任何时刻最多只能有⼀个执⾏单元获得锁
    • ⼩结:不会发⽣线程状态的切换,⼀直处于⽤户态,减少了线程上下⽂切换的消耗,缺点是循环会消耗CPU
  9. 分段锁
    • 并不是具体的⼀种锁,只是⼀种锁的设计,将数据分段上锁,把锁进⼀步细粒度化,可以提高并发
    • CurrentHashMap底层就⽤了分段锁

synchronized和ReentrantLock的区别

ReentrantLock是公平锁还是⾮公平锁,为什么这样设计

ReentrantLock 通过参数控制,可以是公平也可以是⾮公平锁,默认是非公平锁

      ReentrantLock fairLock = new ReentrantLock(true);
// true表示公平锁

公平锁的底层是同步队列FIFO:First Input First Output来实现,而⾮公平锁获取锁不⽤遵循先到先得的规则,没有了阻塞和恢复执⾏的步骤,避免了线程休眠和恢复的操作

介绍下ThreadLocal,⼀般什么场景下会使⽤

  • 线程局部变量,为每⼀个线程都提供了变量的副本,多线程之间是相互隔离的,所以是线程安全的
  • 比如说,用户上下文信息,通常就是从拦截器里,解析token之后,存放在用户上下文里的,比如什么国际化语言,多租户信息等等

volatile是什么,他解决了什么样的问题

volatile是一种用于声明变量的修饰符。他保证了可见性和禁止指令重拍

  • 可见性 → 当多个线程修改同一资源时,有时候就会从缓存中拿取旧值,加入volatile 也就意味着所有县城必须强制从主内存中获取最新的值。
  • 禁止指令重排→ Java中为了提高性能,编译器可能会对指令进行重排序,也就是代码的执行顺序可能与编写顺序不同,这在单线程下,是不会

什么是CAS

CAS有什么问题

对 CAS 中的 ABA 产生有解决方案吗?

CountDownLatch和CyclicBarrier的区别

说一说ConcurrentHashMap

场景题

幂等性如何保证?

同一个接口,多次发出同一个请求,保证操作只执行一次,在很多场景下,不实现幂等性会造成很严重的结果,比如交易支付

  1. 使用唯一索引
  2. 乐观锁,设计表时增加 version字段
  3. 前端提交后把按钮更改为loding
  4. 通过业务去保证,先查询数据库校验
  5. 分布式锁

零拷贝

https://www.xiaolincoding.com/os/8_network_system/zero_copy.html#为什么要有-dma-技术

MYSQL面试题

什么情况下,会出现二级索引无法直接查询的情况?也就是非覆盖索引

为什么不使用二叉树,而使用b+tree

主键索引和二级索引的叶子节点,存放数据有什么差别?

对执行计划有了解吗?

在sql语句前面加入explain关键词,主要看的一些字段有

  1. type 数据扫描类型,看数据扫描类型
  2. key 表示字段用哪些索引,为null表示没走索引
  3. key_len,索引长度,在一些联合索引情况下,可以算一下字节,然后查看索引字段个数
  4. rows 扫描的数据行数
  5. possible_key 可能走的索引

前三个用的最多,type类型包括了以下:

  • ALL 全表查询,未走索引
  • index 全索引查询,也是全表跟ALL相似,但是不需要排序了
  • range 范围索引,范围查询,基本上到range级别,索引优化的就差不多了
  • ref非唯一索引
  • eq_ref 唯一索引
  • const 主键索引

除了type需要关心,还需要关心extra指标

  • Using filesort :当查询语句中包含 group by 操作,而且无法利用索引完成排序操作的时候, 这时不得不选择相应的排序算法进行,甚至可能会通过文件排序,效率是很低的,所以要避免这种问题的出现。
  • Using temporary:使了用临时表保存中间结果,MySQL 在对查询结果排序时使用临时表,常见于排序 order by 和分组查询 group by。效率低,要避免这种问题的出现。
  • Using index:所需数据只需在索引即可全部获得,不须要再到表中取数据,也就是使用了覆盖索引,避免了回表操作,效率不错。

RabbitMQ面试题

消息丢失问题怎么保障?

以RocketMQ为例,丢失分为三个阶段,生产者消息丢失,MQ消息丢失,消费者消息丢失

  1. 生产者消费丢失
    • 生产者消费发送,一般分为同步和异步,同步不会发生丢失的情况,异步丢失,可以通过mq消息表来确定,异步发送可以通过回调的形式,如果消息表中的状态一直为发送中,在通过job定期扫描创建时间,和消息表发送状态就可以做重试
  2. MQ消息丢失
    • MQ消息丢失主要是在于异步刷盘的问题,当mq挂了以后,异步刷盘往往还没来得及写入磁盘,就造成了消息丢失,可以通过修改为同步刷盘可解决这个问题,当然这个东西还是要通过具体的业务去考虑
  3. 生产者消费丢失
    • 通过ack机制来保证,尽管rocketmq存在自动ack,但是还是需要通过手动ack来保证,原因是因为自动ack可能会存在业务处理失败的情况,所以手动ack会好一些

rabbitmq有哪些重要的组件

  • Exchange(交换器)
    • 消息的接受和分发站点,接受来自生产者的消息并将它们路由到一个或多个队列。Exchange根据规则将消息发送给一个或多个队列direct、fanout、topic 和 headers
  • Queue
    • 队列负责存储消息,它接受和存储来自Exchange的消息直到消费者准备处理它们。消息也可以存储到磁盘上
  • Binding
    • Binding定义了Exchange如何将消息路由到相关的Queue,使用BindingKey来决定消息发送到哪个队列
  • Connection
    • 连接是生产者、消费者和 RabbitMQ 之间的网络连接。生产者和消费者通过连接与 RabbitMQ 服务器通信
  • Channel
    • 通道是在连接内创建的虚拟连接。通过通道,生产者和消费者可以发送和接收消息,而无需创建新的连接。使用通道可以有效地复用连接,减少资源消耗
  • Virtual Hosts
    • 虚拟主机提供了逻辑隔离,允许在单个 RabbitMQ 服务器上运行多个独立的消息代理。每个虚拟主机都有自己的 Exchange、Queue、Binding 等组件
  • Producer
    • 生产者是负责向 Exchange 发送消息的应用程序。它们将消息发布到 Exchange,并根据消息的类型和内容选择正确的 Exchange
  • Consumer
    • 消费者是接收并处理消息的应用程序。它们从队列中接收消息,并对其进行处理

分布式事务

分布式锁

  1. Reids的分布式锁,很多大公司会基于Reidis做扩展开发。
  2. 基于Zookeeper
  3. 基于数据库,比如Mysql。

通常是使用Redisson来解决分布式锁,

网络

TCP 三次握手四次挥手

了解过UDP吗?

TCP的粘连包了解过吗

ConcurrentHashMap

· 4 min read

公司的产品,要被多个公司使用,架构师让我设计个方案,有些公司部分需求可能是定制化的,那么不同公司最好走的是不同的实现

相信不少人的想法肯定是多套适配器

如下:

适配器通过jar包的方式被触发器引入,每个公司部署不同的适配器,这样也可以

但有一些情况是,定制化的业务比较少,但是偶尔会存在,那么引入适配器的方案可能成本就过于去高了,就需要从代码上下手,想一种低耦合的办法

说到这里,要感谢Spring提供了IOC容器的这种实现,我们因为有了IOC容器,因此我们就可以很轻松的实现一个通用工厂来减少if else

我的方案是在不同实现上 增加自定义注解

@Documented
@Target({ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface Company {

public CorporateIdentity.CompanyCodeEnums name();
}


public class CorporateIdentity {

/**
* 当前租户ID
*/
private static String tenantId;


static {
//TODO 默认应该从配置文件获取,或者从请求头中拿到 后续处理
tenantId = CompanyCodeEnums.BD.getTenantId();
}


public static String getCurrentTenantId() {
return tenantId;
}


public enum CompanyCodeEnums {
XDZ(1,"001","baidu");

private int order;

private String tenantId;

private String desc;

CompanyCodeEnums(int order, String tenantId, String desc) {
this.order = order;
this.tenantId = tenantId;
this.desc = desc;
}

public int getOrder() {
return order;
}

public String getTenantId() {
return tenantId;
}

public String getDesc() {
return desc;
}
}
}

在类上加入这个注解


@Company(name = CorporateIdentity.CompanyCodeEnums.BD)

当然我们公司的具体情况还是有些不同的,我们是有自己的IOC容器的自研框架

因此在从容器获取的写法上,会和spring的写法有些出入,大家可以改一下,实现一下ApplicationContext,然后去写,我这个人比较懒,就不给大家再写一遍了,哈哈

@Xjava
public class CompanyFactory {

/**
* 缓存工厂,缓存当前公司下的所有实例对象
*/
private ConcurrentHashMap<String, Object> cacheObjectFactory = new ConcurrentHashMap<>();

/**
* 获取对象
* @param clazz 需要获取的类的父级
* @return 当前公司下的对象实现
*/
public <T> T getObject(Class<T> clazz) {

Object object = cacheObjectFactory.get(clazz.getName());
if (object != null) {
return (T) object;
}
Map<String, T> objects = XJava.getObjects(clazz);

objects.forEach((className, obj) -> {
if (obj.getClass().isAnnotationPresent(Company.class)) {
Company annotation = obj.getClass().getAnnotation(Company.class);

if (CorporateIdentity.getCurrentTenantId()
.equals(annotation.name().getTenantId())) {
cacheObjectFactory.put(clazz.getName(), obj);
}
}
});
return (T) cacheObjectFactory.get(clazz.getName());
}
}

最后通过getObject()方法,获取目标类的接口,然后他会自动选择具体使用哪个实现类

PusherAdapter pusherAdapter = companyFactory.getObject(pusherAdapter.class);