MySQL事务

1,054 阅读8分钟

引言

通俗来讲事务就是多步操作要么全部成功要么全部失败,保证最终状态一致。 为了简化应用程序,使其可以忽略一些潜在错误和并发问题,数据库层对事务的ACID特性做了统一支持。

事务的基本特性

事务有四大特性:原子性(Atomicity) 一致性(Consistency) 隔离性(Isolation) 持久性(Durability),俗称:ACID

  • 原子性:多个操作要么全部执行成功,只要有一个执行失败,就回滚所有操作,回到最初的状态。
  • 一致性:保证数据从一个有效状态,转变到另一个有效状态,只要这些转变状态的数据满足一开始设立的规则就可以了。

Consistency ensures that a transaction can only bring the database from one valid state to another, maintaining database invariants: any data written to the database must be valid according to all defined rules, including constraints, cascades,triggers, and any combination thereof. This prevents database corruption by an illegal transaction, but does not guarantee that a transaction is correct.”

  • 隔离性:一个事物在最终提交之前,数据的改变对其他事务是不可见的。
  • 持久性:事务的最终操作结果能被持久保存。

MySQL事务

接下来我们介绍MySQL中的事务是如何保证ACID特性的。(以下不做特殊说明,都默认InnoDB引擎)

原子性:

我们在MySQL中可以使用以下语句开启一个事务:
BEGIN / START TRANSACTION (START TRANSACTION还可以指定只读事务和读写事务,不过使用的比较少)
提交事务:COMMIT
回滚事务:ROLLBACK
站在使用者的角度,我们开启一个事务,然后执行SQL语句,最终COMMIT成功则所有的SQL都会成功执行,如果失败执行ROLLBACK操作所有的语句都回到最初状态。 MySQL对数据的变更操作总是会记录undo log, undo log中记录了一条数据被修改的链条,当需要执行回滚操作时,根据对应的事务ID,找到回滚点对数据执行恢复操作。如果执行commit的话,MySQL把最终结果刷到磁盘中。 为了保证事务执行中,异常宕机导致事务仍可回滚,undo log的数据也是会被持久化到磁盘的。如果一些比较大的事务undo内存缓存失效时,回滚操作可能会伴随大量的磁盘IO。

一致性:

MySQL中的一致性主要包括,数据在任何时候状态都是一致的,如何保证在实例异常崩溃情况下的一致性,MySQL主要依靠Doublewrite、crash recover来保证事务的一致性。 为什么需要Doublewrite
InnoDB是以页为单位存储数据的
Page Size默认16KB
EXT家族文件系统的IO最小单元通常设置为1KB 2KB 或者4KB(传送门)
磁盘的最小IO是一个扇区:512字节
因此再写一页数据时,有可能存在写了2KB电脑异常断电的情况,这个时候的页数据是损坏的。 为了解决这个问题MySQL在脏数据写入磁盘时,

  • 先把数据复制到Doublewrite buffer
  • 把数据顺序写入共享表空间的文件(第一次写,因为是顺序写所以效率很高)
  • 然后把数据分别写入各个表空间(第二次写,随机写效率低) 如果在写各表的表空间时发生异常,可以依赖共享表空间的数据进行数据恢复。(Doublewrite会带来一些性能损耗,可以根据自己实际的业务场景选择是否关闭) crash recover主要是依赖MySQL的binlog和InnoDB的redo log,在实例异常崩溃重启之后,进行数据恢复。具体过程这里就不描述了,感兴趣的同学可以参照传送门

隔离性:

InnoDB支持四种隔离级别:

  • 读未提交(READ-UNCOMMITTED):事务在commit之前,它的变更就能被别的事务读到
  • 读已提交(READ-COMMITTED):事务在commit之后,他的变更才能被看到
  • 可重复读(REPEATABLE-READ):在一个事务内部读到的数据总是一致的
  • 序列化(SERIALIZABLE):出现读写锁冲突时,事务要依次顺序执行 InnoDB引擎默认是可重复读隔离级别,也是业务场景中使用最多的隔离级别。下面我们主要介绍下,事务可重复读的实现方式。 在可重复读隔离级别下,事务启动时,会给数据创建一个“视图”,这里说视图并不是物理存在的,而是逻辑上的快照。 前文已经提到过,当我们对数据进行变更时,会产生undo log记录,记录的变更操作包括INSERT/UPDATE/DELETE。INSERT的变更记录处理比较简单,在事务commit之后直接删除就可以了。UPDATE和DELETE的变更记录稍微复杂一些,需要维护多个版本信息。InnoDB中的每一个事务也会依照顺序生成递增的事务ID(trx_id)。下图就是一条记录在三个事务中存在三个版本的示意图。可以依据当前的trx_id在unlog的“链表”中,找到记录在当前事务的状态,这也是MVCC的实现原理。

image.png

举一个具体的例子: 我们先构造一张curriculum(课程表):

idteacher
11
33
55

事务的执行顺序:

事务一事物二
START TRANSACTION WITH CONSISTENT SNAPSHOTSTART TRANSACTION WITH CONSISTENT SNAPSHOT
update curriculum set teacher=100 where id = 1;
commit;
select teacher from curriculum where id=1;
commit;

此时事务一中查到的teacher值是1。这里很容易理解,因为我们开启了事务并创建了视图,id=1的记录虽然在事务二中被修改为100了,但是在事务一仍然可以根据trx_id和undo log中的“链条”找到自己当前trx_id对应的id=1记录的值。

我们再举一个例子:

事务一事物二
START TRANSACTION WITH CONSISTENT SNAPSHOTSTART TRANSACTION WITH CONSISTENT SNAPSHOT
update curriculum set teacher=100 where id = 1;
commit;
update curriculum set teacher=teacher+1 where id = 1;
select teacher from curriculum where id=1;
commit;

和上面的例子相比我们在事务一的查询之前,增加了一条update语句,那么这时查询到teacher的值是多少呢?查询出来的值是101。 为什么是101不是2呢?因为事务二已经对id=1的列进行了修改,此时如果事务一还是按照事务初始记录的状态更新数据会导致,事务二的更新操作丢失,而出现数据不一致,这个时候就需要读当前数据了。总结起来就是,* 查询操作默认“快照读”,有了更新操作和更新操作后的查询需要获取“当前读”的数据。 *

进一步演化下这个例子:

事务一事物二
START TRANSACTION WITH CONSISTENT SNAPSHOTSTART TRANSACTION WITH CONSISTENT SNAPSHOT
update curriculum set teacher=100 where id = 1;
update curriculum set teacher=teacher+1 where id = 1;
select teacher from curriculum where id=1;
commit;
commit;

把事务二的commit放到了事务一的更新语句之后。此时因为事务一和事务二同时修改id=1这一条记录,事务二首先获取到了此记录的写锁,事务一锁等待,等事务二commit完成之后,事务一中的update语句才能正常执行,执行结果和上一个例子一样。 MySQL的锁比较复杂,我们单独有一篇文章介绍传送门

持久性:

MySQL为了保证数据不丢失记录了多份日志。 首先是binlog,binlog的写入分为两步:

  • 把日志写入binlog cache(内存)
  • 把binlog cache刷入磁盘 事务提交时,MySQL的执行器会把binlog cache里的完整事务写入binlog中,并清空binlog cache。每个线程都有自己的binglog cache。binlog写文件的时候还涉及到两个操作一个是write一个是fsync,write只是把数据写入了文件系统的缓冲区,fsync会直接落盘。MySQL提供了一个参数sync_binlog。 sync_binlog=0时每次提交事务只write不主动fsync,sync_binlog=n时表明n次commit之后,统一调用fsync落盘。 redo log的写入和binlog是很像的也是有两个阶段
  • 把事务的日志写到redo log buffer(内存)
  • redo log buffer中的数据刷入磁盘 不过两者又有所区别,redo log buffer通过innodb_flush_log_at_trx_commit参数决定落盘的策略。
  • 值为0时:每次事务提交都只把redo log留在redo log buffer
  • 值为1时:每次事务提交把redo log直接fsync到磁盘
  • 值为2时:每次事务提交把redo log 写到文件系统的缓存 InnoDB会定时检测事务的日志,按照上面的配置策略进行落盘。 (画外音:为了提升写磁盘的效率,MySQL采用了“组提交”机制,减少刷盘的次数。) MySQL为了保证最终落盘的数据是准确的,采用了两阶段提交的策略:
  • 先写redo log,进入prepare阶段
  • 再写binlog,等binlog完成后,commit,提交事务 这样当实例异常崩溃重启之后,如果redo log中已经commit,则数据继续提交就好了;如果redo log只在prepare状态,则判断binlog是否存在完整事务日志,如果日志完整继续commit事务,如果不完整根据undo log中的数据版本进行事务回滚。

参考文献:
mysql.taobao.org/monthly/201… mysql.taobao.org/monthly/201… mysql.taobao.org/monthly/201… dev.mysql.com/doc/refman/…