本文首发于公众号:Hunter后端
原文链接:MySQL面试必备三之事务
这一篇笔记介绍一下 MySQL 的事务,面试中常被问到关于事务的几个问题如下:
以下是本篇笔记目录:
所谓事务,就是一系列的 SQL 组合,这些 SQL 操作要么全部执行,要么都不执行,是一个不可分割的工作单位。
比如我们在支付系统中想要完成一个转账功能,比如从 A 账户转账一百元给 B 账户,那么从 A 账户的总额中减去 100,然后在 B 账户上加上 100,这两个就需要全部执行才算是这个转账操作的实现。
这个过程就包含了 A 账户的减少 100,B 账户的加上 100,这两个操作加起来就是一个完整的事务。
事务的特性有四个,为ACID,分别是 A(Atomicity)、C(Consistency)、I(Isolation) 和 D(Duration),分别表示原子性、一致性、隔离性和持久性。
事务的原子性指的是一个事务中的所有操作要么全部完成,要么全部失败,如果在执行事务的过程中,某个 SQL 执行失败,那么这个事务中之前执行操作全部回滚,恢复到执行事务之前的状态。
一致性指的是事务执行前后,数据库的状态应该保持一致,即数据库的完整性不会被破坏。
这个一致性的理解为在执行事务前后数据库应该符合事务的约束条件,从而保证数据的正确性。
比如我们设置了某个字段的属性应当大于或等于 0,但在某个操作过程中如果更新该字段的值小于 0,那么则属于破坏了数据的一致性,事务会回滚到执行前,从而保证数据库状态的一致性。
隔离性指的是多个事务并发执行时,每个事务都应该独立于其他事务,互不干扰,从而避免数据并发访问引起的问题。
事务的隔离分为多个级别,这个在后面再介绍。
事务的持久性指的是事务执行完毕之后,对数据的修改就是永久的,即便是系统故障,修改的数据也不会丢失。
事务的执行过程会包含几个步骤,事务的开始、SQL 操作、提交或者回滚。
比如我们想要给 id=1
的账号减去一百元,然后给 id=2
的账号加上一百元,使用事务来操作的示例如下:
START TRANSACTION;
UPDATE user_account set money = money - 100 WHERE id = 1;
UPDATE user_account set money = money + 100 WHERE id = 2;
COMMIT;
这里,我们通过 START TRANSACTION
开启一个事务,中间执行 SQL 操作,以 COMMIT
提交事务为结束。
如果我们想执行回滚操作,可以直接使用 ROLLBACK
:
START TRANSACTION;
UPDATE user_account set money = money - 100 WHERE id = 1;
UPDATE user_account set money = money + 100 WHERE id = 2;
ROLLBACK;
这里的回滚操作会回滚到事务执行前。
如果我们的事务包含的 SQL 很长,我们并不想直接回滚到事务开启前,而是事务中间的某个步骤,我们可以使用保存点来进行回滚操作:
-- 开始事务
START TRANSACTION;
-- 执行 SQL 操作
UPDATE accounts SET balance = balance - 100 WHERE account_id = 123;
SAVEPOINT before_insert;
INSERT INTO transaction_log (account_id, amount, type) VALUES (123, 100, 'debit');
-- 检查条件
IF some_condition THEN
-- 回滚到保存点
ROLLBACK TO before_insert;
ELSE
-- 提交事务
COMMIT;
END IF;
在这里,我们通过 SAVEPOINT
来创建保存点,并在后面的代码里通过 IF 条件进行判断,选择性的回到该保存点。
一个事务是可以包含多个保存点的。
前面几条介绍的都是使用 START TRANSACTION
显式地开始一个事务,而至于单条 SQL 语句,比如 INSERT 或者 UPDATE 这种,在默认情况下是自动提交的,所以不用手动进行 COMMIT 操作,它们也属于单独的事务。
在我们访问数据库时,可能同一时刻有多个事务在访问操作数据库,那么这样可能会导致一些问题。
所谓脏读就是在在某个事务的执行过程中可以读到其他事务未提交的数据,这个现象就是脏读。
因为一个事务的执行是可能包含多个 SQL 的,在某种事务隔离级别下就可能存在 A 事务执行了 SQL 但是还未提交,这时候 B 事务执行过程中就读取到了 A 事务更改的数据。
不可重复读的现象指的是在同一个事务中,有两个读取数据的 SQL,这两次读取的数据内容都不一样,这种现象就称为不可重复读。
这个现象产生的原因在于这两次读取 SQL 的过程中,有其他事务更新了这条数据并提交了。
不可重复读偏重的点在于对数据的修改。
幻读的现象指的是在同一个事务中,两次查询数据返回的结果的条数不一样,它产生的原因同样是两次查询期间有其他事务提交了,但它的侧重点是其他事务是对数据的插入或者删除。
事务的隔离级别分别是读未提交(Read Uncommited)、读已提交(Read Commited)、可重复读(Repeatable Read)、串行化(Serializable)。
读未提交指的是一个事务可以读取到其他事务未提交的内容。
在这个隔离级别下,如果有一个事务 A,包含多条 SQL 操作,执行到其中某条 SQL,但是还没有执行 COMMIT 操作,这个时候另一个事务 B 读取事务 A 操作过的 SQL 数据,就可以读取到对应内容,这个过程就是读未提交。
读未提交这个隔离级别可能会造成数据的脏读问题。
读已提交指的是事务可以读取到其他事务已经提交的数据,这个隔离级别可以解决脏读问题,但是不可以解决不可重复读和幻读的问题。
比如一个事务 A,在其执行过程中先读取了某条数据,这个时候另一个事务 B 开启一个事务并提交,事务 B 修改了事务 A 前面读取的数据内容,这个时候事务 A 在后面的操作又读取了这条数据,会发现和第一次读的时候数据不一致,这个就是读已提交可能造成的问题。
可重复读则是在事务开始的时候会先获取一个当前时刻数据的快照,并且在整个事务的过程中都会从这个快照中读取数据,这个就是可重复读。
可重复读可以解决不可重复读的问题,因为不可重复读针对的是某条数据本身,而可重复读会对数据本身做快照处理,所以可以解决不可重复读的问题。
而为什么不可以解决幻读的问题呢?
这个我们后面再介绍。
串行化是最高的隔离级别,在这个隔离级别下,会将分别对读操作和写操作加锁,当一个事务正在执行,其他事务必须等前一个事务执行完毕之后才能执行。
在这个隔离级别下,可以解决前面并发事务带来的所有问题,包括不可重复读和幻读,但同时,这种方式也会降低数据库的并发性能,因为事务需要按照其他事务释放锁才能执行。
注意:MySQL 默认的隔离级别是可重复读。
接着来说一说为什么可重复读隔离级别不可以解决幻读的问题。
在说明这个问题的原因之前,先来介绍一下快照读和当前读。
快照读指的是在事务开始的时候,事务会创建一个数据的快照,在接下来这个事务的整个过程中,都会使用这个快照来读取数据。
当前读则是指在读取数据时,直接读取库里最新的数据,而不使用事务开启时创建的快照数据。
在 MySQL 中,除了普通的 SELECT
查询语句是快照读,UPDATE、INSERT、DELETE 操作都是当前读,也就是对数据进行更新、插入和删除的时候都是会查询到数据库最新的数据然后进行操作。
除此之外,对 SELECT 操作进行加锁操作也是当前读,比如共享锁 select ... lock in share mode
和排他锁 select ... for update
,这个我们后面再介绍。
我们可以通过下面一个例子来进行阐述,在可重复读隔离级别下幻读操作是如何产生的。
比如有两个事务,分别是事务 A 和事务 B,A 事务开启后,查询数据库中的数据,这个时候事务 B 开启,并且使用 INSERT 插入一条数据并提交,在这之后,A 事务对数据库中的数据进行一个 UPDATE 全量数据的操作,之后再进行一个 SELECT 的操作。
下面的代码示例,我们用前面的 t 序号作为执行的时间:
create table users (
id int not null auto_increment primary key,
name varchar(20) not null
);
INSERT INTO users (id, name) values(1, "张三");
-- t1 开启事务A
START TRANSACTION;
-- t2 查询数据
SELECT * from users;
-- t6 查询数据
SELECT * from users;
-- t7 更新全部数据
UPDATE users SET name = "王五" WHERE id >= 1;
-- t8 查询数据
SELECT * FROM users;
-- t9 提交事务A
COMMIT;
-- t3 开启事务B
START TRANSACTION;
-- t4 事务B插入一条数据
INSERT INTO users (id, name) values(2, "李四");
-- t5 提交事务B
COMMIT;
对于上面的代码,我们分别开启两个 MySQL 终端然后按照时间顺序执行,可以看到以下输出:
事务 A 的整体操作如下:
事务 B 的整体操作如下:
可以看到事务 A 在第二次进行查询的时候数据就会新增一条,和第一次查询的时候数据不一致了,这个过程就产生了幻读。
前面介绍了即便是可重复读隔离级别下,也还是会可能产生幻读问题,那么如何解决幻读问题呢,本质上还是加锁,比如在查询数据的时候使用 select for update
操作对查询的数据加上间隙锁,这样就可以避免其他事务插入新的数据,关于锁的概念和使用,我们在后面再详细介绍。
如果想获取更多相关文章,可扫码关注阅读: