MySQL(六) 数据库事务
目录
开发人员和数据库打交道时,总是会用到事务,其中最经典的例子就是转账。转账逻辑具体到程序里会有一系列的操作,比如查询余额、做加减法、更新余额等,如果单独看待转账这个需求,需要保证的是转账前后,用户的余额必须保证是一致的即可;另一方面,事务同样也是 MySQL 等关系型数据库区别于 NoSQL 的重要方面,是保证数据一致性的重要手段。本文主要了解 MySQL 中事务的实现原理以及相关的事务并发控制协议理论。
¶概述
❓为什么需要事务?
简而言之,通过事务能够保证数据库中的一组操作,要么全部成功,要么全部失败,在 MySQL 中,事务支持是在引擎层实现的。由于 MySQL 的引擎是可插拔式的,所以并不是所有的引擎都支持事务,比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 会被 InnoDB 取代的原因之一。
🤔 一个数据库有了 ACID 能怎么样?
📓 按照严格的标准,只有同时满足 ACID 特性才是事务;但是在各大数据库厂商的实现中,真正满足 ACID 的事务少之又少,例如:
👴 因此与其说 ACID 是事务必须满足的条件,不如说它们是衡量事务的四个维度
¶事务的 ACID 特性
如果一个数据库声称支持事务,那么该数据库必须要具备 ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性)四个特性,各个特性具体含义将在下面进行阐述。
¶原子性(Atomicity)
原子性是指事务包含的所有操作要么全部成功执行,要么全部执行失败并回滚。其中事务成功的条件是事务里的所有操作都成功,只要有一个操作失败,整个事务就失败,需要回滚。具体而言,原子性是指一个事务是一个不可分割的工作单位。通过原子性能够保证,事务中的 sql 语句要么都做,要么都不做;如果事务中一个 sql 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态
MySQL 的日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等。InnoDB 存储引擎提供了两种事务日志:redo log
&undo log
,其中redo log
用于保证事务持久性;undo log
则是事务原子性和隔离性实现的基础
undo log
属于逻辑日志,它记录的是 sql 执行相关的信息,当发生回滚时,InnoDB 会根据undo log
的内容做与之相反的工作,执行语句大致如下:
¶一致性(Consistency)
一致性是指任何一个数据库事务的执行,应该让数据库保持在一致状态,并不会改变数据库原本的一致性约束
一致性时事务追求的最终目标,ACID 中的原子性、持久性和个理性,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。实现一致性的措施包括如下几点:
¶隔离性(Isolation)
数据库能够提供给多个用户并发访问,当多个用户同时操作同一张表时,数据库就会为每一个用户开启的事务,避免其他事务的操作对自身事务的影响。通过事务的隔离特性,能够保证多个并发事务之间不会互相影响。隔离性在数据库系统中是由隔离级别来表现的,这也是本文接下来详细介绍的重点。
既然隔离性追求的是并发情形下事务之间互不干扰,简单考虑的话,事务的隔离性主要可以分为两个方面:
¶持久性(Durability)
持久性是指一旦一个事务提交后,事务所做的修改将会永久的保存在数据库上,即使数据库系统出现宕机等其他故障,已经完成的事务操作也不会丢失。
其实,持久性是一个有点模糊的概念,因为实际上持久性也分很多不同的级别。有些持久性策略能够提供非常强的安全保障,而有些则未必。并且不可能有能做到 100% 的持久性保证的策略,故障恢复的错误率难免会发生。
InnoDB 作为 MySQL 的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘 IO,效率会很低。为此,InnoDB 提供了缓存(Buffer Pool),Buffer Pool 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从 Buffer Pool 中读取,如果 Buffer Pool 中没有,则从磁盘读取后放入 Buffer Pool;当向数据库写入数据时,会首先写入 Buffer Pool,Buffer Pool 中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。
Buffer Pool 的使用大大提高了读写数据的效率,但是也带了新的问题:如果 MySQL 宕机,而此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。
于是,redo log 被引入来解决这个问题:当数据修改时,除了修改 Buffer Pool 中的数据,还会在 redo log 记录这次操作;当事务提交时,会调用 fsync 接口对 redo log 进行刷盘。如果 MySQL 宕机,重启时可以读取 redo log 中的数据,对数据库进行恢复。redo log 采用的是 WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到 Buffer Pool,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求。
既然 redo log 也需要在事务提交时将日志写入磁盘,为什么它比直接将 Buffer Pool 中修改的数据写入磁盘(即刷脏)要快呢?主要有以下两方面的原因:
¶隔离性与隔离级别
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了「 隔离级别 」的概念。并且随着隔离级别不断的提升,一条简单的 SQL 语句的执行效率会逐渐下降,因此很多时候,开发人员都是在隔离级别和执行效率之间寻找一个平衡点。
隔离问题 | 含义 |
---|---|
脏读 | 一个事务读取到了另外一个事务未提交的数据 |
不可重复读 | 同一个事务中,多次读取到的数据不一致 |
幻读 | 一个事务读取数据时,另外一个事务进行更新,导致第一个事务读取到了没有更新的数据 |
❓ 既然幻读和不可重复读都是前后两次读取到的内容不一致,那么这两者到底有什么不同尼?
SQL 标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable ),具体的含义如下表
隔离级别 | 含义 |
---|---|
读未提交(read uncommitted) | 一个事务还没提交时,它做的变更就能被别的事务看到 |
读提交(read committed) | 一个事务提交之后,它做的变更才会被其他事务看到 |
可重复读(repeatable read) | 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的 |
串行化(serializable ) | 对于同一行记录,「 写 」会加「 写锁 」,「 读 」会加「 读锁 」。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行 |
其中「 读提交 」和「 可重复读 」比较难理解,这里用一个例子说明这几种隔离级别。
在实现事务的隔离级别时,数据库中会创建一个视图,访问的时候以示图的逻辑结果为准。
- 在 「 可重复读 」隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图;
- 在 「 读提交 」隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的;
- 在 「 读未提交 」隔离级别下直接返回记录上的最新值,没有视图概念;
- 在 「串行化 」隔离级别下直接用加锁的方式来避免并行访问
所以,在不同隔离界别下,数据库的行为是不同的。Oracle 数据库的默认隔离级别是 「 读提交 」,因此对于一些从 Oracle 迁移到 MySQL 的应用,为保证数据库隔离级别的一致,一定要记得将 MySQL 的隔离级别设置为「 读提交 」
📓 这四种隔离级别都有着各自存在的道理,并没有那种隔离级别一定比另一种隔离级别要好的情况,都是根据自己的业务情况来确定事务的隔离级别。
❓ 什么时候需要可重复读的场景?
¶事务隔离的实现
🎯 本小节将详细展示在可重复读隔离级别下,事务隔离具体是如何实现?
在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。
❓ 有了回滚日志可以方便事务执行撤销操作,但是这些回滚日志要保留到什么时候?什么时候应该删除这些回滚日志?
基于以上的原因,也可以解释,为什么在开发中不建议使用长事务。长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有 20GB,而回滚段有 200GB 的库。最终只好为了清理回滚段,重建整个库。除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库
¶数据库事务的启动方式
开发过程中当然应该避免使用长事务,其实很多时候业务开发人员并不是有意使用长事务,通常都是由于误用所致。总结来说,MySQL 的事务启动方式有以下几种:
❓ 如何避免长事务对业务的影响?
¶事务模型
状态 | 含义 |
---|---|
活动的 | 初始状态,事务执行时处于这个状态 |
部分提交的 | 最后一条语句执行后 |
失败的 | 发现正常的执行不能继续进行,必须进行回滚 |
中止的 | 事务回滚并且数据库已恢复到事务开始执行前到状态后 |
提交的 | 成功完成 |
撤销已提交事务所造成的影响的唯一方法是执行一个补偿事务,这也是回滚能够成功的原因。
¶数据库的事务
之前都是一些理论知识中对于事务的描述,解析来主要关注于 MySQL 中如何使用事务控制语言(TCL,Transaction Control Language)进行 MySQL 中的事务操作。
¶MySQL 中的事务
MySQL 中的事务主要分为两种:隐式事务
及显示事务
,其中隐式事务没有明显的开始和结束的标记(INSERT,UPDATE,DELETE 语句本身就是一个事务);显示事务具有明显的开启和结束的标记
¶事务的创建
MySQL 系统默认启用隐式事务:每执行一条 SQL 语句,都是一个单独的事务。如果需要在一个事务中包含多条 SQL 语句,那么需要先关闭隐式事务.
- 将自动提交功能为禁用
1 | show variables LIKe 'autocommit'; #查看当前数据库是否为隐式事务 |
- 使用显示事务进行操作
开启事务:start transaction
;
结束事务:commit
或rollback
。
在执行 SQL 语句之前,先执行strat transaction
,这就开启了一个事务(事务的起点),然后可以去执行多条 SQL 语句,最后要结束事务,commit 表示提交,即事务中的多条 SQL 语句所做出的影响会持久化到数据库中。或者 rollback,表示回滚,即回滚到事务的起点,之前做的所有操作都被撤消了!也可以打断点,此时事务回滚不再是回滚到起点,而是回滚到断点处
1 | #创建事务要用到的表 |
¶数据库的隔离级别
通过设置事务的隔离级别可以解决脏读、不可重复读或幻读等问题。
READ UNCOMMITTED
READ COMMITTED
可以避免脏读REPEATABLE READ
可以避免脏读、不可重复读和一部分幻读SERIALIZABLE
可以避免脏读、不可重复读和幻读