提到事务,大家肯定不陌生。最经典的例子就是银行转账。
比如,A 账户给 B 账户转账 100。在这种交易的过程中,有几个问题值得思考:
要保证交易正常可靠地进行,数据库就得解决上面的四个问题,这也就是事务诞生的背景,它能解决上面的四个问题。
简单来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在MySQL 中,事务支持是在引擎层实现的。
我们知道,MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。
比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。
下面,配合实例,我们分析 InnoDB 存储引擎在事务支持方面对于隔离性的实现。
我们知道事务的四个特性:ACID(Atomicity
、Consistency
、Isolation
、Durability
,即原子性、一致性、隔离性、持久性)。
当数据库上有多个事务同时执行的时候,就可能会出现问题:
为了解决这些问题,就有了隔离级别的概念。
在谈隔离级别之前,我们首先要知道,隔离的越严实,效率就会越低。因此很多时候,我们都要在二者之间寻找一个平衡点。
SQL标准的事务隔离级别包括:
read uncommitted
):一个事务还没提交时,它做的变更就能被别的事务看到;read committed
):一个事务提交以后,它做的变更才会被其它事务看到;repeatable read
):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其它事务也是不可见的;serializable
):对于同一行记录,「写」会加「写锁」,「读」会加「读锁」。当出现读写锁冲突的时候,后访问的事务必须等待前一个事务执行完成,才会继续执行。在 MySQL 数据库中,默认的事务隔离级别是 RR。
MySQL8 之前的查询命令是:
SELECT @@GLOBAL.tx_isolation, @@tx_isolation;
MySQL8 开始查询命令是:
SELECT @@GLOBAL.transaction_isolation, @@transaction_isolation;
根据上图,可以看到,默认的隔离级别为 REPEATABLE-READ
,「全局隔离级别」和「当前会话隔离级别」是相同的。
我们可以通过如下命令修改隔离级别(建议在修改时,仅修改当前 session 隔离级别即可,不用修改全局的隔离级别):
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
上面这条 SQL 表示,仅把当前 session
的数据库隔离级别设置为 READ UNCOMMITTED
,设置成功后,再次查询隔离级别,发现当前 session
的隔离级别已经变了,如图:
注意,这里只是修改了当前 session
的隔离级别,换一个 session
之后,隔离级别又会恢复到默认的隔离级别,如果使用的是 Navicat 的话,不同的查询窗口就对应了不同的 session
。
READ UNCOMMITTED
是最低隔离级别,这种隔离级别中存在脏读、不可重复读以及幻读问题。
我们通过这个隔离级别,搞懂这三个问题到底是怎么回事。
建表语句:
CREATE TABLE account (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR (50) NOT NULL,
balance BIGINT NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY idx_name (name)
) ENGINE = INNODB
预设两条数据,如下: zhangsan
和 zhaowu
两个用户,两个人的账户各有 1000 块。
下面通过模拟这两个用户之间的一个转账操作,借此分析这三个问题到底是怎么回事。
一个事务读到另外一个事务还没有提交的数据,称之为脏读。
具体操作如下:
START TRANSACTION;
UPDATE account set balance=balance+100 where name='zhangsan';
UPDATE account set balance=balance-100 where name='zhaowu';
COMMIT;
READ UNCOMMITTED
,如下:SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
START TRANSACTION;
SELECT * FROM ACCOUNT;
COMMIT;
zhangsan
这个账户添加 100 元。SELECT * FROM ACCOUNT;
),结果如下:可以看到,A 窗口中的事务,虽然还未提交,但是 B 窗口中已经可以查询到数据的相关变化了。
这就是脏读问题。
不可重复读是指一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。
具体操作步骤如下(操作之前先将两个账户的钱都恢复为1000):
READ UNCOMMITTED
。zhangsan
的账户:START TRANSACTION;
SELECT * FROM ACCOUNT WHERE NAME='zhangsan';
COMMIT;
zhangsan
这个账户添加 100 块钱,如下:START TRANSACTION;
update account set balance=balance+100 where name='zhangsan';
COMMIT;
zhangsan
的账户,结果如下:zhangsan
的账户已经发生了变化,即前后两次查看 zhangsan
账户,结果不一致,这就是不可重复读。
不可重复读和脏读的区别在于:脏读是看到了其它事务未提交的数据,而不可重复读是看到了其它事务已经提交的数据(由于当前 SQL 也是在事务中,因此有可能并不想看到其它事务已经提交的数据)。
幻读和不可重复读非常像。幻读指的,一个事务里面,后一个请求看到的比之前相同请求看到的,多了记录出来。 幻读仅专指「新插入的行」。
我来举一个简单例子。
在 A 窗口中输入如下 SQL:
START TRANSACTION;
insert into account(name,balance) values('wangliu',1000);
COMMIT;
然后在 B 窗口输入如下 SQL:
START TRANSACTION;
SELECT * from account;
delete from account where name='wangliu';
COMMIT;
执行步骤如下:
READ UNCOMMITTED
。zhangsan
和 zhaowu
。wangliu
的用户,注意不用提交事务。wangliu
这个用户。wangliu
的记录,这个时候删除就会出问题,虽然在 B 窗口中可以查询到 wangliu
,但是这条记录还没有提交,是因为脏读的原因才看到了,所以是没法删除的。此时就产生了幻觉,明明有个 wangliu
,却无法删除。这就是幻读。
看了上面的案例,大家应该明白了脏读、不可重复读以及幻读各自的含义了。
和 READ UNCOMMITTED
相比,READ COMMITTED
主要解决了脏读的问题,对于不可重复读和幻读则未解决。
将事务的隔离级别改为 READ COMMITTED
之后,重复上面关于脏读案例的测试,发现已经不存在脏读问题了;重复上面关于不可重复读案例的测试,发现不可重复读问题依然存在。
上面那个案例不适用于幻读的测试,我们换一个幻读的测试案例。
继续两个窗口 A 和 B,将 B 窗口的隔离级别改为 READ COMMITTED
,然后在 A 窗口输入如下测试 SQL:
START TRANSACTION;
insert into account(name,balance) values('wangliu',1000);
COMMIT;
在 B 窗口输入如下测试 SQL:
START TRANSACTION;
SELECT * from account;
insert into account(name,balance) values('wangliu',1000);
COMMIT;
执行步骤如下:
和 READ COMMITTED
相比,REPEATABLE READ
进一步解决了不可重复读的问题,但是幻读则未解决。
REPEATABLE READ
中关于幻读的测试和上一小节基本一致,不同的是第二步中执行完插入 SQL 后记得提交事务。
由于 REPEATABLE READ
已经解决了不可重复读,因此第二步即使提交了事务,第三步也查不到已经提交的数据,第四步继续插入就会出错。
SERIALIZABLE
提供了事务之间最大限度的隔离,在这种隔离级别中,事务一个接一个顺序的执行,不会发生脏读、不可重复读以及幻读问题,最安全。
如果设置当前事务隔离级别为 SERIALIZABLE
,那么此时开启其它事务时,就会发生阻塞,必须等当前事务提交了,其它事务才能开启成功,因此前面的脏读、不可重复读以及幻读问题这里都不会发生。
在 InnoDB 中事务隔离性是由「锁」来实现的。
首先说 READ UNCOMMITTED
,它是性能最好,也可以说它是最野蛮的方式,但是它压根儿就不加锁,所以根本谈不上什么隔离效果,可以理解为没有隔离。
再来说 SERIALIZABLE
。读的时候加共享锁,也就是其它事务可以并发读,但是不能写。写的时候加排它锁,其它事务不能并发写也不能并发读。
最后说 READ COMMITTED
和 REPEATABLE READ
。这两种隔离级别是比较复杂的,既要允许一定的并发,又想要兼顾的解决问题。
为了实现可重复读,MySQL 采用了 MVCC (多版本并发控制) 的方式。
有关锁和MVCC的分析,我们在后面文章进行详细说明。
这里我们只需要知道,MVCC 只在 READ COMMITTED
和 REPEATABLE READ
这两个隔离级别下工作。
最后,「幻读」InnoDB 通过引入间隙锁的方式解决。
本文我们分析了并发访问引发的三个问题:
针对这三个问题,SQL 标准定义了四种隔离级别:
从上往下,隔离强度逐渐增强,性能逐渐变差。采用哪种隔离级别要根据系统需求权衡决定,其中,可重复读是 MySQL 的默认级别。
事务隔离其实就是为了解决上面提到的脏读、不可重复读、幻读这几个问题,下面展示了 4 种隔离级别对这三个问题的解决程度。
事务隔离性主要是由「锁」来实现的,为了解决可重复读,MySQL 采用了 MVCC (多版本并发控制) 的方式。
好了,有关事务隔离机制的就先介绍到这了,我们下篇见。