vlambda博客
学习文章列表

MySQL 事物详解--不看可惜了

事务的四个重要特性 --- ACID 特性

原子性(Atomicity) 事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。一致性(Consistency) 指事务将数据库从一种状态转变为另一种一致的的状态。事务开始前和结束后,数据库的完整性约束没有被破坏。隔离性(Isolation) 要求每个读写事务的对象对其他事务的操作对象能互相分离,即该事务提交前对其他事务不可见。也可以理解为多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行结果。这指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自完整的数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。 Tips:MySQL 通过锁机制来保证事务的隔离性。持久性(Durability) 事务一旦提交,则其结果就是永久性的。即使发生宕机的故障,数据库也能将数据恢复,也就是说事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。这只是从事务本身的角度来保证,排除 RDBMS(关系型数据库管理系统,例如 Oracle、MySQL 等)本身发生的故障。Tips:MySQL 使用 redo log 来保证事务的持久性。

事务的四种隔离级别

在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。我们的数据库锁,也是为了构建这些隔离级别存在的。

隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
未提交读(Read Uncommitted) 可能 可能 可能
已提交读(Read Committed) 不可能 可能 可能
可重复读(Repeated Read) 不可能 不可能 可能
可串行化(Serializable) 不可能 不可能 不可能

未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据。已提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)。可重复读(Repeated Read):在同一个事务内的查询都是事务开始时刻一致的,InnoDB 默认级别。在 SQL 标准中,该隔离级别消除了不可重复读,但是还存在幻读。可串行化(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞。

通过上述描述可以看出,Read Uncommitted 这种级别,数据库一般都不会用,而且任何操作都不会加锁。

MySQL 事务级别详解

以下信息都是针对 MySQL 8.0 版本进行测试。

新建一张测试表 demo,SQL 如下:


CREATE TABLE `demo` ( `ID` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键', `name` varchar(64) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'demo name', `author` varchar(64) COLLATE utf8mb4_general_ci NOT NULL COMMENT 'demo author', PRIMARY KEY (`ID`), KEY `IX_name` (`name`)) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

查看 MySQL 当前事务级别:select @@session.transaction_isolation;

未提交读(Read Uncommitted)

该隔离级别的事务会读到其它未提交事务的数据,此现象也称之为脏读。

准备数据:


SET @@session.transaction_isolation = 'READ-UNCOMMITTED';
insert into demo(name, author) values('name1', 'tommy');

准备两个终端,MySQL 终端 1 和 MySQL 终端 2,再准备一张测试表 demo,写入一条测试数据并调整隔离级别为 Read Uncommitted,任意一个终端执行即可。登录 MySQL 终端 1,开启一个事务,将 ID 为 1 对应的 name1 的记录更新为 name2。


begin;update demo set name = 'name2' where id = 1; select * from demo; -- 此时看到一条 name name2 的记录

登录 MySQL 终端 2,开启一个事务后查看表中的数据。


use demo; begin; select * from demo; -- 此时看到一条 name name2 的记录

最后一步读取到了 MySQL 终端 1 中未提交的事务(没有 commit 提交动作),即产生了脏读,大部分业务场景都不允许脏读出现,但是此隔离级别下数据库的并发是最好的。由于会出现脏读,所以这种隔离级别一般数据库都不会使用。

已提交读(Read Committed)

一个事务可以读取另一个已提交的事务,多次读取会造成不一样的结果,此现象称为不可重复读问题,Oracle 和 SQL Server 的默认隔离级别,但是不是 MySQL 的默认隔离级别。

准备数据:


SET @@session.transaction_isolation = 'READ-COMMITTED';
insert into demo(name, author) values('name1', 'tommy');

准备两个终端,MySQL 终端 1 和 MySQL 终端 2,再准备一张测试表 demo,写入一条测试数据并调整隔离级别为 Read Committed,任意一个终端执行即可。登录 MySQL 终端 1,开启一个事务,将 ID 为 1 对应的 name1 的记录更新为 name2。


begin; update demo set name = 'name2' where id = 1;select * from demo; -- 此时看到一条 name name2 的记录

登录 MySQL 终端 2,开启一个事务后查看表中的数据。


use demo; begin; select * from demo; -- 此时看到一条 name name1 的记录

切换 MySQL 终端 1,提交事务。


commit;

切换 MySQL 终端 2。


select * from test; -- 此时看到一条 name name2 的记录

MySQL 终端 2 在开启了一个事务之后,在第一次读取 demo 表(此时 MySQL 终端 1 的事务还未提交)时 name 的值为 'name1',在第二次读取 demo 表(此时 MySQL 终端 1 的事务已经提交)时 name 列的值 'name1' 已经变为 'name2',说明在此隔离级别下只能读取到已提交的事务。

可重复读(Repeated Read)

该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间点的状态,因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。MySQL 的 InnoDB 引擎可以通过 next-key locks(行锁) 机制来避免幻读。使用行锁来避免幻读会在后续锁的介绍中进行解释。

准备数据:


SET @@session.transaction_isolation = 'REPEATABLE-READ';

准备两个终端,MySQL 终端 1 和 MySQL 终端 2,再准备一张测试表 demo,写入一条测试数据并调整隔离级别为 Repeated Read,任意一个终端执行即可。登录 MySQL 终端 1,开启一个事务。


begin; select * from demo; -- 无记录

登录 MySQL 终端 2,开启一个事务后查看表中的数据。


begin;select * from demo; -- 无记录

切换 MySQL 终端 1,提交事务。


insert into demo(id, name, author) values(1, 'name1', 'tommy'); commit;

切换 MySQL 终端 2。


select * from demo; --此时查询还是无记录

以上可以证明,在该隔离级别下已经读取不到别的已提交的事务,如果想看到 MySQL 终端 1 提交的事务,在 MySQL 终端 2 将当前事务提交后再次查询就可以读取到 MySQL 终端 1 提交的事务。我们接着实验,看看在该隔离级别下是否会存在别的问题。

此时接着在 MySQL 终端 2 插入一条数据。


insert into demo(id, name, author) values(1, 'name1', 'tommy'); Duplicate entry '1' for key 'PRIMARY',主键冲突。

这时你肯定会有疑问,明明在上一步没有数据,为什么在这里会报错呢?其实这就是该隔离级别下可能产生的问题,MySQL 称之为幻读。注意我在这里强调的是 MySQL 数据库,Oracle 数据库对于幻读的定义可能有所不同。

可串行化(Serializable)

在该隔离级别下事务都是串行顺序执行的,MySQL 数据库的 InnoDB 引擎会给读操作隐式加一把读共享锁,从而避免了脏读、不可重读复读和幻读问题。

准备数据:


SET @@session.transaction_isolation = 'SERIALIZABLE';

准备两个终端,MySQL 终端 1 和 MySQL 终端 2,再准备一张测试表 demo,写入一条测试数据并调整隔离级别为 Serializable,任意一个终端执行即可。登录 MySQL 终端 1,开启一个事务,并写入一条数据。


begin; insert into demo(id, name, author) values(1, 'name1', 'tommy');

登录 MySQL 终端 2,开启一个事务。


begin;select * from test; -- 此时会一直卡住

立马切换到 MySQL 终端 1,提交事务。


commit;

一旦事务提交,MySQL 终端 2 会立马返回 ID 为 1 的记录,否则会一直卡住,直到超时,其中超时参数是由 innodb_lock_wait_timeout 控制。该隔离级别的数据库并发能力最弱,因为每条 select 语句都会加锁。