1. SpringCloud常用组件有哪些?
最常用的5个组件是Nacos、Gateway网关、Feign远程调用、Sentinel熔断降级和Ribbon负载均衡。
其中Nacos是eureka功能的升级版,是阿里巴巴开发的,不仅仅可以作为注册中心,还可以作为配置中心。
1.1 注册中心
是服务实例信息存储的地方,是服务间相互交互数据的桥梁。
- 服务注册:所有的服务启动时,向注册中心登记服务信息(ip与端口),并定时发送心跳包给注册中心
- 服务发现:当注册中心长时间没收到服务的心跳包时,会把服务从列表中移除;当有新的服务注册时,更新服务列表。以上2种操作后,消费者从注册中心拉取最新列表更新到本地。
服务消费者在需要调用服务时,可以通过Nacos的服务发现功能获取服务提供者的地址列表,从而实现负载均衡和服务调用
1.2 配置中心
配置中心则是负责管理应用程序的配置信息,nacos提供了一个可视化的配置管理界面,支持多种格式的配置文件,支持动态配置管理,当配置信息发生变化时,Nacos会自动推送最新的配置信息给应用程序,从而实现配置的实时更新
1.3 网关Gateway
用于实现服务的路由、负载均衡、过滤和熔断等功能。采用了异步非阻塞的模型,采用Netty作为底层的HTTP库,提高了网关的吞吐量和性能。
网关中的过滤器:
- 默认过滤器(只能使用SpringCloudGateway自带的过滤器)。
- 路由过滤器(只能使用SpringCloudGateway自带的过滤器)。
- 全局过滤器(创建一个类实现GlobalFilter接口,重写filter方法.可用于鉴权)。
- 三种过滤器默认执行顺序(默认 -> 路由 -> 全局)全局过滤器实现Ordered接口,重写getOrder方法,返回-1即可实现在其他两个过滤器前执行(默认 和 路由 过滤器的Order默认是1开始的)。
1.4 Feign远程调用
- 定义接口:在Feign客户端的接口中定义需要调用的远程服务的方法,使用注解指定服务名称、请求方式、请求路径、请求参数,响应结果等信息。
- 解析接口:在应用启动时,Feign会扫描所有带有@FeignClient注解的接口,并将其解析成可执行的HTTP请求,生成动态代理对象并保存在Spring的容器中。
- 发起请求:当应用调用Feign客户端的接口方法时,Feign中依赖的ribbon组件会去获取@FeignClient注解中name属性的值,即服务名,通过这个服务名去找注册中心拉取服务提供者列表,缓存到本地。基于负载均衡的方式选择一个服务提供者,根据接口声明的方法上的请求路径,请求参数,请求方法,发送http请求。
- 响应结果:远程服务接收到请求后,会根据请求路径和请求参数执行相应的逻辑,并将结果封装成HTTP响应返回。Feign客户端接收到响应后,根据响应结果类型进行反序列化,并返回给应用程序。以接口和注解的方式,实现对HTTP请求的映射和调用。
底层原理:
Feign的底层实现基于动态代理和HTTP客户端,它将接口与HTTP请求/响应绑定在一起,通过注解配置的方式简化了服务调用的过程,提高了代码的可读性和可维护性。
1.5 Sentinel熔断降级
Sentinel是一款开源的服务治理组件,主要提供流量控制、熔断降级和系统负载保护等功能。熔断降级是指在分布式系统中,即当系统出现异常或不可用时,通过断路器的方式将请求快速失败,避免请求不断堆积,引起系统的崩溃(雪崩)。Sentinel在防止微服雪崩上有以下4种方案:
- 超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待。
- 舱壁模式:限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。
- 熔断降级:由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。
- 流量控制:限制业务访问的QPS,避免服务因流量的突增而故障。
1.6 Ribbon负载均衡
主要作用是在服务消费者与服务提供者之间进行负载均衡,将请求分发到不同的服务实例上,从而提高系统的可用性和性能。Ribbon通过向注册中心获取服务实例信息,并通过算法选择合适的实例进行请求转发,从而实现负载均衡。可以通过配置不同的负载均衡策略,实现不同的负载均衡方式,如轮询、随机、加权随机、最少活跃数。
2. Spring Bean的生命周期
bean的生命周期是指:Spring 容器在管理 Bean 时,会根据特定的顺序进行构造方法、依赖注入、初始化、执行业务逻辑等一系列操作,最终销毁 Bean。
-
实例化(构造方法):获取BeanDefinition,通过反射或工厂方法执行bean的构造方法。
-
属性注入:bean的属性值的依赖注入,如执行@Autowired, @Resource。
-
执行Aware的接口实现,主要有:
- BeanNameAware 设置bean对象名称
- BeanFactoryAware获取bean对象信息
- ApplicationContextAware暴露容器对象
-
执行postProcessBeforeInitialization方法,允许用户对bean对象属性注入后,对bean对象进行处理。
-
初始化:如果bean实现了InitializingBean接口,Spring容器将调用afterPropertiesSet()方法;如果在bean配置中声明了init-method属性(xml文件配置方式)或注解@PostContruct,则Spring容器将调用指定的方法。同时将bean对象存入单例池中(此时就可以使用bean了)。
-
执行postProcessAfterInitialization方法,允许在bean对象创建后再次进行处理。
-
销毁:如果bean实现了DisposableBean接口,Spring容器将在关闭应用程序上下文时调用destroy()方法;如果在bean配置中声明了destroy-method属性,则Spring容器将调用指定的方法。
【注意】:
单例bean,只有一个实例会在应用程序上下文中存在,并且在容器关闭时销毁;对于原型(多例prototype)bean,每次调用getBean()方法时都会创建一个新实例,不纳入spring管理,由用户自行管理,最终由垃圾回收器进行回收。
3. SpringMVC 执行流程
mvc的执行流程大致分为11步:
- 用户发送出请求到前端控制器DispatcherServlet。
- DispatcherServlet收到请求调用HandlerMapping(处理器映射器)。
- HandlerMapping找到具体的处理器(可查找xml配置或注解配置),生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet。
- DispatcherServlet调用HandlerAdapter(处理器适配器)。
- HandlerAdapter经过适配调用具体的处理器(Handler/Controller)。
- Controller执行完成返回ModelAndView对象。
- HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet。
- DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)。
- ViewReslover解析后返回具体View(视图)。
- DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)。
- DispatcherServlet响应用户。
4. Spring Boot自动装配原理
- 启动类上@SpringBootApplication注解。
- 底层用了3个核心注解,@SpringBootConfiguration声明当前类是一个配置类,@ComponentScan默认扫描启动类所在的包及其子包,@EnableAutoConfiguration开启自动配置。
- @EnableAutoConfiguration底层封装了@Import注解,指定了一个ImportSelector接口的实现类,低版本调用selectImports(),高版本调用getAutoConfigurationEntry(),读取当前项目下所有依赖jar包中META-INF/spring.factories等两个文件里面定义的配置类。
- 在配置类中定义一个@Bean标识的方法,还定义了@Conditional开头的注解,条件如果满足,Spring会自动调用配置类中@Bean标识的方法,并把方法的返回值注册到IOC容器中。
5. Sql优化(索引类型、失效场景、执行计划)b+tree底层
5.1 sql优化的方法有
- 执行计划:是SQL在数据库中执行时的表现情况,通常用于SQL性能分析,优化等场景。在MySQL使用 explain 关键字来查看SQL的执行计划。
- 如何查看sql执行计划:在查询语句前加explain关键字。
explain的作用:
- 查看表的读取顺序
- 查看数据库读取操作的操作类型
- 查看哪些索引有可能被用到
- 查看哪些索引真正被用到
- 查看表之间的引用
- 查看表中有多少行记录被优化器查询
-
字段分析:
- 1.id:select 查询的序列号,包含一组数字,表示查询中执行Select子句或操作表的顺序(id值相同:执行顺序由上而下;id值不同:id值越大优先级越高)。
- 2.select_type:表示SELECT语句的类型。
- 3.table:显示这查询的数据是关于哪张表的。
- 4.type:区分索引,这是重要的列,显示连接使用了何种类型。从最好到最差的连接类型为:system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL;一般来说,得保证查询至少达到range级别,最好能达到ref,到index就可以。
- 5.possible_keys:指出MySQL能使用哪个索引在该表中找到行。如果是空的,就是没有相关的索引。
- 6.key:实际使用到的索引。如果为NULL,则没有使用索引。
- 7.key_len:最长的索引宽度。如果键是NULL,长度就是NULL。在不损失精确性的情况下,长度越短越好。
- 8.ref:显示使用哪个列或常数与key一起从表中选择行。
- 9.rows:显示MySQL认为它执行查询时必须检查的行数。
- 10.Extra:执行状态说明,该列包含MySQL解决查询的详细信息。
其中4.type是重点
-
优化
- SELECT语句务必指明字段名称(避免直接使用select * )。
- SQL语句要避免造成索引失效的写法。
- SQL语句中IN包含的值不应过多。
- 当只需要一条数据的时候,使用limit 1。
- 如果排序字段没有用到索引,就尽量少排序。
- 如果限制条件中其他字段没有索引,尽量少用or。
- 尽量用union all代替union。
- 避免在where子句中对字段进行null值判断。
- 不建议使用%前缀模糊查询。
- 避免在where子句中对字段进行表达式操作。
- Join优化 能用innerjoin 就不用left join right join,如必须使用 一定要已小表为驱动 A left join B。
-
其中常见索引的类型:普通索引、唯一索引、主键索引、复合索引
-
失效场景:
当索引的列出现这些操作时会导致索引失效:
- 使用函数或表达式,如left(索引列,4)
- 使用 NOT 或 != 操作符、使用or操作符
- 对索引列进行运算(where 索引列+3>?)
- like模糊查询前置%
- 使用复合索引时(必须要用到第一个列,索引才会生效。即最左前缀原则)
- b+树的底层:
B+树是一种多路平衡查找树,它采用平衡树的思想,能够高效地支持数据的CRID操作。
B+Tree中所有数据都存储在叶子节点中,叶子节点之间通过指针连接形成链表,便于范围查询;非叶子节点只存储索引,不存储实际数据,使得非叶子节点能够存储更多的索引,一个非叶子节点可以储存的数据为16kb,一个key一般为8字节,指针一般是6个字节,所以一个非叶子节点大概就可以储存1000条数据,第二层就可以达到百万条数据,第三层就达到了千万条数据,所以b+树一般是2-4层。
6. SpringIOC与AOP
IOC:把对象的创建与销毁都交给spring来管理,当要用的时候只需从它的容器里获取即可。Spring的IOC通常指IOC容器与DI注入。
IOC容器: Spring创建的bean对象都放到一个容器,这个容器也称为IOC容器,我也称它为对象共享池。
DI:依赖注入也称为属性注入。如果对象都是由spring来管理的,用的时候也可以通过DI进入依赖注入即可。常用的DI包括以下:
- setXXX方法
- @Autowired (不推荐):Spring的注解,通过类型匹配,如果找不到则报错,如果这个类型下有多个该类型对象时,则按属性名匹配,如果属性名与beanName对不上,则报错,需要通过@Qualifier来指定beanName。
- @Resource(推荐):java标准注解,优先通过指定的beanName注入,效率较@Autowired高。当不指定beanName时,默认使用属性名,如果属性名没到到则以类型方式匹配注入
- 构造方法(推荐)
AOP:
- 面向切面编程,在不修改原有代码的基础上对目标方法进行增强。
- Spring实现AOP的底层原理包含2种:
- JDK动态代理,针对接口时使用
- CGLIB,以子类继承父类的方式,重写父类方法来增强。
- AOP的概念术语:切面=切入点(匹配的目标对象方法)+通知(对方法实现增强的那部分代码)。
- Spring实现通知的类型有5种:
- Before前置通知
- After后置通知
- After-Throwing异常通知
- After-Returning最终通知
- Around-环绕通知:可实现以上4种通知
- 使用场景如:spring的事务管理、封装管理日志的保存、权限控制、异常处理...等。
7. Java中的线程池
7.1 什么是线程池及它的作用
线程池是一种管理与复用线程的机制。主要的作用如下:
- 降低资源消耗。通过重复利用已创建线程来降低线程创建与销毁时所带来的资源消耗。线程的创建与销毁需要耗费比较多的系统资源。
- 提高响应速度。任务提交后,可以无需等待线程的创建就能立即执行。
- 提高线程的可管理性。无限的创建销毁不仅消耗系统资源还会降低系统稳定性,使用线程池可以进行统一的分配、调优与监控。
7.2 为什么创建线程会消耗比较多的资源或成本比较高
java创建成本高是因为jvm底层需要进行大量的工作:
- 为线程堆栈分配和初始大量内存块(1个线程栈默认占用1M的内存空间)。
- 需要进行系统调用(jvm本地方法调用操作系统api),以便在主机OS中创建/注册本机线程。
- 描述符需要创建、初始化并添加到jvm内部数据结构中。
jvm需要对以上过程中所创建的堆栈内存对象的访问、JVM线程的描述符、OS本机线程线程的描述符进行绑定,当销毁时,需要对以上资源的解绑,释放与恢复。
描述符:定义对某种对象类型的属性/方法的访问方式。
7.3 线程池的运行流程或工作原理
- 线程池新创建时,并不会马上创建线程,当有任务提交时才会去创建。
- 当提交一个任务时,如果存在空闲线程,则会分配一个空闲的线程来执行任务。
- 如果没有空闲的线程时,则先判断存活的线程数是否超过核心线程数,如果没超过,则创建线程并执行任务。
- 如果存活线程数超过核心线程数,则判断工作队列是否已满,如果未满则将任务存入工作队列中等待空闲线程处理。
- 如果工作队列已满,则判断存活线程数是否超过最大线程数,未超过则创建临时线程并执行任务,如果存活线程数超过最大线程数,则执行拒绝策略。
- 关闭线程池:调用shutdown()或shutdownNow()方法关闭线程池,线程池会拒绝接受新的任务并尝试停止已有的任务执行,同时会等待所有线程执行完毕并释放资源 shutdown()是会等待所有已经提交的任务执行完成,再关闭。 shutdownNow()则是让线程池会立即停止所有正在执行的任务,并尝试中断所有处于等待状态的线程,同时返回一个未执行的任务列表。
7.4 线程池的分类
- 手工创建,通过new ThreadPoolExecutor方式创建线程池。
- 通过jdk自带的4种:
- FixedThreadPool:固定大小线程池
- CachedThreadPool缓存线程池
- SingleThreadPool单线程池
- ScheduledThreadPool定时线程池
7.5 线程池的主要参数
- corePoolSize:核心线程数大小
- maximumPoolSize:线程池最大线程数
- keepAliveTime:线程空闲时间
- unit:keepAliveTime的时间单位
- workQueue:任务队列
- threadFactory:线程工厂
- RejectHandler:拒绝策略
7.6 线程池的4种拒绝策略
- CallerRunsPolicy:在任务被拒绝添加后,会在调用者线程中直接执行被拒绝的任务。
- DiscardPolicy:直接丢弃被拒绝的任务,不做任何处理。
- AbortPolicy:直接抛出 RejectedExecutionException 异常。
- DiscardOldestPolicy:将最早被放入等待队列的任务丢弃,然后将新任务加入等待队列)。
7.7 线程池状态
- RUNNING:线程池处于正常运行状态,可以接受新的任务并处理已有的任务。
- SHUTDOWN:线程池不再接受新的任务,但是会处理完队列中已有的任务。
- STOP:线程池不再接受新的任务,并且会尝试中断正在执行的任务。
- TIDYING:线程池中的所有任务都已经执行完毕,正在进行资源回收和清理工作。
- TERMINATED:线程池已经被完全终止,不再接受新的任务并且已经释放所有的资源。
通过ThreadPoolExecutor类的getPoolSize()方法获取当前线程池中的线程数量;
通过getActiveCount()方法获取正在执行任务的线程数量;
通过getTaskCount()方法获取已经提交但还未执行的任务数量;
通过shutdown()、shutdownNow()等方法来改变线程池的状态,以达到暂停或终止线程池的目的。
7.8 线程池核心数的设置
基础知识 CPU、L1/L2/L3 缓存、内存、硬盘/网络的执行速度
1秒=1000毫秒=1000000微秒=1000000000纳秒
1s=10^3ms=10^6μs=10^9ns
名称 |
速度计量单位 |
CPU |
0.几纳秒 |
L1 |
0.几纳秒 |
L2 |
10-20纳秒 |
L3 |
20-30纳秒 |
物理内存 |
微秒(CPU操作) |
硬盘(机械) |
10几毫秒 |
网络 |
几十到几百毫秒 |
线程任务类型分为2种
CPU密集型: 进行大量的计算,消耗CPU资源,比如计算圆周率、视频高清解码、数据的加解密等。这种任务操作都是比较耗时间的操作,任务越多花在任务切换的时间就越多,CPU执行任务效率就越低。所以,应当减少任务时切换的时间,即理想状态下,一个CPU处理一个线程时就不需要切换线程了,CPU密集型任务同时进行的数量应当等于CPU的核心数。
IO密集型:以数据流的传输、存取为主的操作,如:网络(调用三方接口)、磁盘IO(文件操作)等。这类任务操作是CPU消耗较少,任务大部分时间都在等待IO操作完成(IO的速度远低于CPU和内存的速度)。对于这种任务,任务越多,CPU效率越高(把等待的时间用来处理其它任务),但是也有限度。
咱们开发接口时,像调别的应用接口、数据库操作、文件操作等,基本上都是属于IO密集型任务。
核心线程数的设置
所以咱们在设置核心线程数时,得根据这2种类型来选择。而IO密集型还有一种情况是线程易阻塞型的,需要计算阻塞系数,他是这么配置线程核心线程数的:CPU核数 / 1 – 阻塞系数(0.8~0.9之间)所以:
CPU密集型:核心线程数=cpu核心数+1
IO密集型:核心线程数=cpu核心数*2
java中获取CPU核心数: Runtime.getRuntime().availableProcessors()
7.9 项目中是怎么创建使用线程池的?
- 开启异步使用 启动类上添加@EnableAsync。
- 注册线程池 在配置文件中通过@Bean(name = xxx)进行配置。
- 通过new ThreadPoolTaskExecutor来创建线程池
- 设置核心线程数
- 设置最大线程数
- 设置等待队列容量
- 设置临时线程活跃时间
- 设置默认线程名称前缀
- 设置拒绝策略
- 设置 等待所有任务结束后再关闭线程池
- 添加@Async(value=xxx线程池bean对象名称)注解。
7.10 为什么不使用JDK自带的线程池?
JDK自带的4种线程池,在高并发下可能出问题:
线程池创建方式 |
缺点 |
Executors.fixedThreadPool固定大小线程池 |
所创建的等待队列任务最大值为Integer.MAX_VALUE,并高发时,可能导致内存溢出 |
Executors.cachedThreadPool缓存线程池 |
所使用的等待队列为SynchronousQueue(队列不存储任务),最大线程数为Integer.MAX_VALUE。在高并发下创建过多的线程而导致内存溢出 |
Executors.SingleThreadPool单线程池 |
同fixedThreadPool |
Executors.scheduledThreadPool定时线程池 |
最大线程数为Integer.MAX_VALUE,临时线程存活时长为0ns(0纳秒,即超出核心线程数时,会经常创建与销毁线程),等待队列上限为Integer.MAX_VALUE。 |
7.11 使用线程池时需要注意哪些问题?
- 线程池默认使用无界队列,任务过多导致OOM。
- 线程池创建线程过多,导致oom。
- 共享线程池使用时,次要逻辑拖垮主要逻辑。
- 线程池拒绝策略使用不当,导致阻塞。当使用DiscardPolicy或DiscardOldestPolicy并且在被拒绝任务时,使用submit返回的future对象调用了get方法时,由于任务被放弃而不会执行,所以永远不会有get结果返回。
- 单独使用@Async时,spring内置的线程池SimpleAsyncTaskExecutor 不复用线程,即每次都是创建新的线程去执行任务。
- 使用线程池时,没有使用自定义命名,或设置线程名称前缀。日志中不便区分是系统线程还是某个业务线程(可通过CustomizableThreadFactory来设置线程名称前缀)。
- 线程池参数设置不合理 如:核心线程数,导致效率不高。
- 线程池异常处理:submit提交异常没有处理,需要自己捕获。
- 线程池使用完后,没关闭。
- 线程池与ThreadLocal搭配时,可能导致线程复用问题。线程使用完后没有清理threadLocalMap里的内容,最终导致堆中内存OOM。
8. 多线程的三大特性?如何解决?线程有哪些状态?
8.1 三大特性是指
- 原子性:原子性是指一个操作是不可被中断的,要么执行成功,要么全部失败。在多线程中,如果多个线程同时访问同一份数据,可能会发生数据竞争的问题,导致数据不一致。使用synchronized关键字、Lock对象等方式可以保证原子性。
- 可见性:可见性是指一个线程修改的状态对其他线程是可见的。在多线程中,由于线程之间的数据共享,一个线程修改的变量可能对其他线程不可见,导致数据不一致的问题。使用volatile关键字可以保证可见性。
- 有序性:有序性是指程序执行的顺序按照代码的先后顺序执行。在多线程中,由于线程的交替执行,程序执行的顺序可能发生变化,导致程序出现错误。使用synchronized关键字、Lock对象等方式可以保证有序性。
8.2 线程的状态
- 新建(New):线程被创建但未启动;
- 运行(Runnable):线程正在执行;
- 阻塞(Blocked):线程被阻塞,等待某个条件的唤醒;
- 等待(Waiting):线程处于无限期等待状态,需要其他线程显式地唤醒;
- 计时等待(Timed Waiting):线程处于有限期等待状态,等待一定时间后自动唤醒;
- 终止(Terminated):线程执行完毕或因异常退出。
9. 如何优雅的退出线程?
- 设置退出标志:在程序需要退出时,可以设置一个标志位,标识线程需要退出。【推荐】:可记录到业务具体执行到哪个步骤,还有哪些没执行等,以便后续启动后可继续执行下去。
- 中断阻塞:对于正在阻塞的线程,可以通过调用 interrupt() 方法中断线程的阻塞状态,使其抛出 InterruptedException 异常,从而退出阻塞。
- 等待线程结束:对于正在运行的线程,可以调用 join() 方法等待其结束。这个方法会阻塞当前线程,直到目标线程结束。
- 清理资源:线程结束后,需要清理线程所占用的资源,如关闭文件、网络连接等。
10. HashMap
- Hashmap是Java中的一个数据结构,可以实现键值对的存储和查询,它基于哈希表实现储存结构。初始大小为16,扩容因子为0.75,每次扩容2倍。扩容阈值:当容器中的数据数量size>=数组长度扩容因子时,扩容后数组长度为原有长度的2倍。
- JDK1.7及以前的版本使用的是数组+链表的结构1.7使用的是拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值通过头插法加入链表,1.8是尾插法。
- JDK1.8之后使用的是数组+链表+红黑树的结构jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8) 时且数组长度大于等64,将链表转化为红黑树,以减少搜索时间。扩容 resize( ) 时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表。
- 死循环问题在数组进行扩容的时候,因为1.7 链表是头插法,在多线程下进行数据迁移的过程中(扩缩容时),有可能导致死循环。在JDK1.8中将扩容算法做了调整,不再将元素加入链表头,而是使用尾插法,解决死循环问题。
对于死循环问题,还可以这样答,HashMap本身就是线程不安全的,不应在多线程下使用,如果不在多线程下使用,则不会出现死循环问题。
如果要使用线程安全的,可以使用HashTable、或Collections.synchronizedMap、ConcurrentHashMap(推荐)。
11. Java8大基础数据类型/5大数据库约束
Java基础数据类型:
- 整数型:byte占1个字节、short占2个字节、int占4个字节、long占8个字节。
- 浮点型:float 占4个字节、double占8个字节。
- 字符型:char 占2个字节。
- 布尔:boolean,字节型,占用1个字节。
包装类 全部占8个字节。
数据库有哪些约束:
主键约束、外键约束、唯一约束、非空约束、默认约束。
12. 拦截器和过滤器区别?执行流程?
12.1 区别
- 拦截器可以获取IOC容器中的各个bean,而过滤器就不行。
- 过滤器几乎对所有的请求都可以起作用,而拦截器只能对SpringMVC请求起作用。
- 请求先被过滤器拦截,再进入拦截器,最后进入控制层。
- 拦截器是Spring容器中的,而过滤器是tomcat的Servlet容器。
12.2 执行流程
- 过滤器:当一个客户端发送请求时,先被过滤器拦截处理,执行过滤器中的方法,如果通过了过滤器中的校验,则会继续处理该请求;否则,直接返回错误信息给客户端,中断后续操作。
- 拦截器:在请求到达控制器前,拦截器会先对请求进行处理,在拦截器中的preHandle()方法中,可以对请求进行拦截、处理等操作,如果拦截器放行,则继续进入控制器执行业务逻辑;在控制器执行完成后,会执行拦截器中的postHandle()方法和afterCompletion()方法进行处理。
- 在执行顺序上,过滤器在拦截器之前执行。
13. java中有哪些集合?有什么区别?
有list、set、map三种:
- List集合:List是一种有序的集合,可以存储重复的元素,常见的List集合有ArrayList、LinkedList和Vector。其中,ArrayList是基于数组实现的,支持随机访问和快速添加和删除元素;LinkedList是基于链表实现的,支持快速添加和删除元素,但是不支持随机访问;Vector是线程安全的List集合,但是性能相对较差。List集合通常用于存储需要保持顺序的数据。使用场景:存储需要保持顺序的数据,例如用户操作日志、消息队列等。实现栈、队列等数据结构,例如使用LinkedList实现队列、栈等。在集合中进行随机访问,例如需要通过索引获取某个元素。
- Set集合:Set是一种不允许重复元素的集合,常见的Set集合有HashSet、LinkedHashSet和TreeSet。其中,HashSet是基于哈希表实现的,元素的存储顺序不固定;LinkedHashSet是基于哈希表和链表实现的,元素的存储顺序与添加顺序一致;TreeSet是基于红黑树实现的,元素会按照自然顺序或者指定的比较器进行排序。Set集合通常用于去重和判断某个元素是否存在。使用场景:去重操作,例如过滤重复的数据、统计数据中不同元素的个数等。判断某个元素是否存在,例如判断用户是否已经登录、统计数据中某个元素出现的次数等。在使用迭代器遍历集合时,避免重复遍历相同的元素。
- Map集合:Map是一种键值对的集合,常见的Map集合有HashMap、LinkedHashMap和TreeMap。其中,HashMap是基于哈希表实现的,键值对的存储顺序不固定;LinkedHashMap是基于哈希表和链表实现的,键值对的存储顺序与添加顺序一致;TreeMap是基于红黑树实现的,键值对会按照键的自然顺序或者指定的比较器进行排序。Map集合通常用于快速查找和存储键值对的数据。使用场景:存储键值对的数据,例如存储用户信息、缓存数据、记录数据统计信息等。快速查找,例如根据键快速查找对应的值、查找最近更新的数据等。存储需要排序的键值对,例如使用TreeMap实现有序映射。
14. ThreadLocal 是一个什么样的技术?
14.1 ThreadLocal的实现原理
ThreadLocal通过在每个线程中维护一个ThreadLocalMap对象来实现线程隔离。ThreadLocalMap以ThreadLocal对象作为键,线程私有的变量副本作为值。每个线程都有自己的ThreadLocalMap,线程可以通过 ThreadLocal的 get()和set()方法来获取和设置自己线程的 ThreadLocal 变量的值。
14.2 应用场景
- 多线程环境下需要独立存储和获取数据的场景,例如线程池中的任务需要使用各自独立的数据库连接计数器等。
- 线程上下文传递信息,例如Web框架中将请求信息或用户登录信息存储在 ThreadLocal中,方便各层次方法调用时获取,避免了传递参数的麻烦。
14.3 坑与解决方法
14.3.1 内存泄漏问题
由于ThreadLocal的生命周期和线程的生命周期绑定,使用完ThreadLocal后,需要调用 remove()方法进行清理,避免内存泄漏。
解决方法
在使用完ThreadLocal后,在合适的地方调用remove()方法清理资源,可以使用try-finally语句块确保清理操作的执行,或者使用ThreadLocal的initialValue()方法设置初始值,这样在线程结束后会自动清理。
// 设置初始化方法
private static ThreadLocal<Object> threadLocal = ThreadLocal.withInitial(()-> {
return "初始化值,类型要与泛型相同";
});
// 使用完ThreadLocal后,调用remove()方法清理
threadLocal.remove();
14.3.2 共享变量问题
如果多个线程共享了同一个ThreadLocal变量,可能会导致数据错乱或不确定的结果。每个线程应持有自己的ThreadLocal变量实例。
解决方法: 对于需要在多个线程之间共享变量的情况,应该创建多个ThreadLocal实例,每个线程持有自己的实例。
14.3.3 线程池使用时注意
在使用线程池时,需要特别小心 ThreadLocal 的使用,避免由于线程的重用而导致 ThreadLocal数据的混乱。
解决方法: 使用线程池时,应避免使用ThreadLocal变量或者在使用前后显式清理ThreadLocal变量,确保每次任务执行时 ThreadLocal的状态是干净的。
总的来说,使用ThreadLocal时需要注意其生命周期、清理和共享的问题,合理使用并及时清理ThreadLocal,可以避免潜在的问题发生。
14.4 最简回答
ThreadLocal是一种 Java技术,它允许在多线程环境中维护线程私有的变量副本。底层实现会使用一个类似于 Map的结构来存储每个线程的变量副本。ThreadLocal并不是强引用或弱引用,它使用弱引用作为键来维护各个线程的变量副本,但变量本身由线程强引用。在使用ThreadLocal时,可能会出现内存泄漏的问题。如果线程结束了,但ThreadLocal中的变量没有被手动清理,那么该变量会一直存在于 ThreadLocal的 Map 中,导致内存泄漏。解决这个问题的常见方式是在使用完ThreadLocal后调用remove()方法将变量从ThreadLocal中移除,或者使用Java8中的ThreadLocal的InitialValue方法来提供默认值。另外,也可以使用ThreadLocal的弱引用方式来解决内存泄漏问题,例如使用InheritableThreadLocal。
15. Redis数据类型有哪些?雪崩、击穿、穿透?
15.1 数据类型
数据类型 |
底层数据结构 |
使用场景 |
String |
普通字符串,由单个char组成的集合。类型有embStr, int, raw |
用于缓存、计数器、分布式锁等场景 |
Set |
有序数组:存储的数据都为整数且数目不超过512个 散列数组:数据较大,满足不了有序数组后 |
可以进行集合运算,例如并集、交集、差集等操作常用于标签、好友列表等场景 |
Zset |
压缩列表6以后为Dict即HashTable:单个数据(字符串或其他)小于64字节,个数小于512个跳表:当数据过大,不满足以上2个条件后 |
可以按照分数排序,也可以进行范围查询。常用于排行榜、计分系统等场景 |
List |
压缩列表zipList: 列表中保存的单个数据(字符串或其他)小于64字节,列表中数据个数小于512个。双向循环链表: 当数据过大,不满足以上2个条件后。 |
可以进行队列和栈的操作常用于消息队列、任务队列等场景 |
Hash |
压缩列表zipList: 列表中保存的单个数据(字符串或其他)小于64字节,列表中数据个数小于512个。散列表: 当数据过大,不满足以上2个条件后。 |
用于存储对象,例如用户信息、商品信息等 |
15.2 缓存问题
- 缓存穿透:当一个查询的数据在缓存中不存在,但在数据库中也不存在时,这个查询会穿透到数据库,导致数据库负载过高 解决方法使用布隆过滤器,将不存在于数据库的数据进行过滤;对于查询结果为空的 key,设置空值缓存,有效时间设置短一些。
- 缓存击穿:当一个热点数据过期时,大量的请求会同时访问数据库,导致数据库负载过高。解决方法有:设置热点数据永不过期,定期更新缓存。
- 缓存雪崩:当大量的缓存数据同时过期时,大量的请求会同时访问数据库,导致数据库负载过高 解决方法:给缓存的key设置随机过期时间,防止大量的缓存数据同时过期。
16. Redis key过期策略?内存淘汰机制?
16.1 过期策略
- 基于惰性删除的策略:当访问一个已经过期的key时,Redis会立即将它删除。
- 基于定期删除的策略:Redis会定期地检查所有key是否过期,将过期的key删除。
- 基于内存淘汰的策略:当Redis的内存达到一定限制时,Redis会通过一些算法,将一些冷门的、不常使用的key删除,腾出空间。
16.2 淘汰策略:
- noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
- allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
- volatile-lru:加入键的时候如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
- allkeys-random:加入键的时候如果过限,从所有key随机删除
- volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
- volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
- volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
- allkeys-lfu:从所有键中驱逐使用频率最少的键
16.3 简单记忆
- 最近最少使用(LRU)算法:淘汰最近最少使用的key。
- 最不经常使用(LFU)算法:淘汰最近使用次数最少的key。
- 随机淘汰算法:随机选择一个key进行淘汰。
- 根据ttl:生存时间 默认是不进行淘汰,直接返回异常。
17. Redis持久化策略?RDB与AOF区别?底层原理?
- RDB: 是一种快照式持久化方式,当触发某些特定的事件时,Redis会将内存中的数据保存到磁盘上一个指定的文件中,文件后缀一般为rdb 。RDB持久化的优点是备份文件小、加载速度快,适合用于大规模数据恢复。缺点是数据可能会丢失,因为数据并不是实时保存的
- AOF: 则是将所有写操作追加到一个日志文件中,即追加式持久化方式。在AOF模式下,每个写操作都会以文本的形式记录在一个追加文件中,从而记录了所有数据的历史操作 OF持久化的优点是数据不会丢失,因为每个写操作都会记录在追加文件中,可以避免因为某个事件没有触发而导致数据丢失。缺点是备份文件大、加载速度相对RDB慢,适合用于小规模数据恢复。
18. 实现分布式锁的方法
- 基于数据库主键或唯一索引实现分布式锁:在数据库表中创建一个带有唯一性约束条件的字段作为分布式锁,当需要获得锁时,向数据库表中插入一条记录,如果插入成功则表示获得了锁,否则表示锁已被其他线程占用。释放锁时,删除该记录即可。
- 基于 ZooKeeper 实现分布式锁:ZooKeeper 的顺序节点可以用来实现分布式锁。当需要获得锁时,创建一个带有顺序号的节点,并检查当前所有节点中是否自己的节点编号最小,如果是则表示获得了锁,否则监听自己前面一个节点的删除事件,等待锁释放。释放锁时,删除自己创建的节点即可。
- 使用 Redisson 实现分布式锁:Redisson 是一个基于 Redis 的分布式 Java 对象框架,提供了分布式锁的实现。通过 Redisson 的 getLock 方法可以获得一个锁对象,调用锁对象的 lock 和 unlock 方法即可获得和释放锁。Redisson 支持多种锁模式,包括可重入锁、公平锁、联锁等。
19. Spring事务隔离级别?事务失效场景?如何改变传播行为?
19.1 事务隔离级别
读未提交、读已提交、可重复读和串行化
使用@Transactional注解的isolation属性来控制事务的隔离级别
19.2 事务失效
- 事务使用在静态方法上会失效
- 事务使用在非public上(访问权限问题)
- 事务中对异常进行捕获,出现异常后spring框架无法感知到异常(非RuntimeException),事务就会失效
- 方法用 final 修饰
- 方法内部调用
- 未被 spring 管理
- 多线程调用
- 表不支持事务
19.3 传播行为
- REQUIRED:如果当前存在一个事务,则加入该事务;否则创建一个新的事务。这是默认传播行为。(默认)
- REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则将当前事务挂起。
- MANDATORY:如果当前存在一个事务,则加入该事务;否则抛出异常。
- SUPPORTS:如果当前存在一个事务,则加入该事务;否则以非事务的方式执行。
- NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,则将当前事务挂起。
- NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
- NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与REQUIRED类似的操作修改传播行为使用@Transactional注解的propagation属性来控制事务的传播行为
20. RabbitMQ如何保证消息可靠性?
20.1 生产者
对于发送消息的场景,正常情况没有问题,直接发送即可:
如果是非正常情况就需要特殊处理了,一般会有三种非正常情况需要处理:
● 第一种情况,消息发送到交换机(exchange),但是没有队列与交换机绑定,消息会丢失。
● 第二种情况,在消息的发送后进行确认,如果发送失败需要将消息持久化,例如:发送的交换机不存在的情况。
● 第三种情况,由于网络、MQ服务宕机等原因导致消息没有发送到MQ服务器。
第一种情况:
对于消息只是到了交换机,并没有到达队列,这种情况记录日志即可,因为我们也不确定哪个队列需要这个消息。
配置如下(nacos中的shared-spring-rabbitmq.yml文件):
package com.sl.mq.config;
import cn.hutool.core.util.StrUtil;
import com.sl.transport.common.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
@Slf4j
@Configuration
public class MessageConfig implements ApplicationContextAware {
/**
* 发送者回执 没有路由到队列的情况
*
* @param applicationContext 应用上下文
* @throws BeansException 异常
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 获取RabbitTemplate
RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
// 设置ReturnCallback
rabbitTemplate.setReturnsCallback(message -> {
if (StrUtil.contains(message.getExchange(), Constants.MQ.DELAYED_KEYWORD)) {
//延迟消息没有发到队列是正常情况,无需记录日志
return;
}
// 投递失败,记录日志
log.error("消息没有投递到队列,应答码:{},原因:{},交换机:{},路由键:{},消息:{}",
message.getReplyCode(), message.getReplyText(), message.getExchange(), message.getRoutingKey(), message.getMessage());
});
}
}
第二种情况:
在配文件中开启配置publisher-confirm-type,即可在发送消息时添加回调方法:
在代码中进行处理,将消息数据持久化到数据库中,后续通过xxl-job进行处理,将消息进行重新发送。
同样,如果出现异常情况也是将消息持久化:
第三种情况:
将发送消息的代码进行try{}catch{}处理,如果出现异常会通过Spring-retry机制进重试,最多重试3次,如果依然失败就将消息数据进行持久化:
设置重试:
最终的落库操作:
xxl-job任务,主要负责从数据库中查询出错误消息数据然后进行重试:
package com.sl.mq.job;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.sl.mq.entity.FailMsgEntity;
import com.sl.mq.service.FailMsgService;
import com.sl.mq.service.MQService;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
/**
* 失败消息的处理任务
*/
@Slf4j
@Component
@ConditionalOnBean({MQService.class, FailMsgService.class})
public class FailMsgJob {
@Resource
private FailMsgService failMsgService;
@Resource
private MQService mqService;
@XxlJob("failMsgJob")
public void execute() {
//查询失败的数据,每次最多处理100条错误消息
LambdaQueryWrapper<FailMsgEntity> queryWrapper = new LambdaQueryWrapper<FailMsgEntity>()
.orderByAsc(FailMsgEntity::getCreated)
.last("limit 100");
List<FailMsgEntity> failMsgEntityList = this.failMsgService.list(queryWrapper);
if (CollUtil.isEmpty(failMsgEntityList)) {
return;
}
for (FailMsgEntity failMsgEntity : failMsgEntityList) {
try {
//发送消息
this.mqService.sendMsg(failMsgEntity.getExchange(), failMsgEntity.getRoutingKey(), failMsgEntity.getMsg());
//删除数据
this.failMsgService.removeById(failMsgEntity.getId());
} catch (Exception e) {
log.error("处理错误消息失败, failMsgEntity = {}", failMsgEntity);
}
}
}
}
xxl-job中的任务调度:
- 首先把消息内容持久化到数据库中
- 调用客户端发送消息:
a. 连接不上MQ,重试3次后更新数据库中消息状态为发送失败,加上失败原因
b. 使用生产者确认:
ⅰ. 消息没到达交换机,即返回了nack:重试3次后更新数据库中消息状态为发送失败,加上失败原因
ⅱ. 消息没到达队列:更新数据库中消息状态为发送失败,加上失败原因。忽略延迟交换机。
c. 使用xxl-job定时任务 对 连接不上的,消息没到交换机的进行重试。重试次数达到后,则发邮件通知运维人员。
20.2 MQ
20.2.1 持久化
声明(创建)交换机(durable为true)、
队列(durable为true)
发送消息时指定持久化(MessageDeliveryMode.PERSISTENT)
20.2.2不能自动删除
交换机与队列都不能自动删除
20.3 消费者
20.3.1 确认模式
共有3种确认模式:默认使用auto
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: none|auto|manual
- none:代表消费就删除队列中的消息,不管消费者成功与否。
- auto:如果消费者代码出异常,则自动返回nack并重入队列。如果消费者没出异常则返回ack。导致不断的重试。
- manual: 手工ack,消费者方法参数中,需要添加Channel与Message参数,通过channel.basicAck或channel.basicNack。
20.3.2 Spring重试机制
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000 # 初识的失败等待时长为1秒
multiplier: 3 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 4 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
20.3.3 失败策略
重试次数耗尽,如果消息依然失败,则需要有MessageRecover。有3种不同的实现
- RejectAndDoNotRequeueRecover:重试耗尽后,直接reject,丢弃消息。默认就是这种方式
- ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队
- RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机。
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
20.3.4 死信
重试次数耗尽时,使用第一种RejectAndDoNotRequeueRecover,则消息会进入死信,由运维人员来处理。
21. 延迟队列最后一秒钟跳转支付界面然后库存进行回滚用户已经支付的问题?
微服支付文档:
- 统一下单 传入订单失效时间(下单时生成二维码时,把失效时间一起传给微信)
- 延迟队列,比失效时间多10秒后再处理(取消订单前,查询订单获取订单最新支付的状态,再决定是否更改订单状态)调用支付统一下单接口,传入的主要参数(订单号,商品名称,回调地址,价格,订单失效时间time_expire),我们自己的失效时间可以延迟10秒,如果没有接收的支付平台的消息,则调用查询订单API,主要参数为商户id、子商户id、微信支付订单号。
22. Spring的三级缓存与循环依赖的解决?
22.1 三级缓存
名称 |
源码名称 |
数据类型 |
作用 |
一级缓存 |
singleObjects |
ConcurrentHashMap(256) |
单例池,缓存已经经历生命周期初始化完成的bean对象 |
二级缓存 |
earlySingletonObjects |
HashMap(16) |
缓存早期的bean对象(生命周期还没走完),如:只完成构造方法 |
三级缓存 |
singletonFactories |
ConcurrentHashMap(16) |
缓存ObjectFactory,用来创建某个对象 |
22.2 Spring是如何解决循环依赖的?
spring循环依赖是指两个或更多个Bean之间存在相互依赖的情况,例如A依赖于B,而B依赖于A
- 实例化A,得到原始对象A,并且同时生成一个原始对象A对应的ObjectFactory对象
- 将ObjectFactory对象存储到三级缓存中
- 需要注入B,发现B对象在一级缓存和二级缓存都不存在,并且三级缓存中也不存在B对象所对应的ObjectFactory对象
- 实例化B,得到原始对象B,并且同时生成一个原始对象B对应的ObjectFactory对象,然后将该ObjectFactory对象也存储到三级缓存中
- 需要注入A,发现A对象在一级缓存和二级缓存都不存在,但是三级缓存中存在A对象所对应的ObjectFactory对象
- 通过A对象所对应的ObjectFactory对象创建A对象的代理对象
- 将A对象的代理对象存储到二级缓存中,删除三级缓存中的A的ObejectFactory对象
- 将A对象的代理对象注入给B,B对象执行后面的生命周期阶段,最终B对象创建成功
- 将B对象存储到一级缓存中,同时删除三级缓存中B的ObejctFactory对象
- 将B对象注入给A,A对象执行后面的生命周期阶段,最终A对象创建成功,将二级缓存的A的代理对象存储到一级缓存中
注意:
- 后面的生命周期阶段会按照本身的逻辑进行AOP, 在进行AOP之前会判断是否已经进行了AOP,如果已经进行了AOP就不会进行AOP操作了。
- singletonFactories : 缓存的是一个ObjectFactory,主要用来去生成原始对象进行了AOP之后得到的代理对象,在每个Bean的生成过程中,都会提前暴露一个工厂,这个工厂可能用到,也可能用不到,如果没有出现循环依赖依赖本bean,那么这个工厂无用,本bean按照自己的生命周期执行,执行完后直接把本bean放入singletonObjects中即可,如果出现了循环依赖依赖了本bean,则另外那个bean执行ObjectFactory提交得到一个AOP之后的代理对象(如果没有AOP,则直接得到一个原始对象)。
22.3 解决循环依赖的方案
三种解决方案:
- 通过构造函数注入解决循环依赖:当Bean之间有循环依赖时,使用构造函数注入可以解决问题。因为Spring容器可以通过构造函数来创建对象,并将它们注入到依赖项中。这种方式需要确保依赖关系是通过构造函数来注入的,而不是通过Setter方法。
- 通过Setter方法注入解决循环依赖:另一种解决方案是使用Setter方法注入。在这种情况下,Spring容器会先创建所有Bean的实例,然后通过Setter方法注入依赖项。如果循环依赖,容器将返回一个代理对象,稍后再将实际对象注入到代理中。
- 使用@Lazy注解解决循环依赖:@Lazy注解可以用于延迟注入依赖项。当一个Bean被标记为延迟注入时,Spring容器会在需要时才创建它。这种方式可以解决循环依赖的问题,因为每个Bean都是在需要时才被创建的,而不是在启动时一次性创建所有Bean。
23. 继承?封装?多态?接口和抽象类的区别?
23.1 面向对象编程的3大特性
- 封装是指将对象的状态和行为包装在一起,对外部隐藏对象的实现细节,只暴露出一些公共的接口,通过这些接口访问对象的属性和方法。封装的好处是增强了对象的安全性和可靠性,同时也方便了对象的使用和维护。
- 继承是指一个类(称为子类或派生类)可以继承另一个类(称为父类或基类)的属性和方法。子类可以通过继承获得父类的属性和方法,并且还可以在此基础上增加自己的属性和方法。
- 多态是指同一种行为具有多种不同的表现形式或状态。具体来说,多态性可以通过方法的重载和覆盖、接口和抽象类等机制实现。多态性的好处是增强了程序的灵活性和可扩展性,同时也方便了代码的复用和维护。
23.2 接口和抽象类区别
- 接口和抽象类都是用来实现多态性的机制。
- 接口是一种纯抽象的类型,只包含方法声明而没有实现。
- 通过实现接口,一个类可以具备接口中声明的方法,从而实现多态性。
- 抽象类是一种包含抽象方法的类,抽象方法只有声明而没有实现。
- 通过继承抽象类,子类必须实现父类的抽象方法,从而实现多态性。
- 接口和抽象类的区别在于:接口只包含方法的声明而没有实现,而抽象类包含抽象方法和具体方法;一个类可以实现多个接口,但只能继承一个抽象类。
使用场景
- 抽象类用于表示一类事物,抽象概念产品描述,通过用于封装工具,父类封装好,子类直接用。AQS(AbstractQueuedSynchronizer 锁的始祖)
- 接口表示一种能力、规范、约束,实现接口表示具有某种能力。JDK1.8有默认方法有方法体(方法声明前加default)。
24. 单例模式?(其它设计模式)
24.1 单例模式
懒汉式,饿汉式、双检锁(DCL)、内部类、枚举
24.2 四大原则
构造方法私有、以静态方法返回或枚举返回实例、确保只有一个实例、确保反序列时不会重新构建对象。
24.3 为什么使用单例模式
- 节省资源:在某些情况下,创建对象的开销比较大,如果每次都创建新的对象,会消耗大量的系统资源。使用单例模式可以在应用程序中只创建一个实例,避免了重复创建对象的开销,从而节省了系统资源。
- 简化代码:如果某个类需要在应用程序中频繁使用,那么在每个地方都创建该类的实例会导致代码变得复杂。使用单例模式可以将类的实例化过程封装在一个单独的类中,并提供一个全局访问点,从而简化了代码结构。
- 维护全局状态:在某些情况下,需要在应用程序中维护一个全局状态,例如日志对象或配置文件对象。使用单例模式可以保证该类只有一个实例,从而保证了全局状态的一致性。
- 保证线程安全:在多线程环境下,如果多个线程同时访问一个对象,并且该对象没有进行任何线程安全的处理,那么可能会出现数据竞争和其他线程安全问题。使用单例模式可以保证该类只有一个实例,并且提供了一个全局访问点,从而避免了线程安全问题。
- 使用单例主要保证:
25. stream的常用API
- filter:过滤集合中不符合条件的元素。
- map:将集合中的每个元素映射为另一个元素。
- flatMap:将集合中的每个元素映射为多个元素,并将结果合并成一个流。
- sorted:对集合进行排序。
- distinct:去除集合中的重复元素。
- limit:限制集合中元素的数量。
- skip:跳过集合中的前几个元素。
- reduce:将集合中的元素逐个聚合成一个结果。
- collect:将集合中的元素收集到一个集合中。
- forEach:对集合中的每个元素执行一个操作。
- count:计算集合中元素的数量。
- anyMatch:判断集合中是否存在满足条件的元素。
- allMatch:判断集合中所有元素是否都满足条件。
- noneMatch:判断集合中是否没有元素满足条件。
- findFirst:查找集合中第一个符合条件的元素。
- findAny:查找集合中任意一个符合条件的元素。
26. 死锁的解决方法
死锁是指两个或多个进程或线程无限期地等待彼此释放所占用的资源
避免循环依赖:死锁通常是由于多个线程对资源的互相依赖导致的。因此,可以尝试在设计时避免循环依赖,从而避免死锁的发生。
- 统一获取锁的顺序:当多个线程需要获取多个锁时,可以规定所有线程必须按照同样的顺序获取锁,从而避免不同线程之间的锁竞争导致的死锁。
- 设置超时时间:如果一个线程在一定时间内无法获取所需的锁,就会自动放弃该锁并退出,从而避免死锁的发生。这个方法可以通过设置锁的超时时间来实现。
- 使用资源分配图:资源分配图可以帮助我们理解资源之间的依赖关系,并且可以通过检查图中的环路来检测是否有死锁的情况发生。
- 引入抢占机制:抢占机制可以中断正在运行的线程并强制释放它所占用的资源,从而避免死锁的发生。但是需要注意,这种方法可能会导致程序执行的不确定性。
27. Spring中有哪些设计模式?你的项目中用了什么设计模式?
27.1 Spring中的设计模式
设计模式 |
在哪里用到 |
描述 |
工厂模式 |
BeanFactory和ApplicationContext |
|
单例模式 |
Spring容器中的Bean |
|
代理模式 |
Spring AOP |
基于反射实现动态代码 |
模板方法模式 |
Spring中以Template结尾的类 |
使用继承的方式实现 |
观察者模式 |
Spring事件驱动模型(发布订阅事件) |
|
适配器模式 |
AOP中的AdvisorAdapter MVC中的HandlerAdapter |
|
装饰器模式 |
Spring中的含有Wrapper和Decorator的类 |
|
策略模式 |
资源访问Resource接口 |
|
链式模式 |
springMVC中的拦截器 |
|
28. 如何防止重复提交
使用拦截器|过滤器+Redis来解决
- 获取请求对象Request,获取请求路径与请求参数,获取登录用户id
- 以请求路径+参数+登录用户id为key,使用字符串的incr命令,自增长加1,根据操作后的返回值判断
- 如果返回1,则通过expire命令设置这个key的有效时间为15s(不允许15s内重复提交)
- 如果返回值>1,则代表之前用户已经提交过了,则拦截用户请求
使用字符串的incr,由于单次incr的操作具有原子性,每次操作后的值一定会不一样,则是自增长的,就可以通过是否>1来判断它增长了几次,也就是提交了几次。
expire超时失效:如果超过15S,key会自动消失,那时用户又可以提交请求了。
29. 如何解决消息重复消费问题
消息重复消费无可避免,它是一种消息可靠性的保证。因此当消息重复消费时,只要做幂等处理即可
- 发送时,给每个消息一个唯一标识,当要开始处理消息时,先执行保存到redis或数据库中。redis中setnx如果数据已存在会返回0,不存在返回1。数据库则使用唯一索引,如果数据存在表中,则插入数据时报错
- 给要更新的数据,设置更新的条件(乐观锁方式)。如:update set version=version+1, ... where version=查询时的版本。或更新时使用update table set status=1 where status=0。看返回的affected row的值,如果是1代表没处理过,返回0代表处理过了。
- 给1个队列的1个消费者消费,消费前先查询判断是否处理过,如是处理过则不处理(status!=0)。处理后马上更新状态为其它值。即只处理某种状态的数据,其它不处理.
30. 如何优化接口
则检查接口经过的每个环节所花费的时长,看哪个环节花的时间最长。
31. Redis主从复制原理
31.1 全量同步
- slave节点向master请求增量同步(replId与offset)
- master节点判断replid,发现不一致,拒绝增量同步
- master将完整内存数据生成RDB,发送RDB到slave
- slave清空本地数据,加载master的RDB
- master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
- slave执行接收到的命令,保持与master之间的同步
31.2 增量同步
- slave节点向master请求增量同步(replId与offset)
- master节点判断replid,如果不是第一次则回复Continue
- master去repl_baklog中获取slave发来的offset之后的数据,将offset之后的命令发送给slave,slave执行命令完成同步
31.3 repl_backlog原理
repl_backlog是一个环形数组,大小16KB,master将新产生的数据存放在内存中,并且也存在repl_backlog中,并且每次更新都给它打上一个offset标记,每次slave同步都同步到最新的offset,在同步的间隔期间,会产生新的数据,offset会变大,salve每次同步的数据都是原来的数据与新的offset之间的数据
当master产生的新数据超过16kb时,会从头开始覆盖原来的数据,如果salve集群宕机过久,导致主从数据的offset(从小到大一直增长)差值过大(超过了16K的数据),master会拒绝增量同步,转而变为全量同步;反之则执行增量同步时。