后端工程师 提纲
2023-01-30 10:36:26 0 举报
AI智能生成
后端工程师 提纲
作者其他创作
大纲/内容
消息中间件
kafka
子主题
Rabbit MQ
搜索引擎
ES
Solr
虚拟化 CI/CD
大数据
Hadoop
Spark
概述
Spark和Hadoop的对比
Hadoop的核心就是HDFS和MapReduce,它的MapReduce计算模型延迟过高,所有的计算都必须转化为Map和Reduce两个操作,无法胜任实时,快速的计算需求。每次操作迭代是写入磁盘的,每次写入读取效率很低,IO开销很大。
Spark计算模型也是Map和Reduce,但是还提供了多种数据集操作,更灵活,Spark提供了内存计算,中间结果直接放入到内存中,提高效率。Spark基于DAG的任务调度机制,要优于MapReduce的迭代执行机制。Spark只有在shuffle的时候才会把数据存放在磁盘
其实他们两个没有什么对比性,因为Spark也仅仅是替代了Hadoop中的MapReduce模型。
因为Spark是基于内存的,但是由于内存的限制。可能会由于内存资源不足导致job失败,所以Spark并不能完全取代mapreduce
Spark运行架构
采用标准的主从结构,
基础
核心
RDD
what
RDD是弹性分布式数据集,是Spark中最基础的处理模型。他是一个抽象的数据集合。
可以类比为普通编程语言中的List,Map等集合,但是这些数据集合都是本地集合,都是存储在一个进程中的,无法延展到其他的进程中。RDD也是一个数据集合,但是他是分布式的,他的数据存储是跨越机器存储的,也就是说他是进程并行计算的。
RDD的数据可以存储在内存中也可以存储在磁盘中。
特性
- RDD是有分区的,分区是RDD数据存储的最小单位
2. RDD的方法会作用于所有的分区上
3. RDD之前是具有依赖或者血缘关系的
4. K-V型的RDD可以有分区器
5. EDD的分区规划,会尽量捞金数据所在的服务器
创建
通过本地对象装分布式RDD,默认分区为cpu核数
通过读取文件来创建分布式RDD
算子
转化型算子
单Value类型算子
map
flatMap
filter
distinct
glom
双Value类型算子
union
intersection
mapValue
K-V类型算子
reduceByKey
groupByKey
groupBy
join
行动型算子
countByKey:KV型-统计key出现的次数
count:将数据统一收集到Driver,形成一个list对象
reduce:分区内预聚合,再聚合分区间数据。最后返回。
fold:和reduce作用一样,但是聚合的时候会有一个初始值。初始值会参与分区内的计算,并且还会参加分区间的计算
reduceByKey和groupByKey的区别
reduce有分组和聚合的功能,group只有分组的功能。
reduce性能要比group好,因为reduce在分组之前进行了一次预聚合,那么在shuffle分组节点,被shuffle的数据可以极大的减少。
当数据量越大,优先使用reducebyKey
Flink
Spark和Flink的对比
两个都是基于内存的计算降价,都可以获得较好的计算性能。
底层调优
数据结构与算法
语言基础
Java
基础
异常
Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才
可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
可以被抛出(throw)或者捕获(catch),它是异常处理机制的基本组成类型。
Exception 和 Error 体现了 Java 平台设计者对不同异常情况的分类。
Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。
运行时异常
NullPointerException、
ArrayIndexOutOfBoundsException
ArrayIndexOutOfBoundsException
编译时异常
IO异常
Error 是指在正常情况下,不能被预知的错误,并且不可恢复,常见的比如 OutOfMemoryError 之类,都是 Error 的子类。
try-catch-final
try 用来捕获异常
catch 用来处理异常结果
finally 无论前面有没有处理异常结果,finally语句都会执行,如果try catch中有return 语句,finally中的代码将在return之前执行
finally 里面不要写return 语句,因为当try catch 和 finally里面都有return 语句时,最终执行的是finally块里面的return 语句
finally 代码块一定执行吗? 不一定,当代码里面手动退出jvm。就不会执行
内部类
将一个类的定义放在另外一个类的内部去定义。
在外部类里面,如果创建了一个内部类对象,这个内部类对象是可以访问外部类中的所有成员变量,包括private
接口和抽象类
接口的访问权限默认只能是public,抽象类的访问权限可以是四个
接口中有抽象的方法和用default修饰的默认的方法,抽象类中有抽象的方法,有具体的方法,还可以有静态代码块
接口中不能有构造方法,抽象类有构造方法
借口能多实现,抽象类只能单继承。
访问权限控制
private
只能在本类中访问
默认
只能在本包中访问
protected
同一个包以及子类
public
全局可用
初始化
构造器
new的时候会为对象分配内存,并调用相应的构造器,确保我们在操作对象的时候,对象已经被创建出来了。
即使我们没有写构造方法,系统也会为你加上一个默认的无餐构造方法。
如果我们自己写了构造方法,那么系统一定不会给你加新的构造方法,你必须按照你的构造器来new对象
方法重载
发生在同一个类里面,相同的方法名,不同的参数列表,不同的返回值,不同的方法内容
方法重写
发生在子类和父类里面,相同的方法名字,相同的参数列表,相同的返回值,不同的方法内容。
this关键字
只能在方法内部使用,表示调用方法的那个对象。当我们需要当前对象的引用的时候,我们才使用this。
如果需要在一个构造方法里面调用另外一个构造方法,可以使用this,但是需要吧this放在构造方法的开头。
static
用来修饰变量,表示这是一个类变量
用来修饰方法,表示这是一个静态方法,可以直接使用,不用通过对象来调用
用来修饰代码块,在new 一个对象的时候执行,并且只会执行一次。
final关键字
用来修饰变量,表示变量不可变
用来修饰方法,表示方法不能被重写
用来修饰类,表示类不能被继承
包装类
Integer 有一个范围[-127-127] 只里面是从常量池里面取的,不在这个范围底层会new 一个值出来,所以在这个范围内 == 是相同的,不在这个范围内 == 是不同的
基本数据类型,包装类,String 是引用类型,但是他们是值传递
值传递:指的是在方法调用的时候,传递给方法的形参是一个值,不是引用,这个值呗修改不会影响原来的值
引用传递:值得是方法调用的时候,传递给方法的形参是引用,修改值会影响原来的值。
子主题
范型
ArrayList 中需要传入一个Person对象,如果传入了其他对象就会报错,使用泛型可以增强代码的可读性和稳定性
泛型大概有三种方式
泛型类
泛型接口
泛型方法
反射
反射是在程序运行的过程中,可以动态的胡哦去Java类的属性和方法,并且可以调用方法和修改属性
反射提供了三个类,一个Field,一个Method,一个Constructor ,可以用来操作属性,方法和创建对象
缺点是 有性能开销,因为涉及到动态类型的解析,无视泛型的安全检查机制,
有点是代码更灵活,为各种框架提供了开箱即用提供了便利。
枚举
IO
多线程和并发
多线程
1. 什么是进程,什么是线程,有什么区别?
2. 如果关闭一个线程?
3. 线程的生命周期和状态?以及线程各个状态之前的切换?
4. 什么是上下文切换?
5. 什么是死锁,如果避免死锁?
6. sleep() 方法和 wait() 方法区别和共同点?
7. 调用 start() 方法时会执行 run() 方法,为什么不直接使用 run() 方法?
并发
锁相关
1. synchronized 关键字的了解?原理?
2. 怎么使用 synchronized 关键字
3. JDK1.6 之后的 synchronized 关键字底层做了哪些优化,可以详细介绍一下这些优化吗
4. ReentrantLock 的原理?
5. synchronized 和 ReentrantLock 相同点和不同点?
volatile
1. JMM内存模型
2. 并发编程的三个特性。
3. synchronized 关键字和 volatile 关键字的区别
ThreadLocal
有什么用?
原理
内存泄漏问题
AQS
有什么用?
原理?
线程同步工具类
CountDownLatch
Semaphore
CyclicBarrier
CountDownLatch 和 CyclicBarrier的区别
线程池
线程池原理
8大参数
拒绝策略
如何关闭一个线程池
线程的运行状态
running
shutdown
stop
tidying
terminated
1. 首先执行shutdown方法,此时线程处于shutdown状态,不能提交任务,但是线程池可以处理任务
2. 当线程池的队列中的任务被执行完毕,会自动进入到tidying状态
3. 循环判断当前线程是否处于terminated状态,线程池提供了一个awaitTerminatation方法用来循环判断
线程池关闭方法shutdown方法和shutdownNow方法有什么区别?
1. 前者会处于shutdown状态,后者调用线程池处于stop状态
2.前者不会清空队列,队列任务执行完毕,会清空队列,后者会直接清空队列
3. 前者会中断空闲的线程,后者会中断所有的线程
Atomic类
集合
Java中的集合可以分为两大类,一个是Collection接口,一类是Map接口,两个是并列的关系
Collection
Collection接口实现了Iterable迭代器接口
迭代器接口是绝大多数集合类的最顶层的接口
实现此接口使集合对象可以通过迭代器遍历自身元素
迭代器接口里面需要实现hashNext()方法和next()方法
当然除此之外,我们的绝大多数集合容器都不会去使用迭代器,可以使用for 循环 或者foreach来遍历元素。
List
ArrayList
数组
随机访问速度快 增删效率低 线程不安全
扩容机制
ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去。
默认情况下,新的容量会是原容量的1.5倍
扩容的计算方式:旧的容量右移一位 + 旧容量
ArrayList 的底层是用动态数组来实现的。我们以无参数构造方法创建集合还没有添加元素时,其实它是个空数组,只有当我们添加第一个元素时,内部会调用扩容方法并返回最小容量10
最后会调用Arrays工具类里面的copy方法把原来的数组复制到新数组里面
扩容会有安全问题:
因为扩容的时候有一个i++的操作,在多线程的情况下,多个线程add的时候,会有安全问题
add方法
一般情况下add是往数组尾部追加,这个时候时间复杂度是O1,
如果需要给指定位置添加元素,这个时候时间复杂度是On的,因为指定位置添加元素了,后面的元素就需要相应的向后移动。最差情况下时间复杂度是On
add方法不是线程安全的,因为如果遇到扩容,在多线程环境下有现成安全问题。
add可以添加null值。
remove方法
删除方法的复杂度是On,因为需要把后面数组位置的元素往前移动
get方法
随机访问:时间复杂度是O1
顺序访问:可以通过迭代器的方式实现顺序访问
LinkedList
双向链表
适合插入删除频繁的情况 内部维护了链表的长度
实现了List接口和Queue接口,所以是一个双向的链表
ArrayList和LinkedList区别
适合场景不同:LinkedList在生产中使用较ArrayList少很多,因为一般程序都是读多写少,LinkedList却更适合写多读少的情况。
底层数据结构和复杂度:ArrayList底层实现是数组,随机访问速度快,增删速度慢,LinkedList正好相反。
开销不同:ArrayList创建的时候需要申请一块连续的内存空间,而LinkedList不需要,但是LinkedList需要每一个节点存储额外的两个前后指针
Set
它不允许重复元素出现,这是因为底层是HashMap实现的,HashSet的元素都放在Hash Map的key上面,而value是一个固定的对象,hashMap的key本身就是不能重复的,所以说 HashSet也就不允许重复元素出现了。
HashMap允许存在null,但是只能存在一个null
Queue
Map
HashMap
简介
HashMap是一个线程不安全的类
底层数据结构是 数组 + 链表 + 红黑树的实现,使用红黑树主要是因为链表过长导致的查询效率变慢的问题
链表的查询复杂度是On,红黑树的查询复杂度是lgn
HashMap初始大小是16,并且也特意要求了Map容量一定是2的幂
因为当容量是2的幂时候,n-1的二进制末尾都是11111这样的形式,那么在添加元素的时候进行hash运算的时候,能够充分的分散,是的添加的元素均匀的分布在hashmap的每一个位置上,减少hash碰撞
首先还是为了减少碰撞,尽量让数据均匀,hash的取值是非常大的,大约有40亿的映射空间,但是我们的内存肯定是放不下的
所以需要将hash值和我们数组的长度做取模运算,得到的余数才能作为我们数组的下标
hash % n = (n-1)& hash
2的幂次方,他的低位全部都是0,比如说16的二进制表示是10000,-1之后,二级制表示就是1111 那么 和hash做位运算结果 就 等价于 hash和数组的长度取模运算
如何减少hash冲突?
发生冲突,就去寻找下一个空的散列位置,只要散列表足够大,空的散列地址总能够找到
将hash表的每一个单元作为链表的头节点,所有的hash地址为i的元素构成一个链表,hash冲突的时候就把当前key放在链表的尾部
rehash法,重新计算一个hash地址,直到不存在hash冲突为止
HashMap允许null键和null值,其中null键的hash值是0
为什么不用二叉树,而选择红黑树
二叉查找树在极端的情况下会退化为一个线性结构,也就是变成一个链表了,便利查找会非常慢
而红黑树插入新的数据可能需要通过左旋,右旋变色等操作来保持平衡,引入红黑树就是为了查找数据更快,解决链表过深的问题
如果链表长度很短,根本没有必要引入红黑树,引入反而更慢
put方法
get方法
根据key找到对应的value并返回,如果找不到,返回null
首先计算hash值,根据hash值找到对应的hash桶,如果hash桶的头节点key就是想要的key,就直接返回头节点。
否则判断头节点的类型,如果是树节点,就从红黑树中进行查找,否则就使用普通的链表进行查找,找到返回该节点。
resize方法
扩容方法,当map元素的总数超过Entry数组的0.75的时候,就会触发扩容操作
会创建原来hashmap两倍大小的数组,然后对之前的key进行rehash操作,让原来老的key在新的表上找到位置
扩容之后原来旧的key的下表只有两种情况:原下标位置或原下标+旧数组的大小
如果数据很大,扩容会带来性能的损失,在性能要求很高的地方,这种损失很有可能知名
remove方法
红黑树退化成链表
ConcurrentHashMap
线程安全的HahMap。除了加锁之后原理上并没有太大的区别,hashmap允许键值对有null,但是ConcurrentHashMap不允许
为什么ConcurrentHashMap不允许设置null?
和HashTable对比
使用的是syn,当多个线程同步访问同步方法的时候就会造成阻塞,比如put的时候,一个线程在put,后面的线程put操作就会呗阻塞,也不能get。他是锁了整个数据结构
锁机制分析
1.7 使用分段锁,相当于把一个HashMap 分成多个段,每一段分配一把锁 ReentrantLock,这样支持多线程访问,底层实现采用 数组 + 链表的存储结构
1.8 使用CAS + syn + 红黑树,取消了分段锁,相比使用ReentrantLock,syn锁的粒度降低了,
开源框架
Java
Spring
1. 什么是Spring,如何理解IOC和AOP
2. 什么是IOC,如何实现IOC?
3. Spring的加载流程?IOC的加载流程?
4. Spring怎么去解决循环依赖问题的?
5. Spring对象的生命周期?
6. @Autowired 和 @Resource 的区别是什么?
7. Bean的作用域?
8. Spring中的Bean是线程安全的吗?
9. Spring 框架中用到了哪些设计模式?
10. Spring事务的隔离级别?
11. Spring AOP的执行顺序?
Mybatis
Spring MVC
Spring data JPA
数据库
Mysql
手写sql
学生,成绩,课程表查询
1. 查询各科成绩最高和最低的分, 以如下的形式显示:课程名称,最高分,最低分
SQL优化
步骤
首先查看sql慢日志,一旦某个sql执行时间超过了你设定的时间,就会写入到慢日志里面,这个时间默认是10秒,我们一般会重新设置
expire 查看执行计划,关注几个字段 select_type(SQL执行的类型),possibily key(实际建的索引),keys(实际sql执行走的索引),主要是避免全盘扫描
优化策略
不要在字段开头使用模糊查询,比如 like “%aaa”,一定走全盘扫描,不要吧%放在前面,如果真的要吧%放在前面,就需要考虑数据量,几千条那没事,不用花里胡哨的,可以吧%放前面。但是数据量特别大,使用instr来匹配。
避免使用非等值查询,比如 in <> 这种情况,不会走索引。可以用between
避免使用or,可以使用union all
每一个字段尽可能设置默认值0,不要对null进行判断,不会走索引
where 后面字段的类型需要与db一致,比如db里面是varchar,where后面是int,会有一个隐式转换,有开销。
最左前缀匹配原则
order by 条件要与where中的条件一致
where 条件子句需要建立索引
索引
原理
分类
存储引擎
MongoDB
Neo4j
阿里云OSS
分布式&微服务
分布式
Spring Cloud
第一代
注册中心Eureka
元数据
标准元数据:主机名,ip地址,端口号等信息.
自定义元数据:需要通过metadata-map去定义,这些元数据可以在远程客户端中访问。通过discoveryClient.getInstances可以访问元数据。
所有的元数据都会被保存在server端的注册信息表中,client就可以直接读取。
客户端
服务提供者
服务消费者
服务启动注册
客户端下架服务
服务端(注册中心)
服务启动
接口暴露
服务下线
自我保护
失效剔除
服务续约
负载均衡Ribbon
原理
负载均衡算法
轮询
在10次之内获取,如果获取不到,会返回一个空server
随机
会随机选择一个server,但是如果随机获取的这个server为空,会一直循环获取
重试
在一个时间间隔内 循环重试
最小连接数策略
遍历所有的server,看哪一个的连接数最小用哪个
可用过滤策略
对轮询进行了扩展,再获取到一个server之后,它会去判断是否超时,是否连接数超限,没有才返回成功
区域权衡策略(ZoneAvoidanceRule)
默认策略
源码分析
@LoadBalanced
加了@LoadBalanced注解之后,可将普通的RestTemplate对象使用LoadBalancerClient去处理
@RibbonAutoConfiguration
inceprot()方法
熔断器Hystrix
微服务中的雪崩
舱壁模式
源码分析
配置中心 Config
服务调用 Feign
网关 Gateway
链路追踪 Sleuth
消息中心 Stream
第二代
Nacos
zookeeper
缓存系统
Redis
基础
单线程为什么快?优势?
单线程指的是什么?
多线程的开销
增加吞吐率,但是速率会慢
上下文切换
共享变量的并发访问控制
内存数据库
多路复用机制
阻塞的IO(BIO)
非阻塞的IO(NIO)
高性能,高可靠性
持久化保证数据不丢失
主从,高可靠
哨兵,高可靠
如何避免数据丢失?持久化?
AOF
将所有的写命令以日志的形式追加记录下来,恢复的时候将文件中的命令读出来。
AOF日志会变得巨大,所以Redis提供了日志重整的机制,通过读取内存中的数据重新产生一份数据写入日志
优点:AOF是写后日志,这样带来的好处是,记录的所有操作命令都是正确的,不需要额外的语法检查,确保redis重启时能够正确的读取回复数据
优点:aof是追加的方式写,没有随机磁盘io的开销。
缺点:写后日志,先执行命令,后写日志 。可能会数据丢失
缺点:不适合数据量特别大的场景。因为aof恢复数据会很慢。
RDB
bgsave:fork()一个子进程,父子进程会共享内存,共享的这块内存标记为只读的状态,子进程专门用于写入 RDB 文件,避免了主线程的阻塞
但是对于父进程来说,是有可能收到写命令的,此时会将原来内存对应的数据页复制出来,然后对这个副本进行修改。
优点:适合大规模数据备份的恢复过程。
缺点:每隔一段时间进行备份,所以一旦最后一次持久化时候redis挂了,数据就丢失了
缺点:fork()子进程会占用一定的内存空间,所以需要控制RDB快照的频率,不能让他频繁的执行。
AOF和RDB混合持久化
内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。
主从库如何实现数据一致?
1. 读写分离:读操作:主库、从库都可以接收 写操作:首先主库执行,然后,主库将写操作同步给从库
原理:
1. 首先,启动的实例可以通过replicaof完成主从库的确立。
第一阶段:从给主发psync,runid=?,offset=-1. 主给从发 fullrepsync,runid 以及 offset
第二阶段:从库收到,会进行全量复制,依赖于内存快照生成的 RDB 文件。主执行 bgsave 生成 RDB 文件发给从。从先清空当前数据库,然后加载 RDB
第三阶段:由于在执行第二阶段的时候,主库可能会有写操作,主会把 replication buffer里面的修改操作发给从
同步之后,会保持长链接
有什么问题?
问题:生成RDB,传输RDB是耗时操作,如果我们的从库过多,主需要给所有的从进行同步,那么会影响主线程进行其他操作。这个问题怎么解决
解决方案:我们可以通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。
问题:同步完成之后,会保持长链接,如果网络断了呢(从库挂了)
解决方案:主从库断连后,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区。
解决方案:repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。
解决方案:网络没有断,repl_backlog_buffer 中主库和从库的位置是一样的,网络断了,主库就要把新的修改操作写入到这个buffer里面。这样主库和从库的位置就会拉大。但是这是一个环形缓冲区,可能会把主库过了一圈把从库覆盖了。一般这个环形缓冲区大小需要根据业务场景设置,这个就需要有经验的人来进行了。
解决方案:网络建立链接之后,就会吧repl_backlog_buffer里面这块差距补上,也就是说只同步这块偏移量。
哨兵模式,主库挂了,如何选举?
简单介绍
哨兵是特殊的 Redis 进程,主从实例运行,它也在运行。哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知
监控:判断主从库下线
选主:
通知:从库执行 replicaof 命令,与新主库同步。通知客户端,与新主库建立链接
监控,主库真的挂了吗?
如果哨兵没有收到从库的回复,就直接标记为下线,因为从库断开不影响整体,但是如果没有收到主库的回复,不能直接标记为下线,还要进一步判断是否是真的下线了。
通常情况下,哨兵也是一个集群,会通过哨兵集群一起来判断主库是否挂了,当集群里面有半数以上的哨兵都发现主库挂了,才会将主库标记为下线
选择哪个从库实例作为主库?
SETNX 命令,它用于设置键值对的值,这个命令在执行时会判断键值对是否存在,如果不存在,就设置键值对的值,如果存在,就不做任何设置。解锁的时候直接DEL删除即可。
从 从库里面选择新主库,是哨兵筛选的一个过程,首先从库必须在线,并且从库的复制速度需要最快。才会被选举为主库
哨兵集群中有实例挂了,怎么办,会影响主库状态判断和选主吗
存在故障节点时,只要集群中大多数节点状态正常,集群依旧可以对外提供服务。
哨兵是怎么样组成一个集群的?
哨兵A只要与主库建立了链接,就可以在主库上发布信息了,比如发布自己的ip和端口号,当其他的哨兵B与主库建立链接后,就会收到该哨兵A的信息,同时,该哨兵A也会收到其他哨兵B的信息
哨兵是怎么样和主从库之间建立关系的?
当哨兵和主库建立链接之后,同时根据发布订阅模式,烧饼之间也会建立关系,之后,哨兵会给主库发送INFO命令,主库接受到这个命令后,就会把从库列表返回给哨兵。接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控。哨兵 1 和 3 可以通过相同的方法和从库建立连接。
基于哨兵自身的 pub/sub 功能,实现了客户端和哨兵之间的事件通知。
Redis实现分布式锁
锁可以用一个变量来表示,当加锁的时候,把这个变量从0变成1,当其他的线程过来的时候,发现这个变量是1 ,加锁失败。所以,单纯的分析加锁的过程有3个操作,读取变量的值,判断变量的值,修改变量的值。要实现加锁,需要把这三部操作变成原子的。
SETNX也有一个问题,如果在SETNX操作变量的时候,在具体的业务操作出现了错误,导致DEL命令无法被执行,那么这个线程就会一直持有这把锁。所以我们一般会给这个key设置一个过期时间。如果没有del就自动过期。但我们需要对这个业务的执行时间有一定的预估,因为有可能当前线程没有执行完锁就过期了。
同时我们可以使用lua脚本,吧所有的命令写到lua脚本里面,lua脚本是原子操作。
Redis实现事物
Redis 提供了 MULTI、EXEC 两个命令实现事物
但是所有的命令不会直接执行,而是放在了一个队列中,当执行exec命令的时候,才会只想命令。
Redis通过watch来保证隔离性,WATCH 机制的作用是,在事务执行前,监控一个或多个键的值变化情况,当事务调用 EXEC 命令执行时,WATCH 机制会先检查监控的键是否被其它客户端修改了。如果修改了,就放弃事务执行,避免事务的隔离性被破坏。然后,客户端可以再次执行事务,此时,如果没有并发修改事务数据的操作了,事务就能正常执行,隔离性也得到了保证
删除策略
定期删除
惰性删除
一个key过期之后,不会立即删除,当再有一个请求到来的时候,会判断key有没有过期,如果过期了就删除。
缓存淘汰策略
在设置了过期时间的key进行淘汰
volatile-random
在设置了过期时间的键值对中,进行随机删除
volatile-tll
根据过期时间的先后进行删除,越早过期的越先被删除。
volatile-lru
会使用 LRU(最近最少使用的原则) 算法筛选设置了过期时间的键值对
volatile-lfu
会使用 LFU 算法选择设置了过期时间的键值对
在所有的key上面进行淘汰
allkeys-random
从所有键值对中随机选择并删除数据
allkeys-lru
使用 LRU(最近最少使用的原则) 算法在所有数据中进行筛选
allkeys-lfu
使用 LFU 算法在所有数据中进行筛选
不进行淘汰,直到内存满了
redis不会提供服务,直接报错
兜底策略。如果删除了,必须把这个数据回写到数据库,这样保证数据不会丢失。
类型
String
64位Long类型整数
直接存储数字,此时是int编码
字符串小于44字节
SDS,由RedisObject中ptr直接连着SDS,此时是embstr编码
字符串大于44字节
SDS,由RedisObject中的ptr指针指向SDS
Set
Hash
字典
hash表
hash表数组table
table1
链表
table2
链表
table3
链表
table4
链表
hash表的大小size
hash表数组的容量初始大小为4,数据大了就需要扩容,新的容量是现在的一倍,即4,8,16,32
key的索引值 = hash算法值和hash表容量取余
解决hash冲突:采用的便是链地址法,通过next这个指针可以将多个哈希值相同的键值对连接在一起,用来解决哈希冲突。
扩容:rehash ,把hash表1上面的数据慢慢迁移到hash表2上面,因为redis毕竟是一个内存数据库,如果数据量特别大,会导致明显的内存卡顿,所以进行rehash,在进行渐进式rehash期间,字典的删除查找更新等操作可能会在两个哈希表上进行,第一个哈希表没有找到,就会去第二个哈希表上进行查找。但是进行 增加操作,一定是在新的哈希表上进行的。
压缩列表
2.哈希对象保存的键值对数量小于 512 个;
压缩列表是连续内存块组成的顺序数据结构
entry节点是相连的。
Zset
跳跃表
将一个有序链表分层,从下层抽出特定的数据组成上层。每一层都是一个有序链表
查找的时候,就相当于是一个树的查找过程,时间复杂度是log(n)
List
GEO
Huyloglog
基数统计,也就是说他本身会对数据做一个去重,他一般用来统计一个集合中不重复的个数。
占用内存是固定的,但是他是有一定的误差存在。
应用场景为统计网站的UV
Bitmap
setbit key offset value
可以给每个offset位置设置0或者1
RedisObject
4位type,表示类型
4位encoding 表示内部编码
24位LRU 记录对象最后一次被访问的时间
refcount 记录对象被引用的次数
ptr 指向底层的具体数据的指针
实战
影响Redis性能的问题有哪些?有哪些阻塞点?
bigkey 删除操作
删除操作的本质是要释放键值对占用的内存空间,在应用程序释放内存时
操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。
这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序
删除操作是惰性删除,这个操作会被封装成一个任务,放到队列里面,当子线程执行到当前队列的时候,删除操作才会被执行
UNLINK 命令,删除大key
SCAN 命令读取数据,然后再进行删除。因为用 SCAN 命令可以每次只读取一部分数据并进行删除
清空数据库
可以在 FLUSHDB 和 FLUSHALL 命令后加上 ASYNC 选项,这样就可以让后台子线程异步地清空数据库
主库生成、传输 RDB 文件,从库接收 RDB 文件、加载 RDB 文件;
全量查询和聚合操作,交并差运算
数据库和缓存 数据一致性问题
本质就是要保证缓存中的数据和我数据库中的数据一致。
不管是先更新缓存或者是先更新数据库,都会导致数据不一致的问题。因为这两个操作需要合在一起执行,不是原子的,所以如果有一个更新失败,都有可能从另外一个里面读到旧值
方案一:加锁,加锁是能够保证原子性,但是性能不高。
方案二:重试机制。具体一点,就是把要待更新的数据放在消息队列中,比如说kafka中,当cache 和 db任何一个更新失败的时候,都能够从消息队列中重新读取到这些值。然后再次执行,进行重试,如果重试超过一定的次数,我们其实就可以直接向业务层发送报错信息了。如果能够更新成功,就把这个值从消息队列中移除,以免重复。
用户登陆生成token信息
1. 用户登陆,前端调用后端接口,用户信息正确,生成token令牌,存放到redis中,key为token,value为user对象,同时设置过期时间。把token令牌相应给前端
2. 获取用户信息,前端把token放在header里面,后端getHeader("token")获取到token,redis里面get token 返回后端
3. 拦截器操作,如果设置了拦截器,拦截器里面获取到了token信息,token信息存在,说明我们这个时候需要操作token信息了,同时重置token的时间。
4. 退出功能,直接删除token信息,退出之后,前端会跳转到首页,同时清除localstrage里面的token信息。
4. 前端操作:前端拿到我的token信息之后,存在js的localStorage对象里面,当前端给后端发送请求的时候,通过ajax把token信息放在header里面就可以了
缓存雪崩
缓存中有大量的数据同时过期,大量的请求到达db,我们一般需要避免大量的数据设置相同的过期时间,我们可以给这些数据一个较小的随机数。
或者通过一种服务降级的方式,直接返回一些与设定的信息
还有一种情况就是我们直接在系统的请求入口的前端控制每秒进入系统的请求数 ,可以通过分页的方式,避免过多的请求被同时发送。
缓存击穿
某一个热点数据的请求过期,无法在缓存中进行处理,请求到达db。这种情况下,我们可以直接设置某一个频繁访问的热点数据不过期。
缓存穿透
恶意攻击,专门访问我缓存和数据库中都没有的数据。第一种方案,缓存空值。第二种方案,就是在业务请求和缓存层之间加一层布隆过滤器,因为布隆过滤器能够快速的判断key是否存在,避免从请求到达缓存或者数据库
Reids分片集群
Codis
Codis架构
Codis Proxy负责连接客户端请求,然后吧请求转发到特定的Codis server。
Codis server有很多,数据都是分片的存放在codis server上面的
Codis server处理完成之后吧数据给Codis Proxy。由Codis fe提供web界面管理和操作集群,由zookeeper保存元数据。
数据是怎么在多个实例上分布的?
Codis 集群一共有 1024 个 Slot,编号依次是 0 到 1023
我们可以手动分配slot给server,也可以通过在codis dashbaord的web界面上自动分配
当我们读写一个数据的时候,key会根据一个hash算法 通过与1024去余,确定这个key存在于哪一个slot,从而确定在哪一个server上面
key和 Codis server这样就形成了一个 对应关系,这个映射关系会缓存到Coids proxy本地,同时也会存在zookeeper中。
如果数据发生了更新,codis dashbaord就会吧新的映射表发给Codis Proxy,同时更新zookeeper。
集群扩容和数据迁移如何进行?
增加Codis Proxy
直接增加Proxy,然后通过Codis dasdboard吧proxy加入到集群,
此时,codis proxy 的访问连接信息都会保存在 Zookeeper 上
增加Codis Server
增加server意味着需要吧源server的数据迁移到目的server上面
首先把源server上面的key发送给目的server,目的server收到,给源server一个确认,源server删除key。重复这样,直到源server数据迁移完成。
迁移的过程中这个key会被标记为只读的状态,防止我在迁移过程中数据发生了改变。
同步迁移
异步迁移
怎么保证集群可靠性?
Codis server 其实就是一个Redis实例,所以主从,哨兵都是可以在Codis server上面应用的。从而保证了 server 的可靠性
Zookeeper 集群使用多个实例来保存数据,只要有超过半数的 Zookeeper 实例可以正常工作, Zookeeper 集群就可以提供服务,也可以保证这些数据的可靠性。
Redis Cluster
redis Cluster 架构
数据是如何存储在多个实例上的?
Cluster 也有16384个slot,首先需要吧这些slot映射到不同的实例上面。
每一个key会根据hash算法和16384取余,这样就确定了key在哪一个slot上面,从而去定在哪一个server上面
客户端如何定位数据?
客户端通过重定向机制来定位。
Redis Clueter实例之间是互相连接的,他们之间是会进行slot映射关系的分享的。
Guava Cache
操作系统
进程和线程
1. 进程和线程的区别?
进程是资源分配的基本单位,PCB描述了进程的基本信息和状态,所谓的创建进程撤销进程都是对PCB的操作。
线程是CPU调度的基本单位,进程里面包含线程,线程共享进程资源。
2. 进程的状态以及之间的转换?
状态:创建,就绪,运行,阻塞,销毁。
转换:只有就绪和运行状态可以相互转换,就绪状态的进程通过进程调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。
转换:运行 To 阻塞 IO事件
转换:阻塞 To 运行 IO时间完成
3. 进程调度算法?
先来先服务
按照请求的顺序进行调度。
有利于长作业,不利于短作业。短作业必须等到他前面的长作业执行完
非抢占式的调度算法
短作业优先
长作业有可能会饿死,处于一直等待短作业执行完毕的状态
如果一直有短作业到来,那么长作业永远得不到调度。
非抢占式的调度算法
最短剩余时间优先
时间片轮转
将所有就绪进程按 先来先服务 的原则排成一个队列,给队列里面的进程分配时间片,时间片完成,执行下一个。
优先级调度
给进程分配优先级,按照优先级调度。
多级反馈
时间片轮转和优先级调度结合
4. 进程同步的方式?
临界区
每个进程在进入临界区之前,需要先进行检查
同步与互斥
同步:多个进程因为合作产生的直接制约关系,使得进程有一定的先后执行关系。
互斥:多个进程在同一时刻只有一个进程能进入临界区。
信号量
管程
5. 进程通信的方式?
管道
管道是一种半双工的通信方式,数据只能单向流动,而且只能在父子进程间使用
管道是通过调用 pipe 函数创建的,fd[0] 用于读,fd[1] 用于写。
命名管道
命名管道,去除了管道只能在父子进程中使用的限制
队列
消息队列独立于发送与接收进程。进程终止时,消息队列及其内容并不会被删除。
读进程可以根据消息类型有选择地接收消息
信号量
它是一个计数器,用于为多个进程提供对共享数据对象的访问
共享存储
允许多个进程共享一个给定的存储区
因为多个进程可以同时操作,所以需要进行同步。
信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问
套接字
用于不同机器间的进程通信。
用户态
当进程执行用户代码的时候,就处于用户态
当进程执行系统调用的时候,就处于内核态。
转换
a.用户态进程主动调用系统函数。
b.进程运行在用户态的时候,出现了不可知的异常,那么就需要内核相关的程序去处理异常,就会进入内核态。
c.IO中断,如果IO中断之后,会去执行与中断信号对应的处理程序
死锁
1. 死锁产生的四个必要条件?
互斥
一个资源只能分配给一个进程,另外一个进程过来了,只能阻塞
循环等待
两个或者两个以上的进程组成一条环路阻塞。
不可剥夺
一个进程里面的资源不能被抢占,只能由占有该资源的进程显示的释放
占有等待
一个进程占有资源,他可以在不释放的情况下,再次占有其他的资源
2. 死锁的预防?
破坏4个条件,但是破坏不可剥夺条件是不能实现的,因为只能由占有该资源的进程显示的释放资源。
3. 死锁的避免?
银行家算法,一个银行家承诺给多个用户贷款,每个用户都有一定的期望,银行家算法就是判断,我把钱给某一个用户,肯剩余的钱能否满足下一个用户。
内存管理
1. 虚拟内存的作用?
平时我们使用win电脑,很有可能出现软件的使用内存超过了本身的物理内存,但是仍然是可以的,多出来的这部分其实就是虚拟内存
虚拟内存它使得每一个应用进程认为它拥有了一块连续完整的地址空间
而实际上,内存是被分为多个物理碎片,还有一部分存储在外部的磁盘上面,我需要的时候我就会进行数据交换。
也就是说在我需要的时候,我的磁盘会匀出一部分空间当作内存使用,来保证内存可用。
win电脑我们可以手动的去设置自己电脑的虚拟内存的大小,主要的思想就是把内存扩展到硬盘空间。
2. 页面置换算法?
最佳置换算法(理想,不可能实现)
从主存中移出永远不再需要的页面;如无这样的页面存在,则选择最长时间不需要访问的页面
先进先出置换算法
当需要淘汰一个页面时,先进入主存的页面先淘汰。
最近最久未使用
淘汰过去一段时间里不曾被访问过的页面
时钟(CLOCK)置换算法
3. 内存管理的方式
分段
进程的地址空间,按照程序的自身逻辑划分为若干个段,每段从0开始
内存以段为单位,每个段在内存中占据连续的空间,各个段之间可以不相邻
优点:能够实现信息的共享和保护
缺点:段的长度由用户定义,如果过长,为其分配很大的连续空间,占用内存。
缺点:分段会产生内存碎片。
分页
内存空间划分为大小相等的页,进程也会分成和内存页面大小相等的页面
进程的页面和内存的页面有一一对应的关系,他们可以离散的进行分布。
优点:内存利用率高,不会产生外部碎片,只会有少量的页内碎片
缺点:不能按照逻辑实现信息的共享和保护。
段页
进程按照逻辑分段,再将各个段划分为大小相等的页面
内存划分为大小相同的页(内存快),系统以块为单位为进程分配内存。
文件管理
Select,poll,epoll的区别
首先select,poll,epoll都是IO多路复用的三种机制。
如果没有这3个函数,传统的操作,用程序来控制,在linux系统中,每一个网络链接都是一个文件,都是以文件描述符的形式存在。我们需要手动程序判断文件描述符是否为空,不为空读取,这个效率是比较慢的。
select
操作系统有用户态和内核态,对于select函数,他会把所有的文件描述符拷贝到内核态,由操作系统内核去判断是否有数据。如果有数据,就将FD置位,select函数返回。之后再次遍历整个文件描述符,判断哪一个FD是置位的,如果置位,说明有数据,就读取,做一些处理。
缺点:每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,那这个开销也很大
缺点:fd_set是一个bitmap,大小只有1024,大小有上线
缺点:每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很大时,那这个开销也很大
缺点:select返回的fd_set,程序不知道哪一位有数据,需要我们再次遍历一次,判断哪一个置位。
poll
同样是吧所有的文件描述符拷贝到内核态,与select不同的是,select使用的是数组存储fd,poll使用的是结构体pollfd存储。
优点:长度更大,select只能是1024,poll可以很多
优点:pollfd 可以重用
缺点:仍然需要拷贝,poll结果返回仍然需要遍历所有才知道哪一位有数据。
epoll
首先创建epfd,存放fd_events,用户态和内核态共享这块区域,不会进行拷贝,内核态去判断哪一个文件描述符有数据,吧有数据的fd放到队列的首部,count++。epoll返回之后,就知道有前面count位有数据,直接读取,不会遍历
优点:用户态内核态共享,没有拷贝的开销
优点:吧有数据的fd放到队列,不需要遍历,直接读取前count位即可。
Linux命令
查看日志:tail -f
查看端口占用情况:lsof -i:8080
lsof: 列出当前系统打开的文件
打包文件: tar -zcvf xxx.tar.gz xxx
zip打包文件:zip -r xxx.zip xxx
解压文件: tar -xvf xxx.tar.gz
字符串匹配:grep
查找 find
fis3 工具
查看系统资源使用情况 top
查看所有进程相关信息 ps ux
显示CPU info的信息 cat /proc/cpuinfo
计算机网络
1. 网络分层协议,从下到上?
物理层:数据传输单位是比特,作用是尽可能的屏蔽掉传输介质和物理设备之间的差异。
数据链路层:数据传输单位是帧,作用是吧网路层交下来的IP数据包封装成帧。
网络层:数据传输单位是分组,作用是找到一个合适的路径,实现主机与目的主机之间的通信。IP
传输层:作用是负责两个主机之间进程之间的通信。协议TCP,UDP。
应用层:作用是为应用进程提供数据传输服务。协议HTTP,DNS,SMTP。
2. TCP三次握手?为什么需要三次握手,两次行不行?
客户端A —— 发送带有SYN的数据包 ——服务端B
服务端B —— 发送带有SYN/ACK的数据包 ——客户端A
客户端A ——发送带有ACK的数据包 ——服务端B
防止已失效的请求报文又传送到了服务端,第一次握手的数据包可能超时,对于客户端来说这是一个失效的,但是对于服务端来说,他不知道,他也会接受,第二次握手的时候,客户端就不会确认,造成浪费。
三次握手是TCP建立链接最少需要的次数,当然四次也行,五次也行,但是浪费资源。但是如果三次握手都不能保证是否建立,四次也不行。
如果只是两次握手, 至多只 有连接发起方的起始序列号能被确认, 另一方选择的序列号则得不到确认
3. TCP四次挥手?为什么需要四次挥手,三次行不行?
首先需要关闭客户端——服务端的链接。这一步需要2次挥手
但是服务的也有可能还有数据要发送给客户端,于是就会等服务端吧数据发送完毕之后。
关闭服务端——客户端的链接。
第四次挥手的数据包可能丢失,导致服务端最终没有收到确认。
第四次挥手的时候,需要等待客户端2MSL时间,确保服务端收到了确认。
4. TCP 和 UDP协议的区别?
TCP面向连接的,以字节流为传输服务,可靠的数据传输服务,效率慢
UDP是面向无连接的,以报文段为单位传输,尽最大努力传输服务,效率快
5. TCP 如何保证可靠性?
TCP的字节流被分割为TCP认为最适合发送的数据块。
TCP会把字节流转换为分组,形成包,并且会对包进行编号,接收方对包进行排序,把有序的数据传输给应用层。
TCP维持了一个强制的校验和,如果收到的数据包的校验和和我TCP包首部不一致,TCP会丢弃这个数据报。
TCP会丢弃重复的数据。
TCP通过滑动窗口实现流量控制。当接收方来不及处理发送方的数据,能够提示发送方降低速率。
TCP通过拥塞控制,实现网络拥塞时,减少数据的发送。
ARQ协议,又叫做停等协议,每发送完一个分组就停止发送,等待接收方的确认。
超时重穿,TCP发送一个数据报后,会启动一个计时器,如果在一定的时间内没有收到确认,重发这个报文段。
6. 滑动窗口和流量控制?
在早期的网络中,通信双方不考虑网络的拥挤状况就直接发送数据,可能会导致阻塞调包,谁也发送不了数据。
在通信的过程中,发送方窗口的大小取决于接收窗口和拥塞窗口的最小值。
7. 拥塞控制四个算法?
拥塞控制:为了防止过多的数据注入到网络中
慢开始,就是刚开始发送数据,拥塞窗口值慢慢从一开始增大,每经过一个轮次,一个往返
时延 RTT,按指数增长
时延 RTT,按指数增长
拥塞避免,当拥塞窗口值到达满开始门限,开始按"加法增大",每经过一个轮次,窗口值+1,
当发生网络拥塞,窗口值降为 1,继续慢开始算法,此时慢开始的门限值变为拥塞窗口的一半
当发生网络拥塞,窗口值降为 1,继续慢开始算法,此时慢开始的门限值变为拥塞窗口的一半
快重传和快恢复,就是当收到 3 个重复的确认段,就开始执行快重传算法,直接将窗口值降
为门限值.执行拥塞避免
为门限值.执行拥塞避免
8. 从浏览器输入URL到显示网页的过程?
DNS解析
TCP三次握手
发送HTTP请求
响应HTTP
渲染页面
TCP四次挥手
9. HTTP 状态码
200 成功
301 永久重定向
302 暂时重定向
400 坏的请求 服务端识别不了
401 身份没有认证
403 服务端识别了,但是拒绝了
404 找不到资源
500 服务端内部错误
502 网关错误
10. HTTP 1.0和HTTP 1.1的主要区别是什么?
1.0
浏览器每次请求都需要与服务器建立一个TCP连接,处理完请求之后断开链接。
1.1
默认使用Connection:keep-alive(长连接),避免了连接建立和释放的开销。
通过Content-Length字段来判断当前请求的数据是否已经全部接受。
不允许同时存在两个并行的响应。
2.0
2.0使用二进制格式
header压缩
11. HTTP 和 HTTPS 的区别?
HTTP端口号80,HTTPS是443
HTTP是明文传输,安全性较差,HTTPS是加密传输。
HTTP响应速度快,HTTPS消耗资源多,需要用到CA证书
12. Cookie和Session的区别?
因为 HTTP 是无状态的,在同一个连接中,两个执行成功的请求之间是没有关系的,引入
Cookie 和 Session 就可以解决这些问题
Cookie 和 Session 就可以解决这些问题
Cookie 是客户端地解决方案,
当用户访问支持 Cookie 的网页时,用户会提供包括用户名和密码等个人信息,并且提交
至服务器,当服务器向客户端返回的同时,也会发回这些个人信息,这些信息存放在 HTTP
响应头,当浏览器接收到响应时,浏览器会将这些信息存放在固定的位置,当再一次发送
这些请求的时候,就会把相应的 Cookie 发送给服务器,这些信息存放在 HTTP 请求头里
面,服务器收到后,会解析 Cookie 生成这些特殊信息.
当用户访问支持 Cookie 的网页时,用户会提供包括用户名和密码等个人信息,并且提交
至服务器,当服务器向客户端返回的同时,也会发回这些个人信息,这些信息存放在 HTTP
响应头,当浏览器接收到响应时,浏览器会将这些信息存放在固定的位置,当再一次发送
这些请求的时候,就会把相应的 Cookie 发送给服务器,这些信息存放在 HTTP 请求头里
面,服务器收到后,会解析 Cookie 生成这些特殊信息.
Session 是一种服务器端的机制,服务器使用了一种类似散列表的结构来保存信息,当浏
览器发送一个请求给服务器,服务器创建一个 session 对象,当服务器发送响应给浏览器,
会把 sessionid 存在 cookie 里面,由 cookie 发送给浏览器,当下一次访问服务器的时候,
会把 cookie 发给服务器,里面携带了 sessionid,服务器根据 sessionid 找到对应的
session
览器发送一个请求给服务器,服务器创建一个 session 对象,当服务器发送响应给浏览器,
会把 sessionid 存在 cookie 里面,由 cookie 发送给浏览器,当下一次访问服务器的时候,
会把 cookie 发给服务器,里面携带了 sessionid,服务器根据 sessionid 找到对应的
session
13. GET 和 POST的区别?
get请求数据存放在url中,post请求数据存放在body里面
相比较post请求来说,get请求安全性较差。
GET 方法是幂等的,同样的请求被指向一次与连续执行多次的效果是一样的,但是 POST
方法不是,每次请求 POST 会对服务器资源进行改变
方法不是,每次请求 POST 会对服务器资源进行改变
get请求有长度限制。
14.ICMP的应用?
ping命令
15. 转发和重定向?
转发是请求转发,他是request里面的方法,重定向是相应重定向,他是response里面的方法
请求转发url会改变,响应重定向url不会改变
转发是两次请求,不能用request共享数据。重定向是一次请求,可以用request共享数据。
16. HTTPS连接建立的过程?
1. 首先客户端给服务的发送一个请求
2.服务端接受请求,发送一个SSL证书给客户端,SSL证书里面包括了有效期、所有者、签名以及公钥
3.客户端验证证书的合法性,同时客户端生成一个对称密匙,使用公匙进行加密,发送到服务端
4. 服务端使用私匙进行解密,得到对称密匙,使用对称密匙加密数据发送给客户端。
5. 随后客户端和服务端就使用对称密钥进行信息传输
自由主题
0 条评论
下一页