一些常问的问题

  • 死锁
  • Java SPI
  • String、StringBuffer、StringBuilder 区别
  • cookie和session的区别
  • 为什么有Spring IOC和DI
  • 为什么要用Spring
  • Spring中用到了哪些设计模式
  • Spring AOP原理
  • SQL优化
  • 包装类
    • 为什么两个Integer的对象不能用==来判断(-128~127陷阱)
    • Integer与int的区别
  • Mysql的性能调优
    • 如何定位哪些SQL需要调优
    • 索引的优点和缺点
    • 聚簇索引和非聚簇索引
    • 索引为什么选择B+而不是红黑树或者B树
    • 单列索引和联合索引
    • 索引失效的时候
    • 什么情况下不建立索引
    • InnoDB和MyISAm的区别
  • MySQL的锁机制和MVCC
    • MySQL update 是锁行还是锁表
    • MVCC 过程中会加锁吗
    • 三种并发场景
      • 读读
      • 读写
      • 写写
  • Mysql的ACID特性实现
  • InnoDB如何解决幻读
    • 事务隔离级别
    • 脏读、幻读和可重复读
      • 脏读
      • 可重复读
      • 不可重复读
      • 幻读
    • InnoDB如何解决幻读问题
  • Mybatis
    • 分页
    • 缓存机制
      • 一级缓存
      • 二级缓存
  • Spring IOC
    • IOC是什么
    • Bean的定义方式
    • IOC工作流程
    • @Resource和@Autowired的区别
  • Spring Bean
    • Spring Bean的生命周期
      • 创建前准备阶段
      • 创建实例阶段
      • 依赖注入阶段
      • 容器缓存阶段
      • 销毁实例阶段
  • Spring的三级缓存解决循环依赖
  • 对Spring MVC的理解
  • Springboot
    • Springboot的优点
    • SpringBoot 中的 starter 到底是什么 ?
    • 运行 SpringBoot 有哪几种方式?
    • Spring Boot 的核心注解是哪个?
    • Springboot自动装配
    • Springboot Starter
    • Spring Boot 打成的 jar 和普通的 jar 有什么区别 ?
  • Dubbo
    • 整体框架
    • 工作流程
  • RPC
    • 什么是RPC
    • HTTP和RPC的区别
  • 微服务
    • 微服务治理
    • Spring Cloud
    • CAP模型
    • 微服务和分布式的区别
  • Redis
    • Redis数据类型
    • Redis为什么这么快(Redis使用单线程还是多线程,是否有线程安全问题)
    • Redis持久化机制
      • RDB
      • AOF
    • 缓存击穿
  • Zookeeper
    • 分布式系统中的三种典型应用场景
  • Docker
    • docker实现
    • docker工作流程
    • 用docker的好处
    • docker核心组件
  • JVM
    • Class Files类文件
    • Class Loader Subsystem 类加载机制
    • Java内存区域(运行时数据区域)
      • 为什么使用元空间替换了永久代
    • GC
      • 不同的GC介绍
      • 强引用、软引用、弱引用、虚引用
  • K8s Kubernetes
    • 容器编排技术
  • 负载均衡
    • 不同层的负载均衡
    • 负载均衡算法
  • 消息队列
  • 异常
    • 受检异常和非受检异常
  • Volatile关键字
  • 缓存雪崩与缓存穿透
    • 缓存雪崩
    • 缓存穿透
  • ThreadLocal
  • 线程安全
    • 原子性
    • 可见性
    • 有序性
  • Dubbo感知服务下线(Zookeeper)
    • 行锁、间隙锁和临键锁、表锁
    • 可重入锁
    • ReentrantLock 实现原理
      • 什么是ReentrantLock
      • 实现原理
    • CAS(Compare and Swap)
      • 乐观锁与悲观锁
    • Synchronized 锁升级的原理
    • 公平锁和非公平锁
  • Nacos
  • 线程池
    • 当任务数超过线程池的核心线程数时,如何让它不进入队列,而是直接启用最大线程数
    • java官方提供的线程池的特点
  • IO和NIO
    • IO
    • NIO
  • 幂等性问题
  • new String("abc")到底创建了几个对象
  • 限流算法
  • Kafka
    • Kafka避免重复消费
    • 如何保证消费顺序性
  • 序列化和反序列化
  • Hashmap
    • Hashmap什么时候扩容,为什么要扩容
      • HashMap 是如何扩容的?
      • 为什么扩容因子是0.75?
    • Hashmap和HashTable的区别
    • 为什么重写equals() 就一定要重写hashCode() 方法
  • Thread和Ruunable区别
  • 深拷贝和浅拷贝
  • Wait和Sleep的区别
  • 项目的难点(生产环境服务器处理效率变慢)
    • CPU
    • 磁盘IO
    • 内存
  • JVM调优
  • RabbitMQ
    • AMQP
    • 如何保证消息的可靠性
    • 工作模式
    • 高可用的实现
    • 各个MQ的比较
  • 分布式ID
    • 雪花算法

死锁

死锁,简单来说就是两个或者两个以上的线程在执行过程中,去争夺同一个共享资源导
致相互等待的现象。如果没有外部干预,线程会一直处于阻塞状态,无法往下执行。这
样一直等待处于阻塞状态的线程,被称为死锁线程。

死锁出现要满足四个条件:

Java SPI

Java SPI 是Java 里面提供的一种接口扩展机制。

主要思想是将装配的控制权移到程序之外实现标准和实现的解耦,以及提供动态可插拔的能力

它的作用我认为有两个:

  • 标准定义和接口实现分离,在模块化开发中很好的实现了解耦
  • 实现功能的扩展,更好的满足定制化的需求
    • 除了Java 的SPI 以外,基于SPI 思想的扩展实现还有很多,比如Spring 里面的SpringFactoriesLoader。
    • Dubbo 里面的ExtensionLoader,并且Dubbo 还在SPI 基础上做了更进一步优化,提供了激活扩展点、自适应扩展点。

String、StringBuffer、StringBuilder 区别

从四个角度来说明:

  • 第一个,可变性
    String 内部的value 值是final 修饰的,所以它是不可变类。所以每次修改String 的值,都会产生一个新的对象
    StringBuffer 和StringBuilder 是可变类,字符串的变更不会产生新的对象
  • 第二个,线程安全性
    String 是不可变类,所以它是线程安全的
    StringBuffer 是线程安全的,因为它每个操作方法都加了synchronized 同步关键字
    StringBuilder 不是线程安全的。
    所以在多线程环境下对字符串进行操作,应该使用StringBuffer,否则使用StringBuilder
  • 第三个,性能方面。
    String 的性能是最低的,因为不可变意味着在做字符串拼接和修改的时候,需要重新创建新的对象以及分配内存。
    其次是StringBuffer 要比String 性能高,因为它的可变性使得字符串可以直接被修改
    最后是StringBuilder,它比StringBuffer 的性能高,因为StringBuffer 加了同步锁
  • 第四个,存储方面。
    String 存储在字符串常量池里面
    StringBuffer 和StringBuilder 存储在堆内存空间

最后再补充一下, StringBuilder 和StringBuffer 都是派生自AbstractStringBuilder这个抽象类。

cookie和session的区别

(1)浏览器端第一次发送请求到服务器端
(2)服务器端创建Cookie,该Cookie中包含用户的信息,然后将该Cookie发送到浏览器端
(3)浏览器端再次访问服务器端时会携带服务器端创建的Cookie
(4)服务器端通过Cookie中携带的数据区分不同的用户



session是服务器端的容器对象,本质上是一个ConcurrentHashMap,session ID可以弥补HTTP无状态的不足(不能确定客户端就用session ID来确定)

为什么有Spring IOC和DI

用new创建对象依赖关系非常复杂,耦合度高

IOC实现对象管理(创建对象、查找依赖的对象),把创建的对象交给IOC控制,要用到的时候直接去IOC容器找

DI:如果bean之间存在依赖关系,IOC容器需要自动实现依赖对象的实例注入,三种注入方式:

  • 接口注入
  • setter注入
  • 构造器注入

@Resource和@Autowired分别根据bean的name和bean的类型实现依赖注入

为什么要用Spring

Spring 是一个轻量级应用框架,它提供了IoC 和AOP 这两个核心的功能。
它的核心目的是为了简化企业级应用程序的开发,使得开发者只需要关心业务需求,不
需要关心Bean 的管理,
以及通过切面增强功能减少代码的侵入性。
从Spring 本身的特性来看,我认为有几个关键点是我们选择Spring 框架的原因。

  • 轻量:Spring 是轻量的,基本的版本大约2MB。
  • IOC/DI:Spring 通过IOC 容器实现了Bean 的生命周期的管理,以及通过DI 实现依赖注入,从而实现了对象依赖的松耦合管理
  • 面向切面的编程(AOP):Spring 支持面向切面的编程,从而把应用业务逻辑和系统服务分开。
  • MVC 框架:Spring MVC 提供了功能更加强大且更加灵活的Web 框架支持
  • 事务管理:Spring 通过AOP 实现了事务的统一管理,对应用开发中的事务处理提
    供了非常灵活的支持

Spring中用到了哪些设计模式

  • 工厂模式 : Spring 使用工厂模式通过 BeanFactory、ApplicationContext 创建 bean 对象。
  • 代理模式 : Spring AOP 功能的实现。
  • 单例模式 : Spring 中的 Bean 默认都是单例的。
  • 模板方法模式 : Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。
  • 包装器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
  • 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
  • 适配器模式 : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配Controller。

Spring AOP原理


1、第一阶段:创建代理对象阶段
在Spring 中,创建Bean 实例都是从getBean()方法开始的(出现getBean()动画),在实例创建之后,Spring 容器将根据AOP 的配置去匹配目标类的类名,看目标类的类名是否满足切面规则。如果满足满足切面规则,就会调用ProxyFactory 创建代理Bean并缓存到IoC 容器中。(出现调用ProxyFactory 创建代理Bean 动画)根据目标对象的自动选择不同的代理策略。(出现选择代理策略动画)如果目标类实现了接口,Spring会默认选择JDK Proxy,如果目标类没有实现接口,Spring 会默认选择Cglib Proxy,当然,我们也可以通过配置强制使用Cglib Proxy。
2、第二阶段:拦截目标对象阶段
当用户调用目标对象的某个方法时,将会被一个叫做AopProxy 的对象拦截,Spring 将所有的调用策略封装到了这个对象中,它默认实现了InvocationHandler 接口,也就是调用代理对象的外层拦截器。在这个接口的invoke()方法中,按顺序执行符合所有AOP 拦截规则的拦截器链。
3、第三阶段:调用代理对象阶段
Spring AOP 拦截器链中的每个元素被命名为MethodInterceptor其实就是切面配置中的Advice 通知。这个回调通知可以简单地理解为是新生成的代理Bean 中的方法。(出现执行织入代码动画)也就是我们常说的被织入的代码片段,这些被织入的代码片段会在这个阶段执行。
4、第四阶段:调用目标对象阶段
MethodInterceptor 接口也有一个invoke()方法,(出现执行MethodInterceptor的invoke()方法动画)在MethodInterceptor 的invoke()方法中会触发对目标对象方法的调用,也就是反射调用目标对象的方法。(出现调用目标对象动画)
Spring AOP 原理就分析到这里,最后,总结一下不迷路:

1、代理对象:就是由Spring 代理策略生成的对象;

2、目标对象:就是我们自己写的业务代码;

3、织入代码:就是要在我们自己写的业务代码增加的代码片段;

4、切面通知:就是封装织入代码片段的回调方法;

5、MethodInvocation:负责执行拦截器链,在proceed()方法中执行;

6、MethodInterceptor:负责执行织入的代码片段,在invoke()方法中执行。

SQL优化

  • 加索引,但要选择合适的列,避免like,函数导致索引失效
  • 只返回必要的列
  • 根据查询分析器,避免全表扫描和子查询
  • 分库分表(并发连接数过高
  • 读写分离(针对读多写少的场景)

包装类

为什么两个Integer的对象不能用==来判断(-128~127陷阱)

引入享元(Flyweight)模式,通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。

Integer 内部维护了一个IntegerCache,它缓存了-128 到127 这个区间的数值对应的Integer 类型。

一旦程序调用valueOf 方法,如果数字是在-128 到127 之间就直接在cache 缓存数组中去取Integer 对象。

Integer内部对-128-127之间的数据做了一层缓存,如果两个Integer对象取值范围都在-128-127之间,返回结果肯定是true

Integer与int的区别

Integer 和int 的区别有很多,我简单说3 个方面

  • Integer 的初始值是null,int 的初始值是0
  • Integer 存储在堆内存,int 类型是直接存储在栈空间
  • Integer 是对象类型,它封装了很多的方法和属性,我们在使用的时候更加灵活。
  • 基本类型和Integer 类型混合使用时,Java 会自动通过拆箱和装箱实现类型转换
    至于为什么要设计封装类型,最主要的原因是Java 本身是面向对象的语言,一切操作都是以对象作为基础。
    比如像集合里面存储的元素,也只支持存储Object 类型,普通类型无法通过集合来存储。

Mysql的性能调优

如何定位哪些SQL需要调优

SQL优化又能分为三步曲

  • 第一、慢SQL的定位和排查
    可以通过**慢查询日志和慢查询日志分析工具(两个工具mysqldumpslow及pt-query-digest)**得到有问题的SQL列表。MySQL——慢查询日志分析 mysql慢查询分析工具比较与实战

  • 第二、执行计划分析
    针对慢SQL,我们可以使用关键字explain来查看当前sql的执行计划.可以重点关注type key rows filterd等字段,从而定位该SQL执行慢的根本原因。再有的放矢的进行优化

  • 第三、使用show profile工具
    Show Profile是MySQL提供的可以用来分析当前会话中,SQL语句资源消耗情况的工具,可用于SQL调优的测量。在当前会话中.默认情况下处于show profile是关闭状态,打开之后保存最近15次的运行结果

  • 表结构和索引

    • 分库分表
    • 读写分离
    • 为字段选择合适的数据类型
    • 适当的反范式设计
    • 为查询操作创建必要的索引
  • SQL语句优化

    • 通过慢查询分析需要优化的SQL,利用explain或者profile工具分析SQL执行计划
    • 仅查询需要的列
    • 使用索引列扫描
  • Mysql参数优化

    • buffer_pool大小设置为内存的50%-70%
  • 硬件及系统配置

索引的优点和缺点

优点:

  1. 通过B+树的结构来存储数据,可以大大减少数据检索时的磁盘IO 次数,从而提升数据查询的性能
  2. B+树索引在进行范围查找的时候,只需要找到起始节点,然后基于叶子节点的链表结构往下读取即可,查询效率较高。
  3. 通过唯一索引约束,可以保证数据表中每一行数据的唯一性

缺点:

  1. 数据的增加、修改、删除,需要涉及到索引的维护,当数据量较大的情况下,索引的维护会带来较大的性能开销。
  2. 一个表中允许存在一个聚簇索引和多个非聚簇索引,但是索引数不能创建太多,否则造成的索引维护成本过高
  3. 创建索引的时候,需要考虑到索引字段值的分散性,如果字段的重复数据过多,创建索引反而会带来性能降低

聚簇索引和非聚簇索引

聚簇索引(Clustered Index)一般指的是主键索引(如果存在主键索引的话)

在非聚簇索引的叶子节点上存储的并不是真正的行数据,而是主键 ID,所以当我们使用非聚簇索引进行查询时,首先会得到一个主键 ID,然后再使用主键 ID 去聚簇索引上找到真正的行数据,我们把这个过程称之为回表查询。

每个索引都会对应一颗 B+ 树,而聚簇索引和非聚簇索引最大的区别在于叶子节点存储的数据不同聚簇索引叶子节点存储的是行数据,因此通过聚簇索引可以直接找到真正的行数据;而非聚簇索引叶子节点存储的是主键信息,所以使用非聚簇索引还需要回表查询,因此我们可以得出聚簇索引和非聚簇索引的区别主要有以下几个:

  • 聚簇索引叶子节点存储的是行数据;而非聚簇索引叶子节点存储的是聚簇索引(通常是主键 ID)。
  • 聚簇索引查询效率更高,而非聚簇索引需要进行回表查询,因此性能不如聚簇索引。
  • 聚簇索引一般为主键索引,而主键一个表中只能有一个,因此聚簇索引一个表中也只能有一个,而非聚簇索引则没有数量上的限制。

InnoDB下 主键是聚簇索引

MyISAM下 主键不是聚簇索引

innodb聚簇索引和myisam非聚簇索引

索引键值不是数字是怎么解决的(都视为字符串,有时候使用前缀索引)

索引为什么选择B+而不是红黑树或者B树

在大规模数据存储的时候,红黑树往往出现由于树的深度过大而造成磁盘IO读写过于频繁,进而导致效率低下的情况。

磁盘查找存取的次数往往由树的高度所决定

B+树所有的Data域在叶子节点,所有的叶子节点用指针串起来。这样遍历叶子节点就能获得全部数据,这样就能进行区间访问啦。在数据库中基于范围的查询是非常频繁的,而B树不支持这样的遍历操作。

单列索引和联合索引

如果仅用聚集索引的起始列作为查询条件和同时用到复合聚集索引的全部列的查询速度是几乎一样的,甚至比用上全部的复合索引列还要略快(在查询结果集数目一样的情况下);而如果仅用复合聚集索引的非起始列作为查询条件的话,这个索引是不起任何作用的

试验步骤:
(1)建立索引idx1 on col1
执行select * from table1 where col1=A 使用idx1
执行select * from table1 where col1=A and col2=B 也使用idx1

(2)删除索引idx1,然后建立idx2 on (col1,col2)复合索引 执行以上两个查询,也都使用idx2

(3)如果两个索引idx1,idx2都存在
并不是 where col1='A’用idx1;where col1=A and col2=B 用idx2。 其查询优化器使用其中一个以前常用索引。要么都用idx1,要么都用idx2.

由此可见,
(1)对一张表来说,如果有一个复合索引 on (col1,col2),就没有必要同时建立一个单索引 on col1
(2)如果查询条件需要,可以在已有单索引 on col1的情况下,添加复合索引on (col1,col2),对于效率有一定的提高。
(3)同时建立多字段(包含5、6个字段)的复合索引没有特别多的好处,相对而言,建立多个窄字段(仅包含一个,或顶多2个字段)的索引可以达到更好的效率和灵活性

索引失效的时候

  1. 在索引列上做运算,比如使用函数,Mysql 在生成执行计划的时候,它是根据统计信息来判断是否要使用索引的。而在索引列上加函数运算,导致Mysql 无法识别索引列,也就不会再走索引了。
    不过从Mysql8 开始,增加了函数索引可以解决这个问题。
  2. 在一个由多列构成的组合索引中,需要按照最左匹配法则,也就是从索引的最左列开始顺序检索,否则不会走索引
    在组合索引中,索引的存储结构是按照索引列的顺序来存储的,因此在sql 中也需要按照这个顺序才能进行逐一匹配。
    否则InnoDB 无法识别索引导致索引失效。
  3. 索引列存在隐式转化的时候, 比如索引列是字符串类型,但是在sql 查询中没有使用引号。
    那么Mysql 会自动进行类型转化,从而导致索引失效
  4. 索引列使用不等于号、not 查询的时候,由于索引数据的检索效率非常低,因此Mysql 引擎会判断不走索引。
  5. 使用like 通配符匹配后缀%xxx 的时候,由于这种方式不符合索引的最左匹配原则,所以也不会走索引。
    但是反过来,如果通配符匹配的是前缀xxx%,符合最左匹配,也会走索引
  6. 使用or 连接查询的时候or 语句前后没有同时使用索引,那么索引会失效。只有or 左右查询字段都是索引列的时候,才会生效。
  7. 查询条件涉及到大量数据。当查询条件涉及到大量数据时,例如返回表中大部分数据的查询,MySQL 可能会认为使用索引并不高效,因此会放弃使用索引。

什么情况下不建立索引

  1. 数据量太小的情况下,即使没有索引,查询的速度也比较快,这个时候建立索引反而会增加维护成本和查询时间
  2. 数据离散度不高的列,比如性别、年龄这种,创建索引反而会降低检索效率,从底层原理来说,相当于增加了B+树的扫描范围
  3. 存在函数操作的情况,如果查询条件包含函数操作,那这个时候可能不会走索引,所以建了索引意义不大
  4. 频繁变更的表,比如经常需要更新、删除或插入记录,那么对这个表建立索引的开销就会很大,甚至可能影响到整个数据库的性能。

InnoDB和MyISAm的区别

  • 第一个,数据存储的方式不同,MyISAM 中的数据和索引是分开存储的,而InnoDB 是把索引和数据存储在同一个文件里面。
  • 第二个,对于事务的支持不同,MyISAM 不支持事务,而InnoDB 支持ACID 特性的事务处理
  • 第三个,对于的支持不同,MyISAM 只支持表锁,而InnoDB 可以根据不同的情况,支持行锁,表锁,间隙锁,临键锁
  • 第四个,MyISAM 不支持外键,InnoDB 支持外键
  • 性能上存在差异:MyISAM 的读取速度比InnoDB 快,但是在高并发环境下,InnoDB 的性能更好。这是因为InnoDB 支持行级锁和事务处理,而MyISAM 不支持。所以,如果是读多写少的情况下,使用MyISAM 引擎会更合适
  • 数据安全不同:InnoDB 支持崩溃恢复和数据恢复,而MyISAM 不支持。如果MySQL 崩溃了或者发生意外故障,InnoDB 可以通过恢复日志来恢复数据。

MySQL的锁机制和MVCC

https://tonydong.blog.csdn.net/article/details/103324323

MySQL update 是锁行还是锁表

MySQL 的Update 操作既可以锁行,也可以锁表,
具体使用哪种锁类型,取决于执行的Update 语句的条件、事务隔离级别等因素。

  • 如果update 语句中的where 条件包含了索引列,并且只更新一条数据,那这个时候就加行锁
    如果where 条件中不包含索引列,这个时候会加表锁
  • 另外,根据查询范围不同,Mysql 也会选择不同粒度的锁来避免幻读问题。
    比如针对主键索引的for update 操作:
SELECT * FROM t WHERE id = 10 FOR UPDATE;

Mysql 会增加Next-Key Lock 来锁定id=10 索引所在的区间

  • 另外,针对于索引区间的查询或者修改
SELECT * FROM user WHERE id BETWEEN 1 AND 100 FOR UPDATE;

MVCC 过程中会加锁吗

MVCC 机制,全称(Multi-Version Concurrency Control)多版本并发控制,是确保在高并发下,
多个事务读取数据时不加锁也可以多次读取相同的值

MVCC 在**读已提交(READ COMMITTED)、可重复读(REPEATABLE READ 简称RR)**模式下才生效。
MVCC 在可重复读的事物隔离级别下,可以解决脏读、脏写、不可重复读等问题。

我们知道,MVCC 是基于乐观锁的实现,所以很自然的想到MVCC 是不是不会加锁。

每个事务都可以读取已提交的快照,而不需要获得共享锁或排它锁。

在写操作的时候,MVCC 会使用一种叫为**“写时复制”(Copy-On-Write)**的技术,也就是在修改数据之前先将数据复制一份,从而创建一个新的快照。

当一个事务需要修改数据时,MVCC 会首先检查修改数据的快照版本号是否与该事务的快照版本一致,如果一致则表示可以修改这条数据,否则该事务需要等待其他事务完成对该数据的修改。

另外,这个事务在新快照之上修改的结果,不会影响原始数据,其他事务可以继续读取原始数据的快照,从而解决了脏读、不可重复度问题。

所以,正是有了MVCC 机制,让多个事务对同一条数据进行读写时,不需要加锁也不会出现读写冲突。

三种并发场景

读读

就是线程A 与线程B 同时在进行读操作,这种情况下不会出现任何并发问题。

读写

就是线程A 与线程B 在同一时刻分别进行读和写操作。
这种情况下,可能会对数据库中的数据造成以下问题:

  • 事物隔离性问题,
  • 出现脏读,幻读,不可重复读的问题

写写

就是线程A 与线程B 同时进行写操作
这种情况下可能会存在数据更新丢失的问题。
而MVCC 就是为了解决事务操作中并发安全性问题的无锁并发控制技术全称为
Multi-Version Concurrency Control ,也就是多版本并发控制。它是通过数据库记录
中的隐式字段,undo 日志,Read View 来实现的。
MVCC 主要解决了三个问题

  • 第一个是:通过MVCC 可以解决读写并发阻塞问题从而提升数据并发处理能力
  • 第二个是:MVCC 采用了乐观锁的方式实现降低了死锁的概率(并不是完全不发生死锁)
  • 第三个是:解决了一致性读的问题也就是事务启动时根据某个条件读取到的数据,

直到事务结束时,再次执行相同条件,还是读到同一份数据,不会发生变化。

而我们在使用MVCC 时一般会根据业务场景来选择组合搭配乐观锁或悲观锁

这两个组合中,MVCC 用来解决读写冲突,乐观锁或者悲观锁解决写写冲突从而最大
程度的提高数据库并发性能。

Mysql的ACID特性实现

  • A 表示Atomic 原子性,也就是需要保证多个DML 操作是原子的,要么都成功,要么都失败。

    • 通过Undo_LOG实现
  • C 表示一致性,表示数据的完整性约束没有被破坏,这个更多是依赖于业务层面的保证,数据库本身也提供了一些,比如主键的唯一余数,字段长度和类型的保证等等。
  • I 表示事物的隔离性,也就是多个并行事务对同一个数据进行操作的时候,如何避免多个事务的干扰导致数据混乱的问题。
    而InnoDB 实现了SQL92 的标准,提供了四种隔离级别的实现。分别是:
    RU(未提交读)
    RC(已提交读)
    RR(可重复读)
    Serializable(串行化)
    InnoDB 默认的隔离级别是RR(可重复读),然后使用了MVCC 机制解决了脏读和不可重复读的问题,然后使用了行锁/表锁的方式解决了幻读的问题
  • D,表示持久性,也就是只要事务提交成功,那对于这个数据的结果的影响一定是永久性的。
    不能因为宕机或者其他原因导致数据变更失效。

    • 理论上来说,事务提交之后直接把数据持久化到磁盘就行了,但是因为随机磁盘IO 的效率确实很低,所以InnoDB 设计了Buffer Pool 缓冲区来优化,也就是数据发生变更的时候先更新内存缓冲区,然后在合适的时机再持久化到磁盘。
    • 那在持久化这个过程中,如果数据库宕机,就会导致数据丢失,也就无法满足持久性了。
      所以InnoDB 引入了Redo_LOG 文件,这个文件存储了数据被修改之后的值,当我们通过事务对数据进行变更操作的时候,除了修改内存缓冲区里面的数据以外,还会把本次修改的值追加到REDO_LOG 里面
    • 当提交事务的时候,直接把REDO_LOG 日志刷到磁盘上持久化,一旦数据库出现宕机,在Mysql 重启在以后可以直接用REDO_LOG 里面保存的重写日志读取出来,再执行一遍从而保证持久性。

InnoDB如何解决幻读

事务隔离级别

  • 读未提交(Read Uncommitted):该级别下,没有使用任何锁机制,一个事务可以读取另一个事务未提交的数据,容易导致脏读和不可重复读等问题。
  • 读已提交(Read Committed):该级别下,使用读锁(S锁)来保证并发读取的一致性。一个事务只能读取已经提交的数据,避免了脏读问题,但是可能导致不可重复读问题。
  • 可重复读(Repeatable Read):该级别下,使用读锁(S锁)和行级锁(X锁)来保证并发读取的一致性。一个事务在执行期间看到的数据是一致的,即使其他事务在同一时间对数据进行修改,也不会影响该事务读取的数据。但是,该级别下仍然可能出现幻读问题。
  • 串行化(Serializable):该级别下,使用读锁(S锁)和写锁(X锁)来保证事务的串行执行。每个事务都以串行方式执行,即每个事务都必须等待其他事务执行完毕后才能开始执行。这可以避免所有并发问题,但是会降低并发性能。

脏读、幻读和可重复读

脏读

脏读指的是读到了其他事务未提交的数据,未提交意味着这些数据可能会回滚,也就是可能最终不会存到数据库中,也就是不存在的数据。读到了并一定最终存在的数据,这就是脏读。

可重复读

可重复读指的是在一个事务内,最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。通常针对数据更新(UPDATE)操作。

不可重复读

对比可重复读,不可重复读指的是在同一事务内,不同的时刻读到的同一批数据可能是不一样的,可能会受到其他事务的影响,比如其他事务改了这批数据并提交了。通常针对数据更新(UPDATE)操作

幻读

幻读是针对数据插入(INSERT)操作来说的。假设事务A对某些行的内容作了更改,但是还未提交,此时事务B插入了与事务A更改前的记录相同的记录行,并且在事务A提交之前先提交了,而这时,在事务A中查询,会发现好像刚刚的更改对于某些数据未起作用,但其实是事务B刚插入进来的,让用户感觉很魔幻,感觉出现了幻觉,这就叫幻读。
两次查询相同的范围时,得到的结果不一致

第一个事务里面我们执行了一个范围查询,这个时候满足条件的数据只有一条
第二个事务里面,它插入了一行数据,并且提交了
接着第一个事务再去查询的时候,得到的结果比第一查询的结果多出来了一条数据。

InnoDB如何解决幻读问题

间隙锁和next-key Lock机制

幻读是指在同一个事务中,前后两次查询相同的范围时,得到的结果不一致!
注意,这里强调的是范围查询,也就是说,noDB引擎要解决幻读问题,必须要保证一个点,就是如果一个事务通过

类似这样的语句确定范围的时候,需要阻塞类似

的插入语句

当对查询范围id>4 and id<7加锁的时候,会针对B+树中(4,7)这个开区间范围的索引加间隙锁。
意味着在这种情况下,其他事务对这个区间的数据进行插入、更新、删除都会被锁住。

next-key Lock能锁多个区间:

Mybatis

分页

  • 第一种,在Mybatis Mapper 配置文件里面直接写分页SQL,这种方式比较灵活,实现也
    简单。
  • 第二种,使用Mybatis 提供的RowBounds 对象,一次性加载所有符合查询条件的目标数据,根据分页参数值在内存中实现分页,实现内存级别分页。
  • 第三种,基于Mybatis 里面的Interceptor 拦截器,在select 语句执行之前动态拼接分页关键字,插件(PageHelper)就是用这个实现的。

缓存机制

Mybatis 里面设计了二级缓存来提升数据的检索效率,避免每次数据的访问都需要去查询数据库。

一级缓存

一级缓存,是SqlSession 级别的缓存,也叫本地缓存,因为每个用户在执行查询的时候都需要使用SqlSession 来执行,

为了避免每次都去查数据库,Mybatis 把查询出来的数据保存到SqlSession 的本地缓存中,后续的SQL 如果命中缓存,就可以直接从本地缓存读取了。

二级缓存

如果想要实现跨SqlSession 级别的缓存?那么一级缓存就无法实现了,因此在Mybatis 里面引入了二级缓存,就是当多个用户

在查询数据的时候,只有有任何一个SqlSession 拿到了数据就会放入到二级缓存里面,
其他的SqlSession 就可以从二级缓存加载数据。

每个(如图)一级缓存的具体实现原理是:
在SqlSession 里面持有一个Executor,每个Executor 中有一个LocalCache 对象。

当用户发起查询的时候,Mybatis 会根据执行语句在Local Cache 里面查询,如果没命中,再去查询数据库并写入到LocalCache,否则直接返回。

所以,以及缓存的生命周期是SqlSessiion,而且在多个Sqlsession 或者分布式环境下,可能会导致数据库写操作出现脏数据。

(如图)二级缓存的具体实现原理是:
使用CachingExecutor 装饰了Executor,所以在进入一级缓存的查询流程之前,会先通过CachingExecutor 进行二级缓存的查询。

开启二级缓存以后,会被多个SqlSession 共享,所以它是一个全局缓存。因此它的查询流程是先查二级缓存,再查一级缓存,最后再查数据库。

另外,MyBatis 的二级缓存相对于一级缓存来说,实现了SqlSession 之间缓存数据的共享,同时缓存粒度也能够到namespace 级别,并且还可以通过Cache 接口实现类不同的组合,对Cache 的可控性也更强。

Spring IOC

IOC是什么

IOC 的全称是Inversion Of Control, 也就是控制反转,它的核心思想是把对象的管理权限交给容器

(动态出现图2)应用程序如果需要使用到某个对象实例,直接从IOC 容器中去获取就行,这样设计的好处是降低了程序里面对象与对象之间的耦合性

使得程序的整个体系结构变得更加灵活。

Bean的定义方式

Spring 里面很多方式去定义Bean,(如图)比如XML 里面的标签、@Service、@Component、@Repository、@Configuration 配置类中的@Bean 注解等等。

Spring 在启动的时候,会去解析这些Bean 然后保存到IOC 容器里面。

IOC工作流程

  • IOC容器初始化:根据程序中定义的XML或者注解等Bean的声明方式解析或加载后生成BeanDefinition,将其注册到IOC容器中;解析得到的BeanDefinition实体包含bean的一些定义和对象属性,把BeanDefinition实体存到map集合里面,IOC就是对这些定义信息进行处理和维护

  • Bean的初始化和依赖注入:通过反射对没有设置lazy-init属性的单例bean进行初始化,完成bean依赖注入

  • Bean 的使用:通常我们会通过@Autowired 或者BeanFactory.getBean()从IOC 容器中获
    取指定的bean 实例

@Resource和@Autowired的区别

  • @Autowired 是根据type 来匹配,@Resource 可以根据name 和type 来匹配,
    默认是name 匹配
  • @Autowired 是Spring 定义的注解,@Resource 是JSR 250 规范里面定义的注
    解,而Spring 对JSR 250 规范提供了支持。
  • @Autowired 如果需要支持name 匹配,就需要配合@Primary 或者@Qualifier
    来实现。

Spring Bean

Spring Bean的生命周期

Spring生命周期全过程大致分为五个阶段:创建前准备阶段创建实例阶段、依赖注入阶段、容器缓存阶段和销毁实例阶段。

实例化->Instantiation
属性赋值->Populate
初始化->Initialization
销毁->Destruction
实例化 -> 属性赋值 -> 初始化 -> 销毁

  • Bean 在开始加载之前,需要从上下文和相关配置中解析并查找Bean 有关的扩展实现
  • 通过反射来创建Bean 的实例对象,并且扫描和解析Bean 声明的一些属性。
  • 如果被实例化的Bean 存在依赖其他Bean 对象的情况,则需要对这些依赖bean 进行对象注入。比如常见的@Autowired、setter 注入等依赖注入的配置形式。

创建前准备阶段


这个阶段主要是在开始Bean 加载之前,从Spring 上下文和相关配置中解析并查找Bean 有关的配置内容,比如init-method-容器在初始化bean 时调用的方法、destory-method,容器在销毁Bean 时调用的方法。
以及,BeanFactoryPostProcessor 这类的bean 加载过程中的前置和后置处理。

创建实例阶段

这个阶段主要是通过反射来创建Bean 的实例对象,并且扫描和解析Bean 声明的一些属性

依赖注入阶段


在这个阶段,会检测被实例化的Bean 是否存在其他依赖,如果存在其他依赖,就需要对这些被依赖Bean 进行注入。比如通过读取@Autowired、@Setter 等依赖注入的配置。

在这个阶段还会触发一些扩展的调用,比如常见的扩展类:BeanPostProcessors(用来实现Bean 初始化前后的回调)、InitializingBean 类(这个类有一个afterPropertiesSet()方法,给属性赋值)、还有BeanFactoryAware 等等。

容器缓存阶段


容器缓存阶段主要是把Bean 保存到IoC 容器中缓存起来,到了这个阶段,Bean 就可以被开发者使用了。
这个阶段涉及到的操作,常见的有,init-method 这个属性配置的方法,会在这个阶段调用。
在比如BeanPostProcessors 方法中的后置处理器方法如:postProcessAfterInitialization,也是在这个阶段触发的。

销毁实例阶段


这个阶段,是完成Spring 应用上下文关闭时,将销毁Spring 上下文中所有的Bean。如果Bean 实现了DisposableBean 接口,或者配置了destory-method 属性,将会在这个阶段被调用。

Spring的三级缓存解决循环依赖

所谓三级缓存,其实就是用来存放不同类型的Bean。

  • 第一级缓存存放完全初始化好的Bean,这个Bean 可以直接使用了
  • 第二级缓存存放原始的Bean 对象,也就是说Bean 里面的属性还没有进行赋值
  • 第三级缓存存放Bean 工厂对象,用来生成原始Bean 对象并放入到二级缓存中
    假设BeanA 和BeanB 存在循环依赖,那么在三级缓存的设计下,我画了这样一个图来描述工作原理。

  • 初始化BeanA,先把BeanA 实例化,然后把BeanA 包装成ObjectFactory 对象保存到三级缓存中。
  • 接着BeanA 开始对属性BeanB 进行依赖注入,于是开始初始化BeanB,同样做两件事,创建BeanB 实例,以及加入到三级缓存。
  • 然后,BeanB 也开始进行依赖注入,在三级缓存中找到了BeanA,于是完成BeanA的依赖注入
  • BeanB 初始化成功以后保存到一级缓存,于是BeanA 可以成功拿到BeanB 的实例,从而完成正常的依赖注入。

整个流程看起来很复杂,但是它的核心思想就是把Bean 的实例化和Bean 中属性的依赖注入这两个过程分离出来

不过要注意的是,Spring 本身只能解决单实例存在的循环引用问题,但是存在以下四种情况需要人为干预:

  • 多实例的Setter 注入导致的循环依赖,需要把Bean 改成单例。
  • 构造器注入导致的循环依赖,可以通过@Lazy 注解
  • DependsOn 导致的循环依赖,找到注解循环依赖的地方,迫使它不循环依赖。
  • 单例的代理对象Setter 注入导致的循环依赖,
    • 可以使用@Lazy 注解,
    • 或者使用@DependsOn 注解指定加载先后关系。

对Spring MVC的理解

是属于springFramework的一个模块,在Servlet基础上构建并且使用了MVC模式,简化Servlet+JSP开发方式

  1. 把传统MyC框架里面的Controller控制器做了拆分,分成了前端控制器Dispatcherservlet和后端控制器Controller
  2. 把Mode摸型折分成业务层Service和数据访问层Repository
  3. 在视图层,可以支特不同的视图让如Frggmark、velocity、JSP等等

Spring MVC的工作流程如下:

  1. 用户发送请求至前端控制器(DispatcherServlet)。
  2. 前端控制器(DispatcherServlet)收到请求,调用处理器映射器(HandlerMapping)。
  3. 处理器映射器找到具体的处理器(可以根据XML配置、注解、实现接口进行查找),生成处理器对象以及处理器拦截器(如果有则生成)一并返回给前端控制器。
  4. 前端控制器调用处理器适配器(HandlerAdapter)。
  5. 处理器适配器根据适配调用具体的处理器。
  6. 处理器执行完成返回ModelAndView。
  7. 处理器适配器把处理器返回的ModelAndView返回给前端控制器。
  8. 前端控制器把ModelAndView发送给视图解析器(ViewResolver)。
  9. 视图解析器解析ModelAndView,返回具体的视图对象。
  10. 前端控制器根据视图对象进行视图渲染,最终返回结果给客户端。

1、第一阶段:配置阶段
配置阶段,主要是完成对xml 配置和注解配置。
具体步骤如下:
首先,从web.xml 开始,配置DispatcherServlet 的url 匹配规则Spring 主配置文件的加载路径然后,就是配置注解,比如@Controller、@Service、@Autowrited 以及@RequestMapping 等。
2、第二阶段:初始化阶段
初始化阶段,主要是加载并解析配置信息以及IoC 容器、DI 操作和HandlerMapping的初始化。
具体步骤如下:
首先,Wer 容器启动以后,会由Web 容器自动调用DispatcherServlet 的init()方法。
然后,在init()方法中,会初始化IoC 容器,IoC 容器其实就是个Map。
紧接着,根据配置好的扫描包路径,扫描出相关的类,并利用反射进行实例化,存放到IoC 容器中
缓存之后,Spring 将再次迭代扫描IoC 容器中的实例,给需要自动赋值的属性自动赋值。哪些属性需要自动赋值呢?比如加了@Autowrited 的属性。
最后,读取@RequestMapping 注解,获得请求url,将url 和Method 建议一对一的映射关系并缓存起来。我们可以简单粗暴地理解为缓存在一个Map 中,它的Key 就是url,它的值是Method。
3、第三阶段:运行阶段
运行阶段,在Spring 启动以后,等待用户请求,完成内部调度并返回响应结果。
具体步骤如下:
用户在浏览器输入url 之后,Web 容器会接收到用户请求。Web 容器会自动调用doGet()或者doPost()方法。从doGet()或者doPost()方法中,我们可以获得两个对象,分别是request 和response。通过request 可以获得用户请求带过来的信息,通过response 可以往浏览器端输出响应结果。
然后,根据request 中获得的请求url,可以从HandlerMapping 中找到对应Method。
接着,还是利用反射调用方法,将获得方法调用的返回结果。
最后,将返回结果通过response 输出到浏览器,用户就可以看到响应结果。

SpringMVC 是一种基于Java 语言开发,实现了Web MVC 设计模式,请求驱动类型的轻量级Web 框架。
采用了MVC 架构模式的思想,通过把Model,View,Controller 分离,将Web 层进行职责解耦,从而把复杂的Web 应用分成逻辑清晰的几个组件,在Spring MVC 中有9 大重要的组件。
下面详细说明一下这些组件的作用和初始化方法:
1、MultipartResolver 文件处理器
对应的初始化方法是initMultipartResolver(context),用于处理上传请求。
2、LocaleResolver 当前环境处理器
其对应的初始化方法是initLocaleResolver(context)
SpringMVC 主要有两个地方用到了Locale:一是ViewResolver 视图解析的时候;二是用到国际化资源或者主题的时候。
3、ThemeResolver 主题处理器
其对应的初始化方法是initThemeResolver(context),用于解析主题。也就是解析样式、图片及它们所形成的显示效果的集合。
4、HandlerMapping 处理器映射器
其对应的初始化方法是initHandlerMappings(context) ,在SpringMVC 中会有很多请求,每个请求都需要一个Handler 处理。
HandlerMapping 的作用便是找到请求相应的处理器Handler 和Interceptor。
5、HandlerAdapter 处理器适配器
其对应的初始化方法是initHandlerAdapters(context)
从名字上看,它就是一个适配器。HandlerAdapters 要做的事情就是如何让固定的Servlet 处理方法调用灵活的Handler 来进行处理
6、HandlerExceptionResolver 异常处理器
对应的初始化方法是initHandlerExceptionResolvers(context),它的主要作用是处理其他组件产生的异常情况。
7、RequestToViewNameTranslator 视图名称翻译器
其对应的初始化方法是initRequestToViewNameTranslator(context)
它的作用是从请求中获取ViewName。
有的Handler 处理完后并没有设置View 也没有设置ViewName,这时就需要从request 中获取,而RequestToViewNameTranslator 就是为request 提供获取ViewName 的实现。
8、ViewResolvers 页面渲染处理器
其对应的初始化方法是initViewResolvers(context)
ViewResolvers 的主要作用是将String 类型的视图名和Locale 解析为View 类型的视图。
9、FlashMapManager 参数传递管理器
其对应的初始化方法是initFlashMapManager(context)在实际应用中,为了避免重复提交,我们可以在处理完post 请求后重定向到另外一个get 请求,这个get 请求可以用来返回页面渲染需要的信息。
FlashMap 就是用于这种请求重定向场景中的参数传递。
在Spring MVC 的九大组件中,涉及到请求处理响应的核心组件分别是:
11. HandlerMapping
12. HandlerAdapter
13. ViewResolver
这张图表示这三个组件的整体执行流程,具体调用分为以下几个步骤:

1、HandlerMapping 回到调用HandlerAdapter
2、HandlerAdapter 会返回ModelAndView
3、ModelAndView 根据用户传入参数得到ViewResolvers
4、ViewResolvers 会将用户传入的参数封装为View,交给引擎进行渲染。
注意:有大家最熟悉的两个类:ModelAndView 和View 类并不属于Spring MVC 九大组件之列。

Springboot

Springboot的优点

内置servlet容器,不需要在服务器部署 tomcat。只需要将项目打成 jar 包,使用 java -jar xxx.jar一键式启动项目
SpringBoot提供了starter,把常用库聚合在一起,简化复杂的环境配置,快速搭建spring应用环境
可以快速创建独立运行的spring项目,集成主流框架
准生产环境的运行应用监控

SpringBoot 中的 starter 到底是什么 ?

starter提供了一个自动化配置类,一般命名为 XXXAutoConfiguration ,在这个配置类中通过条件注解来决定一个配置是否生效(条件注解就是 Spring 中原本就有的),然后它还会提供一系列的默认配置,也允许开发者根据实际情况自定义相关配置,然后通过类型安全的属性注入将这些配置属性注入进来,新注入的属性会代替掉默认属性。正因为如此,很多第三方框架,我们只需要引入依赖就可以直接使用了。

运行 SpringBoot 有哪几种方式?

打包用命令或者者放到容器中运行
用 Maven/Gradle 插件运行
直接执行 main 方法运行
SpringBoot 常用的 Starter 有哪些?

  • spring-boot-starter-web :提供 Spring MVC + 内嵌的 Tomcat 。
  • spring-boot-starter-data-jpa :提供 Spring JPA + Hibernate 。
  • spring-boot-starter-data-Redis :提供 Redis 。
  • mybatis-spring-boot-starter :提供 MyBatis 。

Spring Boot 的核心注解是哪个?

启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解:

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

  • @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。

  • @ComponentScan:Spring组件扫描。

Springboot自动装配

SpringBoot 自动装配主要是基于注解编程和约定优于配置的思想来设计的。

实际应用只要在启动类上加@SpringApplication就行,这是个复合注解,包含了

  • @SpringBootConfiguration就是@Configuration注解标识的类中声明了1个或者多个@Bean方法,Spring容器可以使用这些方法来注入Bean
  • @EnableAutoConfiguration(实现自动装配的注解)
  • @ComponentScan

自动装配的实现:

  • 第一步:启动依赖组件的时候,组件中必须要包含@Configuration 的配置类,在这个配置类里面声明为@Bean 注解,就将方法的返回值或者属性值注入到IoC 容器中。
  • 第二步:如果是使用第三方jar 包,Spring Boot 采用SPI 机制,只需要在**/META-INF/目录下增加spring.factories 配置文件**。然后,Spring Boot 会根据约定规则,自动使用SpringFactoriesLoader 来加载配置文件中的内容
  • 第三步:Spring 获取到第三方jar 中的配置以后,会使用调用ImportSelector 接口来完成动态加载。

我们使用Spring 创建Web 程序时需要导入非常多的Maven 依赖,而Spring Boot 只需要一个Maven 依赖来创建Web 程序,并且Spring Boot 还把我们最常用的依赖都放到了一起,我们只需要引入spring-boot-starter-web 这一个依赖就可以完成一个简单的Web 应用。

Springboot Starter

Starter 是Spring Boot 的四大核心功能特性之一,除此之外,Spring Boot 还有自动装配、Actuator 监控等特性。

Spring Boot 里面的这些特性,都是为了让开发者在开发基于Spring 生态下的企业级应用时,只需要关心业务逻辑,减少对配置和外部环境的依赖。

其中,Starter 是启动依赖,它的主要作用有几个。

  1. Starter 组件以功能为纬度,来维护对应的jar 包的版本依赖,使得开发者可以不需要去关心这些版本冲突这种容易出错的细节。
  2. Starter 组件会把对应功能的所有jar 包依赖全部导入进来,避免了开发者自己去引入依赖带来的麻烦。
  3. Starter 内部集成了自动装配的机制,也就说在程序中依赖对应的starter 组件以后,这个组件自动会集成到Spring 生态下,并且对于相关Bean 的管理,也是基于自动装配机制来完成。
  4. 依赖Starter 组件后,这个组件对应的功能所需要维护的外部化配置,会自动集成到Spring Boot 里面,我们只需要在application.properties 文件里面进行维护就行了,比如Redis 这个starter,只需要在application.properties文件里面添加redis 的连接信息就可以直接使用了。

在我看来,Starter 组件几乎完美的体现了Spring Boot 里面约定优于配置的理念。另外,Spring Boot 官方提供了很多的Starter 组件,比如Redis、JPA、MongoDB等等。

但是官方并不一定维护了所有中间件的Starter,所以对于不存在的Starter,第三方组件一般会自己去维护一个。

官方的starter 和第三方的starter 组件,最大的区别在于命名上。

官方维护的starter 的以spring-boot-starter 开头的前缀。

第三方维护的starter 是以spring-boot-starter 结尾的后缀

这也是一种约定优于配置的体现。

Spring Boot 打成的 jar 和普通的 jar 有什么区别 ?

Spring Boot 项目最终打包成的 jar 是可执行 jar ,这种 jar 可以直接通过 java -jar xxx.jar 命令来运行,这种 jar 不可以作为普通的 jar 被其他项目依赖,即使依赖了也无法使用其中的类
Spring Boot 的 jar 无法被其他项目依赖,主要还是他和普通 jar 的结构不同。普通的 jar 包,解压后直接就是包名,包里就是我们的代码,而 Spring Boot 打包成的可执行 jar 解压后,在 \BOOT-INF\classes 目录下才是我们的代码,因此无法被直接引用。如果非要引用,可以在 pom.xml 文件中增加配置,将 Spring Boot 项目打包成两个 jar ,一个可执行,一个可引用。

Dubbo

整体框架


第一层的Business 业务逻辑层由我们自己来提供接口和实现还有一些配置信息。
第二层的RPC 调用的核心层负责封装和实现整个RPC 的调用过程、负载均衡、集群容错、代理等核心功能。
Remoting 则是对网络传输协议和数据转换的封装。

根据Dubbo 官方文档的介绍,Dubbo 提供了六大核心能力
 面向接口代理的高性能RPC 调用。
 智能容错和负载均衡。
 服务自动注册和发现。
 高度可扩展能力。
 运行期流量调度。
 可视化的服务治理与运维。

工作流程

1.服务启动的时候,provider 和consumer 根据配置信息,连接到注册中心register,分别向注册中心注册和订阅服务
2.register 根据服务订阅关系,返回provider 信息到consumer,同时consumer 会把provider 信息缓存到本地。如果信息有变更,consumer 会收到来自register 的推送
3.consumer 生成代理对象,同时根据负载均衡策略,选择一台provider,同时定时向monitor 记录接口的调用次数和时间信息
4.拿到代理对象之后,consumer 通过代理对象发起接口调用
5.provider 收到请求后对数据进行反序列化,然后通过代理调用具体的接口实现

RPC

什么是RPC

实际上,远程调用是指跨进程的功能调用,跨进程可以理解成一个计算机节点的多个进程,或者多个计算机节点的多个进程。

全称为Remote Procedure Call,翻译过来就是远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议,凡是符合该协议的框架,我们都可以称它为RPC 框架。

关于RPC 协议,通俗来讲就是,A 计算机提供一个服务,B 计算机可以像调用本地服
务那样调用A 计算机的服务。

要实现RPC,需要通过网络传输数据,并对调用的过程进行封装。

现在比较流行的RPC 框架,都会采用TCP 作为底层传输协议。

RPC 强调的是过程调用,调用的过程对用户而言是是透明的,用户不需要关心调用的细
节,可以像调用本地服务一样调用远程服务。

  • 1)客户端(Client),服务的调用方。
  • 2)服务端(Server),真正的服务提供者。
  • 3)客户端存根(Client Stub),存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
  • 4)服务端存根(Server Stub),接收客户端发送过来的消息,将消息解包,并调用本地的方法

HTTP和RPC的区别

而Http 协议是为Web 浏览器与Web 服务器之间的通信而设计的远程通信协议,它定义了通信协议的报文规范(如图),我们可以使用http 协议来实现跨网络节点的数据传输。

  • 功能特性:

    • http 是一个属于应用层的超文本传输协议,是万维网数据通信的基础,主要服务在网页端和服务端的数据传输上。
    • RPC 是一个远程过程调用协议,它的定位是实现不同计算机应用之间的数据通信,屏蔽通信底层的复杂性,让开发者就像调用本地服务一样完成远程服务的调用。
  • 实现原理:
    • http 协议是一个已经实现并且成熟的应用层协议,它定义了通信的报文格式Request Body 和Request Header,以及Response Body 和Response Header。也就是说,符合这样一个协议特征的通信协议,才是http 协议。
    • RPC 只是一种协议的规范,它并没有具体实现,只有按照RPC 通信协议规范实现的通信框架,也就是RPC 框架,才是协议的具体实现,比如Dubbo、gRPC 等。因此,我们可以在实现RPC 框架的时候,自定义报文通信的协议规范、自定义序列化方式、自定义网络通信协议的类型等等
    • 因此,从这个层面来说,http 是成熟的应用协议,而RPC 只是定义了不同服务之间的
      通信规范。
  • 应用层面:
    • http 协议和实现了RPC 协议的框架都能够实现跨网络节点的服务之间通信。并且他们底层都是使用TCP 协议作为通信基础
    • 但是,由于RPC 只是一种标准协议,只要符合RPC 协议的框架都属于RPC 框架。因此,RPC 的网络通信层也可以使用HTTP 协议来实现,比如gRPC、OpenFeign底层都采用了http 协议。

微服务

微服务治理

  • 服务注册和发现
  • 负载均衡 Round Robin
  • 扩缩容
  • 流量治理(限流、熔断、过载保护、降级)
  • 稳定性治理

Spring Cloud

Spring Cloud 是一套分布式微服务的技术解决方案,它提供了快速构建分布式系统的常用的一些组件

比如说配置管理、服务的注册与发现、服务调用的负载均衡、资源隔离、熔断降级等等

不过Spring Cloud 只是Spring 官方提供的一套微服务标准定义,而真正的实现目前有两套体系用的比较多。

  • Spring Cloud Netflix 是基于Netflix 这个公司的开源组件集成的一套微服务解决方案,其中的组件有
  1. Ribbon——负载均衡
  2. Hystrix——服务熔断
  3. Zuul——网关
  4. Eureka——服务注册与发现
  5. Feign——服务调用
  • Spring Cloud Alibaba 是基于阿里巴巴开源组件集成的一套微服务解决方案,其中包
  1. Dubbo——消息通讯(RPC)

  2. Nacos——服务注册与发现+配置中心

  3. Seata——事务隔离

  4. Sentinel——熔断降级

  5. 在Spring Cloud 出现之前,为了解决微服务架构里面的各种技术问题,需要去集成各种开源框架,因为标准和兼容性问题,所以在实践的时候很麻烦,而SpringCloud 统一了这样一个标准。

  6. 降低了微服务架构的开发难度,只需要在Spring Boot 的项目基础上通过starter启动依赖集成相关组件就能轻松解决各种问题。

有了Spring Cloud 这样的技术生态,使得我们在落地微服务架构时。不用去考虑第三方技术集成带来额外成本,只要通过配置组件来完成架构下的技术问题,从而可以让我们更加侧重性能方面

CAP模型

CAP 模型是说,在一个分布式系统里面,不可能同时满足三个点

  • 一致性(Consistency),访问分布式系统中的每一个节点都能获得最新的数据
  • 可用性(Availability),每次请求都能获得一个有效的访问,但不保证数据是最新的。
  • 分区容错性(Partition tolerance),分区相当于对通信耗时的要求,系统如果不能在时限范围内达成数据一致,就意味着发生了分区的情况。即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性或可用性的服务

分区容错性必然是存在的,因此我们只能在一致性和可用性之间做选择。

微服务和分布式的区别

简单来说,分布式是一组通过网络进行通信,并且为了完成共同的计算任务的计算机节点组成的系统。

分布式系统的设计理念,其实是来自于小型机或者大型机的计算能力的瓶颈和成本的增加。

在集中式系统里面,要想提升程序的运行性能,只能不断的升级CPU 以及增加内存,但是硬件的提升本身也是有瓶颈的,所以当企业对于计算要求越来越高的时候,集中式架构已经无法满足需求了。

在这样的背景下, 就产生了分布式计算,也就是把一个计算任务分配给多个计算机节点去运行。

但是对于用户或者客户端来说,感知不到背后的逻辑,就像访问单个计算机一样,他看到的仍然是一个整体。

在分布式系统中,软件架构也需要作出相应的调整,需要把原本的单体应用进行拆分,部署到多个计算机节点上,然后各个服务之间使用远程通信协议实现计算结果的数据交互。

针对这种分布式部署的应用架构,我们称为SOA(面向服务)的架构。

其次,我再解释一下微服务架构。

其实微服务架构本身就是一种分布式架构,它强调的是对部署在各个计算机上的应用服务的粒度。

它的核心思想是,针对拆分的服务节点做更进一步的解耦。
也就是说,针对SOA 服务化架构下的单个业务服务,以更加细粒度的方式进一步拆分。
每个拆分出来的微服务由独立的小团队负责,最好在3 人左右。
拆分的好处是使得程序的扩展性更强,开发迭代效率更高。
对于一些大型的互联网项目来说,微服务能够在不影响用户使用的情况下非常方便的实现产品功能的创新和上线。

Redis

  1. Redis 是一个高性能的基于Key-Value 结构存储的Nosql 开源数据库。
  2. 目前市面上绝大部分公司都采用Redis 来实现分布式缓存,从而提高数据的检索效
    率。
  3. Redis 之所以这么流行,主要有几个特点:
    a. 它是基于内存存储,在进行数据IO 操作时,能够10WQPS
    b. 提供了非常丰富的数据存储结构,如String、List、Hash、Set、ZSet 等。
    c. Redis 底层采用单线程实现数据的IO,所以在数据算法层面并不需要要考虑并
    发安全性,所以底层算法上的时间复杂度基本上都是常量。
  4. Redis 虽然是内存存储,但是它也可以支持持久化,避免因为服务器故障导致数据
    丢失的问题

Redis数据类型

Keys:非二进制安全的字符类型
Values:
string:最基本的数据类型,二进制安全的字符串,最大512M。
list:按照添加顺序保持顺序的字符串列表。
set:无序的字符串集合,不存在重复的元素。
sorted set:已排序的字符串集合。
hash:key-value对的一种集合。
bitmap:更细化的一种操作,以bit为单位。
hyperloglog:基于概率的数据结构。 # 2.8.9新增
Geo:地理位置信息储存起来, 并对这些信息进行操作 # 3.2新增
流(Stream):# 5.0新增

Redis为什么这么快(Redis使用单线程还是多线程,是否有线程安全问题)

Redis Server 本身可能出现的性能瓶颈点无非就是网络IO、CPU、内存。但是CPU不是Redis 的瓶颈点,所以没必要使用多线程来执行指令。

如果采用多线程,意味着对于redis 的所有指令操作,都必须要考虑到线程安全问题,也就是说需要加锁来解决,这种方式带来的性能影响反而更大。

网络层面,Redis 采用多路复用的设计,提升了并发处理的连接数,不过这个阶段,Server 端的所有IO 操作,都是由同一个主线程处理的,这个时候IO 的瓶颈就会影响到Redis 端的整体处理性能。

所以从Redis6.0 开始,在网络IO多路复用层面增加了多线程的处理,来优化IO 处理的能力。

不过,具体的数据操作仍然是由主线程来处理的,所以我们可以认为Redis 对于数据的处理依然是单线程

CPU 层面来说,Redis 只需要采用单线程即可,原因有两个。

  • 如果采用多线程,对于Redis 中的数据操作,都需要通过同步的方式来保证线程安全性,这反而会影响到redis 的性能
  • 在Linux 系统上Redis 通过pipelining 可以处理100w 个请求每秒,而应用程序的计算复杂度主要是O(N) 或O(log(N)) ,不会消耗太多CPU

内存层面来说,Redis 本身就是一个内存数据库,内存的IO 速度本身就很快,所以
内存的瓶颈只是受限于内存大小。

最后,Redis 本身的数据结构也做了很多的优化,比如压缩表、跳跃表等方式降低了时间复杂度,同时还提供了不同时间复杂度的数据类型。

Redis持久化机制

Redis 本身是一个基于Key-Value 结构的内存数据库,为了避免Redis 故障导致数据丢失的问题,所以提供了RDB 和AOF 两种持久化机制。

RDB

通过快照的方式来实现持久化的,也就是说会根据快照的触发条件,把内存里面的数据快照写入到磁盘以二进制的压缩文件进行存储

RDB 快照的触发方式有很多,比如

  • 执行bgsave 命令触发异步快照,执行save 命令触发同步快照,同步快照会阻塞客户端的执行指令
  • 根据redis.conf 文件里面的配置,自动触发bgsave
  • 主从复制的时候触发

AOF

AOF 持久化,它是一种近乎实时的方式,把Redis Server 执行的事务命令进行追加存储。简单来说,就是客户端执行一个数据变更的操作,Redis Server 就会把这个命令追加到aof 缓冲区的末尾,然后再把缓冲区的数据写入到磁盘的AOF 文件里面,至于最终什么时候真正持久化到磁盘,是根据刷盘的策略来决定的。


另外,因为AOF 这种指令追加的方式,会造成AOF 文件过大,带来明显的IO 性能问题,所以Redis 针对这种情况提供了AOF 重写机制,也就是说当AOF 文件的大小达到某个阈值的时候,就会把这个文件里面相同的指令进行压缩

AOF 文件重写的具体过程分为几步:
首先,根据当前Redis 内存里面的数据,重新构建一个新的AOF 文件
然后,读取当前Redis 里面的数据,写入到新的AOF 文件里面
最后,重写完成以后,用新的AOF 文件覆盖现有的AOF 文件
另外,因为AOF 在重写的过程中需要读取当前内存里面所有的键值数据,再生成对应
的一条指令进行保存。
而这个过程是比较耗时的,对业务会产生影响。
所以Redis 把重写的过程放在一个后台子进程里面来完成,
这样一来,子进程在做重写的时候,主进程依然可以继续处理客户端请求。
最后,为了避免子进程在重写过程中,主进程的数据发生变化,导致AOF 文件和Redis 内存中的数据不一致的问题,Redis 还做了一层优化。
就是子进程在重写的过程中,主进程的数据变更需要追加到AOF 重写缓冲区里面
等到AOF 文件重写完成以后,再把AOF 重写缓冲区里面的内容追加到新的AOF 文件里面。

因此,基于对RDB 和AOF 的工作原理的理解,我认为RDB 和AOF 的优缺点有两个。

  • RDB 是每隔一段时间触发持久化,因此数据安全性低AOF 可以做到实时持久化,数据安全性较高
  • RDB 文件默认采用压缩的方式持久化,AOF 存储的是执行指令,所以RDB 在数据恢复的时候性能比AOF 要好

缓存击穿

缓存击穿,表示请求因为某些原因全部打到了数据库,缓存并没有起到流量缓冲的作用。
我认为有2 种情况会导致缓存击穿。

  • 在Redis 里面保存的热点key,在缓存过期的瞬间,有大量请求进来,导致请求全部打在数据库上。
  • 客户端恶意发起大量不存在的key 的请求,由于访问的key 对应的数据本身就不存在,

解决方法:

  1. 对于热点数据,我们可以不设置过期时间,或者在访问数据的时候对数据过期时间进行续期
  2. 对于访问量较高的缓存数据,我们可以设计多级缓存,尽量减少后端存储设备的压力。
  3. 使用分布式锁,当发现缓存失效的时候,不是先从数据库加载,而是先获取分布式锁,.
    获得分布式锁的线程从数据库查询数据后写回到缓存里面。后续没有获得锁的线程就只需要等待和重试即可。这个方案牺牲了一定的性能,但是确保护了数据库避免被压垮。
  4. 对于恶意攻击类的场景,可以使用布隆过滤器,应用启动的时候把存在的数据缓存
    到布隆过滤器里面

    每一次请求进来的时候先访问布隆过滤器,如果不存在,则说明这个数据一定没有在数据库里面,就没必要再去访问数据库了。
    另外,我们在整个缓存架构设计中,除了尽可能避免缓存穿透的问题,还需要从全局视角做整体考虑

Zookeeper

分布式系统中的三种典型应用场景

  • 第一种:集群管理
    在多个节点组成的集群中,为了保证集群的HA 特性,每个节点都会冗余一份数据副本。
    这种情况下需要保证客户端访问集群中的任意一个节点都是最新的数据。
  • 第二种:分布式锁
    如何保证跨进程的共享资源的并发安全性,对于分布式系统来说也是一个比较大的
    挑战,而为了达到这样一个目的,必须要使用跨进程的锁也就是分布式锁来实现。
  • 第三种:Master 选举
    在多个节点组成的集群中,为了降低集群数据同步的复杂度,一般会存在Master 和
    Slave 两种角色的节点,Master 负责事务和非事务请求处理,Slave 负责非事务请求处
    理。但是在分布式系统中如何确定某个节点是Master 还是Slave,也成了一个难度不
    小的挑战。

基于这三类常见场景的需求,所以产生了Zookeeper 这样一个中间件。
它是一个分布式开源协调组件,简单来说,就是类似于一个裁判员的角色,专门负责协
调和解决分布式系统中的各类问题。

  1. 集群管理
    Zookeeper 提供了CP 的模型,来保证集群中的每个节点的数据一致性,当然Zk 本身的集群并不是CP 模型,而是顺序一致性模型,如果要保证CP 特性,需要调用sync
    同步方法。
  2. 分布式锁
    Zookeeper 提供了多种不同的节点类型,如持久化节点、临时节点、有序节点、容
    器节点等,其中对于分布式锁这个场景来说,Zookeeper 可以利用有序节点(插入图
    片)的特性来实现。除此之外,还可以利用同一级节点的唯一性特性来实现分布式锁。
  3. Master 选举
    Zookeeper 可以利用持久化节点来存储和管理其他集群节点的信息,从而进行
    Master 选举机制。或者还可以利用集群中的有序节点特性,来实现Master 选举。
    目前主流的Kafka、Hbase、Hadoop 都是通过Zookeeper 来实现集群节点的主从
    选举。
    总的来说,Zookeeper 就是经典的分布式数据一致性解决方案,致力于为分布式应用
    提供高性能、高可用,并且具有严格顺序访问控制能力的分布式协调服务。它底层通过
    基于Paxos 算法演化而来的ZAB 协议实现。
    以上就是我对于Zookeeper 的理解。

Docker

Docker 是一个超轻量级的虚拟机,也是实现容器化技术的一种应用工具。

与虚拟机通过操作系统实现隔离不同,容器技术只隔离应用程序的运行时环境但容器之间可以共享同一个操作系统

Docker容器与传统虚拟化方式的不同之处,传统的虚拟技术,在将物理硬件虚拟成多套硬件后,需要再每套硬件上都部署一个操作系统,接着在这些操作系统上运行相应的应用程序。而Docker容器内的应用程序进程直接运行在宿主机(真实物理机)的内核上,Docker引擎将一些各自独立的应用程序和它们各自的依赖打包,相互独立直接运行于未经虚拟化的宿主机硬件上,同时各个容器也没有自己的内核,显然比传统虚拟机更轻便。

docker实现

docker基于Linux内核提供这样几项功能实现的:

  • NameSpace
    我们知道Linux中的PID、IPC、网络等资源是全局的,而NameSpace机制是一种资源隔离方案,在该机制下这些资源就不再是全局的了,而是属于某个特定的NameSpace,各个NameSpace下的资源互不干扰,这就使得每个NameSpace看上去就像一个独立的操作系统一样,但是只有NameSpace是不够。
  • Control groups
    虽然有了NameSpace技术可以实现资源隔离,但进程还是可以不受控的访问系统资源,比如CPU、内存、磁盘、网络等,为了控制容器中进程对资源的访问,Docker采用control groups技术(也就是cgroup),有了cgroup就可以控制容器中进程对系统资源的消耗了,比如你可以限制某个容器使用内存的上限、可以在哪些CPU上运行等等。

docker工作流程

docker build

当我们写完dockerfile交给docker“编译”时使用这个命令,那么client在接收到请求后转发给docker daemon,接着docker daemon根据dockerfile创建出“可执行程序”image。

docker run

有了“可执行程序”image后就可以运行程序了,接下来使用命令docker run,docker daemon接收到该命令后找到具体的image,然后加载到内存开始执行,image执行起来就是所谓的container。

docker pull

其实docker build和docker run是两个最核心的命令,会用这两个命令基本上docker就可以用起来了,剩下的就是一些补充。

那么docker pull是什么意思呢?

Docker Hub,docker官方的“应用商店”,你可以在这里下载到别人编写好的image,这样你就不用自己编写dockerfile了。

docker registry 可以用来存放各种image,公共的可以供任何人下载image的仓库就是docker Hub。那么该怎么从Docker Hub中下载image呢,就是这里的docker pull命令了。

因此,这个命令的实现也很简单,那就是用户通过docker client发送命令,docker daemon接收到命令后向docker registry发送image下载请求,下载后存放在本地,这样我们就可以使用image了。

用docker的好处

  • 资源占用小
    由于容器不需要进行硬件虚拟,也不需要运行完整操作系统等额外的资源开销,使得
    Docker 对系统资源的利用率更高,无论是应用执行速度还是文件存储速度,都要比传统
    虚拟机技术更高效,内存消耗更少。

  • 启动速度快
    传统的虚拟机技术启动应用服务往往需要较长时间,而Docker 容器应用,由于直接运
    行于宿主内核,无需启动完整的操作系统,因此可以做到秒级,甚至毫秒级的启动时间,
    大大的节约了开发,测试,部署的时间。

  • 迁移更轻松
    由于Docker 确保了执行环境的一致性,使得应用的迁移更加容易,
    Docker 可以在很多平台上运行,无论是物理机,虚拟机,公有云,私有云,它们的运
    行结果是一致的,因此用户可以很轻易的将一个平台上运行的应用,迁移到另一个平台上,而不用担心运行环境的变化导致应用无法正常运行这类的问题。

  • 维护和拓展更轻松
    docker 使用的分层存储和镜像技术,让应用重复部分的复用更容易,也让应用的维护更新更简单,基于基础镜像进一步扩展镜像也变得十分简单。
    另外,docker 团队和各个开源项目团队一起维护了一大批高质量的官网镜像,既可以直接在生产环境使用,又可以作为基础进一步定制,大大降低了应用服务的镜像制作成本。

  • 运行环境一致
    开发过程中一个常见的问题是环境一致性问题,由于开发环境,测试环境,生产环境不一致,导致有些bug 并未在开发过程中被发现,而Docker 的镜像提供了除内核外完整的运行时环境,确保了应用运行环境一致性。

  • 持续交付和部署
    使用Docker 可以通过定制应用镜像来实现持续集成,持续交付,部署。开发人员可以通过Dockerfile 来进行镜像构建,并结合持续集成系统进行集成测试,而运维人员则可以在生产环境中快速部署该镜像,甚至结合持续部署系统进行自动部署

docker核心组件

  • 镜像(Image)——一个特殊的文件系统
    简单地理解,Docker 镜像就是一个Linux 的文件系统(Root FileSystem),这个文件系统里面包含可以运行在Linux 内核的程序以及相应的数据。
    一个镜像可以包含一个完整的操作系统环境,里面仅安装了Apache 或用户需要的其它应用程序。镜像可以用来创建Docker 容器。Docker 提供了一个很简单的机制来创建镜像或者更新现有的镜像,用户甚至可以直接从其他人那里下载一个已经做好的镜像来直接使用。
  • 容器(Container)——镜像运行的实体
    Docker 利用容器来运行应用。容器是从镜像创建的运行实例。它可以被启动、开始、停止、删除。每个容器都是相互隔离的、保证安全的平台。
    可以把容器看做是一个简易版的Linux 环境(包括root 用户权限、进程空间、用户空间和网络空间等)和运行在其中的应用程序。
  • 仓库(Repository)——集中存放镜像文件的地方
    仓库是集中存放镜像文件的场所。很多人会把仓库和仓库注册服务器(Registry)混为一谈。实际上,仓库注册服务器上往往存放着多个仓库,每个仓库中又包含了多个镜像,每个镜像有不同的标签(tag)。
    仓库分为公开仓库(Public)和私有仓库(Private)两种形式。最大的公开仓库是Docker
    Hub,存放了数量庞大的镜像供用户下载。

JVM

Java 虚拟机是Java 语言的运行环境
之所以需要Java 虚拟机,主要是为Java 语言提供Write Once,Run Anywhere 能力。
实际上,一次编写,到处运行这个能力本身是不可能实现的。因为不同的操作系统和硬件。
最终执行的指令会有较大的差异。
而Java 虚拟机就是解决这个问题的,它能根据不同的操作系统和硬件差异,生成符合这个平台机器指令。(跨平台
简单理解,它就相当于一个翻译工具,在window 下,翻译成window 可执行的指令,在linux 下,翻译成linux 下可执行的指令。
除了这个因素,自动回收垃圾这个功能也是原因之一,它让开发者省去了垃圾回收这个工作。
减少了程序开发的复杂性。

JVM 所处的位置,同时也能看出它两个作用:

  • 运⾏并管理Java 源码⽂件所⽣成的Class⽂件,
  • 在不同的操作系统上安装不同的JVM,从⽽实现了跨平台的保证

Class Files类文件

Class⽂件是由源码⽂件⽣成的

JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。

类文件结构:https://javaguide.cn/java/jvm/class-file-structure.html

  • 魔数: Class 文件的头 4 个字节,确定这个文件是否为一个能被虚拟机接收的 Class 文件。
  • Class 文件版本号:紧接着魔数的四个字节存储的是 Class 文件的版本号:第 5 和第 6 个字节是次版本号,第 7 和第 8 个字节是主版本号。
  • 常量池(Constant Pool):主要存放两大常量:字面量和符号引用
    • 字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。
    • 符号引用则属于编译原理方面的概念。包括下面三类常量:
      • 类和接口的全限定名
      • 字段的名称和描述符
      • 方法的名称和描述符
  • 访问标志(Access Flags):类访问和属性修饰符,如abstract,final,public
  • 当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合
    • 注意java是单继承的,父类只能有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类
  • 字段表集合(Fields)
  • 方法表集合(Methods):访问标志、名称索引、描述符索引、属性表

Class Loader Subsystem 类加载机制

类从被加载到虚拟机内存中开始到卸载出内存为止,它的整个生命周期可以简单概括为 7 个阶段::加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)。其中,验证、准备和解析这三个阶段可以统称为连接(Linking)。

参考https://javaguide.cn/java/jvm/class-loading-process.html

Java内存区域(运行时数据区域)

在Java7 里面,JVM 运行时数据区是这样的(如图)。

在Hotspot 虚拟机中,方法区的实现是在永久代里面,它里面主要存储运行时常量池、Klass 类元信息等。
永久代属于JVM 运行时内存中的一块存储空间,我们可以通过-XX:PermSize 来设置永久代的大小。
内存不够的时候,会触发垃圾回收

在JDK1.8 里面,JVM 运行时数据区是这样的(如图)
在Hotspot 虚拟机中,取消了永久代,由元空间来实现方法区的数据存储。
元空间不属于JVM 内存,而是直接使用本地内存,因此不需要考虑GC 问题
默认情况下元空间是可以无限制的使用本地内存的,但是我们也可以使用JVM 参数来
限制内存使用大小。

为什么使用元空间替换了永久代

  • 在1.7 版本里面,永久代内存是有上限的,虽然我们可以通过参数来设置,但是JVM
    加载的class 总数、大小是很难确定的

    • 很容易出现OOM 问题。
    • 但是元空间是存储在本地内存里面,内存上限比较大,可以很好的避免这个问题。
  • 永久代的对象是通过FullGC 进行垃圾收集,也就是和老年代同时实现垃圾收集。替换成元空间以后,简化了Full GC。可以在不进行暂停的情况下并发地释放类数据,同时也提升了GC 的性能
  • Oracle 要合并Hotspot 和JRockit 的代码,而JRockit 没有永久代。

GC

不同的GC介绍

  • Serial GC,它是最古老的垃圾收集器,“Serial”体现在其收集工作是单线程的,并且在进行垃圾收集过程中,会进入臭名昭著的“Stop-The-World”状态。当然,其单线程设计也意味着精简的GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是Client 模式下JVM 的默认选项。从年代的角度,通常将其老年代实现单独称作Serial Old,它采用了标记- 整理(MarkCompact)算法,区别于新生代的复制算法。
    Serial GC 的对应JVM 参数是
    -XX:+UseSerialGC
  • ParNew GC,很明显是个新生代GC 实现,它实际是Serial GC 的多线程版本,最常见的应用场景是配合老年代的CMS GC 工作,下面是对应参数
    -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
  • CMS(Concurrent Mark Sweep) GC,基于标记- 清除Mark-Sweep算法,设计目标是尽量减少停顿时间,这一点对于Web 等反应时间敏感的应用非常重要,一直到今天,仍然有很多系统使用CMS GC。但是,CMS 采用的标记- 清除算法,存在着内存碎片化问题,所以难以避免在长时间运行等情况下发生full GC,导致恶劣的停顿。另外,既然强调了并发(Concurrent),CMS 会占用更多CPU 资源,并和用户线程争抢。
  • Parrallel GC,在早期JDK 8 等版本中,它是server 模式JVM 的默认GC选择,也被称作是吞吐量优先的GC。它的算法和Serial GC 比较相似,尽管实现要复杂的多,其特点是新生代和老年代GC 都是并行进行的,在常见的服务器环境中更加高效。开启选项是:
    -XX:+UseParallelGC
    另外,Parallel GC 引入了开发者友好的配置项,我们可以直接设置暂停时间或吞吐量等目标,JVM 会自动进行适应性调整,例如下面参数:
    -XX:MaxGCPauseMillis=value
    -XX:GCTimeRatio=N // GC 时间和用户时间比例= 1 / (N+1)
  • G1 GC 这是一种兼顾吞吐量和停顿时间的GC 实现,是Oracle JDK 9 以后的默认GC 选项。G1 可以直观的设定停顿时间的目标,相比于CMS GC,G1 未必能做到CMS 在最好情况下的延时停顿,但是最差情况要好很多。G1 GC 仍然存在着年代的概念,但是其内存结构并不是简单的条带式划分,而是类似棋盘的一个个region。
    Region 之间是复制算法,但整体上实际可看作是标记- 整理(Mark-Compact)算法,可以有效地避免内存碎片,尤其是当Java 堆非常大的时候,G1 的优势更加明显。G1 吞吐量和停顿表现都非常不错,并且仍然在不断地完善,与此同时CMS 已经在JDK9 中被标记为废弃(deprecated),所以G1 GC 值得你深入掌握。

强引用、软引用、弱引用、虚引用

不同的引用类型,主要体现的是对象不同的可达性状态和对垃圾收集的影响。

  • 强引用,就是普通对象的引用,只要还有强引用指向一个对象,就能表示对象还“活着”,垃圾收集器无法回收这一类对象。

    • 只有在没有其他引用关系,或者超过了引用的作用域,再或者显示的把引用赋值为null的时候,垃圾回收器才能进行内存回收。
  • 软引用,是一种相对强引用弱化一些的引用,只有当JVM 认为内存不足时,才会去试图回收软引用指向的对象
    • 软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
  • 弱引用,相对强引用而言,它允许在存在引用关联的情况下被垃圾回收的对象在垃圾回收器线程扫描它所管辖的内存区域的过程中,
    一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,垃圾回收期都会回收该内存
  • 虚引用,它不会决定对象的生命周期,它提供了一种确保对象被finalize 以后,去做某些事情的机制。
    • 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
    • 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收,然后我们就可以在引用的对象的内存回收之前采取必要的行动。

K8s Kubernetes

k8s文档
一种容器自动部署、扩容以及管理的技术

Master(主节点): 控制 Kubernetes 节点的机器,也是创建作业任务的地方。
Node(节点): 这些机器在 Kubernetes 主节点的控制下执行被分配的任务。
Pod: 由一个或多个容器构成的集合,作为一个整体被部署到一个单一节点。同一个 pod 中的容器共享 IP 地址、进程间通讯(IPC)、主机名以及其它资源。Pod 将底层容器的网络和存储抽象出来,使得集群内的容器迁移更为便捷。
Replication controller(复制控制器): 控制一个 pod 在集群上运行的实例数量。
Service(服务): 将服务内容与具体的 pod 分离。Kubernetes 服务代理负责自动将服务请求分发到正确的 pod 处,不管 pod 移动到集群中的什么位置,甚至可以被替换掉。
Kubelet: 这个守护进程运行在各个工作节点上,负责获取容器列表,保证被声明的容器已经启动并且正常运行。
kubectl: 这是 Kubernetes 的命令行配置工具。

docker 在 Kubernetes 中的角色
Docker 技术依然执行它原本的任务。当 kubernetes 把 pod 调度到节点上,节点上的 kubelet 会指示 docker 启动特定的容器。接着,kubelet 会通过 docker 持续地收集容器的信息,然后提交到主节点上。Docker 如往常一样拉取容器镜像、启动或停止容器。不同点仅仅在于这是由自动化系统控制而非管理员在每个节点上手动操作的。

容器编排技术

在K8s 中最小的操作单元是Pod,它可以理解为是container 的逻辑单位,不会直接操作docker 中的container,而是会以Pod 的形式来操作,因为Pod 里面包含了各个类型的container。

在worker 中是如何操作Pod 的呢?

就是通过kubelet 组件完成的。当然可以发现,整个K8s 集群中还可以持久化一些数据到ETCD 组件里,可以通过Dashboard 查看整个集群资源的状态,可以通过DNS 组件进行相应的域名解析等, 这些组件和功能都是可选的、可扩展的。

到这里我们不妨再想一个问题,K8s 集群中的物理机可以是不同的硬件和操作系统?

显然是没问题的,这样一来我们就可以通过Docker 和K8s 来屏蔽基础设施的差异性,在上层进行容器创建以及管理就变得方便多了,所以K8s 也是云原生中很重要的基础保障。

如图所示,最下面的基础设施可以是各种公有云、私有云、混合云,在此基础上构建K8s 的集群支撑,然后就可以非常方便调度管理不同的容器了。

负载均衡

负载均衡机制的核心目的是让客户端的请求合理均匀的分发到多台目标服务器,由于请求被多个节点分发,使得服务端的性能得到有效的提升

常见的实现方案有三种:
 基于DNS 实现负载均衡:用户通过域名访问某个网站时,会先通过DNS 服务器进行域名解
析得到一个IP 地址,DNS 服务器可以随机分配一个IP 地址进行访问。由于DNS 多级缓存的特性,当我们修改DNS 配置之后,会因为缓存导致IP 变更不及时,从而影响负载均衡的效果。
 基于硬件实现负载均衡
 基于软件实现负载均衡

不同层的负载均衡

  • 二层(数据链路层)负载:基于Mac 地址来实现请求分发,一般采用虚拟Mac 的方式实现,服务器收到请求后,通过动态分配后端服务的实际Mac 地址进行响应从而实现负载均衡
  • 三层(网络层)负载:基于IP 层负载,一般通过虚拟IP 的方式实现,外部请求访问虚拟IP,服务器收到请求后根据后端实际IP 地址进行转发。
  • 四层(传输层)负载:通过请求报文中的目标地址和端口进行负载,Nginx、F5、LVS 等都可以实现四层负载。(展示图片)
  • 七层(应用层)负载:七层负载是基于应用层负载,也就是服务器端可以根据http 协议中请求的报文信息来决定把请求分发到哪个目标服务器上,比如Cookie、消息体、RequestHeader 等。

负载均衡算法

  1. 轮询,也就是多个服务器按照顺序轮询返回,这样每个服务器都能获得相同的请求
    次数
  2. 随机,根据随机算法获得一个目标服务地址(就像古时候皇帝翻牌子),由于该算
    法具备随机性,因此每个服务器获得的请求数量不一定均等。
  3. 一致性hash,也就是对于具有相同hash 码的请求,永远发送到同一个节点上。
  4. 最小连接数,根据目标服务器的请求数量来决定请求分发的权重,也就是目标服务
    集群中,请求更少的节点将会获得更多的请求。这是负载均衡中比较好的策略,真
    正能够实现目标服务器的请求均衡。

其中1、3和4都可以根据其处理请求的能力和资源状况被赋予不同的权重

消息队列

消息队列Message Queue,简称MQ。是一种应用间的通信方式,主要由三个部分组成。

  • 生产者:Producer
    消息的产生者与调用端
    主要负责消息所承载的业务信息的实例化
    是一个队列的发起方
  • 代理:Broker
    主要的处理单元
    负责消息的存储、投递、及各种队列附加功能的实现
    是消息队列最核心的组成部分
  • 消费者:Consumer
    一个消息队列的终端
    也是消息的调用端
    具体是根据消息承载的信息,处理各种业务逻辑。

三种场景

  1. 异步处理
    主要应用于对实时性要求不严格的场景,
    比如:用户注册发送验证码、下单通知、发送优惠券等等。
    服务方只需要把协商好的消息发送到消息队列剩下的由消费消息的服务去处理不用等待消费服务返回结果

  2. 应用解耦

应用解耦可以看作是把相关但耦合度不高的系统联系起来。
比如订单系统与WMS、EHR 系统,有关联但不哪么紧密,每个系统之间只需要把约定的消息发送到MQ,另外的系统去消费即可。
解决了各个系统可以采用不同的架构、语言来实现,从而大大增加了系统的灵活性。

  1. 流量削峰
    流量削峰一般应用在大流量入口且短时间内业务需求处理不完的服务中心,为了权衡高可用,把大量的并行任务发送到MQ 中,依据MQ 的存储及分发功能,平稳的处理后续的业务,起到一个大流量缓冲的作用。

异常

继承自Throwable 这个类中,分别是Error 和Exception,

Error 是程序报错,系统收到无法处理的错误消息,它和程序本身无关。
Excetpion 是指程序运行时抛出需要处理的异常信息如果不主动捕获,则会被jvm 处
理。

受检异常和非受检异常

前面说过受检异常和非受检异常均派生自Exception 这个类。

  1. 受检异常的定义是程序在编译阶段必须要主动捕获的异常,遇到该异常有两种处理
    方法
    通过try/catch 捕获该异常或者通过throw 把异常抛出去
  2. 非受检异常的定义是程序不需要主动捕获该异常,一般发生在程序运行期间,比如
    NullPointException

除了Error 和RuntimeException 及派生类以外,其他异常都是属于受检异常,
比如IOException、SQLException。

受检异常优点有两个:
 第一,它可以响应一个明确的错误机制,这些错误在写代码的时候可以随时捕获并
且能很好的提高代码的健壮性。
 第二,在一些连接操作中,它能很好的提醒我们关注异常信息,并做好预防工作。

受检异常的缺点是:抛出受检异常的时候需要上声明,而这个做法会直接破坏方法
签名导致版本不兼容

非受检异常的好处是可以去掉一些不需要的异常处理代码,而不好之处是开发人员可能
忽略某些应该处理的异常,导致带来一些隐藏很深的Bug

Volatile关键字

volatile 关键字有两个作用。

  1. 可以保证在多线程环境下共享变量的可见性。
  2. 通过增加内存屏障防止多个指令之间的重排序。
    我理解的可见性,是指当某一个线程对共享变量的修改,其他线程可以立刻看到修改之后的值。
    其实这个可见性问题,我认为本质上是由几个方面造成的。

CPU 层面的高速缓存,在CPU 里面设计了三级缓存去解决CPU 运算效率和内存IO 效率问题,但是带来的就是缓存的一致性问题,而在多线程并行执行的情况下,缓存一致性就会导致可见性问题。

所以,如果对共享变量增加了volatile 关键字,那么在编译器层面,就不会去触发编译器优化,同时再JVM 里面,会插入内存屏障指令来避免重排序问题。
当然,除了volatile 以外,从JDK5 开始,JMM 就使用了一种Happens-Before 模型去描述多线程之间的内存可见性问题。
如果两个操作之间具备Happens-Before 关系,那么意味着这两个操作具备可见性关系,不需要再额外去考虑增加volatile 关键字来提供可见性保障。

缓存雪崩与缓存穿透

缓存雪崩

存储在缓存里面的大量数据,在同一个时刻全部过期,原本缓存组件抗住的大部分流量全部请求到了数据库。导致数据库压力增加造成数据库服务器崩溃的现象。


导致缓存雪崩的主要原因,我认为有两个:

  1. 缓存中间件宕机,当然可以对缓存中间件做高可用集群来避免。
  2. 缓存中大部分key 都设置了相同的过期时间,导致同一时刻这些key 都过期了。对
    于这样的情况,可以在失效时间上增加一个1 到5 分钟的随机值

缓存穿透

表示是短时间内有大量的不存在的key 请求到应用里面,而这些不存在的key 在缓存里面又找不到,从而全部穿透到了数据库,造成数据库压力。

我认为这个场景的核心问题是针对缓存的一种攻击行为,因为在正常的业务里面,即便是出现了这样的情况,由于缓存的不断预热,影响不会很大。
而攻击行为就需要具备时间是的持续性,而只有key 确实在数据库里面也不存在的情况下,才能达到这个目的,所以,我认为有两个方法可以解决:

  1. 把无效的key 也保存到Redis 里面,并且设置一个特殊的值,比如“null”,这样的话下次再来访问,就不会去查数据库了。
  2. 但是如果攻击者不断用随机的不存在的key 来访问,也还是会存在问题,所以可以用布隆过滤器来实现,在系统启动的时候把目标数据全部缓存到布隆过滤器里面,当攻击者用不存在的key 来请求的时候,先到布隆过滤器里面查询,如果不存在,那意味着这个key 在数据库里面也不存在。布隆过滤器还有一个好处,就是它采用了bitmap 来进行数据存储,占用的内存空间很少。

ThreadLocal

  1. ThreadLocal 是一种线程隔离机制,它提供了多线程环境下对于共享变量访问的安全性。

  2. 在多线程访问共享变量的场景中,一般的解决办法是对共享变量加锁,从而保证在同一时刻只有一个线程能够对共享变量进行更新,并且基于Happens-Before 规则里面的监视器锁规则,又保证了数据修改后对其他线程的可见性。

  3. 但是加锁会带来性能的下降,所以ThreadLocal 用了一种空间换时间的设计思想,也就是说在每个线程里面,都有一个容器来存储共享变量的副本,然后每个线程只对自己的变量副本来做更新操作,这样既解决了线程安全问题,又避免了多线程竞争加锁的开销。

  4. ThreadLocal 的具体实现原理是,在Thread 类里面有一个成员变量ThreadLocalMap,它专门来存储当前线程的共享变量副本,后续这个线程对于共享变量的操作,都是从这个ThreadLocalMap 里面进行变更,不会影响全局共享变量的值。

线程安全

简单来说,在多个线程访问某个方法或者对象的时候,不管通过任何的方式调用以及线
程如何去交替执行。

在程序中不做任何同步干预操作的情况下,这个方法或者对象的执行/修改都能按照预
期的结果来反馈,那么这个类就是线程安全的

实际上,线程安全问题的具体表现体现在三个方面,原子性、有序性、可见性

原子性

原子性呢,是指当一个线程执行一系列程序指令操作的时候,它应该是不可中断的,因
为一旦出现中断,站在多线程的视角来看,这一系列的程序指令会出现前后执行结果不
一致的问题。

这个和数据库里面的原子性是一样的,简单来说就是一段程序只能由一个线程完整的执
行完成,而不能存在多个线程干扰。

CPU 的上下文切换,是导致原子性问题的核心,而JVM 里面提供了Synchronized 关键字来解决原子性问题

可见性

可见性,就是说在多线程环境下,由于读和写是发生在不同的线程里面,有可能出现某个线程对共享变量的修改,对其他线程不是实时可见的

导致可见性问题的原因有很多,比如CPU 的高速缓存、CPU 的指令重排序、编译器的指令重排序。

有序性

有序性,指的是程序编写的指令顺序和最终CPU 运行的指令顺序可能出现不一致的现象,这种现象也可以称为指令重排序,所以有序性也会导致可见性问题。

可见性和有序性可以通过JVM 里面提供了一个Volatile 关键字来解决。

导致有序性、原子性、可见性问题的本质,是计算机工程师为了最大化提升CPU 利用率导致的。比如为了提升CPU 利用率,设计了三级缓存、设计了StoreBuffer、设计了缓存行这种预读机制、在操作系统里面,设计了线程模型、在编译器里面,设计了编译器的深度优化机制。

Dubbo感知服务下线(Zookeeper)

首先,Dubbo 默认采用Zookeeper 实现服务的注册与服务发现,简单来说啊,就是多
个Dubbo 服务之间的通信地址,是使用Zookeeper 来维护的。

(如图)在Zookeeper 上,会采用树形结构的方式来维护Dubbo 服务提供端的协议
地址,Dubbo 服务消费端会从Zookeeper Server 上去查找目标服务的地址列表,从而完成
服务的注册和消费的功能。

Zookeeper 会通过心跳检测机制,来判断Dubbo 服务提供端的运行状态,来决定是否
应该把这个服务从地址列表剔除。

当Dubbo 服务提供方出现故障导致Zookeeper 剔除了这个服务的地址,那么Dubbo 服务消费端需要感知到地址的变化,从而避免后续的请求发送到故障节点,导致请求失败。

也就是说Dubbo 要提供服务下线的动态感知能力。

(如图)这个能力是通过Zookeeper 里面提供的Watch 机制来实现的,简单来说呢,Dubbo 服务消费端会使用Zookeeper 里面的Watch 来针对Zookeeper Server 端的/providers 节点注册监听,一旦这个节点下的子节点发生变化,Zookeeper Server 就会发送一个事件通知Dubbo Client 端.

行锁、间隙锁和临键锁、表锁

行锁、临键锁、间隙锁,都是Mysql 里面InnoDB 引擎下解决事务隔离性的一系列排他锁

  • 行锁,也称为记录锁。
    当我们针对主键或者唯一索引加锁的时候,Mysql 默认会对查询的这一行数据加行锁,
    避免其他事务对这一行数据进行修改。
  • 间隙锁,顾名思义,就是锁定一个索引区间。
    在普通索引或者唯一索引列上,由于索引是基于B+树的结构存储,所以默认会存在一
    个索引区间。
    而间隙锁,就是某个事物对索引列加锁的时候,默认锁定对应索引的左右开区间范围。
    在基于索引列的范围查询,无论是否是唯一索引,都会自动触发间隙锁。
    比如基于between 的范围查询,就会产生一个左右开区间的间隙锁。
  • 最后一个是临键锁,(如图)它相当于行锁+间隙锁的组合,也就是它的锁定范围既包
    含了索引记录,也包含了索引区间
    它会锁定一个左开右闭区间的数据范围。

可重入锁

简单来说,就是在运行的某个函数或者代码,因为抢占资源或者中断等原因导致函数或者代码的运行中断,等待中断程序执行结束后重新进入到这个函数或者代码中运行,并且运行结果不会受到影响,那么这个函数或者代码就是可重入的。

(如图) 而可重入锁,简单来说就是一个线程如果抢占到了互斥锁资源,在锁释放之前再去竞争同一把锁的时候,不需要等待,只需要记录重入次数

在多线程并发编程里面,绝大部分锁都是可重入的,比如Synchronized、ReentrantLock 等,但是也有不支持重入的锁,比如JDK8 里面提供的读写锁StampedLock。

锁的可重入性,主要解决的问题是避免线程死锁的问题。因为一个已经获得同步锁X 的线程,在释放锁X 之前再去竞争锁X 的时候,相当于会出现自己要等待自己释放锁,这很显然是无法成立的。

ReentrantLock 实现原理

什么是ReentrantLock

ReentrantLock 是一种可重入的排它锁,主要用来解决多线程对共享资源竞争的问题。
它有三个比较核心的特性:

  • 第1,它支持可重入,也就是获得锁的线程在释放锁之前再次去竞争同一把锁的时候,不需要加锁就可以直接访问。
  • 第2,它支持公平和非公平特性
  • 第3,它提供了阻塞竞争锁和非阻塞竞争锁的两种方法,分别是lock()和tryLock()。

实现原理

ReentrantLock 的底层实现有几个非常关键的技术。

  • 第1 个,锁的竞争,ReentrantLock 是通过互斥变量,使用CAS 机制Compare and Swap来实现的。
    没有竞争到锁的线程,使用了AbstractQueuedSynchronizer 这样一个队列同步器来存储,底层是通过双向链表来实现的。当锁被释放之后,会从AQS 队列里面的头部唤醒下一个等待锁的线程。
  • 第2 个,公平和非公平的特性,主要是体现在竞争锁的时候,是否需要判断AQS 队列存在等待中的线程。
  • 第3 个,锁的重入特性,在AQS 里面有一个成员变量来保存当前获得锁的线程,当同一个线程下次再来竞争锁的时候,就不会去走锁竞争的逻辑,而是直接增加重入次数

CAS(Compare and Swap)

CAS,是Compare and Swap的简称,在这个机制中有三个核心的参数:

主内存中存放的共享变量的值:V(一般情况下这个V是内存的地址值,通过这个地址可以获得内存中的值)
工作内存中共享变量的副本值,也叫预期值:A
需要将共享变量更新到的最新值B

简单的说法:
cas,比较并交换。cas算法的过程是这样的,cas包括有三个值:
v表示要更新的变量
A表示预期值,就是旧的值
B表示新值
更新时,判断只有A的值等于V变量的当前旧值时,才会将B新值赋给A,更新为新值。
否则,则认为已经有其他线程更新过了,则当前线程什么都不操作,最后cas放回当前v变量的真实值。

缺点

  1. ABA问题
    ABA问题:CAS在操作的时候会检查变量的值是否被更改过,如果没有则更新值,但是带来一个问题,最开始的值是A,接着变成B,最后又变成了A。经过检查这个值确实没有修改过,因为最后的值还是A,但是实际上这个值确实已经被修改过了。
    为了解决这个问题,在每次进行操作的时候加上一个版本号,每次操作的就是两个值,一个版本号和某个值,A——>B——>A问题就变成了1A——>2B——>3A。
    在jdk中提供了AtomicStampedReference类解决ABA问题,用Pair这个内部类实现,包含两个属性,分别代表版本号和引用,在compareAndSet中先对当前引用进行检查,再对版本号标志进行检查,只有全部相等才更新值。

  2. 可能会消耗较高的CPU
    看起来CAS比锁的效率高,从阻塞机制变成了非阻塞机制,减少了线程之间等待的时间。每个方法不能绝对的比另一个好,在线程之间竞争程度大的时候,如果使用CAS,每次都有很多的线程在竞争,也就是说CAS机制不能更新成功。这种情况下CAS机制会一直重试,这样就会比较耗费CPU。
    因此可以看出,如果线程之间竞争程度小,使用CAS是一个很好的选择;但是如果竞争很大,使用锁可能是个更好的选择。在并发量非常高的环境中,如果仍然想通过原子类来更新的话,可以使用AtomicLong的替代类:LongAdder。

  3. 不能保证代码块的原子性
    Java中的CAS机制只能保证共享变量操作的原子性,而不能保证代码块的原子性

优点

  • 可以保证变量操作的原子性;
  • 并发量不是很高的情况下,使用CAS机制比使用锁机制效率更高;
  • 在线程对共享资源占用时间较短的情况下,使用CAS机制效率也会较高。

乐观锁与悲观锁

悲观锁更新的方式认为:在更新数据的时候大概率会有其他线程去争夺共享资源,所以悲观锁的做法是:
第一个获取资源的线程会将资源锁定起来,其他没争夺到资源的线程只能进入阻塞队列,等第一个获取资源的线程释放锁之后,这些线程才能有机会重新争夺资源。
synchronized就是java中悲观锁的典型实现,synchronized使用起来非常简单方便,但是会使没争抢到资源的线程进入阻塞状态,线程在阻塞状态和Runnable状态之间切换效率较低(比较慢)。
比如你的更新操作其实是非常快的,这种情况下你还用synchronized将其他线程都锁住了,线程从Blocked状态切换回Runnable的时间可能比你的更新操作的时间还要长。

乐观锁更新方式认为:在更新数据的时候其他线程争抢这个共享变量的概率非常小,所以更新数据的时候不会对共享数据加锁。
但是在正式更新数据之前会检查数据是否被其他线程改变过,如果未被其他线程改变过就将共享变量更新成最新值,如果发现共享变量已经被其他线程更新过了,就重试,直到成功为止。CAS机制就是乐观锁的典型实现。

Synchronized 锁升级的原理

Synchronized 在jdk1.6 版本之前,是通过重量级锁的方式来实现线程之间锁的竞争。
之所以称它为重量级锁,是因为它的底层依赖操作系统的Mutex Lock 来实现互斥功能
(如图)Mutex 是系统方法,由于权限隔离的关系,应用程序调用系统方法时需要切换到内核态来执行。
这里涉及到用户态向内核态的切换,这个切换会带来性能的损耗

  1. 在jdk1.6 版本中,synchronized 增加了锁升级的机制,来平衡数据安全性和性能。
    简单来说,就是线程去访问synchronized 同步代码块的时候,synchronized 根据线程竞争情况,会先尝试在不加重量级锁的情况下去保证线程安全性。所以引入了偏向锁和轻量级锁的机制。
  • 偏向锁,就是直接把当前锁偏向于某个线程,简单来说就是通过CAS 修改偏向锁标记,
    这种锁适合同一个线程多次去申请同一个锁资源并且没有其他线程竞争的场景。
  • 轻量级锁也可以称为自旋锁,基于自适应自旋的机制,通过多次自旋重试去竞争锁。自旋锁优点在于它避免了用户态到内核态的切换带来的性能开销
  1. (如图)Synchronized 引入了锁升级的机制之后,如果有线程去竞争锁:
    首先,synchronized 会尝试使用偏向锁的方式去竞争锁资源,如果能够竞争到偏向锁,表示加锁成功直接返回。
  • 如果竞争锁失败,说明当前锁已经偏向了其他线程,需要将锁升级到轻量级锁
  • 在轻量级锁状态下,竞争锁的线程根据自适应自旋次数去尝试抢占锁资源,如果在轻量级锁状态下还是没有竞争到锁,就只能升级到重量级锁
  • 在重量级锁状态下,没有竞争到锁的线程就会被阻塞,线程状态是Blocked。处于锁等待状态的线程需要等待获得锁的线程来触发唤醒。

总的来说, Synchronized 的锁升级的设计思想,在我看来本质上是一种性能和安全性的平衡,也就是如何在不加锁的情况下能够保证线程安全性
这种思想在编程领域比较常见,比如Mysql 里面的MVCC 使用版本链的方式来解决多个并行事务的竞争问题。

公平锁和非公平锁

公平,指的是竞争锁资源的线程,严格按照请求顺序来分配锁。
非公平,表示竞争锁资源的线程,允许插队来抢占锁资源。

ReentrantLock 和Synchronized默认都是非公平锁的策略,之所以要这么设计,考虑到了性能原因。

因为一个竞争锁的线程如果按照公平的策略去阻塞等待,同时AQS 再把等待队列里面的线程唤醒,这里会涉及到内核态的切换,对性能的影响比较大。

如果是非公平策略,当前线程正好在上一个线程释放锁的临界点抢占到了锁,就意味着这个线程不需要切换到内核态,虽然对原本应该要被唤醒的线程不公平,但是提升了锁竞争的性能。

Nacos

Nacos 是采用长轮询的方式向Nacos Server 端发起配置更新查询的功能。

所谓长轮询,(如图)就是客户端发起一次轮询请求到服务端,当服务端配置没有任何变更的时候,这个连接一直打开。直到服务端有配置或者连接超时后返回。

Nacos Client 端需要获取服务端变更的配置,前提是要有一个比较,也就是拿客户端本地的配置信息和服务端的配置信息进行比较

一旦发现和服务端的配置有差异,就表示服务端配置有更新,于是把更新的配置拉到本地。
在这个过程中,有可能因为客户端配置比较多,导致比较的时间较长,使得配置同步较慢的问题。
于是Nacos 针对这个场景,做了两个方面的优化。

  1. 减少网络通信的数据量,客户端把需要进行比较的配置进行分片,每一个分片大小是3000,
    也就是说,每次最多拿3000 个配置去Nacos Server 端进行比较。
  2. 分阶段进行比较和更新,
    第一阶段,客户端把这3000 个配置的key 以及对应的value 值的md5 拼接成一个字符串,然后发送到Nacos Server 端进行判断,服务端会逐个比较这些配置中md5 不同的key,把存在更新的key 返回给客户端。
    第二阶段,客户端拿到这些变更的key,循环逐个去调用服务单获取这些key 的value值。这两个优化,核心目的是减少网络通信数据包的大小,把一次大的数据包通信拆分成了多次小的数据包通信。虽然会增加网络通信次数,但是对整体的性能有较大的提升。

最后,再采用长连接这种方式,既减少了pull 轮询次数,又利用了长连接的优势,很好的实现了配置的动态更新同步功能。

线程池

首先,线程池本质上是一种池化技术,而池化技术是一种资源复用的思想,比较常见的有连接池、内存池、对象池。

而线程池里面复用的是线程资源,它的核心设计目标,我认为有两个:

  1. 减少线程的频繁创建和销毁带来的性能开销,因为线程创建会涉及到CPU 上下文切换、内存分配等工作。
  2. 线程池本身会有参数来控制线程创建的数量,这样就可以避免无休止的创建线程带来的资源利用率过高的问题,起到了资源保护的作用。

线程池里面采用了生产者消费者的模式,来实现线程复用。

生产者不断生产任务保存到容器,消费者不断从容器中消费任务。
在线程池里面,因为需要保证工作线程的重复使用,
并且这些线程应该是有任务的时候执行,没任务的时候等待并释放CPU 资源。
因此(如图),它使用了阻塞队列来实现这样一个需求。
提交任务到线程池里面的线程称为生产者线程,它不断往线程池里面传递任务
这些任务会保存到线程池的阻塞队列里面
然后线程池里面的工作线程不断从阻塞队列获取任务去执行

所以为了实现线程的复用,线程池里面用到了阻塞队列,简单来说就是线程池里面的工作线程处于一直运行状态,它会从阻塞队列中去获取待执行的任务,一旦队列空了,那这个工作线程就会被阻塞直到下次有新的任务进来

也就是说,工作线程是根据任务的情况实现阻塞和唤醒,从而达到线程复用的目的。

最后,线程池里面的资源限制,是通过几个关键参数来控制的,分别是核心线程数、最大线程数。

核心线程数表示默认长期存在的工作线程,而最大线程数是根据任务的情况动态创建的线程,主要是提高阻塞队列中任务的处理效率。

当任务数超过线程池的核心线程数时,如何让它不进入队列,而是直接启用最大线程数

当我们提交一个任务到线程池的时候,它的工作原理分为四步。

  • 第一步,预热核心线程
  • 第二步,把任务添加到阻塞队列
  • 第三步,如果添加到阻塞队列失败,则创建非核心线程增加处理效率
  • 第四步,如果非核心线程数达到了阈值,就触发拒绝策略

所以,如果希望这个任务不进入队列,那么只需要去影响第二步的执行逻辑就行了。
Java 中线程池提供的构造方法里面,有一个参数可以修改阻塞队列的类型。
其中,就有一个阻塞队列叫SynchronousQueue(如图), 这个队列不能存储任何元素。
它的特性是,每生产一个任务,就必须要指派一个消费者来处理,否则就会阻塞生产者。

基于这个特性,只要把线程池的阻塞队列替换成SynchronousQueue。
就能够避免任务进入到阻塞队列,而是直接启动最大线程数去处理这个任务。

java官方提供的线程池的特点

  • newCachedThreadPool, 是一种可以缓存的线程池,它可以用来处理大量短期的突发流量。
    它的特点有三个,最大线程数是Integer.MaxValue,线程存活时间是60 秒,
    阻塞队列用的是SynchronousQueue,这是一种不存才任何元素的阻塞队列,也就是每提交一个任务给到线程池,都会分配一个工作线程来处理,由于最大线程数没有限制。
    所以它可以处理大量的任务,另外每个工作线程又可以存活60s,使得这些工作线程可以缓存起来应对更多任务的处理。
  • newFixedThreadPool,是一种固定线程数量的线程池。它的特点是核心线程和最大线程数量都是一个固定的值,如果任务比较多工作线程处理不过来,就会加入到阻塞队列里面等待。
  • newSingleThreadExecutor,只有一个工作线程的线程池。并且线程数量无法动态更改,因此可以保证所有的任务都按照FIFO 的方式顺序执行。
  • newScheduledThreadPool,具有延迟执行功能的线程池可以用它来实现定时调度
  • newWorkStealingPool,Java8 里面新加入的一个线程池。它内部会构建一个ForkJoinPool,利用工作窃取的算法并行处理请求。

IO和NIO

IO

I/O ,指的是IO 流, 它可以实现数据从磁盘中的读取以及写入。
实际上,除了磁盘以外,内存、网络都可以作为I/O 流的数据来源和目的地。
在Java 里面,提供了字符流和字节流两种方式来实现数据流的操作。
其次,当程序是面向网络进行数据的IO 操作的时候,Java 里面提供了Socket 的方式来实现。

通过这种方式可以实现数据的网络传输。

(如图)基于Socket 的IO 通信,它是属于阻塞式IO,也就是说,在连接以及IO 事件未就绪的情况下,当前的连接会处于阻塞等待的状态。

如果一旦某个连接处于阻塞状态,那么后续的连接都得等待。所以服务端能够处理的连
接数量非常有限。

NIO

NIO,是JDK1.4 里面新增的一种NEW IO 机制,相比于传统的IO,NIO 在效率上做了很大的优化,并且新增了几个核心组件。

Channel、Buffer、Selectors。
(如图)另外,还提供了非阻塞的特性,所以,对于网络IO 来说,NIO 通常也称为No-Block IO,非阻塞IO。

也就是说,通过NIO 进行网络数据传输的时候,如果连接未就绪或者IO 事件未就绪的情况下,服务端不会阻塞当前连接,而是继续去轮询后续的连接来处理

所以在NIO 里面,服务端能够并行处理的链接数量更多。

因此,总的来说,IO 和NIO 的区别,站在网络IO 的视角来说,前者是阻塞IO,后者是非阻塞IO。

幂等性问题

幂等是指一个方法被多次重复执行的时候产生的影响和第一次执行的影响相同

之所以要考虑到幂等性问题,是因为在网络通信中,存在两种行为可能会导致接口被重复执行

  1. 用户的重复提交或者用户的恶意攻击,导致这个请求会被多次重复执行。
  2. 在分布式架构中,为了避免网络通信导致的数据丢失,在服务之间进行通信的时候都会设计超时重试的机制,而这种机制有可能导致服务端接口被重复调用。
    所以在程序设计中,对于数据变更类操作的接口,需要保证接口的幂等性。

而幂等性的核心思想,其实就是保证这个接口的执行结果只影响一次,后续即便再次调用,也不能对数据产生影响,所以基于这样一个诉求,常见的解决方法有很多。

  1. 使用数据库的唯一约束实现幂等,比如对于数据插入类的场景,比如创建订单,因为订单号肯定是唯一的,所以如果是多次调用就会触发数据库的唯一约束异常,从而避免一个请求创建多个订单的问题。

  2. 使用redis 里面提供的setNX 指令,比如对于MQ 消费的场景,为了避免MQ 重复消费导致数据多次被修改的问题,可以在接受到MQ 的消息时,把这个消息通过setNx 写入到redis 里面,一旦这个消息被消费过,就不会再次消费。

  3. 使用状态机来实现幂等,所谓的状态机是指一条数据的完整运行状态的转换流程,比如订单状态,因为它的状态只会向前变更,所以多次修改同一条数据的时候,一旦状态发生变更,那么对这条数据修改造成的影响只会发生一次。

new String(“abc”)到底创建了几个对象

首先,这个代码里面有一个new 关键字,这个关键字是在程序运行时,根据已经加载的系统类String,在堆内存里面实例化的一个字符串对象。

然后,在这个String 的构造方法里面,传递了一个“abc”字符串,因为String 里面的字符串成员变量是final 修饰的,所以它是一个字符串常量。

接下来,JVM 会拿字面量“abc” 去字符串常量池里面试图去获取它对应的String 对象引用,如果拿不到,就会在堆内存里面创建一个”abc”的String 对象,并且把引用保存到字符串常量池里面。

后续如果再有字面量“abc”的定义,因为字符串常量池里面已经存在了字面量“abc”的引用,所以只需要从常量池获取对应的引用就可以了,不需要再创建。

所以,对于这个问题,我认为的答案是

  1. 如果abc 这个字符串常量不存在,则创建两个对象,分别是abc 这个字符串常量,以及new String 这个实例对象
  2. 如果abc 这字符串常量存在,则只会创建一个对象

限流算法

常见的限流算法有5 种。

  1. (如图)计数器限流,一般用在单一维度的访问频率限制上,比如短信验证码每隔
    60s 只能发送一次,或者接口调用次数等它的实现方法很简单,每调用一次就加1,处理结束以后减一。

  2. (如图)滑动窗口限流,本质上也是一种计数器,只是通过以时间为维度的可滑动窗口设计,来减少了临界值带来的并发超过阈值的问题。
    每次进行数据统计的时候,只需要统计这个窗口内每个时间刻度的访问量就可以了。
    Spring Cloud 里面的熔断框架Hystrix ,以及Spring Cloud Alibaba 里面的Sentinel都采用了滑动窗口来做数据统计。

  3. (如图)漏桶算法,它是一种恒定速率的限流算法,不管请求量是多少,服务端的处理效率是恒定的。基于MQ 来实现的生产者消费者模型,其实算是一种漏桶限流算法。

  4. (如图)令牌桶算法,相对漏桶算法来说,它可以处理突发流量的问题。它的核心思想是,令牌桶以恒定速率去生成令牌保存到令牌桶里面,桶的大小是固定的,令牌桶满了以后就不再生成令牌。
    每个客户端请求进来的时候,必须要从令牌桶获得一个令牌才能访问,否则排队等待。在流量低峰的时候,令牌桶会出现堆积,因此当出现瞬时高峰的时候,有足够多的令牌可以获取,因此令牌桶能够允许瞬时流量的处理。
    网关层面的限流、或者接口调用的限流,都可以使用令牌桶算法,像Google 的Guava,和edisson 的限流,都用到了令牌桶算法

Kafka

Kafka避免重复消费

Kafka Broker 上存储的消息,都有一个Offset 标记。然后kafka 的消费者是通过offSet 标记来维护当前已经消费的数据,每消费一批数据,Kafka Broker 就会更新OffSet 的值,避免重复消费。

为什么会产生重复消费:

  • Kafka 消费端的自动提交逻辑有一个默认的5 秒间隔,也就是说在5 秒之后的下一次向Broker 拉取消息的时候提交。
    所以在Consumer 消费的过程中,应用程序被强制kill 掉或者宕机,可能会导致Offset没提交,从而产生重复提交的问题。

  • 在Kafka 里面有一个Partition Balance 机制,就是把多个Partition 均衡的分配给多个消费者。
    Consumer 端会从分配的Partition 里面去消费消息,如果Consumer 在默认的5 分钟内没办法处理完这一批消息。
    就会触发Kafka 的Rebalance 机制,从而导致Offset 自动提交失败。
    而在重新Rebalance 之后,Consumer 还是会从之前没提交的Offset 位置开始消费,也会导致消息重复消费的问题。

解决方法:

  1. 提高消费端的处理性能避免触发Balance,比如可以用异步的方式来处理消息,缩短单个消息消费的市场。或者还可以调整消息处理的超时时间。还可以减少一次性从Broker 上拉取数据的条数。
  2. 可以针对消息生成md5 然后保存到mysql 或者redis 里面,在处理消息之前先去mysql 或者redis 里面判断是否已经消费过。这个方案其实就是利用幂等性的思想。

如何保证消费顺序性

序列化和反序列化

解决的问题:如何把当前JVM 进程里面的一个对象跨网络传输到另外一个JVM进程里面。

  • 序列化,就是把内存里面的对象转化为字节流,以便用来实现存储或者传输。
  • 反序列化,就是根据从文件或者网络上获取到的对象的字节流,根据字节流里面保存的对象描述信息和状态,重新构建一个新的对象。

Hashmap

为了避免链表过长的问题,当链表长度大于8 并且数组长度大于等于64 的时候,HashMap 会把链表转化为红黑树(如图)。

HashMap 底层采用了数组的结构来存储数据元素,数组的默认长度是16

从而减少链表数据查询的时间复杂度问题,提升查询性能。

解决hash 冲突问题的方法有很多,比如

  • 再hash 法,就是如果某个hash 函数产生了冲突,再用另外一个hash 进行计算,比如布隆过滤器就采用了这种方法。
  • 开放寻址法,就是直接从冲突的数组位置往下寻找一个空的数组下标进行数据存储,这个在ThreadLocal 里面有使用到。
  • 建立公共溢出区,也就是把存在冲突的key 统一放在一个公共溢出区里面。

Hashmap什么时候扩容,为什么要扩容

当我们创建一个集合对象的时候,实际上就是在内存中一次性申请一块内存空间。

而这个内存空间大小是在创建集合对象的时候指定的。
比如List 的默认大小是10、HashMap 的默认大小是16

当集合的存储容量达到某个阈值的时候,集合就会进行动态扩容,从而更好的满足更多数据的存储。

List 和HashMap,本质上都是一个数组结构,所以基本上只需要新建一个更长的数组
然后把原来数组中的数据拷贝到新数组
就行了。

HashMap 是如何扩容的?

当HashMap 中元素个数超过临界值时会自动触发扩容,这个临界值有一个计算公式。
threashold=loadFactor*capacity(屏幕显示)。
loadFactor 的默认值是0.75,capacity 的默认值是16,也就是元素个数达到12 的时候触发扩容。

扩容后的大小是原来的2 倍

由于动态扩容机制的存在,所以我们在实际应用中,需要注意在集合初始化的时候明确指定集合的大小。避免频繁扩容带来性能上的影响。

假设我们要向HashMap 中存储1024 个元素,如果按照默认值16,随着元素的不断
增加,会造成7 次扩容。而这7 次扩容需要重新创建Hash 表,并且进行数据迁移,对性能影响非常大。

为什么扩容因子是0.75?

扩容因子表示Hash 表中元素的填充程度,扩容因子的值越大,那么触发扩容的元素个数更多,

虽然空间利用率比较高,但是hash 冲突的概率会增加。

扩容因子的值越小,触发扩容的元素个数就越少,也意味着hash 冲突的概率减少,但是对内存空间的浪费就比较多,而且还会增加扩容的频率。

因此,扩容因子的值的设置,本质上就是在冲突的概率以及空间利用率之间的平衡
0.75 这个值的来源,和统计学里面的泊松分布有关。

我们知道,HashMap 里面采用链式寻址法来解决hash 冲突问题,为了避免链表过长带来时间复杂度的增加所以链表长度大于等于7 的时候,就会转化为红黑树,提升检索效率。

当扩容因子在0.75 的时候,链表长度达到8 的可能性几乎为0,也就是比较好的达到了空间成本和时间成本的平衡。

标准回答:
当HashMap 元素个数达到扩容阈值,默认是12 的时候,会触发扩容。
默认扩容的大小是原来数组长度的2 倍,HashMap 的最大容量是Integer.MAX_VALUE,也就是2 的31 次方-1。

Hashmap和HashTable的区别

  • 从功能特性的角度来说

    • HashTable 是线程安全的,而HashMap 不是。
    • HashMap 的性能要比HashTable 更好,因为,HashTable 采用了全局同步锁来保证安全性,对性能影响较大
  • 从内部实现的角度来说
    • HashTable 使用数组加链表、HashMap 采用了数组+链表+红黑树,链表长度大于等于8 并且数组长度大于64 的时候,就会把链表转化为红黑树
    • HashMap 初始容量是16、HashTable 初始容量是11
    • HashMap 可以使用null 作为key,HashMap 会把null 转化为0 进行存储,而Hashtable 不允许。

最后,他们两个的key 的散列算法不同,HashTable 直接是使用key 的hashcode 对数组长度做取模。
而HashMap 对key 的hashcode 做了二次散列,从而避免key 的分布不均匀问题影响到查询性能。

为什么重写equals() 就一定要重写hashCode() 方法

equals 方法是String 这个类里面的实现。
从代码中可以看到,当调用equals 比较两个对象的时候,会做两个操作

  1. 用==号比较两个对象的内存地址,如果地址相同则返回true
  2. 否则,继续比较字符串的值,如果两个字符串的值完全相等,同样返回true

但是如果我们只重写了equals 方法,就有可能导致hashCode 不相同。
一旦出现这种情况,就导致这个类无法和所有集合类一起工作

equals 和hashCode()有什么关系呢?

  • 首先,Java 里面任何一个对象都有一个native 的hashCode()方法
  • 其次,这个方法在散列集合中会用到,比如HashTable、HashMap 这些,当添加
    元素的时候,需要判断元素是否存在,而如果用equals 效率太低,所以一般是直接用对象的hashCode 的值进行取模运算。

    • 如果table 中没有该hashcode 值,它就可以直接存进去,不用再进行任何比较了;
    • 如果存在该hashcode 值, 就调用它的equals 方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals 方法的次数就大大降低了
      hashCode 的值默认是JVM 使用随机数来生成的,两个不同的对象,可能生成的HashCode 会相同。
      这种情况在Hash 表里面就是所谓的哈希冲突,通常会使用链表或者线性探测等方式来解决冲突问题。
      但是如果两个完全相同的对象,也就是内存地址指向同一个,那么他们的hashCode一定是相同的。

Thread和Ruunable区别

  1. Thread 是一个类,Runnable 是接口,因为在Java 语言里面的继承特性,接口可以支持多继承,而类只能单一继承
    所以如果在已经存在继承关系的类里面要实现线程的话,只能实现Runnable 接口
  2. Runnable 表示一个线程的顶级接口,Thread 类其实也是实现了Runnable 接口
  3. 站在面向对象的思想来说,Runnable 相当于一个任务,而Thread 才是真正处理的线程,所以我们只需要用Runnable 去定义一个具体的任务,然后交给Thread去处理就可以了,这样达到了松耦合的设计目的。
  4. Runnable 接口定义了线程执行任务的标准方法run,所以它所以,基于这四个点的原因,所以在实际应用中,建议实现Runnable 接口实现线程的任务定义,然后使用Thread 的start 方法去启动启动线程并执行Runnable 这个任务。

深拷贝和浅拷贝

深拷贝和浅拷贝是用来描述对象或者对象数组这种引用数据类型的复制场景的。
浅拷贝,(如图)就是只复制某个对象的指针,而不复制对象本身
这种复制方式意味着两个引用指针指向被复制对象的同一块内存地址。

深拷贝,(如图)会完全创建一个一模一样的新对象,新对象和老对象不共享内存,也就意味着对新对象的修改不会影响老对象的值。

在Java 里面,无论是深拷贝还是浅拷贝,都需要通过实现Cloneable 接口,并实现clone()方法。
然后我们可以在clone()方法里面实现浅拷贝或者深拷贝的逻辑。
实现深拷贝的方法有很多,比如

  1. 通过序列化的方式实现,也就是把一个对象先序列化一遍,然后再反序列化回来,就会得到一个完整的新对象
  2. 在clone()方法里面重写克隆逻辑,也就是对克隆对象内部的引用变量再进行一次克隆

Wait和Sleep的区别

Object.wait()方法,会释放锁资源以及CPU 资源
Thread.sleep()方法,不会释放锁资源,但是会释放CPU 资源。

首先,wait()方法是让一个线程进入到阻塞状态,而这个方法必须要写在一个Synchronized 同步代码块里面

因为wait/notify 是基于共享内存来实现线程通信的工具,这个通信涉及到条件的竞争,所以在调用这两个方法之前必须要竞争锁资源。

当线程调用wait 方法的时候,表示当前线程的工作处理完了,意味着让其他竞争同一个共享资源的线程有机会去执行。

但前提是其他线程需要竞争到锁资源,所以wait 方法必须要释放锁,否则就会导致死锁的问题。

然后,Thread.sleep()方法,只是让一个线程单纯进入睡眠状态,这个方法并没有强制要求加synchronized 同步锁。

而且从它的功能和语义来说,也没有这个必要。

当然,如果是在一个Synchronized 同步代码块里面调用这个Thread.sleep,也并不会触发锁的释放。

最后,凡是让线程进入阻塞状态的方法,操作系统都会重新调度实现CPU 时间片切换,这样设计的目的是提升CPU 的利用率。

项目的难点(生产环境服务器处理效率变慢)

主要会涉及到三个维度:

  • CPU 的利用率
  • 磁盘IO 效率
  • 内存

CPU

CPU 利用率过高或者CPU 利用率过低,都会影响程序的处理效率。
利用率过高,说明当前服务器要处理的指令比较多,当CPU 忙不过来的时候,指令的
运算效率自然就会下降。
反馈在用户上的感受就是程序响应变慢了。
针对这个问题,我们可以使用top 命令查询当前系统中占用CPU 过高的进程,以及定
位到这个进程中比较活跃的线程。
再通过jstack 命令打印当前虚拟机的线程快照,然后根据快照日志排查问题代码。
如果CPU 利用率过低,说明程序资源使用不够,可以增加线程数量提升程序性能。

磁盘IO

程序运算过程中,会直接或者间接涉及到一些磁盘IO 相关的操作,比如程序直接读写
磁盘,或者程序依赖的第三方组件涉及到磁盘的持久化存储,所以磁盘的IO 效率也会对程序
运行效率产生影响。
针对这个情况,可以使用iostat 命令查看,如果磁盘负载较高,可以针对性的进行优化,比如

  • 借助缓存系统,减少磁盘IO 次数
  • 用顺序写替代随机写入,减少寻址开销
  • 使用mmap 替代read/write,减少内存拷贝次数
    另外,系统IO 的瓶颈可以通过CPU 和负载的非线性关系体现出来。当负载增大时,系
    统吞吐量不能有效增大,CPU 不能线性增长,其中一种可能是IO 出现阻塞。

内存

最后,就是内存的瓶颈,内存作为一块临时存储数据的组件,所有CPU 运算的指令都
需要从内存中去读写。
内存的合理使用,可以减少应用和磁盘的直接IO 频率,以及减少网络IO 的频率,极大提升IO 性能。
其次,作为Java 应用程序的运行平台JVM,对于内存的合理分配,能够避免频繁的YGC和FULL GC。
内存使用率比较高的时候, 可以dump 出JVM 堆内存,然后借助MAT 工具进行分析,查出大对象或者占用最多的对象,以及排查是否存在内存泄漏的问题。
如果dump 出的堆内存文件正常,此时可以考虑堆外内存被大量使用导致出现问题,需要借助操作系统指令pmap 查出进程的内存分配情况。
如果CPU 和内存使用率都很正常,那就需要进一步开启GC 日志,分析用户线程暂停的时间、各部分内存区域GC 次数和时间等指标,可以借助jstat 或可视化工具GCeasy 等,如果问题出在GC 上面的话,考虑是否是内存不够、根据垃圾对象的特点进行参数调优、使用更适合的垃圾收集器;

分析jstack 出来的各个线程状态。如果问题实在比较隐蔽,考虑是否可以开启jmx,使用visualmv 等可视化工具远程监控与分析。

JVM调优

参考https://zhuanlan.zhihu.com/p/488615913

https://www.cnblogs.com/three-fighter/p/14644152.html

在我的项目当中,由于微服务过多内存不够用,解决方法:

每个服务的内存改为512M,-Xms512m -Xms512m -Xss256k

更改IDEA可使用内存

-Xms512m # 初始堆大小
-Xmx2048m # 最大堆大小
-XX:ReservedCodeCacheSize=512m
-XX:+UseConcMarkSweepGC
-XX:SoftRefLRUPolicyMSPerMB=100 # 软引用保持活动的时间(单位毫秒)

RabbitMQ

RabbitMQ 整体上是一个生产者与消费者模型

  • 生产者 :消息生产者,就是投递消息的一方。消息一般包含两个部分:消息体(payload)和标签(Label)。
  • 消费者:消费消息,也就是接收消息的一方。消费者连接到 RabbitMQ 服务器,并订阅到队列上。消费消息时只消费消息体,丢弃标签。
  • Broker:可以看做 RabbitMQ 的服务节点。一般请下一个 Broker 可以看做一个 RabbitMQ 服务器。
  • Queue :RabbitMQ 的内部对象,用于存储消息。多个消费者可以订阅同一队列,这时队列中的消息会被平摊(轮询)给多个消费者进行处理。
  • Exchange : 生产者将消息发送到交换器,由交换器将消息路由到一个或者多个队列中。当路由不到时,或返回给生产者或直接丢弃。

RabbitMQ 中消息只能存储在 队列 中,这一点和 Kafka 这种消息中间件相反。Kafka 将消息存储在 topic(主题) 这个逻辑层面,而相对应的队列逻辑只是 topic 实际存储文件中的位移标识。

RabbitMQ 的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,这样避免消息被重复消费。

AMQP

RabbitMQ 就是 AMQP 协议的 Erlang 的实现(当然 RabbitMQ 还支持 STOMP2、 MQTT3 等协议 ) AMQP 的模型架构 和 RabbitMQ 的模型架构是一样的,生产者将消息发送给交换器,交换器和队列绑定 。

RabbitMQ 中的交换器、交换器类型、队列、绑定、路由键等都是遵循的 AMQP 协议中相 应的概念。目前 RabbitMQ 最新版本默认支持的是 AMQP 0-9-1。

AMQP 协议的三层:

  • Module Layer:协议最高层,主要定义了一些客户端调用的命令,客户端可以用这些命令实现自己的业务逻辑。
  • Session Layer:中间层,主要负责客户端命令发送给服务器,再将服务端应答返回客户端,提供可靠性同步机制和错误处理。
  • TransportLayer:最底层,主要传输二进制数据流,提供帧的处理、信道服用、错误检测和数据表示等。

AMQP 模型的三大组件:

  • 交换器 (Exchange):消息代理服务器中用于把消息路由到队列的组件。
  • 队列 (Queue):用来存储消息的数据结构,位于硬盘或内存中。
  • 绑定 (Binding):一套规则,告知交换器消息应该将消息投递给哪个队列。

如何保证消息的可靠性

消息到 MQ 的过程中搞丢,MQ 自己搞丢,MQ 到消费过程中搞丢。

  • 生产者到 RabbitMQ:事务机制和 Confirm 机制,注意:事务机制和 Confirm 机制是互斥的,两者不能共存,会导致 RabbitMQ 报错。
  • RabbitMQ 自身:持久化、集群、普通模式、镜像模式。
  • RabbitMQ 到消费者:basicAck 机制死信队列、消息补偿机制。

工作模式

参考https://www.cnblogs.com/Jeely/p/10784013.html
routing路由模式

消息生产者将消息发送给交换机按照路由判断,路由是字符串(info) 当前产生的消息携带路由字符(对象的方法),交换机根据路由的key,只能匹配上路由key对应的消息队列,对应的消费者才能消费消息;

根据业务功能定义路由字符串

高可用的实现

RabbitMQ 高可用实现方式有两种,

  • 第一种是普通集群模式,在这种模式下,一个Queue 的消息只会存在集群的一个节点上,集群里面的其他节点会同步Queue 所在节点的元数据,消息在生产和消费的时候,不管请求发送到集群的哪个节点,最终都会路由到Queue 所在节点上去存储和拉取消息。
    这种方式并不能保证Queue 的高可用性,但是它可以提升RabbitMQ 的消息吞吐能力
  • 第二种是镜像集群,也就是集群里面的每个节点都会存储Queue 的数据副本
    意味着每次生产消息的时候,都需要把消息内容同步给集群中的其他节点
    这种方式能够保证Queue 的高可用性,但是集群副本之间的同步会带来性能的损耗。

各个MQ的比较

吞吐量。开发语言、延迟、高可用、功能特性、支持协议、持久化

分布式ID

分布式全局ID 的的解决方案有很多,比如:

  • 使用Mysql 的全局表
  • 使用Zookeeper 的有序节点
  • 使用MongoDB 的objectid
  • redis 的自增id
  • UUID 等等

还需要考虑更多的因素:

  • 有序性, 有序的ID 能够更好的确认数据的位置,以及B+数的存储结构中,范围查询的效率更高,并且可以提升B+树数据维护的效率。
  • 安全性,避免恶意爬去数据造成数据泄露
  • 可用性,ID 生成系统的可用性要求非常高,一旦出现故障就会造成业务不可用的问题
  • 性能,全局id 生成系统需要满足整个公司的业务需求,涉及到亿级别的调用,对性能要求较高

雪花算法

它是由64 位长度组成的全局id 生成算法,通过对64 位进行区间划分来表述不同含义实现唯一性。


它是由一个64 位的long 类型数字组成,分为四个部分。

  • 第一部分,用1 个bit 表示符号位,一般情况下是0
  • 第二部分,用41 个bit 来表示时间戳,使用系统时间的毫秒数
  • 第三部分,用10 个bit 来记录工作机器id,这样就可以保证在多个服务器上生成的id 的唯一性。
    如果存在跨机房部署,我们还可以把它分成两个5bit,前面5 个bit 可以表示机房id,后面5 个bit 可以表示机器id。
  • 第四个部分,用12 个bit 表示序列号,表示一个递增序列,用来记录同毫秒内产生的不同id

它的好处是:

  • 算法实现简单
  • 不存在太多外部依赖
  • 可以生成有意义的有序编号
  • 基于位运算,性能也很好,Twitter 测试的峰值是10 万个每秒。

【Java】一些常问的问题相关推荐

  1. Java面试常问计算机网络问题

    转载自   Java面试常问计算机网络问题 一.GET 和 POST 的区别 GET请注意,查询字符串(名称/值对)是在 GET 请求的 URL 中发送的:/test/demo_form.asp?na ...

  2. 指南Java面试常问问题及答案

    Java 面试常问问题及答案(非常详细) 一:java 基础 1.简述 string 对象,StringBuffer.StringBuilder 区分 string 是 final 的,内部用一个 f ...

  3. java面试常问知识点,快醒醒吧

    前言 今天这篇文章是比较偏"教程"一点的文章.但也由浅入深,认真地分析了源码,并且介绍了一些在使用Spring Cache中常见的问题和解决方案,肯定是比简单的入门文档更有深度一些 ...

  4. Java面试常问基础知识(持续更新)

    欢迎关注我的知乎专栏[数据池塘],专注于分享机器学习,数据挖掘相关内容:HTTPS://zhuanlan.zhihu.com/datapool 本文中的知识都是我自己或同学在面试过程中常被问到的,在此 ...

  5. 三年Java开发,java基础常问面试题

    一.首先本职工作一定要做好做精 本人之前在干兼职的时候,也忽视过本职工作,从而导致自己落后平均技术水平,虽然之后迎头赶上,但这不能不算是个遗憾.前在接一些活的时候就感觉技术的重要性了,如果当年我技术再 ...

  6. java基础常问面试题,面试必问

    一.首先本职工作一定要做好做精 本人之前在干兼职的时候,也忽视过本职工作,从而导致自己落后平均技术水平,虽然之后迎头赶上,但这不能不算是个遗憾.前在接一些活的时候就感觉技术的重要性了,如果当年我技术再 ...

  7. java面试常问问题及答案,附源代码

    找大厂面试题,看套路!Java面试题及答案及面试解析请阅读严宏博士的Java模式或设计模式解释中的桥梁模式). 封装:一般认为封装是将数据和操作数据的方法绑定起来,数据的访问只能通过定义的界面进行.面 ...

  8. 120道java最常问面试题!

    点击上方关注 "终端研发部" 设为"星标",和你一起掌握更多数据库知识 不积跬步无以至千里,下面的内容是对网上原有的Java面试题集及答案进行了全面修订之后给出 ...

  9. 程序员开发指南!java面试常问问题

    正文 如果你参加过一些大厂面试,肯定会遇到一些开放性的问题: 1. 写一段程序,让其运行时的表现为触发了5次Young GC.3次Full GC.然后3次Young GC: 2. 如果一个Java进程 ...

  10. java 面试常问问题

    1.servlet执行流程 客户端发出http请求,web服务器将请求转发到servlet容器,servlet容器解析url并根据web.xml找到相对应的servlet,并将request.resp ...

最新文章

  1. 计算机网络实验可变长子网掩码,计算机网络实验3-子网掩码与划分子网实验报告.docx...
  2. jQuery中的 $.ajax的一些方法
  3. 从IT转行做网店奋斗历程
  4. 设计模式 - 基本功的重要性
  5. 国内大学毕业论文LaTeX模板集合
  6. 可被三整除的最大和—leetcode1262
  7. (双指针、二分Binary Search) leetcode 658. Find K closest Elements
  8. 数据分析与挖掘理论-概述
  9. python 抽象类分析
  10. XP下安装SQL2000企业版本(转载)
  11. 解释一下为什么数据文件最好采用单字符作为字段分隔符
  12. Virtualbox主机与虚拟机相互访问
  13. R语言ggplot2可视化在轴标签中添加上标(Superscript)和下标(subscript)实战
  14. yahoo.cn邮箱foxmail收发攻略
  15. 【数字图像处理之(一)】数字图像处理与相关领域概述
  16. C语言实现人民币小写转大写
  17. MongoDB分片实战
  18. word2007表格计算机,电脑员好做吗?使用word2007表格?
  19. 用API能否修改Revit链接模型
  20. Linux中断的unblance问题

热门文章

  1. 点云数据读取及可视化
  2. Android BugReport异常快速排查手册
  3. BJ模拟 Pandaria(可持续化并查集)
  4. 【C语言】一些概念的基本解释
  5. 华为明年即将推出2款5nm芯片,覆盖中高端,或将全面反超高通
  6. linux 回收子线程 和取消(杀死)线程
  7. 点云配准算法综述-完整解读
  8. eclipse可以写前端吗_php可以做前端吗
  9. CSS 之 z-index 属性详解
  10. C++程序设计课程师生互动(2012年春第12周)