请移步至 【DobbyKim 的每日一题】 查看更多的题目~
本文章为【极客时间】课程【MySQL 实战 45 讲】的内容整理
什么是事务
在计算机术语中,事务(Transaction)指的是访问并可能更新数据库中各种数据项的一个程序执行单元。
我们为什么需要事务?
事务是为了解决数据安全操作提出的解决方案,事务的控制实际上就是控制数据的安全访问与隔离。
举一个简单的例子:
银行转账,A 账户将自己的 1000 元转账给 B ,那么业务实现的逻辑首先是将 A 的余额减少 1000,然后往 B 的余额里增加 1000,假如这个过程中出现意外,导致过程中断,A 已经扣款成功,B 还没来得及增加,就会导致 B 损失了1000 元。所以我们必须做出控制,要求 A 账户转帐业务撤销,这才能保证业务的正确性,完成这个操作就需要事务,将 A 账户资金减少和 B 账户资金增加放到同一个事务里,要么全部执行成功,要么全部失败,这样就保证了数据的安全性。
MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。
MySQL 事务四大特性
MySQL 事务包含四大特性(ACID),分别为:原子性(Atomicity),一致性(Consistency),隔离性(Isolation),持久性(Durability)。
原子性(Atomicity)
什么是原子性?
原子性是指事务是应用中不可再分的最小执行体,即事务包含的一系列操作要么全部成功,要么全部失败(rollback),绝对不存在部分成功或者部分失败的情况。
一致性(Consistency)
一致性可以理解为堆数据完整性约束的遵循。这些约束可能包括主键约束,唯一索引约束,外键约束等等。事务执行前后,数据都是合法的,不会违背任何数据的完整性。
举例:拿转账来说,A 和 B 加起来有 5000 元,无论 A 和 B 如何转账,转了几次账,A 和 B 加起来的钱永远都是 5000 元。
隔离性(Isolation)
隔离性是指当多个用户以并发的方式操作数据库,比如操作同一个表,数据库为每一个用户开启的事物,不能被其他的事务所干扰或者影响,事务之间是彼此隔离的。
永久性(Durability)
永久性是指一个事务一旦提交了,那么其对数据库中数据的改变就是永久的,即使是数据库发生了故障时,也不会丢失事务提交的数据。
隔离性与隔离级别
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read),不可重复读(non-repeatable read),幻读(phantom read)这些问题,为了解决这些问题,就有了 MySQL“隔离级别”的概念。
在了解 MySQL 的“隔离级别”之前,我们先来了解一下什么是脏读,幻读与不可重复读。
- 脏读
脏读是指当事务 A 正在访问数据,并且对数据进行了修改,而这个修改还没有提交到数据库中,此时另一个事务 B 也访问到了这个数据,然后使用了这个数据,结果事务 A 发生回滚,那么事务 B 读到的就是一个“脏数据”。
示意图:
- 不可重复读
不可重复读是指在一个事务内 ,多次读同一数据。例如:事务 B 读取某一数据,在事务 B 还没有结束时,另外一个事务 A 也访问了该同一数据,并且修改了这一数据。那么,在事务 B 的两次读数据之间,由于事务 A 对数据的修改,导致事务 B 两次读到的的数据可能是不一样的,这就是不可重复读。例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。
示意图:
- 幻读
幻读是指:同样一个查询在整个事务中多次执行,查询所得结果不同。
例如,事务 B 对表中全部记录做了更新操作,尚未提交前,事务 A 又插入了一条记录,那么事务 B 再次读取数据库时,就会发现还有一条记录(即事务 A 新插入的记录)没有做更新。
示意图:
事务的隔离级别
在谈事务的隔离级别之前,你需要知道的是,事务的隔离级别越高,效率就越低。因此很多的时候,我们需要在二者之间寻找一个平衡点。
SQL 标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。
-
读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
-
读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
-
可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
-
串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
示例:
数据表 T 中只有一列,其中一行的值为 1;
create table T(c int) engine=InnoDB;
insert into T(c) values(1);
我们来看看在不同的隔离级别下,事务 A 会得到哪些返回结果,也就是图中的 V1,V2,V3 的值分别是什么。
- 若隔离级别是“读未提交”,V1 的值 就是 2。此时虽然事务 B 还没有提交,但是结果已经被 A 看到了。这就导致了脏读,因此,V2,V3 的值也都是 2。
- 若隔离级别是“读提交”,脏读就可以避免,V1 的值是 1;但是在事务 B 提交之后,事务 A 查询得到的值 V2 就是 2,这就导致了原始读取不可重复,即:不可重复读。 V3 的值也是 2。
- 若隔离级别是“可重复读”,则 V1,V2 都是 1,V3 是 2。因为隔离级别设置为可重复读,就要求事务在执行期间看到的数据前后必须是一致的,所以 V2 查询的值为 1。
- 若隔离级别是“串行化”,则事务 B 在执行 “将 1 改成 2” 的时候,会被锁住。直到事务 A 提交之后,事务 B 才可以继续执行。所以从 A 的角度来看,V1,V2 的值是 1,V3 的值是 2。
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
- RU(读未提交)隔离级别下直接返回记录上的最新的值,没有视图这一概念
- RC(读提交)隔离级别下,MVCC 视图会在每一个语句前创建一个,所以在 RC 级别下,一个事务是可以看到另外一个事务已经提交的内容,因为它在每一次查询之前都会重新给予最新的数据创建一个新的 MVCC 视图
- RR(可重复读)隔离级别下,MVCC 视图是在开始事务的时候就创建好了,这个视图会一直使用,直到该事务结束。
- Serializable(序列化)隔离级别下,也没有视图这一概念,它是通过锁来实现数据访问隔离的。
值得一提的是,Oracle 数据库的默认隔离级别是 RC,因此如果对于一些从 Oracle 迁移到 MySQL 上的应用,为了保证数据隔离级别的一致,需要将 MySQL 的隔离级别设置为 RC(读提交)。
事务隔离的实现
在 MySQL 中,每一条记录在更新的时候,除了将变更记录到 redo log 日志中,还会记录一条变更相反的回滚记录,回滚记录被记录在 undo log 日志中。
假设一个值从 1 按照顺序改成了 2,3,4。在 undo log 日志中就会有类似下面的记录:
当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。
对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。同时你会发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。你一定会问,回滚日志总不能一直保留吧,什么时候删除呢?
答案是,在不需要的时候才删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的 read-view 的时候。
为什么尽可能不要使用长事务?
答案是:因为长事务的存在会导致 undo log 日志一直存在不被删除。
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。
事务的开启方式
MySQL 开启事务的方式有以下几种:
- 显示启动事务语句,begin 或 start transaction。配套的提交语句为 commit,回滚语句是 rollback。
- set autocommit = 0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。