MySQL事务
引言
通俗来讲事务就是多步操作要么全部成功要么全部失败,保证最终状态一致。为了简化应用程序,使其可以忽略一些潜在错误和并发问题,数据库层对事务的ACID特性做了统一支持。
事务的基本特性
事务有四大特性:原子性(Atomicity) 一致性(Consistency) 隔离性(Isolation) 持久性(Durability),俗称:ACID
- 原子性:多个操作要么全部执行成功,只要有一个执行失败,就回滚所有操作,回到最初的状态。
- 一致性:保证数据从一个有效状态,转变到另一个有效状态,只要这些转变状态的数据满足一开始设立的规则就可以了。
- 隔离性:一个事物在最终提交之前,数据的改变对其他事务是不可见的。
- 持久性:事务的最终操作结果能被持久保存。
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,在实例异常崩溃重启之后,进行数据恢复。具体过程这里就不描述了,感兴趣的同学可以参照(http://mysql.taobao.org/monthly/2018/07/05/)。
隔离性:
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
的实现原理。
举一个具体的例子:我们先构造一张curriculum(课程表):
id | teacher |
---|---|
1 | 1 |
3 | 3 |
5 | 5 |
事务的执行顺序:
事务一 | 事务二 |
---|---|
START TRANSACTION WITH CONSISTENT SNAPSHOT | START 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 SNAPSHOT | START 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 SNAPSHOT | START 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中的数据版本进行事务回滚。
参考文献: http://mysql.taobao.org/monthly/2015/12/01/ http://mysql.taobao.org/monthly/2015/04/01/ http://mysql.taobao.org/monthly/2018/07/05/ https://dev.mysql.com/doc/refman/8.0/en/innodb-doublewrite-buffer.html
相关文章
- MySQL Error number: MY-010854; Symbol: ER_BINLOG_CANT_FIND_LOG_IN_INDEX; SQLSTATE: HY000 报错 故障修复 远程处理
- MySQL中自增整数ID的应用(mysql递增id)
- MySQL与微软携手共同开发数据库新时代(mysql微软)
- 设计MySQL实体类设计实战(mysql实体类)
- MySQL中的时间数据类型及其应用(mysql时间数据类型)
- 认识MySQL命令行:入门教程(进入mysql命令行)
- MySQL数据库管理程序:最佳实践(mysql管理程序)
- MySQL查询中的去重处理(mysql去重查询)
- Mysql安全性之易语言实现(易语言mysql安全)
- MySQL中字符串汉字排序的方法(mysql汉字排序)
- MySQL事务和锁:保证数据一致性与并发性(mysql事务与锁)
- 提高数据库技能,推荐适合初学者的Mysql学习书籍!(mysql学习书籍)
- MySQL更新操作的实现方法介绍(mysql如何更新)
- MySQL 数据监听:监测数据库操作(mysql数据监听)
- 如何使用 MySQL 导出所有表?快速简单的步骤(mysql导出所有表)
- MySQL查看表的类型的方法(mysql 查看表类型)
- 深入解析MySQL的Next Row功能(mysql中下一行)
- MySQL中LIKE的匹配原理(mysql中like原理)
- MySQL中like函数的使用方法及注意事项(mysql中like函数)
- 解决C语言与MySQL之间的连接问题(c mysql连接问题)
- 使用Ajax和Mysql实现简单点赞功能(ajax mysql点赞)
- 2019年MySQL服务器环境安装指南(2019mysql安装)
- 探究MySQL多连接环境下的事务处理(mysql不同连接的事务)
- MySQL应该选择32位还是64位(mysql下64还是32)