Sunskey

日拱一卒,不期而至

0%

order by工作原理

全字段排序

MySQL会给每个线程分配一块内存用于排序,称为sort buffer。

1.初始化 sort_buffer,确定放入 name、city、age 这三个字段;

2.从索引 city 找到第一个满足 city=’杭州’条件的主键 id,也就是图中的 ID_X;

3.到主键 id 索引取出整行,取 name、city、age 三个字段的值,存入 sort_buffer 中;

4.从索引 city 取下一个记录的主键 id;

5.重复步骤 3、4 直到 city 的值不满足查询条件为止,对应的主键 id 也就是图中的 ID_Y;

6.对 sort_buffer 中的数据按照字段 name 做快速排序;

7.按照排序结果取前 1000 行返回给客户端。

sort_buffer_size ,就是MySQL为排序开辟的内存(sort_buffer)的大小。如果要排序的数据量小于sort_buffer_size,排序就在内存中完成。如果数据量太大,内存放不下,则不得不利用磁盘临时文件辅助排序。

rowid排序

如果sort_buffer里面要存放的字段数太多,这样内存能存的行数很少,要分成很多个临时文件,排序性能会很差。可以通过修改max_length_for_sort_data的值来改变排序算法。

1.初始化 sort_buffer,确定放入两个字段,即 name 和 id;

2.从索引 city 找到第一个满足 city=’杭州’条件的主键 id,也就是图中的 ID_X;

3.到主键 id 索引取出整行,取 name、id 这两个字段,存入 sort_buffer 中;

4.从索引 city 取下一个记录的主键 id;

5.重复步骤 3、4 直到不满足 city=’杭州’条件为止,也就是图中的 ID_Y;

6.对 sort_buffer 中的数据按照字段 name 进行排序;

7.遍历排序结果,取前 1000 行,并按照 id 的值回到原表中取出 city、name 和 age 三个字段返回给客户端。

全字段排序 VS Rowid排序

由于Rowid排序涉及到回表,如果内存足够,就优先使用内存,即全字段排序。

优化手段:可以使用覆盖索引,索引上的信息足够满足查询请求,不需要再回主键索引上去取数据。

保证数据不丢失的秘密

binlog写入机制

binlog写入逻辑:事务执行过程中,先把日志写到binlog cache,事务提交的时候,再把binlog cache写到binlog中。

每个线程由自己的binlog cache ,但是共用同一份binlog文件。

write,指的是把日志写入文件系统的page cache,并没有把数据持久化到磁盘,所以速度快。

fsync,将数据持久化到磁盘的操作。

write和fsync的时机,是由参数sync_binlog控制的;

1.sync_binlog = 0的时候,表示每次提交事务都只write,不fsync;

2.sync_binlog =1的时候,表示每次提交事务都会执行fsync;

3.sync_binlog=N(N>1) 的时候,表示每次提交事务都 write,但累积 N 个事务后才 fsync。

redo log的写入机制

img

1.存在redo log buffer中,物理上是在MySQL进程内存中。

2.写到磁盘(write),但是没有持久化(fsync),物理上文件系统的page cache里面。

3.持久化到磁盘,对应的是hard disk。

InnoDB提供了innodb_flush_log_at_trx_commit参数控制redo log的写入策略。

0:表示每次事务提交时都只是把redo log留在redo log buffer中

1:表示每次事务提交都将redo log持久化硬盘

2:每次提交都只是把redo log写到page cache

InnoDB有一个后台线程,每间隔一秒,就会把redo log buffer中的日志,调用write 写到文件系统的page cache ,然后fsync持久化磁盘。

3种情况会把让没有提交事务的redo log写入到磁盘中

1.后台线程每秒一次的轮询操作。

2.redo log buffer占用的空间即将达到innodb_log_buffer_size一半的时候,后台线程会主动写盘。

3.并行的事务提交的时候,顺带将这个事务的redo log buffer持久化到磁盘。

MySQL的双一配置。指的就是sync_binlog和innodb_flush_log_at_trx_commit都设置成1。一个事务完整提交前,需要等待两次刷盘,一次是redo log(prepare ),一次是binlog.

日志逻辑序列(LSN)是单调递增的,用来对应redo log的一个个写入点。每次写入length的redo log,LSN的值就会加上length。

一次组提交里面,里面的组员越多,节约磁盘IOPS的效果越好。

日志记录过程

如果想binlog提升组提交的效果,可以通过设置binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 来实现。

1.binlog_group_commit_sync_delay参数,表示多少微秒后才调用fsync

2.binlog_group_commit_sync_no_delay_count,表示多少次以后才调用fsync

如果MySQL出现了性能瓶颈,而且瓶颈在IO上

1.通过设置binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count参数你,减少binlog的写盘速度。

2.将sync_binlog设置大于1,这样做的风险是,这个主机掉电会丢失binlog日志。

3.innodb_flush_log_at_trx_commit 设置为 2。这样做的风险是,主机掉电的时候会丢数据。

MySQL 提高性能的办法

短连接存在的一个风险,就是一旦数据库处理得慢一些,连接数就会暴涨。

max_connection参数,用来控制一个MySQL最大连接数,超过这个值,系统就会提示“too many connections”从而导致数据库不可用。

解决方案

1.先处理掉那些占着链接但是不工作的线程。

如果连接数过多,可以优先断开事务外空闲太久的链接;如果这样还不够,在考虑断开事务内空闲太久的链接。

使用 kill connection + id的方式,使用show processlist查看结果。

2.减少连接过程的消耗

跳过权限验证的方法是:重启数据库,并使用-skip-grant-tables参数启动。这样连接过程和语句执行过程都会跳过权限校验阶段。(但是不建议这么做,风险极高)。

慢查询性能问题

1.索引没有设计好

2.SQL语句没写好

3.MySQL选错了索引

1.上线前,在测试环境,把慢查询日志打开,并且把long_query_time设置成0,确保每个语句都会被记录

2.在测试表插入线上的数据,做一遍回归测试

3.观察慢查询里每类语句的输出,特别留意Row_examined字段是否一致。

call query_rewrite.flush_rewrite_rules() 这个存储过程,是让插入的新规则生效,也就是我们说的“查询重写”。

QPS突增问题

1.如果是由全新业务的bug导致的,那么就可以从数据库直接把白名单去掉。

2.如果这个新功能使用的单独的数据库用户,可以直接删除用户。

3.如果跟主题功能绑定在一块,使用语句重写的功能,把压力最大的SQL直接重写成“select 1”返回。

数据表空间回收

MySQL8.0版本前,表的结构是存在以.frm为后缀的文件里。而8.0后,允许把表结构定义放在系统数据表中。因为结构定义占用的空间很小。

表数据既可以存在共享表空间里面,也可以是单独的文件。这个行为是由参数innodb_file_per_table控制。从Mysql5.6.6以后,默认为on。

off:表的数据放在系统共享表空间,也就是跟数据字典放在一起;

on:每个InnoDB表数据存储在一个以.idb为后缀的文件中。

删除数据

删除一个数据页上的所有记录,整个数据页的复用跟记录的复用是不同的。

使用delete 命令只是把记录或者数据页标记为“可复用”,不能回收表的空间,而没有使用的空间,看起来就像空洞,不止删除数据回暖造成空洞,插入数据也会。

重建表

新建一个与原表结构相同的新表,然后按照主键ID递增的顺序,把数据一行一行地从原表读到表B,然后用B替换A,从而起到收缩表A空间的作用,解决空洞的问题。

在MySQL5.5之后,可以之前使用alter table A engine = InnoDB来重建表。

在这个过程中,如果有新数据要写入表A的话,就回造成数据丢失。

为了保证整个重建表的正常,原表不能进行更新,5.6版本后引入了Online DDL,优化了该过程。

Online DDL

1.建立一个临时文件,扫描表A主键的所有数据页。

2.用数据页中表A的记录生成B+树,存储到临时文件中。

3.生成的临时文件的过程中,将所有对A的操作记录再一个日志文件(row log)中。

4.临时文件生成后,将日志文件的操作应用到临时文件中,得到与表A相同的数据文件。

5.用临时文件替换表A的数据文件。

整个过程中,MDL的写锁会转化为MDL的读锁,从而提高效率。

整个过程很消耗CPU和IO,如果想安全操作,推荐使用gh-os来做。

Online和inplace 的区别

对于server层没有把数据移动到临时表,是一个“原地”操作,这就过程称为inplace。

1.DDL过程如果是Online的,就一定是inplace的

2.inplace的DDL,有可能不是Online的。8.0后添加全文索引(FULLTEXT index)和空间索引(SPATIAL index)就属于这种情况。

optimize table、analyze table和alter table三种重建表的区别

1.从MySQL 5.6版本开始,alter table t engine = InnoDB默认就是Online DDL。

2.analyze table t,只是对索引信息进行重新统计,没有修改数据,加入MDL读锁。

3.optimize table = analyze + alter

count(*)很“慢”?

实现方式

1.MyISAM引擎把一个表的总行数存在磁盘上,执行count(*)的时候会直接返回这个数。

2.InnoDB时,则需要一行一行的从引擎里面读出来,然后累计计数。

对于count(*)这样的操作,MySQL优化器会找到最小的那棵树来遍历。再保证逻辑正确的前提下,尽量减少扫描的数据量,是数据库系统设计的通用法则之一。

show table status命令是采用采样估计来估算的,不能直接使用。

解决办法

1.通过缓存保存计数。

将计数保存在缓存系统中的方式,还不只是丢失更新的问题。即使Redis 正常工作,这个值还是逻辑上不精确的。

2.在数据库保存计数

由于事务的可见性,可保证逻辑上的数据的一致性。

不同count的用法

count()函数的参数不是NULL,累计值就加1,否则不加。最后返回累计值。

count(*),count(主键),count(1),count(字段)的区别

count(主键id),遍历每一行把每一行的id值取出来,返回给id层,server层拿到id后,判断不为空,则按行累加。

count(1),server层对于返回的每一行,放一个数字“1”进去,按行累加。

count(字段):一行一行的从记录里面读取字段

1.定义为not null的话,一行行的从记录里面读出这个字段,判断不能为null,按行累加。

2.定义为允许null,那么执行的时候,判断到有可能是null,还要把值取出来判断一下,不是null才累加。

count(*),并不会把全部字段取出来,而是专门做了优化,不取值。

按照效率排序的话:count(字段)<count(主键 id)<count(1)≈count(*)。

SQL执行过慢的原因

1.查询长时间不返回

  • 等待MDL锁

mysql> select * from t where id=1;

一般是表t被锁住了。可以利用show processlist看看语句处于什么状态。

如果是Waiting for table metadata lock的示意图,则表示有一个线程正在表t上请求或者持有MDL写锁。

通过查询sys.schema_table_lock_waits这张表,我们可以直接找到阻塞进程的id,然后把这个链接kill掉。

  • 等flush

有一个线程正要对表t做flush操作,MySQL里面对表做flush操作的用法。

flush tables t with read lock;

flush tables with read lock;

这个两个flush语句,如果制定表 t 的话,代表的只关闭表t,如果没有指定具体的表名,则表示关闭MySQL里所有打开的表。

所以出现Waiting for table flush 状态的可能情况是:有一个命令被别的语句堵住,然后它有堵住了我们select的语句。

  • 等行锁

如果一个事务在这行记录上面上持有一个写锁,那么我们的select 语句就会被堵住。

可以通过sys.innodb_lock_waits表查到。

2.查询慢

1.没有索引

2.为了一致性读,undo log日志过长导致的,判断是不是有长事务。

SQL为什么突然变“慢”

当内存数据页和磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”。内存数据写入到磁盘后,内存和磁盘的数据页的内容就一直了,称为“干净页”。刷新脏页的过程为称为flush。

flush的4种场景

1.对于InnoDB 的redo log写满了。

2.对应的系统内存不足。在需要新的内存页,而内存不够用的时候,就要淘汰一些数据页,空出内存给别的数据页使用。如果淘汰的时“脏页”,则先写入磁盘。

3.MySQL会合理的安排时间,见缝插针的刷新“脏页”。

4.MySQL重启的时候。

刷新脏页虽然时常态,但都是明显影响性能的。

1.一个查询要淘汰的脏页过多,会导致查询的响应时间明显变长。

2.日志写满,更新性能全部堵住,写性能跌为0,这种情况对敏感的业务来说,不能接受。

InnoDB刷新脏页的控制策略

1.调用fio工具,来正确设置innodb_io_capacity的这个参数

Innodb的刷盘速度主要考虑两个因素:一个脏页比例,一个是redo log写盘速度。

合理设置innodb_io_capacity的值,并且多关注脏页比例,**不要让它接近75%**。

脏页的比例是通过Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total得到的

其中innodb_flush_neighbors 参数为1会顺带把邻居的脏页同步刷新,适用于机械硬盘的场景。5.8以后默认

innodb_flush_neighbors 为0。

MySQL 索引相关知识

索引的选择

查询过程

普通索引:在查找满足条件的第一个记录后,查找下一个记录你,直到碰到不满足的条件的记录。

唯一索引:在查找第一个满足条件的记录后,就会停止继续检索。

更新过程

唯一索引的更新不能使用change buffer。而普通索引则可以。

索引的选择,两类索引在查询性能上没差别的,主要是考虑对更新的性能。推荐使用普通索引。

changebuffer

定义:当需要更新一个数据的时候,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB将会将这些更新操作缓存在change buffer中,这样就不需要从磁盘中读入数据页了。在下次查询需要访问这个数据页的时候,将数据读入内存,然后执行change buffer中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。

作用:减少读磁盘,语句的执行速度会得到明显的提升 ,提高内存利用率。

change buffer的时使用场景:主要目的就是将记录的变更动作缓存下来,在一个数据页做merge之前,记录的越多,收益越大。对于写多读少的业务(例如:账单类、日志类)使用效果较好。但是对于写入之后马上会做查询的场景。由于更新之后马上会读取数据页,出发merge的过程,这样反而会增加change buffer的维护代价。

change buffer:是buffer poll里面的内存,可以通过innodb_change_buffer_max_size去设置。

changebuffer和 redo log的区别

redo log主要节省的是随机磁盘的IO消耗(转化顺序写),而change buffer主要节省的是随机读磁盘的IO的消耗。

MySQL索引的错误选择

优化器选择扫描行数、临时表,是否排序等因素综合找出一个最优的解决方案,用最小的代价去执行语句。

扫行行数:通过“区分度”来建立估算扫描行数,显然“基础”越大,索引的区分度越好。

通过show index from table 中cardinality属性可以得到区分度。

采用统计:InnoDB会默认选择N个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到这个索引的基数。当变更行数超过1/M的时候,会自动触发重新做一次索引统计。

在MySQL中,有两种存储索引的统计方式,可通过innodb_stats_persistent的值来选择。

on:持久化存储。默认N是20,M是10。

off:内存。默认N是8,M是16。

索引选择异常和处理

1.采用force index强行选择索引。

2.修改语句,引导MySQL使用我们期望的索引。

3.在有些场景下,我们可以新建一个索引,提供给优化器做选择或者删除误用的索引。

字符串加索引

mysql> alter table SUser add index index1(email);
mysql> alter table SUser add index index2(email(6));

select id,name,email from SUser where email=’zhangssxyz@xxx.com’;

如果使用全索引

1.从index1索引树找到满足索引值”zhangssxyz”的这条记录,取得ID2的值

2.到主键查到主键值是ID2的行,判断email的值是否正确,将这行记录加入结果集

3.取index1索引树上刚刚查到的位置的下一条记录,发现不满足email=’zhangssxyz@xxx.com’的条件,循环结束。

使用部分索引

1.从index2 索引树找到满足索引值“zhangs”的记录,找到一个是ID!;

2.到主键查询主键值ID1的行,判断email的值不是“zhangsss@xxx.com”,丢弃。

3.取index2刚刚位置的下一条记录,发现仍然是“zhangs”,取出ID2,回表判断值是否正确,加入结果集

4.重复上述操作,直到在index2上取到的值不是“zhangs”时,循环结束

使用好前缀索引,定义好长度,就可以做好既节约空间,又不用额外增加太多的查询成本。

mysql> select
count(distinct left(email,4))as L4,
count(distinct left(email,5))as L5,
count(distinct left(email,6))as L6,
count(distinct left(email,7))as L7,
from SUser;

在L4-L7中,找出不小于L*95%的值。

使用前缀索引就用不上覆盖索引的查询性能的优化了。

字符串索引的劣势

对于区分度不高的场景,例如身份证前6位对于一个市、县区分度并不高的解决方案。

1.使用倒序存储

2.使用hash字段

hash字段和倒序存储的区别

1.倒序存储不消耗额外的空间,不需要新增hash字段。

2.自增hash字段的方式,倒序rerverse()相比hash函数的效率更高。

3.从查询效率上看,使用hash字段方式的查询性能相对稳定。

索引的失效

1.对索引字段做函数操作,可能会破环索引值的有序性,因此优化器就决定放弃走树搜索功能。

2.隐式的类型转换

3.隐式的字符编码转化

后台服务出现明显“变慢”

具体步骤

1.检查应用本身的错误日志。

2.可以检查系统级别的资源占用情况,例如CPU、内存是否被其他进程大量占用。

3.监控Java 服务自身,例如GC 日志里面是否观察到FULL GC等恶劣情况,或者其他进程大量占用,并且这种占用是否不符合系统的正常运行情况。

4.如果不能定位具体问题,对应用进行Profing也是个办法,但会影响系统的响应速度。

5.定位错误后,采取相应的不久措施。

性能分析

1.自上而下。从应用的顶层,逐步深入到具体的不同模块。

2.自下而上。从类似CPU这种硬件底层,判断Cache-Miss之类的问题和调优机会,出发点是指令级别优化。

自上而下(单机应用)

系统性能分析中,CPU、内存和IO是主要关注项。

CPU排查

查看负载有没有明显的变化,利用top和jstack 找出最耗费CPU的线程。利用vmstat,查看上下文切换的数量,

如果每秒上下文切换很高,并且比系统中断高很多,就表明有可能是因为不合理的多线程调度导致的。

内存排查

利用free 之类查看内存使用,进一步判断swap使用情况,top命令输出中Virt作为虚拟内存使用量,就是物理内存(Res)和swap 求和。

IO排查

利用iostat等命令判断磁盘的健康情况。

JVM 层面的性能分析

1.利用JMC、JConsole等工具进行运行时监控。

2.利用各种工具,在运行时进行堆转存储分析,获取各种角度的统计数据。

3.GC 日志等手段,诊断 Full GC 、Minor GC、或者引用堆积。

4.应用Profiling,来实现从Hotspot JVM内部收集底层信息,通常会有2%左右的损耗。

Lambda让程序“慢30倍”?

一般来说,可以认为Lambda/Stream 提供了与传统方式接近对等的性能。主要的开销:初始化的开销。

Lambda并不是语法糖,而是一种新的工作机制。在首次调用时,JVM需要为其构建。如果 Java 应用启动过程中引入很多Lambda语句,会导致启动过程变慢。

通过引入基准测试,可以定义性能对比的明确条件、具体的指标,进而保证得到定量的、可重复的对比数据,这是工程中的实际需要。

微基准测试

当需要对一个大型软件的某小部分的性能评估时,就可以考虑微基准测试。

微基准测试通常是API级别的验证,一般是偏基础、底层平台开发者的需求。(通常使用Benchmark)。

微基准框架

目前最为广泛的框架之一就是 JMH。如果做 Java API级别的性能对比,JMH往往是首选。

微基准框架测试过程的闭坑

1.保证代码经过了足够并且合适的预热。JVM会对代码进行优化,JIT会一定条件下将代码编译本地代码。

2.防止 JVM 进行无效代码消除,尽量保证方法有返回值,而不是void方法。

3.防止发生常量折叠。JVM 如果发现计算过程依赖常量,就可能会直接计算结果。JMH提供了State机制来解决这个问题。

4.避免GC 对程序的影响,可以采用 Serial GC 或者 JDK 11引入的Epsilon GC,从而排除相关的影响。

JVM优化代码

JVM 在对代码执行的优化可分为运行时优化(runtime)和即时编译器(JIT)优化。

运行时优化

主要是解释执行和动态编译通用的一些机制,比如锁机制、内存分配机制(如TLAB)等。还有一些专门用于优化解释执行效率的,比如说模版解释器、内联缓存等。

即时编译器优化

指将热点的代码以方法为单位转换成机器码,直接运行在底层硬件之上。比如静态编译器可以使用方法内联、逃逸分析以及基于profile的投机性优化。

Java优化的落地

编译期:编译器或者相关API等奖源码转换为字节码的过程,可以直接利用反编译工具,查看细节。

JVM运行时的优化:通常是编译器和解释器共同作用的结果。

解释器和编译器会进行一些通用的优化。例如锁优化和Intrinsic 机制。

即时编译器:通过对方法调用的计数统计,甄别出热点代码,编译为本地点吗。另一个优化场景,则是针对热点循环代码,利用栈上替换技术。JIT可以看作基于两个计数器实现,方法计数器和回边计数器提供给JVM 统计数据,以定位到热点代码。

勘查优化
  • 编译器查看

-XX:+PrintCompilation 打印编译的细节

XX:UnlockDiagnosticVMOptions -XX:+LogCompilation -XX:LogFile= 输出更多的编译细节

-XX:+PrintInlining 打印内联的发生

  • 代码缓存

可以利用 JMC、JConsole 等查看具体的统计信息。

  • 调整热点代码门限值

利用CompileThreshold 调整JIT的默认门限,利用UseCounterDecay关闭计数器衰减。

  • 调整Code Cache 大小

利用XX:ReservedCodeCacheSize调整Code Cache的大小,可以调整初始化大小-XX:InitialCodeCacheSize

  • 调整编译器线程数,或者选择适当的编译器模式

client模式只会有一个编译线程。而server模式是默认两个,C1和C2,会根据CPU内核数目计算C1 和 C2 的数值。

增强的多处理器中,增大编译线程数,可能会充分利用CPU资源,但是当系统繁忙时,反而会导致线程争抢过多资源。

  • 其他的优化

减少进入安全点。

在 JIT 过程中,逆优化等场景需要插入安全点。

常规的锁优化解决。利用-XX:-UseBiasedLocking 关闭偏向锁。

Redis key的淘汰策略

过期策略

定时删除

在设置某个key 的过期时间的同时,创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。

定期删除

每隔一段时间,我们就对一些key进行检查,删除里面过期的key。

惰性删除

在客户端访问这个key的时候,redis对key的过期时间进行检查,如果过期了就立即删除,不会返回东西。会导致很多过期的key仍然存在内存中。

Redis就是使用惰性删除定期删除两种策略配合使用。

实现方式

惰性删除:Redis的惰性删除策略由 db.c/expireIfNeeded 函数实现.

定期删除:由redis.c/activeExpireCycle 函数实现,函数以一定的频率运行,每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键(默认没100ms一次)。

内存的淘汰策略

默认无限制,通常设置为物理内存的3/4。

当现有内存大于maxmemory时,便会触发redis主动淘汰内存方式,通过设置maxmemory-policy,有如下几种淘汰方式。

  • volatile-lru:利用LRU算法移除设置过过期时间的key
  • allkeys-lru: 利用LRU算法移除任何key。通常使用该方式
  • volatile-random : 移除设置过过期时间的随机key。
  • allkeys-random: 无差别的随机移除。
  • volatile-ttl:移除即将过期的key
  • noeviction: 不移除任何key,只是返回一个写错误,默认选项,一般不会选用。

Redis持久化

为了避免内存中的数据丢失,Redis提供了对持久化的支持,可以选择不同的方式保存到磁盘中。

RDB(Redis Database)

在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是Snapshot快照,恢复时将快照文件直接读入内存。

Snapshot的两种模式

1.SAVE: save只管保存,其他的不管,全部阻塞。

2.BGSAVE: Redis会在异步后台(fork子进程)进行快照操作,主进程仍然可以响应客户端的请求,保存完成后通知主进程,可以通过lastsave命令获取最后一次成功执行快照的时间。

优势
  • 适合大规模的数据恢复
  • 对数据完整性和一致性要求不高
  • 与AOF模式相比,通过RDB文件恢复数据比较快
  • 通过fork进程的方式备份,对Redis服务器的性能影响较小。
劣势
  • 每隔一段时间备份,会丢失最后一次快照后的数据
  • Fork的时候,内存消耗大,会占用2倍的内存
  • 使用save时会造成Redis服务器阻塞,导致不可用。
  • 使用bgsave fork子进程时,如果数据量太大,forks的过程也会发生阻塞另外比较费内存。

AOF(Append-Only-File)

以日志的形式来记录每个写操作,将Redis执行过的所有写指令。只许追加但不可以改写文件。redis载入AOF文件的时候,会创建一个虚拟的client,把AOF中每一条命令都执行一遍。在RDB和AOF备份文件都有的情况下,redis会优先执行AOF中的文件。

AOF备份的三种策略
  • always:将缓存区的内容总是及时写到AOF文件中。
  • everysec: 将缓存区的内容每隔一秒写入AOF文件中(默认策略)。
  • no: 写入AOF文件中的操作由操作系统决定,一般系统会等到缓存区被填满,才会同步数据到磁盘。
AOF重写

为了解决AOF文件膨胀的问题,Redis提供AOF重写功能:Redis服务器可以创建一个新的AOF文件来替换旧文件,Redis保证新旧两个文件所保存的数据库状态一样。

AOF_REWRITE这个函数会保证服务器的高性能,会Fork子进程进而执行AOF重写程序,同时,为了保障主进程和AOF文件数据不一致的问题。Redis增加了 一个AOF缓存区,等子进程完成后 ,会将AOF缓存区中的内容写入到新的AOF文件中。

默认重写出发机制时上次rewrite后大小的一倍并且文件大于64MB。

优点

1.AOF只是追加文件,对服务器的影响小。

2.可以采用不同的策略,在数据的完整性和性能之间做出平衡性。

缺点

1.一般AOF文件要大于RDB文件恢复数据的速度比RDB慢

2.aof运行效率慢于rdb,同步策略效率较好,不同步效率与rdb相同。

mysql基础架构

链接器:连接器负责跟客户端建立连接、获取权限、维持和管理连接。

长链接积累下来会导致内存占用太大,被系统强行杀掉,从而导致MySQL重启。

解决方案:

1.定期断开长链接。

2.利用5.7版本以上mysql_reset_connection来重新初始化资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。

查询缓存:之前的sql会以key-value 的形式存到内存中,根据语句直接返回结果。

不建议使用,对表的更新会清空所有缓存。5.8版本中彻底清除了缓存功能。

分析器:进行sql语句的词法分析和语法分析。

优化器:执行计划生成, 确定索引。

执行器:判断有无权限,根据引擎提供的接口,具体去操控存储引擎。

日志redo log和binlog

redo log:采用的是WAL(Write-Ahead Logging)技术,InnoDB会先把记录写入到redo log里面,并更新内存,I

nnoDB会在适当的时候更新到磁盘里面。InnoDB 的 redo log 是固定大小的,从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。

当write pos追check point是,就代表着要更新进磁盘,然后把checkout point 往前推进。有了redo log,即使是发生崩溃或者重启,数据也不会丢失,这个能力为称为crash-safe。InnoDB 引擎特有的日志。

Binlog:Server层特有的操作日志,称为bingo(归档日志)。binlog没有crash-safe 能力。

两种日志的区别:

1.bin log是server层的日志,而redo log是属于InnoDB特有的。

2.redo是物理日志,记录的做的什么修改。而binlog是逻辑日志,记录语句的原始逻辑。

3.redo log是循环写,而bin log是追加写,不会覆盖。

update 流程顺序

两阶段提交:为了让redo log和bin log日志具有一致性。

事务

事务就是要保证一组数据库操作,要么全部成功,要么全部失败。具有ACID特性

A:原子性

C:一致性

I:隔离性

D: 持久性

事务隔离级别

1.读未提交:指一个事务能读到另一个事务还未提交的数据。

2.读己提交:一个事务提交后,它做的变更在能被其他事务看见。

3.可重复读:一个事务在执行过程中,总跟启动时看到的数据是一致的。

4.串行话:对同一条记录会加锁,当出现锁冲突的时候,必须等待前一个事务执行完成。

事务的启动方式:

1.显示启动,begin或者start tranction。配套提交commit,回滚语句rollback。

2.set autocommit = 0 ,执行语句的时候自动开启,直到主动commit或者rollback。

执行 commit work and chain,则是提交事务并自动启动下一个事务。

索引

索引的作用:索引就是为了提交数据的查询效率,就像是书的目录。

索引的结构:

哈希表:只用于等值查询的场景。

有序数组:适用于等值查询和范围查询的场景,插入和删除成本过高,适用于静态存储。

N叉树:比较均衡,插入和查询的效率为logN。

主键索引:存储整行数据。在InnoDB中,也为称为聚簇索引。

非主键索引:存储主键索引的值。在InnoDB中,也为称为非聚簇索引。

覆盖索引:覆盖索引可以减少树的搜索次数,显著提升查询性能。

MySQL锁

全局锁:对整个数据库实例加锁。提供了全局读锁的方法,命令是Flush tables with read lock。

加锁后,整个数据处于只读操作,任何线程的修改、事务等命令将被阻塞。

使用场景:全库逻辑备份。

为什么不使用set global readonly=true的方式

1.readonly有时被用来执行判断逻辑,例如:主从库。

2.readonly不会主动释放,会让整个库一直处于只读状态,风险较高。

表级锁:表锁和元数据锁MDL(meta data lock)

1.表锁的语法:lock tables … read/write。在链接断开的时候自动释放,也会限制别的线程和本线程的操作对象。

2.MDL 不需要显示的使用,在访问一个表的时候自动加上。MDL的作用是保证读写的正确性。MDL是MySQL 5.5时引入,当对一个表进行增删改查的时候,会加MDL读锁。当对表的结构做变更的时候,加MDL写锁。

读锁之间不互斥,读写锁,写锁之间互斥。

如何安全的给小表加字段

1.kill掉长事务,事务不提交,就会一直占着MDL锁。

2.在alter table语句中设置等待时间

对于全部是InnnoDB引擎的表,备份选择建议使用single-transaction参数。

行锁:针对数据表中行记录的锁。

InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立即释放,而是要等到事务结束时才释放。

死锁和死锁检测

当并发系统中不同线程出现循环等待资源,涉及的线程都在等待别的线程释放资源,就会导致这几个线程都进入无限等待的情况,称为死锁。

死锁实例

事务A和事务B会相互等待。

死锁解决方案

1.直接进入等待超时,可以设置innodb_lock_wait_timeout 来等待。

2.发起死锁检测,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect 设置为 on。死锁检测会耗费大量的cpu资源,每次都需要判断自己的加入是否导致了死锁。

死锁检测的方法

1.确保程序中一定不会出现死锁。

2.控制并发度。

begin和start transaction 命令并不是一个事务的起点,在执行到它们第一个操作InnoDB表的语句,事务才真正启动。如果想立马开启事务,可以使用start transaction with consistent snapshot。

MVCC的实现原理

每次数据更新的时候,会生成一个新的数据版本,并且把transaction id 赋值给这个数据版本的事务ID,记为row_trx_id。

对于当前事务的启动瞬间来说,一个数据的row trx_id,有以下几种可能。

1.如果落在了绿色部分,表示这个版本是已经提交的事务或者是当前事务自己生成的,这个数据是可见的。

2.如果落在了红色部分,则表示这个版本是将来启动的事务生成的,是肯定不可见的。

3.如果落在黄色部分,包括两种情况:

a.若 row trx_id 在数组中,表示这个版本是由未提交的事务生成的,不可见;

b.若row trx_id 不再数组中,表示这个版本是已经提交了的事务生成的,可见;

InnoDB利用了“所有数据都有多个版本”的这个特性,实现了“秒级创建快照”能力。

一个数据版本,对于一个事务视图,除了自己的更新总是可见以外,有三种情况:

1.版本未提交,不可见;

2.版本已提交,但是是在视图创建后提交的,不可见;

3.版本已提交,而且是在视图创建前提交的,可见。

更新数据都是先读后写的,而这个读,只能是当前版本的值,成为当前读。

除了 update 语句外,select 语句如果加锁,也是当前读。

加入读锁(S锁,共享锁):select k from t where id=1 lock in share mode;

加入写锁(X锁,拍他锁):select k from t where id=1 for update;

事务的可重复读怎么实现的?

可重复读的核心就是一致性读;而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。

读已提交和可重复读的逻辑类似,主要的区别是:

1.可重复读的隔离级别下,只需要在事务开的时候创建一致性视图,之后事务里的其他查询都公用这个一致性视图。

2.在读提交隔离级别下,每一个语句执行前都会重新计算出一个新的视图。