知识库——第三方框架

1. Spring

1.1. Spring 模块组成

  • Spring Core 框架核心,提供 IOC 容器,管理 bean 对象
  • Spring Context 提供上下文信息
  • Spring Dao 提供 JDBC 抽象层
  • Spring ORM 提供“对象/关系”映射 APIs 的集成层
  • Spring AOP 切面编程功能
  • Spring Web 提供 web 开发的上下文信息
  • Spring Web MVC 提供了 web 应用的 model-view-controller 实现

1.2. AOP面向切面编程

  • 不改变原逻辑增加额外功能,将多个类公共行为封装为可重用模块,降低系统耦合度
  • Spring 注解 @Aspect,应用于拦截器,认证、日志、同一异常处理(@ControllerAdvice)等等
  • 实现方式
  • 动态代理技术 (JDK 动态代理、CGLib 动态代理)
  • 静态织入方式

1.3. IoC控制反转

  • 把创建和查找依赖对象的控制权交给 IoC 容器
  • DI 依赖注入是IOC容器装配和注入对象的一种方式
  • 作用
  • 松耦合
  • 资源集中管理
  • 功能可复用

1.4. 三种依赖注入的方式

1.4.1. 构造方法注入

将被依赖对象通过构造函数的参数注入给依赖对象,并且在初始化对象的时候注入。

优点: 对象初始化完成后便可获得可使用的对象。
缺点: 当需要注入的对象很多时,构造器参数列表将会很长;不够灵活。若有多种注入方式,每种方式只需注入指定几个依赖,那么就需要提供多个重载的构造函数。

1.4.2. setter 方法注入

IoC Service Provider 通过调用成员变量提供的setter函数将被依赖对象注入给依赖类。

优点: 灵活,可以选择性地注入需要的对象。
缺点: 依赖对象初始化完成后由于尚未注入被依赖对象,因此还不能使用。

1.4.3. 接口注入

依赖类必须要实现指定的接口,然后实现该接口中的一个函数,该函数就是用于依赖注入,该函数的参数就是要注入的对象。

优点: 接口注入中,接口的名字、函数的名字都不重要,只要保证函数的参数是要注入的对象类型即可。
缺点: 侵入行太强,很少使用。

1.5. Spring 使用的注入方式

Spring 的注入方式只有两种,分别是 setter 注入和构造方法注入。
通常使用的 @Autowried 注解实际是 setter 注入的一种变体。

Spring 的注入模式有四种:

  • no
  • byType
  • byName
  • constructor

1.6. 用到哪些设计模式

  • 工厂模式:Spring 使用工厂模式可以通过 BeanFactoryApplicationContext 创建 bean 对象;
  • 单例模式:Spring 中 bean 的默认作用域就是 singleton(单例)的;
  • 代理模式:Spring AOP 就是基于动态代理的;
  • 观察者模式:Spring 事件驱动模型就是观察者模式很经典的一个应用。ApplicationListener 监听器;
  • 适配器模式:Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配 Controller;
  • 模版方法模式:Spring 中 jdbcTemplate、hibernateTemplate 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式;
  • 装饰者模式

1.7. @Transactional 注解哪些情况下会失效?

  • 作用在非 public 方法上
  • 方法异常被捕获
  • 数据库不支持事务(例如 MySQL 的 MyISAM)
  • 没开启事务注解
  • 同一类中加 @Transactional 方法被无 @Transactional 的方法调用,事务失效

1.8. 常用注解

  • bean定义注解
  • @component 描述Spring框架中的bean
  • @Repository 用于对DAO实现类进行标注
  • @Service 用于对业务类进行标注
  • @Controller 用于对控制类进行标注
  • Spring属性注入
  • @Autowired() 自动注入
  • @Autowired(required=true) 找到匹配的Bean
  • @Qualifier() 可指定Bean的名称。一个接口有多个实现类可指定使用哪种实现
  • @Resource() 和 Autowired() 功能相似,@Resource() 是 JDk 自带注解
  • 其他输入
  • @PostConStruct() 初始化
  • @PreDestory() 销毁
  • @Scope() 指定作用域
  • @Profile() 指定环境bean生效

1.9. bean循环引用如何解决

Spring Bean 的循环依赖问题,是指类A通过构造函数注入类B的实例(或者B中声明的Bean),而类B通过构造函数注入类A的实例(或者A中声明的 Bean),即将类A和类B的bean配置为相互注入,则 Spring IoC 容器会在运行时检测到此循环引用,并引发一个 BeanCurrentlyInCreationException。

  • 延迟加载 @Lazy,例如
@Component
public class CircularDependencyA {

	private CircularDependencyB circB;

	@Autowired
	public CircularDependencyA(@Lazy CircularDependencyB circB) {
		this.circB = circB;
	}
}
  • 在实例变量上使用 @Autowired 注解,让 Spring 决定在合适的时机注入,而非在初始化类的时候就注入。
  • 用基于 setter 方法的依赖注入取代基于构造函数的依赖注入来解决循环依赖。

1.10. 动态代理是什么?应用场景?如何实现

动态代理:在运行时,创建目标类,可以调用和扩展目标类的方法。

应用场景:

  • 统计每个 api 的请求耗时
  • 统一的日志输出
  • 校验被调用的 api 是否已经登录和权限鉴定
  • Spring的 AOP 功能模块就是采用动态代理的机制来实现切面编程

实现方法:

JDK 动态代理
JDK 动态代理只提供接口的代理,不支持类的代理。核心 InvocationHandler 接口和 Proxy 类,InvocationHandler 通过 invoke() 方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy 利用 InvocationHandler 动态创建一个符合某一接口的的实例, 生成目标类的代理对象。

CGLib 动态代理
如果代理类没有实现 InvocationHandler 接口,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现 AOP。CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为 final,那么它是无法使用CGLIB做动态代理的。

1.11. Spring MVC

在这里插入图片描述

1.11.1. 工作原理

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

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

3、 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet。

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

5、 HandlerAdapter 经过适配调用具体的处理器(Controller,也叫后端控制器)。

6、 Controller 执行完成返回 ModelAndView。

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

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

9、 ViewReslover 解析后返回具体视图 View。

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

11、 DispatcherServlet 响应用户。

1.11.2. 组件说明

DispatcherServlet:作为前端控制器,整个流程控制的中心,控制其它组件执行,统一调度,降低组件之间的耦合性,提高每个组件的扩展性。

HandlerMapping:通过扩展处理器映射器实现不同的映射方式,例如:配置文件方式,实现接口方式,注解方式等。

HandlAdapter:通过扩展处理器适配器,支持更多类型的处理器。

ViewResolver:通过扩展视图解析器,支持更多类型的视图解析,例如:jsp、freemarker、pdf、excel等。

2. Redis

2.1. 支持五种数据类型

  • string 字符串 (value最大存储512M)
  • hash 哈希(元素最多2^32-1)
  • list 列表(元素最多2^32-1)
  • set 集合(元素最多2^32-1)
  • sorted set 有序集合(同set)

2.2. 单线程但性能依然好原因

  • 单线程避免线程切换的消耗(redis6已支持多线程)
  • 基于内存,内存读写都很快的
  • 使用高性能数据结构,比如hash和跳表
  • 使用非阻塞的IO多路复用机制

2.3. 持久化

  • RDB:单文件灾难恢复操作简单,性能较高;每隔一段时间持久化,故障可能导致数据丢失
  • AOF:日志追加,记录每个命令操作到 aof 文件中一次,宕机保证数据不会丢失,redis-check-aof 工具解决数据一致性问题;持久化文件较大,恢复速度慢

2.4. 过期键删除策略

  • 定时删除(创建定时器达到过期时间删除)
  • 惰性删除(当获取键时才检查是否删除)
  • 定期删除(定期统一检查删除)

redis采用惰性删除和定期删除结合的策略

2.5. 常用客户端

Redisson、Jedis、Lettuce

2.6. 处理过大量的key同一时间过期吗?需要注意什么?

可能导致Redis短时间卡顿现象,量大时还可能出现缓存雪崩。过期时间不要求很精确的话,在时间上加一个随机值,使过期时间分散一些

2.7. Redis 适用场景

  • 数据缓存
  • 计数器、排行榜
  • 集合队列,发布和订阅

2.8. key搜索禁止使用keys命令(key很多时搜索会导致线程堵塞),应该使用scan命令

2.9. 分布式锁

  • 定义一个key,value为请求唯一id,设置超时时间,setnx成功即获得锁,删除key要校验value(可用lua脚本把读和删除原子化)
  • 使用框架 Redisson

2.10. pipeline

建立管道长链路,一次性处理多个命令,提高吞吐量,但不能保证原子操作

2.11. 缓存穿透、击穿和雪崩

  • 缓存穿透:key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
  • 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力;或者查询返回的数据为空仍然进行缓存,过期时间设置较短。
  • 缓存击穿:key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
  • 使用互斥锁(mutex key)SETNX。缓存失效的时候,不立即去load db,而是去set一个mutex key,成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。
  • 缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。
  • 在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件

2.12. 主从、哨兵和 cluster 集群

  • 主从:分摊读写压力
  • 哨兵:高可用
  • cluster 集群:兼顾两者

2.13. Redis集群不可用(集群三主三从,至少需要三台机器,互为主从,一台服务器宕机还能用)

  • 半数宕机(fail一个主节点需要一半主以上投票通过)
  • 某一结点主从全都宕机

2.14. 消息队列实现方式

  • 基于异步消息队列 List lpush-brpop(rpush-blpop)
  • 使用rpush和lpush操作入队列,lpop和rpop操作出队列
  • 当队列为空时,lpop和rpop会一直空轮训,消耗资源;所以引入阻塞读blpop和brpop(b代表blocking),阻塞读在队列没有数据的时候进入休眠状态
  • PUB/SUB 订阅/发布模式
  • SUBSCRIBE,用于订阅信道
  • PUBLISH,向信道发送消息
  • UNSUBSCRIBE,取消订阅
  • 基于 sorted set 的实现
  • zadd 添加带分数元素
  • zrange或zrevrange返回有序集合,指定区间的成员。使用0,0区间来获取处于顶部的元素
  • zrem 移除元素,消费后移除
  • 基于 stream 类型的实现。stream 是redis 5.0后新增的数据结构

2.15. 延迟消息实现

利用 zadd 和 zrangebyscore 来实现存入和读取消息

2.16. Sortes Set(有序列表)实现,压缩列表和跳表

2.16.1. 压缩列表 ziplist

  • 有序集合保存的元素数量小于128个
  • 有序集合保存的所有元素的长度小于64字节

2.16.2. 跳表 skiplist

  • 一种基于有序链表的扩展,跳表会维护多个索引链表和原链表
  • 查找次数近似于层数,时间复杂度为O(logn),插入、删除也为 O(logn)
  • 跳表是一种随机化的数据结构(通过抛硬币来决定插入层数)
  • 空间复杂度为 O(n)

2.17. 一致性哈希和哈希槽

2.17.1. 一致性哈希(一致性hash是一个0-2^32的闭合圆)

  • 用于解决分布式缓存系统中的数据选择节点存储问题
    和数据选择节点读取问题
    以及在增删节点后减少数据缓存的消失范畴,防止雪崩的发生。
  • 顺时针找到归属的节点

2.17.2. 哈希槽(redis cluster一共有2^14=16384个槽)

  • redis cluster集群没有采用一致性哈希方案,而是采用数据分片中的哈希槽来进行数据存储与读取的。
  • 根据CRC-16(key)384的值来判断属于哪个槽区,从而判断该key属于哪个节点

2.18. 使用场景

  • 会话缓存(Session Cache),是 Redis 最常使用的一种情景
  • 全页缓存(FPC)
  • 用作网络版集合和队
  • 排行榜和计数器,Redis 在内存中对数字递增、递减的操作实现的非常好。Set 和 Sorted Set 使得我们在执行这些操作的时候非常简单
  • 发布和订阅

3. MQ

3.1. MQ 作用

  • 应用解耦:生产和消费分离,甚至可以多个消费者
  • 异步处理:比回调请求获取数据更快,性能更高
  • 流量削峰:限制最大入库数据量,不超过系统所能承受的最大请求量,避免数据库压力过大

3.2. 缺陷

  • 系统可用性降低,因为MQ可能宕机:以前只要担心系统的问题,现在还要考虑 MQ 挂掉的问题,MQ 挂掉,所关联的系统都会无法提供服务;
  • 复杂度变高:要考虑消息丢失、消息重复消费等问题;
  • 一致性问题:多个 MQ 消费系统,部分成功,部分失败,要考虑事务问题。

3.3. 常用MQ

  • ActiveMQ:支持万级的吞吐量,较成熟完善;官方更新迭代较少,社区的活跃度不是很高,有消息丢失的情况。
  • RabbitMQ:延时低,微妙级延时,社区活跃度高,bug 修复及时,而且提供了很友善的后台界面;用 Erlang 语言开发,只熟悉 Java 的无法阅读源码和自行修复 bug。
  • RocketMQ:阿里维护的消息中间件,可以达到十万级的吞吐量,支持分布式事务。
  • Kafka:分布式的中间件,最大优点是其吞吐量高,一般运用于大数据系统的实时运算和日志采集的场景,功能简单,可靠性高,扩展性高;缺点是可能导致重复消费。

3.4. 使用场景

  • 秒杀抢购场景流量削峰,入队列,超过最大长度丢弃
  • 异步处理和解耦。如注册用户发邮件验证,提交MQ由业务模块消费,同时将两个模块解耦
  • 日志采集,常用 kafka
  • 消息通讯。点对点或发布/订阅模式

3.5. 高可用方法

  • RabbitMQ 镜像集群,多节点复制 queue 节点信息
  • ActiveMQ 部署主从热备
  • RocketMQ 有多 master 多 slave 异步复制模式和多 master 多 slave 同步双写模式支持集群部署模式

3.6. 如何保证消息不被重复消费

  • 消息带上全局唯一id,缓存redis校验或入库校验
  • 消息入库可用数据库唯一键约束

3.7. 如何保证消息不丢失?

  • 生产者丢失:主流的 MQ 都有确认机制或事务机制,可以保证生产者将消息送达到 MQ。如 RabbitMQ 就有事务模式和 confirm 模式。
  • MQ丢失:MQ 成功接收消息内部处理出错、宕机等情况。解决办法:开启 MQ 的持久化配置。
  • 消费者丢失:采用消息自动确认模式,消费者取到消息未处理挂掉了。 解决办法:改为手动确认模式,消费者成功消费消息再确认。

3.8. 如何保证消息的顺序性

  • 生产者保证消息入队的顺序;
  • MQ 本身是一种先进先出的数据接口,将同一类消息,发到同一个 queue 中,保证出队是有序的;
  • 避免多消费者并发消费同一个 queue 中的消息。

3.9. 消息大量堆积如何处理

消息的积压来自于两方面:要么发送快了,要么消费变慢了。

  • 扩容消费端的实例数提高消费能力。或者启用多个消费者,并发接收消息消费;
  • 另起一套MQ环境给当前业务使用,堆积消息的MQ环境自己消费,不要影响当前业务;
  • 如果短时间内没有服务器资源扩容,没办法的办法是将系统降级,通过关闭某些不重要的业务,减少发送的数据量,最低限度让系统还能正常运转,服务重要业务;
  • 监控发现,产生和消费消息的速度没什么变化,出现消息积压的情况,检查是有消费失败反复消费的情况;
  • 监控发现,消费消息的速度变慢,检查消费实例,日志中是否有大量消费错误、消费线程是否死锁、是否卡在某些资源上。