2023.09.15
数字马力一面
🌝1.面向对象的几个核心特点
封装: 封装是指将数据和对数据的操作封装在一个单元中,通过访问权限控制不 同部分的可见性;关于他的作用主要有以下两点: ①将对象的属性用 private关键字进行修饰将属性私有化,只提供 get和set 方法,能起到保护数据安全性的作用 ②将与业务无关的逻辑代码进行封装,可以在一定程度上提高代码复用性, 减少代码冗余;
继承: 继承是使用已经存在的类作为基类来创建新的类,新的类可以继承父类 的属性或方法也可以有自己的属性或方法;关于继承我个人理解主要有以下三 点: ①子类拥有父类除私有类型的所有属性和方法; ②子类可以有自己的方法和 属性,即子类可以对父类进行扩展; ③子类可以用自己的方式实现父类中的方法; 总的来说,继承可以提高代码的复用性;
多态:多态是指同一类型的对象,在不同的情况下表现出不同的行为;这其中重载和重写都是实现多态的方式,
①重载:重载时发生在一个类中,同名的方法具有不同的参数列表 ②重写发生在父类与子类的继承中,要求子类中的方法与父类 中方法的名称,返回类型,参数相同,且要易于父类 或与父类同等访问级别;
抽象(Abstraction):抽象是指将对象的共同特征提取出来形成抽象类或接口, 通过继承和实现来实现具体的功能。抽象可以隐藏细节、简化复杂性,并提供统 一的接口供其他代码使用。
🌑2.关于集合方面熟悉哪一些,说一下 ConcurrentHashMap 为什么是线程安全的 性能方面做了什么优化
Hashtable 对比 ConcurrentHashMap
-
Hashtable 与 ConcurrentHashMap 都是线程安全的 Map 集合
-
Hashtable 并发度低,整个 Hashtable 对应一把锁,同一时刻,只能有一个线程操作它
-
ConcurrentHashMap 并发度高,整个 ConcurrentHashMap 对应多把锁,只要线程访问的是不同锁,那么不会冲突
ConcurrentHashMap 1.7
-
数据结构:
Segment(大数组) + HashEntry(小数组) + 链表
,每个 Segment 对应一把锁,如果多个线程访问不同的 Segment,则不会冲突
ConcurrentHashMap 1.8
-
数据结构:
Node 数组 + 链表或红黑树
,数组的每个头节点作为锁,如果多个线程访问的头节点不同,则不会冲突。首次生成头节点时如果发生竞争,利用 cas 而非 syncronized,进一步提升性能
🌚3.JVM 的内存模型,本地方法栈中使用栈帧有什么好处
JVM内存模型:
JVM内存区域最粗略的划分可以分为堆和栈 ,当然,按照虚拟机规范,可以划分为以下几个区域:
JVM 内存分为线程私有区和线程共享区,其中 方法区
和 堆
是线程共享区, 虚拟机栈 、本地⽅法栈和程序计数器是线程隔离的数据区。栈是运行时的单位,而堆是存储的单位。
1)程序计数器
程序计数器(Program Counter Register)也被称为 PC 寄存器,是⼀块较⼩的内存空间。 它可以看作是当前线程所执⾏的字节码的⾏号指⽰器。
2)Java 虚拟机栈
Java 虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的⽣命周期与线程相同。 Java 虚拟机栈描述的是 Java ⽅法执⾏的线程内存模型:⽅法执行时,JVM 会同步创建⼀个栈帧,用来存储局部变量表、操作数栈、动态连接等。
3)本地方法栈
本地⽅法栈(Native Method Stacks)与虚拟机栈所发挥的作⽤是⾮常相似的,其区别只是虚拟机栈为虚拟机执行Java ⽅法(也就是字节码)服务,⽽本地⽅法栈则是为虚拟机使⽤到的本地(Native) ⽅法服务。
Java 虚拟机规范允许本地⽅法栈被实现成固定⼤⼩的或者是根据计算动态扩展和收缩的。
4)Java 堆
对于 Java 应⽤程序来说,Java 堆(Java Heap)是虚拟机所管理的内存中最⼤的⼀块。Java 堆是被 所有线程共享的⼀块内存区域,在虚拟机启动时创建。此内存区域的唯⼀⽬的就是存放对象实例, Java ⾥“⼏乎”所有的对象实例都在这⾥分配内存。
Java 堆是垃圾收集器管理的内存区域,因此⼀些资料中它也被称作“GC 堆”(Garbage Collected Heap,)。从回收内存的⾓度看,由于现代垃圾收集器⼤部分都是基于分代收集理论设计的,所以 Java 堆中经常会出现 新⽣代 、 ⽼年代 、 Eden 空 间 、 From Survivor 空 间 、 To Survivor空 间 等名 词,需要注意的是这种划分只是根据垃圾回收机制来进⾏的划分,不是 Java 虚拟机规范本⾝制定的。
5)⽅法区
⽅法区是⽐较特别的⼀块区域,和堆类似,它也是各个线程共享的内存区域,⽤于存储已被虚拟机加载 的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
它特别在 Java 虚拟机规范对它的约束⾮常宽松,所以⽅法区的具体实现历经了许多变迁,例如 jdk1.7 之前使⽤永久代作为⽅法区的实现。
使用栈帧的好处:
Java栈(Stack): Java栈是每个线程私有的内存区域,用于存储栈帧(Stack Frame)。每当一个方法被调用时,会为该方法创建一个新的栈帧。每个栈帧包含了方法的局部变量、操作数栈以及指向当前方法所属类的运行时常量池的引用。
①独立的执行环境:每个方法在本地方法栈中有一个对应的栈帧,栈帧包含了方 法的局部变量、操作数栈以及其他与方法执行相关的信息。通过使用栈帧,可以 为每个方法提供独立的执行环境,确保方法之间的数据不会相互干扰。
②方法调用和返回的支持:栈帧在方法调用和返回时发挥重要作用。当一个方法 被调用时,JVM 会创建一个新的栈帧并将其推入本地方法栈,用于保存方法的参 数、局部变量等信息。当方法执行完毕后,对应的栈帧会被弹出,恢复上一次方 法调用的状态。
③局部变量的管理:栈帧用于管理方法的局部变量。每个栈帧都包含了方法的局 部变量表,用于存储方法内部定义的局部变量。通过使用栈帧,可以有效地管理 方法中的局部变量的生命周期和作用域。
④资源分配和释放:栈帧的创建和销毁都是在方法调用和返回时自动完成的。这 种自动化的资源分配和释放机制可以确保方法执行期间所需要的资源能够得到 合理的管理,避免资源泄露和内存溢出等问题。
🌕4.缓存雪崩(不局限于 Redis)
缓存击穿:某一热点 key 在缓存和数据库中都存在,它过期时,这时由于并发用户特别多,同时读缓存没读到,又同时去数据库去读,压垮数据库
1.热点数据不过期 2.对【查询缓存没有,查询数据库,结果放入缓存】这三步进行加锁,这时只有一个客户端能获得锁,其它客户端会被阻塞,等锁释放开,缓存已有了数据,其它客户端就不必访问数据库了。但会影响吞吐量(有损方案)
缓存雪崩:
情况1:就是指缓存由于某些原因(比如 宕机、cache 服务挂了或者不响应)整 体 crash 掉了,导致大量请求到达后端数据库,从而导致数据库崩溃,整个系统 崩溃,发生灾难。
1. 错开过期时间:在过期时间上加上随机值(比如 1~5 分钟) 2. 服务降级:暂停非核心数据查询缓存,返回预定义信息(错误页面,空值等)
情况2:Redis 实例宕机,大量请求进入数据库
1. 事前预防:搭建高可用集群 2. 多级缓存:缺点是实现复杂度高 3. 熔断:通过监控一旦雪崩出现,暂停缓存访问待实例恢复,返回预定义信息(有损方案) 4. 限流:通过监控一旦发现数据库访问量超过阈值,限制访问数据库的请求数(有损方案)
缓存穿透:如果一个 key 在缓存和数据库都不存在,那么访问这个 key 每次都会进入数据库
1. 如果数据库没有,也将此不存在的 key 关联 null 值放入缓存,缺点是这样的 key 没有任何业务作用,白占空间 2. 布隆过滤器
🌗5.Redis 的持久化机制
Redis持久化方式有哪些?有什么区别?
RDB
RDB持久化是把当前进程数据⽣成快照保存到硬盘的过程,触发RDB持久化过程分为⼿动触发和⾃动触发。 RDB⽂件是⼀个压缩的⼆进制⽂件,通过它可以还原某个时刻数据库的状态。由于RDB⽂件是保存在硬盘上的,所以即使Redis崩溃或者退出,只要RDB⽂件存在,就可以⽤它来恢复还原数据库的状态。
⼿动触发分别对应save和bgsave命令:
-
save命令:阻塞当前Redis服务器,直到RDB过程完成为⽌,对于内存⽐较⼤的实例会造成长时间 阻塞,线上环境不建议使⽤。
-
bgsave命令:Redis进程执⾏fork操作创建⼦进程,RDB持久化过程由⼦进程负责,完成后⾃动 结束。阻塞只发⽣在fork阶段,⼀般时间很短。
以下场景会⾃动触发RDB持久化:
-
使⽤save相关配置,如“save m n”。表⽰m秒内数据集存在n次修改时,⾃动触发bgsave。
-
如果从节点执⾏全量复制操作,主节点⾃动执⾏bgsave⽣成RDB⽂件并发送给从节点
-
执⾏debug reload命令重新加载Redis时,也会⾃动触发save操作
-
默认情况下执⾏shutdown命令时,如果没有开启AOF持久化功能则⾃动执⾏bgsave。
AOF
AOF(append only file)持久化:以独⽴⽇志的⽅式记录每次写命令, 重启时再重新执⾏AOF⽂件中的命令达到恢复数据的⽬的。AOF的主要作⽤是解决了数据持久化的实时性,⽬前已经是Redis持久 化的主流⽅式。
AOF的⼯作流程操作:命令写⼊ (append)、⽂件同步(sync)、⽂件重写(rewrite)、重启加载 (load)
流程如下:
1)所有的写⼊命令会追加到aof_buf(缓冲区)中。
2)AOF缓冲区根据对应的策略向硬盘做同步操作。
3)随着AOF⽂件越来越⼤,需要定期对AOF⽂件进⾏重写,达到压缩 的⽬的。
4)当Redis服务器重启时,可以加载AOF⽂件进⾏数据恢复。
RDB 和 AOF 各自有什么优缺点?
RDB | 优点
-
只有⼀个紧凑的⼆进制⽂件 dump.rdb ,⾮常适合备份、全量复制的场景。
-
容灾性好,可以把RDB⽂件拷贝道远程机器或者⽂件系统张,⽤于容灾恢复。
-
恢复速度快,RDB恢复数据的速度远远快于AOF的⽅式
RDB | 缺点
-
实时性低,RDB 是间隔⼀段时间进⾏持久化,没法做到实时持久化/秒级持久化。如果在这⼀间 隔事件发⽣故障,数据会丢失。
-
存在兼容问题,Redis演进过程存在多个格式的RDB版本,存在⽼版本Redis⽆法兼容新版本 RDB的问题。
AOF | 优点
-
实时性好,aof 持久化可以配置 appendfsync 属性,有 always ,每进⾏⼀次命令操作就记录 到 aof ⽂件中⼀次。
-
通过 append 模式写⽂件,即使中途服务器宕机,可以通过 redis-check-aof ⼯具解决数据⼀致性问题。
AOF | 缺点
-
AOF ⽂件⽐ RDB ⽂件⼤,且 恢复速度慢。
-
数据集⼤ 的时候,⽐ RDB 启动效率低。
RDB 和 AOF 如何选择?
-
⼀般来说, 如果想达到⾜以媲美数据库的 数据安全性,应该 同时使⽤两种持久化功能。在这种情况下,当 Redis 重启的时候会优先载⼊ AOF ⽂件来恢复原始的数据,因为在通常情况下 AOF ⽂件保存的数据集要⽐ RDB ⽂件保存的数据集要完整。
-
如果 可以接受数分钟以内的数据丢失,那么可以 只使⽤ RDB 持久化。
-
有很多⽤户都只使⽤ AOF 持久化,但并不推荐这种⽅式,因为定时⽣成 RDB 快照(snapshot) ⾮常便于进⾏数据备份, 并且 RDB 恢复数据集的速度也要⽐ AOF 恢复的速度要快,除此之 外,使⽤ RDB 还可以避免 AOF 程序的 bug。
-
如果只需要数据在服务器运⾏的时候存在,也可以不使⽤任何持久化⽅式。
Redis的数据恢复?
当Redis发⽣了故障,可以从RDB或者AOF中恢复数据。
恢复的过程也很简单,把RDB或者AOF⽂件拷贝到Redis的数据⽬录下,如果使⽤AOF恢复,配置⽂件开启AOF,然后启动redis-server即可。
Redis 启动时加载数据的流程:
-
AOF持久化开启且存在AOF⽂件时,优先加载AOF⽂件。
-
AOF关闭或者AOF⽂件不存在时,加载RDB⽂件。
-
加载AOF/RDB⽂件成功后,Redis启动成功。
-
AOF/RDB⽂件存在错误时,Redis启动失败并打印错误信息。
Redis 4.0 的混合持久化
重启 Redis 时,我们很少使⽤ RDB 来恢复内存状态,因为会丢失⼤量数据。我们通常使⽤ AOF ⽇志重放,但是重放 AOF ⽇志性能相对 RDB 来说要慢很多,这样在 Redis 实例很⼤的情况下,启动需要花费很长的时间。
Redis 4.0 为了解决这个问题,带来了⼀个新的持久化选项— —混合持久化。将 rdb ⽂件的内容和增 量的 AOF ⽇志⽂件存在⼀起。这⾥的 AOF ⽇志不再是全量的⽇志,⽽是⾃持久化开始到持久化结 束 的这段时间发⽣的增量 AOF ⽇志,通常这部分 AOF 日志很小:
于是在 Redis 重启的时候,可以先加载 rdb 的内容,然后再重放增量 AOF日志就可以完全替代之前的 AOF 全量⽂件重放,重启效率因此⼤幅得到提升。
🌒6.Redis 的分布式部署
6.1Redis 集群
前⾯说到了主从存在⾼可⽤和分布式的问题,哨兵解决了⾼可⽤的问题,⽽集群就是终极⽅案,⼀举解决⾼可⽤和分布式问题。
-
数据分区: 数据分区 (或称数据分⽚) 是集群最核⼼的功能。集群将数据分散到多个节点,一方面突破了 Redis 单机内存大小的限制,存储容量⼤⼤增加;另一方面每个主节点都可以对外提 供读服务和写服务,大大提⾼了集群的响应能⼒。
-
高可用: 集群⽀持主从复制和主节点的 ⾃动故障转移 (与哨兵类似),当任⼀节点发⽣故障 时,集群仍然可以对外提供服务。
6.2集群中数据如何分区
分布式的存储中,要把数据集按照分区规则映射到多个节点,常见的数据分区规则三种:
⽅案⼀:节点取余分区
节点取余分区,⾮常好理解,使⽤特定的数据,⽐如Redis的键,或者⽤户I D之类,对响应的hash值取 余:hash(key)% N,来确定数据映射到哪⼀个节点上。
不过该⽅案最⼤的问题是,当节点数量变化时,如扩容或收缩节点,数据节点映射关 系需要重新计 算,会导致数据的重新迁移。
方案⼆:⼀致性哈希分区
将整个 Hash 值空间组织成⼀个虚拟的圆环,然后将缓存节点的 IP 地址或者主机名做 Hash 取值后, 放置在这个圆环上。当我们需要确定某⼀个 Key 需 要存取到哪个节点上的时候,先对这个 Key 做同 样的 Hash 取值,确定在环上的位置,然后按照顺时针⽅向在环上“⾏⾛”,遇到的第⼀个缓存节点就 是要访问的节点。
⽐如说下⾯ 这张图⾥⾯,Key 1 和 Key 2 会落⼊到 Node 1 中,Key 3、Key 4 会落⼊到 Node 2 中,Key 5 落⼊到 Node 3 中,Key 6 落⼊到 Node 4 中。
这种⽅式相⽐节点取余最⼤的好处在于加⼊和删除节点只影响哈希环中 相邻的节点,对其他节点⽆影响。
但它还是存在问题:
-
缓存节点在圆环上分布不平均,会造成部分缓存节点的压⼒较⼤
-
当某个节点故障时,这个节点所要承担的所有访问都会被顺移到另⼀个节点上,会对后⾯这个节点 造成⼒。
⽅案三:虚拟槽分区
这个⽅案 ⼀致性哈希分区的基础上,引⼊了 虚拟节点 的概念。Redis 集群使⽤的便是该⽅案,其中 的虚拟节点称为 槽(slot)。槽是介于数据和实际节点之间的虚拟概念,每个实际节点包含⼀定数量 的槽,每个槽包含哈希值在⼀定范围内的数据。
在使⽤了槽的⼀致性哈希分区中,槽是数据管理和迁移的基本单位。槽解耦了数据和实际节点 之间的 关系,增加或删除节点对系统的影响很⼩。仍以上图为例,系统中有 4 个实际节点,假设为其分配 16
个槽(0-15);
-
槽 0-3 位于 node1;4-7 位于 node2;以此类推....
如果此时删除 node2 ,只需要将槽 4-7 重新分配即可,例如槽 4-5 分配给 node1 ,槽 6 分配给 node3 ,槽 7 分配给 node4 ,数据在其他节点的分布仍然较为均衡
6.3Redis集群的原理
Redis集群通过数据分区来实现数据的分布式存储,通过⾃动故障转移实现⾼可⽤。
集群创建
数据分区是在集群创建的时候完成的。
设置节点
Redis集群⼀般由多个节点组成,节点数量⾄少为6个才能保证组成完整⾼可⽤的集群。每个节点需要 开启配置cluster-enabled yes,让Redis运⾏在集群模式下。
节点握⼿
节点握⼿是指⼀批运⾏在集群模式下的节点通过Gossip协议彼此通信, 达到感知对⽅的过程。节点握 ⼿是集群彼此通信的第⼀步,由客户端发起命 令:cluster meet{ip}{port}。完成节点握⼿之后,⼀个个的Redis节点就组成了⼀个多节点的集群。
分配槽(slot)
Redis集群把所有的数据映射到16384个槽中。每个节点对应若⼲个槽,只有当节点分配了槽,才能响 应和这些槽关联的键命令。通过 cluster addslots命令为节点分配槽
故障转移
Redis集群的故障转移和哨兵的故障转移类似,但是Redis集群中所有的节点都要承担状态维护的任 务。
故障发现
Redis集群内节点通过ping/pong消息实现节点通信,集群中每个节点都会定期向其他节点发送ping消 息,接收节点回复pong 消息作为响应。如果在cluster-node-timeout时间内通信⼀直失败,则发送节 点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态
当某个节点判断另⼀个节点主观下线后,相应的节点状态会跟随消息在集群内传播。通过Gossip消息 传播,集群内节点不断收集到故障节点的下线报告。当 半数以上持有槽的主节点都标记某个节点是主 观下线时。触发客观下线流程。
故障恢复
故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它 的从节点中选出⼀个替换它, 从⽽保证集群的⾼可⽤。
-
资格检查 每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障 的主节点。
-
准备选举时间 当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该 时间后才能执⾏后续流 程。
-
发起选举 当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程。
-
选举投票 持有槽的主节点处理故障选举消息。投票过程其实是⼀个领导者选举的过程,如集群内有N个持 有槽的主节 点代表有N张选票。由于在每个配置纪元内持有槽的主节点只能投票给⼀个 从节点, 因此只能有⼀个从节点获得N/2+1的选票,保证能够找出唯⼀的从节点。
-
替换主节点 当从节点收集到⾜够的选票之后,触发替换主节点操作。
部署Redis集群⾄少需要⼏个物理节点?
在投票选举的环节,故障主节点也算在投票数内,假设集群内节点规模是3主3从,其中有2 个主节点 部署在⼀台机器上,当这台机器宕机时,由于从节点⽆法收集到 3/2+1个主节点选票将导致故障转移 失败。这个问题也适⽤于故障发现环节。因此部署集群时所有主节点最少需要部署在3台物理机上才能 避免单点问题。
6.4集群的伸缩
Redis集群提供了灵活的节点扩容和收缩⽅案,可以在不影响集群对外服务的情况下,为集群添加节点 进⾏扩容也可以下线部分节点进⾏缩容。
其实,集群 扩容和缩容的关键点,就在于槽和节点的对应关系,扩容和缩容就是将⼀部分槽和数据迁移给新节点。
例如下⾯⼀个集群,每个节点对应若⼲个槽,每个槽对应⼀定的数据,如果希望加⼊1个节点希望实现 集群扩容时,需要通过相关命令把⼀部分槽和内容迁移给新节点。
缩容也是类似,先把槽和数据迁移到其它节点,再把对应的节点下线。
🌚7.Redis 为什么能用于分布式锁的实现
哪些特性保证了分布式锁是可靠的, Redis 为什么可以保证是线程安全的(Redis 没有线程安全的问题吗?)
为什么需要分布式锁
我们知道,当多个线程并发操作某个对象时,可以通过synchronized来保证同一时刻只能有一个线程获取到对象锁进而处理synchronized关键字修饰的代码块或方法。既然已经有了synchronized锁,为什么这里又要引入分布式锁呢?
因为现在的系统基本都是分布式部署的,一个应用会被部署到多台服务器上,synchronized只能控制当前服务器自身的线程安全,并不能跨服务器控制并发安全。比如下图,同一时刻有4个线程新增同一件商品,其中两个线程由服务器A处理,另外两个线程由服务器B处理,那么最后的结果就是两台服务器各执行了一次新增动作。这显然不符合预期。
而本篇文章要介绍的分布式锁就是为了解决这种问题的。
什么是分布式锁
分布式锁,就是控制分布式系统中不同进程共同访问同一共享资源的一种锁的实现。
所谓当局者迷,旁观者清,先举个生活中的例子,就拿高铁举例,每辆高铁都有自己的运行路线,但这些路线可能会与其他高铁的路线重叠,如果只让高铁内部的司机操控路线,那就可能出现撞车事故,因为司机不知道其他高铁的运行路线是什么。所以,中控室就发挥作用了,中控室会监控每辆高铁,高铁在什么时间走什么样的路线全部由中控室指挥。
分布式锁就是基于这种思想实现的,它需要在我们分布式应用的外面使用一个第三方组件(可以是数据库、Redis、Zookeeper等)进行全局锁的监控,由这个组件决定什么时候加锁,什么时候释放锁。
Redis如何实现分布式锁
在聊Redis如何实现分布式锁之前,我们要先聊一下redis的一个命令:setnx key value。我们知道,Redis设置一个key最常用的命令是:set key value,该命令不管key是否存在,都会将key的值设置成value,并返回成功:
setnx key value 也是设置key的值为value,不过,它会先判断key是否已经存在,如果key不存在,那么就设置key的值为value,并返回1;如果key已经存在,则不更新key的值,直接返回0:
● 最简单的版本:setnx key value
基于setnx命令的特性,我们就可以实现一个最简单的分布式锁了。我们通过向Redis发送 setnx 命令,然后判断Redis返回的结果是否为1,结果是1就表示setnx成功了,那本次就获得锁了,可以继续执行业务逻辑;如果结果是0,则表示setnx失败了,那本次就没有获取到锁,可以通过循环的方式一直尝试获取锁,直至其他客户端释放了锁(delete掉key)后,就可以正常执行setnx命令获取到锁。流程如下:
这种方式虽然实现了分布式锁的功能,但有一个很明显的问题:没有给key设置过期时间,万一程序在发送delete命令释放锁之前宕机了,那么这个key就会永久的存储在Redis中了,其他客户端也永远获取不到这把锁了。
● 升级版本:设置key的过期时间
针对上面的问题,我们可以基于setnx key value的基础上,同时给key设置一个过期时间。Redis已经提供了这样的命令:set key value ex seconds nx。其中,ex seconds 表示给key设置过期时间,单位为秒,nx 表示该set命令具备setnx的特性。效果如下:
我们设置name的过期时间为60秒,60秒内执行该set命令时,会直接返回nil。60秒后,我们再执行set命令,可以执行成功,效果如下:
基于这个特性,升级后的分布式锁流程如下:
这种方式虽然解决了一些问题,但却引来了另外一个问题:存在锁误删的情况,也就是把别人加的锁释放了。例如,client1获得锁之后开始执行业务处理,但业务处理耗时较长,超过了锁的过期时间,导致业务处理还没结束时,锁却过期自动删除了(相当于属于client1的锁被释放了),此时,client2就会获取到这把锁,然后执行自己的业务处理,也就在此时,client1的业务处理结束了,然后向Redis发送了delete key的命令来释放锁,Redis接收到命令后,就直接将key删掉了,但此时这个key是属于client2的,所以,相当于client1把client2的锁给释放掉了:
● 二次升级版本:value使用唯一值,删除锁时判断value是否当前线程的
要解决上面的问题,最省事的做法就是把锁的过期时间设置长一点,要远大于业务处理时间,但这样就会严重影响系统的性能,假如一台服务器在释放锁之前宕机了,而锁的超时时间设置了一个小时,那么在这一个小时内,其他线程访问这个服务时就一直阻塞在那里。所以,一般不推荐使用这种方式。
另一种解决方法就是在set key value ex seconds nx时,把value设置成一个唯一值,每个线程的value都不一样,在删除key之前,先通过get key命令得到value,然后判断value是否是自己线程生成的,如果是,则删除掉key释放锁,如果不是,则不删除key。正常流程如下:
当业务处理还没结束的时候,key自动过期了,也可以正常释放自己的锁,不影响其他线程:
二次升级后的方案看起来似乎已经没什么问题了,但其实不然。仔细分析流程后我们发现,判断锁是否属于当前线程和释放锁两个步骤并不是原子操作。正常来说,如果线程1通过get操作从Redis中得到的value是123,那么就会执行删除锁的操作,但假如在执行删除锁的动作之前,系统卡顿了几秒钟,恰好在这几秒钟内,key自动过期了,线程2就顺利获取到锁开始执行自己的逻辑了,此时,线程1卡顿恢复了,开始继续执行删除锁的动作,那么此时删除的还是线程2的锁。
● 终极版本:Lua脚本
针对上述Redis原始命令无法满足部分业务原子性操作的问题,Redis提供了Lua脚本的支持。Lua脚本是一种轻量小巧的脚本语言,它支持原子性操作,Redis会将整个Lua脚本作为一个整体执行,中间不会被其他请求插入,因此Redis执行Lua脚本是一个原子操作。
在上面的流程中,我们把get key value、判断value是否属于当前线程、删除锁这三步写到Lua脚本中,使它们变成一个整体交个Redis执行,改造后流程如下:
这样改造之后,就解决了释放锁时取值、判断值、删除锁等多个步骤无法保证原子操作的问题了。关于Lua脚本的语法可以自行学习,并不复杂,很简单,这里就不做过多讲述。
既然Lua脚本可以在释放锁时使用,那肯定也能在加锁时使用,而且一般情况下,推荐使用Lua脚本,因为在使用上面set key value ex seconds nx命令加锁时,并不能做到重入锁的效果,也就是当一个线程获取到锁后,在没有释放这把锁之前,当前线程自己也无法再获得这把锁,这显然会影响系统的性能。使用Lua脚本就可以解决这个问题,我们可以在Lua脚本中先判断锁(key)是否存在,如果存在则再判断持有这把锁的线程是否是当前线程,如果不是则加锁失败,否则当前线程再次持有这把锁,并把锁的重入次数+1。在释放锁时,也是先判断持有锁的线程是否是当前线程,如果是则将锁的重入次数-1,直至重入次数减至0,即可删除该锁(key)。
实际项目开发中,其实基本不用自己写上面这些分布式锁的实现逻辑,而是使用一些很成熟的第三方工具,当下比较流行的就是Redisson,它既提供了Redis的基本命令的封装,也提供了Redis分布式锁的封装,使用非常简单,只需直接调用相应方法即可。但工具虽然好用,底层原理还是要理解的,这就是本篇文章的目的。
🌚8. 如果 Redis 是分布式集群部署,Redis 的分布式锁是如何实现的
(说了主从复制,面试官说在集群模式下呢即各个节点都为主节点)
面试官解释:这个是集群模式的一个特征,根据 Key 什么的,对一台机器加锁锁 的可能是锁的是一个 ID 或者其他的什么,然后,存储的时候 ID 可能会固定路由 到一台机器上去
可以使用 Redis 官方提供的 readlock 算法:
①所有节点都去获取一个当前时间的时间戳; ②每一个节点都生成自己的随即机唯一标识,然后,客户端根据时间戳以及各个 锁的不同标识依次对各个节点执行获取锁的操作,且需要设置一个获取锁的等待 时间该时间要小于锁的过期时间,当获取锁失败的时候应立即尝试下一个节点; ③若获取到超过半数节点的锁,且获取锁的时间小于锁的过期时间,即认为获取 锁成功,若获取锁失败则向所有节点发起释放锁的操作,还可以通过设置过期时 间防止死锁问题的产生
🌚9.Redis 的哨兵模式以及其相关的算法,选节点的算法,raft 算法了解吗
(分布式系统中实现一致性的算法,故障恢复的一种策略)
主从复制存在的问题:
⼀旦主节点出现故障,需要⼿动将⼀个从节点晋升为主节点,同时需要修改应⽤⽅的主节点地址, 还需要命令其他从节点去复制新的主节点,整个过程都需要⼈⼯⼲预。 主节点的写能⼒受到单机的限制。 主节点的存储能⼒受到单机的限制。
Redis Sentinel(哨兵)
完成⾃动故障转移
Redis Sentinel ,它由两部分组成,哨兵节点和数据节点:
-
哨兵节点: 哨兵系统由⼀个或多个哨兵节点组成,哨兵节点是特殊的 Redis 节点,不存储数据, 对数据节点进⾏监控。
-
数据节点: 主节点和从节点都是数据节点;
在复制的基础上,哨兵实现了 ⾃动化的故障恢复 功能,下⾯是官⽅对于哨兵功能的描述:
-
监控(Monitoring): 哨兵会不断地检查主节点和从节点是否运作正常。
-
⾃动故障转移(Automatic failover): 当 主节点 不能正常⼯作时,哨兵会开始 ⾃动故障转移 操作,它会将失效主节点的其中⼀个 从节点升级为新的主节点,并让其他从节点改为复制新的主 节点。
-
配置提供者(Configuration provider): 客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。
-
通知(Notification): 哨兵可以将故障转移的结果发送给客户端。
其中,监控和⾃动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移。⽽配置提供者和通知 功能,则需要在与客户端的交互中才能体现。
Redis Sentinel(哨兵)实现原理
1)定时监控
Sentinel通过三个定时监控任务完成对各个节点发现和监控:
-
每隔1 0秒,每个Sentinel节点会向主节点和从节点发送info命令获取最新的拓扑结构
-
每隔2秒,每个Sentinel节点会向Redis数据节点的sentinel:hello 频道上发送该Sentinel 节点对于主节点的判断以及当前Sentinel节点的信息
-
每隔1秒,每个Sentinel节点会向主节点、从节点、其余Sentinel节点发送⼀条ping命令做⼀ 次⼼跳检测,来确认这些节点当前是否可达
2)主观下线和客观下线
主观下线就是哨兵节点认为某个节点有问题,客观下线就是超过⼀定数量的哨兵节点认为主节点有 问题。
-
主观下线 每个Sentinel节点会每隔1秒对主节点、从节点、其他Sentinel节点发送ping命令做⼼跳检测,当 这些节点超过 down-after-milliseconds没有进⾏有效回复,Sentinel节点就会对该节点做失败 判定,这个⾏为叫做主观下线。
-
客观下线 当Sentinel主观下线的节点是主节点时,该Sentinel节点会通过sentinel is- master-down-byaddr命令向其他Sentinel节点询问对主节点的判断,当超过 < quorum>个数,Sentinel节点认为 主节点确实有问题,这时该Sentinel节点会做出客观下线的决定
3)领导者Sentinel节点选举
Sentinel节点之间会做⼀个领导者选举的⼯作,选出⼀个Sentinel节点作为领导者进⾏故障转移的 ⼯作。Redis使⽤了Raft算法实现领导者选举。
4)故障转移
-
在从节点列表中选出⼀个节点作为新的主节点,这⼀步是相对复杂⼀些的⼀步
-
Sentinel领导者节点会对第⼀步选出来的从节点执⾏slaveof no one命令让其成为主节点
-
Sentinel领导者节点会向剩余的从节点发送命令,让它们成为新主节点的从节点
-
Sentinel节点集合会将原来的主节点更新为从节点,并保持着对其关注,当其恢复后命令它去复制新的主节点
领导者Sentinel节点选举Raft
-
每个在线的Sentinel节点都有资格成为领导者,当它确认主节点主观 下线时候,会向其他 Sentinel节点发送sentinel is-master-down-by-addr命令, 要求将⾃⼰设置为领导者。
-
收到命令的Sentinel节点,如果没有同意过其他Sentinel节点的sentinel is-master-down-byaddr命令,将同意该请求,否则拒绝。
-
如果该Sentinel节点发现⾃⼰的票数已经⼤于等于max(quorum, num(sentinels)/2+1), 那么它将成为领导者。
-
如果此过程没有选举出领导者,将进⼊下⼀次选举。
参考资料
https://www.youtube.com/watch?v=vYp4LYbnnW8 Raft 作者讲解 Raft
Raft Consensus Algorithm Raft 资源
Raft Scope Raft 可视化
Leader 选举
-
Leader 会不断向选民发送 AppendEntries 请求,证明自己活着
-
选民收到 Leader AppendEntries 请求后会重置自己的 timeout 时间
-
选民收不到 AppendEntries 请求超时后,转换角色为候选者,并将任期加1,发送 RequestVote 请求,推选自己
-
选民收到第一个 RequestVote,会向该候选者投一票,这样即使有多个候选者,必定会选出一个 Leader,选票过半即当选,如果落选会变回选民
-
每一任期最多有一个 Leader,但也可能没有(选票都不过半的情况,需要再进行一轮投票,timeout 在 T~2T 间随机)
-
任期由各个 server 自己维护即可,无需全局维护,在超时后加1,在接收到任意消息时更新为更新的任期,遇到更旧的任期,视为错误
🌒10.谈一谈对注册中心的了解,rpc 是什么
API项目中我用到过啊,这个必须得答出来了
“刚好我API这个项目中用到过RPC”
(注册中心本质上也是 rpc 框架,远程 程序调用),实现 rpc 的相关协议,例如,http 协议也属于其中,但是,http 过 于重量级
面试官解释: 注册中心就是要把服务注册上去,注册上去以后,别人从注册中心拉 取你注册的服务,当拉取到服务之后怎么调呢,最后,调还是要通过 rpc 去调, 可能有一些是自定义的协议,本质上是一种远程调用的方式,不用 http 是因为 http 太重量级了,不同的框架都有自己不同的实现
RPC 是指远程过程调用,也就是说两台服务器 A,B,一个应用部署在 A 服务器 上,想要调用 B 服务器上应用提供的函数/方法,由于不在一个内存空间,不能 直接调用,需要通过网络来表达调用的语义和传达调用的数据
RPC
首先强调 RPC 的主要作用,然后阐述 RPC 和 HTTP 之间的关系
应用场景:你在项目 A 中编写了一个非常有用的函数,现在你在项目 B 中也想要使用这个函数。但问题是,项目 A 和项目 B 是独立运行的,它们不共享同一片内存,也不在同一个进程中。那么,你怎么做才能调用项目 A 中的那个函数呢?
1.解决方法:
-
复制代码和依赖、环境
-
HTTP 请求(提供一个接口,供其他项目调用)
-
RPC
-
把公共的代码打个 jar 包,其他项目去引用(客户端 SDK)
进一步说明: 首先是直接复制粘贴别的项目的方法,然而这可能引发环境依赖和代码问题,因为项目之间各自有独特的设置和条件。后期不方便更新与维护
另一个方法是使用 HTTP 请求,例如,在我们的 yuapi-backend 项目中,有一个 invokeCount 的方法,我们可以将其写到 controller 中并为其创建一个 API 接口,以便其他项目可以调用,这个方法是可行的,但同样需要注意它的限制和适用性。
除此之外,还有其他一些方法,例如 RPC 和微服务等。尤其是 RPC,它与 HTTP 请求的区别是面试中的一个常见问题,稍后会简要解释。
此外,还有一种常见的方法是将公共代码打包成一个 jar 包,供其他项目引用。这种方法在 API 开放平台项目中很常见,就像 yuapi-client-sdk 项目的做法一样,将其打包成 jar 包,yuapi-backend 项目和 yuapi-gateway 项目都了引用这个 jar 包。
2.HTTP 请求怎么调用?
1提供方开发一个接口(地址、请求方法、参数、返回值)
2调用方使用 HTTP Client 之类的代码包去发送 HTTP 请求
3. RPC
作用:像调用本地方法一样调用远程方法。
和直接 HTTP 调用的区别: 1对开发者更透明,减少了很多的沟通成本。 2RPC 向远程服务器发送请求时,未必要使用 HTTP 协议,比如还可以用 TCP / IP,性能更高。(内部服务更适用)。
RPC调用模型:
进一步说明: RPC 和 HTTP 请求在本质上有一些区别。RPC 相对于 HTTP 请求更加透明,这意味着它在开发者间更具透明度。这种透明性是如何理解的呢?一个广泛的定义是,RPC 的目标是使远程方法的调用就像调用本地方法一样,从而实现更为便捷的远程通信。
以 HTTP 请求为例,调用方需要完成诸多步骤。首先,它需要发送 HTTP 请求,使用一个 HTTP 客户端的代码库。以我们之前提到的 yuapi-client-sdk 项目为例,我们可以看到客户端对象是如何使用 HTTPUtil 这个 hutool 工具类的包来发送请求的,这意味着调用方需要编写特定的代码段来发送请求,然后处理返回值,可能还需要解析返回值并封装参数的映射关系等。
在调用本地方法时,我们可以直接传递参数给方法,比如接口统计的方法(如下图所示)。我们之前提到的 yuapi-client-sdk 项目里,你需要自行封装 HTTP 请求,将参数打包成一个参数映射(map),然后按照客户端工具类的方式发送请求。此外,你还需要解析返回值,将其中的 code、data 以及 message 等信息提取出来。
但是,如果我们使用 RPC 方式,就能够实现与调用本地方法类似的体验。你可以直接指定要传递的参数,并且也能够直接获得返回值,无论是布尔类型还是字符串。RPC 方式不需要像 HTTP 请求那样进行额外的封装,但如果你需要封装,当然也可以根据需求自行处理。
因此,我们可以说 RPC 的主要目标之一是模仿调用本地方法的方式,从而实现远程调用。比方说,现在在 yuapi-backend 项目中,有一个方法写在了这个项目里,那么我们只需一行代码:userInterfaceInfoService.invokeCount,就可以调用该方法。
同样的,如果切换到另一个项目,比如 yuapi-gateway,在这个网关项目中,我们同样可以直接使用这行代码,做到方法调用的无缝切换。
RPC 的主要职责就是这个,它的最大作用在于模拟本地方法调用的体验。看上去是请求本地代码,实际上,它可能会请求到其他项目、其他服务器等等。这就是 RPC 的最大价值所在。如果大家不理解,可以想一下刚刚的例子,你只需知道 RPC 最大的优势在于它的透明性,你不需要了解它是如何在 HTTP Client 中怎么封装参数,只需直接调用现成的方法即可,这样可以大大减少沟通成本。
💡 有同学问:feign 不也是动态生成的 httpclient 吗?
Feign 本质上也是动态生成的 HTTP 客户端,但它在调用远程方法时更加精简了 HTTP 请求的步骤。尽管 Feign 使远程调用看起来像是调用本地方法,但实际上与 RPC 仍然有一些微小的区别。虽然两者都可以实现类似的功能,但它们在底层协议上存在差异。
RPC(Remote Procedure Call 远程过程调用) 的一个关键优势在于,它可以使用多种底层协议进行远程调用,而不限于 HTTP 协议。虽然 HTTP 协议可以实现类似的功能,但考虑到性能,RPC 可以选择更原生的协议,如 TCP/IP。而且,网络上还存在其他性能更高的协议,可以根据需要进行选择。
在微服务项目中,对于内部接口,使用 RPC 可能会获得更好的性能。然而,选择使用 Feign 还是 RPC 取决于具体的技术需求,没有绝对的优劣之分。需要注意的是,RPC 和 HTTP 请求可以结合使用,因为 RPC 的底层协议可以是 HTTP,也可以是其他协议,如 TCP/IP 或自定义协议。
综上所述,RPC 和 HTTP 请求是可以互相结合的,但 RPC 在协议的选择上更加灵活。当面试被问到这方面的问题时,首先强调 RPC 的主要作用,然后阐述 RPC 和 HTTP 之间的关系。
让我们简单绘制一张图,来说明 RPC 的工作流程。我们刚刚已经了解了 RPC 的作用以及为什么需要使用 RPC,现在详细说明实现类似本地方法调用的方式来调用远程方法。这需要我们的两个项目,即方法提供者项目和方法调用者项目,来进行一些特定的操作。 在这里,引入一个简单的概念,对于学习过操作系统的同学应该很熟悉。为了实现 RPC,我们需要涉及几个关键角色。 1.提供者(Producer/Provider): 首先,我们需要一个项目来提供方法,这个项目被称为提供者。它的主要任务是为其他人提供已经写好的代码,让其他人可以使用。举例来说,我们可以提供一个名为 invokeCount 的方法。 2.调用方(Invoker/Consumer): 一旦服务提供者提供了服务,调用方需要能够找到这个服务的位置。这就需要一个存储,用于存储已提供的方法,调用方需要知道提供者的地址和 invokeCount 方法,这里需要一个公共的存储。 3.存储: 这是一个公共存储,用于存储提供者提供的方法信息。调用方可以从这个存储中获取提供者的地址和方法信息,例如,提供者的地址可能是 123.123.123.1,而方法是 invokeCount,这些信息会存储在这个存储器中。 调用方可以从存储中获取信息后,就知道调用 invokeCount 方法时需要访问 123.123.123.1,这就是 RPC 的基本流程,这三个角色构成了整个 RPC 模型。
存储有时也会被称为注册中心,它管理着服务信息,包括提供者的 IP 地址等等。调用方从这里获取信息,以便进行调用。
然而,需要注意的是在整个流程中,最终的调用并不是由注册中心来完成的。虽然注册中心会提供信息,但实际上调用方需要自己进行最后的调用动作。注册中心的作用是告诉调用方提供者的地址等信息,然后调用方会根据这些信息来完成最后的调用。
一般情况下,调用方会直接寻找提供者进行调用,而不是依赖注册中心来完成实际的调用过程。注册中心主要的功能是提供地址信息,而并不会承担将调用方所需的内容传送到提供者的角色,整个过程遵循这样的流程。
Dubbo
Dubbo 的作用就是将你的提供者、调用方和注册中心这三者通过其框架整合在一起,从而完成整个远程过程调用(RPC)。因此,Dubbo 可以被视为一个实现了 RPC 功能的框架,而 Zookeeper 则是一个可作为注册中心的存储, Nacos 也可以作为注册中心。
两种使用方式: 1)Spring Boot 代码(注解 + 编程式):写 Java 接口,服务提供者和消费者都去引用这个接口。 2)IDL(接口调用语言):创建一个公共的接口定义文件,服务提供者和消费者读取这个文件。优点是跨语言,所有的框架都认识。 Dubbo底层用的是 Triple 协议:Triple 协议。
🌗11.DNS还用到了哪些更底层的协议
(DNS协议详解_dns协议数据包_lliuhao--的博客-CSDN博客)
就近原则
🌚12.MAC 相关协议
为什么要用到MAC协议:
-
IP地址是全球范围内的地址,用于在不同网络和子网之间进行通信。它们是逻辑地址,可被路由器用于寻址和路由数据。
-
MAC地址是局域网络内的物理地址,用于在同一网络中的设备之间直接通信。它们是硬件地址,与网络适配器紧密相关,通常由网络交换机和局域网络设备用于数据帧的传输。
MAC组成(16+16)
1、特殊控制位(CSR):CSR是用于标识MAC地址的特殊位。每个MAC地址都有一个唯一的CSR值,这些值是由设备制造商分配的。
2、数据位(Data):数据位包含了设备的其他信息,例如生产厂商、产品类型和序列号等。
MAC地址是数据链路层和物理层使⽤的地址,是写在⽹卡上的物理地址,⽤来定义⽹络设备的位置,不可变更。
信道划分的MAC协议: 时间(TDMA)、频带(FDMA)、码片(CDMA)划分
随机访问MAC协议: ALOHA,S-ALOHA,CSMA,CSMA/CD,其中CSMA/CD应用于以太网,CSMA/CA应用于802.11无线局域网
轮转访问MAC协议: 主节点轮询;令牌传递 蓝牙、FDDI、令牌环网
🌗13.说一下 HTTPS 协议,TLS 和 SSL 的区别,他是怎么保证传输
TLS和SSL的区别:
1、标准制定上的差异 SSL协议是由Netscape公司在1994年首次提出的,是由Netscape公司开发的安全传输协议,而TLS协议是由IETF在1999年提出的,是SSL协议的继承者,它使用更严格的安全机制,比SSL更安全。
2、握手过程的差异 SSL和TLS协议在握手过程的一些细节上也有所不同。握手过程是建立TLS/SSL连接的第一步,涉及双方的认证和密钥协商。在SSL握手过程中使用的一些算法和加密机制被认为是不安全的,如MD5和SHA-1,而TLS协议使用更安全的算法和机制,如SHA-256和SHA-384。
3、加密算法的差异 TLS协议对加密算法的选择比SSL更严格,SSL只支持RC4和DES等加密算法,而TLS协议支持更多的加密算法,包括AES、Camellia和其他。AES算法是最常用的对称加密算法之一,比RC4和DES更安全。
4、安全方面的差异 尽管SSL协议在过去被广泛使用,但由于存在一些安全缺陷和漏洞,它已被TLS协议所取代。例如,SSL协议中的POODLE攻击和Heartbleed漏洞被认为是严重的安全问题。相比之下,TLS协议在安全性方面更加严格,尤其是在加密算法的选择、握手过程中的算法和机制等方面,都更加安全可靠。
总的来说,TLS协议是SSL的继承者,与SSL相比,它在安全性、加密算法和握手过程方面都有了很大的改进和提高。虽然仍有一些网站和应用程序使用SSL协议,但随着时间的推移,这些网站和应用程序将被更安全可靠的网站和应用程序取代。
http和https的区别:
-
HTTP 是超⽂本传输协议,信息是明⽂传输,存在安全风险的问题。HTTPS 则解决 HTTP 不安全的缺陷,在 TCP 和 HTTP ⽹络层之间加⼊了 SSL/TLS 安全协议,使得报⽂能够加密传输。
-
HTTP 连接建⽴相对简单, TCP 三次握⼿之后便可进⾏ HTTP 的报⽂传输。⽽ HTTPS 在 TCP 三次握⼿之后,还需进⾏ SSL/TLS 的握⼿过程,才可进⼊加密报⽂传输。
-
HTTP 的端⼜号是80,HTTPS 的端⼜号是443。
-
HTTPS 协议需要向 CA(证书权威机构)申请数字证书,来保证服务器的⾝份是可信的。
为什么要⽤ HTTPS?解决了哪些问题?
因为 HTTP 是明⽂传输,存在安全上的风险:窃听、篡改、冒充
所以引⼊了 HTTPS,HTTPS 在 HTTP 与 TCP 层之间加⼊了 SSL/TLS 协议,可以很好的解决了这 些风险:
信息加密:交互信息⽆法被窃取。 校验机制:⽆法篡改通信内容,篡改了就不能正常显⽰。 ⾝份证书:能证明淘宝是真淘宝。 所以 SSL/TLS 协议是能保证通信是安全的。
HTTPS工作流程:
这道题有⼏个要点:公私钥、数字证书、加密、对称加密、⾮对称加密。
1.客户端发起HTTPS请求,连接到服务端的443端。 2.服务端有⼀套数字证书(证书内容有公钥、证书颁发机构、失效⽇期等)。 3.服务端将⾃⼰的数字证书发送给客户端(公钥在证书⾥⾯,私钥由服务器持有)。 4.客户端收到数字证书之后,会验证证书的合法性。如果证书验证通过,就会⽣成⼀个随机的对称密钥,⽤证书的公钥加密。 5.客户端将公钥加密后的密钥发送到服务器。 6.服务器接收到客户端发来的密⽂密钥之后,⽤⾃⼰之前保留的私钥对其进⾏⾮对称解密,解密之后就得到客户端的密钥,然后⽤客户端密钥对返回数据进⾏对称加密,酱紫传输的数据都是密⽂啦。 7.服务器将加密后的密⽂返回到客户端。 8.客户端收到后,⽤⾃⼰的密钥对其进⾏对称解密,得到服务器返回的数据。
客户端怎么去校验证书的合法性
⾸先,服务端的证书从哪来的呢? 为了让服务端的公钥被⼤家信任,服务端的证书都是由CA(CertificateAuthority,证书认证机 构)签名的,CA就是⽹络世界⾥的公安局、公证中⼼,具有极⾼的可信度,所以由它来给各个公钥签 名,信任的⼀⽅签发的证书,那必然证书也是被信任的
CA 签发证书的过程,如上图左边部分:
-
⾸先 CA 会把持有者的公钥、⽤途、颁发者、有效时间等信息打成⼀个包,然后对这些信息进⾏ Hash 计算,得到⼀个 Hash 值;
-
然后 CA 会使⽤⾃⼰的私钥将该 Hash 值加密,⽣成 Certificate Signature,也就是 CA 对证书 做了签名;
-
最后将 Certificate Signature 添加在⽂件证书上,形成数字证书;
客户端校验服务端的数字证书的过程,如上图右边部分:
-
⾸先客户端会使⽤同样的 Hash 算法获取该证书的 Hash 值 H1;
-
通常浏览器和操作系统中集成了 CA 的公钥信息,浏览器收到证书后可以使⽤ CA 的公钥解密 Certificate
-
Signature 内容,得到⼀个 Hash 值 H2 ;
-
最后⽐较 H1 和 H2,如果值相同,则为可信赖的证书,否则则认为证书不可信。
假如在 HTTPS 的通信过程中,中间⼈篡改了证书原⽂,由于他没有 CA 机构的私钥,所以 CA 公钥 解密的内容就不⼀致。