面试问题收集

AOP的实现过程

如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理实现 AOP;
如果目标对象没有实现了接口,则采用 CGLIB 库;
Spring 会自动在 JDK 动态代理和 CGLIB 动态代理之间转换。

类的加载过程

面试问题收集
类加载过程主要包含加载、验证、准备、解析、初始化、使用、卸载七个方面,下面一一阐述。
  一、加载
  在加载阶段,虚拟机主要完成三件事:
  1.通过一个类的全限定名来获取定义此类的二进制字节流。
  2.将这个字节流所代表的静态存储结构转化为方法区域的运行时数据结构。
  3.在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区域数据的访问入口
  二、验证
  验证阶段作用是保证Class文件的字节流包含的信息符合JVM规范,不会给JVM造成危害。如果验证失败,就会抛出一个java.lang.VerifyError异常或其子类异常。验证过程分为四个阶段
  1.文件格式验证:验证字节流文件是否符合Class文件格式的规范,并且能被当前虚拟机正确的处理。
  2.元数据验证:是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言的规范。
  3.字节码验证:主要是进行数据流和控制流的分析,保证被校验类的方法在运行时不会危害虚拟机。
  4.符号引用验证:符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在解析阶段中发生。
  三、准备
  准备阶段为变量分配内存并设置类变量的初始化。在这个阶段分配的仅为类的变量(static修饰的变量),而不包括类的实例变量。对已非final的变量,JVM会将其设置成“零值”,而不是其赋值语句的值:
  pirvate static int size = 12;
  那么在这个阶段,size的值为0,而不是12。 final修饰的类变量将会赋值成真实的值。
  四、解析
  解析过程是将常量池内的符号引用替换成直接引用。主要包括四种类型引用的解析。类或接口的解析、字段解析、方法解析、接口方法解析。
  五、初始化
  在准备阶段,类变量已经经过一次初始化了,在这个阶段,则是根据程序员通过程序制定的计划去初始化类的变量和其他资源。这些资源有static{}块,构造函数,父类的初始化等。
  至于使用和卸载阶段阶段,这里不再过多说明,使用过程就是根据程序定义的行为执行,卸载由GC完成。

spring自动装配原理

  spring框架默认不支持自动装配的,要想使用自动装配需要修改spring配置文件中标签的autowire属性。
自动装配属性有6个值可选,分别代表不同的含义:
  byName ->从Spring环境中获取目标对象时,目标对象中的属性会根据名称在整个Spring环境中查找标签的id属性值。如果有相同的,那么获取这个对象,实现关联。整个Spring环境:表示所有的spring配置文件中查找,那么id不能有重复的。
  byType ->从Spring环境中获取目标对象时,目标对象中的属性会根据类型在整个spring环境中查找标签的class属性值。如果有相同的,那么获取这个对象,实现关联。
  缺点:如果存在多个相同类型的bean对象,会出错;如果属性为单一类型的数据,那么查找到多个关联对象会发生错误;如果属性为数组或集合(泛型)类型,那么查找到多个关联对象不会发生异常。
  constructor ->使用构造方法完成对象注入,其实也是根据构造方法的参数类型进行对象查找,相当于采用byType的方式。
  autodetect ->自动选择:如果对象没有无参数的构造方法,那么自动选择constructor的自动装配方式进行构造注入。如果对象含有无参数的构造方法,那么自动选择byType的自动装配方式进行setter注入。
  no ->不支持自动装配功能
  default ->表示默认采用上一级标签的自动装配的取值。如果存在多个配置文件的话,那么每一个配置文件的自动装配方式都是独立的。

hash扩容原理

  当HashMap中的元素个数超过数组大小(数组总大小length,不是数组中个数size)loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过160.75=12(这个值就是代码中的threshold值,也叫做临界值)的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置。

  0.75这个值成为负载因子,那么为什么负载因子为0.75呢?这是通过大量实验统计得出来的,如果过小,比如0.5,那么当存放的元素超过一半时就进行扩容,会造成资源的浪费;如果过大,比如1,那么当元素满的时候才进行扩容,会使get,put操作的碰撞几率增加

数据库锁的实现原理

面试问题收集
  悲观锁:数据库总是认为多个数据库并发操作会发生冲突,所以总是要求加锁操作。悲观锁主要表锁、行锁、页锁。
  乐观锁:数据库总是认为多个数据库并发操作不会发生冲突,所以总是不加锁操作。所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。乐观锁的实现方式一般包括使用版本号和时间戳。
  表级锁:读锁锁表,会阻碍其他事务修改表数据。写锁锁表会阻碍其他事务读与写。
  页级锁:就是对页加锁
  行级锁:行锁分为共享锁和排他锁,共享锁:一个事务对一行的共享只读锁。排它锁:一个事务对一行的排他读写锁。
  共享锁
    加锁与解锁:当一个事务执行select语句时,数据库系统会为这个事务分配一把共享锁,来锁定被查询的数据。在默认情况下,数据被读取后,数据库系统立即解除共享锁。例如,当一个事务执行查询“SELECT * FROM accounts”语句时,数据库系统首先锁定第一行,读取之后,解除对第一行的锁定,然后锁定第二行。这样,在一个事务读操作过程中,允许其他事务同时更新accounts表中未锁定的行。
    兼容性:如果数据资源上放置了共享锁,还能再放置共享锁和更新锁。
    并发性能:具有良好的并发性能,当数据被放置共享锁后,还可以再放置共享锁或更新锁。所以并发性能很好。
  排它锁:
    加锁与解锁:当一个事务执行insert、update或delete语句时,数据库系统会自动对SQL语句操纵的数据资源使用独占锁。如果该数据资源已经有其他锁(任何锁)存在时,就无法对其再放置独占锁了。
    兼容性:独占锁不能和其他锁兼容,如果数据资源上已经加了独占锁,就不能再放置其他的锁了。同样,如果数据资源上已经放置了其他锁,那么也就不能再放置独占锁了。
    并发性能:最差。只允许一个事务访问锁定的数据,如果其他事务也需要访问该数据,就必须等待。
  更新锁:
    加锁与解锁:当一个事务执行update语句时,数据库系统会先为事务分配一把更新锁。当读取数据完毕,执行更新操作时,会把更新锁升级为独占锁。
    兼容性:更新锁与共享锁是兼容的,也就是说,一个资源可以同时放置更新锁和共享锁,但是最多放置一把更新锁。这样,当多个事务更新相同的数据时,只有一个事务能获得更新锁,然后 再把更新锁升级为独占锁,其他事务必须等到前一个事务结束后,才能获取得更新锁,这就避免了死锁。
    并发性能:允许多个事务同时读锁定的资源,但不允许其他事务修改它。
  意向锁(意向共享,意向更新)
  在判断每一行是否已经被行锁锁定效率比较低下,因此使用意向锁,当发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞。

数据库事务实现原理

  原子性(Atomicity):事务中的所有操作作为一个整体像原子一样不可分割,要么全部成功,要么全部失败。
  一致性(Consistency):事务的执行结果必须使数据库从一个一致性状态到另一个一致性状态。一致性状态是指:1.系统的状态满足数据的完整性约束(主码,参照完整性,check约束等) 2.系统的状态反应数据库本应描述的现实世界的真实状态,比如转账前后两个账户的金额总和应该保持不变。
  隔离性(Isolation):并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样。比如多个用户同时往一个账户转账,最后账户的结果应该和他们按先后次序转账的结果一样。
  持久性(Durability):事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失。
  在事务的ACID特性中,C即一致性是事务的根本追求,而对数据一致性的破坏主要来自两个方面
1.事务的并发执行
2.事务故障或系统故障
  数据库系统是通过并发控制技术和日志恢复技术来避免这种情况发生的。
  并发控制技术保证了事务的隔离性,使数据库的一致性状态不会因为并发执行的操作被破坏。
  日志恢复技术保证了事务的原子性,使一致性状态不会因事务或系统故障被破坏。同时使已提交的对数据库的修改不会因系统崩溃而丢失,保证了事务的持久性。
面试问题收集

数据库的索引设计思想

1.值是唯一的
  唯一性索引的值是唯一的,可以更快速的通过该索引来确定某条记录。
  例如,学生表中学号是具有唯一性的字段。为该字段建立唯一性索引可以很快的确定某个学生的信息。
  如果使用姓名的话,可能存在同名现象,从而降低查询速度。
主键索引和唯一键索引,在查询中使用是效率最高的。
注意:如果重复值较多,可以考虑采用联合索引
2.为经常需要排序、分组、联合查询的字段建立索引
  经常需要ORDER BY、GROUP BY,join on等操作的字段,排序操作会浪费很多时间。
  如果为其建立索引,可以有效地避免排序操作。
3.为经常作为where条件的字段建立索引
  如果某个字段经常用来做查询条件,那么该字段的查询速度会影响整个表的查询速度。因此,为这样的字段建立索引,可以提高整个表的查询速度。
  3.1 经常查询
  3.2 列值的重复值少(业务层面调整)
注:如果经常作为条件的列,重复值特别多,可以建立联合索引。
4.尽量使用前缀来索引
  如果索引字段的值很长,最好使用值的前缀来索引。例如,TEXT和BLOG类型的字段,进行全文检索会很浪费时间。如果只检索字段的前面的若干个字符,这样可以提高检索速度。
5.限制索引数目
  索引的数目不是越多越好。每个索引都需要占用磁盘空间,索引越多,需要的磁盘空间就越大。修改表时,对索引的重构和更新很麻烦。越多的索引,会使更新表变得很浪费时间。

索引的数据结构

B+Tree和B-Tree

@SpringBootApplication底层

在SpringBoot中采用@SpringBootApplication,等价于定义@[email protected][email protected]
[email protected]**
@SpringBootConfiguration或@Configuration(Bean定义)等价于之前XML配置中的< bean id="" class="">
  这个注解主要是继承@Configuration注解,这个我们就是为了加载配置文件用的;
[email protected](Bean扫描)
等价于之前XML配置中的<context:component-scan base-pakcage="">
@ComponentScan,在启动类前使用,可以指定扫描包路径,也可以按默认路径规则加载。在包中的组件前面,需要使用@Controller、@Service、@Repository、@Component等扫描标记。
  这个主要有2个作用,组件扫描和自动装配;
[email protected]详解
  3.1.Boot内部定义了很多自动配置类,在xxx-autoconfigure.jar包的META-INF spring.factories文件定义
  3.2.Boot自动配置类,实际就是采用了@[email protected]标记实现了Bean定义
  3.3.在开启@EnableAutoConfiguration标记后,会自动导入AutoConfigurationImportSelector组件,该组件内部调SpringFactoriesLoader加载autoconfigure.jar包中的spring.factories文件,根据文件定义列表载入各个配置类
  3.4.各个XxxAutoConfiguration配置类会创建相应对象载入Spring容器中,例如DataSource、JdbcTemplate、DispatcherServlet、HandlerMapping、RedisTemplate、MongoTemplate、RestTemplate等

Redis分布式锁的原理

Redis 分布式锁命令
setnx当且仅当 key 不存在。若给定的 key 已经存在,则 setnx不做任何动作。setnx 是『set if not exists』(如果不存在,则 set)的简写,setnx 具有原子性。
getset先 get 旧值,后set 新值,并返回 key 的旧值(old value),具有原子性。当 key 存在但不是字符串类型时,返回一个错误;当key 不存在的时候,返回nil ,在Java里就是 null。
expire 设置 key 的有效期
del 删除 key
与时间戳的结合
  分布式锁的值是按 系统当前时间 System.currentTimeMillis()+Key 有效期组成
1.Redis 分布式锁流程图
面试问题收集
2.Redis 分布式锁优化版流程
面试问题收集

redis分布式锁应用场景

  线程间并发问题和进程间并发问题都是可以通过分布式锁解决的,但是强烈不建议这样做!因为采用分布式锁解决这些小问题是非常消耗资源的!分布式锁应该用来解决分布式情况下的多进程并发问题才是最合适的。
  有这样一个情境,线程A和线程B都共享某个变量X。
  如果是单机情况下(单JVM),线程之间共享内存,只要使用线程锁就可以解决并发问题。
  如果是分布式情况下(多JVM),线程A和线程B很可能不是在同一JVM中,这样线程锁就无法起到作用了,这时候就要用到分布式锁来解决。

Redis实现分布式锁的场景分析

1.单节点部署场景
  举例说明,系统A和系统B是两个部署在不同节点的相同应用(集群部署),这时客户端请求传来,两个系统都受到了请求,并且该请求是对数据表进行插入操作,如果这个时候不加锁来控制,可能会导致数据库新增两条记录,这时系统也不能允许的,由于是在不同应用内,在单个应用内加JVM级别的锁,另一个应用是感知不到的,这时需要用到分布式锁。
  接下来我们看看这种场景如何实现安全的分布式锁,由于是单节点部署场景,我们可以用setnx命令,以请求的唯一主键作为key,由于该操作是原子操作,当系统A设值成功后,系统B是无法设置成功的, 这时A就可以进行查询并插入操作,操作数据库完成后,删除key,此时系统B才能设值成功,但是由于查询到数据库有记录,所以并不会插入数据,这样就解决了该问题。但是这里会有个问题,如果redis挂机了,这里的锁不是永远都不释放了吗, 所以为了解决这个问题,redis提供了set命令,可传入超时时间的,那么在指定的时间范围内,如果没有释放锁,则该锁自动过期。如果执行时间超过超时时间呢,比如系统A还未执行完任务,就释放了锁,系统B接着执行任务,这时,系统A执行完了,把锁删掉(此时删除的时系统B获取的锁)。
  方案一: 为了避免这种情况,在del锁之前可以做一个判断,验证key对应的value是不是自己线程的ID.如果要考虑原子性问题,可以使用Lua脚本来实现,保证验证和删除的原子性。
  方案二:我们可以让获得锁的线程开启一个守护线程,用来给快要过期的锁加长超时时间。当系统A中的线程执行完任务,再显式关掉守护线程。
2.多节点部署场景
  多节点部署,是指redis不止一个节点,在一主多从,或者多主多从,以及去中心化架构上,上面的分布式锁的解决方案,就可能会出现由主节点宕机导致锁失效问题。比如说,R1为主节点,R2, R3是从节点,如果系统A刚获取到锁,还未开始执行时,R1还没把key同步到其他机器时,就宕机了,,此时R2由从节点升级为主节点,但是系统A获取的锁就失效了,此时系统B如果重新获取锁,那么就会导致并发问题,为了解决这个问题,redis给我们提供了RedLock,用来解决多节点部署的分布式锁如何安全获取问题。
  在redis分布式环境中,我们假设有N个Redis master.这些节点相互之间时独立的,不存在主从复制或者其他集群协调机制。我们确保将在N个实例上使用在Redis单实例下相同的方式获取和释放锁。现在我们假设有5个Redis master节点。为了获取到锁,客户端应该执行以下操作:获取当前Unix时间,以毫秒为单位。
  依次尝试从5个实例,使用相同的key和具有唯一性的value获取锁。当向Redis请求获取锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间,是为了避免服务器端Redis已经挂掉,客户端还在一直等待响应结果。如果没有及时响应,客户端尽快去另一个Redis实例请求获取锁。
  客户端使用当前时间减去开始获取锁的时间,就得到获取锁使用的时间,当且仅当从大多数(N/2 + 1, 这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效的时间,锁才算获取成功。key的真实有效时间等于有效时间减去获取锁所使用的时间。
  如果因为某些原因,获取锁失败(没有在至少N/2 + 1个Redis实例上获取到锁),客户端应该在所有的Redis实例上进行解锁(即便是某些Redis实例根本没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致一段时间不能重新获取锁)
注意: value一定要具有唯一性,防止误删锁,比如可以使用UUID+threadId
上面这种分布式锁的实现方案,有个响亮的名号:RedLock,Reddison有对RedLock算法做了封装,我们可以直接使用其中的API。