简介

提起数据库,那不得不说到事务,但事务并不是MySQL数据库的专属,而是SQL语言的特性,事务就是一组原子性的SQL查询。该组查询语句要么全部执行成功,要么全部执行失败。如果其中有任何一条语句执行失败或因为数据库奔溃等其它原因无法执行,那么所有语句都不会执行。

事务特性

事务的特性就是我们俗称的ACID,下面依次进行解释:

  1. A:原子性(Atomicity

一个事务必须是一个不可分割的最小执行单元,整个事务中的所有操作要么全部执行成功,要么全部执行失败后回滚,对于一个事务而言,不可能出现其中一部分操作执行成功,另一部分操作执行失败的情况,这就是事务的原子性。

  1. C:一致性(consistency

数据库总是从一个一致性的状态转换到另一个一致性的状态。例如银行转账,用户A的账户向用户B的账户转账100元人民币,在这个事务中,首先会从用户A的账户中扣减100元,然后给用户B的账户增加100元,由于事务的原子性的保证,即使在从用户A的账户中扣减100元后数据库系统奔溃,用户A的账户上也不会损失100元,因为事务没有最终进行提交,用户A的账户扣减不会保存至数据库中,同时由于系统奔溃,用户B的账户也不会增加金额。也就是说,经过一次事务,整个账户系统中的金额总数不会改变,这就是事务的一致性。

  1. I:隔离性(isolation

隔离性是指多个客户端并发访问数据库时,一个客户端的事务不能被其它客户端的事务所干扰,并发事务之间要做到事务隔离。

隔离性需要结合事务的隔离级别来说,下文会对事务的隔离级别进行展开说明。

通常的隔离性是:一个事务中的修改在最终提交事务之前,对其它事务是不可见的。还是银行转账的例子,在用户A给用户B转账的事务提交之前,如果用户A还向另一个用户C转账,这时候看用户A的账户是还未扣减100元的,这就是事务的隔离性。

  1. D:持久性(durability

一旦事务提交,则该事务中所做的所有修改都将永久保存至数据库中,会被持久化至磁盘上,即使此时系统发生奔溃,事务中发生修改的数据也不会丢失。

MySQL中,事务的实现在引擎层。MySQL是一个支持多引擎的系统,但并不是所有引擎都支持事务,例如MySQL原生的MyISAM引擎就不支持事务,而我们熟知的InnoDB引擎则是支持事务的。

并发访问的问题

当多个事务并发执行的时候,就会出现并发问题,为了解决这些问题,提出了隔离级别的概念,每一种隔离级别都可以解决一个并发问题。

事务的并发执行存在以下三种问题:

  1. 脏读

一个事务读到了另一个事务中未提交的数据。

  1. 不可重复读

假设存在这样一个事务,首先查询一条记录,然后修改该条记录的值,再次查询该条记录,那么对于这个事务而言,两次查询到的记录的值不一致,这被称为不可重复读。

  1. 幻读

假设存在这样一个事务,首先条件查询某个范围内的记录,此时某个并发执行的其它事务在该范围内插入了新的记录,当之前的事务再次以相同的条件范围查询时,就会查询到新插入的行(称为幻行)。InnoDB引擎通过多版本并发控制(MVCC)来解决幻读的问题。

事务的隔离级别

每一种隔离级别都解决了一个并发问题,但必须明白,问题解决的越多,效率就会越低。因此很多时候我们需要根据实际业务场景来选择合适的隔离级别。

SQL标准的事务隔离级别包括:读未提交(read uncommitted)、读已提交(read committed)、可重复读(repeatable read)和串行化(serializable)。

  1. 读未提交(read uncommitted

可读取其它事务中未提交的数据,不能解决任何并发问题。

  1. 读已提交(read committed

只能读取其它事务中已提交的数据,可以解决脏读问题。

  1. 可重复读(repeatable read

一个事务执行过程中能读取到的数据,总是跟这个事务在启动时读到的数据保持一致。可以解决脏读和不可重复读问题。

实现原理是在事务启动时创建一个视图,整个事务存在期间都会查询该视图。

  1. 串行化(serializable

最高的事务隔离级别,使用读写锁强制所有操作串行排队执行,解决了所有的并发问题。但其效率也最为低下。对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突时,后启动的事务必须等待前一个事务执行完并提交后,才能执行。

不同隔离级别之间的性能比较(从高到低):读未提交 > 读已提交 > 可重复读 > 串行化。

性能越高,并发安全性越低。MySQL5.0版本之后开始,选择可重复读做为默认的隔离级别。

MySQL为什么选择可重复读作为默认的隔离级别?

传送门

深入理解事务隔离级别

这里我们用一个例子来尝试理解事务的隔离级别。

初始化表T,插入一条记录:

1
2
3
4
5
6
7
CREATE TABLE `T` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`num` int(11) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

insert into `T` (`num`) values (1);

假设有两个事务的执行顺序如以下表格所示:

时间轴事务1事务2
t0beginbegin
t1select num from T where id = 1;
t2select num from T where id = 1;
t3update T set num = 2 where id = 1;
t4select num from T where id = 1;
t5commit;
t6select num from T where id = 1;
t7commit;
t8select num from T where id = 1;

t1时刻事务1查询得到的num值为1

t4时刻事务1查询得到的num值为N1t6时刻事务1查询得到的num值为N2t8时刻事务1查询得到的num值为N3

在不同隔离级别下,N1N2N3的值会一样吗?分别是多少?

  1. 读未提交隔离级别下:

事务1可读到事务2中未提交数据,于是N1=N2=N3=2

  1. 读已提交隔离级别下:

事务1只能读到事务2中已提交的数据,于是N1=1, N2=2, N3=2

  1. 可重复读隔离级别下:

事务1中读到的数据总是跟该事务刚启动时读到的数据保持一致,于是N1=1, N2=1, N3=2

  1. 串行化隔离级别下:

对同一行数据的操作强制排队执行。t1时刻事务1加读锁;t2时刻事务2加读锁;t3时刻事务2尝试加写锁,进入阻塞状态,等待事务1提交后才会执行updatet8时刻事务2已提交。于是N1=1, N2=1, N3=2

总结

事务的四大特性:ACID

事务的并发问题:脏读、不可重复读和幻读。

事务的隔离级别:读未提交、读已提交(解决了脏读)、可重复读(解决了不可重复读)和串行化(解决了幻读,无并发问题)。

参考

  • 《MySQL实战45讲》 - 极客时间