事务的特性和隔离级别

数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。事务的使用是数据库管理系统区别文件系统的重要特征之一。

事务的特性

数据库事务的四大特性指的是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),人们习惯称之为 ACID 特性。

  • 原子性(Atomicity)

事务是数据库操作的基本单位,要么全部做完,要么全部不做,不能分割。

  • 一致性(Consistency)

事务将数据库从一种状态转变为另一种一致的的状态。事务开始前和结束后,数据库的完整性约束没有被破坏。
例如工号带有唯一属性,如果经过一个修改工号的事务后,工号变的非唯一了,则表明一致性遭到了破坏。

  • 隔离性(Isolation)

事务之间是相互隔离的,一个事务操作时对其它事务不可见,一个事务不应该影响其它事务运行效果。

  • 持久性(Durability)

事务一旦提交,则其结果就是永久性的。

事务的隔离级别

SQL 标准定义了四种隔离级别。

READ UNCOMMITTED(读未提交)

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

  1. 准备两个终端,在此命名为 mysql 终端 1 和 mysql 终端 2,再准备一张测试表 test,写入一条测试数据并调整隔离级别为 READ UNCOMMITTED,任意一个终端执行即可。
1
2
3
4
5
SET @@session.transaction_isolation = 'READ-UNCOMMITTED';
create database test;
use test;
create table test(id int primary key);
insert into test(id) values(1);
  1. 登录 mysql 终端 1,开启一个事务,将 ID 为 1 的记录更新为 2。
1
2
3
begin;
update test set id = 2 where id = 1;
select * from test; -- 此时看到一条ID为2的记录
  1. 登录 mysql 终端 2,开启一个事务后查看表中的数据。
1
2
3
use test;
begin;
select * from test; -- 此时看到一条 ID 为 2 的记录

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

READ COMMITTED(读提交)

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

  1. 准备两个终端,在此命名为 mysql 终端 1 和 mysql 终端 2,再准备一张测试表 test,写入一条测试数据并调整隔离级别为 READ COMMITTED,任意一个终端执行即可。
1
2
3
4
5
SET @@session.transaction_isolation = 'READ-COMMITTED';
create database test;
use test;
create table test(id int primary key);
insert into test(id) values(1);
  1. 登录 mysql 终端 1,开启一个事务,将 ID 为 1 的记录更新为 2,并确认记录数变更过来。
1
2
3
begin;
update test set id = 2 where id = 1;
select * from test; -- 此时看到一条记录为 2
  1. 登录 mysql 终端 2,开启一个事务后,查看表中的数据。
1
2
3
use test;
begin;
select * from test; -- 此时看一条 ID 为 1 的记录
  1. 登录 mysql 终端 1,提交事务。
1
commit;
  1. 切换到 mysql 终端 2。
1
select * from test; -- 此时看到一条 ID 为 2 的记录

mysql 终端 2 在开启了一个事务之后,在第一次读取 test 表(此时 mysql 终端 1 的事务还未提交)时 ID 为 1,在第二次读取 test 表(此时 mysql 终端 1 的事务已经提交)时 ID 已经变为 2,说明在此隔离级别下已经读取到已提交的事务。

REPEATABLE READ(可重复读)

该隔离级别是 MySQL 默认的隔离级别,在同一个事务里,select 的结果是事务开始时时间点的状态,因此,同样的 select 操作读到的结果会是一致的,但是,会有幻读现象。

  1. 准备两个终端,在此命名为 mysql 终端 1 和 mysql 终端 2,准备一张测试表 test 并调整隔离级别为 REPEATABLE READ,任意一个终端执行即可。
1
2
3
4
SET @@session.transaction_isolation = 'REPEATABLE-READ';
create database test;
use test;
create table test(id int primary key,name varchar(20));
  1. 登录 mysql 终端 1,开启一个事务。
1
2
begin;
select * from test; -- 无记录
  1. 登录 mysql 终端 2,开启一个事务。
1
2
begin;
select * from test; -- 无记录
  1. 切换到 mysql 终端 1,增加一条记录并提交。
1
2
insert into test(id,name) values(1,'a');
commit;
  1. 切换到 msyql 终端 2。
1
select * from test; --此时查询还是无记录

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

  1. 此时接着在 mysql 终端 2 插入一条数据。
1
insert into test(id,name) values(1,'b'); -- 此时报主键冲突的错误

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

SERIALIZABLE(序列化)

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

  1. 准备两个终端,在此命名为 mysql 终端 1 和 mysql 终端 2,分别登入 mysql,准备一张测试表 test 并调整隔离级别为 SERIALIZABLE,任意一个终端执行即可。
1
2
3
4
SET @@session.transaction_isolation = 'SERIALIZABLE';
create database test;
use test;
create table test(id int primary key);
  1. 登录 mysql 终端 1,开启一个事务,并写入一条数据。
1
2
begin;
insert into test(id) values(1);
  1. 登录 mysql 终端 2,开启一个事务。
1
2
begin;
select * from test; -- 此时会一直卡住
  1. 立马切换到 mysql 终端 1,提交事务。
1
commit;

一旦事务提交,msyql 终端 2 会立马返回 ID 为 1 的记录,否则会一直卡住,直到超时,其中超时参数是由 innodb_lock_wait_timeout 控制。由于每条 select 语句都会加锁,所以该隔离级别的数据库并发能力最弱,但是有些资料表明该结论也不一定对,如果感兴趣,您可以自行做个压力测试。

表 1 总结了各个隔离级别下产生的一些问题。

隔离级别 脏读 不可重复读 幻读
读未提交
读提交
可重复读
序列化

本文内容摘自:MySQL 事务隔离级别和锁

作者

Jakes Lee

发布于

2019-11-17

更新于

2021-06-12

许可协议

评论