1. 链表

简介

数据结构与算法这一分类的文章,将采用严蔚敏《数据结构与算法》一书的顺序结合leetcode题目Go实现,实践与理论相结合的方式记录学习过程。

链表常见问题

  • 节点基础操作
  • 快慢指针
  • 链表合并

节点增删改查

  • 增删改查,反转
  • 237 删除链表指定节点,画图辅助,修改值和指针指向达到删除链表节点目的
  • 203 删除节点元素
  • 83 有序链表删除重复元素
  • 206 链表反转,画图辅助,临时节点存储,修改指针指向

快慢指针

  • 876 链表中间节点,快指针两步,慢指针一步,则快指针遍历完即得到中间节点
  • 141 判断链表是否有环,当快指针追上慢指针,则链表有环
  • 234 判断链表是否回文序列,快慢指针找到链表中间节点,反转以中间节点为头节点的链表b,比较原链表和b,遍历到其中一个的尽头如果都相等则为回文序列
  • 160 两个链表交汇节点,两个链表分叉部分的节点数差值为x,双指针分别按1步,快指针在短链表,则快指针遍历完链表时,快指针回到慢链表从头遍历,慢指针遍历完之后回到快链表,则此时快慢指针到达交汇点的节点数相等。需要注意没有交汇点时的情况判断

3. 队列

简介

数据结构与算法这一分类的文章,将采用严蔚敏《数据结构与算法》一书的顺序结合leetcode题目Go实现,实践与理论相结合的方式记录学习过程。

队列常见问题

  • 事件驱动模拟

2. 栈

简介

数据结构与算法这一分类的文章,将采用严蔚敏《数据结构与算法》一书的顺序结合leetcode题目Go实现,实践与理论相结合的方式记录学习过程。

栈常见问题

  • 运算
  • 最小栈
  • 回溯法
  • 栈与递归

运算

  • 682 根据规则入栈出栈,最后求和
  • 842 根据规则入栈出栈,最后对比
  • 20 左符号入栈,遇到右符号出栈,最后查看栈是否为空

最小栈

  • 155 最小栈设计
  1. 设计为节点包含每次入栈时的最小值
  2. 栈维护最小值,压栈时,存x-min,出栈时如果值小于0,则返回最小值且更新最小值min=min-x,如果大于0,则返回x+min

回溯法

  • 迷宫求解

栈与递归

  • n皇后

5. 数组

简介

数据结构与算法这一分类的文章,将采用严蔚敏《数据结构与算法》一书的顺序结合leetcode题目Go实现,实践与理论相结合的方式记录学习过程。

数组常见问题

  • 对无序数组进行排序
  • 多维数组数组元素按规则变换
  • 数组按指定行列分组
  • 利用数组作为以下标为key的哈希表
  • 配合额外的数据结构如哈希表等
  • 滑动窗口
  • 动态规划
  • 矩阵

排序

  • 977 无序数组元素平方之后升序排序
  • 905 无序数组元素按偶数奇数规则排序
  • 561 无序数组升序排序后,按step为2求和

多维数组数组元素按规则变换

  • 解决思路:根据坐标寻找元素按规则变换的数学方程
  • 832 二维数组反转图片,F(i, j) = F(i, ~j) == 1 ? 0 : 1
  • 999 棋盘规则,可以列方程式 F(i, j) = (B(i, j+x) ? 0 : A(i, j+x)) + … F 表示位于该坐标的结果,B 表示某个坐标方向有没有阻隔, A 表示某个坐标方向有没有黑棋
  • 867 坐标转换,L = [[F(i,j),F(i+1,j)]\ …]
  • 766 遍历i,遍历j,需要满足F(i,j) == F(i+x,j+x) 直到F(i+x,j+x)不存在

利用数组作为以下标为key的哈希表

  • 448 把数值v对应的数组位置i = v 标记为0,标记为0的原先值v(i)对应的数组位置也标记为0,以此类推,最后遍历一遍数组,如果该位置对应的值不为0,则表明该值为缺失的连续值

配合额外的数据结构如哈希表等

  • 217 检查数组是否存在重复
  • 914 计数公约数

滑动窗口

  • 记录窗口起始点,窗口移动
  • 窗口起始点变换条件
  • 窗口记录值时机
  • 674 最长的递增子数组的大小,比较每个窗口的大小
  • 643 窗口大小为4在数组滑动,求每个窗口平均值,最后得出4元连续子数组最大平均值

动态规划

动态规划的套路一般可以分为三个步骤,跟多维数组数组元素按规则变换有点类似,本质都是找到对应的数学方程式:

  1. 定义状态
  2. 状态转移方程
  3. 边界
  • 746 定义F(s)为选定坐标s时的最小和, F(s) = A(s) + min(F(s-1),F(s-2)),F(1) = A(1), F(2) = A(2), 结果为min(F(len(A)-1),F(len(A)-2))

todo

6. 树与二叉树

简介

数据结构与算法这一分类的文章,将采用严蔚敏《数据结构与算法》一书的顺序结合leetcode题目Go实现,实践与理论相结合的方式记录学习过程。

基础

  • 节点拥有的子树数为节点的度,度为0的节点为叶子节点
  • 节点的层次由根开始,根为第一层。节点的最大层次为树的深度或高度
  • 森林是若干不相交互的树集合
  • 二叉树的第i层,至多有2的i-1次方个节点。深度为k的二叉树,至多有2的k次方-1个节点
  • 对于任意二叉树,度为2的节点树为n2,则叶子节点树n = n2 + 1
  • 深度为k且有2的k次方-1个节点的二叉树为满二叉树,完全二叉树为当且仅当每一个节点与满二叉树中编号节点一一对应时
  • 具有n个节点的完全二叉树的深度为log2(n)+1

二叉树常见问题

  • 二叉树的遍历,先序遍历,中序遍历,后序遍历
  • 解决二叉树问题的一个重要思想就是递归,大体套路可以先考虑当前根局部解决的情况,然后递归操作以子节点为根的子树
  • 二叉搜索树
  • 树的变形
  • 深度优先搜索,广度优先搜索
  • 二叉平衡树

递归

  • 617 合并两个二叉树,节点重合的求和。递归思想解决就是,当前根节点重合的求和。左节点的合并两个树的左节点,右节点的合并两个树的右节点,最后考虑边界条件
  • 965 判断二叉树的所有节点数值是否一样,递归判断根节点与左右节点的值是否相同,如果左右节点为空,则不用比较
  • 872 比较两个树的叶子节点值序列是否相同。可以通过递归操作,收集每个树左右子树的叶子节点值,先左后右。最后比较
  • 104 求二叉树的深度。右子树的深度与左子树深度的较大值+1
  • 226 翻转二叉树,根节点的左子树等于翻转之后的右子树,根节点的右子树等于翻转之后的左子树

二叉搜索树

  • 二叉搜索树 左子节点 < 根节点 <= 右子节点或者左子节点 <= 根节点 < 右子节点
  • 700 搜索节点=给定的值
  • 669 给定最小最大值,修剪二叉搜索树。思路:修剪当前树,直到当前树根节点落在范围内,递归修剪左子树和右子树
  • 108 有序数组构建二叉搜索树。数组中间树作为当前根节点,左边部分作为左子树的节点,右边作为右子树节点,递归填充
  • 783 求二叉搜索树相近节点之间最小差值,中序遍历
  • 530 求二叉搜索树节点之间最小差值,中序遍历

树的变形

  • 897 把二叉搜索树变成单向递增,每个节点只有一个右子节点。总体思路,先两边后中间,把右子树变为每个节点只有一个右子节点,把左子树变为每个节点只有左子节点,最后调整为最左子节点为当前树的根节点。递归操作

深度优先搜索,广度优先搜索

  • 深度优先使用栈,广度优先用队列
  • 637 计算二叉树每一层节点。广度优先,累加当前层数值,记录当前层平均值。记录下一层入队节点的个数。

二叉平衡树

4. 字符串

简介

数据结构与算法这一分类的文章,将采用严蔚敏《数据结构与算法》一书的顺序结合leetcode题目Go实现,实践与理论相结合的方式记录学习过程。

字符串常见问题

  • 字符串实质是byte数组,可以用数组的方法来解决字符串的问题,字符串数组可以看作二维数组来解决问题
  • 基础操作
  • 回文序列
  • ASCII码,A-65,a-97,0-48
  • 滑动窗口

基础操作

  • 分割,替换,取子串
  • 929 过滤重复邮箱地址

回文序列

  • 125 筛选26字母和数字字符串,转为数组
  • 680 回文序列变种,允许删除一个元素。则还是按回文序列的规则进行比较,当不匹配时,比较i+1:j序列 或者 i:j-1序列

滑动窗口

  • 28 子串定位,寻找给定字符串1在字符串2的位置,从字符串2滑动,寻找符合条件的位置直到边界

字符串比较

《现代操作系统》阅读笔记二

三、文件系统

文件

  1. 文件命名,文件结构,文件类型
  2. 文件存取:顺序存取,随机存取
  3. 文件属性:如权限,创建修改时间等
  4. 文件操作:常用系统调用如创建,打开等;open:文件属性和磁盘地址装入内存

目录

  1. 层次目录系统,路径名
  2. 目录操作:常用系统调用如创建,打开等;硬连接:文件的节点技术器增加,修改同一份文件。软连接只是符号连接,快捷方式,可以跨越磁盘界限

文件系统的实现

  1. 主引导记录MBR:磁盘的0号扇区,引导计算机
  2. 分区表:MBR后面,记录每个分区的起始结束地址
  3. 分区结构:引导块,超级块,空闲管理,i节点,根目录,文件和目录
  4. 过程:计算机被引导,BIOS读入并执行MBR,确定活动分区,读入并执行第一个块引导快boot block,引导块中的程序将装载该分区中的操作系统
  5. 每个分区都从一个启动块开始
  6. 超级块:启动块之后,包括文件系统类型用的魔数,文件系统中数据块的数量等
  7. 空闲管理:超级块之后,位图或链表形式

文件的实现

  1. 连续分配:删除时会产生磁盘空洞,写入时需要先挑选合适大小的磁盘空洞,常见于CD-ROM
  2. 链表分配:不利于随机存取,指针占了一些字节,致使存储数据不再是2的整数幂
  3. 文件分配表FAT:把磁盘块的指针放在内存的一个表中,目录项中记录起始块号即可。缺点是需要将整个表放在内存中,不适用于大磁盘
  4. i节点:每个文件一个i节点数据结构,列出文件属性和文件块的磁盘地址,具有指针可以指向包含磁盘块地址的块的地址

日志文件系统

  1. 如NTFS和Linux ext3
  2. 基本思想:保存一个用于记录系统下一步将要做什么的日志,当系统崩溃重启时可以查看日志,完成未完成任务
  3. 先写日志项列出要完成的动作,写入磁盘,完成动作后擦除日志项

虚拟文件系统

  1. 和文件相关的系统调用POSIX接口指向虚拟文件系统,由虚拟文件系统调用底层实际的文件系统

文件系统管理和优化

  1. 选定块大小:性能和空间利用率不可兼得
  2. 记录空闲块:位图,空闲列表
  3. 磁盘配额:通过打开文件表中的配额指针,找到配额表中用户的配额表项。
  4. 文件系统的一致性:块检查检查使用表和空闲表并进行校正,目录检查检查文件使用表和文件i节点的计数。rm操作只修改i节点,没有把磁盘块返回空闲表

文件系统性能

如果只需要读一个字,内存比磁盘方位快百万数量级

高速缓存

  1. 逻辑上属于磁盘,实际保存在内存中。与分页类似,置换算法类似
  2. 系统调用SYNC强制性把全部修改过的块写回磁盘,若没有SYNC就移动磁盘,则数据丢失

块提前读

  1. 在需要用到块时,试图提前将其写入高速缓存,提高命中率,如完成k块操作时,预读k+1块到高速缓存
  2. 不适用于随机存取文件

减少磁盘臂运动

  1. 把有可能顺序存取的块放在一起,最好是同一个柱面,减少磁盘臂运动
  2. 使用i节点的文件系统中,需要两次磁盘访问,访问i节点和访问块。所以将i节点存放在磁盘中部,可以减少寻道时间

磁盘碎片整理

  1. 通过移动或复制,使空闲空间连成一片

四、输入/输出

IO硬件原理

IO设备

  1. 块设备:硬盘,CD-ROM,USB
  2. 字符设备:不可寻址没有寻道操作,打印机,网路接口,鼠标等
  3. 时钟,内存映射显示器等

设备控制器

  1. 有寄存器用来与CPU通信
  2. 数据缓冲区
  3. 内存映射IO:每个控制寄存器映射到内存空间中被分配唯一的内存地址
  4. 避免对设备控制器进行高速缓存,操作系统必须管理选择性高速缓存
  5. DMA:直接存储器存取,调控多个设备的数据传送
  6. 中断:设备中断发出信号,由中断控制器检测

IO软件原理

  1. 设备独立性
  2. 统一命名
  3. 错误处理
  4. 同步阻塞,异步中断驱动:大部分物理IO是异步的,CPU启动传输便去做别的
  5. 缓冲
  6. 共享设备和独占设备
  7. 程序控制IO:轮询设备寄存器是否就绪,直到全部IO完成,一直占用CPU
  8. 中断驱动IO:中断发生在每个字符上
  9. DMA控制IO:将中断的次数从每个字符减少的每个缓冲区
  10. 设备驱动程序:为了访问设备的硬件,通常必须是操作系统内核的一部分

磁盘组织成柱面,每个柱面包含若干磁道,磁道数与垂直堆叠的磁头个数相同,磁道分成若干扇区

  1. SATA:磁盘驱动器包含微控制器,负责高速缓存,重叠寻道,坏块重映等
  2. RAID:将一个装满磁盘的盒子安装到计算机,用RAID控制器替换磁盘控制器

磁盘臂调度算法

  1. 磁盘读写时间因素:寻道占主导,旋转延迟,实际数据传输
  2. 磁盘驱动程序维护一张表,按柱面号索引,每个柱面未完成的请求组成一个链表
  3. SSF:最短寻道优先算法,获得最小响应时间与公平性有冲突
  4. 电梯算法:上下移动完成未完成的请求
  5. 磁盘控制器高速缓存:每次将邻近的扇区也读取

时钟

  1. 时钟负责维护时间,根据已知的时间间隔产生中断。防止进程垄断CPU等
  2. 大多数计算机具有一个由电池供电的备份时钟

时钟软件

  1. 维护日时间,防止进程超时,记录CPU使用,处理alarm系统调用,监视定时器,统计信息收集

《现代操作系统》阅读笔记三

五、死锁

资源

  1. 可抢占资源:如存储器,两个进出互相等待对方占有的资源时,其中一方可以释放可抢占性资源,另一方可以顺利执行
  2. 不可抢占资源:无法把资源从占有它的进程抢占过来

死锁

  1. 如果一个进程集合中的每个进程都在等待只能由该进程集合中的其他进程才能引发的事件,该进程集合就是死锁
  2. 死锁条件:互斥条件,占有和等待条件,不可抢占条件,环路等待条件
  3. 处理死锁策略:检测恢复,仔细对资源分配动态避免,破坏死锁条件之一
  4. 检测:需要查阅相关检测算法
  5. 恢复:利用抢占恢复,回滚恢复,杀死进程

预防

  1. 尽量做到尽可能少的进程可以真正请求资源
  2. 破坏占有和等待条件:进程在开始执行前一次性请求所需全部资源,另一种,是进程请求资源时先释放已经持有,再一次性请求
  3. 破坏不可抢占条件:对资源进行虚拟化,由底层进行实际的调用。不适用于操作系统或数据库的记录
  4. 避免出现环路等待:对资源进行编号,每个进程请求需要按照资源的顺序提出
  5. 两阶段加锁:第一阶段将所需记录加锁,如果遇到已经被加锁记录,则释放所有记录,重新第一阶段,第二阶段修改数据然后释放
  6. 通信死锁:超时
  7. 活锁:进程在临界区进行轮询,则可能出现没有进程阻塞,但进程一直尝试失败,常见于资源不足
  8. 饥饿:资源分配迟迟没有轮到该进程

六、多媒体操作系统

多媒体

  1. 特征:使用极高的数据率,多媒体要求实时回放
  2. 要求:存储量大需要进行压缩,实时数据传输
  3. 视频压缩:编码算法和解码算法的速度根据场景不同考量,不必100%可逆,允许有损
  4. 音频压缩:波形编码傅里叶变换,感知编码

七、多处理机系统

多处理机操作系统

  1. 每个CPU有自己的操作系统:无法充分利用资源,任务分配均衡等
  2. 主从多处理机:主处理机容易成为瓶颈过载
  3. 对称多处理机:把操作系统分割成多个临界区,每个临界区由其互斥信号量保护

多处理机同步,调度

  1. 分时
  2. 空间共享:多个CPU上同时调度多个线程
  3. 群调度:一组相关线程作为一个单位调度,一个群的成员在不同的CPU上同时进行,群中所有成员共同开始和结束时间片

八、安全

目标与威胁

  1. 数据机密性–数据暴露
  2. 数据完整性–数据篡改,数据遗失
  3. 系统可用性–拒绝服务
  4. 排外性–病毒控制

密码学原理

  1. 密钥:算法的加密参数
  2. 加密算法公开,加密安全性由独立于算法的密钥决定
  3. 公钥加密:加密运算比较简单,没有密钥的解密运算十分繁琐。公钥是加密密钥,私钥用于解密
  4. 单向函数:f(x)可容易计算出y值,给定y值不能计算x,也称作加密散列函数
  5. 数字签名:对文档运行单向散列函数如MD5,文档拥有者用私钥计算得到D(散列值),接收方用发送方公钥计算得到E(D(散列值))得到实际散列值,再比较

保护机制

  1. 域:一对组合,指定一个对象和一些可在其上运行的操作子集
  2. 基本原则:最低权限原则,每个域拥有最少对象和满足其完成工作的最少权限,安全性最好
  3. unix:进程的域由(UID,GID)定义,每个进程分为用户部分和核心部分,执行系统调用时,从用户态切换到内核态,核心部分可以访问与用户部分不同的对象集,这样系统调用引发域切换
  4. 访问控制表与权能字
  5. 隐写术:如将实际信息隐藏在图片中等

用户认证

  1. 口令
  2. 一次性口令,如网上银行银盾等
  3. 挑战响应认证,密码问题等
  4. 实物认证
  5. 生物识别认证

内部安全

  1. 逻辑炸弹
  2. 后门陷阱
  3. 登录欺骗

代码漏洞

  1. 缓冲区溢出
  2. 代码注入:系统调用时,如命令行
  3. 权限提升攻击:利用计划任务进行攻击等

恶意软件

  1. 木马攻击
  2. 病毒
  3. 蠕虫

防御

  1. 防火墙
  2. 反病毒扫描,完整性检查,行为检查捕捉系统调用
  3. 代码签名

《现代操作系统》阅读笔记一

一. 进程与线程

进程创建

  1. 系统初始化
  2. 正在进行的进程调用进程创建系统调用
  3. 用户请求
  4. 批处理作业初始化

进程终止

  1. 正常退出
  2. 出错退出,自愿
  3. 严重错误,非自愿
  4. 被其他进程杀死

进程层次结构

  1. 系统启动初始化由init特殊进程运行

进程状态

  1. 运行,就绪,阻塞
  2. 进程中断处理,启动,停止由操作系统进程调度程序控制

进程的实现

  1. 操作系统维护一个进程表,每个进程占用一个进程表项(进程控制块PCB)
  2. PCB:包含了进程状态的重要信息,程序计数器,堆栈指针,内存分配状况,打开的文件,进程由运行状态转换到就绪或阻塞需要保存的信息等
  3. 进程切换:保存寄存器值,设置新的堆栈,C中断服务程序运行,调度程序决定下一个进程运行,开始运行新的当前进程
  4. CPU利用率:进程等待IO操作时间与其停留在内存中时间的比对P,内存中同时有N个进程,则利用率=1-P的N次方

线程

  1. 传统:每个进程有一个地址空间和一个控制线程
  2. 对于大量IO操作,多线程允许这些活动重叠进行
  3. 所有线程都有完全一样的地址空间
  4. 线程共享进程内容:地址空间,全局变量,打开文件,子进程,信号与信号处理程序
  5. 线程独有:程序计数器,寄存器,堆栈,状态
  6. 线程创建的线程与当前线程平等,线程可以阻塞等待其他线程返回,线程也可以主动让出CPU

线程的实现

用户级线程

  1. 线程的调度不需要内核,内核不知道多线程的存在,由用户程序自己控制内核切换,切换快
  2. 可以在不支持线程的操作系统实现,允许进程定制调度算法
  3. 同一个进程只能同时有一个线程在运行,如果有一个线程阻塞于系统调用,则整个进程阻塞
  4. 执行系统调用时将导致所属进程被中断,而内核级线程只导致线程中断

内核级线程

  1. 多核机器,一个进程的多个线程可以同时处理
  2. 线程用户态运行内核态调度切换

混合线程模型

  1. 用户线程由运行时库调度器管理,内核线程由操作系统调度器管理
  2. 一对一,多对一,多对多

进程间通信

IPC问题

  1. 进程传递信息给另一个
  2. 多个进程在关键活动不会出现交叉
  3. 正确的顺序

通信方法

  1. 共享内存
  2. 管道
  3. 消息队列
  4. socket
  5. 远程过程调用RPC

临界区

  1. 对进程共享资源的程序片段称作临界区
  2. 避免竞争条件:任何两个进程不能同时处于临界区,不对CPU作假设,临界区外运行的进程不能阻塞其他进程,不能使进程无限等待进入临界区

进程同步

  1. 互斥锁+条件变量
  2. 信号量pv操作

进程调度

  1. 衡量调度:吞吐量系统每小时完成的作业数,周转时间批处理到结束的平均统计时间,CPU利用率

二、存储管理

内存

  1. 地址空间:一个进程可用于寻址内存的一套地址集合
  2. 每个进程的地址空间映射到物理内存的不同部分
  3. 程序装载到内存中连续地址。程序的起始物理地址装载到基址寄存器,程序的长度装载到界限寄存器。进程寄存器相关值由内核维护

交换

  1. 进程完整调入内存运行一段时间,存回磁盘
  2. 被唤醒时重新装载到内存,由基址寄存器和界限寄存器重新映射物理内存

空闲内存管理

  1. 使用位图:每个分配单元对应1位,0表示空闲
  2. 使用链表:每个节点代表一块内存,便于交换

虚拟内存

每个程序有自己的地址空间,被分割成多个快,即页面,每一页有连续的地址范围。这些页被映射到物理内存,不一定需要在内存中才能运行,
当程序引用到一部分不在物理内存中的地址空间时,操作系统负责将缺失的部分装入物理内存并重新执行失败指令。当程序等待它的一部分读入内存时,可以让出CPU

分页

  1. 虚拟地址:没有虚拟内存时,系统直接将虚拟地址送到内存总线。使用虚拟内存,则送到内存管理单元MMU映射为物理地址
  2. 页面:虚拟地址空间按照固定大小划分为固定大小的页面
  3. 页框:物理内存对应页面的单元
  4. 页面找无法映射物理内存时,CPU陷入缺页中断page fault,操作系统选择很少使用的页框,将内容写入磁盘,随后将页面对应的内容读入到内存,修改映射关系,重新执行指令
  5. MMU页表:虚拟地址分成虚拟页号和偏移量,虚拟页号作页表索引,由页表项找到页框号
  6. 引入TLB快表加快虚拟地址到物理地址的转换
  7. 引入多级页表,解决虚拟地址空间巨大的问题
  8. 页面置换算法:最近未使用页面置换算法,先进先出等
  9. 页面大小:小页面减少内存占用,但页表太大,磁盘与内存传输是按页进行,进程切换页表装入硬件寄存器也会变慢。大页面则容易产生更大碎片,浪费内存

链接

  1. 静态链接:将被调用函数的库程序全部装载生成二进制可执行文件,耗空间,每次都需要重新编译
  2. 动态链接:加载了一小段能够在运行时绑定被调用函数的存根例程,在第一次被调用时被装载

内存映射文件

  1. 机制思想:进程发起一个系统调用,将一个文件映射到其虚拟地址空间的一部分,在映射共享的页面时不会读入页面的内容,而是在访问页面时才读入。进程退出或解除文件映射时所有改动的页面会被写回到文件
  2. 如果多个进程同时映射了同一份文件,它们就可以通过共享内存通信

清除策略

  1. 分页守护进程定时检查内存状态,通过置换算法维护空闲页框

进程创建

  1. 确定程序和数据初始大小,创建页表,内存中为页表分配空间初始化
  2. 磁盘交换区中分配空间
  3. 程序正文和数据对交换区空间初始化,以便进程缺页中断时可以调入需要的页面
  4. 页表和磁盘交换区的信息存储在进程表中

进程执行

  1. 重置MMU,刷新TLB,新进程页表成为当前页表

缺页中断

  1. 确定缺页的虚拟地址
  2. 确定缺失的页面,确定磁盘上的位置
  3. 找到合适的页框,读入页面
  4. 备份指令,重新执行
  5. 正在进行IO操作的页面不会被移出内存

进程退出

  1. 最后一个进程退出时,释放页表,页面和页面在硬盘所占用的空间

分段

  1. 每个段由一个从0到最大的线性地址序列构成,每个段都是独立地址空间
  2. 寻址:段号+段内地址
  3. 简化对长度经常变化的数据结构管理,有利于各个过程单独编译链接
  4. 使程序和数据可以划分为逻辑上独立的地址空间,有助于共享,连接和保护
  5. 内存段与段之间的空闲内存外部碎片可以通过内存紧缩
  6. 分段和分页的结合:段内进行分页,提供二维的虚拟内存

Docker文档与实践

入门

镜像与容器

  • 通过官网文档例子构建镜像,可以用镜像运行多个容器。
  • 镜像的发布与拉取,类似于github操作

服务组

  • 大的系统往往需要拆分成多个服务,每个服务构建成独立的镜像。而一个服务组可以统一管理各个服务的部署
  • 通过docker-compose.yml配置,可以配置服务组,包含不同服务使用的镜像,容器数量,资源分配,重启策略,端口,网络
  • docker stack deploy -c docker-compose.yml $services_name
  • 这里的部署前提是docker swarm init,启动集群管理,当前机器为其中的一个节点
  • 部署之前需要确认节点有配置所需要的镜像

集群管理

  • docker swarm init 将当前节点作为管理节点
  • 其他机器可以通过docker swarm join 加入集群,作为其中的一个节点,服务组部署启动的容器将被分布在集群的节点上

stack

  • docker-compose.yml 引入可视化服务,redis服务
  • volumes: 在redis容器内文件系统创建/data,通过映射到宿主系统的实际文件目录,redis数据将可以持久保存在宿主而不会因为容器删除而被删除
  • 通过可视化我们可以直观地看到不同节点运行的服务

开发

开发最佳实践

  • 选择合适的基础镜像,避免构建的镜像太大
  • 多阶段构建,如果不使用多阶段构建,则尽量减少层数
  • 创建一个公共基础镜像,docker只需要加载一次并缓存它
  • 需要考虑基于生产镜像构建用于debug的镜像
  • 添加有意义的镜像tag,包含版本信息,运行环境等
  • 避免在容器存储层写数据,将会使容器膨胀,IO也不如使用数据卷高效
  • 敏感数据使用secrets,非敏感使用configs
  • 尽量使用swarm方式部署,可扩展,配置化,优雅重启,具备一些额外功能如secrets,configs,可以自动获取需要的镜像不需要手动拉取
  • 使用CI/CD自动构建,打tag,测试

Dockerfile最佳实践

  • FROM: 从指定的镜像创建一层,COPY: 从Docker客户端当前目录拷贝文件,RUN: 执行命令,CMD: 容器启动执行命令,容器启动时,添加一层可写容器层在镜像基础上,所有的修改仅发生在这一层
  • 执行docker build时,指定目录为build context,上下文会发送给Docker daemon
  • .dockerignore 类似.gitignore
  • 多阶段构建有助于减小镜像大小和层数,不需要的包不装,构建下一层前清除当前层不需要的东西
  • 应用解耦: 尽可能一个容器一个进程,通过容器间通信配置可以解决依赖
  • 减少层次: RUN,COPY,ADD创建实际的层,其他指令创建临时中间层
  • 构建时指令顺序执行时会检查现有镜像的缓存
  • 从已经缓存的基础镜像开始,下一个指令检查基础镜像的子镜像是否有匹配
  • ADD,COPY是否使用缓存会通过比较文件指纹

创建基础镜像

  • FROM scratch

多阶段构建

  • 同一个Dockerfile
  • 多个FROM,通过–from,下一阶段可以通过引用上一阶段构建好的镜像临时容器中获取需要的文件或程序
  • 可以只构建指定阶段,也可以直接引用其他镜像

网络配置

网络驱动

  • 可插拔
  • bridge: 独立容器通信使用,适用于同一个宿主不同容器通信
  • host: 容器直接使用宿主机的网络,swarm不可用。适用于docker不隔离而容器隔离
  • overlay: 连接多个Docker daemon,用于支持集群swarm services。适用于容器运行在不同宿主,或多服务间通信
  • macvlan: 分配MAC地址给容器,Docker daemon通过MAC地址路由。适用于从虚拟机迁移
  • none: 禁用网络
  • docker network ls 可以看见已有的网络及对应的驱动类型

bridge

  • 分为默认和自定义
  • 使用默认时,每个容器都需要打开指定端口。使用自定义网络时,处于同一网络的容器间端口是相互暴露,而只需要供外部访问的容器打开端口即可
  • 使用自定义网络,容器可以加入和移出自定义网络
  • 自定义网络有单独的配置文件
  • 默认网络使用–link参数可以使容器间共享环境变量,自定义网络则需要考虑从共享数据卷,docker-compose,swarm services实现环境变量共享
  • 自定义网络通过-p参数指定对其他网络的暴露端口
  • 容器创建时可以指定加入的网络,已经运行的可以通过docker network connect加入
  • docker attach 将连接宿主与容器的标准输入输出错误流
  • 实践可以参考https://docs.docker.com/network/network-tutorial-standalone/

host

  • 容器与宿主没有隔离,容器的端口直接使用宿主的端口
  • 可以用于swarm services,如果容器绑定指定端口,则一个节点只能启动一个容器

overlay

  • 用于创建分布式网络连接多个docker daemon,路由转发数据包
  • docker swarm init 初始化时,docker network ls 可以查看到,默认网络ingress
  • 同一个docker daemon下的container通信用默认的bridge网络docker_gwbridge
  • 每一个docker daemon 需要开放系列端口:TCP2377用于集群的管理通信,TCP7946和UDP7946用于节点间通信,UDP4789用于overlay网络转发
  • 自定义overlay网络,可以–opt encrypted加密节点数据传输
  • 可以自定义默认ingress的配置,需要通过移除和重建的方式
  • 修改docker_gwbridge配置时,需要停止docker,也是通过移除和重建的方式

场景

  • 外部访问容器:通过端口映射将容器端口暴露
  • 容器互联:基础服务容器不对外暴露端口,只供应用层服务容器访问,则传统方式通过–link手动互联,更好的方式是自定义网络,将容器加入同一个网络。Docker通过提供环境变量和更新host文件提供基础服务容器连接信息。

存储

持久化存储

volumes

  • 数据卷作为宿主文件系统的一部分,由docker管理,非docker进程无法修改,官方推荐最佳数据持久化方式
  • 可以同时挂载到多个容器,可以同时读写
  • 可以存储数据到云端
  • 可以方便的进行数据迁移

bind mounts

  • 可以在宿主任何位置存储,非docker进程可以修改
  • 直接将宿主的文件或文件夹挂载到容器,不利于管理
  • 使用场景:共享配置文件如dns,在开发环境容器进行打包时可以生成包到指定路径,

tmpfs mounts

  • 存在宿主内存,而不是容器可写存储层
  • 主要基于性能考虑,如大量数据写时

总结

  • 文档帮助我们快速入门及对docker的整体有个大体了解。通过官网的实践例子,我们对docker不再陌生,剩下的则是实际开发生产解决具体问题,以下的几个问题还需要在实际应用过程中进行总结。
  • 如何更快构建更小的镜像,需要了解Dockerfile各个指令,多阶段构建,构建缓存使用等
  • 如何利用集群使服务高效便捷地扩展,如何编排服务组,分配资源等
  • 如何利用网络通信,使不同容器,服务,节点间进行解耦合作
  • 如何解决不同容器,服务,节点间数据持久化,共享
  • 实际生产监控与报警

Nginx文档笔记

一、安装

  1. 安装文档
  2. Ubuntu:除了文档列出的依赖,额外依赖
    1
    2
    3
    4
    5
    sudo apt install libssl-dev
    sudo apt install libxml2-dev
    sudo apt install libxslt1-dev
    sudo apt install libgd-dev
    sudo apt install libgeoip-dev

二、nginx管理

  1. 一个master进程和多个worker进程,如果缓存开启,则缓存加载和缓存管理两个进程也在初始化运行
  2. nginx通过信号管理,除了nginx -s quit/reload/reopen/stop 也可以通过kill 命令发送信号给master进程

三、Web服务器

静态资源

  1. root 指定资源根目录
  2. location 匹配url, =普通字符串匹配,~表示正则匹配
  3. sendfile,sendfile_max_chunk:允许数据从一个文件描述符直接拷贝到另外一个,指定分片大小避免较快的连接长期占用worker进程
  4. tcp_nodelay:禁用nagle算法,避免延迟响应,配合keepalive
  5. backlog:支持更多连接,需要配合内核配置优化

反向代理

  1. proxy_pass: 请求发送到指定服务组
  2. proxy_bind:如果代理服务器有多个网卡,则与上游服务连接时可以指定ip地址
  3. proxy_set_header,proxy_buffer_size,proxy_timeout,proxy_connect_timeout等
  4. SSL: 基本原理与配置与https一致

压缩

  1. gzip
  2. gzip_types
  3. gzip_min_length:低于这个阈值不会进行压缩
  4. gzip_proxied:如果请求来自代理服务器,则不会进行压缩,所以通过检查请求的cache-control,符合条件的才进行压缩
  5. gzip_static:允许直接发送压缩文件
    1
    2
    3
    4
    5
    6
    7
    server {
    gzip on;
    gzip_types text/plain application/xml;
    gzip_proxied no-cache no-store private expired auth;
    gzip_min_length 1000;
    ...
    }

四、负载均衡

HTTP负载均衡

代理

  1. 设置一组上游服务 upstream, 默认使用round robin 算法
  2. 通过proxy_pass 指定上游服务组

算法

  1. round robin:通过server_weight 参数轮询
  2. least_conn:发送给最少连接的上游服务,server_weight 参数作为参考
  3. ip_hash:通过ip地址计算哈希值,如果需要移除一台服务,则可以设置为down,则请求将依规则发给下一台
  4. hash:用户定义的哈希值进行路由
  5. random:随机取出指定数量的服务,从中再根据least_conn算法路由

慢启动

  1. 设置slow_start 避免服务刚恢复时,承载过多请求

服务失败检测

  1. max_fails:失败达到参数设置,则nginx认为服务不可用,默认1次
  2. fail_timeout:认为不可用的持续时间,即不再重试的时间,默认10s

数据共享

  1. 设置zone可以使nginx多个worker进程共享相同的upstream 服务组设置及相关计数

使用DNS

  1. 设置resolver,valid为ttl时间,ipv6可以设置关闭
  2. upstream 下各服务为域名

TCP、UDP负载均衡

  1. 与http负载均衡类似

反向代理时传递客户端的真实IP而非负载均衡器的IP地址

  1. 配置nginx接受代理协议,则nginx将真实客户端ip和端口存储在变量$proxy_protocol_addr,$proxy_protocol_port

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    http {
    #...
    server {
    listen 80 proxy_protocol;
    listen 443 ssl proxy_protocol;
    #...
    }
    }

    stream {
    #...
    server {
    listen 12345 proxy_protocol;
    #...
    }
    }
  2. http修改当前server ip为当前实际客户端的ip

    1
    2
    3
    4
    5
    6
    http {
    server {
    #...
    real_ip_header proxy_protocol;
    }
    }
  3. 将ip地址传递给上游服务

    1
    2
    3
    4
    http {
    proxy_set_header X-Real-IP $proxy_protocol_addr;
    proxy_set_header X-Forwarded-For $proxy_protocol_addr;
    }
  4. 将$proxy_protocol_addr添加到log_format中

五、安全

SSL

安全地协商出一份对称加密秘钥
RSA算法流程

  1. 客户端发送client hello,客户端随机数,带上可选加密参数信息
  2. 服务端响应server hello,服务端随机数,已选加密参数信息
  3. 服务端响应certificate,发送服务器证书,server hello done
  4. 客户端发送证书公钥加密后的预主秘钥,客户端进一步用预主秘钥生成主秘钥,发送第一个用主密钥加密的数据
  5. 服务端用证书私钥解密预主秘钥,生成主密钥,握手完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
http {
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;

server {
listen 443 ssl;
server_name www.example.com;
keepalive_timeout 70;

ssl_certificate www.example.com.crt;
ssl_certificate_key www.example.com.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;
#...
}
}

https优化

  1. 使用keepalive
  2. 复用SSL会话参数,避免重复SSL握手

同一个IP支持多个https server

  1. 因为SSL先于浏览器发送http请求,所以,服务端只提供默认证书,致使请求都指向默认server
  2. 最好的解决方法是,分ip部署https server
  3. 另外可以通过使用多个域名公共证书,同时需要TLS SNI support enabled

HTTP基础认证

  1. auth_basic:对话框的用户区
  2. auth_basic_user_file:用户密码表的文件路径
  3. satisfy allow deny 设置ip限制
  4. auth_request 可以指定认证location

限流

  1. limit_conn_zone limit_conn: 限制连接数,限制每个客户端的连接数也可以设置服务接收的连接数
  2. limit_req_zone limit_req: 限制某个location下,请求的频率;burst,如果请求超出限制区,则burst为队列长度,其他请求立即返回503
  3. limit_rate: 限制location下的带宽

《HTTP权威指南》阅读笔记一

1. 常见概念

  1. MIME:Multipurpose Internet Mail Extension,电子邮件系统和HTTP用于描述标记多媒体内容

  2. URI:统一资源标识符,可以有URL和URN两种形式

  3. URL:协议+服务器地址+资源路径

  4. 事务:通过HTTP报文格式化数据块完成一次请求和响应

2. URL与资源

  1. 大多数URL方案的语法建立在://:@:/;?#

  2. 设计URL使其可以通过任意因特网协议安全传输是很重要的,有些协议如SMTP的传输方法只能使用相对较小,通用的安全字母表中的字符

  3. URL中包含如中文等非安全字母表二进制数据或字符时,则需要进行转义

  4. 转义:百分号+字符编码的十六进制数

3. HTTP报文

  1. 组成:对报文进行描述的起始行start line \r\n作为行结束,包含属性的首部块header,包含数据的body

常见HTTP方法

  1. GET,HEAD,POST,PUT,DELETE,TRACE(对可能经过代理服务器传送的报文进行追踪,服务响应会带上实际收到的请求原文),OPTIONS(决定可以在服务器上执行哪些方法)

已定义状态码

  1. 100-101:信息提示
  2. 200-206:成功
  3. 300-305:重定向
  4. 400-415:客户端错误
  5. 500-505:服务端错误

4. 连接管理

  1. 连接过程:解析主机名,dns查询ip,获取端口号,发起连接,发送请求报文,返回响应报文,关闭连接
  2. HTTP事务时延:DNS查询,连接,请求,处理,响应,关闭
  3. 性能聚焦区域:TCP连接,TCP慢启动拥塞控制,数据聚集的Nagle算法+TCP延迟确认,TIME_WAIT时延和端口耗尽
  4. 并行连接:由客户端的网络带宽限制并行连接数
  5. 持久连接:HTTP/1.1及HTTP/1.0的增强版本 允许设备在事务处理结束时将连接保持打开状态,省去建立连接和慢启动的阶段,1.0可以通过Connection:keep-alive
  6. 管道化连接:HTTP/1.1允许在持久连接上使用请求管道,响应到达前将请求放入队列,不能传送非幂等请求如POST

5. Web服务器

  1. 重定向使用场景:永久删除或临时删除的资源,负载均衡,服务器关联,规范目录名称
  2. 对非持久连接,发送完响应关闭服务端连接;对持久连接,需要正确计算content-length首部

6. 代理

  1. 代理可以监视流量并对其进行修改
  2. 应用场景:安全防火墙,Web缓存,反向代理,内容路由器,转码,匿名
  3. via: 列出了与报文途径的每个中间节点

7. 缓存

解决问题

  1. 冗余的数据传输:服务器多次传输同一份文档,增大负担,消耗带宽
  2. 带宽瓶颈:客户端会以路径上最慢的网速访问服务器,如果客户端能在局域网获取一份副本,将大大提升性能
  3. 瞬间拥塞:使web服务器过载
  4. 距离时延

缓存再验证

  1. 大部分缓存只有在客户端发起请求,且副本时间需要再次检测才会进行再验证
  2. 向原始服务器发送小请求,如果返回304,则缓存继续有效
  3. 首部:if-modified-since,只有在缓存了对象的副本后又对其进行修改,才发送此对象
  4. 商业代理缓存会在via首部附加额外信息以描述命中情况

HTTP缓存体系

  1. 缓存存储策略:决定http响应内容是否可缓存到客户端,Cache-Control
  2. 缓存过期策略:决定客户端是否可以直接使用缓存数据,Expires
  3. 缓存对比策略:将缓存在客户端的数据标识发往服务端,服务端会查看if-modified-since等请求头,对比判断标识是否有效,如果有效则返回304

相关文章

  1. HTTP缓存控制小结
  2. 彻底弄懂 Http 缓存机制

8. 网关,隧道及中继

网关

如应用程序服务器,早期的CGI通用网关接口

隧道

  1. web隧道允许用户通过http连接发送非http流量
  2. 使用http方法CONNECT建立http隧道
  3. SSL隧道:最初的web隧道是为了通过防火墙传输SSL流量。发送CONNECT请求,返回认证请求,发送带有认证信息CONNECT请求,建立连接

中继

  1. 盲转发
  2. 无法处理Connection:keep-alive

9. 爬虫

  1. 链接提取及相对链接的标准化
  2. 避免环路的出现
  3. 广度优先
  4. 节流
  5. 内容指纹

10. 客户端识别与cookie机制

用户识别机制

  1. 承载用户信息的HTTP首部:user-agent,cookie,referer,x-forward–for等
  2. 客户端IP地址:并不能精确识别客户
  3. 用户登录:通过www-authenticate,authorization首部
  4. cookie

11. 基本认证机制

  1. 实际使用并不安全,用户名和密码都是以明文形式传送,安全使用的唯一方式就是结合SSL

12. 摘要认证

  1. 用摘要保护密码,不通过网络发送密码
  2. 单向摘要,MD5输出的128位摘要被写成32个16进制字符
  3. 用随机数防止重放攻击,服务器质询时返回给客户端一个随机数,客户端在计算摘要之前要加上这个随机数

13. 安全HTTP

  1. 对称秘钥加密:编码和解码使用相同的秘钥,流行算法:DES,RC2,RC4
  2. 公开秘钥加密:使用非对称秘钥,只有接收端可以用私钥解密,RSA算法
  3. 公开秘钥加密算法的计算可能会很慢,对称加密更快
  4. 两个节点间通过公开秘钥加密建立安全通信,再用安全通道产生发送临时随机对称秘钥
  5. 数字签名:附加在报文上的特殊加密校验码,可以证明是作者编写了这条报文,防止报文被篡改
  6. 数字证书:对象,过期时间,发布者,公钥,数字签名等,常见标准格式X.509 v3
  7. 浏览器收到服务器证书后,会对签名颁发机构进行检查
  8. SSL握手:交换协议版本号,选择一个两端都了解的密码,对两端的身份进行认证,生成临时会话秘钥。
  9. 服务器可以要求客户端使用客户端证书,常见于组织机构内网
  10. 站点证书有效性:日期检测,签名颁发者可信度检测,签名检测,站点身份检测检查证书域名与访问站点域名是否一致
  11. OpenSSL:SSL和TLS常见的开源实现,创建SSL本地上下文,建立443TCP连接,将SSL层附加到TCP连接,SSL握手
  12. HTTPS SSL隧道协议:http通过CONNECT方法告诉代理,建立一条直接到服务端的连接,以隧道协议传输数据

14. 实体和编码

  1. content-length:检测报文截尾,这对缓存服务器尤其重要;对于持久连接,客户端需要直到报文在哪里结束
  2. 内容编码:如果主体进行编码,content-length说明的就是编码后的长度;编码后,增加content-encoding首部用户客户端解码;客户端通过accept-encoding告知服务端可以接受的编码方式
  3. 实体摘要:发送方在生成初始主体时生成一个数据校验和,这样接收方就可以通过检查这个校验和捕获所有意外,相关首部content-md5
  4. 传输编码:改变报文数据在网络中的传输方式,解决可靠传输存在的问题:未知尺寸,用传输编码发送数据,用特别的结束脚注表明数据结束;安全性(被SSL取代),扰乱报文内容
  5. Transfer-Encoding:传输编码类型,TE:告诉服务器可以使用哪些传输编码扩展
  6. 分块编码:把报文分割为若干个小块紧挨着发送。非持久连接,客户端读取直到服务端关闭连接。持久连接,说明每块大小,最后用0块作为主体结束的信号
  7. 传输编码规则:传输编码集合中要包含分块;必须最后一个作用于报文主体;不能多次作用到一个报文主体上;
  8. 范围请求:允许客户端只请求文档的一部分,在点对点文件共享客户端应用广泛
  9. 差异编码:客户端可以使用A-IM首部说明可以接受的实例操控类型,Delta-base用于计算差异的基线文档Etag,支持差异编码的服务器必须保持页面随时间变化的多个版本

15. 国际化

  1. charset参数和content-language:告知客户端文档的字母表和语言
  2. accept-language和accept-charset:告知服务端支持的字母表和语言及其优先顺序

16. 内容协商与转码

  1. Vary:响应首部列出客户端请求首部,服务端可用这些首部选择文档
  2. 转码:格式转换,信息综合,内容注入

17. 内容发布与分发

  1. CDN:内容分发网络,节点可以是web服务器,反向代理,或缓存

18. 重定向与负载均衡

  1. HTTP重定向:增加时延
  2. DNS重定向:返回多个ip选中的ip,缺点时DNS会缓存
  3. 任播寻址:地理上分散的web服务器拥有相同的ip地址,将地址广告给骨干网路由,则客户端的请求将通过最短路径路由给最近的服务器
  4. IP MAC转发:交换机将分组转发到指定MAC地址
  5. IP地址转发:修改目的IP地址,即NAT网络地址交换
  6. 代理自动配置协议PAC:配置url使用代理

19. 日志记录

  1. 常用日志格式:参考nginx access.log
  2. 对重要的页面进行缓存清除

amqp源码阅读

概览

Python amqp 是celery项目维护的一个AMQP客户端,详细的介绍可以点开链接查看。它的代码量比较小,有助于我们学习AMQP,首先我们了解一下包的目录结构。大致阅读整体代码后,我们能够了解到整体的分层设计大致如图。之后我们再深入每一层的代码实现,由底至上,学习相关的知识点。阅读完整个的源码后,我们再尝试用golang重新撸一遍实现。

amqp包目录结构

amqp源码分层

Transport

这一层主要是基于TCP连接,实现带缓冲区套接字字节流的读写,协议数据报的读写

transport

  • 协议无关性:socket.getaddrinfo 将返回目标地址支持的套接字信息并返回已填入相关信息的网际套接字地址结构sa,可以直接conenct。无需考虑IPv4还是IPv6

  • 设置描述符cloexec,子进程fork之后调用exec函数成功后,会自动关闭文件描述符,避免父进程退出重启后因为端口占用无法重启:

1
2
3
4
5
6
7
8
9
10
11
12
13
def set_cloexec(fd, cloexec):
try:
FD_CLOEXEC = fcntl.FD_CLOEXEC
except AttributeError:
raise NotImplementedError(
'close-on-exec flag not supported on this platform',
)
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
if cloexec:
flags |= FD_CLOEXEC
else:
flags &= ~FD_CLOEXEC
return fcntl.fcntl(fd, fcntl.F_SETFD, flags)
  • 套接字选项设置,详情可以参考《UNIX网络编程卷1》第7章

    1
    2
    self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
    self.sock.setsockopt(SOL_TCP, socket.TCP_NODELAY, 1)
  • 套接字读写,TCPTransport和SSLTransport分别实现了抽象类的套接字读写方法。SSLTransport使用ssl库包裹了当前套接字并使用ssl的读写方法。读数据时,可能出现的异常

1
2
3
errno.ENOENT:recv收到对端发送的RST产生的错误
errno.EAGAIN:如果设置成非阻塞读,没有数据可读时,返回该错误
errno.EINTR:慢系统调用中断,常见于子进程终止时传递信号给父进程
  • 协议数据报读写,从读取的方法,我们可以得到协议数据报的格式 帧类型1字节 + 2字节信道id + 4字节payload size + size字节payload + 1字节结束标志
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    def read_frame(self, unpack=unpack):
    ...
    frame_header = read(7, True)
    read_frame_buffer += frame_header
    frame_type, channel, size = unpack('>BHI', frame_header)
    payload = read(size)
    read_frame_buffer += payload
    ch = ord(read(1))
    ...

    def write_frame(self, frame_type, channel, payload)

数据协议层

这一层主要提供了字节流与上层数据类型的转换工具AMQPReader/AMQPWriter,不同帧类型的数据报的组装和读取解析工具MethodWriter/MethodReader。我们先研究一个具体使用的场景如声明exchange_declare。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Channel(AbstractChannel):
def exchange_declare(self, exchange, type, passive=False, durable=False,
auto_delete=True, nowait=False, arguments=None):
arguments = {} if arguments is None else arguments
args = AMQPWriter()
args.write_short(0)
args.write_shortstr(exchange)
args.write_shortstr(type)
args.write_bit(passive)
args.write_bit(durable)
args.write_bit(auto_delete)
args.write_bit(False) # internal: deprecated
args.write_bit(nowait)
args.write_table(arguments)
self._send_method((40, 10), args)

if auto_delete:
warn(VDeprecationWarning(EXCHANGE_AUTODELETE_DEPRECATED))

if not nowait:
return self.wait(allowed_methods=[
(40, 11), # Channel.exchange_declare_ok
])

通过AMQPWriter,将exchange_declare的参数序列化为字节流args,通过查看_send_method可以知道最后由MethodWriter.write_method执行组装发送,这里的write_frame则由底层的transport提供

1
2
3
4
5
6
7
class MethodWriter(object):
def write_method(self, channel, method_sig, args, content=None):
write_frame = self.dest.write_frame
payload = pack('>HH', method_sig[0], method_sig[1]) + args
...
write_frame(1, channel, payload)
...

至此我们已经初步了解了从上层抽象的操作接口到底层字节流的转换进行通信的过程。而上述只是一个基本的包含参数的操作,对于带有消息内容的发布与接收操作,则增加两种类型:消息头部和消息实体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class MethodWriter(object):
def write_method(self, channel, method_sig, args, content=None):
...
if content:
body = content.body
if isinstance(body, string):
coding = content.properties.get('content_encoding', None)
if coding is None:
coding = content.properties['content_encoding'] = 'UTF-8'

body = body.encode(coding)
properties = content._serialize_properties()
...
if content:
payload = pack('>HHQ', method_sig[0], 0, len(body)) + properties

write_frame(2, channel, payload)

chunk_size = self.frame_max - 8
for i in range(0, len(body), chunk_size):
write_frame(3, channel, body[i:i + chunk_size])
self.bytes_sent += 1


class Message(GenericContent):
PROPERTIES = [
('content_type', 'shortstr'),
('content_encoding', 'shortstr'),
('application_headers', 'table'),
('delivery_mode', 'octet'),
('priority', 'octet'),
('correlation_id', 'shortstr'),
('reply_to', 'shortstr'),
('expiration', 'shortstr'),
('message_id', 'shortstr'),
('timestamp', 'timestamp'),
('type', 'shortstr'),
('user_id', 'shortstr'),
('app_id', 'shortstr'),
('cluster_id', 'shortstr')
]

还是同一个方法,这次我们关注frame_type为2的消息头部,从图可以看出这次需要得到两个关键信息就是消息实体的长度和序列化后的消息头的属性,消息实体的长度计算的是编码后的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class GenericContent(object):
def _load_properties(self, raw_bytes):
r = AMQPReader(raw_bytes)
flags = []
while 1:
flag_bits = r.read_short()
flags.append(flag_bits)
if flag_bits & 1 == 0:
break

shift = 0
d = {}
for key, proptype in self.PROPERTIES:
if shift == 0:
if not flags:
break
flag_bits, flags = flags[0], flags[1:]
shift = 15
if flag_bits & (1 << shift):
d[key] = getattr(r, 'read_' + proptype)()
shift -= 1

self.properties = d

def _serialize_properties(self):
shift = 15
flag_bits = 0
flags = []
raw_bytes = AMQPWriter()
for key, proptype in self.PROPERTIES:
val = self.properties.get(key, None)
if val is not None:
if shift == 0:
flags.append(flag_bits)
flag_bits = 0
shift = 15

flag_bits |= (1 << shift)
if proptype != 'bit':
getattr(raw_bytes, 'write_' + proptype)(val)

shift -= 1

flags.append(flag_bits)
result = AMQPWriter()
for flag_bits in flags:
result.write_short(flag_bits)
result.write(raw_bytes.getvalue())

return result.getvalue()

消息头属性的序列化,仍然是使用AMQPWriter来对值进行转换。需要考虑三个问题,一个是如何知道消息头的属性有多少个,二是属性在字节流的对应位置,三是确定每个属性在字节流对应位置的边界。
写的时候通过从高位到低位设置标记位,并依次根据属性值不同类型,写入属性值转换后的数据。
消息实体类型的处理相对简单,如果数据太大,则分片进行多次发送,需要考虑的是接收端同一个channel需要进行等待组装完整的数据接收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class _PartialMessage(object):
def add_header(self, payload):
...
def add_payload(self, payload):
...

class MethodReader(object):
def read_method(self):
self._next_method()
m = self._quick_get()
if isinstance(m, Exception):
raise m
if isinstance(m, tuple) and isinstance(m[1], AMQPError):
raise m[1]
return m

def _next_method(self):
queue = self.queue
put = self._quick_put
read_frame = self.source.read_frame
while not queue:
try:
frame_type, channel, payload = read_frame()
except Exception as exc:
#
# Connection was closed? Framing Error?
#
put(exc)
break

self.bytes_recv += 1

if frame_type not in (self.expected_types[channel], 8):
put((
channel,
UnexpectedFrame(
'Received frame {0} while expecting type: {1}'.format(
frame_type, self.expected_types[channel]))))
elif frame_type == 1:
self._process_method_frame(channel, payload)
elif frame_type == 2:
self._process_content_header(channel, payload)
elif frame_type == 3:
self._process_content_body(channel, payload)
elif frame_type == 8:
self._process_heartbeat(channel, payload)

MethodReader提供了给上层消费者调用的read_method,内部维护一个临时队列,如果完整的数据每结束,则继续阻塞读取直到合并成完成的数据message对象,这一步临时存储和组装则由_PartialMessage完成

会话层

这一部分,通过对Connection的研究,可以将这部分分为4部分:连接管理,会话建立与关闭的状态转移,channel管理,事件驱动与分发

连接管理

  1. 通过初始化的参数,创建Transport对象
  2. 查看连接是否存活,MSG_PEEK如果套接字有该选项,则支持从套接字缓冲区预读以此检测连接是否存活

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class Connection(object):
    def is_alive(self):
    if HAS_MSG_PEEK:
    sock = self.sock
    prev = sock.gettimeout()
    sock.settimeout(0.0001)
    try:
    sock.recv(1, socket.MSG_PEEK)
    except socket.timeout:
    pass
    except socket.error:
    return False
    finally:
    sock.settimeout(prev)
    return True
  3. 提供保持连接的心跳探活

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    class Connection(object):
    def send_heartbeat(self):
    self.transport.write_frame(8, 0, bytes())

    def heartbeat_tick(self, rate=2):
    if not self.heartbeat:
    return

    # 记录数据包的发送和接收数量记录
    sent_now = self.method_writer.bytes_sent
    recv_now = self.method_reader.bytes_recv
    # 记录此次检查活跃的时间
    if self.prev_sent is None or self.prev_sent != sent_now:
    self.last_heartbeat_sent = monotonic()
    if self.prev_recv is None or self.prev_recv != recv_now:
    self.last_heartbeat_received = monotonic()
    self.prev_sent, self.prev_recv = sent_now, recv_now

    # 发送心跳包
    if monotonic() > self.last_heartbeat_sent + self.heartbeat:
    self.send_heartbeat()
    self.last_heartbeat_sent = monotonic()

    # 检查失败,说明连接已经断开
    if (self.last_heartbeat_received and
    self.last_heartbeat_received + 2 *
    self.heartbeat < monotonic()):
    raise ConnectionForced('Too many heartbeats missed')
  4. 连接关闭,关闭transport,所有打开的channel都进行清理关闭

会话建立与关闭的状态转移

其状态转移过程如下图

1
2
3
4
5
6
7
8
9
10
11
12
GRAMMAR::

connection = open-connection *use-connection close-connection
open-connection = C:protocol-header
S:START C:START-OK
*challenge
S:TUNE C:TUNE-OK
C:OPEN S:OPEN-OK
challenge = S:SECURE C:SECURE-OK
use-connection = *channel
close-connection = C:CLOSE S:CLOSE-OK
/ S:CLOSE C:CLOSE-OK

channel 管理

  1. channel id 管理

事件驱动与分发

Connection作为0号信道,负责该连接下的事件驱动与分发,接收server的数据报,并分发给数据报指定的channel

Redis文档阅读笔记二

1. Redis Mass Insertion

  1. 有时候需要将原先存有的大量数据迁移到新的redis实例,redis提供一些方案可以让这个过程更快
  2. 通过redis-cli一个一个操作太慢
  3. 通过pipeline操作,又会阻塞服务器
  4. 大数据量插入时,先按官网提到的协议生成对应格式的文本文件,然后使用redis-cli的管道模式批量导入

2. Partitioning

  1. 将数据分布到不同的redis实例

2.1 分片策略

  1. 范围分片: 例如根据用户id的区间决定数据划分到哪个实例
  2. hash分片: 先使用哈希函数求得哈希值,再通过取模根据结果例如介于0-3之间决定数据存储在哪个实例.少数客户端在这基础上实现了连续哈希
  3. 客户端分片: 在客户端就决定好读写的实例
  4. 代理分片: 客户端发送请求到代理,由代理决定实际的redis实例并返回响应给客户端,例如Twemproxy就应用这种模式
  5. 查询路由: 客户端发送请求到随机的一个redis实例,redis再转发请求到正确的节点.Redis Cluster 使用了这种模式,不同的是将客户端的连接重定向到正确的redis实例上而不是直接转发.

2.2 分片缺点

  1. 分片后,不能直接对多个key一次操作
  2. 事务不能对多个key操作
  3. 像有序集合数据集被包含在一个大key中无法对内部key进行分片
  4. 增加操作复杂度,例如备份数据,需要整合各个实例的持久化文件
  5. 增加或减少容量比较复杂,Redis Cluster会重新平衡数据,当增加或者移除节点时.而使用客户端或代理分片方式则难以做到,Pre-sharding技术通过迁移实例的方式实现

3. Distributed locks

  1. 已经有多种库实现了分布式redis锁管理,具体https://redis.io/topics/distlock

    3.1 安全性和活跃度保证

  2. 安全性: 互斥,同一个时刻只能有一个客户端拥有锁
  3. 死锁的释放: 例如当客户端锁住资源发生崩溃而其他客户端获取锁时
  4. 故障容错: 只要大部分redis节点存活,客户端就能进行正常的获取释放锁

3.2 故障转移缺陷例子

  1. 客户端A从主库获取对资源a的锁
  2. 对key的写入发送到从库前,主库崩溃
  3. 从库被提升为主库
  4. 客户端B从主库获取对资源a的锁,此时就违背了安全性原则

3.3 单实例的正确用法

  1. 使用setnx设置key的值,值必须全局唯一,释放锁时检查key是否存在,值是否与预期一致(第二条解释)
  2. 当客户端获取的锁的key带有过期时间.直接使用del最后释放锁的方式,如果过程中因为一些耗时操作导致key过期,此时其他客户端能够获取到锁,则最后del释放锁会把其他客户端的锁也释放.所以通过设置锁key的值作为签名并在最后使用del释放时做检查
  3. 使锁key的值唯一可以使用rc4根据具体信息生成对应随机字符串

3.4 Redlock algorithm

  1. 假设有5个redis独立主库
  2. 客户端先获取当前的时间毫秒级
  3. 顺序获取5个实例的锁,同样的key名和随机值.
  4. 客户端获取锁时,会设置一个相对锁过期时间很小的超时时间,如果一个实例获取不到锁超时则立刻获取下一个实例的
  5. 顺序获取实例锁时,锁的有效时间会逐渐递减,以最后获取实例的锁有效时间为准,最后每个实例锁的过期时间会是一致
  6. 如果已经存在N/2+1实例的锁key或者锁过期,则放弃获取所有实例的锁的操作.这就可以实现互斥原则,当一个客户端获取成功后,其他客户端可以因为没有获取到足够实例的锁而放弃
  7. 该算法基于假设所有机器和进程的时钟频率一致或相对于锁过期时间产生的误差可以忽略不计
  8. 当获取锁失败时,需要及时释放已获取的部分实例锁,可以避免需要等到key过期才能再次获取锁
  9. 文档对安全性和可用性进行了讨论,具体可以看文档
  10. 提高锁的性能可以通过使用非阻塞模式发送所有命令,再读取检查
  11. 需要设置持久化参数fsync=always避免断电或其他灾难后重启key丢失问题
  12. 算法对于断电或灾难重启后的实例不再参与现有活跃的锁.如果客户端A获取到3/5实例锁,而重启新增了一个实例,此时存在N/2+1实例的锁key条件不存在,其他客户端又可以获取锁了.解决问题的方法时,重启后保持一段不可用时间大于其他锁的过期时间.这里会引入一个问题就是如果多个实例重启,在这个不可用期间,意味着新的获取锁可能失败.
  13. 扩展可以考虑可重入锁的实现

4. Redis Keyspace Notifications

  1. key空间报告通过发布订阅模式实现,默认不开启,可以通过配置文件开启
  2. 接收对key产生实际操作的事件

5. Secondary indexing with Redis

  1. redis主要通过key来获取数据,利用redis的一些数据结构可以创建二级索引

    5.1 有序集合的数值索引

  2. 通过有序集合的分值对数据对象进行索引
  3. 常见的操作为,hash结构存储数据对象集合,有序集合对数据对象创建索引
  4. 如果能将多维数据转为线性,则可以利用有序集合对数据进行list索引
  5. 两个分值一样的,则通过C函数memcmp比较

5.2 字典索引

  1. ZRANGEBYLEX可以对值进行检索,包括或排除,可以利用在自动补全场景
  2. 可以再给值加上频率条件
  3. 考虑大小写条件时,可以按小写:频率:大写的方式存储值
  4. 使用组合索引,实际就是将多个字段信息组合后存储为有序集合
  5. 只要找到一种规则就可以合理利用ZRANGEBYLEX对数据进行查询

6. Replication

  1. 主库发送将命令实时发送给从库
  2. 主从连接断开时会重连,并找回连接断开期间主库命令重新同步
  3. 如果找回断开期间部分的命令失败,则执行全部同步,具体由主库发送快照给从库,然后继续保持同步
  4. 从库复制异步进行,从库返回确认也是异步进行
  5. 如果从库落后主库,可以根据配置决定此时从库是否还可以使用旧的数据
  6. 主从复制的一个好处是可以避免主库持久化总是需要将数据写入磁盘,可以通过从库复制实时保存.然而需要注意的是,如果重启主库,主库数据集为空,从库同步复制主库时,从库数据也会被清空
  7. 建议在主从都开启持久化,或者如果不开启持久化则要避免重启机器后自动重启服务
  8. 每一个主库有一个replication id标识,对每个发送给从库的命令会有一个下标.如果从库断连后重连,则会告诉主库最后一个下标,并从该下标开始追赶执行命令同步主库
  9. 从新同步时,主库会开启一个存储进程生成RDB文件(写入磁盘),并缓存新接收的写命令.将RDB文件发送给从库,从库加载RDB文件到内存中,并接收主库缓存的命令然后继续同步
  10. 从库不会对key做expire操作,当主库key过期时执行del操作时发送到从库,从库执行
  11. 当访问从库已过期key时,因为主库的延迟操作,从库根据自身时钟做出判断报告该key不存在
  12. 主库执行Lua脚本时,时间被冻结,所以脚本必须同步到从库执行保证一致效果

7. Redis Persistence

7.1 模式

  1. RDB: 通过对某个时间点数据集存储为快照文件
  2. AOF: 记录每一个写操作,通过重放方式初始化数据
  3. 可以在同一个实例中同时使用这两种模式,重启实例时,AOF模式用于从新初始化数据

7.2 RDB

  1. 随心所欲对某个时间点的数据进行备份
  2. 适合用于灾难恢复
  3. 通过子进程完成备份,而父进程不用磁盘IO,不影响其他命令的进行
  4. 重启初始化数据更快
  5. 派生子进程,在数据集很大时会比较耗时.频繁备份对性能有一定损耗

7.3 AOF

  1. 可以多种文件同步策略,每秒记录或每个写命令时记录,持续性更好
  2. 是一个只加文件,无需定位,容易修复
  3. 如果AOF文件太大时,redis会自动生成新的文件并切换到新的文件
  4. 如果不小心执行了FLUSHALL命令,只要还没有新的命令写入,停止实例.并将最后一个命令删除后重启redis就可以了

7.4 备份数据

  1. redis会避免RDB和AOF的进程在同一时刻进行
  2. 建议设置定时任务生成每个小时的RDB文件放在一个文件夹和生成每天的RDB文件放在另一个文件夹
  3. 定时任务每次执行,清除比较老的RDB文件
  4. 每天转移RDB文件

8. Redis Security

  1. 网络安全策略,将redis运行在虚拟化的linux实例避免直接暴露,外部无法通过防火墙连接redis,客户端通过环回地址与对应端口通信
  2. 安全模式: 从3.2.0版本开始,如果在配置上允许绑定所有端口,而且外部访问无需密码时会进入安全模式,只有通过环回地址的才能够正常访问,其他的客户端返回错误
  3. 认证功能: 密码应该设置足够长
  4. 数据加密: redis并不支持,不过可以再加一层SSL代理.redis推荐Spiped做对称加密
  5. 设置一些命令不可用
  6. NoSQL注入: 注意从不可信赖来源获取可能为Lua脚本作为字符串的问题

Redis文档阅读笔记一

1. Pipelining

  1. Redis是一个TCPServer,使用CS模型
  2. 1次请求将命令集合发送,Redis执行命令后将结果队列化后,再写入返回
  3. 队列化执行结果需要使用内存,如果多次大批量操作需要注意内存的使用
  4. 使用Redis脚本能够处理更快处理批量命令.管道无法在脚本中使用,因为使用管道时在写入之前需要返回响应给客户端(需要注意:这里个人理解可能存在偏差).反之,管道可以使用脚本

2. Redis Pub/Sub

  1. 发布订阅模式: 发布者发布消息到Channel,订阅者订阅Channel接收消息
  2. Redis客户端一旦为订阅模式,不能接收其他命令
  3. redis-cli命令行客户端时进入订阅模式之后只能通过ctrl-c取消订阅,因为此时客户端阻塞等待接收订阅消息
  4. 发布订阅无关于key所在空间,db10发布的,db1订阅仍能接收
  5. 可用模式匹配发布多个channel 和订阅多个channel

3. Redis Lua scripting

  1. EVAL,EVALSHA命令执行Lua脚本
  2. Lua 脚本可以使用redis.call 或redis.pcall执行redis命令
  3. redis.call执行遇到错误时直接抛出Lua异常结果,redis.pcall则会把异常处理成Lua table返回
  4. Lua调用redis命令时把数据转成redis对应数据类型,脚本执行结果返回给客户端时Lua的数据类型转成redis对应数据类型
  5. 使用Lua脚本时对于浮点数最好使用字符串替代
  6. 如果Lua返回数组中包含nil,则数据转换终止,最终只能返回nil之前的结果
  7. redis.error_reply,redis.status_reply 在Lua脚本中是比较有用的按redis数据类型返回结果的方法
  8. 执行Lua脚本时,其他客户端的命令和脚本将无法执行
  9. redis内部缓存机制会缓存脚本,使用EVALSHA,如果redis通过匹配SHA1文摘匹配到脚本,则执行脚本,否则返回错误信息通知使用EVAL代替
  10. 使用SCRIPT FLUSH或重启redis实例会刷新脚本缓存
  11. 脚本自身会被从库复制或写入AOF文件,而不是脚本的结果命令.不过从3.2版本开始,已经可选设置复制结果命令
  12. 脚本不允许设置全局变量

4. Debugging Lua scripts

  1. Redis Lua debugger默认,每一个新的Debug session是一个forked session,这意味着当脚本在debug中时,不会阻塞redis server执行其他命令,同时也意味着debug结束后会回滚脚本执行的结果
  2. 官网有视频详解https://redis.io/topics/ldb

5. Memory optimization

  1. 通过修改redis.conf调整每一种数据类型的最大数量和最大空间
  2. RDB和AOF文件兼容32位和64位,之间可以互转
  3. 合理利用bit和byte操作
  4. 尽可能使用hash结构存储数据
  5. 每个hash最多存储100个field是cpu和内存之间的最佳妥协
  6. redis根据配置文件maxmemory分配内存
  7. 被删除的key实际上并不会立刻释放内存,例如在同一页中存在其他的key未被删除,需要根据峰值内存使用量限定内存使用
  8. redis底层内存分配器会尽可能重复利用被删除key的内存,所以也不用太担心被删除key没有及时释放的问题
  9. 如果不设置maxmemory,所有的内存将可能被吃光
  10. 当超过最大内存限制时,导致写入时out of memory error,但不会因此导致整个机器挂掉

6. Expires

  1. 过期时间只针对key不针对值
  2. 过期时间可以通过persist命令清除
  3. 通过rename重命名key,原key的过期时间仍然有效,如果由别的key rename覆盖,则该key具有别的key的特性
  4. 如果设置的过期时间为过去时间,则key相当于del 而不是expired
  5. 消极检查: 当客户端获取该key时才检查该key是否过期
  6. 积极检查: redis 1秒内执行10个检查过期,每次随机选取20个key,发现过期的则清除,如果发现超过25%过期,则继续下一个检查
  7. 过期执行删除的命令会传递给从库和AOF文件同步执行.从库不会检查key过期,当切换为主库时才会去检查

7. Redis as an LRU (Less Recently Used) cache

7.1 Redis达到最大内存限制时策略

  1. noeviction: 直接抛出异常
  2. allkeys-lru: 将最近不常用的key清除腾出空间
  3. volatile-lru: 将带有过期时间的最近不常用的key清除腾出空间
  4. allkeys-random: 随机将key清除腾出空间
  5. volatile-random: 随机将带有过期时间的key清除腾出空间
  6. volatile-ttl: 将较小剩余存活时间的key清除腾出空间
  7. 如果不确定使用哪种策略,allkeys-lru是一个较好选择
  8. volatile-lru和volatile-random比较适用于只用单个实例,混用缓存和持久key

7.2 近似LRU算法

  1. redis使用的并不是实际的LRU算法,而是大致评估一定样本量中选取最符合的key
  2. 可以通过设置配置样本量参数maxmemory-samples调节精度

7.3 LFU (Least Frequently Used)

  1. 4.0版本以后新增了新策略,根据命中的频率决定清除哪些key
  2. lfu-log-factor和lfu-decay-time是两项主要调节参数

8. Redis transactions

  1. 事务中的所有命令会序列化并串行化执行,在事务过程中,其他客户端发起的请求不会被处理
  2. 所有命令要么全部被处理或不处理(这里的处理并不表示一定执行成功),保证了原子性
  3. 如果使用append-only file,在发生崩溃或强制关闭redis时有可能导致执行事务中部分命令.redis重启后会检测到直接退出.使用redis-check-aof tool修复
  4. MULTI开启事务,命令存储到队列,命令EXEC执行事务所有命令
  5. 执行EXEC检测到命令错误时,会在EXEC直接返回错误信息,并丢弃所有命令
  6. 执行EXEC后,部分命令执行失败,对应的命令返回错误信息,其他命令执行成功
  7. redis不支持回滚:因为官方认为不需要,语法上的错误,在命令队列化时就能检测到,而编码错误导致命令执行失败redis表示不背这个锅,redis追求更简单,更快
  8. 使用WATCH命令实现乐观锁,如果多个客户端对同一个key进行操作并存储时,被观察的key被改变后,其他客户端对该key的修改的事务则会失败,实现了对该key的原子操作
  9. 需要注意的一点,当WATCH某个key之后,key过期了,那EXEC就会正常执行
  10. 使用WATCH可以实现对有序集合操作的原子性
  11. 对事务的操作在脚本中也能实现,而且脚本可以更简单更快

《高性能MySQL》阅读笔记六

1. 可扩展的Mysql

可扩展性: 通过增加资源提升容量的能力

1.1 考虑负载

容量可以简单地认为是处理负载的能力,考虑负载可从以下几个角度

  1. 数据量: 很多应用从不物理删除任何数据,应用所积累的数据量是可扩展的普遍挑战
  2. 用户量: 更多的用户意味着更多的事务,更多的复杂查询
  3. 用户活跃度
  4. 相关数据集的大小

1.2 规划可扩展性

  1. 估算需要承担的负载到底有多少
  2. 大致正确地估计日程表
  3. 应用的功能完成多少
  4. 预期的最大负载是多少
  5. 如果依赖系统的每个部分分担负载,某个部分失效时会发生什么

1.3 向上扩展(垂直扩展)

  1. 单台服务器增加各种高性能硬件
  2. 烧钱有效的方法
  3. 不应该无限制向上扩展

1.4 向外扩展

  1. 策略: 复制,拆分,数据分片
  2. 按功能拆分: 常见做法,根据功能将应用部署在不同服务器,并使用专用的数据库服务器

1.4.1 数据分片

数据分片是目前扩展大型MySQL最通用且最成功的方法

  1. 应用设计初期考虑到,后期实现就比较容易,否则很难将应用从单一数据存储转换为分片架构
  2. 文中举例: 通过用户id来对文章和评论进行分片,而将用户的信息保留在单个节点上
  3. 数据库访问抽象层,降低应用和分片数据之间通信的复杂度
  4. 如非必要尽量不分片
  5. 数据分片最大的挑战就是查找和获取数据
  6. 类似于表分区,选择分区键和数据分片方式是关键,具体请细查

1.5 通过集群扩展

  1. 可以使用集群或数据库分布式技术根据场景适当解决一些问题
  2. 书中提到技术: NDB Cluster, Clustrix等技术

1.6 向内扩展

  1. 对不再需要的数据进行归档和清理
  2. 需要考虑对应用的影响
  3. 需要考虑数据逻辑的一致性,例如清理A表历史数据时需要考虑所有关联数据的处理
  4. 冷热数据分离

1.7 负载均衡

1.7.1 目的

  1. 可扩展性: 如读写分离时从备库读数据
  2. 高效性: 把更多工作分配给更好的机器
  3. 可用性: 使用时刻保持可用的服务器
  4. 透明性: 客户端无需知道服务器
  5. 一致性: 如果应用是有状态的,负载均衡器就应该将相关的查询指向同一个服务器

1.7.2 直接连接

1.7.2.1 复制上的读写分离

  1. 基于查询分离: 将不能容忍脏数据的查询分配到主库,其他分配到备库
  2. 基于脏数据分离: 让应用检查复制延迟,许多报表类应用使用这个策略
  3. 基于会话分离: 可以在会话层做一个标记,如果用户修改了数据,则一段时间内总是指向主库
  4. 基于版本分离: 给用户的操作增加版本号,检查版本号决定从主库还是备库读取数据

1.7.2.3 修改DNS名

  1. 通过变更DNS名指定的服务器实现
  2. 缺点很多,不建议

1.7.2.4 转移IP地址

  1. 在服务器之间转移虚拟地址
  2. 给服务器分配固定的ip地址,为每个逻辑上的服务使用一个虚拟ip地址

1.7.3 引入中间件

  1. 负载均衡器,如HAproxy
  2. 负载均衡算法: 随机, 轮询,最少连接数,最快响应,哈希,权重
  3. 服务器池中增加或移除服务器: 在配置连接池中的服务器时,要保证有足够多未使用的容量

2. 高可用性

  1. 高可用性意味着更少的宕机时间

    2.1 宕机原因

  2. 磁盘空间不足
  3. 糟糕的sql或者服务器bug引起
  4. 糟糕的表和索引设计
  5. 复制问题通常由于主备数据不一致导致

2.2 高可用性实现

  1. 衡量指标: 平均失效时间(MTBF), 平均恢复时间(MTTR)
  2. 避免问题: 适当的配置,监控和规范
  3. 保证在宕机时能快速恢复,系统制造冗余,具备故障转移能力

2.2.1 避免单点失效

  1. 通过增加冗余避免
  2. 共享存储或磁盘复制,如果服务器挂了,备用服务器可以挂载相同的文件系统执行需要的恢复操作
  3. MySQL同步复制

2.2.2 故障转移和故障恢复

  1. 提升备库或切换角色
  2. 虚拟IP地址或IP接管: 当MySQL实例失效时可以将IP地址转移到另一台MySQL服务器上
  3. 使用中间件解决方案

3. 备份与恢复

3.1 设计MySQL备份方案考虑点

  1. 在线备份还是离线备份
  2. 逻辑备份还是物理备份
  3. 非显著数据: 如二进制日志和InnoDB事务日志
  4. 代码: 存储过程,触发器
  5. 服务器配置和复制配置
  6. 外部配置,管理脚本
  7. 增量备份和差异备份
  8. 存储引擎和数据一致性

3.2 备份数据方式

  1. 文件系统中或SAN快照中直接复制数据文件
  2. Percona XtraBackup 做热备份

3.3 InnoDB崩溃恢复

  1. 二级索引损坏: 使用OPTIMIZE TABLE修复损坏的二级索引,此外可以通过构建一个新表重建受影响的索引
  2. 聚簇索引损坏: innodb_force_recovery导出表
  3. 损坏系统结构: 系统结构包括事务日志等,可能需要做整个数据库的导出和还原,因为InnoDB内部绝大部分的工作可能受影响

4. MySQL用户工具

工欲善其事,必先利其器

4.1 接口工具

  1. MySQL Workbench: 一站式的工具
  2. SQLyog: 可视化工具之一

4.2 命令行工具集

  1. Percona Toolkit
  2. MySQL Workbench 工具集

4.3 SQL实用集

  1. common_schema
  2. MySQL Forge

4.4 监测工具

  1. Nagios
  2. Zabbix: 同时支持监控和指标收集的完整系统
  3. Zenoss: Python写的
  4. Hyperic HQ: 基于Java

4.5 Innotop命令行监控

主要包括以下功能

  1. 事务列表
  2. 当前运行的查询
  3. 当前锁和锁等待列表
  4. 服务器状态和变量汇总信息
  5. InnoDB内部信息
  6. 复制监控

《高性能MySQL》阅读笔记五

1. 优化服务器设置

  1. MySQL有大量的可以修改的参数,但不应该随便修改.应该将更多时间花在schema的优化,索引,查询设计上
  2. 配置文件路径: 通常在/etc/my.cnf
  3. 不建议动态修改变量,因为可能导致意外的副作用
  4. 通过基准测试迭代优化
  5. 具体配置项设置请参照官网手册,这里只提及部分

1.1 配置内存使用

  1. 确定可使用内存上限
  2. 每个连接使用多少内存,如排序缓冲和临时表
  3. 确定操作系统内存使用量
  4. 把剩下的分配给缓存,如InnoDB缓存池

1.2 配置MySQL的I/O行为

  1. 有些配置项影响如何同步数据到磁盘及如何恢复操作,这对性能影响很大,而且表现了性能和数据安全之间的平衡

    1.2.1 InnoDB I/O配置

  2. 重要配置: InnoDB日志文件大小,InnoDB怎样刷新日志缓冲,InnoDB怎样执行I/O
  3. InnoDB使用日志减少提交事务时开销,不用每个事务提交时把缓冲池的脏块刷到磁盘中
  4. 事务日志可以把随机IO变成顺序IO,同时如果发生断电,InnoDB可以重放日志恢复已经提交的事务
  5. sync_binlog选项控制MySQL怎么刷新二进制日志到磁盘
  6. 把二进制日志放到一个带有电池保护的写缓存的RAID卷可以极大的提升性能

1.2.2 MyISAM的I/O配置

  1. 因为MyISAM表每次写入都会将索引变更刷新到磁盘
  2. 批量操作时,通过设置delay_key_write可以延迟索引写入,可以提升性能
  3. 配置MyISAM怎样尝试从损坏中恢复

1.3 配置MySQL并发

1.3.1 InnoDB并发配置

  1. 如果在InnoDB并发方面有问题,解决方案通常是升级服务器
  2. innodb_thread_concurrency: 限制一次性可以有多少线程进入内核(根据实践取合适值)
  3. innodb_thread_sleep_delay: 线程第一次进入内核失败等的时间,如果还不能进入则放入等待线程队列
  4. innodb_commit_concurrency: 控制有多少线程可以在同一时间提交
  5. 使用线程池限制并发: MariaDB已经实现

1.3.2 MyISAM并发配置

  1. concurrency_insert: 配置MyISAM打开并发插入

1.4 其他

  1. 基于工作负载的配置: 利用工具分析并调整配置
  2. max_connections: 保证服务器不会因应用程序激增的连接而不堪重负
  3. 安全和稳定的设置: 感兴趣者请自行google
  4. 高级InnoDB设置: 感兴趣者请自行google
  5. InnoDB两个重要配置: innodb_buffer_pool_size和innodb_log_file_size

2. 复制

MySQL内建的复制功能是构建基于MySQL的大规模,高性能应用的基础.同时也是高可用性,可扩展性,灾难恢复,备份及数据仓库等工作的基础

2.1 概述

  1. 解决问题: 让一台服务器的数据与其他服务器保持同步.主库可以同步到多台备库,备库本身也可以配置为另一台服务器的主库
  2. 复制原理: 通过在主库上记录二进制日志,在备库重放日志的方式实现异步的数据复制
  3. 复制方式: 基于行的复制和基于语句的复制
  4. 向后兼容: 新版本只能作为老版本的备库,反之不行

2.2 用途

  1. 数据分布: 在不同地理位置分布数据备份,可以随意停止或开始复制.基于行比基于语句带宽压力更大
  2. 负载均衡: 将读操作分布到多个服务器上
  3. 备份: 复制是备份的一项有意义的技术补充
  4. 高可用性和故障切换: 避免单点失败
  5. MySQL升级测试: 一种普遍做法是使用一个更高版本的MySQL作为备库保证实例升级前查询能够在备库按照预期执行

2.3 过程

  1. 主库把数据更改记录到二进制日志(Binary Log)
  2. 备库将主库上的日志复制到自己的中继日志(Relay Log)
  3. 备库读取中继日志中的事件,将其重放到备库数据上
  4. 局限: 主库上并发运行的查询在备库只能串行化执行,因为只有一个sql线程重放中继日志事件,这是很多工作负载的性能瓶颈

2.4 复制配置

  1. 在每台服务器上创建复制账号: 需要REPLICATION SLAVE权限
  2. 配置主库和备库: 每个服务器的ID需要唯一不能冲突
  3. 通知备库连接到主库并从主库复制数据
  4. CHANGE MASTER TO: 指定备库连接的主库设置
  5. SHOW SLAVE STATUS: 检查复制是否正确执行
  6. START SLAVE: 开始复制
  7. SHOW PROCESSLIST: 查看复制线程,IO线程(发送或获取日志),SQL线程(重放日志)
  8. 推荐配置: 开启sync_binlog

2.5 从另一个服务器开始复制

问题: 主库已经运行一段时间,用一台新安装的备库与之同步
保持同步条件:

  1. 某个时间点的主库的数据快照
  2. 主库当前的二进制日志文件,和获得数据快照时在该二进制日志文件中的偏移量.通过这两个可以确定二进制日志的位置
  3. 从快照时间到现在的二进制日志

克隆备库方法:

  1. 冷备份: 关闭主库,复制数据.主库重启后会使用新的二进制文件,在备库指向这个文件的起始处
  2. 热备份:如果只有MyISAM,可以通过mysqlhotcopy或rsync来复制数据
  3. 如果只包含InnoDB: 可以使用mysqldump转储主库数据并加载到备库,然后设置相应的二进制日志坐标
  4. 使用快照或备份: 使用主库的快照或者备份初始化备库,然后指定二进制日志坐标
  5. 使用Percona Xtrabackup: 备份时不阻塞服务器操作,可以在不影响主库情况下设置备库
  6. 使用另外的备库: 实质就是把另外的备库当成主库进行数据克隆

2.6 复制的原理

2.6.1 基于语句的复制

  1. 主库会记录那些造成数据更改的查询
  2. MySQL5.0之前只支持基于语句的复制
  3. 对于函数,存储过程和触发器在基于语句的复制模式可能存在问题
  4. 更新必须是串行,需要更多的锁

2.6.2 基于行的复制

  1. 将实际的数据记录在二进制日志
  2. 能够更高效复制数据
  3. 基于行的复制事件格式,对人不可读,可以使用mysqlbinlog
  4. 很难进行时间点恢复
  5. 有些操作,如全表更新(update)复制开销会很大

2.7 复制拓扑

2.7.1 基本原则

  1. 一个MySQL备库实例只能有一个主库
  2. 每个备库必须有一个唯一的服务器id
  3. 一个主库可以有多个备库
  4. 如果打开log_slave_update一个备库可以把其主库上的数据变化传播到其他备库

2.7.2 一主多备

  1. 适用于少量写和大量读,可以把读分摊到多个备库上
  2. 当作待用的主库
  3. 放到远程数据中心,用作灾难恢复
  4. 作为备份,培训,开发或测试服务器

2.7.3 双主复制

  1. 个数据库互为主库和备库
  2. 容易造成数据不同步
  3. 通常并不建议使用这种模式

2.7.4 主动被动的双主模式

  1. 类似双主复制,把其中一台配置为只读
  2. 类似于创建一个热备份
  3. 可以用作执行读操作,备份,离线维护及升级

2.7.5 有备库的双主模式

  1. 双主模式下,各自有备库

2.7.6 主库,分发主库和备库

  1. 问题: 备库足够多时会对主库造成很大的负载
  2. 方案: 将其中部分备库当成主库,分发给更多的备库
  3. 通过分发主库,可以对二进制日志事件执行过滤和重写规则

2.8 复制管理和维护

  1. 监控复制: SHOW MASTER STATUS查看主库状态, SHOW BINLOG EVENTS查看复制事件
  2. 测量备库延迟: 可以使用Percona Toolkit里的pt-hearbeat
  3. 确定主备是否一致
  4. 备库换主库: 难点在于获取新主库合适的二进制日志位置
  5. 备库提升为主库分为计划内提升和计划外提升

    2.8.1 计划内提升

  6. 停止向老的主库写入
  7. 备库赶上主库
  8. 备库设置为主库
  9. 将备库和写操作指向新主库,然后开启主库的写入

2.8.2 计划外提升

当主库崩溃时,需要提升一台备库替代

  1. 确定最新的备库
  2. 让所有备库执行完从崩溃前主库获得的中继日志,如果未完成则更换主库,会丢失原先的日志事件
  3. 重新完成主备的配置

2.9 复制的问题和解决方案

2.9.1 数据损坏或丢失

  1. 主库意外关闭: 主库开启sync_binlog避免事件丢失,使用Percona Toolkit中的pt-table-checksum检查主备一致性
  2. 备库意外关闭: 重启后观察MySQL错误日志,想方法获取备库指向主库的日志偏移量
  3. 主库上的二进制日志损坏: 跳过所有损坏的事件,手动找到一个完好的事件开始
  4. 备库上的中继日志损坏: MySQL5.5后能在崩溃后自动重新获取中继日志
  5. 二进制日志于InnoDB事务日志不同步: 除非备库中继日志有保存,否则自求多福

2.9.2 其他

  1. 如果使用myisam,在关闭Mysql前需要确保已经运行了stop slave,否则在服务器关闭时会kill所有正在运行的查询.
  2. 如果是事务型,失败的更新会在主库上回滚而且不会记录到二进制日志
  3. 避免混用事务和非事务: 如果备库发生死锁而主库没有,事务型会回滚而非事务型则不会造成不同步
  4. 主库和备库使用不同存储引擎容易导致问题
  5. 不唯一和未定义备库服务器id
  6. 避免在主库上创建备库上没有的表,因为复制可能中断
  7. 基于语句复制时,主库上没有安全使用临时表的方法.丢失临时表: 备库崩溃时,任何复制线程拥有的临时表都会丢失,重启备库后所有依赖临时表的语句都会失败
  8. InnoDB加锁读引起的锁争用: 将大命令拆成小命令可以有效减少锁竞争
  9. 过大的复制延迟: 定位执行慢的语句,改善机器配置
  10. 其他: 查看官网手册

2.10 复制高级特性

  1. 半同步复制: 当提交事务,客户端收到查询结束反馈前必须保证二进制日志已经传输到至少一台备库上,主库将事务提交到磁盘上之后会增加一些延迟
  2. 复制心跳: 保证备库一直与主库相联系,如果出现断开的网络连接,备库会注意到丢失的心跳数据

2.11 其他复制技术

  1. Percona XtraDB Cluster的同步复制
  2. Tungsten

《高性能MySQL》阅读笔记四

1. 查询性能优化

1.1 优化数据访问

  1. 检查是否检索大量超过需要的数据.是否访问太多行或太多列,增加网络开销,消耗cpu和内存资源
  2. 检查服务器层是否在分析大量超过需要的数据行

1.2 重构查询的方式

1.2.1 切分查询

  1. 有时对于一个大查询我们需要分而治之,切分成小查询每次只完成一部分

1.2.2 分解关联查询

  1. 缓存效率更高: 方便缓存单表查询结果
  2. 执行单个查询可以减少锁的竞争
  3. 在应用层做关联,更容易对数据库进行拆分,更容易做到高性能和可扩展
  4. 使用in 代替关联查询可能比随机的关联要高效
  5. 可以减少冗余记录的查询

1.3 查询执行的基础

1.3.1 查询流程

  1. 先检查缓存
  2. sql解析,预处理,优化器生成相应的执行计划
  3. 调用存储引擎的api执行查询

1.3.2 通信协议

  1. 半双工,任何一时刻要么是服务器向客户端发送数据,要么是客户端向服务端发送数据
  2. 客户端从服务器获取数据时,实际是MySQL向客户端推送数据的过程

1.3.3 查询状态

  1. Sleep: 线程正在等待客户端发送新的请求
  2. Query: 线程正在执行查询或者正在将结果发送给客户端
  3. Locked: 服务器层,线程正在等待表锁
  4. Analyzing and statistics: 线程正在收集存储引擎统计信息,并生成查询的执行计划
  5. Copying to tmp table: 线程正在执行查询,并且将结果集复制到一个临时表中.常见group by或文件排序操作
  6. Sorting result: 线程正在对结果进行排序
  7. Sending data: 线程可能在多个状态之间传送数据或在生成结果集或向客户端返回数据

1.3.4 查询优化

1.3.4.1 语法解析器和预处理

  1. 通过关键字将sql语句进行解析,生成对应的解析树
  2. 解析器使用语法规则验证和解析查询
  3. 预处理器进一步检查解析树是否合法,验证权限

1.3.4.2 查询优化器

  1. 一条查询可以有多种执行方式,优化器找到其中最好的执行计划,MySQL使用基于成本的优化器
  2. 优化类型
  3. 重新定义关联表的顺序
  4. 外联结转化成内连接
  5. 使用等价变换规则
  6. 优化count, min, max函数
  7. 预估并转化为常数表达式
  8. 覆盖索引扫描
  9. 子查询优化
  10. 提前终止查询,如limit
  11. 等值传播
  12. In优化

1.3.4.3 关联查询

  1. 嵌套循环: 先在一个表中循环取出单条数据,然后再嵌套循环到下一个表中寻找匹配的行,如果最后一个联表无法找到更多的行,则返回上一层次关联表
  2. UNION查询和子查询时都会将临时结果存放到一个临时表中

1.3.4.4 执行计划

  1. MySQL生成一棵指令树,通过存储引擎执行完成并返回结果

1.3.4.5 排序优化

  1. 排序是一个成本很高的操作
  2. MySQL排序: 如果数据量小,则在内存中进行; 数据量大则先分块再排序再合并
  3. MySQL4.1后使用单次传输排序: 先读取查询所需要的所有列,再根据给定列排序

1.3.4.6 查询执行引擎

  1. 根据执行计划的指令逐步执行

1.3.4.7 返回结果给客户端

  1. 如果查询可以缓存,则缓存在这个阶段进行
  2. 返回结果的过程是一个增量逐步返回的过程,一旦开始生成第一条结果时就可以开始向客户端返回结果集

1.4 查询优化器的局限

  1. 子查询相对糟糕(不是绝对),如子查询用in
  2. 联表查询与子查询根据场景不同有不同优势
  3. MySQL无法将限制条件下推到子查询
  4. 索引合并优化
  5. MySQL无法利用多核特性并行执行查询
  6. MySQL并不支持哈希关联, MariaDB已经实现了真正的哈希关联
  7. 松散索引扫描,无法按照不连续的方式扫描一个索引
  8. 最大值最小值函数的优化一般
  9. 不允许同一张表上同时查询和更新, 如update set 等于 select 自己.解决方法,可以通过关联临时表

1.5 查询优化器的提示

  1. 设置查询优化器参数,可以阅读官方手册
  2. 一般除非需要,修改查询优化器参数会提高维护成本

1.6 优化特定类型的查询

  1. 关联查询: on的列加索引; 使用group by和order by 只使用一个表的列可以利用索引
  2. 优化LIMIT分页: 尽量使用覆盖索引
  3. 子查询: 尽量使用关联查询替换
  4. 静态查询分析: Percona Toolkit中的pt-query-advisor能解析查询日志,分析查询模式
  5. 使用用户自定义变量: 无法使用查询缓存,可能被优化器优化掉

2. MySQL高级特性

2.1 分区表

2.1.1 应用

  1. 表非常大无法全部放在内存中,或者只在表的最后部分有热点数据其他均是历史数据
  2. 分区表的数据更容易维护
  3. 分区表的数据可以分布在不同的物理设备上
  4. 使用分区表避免某些瓶颈,如InnoDB单个索引的互斥访问
  5. 备份和恢复独立分区,对于大数据集效果较好

2.1.2限制

  1. 一个表最多1024个分区
  2. 分区表达式必须是整数或返回整数的表达式
  3. 如果分区字段有主键或唯一索引列,那么所有主键列和唯一索引都必须包含进来
  4. 分区表中无法使用外键约束

2.1.3 原理

  1. 分区表由多个相关的底层表实现,存储引擎管理它们跟管理普通表一样
  2. select 查询: 分区层打开并锁住所有底层表,优化器判断是否过滤分区,在调用存储引擎api访问各个分区数据
  3. insert: 分区层打开并锁住所有底层表,确定分区,写入
  4. delete: 分区层打开并锁住所有底层表,确定数据所在分区,删除
  5. update: 分区层打开并锁住所有底层表,确定分区,取出数据,更新,确定分区,写入
  6. 打开并锁住所有底层表: 如果存储引擎实现行级锁如InnoDB,则会在分区层释放表锁

2.1.4 分区表类型

  1. 根据范围进行分区: 每个分区储存落在某个范围的记录
  2. 根据键值进行分区,减少InnoDB互斥量竞争
  3. 使用数学模函数进行分区,然后将数据轮询放入不同的分区,适用于只想保留几天的数据

2.1.5 使用

  1. 问题回顾:数据量很大时,除非是索引覆盖查询,否则数据库需要根据索引扫描回表查询,产生大量的随机IO,数据库响应时间很大
  2. 全量扫描数据不要索引,根据分区定位数据位置
  3. 索引数据,分离热点. 将热点数据单独放在一个分区
  4. NULL值会使分区过滤无效: 分区表达式接收NULL值并将其放到第一个分区导致查询时多查分区.解决方法:创建第一个无用分区存放NULL值数据
  5. 分区列和索引列不匹配,查询无法进行分区过滤
  6. 选择分区成本高,插入大量数据时都需要扫描分区定义找到分区
  7. 打开并锁住所有底层表的成本可能很高
  8. 维护分区的成本很高,同alter一样创建临时表然后拷贝数据
  9. 所有分区都必须使用相同的存储引擎

2.1.6 查询优化

  1. 在where条件带入分区列
  2. 创建分区时可以使用表达式,但是查询时只能在使用分区列本身进行比较时才能过滤分区,而不能根据表达式的值过滤分区

2.2 视图

视图本身是一个虚拟表,不存放任何数据,不能对视图创建触发器

2.2.1 算法

  1. 合并算法: 将存放的视图sql和用户发起的查询sql合并后执行
  2. 临时表算法: 由存放的视图sql先创建临时表后根据用户的查询sql查询返回

2.2.2 可更新视图

  1. 可以通过更新视图更新相关表, 所有临时表算法实现的视图都无法更新

2.2.3 视图对性能的影响

  1. 一般情况视图不能提升性能,在某些情况下可以帮助提升性能,需要做比较详细的测试
  2. 视图还可以实现基于列的权限控制不用真正创建列权限

2.2.4 视图的限制

  1. 不保存视图定义的原始sql语句
  2. 查看视图创建的语句,可以通过使用视图的.frm文件的最后一行获取一些信息

2.3 外键约束

  1. InnoDB强制外键使用索引
  2. 查询需要额外访问一些表,需要额外的锁容易导致一些死锁
  3. 如果使用外键做约束,通常在应用程序里实现会更好

2.4 内部存储代码

2.4.1 优点

  1. 离数据最近,节省带宽和网络延迟
  2. 帮助提升安全性,应用程序可以通过存储过程访问那些没有权限的表
  3. 服务器端可以缓存存储过程的执行计划
  4. 维护方便,便于分工

2.4.2 缺点

  1. 调试困难,难以定位问题
  2. 存储代码效率相对差
  3. 增加维护复杂性,存储过程会给数据库服务器增加额外压力
  4. 存在安全隐患,没有什么选项可以控制存储程序的资源消耗,所以一个小错误可能直接把服务器拖死

2.4.3 存储过程和函数

  1. 优化器无法评估存储函数的执行成本
  2. 每个连接都有独立的存储过程的执行计划缓存,多个连接调用同一个存储过程会浪费缓存空间反复缓存同样的执行计划

2.4.4 触发器

  1. 每个表的每个事件只能一个
  2. MySQL只支持基于行的触发,如果变更的数据集非常庞大的化效率会很低
  3. 触发器的问题很难排查
  4. 可能导致死锁和锁等待
  5. 实现一些约束,系统维护任务及更新反范式化数据的时候会比较有用

2.4.5 事件

  1. 类似Linux的定时任务

2.5 游标

  1. MySQL在服务器端提供只读的,单向的游标
  2. 一个存储过程中可以有多个游标,也可以嵌套

2.6 绑定变量

  1. 创建一个绑定变量sql时客户端向服务器发送了一个sql语句原型
  2. 服务器端解析并存储这个sql语句的部分执行计划返回客户端一个sql语句处理句柄
  3. 可以使用问号作为sql的占位,在使用sql接口执行时赋予变量值

2.7 插件

  1. 存储过程插件
  2. 后台插件: 如Percona Server中包含的Handler-Socket
  3. INFORMATION_SCHEMA插件
  4. 全文解析插件: 可以对文档进行分词处理
  5. 审计插件: 可以用作记录事件日志
  6. 认证插件: 扩展认证功能

2.8 字符集和校对

  1. 字符集是指一种从二进制编码到某类字符符号的映射
  2. 校对是指一组用于某个字符集的排序规则

    2.8.1 创建对象时的默认设置

  3. 服务器,数据库,表都有默认的字符集和校对规则,这是一个逐层继承的默认设置
  4. 创建数据库时根据character_set_server设置来设定默认字符集

2.8.2 服务器和客户端通信设置

  1. 服务端总是假设客户端按照character_set_client设置的字符来传输数据和sql语句
  2. 服务器端收到sql语句后根据character_set_connection转换成字符串
  3. 服务器端返回数据时会将其转换为character_set_result

2.8.3 选择字符集和校对规则

  1. 极简原则: 先为服务器选择合理的字符集在根据实际情况让某些列选择合适的字符集

2.8.4 对查询的影响

  1. 不同字符集和校对规则之间的转换会带来额外的开销
  2. 排序查询要求的字符集与服务器数据的字符集相同时才能利用索引进行排序
  3. 当两个字符集不同列关联两个表时,MySQL会尝试转换其中一个列的字符集

2.9 全文索引

  1. 自然语言的全文索引: 相关度是基于匹配的关键词个数及关键词在文档中出现的次数,整个索引中出现次数越少的词语匹配的相关度越高
  2. 布尔全文索引: 只有MyISAM才能使用
  3. 平时没接触过,有兴趣者请自行google

2.10 分布式XA事务

  1. 事务协调器保证所有事务参与者完成工作,通知所有事务提交
  2. 内部XA事务: 存储引擎提交的同时,需要将提交的信息写入二进制日志
  3. 外部XA事务: XA事务是一种在多个服务器之间同步数据的方法,如果由于不能使用MySQL本身的复制或者性能并不是瓶颈可以尝试使用

2.11 查询缓存

  1. 查询缓存系统会跟踪查询中涉及的每个表,如果表发生变化则缓存数据失效
  2. 缓存存放在一个引用表中,通过一个哈希值引用,哈希值包括查询本身,查询数据库等信息
  3. 当sql语句和客户端发送过来的其他原始信息,任何字符上的不同都会导致缓存不命中
  4. 打开查询缓存对读和写都会带来额外的消耗
  5. InnoDB事务修改表时,会将这个表对应的查询缓存都设置失效
  6. 查询缓存被发现是一个影响服务器扩展性的因素
  7. 如果缓存了大量的查询结果,那么失效操作可能会造成系统僵死.因为靠一个全局锁保护,所有该操作都要等锁
  8. 减少碎片, 选择合适的query_cache_min_res_unit可以减少内存浪费
  9. 对于写密集型的应用,直接禁用更好
  10. 高并发环境也不适合.只有明确缓存的好处才使用
  11. 查询缓存的替代方案: 客户端缓存