前置阅读
一、事务的特性
对于事务,我觉得有一句英文描述的非常贴切:All or not, now or never.
事务 (Transaction)可以说是关系型数据库最重要的特性了。SQL 事务就是一个或者多个 SQL 语句的集合,这些 SQL 语句组成了一个单个的逻辑单元,所以它只有两种执行结果:commit 或者 rollback。学过操作系统的同学都会了解到,当程序引入多进程或者多线程时,也随之而来的并发问题。同样事务也是为了保证在并发执行的情况下数据的完整性。
事务开启语句是由 begin 或者 start transaction 命令开始的,或者把自提交特性关闭(set autocommit = 0)。事务结束语句通常使用 commit 或者 rollback。commit 表示提交事务,事务对数据库的修改成为永久性的,rollback 表示回滚事务,撤销正在进行中的所有未提交的修改。
事务的特性即:ACID。
-
Atomicity 原子性
-
Consistency 一致性
-
Isolation 隔离性
Isolation is the database-level property that controls how and when changes are made, and if they become visible to each other, users, and systems.
隔离性是控制修改如何(How)和 何时(When)发生,以及它们是否对于其它事务、用户和系统是可见的的数据库级别属性。
-
Durability 持久性
这里只重点介绍 Isolation (隔离性)是希望突出重点,关于其它三点性质,读者可以自行查阅理解。
注:acid 在英文有酸性的意思。同样,另一个分布式事务,它的特性是 base,它有碱性的意思。
二、事务的隔离性
MySQL InnoDB 存储引擎实现了 SQL 标准的 4 种隔离级别,用来限定事务内外的哪些改变是可见的,哪些是不可见的。
MySQL是各个隔离级别分别是:
-
R ead U ncommitted 读未提交,简称 RU
在一个事务中,它可以读取到其他事务还未提交的数据变化,这种现象称为脏读。
-
R ead C ommitted 读已提交,简称 RC
在一个事务中,可以读取到其他事务已经提交的数据变化,这种现象称为不可重复读。
-
R epeatable R ead 可重复读,简称 RR
它是 MySQL 默认的事务隔离级别。在一个事务中,从它开始直到事务结束,都可以反复读取到事务刚开始看到的数据,并一直不会发生变化,避免了脏读、不可重复读和幻读现象。
-
Serialization 可串行化
不建议在生产环境使用,在某些特殊情况下会使用到。
这四个隔离级别的隔离性是逐渐增大的,隔离性越大,系统的开销越大。默认的隔离级别是 RR,如果实际的应用不需要这个隔离级别,可以降低隔离级别,这样会提高性能。但是除非你对数据库的隔离级别很了解,否则最好只使用默认的隔离级别。
三、隔离性实践
下面会涉及到实际的操作,它依赖的环境是前置条件中的那篇快速搭建环境的博客中所搭建的(MySQL 8.0)。
MySQL 默认的隔离级别是:REPEATABLE-READ,可重复读,简称 RR。
- 查看默认的隔离级别:
show variables like '%isolation';
或者select @@transaction_isolation;
- 修改默认的隔离级别:
SET transaction_isolation = 'READ-UNCOMMITTED';
注:这里要特别注意,在命令行中输入不要漏掉了末尾的 ;
。
注:这里修改的是当前 session 的隔离级别,不是全局的隔离级别,所以需要在每个终端执行。
创建数据库和测试表
创建并使用数据库:CREATE database crazy_dragon;
切换到数据库:USE crazy_dragon;
创建测试表:
sql
CREATE table t_user_account(
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(20) NOT NULL,
balance INT NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这个表是一个简化的用户账户表(id,name,balance)。
注意:balance 余额。
插入一条测试数据:
读未提交 RU
SET transaction_isolation = 'READ-UNCOMMITTED';
在读未提交(RU)的隔离级别下,会出现脏读的现象。这里假设有两个事务,简称事务 A 和 事务 B。在事务 A 在执行的过程中,它是可以感知到事务 B 的 DML 操作的,这样来说其实读未提交就相当于是没有隔离级别了。
事务 A 把 id = 1 的用户 tom 的余额更新为 1200,接着事务 B 进行查询,此时它查出的结果为 1200,然后事务 A 回滚。此时数据库中的余额仍为 1000,但是事务B读取到了1200,这就是脏读。
读已提交 RC
SET transaction_isolation = 'READ-COMMITTED';
在 RC 隔离级别下,可以避免脏读。同样,事务 A 和事务 B,现在的隔离级别是读已提交。所以,只有已经提交的数据,才可以被另一个事务感知到,这样就不会发生脏读了。
读已提交避免了脏读现象,但是还有不可重复读和幻读问题。不可重复读,指的是在一个事务范围内,多次查询结果字段值不同。幻读,值的是在一个事务范围内,多次查询结果行数不同。所以,这里要注意区分这两个的区别:不可重复读,强调是是字段值;幻读,强调的是数据结果的行数。
下面来分别演示不可重复读和幻读的情况。
第二个事务前后两次查询的的 balance 字段结果不同,这就是不可重复读(update)。多次读取的结果无法确定(无法确定另一个事务的改动)。
第二个事务两次查询的结果行数不一致,这就是幻读(insert)。
幻读和不可重复读很相似,但是幻读强调的是查询结果集合的增减,而不是单条数据的更新。
可重复读 RR
默认的隔离级别:Repeatable Read,简称 RR。可以解决不可重复读和幻读。
set transaction_isolation = 'REPEATABLE-READ'
下面分别演示了,在 RR 隔离级别下,不可重复读和幻读问题被解决。
参考博客
Understanding the Isolation Property in a Database (lifewire.com)
MySQL :: MySQL 8.0 Reference Manual :: 15.7.2.1 Transaction Isolation Levels