面试-总结

编程知识 更新时间:2023-05-03 03:26:22

微服务架构

微服务

什么是分布式架构

服务的不同模块部署在不同的服务器上 ,单个节点不能提供完整的服务,需要多节点协调提供服务

什么是微服务架构

SOA(面向服务架构)的升华 业务需要彻底的组件化和服务化,原有的单个业务系统拆分为多个可以独立设计开发 运行的小应用,这些小应用之间通过服务完成交互和集成

为什么会使用微服务架构

1. 易于开发和维护

一个微服务只关注一个特定的业务功能,所以它的业务清晰、代码量较少。开发和维护单个微服务相对是比较简单的。而整个应用是由若干个微服务构建而成的,所以整个应用也会维持在可控状态。

2. 单个微服务启动较快

单个微服务代码量较少,所以启动会比较快。

3. 局部修改容易部署

单体应用只要有修改,就得重新部署整个应用,微服务解决了这样的问题。一般来说,对某个微服务进行修改,只需要重新部署这个服务即可。

4. 技术栈不受限

在微服务中,我们可以结合项目业务及团队的特点,合理地选择技术栈。例如某些服务可使用关系型数据库MySQL;某些微服务有图形计算的需求,我们可以使用Neo4J;甚至可以根据需要,部分微服务使用Java开发,部分微服务使用NodeJS进行开发。

5. 按需伸缩

我们可以根据需求,实现细粒度的扩展。例如:系统中的某个微服务遇到了瓶颈,我们可以结合这个微服务的业务特点,增加内存、升级CPU或者是增加节点,节约了硬件成本

微服务架构带来的问题

1. 运维要求较高

更多的服务意味着更多的运维投入。在单体架构中,只需要保证一个应用的正常运行;而在微服务中,需要保证几十甚至几百个服务的正常运行与协作,这给项目的运维带来了很大的挑战。

2. 分布式固有的复杂性

使用微服务构建的是分布式系统。对于一个分布式系统,系统容错、网络延迟、分布式事务等都带来了很大的挑战。

3. 接口调整成本高

微服务之间通过接口进行通信。如果修改某一个微服务的API,可能所有使用了该接口的微服务都需要做调整。

4. 重复劳动

很多服务可能都会使用到相同的功能,而这个功能并没有达到分解为一个微服务的程度,这个时候,可能各个服务都会开发这一功能,从而导致代码重复。

微服务架构的设计原则

1. 单一职责原则

每个微服务只负责一个独立的模块,尽量减少与其他模块的耦合

2. 服务自治原则

服务自治,是指每个微服务应该具备独立的业务能力、依赖与运行环境。每个服务从开发、测试、构建、部署,都应当可以独立运行,而不应该依赖其他服务。

3. 轻量级通信原则

微服务之间应该通过轻量级通信机制进行交互。轻量级通信机制应该具备两点:首先是它的体量较轻;其次是它应该是跨语言、跨平台的。例如REST协议(GET、POST、PUT、DELETE)

4. 接口明确原则

对应问题中的第三点接口调整成本高,所以要做到每个服务的对外接口应该明确定义,并尽量保持不变

Dubbo与springCloud区别

1,dubbo是阿里巴巴开源的 springCloud是spring团队开源

59 2,dubbo注册中心 是zookeeper springCloud注册中心可以是zk 也可以eureka

3,dubbo默认支持的dobbo协议,也可以设置为基于hession http协议 支持短报文

处理并发能力很高 springCloud 支持http+rest协议 使用更加灵活

4,dubbo可以做为服务注册中心,服务提供者,服务消费者,还有管控中心

springCloud除了dubbo所支持的服务外,还有强大的服务治理体系

openFeign

Feign与openFeign的区别

他们底层都是内置了Ribbon,去调用注册中心的服务。

Feign是Netflix公司写的,是SpringCloud组件中的一个轻量级RESTful的HTTP服务客户端,是SpringCloud中的第一代负载均衡客户端。

OpenFeign是SpringCloud自己研发的,在Feign的基础上支持了Spring MVC的注解,如@RequestMapping等等。是SpringCloud中的第二代负载均衡客户端。

Feign本身不支持Spring MVC的注解,使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务

OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

feign已不在维护,openfeign维护频繁。

如何解决openFeign在远程调用过程中丢失请求头信息?

出现原因:在远程调用的过程中,重新发起一个新的请求 不会携带之前旧的请求头信息

解决 : 利用tomcat请求与线程绑定机制 RequestContextHolder.getRequestAttributes()获取原有请求头的信息

openFeign在发起新请求之前会经过过滤器 因此我们自定义一个过滤器实现RequestInterceptor接口重写apply()方法 在方法内部获取原有请求头中的信息 利用requestTemplate.header()将原有请求头的信息放入到新请求中

Nacos

项目的注册中心选用

CAP定理:C:数据一致性。A:服务可用性。P:分区容错性(服务对网络分区故障的容错性)。

Zookeeper取CAP的CP注重一致性,在可用性方面不太好,假如master节点故障,剩余节点会重新leader选举,选举leader的时间太长30~120s,选举期间整个集群都不可用,这就导致在选举期间注册服务瘫痪,漫长选举导致注册不可用,不能容忍。

Eureka设计时优先保证可用性。取CAP的AP,注重可用性。Eureka各个节点都是平等的,只要有一台Eureka还在就能保证注册服务可用(保证可用性),只不过可能查询到的不是最新的。

Nacos 可以根据网络状态调整 AP 和CP

当网络状态良好时 Nacos 更注重数据的一致性

网络状态不佳 Nacos 更注重数据的可用性

Nacos既是注册中心 也是配置中心

Gateway

作用

负载均衡 过滤请求 验证令牌(统一鉴权) 全局熔断

经过各个过滤器的过滤之后,将满足指定断言规则的请求路由到指定位置

执行原理

1,SpringCloud为每一个FeignClient生成一个代理对象

2,代理对象会分析类与方法上的注解,就可判断出服务名与请求方法名的路由

3,从注册中心获取指定服务名的所有真实地址

4,利用负载均衡策略选择一个最佳地址,利用RestTemplate进行调用

5,等待接收返回结果

对于服务的地址,每次请求都需要去注册中心获取吗?

Feign 维护了一个服务与地址的关系清单List<> 这个list会将服务的真实地址实时更新

如果选择的最佳地址由于某种原因无法使用该如何处理?

SpringCloud Feign会进行重试。可以在配置文件中配置。

spring

什么是spring

Spring是一个轻量级的控制反转(IOC)和面向切面(AOP)的容器框架

AOP

全称:面向切面编程

系统是由许多不同的组件所组成的,每一个组件各负责一块特定功能。除了实现自身核心功能之外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全等其他的核心服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被称为横切关注点,因为它们会跨越系统的多个组件。

当我们需要为分散的对象引入公共行为的时候,OOP(面对对象程序设计)则显得无能为力。也就是说,OOP允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。

日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。

在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP:将程序中的交叉业务逻辑(比如安全,日志,事务等),封装成一个切面,然后注入到目标对象(具体业务逻辑)中去。AOP可以对某个对象或某些对象的功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前额外的做一些事情,在某个方法执行之后额外的做一些事情

AOP的实现原理

JDK 动态代理 底层是 反射 cglib 底层是继承

jdk动态代理,必须有接口,目标类必须实现接口, 没有接口时,需要使用cglib动态代理

五种通知类型:

​ 前置通知
​ 返回后通知
​ 异常后通知
​ 最终通知
​ 环绕通知

spring的事务传播机制

事务的传播行为一般发生在事物的嵌套场景中 默认为REQUIRED

传播机制含义
REQUIRED默认值,支持当前事务,如果没有事务会创建一个新的事务
SUPPORTS支持当前事务,如果没有事务的话以非事务方式执行
MANDATORY支持当前事务,如果没有事务抛出异常
REQUIRES_NEW创建一个新的事务并挂起当前事务
NOT_SUPPORTED以非事务方式执行,如果当前存在事务则将当前事务挂起
NEVER以非事务方式进行,如果存在事务则抛出异常
NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与REQUIRED类似的操作

IOC(控制反转)

第一点:IOC容器 体现的是map集合 其实就是spring解决循环依赖的原理
一级缓存 SingletonObject currentHashMap(256) 存放经历了完整生命周期的bean(实例化+初始化)
二级缓存 earlySingletonObject HashMap(16) 存放的是实例化但是没有初始化的对象 (半成品)
三级缓存 SingletonFactories HashMap(16) 存放生成bean对象的bean工厂,是抽象的,将来需要生成匿名内部类的方式进行调用
第二点:控制反转
对于bean的管理有原来的手动new 变为了容器自动实例化的过程
第三点:依赖注入
动态地将某种依赖关系注入到对象之中。
@Resource:使用bean的名称注入
@Autowired:使用bean的类型注入 ,如果没有类型,才会按照名称注入

IOC 的作用或好处

实现对象间的解耦,同时降低应用开发的代码量和复杂度,使开发人员更专注业务。

Spring有哪些容器类

BeanFactory:这是一个最简单的容器,它主要的功能是为依赖注入(DI)提供支持。
ApplicationContext:Application Context 是 Spring 中的高级容器。和 BeanFactory 类似,它可以加载和管理配置文件中定义的 Bean。 另外,它还增加了企业所需要的功能,比如,从属性文件中解析文本信息和将事件传递给所指定的监听器。
FileSystemXmlApplicationContext:该容器从 XML 文件中加载已被定义的 Bean, 需要提供 XML 文件的完整路径。
ClassPathXmlApplicationContext:同样从 XML 文件中加载已被定义的 Bean,但无需提供完整路径,因为它会从 CLASSPATH 中搜索配置文件。
WebXmlApplicationContext:该容器会在一个 Web 应用程序的范围内加载在 XML 文件中已被定义的 Bean。

Bean的生命周期:

实例化 -> 属性赋值 -> 初始化 -> 销毁

什么是springboot? 为什么会使用springboot?

springboot是spring提供的一个快速开发工具包 包含众多的starter 相当于 spring+springmvc

传统项目中配置文件整合复杂,最大的缺点:jar冲突问题。Springboot配置文件大量减少适合快速开发,Springboot底层实现版本统一,为所有Spring开发者更快的入门。SpringBoot开箱即用,提供各种默认配置来简化项目配置,内嵌式容器简化web项目,没有冗余代码生成和xml配置的要求,尽可能的根据项目依赖来自动配置Spring框架,提供可以直接在生产环境中使用的功能,如性能指标,应用信息和应用健康检查。

spring的三级缓存

名称对象名含义
一级缓存singletonObjects存放已经经历了完整生命周期的Bean对象
二级缓存earlySingletonObjects存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完)
三级缓存singletonFactories存放可以生成Bean的工厂

什么是循环依赖

多个对象之间的依赖关系形成一个闭环

为什么构造器注入属性无法解决循环依赖问题

由于spring中的bean的创建过程为先实例化 再初始化(在进行对象实例化的过程中不必赋值)将实例化好的对象暴露出去,供其他对象调用,然而使用构造器注入,必须要使用构造器完成对象的初始化的操作,就会陷入死循环的状态

Spring是如何解决循环依赖问题的

Spring是使用三级缓存(Map)解决的循环依赖问题

假设A依赖B,B依赖A,Spring创建A实例的过程如下:

1、A依次执行doGetBean方法、依次查询三个缓存是否存在该bean、没有就createBean,实例化完成(早期引用,未完成属性装配),放入三级缓存中,接着执行populateBean方法装配属性,但是发现装配的属性是B对象,走下面步骤。

2、创建B实例,依次执行doGetBean、查询三个缓存、createBean创建实例,接着执行populateBean发现属性中需要A对象。

3、再次调用doGetBean创建A实例,查询三个缓存,在三级缓存singletonFactories得到了A的早期引用(在第一步的时候创建出来了),将它放到二级缓存并移除3级缓存并返回,B完成属性装配,一个完整的对象放到一级缓存singletonObjects中。

4、B完成之后就回到A了,A得到完整的B,肯定也完成全部初始化,也存入一级缓存中。

一级缓存能不能解决循环依赖问题?

不能

在三个级别的缓存中存储的对象是有区别的 一级缓存为完全实例化且初始化的对象 二级缓存实例化但未初始化对象 如果只有一级缓存,如果是并发操作下,就有可能取到实例化但未初始化的对象,就会出现问题

二级缓存能不能解决循环依赖问题 ?

理论上二级缓存可以解决循环依赖问题。

但是如果注入的对象实现了AOP,那么注入到其他bean的时候,不是最终的代理对象,而是原始的。通过三级缓存的ObjectFactory才能实现类最终的代理对象。

但是需要注意,为什么需要在三级缓存中存储匿名内部类(ObjectFactory),原因在于 需要创建代理对象 eg:现有A类,需要生成代理对象 A是否需要进行实例化(需要) 在三级缓存中存放的是生成具体对象的一个匿名内部类,该类可能是代理类也可能是普通的对象,而使用三级缓存可以保证无论是否需要是代理对象,都可以保证使用的是同一个对象,而不会出现,一会儿使用普通bean 一会儿使用代理类

spring中的Bean对象与 new的对象有什么不同

1、Spring是使用反射创建的对象,可指定对象的生命周期;如果是直接new的话就是直接创建一个对象。

2、Spring实现了对象池,一些对象创建和使用完毕之后不会被销毁,放进对象池(某种集合)以备下次使用,下次再需要这个对象,不new,直接从池里取,节省时间。

3、使用new关键字创建的对象属于强引用对象,所谓强引用,就是jvm垃圾回收机制永远不会回收这类对象,这时候需要手动移除引用。如果没有移除,这个对象将一直存在,久而久之,会引起内存泄露问题。

4、使用spring中的IOC就能很好的解决上述问题,使用IOC创建对象的时候,则无需关心由于创建对象的问题而引发的内存泄露问题。

5、spring之所以不用new对象是因为类的构造方法一旦被修改,new的对象就出错了,如果是用了spring,就不用理会构造方法是否被修改,而拿来用就可以

SpringMVC

什么是springMVC

基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架

mvc是一种设计模式(设计模式就是日常开发中编写代码的一种好的方法和经验的总结)。模型(model)-视图(view)-控制器(controller),三层架构的设计模式。用于实现前端页面的展现与后端业务数据处理的分离

SpringMVC 的运行原理

1、用户发送请求到前端控制器DispatcherServlet。

2、DispatcherServlet收到请求调用HandlerMapping处理器映射器。

3、处理器映射器找到具体的处理器(注解或者xml配置),生成处理器以及处理器拦截器(若有),返回给DispatcherServlet。

4、DispatcherServlet调用HandlerAdapter处理器适配器。

5、HandlerAdapter经过适配调用具体的处理器(Controller)

6、Controller执行完成返回ModelAndView

7、HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet

8、DispatcherServlet将ModelAndView传给ViewReslover视图解析器

9、视图解析器解析后返回具体的View

10、DispatcherServlet根据View进行渲染视图(将模型数据填充到视图)

11、DispatcherServlet响应用户

Spring MVC怎么样设定重定向和转发的?

(1)转发:在返回值前面加"forward:“,譬如"forward:user.do?name=method4”

(2)重定向:在返回值前面加"redirect:“,譬如"redirect:http://www.baidu”

转发和重定向的区别

区别一:
  重定向时浏览器上的网址改变
  转发是浏览器上的网址不变
区别二:
  重定向实际上产生了两次请求
转发只有一次请求
重定向:
  发送请求 -->服务器运行–>响应请求,返回给浏览器一个新的地址与响应码–>浏览器根据响应码,判定该响应为重定向,自动发送一个新的请求给服务器,请求地址为之前返回的地址–>服务器运行–>响应请求给浏览器
转发:
  发送请求 -->服务器运行–>进行请求的重新设置,例如通过request.setAttribute(name,value)–>根据转发的地址,获取该地址的网页–>响应请求给浏览器
区别三:
  重定向时的网址可以是任何网址
  转发的网址必须是本站点的网址

Springboot

核心注解

  1. @SpringBootConfiguration

组合了 @Configuration 注解,实现配置文件的功能。

  1. @EnableAutoConfiguration

打开自动配置的功能,也可以关闭某个自动配置的选项。

如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class });

  1. @ComponentScan

Spring组件扫描功能,让spring Boot扫描到Configuration类并把它加入到程序上下文。

Springboot 自动装配原理

Springboot 在启动的时候会调用 run 方法,run 方法会 执行 refreshContext()方法刷新容器,会在类路径下找到/META-INF/spring-factories 文件,该文件中记录中众多的 候选自动配置类,容器会根据我们是否引入依赖是否书 写配置文件的情况,将满足条件的 Bean 注入到容器中, 于是就实现了 springboot 的自动装配.

SpringBoot配置文件有哪些

Spring Boot 的核心配置文件是 application 和 bootstrap 配置文件。

application 配置文件这个容易理解,主要用于 Spring Boot 项目的自动化配置。

bootstrap配置文件的特性:

boostrap 由父 ApplicationContext 加载,比 applicaton 优先加载

boostrap 里面的属性不能被覆盖

bootstrap 配置文件有以下几个应用场景:

使用 Spring Cloud Config 配置中心时,这时需要在 bootstrap 配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息;

一些固定的不能被覆盖的属性;

一些加密/解密的场景;

Mybatis

Mybatis 中#{}与${}的区别

① #{}是预编译处理、是占位符,${}是字符串替换、 是拼 接符。

② Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用 PreparedStatement 来赋值;

③ Mybatis 在处理 时 , 就 是 把 {}时,就是把 {}替换成变量的值,调用 Statement 来赋值;

④ #{}的变量替换是在 DBMS 中、变量替换后,#{}对应的 变量自动加上单引号

的 变 量 替 换 是 在 D B M S 外 、 变 量 替 换 后 , {}的变量替换是在 DBMS 外、变量替换后, DBMS{}对应的 变量不会加上单引号

⑥ 使用#{}可以有效的防止 SQL 注入,提高系统安全性

Mybatis 中一级缓存与二级缓存

(1)MyBatis的缓存分为一级缓存和 二级缓存。

一级缓存是SqlSession级别的缓存,默认开启。

二级缓存是NameSpace级别(Mapper)的缓存,多个SqlSession可以共享,使用时需要进行配置开启。

(2)缓存的查找顺序:二级缓存 => 一级缓存 => 数据库

Redis

Redis是 什么 以及优点

当前最热门的非关系型数据库

第一、redis是基于内存存储计算,性能速读远超mysql等数据库,计算速度很快,所以在使用的时候数据响应很快,

第二、redis支持多种多样的数据结构,如string、hash、set、list等,这些丰富的数据结构,可以满足我们在开发工作大部分常见数据结构,进行存储。

第三、redis丰富的api支持,让我们在使用的时候,常见的查询存储都能够很方便的使用,支持自定的查询的api等等

第四、redis的生态比较成熟,很多家大型公司都在使用,很多相关的知识扩展以及分析

第五、redis分布式集群化扩展性极高,而且稳定,能够支撑大量的数据吞吐,只要硬件支持。

Redis五种数据类型和底层数据类型

String 底层是简单动态字符串

hash Hash类型第一段数据结构有两种:ziplist(压缩列表),hashtable(哈希表).当field-value长度较短且个数较少时,使用ziplist,否则使用HashTable

list 底层是快速双向链表quicklist 在列表元素较少的情况下会使用 ziplist即压缩列表

set set数据结构是dict字典,字典是用哈希表实现的

zset zset底层使用了两个数据结构 hash 和跳跃表

单线程的Redis为什么读写速度快

  1. 纯内存操作

  2. 单线程操作,避免了频繁的上下文切换

  3. 采用了非阻塞I/O多路复用机制

    多路复用: 多路复用是指以同一传输媒质(线路)承载多路信号进行通信的方式

如何保证数据库与缓存的数据一致

第一种方式:双写模式 关注的读情况
修改数据 并修改缓存
问题:在并发情况下,当线程1由于网络卡顿 会被线程2修改修改缓存中数据,导致两个线程最终获取的数据不一致
第二种方式:失效模式
关注的 读写模式
基础操作: 修改数据库 并 删除缓存
当读模式和写模式同时存在的情况下,由于网络卡顿,可能会大致在写的过程中由于网路卡顿,推迟删除的动作,导致数据不一致,因此需要进行二次删除 也即延迟双删 之后,数据能保持一致
如果在极端情况下,卡顿时长超过了延迟时间 则执行兜底数据返回(回源操作)

redis 和 mongoDB 对比

概念区分:
mangodb 更类似于mysql 支持字段索引 游标操作 其优势在于查询功能比较抢到,擅长查询json数据,能够存储海量数据,但是不支持事务
Redis有丰富的数据类型 ,可以做为数据库 消息队列等
1,内存管理机制
redis支持数据的持久化,当内存不够的时候, 会执行内存淘汰策略volatile-lru
mangodb 当内存不够时候,只将热点数据存入内存,其数据存入磁盘
2,支持的数据结构
redis支持丰富的数据结构 string list set zset hash等机构
mangodb支持的数据结构比较单一,但是支持丰富的数据表达索引,最类似于关系型数据的一种系统,支持的查询语言也比较丰富
3,数据量和性能
当物理内存够用到时候,性能 redis>mongodb>mysql数据
当物理内存不够的实时 redis与mongodb都会使用虚拟内存
4,性能
mangodb依赖内存,TPS较高,Reids依赖内存,TPS非常高,性能上redis由于mangodb
注:TPS 服务器每秒处理的事务数
5,可靠性
mangodb 在1.8版本之后,只是持久化(binlog方式)
redis可靠性体现 rdb+aof
6,mangodb内置数据分析功能,而redis不支持
7,支持事务情况
mangodb不支持事务,redis支持事务
8集群
mangodb集群技术更加成熟,reids相对弱一些

redis的持久化

持久化就是将内存的数据写入到磁盘当中,防止服务突然宕机,造成内存数据的丢失。

RDB 默认持久化机制是按照一定的时间将内存中的数据以快照的形式保存到硬盘中 优点:数据恢复速度快 缺点 可能丢失少量新数据 持久化时存储数据效率低

AOF 是将Redis的每一次操作都写入到单独的日志文件中,当重启redis会重新从持久化的的日志中恢复数据 优点: 持久化效率高 不会丢失数据 缺点:恢复数据时速度慢

混合模式(RDB+AOF) 将RDB和AOF混合一起使用,在使用混合模式时,所有的数据操作也是保存在AOF当中,当进行恢复文件的时候,会将原有的AOF删除,并且将其中的数据全部以快照的形式保存至RDB文件当中 优点:持久化效率高 不会丢失数据 保证数据的安全性 数据恢复速度快

Redis的过期策略以及内存淘汰机制

redis采用的是定期删除+惰性删除策略。

定期删除,redis默认每隔100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每隔100ms将所有的key检查一次,而是随机抽取进行检查因此,如果只采用定期删除策略,会导致很多key到时间没有删除。

于是,惰性删除派上用场。也就是说在你 进行get或setnx等操作的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。

如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。

内存淘汰机制

noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)

allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。

volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。

allkeys-random: 回收随机的键使得新添加的数据有空间存放。

volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。

volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。

如何保证 Redis 的高可用

Redis 通过主从加集群架构,实现读写分离,主节点负责写,并将数据同步给其他从节点,从节点负责读,从而实现高并发。

我们项目中redis集群主要搭建了6台,3主(为了保证redis的投票机制)3从(【扩展】高可用),每个主服务器都有一个从服务器,作为备份机。

集群不可用?

a:如果集群任意master挂掉,并且当前的master没有slave,集群就会fail;

b:如果集群超过半数以上master挂掉,无论是否有slave,整个集群都会fail;

Redis有哪些应用场景?

1)String:缓存、限流、分布式锁、计数器、分布式 Session 等。

2)Hash:用户信息、用户主页访问量、组合查询等。

3)List:简单队列、关注列表时间轴。

4)Set:赞、踩、标签等。

5)ZSet:排行榜、好友关系链表。

如何通过Redis实现分布式锁

SETNX key value: 如果key不存在 创建并赋值 设置成功返回1 失败返回0

EXPIRE key 过期时间

这样的方式 不满足原子性

SET key value EX(秒) NX 失败时 返回nil

NX:只有键不存在时 才对键进行设置操作

XX: 只有键存在时 才对键进行设置操作

Mysql

设计表理论规范:三大范式

第一范式:字段不可分割 原子性

第二范式:主键唯一 除主键外其他键全部依赖于主键

第三范式:字段之间不允许出现信息传递 ,列和列之间不存在互相依赖关系

什么是反范式

优点: 反范式的过程就是通过冗余数据来提高查询性能,可以减少表关联和更好进行索引优化

缺点: 存在大量冗余数据,并且数据的维护成本更高

sql主键和外键的区别

1、主键是唯一标识一条记录,不能有重复,不允许为空,而外键可以重复,可以是空值;2、主键是用来保持数据完整性,外键是用来建立与其他表联系用的;3、主键只有一个,外键可以有多个。

mysql的去重查询

根据某些字段的去重查询(不考虑查询其他字段)

select distinct 字段名 from table

select 字段名 from table group by 字段名

索引

索引(Index)是帮助MySQL高效获取数据的数据结构

Mysql如何创建索引

1) 使用 CREATE INDEX 语句

create index 索引名 on 表名(字段名)

2) 使用 CREATE TABLE 语句

3)使用 ALTER TABLE 语句

alter table 表名 add index(字段名)

索引的优劣势

优点:提高数据检索的效率,降低数据库的IO成本。

​ 降低数据排序的成本,降低了CPU的消耗。

缺点:1. 降低更新表的速度 更新表时,mysql不仅要保存数据,而且要保存索引文件,每次更新添加了索引列的字段 也要去调整更新带来的键值对变化后的索引信息

  1. 实际上索引也是一张表,该表保存了主键与索引字段,并指向实体表的记录,所以索引列也是要占用空间的

B+Tree相对于B-Tree有什么不同

  1. 非叶子节点只存储键值信息。

  2. 所有叶子节点之间都有一个链指针。

  3. 数据记录都存放在叶子节点中。

    优点:

    B+树的磁盘读写代价更低

    B+树查询效率更加稳定

    B+树便于范围查询

为什么B+树比B树更适合实现数据库索引?

  • 由于B+树的数据都存储在叶子结点中,叶子结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,而在数据库中基于范围的查询是非常频繁的,所以通常B+树用于数据库索引。
  • B+树的节点只存储索引key值,具体信息的地址存在于叶子节点的地址中。这就使以页为单位的索引中可以存放更多的节点。减少更多的I/O支出。
  • B+树的查询效率更加稳定,任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。

什么情况下你觉得需要建立索引

  1. 经常用于查询的字段
  2. 经常用于连接的字段(如外键)建立索引,可以加快连接的速度
  3. 经常需要排序的字段建立索引,因为索引已经排好序,可以加快排序查询速度

什么情况下不建议建索引

  1. where条件中用不到的字段不适合建立索引
  2. 表记录较少
  3. 需要经常增删改
  4. 参与列计算的列不适合建索引
  5. 区分度不高的字段不适合建立索引,性别等

聚簇索引与非聚簇索引

聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据

非聚簇索引:将数据存储与索引分开结构,索引结构的叶子节点指向了数据的对应行 即保存了对应行数据的地址

innodb有且仅有一个聚簇索引

如果定义了主键,那么InnoDB会使用主键作为聚簇索引 - 如果没有定义主键,那么会使用第一非空的唯一索引(NOT NULL and UNIQUE INDEX)作为聚簇索引 - 如果既没有主键也找不到合适的非空索引,那么InnoDB会自动生成一个不可见的名为ROW_ID的列名为GEN_CLUST_INDEX的聚簇索引,该列是一个6字节的自增数值,随着插入而自增

innodb中,在聚簇索引之上创建的索引称之为辅助索引,辅助索引访问数据总是需要二次查找,非聚簇索引都是辅助索引。辅助索引叶子节点存储的不再是行的物理位置,而是主键值

为什么不推荐使用UUID作为主键

降低数据库写入性能

降低数据库读取性能

另外保存UUID需要占用更大的空间

如何定位并优化慢查询sql

1.根据慢日志定位查询慢sql

2.使用explain等工具分析sql

3.修改sql或尽量让sql走索引

如何优化sql

单张表索引数量建议控制在5个以内

(1)互联网高并发业务,太多索引会影响写性能

(2)生成执行计划时,如果索引太多,会降低性能,并可能导致MySQL选择不到最优索引

(3)异常复杂的查询需求,可以选择ES等更为适合的方式存储

不建议在频繁更新的字段上建立索引

非必要不要进行JOIN查询,如果要进行JOIN查询,被JOIN的字段必须类型相同,并建立索引

禁止使用select *,只获取必要字段

解读:

(1)select *会增加cpu/io/内存/带宽的消耗

(2)指定字段能有效利用索引覆盖

(3)指定字段查询,在表结构变更时,能保证对应用程序无影响

单表优化

  1. 不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描

  2. like以通配符开头(‘%abc…’)mysql索引失效会变成全表扫描的操作 非%开头的like查询如abc%,相当于范围查询,会使用索引

  3. mysql 在使用**不等于(!=或者<>)**的时候无法使用索引会导致全表扫描

  4. is not null 也无法使用索引,但是is null是可以使用索引的

  5. 字符串不加单引号索引失效

总结

对于单键索引,尽量选择针对当前query过滤性更好的索引

在选择组合索引的时候,当前Query中过滤性最好的字段在索引字段顺序中,位置越靠前越好。

在选择组合索引的时候,尽量选择可以能够包含当前query中的where字句中更多字段的索引

在选择组合索引的时候,如果某个字段可能出现范围查询时,尽量把这个字段放在索引次序的最后面(最左匹配原则)

书写sql语句时,尽量避免造成索引失效的情况

关联查询优化

  1. 保证被驱动表的join字段已经被索引
  2. left/right join 时,选择小表作为驱动表,大表作为被驱动表。
  3. inner join 时,mysql会自己帮你把小结果集的表选为驱动表。
  4. 子查询尽量不要放在被驱动表,有可能使用不到索引。
  5. 能够直接多表关联的尽量直接关联,不用子查询。

子查询优化

尽量不要使用not in 或者 not exists

排序优化

尽量走索引排序 而不是文件排序

只有当索引的列顺序和order by字句的顺序完全一致,并且所有的列的要么是升序排列,要么全部是降序排列,所有的列的排序方向都一致时,MySQL才能使用索引来对结果进行排序,如果查询需要关联多张表,则只有当order by子句引用的字段全部为第一张表时,才能使用索引做排序。

分组优化

group by 使用索引的原则几乎跟order by一致 ,唯一区别是groupby 即使没有过滤条件用到索引,也可以直接使用索引。

索引什么时候会失效?

1)like 以%开头,索引无效;当like前缀没有%,后缀有%时,索引有效;

2)or 语句前后没有同时使用索引。当 or 左右查询字段只有一个是索引,该索引失效,只有左右查询字段均为索引时,才会生效;

3)联合索引不使用第一列,索引失效;

4)数据类型出现隐式转化。如 varchar 不加单引号的话可能会自动转换为 int 型,使索引无效,产生全表扫描;

5)在索引列上使用 IS NULL 或 IS NOT NULL操作。最好给列设置默认值。

6)在索引字段上使用not,<>,!=。不等于操作符是永远不会用到索引的,因此对它的处理只会产生全表扫描。 优化方法: key<>0 改为 key>0 or key<0。

7)对索引字段进行计算操作、字段上使用函数。

8)当 MySQL 觉得全表扫描更快时(数据少);

什么是最左匹配原则?

最左原则顾名思义就是从最左边开始匹配的原则,mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,索引可以任意顺序,mysql的查询优化器会帮你优化成索引可以识别的形式,其针对的是组合索引(又名联合索引)。

索引是建议越多越好吗

数据量少的不需要建立索引 建立会增加额外的索引开销

数据变更需要维护索引,因此更多的索引意味着需要更多的维护成本

更多的索引也意味着需要更多的空间

大表怎么优化?

某个表有近千万数据,查询比较慢,如何优化?

当MySQL单表记录数过大时,数据库的性能会明显下降,一些常见的优化措施如下:

  • 限定数据的范围。比如:用户在查询历史信息的时候,可以控制在一个月的时间范围内;
  • 读写分离: 经典的数据库拆分方案,主库负责写,从库负责读;
  • 通过分库分表的方式进行优化,主要有垂直拆分和水平拆分。

锁模块

MyISAM与InnoDB关于锁方面的区别是什么

MyISAM默认的是表级锁 不支持行级锁

MyISAM 的读锁是共享锁,写锁是排它锁

InnoDB 默认的是行级锁 也支持表级锁

InnoDB采用的是二段锁,即加锁和解锁(commit,数据库默认打开自动提交);但是在非SQL加共享锁时,若没改变某行数据,那另一进程亦可修改该行记录。InnoDB的SQL没用到索引时,用的是表级锁;用到时则是行级锁。

共享锁:对于多个不同的事务,对同一个资源共享同一个锁。但是对于insert ,update,delete事务则会自动加上排它锁。在执行语句后面加上lock in share mode就代表对某些资源加上共享锁。 eg: 可同时执行多个select语句。
排它锁:对于多个不同的事务,对同一个资源只能有一把锁。只需在执行语句后+for update 即可。eg:只能一个个进行操作。

MyISAM适用场景

  1. 频繁执行全表count语句。
  2. 对数据进行增删改的频率不高,查询非常频繁。
  3. 没有事务

InnoDB适用场景

  1. 数据库增删改查都相当频繁。

  2. 可靠性要求比较高,要求支持事务。

什么是乐观锁 什么是悲观锁

悲观锁:在操作数据的时候,认为此操作会发生数据冲突,所以在进行每次操作时都要通过获取锁才能进行相同数据的操作。与Java的synchronized很相似,所以悲观锁需要耗费较多的时间。mysql中通过for update 实现排他锁 通过lock in share mode 实现共享锁
乐观锁:操作数据库时,认为此操作不会导致数据冲突,在操作数据时,不进行加锁处理,在进行更新后,再去判断是否有冲突。例如hibernate中的乐观锁两种实现,就是分别基于version和timestamp来实现的

数据库设计规范

一、基础规范

1、表存储引擎必须使用InnoDb,表字符集默认使用utf8,必要时候使用utf8mb4

解读:

(1)通用,无乱码风险,汉字3字节,英文1字节

(2)utf8mb4是utf8的超集,有存储4字节例如表情符号时,使用它

2、禁止使用存储过程,视图,触发器,Event

解读:

(1)对数据库性能影响较大,互联网业务,能让站点层和服务层干的事情,不要交到数据库层

(2)调试,排错,迁移都比较困难,扩展性较差

3、禁止在数据库中存储大文件,例如照片,可以将大文件存储在对象存储系统,数据库中存储路径

4、测试,开发,线上数据库环境必须隔离

二、命名规范

1、库名,表名,列名必须用小写,采用下划线分隔

MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。

解读:abc,Abc,ABC都是给自己埋坑

2、库名,表名,列名必须见名知义,长度不要超过32字符

解读:tmp,wushan谁知道这些库是干嘛的

3、库备份必须以bak为前缀,以日期为后缀

4、从库必须以-s为后缀

5、备库必须以-ss为后缀

三、表设计规范

表必须有主键

禁止使用外键

表和字段名要加注释

增加冗余字段

尽可能的使用 varchar代替 char

优先选择符合存储需要的最小、最简单的数据类型。

若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。这是因为引擎在处理查询和连接时会逐个比较字符串中每一个字符,而对于数字型而言只需要比较一次就够了。

小数类型为 decimal ,禁止使用 float 和 double

RabbitMQ

RabbitMQ是什么

RabbitMQ 是面向消息的中间件 消息队列就是一个使用队列来通信的组件

为什么要使用RabbitMQ ?

1.流量消峰
举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。
2.应用解耦
以电商应用为例,应用中有订单系统、库存系统、物流系统、
支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。
在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障,提升系统的可用性。

3.异步处理
有些服务间调用是异步的,
例如A调用B,B需要花费很长时间执行,但是A需要知道B什么时候可以执行完,以前一般有两种方式,A过一段时间去调用B的查询api查询。或者A提供一个callback api,B执行完之后用
api通知A服务。这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题,A调用B服务后,只需要监听B处理完成的消息,当B处理完成后,会发送一条消息给MQ,MQ会将此消息转发给A服务。这样A服务既不用循环调用B的查询api,
也不用提供callback api。同样B服务也不用做这些操作。A服务还能及时的得到异步处理成功的消息。

RabbitMQ的工作原理

Broker:消息队列服务进程,此进程包括两个部分:Exchange和Queue。
Exchange:消息队列交换机,按一定的规则将消息路由转发到某个队列,对消息进行过虑。
Queue:消息队列,存储消息的队列,消息到达队列并转发给指定的消费方。
Producer:消息生产者,即生产方客户端,生产方客户端将消息发送到MQ。
Consumer:消息消费者,即消费方客户端,接收MQ转发的消息。

消息发布接收流程:

-----发送消息-----
1、生产者和Broker建立TCP连接。
2、生产者和Broker建立通道。
3、生产者通过通道消息发送给Broker,由Exchange将消息进行转发。
4、Exchange将消息转发到指定的Queue(队列)

----接收消息-----
1、消费者和Broker建立TCP连接
2、消费者和Broker建立通道
3、消费者监听指定的Queue(队列)
4、当有消息到达Queue时Broker默认将消息推送给消费者。
5、消费者接收到消息。

如何保证RabbitMQ消息不丢失

为什么会产生消息丢失

一条消息从产生到被消费,中间会经历三个环节:生产者、MQ 内部、消费者,消息在这三个环节中均有可能出现丢失。

生产端可靠性投递
事务消息机制:事务消息机制由于会严重降低性能,使用confirm消息确认机制
confirm消息确认机制: 生产端投递的消息一旦投递到RabbitMQ后,RabbitMQ就会发送一个确认消息给生产端,让生产端知道我已经收到消息了,否则这条消息就可能已经丢失了,需要生产端重新发送消息了。
MQ端可靠性:

message消息到达RabbitMQ后先是到exchange交换机中,然后路由给queue队列,最后发送给消费端。 因此需要给exchange、queue和message都进行持久化。

消费端消息不丢失:

自动ack机制改为手动ack机制。
autoAck参数置为false, 那么RabbitMQ服务端的队列分为两部分: 1.等待投递给消费端的消息 2.已经投递给消费端。如果RabbitMQ一直没有收到消费端的确认信号, RabbitMQ会安排该消息重新进入队列(放在队列头部)等待投递给下一个消费者。

如何解决rabbitMQ 重复消费问题

为了保证消息不被重复消费,首先要保证每个消息是唯一的,所以可以给每一个消息携带一个全局唯一的id,流程如下:

1、消费者监听到消息后获取id,先去查询这个id是否存中

2、如果不存在,则正常消费消息,并把消息的id存入 数据库或者redis中

3、如果存在则丢弃此消息

ES

ES倒排索引

ES搜索是如何排序的

默认情况下,结果集会按照相关性进行排序 – 相关性越高,排名越靠前。为了使结果可以按照相关性进行排序,我们需要一个相关性的值。在ElasticSearch的查询结果中, 相关性分值会用_score字段来给出一个浮点型的数值,所以默认情况下,结果集以_score进行倒序排列.

为什么要使用Elasticsearch?

因为在我们商城中的数据,将来会非常多,所以采用以往的模糊查询,模糊查询前置配置,会放弃索引,导致商品查询是全表扫面,在百万级别的数据库中,效率非常低下,而我们使用ES做一个全文索引,我们将经常查询的商品的某些字段,比如说商品名,描述、价格还有id这些字段我们放入我们索引库里,可以提高查询速度。

ES 如何进行搜索

简单的查询 定义一个接口 继承ElasticsearchRepository类 使用这个接口进行简单的查询 这个接口可以自定义方法

复杂的查询 需要注入ElasticsearchRestTemplate 通过查询条件构造一个Query对象 在进行检索

线程池

为什么要使用线程池

1,在并发情况下,如果频繁的进行线程的创建和销毁,会消耗服务器性能
2,还需要对线程资源进行合理科学的管理

如何获取一个线程池对象

第一种方式: 可以借助于Executors工具类获取
ExecutorService executorService = Executors.newCachedThreadPool();

最大线程数为int类型的最大值 容易OOM

ExecutorService executorService1 = Executors.newFixedThreadPool(5);

阻塞队列LinkedBlockingQueue是无界的 容易OOM
第二种方式: 根据阿里开发手册,推荐自定义线程池
核心线程数
最大线程数/救急线程
救急线程过期时间
过期时间单位
线程工厂
阻塞队列
拒绝策略

第一步:线程池刚创建的时候,里面没有任何线程,等到有任务过来的时候才会创建线程。也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建corePoolSize个线程

第二步:调用execute()提交一个任务时,如果当前的工作线程数<corePoolSize,直接创建新的线程执行这个任务

第三步:如果当时工作线程数量>=corePoolSize,会将任务放入任务队列中缓存

第四步:如果队列已满,并且线程池中工作线程的数量<maximumPoolSize,还是会创建线程执行这个任务

第五步:如果队列已满,并且线程池中的线程已达到maximumPoolSize,这个时候会执行拒绝策略,JAVA线程池默认的策略是AbortPolicy,即抛出RejectedExecutionException异常

拒绝策略:
abortPolicy:推荐使用的拒绝策略
特点:拒绝任务并抛出异常 抛出异常之后,可以让开发者及时调整 核心线程数 最大线程数 等核心参数
discardPolicy
特点: 拒绝任务但是不抛出异常 不推荐使用
discardOldestPolicy
热点:拒绝等待时间最长的任务 不抛出异常 不推荐使用
CallRunsPolicy: 尚品汇商城使用的是该拒绝策略 但是生产环境一般推荐使用

线程池底层工作原理

第一步:线程池刚创建的时候,里面没有任何线程,等到有任务过来的时候才会创建线程。也可以调用 prestartAllCoreThreads() 或者 prestartCoreThread() 方法预创建corePoolSize个线程

第二步:调用execute()提交一个任务时,如果当前的工作线程数<corePoolSize,直接创建新的线程执行这个任务

第三步:如果当时工作线程数量>=corePoolSize,会将任务放入任务队列中缓存

第四步:如果队列已满,并且线程池中工作线程的数量<maximumPoolSize,还是会创建线程执行这个任务

第五步:如果队列已满,并且线程池中的线程已达到maximumPoolSize,这个时候会执行拒绝策略,JAVA线程池默认的策略是AbortPolicy,即抛出RejectedExecutionException异常

线程池拒绝策略

拒绝策略分类含义使用场景
AbortPolicy丢弃任务并抛出异常 RejectedExecutionException我们项目中关于线程池的定义,使用的就是默认的如果这种需求是关键的业务,eg:商品详情/购物车/首页
DiscardPolicy安静的丢弃任务但是不抛出异常设计的时候,一些无关紧要的业务可以采用此策略Eg:单纯的展示某一项数据的情况 文章的浏览量/点赞个数
DiscardOldestPolicy丢弃队列最前面的任务,然后重新提交被拒绝的任务喜新厌旧使用场景不多,可根据特定场景使用
CallerRunsPolicy由调用线程处理该任务使用场景非常少

线程池大小如何设置

  1. 需要分析线程池执行的任务的特性: CPU 密集型还是 IO 密集型
  2. 每个任务执行的平均时长大概是多少

如果是 CPU 密集型,主要是执行计算任务,响应时间很快,cpu 一直在运行,这种任务 cpu的利用率很高,那么线程数的配置应该根据 CPU 核心数来决定,CPU 核心数=最大同时执行线程数,加入 CPU 核心数为 4,那么服务器最多能同时执行 4 个线程。过多的线程会导致上下文切换反而使得效率降低。那线程池的最大线程数可以配置为 cpu 核心数+1

如果是 IO 密集型,主要是进行 IO 操作,执行 IO 操作的时间较长,这是 cpu 出于空闲状态,导致 cpu 的利用率不高,这种情况下可以增加线程池的大小。这种情况下可以结合线程的等待时长来做判断,等待时间越高,那么线程数也相对越多。一般可以配置 cpu 核心数的 2 倍。

synchronized和Lock锁

对比项synchronizedLock
语法层面关键字 源码在 jvm 中,用 c++ 语言实现是接口,源码由 jdk 提供,用 java 语言实现
使用 synchronized 时,退出同步代码块锁会自动释放使用 Lock 时,需要手动调用 unlock 方法释放锁
功能层面二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能API层面的锁
Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量
Lock 有适合不同场景的实现,如 ReentrantLock, ReentrantReadWriteLock
性能层面在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不错在竞争激烈时,Lock 的实现通常会提供更好的性能

synchronieze锁的升级流程:

无锁 偏向锁 轻量级锁 重锁

synchronized 锁升级原理:在锁对象的对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id,再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁,执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁,此过程就构成了 synchronized 锁的升级。

对象在内存中的存储布局

对象头 实例数据 对齐填充(保证八个字节的倍数)

对象头分为对象标记(Mark Word)和类元信息 (存储的是指向该对象类元数据的首地址)

对象标记 存储: 锁标识 hashcode 分代年龄 GC标志

多线程

进程:正在进行中的程序
线程:进程中一个不可分割的执行单元 eg qq程序汇总语言功能
分类
普通线程:一般情况,我们业务中涉及到线程都是普通线程 普通线程又称为是用户线程/业务线程
守护线程:是为了守护普通线程正常执行的一类线程 eg : GC线程
特点:不会单独运行,一旦普通线程执行完毕,守护线程也将结束
普通线程start之前,调用 setDeamon()将普通线程设置为守护线程
调用 isDeamon() 判断当前线程是否是一个守护线程

创建线程的四种方式

第一种:自定义类 继承Thread类重写run方法
第二种:自定义类 实现Runnable接口重写run方法
第三种:自定义类 实现Callable接口重写call方法
第四种: 线程池
问题:实现Runnable与Callable有什么区别?
答:Callable这种方式 可以有返回值 通过FutureTask.get方法获取返回值 Runnable没有返回值
Callable可以手动抛出异常 Runnable不抛出异常

线程状态/线程的生命周期

创建
就绪
Thread.start()
运行
run方法执行
阻塞
等待:Object.wait() Object.notify() Object.notifyAll()
睡眠:Thread.sleep(long time ) 时间到就唤醒

​ 共同点: 都是让当前线程暂时放弃cpu的使用权 进入阻塞状态

​ 不同点:
​ 1,所属方法不同 wait属于 Object类的成员方法 sleep 属于Thread类的静态方法
​ 2,醒来时机不同 wait如果不唤醒 就会一直等待 sleep等待相应的时间后醒来
​ 3,机制不同

​ wait方法的调用必须获得wait对象的锁 sleep无限制

​ wait 等待的时候 不持有锁
​ sleep是抱着锁睡觉,其他线程无法获取到该锁
销毁
​ run方法结束
​ destory方法

死锁

为什么会出现死锁

1.一个资源每次只能被一个线程使用

2.一个线程在阻塞等待某个资源时,不释放已占有资源

3.一个线程已经获得的资源,在未使用完之前,不能被强行剥夺

4.若干线程形成头尾相接的循环等待资源关系

这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。

如何避免出现死锁:

1.要注意加锁顺序,保证每个线程按同样的顺序进行加锁

2.要注意加锁时限,可以针对锁设置一个超时时间

3.要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决

如何排查死锁问题

1.通过jdk工具jps、jstack排查死锁问题

jps:jdk提供的一个工具,可以查看到正在运行的java进程

jstack:jdk提供的一个工具,可以查看java进程中线程堆栈信息。

从信息中查询是否出现死锁 以及定位出现死锁代码 的位置

2.通过jdk提供的工具jconsole排查死锁问题

在jconsole窗口中查看线程堆栈信息

点击“检测死锁”,可以看到程序死锁信息

start()和run()方法的区别

1) start:

用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的 start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行,一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

2) run:

run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。

总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。

电商项目

项目介绍

介绍一下你们的项目

这个项目是一家农产品购物网站,是甲方定制的,我们负责开发。

主要的技术选型:使用springboot整合SpringCloud 以及MyBatis-Plus进行微服务构建,使用nacos作为注册中心和配置中心,使用feign进行服务远程调用,使用gateway网关进行请求负载、请求过滤、统一鉴权和限流,使用Sentinel进行服务的熔断和降级,使用Spring Cloud Sleuth进行链路追踪,针对于项目图片文件资源较多,采用FastDFS进行文件资源存储,使用redis数据库进行数据缓存以及分布式锁的实现,使用ElasticSearch进行商品的搜索业务实现……

项目主要的模块:后台管理系统(商品的管理)、商品详情、商品搜索、购物车、单点登录+社交登录、订单、支付、秒杀等等

项目人员配置:项目经理(PM)1人、产品(PD)2人、界面设计(UI)2人、前端 3人、Java后台(DE)6人,其中1人是开发组长、测试(QA)2人、运维(SRE)1人

生产环境:Nacos 3台

redis无中心化集群6台(3主3从)

mysql数据库(读写分离的情况下,1主2从 4*3 =12)

ES集群3台

rabbitmq集群2台

上线后QPS,用户量、同时在线人数并发数等问题

(1)TPS(每秒事务处理量) QPS(每秒查询率)

(2)并发数: 系统同时处理的request/事务数

(3)响应时间: 一般取平均响应时间

在做这个项目的时候你碰到了哪些问题?你是怎么解决的?

项目中用到了曾经没有用过的技术RabbitMQ ,解决方式:用自己的私人时间主动学习

单点登录模块

什么是单点登录

在分布式系统中,用户只需一次登录,各个系统即可感知该用户已经登录

为什么要做单点登录

单点登录要解决的就是,用户只需要登录一次就可以访问所有相互信任的应用系统

如何实现session共享

基于Nginx的ip_hash 负载均衡 : 每个访客都固定访问一个后端服务器

基于Tomcat的session复制

使用Redis实现session共享

session放到cookie中

nginx负载均衡策略

轮询默认方式
weight权重方式
ip_hash依据ip分配方式
least_conn最少连接方式
fair(第三方)响应时间方式
url_hash(第三方)依据URL分配方式

session和cookie的区别

(1)cookie数据存放在客户的浏览器上,session数据放在服务器上
(2)cookie不是很安全,别人可以分析存放在本 地的COOKIE并进行COOKIE欺骗,如果主要考虑到安全应当使用session
(3)session会在一定时间内保存在服务器上。 当访问增多,会比较占用你服务器的性能,如果主要考虑到减轻服务器性能方面,应当使用COOKIE
(4)单个cookie在客户端的限制是3K,就是说一个站点在客户端存放的COOKIE不能3K。
(5)所以:将登陆信息等重要信息存放为SESSION;其他信息如果需要保留,可以放在COOKIE中

业务流程

单点登录就是访问项目时,用户只需要登录一次,就可以去访问所以内容。我们是采用网关全局过滤器、token认证以及redis来实现, 登录业务,用接收的用户名密码核对后台数据库,核对通过,用JWT生成token,将用户id加载到写入redis,redis的key为token,value为用户id。登录成功返回token与用户信息,将token与用户信息记录到cookie里面重定向用户到之前的来源地址。用户再次访问其他需要登录的内容时通过网关过滤器进行拦截校验

如何实现ID透传

全局过滤器拦截全部的用户请求 获取请求路径 如果是静态资源 放行

如果是拒绝访问的资源 拒绝访问

如果是需要登陆的请求 如果cookie或请求头中的UserId携带 token根据token到redis中查询用户信息 查询不到 返回登陆页 查询到 对exchange中的request增加userId请求头 并放行 如果cookie或请求头中的UserId不携带token 说明用户未登录 跳转到登录页面

剩下的请求均为普通请求 如果cookie或请求头中的UserId携带token 根据token到redis中查询用户信息 查询不到 返回登陆页 查询到 对exchange中的request增加userId请求头 并放行 如果cookie或请求头中没有userId 透传临时id

cookie被禁用了能登录吗?怎么解决的?

不能登录,因为token存在cookie中,解决办法 提示用户:请您启用浏览器Cookie功能或更换浏览器

订单模块

订单提交业务流程

前端携带提交订单数据和orderToken 从redis中查询orderToken是否存在 查询orderToken与删除orderToken 是一个原子操作 执行失败 重复提交 /时间过久 执行成功–>然后验证库存 验证价格 将订单数据入库 移除购物车中被选中的商品 将orderId userId 作为一个消息发送给延迟队列 延迟队列三十分钟后将消息转发至 关单队列 关单业务监听队列 监听到消息 执行关单操作 关单业务不成功有重试机制

将orderId返回给前端 响应成功后前端携带orderId 跳转到支付页面

如何保证订单不重复提交

点击购物车结算按钮 在进入结算页面 生成订单确认信息 的时候,生成一个唯一Token,返回给前端 ,并且作为key存到Redis中 给它设置一个过期时间 30min。

提交订单前, 前端携带之前生成的token 从redis中查询 且删除这条数据 注意这个查询与删除是一个原子操作 (redis的lua脚本) 删除失败代表为重复提交

订单模块为什么要分库分表

为了支撑高并发、数据量大两个问题的

分库的目的是为了提高并发度,简单来说就是TPS(每秒事务处理量),单个数据库每秒并发2000都非常高了,一般保持在1000左右;分表示为了提高查询速度,简单来说就是QPS(每秒查询率),单表数据量大查询慢,可以拆分成多个表; 单表数据量太大,会极大影响sql 执行的性能 当单表数据超过几百万的时候就需要考虑分表

如何对数据库进行拆分

水平拆分,就是把一个表的数据分配到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。

分库分表之后,id 主键如何处理

1.数据库自增 id 设置自增步长 优点:简单 能达到性能目标 缺点: 如果将来需要新增节点 很复杂

2.UUID 本地生成 UUID 太长了,作为主键性能太差了,另外 UUID 不具有有序性,会造成 B+ 树索引在写的时候有过多的随机写操作,频繁修改树结构,从而导致性能下降。

使用 snowflake 算法

超时关单的解决方案

  1. 定时任务
  2. 延时任务
  3. 延时任务+定期补偿
  4. 消息队列

商品详情模块

读多写少的操作 我们考虑增加缓存

业务流程

展示一个商品的详细信息,考虑到这是一个访问频繁且为查询操作,将其进行性能优化,所以我们考虑商品详细信息放入缓存,为了防止缓存穿透,我们使用了布隆过滤器,为了防止缓存击穿,我们采用了分布式锁。务功能对缓存的使用共性很强,因此我们将缓存的业务代码抽取出来,用AOP实现缓存优化,除此之外,由于商品详情展示的数据需要要调用多个查询接口,为了提高响应速度,我们采用线程异步编排的方式进行接口调用。

Redis在你们项目中是怎么使用到的

商品详情中的数据放入缓存,商品的实时价格也放入到数据中;

用Redis实现Session共享

我们项目中同时也将购物车的信息设计存储在redis中,存储的数据接口是hash 用户未登录采用临时Id作为Key,value是购物车对象;用户登录之后将商品添加到购物车后存储到redis中,key是用户id,value是购物车对象;

缓存相关问题

缓存穿透

指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数据库也无此记录。导致每次请求缓存中不命中,数据库也无数据,但是依然与数据库建立链接操作数据。

解决:

  • 固定值攻击:null也要缓存; 给缓存中保存一个占位符"x"即可。并指定一个比较短的过期时间,防止缓存一致判定为null

  • 随机值穿透攻击:

    • 本质:要知道数据库 有没有这个记录,数据库没有,就算缓存中没有,也不用往下放。

    • 解决:

      • 提前:全量缓存。 49: (商品信息), 50: (商品信息)

        • 数据库的所有商品信息,全部放到缓存中。查询只以缓存为准,缓存没有,则代表数据库没有。
        • 缺点:太浪费空间
        • 100w 数据; 10kb 10 Gb数据;
        • 京东有不到 1000w 的sku
      • 提前:缓存id; skuids: (100w 的 id); Long类型几个字节;8 mb

        • 缓存中没有,只需要判断缓存中是否有这个id。如果有,这次查询才可以放行给数据库。否则直接返回为空
        • 业务场景,一亿数据很多
      • 布隆过滤器:快速判断一个东西,之前有没有出现过

      • redis的bitmap

缓存击穿

在上一刻数据刚好过期或者被删除缓存失效:(缓存中无此数据)。准备回源。问布隆过滤器,布隆说数据库有。大量请求同时找数据库要数据

解决 :Redisson 分布式锁

缓存雪崩

某一刻缓存中的数据集体失效

缓存过期时间随机

数据一致性

双写 延迟双删

布隆过滤器

原理:

在初始状态时,对于长度为 m 的位数组,它的所有位都被置为0 当有变量被加入集合时,通过 K 个映射函数将这个变量映射成位图中的 K 个点,把它们置为 1 查询某个变量的时候我们只要看看这些点是不是都是 1 就可以大概率知道集合中有没有它了 可能会出现碰撞

布隆过滤器的优点:

• 时间复杂度低,增加和查询元素的时间复杂为O(N),(N为哈希函数的个数,通常情况比较小)

• 保密性强,布隆过滤器不存储元素本身

• 存储空间小,如果允许存在一定的误判,布隆过滤器是非常节省空间的(相比其他数据结构如Set集合)

布隆过滤器的缺点:

• 有点一定的误判率,但是可以通过调整参数来降低

• 无法获取元素本身

• 很难删除元素 因此需要对布隆过滤器进行定期重建任务

分布式锁的实现方式

  1. 基于数据库实现分布式锁 性能低
  2. 基于缓存(Redis等) 速度快
  3. 基于Zookeeper 可靠性高

你开发中遇到过什么问题:!!关于分布式锁 的一个问题

开始时我们考虑使用redis实现分布式锁 通过set KEY 锁名 ex 3000 nx (拿锁并给锁设置过期时间) 返回1表示得到锁 执行业务逻辑后 删除锁 返回0表示没有得到锁 等待一定时间后 考虑从redis中获取数据

但是 redis主从切换时 有误删锁的情况 所以后续不考虑使用redis 考虑使用redisson ,redission进行了封装,通过调用lock以及unlock方法进行加锁和解锁,使用更简单。

异步编排

查询商品详情页的逻辑非常复杂,数据的获取都需要远程调用,必然需要花费更多的时间 考虑使用异步编排减少查询需要的时间 利用 CompletableFuture

api: CompletableFuture.runAsync(Runnable runnable)—无返回值

​ CompletableFuture.supplyAsync(Supplier<> supplier)—有返回值 thenAccpetAsync

​ CompletableFuture.allOf(CompletableFutures)–编排所有异步任务完成执行完之后调用allOf方法

你们如何实现使用aop完成商品详情的查询

第一步: 自定义注解 @GmallCache
包含属性: 缓存前缀 缓存TTL 过期时间范围 分布式锁key
第二步: 自定义切面 一个普通类 + @Aspect注解 + @Component 注入容器
第三步: 在切面中,会定义一个方法 该方法就是对自定义注解的一个加强 该方法就称之为是切点
第四步: 利用spring AOP 提供的5中通知方式中 环绕通知 来定义

主从复制

主要原理

  • 主MySQL服务器做的增删改操作,都会写入自己的二进制日志(Binary log)

  • 然后从MySQL从服务器打开自己的I/O线程连接主服务器,进行读取主服务器的二进制日志

  • I/O去监听二进制日志,一旦有新的数据,会发起请求连接

  • 这时候会触发dump线程,dump thread响应请求,传送数据给I/O(dump线程要么处于等待,要么处于睡眠)

  • I/O接收到数据之后存放在中继日志

  • SQL thread线程会读取中继日志里的数据,存放到自己的服务器中

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T83VMZQc-1668612267073)(C:\Users\86184\AppData\Roaming\Typora\typora-user-images\1667788545127.png)]

购物车模块

业务流程

我们项目对于购物车的设计主要分为两个部分:用户未登录和已登录两种状态。在添加购物车时需要进行判断用户是否登录,如果未登录,则将购物车的信息放在redis中,采用hash结构存储,key为uuid(用户的临时id)随机生成,field为skuId,value是商品购物项信息,将reids的Key(临时用户id)存放到cookie中。用户已登录状态下,cookie中会存放token,根据token我们能从redis获取到用户的id,所以采用userId作为hash结构的key,field为skuId,value是商品购物项信息。在进入购物列表时,会判断用户的登录状态,如果是未登录的话直接返回未登录购物车数据,如果是已登录,就判断是否存在未登录购物车,存在就进行购物车合并,然后再删除未登录购物车数据。

添加商品

用户在添加商品到购物车时 会判断当前登陆状态 分为未登录购物车和已登录购物车

从redis中获取该购物车 判断该购物车是否包含有该商品 如果包含 校验商品数量是否超出单个商品添加上限 校验通过更新商品数量 同时更新价格 如果不包含该商品

校验购物车的规模和商品数量 校验通过在该购物车中增加该商品的信息

合并购物车

用户在未登录状态 创建过临时购物车 登陆状态后则需要将临时购物车中的数据添加到用户购物车中

检验用户状态是否需要合并购物车 获取userId userTempId userId 为空 代表不需要合并

均不为空时 根据userTempId 查询临时购物车的数据

当临时购物车中有数据时 获取临时购物车中的数据 并将数据加入到 用户购物车中 有则数量更新 无则新增数据

合并完成后删除临时购物车数据

购物车为什么使用redis保存数据

购物车是一个读写都很频繁的业务 使用redis+数据库 会面临数据一致性保证的问题 为了以较快的响应速度来满足用户 的体验度 综合使用redis来做存储

购物车未登录有过期时间吗?已登录后有吗?

未登录的话:7天或15天

已登录情况下是永久存储,reids的数据有过期,主要是释放 redis’中数据。

购物车能添加多少个商品?

49

每个商品最多能加多少件

99

添加购物车减库存吗

不会 只有

进入购物车列表的时候验价格吗

验价格,如果价格有变化,展示新价格,给用户提示

购物车里的商品价格变换了有提示吗?库存无货了有提示吗?

因为进入购物车列表时进行价格的验证 库存不验证

支付模块

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XAlXfUUN-1668612267074)(C:\Users\86184\AppData\Roaming\Typora\typora-user-images\1667564948444.png)]

网络

get和post请求有什么区别

(1)post更安全(不会作为url的一部分,不会被缓存、保存在服务器日志、以及浏览器浏览记录中)

(2)post发送的数据更大(get有url长度限制)

(3)post能发送更多的数据类型(get只能发送ASCII字符)

(4)post比get慢

​ post请求的过程:

(1)浏览器请求tcp连接(第一次握手)
(2)服务器答应进行tcp连接(第二次握手)
(3)浏览器确认,并发送post请求头(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)
(4)服务器返回100 Continue响应
(5)浏览器发送数据
(6)服务器返回200 OK响应
get请求的过程:
(1)浏览器请求tcp连接(第一次握手)
(2)服务器答应进行tcp连接(第二次握手)
(3)浏览器确认,并发送get请求头和数据(第三次握手,这个报文比较小,所以http会在此时进行第一次数据发送)
(4)服务器返回200 OK响应

(5)post用于修改和写入数据,get一般用于搜索排序和筛选之类的操作(淘宝,支付宝的搜索查询都是get提交),目的是资源的获取,读取数据

TCP三次握手

http和https的区别

① 安全性方面:HTTP 明文传输,数据都是未加密 的,安全性较差,HTTPS(SSL+HTTP) 数据传输

过程是加密的,安全性较好。

② 使用 HTTPS 协议需要到 CA(Certificate Authorit y,数字证书认证机构) 申请证书,一般免费证书

较少,因而需要一定费用。证书颁发机构如:Syman tec、Comodo、GoDaddy 和 GlobalSign 等。

③ 响应速度:HTTP 页面响应速度比 HTTPS 快,主 要是因为 HTTP 使用 TCP 三次握手建立连接,客

户端和服务器需要交换 3 个包,而 HTTPS 除了 T CP 的三个包,还要加上 ssl 握手需要的 9 个包,

所以一共是 12 个包。

④ 端口号:http 和 https 使用的是完全不同的连接方 式,用的端口也不一样,前者是 80,后者是 443。

⑤ 资源消耗方面 HTTPS 其实就是建构在 SSL/TLS 之上的 HTTP 协议,所以,要比较 HTTPS 比 HTTP 要更耗费服务器资源。

http 常见的状态码有哪些

200 OK //客户端请求成功

302 found 重定向

400 Bad Request //客户端请求有语法错误,不能被服务器所理解

403 Forbidden //服务器收到请求,但是拒绝提供服务

404 Not Found //请求资源不存在,eg:输入了错误的 URL

500 Internal Server Error //服务器发生不可预期的错误

503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常

Java基础

什么是面向对象 以及对面向对象的理解

面向过程更注重事情的每一个步骤及 顺序,面向对象更注重事情有哪些参与者(对象)、及各自需要做什么

面向过程比较直接高效,而面向对象更易于复用、扩展和维护

三大特性:

封装: 封装是把客观事物抽象成类,并且把自己的属性和方法让可信的类或对象操作,对不可性的隐藏

继承: 可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。

多态: 基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同。 父类引用指向子类对象,同时父类无法调用子类特有功能

同一个行为具有多个不同表现形式或形态的能力。

父类引用指向子类对象,例如 List list = new ArrayList();就是典型的一种多态的体现形式。

String、 StringBuffer. StringBuilder的区别

1.String是不可变的,如果尝试去修改,会新生成一个字符串对象,StringBuffer和StringBuilder是可变的

2.StringBuffer是线程安全的,StringBuilder是线程不安全的,所以在单线程环境下StringBuilder效率会更高

==和equals的区别

== 的作用:
  基本类型:比较值是否相等
  引用类型:比较内存地址值是否相等

equals 的作用:
  引用类型:默认情况下,比较内存地址值是否相等。可以按照需求逻辑,重写对象的equals方法。

hashCode()与equals()

1.如果两个对象相同,那么它们的hashCode值一定要相同

2.如果两个对象的hashCode相同,它们并不一定相同。

3.equals()相等的两个对象,hashcode()一定相等;equals()不相等的两个对象,却并不能证明他们的hashcode()不相等。

集合

说说你了解的集合

一是Collection集合,二是Map集合。

  • Collection集合下有List、Set、Queue。
  • Map集合下有HashMap、LinkedHashMap、TreeMap、HashTable、ConcurrentHashMap。
    • List集合下有ArrayList、LinkedList、Vector、CopyOnWriteArrayList。
    • Set集合下有HashSet、LinkedHashSet、TreeSet、CopyOnWriteArraySet

说说List和Set的区别

  1. Set 接口实例存储的是无序的,不重复的数据。List 接口实例存储的是有序的,可以重复的元素。都可以存储null值,但是set不能重复所以最多只能有一个空元素。
  2. Set检索效率低下,删除和插入效率高,插入和删除不会引起元素位置改变 <实现类有HashSet,TreeSet>。
  3. List和数组类似,可以动态增长,根据实际存储的数据的长度自动增长List的长度。查找元素效率高,插入删除效率低,因为会引起其他元素位置改变 <实现类有ArrayList,LinkedList,Vector> 。

Arraylist与 LinkedList 异同

  1. Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向循环链表数据结构;
  2. ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。插入末尾还好,如果是中间,则(add(int index, E element))接近O(n);LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。对于随机访问get和set,ArrayList优于LinkedList,因为LinkedList要移动指针。
  3. LinkedList 不支持高效的随机元素访问,而ArrayList 实现了RandmoAccess 接口,所以有随机访问功能。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。所以ArrayList随机访问快,插入慢;LinkedList随机访问慢,插入快。
  4. ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

HashMap

HashMap原理

​ 1.存取无序的
​ 2.键和值位置都可以是null,但是键位置只能是一个null
​ 3.键位置是唯一的,底层的数据结构控制键的
​ 4.jdk1.8前数据结构是:链表 + 数组 jdk1.8之后是 : 链表 + 数组 + 红黑树
​ 5.阈值(边界值) > 8 并且数组长度大于64,才将链表转换为红黑树,变为红黑树的目的是为了高效的查询。

HashMap扩容机制

数组+链表+红黑树 默认的数组长度为0 第一次put后扩为 16

没有发生hash冲突时,优先进行数组的扩容 X2 hash冲突: 如果两个不同对象的hashCode相同,这种现象称为hash冲突。

当链表长度大于8但当前数组的长度小于64时,会先进行数组的扩容 X2

当链表长度大于8并且当前数组的长度大于64时 会将此索引位置上的所有数据改为使用红黑树存储

HashMap的put方法流程总结

1、put(key, value)中直接调用了内部的putVal方法,并且先对key进行了hash操作;hash算法
key HashCode 值 无符号右移16为 做异或运算

2、putVal方法中,先检查HashMap数据结构中的数组是否为空或长度是否为0,如果是的话则进行一次resize操作;

3、以HashMap数组的长度减一与key的hash值进行与运算,得出在数组中的索引,如果索引指定的位置值为空,则新建一个k-v的新节点;

4、如果不满足的3的条件,则说明索引指定的数组位置的已经存在内容,这个时候称之碰撞出现

5、在上面判断流程走完之后,计算HashMap全局的modCount值,以便对外部并发的迭代操作提供修改的Fail-fast判断提供依据,于此同时增加map中的记录数,并判断记录数是否触及容量扩充的阈值,触及则进行一轮resize操作;

6、在步骤4中出现碰撞情况时,从步骤7开始展开新一轮逻辑判断和处理;

7、判断key索引到的节点(暂且称作被碰撞节点)的hash、key是否和当前待插入节点(新节点)的一致,如果是一致的话,则先保存记录下该节点;如果新旧节点的内容不一致时,则再看被碰撞节点是否是树(TreeNode)类型,如果是树类型的话,则按照树的操作去追加新节点内容;如果被碰撞节点不是树类型,则说明当前发生的碰撞在链表中(此时链表尚未转为红黑树),此时进入一轮循环处理逻辑中;

8、循环中,先判断被碰撞节点的后继节点是否为空,为空则将新节点作为后继节点,作为后继节点之后并判断当前链表长度是否超过最大允许链表长度8,如果大于的话,需要进行一轮是否转树的操作;如果在一开始后继节点不为空,则先判断后继节点是否与新节点相同,相同的话就记录并跳出循环;如果两个条件判断都满足则继续循环,直至进入某一个条件判断然后跳出循环;

9、步骤8中转树的操作treeifyBin,如果map的索引表为空或者当前数组长度还小于64(最大转红黑树的索引数组表长度),那么进行resize操作就行了;否则,如果被碰撞节点不为空,那么就顺着被碰撞节点这条树往后新增该新节点;

10、最后,回到那个被记住的被碰撞节点,如果它不为空,默认情况下,新节点的值将会替换被碰撞节点的值,同时返回被碰撞节点的值(V)。

HashMap为什么要加链表

解决哈希冲突 即具有相同桶下标的键值对使用一个链表储存

HashMap为什么要加红黑树

为了提升查询的效率,HashMap 中存在一个阈值,当桶中的元素量超过这个阈值时,桶的数据结构就会从链表转变成红黑树。

HashMap扩容后 原数据会在哪一个位置?

HashMap 扩容后,原来的元素,要么在原位置,要么在原位置+原数组长度 那个位置上

HashMap为什么从头插法变为尾插法

HashMap在jdk1.7中采用头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。 而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了

HashMap数组的长度为什么是2的n次方

索引位置 hash&(length-1) 2的n次方 实际就是1后面n个0 2的n次方-1 实际就是n个1 这样hash始终和一个大部分数字为1的值做与运算 能够减少hash冲突 使得元素尽量均匀的分配到数组中

为什么说HashMap是线程不安全的

在JDK1.8中,在并发执行put操作时会发生数据覆盖的情况。

并发修改异常

原因: 在进行迭代集合的过程中,一旦修改集合数据,会报出并发修改异常 ConcurrentModificationException
Fail-Fast 与 Fail-Safe
fail-fast 一旦发现遍历的同时其它人来修改,则立刻抛异常
fail-safe 发现遍历的同时其它人来修改,应当能有应对策略,例如牺牲一致性来让整个遍历运行完成
ArrayList 是 fail-fast 的典型代表,遍历的同时不能修改,尽快失败
CopyOnWriteArrayList 是 fail-safe 的典型代表,遍历的同时可以修改,原理是读写分离

Java中线程安全的Map集合

JAVA中线程安全的map有:Hashtable、synchronizedMap、ConcurrentHashMap。

HashMap 和HashTable的区别

*(1)线程安全性不同*

HashMap是线程不安全的,HashTable是线程安全的,其中的方法是Synchronize的,在多线程并发的情况下,可以直接使用HashTable,但是使用HashMap时必须自己增加同步处理。

*(2)是否提供contains方法*

HashMap只有containsValue和containsKey方法;HashTable有contains、containsKey和containsValue三个方法,其中contains和containsValue方法功能相同。

*(3)key和value是否允许null值*

Hashtable中,key和value都不允许出现null值。HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。

*(4)数组初始化和扩容机制*

HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。

Hashtable扩容时,将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。

泛型中extends和super的区别

  1. <? extends T>表示包括T在内的任何T的子类
  2. <? super T>表示包括T在内的任何T的父类

深拷贝与浅拷贝的理解

深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。

1.浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象

2.深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的类执行指向的不是同一个对象

Exception和Error有什么区别

Error类和Exception类都是继承Throwable类

Error(错误)是系统中的错误,程序员是不能改变的和处理的,是在程序编译时出现的错误,只能通过修改程序才能修正。一般是指与虚拟机相关的问题,如系统崩溃,虚拟机错误,内存空间不足,方法调用栈溢等。对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。

Exception(异常)表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。

Exception(异常)表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。

Exception又分为两类

CheckedException:(编译时异常) 需要用try——catch显示的捕获,对于可恢复的异常使用CheckedException。

UnCheckedException(RuntimeException):(运行时异常)不需要捕获,对于程序错误(不可恢复)的异常使用RuntimeException

常见的RuntimeException异常

(1)java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对象。

(2)java.lang.ClassNotFoundException 指定的类找不到;出现原因:类的名称和路径加载错误;通常都是程序试图通过字符串来加载某个类时可能引发异常。

(3)java.lang.NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数字型字符。

(4)java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生。

(5)java.lang.IllegalArgumentException 方法传递参数错误。

(6)java.lang.ClassCastException 数据类型转换异常

IO流

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2kYsC88x-1668612267074)(C:\Users\86184\AppData\Roaming\Typora\typora-user-images\1668147993633.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TRfulfHK-1668612267075)(C:\Users\86184\AppData\Roaming\Typora\typora-user-images\1668147975636.png)]

字节流如何转为字符流

字节输入流转字符输入流通过 InputStreamReader 实现,该类的构造函数可以传入 InputStream 对象。

字节输出流转字符输出流通过OutputStreamWriter 实现,该类的构造函数可以传入 OutputStream 对象。

什么是 java 序列化,如何实现 java 序列化?

序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。

序 列 化 的 实 现 : 将 需 要 被 序 列 化 的 类 实 现 Serializable 接 口 , 该 接 口 没 有 需 要 实 现 的 方 法 , implements Serializable 只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造一个ObjectOutputStream(对象流)对象,接着,使用 ObjectOutputStream 对象的 writeObject(Object obj)方法就可以将参数为 obj 的对象写出(即保存其状态),要恢复的话则用输入流。

Java高级

并发和并行

并发针对单核 CPU 而言,它指的是 CPU 交替执行不同任务的能力;并行针对多核 CPU 而言,它指的是多个核心同时执行多个任务的能力。

单核 CPU 只能并发,无法并行;换句话说,并行只可能发生在多核 CPU 中。

在多核 CPU 中,并发和并行一般都会同时存在,它们都是提高 CPU 处理任务能力的重要手段。

设计模式

你了解的设计模式

spring : spring IOC 核心设计思想是 工厂模式

​ spring中Bean对象默认都是单例的 singleton 单例设计模式 spring依赖注入时,使用了 双重判断加锁 的单例模式。

​ Spring AOP 代理模式

什么是动态代理

使用一个代理将对象包装起来,然后用该代理对象取代原始对象。任何对原始对象的调用都要通过代理。

基于接口实现动态代理: JDK动态代理

基于继承实现动态代理: Cglib动态代理

单例设计模式

饿汉式 所谓饿汉式,就是直接创建出类的实例化,然后用private私有化,对外只用静态方法暴露。 线程安全 但是造成内存浪费

懒汉式 所谓懒汉式,就是在需要调用的时候再创建类的实例化。 但是线程不安全

懒汉式 改进版 1 +synchronized 关键字 但是效率太低

​ gaijinban2 双重检查

事务

什么是事务: 事务是逻辑上一组操作,组成这组操作各个逻辑单元,要么一起成功,要么一起失败。

事务的四个特性 ACID

  1. 原子性(atomicity):事务是不可再分的

  2. 一致性(consistency):操作前与操作后数据保持一致

  3. 隔离性(isolation):多个事务执行时,互不影响

  4. 持久性(durability):一旦执行成功,就将永久保存到数据库中

事务并发引起一些读的问题

  1. 脏读 一个事务可以读取另一个事务未提交的数据

  2. 不可重复读 一个事务可以读取另一个事务已提交的数据 单条记录前后不匹配

  3. 虚读(幻读) 一个事务可以读取另一个事务已提交的数据 读取的数据前后多了点或者少了点

    如何解决: 设置事务隔离等级

    • read uncommitted(0) (读取未提交):最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。

    • read committed(2) – oracle默认隔离级别 读已提交

      • (读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
    • repeatable read(4) --MySQL默认隔离级别

      • (可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。 mysql默认
    • Serializable(8)

      • (可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

什么是分布式事务

分布式事务就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。

分布式事务场景:不同应用相同数据库,相同应用不同数据库,不同应用不同数据库。

​ 分布式事务产生的原因:分布式系统异常除了本地事务那些异常之外,还有:机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失…

CAP原理

一致性:在分布式系统中的所有数据备份,在同一时刻是否同样的值。

可用性:数据备份的重要性

分区容忍性:区间可能会失败

总结:CAP理论就是说在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们无法避免的。所以我们只能在一致性和可用性之间进行权衡,没有系统能同时保证这三点。要么选择CP、要么选择AP。

BASE原理

基本可用:基本可用是指分布式系统在出现故障的时候,允许损失部分可用性,即保证核心可用。

软状态:软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性

最终一致性:最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态

总结:数据允许在一段时间内的不一致,只要保证最终一致就可以了。

分布式事务解决方案

消息事务+最终一致性

基于消息中间件的两阶段提交往往用在高并发场景下,将一个分布式事务拆成一个消息事务(A系统的本地操作+发消息)+B系统的本地操作,其中B系统的操作由消息驱动,只要消息事务成功,那么A操作一定成功,消息也一定发出来了,这时候B会收到消息去执行本地操作,如果本地操作失败,消息会重投,直到B操作成功,这样就变相地实现了A与B的分布式事务。

特点:适用于高并发场景

JVM

对象的内存布局

对象在堆内存中的存储布局可以划分为三个部分:对象头(Header) 、实例数据(Instance Data)和对齐填充( Padding)。

对象头分为对象标记(markWord)和类元信息(class painter),类元信息存储的是指向该对象类元数据(klass)的首地址。

MarkWord 存储 对象的HashCode、分代年龄和锁标志位

反射

反射原理

① 反射其实就是一个类认清自己的过程

② 类会在本地被编译成成.class 文件,该文件在类加载的时 候会存放到堆内存中。堆内存中,一个对象包括了对象头, 实例数据 ,实例填充。而对象头包括 markword 和类元 信息(.Class 信息)(markword 包括 hashcode 值,GC 标 识,分代年龄) 该类元信息中记录了该类的基本内容+该类镜像。由于类 的基本信息是存放到了一个由 c++ 实现 instanceKlass (类元信息)对象中,是无法直接通过 java 方式获取,但是 jvm 设计者在 C++对象中保存一个 java 镜像,我们就可以通过 Object.getClass() 获取到镜像,从而获取该对象的实 例就可以执行我们想要执行的方法。

反射机制

所谓的反射机制就是java语言在运行时拥有一项自观的能力。通过这种能力可以彻底的了解自身的情况为下一步的动作做准备。

Java的反射机制的实现要借助于4个类:class,Constructor,Field,Method;

其中class代表- 类对 象,Constructor-类的构造器对象,Field-类的属性对象,Method-类的方法对象。通过这四个对象我们可以粗略的看到一个类的各个组 成部分。

Java反射的作用:

在Java运行时环境中,对于任意一个类,可以知道这个类有哪些属性和方法。对于任意一个对象,可以调用它的任意一个方法。这种动态获取类的信息以及动态调用对象的方法的功能来自于Java 语言的反射(Reflection)机制。

Java 反射机制提供功能

在运行时判断任意一个对象所属的类。

在运行时构造任意一个类的对象。

在运行时判断任意一个类所具有的成员变量和方法。

在运行时调用任意一个对象的方法

类加载器有哪几种

什么是类加载器

实现通过类的权限定名获取该类的二进制字节流的代码块叫做类加载器。

启动类加载器(Bootstrap)C++

扩展类加载器(Extension)Java

应用程序类加载器(AppClassLoader)Java

用户自定义加载器 Java.lang.ClassLoader 的子类,用户

可以定制类的加载方式。

java类加载过程

加载 验证解析 和初始化

双亲委派机制

① 当 AppClassLoader 加载一个 class 时,它首先不会自己 去尝试加载这个类,而是把类加载请求委派给父类加载 器 ExtClassLoader 去完成。

② 当 ExtClassLoader 加载一个 class 时,它首先也不会自己 去尝试加载这个类,而是把类加载请求委派给 BootStrapClassLoader 去完成。

③ 如果 BootStrapClassLoader 加载失败(例如在 $JAVA_HOME/jre/lib 里未查找到该 class),会使用

ExtClassLoader 来尝试加载;④ 若 ExtClassLoader 也加载失败,则会使用 AppClassLoader 来加载

⑤ 如果 AppClassLoader 也加载失败,则会报出异常 ClassNotFoundException

如果一个类 加载器收到了类加载的请求,它首先不会自己去尝试加载 这个类,而是把请求委托给父加载器去完成,依次向上

JVM结构图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D7rl1seC-1668612267076)(C:\Users\86184\AppData\Roaming\Typora\typora-user-images\1667827304586.png)]

Java垃圾回收机制

在java中,程序员是不需要显示的去释放一个对象的内存的,而是由虚拟机自行执行。在JVM中,有一个垃圾回收线程,它是低优先级的,在正常情况下是不会执行的,只有在虚拟机空闲或者当前堆内存不足时,才会触发执行,扫描那些没有被任何引用的对象,并将它们添加到要回收的集合中,进行回收。

GC是什么?为什么要GC

GC就是垃圾回收

垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。

Java 中的引用类型

  1. 强引用:发生 gc 的时候不会被回收。

  2. 软引用:有用但不是必须的对象,在发生内存溢出之前会被回收。

  3. 弱引用:有用但不是必须的对象,在下一次GC时会被回收。

  4. 虚引用(幽灵引用/幻影引用):无法通过虚引用获得对象,用 PhantomReference 实现虚引用,虚引用的用途是在 gc 时返回一个通知。

如何判断对象是否可以被回收?什么时候被回收?

一般有两种方法来判断 引用计数器法 (不用)

可达性分析算法 : 从 GC Roots 开始向下搜索,搜索所走过的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连时,则证明此对象是可以被回收的

当对象对当前使用这个对象的应用程序变得不可触及的时候,这个对象就可以被回收了。

JVM 的垃圾回收算法

1.标记-清除算法 : 标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。

2.复制算法(JDK1.8) : 按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。

3.标记-压缩算法 : 标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。

4.分代算法 : 根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

简述java内存分配

内存分配:

栈区:栈分为java虚拟机栈和本地方法栈

堆区:堆被所有线程共享区域,在虚拟机启动时创建,唯一目的存放对象实例。堆区是gc的主要区域,通常情况下分为两个区块年轻代和年老代。更细一点年轻代又分为Eden区,主要放新创建对象,From survivor 和 To survivor 保存gc后幸存下的对象,默认情况下各自占比 8:1:1。

方法区:被所有线程共享区域,用于存放已被虚拟机加载的类信息,常量,静态变量等数据。被Java虚拟机描述为堆的一个逻辑部分。习惯是也叫它永久代(permanment generation)

程序计数器:当前线程所执行的行号指示器。通过改变计数器的值来确定下一条指令,比如循环,分支,跳转,异常处理,线程恢复等都是依赖计数器来完成。线程私有的。

java内存回收策略以及Minor GC和Major GC

(1)对象优先在堆的Eden区分配。

(2)大对象直接进入老年代。

(3)长期存活的对象将直接进入老年代。

当Eden区没有足够的空间进行分配时,虚拟机会执行一次Minor GC.Minor GC通常发生在新生代的Eden区,在这个区的对象生存期短,往往发生GC的频率较高,回收速度比较快;Full Gc/Major GC 发生在老年代,一般情况下,触发老年代GC的时候不会触发Minor GC,但是通过配置,可以在Full GC之前进行一次Minor GC这样可以加快老年代的回收速度。

什么情况下会产生StackOverflowError(栈溢出)和OutOfMemoryError(堆溢出)?怎么排查?

引发 StackOverFlowError 的常见原因

· 无限递归循环调用(最常见)。

· 执行了大量方法,导致线程栈空间耗尽。

· 方法内声明了海量的局部变量。

· native 代码有栈上分配的逻辑,并且要求的内存还不小,比如 java.SocketInputStream.read0 会在栈上要求分配一个 64KB 的缓存(64位 Linux)。

引发 OutOfMemoryError的常见原因

· 内存bai中加载的数据量过于庞大,如一次从数据库取出过多数据。

· 集合类中有对对象的引用,使用完后未清空,dao使得JVM不能回收。

· 代码中存在死循环或循环产生过多重复的对象实体。

· 启动参数内存值设定的过小。

排查:可以通过jvisualvm进行内存快照分析,参考https://wwwblogs/boboooo/p/13164071.html

更多推荐

面试-总结

本文发布于:2023-04-30 09:17:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/88100e28a91bc3c6e117159c0a84ba13.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!

  • 113773文章数
  • 28855阅读数
  • 0评论数