MySQL事务是什么,它就是一组数据库的操作,是访问数据库的程序单元,事务中可能包含一个或者多个 SQL 语句。这些SQL 语句要么都执行、要么都不执行。我们知道,在MySQL 中,有不同的存储引擎,有的存储引擎比如MyISAM 是不支持事务的,所以说MySQL 事务实际上是发生在 存储引擎部分。
事务主要有四大特性,分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和 持久性(Durability)。它实际上是从四个方面来阐述MySQL 事务的特点,下面就分别来看MySQL 通过什么方式来实现这些特性。
一、原子性#
1. 原子性定义#
原子性就是指事务的不可分割性,对于一个事务而言,就是要么都执行,要么都不执行。在MySQL 中是通过回滚来实现,比如事务中的一个 SQL 语句失败了,那么该事务的所有SQL 语句必须都进行回滚,退回到事务前的状态。
2. InnoDB 中原子性的实现#
上面说到,MySQL 中原子性是通过回滚的方式来实现,那么回滚是怎么实现的?这就涉及到MySQL 中的Undo 日志,原子性就是通过 Undo log 来实现的。
具体是Undo log 会在一个事务中,记录当前 SQL 语句的上一个语句成功的执行状态,如果在执行当前 SQL 语句失败后,就可以通过 Undo log 来回滚到 SQL 语句执行前的状态,这样就能保证事务的原子操作。举个例子,比如插入一条记录:insert into test values(1,'刘备','蜀')
实际上得到的记录图如下,中间的 roll_pointer 是指向undo log的指针。
undo log 在事务提交后,undo log 日志也就会被回收。
二、持久性#
1.持久性定义#
持久性是指事务一旦提交,它对事务的改变是永久性的,哪怕系统发生了故障,也不会改变其提交的结果。持久性是通过 Redo log 来实现的。
2.InnoDB 中持久性的实现#
在讲持久性之前,先介绍一下MySQL 中 Buffer pool,我们知道MySQL 数据是存储在磁盘中,为了实现快速读写数据,我们会在内存中设置一个 Buffer pool 缓冲池,数据库可以直接与 Buffer Pool 进行读取交互,定期再将 Buffer Pool 数据存储到磁盘中,这样会大大提高数据库的读写效率。
但是如果系统断电或者宕机,内存是无法保存信息的,而此时刚好Buffer Pool 数据没有同步到磁盘上,就会造成数据丢失。因此就需要 redo log 来对更新和修改操作进行记录,使得在系统重启时能够恢复到原来的状态。
Redo log 是一种预写式日志(write-Ahead Log),它记录的是在某个数据页上做了什么修改。当有记录需要更新时,InnoDB 引擎会先把记录写到 redo log 中,在系统空闲时,再将操作记录更新到磁盘中。redo log 结构如下图所示:
- write pos 是当前记录的位置
- checkpoint 当前要擦除的位置
- write pos 和 check point 之间是 redo 内存区域中还空着的部分,用于记录新的操作。
redo log 只需要记录真正修改的部分,它的同步效率要比 buffer 同步数据快的多。那么 redo log 何时会同步到磁盘中去,主要是 innodb_flush_log_at_trx_commit
这个参数的设置:
- 0:表示当提交事务时,并不将缓冲区的 redo log 写入磁盘的日志文件,而是等待主线程每秒刷新
- 1:在事务提交时将缓冲区的 redo log 同步写到磁盘中,保证一定会写入成功
- 2:在事务提交时将缓冲区的redo 日志异步写入到磁盘中,即不能完全保证 commit 时肯定会写入到 redo 日志文件,只是有这个动作。
建议这个参数设置为1 ,同步写入磁盘中。
3.Binlog 和 Redo log#
binlog 和 redo log 日志的区别#
我们知道 redo log 是InnoDB 存储引擎的事务日志,那么对于 server 层是否也存在事务日志,答案是确定的,server 层的事务日志就是 binlog (归档日志)。为啥会出现两种事务日志,是因为最开始的 MySQL 中并没有 InnoDB 引擎,MySQL 自带的引擎是 MyISAM ,用的就是 binlog 日志来实现事务。那么两者具体有什么区别呢:
- redo log 是InnoDB 引擎特有的,binlog 是 server 层实现,所有的存储引擎都可以使用
- redo log 是物理日志,它存储的是在数据页上的修改;binlog 是逻辑日志,存储的是sql 语句的原始逻辑
- redo log 空间是固定的,会使用完并覆盖原来的日志。binlog 可以追加写入,不会覆盖原来的日志
binlog 和 redo log 日志的两阶段提交#
既然在MySQL 中存在两种日志,那么为了让两份日志之间的逻辑一致,就需要两阶段提交来实现这一任务。具体怎么实现的,我们以这个语句update T set c=c+1 where ID = 2
来看:
- 1.执行器先通过执行引擎查找 ID=2 这一行,如果数据在内存Buffer Pool 中直接返回。如果在磁盘中,则先从磁盘中读取到内存中,然后再返回。
- 2.对取到的数据进行操作,将值加1后得到新的数据,再调用引擎写入数据,更新内存。
- 3.同时将对数据页的修改记录到 redo log 中,这个时候 redo log 处于第一个阶段 prepare。
- 4.执行器生成对于这个操作的 binlog ,并将 binlog 写入磁盘中
- 5.执行器调用提交事务接口,把刚刚写入的 redo log 修改成 commit 状态,更新到此完成。
三、隔离性#
1.隔离性定义#
隔离性是指事务内部的操作与其他事务是隔离的,并发过程中的各个事务之间不能互相干扰。对于事务的操作,主要分成两种:读操作与写操作之间的影响、写操作与写操作之间的影响。
2.隔离性的实现#
上面我们说到了事务之间的影响主要分成两个方面,那么MySQL 中是如何处理这两种情况的呢?
- 写操作与写操作:就像 java 中的锁一样,通过锁来解决(MySQL 锁后续会出一篇文章详细介绍)
- 幻读
- 写操作与读操作:主要是通过 MVCC 机制来解决(MVCC 后续会出一篇文章进行介绍)
- 脏读
- 不可重复读
写操作与写操作的隔离实现#
我们可以通过锁的方式,来保证同一时刻的一个数据的写操作只能被一个事务所执行。
在MySQL 中,根据加锁范围,大致可以分成三类:全局锁、表级锁和行级锁。 在一个事务修改数据前,需要获取对应的锁才能修改对应的数据。其他事务想要修改该数据,必须要等到之前的事务提交或回滚释放锁后,才能抢这个锁来修改数据。
锁的概况可以通过以下语句进行查询:
# 锁的概况
select * from information_schema.innodb_locks;
# InnoDB 整体状态,也包括锁的情况
show engine innodb status
写操作与读操作的隔离实现#
为了保证性能,我们不能把所有操作都进行上锁,对于写操作和读操作,可以使用不加锁的方式来实现事务隔离。主要就是通过MySQL 中的 MVCC 机制来解决。
四、一致性#
一致性的定义与实现#
一致性的实现就是在前面三个特性实现的基础上而来的,没有前面三个特性的实现,也就达不到最后数据库事务的一致性。
参考资料#
https://time.geekbang.org/column/article/68963
https://www.cnblogs.com/kismetv/p/10331633.html
网络摘文,版权归原作者。