1. InnoDB是如何实现事务的
- 原子性:在Innodb 由
undo log
日志保证事务的原子性,它在执行更新操作前会将旧值写入 undo log 日志文件,可根据该文件回滚,mysql服务器内部可以依赖binlog日志 - 一致性: 保证了原子性、隔离性和持持久性就可以保证了
- 隔离性: 由MVCC多版本控制保证的
- 持久性: 由
buffer pool
+redo log
日志保证的- mysql在执行修改操作将数据写到内存后,会将数据写入写redo log 日志文件
- 如果事务提交成功,但buffer poll的数据还没来得及写入磁盘就宕机了,那么可以用redo log 里面的数据恢复buffer pool里的缓存数据
并发事务处理带来的问题
- 脏读(Dirty Reads)
一句话:事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B事务回滚,A读取的数据无效,不符合一致性要求。 - 不可重读(Non-Repeatable Reads)
一句话:事务A内部的相同查询语句在不同时刻读出的结果不一致,不符合隔离性 - 幻读(Phantom Reads)
一句话:事务A读取到了事务B提交的新增数据,不符合隔离性
隔离性有四个隔离级别
注意: 可重复读的MVCC机制为什么不能解决幻读?
- select操作,是快照读(历史版本)
- insert、update和delete,是当前读(当前版本)所以可重复度隔离级别存在幻读
2. 锁
3. Innodb引擎SQL执行的BufferPool缓存机制
假设要执行以下更新操作
UPDATE user set name='wuzhu' where id = 2;
- 执行器先找存储引擎取 ID=2 这一行。ID 是主键,存储引擎直接在主键索引树搜索找到这一行。如果ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
注意: 这边从磁盘读取的话,是读取到ID=2的数据所在文件页的整页数据到Buffer Pool
-
执行器拿到引擎给的行数据,执行set操作,调用存储引擎接口写入数据。
-
存储引擎将这行新数据更新到
Buffer Pool
中,同时将这个更新操作记录到redo log
里面,此时 redo log 处于prepare
状态。然后告知执行器执行完成了,随时可以提交事务。 -
执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
-
执行器调用存储引擎的提交事务接口,存储引擎把刚刚写入的 redo log 改成提交
commit
状态,更新完成。
2.1 基于2PC的一致性保障
- 从这你可以发现一个关键的问题,那就是必须保证Redo Log和Binlog在事务提交时的数据一致性,要么都存在,要么都不存在。
- MySQL是通过 2PC(two-phase commit protocol)来实现的。
- 2PC它是一种保证分布式事务数据一致性的协议,它中文名叫两阶段提交,它将分布式事务的提交拆分成了2个阶段,分别是Prepare和Commit/Rollback
- Prepare阶段,将Redo Log写入文件,并刷入磁盘,同时将Redo Log状态设置为Prepare。Redo Log写入成功后,再将Binlog同样刷入磁盘后
- Commit阶段,向磁盘中的Redo Log写入Commit标识,表示事务提交。然后执行器调用存储引擎的接口提交事务。这就是整个过程。
这就是2PC提交Redo Log和Binlog的过程,那在这个期间发生了异常,2PC这套机制真的能保证数据一致性吗?
2.2 验证2PC机制的可用性
- 假设Redo Log刷入成功了,但是还没来得及刷入Binlog, MySQL就挂了。
- 此时重启之后会发现Redo Log并没有Commit标识,此时根据记录的事务id找到这个事务,进行回滚。
- 如果Redo Log刷入成功,而且Binlog也刷入成功了,但是还没有来得及将Redo Log从
Prepare
改成Commit
MySQL就挂了,此时重启会发现虽然Redo Log没有Commit标识,但是通过XID查询到的Binlog却已经成功刷入磁盘了。 - 此时,虽然Redo Log没有Commit标识,MySQL也要提交这个事务。
- 因为Binlog一旦写入,就可能会被从库或者任何消费Binlog的消费者给消费。
- 如果此时MySQL不提交事务,则可能造成数据不一致。
- 而且目前Redo Log和Binlog从数据层面上,其实已经Ready了,只是差个标志位。
2.3 redo log buffer 什么时候执行刷盘逻辑?
InnoDB存储引擎为redo log的刷盘策略提供了 innodb_flush_log_at_trx_commit
参数,它支持三种策略
- 设置为0的时候,表示每次事务提交时不进行刷盘操作,等后台线程每隔1秒做一次刷盘
- 设置为1的时候,表示每次事务提交时都将进行刷盘操作(默认值)
- 设置为2的时候,表示每次事务提交时都只把redo log buffer内容写入os cache
- 这种策略在事务提交之前会把redo log写到os cache中,但并不会实时地将redo log刷到磁盘,而是InnoDB存储引擎有一个后台线程,每隔1秒会执行一次刷新磁盘操作。
- 这种情况下如果MySQL进程挂了,操作系统没挂的话,操作系统还是会将os cache刷到磁盘,数据不会丢失。
- 但如果MySQL所在的服务器挂掉了,也就是操作系统都挂了,那么os cache也会被清空,数据还是有可能会丢失
另外InnoDB存储引擎有一个后台线程,每隔1秒,就会把redo log buffer中的内容写到文件系统缓存(os cache),然后调用fsync刷盘。
注意:
也就是说,一个没有提交事务的redo log记录,也可能会刷盘,因为在事务执行过程数据记录是会写入redo log buffer中,而这些记录会被后台线程刷盘(写到磁盘里的redo log文件)
假如一个事务将age=1修改成了age=2,在事务还没有提交的时候,后台线程已经将age=2从 redo log buffer 刷入了磁盘,这个时候会把之前存入undo log的回滚记录 回滚
下面是不同刷盘策略的流程图
为0时,如果MySQL挂了或宕机 可能会有1秒数据的丢失
。
为1时, 只要事务提交成功,变更的数据记录就 一定在硬盘里,不会有任何数据丢失。
如果事务执行期间MySQL挂了或宕机,这部分日志丢了,但是事务并没有提交,所以日志丢了也不会有损失。
为2时, 这种策略在事务提交之前会把redo log写到os cache中,但并不会实时地将redo log刷到磁盘,而是会每1秒执行一次刷新磁盘操作。
这种情况下如果MySQL进程挂了,操作系统没挂的话,操作系统还是会将os cache刷到磁盘,数据不会丢失
2.4 为什么Mysql不能直接更新磁盘上的数据而且设置这么一套复杂Bufferpool缓存机制来执行SQL了?
首先Innodb存储引擎是以页为单位来管理存储空间的,一个数据页默认大小是16KB
- 刷新一个完整的数据页太浪费了,有时候我们仅仅修改了某个文件页中的一个字节,但是由于Innodb以页为单位来进行磁盘I/O的,也就是说该事物提交的时候不得不将一页完整的数据从内存写到磁盘。
- 我们又知道一个文件页默认16KB,如果因为修改了一个字节,每次都将16kb完整的一页数据写回磁盘,显然太耗费资源了。
- 其次,一个事务的提交可能包含很多语句,即使一条语句也可能需要修改多个页面的内容,更为不幸的是这些页数据可能
并不相邻
,这就意味着在将某个事务修改的Buffer Pool写回磁盘时,需要进行很多随机的磁盘I/O,相对于顺序I/O来说,随机I/O写磁盘非常的缓慢。 - 但是,写redo log的话只是把 “在某个数据页上做了什么修改” 通过
顺序写
的方式写到redo log(只包含表空间号、数据页号、磁盘文件偏移 量、更新值) - 所以用redo log形式记录修改内容,性能会远远超过刷数据页的方式,这也让数据库的并发能力更强。
Mysql这套机制看起来复杂,但它同时还能保证各种异常情况下的数据一致性。
2.5 buffer pool内容什么时候会刷到磁盘?
innodb后台会有一个IO线程,会不定时把Buffer Pool的page页刷到磁盘idb文件
如果在还没有执行刷盘的操作前,数据库宕机了,Buffer Pool缓存里的数据丢失了,如何恢复?可以恢复吗?
mysql重启时,会把redo log日志文记录的修改内容,恢复Buffer Pool数据,实现磁盘idb文件的最终一致性