MySQL深入研究--学习总结(5)

前言

接上文,继续学习后续章节。细心的同学已经发现,我整理的并不一定是作者讲的内容,更多是结合自己的理解,加以阐述,所以建议结合原文一起理解。

第20章《幻读是什么,幻读有什么问题?》

先看下幻读的定义:

在一个事务中,两次执行同一个查询SQL,后一次执行结果比前一次执行结果数量变多了,称之为幻读。

在隔离级别中的定义,我们知道RR级别是无法避免幻读的?但是在innoDB中是如果做到避免幻读问题呢?其实innoDB在RR级别下解决幻读问题也并不完美。

现有一张表t,有三个字段 id主键,普通索引C,不带索引d, 目前有数据id=1,d=5,c=5;id=4,d=5,c=5;

事务A 事务B 事务C
begin;
select * from t where d =5 for update;Q1
update t set d =5 where id =4;
select * from t where d =5 for update;Q2
insert into t values (5,5,5)
select * from t where d =5 for update;Q3
commit;

首先我们知道innoDB在RR级别下,select语句是快照读,不存在幻读问题,快照读的实现方式我们之前阐述过,通过MCVV多版本并发控制解决的,本质就是通过undo log实现的,如果还需要更详细的研究,可以参考《一文讲透MVCC原理》

Q2读到事务B的更新预警,属于当前读看到的,不属于幻读.幻读指的是新插入的行。

首先我们看下,幻读不解决可能会出现哪些问题?

1、首先如果只加行锁,执行Q1时,我们希望的是所有d=5的数据都锁定,但d=5的是id=1的数据,只会对id=1加上行锁.当事务B执行时,是对id=4进行更新操作,所以可以操作.同样的事务C也可以执行,那么就违背了,Q1的语义.

2、数据一致性问题:如果事务A在Q1时候又执行了一个按d=5条件更新语句,那么在所有事务都提交后,落入到binlog中的顺序是,事务B更新一个语句把id=4的d改成了5,事务C插入了一个id=5,d=5的数据,事务A执行了一个把d=5的数据更新。此时如果按binlog的执行顺序,则会把事务B事务C的逻辑全部改掉,造成了数据不一致的问题。

所以innoDB为了解决此类幻读问题,就引入了间隙锁(Gap lock);

顾名思义就是在行与行之间也加上锁.

比如上面例子中,有id=1和id=4两个数据,那么我不仅在行上加锁,我再(- ∞ ,1](1,4],(4, + supremum】上也加上锁,那么当你再插入数据的时候,就无缝可入了就会被阻塞住。

但需要注意的是,在行锁中,读写锁,写写锁互相冲突,当一个事务加间隙锁,另外一个事务也可以对同样的范围加间隙锁。

注意:

当我们执行更新加间隙锁时,如果where条件不是索引,那么就会对所有行加行锁和行之间数值范围加间隙锁。

如果执行的where条件是等值并且是非唯一索引列,比如id=5,那么会对id=5加行锁,以及(id=5上一个id,id=5]和(id=5,id=5的下一个id值]加间隙锁。如果是唯一索引,那么就只加行锁。

第21章《为什么只改一行代码,锁这么多》

这章节,老师总结的加锁规则,非常受用:两个原则,两个优化,一个BUG

两个原则:

1、加锁的基本单位是以next-key lock。就是左开右闭。

2、查找过程中访问的到对象才会加锁。

两个优化:

1、在索引上等值查询条件,如果是唯一索引,会优化成行锁。

2、在索引上等值查询条件,会向右遍历找到第一个不符合条件的为止,并会优化成间隙锁。

一个bug:

在唯一索引上的范围查询,会访问到不满足条件为止。

带着这些规则,下面看八个案例,以表T为背景,来实践下。

CREATE TABLE`t`(
`id`int(11)NOT NULL,
`c`int(11)DEFAULT NULL,
`d`int(11)DEFAULT NULL,
PRIMARY KEY(`id`),
KEY`c`(`c`)
) ENGINE=InnoDB;

insert intot values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);

案例一:等值查询的情况

update t set d=d+1 where id =7;

根据主键id进行更新时,由于id=7不存在。根据规则1加锁单位next-key lock :(5,10] 根据优化2: 退化成间隙锁(5,10);

此时分别执行两个SQL:

insert into t values(8,8,8) --由于间隙锁,会block
update t set d=d+1 where id =10; --不在间隙锁范围内,执行成功

经过实验,证实如此。

案例二:非唯一索引等值查询

begin;
select id from t where c=5 lock in share mode;

根据优化2 可知会在索引C间隙锁 (5,10);

但需要注意的是:根据规则2 只有访问到的对象才会加锁,由于使用了覆盖索引,所以不会再主键id上再加锁。

update t set d=d+1 where id =5; --执行不受阻
insert into t values(7,7,7); --blocked

案例三:主键索引范围查询

select * from t where id =10 for update;
select * from t where id>=10 and id <11;

首先从结果上看,这两个SQL执行结果是一样的,但是按照上面总结的加锁规则,加锁过程是不一样的。

SQL1 唯一索引等值查询,则加的是id=10的行锁。

SQL2 首先会查询到id=10这行,加锁单位next-key lock,(5,10],根据优化1,退化成行锁,只加id=10这一行,

再查询id>10,向右找到第一个不满足条件的行id=15,加next-key lock (10,15]

案例四:非唯一索引范围锁

select * from t where c>= 10 and c<11 for update;

首先找到c=10这一行,加next-key (5,10],再根据优化2,向右查询到第一个不满足条件的值,退还成间隙锁(10,15]

案例五:唯一索引范围锁bug

select * from t where id>10 and id<=15 for update;

正常id是唯一索引,找到id=15这时 ,应该就停止扫描了,但是innoDB会依然扫描到第一个不满足条件的id=20为止。

所以就会加(10,15],(15,20],导致id=20这样也会锁上,这是没有必要的。这个bug已被官方证实,但还未修复。

案例六:未唯一索引存在多个相同值的情况

--先插入一条c=10的值,这样表中存在2个c=10的行
insert into t values(30,10,30);

当我们在c=10加锁时,加锁的范围是next-key lock (5,10] next-key lock (10,10]和间隙锁 (10,15)

案例七:limit 语句加锁

delete from t where c =10 limit2;

与案例7的加锁范围不同的时,不会再加间隙锁(10,15);

因为当扫描到c=10,id=30这一行时,已经满足limit2的条件,不会再往后扫描,也就不会再加间隙锁(10,15),从而缩小了锁的范围。

所以在删除数据的时候,我们尽量加limit ,即可以控制删除数量,又可以缩小锁的范围。

案例八:一个说死锁的例子

A执行第一句SQL时,按加锁规则,会加next-key lock (5,10]和间隙锁(10,15)。

B执行时,也会加next-key lock(5,10],会被阻塞.

A再次执行插入数据(8,8,8)时,也会被阻塞,从而导致死锁。

这是为什么了?B 不是没有加锁成功吗?但是我们要注意的时,加next-key lock 本质就是加间隙锁再加行锁。由于间隙锁加之间不是互斥的,所以B加(5,10)的间隙锁时成功了,加c=10的行锁失败。

所以当A再次插入数据时,已被间隙锁阻塞。

第23章《MySQL怎么保证数据不丢失的?》

binlog写入机制

事务执行时,先把日志写到binlog cache,事务提交后再把binlog cache 写入binlog文件。

binlog_cache_size控制了每个线程的binlog cache的内存的大小。如果超过了这个参数就临时存到磁盘中。

但注意的是,binlog cache并不一定是直接落入磁盘的,是先写入binlog files,再刷入磁盘。

这个可以通过参数sync_binlog控制:

设置0,表示每次只写入binlog files,不刷磁盘

设置1,表示每次事务提交,也会刷磁盘

设置n,表示每次都写入binlog files,但是等累计了n个事务才刷磁盘

常见设置为"100-1000",但同时也会有丢失的风险.

redo log写入机制

事务执行时,先把日志写入 redo log buffer,事务提交时,写入文件系统page cache 或者刷入磁盘.也是通过参数来设置:innoDB_flush_log_at_trx_commit

设置0,表示每次事务提交时,依然保留在redo log buffer

设置1,表示每次事务提交直接刷入磁盘

设置2,表示每次事务提交都只写到page cache.

那是不是0,2状态是不是就永不刷到磁盘了,那不是会有丢失的风险吗?

其实不是的,innoDB有一个后台线程,每隔1秒,会把redo log buffer中的日志,写到page cache,然后再调用fsync刷到磁盘.

由于这个后台线程的存在,所以也会把还未提交事务的redo log 也持久化到磁盘.

如果你的如果你的MySQLMySQL现在出现了性能瓶颈,而且瓶颈在现在出现了性能瓶颈,而且瓶颈在IO上,可以通过哪些方法来提升性能呢?上,可以通过哪些方法来提升性能呢?
针对这个问题,可以考虑以下三种方法:
1.设置binlog_group_commit_sync_delay和binlog_group_commit_sync_no_delay_count参数,减少binlog的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。
2.将sync_binlog设置为大于1的值(比较常见是100~1000)。这样做的风险是,主机掉电时会丢binlog日志。
3.将innodb_flush_log_at_trx_commit设置为2。这样做的风险是,主机掉电的时候会丢数据。

posted @ 2021-03-16 09:44  有梦想的老王  阅读(719)  评论(0编辑  收藏  举报