面试题目自存

JVM

构成:类加载器 运行时数据区 执行引擎

1.类加载器分为:

引导类加载器:jre/lib目录下的核心类库:rt.jar charest.jar

扩展类加载器:jre/lib/ex目录下的jar类包

应用程序类加载器:classpath目录下的jar包 自己写的java类

自定义加载器:加载用户自定义路径下的类包

2.运行时数据区分为方法区 堆 栈

堆区:

1.存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)

2.jvm只有一个堆区(heap)被所有线程共享,堆中不存放基本类型和对象引用,只存放对象本身

堆分为年轻区 老年区 永久区(元空间)

年轻区又分为eden区和Survivor区

当eden区内存不足就会触发young GC,从而对年轻区的对象进行回收,而无法回收的对象就会转移到Survivor区

Survivor区又分为from区和to区,刚转移过来的数据会放到from区,而每次触发到youngGc的时候就在from区和to区来回移动,每移动一次对象的gc年龄就会+1,当达到15次还无法回收的时候就会转移到老年区

栈区:

1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中

2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。

3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。

方法区:

1.又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。

2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。

判断对象是否可回收

一个类没有被使用就可以回收了

1.引用计数器:实现简单效率高 但是需要额外内存而且对复杂循环引用或相互依赖但不再使用的对象可能会导致不可回收

2.可达性分析:先确定肯定不可回收的对象作为GC ROOT

暂停用户的所有线程,并使用这些gcroot向下搜索 寻找直接或间接的引用对象,不可达的就可以进行回收

jvm分代年龄为什么是15

java类分为对象头 实例数据 对齐填充

对象头中又分为两部分:一部分用来存储对象自身运行时的相关信息(hashcode 锁状态 分代年龄等),另一部分是类型指针,用来指向对象对应的class对象的地址

其中对象头中分代年龄的大小为四个字节 最大值也就是15

设置15次,是在保证垃圾回收的效率前提下,尽可能减少移动到老年代中的对象数量

垃圾回收方法

1、标记清除(Mark Sweep)

标记清除从根集合进行两次扫描,首先对存活的对象进行标记,在标记完成后统一回收所有未被标记的对象。

存活对象比较多的情况下使用,多用于老年代。需要扫描两次,效率比较低,容易产生碎片。

2、拷贝(Copying)

拷贝算法将可用内存按容量划分为大小相等的两块,从根集合进行一次扫描,对存活的对象进行标记,然后移动到复制另外一块内存空间中,然后再把已使用过的内存空间清理掉。

存活对象比较少的情况下使用,只扫描一次,没有内存碎片,多用于年轻代。造成空间浪费,需要调整对象的引用。

3、标记整理(Mark-Compact)

标记整理,标记过程仍然与“标记-清除”算法一样,首先对存活的对象进行标记,其次不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

不会产生内存碎片,不会造成空间浪费,多用于老年代。需要扫描两次,需要调整对象的引用,效率比较低。

JVM、JRE、JDK三者共同构成了Java的运行和开发环境

JVM和JRE是Java运行环境的核心,而JDK则是Java开发环境的核心。以下是这三者的详细介绍:

JVM(Java Virtual Machine,Java虚拟机)是Java程序运行的核心,它模拟了Java程序的运行环境,使得Java程序能够在不同的操作系统上运行,实现了Java语言的跨平台特性。

JRE(Java Runtime Environment,Java运行时环境)是运行Java程序所需的环境集合,它包括了JVM和Java核心类库,但JRE不包括开发工具,如编译器和调试器。

JDK(Java Development Kit,Java开发工具包)是Java语言的软件开发工具包,它不仅包括了JRE的全部内容,还额外提供了代码编译工具和运行工具,适合用于Java程序的开发。

简而言之,JVM保证了Java语言的跨平台性,而JRE和JDK则分别提供了运行和开发Java程序所需的完整环境。

在Java中,static关键字用于修饰成员变量和方法,使其成为静态变量或静态方法。

静态变量和静态方法与类的实例无关,它们属于类本身,而不是类的单个实例。以下是静态变量的主要特点和作用:

共享性:静态变量是类级别的,所有实例对象共享同一个静态变量。当一个实例对象修改了静态变量的值,这个修改对所有实例对象都是可见的。

生命周期:静态变量的生命周期与类的生命周期一致,它们在类被加载时初始化,在程序结束时销毁。静态变量的值会一直保存在内存中,直到程序结束。

直接通过类名访问:静态变量不依赖于实例对象,可以直接通过类名访问,不需要创建对象。例如:ClassName.staticVariable。

全局变量:静态变量可以在类的任何地方被访问,包括静态方法、实例方法、构造方法等,因此它可以被用作全局变量,在整个类中都可以访问到。

不占用内存空间:每个实例对象都有自己独立的内存空间,用于存储非静态成员变量。静态变量不属于任何一个实例对象,因此不占用每个对象的内存空间。

通过类名直接调用:静态成员不需要通过对象来进行访问,而是直接通过类名来访问。只要类被加载,Java虚拟机就可以根据类名找到它们。

综上所述,静态变量的作用是在类级别上提供共享数据,它们不属于任何一个特定实例对象,而是与整个类相关联。这种特性使得静态变量在需要跨实例共享数据时非常有用,例如在多线程编程中作为共享状态的一部分。同时,由于静态变量直接通过类名访问,它们也可以作为全局变量使用,简化代码访问方式。

SpringBoot

启动过程

1.启动main方法

2.初始化配置

3.创建应用上下文

创建bean工厂对象

4.刷新上下文

配置上下文类的加载器和对象发布处理器

注册并实例化bean工厂发布处理器 bean发布处理器,对类包进行扫描解析

初始化bean对象(tomcat服务器)

实例化bean工厂缓存的bean

3.6 发布通知-通知上下文刷新完成(启动tomcat服务器)

5.通知监听者

Spring Boot常用注解

@SpringBootApplication: SpringBootConfiguration配置类、componentScan扫描包、EnableAutoConfiguration导入其他配置类

@RestController: @ResponseBody和@Controller的作用。

@Component,@Service,@Controller,@Repository: 将类注入容器。

@GetMapping、@PostMapping、@PutMapping、@DeleteMapping: 映射请求,只能接收的对应的请求。

@AutoWired: 按照类型匹配注入。

@Qualifier: 和AutoWired联合使用,在按照类型匹配的基础上,在按照名称匹配。

@Resource: 按照名称匹配依赖注入。

@Bean: 用于将方法返回值对象放入容器。

@RequestParam: 获取查询参数。即url?name=这种形式

@RequestBody: 该注解用于获取请求体数据(body),get没有请求体,故而一般用于post请求。

@PathVariable: 获取路径参数。即url/{id}这种形式。

@Value: 将外部的值动态注入到 Bean 中。

    @Value(“${}”):可以获取配置文件的值。

    @Value(“#{}”):表示SpEl(Spring Expression Language是Spring表达式语言,

@SpringBootApplication注解在内部是如何工作的

@SpringBootApplication = @ComponentScan + @EnableAutoConfiguration + @Configuration

@Configuration 所有带注释的类都被视为SpringBoot的广告配置,它们有资格创建 bean 并返回到 IOC 容器

@ComponentScan 所有带注释的类都将通过包(在哪里寻找)进行扫描,并帮助创建这些类的实例。

@EnableAutoConfiguration 会寻找类路径。基础框架附带了一个名为auto-configure的内置库,它为我们完成了这项工作。它检测某些类的存在以及类路径上的存在,并为我们自动配置它们。

自动装配

SpringBoot自动装配的本质就是通过Spring去读取META-INF/spring.factories中保存的配置类文件然后加载bean定义的过程。

如果是标了@Configuration注解,就是批量加载了里面的bean定义

如何实现 “自动”:通过配置文件获取对应的批量配置类,然后通过配置类批量加载bean定义,只要有写好的配置文件spring.factories就实现了自动。

Spring Boot微服务有哪些层

控制器层:所有具有 API 端点定义方法的控制器。类使用@RestController注解进行注解。

Repository/DAO层:所有repository接口都包含在这一层中,用于查询已选择的数据库(SQL/no SQL)。接口使用@Repository注解进行注解。

服务层:所有业务逻辑都包含在这里。通常在该层访问DAO层以执行一些操作。类使@Service注解进行注解。

接口层/interface层:interface接口类

实体层:映射到现实世界的所有类都包含在该层中。通常所有与 ORM 相关的注释都放在这些类中。— 如果它与 MySQL 连接,我们用@Entity注释表名。

— 如果它连接到 MongoDB,我们用@Document注释集合名称。

SpringBoot读取配置的方式

  1. 配置比较少的情况

使用@Value注解@Value("${person.name:默认值}")

  1. 配置比较多的情况

使用@ConfigurationProperties注解@ConfigurationProperties(prefix = "person")

  1. environment.getProperty("person.name");
  2. 读取外部配置properties

结合@PropertySources@PropertySource@Value三个注解实现

@PropertySources({@PropertySource(value = "classpath:custom.properties",encoding = "UTF-8")})

  1. 读取外部配置yml

PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();

YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();

yamlPropertiesFactoryBean.setResources(new ClassPathResource("custom.yml"));

6.properties.getProperty("person.age")

this.getClass().getClassLoader().getResourceAsStream("custom.properties"), UTF_8);

properties.load(inputStreamReader);

Spring Boot项目中可以替换Tomcat服务器吗

如果需要,我们可以通过在 POM 中添加 maven 排除来删除 Tomcat 服务器。实际上,Web 服务器捆绑在started-web Spring Boot starter 依赖项中。应该添加排除。

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-web</artifactId>

<exclusions>

<exclusion>

<groupId>org.springframework.boot</groupId>

<artifactId> spring-boot-starter-tomcat</artifactId>

</exclusion>

</exclusions>

</dependency>

然后我们必须在删除 tomcat 后添加一个 Web 服务器。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jetty</artifactId>

</dependency>

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-jetty</artifactId>

</dependency>

解决循环依赖

循环依赖分为三种:直接依赖 间接依赖 自我依赖

Spring提供了三级缓存用于解决循环依赖

第一级缓存用来存储已经实例化完成,可直接使用的完整bean实例

第二级缓存用于存储实例化以后 但是还没有设置属性的,也就是里面的依赖注入还没做的bean。

第三级缓存是用来存放bean工厂,主要用来生成原始的bean对象,并放到第二个缓存里面

三级缓存就是将bean的实例化与bean的依赖注入进行分离 从而解决循环依赖。

Springboot实现AOP切面编程

使用场景(安全检查、事务、日志、线程控制)、

引入aop依赖(spring-boot-starter-aop)、

配置文件添加aop配置、

创建切面类(@aspect注解声明切面类)、

@Pointcut 声明切面点、

声明切入方法(5种:@Before前置通知、@After后置通知-无视方法异常、@AfterReturning带有返回值切面-方法政策执行结束、@AfterThrowing异常处理切面、@Around环切)

Spring Cloud

Spring Cloud 流应用程序启动器是基于Spring Boot的Spring集成应用程序,提供与外部系统的集成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。

核心组件Eureka

@EnableEurekaServer

Eureka Client:负责将这个服务的信息注册到Eureka Server中

Eureka Server:注册中心,里面有一个注册表,保存了各个服务所在的机器和端口号

核心组件Feign

@FeignClient注解:Feign就会针对这个接口创建一个动态代理

  1. 对某个接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理
  2. 调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心
  3. Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址
  4. 针对这个地址,发起请求、解析响应

核心组件Ribbon

负载均衡

默认使用轮询方式进行负载

Eureka,Feign,Ribbon三者关系

首先Ribbon会从 Eureka Client里获取到对应的服务注册表,也就知道了所有的服务都部署在了哪些机器上,在监听哪些端口号。

然后Ribbon就可以使用默认的Round Robin算法,从中选择一台机器

Feign就会针对这台机器,构造并发起请求。

核心组件Hystrix

是隔离、熔断以及降级的一个框架

服务熔断:某个微服务挂掉之后,直接返回报错信息不进行请求。

服务降级:将故障原因及未完成操作进行记录。

核心组件Zuul

网关,负责网络路由

简单理解为对外有一个地址供前端调用,对后续的请求进行分发。

统一的降级、限流、认证授权、安全

Redis

Redis是一个基于Key-Value存储结构的开源内存数据库,也是一种NoSQL数据库。

它支持多种数据类型,包括String、Map、Set、ZSet和List,以满足不同应用场景的需求。

Redis以内存存储和优化的数据结构为基础,提供了快速的读写性能和高效的数据访问。常被用作应用与数据库之间的缓存组件,提升数据IO效率。

此外,Redis支持主从复制、哨兵机制和集群方式,实现高可用性和水平扩展。

总而言之,Redis是一款功能强大、灵活且可靠的数据库解决方案,适用于各种企业级应用开发场景。

热KEY

热 Key 问题是指在缓存系统中,某些特定的缓存key受到高频访问,导致对这些热门数据的读取/写入操作集中在少数几个缓存节点上,使得这些节点的负载过高,而其他节点负载较轻甚至空闲。这会造成系统性能不均衡,可能导致部分请求响应变慢或服务不可用。

解决热 Key 问题有这些方案:

1. 缓存预热:在系统启动或业务低峰期,通过批量加载或预先访问热门数据,将这些热门数据提前加载到缓存中。这样可以避免大量请求同时涌入导致的热点问题,提高系统的稳定性和性能。

2. 动态散列:将缓存节点组织成一个哈希环,根据缓存键的哈希值将数据分散存储在多个节点上。通过增加缓存节点的数量,让请求更均匀地分布在各个节点上,减轻热Key 对单个节点的压力。当节点数量发生变化时,可以通过一致性哈希算法进行平滑迁移,避免数据大规模迁移带来的负载过高。

3. 数据分片:将数据按特定规则(如数据范围、业务维度等)分成多个片段,分别存储在不同的缓存节点上。这样可以使热Key 所在的数据尽量均匀地分布在多个节点上,减轻单个节点的压力。

通过综合使用以上解决方案,有效地应对热 Key 问题,提高缓存系统的性能和稳定性。

Redis真的是单线程吗?

所谓的redis单线程其实指的是在网络IO和键值对读写时是通过一个线程完成的。而其他的一些模块比如说持久化存储、集群支撑模块这些都是多线程的。

那为什么网络操作模块和数据存储模块不用多线程呢?

其实非常简单,首先网络IO模块的性能瓶颈就不在CPU上,而是要提升我们的IO利用率,虽然使用多线程能带来一些提升,但是多线程也是存在一定的弊端的,首先是多线程模型下的共享资源和并发控制非常复杂,线程的上线文切换也会带来一定的性能损耗,所以Redis在这块采用的是IO多路复用。

另一方面,Redis的绝大部分操作都是在内存中完成的,内存操作本来就比硬盘读写快了百倍以上,并且在数据结构上也进行了大量的优化,比如hash表和跳表。而使用单线程还能避免多线程下的锁竞争,省去了线程的时间和性能开销也不会存在锁竞争的问题。

Redis是如何解决Hash冲突的?

redis是通过我们的链式hash来解决我们的hash冲突问题,哈希算法产生的哈希值的长度是固定并且是有限的,比如说我们通过MD5算法生成32位的散列值,那么它能生成出来的长度则是有限的,我们的数据如果大于32位是不是就可能存在不同数据生成同一个散列值,那么redis通过链式hash,以不扩容的前提下把有相同值的数据链接起来,但是如果链表变得很长就会导致性能下降,那么redis就采用了rehash的机制来解决,类似于hashmap里面的扩容机制,但是redis中的rehash并不是一次把hash表中的数据映射到另外一目录表,而是通过了一种渐进式的方式来处理,将rehash~散到多次请求过程中,避免阻塞耗时。

Redis处理过期的数据

我们可以通过expire来设置redis的过期时间那么通常情况下我们有三种方式来进行处理过期数据:

定时删除,每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。

惰性删除,惰性删除其实就是当访问数据时判断,如果数据已经过期的话直接删除并返回null回去,它可以只占用很少的一部分系统资源来处理但是这种策略的也有一定的缺陷在面对冷数据会占用空间删除不及时,造成空间浪费,只能靠redis数据淘汰策略来进行处理。

另一种是随机删除,随机删除是每隔一段时间随机取出设置了过期时间的一定数量的key进行检查,然后删除其中过期的key。如果超过了百分之25%的数据过期了,就会再次执行。它通过限制删除操作的时长和频率来减少对CPU和内存空间占用的影响,但是它的删除效果并没有定时制除的效果好,同时也沒有比惰性删除占用的系统资源低。

而我们的redis则采用的是惰性删除与随机删除

Redis保证与数据库的双写一致性

缓存可以提升性能,减轻数据库压力,在获取这部分好处的同时,它却带来了一些新的问题,缓存和数据库之间的数据一致性问题。

首先我们来看看一致性:

* 强一致性

* 弱一致性

解决双写一致性方案:

* 延迟双删

* 延迟双删策略是分布式系统中数据库存储和缓存数据保持一致性的常用策略,但它不是强一致。

* 实现思路:也是非常简单的,先删除缓存然后更新DB在最后延迟 N 秒去再去执行一次缓存删除

* 弊端:小概率会出现不一致情况、耦合程度高

* 通过MQ进行重试删除

* 更新完DB之后进行删除,如果删除失败则向MQ发送一条消息,然后消费者不断进行删除尝试

* binlog异步删除

* 实现思路:低耦合的解决方案是使canal。
canal伪装成mysq的从机,监听主机mysql的二进制文件,当数据发生变化时发送给MQ。最终消费进行删除

缓存穿透

缓存穿透代表的意思是在我们的缓存中没有找到缓存信息,数据库也不存在,那么我们在高并发场景下就会面临所有的请求都会直接打到DB,缓存则失去了它原本的意义,并且极有可能导致数据库压力过大而造成服务不可用。

* 缓存空结果信息

* 布隆过滤器(不存在的一定不存在,存在的可能不存在,通过bitmap实现,想深入布隆过滤器可以专门去看看这部分专题内容)

* 过滤常见非法参数,拦截大部分无效请求

缓存击穿

缓存击穿代表的意思是我们数据库中存在数据,但是缓存中不存在数据.这种场景一般是在缓存失效时发生的.在高并发的场景下极有可能瞬间打垮数据库.

* 我们可以考虑面对请求量大的热点接口直接将缓存设置永不过期.

* 当然我们也可能碰到一些特殊场景不能设置永久缓存,那么我们可以在db为空时设置互斥锁,当查询完db更新至缓存时再释放锁

缓存雪崩

缓存雪崩代表是意思是我们在某一个时间段,碰到大量热点缓存数据过期导致大量请求直接打垮数据库

* 我们可以考虑面对请求量大的热点接口直接将缓存设置永不过期.

* 缓存过期时间可以设置一个随机的波动值,防止大量数据在同一时间过期

Redis持久化方法

持久化就是将数据从内存保存到磁盘的过程,其目的就是为了防止数据丢失。

1.快照方式(RDB,RedisDataBase):将某个时刻的内存数据,以二进制的方式写入磁盘;由于是二进制写入磁盘,因此效率较高,但是一旦 Redis 意外终止,就会导致数据部分丢失;

2.文件追加方式(AOF,AppendOnlyFile):记录所有的操作命令,并以文本的形式追加到文件中;由于是文本形式写入,因此效率较低,又由于 AOF 会记录操作指令,因此保证了数据的完整性。

3.混合持久化方式:Redis 4.0 之后新增的⽅式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前数据以 RDB 形式写入文件的开头,在将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能加降低数据丢失的风险。

持久化策略的设置

在连接服务器终端之后使用 redis-cli 命令开启 Redis 客户端,通过以下命令使用不同的持久化策略:

RDB(快照方式):当 AOF 和混合持久化都没有开启的情况下默认是 RDB 持久化策略;

AOF(文件追加方式):在没有开启混合持久化的情况下,使用 config set appendonly yes 开启;

混合持久化:使用 config set aof-use-rdb-preamble yes 直接开启混合持久化。

不同持久化策略之间的优缺点

RDB(快照方式)

优点:

RDB 内容为二进制数据,占用内存小,更紧凑,适合作为备份文件。

RDB 对灾难恢复非常有用,他是一个紧凑的文件,可以更快的传输到远程服务器进行 Redis 服务。

RDB 可以提高 Redis 的运行速度,每次持久化 Redis 主进程都会 fork() 一个子进程,进行数据持久化到磁盘,Redis 主进程不会执行磁盘 I/O 等操作。

与 AOF 格式文件相比, RDB 文件可以更快的重启,因为文本形式写入磁盘效率低于二进制方式写入磁盘。

缺点:

由于 RDB 只能保存某个时间段的数据,因此一旦中途 Redis 服务意外终止,则会丢失一段时间的 Redis 数据。

RDB 需要经常 fork() 才能使用子进程将其持久化在硬盘上,因此一旦数据集过大, fork()可能会很耗时,并且如果再加上 CPU 性能不佳,可能会导致 Redis 停止为客户端服务几毫秒甚至一秒钟。

AOF(文件追加方式)

优点:

AOF 持久化保存的数据更加完整。因为 AOF 提供了三种保存策略:“每次操作保存、每秒种保存一次、跟随系统的持久化策略保存”,其中每秒保存一次,从数据的安全性和性能两方面考虑都是个不错的选择,也是 AOF 的默认策略,即使发生意外,最多丢失 1s 钟的数据;

AOF 采用命令追加的写入方式,所有不会出现文件损坏问题,即使由于意外情况,也可以使用 redis-check-aof 工具轻松修复;

AOF 持久化文件,跟容易解析,因为他是把所有 Redis 键值操作命令,以文件的方式存入磁盘,即使不小型使用 flushall 命令删除了所有的键值信息,只要使用 AOF 文件,删除最后的 flushall 命令,重启 Redis 即可恢复之前误删的数据。

缺点:

对于相同数据集来说,AOF文件要大于RDB文件。

在 Redis 负载比较高的情况下, RDB比AOF性能更好。

RDB 使用快照持久化 Redis 数据,而 AOF 使用命令追加到 AOF 文件中,因此, RDB 比 AOF 更健壮。

混合持久化

优点:

结合了 RDB 和 AOF 持久化的优点,开头为 RDB 格式,使得 Redis 可以更快启动,同时结合 AOF 优点,降低了大量丢失数据的风险。

缺点:

AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件可读性变的更差。

兼容性差。混合持久化 AOF 文件,就不能用在 Redis 4.0 之前的版本了。

分布式锁

CAP:

一致性高可用性分区容错性

当在分布式模型下,数据只有一份(或有限制),此时需要利用锁技术来控制某一时刻修改数据的进程数。这种锁即为分布式锁

分布式锁应该具备哪些条件

互斥性:在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;

高可用的获取锁与释放锁;

高性能的获取锁与释放锁;

可重入性:具备可重入特性,具备锁失效机制,防止死锁,即就算一个客户端持有锁的期间崩溃而没有主动释放锁,也需要保证后续其他客户端能够加锁成功

非阻塞:具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

分布式锁分类

基于数据库的分布式锁

  1. 悲观锁

利用select … where xx=yy for update排他锁

注意:这里需要注意的是where xx=yy,xx字段必须要走唯一索引,否则会锁表。

优点:

简单易用,好理解,保障数据强一致性。

缺点:

1)在 RR 事务级别,select 的 for update 操作是基于间隙锁(gap lock) 实现的,是一种悲观锁的实现方式,所以存在阻塞问题。

2)高并发情况下,大量请求进来,会导致大部分请求进行排队,影响数据库稳定性,也会耗费服务的CPU等资源。高并发情况下,也会造成占用过多的应用线程,导致业务无法正常响应。

3)如果优先获得锁的线程因为某些原因,一直没有释放掉锁,可能会导致死锁的发生。

4)锁的长时间不释放,会一直占用数据库连接,可能会将数据库连接池撑爆,影响其他服务。

5)MySql数据库会做查询优化,即便使用了索引,优化时发现全表扫效率更高,则可能会将行锁升级为表锁,此时可能就更悲剧了。

6)不支持可重入特性,并且超时等待时间是全局的,不能随便改动。

  1. 乐观锁

update xx set version=new_version where xx=yy and version=Old_version,通过增加递增的版本号字段实现乐观锁

优点:

实现简单,复杂度低

保障数据一致性

缺点:

性能低,并且有锁表的风险

可靠性差

非阻塞操作失败后,需要轮询,占用CPU资源

长时间不commit或者是长时间轮询,可能会占用较多的连接资源

基于ZooKeeper的分布式锁

基于ZooKeeper实现分布式锁的步骤如下:

创建一个目录mylock;

线程A想获取锁就在mylock目录下创建临时顺序节点;

获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;

线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;

线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

优点:

可靠性非常高

性能较好

CAP模型属于CP,基于ZAB一致性算法实现

缺点:

性能并不如Redis(主要原因是在写操作,即获取锁释放锁都需要在Leader上执行,然后同步到follower)

实现复杂度高

基于Redis的分布式锁

主要是基于命令:SETNX key value

实现思想的具体步骤:

获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。

获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

优点:

性能非常高

可靠性较高

CAP模型属于AP

缺点:

复杂度较高

无一致性算法,可靠性并不如Zookeeper

锁删除失败 过期时间不好控制

非阻塞,获取失败后,需要轮询不断尝试获取锁,比较消耗性能,占用cpu资源

分布式锁对比

从理解的难易程度角度(从低到高):数据库 > 缓存 > Zookeeper

从实现的复杂性角度(从低到高):Zookeeper >= 缓存 > 数据库

从性能角度(从高到低):缓存 > Zookeeper >= 数据库

从可靠性角度(从高到低):Zookeeper > 缓存 > 数据库

数据库

三级范式

一级范式 属性不可再分

二级范式 消除主属性部分依赖

三级范式 消除对主属性的传递依赖

BCNF范式 消除多值依赖

索引失效

  1. 使用or进行多条件查询
  2. 使用左like查询
  3. 对列进行了计算:加减乘除
  4. 对列进行了函数计算:to_char,to_date
  5. is not null等判断条件
  6. 对类型进行了转换,int -》String

SQL优化

  1. 避免使用select *
  2. 使用union all 来替代union
  3. 小表驱动大表:使用数据少的表来作为条件反查数据量大的表。
  4. 批量插入:使用insert(list)来进行实现
  5. 善用limit:查询一条可以直接在sql中加上limit 1
  6. 判断是否存在,不进行计数count 而是判断limit 1是否有返回
  7. 创建索引
  8. limit太多时候,可以使用between and

mybatis实现批量插入、批量删除

1、foreach collection="list" item="item" open="(" close=")" separator="),("

2、sqlSessionFactory方法 openSession session.commit()

表连接方式和各自之间的区别

左连接(left join-返回左表全部数据)

右连接(right join-返回右表全部数据)

内连接(inner join-返回共同数据)

外连接(full join-返回两张表全部数据)

mybatis实现分页

pageHelper:pageSize、pageIndex

使用limit offset rows

数据库常用函数和作用

length-长度、

to_date-转换成日期格式、

to_char-转换成字符串格式、

substr-截取子字符串、

upper-大写、

lower-小写

CONCAT 连接字符串

REPLACE 字符串替换

基础

继承 封装 多态

封装:把所有的属性私有化,对每个属性提供getter和setter方法,如果有一个带参的构造函数的话,那一定要写一个不带参的构造函数

继承:子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为

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

八种基础类型

int(32位) long(64位) short(16位) byte(8位)

boolean(1位)char(16位)float(32位)double(64位)

“==”和equals

“==”和equals 最大的区别是“==”是运算符,如果是基本数据类型,则比较存储的值;如果是引用数据类型,则比较所指向对象的地址值。

equals是Object的方法,比较的是所指向的对象的地址值,一般情况下,重写之后比较的是对象的值。

Integer var = 209;boolean res = (var == 209?true:false)的结果是什么?为什么

false;对于 Integer var=? 在-128至127之间的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,

线程的五种状态

new 新建-start 就绪-run 运行-dead 死亡 阻塞

常用设计模式有哪些

单例模式、工厂模式、装饰模式、观察者模式

多线程的执行顺序

  1. thread.join()

public static void main(String[] args) {

final Thread t1 = new Thread(() -> System.out.println("线程1执行"));

Thread t2 = new Thread(() -> {

try {

t1.join();

} catch (InterruptedException e) {

e.printStackTrace();

}

System.out.println("线程2执行");

});

final Thread t3 = new Thread(() -> {

t2.join();

System.out.println("线程3执行");

});

t3.start();

t2.start();

t1.start();

}

  1. CountDownLatch一个线程(或者多个), 等待另外N个线程完成某个事情之后才能执行

public static void main(String[] args) {

System.out.println("全体学生开始考试,一共有两个学生");

new Thread(() -> {

System.out.println("第一个学生开始交卷,countDownLatch减1");

countDownLatch.countDown();

}).start();

new Thread(() -> {

System.out.println("第二个学生开始交卷,countDownLatch减1");

countDownLatch.countDown();

}).start();

countDownLatch.await();

System.out.println("老师清点试卷,在此之前,只要有一个学生没交即countDownLatch不为0时,不能离开考场");

}

  1. newSingleThreadExecutor单线程化的线程池

public static void main(String[] args) {

Thread t1 = new Thread(() -> System.out.println("线程1"), "线程1");

Thread t2 = new Thread(() -> System.out.println("线程2"), "线程2");

Thread t3 = new Thread(() -> System.out.println("线程3"));

ExecutorService executor = Executors.newSingleThreadExecutor();

// 将线程依次加入到线程池中

executor.submit(t1);

executor.submit(t2);

executor.submit(t3);

// 及时将线程池关闭

executor.shutdown();

  1. CompletableFuture

public static void main(String[] args) {

Thread t1 = new Thread(() -> System.out.println("线程1"), "线程1");

Thread t2 = new Thread(() -> System.out.println("线程2"), "线程2");

Thread t3 = new Thread(() -> System.out.println("线程3"));

CompletableFuture.runAsync(() -> t1.start())

.thenRun(() -> t2.start())

.thenRun(() -> t3.start());

}

线程池的7个参数

核心线程数 最大线程数 线程存活时间 线程存活时间单位 任务队列 线程工厂 拒绝策略

核心线程数大小设置

(1)CPU密集型任务,比如找出1-1000000中的素数。

CPU密集型任务的特点,线程在执行任务时会一直使用CPU,所以对于这种情况,就要尽量避免发生线程上下文的切换。比如我们至于一个CPU,如果这两个吸纳成同时执行找素数的任务,那么这个CPU就需要进行额外的上下文切换,从而达到线程并行的效果。

所以对于CPU密集型的任务,线程数量最好就等于核心线程数。

只不过,为了应对线程执行过程发生缺页中断或者其他异常导致线程阻塞的请求,我们可以额外多设置一个线程,这样当某个线程不需要CPU时,可以有替补线程继续使用CPU。

所以对于CPU密集型任务,我们可以设置线程数为CPU核心数+1。

(2)IO密集型任务,比如文件IO、网络IO

线程在执行IO型任务时,可能大量时间都阻塞在IO上,假如现在有10个CPU,如果我们只设置了10个线程来执行IO型任务,那么可能这10个线程都阻塞在了IO上,这样10个CPU都没活干了,所以我们通常设置为2*CPU核心数。

通常,如果IO型任务执行的时间越长,那么阻塞在IO上的线程就越多,我们可以设置更多的线程,但是线程并不是越多越好,我们可以通过下式计算:

线程数 = CPU核心数 * (1 + 线程等待时间 / 线程运行总时间)

线程等待时间:线程没有使用CPU的时间

线程总时间:指的是线程执行完某个任务的总时间

可以使用jvisualvm抽样来估计这两个时间。

(3)混合型任务

类的生命周期

实例化 属性赋值 初始化 注销

序列化与反序列化

序列化的目的是解决网络通信之间的对象传输,比如将一个对象跨网络传输到另一个jvm中进行使用

序列化就是将内存中一个对象转换为字节流进行传输,保证通信双方对于对象的可识别性

反序列化就是根据从文件或者网络上获取的对象字节流,并根据字节流保存的的描述信息和状态,重新构建一个新的对象

将对象转化为json xml 而后转换为字节流进行传输,接收到字节流后进行转换构建

String,StringBuffer, StringBuilder 的区别是什么?String 为什么是不可变的?

1、String 是字符串常量,StringBuffer 和 StringBuilder 都是字符串变量。后两者的字符内容可变,而前 者创建后内容不可变。 2、String 不可变是因为在 JDK 中 String 类被声明为一个 final 类。 3、StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的。 ps:线程安全会带来额外的系统开销,所以 StringBuilder 的效率比 StringBuffer 高。如果对系统中的线 程是否安全很掌握,可用 StringBuffer,可以在线程不安全处加上关键字 Synchronize。

Java 集合类框架的基本接口有哪些?

集合类接口指定了一组叫做元素的对象。集合类接口的每一种具体的实现类都可以选择以它自己的方式对 元素进行保存和排序。有的集合类允许重复的键,有些不允许。Java 集合类提供了一套设计良好的支持对 一组对象进行操作的接口和类。

Java 集合类里面最基本的接口有:

Collection:代表一组对象,每一个对象都是它的子元素。

Set:不包含重复元素的 Collection。

List:有顺序的 collection,并且可以包含重复元素。

Map:可以把键(key)映射到值(value)的对象,键不能重复。

Session, Cookie 区别

  1. Session 由应用服务器维护的一个服务器端的存储空间;Cookie 是客户端的存储空间,由浏览器维护。
  2. 用户可以通过浏览器设置决定是否保存 Cookie,而不能决定是否保存 Session,因为 Session 是由服务器端维护的。
  3. Session 中保存的是对象,Cookie 中保存的是字符串。
  4. Session 和 Cookie 不能跨窗口使用,每打开一个浏览器系统会赋予一个 SessionID,此时的 SessionID 不同,若要完成跨浏览器访问数据,可以使用 Application。
  5. Session、Cookie 都有失效时间,过期后会自动删除,减少系统开销。

对 Spring 的理解,项目中都用什么?怎么用的?对 IOC、和 AOP 的理解及实现原理

Spring 是一个开源框架,处于 MVC 模式中的控制层,它能应对需求快速的变化,其主要原因它有一种面向切面编程(AOP)的优势,其次它提升了系统性能,因为通过依赖倒置机制(IOC),系统中用到的对象 不是在系统加载时就全部实例化,而是在调用到这个类时才会实例化该类的对象,从而提升了系统性能。 这两个优秀的性能使得 Spring 受到许多 J2EE 公司的青睐,如阿里里中使用最多的也是Spring 相关技术。

Spring 的优点

  1. 降低了组件之间的耦合性,实现了软件各层之间的解耦。
  2. 可以使用容易提供的众多服务,如事务管理,消息服务,日志记录等。
  3. 容器提供了 AOP 技术,利用它很容易实现如权限拦截、运行期监控等功能。
  4. Spring 中 AOP 技术是设计模式中的动态代理模式。只需实现 jdk 提供的动态代理接口 InvocationHandler, 所有被代理对象的方法都由 InvocationHandler 接管实际的处理任务。面向切面编程中还要理解切入点、 切面、通知、织入等概念。
  5. Spring 中 IOC 则利用了 Java 强大的反射机制来实现。所谓依赖注入即组件之间的依赖关系由容器在运行 期决定。其中依赖注入的方法有两种,通过构造函数注入,通过 set 方法进行注入。

HashTable,HashMap,TreeMap 区别?

  1. HashTable 线程同步,HashMap 非线程同步。
  2. HashTable 不允许<键,值>有空值,HashMap 允许<键,值>有空值。
  3. HashTable 使用 Enumeration,HashMap 使用 Iterator。
  4. HashTable 中 hash 数组的默认大小是 11,增加方式的 old*2+1,HashMap 中 hash 数组的默认大小 是 16,增长方式一定是 2 的指数倍。
  5. TreeMap 能够把它保存的记录根据键排序,默认是按升序排序。

wait()与 sleep()的区别

关于这两者已经在上面进行详细的说明,这里就做个概括好了:

sleep()来自 Thread 类,和 wait()来自 Object 类。

调用 sleep()方法的过程中,线程不会释放对象锁。 而调用 wait 方法线程会释放对象锁

sleep()睡眠后不出让系统资源,wait 让其他线程可以占用 CPU

sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒.而wait()需要配合 notify()或者 notifyAll()使用

SimpleDateFormat 是线程安全的吗

非常不幸,DateFormat 的所有实现,包括 SimpleDateFormat 都不是线程安全的,因此你不应该在多线程序中使用,除非是在对外线程安全的环境中使用,如将 SimpleDateFormat 限制在 ThreadLocal 中。如果你不这么做,在解析或者格式化日期的时候,可能会获取到一个不正确的结果。因此,从日期、 时间处理的所有实践来说,我强力推荐 joda-time 库。

什么是值传递和引用传递?

值传递是对基本型变量而言的,传递的是该变量的一个副本,改变副本不影响原变量. 引用传递一般是对于对象型变量而言的,传递的是该对象地址的一个副本, 并不是原对象本身 。 一般认为,java 内的传递都是值传递. java 中实例对象的传递是引用传递 。

值传递:简单理解为将数字 1 复制过去一个1进行增加的操作,不会影响原来的1 引用传递:在调用函数时,将实际参数的地址传递到函数中,那么在函数中对参数进行修改, 将会影响到实际参数 比如对 user 对象操作时,实际上传了他的地址,这样在函数中修改姓 名等属性 也会修改 user。

Java 中的 HashMap 的工作原理是什么?

我们知道在 Java 中最常用的两种结构是数组和模拟指针(引用),几乎所有的数据结构都可以利用这两 种来组合实现,

HashMap 也是如此。实际上HashMap 是一个“链表散列”,如下是它数据结构:最左侧 是一个数组,数组中的每一个元素都是一个链表,链表的每一个元素都是 entry。

HashMap 是基于 hashing 的原理,我们使用 put(key, value)存储对象到 HashMap 中,使用 get(key) 从 HashMap 中获取对象。当我们给 put()方法传递键和值时,我们先对键调用 hashCode()方法,返回的 hashCode 用于找到 bucket 位置来储存 Entry 对象。

GC 是什么?为什么要有 GC?

GC 是垃圾收集的意思,内存处理是编程人员容易出现问题的地方,忘记或者错误的内存回收会导致 程序或系统的不稳定甚至崩溃,Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收 内存的目的,Java 语言没有提供释放已分配内存的显式操作方法。Java 程序员不用担心内存管理,因为垃 圾 收 集 器 会 自 动 进 行 管 理 。 要 请 求 垃 圾 收 集 , 可 以 调 用 下 面 的 方 法 之 一 : System.gc() 或 Runtime.getRuntime().gc() ,但 JVM 可以屏蔽掉显式的垃圾回收调用。

垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存。垃圾回收器通常是作为一个单独的低优 先级的线程运行,不可预知的情况下对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收, 程序员不能实时的调用垃圾回收器对某个对象或所有对象进行垃圾回收。在 Java 诞生初期,垃圾回收是 Java 最大的亮点之一,因为服务器端的编程需要有效的防止内存泄露问题,然而时过境迁,如今 Java 的 垃圾回收机制已经成为被诟病的东西。

String s = new String("xyz");创建了几个字符串对象?

两个对象,一个是静态区的"xyz",一个是用 new 创建在堆上的对象。

List、Map、Set 三个接口存取元素时,各有什么特点?

List 以特定索引来存取元素,可以有重复元素。

Set 不能存放重复元素(用对象的 equals()方法来区分元素是否重复)

Map 保存键值对(key-value pair)映射,映射关系可以是一对一或多对一。

Set 和 Map 容器都有基于哈希存储和排序树的两种实现版本,基于哈希存储的版本理论存取时间复杂度为 O(1), 而基于排序树版本的实现在插入或删除元素时会按照元素或元素的键(key)构成排序树从而达到排序和去 重的效果。

Collection 和 Collections 的区别?

Collection 是一个接口,它是 Set、List 等容器的父接口;

Collections 是个一个工具类,提供了一系 列的静态方法来辅助容器操作,这些方法包括对容器的搜索、排序、线程安全化等等。

线程的 sleep()方法和 yield()方法有什么区别?

① sleep()方法给其他线程运行机会时不考虑线程的优先级,因此会给低优先级的线程以运行的机会; yield()方法只会给相同优先级或更高优先级的线程以运行的机会;

② 线程执行 sleep()方法后转入阻塞(blocked)状态,而执行 yield()方法后转入就绪(ready)状态;

③ sleep()方法声明抛出 InterruptedException,而 yield()方法没有声明任何异常; ④ sleep()方法比 yield()方法(跟操作系统 CPU 调度相关)具有更好的可移植性。

事务的 ACID 是指什么

原子性(Atomic):事务中各项操作,要么全做要么全不做,任何一项操作的失败都会导致整个事务的失败;

一致性(Consistent):事务结束后系统状态是一致的;

隔离性(Isolated):并发执行的事务彼此无法看到对方的中间状态;

持久性(Durable):事务完成后所做的改动都会被持久化,即使发生灾难性的失败。通过日志和同步 备份可以在故障发生后重建数据。

举例说明同步和异步。

如果系统中存在临界资源(资源数量少于竞争资源的线程数量的资源),例如正在写的数据以后可能被 另一个线程读到,或者正在读的数据可能已经被另一个线程写过了,那么这些数据就必须进行同步存取(数据库操作中的排他锁就是最好的例子)。当应用程序在对象上调用了一个需要花费很长时间来执行的方法, 并且不希望让程序等待方法的返回时,就应该使用异步编程,在很多情况下采用异步途径往往更有效率。

事实上,所谓的同步就是指阻塞式操作,而异步就是非阻塞式操作。

简述 synchronized 和 java.util.concurrent.locks.Lock 的异同?

Lock 是 Java 5 以后引入的新的 API,

和关键字 synchronized 相比主要相同点:

Lock 能完成 synchronized 所实现的所有功能;

主要不同点:

Lock 有比 synchronized 更精确的线程语义和更好的性能,而且不强制性的要求一定要获得锁。

synchronized 会自动释放锁,而 Lock 一定要求程序员手工释放, 并且最好在 finally 块中释放(这是释放外部资源的最好的地方)。

什么是 CAS

CAS,全称为 Compare and Swap,即比较-替换。

假设有三个操作数:内存值 V、旧的预期值 A、要修 改的值 B,

当且仅当预期值 A 和内存值 V 相同时,才会将内存值修改为 B 并返回 true,否则什么都不做并 返回 false。当然 CAS 一定要 volatile 变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值, 否则旧的预期值 A 对某条线程来说,永远是一个不会变的值 A,只要某次 CAS 操作失败,永远都不可能成 功。

什么是乐观锁和悲观锁

乐观锁:乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作 尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。

悲观锁:悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像 synchronized,不管三七二十一,直接上了锁就操作资源了。

说说 Java 自动装箱与拆箱

装箱就是自动将基本数据类型转换为包装器类型(int-->Integer); 调用方法:Integer 的 valueOf(int) 方法

拆箱就是自动将包装器类型转换为基本数据类型(Integer-->int)。 调用方法:Integer 的 intValue 方法

说说重载和重写的区别

重写:

  1. 发生在父类与子类之间
  2. 方法名,参数列表,返回类型(除过子类中方法的返回类型 是父类中返回类型的子类)必须相同
  3. 访问修饰符的限制一定要大于被重写方法的访问修饰符 (public>protected>default>private)
  4. 重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查 型异常

重载:

  1. 重载 Overload 是一个类中多态性的一种表现
  2. 重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序)
  3. 重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函 数的区分标准。

深拷贝和浅拷贝的区别是什么?

浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他 对象的引用仍然指向原来的对象.换言之,浅拷贝仅仅复制所考虑的对象,而不复 制它所引用的对象.

深拷贝:被复制对象的所有变量都含有与原来的对象相同的值.而那些引用其他 对象的变量将指向被复制过的新对象.而不再是原有的那些被引用的对象.换言 之.深拷贝把要复制的对象所引用的对象都复制了一遍.

BIO、NIO、AIO 有什么区别

BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,

它的特点是模式 简单使用方便,并发处理能力低。

NIO:New IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。

AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO , 异步 IO 的操作基于事件和回调机制。

说说 B 树和 B+树的区别

  1. B 树,每个节点都存储 key 和 data,所有节点组成这棵树,并且叶子节点指 针为 nul,叶子结点不包含任何关键字信息。
  2. B+树,所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字 记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接,所有的非终 端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键 字。(而 B 树的非终节点也包含需要查找的有效信息)

什么时候会触发 FullGC

(1)调用 System.gc 时,系统建议执行 Full GC,但是不必然执行 (2)老年代空间不足

(3)方法去空间不足

(4)通过 Minor GC 后进入老年代的平均大小 > 老年代的可用内存 (5)由 Eden 区、From Space 区向 To Space 区复制时,对象大小大于 To Space 可用内存,则把该 对象转存到老年代,且老年代的可用内存小于该对象大小。即老年代无法存放下 新年代过度到老年 代的对象的时候,会触发 Full GC。

产生死锁的四个必要条件?

  1. 互斥条件:一个资源每次只能被一个线程使用
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放
  3. 不剥夺条件:进程已经获得的资源,在未使用完之前,不能强行剥夺
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系

线程安全需要保证几个基本特征?

原子性,简单说就是相关操作不会中途被其他线程干扰,一般通过同步机制实现。

可见性,是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通 常被解释为将线程本地状态反映到主内存上,volatile 就是负责保证可见性的。

有序性,是保证线程内串行语义,避免指令重排等。

SpringBoot 实现热部署有哪几种方式?

主要有两种方式: Spring Loaded Spring-boot-devtools

Spring 事务的隔离级别有哪些?分别有哪些特性?

  1. DEFAULT (默认) 这是 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔 离级别。
  2. READ_UNCOMMITTED (读未提交) 这是事务最低的隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。 这种隔离级别会产生脏读,不可重复读和幻像读。
  3. READ_COMMITTED (读已提交) 保证一个事务修改的数据提交后才能被另外一个事务读取,另外一个事务不能读 取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现 不可重复读和幻像读。
  4. REPEATABLE_READ (可重复读) 这种事务隔离级别可以防止脏读、不可重复读,但是可能出现幻像读。它除了保 证一个事务不能读取另一个事务未提交的数据外,还保证了不可重复读。
  5. SERIALIZABLE(串行化) 这是花费最高代价但是最可靠的事务隔离级别,事务被处理为顺序执行。除了防 止脏读、不可重复读外,还避免了幻像读。

行级锁定的优、缺点

优点:

1、当在许多线程中访问不同的行时只存在少量锁定冲突。

  1. 回滚时只有少量的更改
  2. 可以长时间锁定单一的行。

缺点:

  1. 比页级或表级锁定占用更多的内存。
  2. 当在表的大部分中使用时,比页级或表级锁定速度慢,因为你必须获取更多 的锁。
  3. 如果你在大部分数据上经常进行 GROUP BY 操作或者必须经常扫描整个表,比 其它锁定明显慢很多。
  4. 用高级别锁定,通过支持不同的类型锁定,你也可以很容易地调节应用程序, 因为其锁成本小于行级锁定。

分布式幂等性如何设计?

在高并发场景的架构里,幂等性是必须得保证的。

解决方案

1,查询和删除不在幂等讨论范围,查询肯定没有幂等的说,删除:第一次删除 成功后,后面来删除直接返回 0,也是返回成功。

2,建唯一索引:唯一索引或唯一组合索引来防止新增数据存在脏数据 (当表存 在唯一索引,并发时新增异常时,再查询一次就可以了,数据应该已经存在了, 返回结果即可)。 3,token 机制:由于重复点击或者网络重发,或者 nginx 重发等情况会导致数 据被重复提交。前端在数据提交前要向后端服务的申请 token,token 放到 Redis 或 JVM 内存,token 有效时间。提交后后台校验 token,同时删除 token,生成 新的 token 返回。redis 要用删除操作来判断 token,删除成功代表 token 校验 通过,如果用 select+delete 来校验 token,存在并发问题,不建议使用。

4,悲观锁 悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况 选用(另外还要考虑 id 是否为主键,如果 id 不是主键或者不是 InnoDB 存储引 擎,那么就会出现锁全表)。

5,乐观锁,给数据库表增加一个 version 字段,可以通过这个字段来判断是否 已经被修改了

6,分布式锁,比如 Redis 、 Zookeeper 的分布式锁。单号为 key,然后给 Key 设置有效期(防止支付失败后,锁一直不释放),来一个请求使用订单号生成一 把锁,业务代码执行完成后再释放锁。

7,保底方案,先查询是否存在此单,不存在进行支付,存在就直接返回支付结果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值