二阶段锁
对于Innodb中的行锁,实际上是在需要的时候才加,但并不是不需要了就立即释放,而是等着事务结束了才会释放。其实这句话就是二阶段锁的含义。因此,如果再事务中需要锁多个行,需要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放,这样尽可能减少持有锁的时间。
现在举个例子:需要在美团app上买一张故宫的门票
- 从顾客的账户中扣除门票钱
- 给美团的账户中增加门票钱
- 记录一条交易log
从上面的例子中可以看到,现在需要update两条记录,insert一条记录。事务是一定需要的,不然一旦某条sql失败,会导致数据不一致问题。那么这三条数据如何安排顺序呢?
按照上面二阶段锁的定义,相关行级锁应该持有时间最小,所以这种背景下,两个update一定要放在最后,第三条放在第一。这似乎就已经是比较好的解决方案。
那下面这个例子:
现在要删除10000行数据,有以下几种方案:
- 执行sql:delete from T limit 10000;
- 一个连接中循环20次 delete from T limit 500;
- 20个连接中执行delete from T limit 500;
这几种方法哪个比较好呢?
- 根据二阶段锁,占用这些行级锁的时间很长,导致其他客户端等待资源时间较长
- 长事务分成短事务,每次事务占用锁的时间比较短,其他客户端等待时间较短,可以提高并发度
- 人为制造所竞争,加剧并发量,实际意义不大
二阶段提交
二阶段提交涉及到一个更新sql语句的处理流程和redo log和binlog
现在看下一条更新sql是如何执行的
CREATE TABLE `new_table` (
`id` int(11) NOT NULL,
`num` int(11) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
现在要把id=1的这一行+1,会这么写
update new_table set num = num + 1 where id=1
相关执行流程
涉及到update操作的时候,会涉及到redo log和binlog
redo log
每次更新的时候,可以想一想,其实并不是每次都更立刻更新到磁盘上了,这样的话IO很频繁,响应也会很慢,为了解决这个问题,出现了redo log。
当有一条记录要更新的时候,会先把记录写进redo log里面,并更新内存,这个时候更新就算完成了,就可以返回客户端了。同事,innodb会在适当的时候,将这个操作记录更新到磁盘里面,而这个往往是系统比较空闲的时候做的。但是如果系统更新的频率比较高,redo log写满了,该怎么办?这个时候只能停止更新,先把redo log的内容刷到磁盘里面,把redo log清空
可以想象,把redo log里面的数据刷到磁盘这个过程是很痛苦的,因为磁盘IO比较高,所以这个时候所有的查询更新操作,都会有抖动,这种情况下也叫做刷脏页,相关刷脏页就不介绍了,可以看下面这个介绍
https://gsmtoday.github.io/2019/02/08/flush/
binlog
从上面执行流程的图可以看到,一个sql执行过程中,主要分为客户端、server端、存储引擎。redo log是存储引擎innodb特有的日志,binlog是server端特有的日志。所以说,在使用过程中,如果数据库异常重启,之前提交的记录会在redo log中,因为redo log是存储引擎级别的,所以记录不会丢失,这个也叫做crash-safe
那么为啥会有两部分日志?myIsam出现的比innodb早,之前在server端是有binlog的,binlog做的是归档工作,,归档工作自然是没有crash-safe能力,所以后来innodb用了redo log来实现crash-safe能力。那么这两个日志主要的区别是什么呢?
- redo log是存储引擎级别的,是innodb特有的;binlog是server端持有的,是所有引擎都有的功能
- redo log是物理日志,记录的是在某个数据页做了什么修改;binlog是逻辑日志,记录的是这个语句的原始逻辑,如果是statement格式的话,记录的是sql语句,如果是row格式的话,记录的是行的内容,包含更新前更新后的
- redo log是循环写的,空间固定(一般是4个g,1个g为一组);binlog是可以追加写入的,就是说binlog文件写完会写入下一个文件,不会覆盖之前的记录
下面介绍一下上面sql语句具体的执行流程
- 首先找到id=1这行。因为id是主键,所以直接走主键索引树查找到对应的记录即可
- 看下当前数据所在数据页是否在内存中,如果在内存中,直接返回给执行器,如果没有再内存中,需要把当前数据页加载到内存中。还要看数据页加载到内存后容量是否足够,如果不够,需要刷脏页
- 执行器拿到存储引擎给的数据,把这个值+1,得到新的数据再写会存储引擎
- 存储引擎把这部分数据更新到内存中,同时将这个更新记录到redo log中(在第几个数据页把某个数据改为某个数据),此时redo log处于prepare状态,告诉执行器执行完成,随时可以提交事务
- 执行器生成这个操作的binlog,把binlog写入磁盘
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的redo log改为commit状态
其实前3步应该还比较好理解,后三步事务结束的时候,分成了两个状态处理了
二阶段提交
为什么要把事务的commit分成两个阶段呢?从prepare->commit。这个主要是为了让binlog和redo log的状态是一致的。
如果不用二阶段提交会是什么结果呢?是数据被存储引擎更新到内存后,是先写binlog还是先commit redo log呢?
- 先写binlog。如果在binlog写完以后,server端crash,这就导致redo log还没写,那么如果系统恢复后,redo log并没有记录这行数据有改动,但是binlog记录了这行数据+1,这就导致binlog恢复后多了一个事务,和原来库中的值已经不同了
- 先commit redo log。redo log commit后,mysql进程异常重启,binlog写入失败,日志备份就没这个变化了。那以后如果使用binlog恢复长期的数据,那就跟真实的数据出现出入了。
所以可见,如果不用二阶段提交,怎么样都有可能出现数据不一致。那么为啥二阶段可以保证?
上面第五步结束后,系统server端crash没有通知到存储引擎,redo log没有写入,但是prepare和binlog完整,重启后会自动commit。也就是说这个时候保存的是修改后的数据
上面第四步结束后,由于没有写入binlog,所以会直接回滚。这样binlog和redo log就一致了。