面试之中兴


redis 的雪崩、穿透和击穿

redis 缓存雪崩

所谓的 redis 缓存雪崩指的是:大量的应用请求无法在 Redis 缓存中进行处理,紧接着,应用将大量请求发送到数据库层,导致数据库层的压力激增。redis 中大量 key 集中过期或者 redis 服务器宕机,从而导致大量请求从数据库获取数据,导致数据库服务器访问压力过大。

解决方式:

  1. 若是由于大量 key 过期所造成的,可以给 key 的 ttl 生存时间值 设置一个过期时间
  2. 若是因为 redis 服务器宕机所导致的,可以搭建 redis 集群,保证高可用
  3. 可以从请求量层面进行解决,对缓存业务添加限流和服务降级策略
  4. 可以添加多级缓存,比如说 nginx 缓存

redis 缓存击穿

所谓的 redis 缓存击穿指的是:热点 key 的过期,从而导致大量访问热点 key 的请求访问数据库,从而导致数据库压力过大。key 中对应数据存在,当 key 中对应的数据在缓存中过期,而此时又有大量请求访问该数据,缓存中过期了,请求会直接访问数据库并回设到缓存中,高并发访问数据库会导致数据库崩溃。

解决方式:热点 key 通常是我们通过后台进行批量添加的,比如秒杀活动,解决热点 key 失效,可以重建缓存

  1. 互斥锁实现:只允许一个线程对 redis 缓存进行重建,其他线程处于等待状态,可以通过 redis 当中的 setnx 实现。缺点是串行化执行,效率低。优点是一致性高。
  2. 逻辑过期:给需要缓存的数据添加一个逻辑过期字段,通过对逻辑过期字段的判断,判断数据有无过期,如果过期则开启一个线程进行缓存重建,并且返回之前的数据。缺点是数据的一致性低,优点是相应速度快。
  3. 在缓存访问非常频繁的热点数据时,不要设置过期时间

redis 缓存穿透

所谓的 redis 缓存穿透指的是:redis 当中没有数据,数据库当中也没有数据,请求每次都是访问数据库,而数据库又没有数据返回。这样一来,缓存也就成了“摆设”,如果应用持续有大量请求访问数据,就会同时给缓存和数据库带来巨大压力

解决方式:

  1. 缓存空值。缺点是有点浪费内存空间,如果这样的请求过多,会导致redis当中有大量这种无用缓存,可以给其设置一个ttl。
  2. 使用布隆过滤器。布隆过滤器原理是一个二进制的数组,存储的是0或1,在添加元素时进行多次 hash 运算,得到多个0或1,存储到相应位置。
  3. 热点参数的限流降级
  4. 进行权限校验
  5. 做好数据格式的校验

数据库三大范式

  • 第一设计范式 :表中的每一列都不能再分(不要出现二维表)

  • 第二设计范式:满足第一设计范式,除主键外每一列都必须依靠主键

  • 第三设计范式:满足第二设计范式,除主键列外,每一列都不能相互依靠

mysql 与 redis 的区别

类型上

从类型上来说,mysql 是关系型数据库,redis 是缓存数据库

作用上

mysql 用于持久化的存储数据到硬盘,功能强大,速度较慢,基于磁盘,读写速度没有 Redis 快,但是不受空间容量限制,性价比高

redis 用于存储使用较为频繁的数据到缓存中,读取速度快,基于内存,读写速度快,也可做持久化,但是内存空间有限,当数据量超过内存空间时,需扩充内存,但内存价格贵

需求上

mysql 和 redis 因为需求的不同,一般都是配合使用。需要高性能的地方使用 Redis,不需要高性能的地方使用 MySQL。存储数据在MySQL 和 Redis 之间做同步。

什么是Redis,为什么用Redis

Redis 是一种支持 key-value 等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化

  • 读写性能优异
    • Redis能读的速度是110000次/s,写的速度是81000次/s (测试条件见下一节)。
  • 数据类型丰富
    • Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • 原子性
    • Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
  • 丰富的特性
    • Redis支持 publish/subscribe, 通知, key 过期等特性。
  • 持久化
    • Redis支持RDB, AOF等持久化方式
  • 发布订阅
    • Redis支持发布/订阅模式
  • 分布式
    • Redis Cluster

动态代理

  • 不使用对象来进行真实操作,使用我们自己创建的代理对象来操作

  • 为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

  • 动态代理的优势在于能够很方便的对代理类中方法进行集中处理,而不用修改每个被代理的方法

  • 不修改源目标的前提下,额外扩展源目标的功能。即通过访问源目标的代理类,再由代理类去访问源目标。

  • 原生的 jdk 的动态代理存在缺陷,代理类和委托类必须实现同一个接口

  • jdk 的动态代理底层实现是基于接口实现

  • cglib 的动态代理底层是基于继承

SpringMVC

前后端不分离

SpringMVC 底层有一个核心对象:DispatcherServlet 前端控制器(分发器),使用了 SpringMVC 框架之后,所有的请求都会执行 DispatcherServlet 这个对象,不再去直接执行对应的 Controller,而是先通过 DispatcherServlet 前端控制器找到该请求路径(URL) 对应的控制器,前端控制器再去调用该控制器执行具体业务。

image-20210309195355880
  1. 一个请求匹配前端控制器 DispatcherServlet 的请求映射路径(在 web.xml 中指定),WEB 容器将该请求转交给 DispatcherServlet 处理
  2. DispatcherServlet 接收到请求后,解析URL,将根据请求信息交给处理器映射器 (HandlerMapping)
  3. HandlerMapping 根据用户的url请求 查找匹配该 url 的 Handler,并返回一个执行链
  4. DispatcherServlet 再请求处理器适配器(HandlerAdapter) 调用相应的 Handler 进行处理并返回 ModelAndView 给 DispatcherServlet
  5. DispatcherServlet 将 ModelAndView 请求 ViewReslover(视图解析器)解析,返回具体 View
  6. DispatcherServlet 对 View 进行渲染视图(即将模型数据填充至视图中)
  7. DispatcherServlet 将页面响应给用户

注意:

  • 在前端浏览器上第一次请求我们的 DispatchServlet 前端控制器(核心组件),会创建该 DispatchServlet 对象的实例,再执行 DispatchServlet 中的 init() 方法 , 从 spring 容器中按照类型注入来获取 DispatchServlet 中的属性对应的组件来进行依赖注入 !!!
  • 如果不是第一次请求的话,各大组件依赖注入完毕,直接执行 doService() 方法来完成后续操作!!!
  • SpringMVC 底层也有自己的一个容器:WebXmlApplicationContext ,和 spring 的 ApplicationContext 容器是父子关系,SpringMVC 的容器是继承了 Spring 容器的,spring 容器是父容器,springmvc 容器是子容器!!!
  • springmvc 在需要使用到某个功能组件的时候,先去自己的 WebXmlApplicationContext 容器中去找,如果没有则去 spring 容器中去找
  • springmvc 可以获取 spring 容器中的 bean,而 spring 则无法获取 springmvc 容器中的 bean !!!!
  • 一般的话像 Controller 层对象一般都是存放在 springmvc 的容器中来共 springmvc 中的处理器适配器去调用!!!
  • Service 层 和 Dao 层对象,一般则是放在 spring 容器中,因为像一些事务的处理和 mybatis 核心对象的生成不是加上注解之后就会立即生效,而是先生成代理对象,一般这些代理对象一般都在 spring 容器中注册!!!

前后端分离

使用 @ResponseBody 来把数据写入到响应体中。所以不需要进行页面的跳转。

  1. 用户发起请求被 DispatchServlet 所处理

  2. DispatchServlet 通过 HandlerMapping 根据具体的请求查找能处理这个请求的 Handler。(HandlerMapping 主要是处理请求和Handler方法的映射关系的)

  3. HandlerMapping 返回一个能够处理请求的执行链给 DispatchServlet,这个链中除了包含 Handler 方法也包含拦截器。

  4. DispatchServlet 拿着执行链去找 HandlerAdapter 执行链中的方法。

  5. HandlerAdater 会去执行对应的 Handler 方法,把数据处理转换成合适的类型,然后作为方法参数传入

  6. Handler 方法执行完后的返回值会被 HandlerAdapter 转换成 ModelAndView 类型。由于使用了 @ResponseBody 注解,返回的ModelAndView 会为 null,并且 HandlerAdapter 会把方法返回值放到响应体中。(HandlerAdater 主要进行 Handler 方法参数和返回值的处理。)

  7. 返回 ModelAndView 给 DispatchServlet。

  8. 因为返回的 ModelAndView 为null,所以不用去解析视图解析和其后面的操作

SpringBoot 自动配置

使用 SpringBoot 之后,一个整合了 SpringMVC 的 WEB 工程开发,变的无比简单,那些繁杂的配置都消失不见了,这是如何做到的?

一切魔力的开始,都是从我们的 main 函数来的,所以我们再次来看下启动类

我们发现特别的地方有两个:

  • 注解:**@SpringBootApplication**
  • run方法:SpringApplication.run()

我们分别来研究这两个部分。

在 Spring 程序 main 方法中,添加**@SpringBootApplication或者@EnableAutoConfiguration**会自动去 maven 中读取每个 starter 中的 spring.factories 文件,该文件里配置了所有需要被创建的 Spring 容器中的 bean

@SpringBootApplication

标注在某个类上,说明这个类是 SpringBoot 的主配置类,SpringBoot 项目就应该运行这个类的 main 方法来启动 SpringBoot 应用。

这里重点的注解有3个:

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

@SpringBootConfiguration

通过源码我们可以看出,在这个注解上面,又有一个@Configuration注解。通过上面的注释阅读我们知道:这个注解的作用就是声明当前类是一个配置类,然后 Spring 会自动扫描到添加了@Configuration的类,并且读取其中的配置信息。而@SpringBootConfiguration 是来声明当前类是 SpringBoot 应用的配置类,项目中只能有一个。所以一般我们无需自己添加。

@EnableAutoConfiguration

开启自动配置功能

总结,SpringBoot 内部对大量的第三方库或 Spring 内部库进行了默认配置,这些配置是否生效,取决于我们是否引入了对应库所需的依赖,如果有那么默认配置就会生效。

所以,我们使用 SpringBoot 构建一个项目,只需要引入所需框架的依赖,配置就可以交给SpringBoot处理了。除非你不希望使用SpringBoot 的默认配置,它也提供了自定义配置的入口。

@ComponentScan

而我们的 @SpringBootApplication 注解声明的类就是 main 函数所在的启动类,因此扫描的包是该类所在包及其子包。因此,一般启动类会放在一个比较前的包目录中。

自动扫描包(扫描当前主启动类同级的包)并加载符合条件的组件或者 bean,将这个 bean 定义加载到 IOC 容器中。

接口与抽象类

  • 一个子类只能继承一个抽象类,但能实现多个接口
  • 抽象类可以有构造方法,接口没有构造方法
  • 抽象类可以有普通成员变量,接口没有普通成员变量
  • 抽象类和接口都可有静态成员变量,抽象类中静态成员变量访问类型任意,接口只能 public static final (默认)
  • 抽象类可以没有抽象方法,抽象类可以有普通方法;接口在 JDK8 之前都是抽象方法,在JDK8可以有default方法,在JDK9中允许有私有普通方法
  • 抽象类可以有静态方法;接口在JDK8之前不能有静态方法,在JDK8中可以有静态方法,且只能被接口类直接调用(不能被实现类的对象调用)
  • 抽象类中的方法可以是public、protected; 接口方法在JDK8之前只有public abstract,在JDK8可以有default方法,在JDK9中允许有private方法
抽象类 接口
构造方法 可以有 不可以有
普通成员变量 可以有 不可以有
静态成员变量 访问类型任意 只能 public static final
方法 可以没有抽象方法,可以有普通方法 JDK8 之前:抽象方法,JDK8可以有default方法,JDK9中允许有私有普通方法
静态方法 可以有 JDK8之前不能有静态方法,JDK8中可以有静态方法,只能被接口类直接调用

为什么需要泛型

  1. 适用于多种数据类型执行相同的代码

    如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;通过泛型,我们可以复用为一个方法:

  2. 泛型中的类型在使用时指定,不需要强制类型转换类型安全,编译器会检查类型

String、StringBuffer 与 StringBuilder

  • 可变和适用范围。String 对象是不可变的,而 StringBuffer 和 StringBuilder 是可变字符序列。每次对 String 的操作相当于生成一个新的 String 对象,而对 StringBuffer 和 StringBuilder 的操作是对对象本身的操作,而不会生成新的对象,所以对于频繁改变内容的字符串避免使用 String,因为频繁的生成对象将会对系统性能产生影响。
  • 线程安全。String 由于有 final 修饰,是不可变的,也就可以理解为常量,线程安全。StringBuilder 和 StringBuffer 的区别在于 StringBuilder 不保证同步,StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,线程安全。StringBuilder 并没有对方法进行加同步锁,非线程安全

List,Set,Map 三者的区别

  • List (对付顺序的好帮⼿):存储的元素是有序的、可重复的。
  • Set (注重独⼀⽆⼆的性质):存储的元素是⽆序的、不可重复的。
  • Map (⽤ Key 来搜索的专家):使⽤键值对(kye-value)存储,类似于数学上的函数 y=f(x),“x”代表 key,”y”代表 value,Key 是⽆序的、不可重复的,value 是⽆序的、可重复的,每个键最多映射到⼀个值。

Arraylist 与 LinkedList 区别

  • 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
  • 底层数据结构: Arraylist 底层使⽤的是 Object 数组; LinkedList 底层使⽤的是 双向链表 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下⾯有介绍到!)
  • 插⼊和删除是否受元素位置的影响:
    • ArrayList 采⽤数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 ⽐如:执⾏ add(E e) ⽅法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插⼊和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进⾏上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执⾏向后位/向前移⼀位的操作。
    • LinkedList 采⽤链表存储,所以对于 add(E e) ⽅法的插⼊,删除元素时间复杂度不受元素位置的影响,近似 **O(1)**,如果是要在指定位置 i 插⼊和删除元素的话( (add(int index, Eelement) ) 时间复杂度近似为 o(n)) 因为需要先移动到指定位置再插⼊。
  • 是否⽀持快速随机访问: LinkedList 不⽀持⾼效的随机元素访问,⽽ ArrayList ⽀持。快速随机访问就是通过元素的序号快速获取元素对象(对应于 get(int index) ⽅法)。
  • 内存空间占⽤: ArrayList 的空间浪费主要体现在在 list 列表的结尾会预留⼀定的容量空间,⽽ LinkedList 的空间花费则体现在它的每⼀个元素都需要消耗⽐ ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。

ArrayList 与 Vector 的区别

  • ArrayList 是 List 的主要实现类,底层使⽤ Object[ ] 存储,适⽤于频繁的查找⼯作,线程不安全 ;
  • Vector 是 List 的古⽼实现类,底层使⽤ Object[ ] 存储,线程安全的。

HashMap 和 Hashtable 的区别

  • 线程是否安全: HashMap 是⾮线程安全的,HashTable 是线程安全的,因为 HashTable 内部的⽅法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使⽤ ConcurrentHashMap 吧!);

  • 效率: 因为线程安全的问题, HashMap 要⽐ HashTable 效率⾼⼀点。另外, HashTable基本被淘汰,不要在代码中使⽤它;

  • Null key Null value 的⽀持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有⼀个,null 作为值可以有多个;HashTable 不允许有 null 键和 null 值,否则会抛出 NullPointerException 。

  • 初始容量大小和每次扩充容量大小的不同 :

    • 创建时如果不指定容量初始值, Hashtable默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。 HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。
    • 创建时如果给定了容量初始值,那么 Hashtable 会直接使⽤你给定的⼤⼩,⽽ HashMap 会将其扩充为 2 的幂次方大小( HashMap 中的 tableSizeFor() ⽅法保证,下⾯给出了源代码)。也就是说 HashMap 总是使⽤ 2 的幂作为哈希表的⼤⼩,后⾯会介绍到为什么是 2 的幂次⽅。
  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红⿊树前会判断,如果当前数组的⻓度⼩于 64,那么会选择先进⾏数组扩容,⽽不是转换为红⿊树)时,将链表转化为红⿊树,以减少搜索时间。Hashtable 没有这样的机制。

HashMap 和 HashSet区别

HashSet 底层就是基于 HashMap 实现的。( HashSet 的源码⾮常⾮常少,因为除了 clone() 、 writeObject() 、 readObject() 是 HashSet ⾃⼰不得不实现之外,其他⽅法都是直接调⽤ HashMap 中的⽅法。

HashMap HashSet
实现了 Map 接⼝ 实现 Set 接⼝
存储键值对 仅存储对象
调⽤ put() 向 map 中添加元素 调⽤ add() ⽅法向 Set 中添加元素
HashMap 使⽤键(Key)计算 hashcode HashSet 使⽤成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以 equals() ⽅法⽤来判断对象的相等性

ConcurrentHashMap 和 Hashtable 的区别

ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的⽅式上不同

  • 底层数据结构: JDK1.7 的 ConcurrentHashMap 底层采⽤ 分段的数组+链表 实现,JDK1.8采⽤的数据结构跟 HashMap1.8 的结构⼀样,数组+链表/红黑二叉树。 Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采⽤ 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的;
  • 实现线程安全的⽅式(重要):
    • JDK1.7 的时候, ConcurrentHashMap (分段锁)对整个桶数组进⾏了分割分段( Segment ),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,⽽是直接⽤ Node 数组+链表+红黑树 的数据结构来实现,并发控制使⽤ synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap ,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
    • Hashtable(同⼀把锁) :使⽤ synchronized 来保证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法时,其他线程也访问同步⽅法,可能会进⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不能使⽤ put 添加元素,也不能使⽤ get,竞争会越来越激烈效率越低。
    • JDK1.8 的 ConcurrentHashMap 不在是 Segment 数组 + HashEntry 数组 + 链表,而是 Node 数组 + 链表 / 红⿊树。不过,Node 只能⽤于链表的情况,红⿊树的情况需要使⽤ TreeNode 。当冲突链表达到⼀定长度时,链表会转换成红⿊树。

TCP建立连接过程的三次握手

TCP 有 6 种标识:SYN(建立联机) ACK(确认) PSH(传送) FIN(结束) RST(重置) URG(紧急)

  • 什么是三次握手

image-20220907194538705

为了保证数据能到达目标,TCP采用三次握手策略:

  1. 发送端首先发送一个带SYN(synchronize)标志的数据包给接收方【第一次的 seq 序列号是随机产生的,这样是为了网络安全,如果不是随机产生初始序列号,黑客将会以很容易的方式获取到你与其他主机之间的初始化序列号,并且伪造序列号进行攻击】
  2. 接收端收到后,回传一个带有 SYN/ACK(acknowledgement)标志的数据包以示传达确认信息【SYN 是为了告诉发送端,发送方到接收方的通道没问题;ACK 用来验证接收方到发送方的通道没问题】
  3. 最后,发送端再回传一个带 ACK 标志的数据包,代表握手结束,若在握手某个过程中某个阶段莫名中断,TCP 协议会再次以相同的顺序发送相同的数据包

TCP断开连接过程的四次挥手?

  • 什么是四次挥手

image-20220907200133430

  1. 主动断开方(客户端/服务端)-发送一个 FIN,用来关闭主动断开方(客户端/服务端)到被动断开方(客户端/服务端)的数据传送
  2. 被动断开方(客户端/服务端)-收到这个 FIN,它发回一 个 ACK,确认序号为收到的序号加1 。和 SYN 一样,一个 FIN 将占用一个序号
  3. 被动断开方(客户端/服务端)-关闭与主动断开方(客户端/服务端)的连接,发送一个 FIN 给主动断开方(客户端/服务端)
  4. 主动断开方(客户端/服务端)-发回 ACK 报文确认,并将确认序号设置为收到序号加1

什么是 Http 协议

HTTP是一个基于TCP/IP通信协议来传递数据的协议。HTTP协议工作于客户端-服务端架构之上,实现可靠性的传输文字、图片、音频、视频等超文本数据的规范,格式简称为“超文本传输协议”。Http协议属于应用层,用户访问的第一层就是http。

特点:

  1. 简单快速:客户端向服务器发送请求时,只需传送请求方法和路径即可。
  2. 灵活:HTTP允许传输任意类型的数据对象。
  3. 无连接:限制每次连接只处理一个请求。服务器处理完客户请求,并收到客户应答后,即断开连接。
  4. 无状态:协议对于事务处理没有记忆能力。
  5. 支持B/S及C/S模式

Http 和 Https 的区别?

  • 端口不同:Http是80,Https443
  • 安全性:http是超文本传输协议,信息是明文传输,https则是通过SSL加密处理的传输协议,更加安全。
  • 是否付费:https需要拿到CA证书,需要付费
  • 连接方式:http和https使用的是完全不同的连接方式(HTTP的连接很简单,是无状态的;HTTPS 协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。)

HTTP状态码

HTTP状态码表示客户端HTTP请求的返回结果、标识服务器处理是否正常、表明请求出现的错误等。状态码的类别:

img

常用状态码:
200: 请求被正常处理
204: 请求被受理但没有资源可以返回
301: 永久性重定向
302: 临时重定向
304: 已缓存
400: 请求报文语法有误,服务器无法识别
403: 请求的对应资源禁止被访问
404: 服务器无法找到对应资源
500: 服务器内部错误
503: 服务器正忙

GET方法与POST方法的区别

  • 功能上: GET一般用来从服务器上获取资源,POST一般用来更新服务器上的资源;
  • 安全性: GET是不安全的,因为GET请求提交的数据将明文出现在URL上(请求头上),可能会泄露私密信息;POST请求参数则被包装到请求体中,相对更安全。
  • 数据量: Get 传输的数据量小,因为受URL长度限制,但效率较高; Post可以传输大量数据,所以上传文件时只能用Post方式;
  • 效率:Get 效率高

TCP 与 UDP

  • TCP/IP即传输控制协议,是面向连接的协议,发送数据前要先建立连接,TCP提供可靠的服务,也就是说,通过TCP连接传输的数据不会丢失,没有重复,并且按顺序到达。(类似于打电话)
  • UDP 是属于TCP/IP协议族中的一种。是无连接的协议,发送数据前不需要建立连接,是没有可靠性的协议。因为不需要建立连接所以可以在在网络上以任何可能的路径传输,因此能否到达目的地,到达目的地的时间以及内容的正确性都是不能被保证的。(类似于发微信)
  • 传输控制协议-TCP:提供面向连接的,可靠的数据传输服务。
  • 用户数据协议-UDP:提供无连接的,尽大努力的数据传输服务(不保证数据传输的可靠性)。
TCP UDP
是否连接 面向连接 无连接
是否可靠 可靠传输,使用 流量控制和拥塞控 制 不可靠传输,不使用流量控制和拥塞控制
连接对象个数 只能是一对一通信 支持一对一,一对多,多对一和多对多交互通信
传输方式 面向字节流 面向报文
首部开销 首部小20字节, 大60字 节 首部开销小,仅8字节
场景 适用于要求可靠传输的应用,例如 文件传输 适用于实时应用(IP电话、视频会议、直播等)

Session、Cookie 和 Token 的对比

  • cookie:cookie 是由 Web 服务器保存在用户浏览器上的文件(key-value格式),可以包含用户相关的信息。客户端向服务器发起请求时,会携带服务器端之前创建的 cookie,服务器端通过 cookie 中携带的数据区分不同的用户。
  • session:session 是浏览器和服务器会话过程中,服务器会分配的一块储存空间给 session。服务器默认会为客户浏览器的 cookie 中设置 sessionid,这个sessionid 就和 cookie 对应,浏览器在向服务器请求过程中传输的 cookie 包含 sessionid ,服务器根据传输 cookie 中的 sessionid 获取出会话中存储的信息,然后确定会话的身份信息。
  • token:Token 是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token 便应运而生。Token 是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个 Token 便将此Token返回给客户端,以后客户端只需带上这个 Token 前来请求数据即可,无需再次带上用户名和密码。使用 Token 的目的:Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。
  • cookie与session区别
    • 安全性:cookie 数据存放在客户端上,安全性较差,session 数据放在服务器上,安全性相对更高
    • 大小限制:cookie 有大小限制,单个cookie保存的数据不能超过4K,session无此限制,理论上只与服务器的内存大小有关;
    • 服务器资源消耗:Session是保存在服务器端上会存在一段时间才会消失,当访问增多,对服务器性能有影响
    • 实现机制:Session的实现常常依赖于Cookie机制,通过Cookie机制回传SessionID;
  • Token 是在服务端产生的。如果前端使用用户名/密码向服务端请求认证,服务端认证成功,那么在服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的合法地位
  • session与token区别
    • session机制存在服务器压力增大,CSRF跨站伪造请求攻击,扩展性不强等问题;
    • session存储在服务器端,token存储在客户端
    • token提供认证和授权功能,作为身份认证,token安全性比session好;
    • session这种会话存储方式方式只适用于客户端代码和服务端代码运行在同一台服务器上,token适用于项目级的前后端分离(前后端代码运行在不同的服务器下)

redis 缓存

使用

将博客浏览量添加到缓存中,开启服务时,首先从数据库中将数据读取并添加到缓存中,以后读取的话就从缓存中读

①在应用启动时把博客的浏览量存储到 redis 中

②更新浏览量时去更新redis中的数据

③每隔10分钟把Redis中的浏览量更新到数据库中

④读取文章浏览量时从redis读取

如何保证缓存与数据库的双写一致性

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
  • 更新的时候,先更新数据库,然后再删除缓存。

SpringSecurity

用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录

用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。

springsecurity 底层实现为一条过滤器链,就是用户请求进来,判断有没有请求的权限,抛出异常,重定向跳转。

image-20211215094003288

SpringSecurity 完整流程

SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。

image-20211214144425527

图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。

UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。

ExceptionTranslationFilter:处理过滤器链中抛出的任何 AccessDeniedException 和 AuthenticationException 。

FilterSecurityInterceptor:负责权限校验的过滤器。

image-20211214151515385

登录

​ ①自定义登录接口

​ 调用 ProviderManager 的方法进行认证 如果认证通过生成 jwt

​ 把用户信息存入redis 中

​ ②自定义 UserDetailsService

​ 在这个实现类中去查询数据库

​ ③因为 UserDetailsService 方法的返回值是 UserDetails 类型,所以需要定义一个类,实现该接口,把用户信息封装在其中。

​ ④使用 SpringSecurity 为我们提供的 BCryptPasswordEncode 来进行密码校验

校验

​ ①定义 Jwt 认证过滤器

​ 获取 token

​ 解析 token 获取其中的 userid

​ 从 redis 中获取用户信息

​ 存入 SecurityContextHolder

自定义失败处理

如果是认证过程中出现的异常会被封装成 AuthenticationException 然后调用 AuthenticationEntryPoint 对象的方法去进行异常处理。

如果是授权过程中出现的异常会被封装成 AccessDeniedException 然后调用AccessDeniedHandler对象的方法去进行异常处理。

所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。

线程池

ThreadPoolExecutor

可重用线程(消费者:不断获取任务来执行)+阻塞队列(生产者消费者模式下平衡速度差异的组件)+main(生产者:源源不断生成任务)

img

  • corePoolSize 核心线程数目 (最多保留的线程数)
  • maximumPoolSize 最大线程数目
  • keepAliveTime 生存时间 - 针对救急线程
  • unit 时间单位 - 针对救急线程
  • workQueue 阻塞队列
  • threadFactory 线程工厂 - 可以为线程创建时起个好名字
  • handler 拒绝策略

拒绝策略

// 1) 死等
// 2) 带超时等待
// 3) 让调用者放弃任务执行 DiscardPolicy 放弃本次任务
// 4) 让调用者抛出异常 AbortPolicy 让调用者抛出 RejectedExecutionException 异常,这是默认策略
// 5) 让调用者自己执行任务 CallerRunsPolicy 让调用者运行任务 
DiscardOldestPolicy 放弃队列中最早的任务,本任务取而代之 

进程与线程

进程

  • 程序由指令和数据组成,但这些指令要运行,数据要读写,就必须将指令加载至 CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的
  • 当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个进程。
  • 进程就可以视为程序的一个实例。

线程

  • 一个进程之内可以分为一到多个线程。
  • 一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给 CPU 执行
  • Java 中,线程作为最小调度单位,进程作为资源分配的最小单位。

死锁

在并发环境下,各进程因竞争资源而造成的一种互相等待对方手里的资源,导致各进程都阻塞,都无法向前推进的现象,就是“死锁”发生后若无外力干涉,这些进程都将无法向前推进。

死锁产生的必要条件

产生死锁必须同时满足以下四个条件,若有一个不满足,死锁就不会发生

  1. 互斥条件:对互斥使用的资源的争夺才会导致死锁
  2. 不剥夺条件:进程获得的资源在未使用完之前,不能由其他进程强行夺走,只能主动释放
  3. 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源又被其他进程占有,此时请求进程被阻塞,但又对自己已有的资源保持不放
  4. 循环等待条件:存在一种进程资源的循环等待链,链中的每一个进程已获得的资源同时被下一个进程所请求

死锁的处理策略

  1. 预防死锁:破坏死锁产生的四个必要条件中的一个或几个
  2. 避免死锁:用某种方法防止系统进入不安全状态,从而避免死锁
  3. 死锁的检测和解除:允许死锁的发生,操作系统检测出死锁后,会采取措施解除死锁

Monitor

Java 对象头

以 32 位虚拟机为例

普通对象的对象头结构如下,其中,Mark Word 主要用来存储对象自身的运行时数据;Klass Word 为指针,指向对应的 Class 对象。

image.png

数组对象:相对于普通对象多了记录数组长度

image.png

Mark Word 结构

不同对象状态下结构和含义也不同

image.png

64 位虚拟机 Mark Word

image.png

所以一个对象的结构如下:

1583678624634

原理之 Monitor (锁)

Monitor 被翻译为监视器或者说管程

每个 java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置为指向 Monitor 对象的指针。

Monitor 结构

1583652360228

  • 刚开始时 Monitor 中的 Owner为 null
  • 当 Thread-2 执行 synchronized(obj){} 代码时就会将 Monitor 的所有者 Owner 设置为 Thread-2,上锁成功,Monitor 中同一时刻只能有一个Owner
  • 当 Thread-2 占据锁时,如果线程 Thread-3,Thread-4 也来执行 synchronized(obj){} 代码,就会进入EntryList中变成 BLOCKED 状态
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争时是非公平
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析

注意:

  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

原理之 synchronized 进阶 (锁升级)

(不涉及 Monitor 的)轻量级锁

  • 轻量级锁的使用场景是:如果一个对象虽然有多个线程要对它进行加锁,但是加锁的时间是错开的(也就是没有人可以竞争的),那么可以使用轻量级锁来进行优化。

  • 轻量级锁对使用者是透明的,即语法仍然是synchronized

  • 每次指向到 synchronized 代码块时,都会创建锁记录(Lock Record)对象,每个线程都会包括一个锁记录的结构,锁记录内部可以储存对象的 Mark Word 和对象引用 reference

1583755737580

  • 让锁记录中的 Object reference 指向对象,并且尝试用 cas(compare and sweep) 替换 Object 对象的 Mark Word ,将 Mark Word 的值存入锁记录中

1583755888236

  • 如果 cas 替换成功,那么对象的对象头储存的就是锁记录的地址和状态00,表示由该线程给对象加锁,如下所示

1583755964276

  • 如果 cas 失败,有两种情况
    • 如果是其它线程已经持有了该 Object 的轻量级锁,那么表示有竞争,将进入锁膨胀阶段
    • 如果是自己的线程已经执行了 synchronized 进行加锁,那么再添加一条 Lock Record 作为重入的计数

1583756190177

  • 当线程退出 synchronized 代码块的时候,如果获取的是取值为 null 的锁记录 ,表示有重入,这时重置锁记录,表示重入计数减一

1583756357835

  • 当线程退出 synchronized 代码块的时候,如果获取的锁记录取值不为 null,那么使用 cas 将 Mark Word 的值恢复给对象
    • 成功,则解锁成功
    • 失败,则说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

(轻量级)锁膨胀(为重量级锁)

如果在尝试加轻量级锁的过程中,cas 操作无法成功,这是有一种情况就是其它线程已经为这个对象加上了轻量级锁,这是就要进行锁膨胀,将轻量级锁变成重量级锁。

  • 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

1583757433691

  • 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
    • 即为对象申请 Monitor 锁,让 Object 指向重量级锁地址
    • 然后自己进入Monitor 的 EntryList 变成 BLOCKED 状态

1583757586447

  • 当Thread-0 退出 synchronized 同步块时,使用 cas 将 Mark Word 的值恢复给对象头,失败,那么会进入重量级锁的解锁过程,即按照 Monitor 的地址找到 Monitor 对象,将 Owner 设置为 null,唤醒 EntryList 中的 Thread-1 线程

(比轻量级锁更轻的)偏向锁

  • 轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
  • Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

这里的线程 id 是操作系统赋予的 id 和 Thread 的 id 是不同的

image-20220815123841079

image.png

偏向状态

1583762169169

一个对象的创建过程

  • 如果开启了偏向锁(默认是开启的),那么对象刚创建之后,Mark Word 值为 0x05,即最后三位的值101,并且这时它的 Thread,epoch,age都是0
  • 偏向锁默认是延迟的,不会在程序启动的时候立刻生效,如果想避免延迟,可以添加虚拟机参数来禁用延迟:-XX:BiasedLockingStartupDelay=0禁用延迟
  • 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
  • 注意:处于偏向锁的对象解锁后,线程 id 仍存储于对象头中

垃圾回收

image-20220730212626643


文章作者: Yang Shiyu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Yang Shiyu !
  目录