不奢望岁月静好 只希望点滴积累

0%

一、图灵机和冯.诺依曼机是不同的计算机吗?

1
简单来说: 图灵机更侧重于计算抽象、后者更侧重于硬件抽象

二、time指令的返回分表代表什么?

1
2
3
4
5
6
7
8
9
10
11
12
real time: 时钟时间(Wall clock time), 进从开始到结束所用的实际时间、包括其它进程使用的时间片和等待io完成的时间

user time: 指进程执行用户态代码(核心之外)使用的时间、执行此进程实际消耗的CPU时间、其它进程和此进程阻塞时间不包含在内

sys time: 指进程在内核态消耗的cpu时间

为什么user+sys > real ?
1. user + sys 为实际的CPU时间、若有多个线程、则可能出现、user+sys包含子进程的时间
2. 在多处理器系统上、可能出现、因为多进程或者线程可并行处理
3. time的输出是由几个不同的系统调用得到的
user time和sys time是从wait(2) 或 time(2) 得到的(根据系统不同决定)
real time是从 ge't'timeofday中结束时间和起始时间相减得到

三、时钟周期是什么

1
2


四、为什么程序无法在linux和win下同时运行?

因为两个系统下的可执行文件的格式不同

1
2
3
4
5
6
7
8
-g 得到的是目标文件
-o 得到的是可执行文件

c -> 汇编 -> 机器码
c->汇编: 编译、汇编、链接 3部分组成
汇编-> 机器码: 通过装载器Loader把可执行文件Load到内存中、CPU从内存读取指令和数据、开始真正的执行程序

目标文件和可执行文件dump得到的内容差不多、因为linux下、都是使用elf(executable and linkable file format)的格式

image.png
image.png

1
2
3
4
5
6
ELF 文件格式信息:
1. text Section: 代码段 用来保存程序的代码和指令
2. .data Section: 数据段 用来保存程序里边设置好的初始化数据信息
3. .rel.text Section: 重定向表Relocation Table 保留的是当前文件里、哪些跳转地址是未知的
4. .symtab Section: 符号表 保留了当前文件里定义的函数和对应的地址信息
连接器会扫描所有的输入目标文件、把所有符号表里的信息收集起来、构成一个全局的符号表、把所有不确定要跳转地址的代码、根据符号表里存储的地址、进行一次修正, 最后 把所有的目标文件的对应段进行一次合并、变成了最终的可执行代码

五、java这样使用虚拟机的编程语言里、程序是如何装载到内存的 ?

1
jvm是上层应用、无需考虑物理分页、一般是直接考虑对象本身的空间大小、物理硬件统一管理由承载jvm的操作系统来解决

六、CPU一直在取下一条指令、为什么还有满载和idle空闲状态呢?

1
2


回顾

超标量: 可以让取指令及指令译码并行进行、

VLIW: 可以搞定指令先后依赖关系、使得一次取一个指令包

最后两个提升CPU性能的设计: 超线程SIMD(单指令多数据流)

超线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
奔腾4处理器失败的重要原因: CPU的流水线级数太深、高达20级、而后期Prescott更是达到了31级、超长流水线、使得各种冒险、并发的方案都用不上

解决冒险、提升并发的方案, 本质上是一种 `指令级并行(Instruction-level parallelism)`
即: CPU 想要在同一个时间、并行的执行两条指令、而代码原本是有先后顺序的、流水线太深的话、之前的分支预测、乱序执行等优化方式都无法起到很好的效果、

2002年底、Pentium4的CPU、第一次引入了超线程(Hyper-Threading技术)
`超线程`: 既然CPU运行在代码层面有前后依赖关系的指令、会遇到各种冒险问题、如果运行完全独立的程序呢? 指令间是没有依赖关系的、那么跟多个CPU运行不同任务、或者多进程、多线程技术有什么区别呢 ?

1.多个CPU上运行不同的程序、单个CPU核心里切换运行不同的线程任务、在同一时间点上、其实一个物理的CPU核心只运行一个线程的指令、所以, 并没有真正做到指令级并行
2. 超线程 是把一个物理CPU核心、伪装成两个逻辑CPU、这个CPU会在硬件层面增加很多电路、使得我们可以在一个CPU核心内部、维护两个不同线程的指令状态信息
eg. 在一个物理CPU核心内部、有双份PC寄存器、指令寄存器及条件码寄存器、可以维护两条并行指令的状态、在外面看起来、好像有两个逻辑层面的CPU同时在运行、也叫`同时多线程`(Simultanneous Multi-Threading, 简称SMT)技术

但: 在CPU其它的功能组件上、eg. 指令译码器和ALU 依然只有一份、因为超线程并不真正运行两个指令、它的目的在于 一个线程A的指令、在流水线停顿的时候、让线程B与执行、利用此时CPU译码器和ALU的空闲来处理、提高CPU利用率

通过很小的CPU代价、实现了同时运行多个线程的效果、通常只要在CPU核心添加10%左右的逻辑功能、增加可以忽略不计的晶体管数量、就可以实现、但同样只能应用在特殊的额应用场景下(线程等待时间较长)
eg. 需要对很多请求的数据库应用、就很适合、各个指令都需要等待访问内存数据,CPU计算并未跑满、但当前指令往往要停顿在流水线上、等待内存数据返回、这时、让CPU里的各个功能单元、处理另一个db查询的案例就很好

image.png

SIMD: 如何加速矩阵算法

1
2
3
4
5
6
7
8
9
10
SIMD: 单指令多数据流(Single Instruction Multiple Data)
SISD: 单指令单数据(Single Instruction Single Data)
多核CPU可以实现MIMD(Multiple Instruction Multiple Data)

为什么SIMD指令可以提高代码执行效率呢?
1.因为SIMD在获取数据和执行指令的时候都是并行的、从内存读数据的时候、SIMD是一次性读多个数据
Intel在引入SSE指令集的时候、在CPU里添加了8128Bits的寄存器、即 16Bytes、一个寄存器一次性可以加载4个整数、比起循环加载、时间就节省了
2. 指令执行层面、4个整数加法、相互之间完全无依赖、也就没有冒险问题要处理、有足够多的FU即可并行执行、这个加法就是4路同时并行的、也会节省时间

基于SIMD的向量计算指令、正是在Intel发布Pentium处理器的时候、被引入的指令集、当时的指令集叫 `MMX`, 即: `Matrix Math eXtensions`的英文缩写、中文名字叫 矩阵数学扩展

image.png

超线程技术是线程级并行的解决方案、很多场景下不一定能带来性能的提升

SIMD是指令级并行的解决方案、在处理向量计算的情况下、同一个向量的不同维度之间的计算是相互独立的、

两者都优化了CPU的使用

为什么需要高速缓存

按照摩尔定律、CPU的访问速度每18个月便会翻一番、相当于每年增长60%、内存的访问速度也在增长、但只有7%左右、使内存和CPU的性能差距不断拉大. 现在一次内存的访问需要120个CPU周期、即: 访问速度有120倍的差距
为了弥补两者的性能差异、真实的把CPU使用起来、现代CPU引入了高速缓存

CPU和内存的性能差距会越来越大.png

1
2
3
4
5
6
7
8
从CPU Cache 被加入CPU开始、内存中的指令、数据会先加载到L1
-L3 Cache中、而不是直接由CPU访问内存去拿、
95%的情况下、CPU只需要访问L1-L3 Cache、从里边读取指令和数据、无需访问内存
注意: CPU Cache不是单存概念上的Cache(eg. 之前说的以内存作为硬盘的缓存)、而是指特定的由SRAM组成的物理芯片

CPU从内存中读取数据到CPU Cache的过程中、是一小块、一小块读取的、不是按照单个数组元素读取的、
这样一小块一小块的数据、在CPU Cache里、称为 `Cache Line`(缓存块)
通常Intel的服务器或者PC里、Cache Line的大小通常是64字节

Cache的数据结构和读取过程是什么样的

1
2
3
现代CPU在数据读取时、无论数据是否已经存储在Cache中、都会先访问Cache
只有Cache中找不到时、才会访问内存、并且将读到的数据写入Cache中、根据时间局部性原理、这样CPU花在等待内存访问上的时间就会大大缩短
通常在基准测试和实际场景中、CPU Cache的命中率可以达到95%以上

与应用相似的架构.png

思考: 那么CPU是如何知道要访问的内存数据、存储在Cache的哪个位置呢 ?

1
2
3
4
5
CPU访问内存数据、是一小块一小块数据来读取的、对于读取内存中的数据、首先拿到的是数据所在的`内存块(Block)`的地址
而直接映射Cache(Direct Mapped Cache)采用的策略就是确保任何一个内存块的地址、
始终映射到一个固定的CPU Cache地址(Cache Line), 而这个映射关系、通常用mod(求余)计算来实现
eg. 将主内存分成0~3132个块、共有8个缓存块、用户需要访问第21号内存块、
21号内存块内容在缓存块中的话、一定在5号缓存块(21%8 = 5)

cache采用mod方式、将内存块映射到对应的CPU Cache.png

思考:
现在13号、5号和21号都应该在5号缓存块中、那如何区分、当前缓存块中是几号内存对应的数据呢 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
对应的缓存块会存储一个组标记、记录当前缓存块内存储的数据对应的内存块、
除组标记外、缓存块中还有两个数据:
1. 从主内存中加载来的实际存放的数据、
2.有效位: 用来标记对应的缓存块中的数据是否是有效的、确保不是机器刚启动时的空数据. 有效位为0时、CPU会直接访问内存、重新加载数据
CPU在读取数据的时候、不是要读取医政股Block、只读取一个需要的整数、称为CPU的一个字Word
具体是哪个字、就用这个字在整个Block中的位置来决定、这个位置称为偏移量Offset
而内存地址对应到Cache Line里的数据结构则多了一个有效位和对应的数据
由 索引 + 有效位 + 组标记 + 数据组成

若: 一个内存中的数据已经在Cache中、访问步骤为:
1. 根据内存地址的地位、计算在Cache中的索引
2. 判断有效位、确认Cache中的数据是有效的
3. 对比内存访问地址的高位、和Cache中的组标记、确认Cache中的数据就是要访问的内存数据、从Cache Line读取对应数据块
4. 根据内存地址的Offset位、从Data Block中读取所需内容
若在23步骤中CPU发现数据不是要访问的数据、CPU会访问内存、将Block Data更新到Cache Line中、同时更新对应的有效位和组标记的数据

内存地址到Cache Line的关系.png

思考:volitile 关键字的作用 ?

1
2
3
4
5
6
7
一种错误理解是 当成锁、认为类似sychronized 关键字、不同的线程对于特定变量的访问会加锁
另一种误解是 当成原子化的操作机制、认为加了volitile、对于变量的自增操作就变成原子性的了

其实: volitile 最核心的知识点要关系到Java内存模型(Java Memory Model)上
虽然JMM只是Java虚拟机这个进程级虚拟机里的一个内存模型、但这个内存模型和CPU、高速缓存和主内存组合在一起的硬件体系很相似
虽然Java内存模型是一个隔离了硬件实现的虚拟机内的抽象模型、但给了一个很好的缓存同步问题的示例.
即: 若我们的数据在不同的线程或者CPU核心里去更新、因为不同的线程或者CPU核有各自的缓存、有可能在A线程的更新B线程看不到

CPU高速缓存的写入

1
2
3
4
5
现在使用的Intel CPU、通常都是多核的、每一个CPU核里、都有独立属于自己的L1、L2的Cache 和 多个CPU共用的L3的Cache、主内存
因为CPU Cache的访问速度要比主内存快很多、L1/L2 Cache的速度也比L3 快、所以、CPU都是尽可能的从CPU Cache中获取数据、而不是每次从主内存获取

Java内存模型里、每个线程都有自己的线程桟、每次数据读取其实是从本地线程桟的Cache副本里读取、而不是主内存
若对数据只读还好、但事实是读写同时存在的、这时思考两个问题:
  1. 写Cache的性能也比写主内存快、那写入时应该写主内存还是Cache ?
  2. 若直接写主内存、Cache里的数据是否会失效呢 ?

写直达 Write-Through

image.png

1
2
3
4
如上图、最简单的一种写入策略, 写直达. 在这个策略里、每一次数据都要写到主内存里、
写入前, 先判断数据是否在Cache中、若在、先更新Cache、再写入主内存; 若不在、只更新主内存
缺点是: 无论数据是否在Cache里、都需要把数据写到主内存、效率较低
类似volatile关键字、始终把数据同步到主内存里

写回 Write-back

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
不把写入同步到主内存、只写CPU Cache呢?是否可行 ?
如上图所示就是只写Cache的策略 -> 每次只把数据写回Cache、只有在CPU Cache里的数据要被替换的时候、才将数据写回主内存

过程: 若发现要写回的数据就在Cache中、只更新Cache里的数据即可、同时标记Cache里这个Block的数据是Dirty(与主存不一致)
若发现要写入的数据对应的Cache Block里、放的是别的内存地址的数据
check下数据是否是Dirty、
若是、先写回主内存然后把当前要写入的数据写入到Cache、同时把Cache Block标记成Dirty
若不是、直接将数据写入Cache、然后把Cache Block标记为Dirty

在使用`写回`这个策略的时候、加载内存数据到Cache里的时候、也要多一步同步脏Cache的动作
若加载内存里的数据到Cache的时候、发现Cache Block里有Dirty标记、需要先把Cache Block的数据写回主内存、才能加载数据覆盖掉Cache

在写回这个策略里、若大量的操作、都能命中缓存、大部分时间都不需要读写主内存、性能会比写直达好

然而、无论写直达还是写回、都未解决多线程或者多CPU缓存一致性问题….

在学习taotao商城这个视频的过程中、用到了fastdfs、刚好自己有一台服务器就自己学习了下、参考了网上多篇文章、感谢各位分享~~

FastDFSc写的开源分布式文件系统、充分考虑了冗余被人、负载均衡、及线性扩容机制、注重高可用、高性能指标、可以很方便的搭建一套高性能的文件服务器集群提供文件上传、下载等服务~

###一、FastDFS架构
FastDFS包括Tracker serverStorage server, 客户端请求Tracker server进行文件上传、下载,通过Tracker server调度最终由Storage server完成文件上传和下载。 Tracker server 的角色类似于dubboregistrymoniter、并不直接提供服务、而是storage server启动时注册到tracker server, client通过tracker server连接storage server, client不知道自己连接的是哪一台storage server, 连接完成后、上传和下载是client直接请求storage server, 可类比于 dubbo consumer通过registry连接dubbo service 但、连接完成之后是consumerservice直接通信

fastdfs.jpg

#####文件上传流程
文件上传流程.png

文件下载流程

文件下载流程.png

####1.Tracker Server
Tracker server作用是负载均衡和调度,通过Tracker server在文件上传时可以根据一些策略找到Storage server提供文件上传服务,可以将tracker称为追踪服务器或调度服务器

####1.Storage Server
Storage server作用是文件存储,客户端上传的文件最终存储在Storage服务器上,Storage server没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器。

二、FastDFS安装

####1.安装libfastcommon

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. 下载:
wget https://github.com/happyfish100/libfastcommon/archive/V1.0.7.tar.gz
2. 修改名字:mv V1.0.7 libfastcommon-1.0.7.tar.gz
3. 解压:tar zxvf libfastcommon-1.0.7.tar.gz
4. cd libfastcommon-1.0.7/
5. 编译:./make.sh
6. 安装:./make.sh install

另外:
设置几个软链接、方便后续扩展nginx时使用:
ln -s /usr/lib64/libfastcommon.so /usr/local/lib/libfastcommon.so
ln -s /usr/lib64/libfastcommon.so /usr/lib/libfastcommon.so
ln -s /usr/lib64/libfdfsclient.so /usr/local/lib/libfdfsclient.so
ln -s /usr/lib64/libfdfsclient.so /usr/lib/libfdfsclient.so

2. 安装 tracker

1
2
3
4
5
6
7
1. 下载:
wget https://github.com/happyfish100/fastdfs/archive/V5.05.tar.gz
2. 修改名字:mv V5.05 FastDFS_v5.05.tar.gz
3. 解压:tar zxvf FastDFS_v5.05.tar.gz
4. 进入解压后目录:cd fastdfs-5.05/
5. 编译:./make.sh
6. 安装:./make.sh install

3. 修改tracker配置文件

1
2
3
4
5
6
7
8
2安装完成后、在/etd/fdfs下有tracker的配置文件
复制一份:cp /etc/fdfs/tracker.conf.sample /etc/fdfs/tracker.conf
mkdir -p /usr/local/fastdfs/ (此处可以根据自己的情况和习惯存放)
base_path= /usr/local/fastdfs/

启动 tracker 服务:/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf
重启 tracker 服务:/usr/bin/fdfs_trackerd /etc/fdfs/tracker.conf restart
查看是否有 tracker 进程:ps aux | grep tracker

####4. storage(存储节点)服务部署

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
一般 storage 服务我们会单独部署到一台服务器上,但是这里为了方便(~~我只有一台服务器~~)就安装在同一台上了

如果单独部署到一台服务器上、上边tracker的部署步骤重新来一遍即可
这里是同一台server、只修改配置~~

复制一份配置:cp /etc/fdfs/storage.conf.sample /etc/fdfs/storage.conf
编辑:vim /etc/fdfs/storage.conf
base_path= /usr/local/fastdfs/

创建目录:mkdir /usr/local/fastdfs/storage/images-data
store_path0= /usr/local/fastdfs/storage/images-data

图片实际存放路径,如果有多个,这里可以有多行(要创建多个目录):
store_path0=/opt/fastdfs/storage/images-data0
store_path1=/opt/fastdfs/storage/images-data1

subdir_count_per_path=256
是用来配置目录个数的、如果只是练习不做实际存储服务、可改小一点儿

指定 tracker 服务器的 IP 和端口
tracker_server=192.168.1.114:22122 (192.168.1.114)是你的server服务器ip、本机也可以使用(0.0.0.0:22122)、记得不可使用127.0.0.1

5. 测试服务

1
2
3
启动 storage 服务:/usr/bin/fdfs_storaged /etc/fdfs/storage.conf,首次启动会很慢,因为它在创建预设存储文件的目录
重启 storage 服务:/usr/bin/fdfs_storaged /etc/fdfs/storage.conf restart
查看是否有 storage 进程:ps aux | grep storage

####6. 查看tracker是否可以正常与storage通信

1
2
3
4
5
6
7
8
fdfs_monitor /etc/fdfs/storage.conf
...
Storage 1:
id = 192.168.2.231
ip_addr = 192.168.2.231 ACTIVE --若看到ACTIVE这个字样、代表可以正常通信
...
查看storage和tracker是否正常启动:
ps aux | grep fdfs

进程.png

7. 使用fdfs_client测试

1
2
3
4
5
6
7
8
9
10
11
12
13
复制一份配置:cp /etc/fdfs/client.conf.sample /etc/fdfs/client.conf
编辑:vim /etc/fdfs/client.conf

base_path= /usr/local/fastdfs/

指定 tracker 服务器的 IP 和端口
tracker_server=192.168.1.114:22122
log_level=info

echo asasasa > ~/test.txt

测试:fdfs_test /etc/fdfs/client.conf upload ~/test.txt
可以看到如下图所示、就是上传成功了

test.png

####8. 安装Nginx和其插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
如果Nginx已经安装过,则仅需要fastdfs-nginx-module_v1.16.tar.gz
下载nginx:wget http://nginx.org/download/nginx-1.11.8.tar.gz
下载Nginx插件:wget http://jaist.dl.sourceforge.NET/project/fastdfs/FastDFS%20Nginx%20Module%20Source%20Code/fastdfs-nginx-module_v1.16.tar.gz
解压 Nginx 模块:tar zxvf fastdfs-nginx-module_v1.16.tar.gz
进入解压后的目录
cd fastdfs-nginx-module
vim src/config
修改:去掉local、因为实际安装fastdfs时、是放到了/usr/include
1. CORE_INCS="$CORE_INCS /usr/local/include/fastdfs /usr/local/include/fastcommon/"
-> CORE_INCS="$CORE_INCS /usr/include/fastdfs /usr/include/fastcommon/"

2. CORE_LIBS="$CORE_LIBS -L/usr/local/lib -lfastcommon -lfdfsclient"
-> CORE_LIBS="$CORE_LIBS -L/usr/lib -lfastcommon -lfdfsclient"

回到nginx的解压目录
cd ../nginx-1.11.8
sudo ./configure --prefix=/usr/local/nginx --sbin-path=/usr/local/bin/nginx --conf-path=/usr/local/etc/nginx/nginx.conf --pid-path=/usr/local/var/run/nginx.pid --lock-path=/usr/local/var/run/nginx.lock --error-log-path=/usr/local/var/log/nginx/^Cror.log --http-log-path=/usr/local/var/log/nginx/access.log --with-http_gzip_static_module --with-http_stub_status_module --with-http_ssl_module --with-file-aio --add-module=/home/nj/build/fastdfs-nginx-module/src

sudo make && sudo make install (若是有权限的账户、可以不用加sudo、我使用的是普通用户)

####9. 整个fastdfs-nginx-module和nginx

1
2
3
4
5
6
7
8
copy fastdfs-nginx-module的配置文件到 /etc/fdfs下、方便查找
cp /home/nj/build/fastdfs-nginx-module/src/mod_fdfs.conf /etc/fdfs
vi /etc/fdfs/mod_fdfs.conf

base_path=/usr/local/fastdfs
tracker_server=192.168.1.114:22122
url_have_group_name = true
store_path0=/usr/local/fastdfs/storage

####10. 然后配置Nginx,添加如下内容

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 80;
server_name localhost;

...

# 配置fastdfs的访问路径
location /group1/M00 {
ngx_fastdfs_module;
}
...
}
启动nginx

用浏览器访问刚才步骤7中测试上传的文件:

1
http://xxxx/group1/M00/00/00/fwAAAVu0UTSAZiNHAAAACE6c2W4921_big.txt

图片存储测试.png

^.^哦啦~ 到此安装完成、可以使用了~

####另外:

1
2
3
fastdfs提供了php_client、可以使用php调用fastdfs的服务
参考:
https://github.com/happyfish100/fastdfs/tree/master/php_client/FastDFS%20Nginx%20Module%20Source%20Code/fastdfs-nginx-module_v1.16.tar.gz

参考资源

  1. https://github.com/happyfish100/fastdfs/tree/master/php_client
  2. https://www.waitig.com/fastdfs%E5%AE%89%E8%A3%85%E6%AD%A5%E9%AA%A4.html
  3. https://www.codetd.com/article/3138763
  4. http://soartju.iteye.com/blog/803477
    等多个网络资源…

年轻代log解读

年轻代以ParNew收集器为例, 采用的是复制算法, log如下:
image.png

1
2
3
4
5
6
7
8
9
10
11
2019-02-01T21:18:15:00.382+0800: 718675.758: Application time: 0.7177964 seconds
{Heap before GC invocations=65632 (full 18):
par new generation total 471872K, used 421490K [0x0000000080000000, 0x00000000a0000000, 0x00000000a0000000)
eden space 419456K, 100% used [0x0000000080000000, 0x00000000999a0000, 0x00000000999a0000)
from space 52416K, 3% used [0x00000000999a0000, 0x0000000099b9c830, 0x000000009ccd0000)
to space 52416K, 0% used [0x000000009ccd0000, 0x000000009ccd0000, 0x00000000a0000000)
concurrent mark-sweep generation total 1572864K, used 905696K [0x00000000a0000000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 104594K, capacity 113116K, committed 113664K, reserved 1148928K
class space used 12280K, capacity 13738K, committed 13824K, reserved 1048576K

2019-02-01T18:15:00.383+0800: 718675.758: [GC (Allocation Failure) 2019-02-01T18:15:00.384+0800: 718675.759: [ParNew: 109872K->5255K(118016K), 0.0211147 secs] 142365K->37748K(511232K), 0.0212949 secs] [Times: user=0.26 sys=0.04, real=0.02 secs]

其中:
*Heap before GC invocations=65632 (full 18): invocations=65632表示经历的gc的次数 full 18表示经历的full gc的次数

  • 2019-02-01T18:15:00.383:发生minor gc的时间
  • 718675.758:GC时、相对GVM启动时间的偏移量
  • ParNew 收集器的名称 , 会STW(https://www.jianshu.com/p/d07194a20ec1)
  • 109872K->5255K:收集前后年轻代的使用情况
  • 118016K:整个年轻代的容量
  • 0.0211147 secs: stw的时间
  • 142365K->37748K:GC前后整个堆的使用情况
  • 511232K:整个堆的容量
  • 0.0212949 secs:ParNew 收集器标记和复制年轻代活着的对象所花费的时间(包括和老年代通信的开销、对象晋升到老年代开销、垃圾收集周期结束一些最后的清理对象等的花销)
  • user:GC 线程在垃圾收集期间所使用的 CPU 总时间
  • sys:系统调用或者等待系统事件花费的时间
  • real: 应用被暂停的时钟时间、由于GC是多线程的、real可能会小于 user+sys, 如果是单线程的、real是接近 user+sys的时间的

CMS log 大致解读

1
2
3
4
5
6
7
8
9
10
11
12
13
2018-04-12T13:48:26.233+0800: 15578.148: [GC [1 CMS-initial-mark: 6294851K(20971520K)] 6354687K(24746432K), 0.0466580 secs] [Times: user=0.04 sys=0.00, real=0.04 secs]
2018-04-12T13:48:26.280+0800: 15578.195: [CMS-concurrent-mark-start]
2018-04-12T13:48:26.418+0800: 15578.333: [CMS-concurrent-mark: 0.138/0.138 secs] [Times: user=1.01 sys=0.21, real=0.14 secs]
2018-04-12T13:48:26.418+0800: 15578.334: [CMS-concurrent-preclean-start]
2018-04-12T13:48:26.476+0800: 15578.391: [CMS-concurrent-preclean: 0.056/0.057 secs] [Times: user=0.20 sys=0.12, real=0.06 secs]
2018-04-12T13:48:26.476+0800: 15578.391: [CMS-concurrent-abortable-preclean-start]
2018-04-12T13:48:29.989+0800: 15581.905: [CMS-concurrent-abortable-preclean: 3.506/3.514 secs] [Times: user=11.93 sys=6.77, real=3.51 secs]
2018-04-12T13:48:29.991+0800: 15581.906: [GC[YG occupancy: 1805641 K (3774912 K)]2018-04-12T13:48:29.991+0800: 15581.906: [GC2018-04-12T13:48:29.991+0800: 15581.906: [ParNew: 1805641K->48395K(3774912K), 0.0826620 secs] 8100493K->6348225K(24746432K), 0.0829480 secs] [Times: user=0.81 sys=0.00, real=0.09 secs]2018-04-12T13:48:30.074+0800: 15581.989: [Rescan (parallel) , 0.0429390 secs]2018-04-12T13:48:30.117+0800: 15582.032: [weak refs processing, 0.0027800 secs]2018-04-12T13:48:30.119+0800: 15582.035: [class unloading, 0.0033120 secs]2018-04-12T13:48:30.123+0800: 15582.038: [scrub symbol table, 0.0016780 secs]2018-04-12T13:48:30.124+0800: 15582.040: [scrub string table, 0.0004780 secs] [1 CMS-remark: 6299829K(20971520K)] 6348225K(24746432K), 0.1365130 secs] [Times: user=1.24 sys=0.00, real=0.14 secs]
2018-04-12T13:48:30.128+0800: 15582.043: [CMS-concurrent-sweep-start]
2018-04-12T13:48:36.638+0800: 15588.553: [GC2018-04-12T13:48:36.638+0800: 15588.554: [ParNew: 3403915K->52142K(3774912K), 0.0874610 secs] 4836483K->1489601K(24746432K), 0.0877490 secs] [Times: user=0.84 sys=0.00, real=0.09 secs]
2018-04-12T13:48:38.412+0800: 15590.327: [CMS-concurrent-sweep: 8.193/8.284 secs] [Times: user=30.34 sys=16.44, real=8.28 secs]
2018-04-12T13:48:38.419+0800: 15590.334: [CMS-concurrent-reset-start]
2018-04-12T13:48:38.462+0800: 15590.377: [CMS-concurrent-reset: 0.044/0.044 secs] [Times: user=0.15 sys=0.10, real=0.04 secs]

由于CMS涉及的阶段比较多、分阶段来说下~

初始标记 initial mark阶段

STW中的一次、为了标记直接被GC root引用或者年轻代 存活对象引用的所有对象(https://plumbr.io/handbook/garbage-collection-algorithms-implementations#concurrent-mark-and-sweep)
image.png

上述示例的对应信息为:

1
2018-04-12T13:48:26.233+0800: 15578.148: [GC [1 CMS-initial-mark: 6294851K(20971520K)] 6354687K(24746432K), 0.0466580 secs] [Times: user=0.04 sys=0.00, real=0.04 secs]

其中:

  • 2018-04-12T13:48:26.233+0800:GC开始时间(同 ParNew收集器)
  • 15578.148:JVM运行时间(同 ParNew收集器)
  • CMS-initial-mark:CMS当前阶段(这里代表 初始标记)
  • 6294851K:当前老年代使用的容量
  • (20971520K):老年代可使用的最大容量
  • 6354687K:整个堆目前使用的情况
  • (24746432K):整个堆当前可用情况
  • 0.0466580 secs:该阶段持续时间
  • [Times: user=0.04 sys=0.00, real=0.04 secs]ParNew收集器
并发标记阶段(Concurrent Mark)

遍历老年代,然后标记所有存活的对象、会根据上个阶段找到的 GC Roots 遍历查找,与用户的应用程序并发运行
注意不是所有的老年代存活对象都会被标记、因为在标记期间引用关系可能会发生改变(https://plumbr.io/handbook/garbage-collection-algorithms-implementations#concurrent-mark-and-sweep)、
image.png
与上图对比可用发现已经有一个对象的引用关系发生了改变

本阶段log如下:

1
2
>2018-04-12T13:48:26.280+0800: 15578.195: [CMS-concurrent-mark-start]
>2018-04-12T13:48:26.418+0800: 15578.333: [CMS-concurrent-mark: 0.138/0.138 secs] [Times: user=1.01 sys=0.21, real=0.14 secs]
  • CMS-concurrent-mark: 并发收集阶段, 遍历老年代、标记所有存活对象
  • 0.138/0.138 secs这个阶段的持续时间时钟时间
  • [Times: user=1.01 sys=0.21, real=0.14 secs]:同上、但是从并发标记的开始时间计算的、期间是并发进行、所以参考意义不大(包含的不仅仅是gc线程的工作)
并发预清理(concurrent preclean)

也是一个并发阶段,与应用的线程并发运行不会 stop应用的线程
在并发运行的过程中、一些对象的引用可能会发生变化、发生这种情况时、jvm会将这个对象的区域Card标记为Dirty,也就是Card Marking

image.png
preclean阶段、能够从Dirty对象可到达的对象也会被标记、标记完成之后、Dirty Card就会被清除了、
image.png
这个阶段的log如下:

1
2
2018-04-12T13:48:26.418+0800: 15578.334: [CMS-concurrent-preclean-start]
2018-04-12T13:48:26.476+0800: 15578.391: [CMS-concurrent-preclean: 0.056/0.057 secs] [Times: user=0.20 sys=0.12, real=0.06 secs]

CMS-concurrent-preclean 阶段名称、对前边并发标记阶段中引用发生变化的对象进行标记
0.056/0.057 secs 这个阶段持续的时间与时钟时间
[Times: user=0.20 sys=0.12, real=0.06 secs]并发标记阶段

可中止的并发预清理(Concurrent Abortable Preclean)

并发 & 不影响用户线程, 是为了 尽量承担STW中的最终标记阶段的工作, 这个阶段是在重复做很多相同的工作,直接满足一些条件(比如:重复迭代的次数、完成的工作量或者时钟时间等)
log如下:

1
2
>2018-04-12T13:48:26.476+0800: 15578.391: [CMS-concurrent-abortable-preclean-start]
>2018-04-12T13:48:29.989+0800: 15581.905: [CMS-concurrent-abortable-preclean: 3.506/3.514 secs] [Times: user=11.93 sys=6.77, real=3.51 secs]

CMS-concurrent-abortable-preclean阶段名称
3.506/3.514 secs通常在5s左右、
[Times: user=11.93 sys=6.77, real=3.51 secs]同预清理阶段
主要做了两件事:

  • 处理 From 和 To 区的对象,标记可达的老年代对象
  • 和上一个阶段一样,扫描处理Dirty Card中的对象

具体执行多久,取决于许多因素,满足其中一个条件将会中止运行:

  • 执行循环次数达到了阈值
  • 执行时间达到了阈值
  • 新生代Eden区的内存使用率达到了阈值
最终标记阶段(Final Remark)

第二个 STW阶段, 也是最后一个、目标是标记所有老年代所有的存活对象,由于之前的阶段是并发执行的,gc 线程可能跟不上应用程序的变化,为了完成标记老年代所有存活对象的目标,STW 就非常有必要了, 通常 CMS 的 Final Remark 阶段会在年轻代尽可能干净的时候运行,目的是为了减少连续 STW 发生的可能性(年轻代存活对象过多的话,也会导致老年代涉及的存活对象会很多)。这个阶段会比前面的几个阶段更复杂一些,相关日志如下:

1
2
>2018-04-12T13:48:29.991+0800: 15581.906: 
>[GC[YG occupancy: 1805641 K (3774912 K)]2018-04-12T13:48:29.991+0800: 15581.906: [GC2018-04-12T13:48:29.991+0800: 15581.906: [ParNew: 1805641K->48395K(3774912K), 0.0826620 secs] 8100493K->6348225K(24746432K), 0.0829480 secs] [Times: user=0.81 sys=0.00, real=0.09 secs]2018-04-12T13:48:30.074+0800: 15581.989: [Rescan (parallel) , 0.0429390 secs]2018-04-12T13:48:30.117+0800: 15582.032: [weak refs processing, 0.0027800 secs]2018-04-12T13:48:30.119+0800: 15582.035: [class unloading, 0.0033120 secs]2018-04-12T13:48:30.123+0800: 15582.038: [scrub symbol table, 0.0016780 secs]2018-04-12T13:48:30.124+0800: 15582.040: [scrub string table, 0.0004780 secs] [1 CMS-remark: 6299829K(20971520K)] 6348225K(24746432K), 0.1365130 secs] [Times: user=1.24 sys=0.00, real=0.14 secs]

YG occupancy: 1805641 K (3774912 K) 年轻代当前占用量及容量
ParNew:...触发了一次young gc, 触发的原因是为了减少 年轻代的存活对象、尽量是年前代干净一些
[Rescan (parallel) , 0.0429390 secs] 这个 Rescan 是当应用暂停的情况下完成对所有存活对象的标记,这个阶段是并行处理的,这里花费了 0.0429390s
[weak refs processing, 0.0027800 secs] 第一个子阶段,它的工作是处理弱引用
[class unloading, 0.0033120 secs] 删除无用class
[scrub symbol table, 0.0016780 secs] ... [scrub string table, 0.0004780 secs] 最后一个子阶段, cleaning up symbol and string tables which hold class-level metadata and internalized string respectively
6299829K(20971520K) 这个阶段之后,老年代的使用量与总量
6348225K(24746432K)这个阶段后,堆的使用量与总量
0.1365130 secs阶段持续时长
[Times: user=1.24 sys=0.00, real=0.14 secs]对应时间信息

并发清理(Concurrent Sweep)

不需要 STW,它是与用户的应用程序并发运行, 清除那些不再使用的对象,回收它们的占用空间为将来使用(https://plumbr.io/handbook/garbage-collection-algorithms-implementations#concurrent-mark-and-sweep)
image.png
log如下(这中间又发生了一次 Young GC):

1
2
3
2018-04-12T13:48:30.128+0800: 15582.043: [CMS-concurrent-sweep-start]
2018-04-12T13:48:36.638+0800: 15588.553: [GC2018-04-12T13:48:36.638+0800: 15588.554: [ParNew: 3403915K->52142K(3774912K), 0.0874610 secs] 4836483K->1489601K(24746432K), 0.0877490 secs] [Times: user=0.84 sys=0.00, real=0.09 secs]
2018-04-12T13:48:38.412+0800: 15590.327: [CMS-concurrent-sweep: 8.193/8.284 secs] [Times: user=30.34 sys=16.44, real=8.28 secs]

*CMS-concurrent-sweep 阶段名称: 主要是清除那些没有被标记的对象,回收它们的占用空间

  • 8.193/8.284 secs 这个阶段的持续时间与时钟时间
  • [Times: user=30.34 sys=16.44, real=8.28 secs] 同上
并发重置(Concurrent Reset)

这个阶段也是并发执行的,它会重设 CMS 内部的数据结构,为下次的 GC 做准备

1
2
2018-04-12T13:48:38.419+0800: 15590.334: [CMS-concurrent-reset-start]
2018-04-12T13:48:38.462+0800: 15590.377: [CMS-concurrent-reset: 0.044/0.044 secs] [Times: user=0.15 sys=0.10, real=0.04 secs]

*CMS-concurrent-reset 阶段名称

  • 0.044/0.044 secs 这个阶段的持续时间与时钟时间
  • [Times: user=0.15 sys=0.10, real=0.04 secs] 同上

小结:

CMS 通过将大量工作分散到并发处理阶段来在减少 STW 时间,在这块做得非常优秀,但是 CMS 也有一些其他的问题:

  • CMS 收集器无法处理浮动垃圾( Floating Garbage),可能出现 “Concurrnet Mode Failure” 失败而导致另一次 Full GC 的产生,可能引发串行 Full GC;
  • 空间碎片,导致无法分配大对象,CMS 收集器提供了一个 -XX:+UseCMSCompactAtFullCollection 开关参数(默认就是开启的),用于在 CMS 收集器顶不住要进行 Full GC 时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长;
  • 对于堆比较大的应用上,GC 的时间难以预估

so. G1开始兴起、

  • 由于G1采用复制算法、很好的避免了 空间碎片
  • 采用region分区思想、解决了停顿时长可控
  • 不过貌似并没有从根源上解决 浮动垃圾的问题~~

参考:

####基本概念:

新生代:大多数对象在eden区生产、很多对象的什么周期很短、每次新生代的垃圾回收(minor gc)后只有少量对象存活
所以选用的是复制算法、只需要少量的复制成本就可以完成回收
young generation 一般又分为 eden区 和 survivor 区(一般2survivor区)
大部分对象在eden区生产、eden区满时、还存活的对象被复制到两个survivor区中的一个、这个survivor区满时
此区存活切不满足晋升条件的对象将被复制到另一个survivor区(对象每经历一次minor gcage + 1,
到达年龄阈值后、被放到老年代old generation, 在SerialParNew GC)两种垃圾回收器中、晋升年龄阈值
由参数 MaxTenuringThreshould设定、默认15

老年代:old generation在新生代中经历了n次垃圾回收后仍然存活的对象、会被放到老年代、改区域对象存活的几率比较高
老年代的垃圾回收(Major gc)通常使用 标记-清理或者标记-整理算法,整堆的回收(包括young和old)称为full gc
(HotSpot VM)中、除了CMS之外、其它能收集老年代的gc都会同时回收整个GC堆、包括新生代

永久代: perm generation主要存放元数据、eg. class, method的元信息、与垃圾回收要回收的关系不大、相对新生代
和老年代、改区域对垃圾回收的影响较小

常见的垃圾回收器

新生代垃圾收集器
  1. Serial:串行收集器、采用复制算法、jdk1.3之前的唯一选择、单线程、进行垃圾收集时、会STW
    `jdk1.3`之后又很多优秀收集器、单由于它 对于限定单个cpi的环境来说、没有线程交互的开销、简单高效
    至今仍然是 hotspot 虚拟机运行在client模式下的默认新生代收集器

serial & serial old.png

  1. ParNewSerial的多线程版本、除了使用多线程进行垃圾收集外、其余行为包括Serial收集器可用的控制参数
    `收集算法`, `stw`, `对象分配规则`, `回收策略`等 与`Serial`收集器的完全相同、两者公用了很多代码
    ParNew收集器除了使用多线程收集外、其它与Serial收集器并无太多创新之处、`但`是在Server模式下的首选
    一个与性能无关的原因:除Serial外、它是唯一能够和`CMS`收集器(`Concurrent Mark Sweep`)配合工作的         
    `ParNew` 收集器在 `单CPU` 的环境中、不会有比`Serial`收集器更好的效果、甚至由于存在线程交互的开销
    在两个CPU的环境中都不能百分之百的保证超越、在多CPU的环境下、随着`CPU的增加`、在GC时、
    对资源的有效利用是有好处的、默认开启的`收集线程`数和`CPU的数量`相同, 在CPU非常多的时候、可以通过 
    `-XX:ParallerGCThreads`参数设置

ParNew & Serial Old.png

  1. Parallel Scavenge: 收集器是并行多线程 新生代收集器, 使用复制算法
    它与其它收集器的关注点不同、CMS等收集器的关注点是:尽可能缩短垃圾收集时用户线程的停顿时间
    而Parallel Scavenge的关注点是:达到一个可控制的吞吐量
    `停顿短`适合需要与用户交互的程序、良好的响应可以提高用户体验
    `高吞吐量`则可以高效的利用CPU时间、尽快的完成任务、适合在后台运算无需太多交互的任务
    `Parallel Scavenge`收集器提供了一个开关参数 `-XX:UseAdaptiveSizePolicy`指定后可以不用人工指定
    新生代大小(-Xmn)、Eden和Survivor区的比率(-XX:SurvivorRatio)、
    晋升老年代年龄(-XX:PretenureSizeThreshold)等参数细节了、虚拟机会根据当前系统的运行情况、动态调整参数
    这种方式称为:`GC的自适应调节策略`
    `注意` Parallel Scavenge收集器无法和CMS配合使用、在`JDK1.6`推出`Parallel Old`之前、
    只能和`Serail Old`收集器一起使用
    ·
    Paralle Scavenge.png
老年代收集器
  1. Serial OldSerial收集器的老年代版本、单线程, 使用标记-整理(Mark-Compact)算法

    1) JDK1.5及之前的版本(Parallel Old)诞生之前、与 `Parallel Scavenge`配合使用
    2) 作为CMS收集器的后备预案、在并发手机发生 `Concurrent Mode Failuer`时使用
  2. Parallel OldParallel Scavenge的老年代版本, 使用多线程标记-整理算法, JDK1.6的版本才开始提供

    在它出现之后`吞吐量优先`才有了名副其实的应用组合、在注重吞吐量和cpu敏感的场合、都可以优先考虑
    `Parallel Scavenge`和`Parallel Old`组合     
  3. CMS:Concurrent Mark Sweep收集器是一种以最短回收停顿时间为目标的收集器、使用标记-清除算法
    基本流程:
    1) 初始标记:CMS Initial mark仅标记GC Roots能直接关联的对象、速度很快, 会 STW
    2) 并发标记:CMS Concurrent markGC Roots Tracing的过程、耗时最长、但不会STW
    3) 重新标记:CMS remark为了修正并发标记期间用户程序继续访问导致标记产生变动的对象的标记

    时间会比初始标记长、单远小于并发标记, 需要 STW

    4) 并发清除: CMS concurrent sweep
    缺点:1) CPU资源敏感、在并发阶段不会导致停顿、单会占用CPU资源、导致应用程序变慢、总的吞吐量降低

      默认启动的回收线程数是`(CPU数+3)/4`,即不少于25%的CPU资源、且随着cpu的增加而下降
      但、CPU资源不足时、cms对用户程序的影响可能变大、若CPU的负载本身就很高、还要分出一般运算能力
      去执行垃圾收集、系统也会有明显的卡顿
    2)无法处理浮动垃圾`Floating Garbage`可能出现`Concurrent Mode Failure`而导致Full gc
       由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。
       出现在标记过程之后,无法再当次收集中处理掉它们,只能待下一次GC时再清理掉。这一部分垃圾就被称为`浮动垃圾`
    3) 产生空间碎片: 空间碎片过多时、会导致老年代空间有剩余、单无法找到足够大连续空间来分配当前对象

cms.png

  1. G1 收集器
    G1: Garbage First收集器是新的、面向Server应用、HotSpot团队赋予它的使命是:在将来替换掉CMS收集器
    1) 并发与并行:G1可充分利用多CPU、多核条件下的硬件优势、使用多个CPU来缩短STW的停顿时间、
       部分收集器原本需要停顿Java线程执行的GC动作、G1可以通过并发的方式让Java程序继续执行
    2) 分代收集: G1可以不需要其它收集器的配合自己管理整个GC堆、可以采用不同方式去处理新创建的对象和
       已存活一段时间、经过了n次GC的old对象来获取更好的收集效果            
    3) 空间整合: G1从整体来看是基于`标记-整理`实现的, 从局部(两个Region间)来看是基于复制的
       所以、在G1运行期间不会产生大量的内存空间碎片、避免提前触发full gc
    4) 可预测的停顿: G1和CMS的共同关注点是降级停顿、但G1除了降低停顿之外、还能建立可预测的停顿时间模型
       让使用者明确指定在一个长度为M ms的时间片段内、消耗在GC上的时间不得超过N ms、
       这几乎是实时Java(RTSJ)的垃圾收集器的特征了

G1收集器:https://www.jianshu.com/p/5a95ce2bbb36

参考

常见的索引模型

1
2
3
4
5
1. hash 一种key-value结构、k-v映射、写入、等值查询很快、但范围查询比较慢、但适用于 等值查询的场景
2. 有序数组在等值查询和范围查询的性能表现上都很优秀
但在插入时必须要挪动后续所有记录、成本太高、
所以只适用于静态存储. eg. 城市信息表这种不经常变动的数据
3. 树 在查询和写入的效率上都还不错

hash表示意图.png

innodb的索引模型

在innodb中、表都是根据主键顺序、以索引的形式存放的, 这种存储方式的表称为索引组织表
innodb使用了B+树索引模型,数据都是存储在B+树的、每个索引对应一颗B+树

1
2
3
4
5
6
create table T-inx(
id int primary key ,
k int not null ,
name varchar(16) ,
index (k)
)engine=innodb

i索引树示意图.png

1
2
3
从图中可以看出、索引树分为主键索引和非主键索引两种类型、
主键索引存储的是整行数据、在innodb里, 主键索引也被称为聚簇索引(clustered index).
非主键索引的叶子节点内容是主键值, 也称为二级索引(secondary index).

Q: 那么基于主键索引 和 基于普通索引的查询有什么区别 ?

1
2
3
4
若 sql 是 select * from T where ID=500; 即主键查询的方式、则只需要搜索ID这颗 B+ 树
若 sql 是 select * from T where k=5, 则需要先搜索k这颗索引树、得到id的值为500, 再到ID索引树搜索一次、这个过程称为 回表

也就是: 基于非主键索引的查询需要多扫描一颗索引树, 所以需要尽量的使用主键查询

Q: 索引维护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
B+ 树为了维护索引有序性、在插入新值的时候需要做必要的维护.
若插入的新值较大、只需要再最后插入新的记录、若插入的值为中间值、则相对麻烦,需要先挪动后边的数据、腾出位置
更糟糕的是: 若最后一个数据所在的数据页已经满了、根据B+树的算法、需要申请一个新的数据页、然后挪动部分数据过去
这个过程称为 页分裂, 除了性能外、页分裂还影响数据页的利用率

当相邻页有数据删除之后、由于数据页的利用率很低、会进行页合并

场景: 自增主键的作用?
插入新的记录时、系统会获取当前最大值+1 作为下一条记录的id
即: 自增主键的插入数据模式、正好符合了递增插入、不涉及挪动其它记录、也不会触发叶子节点的分裂
而有业务逻辑的字段做主键、则往往不能保证有序写入、这样写数据的成本相对高

从存储上看、主键长度越小、普通索引的叶子节点就越小、索引占用的空间也就越小

so. 从性能和存储上来看、自增主键往往是最合理的选择

表初始化语句

1
2
3
4
5
6
7
create table T-index(
ID int primary key,
k int not null default '',
index k(k)
)engine=Innodb;

insert into T-index values (100, 1, 'aa'), (200, 2, 'bb'), (300, 3, 'cc'), , (500, 5, 'ee'), (600, 6,'ff'),(700,7,'gg');

Q: 执行sqlselect * from T-index where k between 3 and 5需要执行几次树的搜搜操作、会扫描多少行 ?
A: 执行流程:

1
2
3
4
5
6
7
1. 在 k 索引树上找到 k=3 的记录、取得id=300;
2. 再到 id 索引树上查找 id=300 对应的记录R3;
3. 在 k 索引树上找到 下一个值 k=5, 取得 id=500;
4. 再回到 id 索引树上找到 id=500 对应的记录 R4;
5. 在k索引树上取下一个值 k=6, 不满足条件、循环两次.

在步骤 24中、回到主键索引查找数据的过程、称为回表

所以、一共执行了 3次搜索、2次回表、扫描3行

覆盖索引

若执行的语句是
select ID from T-index where k between 3 and 5;
此时只需要查询 ID 的值、而ID的值已经在 K 索引树上了、可以直接提供查询结果、
无需回表、即: 在本次查询中、索引 k 已经覆盖了查询需求、
称为覆盖索引

  • 在引擎内部使用覆盖索引在索引 k 上其实读了3个记录、R3~R5、
    但是对于 MySQL的server来说、只从引擎拿到了2条记录、认为扫描行是2

so. 覆盖索引在一定程度上可以减少回表扫描的次数

Q: 在一个市民表上、是否有必要将身份证和名字建立联合索引 ?

1
2
3
4
5
6
7
8
9
10
create  table `user` (
`id` int(11) not null ,
`id_card` varchar(32) default null ,
`name` varchar(32) default null ,
`age` int(11) default null ,
`ismale` tinyint(1) default null ,
PRIMARY key (`id`) ,
key `id_card` (`id_card`),
key `name_age` (`name`, `age`)
) engine=Innodb

A:

1
2
3
4
5
6
7
这种情况需要看实际的业务场景:
1. 若经常的查询需求是:
根据身份证号查询市民信息、则 只需要在身份证字段上建立索引, 再建(id,name)的联合索引就会浪费空间

2. 若经常的查询需求是:
根据身份证号查询name, 则建立联合索引就有很大的意义、它可以在这个高频请求上用到覆盖索引,
不在需要回表查询, 提高查询速度

Q: 如果现在有一个非高频请求、根据身份证号查询家庭地址, 需要再设计一个联合索引么 ?
A:

1
2
3
4
5
索引项是按照索引出现的字段排序的:
eg. 利用(name, age)的联合索引查找所有名字是 zhangsan 的人时, 可快速定位到 ID4, 然后向后遍历得到所有结果
不只是索引的全部定义. 只要满足最左前缀原则、就可以利用索引来加速检索
所以: 基于已经建立了 (id_card, name) 的联合索引、无需再建立 (id_card, addr)的联合索引、
利用最左前缀的原则、它可以使用 (id_card, name) 的联合索引

索引下推

思考:
以(name, age)联合索引为例、需要检索所有 名字第一个字为张,且年龄为10的男孩
SQL如下:

1
select * from user where name like `张%` and age=10 and ismale=1;

拿到记录行之后、还要进一步判断其它条件是否满足、这个怎么处理的呢?

1
2
3
4
5
6
7
1. 在MySQL 5.6 之前、只能从 ID3 开始、一个个回表. 在主键索引上找到数据行、再对比字段值
2.5.6 引入了索引下推(index condition pushdown), 可以在索引遍历的过程中对索引包含的字段优先判断
过滤掉不满足条件的记录、减少回表次数

在无索引下推时、innodb不会看age的值、只是顺序把 `name`第一个字是`张`的记录取出、回表、需要回表4
有索引下推时、innodb在(name, age)内部就判断了age是否=10,不等于10的记录、直接判断并跳过
只需要取回ID4、ID5两条记录、所以只需要回表2

Q: 为什么需要重建索引?

1
2
索引因为删除、或者页分裂等原因、导致数据页有空洞、重建索引的过程会创建一个新的索引、把数据按顺序插入、这样页面的利用率最高、
也就是索引更紧凑、更省空间

Q:

1
2
3
4
5
6
7
8
9
10
11
12
场景: DBA入职时、发现自己接手维护的库、有一个表定义:

create table `geek`(
`a` int(11) not null,
`b` int(11) not null,
`c` int(11) not null,
`d` int(11) not null,
primary key(`a`, `b`),
key `c`(`c`),
key `ca`(`c`, `a`),
key `cb`(`c`, `b`)
)engine=innodb;

为什么需要ca、cb这两个联合索引呢 ?同事的解释是: 因为业务常用下边的sql:

1
2
select * from geek where c=N order by a limit 1;
select * from geek where c=N order by b limit 1;

那么这种解释对么 ?

1
2
3
4
主键a、b的聚簇索引组织顺序相当于 order by a, b 即: 先按a排序、再按b排序、c无序
索引 ca的组织是先按c排序、再按a排序、同时记录主键b
索引 cb的组织是先按c排序、再按b排序、同时记录主键a
so. ca可以去掉、cb需要保留

Q: using where的时候、需要回表查询数据、然后把数据传输给server层、server来过滤数据、那么这些数据是存在哪儿的呢 ?

1
无需存储、就是一个临时表、读出来立马判断、然后扫描下一行是否可以复用

Q: limit起到限制扫描行数的作用、并且有using where的时候、limit这个操作在存储引擎层做的、还是在server层做的 ?

1
server层、接上面一个Q、读完以后、顺便判断一下limit够不够就可以了、够了就结束循环

Q: extra列线上 using index condition 代表使用了索引下推 ?

1
ICP代表可以下推、但 '可以、不一定是'

Q: 备库使用 –single-transaction 做逻辑备份的时候、若从主库的binlog传来一个DDL语句会如何?
A:备份关键语句:

1
2
3
4
5
6
7
8
9
10
11
12
Q1:SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Q2:START TRANSACTION WITH CONSISTENT SNAPSHOT;
/* other tables */
Q3:SAVEPOINT sp;
/* 时刻 1 */
Q4:show create table `t1`;
/* 时刻 2 */
Q5:SELECT * FROM `t1`;
/* 时刻 3 */
Q6:ROLLBACK TO SAVEPOINT sp;
/* 时刻 4 */
/* other tables */
1
2
3
4
5
6
7
8
9
10
在备份开始的时候、为了确保RR(可重复读)、再设置一次隔离级别Q1
启动事务、用 with consistent snapshot 来确保这个语句执行完可以得到一个一致性视图 Q2
设置保存点 Q3
show create 是为了那点表结构 Q4、正式导数据 Q5 回滚到savepoint ap是为了释放t1的MDL锁 Q6

DDL从主库传过来的时间不同、则影响不同
1. 若在Q4之前到达: 无影响、那点的是DDL后的表结构
2. 若在时刻2到达、表结构被修改过、Q5执行的时候、报 Table definition has changed. please retry transaction. mysqldump 终止
3. 在时刻23之间到达、mysqldump占着t1的MDL锁、binlog被阻塞、现象: 主从延迟、直到 Q6 完成.
4. 从时刻2开始、mysqldump释放了 MDL锁、现象: 无影响、备份拿到的是 DDL前 的表结构

Q:

删除100000行表记录时,有3种做法

  1. 直接delete from T limit 100000;
  2. 在一个连接中 delete from T limit 5000; loop 20
  3. 在20个连接中 delete from T limit 5000; 20 connections;
    如何选择?

A:

1
2
3
尽量的选择第二种方式
1. 单个语句占用的时间较长、锁的时间也会较长、而且打的事务也会造成主从延迟
3.20个连接中同时执行 delete from T limit 5000, 可能会造成认为锁冲突

mysql 事务

  1. ACID原则:
    1
    2
    3
    4
    Automicity: 原子性
    Consistency: 一致性
    Isolation: 隔离性
    Durability: 持久性
  1. SQL隔离级别
    1
    2
    3
    4
    1. 读未提交: read uncommited, 事务未提交时、别的事务就能读到它的变更
    2. 读提交: read commited, 事务提交之后、才能被别的事务读到变更
    3. 可重复读: repeatable read, 事务在执行过程中看到的数据、始终保持跟事务启动时看到的一致
    4. 串行化: seriable, 对同一记录、写会加写锁、读会加读锁、读写锁冲突的时候、必须等到前一个锁释放才能执行

image.png

不同事物隔离级别下、事物A的返回结果

  • 若隔离级别是读未提交, 则 V1 的值是2、此时、B未提交A可以读到、
    V2, V3 的值也都是2
  • 若.. 是读提交, 则V1是1、V2 是2、事物B的更新在提交后可以被A读到、则V3的值也是2
  • 若隔离级别是可重复读、则V1, V2的值是1、V3的值2、因为V2事务A提交之前、所以、V1, V2 的值为1
  • 若隔离级别是可串行化事务B在执行1->2的过程会被锁、直到A提交、所以V1, V2是1 、

查看事务隔离级别

1
2
3
4
5
6
7
mysql> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.04 sec)
1
2
3
4
5
6
mysql> select @@transaction_isolation;
+-----------------+
| @@transaction_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
mysql 全局事务隔离级别修改后、在本会话不会生效、只影响后续会话
1
2
3
4
5
6
7
8
9
10
mysql> set global transaction isolation level read committed;
Query OK, 0 rows affected (0.01 sec)

mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
1 row in set (0.00 sec)
mysql session级别事务修改、只影响当前会话
1
2
3
4
5
6
7
8
9
10
mysql> set session  transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)

mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED |
+-------------------------+
1 row in set (0.00 sec)

*Q:

1
2
3
4
5
6
7
8
9
1.合适需要RR级别呢 ?
假设正在对数据做校对、是不是希望在校对过程中、用户产生的交易不会影响校对的结果 ?~~
2. 回滚日志什么时候删除?
系统会判断当没有事务需要用到这些回滚日志的时候,回滚日志会被删除
3. 什么时候不需要了?
当系统里么有比这个回滚日志更早的read-view的时候
4. 为什么尽量不要使用长事务
长事务意味着系统里面会存在很老的事务视图,在这个事务提交之前,回滚记录都要保留,这会导致大量占用存储空间。除此之外,长事务还占用锁资源,可能会拖垮库
5.

事务隔离的实现

1
事务隔离的实现:每条记录在更新的时候都会同时记录一条回滚操作。同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(MVCC)

Q: 由上所述、若事务隔离级别为RR、事务T启动的时候会创建一个一致性视图read-view、执行期间、若有其它事务修改了数据、事务T看到的数据不变
但: 这个事务要更新R1时、恰好R1被T2占有行锁、则T会进入等待状态、此时: 当它拿到行锁、可以执行更新的时候、读到的值是什么呢 ?

1
2
3
4
5
6
7
create table `t`(
`id` int(11) not null ,
`k` int(11) default null ,
primary key (`id`)
)engine=innodb;

insert into t(id, k) values (1, 1),(2, 2);

SQL执行顺序如下:
image.png

那么两个查询语句得到的结果分表是什么呢?

A: 先看下几个概念:

1
2
3
4
5
6
7
1. begin/start transaction 并不是事务的七点、在执行到它们之后的第一个innodb表语句、事务才真正启动、马上执行事务、可以使用:
start transaction with consistent snapshot;

2. mysql中两个视图的概念
1) view: 是用查询语句创建的虚拟表、在调用的时候执行查询语句并生成结果、create view ...
2) innodb在实现MVCC时用到的一致性读视图、即 cinsistent read view, 用于执行 RC(Read Committed, 读提交)
和RR(Repeatable Read, 可重复读)隔离级别的实现

两种启动事务的方式
1.一致性视图是在执行第一个快照语句时创建
2 在执行 start transaction with consistent snapshot 时创建

大前提:

1
2
3
1. 事务隔离级别为RR
2. autocommit=1
3. 注意事务启动的时机

结果:

1
A 查询得到1 B查询得到3 C更新成功

so. 一脸迷茫了、^.^…
来看下: 快照在MVCC里是如何工作的

1
2
3
4
5
6
7
在RR级别下、事务在启动的时候就拍了个快照(基于db)

快照如何实现?
1. innodb每个事务有一个唯一id、叫 transaction id. 在事务开始时向事务系统申请、严格递增
2. 每行数据有多个版本、每次更新都会生成一个新的数据版本、并把trx id赋值给这个数据版本的事务id、记为: row trx_id
旧的数据版本要保留、且在新的数据版本中可以拿到、即: 表中一行记录、有多个版本row、每个版本有自己的row trx_id
一个记录被连续更新后的状态如下:

image.png

1
2
3
4
5
6
7
8
9
10
11
12
虚线内是同一行数据的4个版本、当前最新版本是V4、k的值是22、是被 transaction id25的事务更新的

undo log呢?
上图中的三个虚线就是undo log、而V1 V2 V3 并不是物理存在的、而是在需要的时候根据当前版本和undo log计算的
事务只认、事务启动之前提交的内容、如果提交之后的并不认、必须要找到它的上一个版本、若上一个版本也不可见、则继续查找

实现:
innodb为每个事务构造了一个数组、用来保存事务启动的瞬间、当前正在活跃(启动但未提交)的所有事务id

数组里id的最小值即为 低水位、当前系统已创建过的事务id的最大值+1 即为 高水位
视图数组和高水位组成了当前事务的一致性视图 read-view
数据版本的可见性规则、就是基于row trx_id和一致性视图的对比结果得到的

image.png

1
2
3
4
5
6
7
8
对于当前事务启动的瞬间、一个数据版本的row trx_id有几种情况
1. 若落在绿色部分、表示已提交事务或当前事务自己生成的、可见
2. 红色部分、表示这个版本是将来启动的事务生成的、不可见
3. 黄色部分、
a. 若 row trx_id 在数组中、表示由未提交的事务生成的、不可见
b. 若不在数组中、表示已提交事务生成、可见

有了row trx_id、事务的快照就是静态的了...

假设:

  1. 事务开启前、系统只有一个活跃事务id 99
  2. 事务A、B、C的版本号 100、101、102 且当前系统只有这4个事务
  3. 事务开始前、(1,1)这行数据的 row trx_id是90

事务查询逻辑图:
image.png

所以、事务A的查询流程:

1
2
3
4
5
1. 找到(1,3) 判断trx_od=101 比高水位大、处于红色区域、不可见
2. 找到上个版本、row trx_id=102 比高水位大、处于红色区域、不可见
3. 继续、找到(1,1) row trx_id=90、比低水位小、处于绿色区域、可见

虽然这行数据被修改过、但事务A无论在何时查询、结果都是一致的、称为一致性读

更新逻辑

那么是不是有个疑问:
按照一致性读、好像 事务B的update语句、是不对的 ?

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
B进行update时、是不是看不到(1,2)?怎么计算出来的(1,3) ?

是的: 如果在事务B更新之前查询一次数据、会发现、返回的 k 的确是1
但是更新数据的时候、就不能再历史版本上更新了、否则、事务C的更新就丢失了
so. 事务B此时的更新是在(1,2) 的基础上进行的操作

规则:
更新都是先读后写的、这个读是当前读(current read)、只能读当前的值

其实、除了update、若select加锁、也是当前读

mysql> select k from t where id=1 lock in share mode; // 读锁(S锁、共享锁)
mysql> select k from t where id=1 for update; // 写锁(X锁、排他锁)

再往前一步:
若 C不是马上提交的、而是事务C’ 会如何?

image.png

1
2
3
4
5
事务 c' 不同的是、未马上提交、在它提交前、事务B的更新发起、而此时 C' 未提交、但是(1,2)这个版本也生成了、并且是当前最新版本
那么B如何处理?

此时就要考虑 两阶段锁协议、事务C'未提交、则写锁未释放、事务B是当前读、必须读最新版本、且必须加锁、
则B被阻塞、必须等到C'释放、才可继续

Q: 为何表结构不支持 可重复读?

1
表结构无对应的数据行、也没有 row trx_id 所以、只能遵循当前读

Q: 使用如下表结构和初始化语句作为实验环境、事务隔离级别是可重复读、想把 字段 c和id 等值的行 的c值清零、发现 并未改掉
解释这种情况出现的场景及原理, 及如何避免?

1
2
3
4
5
6
create table `t`(
`id` int(11) not null ,
`c` int(11) default null ,
primary key (`id`)
)engine=innodb
insert into t(id, c) values (1,1), (2,2), (3, 3), (4, 4);

A: 场景一

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
会话A
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t;
+----+------+
| id | c |
+----+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
+----+------+
4 rows in set (0.01 sec)

mysql> update t set c=0 whete id=c;

会话B
mysql> update t set c=c+1;
Query OK, 4 rows affected (0.01 sec)
Rows matched: 4 Changed: 4 Warnings: 0

会话A
mysql> select * from t;
+----+------+
| id | c |
+----+------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
+----+------+
4 rows in set (0.00 sec)

会话A
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from t;
+----+------+
| id | c |
+----+------+
| 1 | 2 |
| 2 | 3 |
| 3 | 4 |
| 4 | 5 |
+----+------+
4 rows in set (0.00 sec)

即:
image.png

场景二:
image.png

记住:update时、为当前读 current read

幻读

1
2
3
4
1. 在 可重复读 RR 隔离级别下、普通的查询是快照读、是不会看到别的事务插入数据的、
因此 幻读是在 当前读 下才会发生
2. 数据被修改、在当前读可以看到修改后的结果、这个不叫幻读、
幻读 专指 新插入的行被当前读读到

间隙锁-gaps lock

1
2
3
4
5
6
7
8
9
10
11
间隙锁: 锁的是两个值之间的空隙
跟间隙锁存在冲突关系的是: 往这个间隙插入记录、间隙锁之间不存在冲突关系

行锁冲突关系: 读-读: no 读-写:no 写写: yes
即: 跟间隙锁存在冲突关系的、是另外一个行锁

间隙锁的引入会导致同样的语句锁住更大的范围、影响并发
间隙锁只有在RR的隔离级别下才会生效、

把隔离级别设为读提交的话、就没有间隙锁了、但是、要解决可能出现的数据和日志不一致的问题、
需要把binlog的格式设置为row

count(1) count(*) count(id)

mysiam:
将表的总行数保存到了磁盘上、所以对于无条件的count(*) 是很快返回的

innodb实现:
count(*) 是把数据记录一行行的拿出来判断、
(innodb是索引组织表、主键索引的叶子节点是数据记录、普通索引的叶子节点是主键值、会小很多、mysql会选择最小的那颗索引树来遍历、在保证逻辑正确的情况下、尽量减少扫描的数据量)
count(1) 遍历表、但不取值、扫描记录时、返回1给server
count(id) 会把id返回给server
count(field)
若定义为非null、会先判断该字段记录是否为null、非null才累加
若定义为null、从记录读出字段、累加

为什么mysql不直接记录count数?

innodb要保证事务执行、不同会话、commit前后 总行数是会发生改变的

show table status 替代?

得到的结果是通过采样估算得到的、不精准、最高偏差有40%-50%

采样缓存系统保存计数?

1.redis重启、数据丢失
2.redis和db本身记录增加先后的问题会导致短时间的不精准

使用mysql表保存总记录?

可以、使用mysql的事务来保证、但是会影响性能

mysql两阶段提交的过程:
image.png

在不同阶段crash对于系统的影响

1
2
3
4
5
1. 图中A时刻(redo log写完、binlog还没写的时候)crash、binlog还没写、redo log也没提交、重启事务会回滚~
2.若是在B、binlog写完、redo log还未commit时、crash会发生什么?
a. 若redo log也是完整的-有了commit标识、直接提交
b. 若redo log只有完整的prepare、则判断对应的binlog是否完整、
完整-提交事务; 否则: 回滚事务

mysql如何知道binlog是完整的?

1
2
3
4
一个事务的binlog有完整的格式:
statement格式的: 最后会有commit
row格式的: 最后会有Xid event
MySQL5.6.2 之后、还引入了checksum检查日志中间出错的情况

redo log和binlog是如何关联的

1
2
3
有一个共同的字段XID、crash恢复的时候会按顺序扫描redo log:
1. 遇到既有prepare 又有commitredo log直接提交
2. 遇到只有prepare的、就拿XID去对应的binlog查找事务

处于prepare阶段的redo log + 完整的binlog重启就能恢复、为什么这么设计?

1
在时刻B、binlog就已经被写入了、若是应用于从库、从库就有了这条记录、为了保证主从数据的一致性、就必须保证主库也有这条记录、所以把redo log提交

为什么不是先写完redo log再写binlog ?

1
2
比较典型的分布式问题
redo log提交了、又不能回滚(回滚可能会覆盖掉其它的事务)、所以redo log直接提交、binlog写入失败的时候、由于不能回滚、就会比从库多一个事务

为什么不直接使用binlog、不用redo log ?

1
2
3
4
5
6
7
8
若是历史原因: innodb本身不是mysql的原生引擎、原生引擎是mysiam、不支持崩溃恢复
innodb在加入mysql之前、就可以支持崩溃恢复和事务
innodb发现binlog没有崩溃恢复的能力、那就直接使用redo log吧、
如果用binlog支持崩溃恢复呢 ?流程如下图
在这样的流程下、binlog还是不能支持崩溃恢复、不能支持恢复数据页
若binlog2写完、未commit的时候crash、引擎内部事务2会回滚、应用binlog2可以补回来、但对于binlog1事务已经提交、不会再应用binlog1

innodb使用的是WAL、在写完内存和日志的时候、事务就算完成了、若以后崩溃、依赖日志恢复数据页、图中1位置crash 事务1可能会丢失、且是数据页级的丢失、binlog未记录数据页的更新细节、不支持数据页恢复

image.png

能只用redo log不用binlog吗?

1
2
3
4
1. 只从崩溃恢复的角度来讲、可以关掉binlog、系统依然是crash-safe的、但binlog有redo log不可替代的功能
a. 归档. redo log是循环写、日志无法保留
b. mysql系统依赖于binlog、binlog作为mysql本身就有的功能、
c. 一些异构系统、需要消费binlog来更新数据

redo log一般设置多大?

1
redo log过小、会导致很快写满、不得不强行刷盘、这样WAL的能力就发挥不出来了、若磁盘在T级别、就直接设置为G级别吧~

正常运行的实例、数据写入后的最终落盘是从redo log更新的还是从buffer poll更新的?

1
2
3
redo log 并没有记录数据页的完整数据、所以本身没有能力更新磁盘数据页、
1. 数据页被修改后与磁盘数据页不一致、最终落盘就是把内存中的数据写入磁盘、与redo log无关
2. 崩溃恢复的场景中、innodb如果判断到一个数据页可能在崩溃恢复的时候丢失了更新就会将它读入内存、然后让redo log更新内存内容、更新完、内存变成脏页、回到1的情况

redo log buffer是什么?是先修改内存、还是写写redo log ?

1
2
3
4
5
6
7
8
9
在事务更新过程中 redo log 是要多次写的、
eg. begin;
insert into t1...;
insert into t2...;
commit;
这个事务要在两个表中插入记录、在插入的过程中、生成的日志都得先保存起来、但又不能在还没commit的时候写redo log

所以 redo log buffer就是一块内存、用来保存 redo log日志的. 即 在执行第一个insert的时候、数据的内存被修改了、redo log buffer也写入了日志
但真正写入redo log(ib_logfile+日志)是在执行commit语句的时候做的、

update记录为原值的时候、mysql如何操作?

1
2
3
4
5
1. update是先读后写、发现值本来就是原值、不操作、直接返回
2. mysql调用innodb引擎接口修改、引擎发现与原值相同、不更新、直接返回
3. innodb认真的执行了更新、改加锁的加锁?

答案是3、可以通过事务来验证

过程请参考: https://time.geekbang.org/column/article/73479

varchar(255)是边界、>255需要两个字节存储、小于需要1个字节

  1. mysql 源码编译启动报错

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    mysql启动报错:Starting MySQL... ERROR! The server quit without updating PID file

    看errlog 发现:
    2018-01-24 07:57:03 67547 [ERROR] Fatal error: Can't open and lock privilege tables: Table 'mysql.user' doesn't exist
    or: 2018-01-24 07:57:03 [ERROR] Can't locate the language directory.

    重新初始化db:
    mysql_install_db --user=mysql --basedir=/home/devil/mysql57/ --datadir=/home/devil/mysql57/data/

    出现:
    2019-04-18 21:23:16 [WARNING] mysql_install_db is deprecated. Please consider switching to mysqld --initialize

    so.
    mysqld --initialize --user=mysql --basedir=/home/devil/mysql57/ --datadir=/home/devil/mysql57/data/

    可以看到初始化成功. 并生成了临时密码
  2. mysql binlog 查看

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    a. 登录mysql查看
    1) 只查看第一个binlog文件的内容
    show binlog events;
    2) 查看指定binlog文件的内容
    show binlog events in 'mysql-binlog.000001';
    3) 查看当前正在写入的binlog
    show master status;
    4) 获取binlog文件列表
    show binary logs;

    b. 使用mysqlbinlog工具查看
    1) 本地查看
    基于开始/结束时间:
    mysqlbinlog --start-datatime='2019-04-10 00:00:00' --stop-datatime='2019-04-10 01:00:00'
    基于pos值
    mysqlbinlog --start-postion=107 --stop-position=1000 -d 库名 二进制文件

    2) 远程查看
    mysqlbinlog -u{uname} -p{pass} -htest.com -P3306 \
    --read-from-remote-server --start-datetime='2013-09-10 23:00:00' --stop-datetime='2013-09-10 23:30:00' mysql-bin.000001 > t.binlog
1
2
3
4
5
6
set optimizer_trace='enabled=on' 打开trace记录
select trace from `information_schema`.`optimizer_trace`; 查看trace记录

tmp_table_size 内存临时表大小
sort_buffer_size 用于排序的内存大小、超过会使用文件排序
max_length_for_sort_data 单行数据量超过这个值会使用rowid排序
1
2
3
4
查看sql被哪个语句阻塞
select * from t sys.innodb_lock_waits where locked_table=`'test'.'t'

select blocking_pid from sys.schema_table_lock_waits 可以找到阻塞的pid

普通索引和唯一索引

场景:
1
2
3
假设维护一个市民系统、每个人有一个唯一的身份证号、经常会根据身份证号查询、sql如下:
select name from user where id_card='xxx';
一定会考虑在id_card上建立索引、那么、这个索引应该是唯一索引还是普通索引呢 ?
思考:

id_card字段比较大、不建议作为主键、那么、1.普通索引 2.唯一索引 如何选择?依据又是什么呢 ?

mysql索引结构组织树.png

分析:

查询过程

1
2
3
4
5
6
7
8
9
10
11
12
假设执行的语句为 select t from T where k=5;
搜索从根开始、按层搜索到叶子节点、可以认为数据页内部通过二分法来定位
1. 对于普通索引、查找到第一条记录(5, 500) 之后、需要查找下一个记录、直到碰到第一个不满足k=5的记录
2. 对于唯一索引】由于定义了唯一性、查找到(5, 500) 之后、停止检索、直接返回
那么带来的性能差异呢 ? - 微乎其微

innodb的数据是按照数据页为单位来读写的、即: 当需要读一条记录的时候、不是讲记录本身从磁盘读出、而是以页为单位、
将整个数据页读取到内存、数据页大小默认为16k

因为是按页读取、当找到k=5时、它所在的数据页已经在内存了、对于普通索引来说、多做的一次'查找和判断下一条记录'只需要一次指针查找和一次计算
不幸的是、恰好l=5是数据页的最后一条记录呢 ?....
必须读取下一个数据页, 对于整型字段、16k可以放近千个key、出现这种情况的概率很低、所以计算平均性能差异时、可认为这个操作成本对于现在的CPU来说可以忽略不计

更新过程

change_buffer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
先说下change_buffer的概念:
更新数据页时,若数据页在内存中 -> 直接更新,
不在内存 -> 在不影响一致性读的情况下、会讲更新操作缓存在change_buffer
这样就不需要从磁盘读入数据页, 下次查询需要访问这个数据页时、将数据页读到内存、与change_buffer合并(称为merge)

虽然名字叫change_buffer、实际可持久化、在内存中有copy、也会被写入磁盘

触发merge:
1. 访问数据页
2. 后台线程定期merge
3. db正常关闭、shutdown
4. 达到change_buffer的可用最大内存、触发merge、然后淘汰

显然、如果更新操作先记录在change_buffer、减少读磁盘、可以提高sql执行效率
而且、数据读入内存、是需要占用buffer_pool的、这种方式还可以避免占用内存、提高内存使用效率

那么什么时候可以使用buffer_pool?
1. change_buffer 占用 buffer_pool 的内存、通过 innodb_change_buffer_max_size 调整、设为50
表示change_buffer 最多占用buffer_pool的50%
2. 不能无限增大、不能 > buffer_pool
3. 对唯一索引来说、所有的更新都要判断操作是否违反唯一约束、
eg. 要插入(4, 400)这个记录、必须先判断记录是否存在、必须先将数据页读入内存
若已读入内存、直接更新内存更快、无需使用 change_buffer、
实际上、唯一索引的更新也不能使用change_buffer、只有普通索引可用
理解了 change_buffer、看下插入(4,400)
1
2
3
4
5
6
7
8
9
10
11
12
1. 更新的记录目标页在内存 - 无差别(只有一个判断的差别)
1) 唯一索引: 找到35之间位置、判断无冲突、直接插入
2) 普通索引: 找到35之间、直接插入

2. 更新记录目标页不在内存
1) 唯一索引: 将数据页读入内存、判断无冲突、插入
2) 普通索引: 将记录更新在change_buffer、执行结束
将数据从磁盘读到内存、涉及io随机访问、change_buffer减少了随意访问磁盘、性能会明显提升

3. 案例:
业务库的内存命中率突然下降、整个系统处于阻塞状态、更新全部阻塞、
深入排查后发现: 业务有大量的插入操作、而、前一天晚上上、将普通索引改成了唯一索引、使写操作的效率下降
change_buffer使用场景
1
2
3
4
5
6
7
change_buffer对于查询无明显效果、也只适用于普通索引、那么 普通索引的所有场景、change_buffer都能起到加速作用么 ?

1. merge 的时候是真正数据更新的时刻、change_buffer主要是将记录变更的动作缓存、so.merge前变更越多、收益越大
对于写多、读少的业务、change_buffer的效果最好、常见业务模型: 账单类、日志类系统

2. 若业务场景是、写完立马会有查询、由于立马访问数据页、会立即触发merge、随机访问的io次数不会减少、
反而增加了change_buffer的维护代价、不适合使用
索引选择和实践
1
2
1. 若所有更新后跟着记录的查询、应关闭change_buffer
2. 若有一个机械硬盘的历史库、应尽量使用普通索引、调大change_buffer、确保历史数据的写入效率
change_buffer 和 redo log
1
2
3
假设执行更新
insert into t(id, k) values(id1, k1), (id2, k2)
当前k索引树的状态、k1所在的数据页在内存(innodb buffer pool)中、

带change_buffer的数据更新.png

1
2
3
4
5
6
7
8
9
涉及: 内存、redolog(ib_log_fileX)、数据表空间(t.ibd)、系统表空间(ibdata1) 四个部分
更新做了如下操作:
1. page1在内存、直接更新内存 (图中1)
2. page2不在内存、在内存的change_buffer区域、记录下 '往page2插入1行' 这个信息 (图中2)
3.12两个动作写入redo log(图中34)

所以、这条更新写了两处内存、一次磁盘(两次操作合写一次磁盘)还是顺序写、

图中两个虚线、是后台操作、不影响更新的响应时间

读请求
带change_buffer的读.png

1
2
3
4
5
6
7
8
select * from t where k in (k1, k2)

1. 假设、读发生在更新不久、内存中数据还在、此时与系统表空间和redolog无关、直接读即可
2. 读page1时、从内存返回、读page2时、需要把page2从磁盘读入内存、然后应用change buffer的操作日志、合并数据

so. 简单对比这两个机制在提升更新性能上的收益的话、
redo log节省的是随机写 IO 的消耗、转为顺序写
change buffer节省的是随机读io的消耗

Q: change_buffer一开始是写内存的、此时若掉电重启、会导致change_buffer丢失么 ? 若丢失、从磁盘读入时、就丢了merge、相当于丢了数据…

A:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
虽然只是更新内存、但: 事务提交时、把change buffer的操作记录到了 redo log. 所以恢复时、change buffer可以找回

1.change buffer有一部分在内存有一部分在ibdata.
做purge操作,应该就会把change buffer里相应的数据持久化到ibdata
2.redo log里记录了数据页的修改以及change buffer新写入的信息
如果掉电,持久化的change buffer数据已经purge,不用恢复。主要分析没有持久化的数据
情况又分为以下几种:
(1)change buffer写入,redo log虽然做了fsync但未commit,binlog未fsync到磁盘,这部分数据丢失
(2)change buffer写入,redo log写入但没有commit,binlog以及fsync到磁盘,先从binlog恢复redo log,再从redo log恢复change buffer
(3)change buffer写入,redo log和binlog都已经fsync.那么直接从redo log里恢复


merge流程:
1. 从磁盘读入数据页到内存
2. 从change buffer找出这个数据页的change buffer记录(可能是多个)、依次应用、得到新版数据页
3. 写redo log. 这个redo log包含了数据的变更和change buffer的变更
merge流程完成、哈哈、此时 数据页和内存中change buffer对应的磁盘位置都还没修改、属于脏页、之后各自刷回自己的物理数据 -> 另外一个流程

#####

1
2
3
4
5
6
7
内存命中率: ib_bp_hit=1000 – (t2.iReads – t1.iReads)/(t2.iReadRequest – t1.iReadRequest)*1000

change buffer相当于推迟更新、对于MVCC是否有影响?比如加锁?
锁是单独的数据结构、若数据页上有锁、change buffer在判断能否使用时、会认为否

change buffer中、有此行记录的条件下、再次修改、是增加还是原地修改?
增加