Java进阶

AOP

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

IOC

IOC容器

实际上就是个map(key,value),里面存各种对象(在xml里配置的bean节点、@Component、@Controller),在项目启动的时候会读取配置文件里面的bean节点,根据全限定类名使用反射创建对象放进map里、扫描到注解的类还是通过反射创建对象放到map。

在代码需要用到里面对象时,再通过DI注入(autowired、resource等,xml里bean节点内的ref属性,项目启动时会读取xml节点ref属性根据id注入,也会扫描这些注解根据类型或id注入,id就是对象名)。

控制反转

当A运行到需要B时,IOC容器会主动创建一个B注入到对象A需要的地方。对象A获得依赖对象B的过程,由主动行为变为了被动行为

依赖注入

实现IOC的方法,IOC容器在运行期间,动态地将某种依赖关系注入到对象中。

BeanFactory和ApplicationContext

ApplicationContext是BeanFactory的子接口,提供了更完整的功能:

  1. 继承MessageSource,因此支持国际化,比如可以用MessageSource实现资源文件的读取(MessageSource接口中的getMessage方法)
  2. 统一的资源文件访问方式
  3. 提供在监听器中注册bean的事件(疑惑)
  4. 同时加载多个配置文件
  5. 载入多个(有继承关系)上下文,使得每一个上下文都专注于一个特定的层次,比如应用的web层

不同点:

  1. BeanFactory延迟加载注入Bean,而ApplicationContext在容器启动时一次性创建所以的Bean,这样有利于检查所依赖属性是否注入,无需等待,不足是占用内存空间
  2. BeanFactory通常以编程方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。
  3. 他们都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,区别:BeanFactory需要手动注册,而ApplicationContext则是自动注册

BeanFactoryPostProcessor在容器实例化任何其它bean之前读取配置元数据,并可以根据需要进行修改。

BeanPostProcessor是在spring容器加载了bean的定义文件并且实例化bean之后执行的。BeanPostProcessor的执行顺序是在BeanFactoryPostProcessor之后。

factoryBean和beanFactory

beanFactory是IOC容器的接口,factoryBean是自定义实例化bean的一个工厂接口,给bean加上了一个简单工厂和装饰模式。

Spring Bean生命周期

首先是实例化、属性赋值、初始化、销毁这 4 个大阶段;

再是初始化的具体操作,有 Aware 接口的依赖注入、BeanPostProcessor 在初始化前后的处理以及 InitializingBean 和 init-method 的初始化操作;

销毁的具体操作,有注册相关销毁回调接口,最后通过DisposableBean 和 destory-method 进行销毁。

https://arthurjq.com/2020/12/29/java/spring-bean-life-cycle/

三级缓存(解决循环依赖):

一级:singletonObjects map<beanName,Object> (单例池)

二级:earlySingletonObjects map<beanName,Object> (提前AOP)

三级:singletonFactories (放了个lambda)

earlyProxyReferences ConcurrentHashMap<beanName,bean的原始对象>(循环引用时记录是否提前生成了代理对象)

creatingSet ()

循环依赖

假设Aservice和Bservice互相依赖,当Aservice出现循环依赖的话会提前AOP

  1. creatingSet.add(Aservice)
  2. class→实例化得到Aservice原始对象→singletonFactories map<beanName,lambda(beanName,BeanDefinition,Aservice原始对象)>
  3. 给Bservice属性赋值→从单例池找Bservice→找不到→创建Bservice的bean
    1. class→实例化得到Bservice原始对象
    2. 给Aservice属性赋值→从单例池找Aservice→找不到→creatingSet→Aservice出现循环→earlySingletonObjects→singletonFactories→lambda→AOP→Aservice代理对象→放入二级缓存(调用getEarlyBeanReference()提前生成代理对象)
    3. 给其他属性赋值
    4. 其余AOP
    5. 将对象放入单例池
  4. 给其他属性赋值
  5. 其余事情AOP→Aservice代理对象→postProcessAfterInitialization()(看earlyProxyReferences有没有提前生成代理对象)
  6. earlySingletonObjects.get(Aservice)
  7. creatingSet.remove(Aservice)
  8. 将对象放入单例池

Spring Bean作用域

Spring框架中的设计模式

简单工厂

由一个工厂类根据传入的参数,动态决定应该创建哪一个产品类

BeanFactory就是简单工厂模式的体现,根据传入一个唯一的标识来获得Bean对象,但是否是在传入参数后创建还是传入参数前创建根据具体情况来定。

工厂模式

实现了FactoryBean接口的bean,spring会在使用getBean()调用获得该bea n时,会自动调用该bean的getObject()方法,所以返回的不是factory这个bean,而是这个bean.getObject()方法的返回值。

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点

Spring提供了全局的访问点BeanFactory,但是没有从构造器级别时控制单例,这是因为spring管理的是任意的java对象。

适配器模式

Spring定义了一个适配接口,使每一种Controller有一种对应的适配器实现类(SpringMVC中HandlerAdapter),让适配器代替Controller执行相应的方法(Handler)。这样在扩展Controller时,只需要增加一个适配器类就完成了SpringMVC的扩展。

装饰器模式

动态地给一个对象添加一些额外的职责。增加功能比生成子类更加灵活。

Spring中用到的装饰器模式在类名上有两种表现:1、类名中含有wrapper;2、类名中含有Decorator;3、InputStream,outputStream

动态代理

切面在应用运行的时候被织入。在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。

在一个方法上加了@Transactional(申明式事务)注解后,Spring会基于这个类生成一个代理对象,Autowired后getBean从AOP容器中取得代理对象,会将这个代理对象作为bean,当在使用这个代理对象的方法时,代理逻辑会先把事务的自动提交设置为false。

观察者模式

Spring的事件驱动模型使用的是观察者模式,ApplicationContext事件机制是观察者模式的实现,通过ApplicationEvent类和ApplicationListener接口,可以实现ApplicationContext事件处理。

策略模式

Spring框架的资源访问Resource接口,该接口提供了更强的资源访问能力,Spring框架本身大量使用了Resource接口来访问底层资源。

public interface Resource extends InputStreamSource { 
boolean exists(); 
boolean isReadable(); 
boolean isOpen(); 
URL getURL() throws IOException; 
URI getURI() throws IOException; 
File getFile() throws IOException; 
long contentLength() throws IOException; 
long lastModified() throws IOException; 
Resource createRelative(String relativePath) throws IOException; 
String getFilename(); 
String getDescription(); 
}

Bean的自动装配

autowire属性五种装配方式

no

缺省情况下,自动配置时通过“ref”属性手动设定。

手动装配:以value或ref的方式明确指定属性值

byName

根据bean的属性名称进行自动装配

Customer的属性名称为person,Spring会将bean id为person的bean通过setter方法进行自动装配

<bean id="customer" class="com.xxx.xxx.Customer" autowire="byName"/>
<bean id="person" class="com.xxx.xxx.Person"/>

byType

根据bean的类型进行自动装配

Customer的属性person的类型为Person,Spring会将Person类型通过setter方法进行自动装配

<bean id="customer" class="com.xxx.xxx.Customer" autowire="byType"/>
<bean id="person" class="com.xxx.xxx.Person"/>

constructor

类似byType,不过是应用于构造器的参数。如果一个bean与构造器参数的类型相同,则进行自动装配,否则导致异常。

Customer构造函数的参数person的类型为Person,Spring会将Person类型通过构造方法进行自动装配

<bean id="customer" class="com.xxx.xxx.Customer" autowire="constructor"/>
<bean id="person" class="com.xxx.xxx.Person"/>

autodetect

如果有默认的构造器,则通过constructor方式进行自动装配,否则使用byType方式进行自动装配

@Autowired自动装配bean

可在字段,setter方法,构造函数上使用。

SpringMVC工作流程

  1. 用户发送请求至前端控制器DispatcherServlet
  2. DispatcherServlet收到请求调用HandlerMapping处理器映射器。(Map< url , handler >,其中url可以是bean id、@RequestMapping、key)
  3. HandlerMapping找到具体的处理器(可以根据xml配置、注解进行查找),生成handler及处理器拦截器(如果有则生成)一并返回给DispatcherServlet
  4. DispatcherServlet调用HandlerAdapter处理器适配器(Controller接口定义整个类为一个Handler、@RequestMapping定义方法、Servlet三种方法都有各自的适配器,support方法遍历适配器,找到后执行handle调用真正的handler)
  5. HandlerAdapter经过适配调用具体的处理器(Controller,也叫后端控制器)
  6. Controller执行完成返回ModelAndView
  7. HandlerAdapter将ModelAndView返回给DispatcherServlet
  8. DispatcherServlet把ModelAndView传给ViewReslover视图解析器
  9. ViewReslover解析后返回具体View
  10. DispatcherServlet根据View进行渲染视图(将模型数据填充至视图中,比如JSP)
  11. DispatcherServlet响应用户

SpringBoot自动装配

从主启动的SpringBootApplication注解里可以看出Spring的配置类也是Spring的一个Component。EnableAutoConfiguration注解开启自动装配功能,使用import注解获取扫描的包路径,将主配置类的所在包及子包里面的所有组件加载到Spring容器。

#{ }和${ }

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

  1. Mybatis在处理#{ }时,会将sql中的#{ }替换为 ?号,调用PreparedStatement来赋值(会自动加单引号);
  2. 在处理${ }时,就是替换成变量值,调用Statement来赋值;

绝大多数需求为单条记录查询时可以选择哈希索引

定义有外键的数据列一定要建立索引

慢查询优化

  1. 是否load了额外的数据
  2. 刷新脏页redo log在持久化
  3. 是否走索引
  4. 从库在执行sql
  5. 横向或纵向分表

ACID靠什么保证

  1. A由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql

  2. C由其他三大特性保证,程序代码要保证业务上的一致性

  3. I由MVCC来保证

  4. D由内存 + redo log来保证,mysql修改数据同时在内存和redo log记录这次操作,宕机的时候可以从redo log恢复。redo log记录了数据修改的状态

    InnoDB redo log写盘,InnoDB 事务进入prepare 状态。

    如果前面 prepare成功,binlog 写盘,再继续将事务日志持久化到binlog。如果持久化成功,那么InnoDB事务则进入commit状态(在 redo log里面写一个commit记录)

    所以说如果redo log中由commit,说明binlog持久化成功

    redo log的刷盘会在系统空闲时进行。(Mysql的主从同步(复制)通过binlog来同步)

MVCC

多版本并发控制(Multi-Version Concurrency Control):读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特定版本的数据,版本链

MVCC只在 READ COMMITED 和REPEATABLE READ 下工作。因为READ UNCOMMITED 总是读取最新的数据行,而不是符合当前事务版本的数据行。而serializable则会对所有读取的行都加锁。

聚簇索引记录中有两个必要的隐藏列:

  1. trx_id:用来存储每次对某条聚簇索引记录进行修改的时候的事务id
  2. roll_pointer:每次对聚簇索引修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了个指针,指向索引记录的上一个版本的位置。(注意插入操作的undo日志没有这个属性,因为它没有老版本)

mysql主从同步原理

mysql主从复制

主要由三个线程:master(binlog dump thread)、slave(I / O thread、SQL thread)

  1. 主节点binlog,主从复制的基础是主库记录数据库的所有变更记录到binlog。binlog是数据库服务器启动的那一刻起,保存所有修改数据库结构或内容的一个文件。
  2. 主节点log dump线程,当binlog有变动时,log dump线程读取其内容并发送给从节点。
  3. 从节点I /O 线程接受binlog 内容,并将其写入到relay log 文件中。(relay log在从节点)
  4. 从节点的SQL线程读取 relay log 文件内容对数据更新进行重放,最终保证主从数据库一致性。

注:主从节点使用 binlog文件 + position 偏移量来定位主从同步的位置,从节点会保存其已接收到的偏移量,如果从节点发生宕机重启,则会自动从position的位置同步。

mysql默认的复制方式是异步的,主库把日志发送给从库不关心从库是否已经处理。有一个问题就是假设主库挂了,从库处理失败,这时候从库升为主库,日志丢失了。

全同步复制:主库写入binlog后强制同步日志到从库,所有从库执行完才返回客户端。

半同步复制:从库写入日志成功后返回ACK确认给主库,主库收到至少一个从库的确认就认为完成。

负载均衡类型

DNS方式实现负载均衡

硬件负载均衡:F5和A10

软件负载均衡:

  1. Nginx:七层负载均衡,支持HTTP、E-mail协议,同时也支持4层负载均衡(解析url,与客户端和服务端都要保持长连接,性能损耗)
  2. HAproxy:支持七层规则,性能不错。OpenStack 默认使用
  3. LVS:运行在内核态,性能最高,严格说工作在第三层(ip——>Server)

负载均衡算法

  1. 轮询法:按顺序轮流分配
  2. 加权轮询:按照权重分配到后端
  3. 随机法
  4. 加权随机法
  5. ip_hash法,解决session共享问题
  6. 最小连接数法,看谁最空闲

分布式下Session共享方案

分布式锁解决方案

需要这个锁独立于每一个服务之外,而不是在服务里面。

数据库

利用主键冲突控制一次只有一个线程能获取锁,非阻塞(要自己写阻塞代码)、不可重入(递归不支持,要自己实现AQS)、单点、不支持失效时间(要自己写定时器)

比如在数据库中建张表,采用唯一约束,要获得锁就往数据库中插入同一个key的记录放到唯一键上面,如果放进去了就是拿到锁了。

Zookeeper分布式锁

zk通过Znode解决死锁问题,一旦客户端获取锁后突然挂掉,这个临时节点会自动删除,其他客户端自动获取锁。

临时顺序节点解决惊群效应(所有处于阻塞状态的线程去争抢资源获取锁)。

Redis分布式锁

setNX命令,单线程处理网络请求,不需要考虑并发安全性。

Redis是个第三方中间件,在集群中很方便地实现分布式锁,所有服务节点设置相同地key,返回0则获取锁失败,T1申请到key后T2肯定申请不到。

setNX问题

  1. 早期版本没有超时参数,需要单独设置,存在死锁问题(key不会超时)
  2. 后期版本提供加锁和设置时间原子操作(set(NX,timeout)),但是存在任务超时,锁自动释放,导致并发问题,加锁和释放锁不是同一线程问题(假如T1获得锁要执行15s,锁失效时间为10s,在这5s内T2获得了锁,T1执行完了要把锁释放掉,结果把T2的锁释放了,解决方法可以在value中存上线程的唯一标识或者uuid)

删除锁:判断线程唯一标识再删除

可重入性及锁续期没有实现,通过redisson解决(类似AQS实现(count计数),看门狗监听机制(设置一个监听器监听任务,任务没有执行完就延长过期时间))

redlock:以上的机制都只操作单节点,即使Redis通过Sentinel保证高可用,如果这个master节点由于某些发生了主从切换,那么就会出现锁丢失的情况(主从节点间是异步通信,Redis同步设置可能数据丢失)。redlock从多个节点申请锁,当一半以上节点获取成功才算成功,redisson有相应的实现

SpringCloud和Dubbo区别

底层协议:springcloud基于http协议,dubbo基于tcp协议

注册中心:springcloud使用eureka,dubbo推荐使用zookeeper

模型定义:dubbo将一个接口定义为一个服务,springcloud将一个应用定义为一个服务

springcloud是一个生态,而dubbo是springcloud生态中关于服务调用一种解决方案(服务治理)

Hystrix实现机制

分布式容错框架:熔断降级监控

资源隔离

  1. 线程隔离:Hystrix会给每个Command分配一个单独的线程池,这样在进行单个服务调用的时候,就可以在独立的线程池里面进行,而不会对其他线程池造成影响
  2. 信号量隔离:客户端向依赖服务发起请求时,首先要获取一个信号量才能真正发起调用,由于信号量的数量有限,当并发量超过信号量个数时,后续的请求都会直接拒绝,进入fallback流程。信号量隔离主要是通过控制并发请求量,防止请求线程大面积阻塞,从而达到限流和防止雪崩的目的。

熔断和降级

调用服务失败后快速失败

熔断:为了防止异常不扩散,保证系统的稳定性

降级:编写好调用失败的补救逻辑,然后对服务直接停止运行,这样这些接口就无法正常调用,但又不至于直接报错,只是服务水平下降

  1. 通过HystrixCommand或者HystrixObservableCommand将所有的外部系统(依赖)包装起来,整个包装对象是单独运行在一个线程之中(这是典型的命令模式)。
  2. 超时请求应该超过你定义的阈值
  3. 为每个依赖关系维护一个小的线程池(或信号量);如果它满了,那么依赖关系的请求将立即被拒绝,而不是排队等待。
  4. 统计成功,失败(由客户端抛出的异常),超时和线程拒绝。
  5. 打开断路器可以在一段时间内停止对特定服务的所有请求,如果服务的错误百分比通过阈值,手动或自动地关闭断路器。
  6. 当请求被拒绝、连接超时或者断路器打开,直接执行fallback
  7. 近乎实时监控指标和配置变化。

RabbitMQ如何保证消息发送接收?

发送方确认机制

信道需要设置为confirm模式,则所有在信道上发布的消息都会分配一个唯一 ID

一旦消息被投递到queue(可持久化的消息需要写入磁盘),信道会发送一个确认给生产者(包含消息唯一ID)。

如果RabbitMQ 发生内部错误从而导致消息丢失,会发送一条nack(未确认)消息给生产者。

所有被发送的消息都将被confirm(即 ack)或者被nack一次。但是没有对消息被confirm的快慢做任何保证,并且同一条消息不会即被confirm又被nack。

发送方确认模式是异步的,生产者应用程序在等待确认的同时,可以继续发送消息。当确认消息到达生产者,生产者的回调方法会被触发。

ConfirmCallback接口:只确认是否正确到达 Exchange 中,成功到达则回调,其中的confirm(ID)方法返回给生产者

ReturnCallback接口:消息失败返回时回调

消费者的确认

消费者在声明队列时,可以指定noAck参数,当noAck = false时,RabbitMQ会等待消费者显式发回ack信号后才从内存(或者硬盘,持久化消息)中移除(手动提交),否则,消息被消费后立即删除。

如果noAck = false,消费者接受每一条消息后都必须确认,只有消费者确认了消息,RabbitMQ才能安全地把消息从队列中删除。

RabbitMQ不会为未ack的消息设置超时时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开。这样设计的原因是RabbitMQ允许消费者消费一条消息的时间可以很长(也起到限流的作用),保证数据的最终一致性。因为如果消费者不返回ack的话,RabbitMQ也不知道消费者是否处理完了没有,擅自把消息发给其他消费者或者删除都会导致数据不一致

如果消费者返回ack之前断开了链接。RabbitMQ会重新发送给下一个订阅的消费者。(可能存在消息重复消费的隐患,需要业务方保持数据一致,幂等性)

RabbitMQ事务消息

RabbitMQ死信队列和延时队列


   转载规则


《Java进阶》 锦泉 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录