mysql

索引优化策略

  1. 尽量全值匹配
  2. 不要在索引上做任何计算(函数、类型转换)
  3. 在索引上使用范围比较厚,后边的索引会失效(尽量将范围条件放最后)
  4. 尽量使用覆盖索引(查询列与索引列相同)
  5. 不等于要慎用
  6. not null/is not null 对索引也有影响
  7. like查询,开头出现通配符会使索引失效(%任意字符人一次/_单个字符)
  8. 字符类型值得引号不能去掉,否则会进行类型转换导致索引失效
  9. 存在多个or时(索引失效),用union代替
  10. 用exists()代替in(),in不会走索引=》外查询数量大于内查询时用in,外查询数量小于内查询时用exist

回表

通过普通索引查询到的结果中没有目标信息,需要去主键索引树中再次查找

覆盖索引

查询条件做了索引,查询结果刚好与索引完全匹配

前缀索引(最左原则)

当查询条件精准的匹配到索引的左边的连续一列或多列时,索引能够被使用到

索引下推

在MySQL 5.6之前,只能从ID3开始一个个回表。到主键索引上找出数据行,再对比字段值。
而MySQL 5.6 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数

问题

如果你要重建索引 k,你的两个SQL语句可以这么写:

alter table T drop index k;
alter table T add index(k);

如果你要重建主键索引,也可以这么写:

alter table T drop primary key;
alter table T add primary key(id);

答:重建索引k的做法是合理的,可以达到省空间的目的。但是,重建主键的过程不合理。不论是删除主键还是创建主键,都会将整个表重建。所以连着执行这两个语句的话,第一个语句就白做了。这两个语句,你可以用这个语句代替 : alter table T engine=InnoDB。

hash和b+树

hash适合查找一个但是范围查找就非常困难,所以选b+树

为什么innodb必需有主键,并推荐整型自增

  1. 有主建是为了建立索引,如果不设置主键,mysql也会帮我们创建一列字段去作为索引,所以我们应该自己创建主键
  2. 通常在UUID和整形中选择,UUID比整形占用的字节大得多,相对来说每个非叶子节点能存储的索引就少了
  3. 在查找的时候,比较整形的速度比比较字符串的速度快得多,字符串的比较需要先转成ASCII码然后通过值来比大小
  4. 自增为了减少叶子节点分裂次数

any()

select ...from ... where a > any(...);

->

select ...from ... where a > result1 or a > result2 or a > result3;

all()

ALL关键字与any关键字类似,只不过上面的or改成and。即:

select ...from ... where a > all(...);

->

select ...from ... where a > result1 and a > result2 and a > result3;

some()

some关键字和any关键字是一样的功能。所以:

select ...from ... where a > some(...);

->

select ...from ... where a > result1 or a > result2 or a > result3;

存储引擎

存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。现在最常用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认存储引擎。

引擎

并发问题

脏读

事务A第一次读取到事务B未提交的数据,这时候事务B回滚了,那么A读取到的数据就是无效的脏数据

不可重复读

事务A第一次读取到数据,事务B更新了数据并提交了事务,事务A第二次读取数据与第一次不一致(值不一致)

幻读

事务A第一次读取到数据,之后事务B更新了数据提交了事务,事务A第二次读取数据与第一次不一致(记录数不一致)

隔离级别

Read Uncommitted(读未提交)

一个事务还没提交时,它做的变更就能被别的事务看到

Read Committed(读已提交)

一个事务提交之后,它做的变更才会被其他事务看到

Repeatable Read(可重复读)

一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的

Serializable(串行化)

顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行

如何避免长事务

如果一个事务不提交,那么它的对应的回滚段就不会被删除,这样就导致存储空间被占用
首先,从应用开发端来看:

1.确认是否使用了set autocommit=0。这个确认工作可以在测试环境中开展,把MySQL的general_log开起来,然后随便跑一个业务逻辑,通过general_log的日志来确认。一般框架如果会设置这个值,也就会提供参数来控制行为,你的目标就是把它改成1。

2.确认是否有不必要的只读事务。有些框架会习惯不管什么语句先用begin/commit框起来。我见过有些是业务并没有这个需要,但是也把好几个select语句放到了事务中。这种只读事务可以去掉。

3.业务连接数据库的时候,根据业务本身的预估,通过SET MAX_EXECUTION_TIME命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。(为什么会意外?在后续的文章中会提到这类案例)

其次,从数据库端来看:
1.监控 information_schema.Innodb_trx表,设置长事务阈值,超过就报警/或者kill;
2.Percona的pt-kill这个工具不错,推荐使用;
3.在业务功能测试阶段要求输出所有的general_log,分析日志行为提前发现问题;

4.如果使用的是MySQL 5.6或者更新版本,把innodb_undo_tablespaces设置成2(或更大的值)。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。

视图的好处

https://blog.csdn.net/hxnlyw/article/details/81669964

general_log

开启 general log 将所有到达MySQL Server的SQL语句记录下来。
开启通用日志
set global general_log=ON;
一般不会开启开功能,因为log的量会非常庞大。但个别情况下可能会临时的开一会儿general log以供排障使用。
select * from mysql.general_log

slow_log

show global VARIABLES LIKE "long%";
set GLOBAL long_query_time=10;

show GLOBAL VARIABLES LIKE "%slow%";
set GLOBAL slow_query_log=OFF;
对于需要启用慢查日志,又需要比够获得更高的系统性能,那么建议优先记录到文件

全局锁

顾名思义,全局锁就是对整个数据库实例加锁。MySQL提供了一个加全局读锁的方法,命令是
Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。

全局锁的典型使用场景是,做全库逻辑备份。也就是把整库每个表都select出来存成文本。

以前有一种做法,是通过FTWRL确保不会有其他线程对数据库做更新,然后对整个库做备份。注意,在备份过程中整个库完全处于只读状态。

但是让整库都只读,听上去就很危险:

1.如果你在主库上备份,那么在备份期间都不能执行更新,业务基本上就得停摆;
2.如果你在从库上备份,那么备份期间从库不能执行主库同步过来的binlog,会导致主从延迟

官方自带的逻辑备份工具是mysqldump。当mysqldump使用参数–single-transaction的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于MVCC的支持,这个过程中数据是可以正常更新的。

你一定在疑惑,有了这个功能,为什么还需要FTWRL呢?一致性读是好,但前提是引擎要支持这个隔离级别。比如,对于MyISAM这种不支持事务的引擎,如果备份过程中有更新,总是只能取到最新的数据,那么就破坏了备份的一致性。这时,我们就需要使用FTWRL命令了。

所以,single-transaction方法只适用于所有的表使用事务引擎的库。如果有的表使用了不支持事务的引擎,那么备份就只能通过FTWRL方法。这往往是DBA要求业务开发人员使用InnoDB替代MyISAM的原因之一。

你也许会问,既然要全库只读,为什么不使用set global readonly=true的方式呢?确实readonly方式也可以让全库进入只读状态,但我还是会建议你用FTWRL方式,主要有两个原因:

  • 一是,在有些系统中,readonly的值会被用来做其他逻辑,比如用来判断一个库是主库还是备库。因此,修改global变量的方式影响面更大,我不建议你使用。
  • 二是,在异常处理机制上有差异。如果执行FTWRL命令之后由于客户端发生异常断开,那么MySQL会自动释放这个全局锁,整个库回到可以正常更新的状态。而将整个库设置为readonly之后,如果客户端发生异常,则数据库就会一直保持readonly状态,这样会导致整个库长时间处于不可写状态,风险较高。

表级锁

表锁

表锁的语法是 lock tables … read/write。与FTWRL类似,可以用unlock tables主动释放锁,也可以在客户端断开的时候自动释放。需要注意,lock tables语法除了会限制别的线程的读写外,也限定了本线程接下来的操作对象。

举个例子, 如果在某个线程A中执行lock tables t1 read, t2 write; 这个语句,则其他线程写t1、读写t2的语句都会被阻塞。同时,线程A在执行unlock tables之前,也只能执行读t1、读写t2的操作。连写t1都不允许,自然也不能访问其他表。

在还没有出现更细粒度的锁的时候,表锁是最常用的处理并发的方式。而对于InnoDB这种支持行锁的引擎,一般不使用lock tables命令来控制并发,毕竟锁住整个表的影响面还是太大。

元数据锁(meta data lock,MDL)

MDL不需要显式使用,在访问一个表的时候会被自动加上。MDL的作用是,保证读写的正确性。你可以想象一下,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。

因此,在MySQL 5.5版本中引入了MDL,当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。

  • 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
  • 读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。

虽然MDL锁是系统默认会加的,但却是你不能忽略的一个机制。比如下面这个例子,我经常看到有人掉到这个坑里:给一个小表加个字段,导致整个库挂了。

你肯定知道,给一个表加字段,或者修改字段,或者加索引,需要扫描全表的数据。在对大表操作的时候,你肯定会特别小心,以免对线上服务造成影响。而实际上,即使是小表,操作不慎也会出问题。我们来看一下下面的操作序列,假设表t是一个小表。

我们可以看到session A先启动,这时候会对表t加一个MDL读锁。由于session B需要的也是MDL读锁,因此可以正常执行。

之后session C会被blocked,是因为session A的MDL读锁还没有释放,而session C需要MDL写锁,因此只能被阻塞。

如果只有session C自己被阻塞还没什么关系,但是之后所有要在表t上新申请MDL读锁的请求也会被session C阻塞。前面我们说了,所有对表的增删改查操作都需要先申请MDL读锁,就都被锁住,等于这个表现在完全不可读写了。

如果某个表上的查询语句频繁,而且客户端有重试机制,也就是说超时后会再起一个新session再请求的话,这个库的线程很快就会爆满。

你现在应该知道了,事务中的MDL锁,在语句执行开始时申请,但是语句结束后并不会马上释放,而会等到整个事务提交后再释放。

基于上面的分析,我们来讨论一个问题,如何安全地给小表加字段?
首先我们要解决长事务,事务不提交,就会一直占着MDL锁。在MySQL的information_schema 库的 innodb_trx 表中,你可以查到当前执行中的事务。如果你要做DDL变更的表刚好有长事务在执行,要考虑先暂停DDL,或者kill掉这个长事务。

但考虑一下这个场景。如果你要变更的表是一个热点表,虽然数据量不大,但是上面的请求很频繁,而你不得不加个字段,你该怎么做呢?

这时候kill可能未必管用,因为新的请求马上就来了。比较理想的机制是,在alter table语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到MDL写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。之后开发人员或者DBA再通过重试命令重复这个过程。

MariaDB已经合并了AliSQL的这个功能,所以这两个开源分支目前都支持DDL NOWAIT/WAIT n这个语法。

行锁

在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议

for update是在数据库中上锁用的,可以为数据库中的行上一个排它锁。当一个事务的操作未完成时候,其他事务可以读取但是不能写入或更新

1.for update 仅适用于InnoDB,并且必须开启事务,在begin与commit之间才生效。
2.要测试for update的锁表情况,可以利用MySQL的Command Mode,开启二个视窗来做测试。
for update锁住表或者锁住行,只允许当前事务进行操作(读写),其他事务被阻塞,直到当前事务提交或者回滚,被阻塞的事务自动执行 for update nowait 锁住表或者锁住行,只允许当前事务进行操作(读写),其他事务被拒绝,事务占据的statement连接也会被断开

死锁

  • 一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout来设置。
  • 另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。

redo log

InnoDB引擎使用,大小固定可复用,redo log是物理日志,记录的是“在某个数据页上做了什么修改”。

binlog

mysql server层的日志,binlog是逻辑日志,记录的是这个语句的原始逻辑,比如“给ID=2这一行的c字段加1 ”。

crash safe

即使mysql异常重启,数据也不会丢失,redo log 和binlog的两阶段提交模式共同组成了mysql的crash safe能力

你可能感兴趣的:(mysql)