MySQL实战45讲(06--10)-笔记

06 | 全局锁和表锁 :给表加个字段怎么有这么多阻碍?

根据加锁的范围,MySQL 里面的锁大致可以分成全局锁、表级锁和行锁三类。

全局锁

MySQL 提供了一个加全局读锁的方法,命令是Flush tables with read lock (FTWRL)

当你需要MySQL处于只读状态,其他线程的语句会阻塞。

全局锁的典型使用场景是,做全库逻辑备份。

表级锁

MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)。

表锁的语法是 lock tables … read/write。

例如: lock tables t1 read, t2 write;

则其他线程写t1、读写 t2 的语句都会被阻塞。

线程 在执行 unlock tables 之前,也只能执行读 t1、写 t2 的操作。连读 t2 都不允许,自然也不能访问其他表。

另一类表级的锁是 MDL(metadata lock)。

MDL 不需要显式使用,在访问一个表的时候会被自动加上。MDL 的作用是,保证读写的正确性。

在 MySQL 5.5 版本中引入了 MDL,当对一个表做增删改查操作的时候,加 MDL 读锁;

当要对表做结构变更操作的时候,加 MDL 写锁。

  1. 读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
  2. 读写锁之间,写锁之间是互斥的,用来保证变更表结构操作的安全性。

因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。(MDL 锁是系统默认会加)

一个小坑:给一个小表加个字段,导致整个库挂了。

A启动,对t加MDL读锁----》B需要MDL读锁,正常----》C会blocked,因为A没有释放,而C要MDL的写锁----》之后所有的新申请也锁住了----》假如客户端有重试机制----》线程很快就会爆

如何安全地给小表加字段?

比较理想的机制是,在 alter table 语句里面设定等待时间,如果在这个指定的等待时间里面能够拿到 MDL 写锁最好,拿不到也不要阻塞后面的业务语句,先放弃。

小结

全局锁 建议你选择使用–single-<transaction 参数
表锁 一般是在数据库引擎不支持行锁的时候才会被用到的

07 | 行锁功过:怎么减少行锁对性能的影响?

行锁就是针对数据表中行记录的锁

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

果你的事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。

死锁和死锁检测

当出现死锁以后,有两种策略:

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

怎么解决由这种热点行更新导致的性能问题呢?

一个思路是控制并发度,这个并发控制要做在数据库服务端,修改 MySQL 源码。

不能实现这样的方案,可以从设计上优化,考虑通过将一行改成逻辑上的多行来减少锁冲突。

08 | 事务到底是隔离的还是不隔离的?

在 MySQL 里,有两个“视图”的概念:

  1. 一个是 view。它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果。创建视图的语法是 create view,而它的查询方法与表一样。
  2. 另一个是 InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。

它没有物理结构,用来在事务执行期间定义“我能看到什么数据”。

“快照”在 MVCC 里是怎么工作的?

InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向InnoDB 的事务系统申请的,是按申请顺序严格递增的。

虚线箭头,就是 undo log(回滚日志),V1、V2、V3 并不是物理上真实存在的,而是每次需要的时候根据当前版本和 undo log 计算出来的。

InnoDB 代码实现上,一个事务只需要在启动的时候,找到所有已经提交的事务 ID 的最大值,记为 up_limit_id;

然后声明说,“如果一个数据版本的 row trx_id 大于 up_limit_id,我就不认,我必须要找到它的上一个版本”。

当然,如果一个事务自己更新的数据,它自己还是要认的。

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

例子:

mysql> 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);

Q1返回的是3---------Q2返回的是1

好,现在事务 A 要来读数据了,它的 up_limit_id 是 99。当然了,读数据都是从当前版本读起的。所以,Q2 的读数据流程是这样的:

  • 找到 (1,3) 的时候,判断出 row trx_id=101 大于 up_limit_id,要不起;
  • 接着,找到上一个历史版本,一看 row trx_id=102,还是要不起;
  • 再往前找,终于找到了(1,1),它的 row trx_id=90,是可以承认的数据。

这样执行下来,事务 A 读到的这个数据,跟它在刚开始启动的时候读到的相同,所以我们称之为一致性读。

这里你可以顺便再想一个问题。(1,1) 这个历史版本,什么时候可以被删除掉呢?

答案是,当没有事务再需要它的时候,就可以删掉。

更新逻辑

事务 B 的 update 语句,读的到底是哪个版本?

在更新的时候,当前读取到的数据是 (1,2),更新后生成了新版本的数据 (1,3),这个新版本的 row trx_id 是 101。

更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读(current read)。

下面这两个 select 语句,分别加了读锁(S 锁,共享锁)和写锁(X 锁,排他锁)

mysql> select k from t where id=1 lock in share mode;

mysql> select k from t where id=1 for update;

现在,我们再回到文章开头的问题:事务的可重复读的能力是怎么实现的?

可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。

如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。

而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:

  • 在可重复读隔离级别下,只需要在事务开始的时候找到那个 up_limit_id,之后事务里的其他查询都共用这个 up_limit_id;
  • 在读提交隔离级别下,每一个语句执行前都会重新算一次 up_limit_id 的值。
事务 A 的 Q2 语句开始执行的时候,由于事务 B(101)、C(102)都已经提交

所以 Q2 的 up_limit_id 的值就应该是事务 C 的 transaction id,

即 102。那么,它在读到(1,3)的时候,就满足了 up_limt_id(102) ≥row trx_id(101) 的条件,所以返回了 k=3。

显然地,语句 Q1 的查询结果 k=3。

小结

InnoDB 的行数据有多个版本,每个数据版本有自己的 row trx_id,每个事务或者语句有自己up_limit_id

普通查询语句是一致性读,一致性读会根据 row trx_id 和 up_limit_id 的大小决定数据版本的可见性。

  • 对于可重复读,查询只承认在事务启动前就已经提交完成的数据;
  • 对于读提交,查询只承认在语句启动前就已经提交完成的数据;

而当前读,总是读取已经提交完成的最新版本。

09 | 普通索引和唯一索引,应该怎么选择?

查询过程

假设,执行查询的语句是 select id from T where k=5。这个查询语句在索引树上查找的过程,先是通过 B+ 树从树根开始,按层搜索到叶子节点,也就是图中右下角的这个数据页,然后可以认为数据页内部通过二分法来定位记录。

更新过程

先介绍一下change buffer

是可以持久化的数据,在内存中有拷贝,也会被写入到磁盘上。

当需要更新一个数据页时,如果数据页在内存中就直接更新,

而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InooDB 会将这些更新操作缓存在 change buffer 中。

change buffer 用的是 buffer pool 里的内存,因此不能无限增大。change buffer 的大小,可以通过参数 innodb_change_buffer_max_size 来动态设置。

什么条件下可以使用 change buffer 呢?

对于唯一索引来说,所有的更新操作都要先判断这个操作是否违反唯一性约束。判断唯一的时候就需要将数据页读入内存,所以没有必要使用change buffer。

实际上也只有普通索引可以使用。

如果要在这张表中插入一个新记录的话,InnoDB 的处理流程是怎样的?

  1. 第一种情况是,这个记录要更新的目标页在内存中。

    普通索引和唯一索引对更新语句性能影响的差别,只是一个判断冲突,只会耗费微小的CPU 时间。

  2. 第二种情况是,这个记录要更新的目标页不在内存中。

    这时,InnoDB 的处理流程如下:

    对于唯一索引来说,需要将数据页读入内存,判断到没有冲突,插入这个值,语句执行结束;

    对于普通索引来说,则是将更新记录在 change buffer,语句执行就结束了。

将数据从磁盘读入内存涉及随机 IO 的访问,是数据库里面成本最高的操作之一。change buffer 因为减少了随机磁盘访问,所以对更新性能的提升是会很明显的。

change buffer 的使用场景

写多读少的业务来说,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。

这种业务模型常见的就是账单类、日志类的系统。

更新模式是写入之后马上会做查询,

那么即使满足了条件,将更新先记录在 change buffer,但之后由于马上要访问这个数据页,会立即触发 purge 过程。这样随机访问 IO 的次数不会减少,反而增加了 change buffer 的维护代价。

索引选择和实践

我建议你尽量选择普通索引,

如果所有的更新后面,都马上伴随着对这个记录的查询,那么你应该关闭 change buffer。而在其他情况下,change buffer 都能提升更新性能。

change buffer 和 redo log

redo log 主要节省的是随机写磁盘的 IO 消耗(转成顺序写),

而 change buffer 主要节省的则是随机读磁盘的 IO 消耗。

10 | MySQL为什么有时候会选错索引?

就是优化器还是有点”笨“

优化器的逻辑

选择索引是优化器的工作

扫描行数是怎么判断的?

MySQL 在真正开始执行语句之前,并不能精确地知道满足这个条件的记录有多少条,而只能根据统计信息来估算记录数。这个统计信息就是索引的“区分度”。

显然,一个索引上不同的值越多,这个索引的区分度就越好。而一个索引上不同的值的个数,我们称之为“基数”(cardinality)。也就是说,这个基数越大,索引的区分度越好。

MySQL 是怎样得到索引的基数的呢?

“采样统计”

在 MySQL 中,有两种存储索引统计的方式,可以通过设置参数 innodb_stats_persistent 的值来选择:
设置为 on 的时候,表示统计信息会持久化存储。这时,默认的 N 是 20,M 是 10。
设置为 off 的时候,表示统计信息只存储在内存中。这时,默认的 N 是 8,M 是 16。

统计信息不对,那就修正。analyze table 表名 命令,通过 analyze 命令可以解决很多问题,也有例外,后面说。

索引选择异常和处理

一种方法是,用 force index 强行选择一个索引。( force index 最主要的问题还是变更的及时性)

第二种方法就是,我们可以考虑修改语句,引导 MySQL 使用我们期望的索引。(例如:order by b limit 1 和 order by b,a limit 1)

第三种方法是,在有些场景下,我们可以新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引。

posted @ 2021-08-04 14:58  Ricardo_ML  阅读(182)  评论(1编辑  收藏  举报