知识点
一、Linux
1.1 常用命令
1.1.1 lsof(list open files)
-
一个列出当前系统打开文件的工具。
-
常用的用法:
lsof -i # 列出所有网络连接 lsof -i:80 # 查看使用80端口的文件 lsof -i:udp # 查看所有udp网络连接信息 lsof -i:tcp # 查看所有tcp网络连接信息 lsof -i udp:55 # 查看谁在使用某个特定的udp端口 lsof -i tcp:3306 # 查看设在使用某个特定的tcp端口 lsof -a -u test -i # 列出test用户所有活跃的网络进程 lsof -p pid # 列出pid对应的文件信息
1.1.2 netstat
-
一个监控TCP/IP网络的工具,它可以显示路由表、实际的网络连接以及每一个网络接口设备的状态信息。
-
常用参数
-a(all) # 显示所有选项,默认不显示LISTEN相关 -t(tcp) # 显示tcp相关选项 -u(udp) # 显示udp相关选项 -n # 拒绝显示别名,能显示数字的全部转化成数字 -l # 列出有在listen的服务状态 -p # 显示进程及进程id
-
常用命令:
netstat -a # 列出所有端口 netstat -au # 列出所有udp端口 netstat -at # 列出所有tcp端口 netstat -l # 列出监听端口 netstat -alput # 列出所有的tcp、udp信息,并显示pid # netstat 一般搭配grep命令使用
1.1.3 top
-
用来监控Linux的系统状况,是常用的性能分析工具,能够实时的显示各个进程的资源占用情况
-
信息说明
-
第一行:
内容 含义 18:19:08 表示当前时间 up 39 min 系统运行时间,格式为h:m 1 users 当前登录用户数 load average:0.08, 0.06,0.14 系统负载,三个数值分别是1分钟,5分钟,15分钟前到现在的平均值。如果这个数除以逻辑cpu的数量结果高于5的时候就表明系统在超负荷运转了。 -
第二行:
内容 含义 338 total 进程总数 1 running 正在运行的进程数 260 sleep 睡眠的进程数 0 stoped 停止的进程数 0 zombie 僵尸进程数 -
第三行
内容 含义 0.4 us 用户空间占用CPU百分比 0.3 sy 内核空间占用CPU百分比 0.0 ni 用户进程空间内改变过优先级的进程占用CPU百分比 99.3 id 空闲CPU百分比 0.0 wa 等待输入输出的CPU时间百分比 0.0 hi 硬中断占用CPU的百分比 0.0 si 软中断占用CPU的百分比 0.0 st -
第四行
内容 含义 xxx total 物理内存总量 xxx used 使用的物理内存总量 xxx free 空闲内存总量 xxx buff/cache 用作内核缓存的内存量 -
第五行
内容 含义 xxx total 使用的交换区总量 xxx used 空闲交换区总量 xxx free 缓冲的交换区总量 xxx avail Mem 代表可用于进程下一次分配的物理内存数量
1.1.4 grep
1.1.5 sed
1.1.6 awk
1.1.7 GDB
1.2 多线程下的锁
1.2.1 互斥锁
同一时刻只有一个线程能持有该锁,其余线程等待持有锁的线程释放锁之后才能获得该锁,等待锁的线程进入阻塞状态。
1.2.2 读/写锁
-
读锁
读锁之间是共享的,一个线程加读锁之后,其余线程可以加读锁,但是不能加写锁,读锁和写锁互斥
-
写锁
写锁就相当于一个互斥锁,加上了写锁之后不能再加任何锁。
-
适用的场景:
适用于读多写少的场景
1.2.3 自旋锁
自旋锁是一种特殊的锁,当资源被加锁之后,该线程不是被阻塞而是陷入循环,一直检查锁是否释放。释放了就获取该锁。
-
优缺点:
优点是减少了线程从睡眠到唤醒的消耗,缺点是一直占用CPU资源
-
使用场景:
适用于资源被锁住的时间短,而又不希望在线程的唤醒上浪费资源的情况。
1.2.4 乐观锁
顾名思义,就是很乐观,每次获取数据时都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有无更新这个数据。可使用版本号等机制。乐观锁适用与读多的应用场景,可提高系统的吞吞吐率。
1.2.5 悲观锁
顾名思义,就是很悲观,每次获取数据的时候都认为别人会修改,所以每次拿到数据都会加锁,这样别人想要获取这个数据就会阻塞知道有锁可拿。传统关系型数据库里边就用到了很多这种锁机制。他指的是对数据被外界修改时持保守态度,因此在整个数据处理过程中,将数据置于锁定状态,往往依靠数据库提供的锁机制。
1.3 进程和线程
1.3.1 进程间通信的方式:
-
管道
-
有名管道
在磁盘上会存储一个管道文件标识(inode),但是不会占据磁盘空间,数据不会存储到磁盘上
- 使用mkfifo创建管道,产生的命名管道在文件系统中存在
- 可以在没有亲缘关系的进程之间通信
-
无名管道
利用文件描述符作为读写管道
- 使用pipe创建管道,一个文件描述符数组fds,fds[0]为读端,fds[1]为写端
- 只能用于父子进程之间的通信
-
-
信号
-
信号量
-
消息队列
-
共享内存
- 通信效率最高,因为不涉及进程间任何数据传输
-
套接字
1.3.2 线程间通信的方式
- 互斥量
- 条件变量
- 信号量
1.3.3 僵尸进程和孤儿进程
1.3.3.1 僵尸进程
对于多进程程序而言,父进程一般需要跟踪子进程的退出状态。因此,当子进程结束运行时,内核不会立即释放该进程的进程表表项,以满足父进程后续对该子进程退出信息的查询(如果父进程还在运行)
-
子进程进入僵尸态的两种情况
- 在子进程结束运行之后,父进程读取其退出状态之前,我们称该子进程处于僵尸态。
- 父进程结束或者异常终止,而子进程继续运行。此时子进程的PPID将会被设置为1,即init进程。init进程接管了该子进程,并等待它结束。在父进程退出之后,在子进程退出之前,该子进程处于僵尸态。
-
避免/杀死僵尸进程
-
避免僵尸进程
-
使用下面这对函数在父进程中调用,以等待子进程的结束,并获取子进程的返回信息,从而避免了僵尸进程的产生,或者使子进程的僵尸态立即结束。
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int * stat_loc); pid_t waitpid(pid_t pid,int* stat_loc,int options);
-
wait
函数将阻塞进程,直到该进程的某个子进程结束运行为止。它返回结束运行的子进程的PID,并将该子进程的退出状态信息存储于stat_loc参数指向的内存信息中 -
waitpid
只等待由pid参数指定的子进程。如果pid=-1,那么它就和wait函数相同。 -
这两个函数一般配合一个信号
SIGCHLD
使用。当一个进程结束时,他将给其父进程发送一个SIGCHLD
信号。当信号触发时,设置一个回调函数,用于调用wait
和waitpid
函数接收子进程的返回信息。
-
-
杀死僵尸进程
- 杀死其父进程(kill,但kill杀不了僵尸进程)
- 重启系统
-
1.3.3.2 孤儿进程
一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程就会成为孤儿进程。孤儿进程将被init进程收养(进程号为1),并由init进程对他们完成状态收集工作。
二、操作系统
2.1 内存管理
2.1.1 无存储器抽象
- 每个程序都直接访问物理内存
- 在内存中同时运行多个个程序是不可能的,因为多个程序的地址有可能冲突,造成程序的崩溃
- 如果真要在无存储器抽象上运行多个程序,操作系统只需要把当前内存中所有内容保存到磁盘文件中,然后把下一个程度读到内存中运行即可。只要在某一个时间内存中只有一个程序,就不会发生冲突
- 无存储器抽象的缺点:
- 直接修改物理内存,容易破坏操作系统,从而使系统慢慢停止运行
- 同时运行多个程序很难实现
2.1.2 一种存储器抽象:地址空间
地址空间是一个进程可用于寻址内存的一套地址集合,每个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的地址空间
地址空间为程序创造了一种抽象的内存。
- 问题在于给每一个程序一个自己独有的地址空间。使得程序A的地址28所对应的物理地址和程序B中地址28所对应的物理地址不同
- 针对上述问题,可以使用动态重定位,经典的办法是使用两个寄存器:基址寄存器和界限寄存器。当一个程序运行时,程序的其实物理地址装载到基址寄存器,程序的长度装载到界限存储器。
- 缺点:每次访问内存都需要进行加法和比较运算,速度较慢
2.1.3 交换技术
- 一种用于处理内存超载的技术,即把一个程序完整调入内存,使该进程运行一段时间,然后把它存回磁盘
- 交换技术会在内存中产生大量的空闲区,虽然可以通过内存紧缩来将小的空闲区合成一大块,但内存紧缩会占用大量的CPU时间。
2.1.4 虚拟内存
-
虚拟内存的基本思想
每个程序拥有自己的地址空间,这个空间被分割成很多个块,每个块称作一页或页面。每一页有连续的地址范围,这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行。
当程序引用到一部分在物理内存中的地址空间时,由硬件立即执行必要的映射。当程序引用到一部分不再物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存,并重新执行失败的指令。
-
使用虚拟内存的目的:
- 保护操作系统,使程序无法直接访问物理内存地址
- 解决内存超载的问题。将程序全部加载到内存,内存太小则可能无法运行程序,虚拟内存实现可以只加载需要的一部分到内存中运行。
- 实现同时运行多个程序而互不影响
-
虚拟内存实现的技术有:分页,分段,段页式
2.1.4.1 分页
分段即将程序的地址空间按照固定大小划分为页或者页面,物理内存划分的单元称为页框,通常页框大小和页面相同。页面被加载到物理内存(即和页框进行映射),但并不是所有的页面都加载到内存程序才能运行。页表用于保存页面和页框的映射关系,页表里的一条条页表项就是页面和页框的映射。当程序引用一部分页面时,若页面已经被加载到物理内存,则立即执行映射,若没有加载到物理内存,则引起一个缺页中断,由操作系统复杂将缺失的页面装载到内存。
-
分页需要解决的两个问题
- 虚拟地址到物理地址的映射要非常快(使用快表TLB)
- 虚拟地址空间非常大,页表也会很大(使用多级页表和倒排页表)
-
页面置换算法:
- LRU,最近最少使用算法
2.1.4.2 分段
将程序的地址空间划分为多个相互独立的称为段的地址空间,每个段由一个从0到最大线性地址序列构成,每个段可以独立的增长或减小而不会影响到其他的段。
- 分段适合处理在执行过程中大小有变化的数据结构。
- 分段的优点有简化链接和共享,有利于为不同的段提供不同保护
2.1.4.3 段页式
- 结合了分段和分页的优点
- 分段:易于编程、模块化、保护和共享
- 分页:统一的页面大小,使用时不需要全部装载到内存
2.2 死锁
2.2.1 死锁的定义
如果一个进程集合中的每个进程都在等待只能由该进程集合中的其他进程才能引发的事件,那么该进程集合就是死锁的。
2.2.2 死锁产生的必要条件
- 互斥条件。每个资源要么分配给一个进程,要么就是可用的
- 占有和等待条件。已经得到某个资源的进程可以再请求新的资源
- 不可抢占条件。已经分配个一个进程的资源不能被强制性的抢占,它只能被占有它的进程显示的释放。
- 环路等待条件。死锁发生时,系统中一定有两个或两个以上的进程组成一条环路,该环路中的每个进程都等待着下一个进程所占有的锁。
2.2.3 处理死锁的策略
2.2.3.1 忽略该问题
- 鸵鸟算法,遇见死锁当做不存在。如果死锁的频率很低、死锁的代价很小的时候,花费大量的代价去处理死锁有可能是不划算的
2.2.3.2 检测死锁并恢复
-
死锁的检测和预防
在使用这种技术时,系统并不试图阻止死锁的发生,而是允许死锁的发生,当检测到死锁发生后,采取措施进行恢复。
- 死锁的恢复方法:
- 利用抢占恢复
- 利用回滚恢复
- 通过杀死进程恢复
- 死锁的恢复方法:
2.2.3.3 仔细对资源进行分配,动态避免死锁
-
死锁避免
-
银行家算法
-
算法要做的是判断对请求的满足是否会导致进入不安全状态。
-
安全状态和不安全状态的区别是:从安全状态触发,系统能够保证所有的进程都能完成;而从不安全状态出发,则没有这样的保证。
-
-
2.2.3.4 通过破坏死锁的四个必要条件,防止死锁发生
- 死锁预防
- 破坏互斥条件
- 破坏占有并等待条件。请求资源时,先释放已有的资源,然后再次获取
- 破坏不可抢占条件。
- 破坏环路等待条件
2.2.4 活锁
在某些情况下,当进程意识到它不能获取所需要的下一个锁时,就会释放已经获得的锁,并重新尝试获取锁。如果有两个进程交叉获取锁,则会一直处于上诉的循环中,称为活锁。
2.2.5 饥饿
加入CPU优先执行优先级较高的进程,则优先级较低的进程可能永远不会执行,因为系统中有可能一直存在优先级大于最低优先级进程的进程,这种情况就是饥饿。
2.3 进程和线程
三、数据库
3.1 MySQL
3.1.1 MySQL架构
和其他数据库相比,MySQL有点与众不同,它的架构可以在多种不同场景中应用并发挥良好作用。主要体现在存储引擎的架构上,插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取相分离。这种架构可以根据业务的需求和实际需要选择合适的存储引擎
- 连接层。最上层是一些客户端和连接服务。主要完成一些类似于连接处理、授权认证及相关的安全方案。在盖层上引入了线程池了概念,为通过认证安全接入的哭护短提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它所具有的操作权限。
- 服务层。第二层服务层,主要完成大部分的核心服务功能,包括查询解析、分析、优化、缓存以及所有的内置函数,所有跨存储引擎的功能也都在这一层实现,包括触发器、存储过程、视图等
- 引擎层。第三层存储引擎层,存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同,这样我们可以根据自己的实际需要进行选取
- 存储层。第四层为数据存储层,主要是将数据存储在运行于该设备的文件系统上,并完成与存储引擎的交互
3.1.1.1 MySQL的查询流程
客户端请求-->连接器(验证用户身份,给予权限)
-->查询缓存(存在缓存则直接返回,不存在则执行后续操作)
-->分析器(对SQL进行词法分析和语法分析)
-->优化器(主要对执行的SQL优化选择最优的执行方案方法)
-->执行器(执行时会先看用户是否有执行权限,有才去使用这个引擎体提供的接口)
-->去引擎层获取数据返回(如果开启查询缓存则会缓存查询结果)
3.1.2 MySQL存储引擎
存储引擎是MySQL的组件,用于处理不同表类型的SQL操作。不同的存储引擎提供不同的存储机制、索引技巧、锁定水平等功能,使用不同的存储引擎还可以获得特定的功能
MySQL服务器使用可插拔的存储引擎体系结构,可以从运行中的MySQL服务器加载或卸载存储引擎
3.1.2.1 MySQL常见存储引擎
- InnoDB
- MyISAM
- Memory
- NDB
MySQL现在默认的存储引擎是InnoDB,支持事务、行级锁和外键
3.1.2.2 InnoDB和MyISAM
- InnoDB支持事务,MyISAM不支持事务。这是MySQL将默认存储引擎从MyISAM装换位InnoDb的重要原因之一
- InnoDB支持外键,而MyISAM不支持。对一个包含外键的InnoDB表转为MyISAM会失败
- InnoDB是聚簇索引,MyISAM是非聚簇索引。聚簇索引的文件存放在主键索引的叶子节点上,因此InnoDB必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此主键不应该过大,因为主键太大,其他索引也都会很大。而MyISAM是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助素银是独立的
- InnoDB不保存表的具体行数,执行
select count(*) from table
时需要全表扫描。而MyISAM使用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快。 - InnoDB最小的锁粒度是行锁,MyISAM最小的粒度是表锁。
3.1.2 MySQL索引
3.1.2.1 索引的定义
MySQL官方对索引的定义为:索引是帮助MySQL高效获取数据的数据结构,所以说索引的本质是:数据结构
3.1.2.2 索引的优缺点
- 优点
- 提高数据检索效率,降低数据库IO成本
- 降低数据排序的成本,降低CPU消耗
- 缺点
- 索引也是一张表,保存了主键和索引字段,并指向实体表的记录,所以也需要占用内存
- 虽然索引提高了查询速度,但同时也会降低更新表的速度。维护索引有一定的开销
3.1.2.3 索引的分类
- 数据结构角度
- B+索引
- Hash索引
- Full-Text全文索引
- R-Tree索引
- 物理存储角度
- 聚集索引
- 非聚集索引,也叫辅助索引
- 两者都是B+数结构
- 从逻辑角度
- 主键索引:主键索引是一种特殊的唯一索引,不允许有空值
- 普通索引或者单列索引:每个索引只包含单个列,一个表可以有多个单列索引
- 多列索引(复合索引、联合索引):复合索引值多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用。使用符合索引是遵循最左前缀集合
- 唯一索引或非唯一索引
- 空间索引
3.1.3 MySQL事务
3.1.3.1 事务的特性
- A(Atomicity)原子性:整个事务中的所有操作,要么全部完成,要么全部不完成,不可能处于中间某个环节。执行中间出现错误,会被回滚到事务开始前的状态
- C(Consistency)一致性:在事务开始之前和事务结束以后,数据库的完整性约束没有背破坏。事务只能从一个一致性状态到另一个一致性状态
- I(Ioslation)隔离性:一个事务的执行不能被其他事物干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发事务之间不能互相干扰
- D(Durability)持久性:在事务完成以后,该事务对数据库所做的更改便持久的保存在数据库中,并不会被回滚
3.1.3.2 并发事务处理带来的问题
- 丢失更新:事务A和事务B选择同一行,然后基于最初选定的值更新该行时,由于两个事务都不知道彼此的存在,就会发生丢失更新的问题
- 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
- 不可重复读:事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据做了更新并提交,导致事务A多次读取同一数据时,结果不一致
- 幻读:幻读与不可重复读类似。他发生在一个事务A读取了几行数据,接着另一个并发事务B插入了一些数据时,在随后的查询中,事务A就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
3.1.3.3 事务隔离级别
- READ-UNCOMMITTED(读未提交):最低的隔离级别,允许读取尚未提交的数据变更,可能导致脏读,幻读或不可重复读
- READ-COMMITTED(读已提交):允许读取并发事务已经提交的数据,可以阻止脏读、但是不可重复读、幻读任然有可能发生
- REPEATABLE-READ(可重复读):对同一字段的多次读取结果都是一致的,除非数据是被本省事务自己所修改,可以阻止脏读和不可重复读,但幻读任有可能发生
- SERIALIZABLE(可串行化):最高的隔离界别,完全服从ACID的隔离级别。所有事务一次逐个执行,这样书屋之前就完全不可能产生干扰。
3.1.3.4 MVCC多版本并发控制
-
MySQL的大多数事务型存储引擎实现都不是简单的行级锁。基于提升并发性考虑,一般都同时实现了多版本并发控制(MVCC)。
-
可以认为MVCC是行级锁的一个变种,但它在很多情况下避免了加锁操作,因此开销更低,虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只是锁定必要的行
-
MVCC的实现是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长的时间,每个事务看到的数据都是一致的
-
典型的MVCC的实现方式,分为乐观并发控制和悲观并发控制。
- InnoDB的MVCC是通过在每行记录后面保存两个隐藏的列来实现。这两个列一个保存了行的创建时间,一个保存了行的过期时间。当然存储的并不是真实的时间,而是系统版本号。没开始一个新的事物,系统版本号就会自动递增,事物开始时刻的系统版本号会作为事物的版本号,用来和查询到的每行记录的版本号进行比较。
3.1.3.5 事务的实现
- 事务的隔离性是通过锁实现的,而事务的原子性、一致性和持久性则是通过事务日志实现的
- redo log(重做日志):实现持久化和原子性
- 在innoDB的存储引擎中,事务日志通过重做(redo)日志和innoDB存储引擎的日志缓冲(InnoDB Log Buffer)实现。事务开启时,事务中的操作,都会先写入存储引擎的日志缓冲中,在事务提交之前,这些缓冲的日志都需要提前刷新到磁盘上持久化,这就是DBA们口中常说的“日志先行”(Write-Ahead Logging)。当事务提交之后,在Buffer Pool中映射的数据文件才会慢慢刷新到磁盘。此时如果数据库崩溃或者宕机,那么当系统重启进行恢复时,就可以根据redo log中记录的日志,把数据库恢复到崩溃前的一个状态。未完成的事务,可以继续提交,也可以选择回滚,这基于恢复的策略而定。
- 在系统启动的时候,就已经为redo log分配了一块连续的存储空间,以顺序追加的方式记录Redo Log,通过顺序IO来改善性能。所有的事务共享redo log的存储空间,它们的Redo Log按语句的执行顺序,依次交替的记录在一起。
- undo log(回滚日志):实现一致性
- undo log 主要为事务的回滚服务。在事务执行的过程中,除了记录redo log,还会记录一定量的undo log。undo log记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据undo log进行回滚操作。单个事务的回滚,只会回滚当前事务做的操作,并不会影响到其他的事务做的操作。
- Undo记录的是已部分完成并且写入硬盘的未完成的事务,默认情况下回滚日志是记录下表空间中的(共享表空间或者独享表空间)
- redo log(重做日志):实现持久化和原子性
3.2 Redis
3.2.1 五种基础类型和其底层实现
- 字符串
- 底层实现
- int编码的字符串
- embstr编码
- raw编码(SDS)
- 底层编码转换规则
- 当int编码保存的字符串不是整形时,将从int->raw
- 当embstr被修改时,将从embstr->raw
- 底层实现
- 列表
- 底层实现
- 压缩列表(ziplist)
- 双端链表(linkedlist)
- 转换规则
- 列表对象所有字符串的长度都小于64字节
- 列表对象元素数量小于512个
- 满足上述两个条件,则采用
ziplist
- 底层实现
- 哈希
- 底层实现
- 压缩列表(ziplist)
- 字典(hashtable)
- 转换规则
- 哈希对象保存的所有键值对的键和值的字符串长度都小于64字节
- 哈希对象元素数量小于512个
- 满足上述两个条件,则采用
ziplist
- 底层实现
- 集合
- 底层实现
- 整数集合(intset)
- 字典(hashtable)
- 转换规则
- 集合对象保存的所有元素都是整数值
- 集合对象的元素不超过512个
- 满足这两个条件,则使用
intset
- 底层实现
- 有序集合
- 底层实现
- 压缩列表(ziplist)
- 跳跃表(skiplist)
- 字典(hashtable)
- 转换规则
- 有序结合保存的所有成员的长度都小于64字节
- 有序几个保存的元素数量小于128个
- 满足上述这两个条件,则使用
ziplist
zset
同时使用skiplist
和hashtable
来保存有序集合元素。单独使用skiplist
可以保证范围操作快速实现,但无法实现查找单个元素O(1)
;单独使用hashtable
可以保证单个元素O(1)
,但无法保证范围操作的快速实现。所以同时使用两种数据结构。
- 底层实现
3.2.2 Redis分布式锁
3.2.2.1 实现要点
- 互斥性。同一时刻只能有一个客户端持有锁
- 防止死锁发生,如果持有锁的客户端没有释放锁,也要保证锁可以正常释放及其他客户端可以加锁
- 加锁和解锁必须是同一客户端
- 容错性。只要Redis还有节点存活,就可以进行正常的加锁和解锁操作
3.2.2.2 实现
- 保证互斥使用的是Redis中的
setnx(set if not exist)
- 防止死锁采用的方法是给锁加一个超时时间,时间到自动释放锁
3.2.2.3 问题
- 使用
setnx
获取一个锁之后,客户端崩溃,锁无法释放- 给获取到的锁都加一个超时时间,时间到后其他客户端可以继续获得这个锁
- 客户端获得一个锁,设置超时时间,但在客户端任务执行完毕时,超时时间已经过期,并且这个锁被其他的客户端持有,此时,第一个客户端就有可能释放第二个客户端的锁
- 设置锁key的值为一个随机值,标志是当前客户端加锁。当释放的时候判断其值是否和这个随机值相同,相同才释放锁。但是判断值和胸痛和执行删除命令不是原子性的,所以需要使用Lua脚本保证原子性
3.2.3 Redis持久化
3.2.3.1 RDB
将Redis在内存中的数据保存到磁盘里面,避免数据的以外丢失。RDB既可以手动执行,也可以自动执行,将数据保存到一个RDB文件中
- 执行RDB持久化的两个命令:
save
和bgsave
,save
会阻塞当前进程,直到持久化完成,bgsave
会fork
出一个子进程执行RDB - Redis没有主动还行的载入RDB文件的命令,只有启动Reds时自动加载RDB文件
- RDB的优先级低于AOF,AOF开启时,优先使用AOF回复数据
- 可以设置
save
自动执行的条件来让服务器自动执行RDG持久化
3.2.3.2 AOF
AOF是通过保存Redis服务器所执行的写命令来记录数据库数据的
- AOF持久化机制维护一个
aof_buf
来存放提交的命令,用于提高效率,命令先存入aof_buf
,然后根据服务器采取的策略来将aof_buf
中的命令同步到AOF文件中。 - 有三种AOF持久化策略
always
:安全性最好,效率最差everysec
:安全性好,丢失只会丢失1s的数据no
:效率高,但安全性差
- 为了解决AOF文件膨胀的问题,AOF有重写功能,AOF重写是对现有数据库的数据读取来实现的,无需操作AOF文件
- 后台重写AOF也是通过
fork
子进程实现的
3.2.4 Redis渐进Hash
四、计算机网络
4.1 TCP
4.1.1 概述
- TCP是面向连接的
- TCP是点对点的
- TCP提供可靠交付服务
- TCP提供全双工通信
- TCP是面向字节流的
4.1.2 三次握手
- 为什么要三次握手?
- 防止已失效的连接请求报文段突然传送到了B,因而产生错误。假定出现这样一种异常的情况,即A发送的第一个连接请求报文没有丢失,而是在某些网络节点长时间滞留了,以致延误到连接释放以后的某个时间才到达B。本来这是一个早已失效的报文段,但B接收到此失效的连接请求报文段后,就误认为是A又发出一次连接请求,于是就向A发出确认报文段,同意建立连接,加入没有第三次握手,那么只要B发出确认,新的连接就建立了。由于现在A并没有发出建立连接的请求,因此不会理睬B的确认,也不会向B发送数据,但B却以为连接已经建立,并一直等待A发来的数据。
4.1.3 四次挥手
- 为什么在TIME-WAIT状态必须等待2MSL的时间?
- 保证A发送的最后一个ACK报文段能够到达B,确保连接的正常关闭,这个ACK报文有可能丢失。
- 防止“已失效的连接请求报文段”出现在本连接中。等待2MSL后,就可以使本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接中不会出现这种旧的连接请求报文段。
4.1.4 保活计时器
- 作用:解决客户端出现故障断开连接但服务器不知道的情况下浪费服务器资源的问题。
- 服务端每收到一次客户的数据,就重新设置保活计时器,时间的设置通常是两小时。若两小时没有收到客户的数据,服务器就发送一个探测报文段,以后每隔75s发送一次,若一连发送10个探测报文段客户任无客户响应,服务器就认为客户端出现了故障,接着就关闭这个连接
4.1.5 流量控制
所谓流量控制,就是让发送方的发送速率不要太快,要让接收方来得及接收。发送方的发送窗口不能大于接收方的接收窗口
流量控制往往指的都是点对点通信量的控制,是一个端到端的问题。流量控制要做的是抑制发送端的速率,以便接收端来得及接收
4.1.6 拥塞控制
所谓拥塞控制,就是防止过多的数据注入到网络中,这样可使网络中的路由器或链路不至于过载。拥塞控制是一个全局性的过程
- 在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏,这种情况就叫做拥塞。
- 拥塞控制用到的算法
- 慢开始
- 拥塞避免
- 快恢复
- 快重传
4.2 UDP
4.2.1 概述
- UDP是无连接的
- UDP是尽最大努力交付
- UDP是面向报文的
- UDP没有拥塞控制
- UDP支持一对一、一对多、多对一和多对多的交互通信
- UDP的首部开销小
4.3 HTTP
4.3.1 概述
- 支持客户服务器模式
- 简单快速。客户端向服务器请求数据时,只需传送请求方法和路径
- 灵活。HTTP允许传输任意类型的对象
- 无连接的。虽然HTTP使用TCP连接,但通信的双方在交换HTTP报文前不需要先建立HTTP连接
- 无状态的。同一个客户端第二次访问同一个服务器上的页面时,服务器的响应与第一次被访问时相同
4.3.2 HTTP的状态码
-
1xx:指示信息,表示请求以接收,继续处理
-
2xx:成功,表示请求已被成功接收,处理
- 200(OK):客户端请求成功
- 204(No Content):无内容,服务器成功处理,但未返回内容。一般用在客户端向服务器发送消息,而服务器不用向客户端返回什么信息。不会刷新页面。
- 206(Partial Content):服务器已经完成了部分GET请求(客户端进行了范围请求)。响应报文中包含Content Range制定范围的实体内容。
-
3xx:重定向
- 301(Moved Permanently):永久重定向,表示请求的资源已经永久的搬到了其他位置。
- 302(Found):临时重定向,表示请求的资源临时的搬到了其他位置。
- 303(See Other):临时重定向,应使用GET定向获取请求资源。303和302功能一样,区别只是303明确使用GET访问。
- 307(Temporary Redirect):临时重定向,和302含义相同。只是强制使用POST
- 304(Not Modified):表示客户端发送附带条件的请求时,条件不满足。返回304时,不包含任何响应的主题。
-
4xx:客户端错误
- 400(Bad Request):客户端请求有语法错误,服务器无法理解
- 401(Unauthorized):请求未经授权,这个状态代码必须和www-Authenticate报头域一起使用
- 403(Forbidden):服务器收到请求,但是拒绝提供服务
- 404(Not Found):请求资源不存在
- 415(Unsupported media type):不支持的媒体类型
-
500:服务器错误
-
500(interval Server Error):服务器发生不可预期的错误
-
503(Server Unavaliable):服务器当前不能处理客户请求,一段时间后才可以
-
4.3.3 HTTP1.0和HTTP1.1的区别
- 缓存处理。在HTTP1.0中主要使用header里的If-Modified-Since,Expires来作为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略如Entity tag、If-Match等更多可供选择的缓存头来控制缓存策略。
- 带宽优化及网络连接的使用。HTTP1.0,存在一些浪费带宽的情况,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送了过来,并且不支持断点续传的功能。HTTP1.1则在请求头引入了range域,它允许只请求资源的某个部分,即返回码是206(Partial Content)。
- 错误通知。在HTTP1.1中新增了24个错误的状态码。如409(Confict)表示请求的资源与资源当前状态发生冲突。410(Gone)表示服务器上的某个资源被永久性的删除了。
- Host头处理。在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机,并且他们共享一个IP。HTTP1.1的请求消息和响应消息都应支持Host头域。
- 长连接。HTTP1.1支持长连接和请求的流水线处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。HTTP1.0每发送一次数据都要建立一次TCP连接,发送完后就断开TCP连接。
4.3.4 HTTP1.1和HTTP2.0的区别
- 多路复用。HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求数量比HTTP1.1大了几个量级
- 数据压缩。HTTP1.1不支持Header数据的压缩,HTTP2.0使用HPACK算法对header的数据进行压缩,这样数据体积小了,在网络上传输就会更快
- 服务器推送。服务器推送能把卡护短所需要的的资源伴随着index.html一起发送到客户端,省去了客户端重复请求的步骤。正因为没有发起请求,建立连接等操作,所以静态资源通过服务端推送的方式可以极大地提升速度。
4.3.5 会话技术
由于HTTP请求是无状态的、无连接的,所以如果需要保存一些信息、记录用户状态的话,就需要借用一些技术,如Cookie和Session。
4.3.5.1 Cookie
- 客户端会话技术,数据存储到客户端
- 浏览器对单个Cookie有大小限制(4kb),以及对同一个域名下的总的Cookie的数量也有限制(20个)
4.3.5.2 Session
- 服务器会话技术,将数据存储到服务器
- Session可以存储任意类型、任意大小的数据,但是对服务器有较大的负担
- Session是通过在Cookie中记录一个Session Id唉实现对用户身份的认证,每次请求客户端都会将这个SessionID发送给服务器。(如果禁用了Cookie,则使用一种叫做URL重写的技术来进行会话的跟踪,即每次HTTP交互,URL后面都会被附加上一个诸如sid=xxx的参数,服务端使用这个参数来识别用户)
4.3.5.3 Cookie和Session的区别
- Session存放数据在服务端,安全;Cookie存放数据在客户端,不安全
- Session存储数据没有限制;Cookie存储数据有限制(4kb)
- Session数据相对安全;Cookie数据相对不安全
4.4 HTTPS
HTTPS是由SSL/TLS+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比HTTP协议更加安全
4.4.1 概述
- HTTPS的加密方式有对称加密和非对称加密
- 对称加密用于传输数据
- 非对称加密用于加密对称加密过程中的密钥
4.4.2HTTPS的握手流程
4.5 网络协议对比
4.5.1 TCP和UDP的区别
- TCP是面向连接的,保证可靠交付的;UDP是无连接的,尽最大努力交付的
- TCP是点对点通信;UDP可以一对一、一对多、多对一、多对多通信
- TCP首部开销是20字节;UDP首部开销是8字节
- TCP是面向字节流的;UDP是面向数据报的
4.5.2 HTTP和HTTPS的区别
- HTTP以明文传输数据;HTTPS以密文传输数据
- HTTP的端口为80;HTTPS的端口为443
- HTTP不需要CA证书;HTTPS需要CA证书
4.6 DNS
4.7 网络安全
五、数据结构
5.1 hash
5.1.1 哈希表的原理
- 散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系
f
,是的每个关键字key
对应一个存储位置f(key)
- 对应关系
f
成为散列函数(哈希函数),采用散列技术将记录存储在一块连续的存储空间中,这块连续的存储空间称为散列表或哈希表
5.1.2 哈希函数的构造方法
- 直接定址法
- 数字分析法
- 平方取中法
- 折叠法
- 除数留余法
- 随机数法
5.1.3 处理哈希冲突的方法
- 开放定址法
- 线性探测
- 再散列函数法
- 链地址法
5.2 快速排序
5.2.1 快排的基本思想
快排使用分治的思想,通过一趟排序将待排序的序列分为两部分,其中一部分关键字均比一部分小。之后分别对这两部分再排序,以达到其本身有序。
5.2.2 快排的步骤
- 选择基准
- 分割操作
- 递归地对两个字序列排序
5.2.3 选择基准的方式
最理想的是将序列分为两个等长的子序列。
- 固定位置:第一个或者最后一个元素作为基准。
- 随机位置:随机选择一个位置作为基准
- 三数取中:取第一个、最后一个和中间位置三个数,选择其中位数作为基准
5.2.4 快排的优化
- 当待排序的序列分隔到一定的长度后,使用插入排序。对于序列比较短,插入排序的效率比快速排序更好
- 在一次分割操作结束后,可以将key值相等的元素聚在一起,下次分隔时,不再对key相等的元素分割。
- 优化递归操作
- 使用多线程处理子序列
六、网络编程
6.1 Libevent库
6.1.1 特点
- 基于事件驱动,高性能
- 轻量级,专注于网络
- 跨平台
- 支持多种IO复用技术,
epoll
,select
,poll/dev
,kqueue
- 支持信号、定时器和I/O等事件
6.1.2 大体结构
- 并发网络模型使用Reactor
- IO事件和
signal
事件存放的数据结构为链表signal
统一到IO复用中使用的是pipe
或者socketpair
- 定时事件存放的数据结构为小根堆
- 定时器统一到IO使用使用的是类似于
epoll_wait
的最大等待时间
- 定时器统一到IO使用使用的是类似于
6.2 IO复用
6.2.1 select
6.2.2 poll
6.2.3 epoll
七、C++
7.1 面试问题
-
类和结构体的区别
- 结构体的默认访问控制是
public
,类的默认访问控制是private
- 结构体的默认继承是
public
,类的默认继承是private
- 除了上述两个不同点之外,C++中的机构体和类没有本质区别,只是一个关键字的问题
- 结构体的默认访问控制是
-
在基类中的虚函数,在子类中是否也是虚函数
- 在子类中依旧是虚函数
-
早期C语言如何实现多个返回值
- 全局变量
- 传递数组指针
- 传递结构体指针
-
宏定义的问题
- 只能简单的替换
- 不能取其地址,因为被宏替换为了一个右值
- 宏定义不区分数据类型,在遇到重载的时候会有问题
-
内联函数的作用、优缺点、使用场景
当函数被声明为内联时,编译器在编译期会将其内联展开(直接使用代码替换掉函数调用),而不是按通常的函数调用机制进行调用
- 作用:提高程序运行速度,通过避免函数调用开销
- 优点:提高程序运行效率,替换宏定义函数
- 缺点:内联函数增大了可执行程序的体积,内联函数的展开是编译阶段,若内联函数修改,则需新编译整个程序
- 使用场景:函数代码少于10行或更少才使用内联。
7.2 堆和栈的区别
- 管理方式:栈是由编译器自动控制,无须我们手工控制,堆是由程序员申请和释放,容易产生内存泄露。
- 空间大小:一般来说,32bit系统下堆可以达到4GB,而栈只有1M,但是栈空间可以修改
- 碎片问题:对于堆来说,频繁的
new/delete
会造成内存空间的不连续,使程序效率降低;对于栈来说则不会有这个问题,因为栈是先进后出的数据结构 - 分配方式:堆都是动态分配的;栈可以静态分配和动态分配,静态分配由编译器分配,动态分配由alloca函数进行分配,由编译器实现
- 分配效率:栈的效率优于堆。栈是机器系统提供的数据结构,底层会对栈提供支持,堆是由
c/c++
函数库提供的,机制复杂
7.3 vector
-
底层使用的是数组,初始的大小为0。一个vector占用的空间是三个指针的空间(具体空间随系统版本不同而不同,32位指针4字节,64位指针8字节)。
-
vector中的数据成员
iterator start; //目前使用空间的头 iterator finish; //目前使用空间的尾 iterator end_of_storage; //目前可用空间的尾
-
vector空间的增加每次以两倍递增。每次增加都会重新申请一块空间是之前两倍大小的连续空间,然后将旧值拷贝到新的空间中,最后释放原来的内存。这个过程会使得之前的迭代器失效。
7.4 hashtable
- 解决hash冲突的方式是开链法
- hashtable表格大小虽然不要求其为质数,但SGI STL任然以质数来设计表格大小,其提供28个质数作为扩容时的选择,并提供一个函数选用其中最合适的质数
7.5 空间分配器(STL)
7.5.1 std::allocator
SGI STL定义了std::allocator,但并未使用,也不推荐使用,主要是因为效率不佳,它只对::operator new/delete
做了一层薄的封装。
7.5.2 std::alloc
::operator new/delete
底层使用malloc
和free
SGI
设计双层配置器- 第一级配置器直接使用
malloc
和free
- 第二级配置器:
- 当配置空间足够大,大于128bytes,则使用第一级配置器
- 当配置空间足够小,小于128bytes,为降低额外的负担则使用复杂的内存池(memory pool)机制
- 维护16个自由链表,负责16个小型区块次配置能力
- 内存不足,调用第一级配置器。
- 第一级配置器直接使用
- 第一级空间配置器
- 不做任何处理的情况下,如果内存不足,则直接抛出异常
- 不能直接使用C++的
new-handler
机制,但可以仿造一个,其作用是在内存不足时,调用一个自己定义的处理函数 - 如果客户端并未指定“内存不足处理例程”,则
oom_malloc
和oom_realloc
会直接抛出异常。因为这两个函数内部有死循环,会判断客户端是否设置其处理例程,死循环会一直重试释放内存和申请内存
- 第二级空间配置器
- 每次配置一大块内存,并维护对应的自由链表
- 下次如果有相同大小的内存,则直接从内存池中取,客户端返还的小额内存,也会被回收到pool中
- 方便管理,把内存池中的区块提升到8的倍数,所以16个空闲链表各自维护8~128bytes的链表
八、其它
8.1 程序内存空间
8.1.1 BSS段(未初始化数据区)
通常用来存放程序中未初始化的全局变量和静态变量的一块内存。BSS段属于静态分配,由系统自动释放。
8.1.2 data段(数据段)
存放程序中已经初始化的全局变量的一块内存区域。
8.1.3 text段(代码段)
存放程序执行代码的一块内存区域,大小确定,只读。在代码段中也有可能包含一些只读的常数变量,例如字符串常量等
8.1.4 堆
8.1.5 栈
九、编译原理
9.1 源文件到目标文件经历的过程
9.1.1 预处理阶段
设有一个
hello.cpp
文件。预处理阶段将
.c
文件预编译为.i
文件
g++ -E hello.cpp -o hello.i
- 预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行相应的转化。预处理过程还会删除程序中的注释和多余的空白
9.1.2 编译阶段
编译阶段将
.i
文件编译成.s
文件
g++ -S hello.i
编译阶段的任务:
- 词法分析
- 语法分析
- 语义分析
- 中间代码生成
- 代码优化
- 目标代码生成
9.1.3 汇编阶段
汇编阶段将
.s
文件汇编为.o
文件
g++ -c hello.s
- 将汇编语言代码编译称为机器语言指令,并最终生成可定位的目标文件格式
9.1.4 链接阶段
将源程序调用的库函数编译好的
.o
文件链接到我们的源文件中,链接器的输出结果是可执行的目标程序
g++ hello.o -o hello
9.2 动态库和静态库
9.2.1 静态库
- 当程序与静态库链接时,库中目标文件所含的所有的将被程序使用的函数的机器码被复制到最终的可执行文件中。这就会导致最终生成的可执行代码量相对较多,相当于编译器将代码补全了。
- 缺点:
- 占用磁盘和内存空间。静态库会添加到和它连接的每个程序中,而且这些程序运行时被加载到内存会消耗更多的内存
- 静态库在程序编译时会被连接到目标代码中,程序运行时不再需要该静态库
9.2.2 动态库
- 与动态库连接的可执行文件只需要包含他需要的函数的引用表,而不是所有的函数代码,只有在程序执行时,那些需要的代码才被拷贝到内存
- 缺点:
- 执行速度相对较慢。由于运行时要去连接库会花费一定的时间,执行速度会相对慢点
- 动态库在编译时不会被连接到目标代码中,而是在程序运行时才被载入,因此程序运行时需要动态库