MySQL 事务的基础知识

事务的基础知识

1. 数据库事务概述

事务是数据库区别于文件系统的重要特性之一,当我们有了事务就会让数据库中的数据始终保持 一致性,同时我们还能通过事务的机制 恢复到某个时间地点的数据,这样可以保证已提交到数据库的修改不会因为系统崩溃而丢失。

1.1 存储引擎的支持情况

查询当前 MySQL 支持的存储引擎

mysql 复制代码
show engines;
Engine Support Comment Transactions XA Savepoints
MEMORY YES Hash based, stored in memory, useful for temporary tables NO NO NO
MRG_MYISAM YES Collection of identical MyISAM tables NO NO NO
CSV YES CSV storage engine NO NO NO
FEDERATED NO Federated MySQL storage engine
PERFORMANCE_SCHEMA YES Performance Schema NO NO NO
MyISAM YES MyISAM storage engine NO NO NO
InnoDB DEFAULT Supports transactions, row-level locking, and foreign keys(支持事务、行级锁定和外键) YES YES YES
BLACKHOLE YES /dev/null storage engine (anything you write to it disappears) NO NO NO
ARCHIVE YES Archive storage engine NO NO NO

只有 InnoDB 存储引擎是支持事务的。

1.2 基本概念

事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态。

事务处理的原则:保证所有事务都作为 一个工作单元 来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交(commit),那么这些修改就 永远 地保持下来;要么 放弃 所做的所有 修改,整个事务回滚(rollback)到最初状态。

例如:账户转账(aa 向 bb 转账 100 元)

mysql 复制代码
# aa 减 100
update account set money = money - 100 where name = 'AA';
# bb 加 100
update account set money = money + 100 where name = 'BB';

以上的操作就是 "一组逻辑操作单元" ,在逻辑上(业务中)是不可分割的。

注意:如果在为 bb 加钱时,出现故障或者错误,那么将放弃 aa 减钱的操作,将钱退回给 aa。

1.3 事务的 ACIC 特性

1.3.1 原子性(atomicity)

原子性是指事务是 一个不可分割的工作单位,要么全部提交,要么全部失败回滚。

即要么转账成功,要么转账失败,是不存在中间的状态。如果无法保证原子性会怎么样?就会出现数据不一致的情形,a账户减去100元,而b账户增加100元操作失败,系统将无故丢失100元。

1.3.2 一致性(consistency)

(建议参考 Wikipedia 对 一致性(consistency)的阐述)

根据定义,一致性是事务执行前后,数据从一个 合法性状态 变换另外一个 合法性状态 。这种状态是 语义上 的而不是语法上的,根据具体的业务有关。

那什么是合法的数据状态呢?

满足 预定的约束 的状态就叫做合法的状态。通俗一点,这状态是由我们自己来定义的(比如满足现实世界中的约束)满足这个状态,数据就是一致性的,不满足这个状态,数据就是不一致的!,如果事务中的某个操作失败了,系统就会自动撤销当前正在执行的事务,返回到事务操作之前的状态。

举例1:账户的余额必须 >=0

a账户有200元,转账300元出去,此时账户余额为-100元。此时数据就是不一致的。因为定义了余额必须 >=0 状态(规则)。

举例2:不管怎么操作,两个账户的总余额必须不变

a账户200元,转账50元给b账户,a账户的钱扣了,但是b账户因为各种意外,余额并没有增加,此时数据就是不一致的、因为定义了a和b的总余额必须不变的状态(规则)

举例3:唯一性约束

在数据表中我们将 姓名 字段设置为 唯一性约束,这时当事务进行提交或者事务发生回滚的时候,如果数据表中的姓名不唯一,就破坏了事务的一致性要求。

1.3.3 隔离性(isolation)

事务的隔离性是指 一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据 对并发的其他事务是隔离的,并发执行的各个事务之间不能相互干扰。

如果无法保证隔离性会怎么样?

假设a账户有200元,b账户0元。a账户往b账户转账两次,每次金额为50元,分别在两个事务中执行。如果无法保证隔离性,就可能会出现 数据不一致 的情形:

mysql 复制代码
update accounts set money = money - 50 where name = 'aa';

update accounts set money = money + 50 where name = 'bb';

该行为就造成了 "脏写"。

1.3.4 持久性(durability)

持久性是指一个事务一旦被提交,它对数据库中的数据的改变就是 永久性的(写入磁盘了),接下来的其他操作和数据库故障不应该对其有任何影响。

持久性是通过 事务日志 来保证的。日志包括了 重做日志 和 回滚日志。

当我们通过事务对数据进行修改的时候,首先会将数据库的变化信息记录到 重做日志 中,然后再对数据库中对应的行进行修改。这样做的好处是,即使数据库系统崩溃,数据库重启后也能找到没有更新到数据库系统中的 重做日志,重新执行,从而使事务具有持久性。

1.3.5 总结 - 重要概念

ACID 是事务的四大特性,在这四个特性中 原子性是 "基础",隔离性是 "手段",一致性是 "约束条件",而持久性是 "目的"。

数据库事务,其实就是数据库设计者为了方便起见,把需要保证 原子性,隔离性,一致性 和 持久性 的一个或多个数据库操作 称为一个事务。

1.4 事务的状态

我们现在知道 事务 是一个抽象的概念,它其实对应着 一个或多个数据库操作(dml),MySQL 根据这些操作所执行的不同阶段把 事务 大致划分成几个状态:

  • 活动的(active)

事务对应的 数据库操作正在执行过程中时,我们就说该事务处在 活动的 状态。

  • 部分提交的(partially committed)

当事务中的 最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并 没有刷新到磁盘 时,我们就说该事务处在 部分提交的 状态。(在刷盘之前)

  • 失败的(failed)

当事务处在 活动的 或者 部分提交的 状态时,可能遇到了某些错误(数据库自身的错误,操作系统错误或者直接断电等)而无法继续执行,或者人为的停止当前事务的执行,那个该事务处在 失败的 状态。

  • 中止的(aborted)

如果事务执行了一部分而变为 失败的 状态,那么就需要把已经修改的事务中的操作还原到事务执行前的状态(回滚)。这时该事务处在 中止的 状态。

  • 提交的(committed)

当一个处在 部分提交的 状态的事务将修改过的数据都 同步到磁盘 上之后,我们就可以说该事务处在了 提交的 状态。

一个基本的状态转换图如下所示:

如图所示,只有当事务处于 提交的 或者 中止的 状态时,一个事务的生命周期才算结束了。

  • 对于 已经提交的事务 来说,该事务对数据库所做的修改将 永久生效。
  • 对于 处于中止状态的事务,该事务对数据库所做的所有修改都 会被回滚到没执行该事务之前的状态。

2.如何使用事务

一个事务的完整过程:

步骤1:开启事务

步骤2:一系列的 dml 操作 ...

步骤3:事务结束的状态(提交 commit,中止 rollback)

使用事务有两种方式,分别为 显示事务 和 隐式事务。

2.1 显式事务

start transaction 或者 begin,作用是 显式开启一个事务。

步骤1:开启事务

mysql 复制代码
BEGIN;
# 或者
START TRANSACTION;

注意:使用以上的方式开启事务,是不受 autocommit (自动提交)变量影响的。

start transaction 语句相较于 begin 特别之处在于,后面能跟随几个 修饰符:

  • read only:只读事务。属于该事务的数据库操作 只能读取数据,不能修改数据。

补充:只读事务中只是不允许修改哪些其他事务也能访问到表中的数据(事务共享表 - 数据),对于临时表来说(使用 create tmeporary table 创建的表),由于它们只能在当前会话中可见(事务独享表 - 数据),所以只读事务其实也是可以对临时表进行增,删,改操作的。

  • read write:可读写事务(默认)。属于该事务的数据库操作 可以读取数据,也可以修改数据。
  • with consistent snapshot:开启一致性读。

比如:

mysql 复制代码
START TRANSACTION READ only;  # 开启一个只读事务

START TRANSACTION READ only, WITH CONSISTENT SNAPSHOT; # 开启只读事务和一致性读

START TRANSACTION READ WRITE, WITH CONSISTENT SNAPSHOT; # 开启读写事务和一致性读

步骤2:一系列事务中的操作(主要是dml操作,不含ddl)

步骤3:事务结束的状态(提交 commit,中止 rollback)

mysql 复制代码
# 提交事务,当提交事务后,对数据库的修改时永久性的
commit; 
mysql 复制代码
# 回滚事务,即撤销正在进行的所有没有提交的修改
rollback;
mysql 复制代码
# 将事务回滚到某个保存点
rollback to [savepoint];

其中关于 savepoint 相关操作有:

  • 创建保存点:
mysql 复制代码
# 在事务中创建保存点,方便后续针对保存点进行回滚,一个事务中可以存在多个保存点
savepoint 保存点名称;
  • 删除保存点:
mysql 复制代码
# 删除某个保存点
release savepoint 保存点名称;

2.2 隐式事务

MySQL 中有一个系统变量 autocommit(默认:开启):

  • 查看 autocommit
mysql 复制代码
SHOW VARIABLES LIKE 'autocommit';
# 或者
SELECT @@autocommit;
# 或者
SELECT @@global.autocommit;
  • 设置 autocommit
mysql 复制代码
SET autocommit = TRUE; # 会话级别,只在当前会话生效
# 或者
SET GLOBAL autocommit = TRUE; # 注意:全局设置,MySQL服务器重启后失效

默认情况下,如果我们不显式的使用 start transaction 或者 begin 语句开启一个事务,那么每一条语句都算是一个独立的事务,这种特性称之为事务的 自动提交。也就是说,不以 start transaction 或者 begin 语句显式的开启一个事务,那么执行的 dml 操作就相当于放到独立的事务中执行。

关闭 自动提交 的功能两中方法:

  • 显式使用 start transaction 或者 begin 语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭 自动提交 的功能。
  • 把系统变量 autocommit 的值设置为 OFF。
mysql 复制代码
SET autocommit = false; # 会话级别,只在当前会话生效
#或者
SET autocommit = OFF; # 会话级别,只在当前会话生效
#或者
SET autocommit = 0; # 会话级别,只在当前会话生效

这样的话,写入的多条语句就算是属于同一个事务了,直到我们显式的写出 commit 或者 rollback (提交或回滚)。

补充:Oracle 默认不自动提交事务,需要手动 commit 或者 rollback,而 MySQL 默认自动提交。

2.3 隐式提交数据的情况(重要)

  • 使用数据库定义语言(DDL)

当我们使用 create,alter,drop 等语句去 修改数据库对象 时(数据库,表,视图,触发器,存储过程,视图),就会隐式的提交前面语句所属于的事务:

mysql 复制代码
begin; # 开启事务
select .... # 事务中的语句
update .... # 事务中的语句
.... # 事务中的语句

create table .... # 此时会隐式的提交前边语句所属于的事务 
  • 隐式使用或者修改 "mysql" 库中的表(这里的mysql指的系统数据库中名字为 "mysql" 数据库)。

当我们使用 alter user,create user,drop user,grant,rename user,revoke,set password 等语句 修改用户,权限 时也会隐式的提交前边语句所属于的事务。

  • 使用 "事务控制" 或关于 "锁" 定的语句

    • 当我们在一个事务中还没提交或者回滚时就又使用 start transaction 或者 begin 语句开启了另一个事务时,会 隐式的提交 上一个事务:
    mysql 复制代码
    begin; # 开启事务
    select .... # 事务中的语句
    update .... # 事务中的语句
    .... # 事务中的语句
    
    begin; # 又开启了一个事务 此时会隐式的提交上一个事务 
    • 当前的 autocommit 系统变量的值为 OFF,我们手动修改为 ON 时,也会 隐式的提交 前面语句所属的事务。
    • 使用 lock tables,unlock tables 等关于 "锁" 定的语句时也会 隐式的提交 前边语句所属的事务。
  • 关于 MySQL 复制的一些语句(主从复制)

使用 start slave,stop slave,reset slave,change master to 等语句时会 隐式的提交 前边语句所属的事务。

  • 其他的一些语句

使用 analyze table(分析表),cache index,check table(检查表),flush(刷新),load index into cache,optimize table(优化表),repair table,reset 等语句也会 隐式的提交 前边语句所属的事务。

2.4 举例:提交与回滚 - 显式与隐式

mysql 复制代码
# 创建测试表
create TABLE IF NOT EXISTS xld_begin(
id INT UNSIGNED PRIMARY KEY auto_increment COMMENT '主键id',
name VARCHAR(15) NOT NULL COMMENT '名称',
age TINYINT UNSIGNED COMMENT '年龄',
INDEX idx_age(age)
)ENGINE = INNODB DEFAULT CHARSET = utf8;
  • 回滚(rollback) - 事务
mysql 复制代码
# 查询数据
SELECT * FROM xld_begin;

# 查看自动提交是否开启
SHOW VARIABLES LIKE '%autocommit%'; # ON 开启

# 给表添加一条记录
INSERT INTO xld_begin(name,age)VALUE ('张三',10);

# 查询数据
SELECT * FROM xld_begin;

# 开启一个事务
BEGIN;

# 在事务中添加一条记录
INSERT INTO xld_begin(name,age) VALUE ('李四',10);

# 查询数据
SELECT * FROM xld_begin;

# 程序错误
INSERT INTO xld_begin(name,age)VALUE (NULL,10);

# 回滚事务 - 事务结束
ROLLBACK;

# 查询数据
SELECT * FROM xld_begin;
  • 提交(commit) - 事务
mysql 复制代码
# 清空表 (DDL)不受事务控制
TRUNCATE TABLE xld_begin;

# 查询数据
SELECT * FROM xld_begin;

# 查看自动提交是否开启
SHOW VARIABLES LIKE '%autocommit%'; # ON 开启

# 给表添加一条记录
INSERT INTO xld_begin(name,age)VALUE ('张三',10);

# 查询数据
SELECT * FROM xld_begin;

# 开启一个事务
BEGIN;

# 在事务中给表添加一条记录
insert into xld_begin(name,age) VALUE ('李四',12);

# 修改张三的年龄
update xld_begin set age = 12 WHERE name = '张三';

# 查询数据
SELECT * FROM xld_begin;

# 提交事务 - 事务结束
COMMIT;

# 查询数据
SELECT * FROM xld_begin;
  • 链事务
mysql 复制代码
# 清空表 (DDL)不受事务控制
TRUNCATE TABLE xld_begin;

# 查询数据
SELECT * FROM xld_begin;

# 查看自动提交是否开启
SHOW VARIABLES LIKE '%autocommit%'; # ON 开启

# 开启链事务
SET @@SESSION.completion_type = 1;

# 查看链事务是否开启成功
SELECT @@session.completion_type;

# 给表添加一条记录
INSERT INTO xld_begin(name,age)VALUE ('张三',10);

# 查询数据
SELECT * FROM xld_begin;

# 开启事务
BEGIN;

# 在事务中给表添加一条记录
insert into xld_begin(name,age) VALUE ('李四',12);

# 查询数据
SELECT * FROM xld_begin;

# 提交事务 - 事务结束
COMMIT;

# 查询数据
SELECT * FROM xld_begin;

# 在事务中给表添加一条记录
insert into xld_begin(name,age) VALUE ('王五',14);

# 程序错误
INSERT INTO xld_begin(name,age)VALUE (NULL,10);

# 查询数据
SELECT * FROM xld_begin;

ROLLBACK;

# 查询数据
SELECT * FROM xld_begin;

completion_type 系统变量(全局/会话)的使用:

  • 查看 completion_type
mysql 复制代码
SHOW VARIABLES LIKE '%completion%';
# 或者
SELECT @@completion_type;
  • 设置 completion_type
mysql 复制代码
SET @@completion_type = 1; # 开启链式事务
  • completion_type 的参数配置说明:
    • completion_type = 0(默认),当我们执行 commit 时会提交事务,在执行下一个事务时,需要使用 start transaction 或者 begin 来开启。
    • completion_type = 1,这种情况下,当我们执行 commit 提交事务后,相当于执行了 commit and chain,也就是开启一个 链式事务,即当我们提交事务之后会开启一个相同隔离级别的事务。(注意:此时的"自动提交"是失效的,必须手动结束事务)
    • completion_type = 2,这种情况下 commit = commit and release,当我们执行 commit 提交事务后,会自动与服务器断开连接。

2.5 测试 innodb 与 myisam 事务的支持情况

mysql 复制代码
# 创建 innodb 事务表
create TABLE IF NOT EXISTS xld_begin_innodb(
id int UNSIGNED PRIMARY KEY auto_increment COMMENT '主键id',
name VARCHAR(15) NOT NULL COMMENT '名称',
age TINYINT UNSIGNED COMMENT '年龄',
INDEX idx_age(age)
) ENGINE = INNODB DEFAULT charset = utf8;

# 创建 myisam 事务表
create TABLE IF not EXISTS xld_begin_myisam(
id INT UNSIGNED PRIMARY KEY auto_increment COMMENT '主键id',
name VARCHAR(15) NOT NULL COMMENT '名称',
age TINYINT UNSIGNED COMMENT '年龄',
index idx_age(age)
)ENGINE = myisam DEFAULT charset = utf8;
  • Innodb 存储引擎支持事务
mysql 复制代码
# 查询数据
SELECT * FROM xld_begin_innodb;

# 开启事务
BEGIN;

# 在事务中给表添加一条记录
INSERT INTO xld_begin_innodb(name,age) value ('张三',10);

# 程序错误
INSERT INTO xld_begin_innodb(name,age) value (NULL,10);

# 回滚事务 - 事务结束
ROLLBACK;

# 查询数据
SELECT * FROM xld_begin_innodb;
  • Myisam 存储引擎不支持事务
mysql 复制代码
# 查询数据
SELECT * FROM xld_begin_myisam;

# 开启事务
BEGIN;

# 在事务中给表添加一条记录
INSERT INTO xld_begin_myisam(name,age) value ('张三',12);

# 程序错误
INSERT INTO xld_begin_myisam(name,age) value (NULL,12);

# 回滚事务 - 事务结束
ROLLBACK;

# 查询数据
SELECT * FROM xld_begin_myisam;

2.6 举例 :事务保存点(savepoint)

mysql 复制代码
# 创建表
create TABLE IF not EXISTS xld_begin_savepoint(
id int UNSIGNED PRIMARY KEY auto_increment COMMENT '主键id',
name VARCHAR(15) not null COMMENT '名称',
age TINYINT UNSIGNED COMMENT '年龄',
index idx_age(age)
)ENGINE = INNODB DEFAULT charset = utf8;
  • 保存点的使用
mysql 复制代码
# 查询数据
SELECT * FROM xld_begin_savepoint;

# 开启事务
BEGIN;

# 在事务中给表添加一条记录
INSERT INTO xld_begin_savepoint(name,age) value ('张三',10);

# 在事务中给表添加一条记录
INSERT INTO xld_begin_savepoint(name,age) value ('李四',10);

# 查询数据
SELECT * FROM xld_begin_savepoint;

# 创建保存点
savepoint xld_savepoint1;

# 在事务中给表添加一条记录
INSERT INTO xld_begin_savepoint(name,age) value ('王五',10);

# 在事务中给表添加一条记录
INSERT INTO xld_begin_savepoint(name,age) value ('刘六',10);

# 查询数据
SELECT * FROM xld_begin_savepoint;

# 创建保存点
savepoint xld_savepoint2;

# 在事务中给表添加一条记录
INSERT INTO xld_begin_savepoint(name,age) value ('王八',10);

# 在事务中给表添加一条记录
INSERT INTO xld_begin_savepoint(name,age) value ('林九',10);

# 查询数据
SELECT * FROM xld_begin_savepoint;

# 回滚到保存点 xld_savepoint2
ROLLBACK TO xld_savepoint2; # 注意:此时,事务还未结束

# 查询数据
SELECT * FROM xld_begin_savepoint;

# 回滚到保存点 xld_savepoint1
ROLLBACK TO xld_savepoint1; # 注意:此时,事务还未结束

# 查询数据
SELECT * FROM xld_begin_savepoint;

# 提交事务 - 事务结束
COMMIT;

# 查询数据
SELECT * FROM xld_begin_savepoint;

3. 事务的隔离级别

MySQL 是一个 客户端(C)/ 服务器(S) 架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称为一个会话(Session)。

每个客户端可以在自己的会话中向服务器发出请求语句(DML),一个请求语句(DML)可能是某个事务的一部分,也就是说 MySQL 服务器可能会同时处理多个事务。事务是有 隔离 的特性的,理论上在某个事务 对某个数据进行访问 时,其他事务应该进行 排队,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样对 性能影响太大,我们既想保持事务的 隔离性,又想让服务器在处理多个事务时(同一个数据) 性能尽量高些,那就看二者如何权衡取舍了。

3.1 数据准备

mysql 复制代码
# 自己想办法吧!!!

3.2 数据并发问题

针对事务的隔离性和并发性,我们怎么做取舍呢?先看一下访问相同数据的事务在 不保证串行执行(也就是执行完一个再执行另一个)的情况下可能会出现哪些问题:

1. 脏写(Dirty Write)

有两个事务:事务A,事务B。如果 事务A 修改了 另一个 事务B 修改过且未提交 的数据,那就意味着发生了 脏写,示意图如下:

有两个事务:事务A,事务B ,事务B 先将 id 为1的name更新为 '李四',然后 事务A 接着又把这条 id 为1的name更新为 '张三' 且提交(commit)了。如果之后 事务B 进行了回滚,那么 事务A 中的更新也将不复存在,这种现象就称之为 脏写。这时 事务A 就没有效果了,明明把数据更新了,最后也提交事务了,最后看到的数据什么变化也没有。

在 MySQL 默认的事务隔离级别下,在 事务A 中执行的更新语句会处于等待状态(加锁了)。

2. 脏读(Dirty Read)

有两个事务:事务A,事务B。事务A 读取 了已经被 事务B 更新但还没有提交 的数据。之后若 事务 B 回滚,事务A 读取 的内容就是 临时且无效 的。(一个事务读到了,另一个事务修改了但未提交的数据)

有两个事务:事务A,事务B ,事务B 先将 id 为1的name更新为 '张三',然后 事务A 再去查询这条 id 为1的记录,如果读的name的值为'张三',而 事务B 不久之后进行了回滚,那么 事务A 中就相当于读到了一个不存在的数据,这种现象就称之为 脏读。

3. 不可重复读(Non-Repeatable Read)

有两个事务:事务A,事务B。事务A 读取 了一条记录,然后在 事务B 中 更新了且提交(commit)该记录。随之 事务A 再次读取了该记录,值就不同了。那这就意味着发生了 不可重复读。(在同一个事务中,多次执行相同的语句,但每次读取的结果不同)

有两个会话 sessionA,sessionB。session B 中提交了几个 隐式事务(注意是隐式事务,意味着语句结束事务就提交了),这些事务都修改了 id 为1的name值,每次事务提交之后,如果 Session A 中的事务都可以查看到最新的值,这种现象也被称之为 不可重复读。

4. 幻读(Phantom)

有两个事务:事务A,事务B。事务A 从一个表中 读取 了一条记录,然后 事务B 给该表 插入 了一些新的记录。之后,如果 事务A 再次读取 同一个表,结果集出现了多几行的话。那就意味着发生了 幻读。()

有两个会话 sessionA,sessionB。session A 中的事务先根据条件 id > 0 这个条件查询数据,得到了 name 为 '张三' 的记录。之后 session B 中提交了一个 隐式事务,该事务向表中插入了一条新纪录。之后 session A 中的事务再次根据相同的条件 id > 0 查询数据,得到的结果集中包含了 session B 中的事务新插入的那条记录,这种现象也就称为 幻读(我们把新插入的那些记录称之为 幻影记录)。

注意1:

有的人会有疑问,那如果 session B 中 删除了 一些符合 id > 0 的记录而不是插入新纪录,那 session A 之后再根据 id > 0 的条件读取的 记录变少了,这种现象算不算 幻读 呢?这种现象 不属于幻读,幻读 强调的是一个事务按照某个 相同条件多次读取 记录时,后读取时读到了之前 没有读到的记录。

注意2:

那对于先前已经读到的记录,之后又读取不到这种情况,算啥呢?这相当于对每一条记录都发生了 不可重复读 的现象。幻读 只是 重点强调了现在读取到了,之前读取时 ,没有获取的到记录。

3.3 SQL 中的四种隔离级别

上面介绍了几种并发事务执行过程中可能遇到的一些问题,这些问题有轻重缓急之分,我们给这些问题按照严重性排一下序:

脏写 > 脏读 > 不可重复读 > 幻读

如何通过舍弃一部分隔离性来换取一部分性能呢?

答:设立一些隔离级别,隔离级别越低,并发问题发生的就越多,性能就越好。

在 SQL 标准 中设立了4个 隔离级别:

  • read uncommitted(读未提交): 在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。不能避免:脏读,不可重复读,幻读。
  • read committed(读已提交):该隔离级别满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这也是大多数数据库系统的默认隔离级别(但不是 MySQL 默认 的)。可以避免脏读。但不能避免:不可重复读,幻读。
  • repeatable read(可重复读):该隔离级别可以做到,事务A 在读到一条数据之后,此时 事务B 对该数据进行了修改并提交,那么 事务A 再读该数据,读到的还是原来的内容。可以避免脏读,不可重复读。但不能避免:幻读
  • serializable(可串行化):该隔离级别可以确保事务在多次读取表中数据时都是相同的数据。在这事务持续期间,禁止其他事务对该表执行插入,更新和删除操作。所有的并发问题都可以避免,但性能十分低下。

SQL 标准 中规定,针对不同的隔离级别,并发事务可以发生不同的问题,具体情况如下:

注意:由于"脏写"这个问题太严重了,不论是那种隔离级别,都不允许"脏写"的情况发生。

不同的隔离级别有不同的现象,并有不同的锁和并发机制,隔离级别越高,数据库的并发性能就越差,4种事务隔离级别与并发性能的关系如下:

3.4 MySQL 支持的四种隔离级别

不同的数据库厂商对 SQL 标准中规定的四种隔离级别的支持也是不一样的。比如,Oracle 就只支持 read committed(读已提交,默认隔离级别) 和 serializable(串行化)。MySQL 虽然支持4种隔离级别,但与 SQL 标准中所规定的各级隔离级别允许发生的问题却有些出入,MySQL 在 repeatable read(可重复读) 隔离级别下,是可以禁止 幻读 问题发生的。

MySQL 的默认隔离级别为:repeatable read。

  • 查看 MySQL 的默认隔离级别:transaction_isolation(全局/会话)
mysql 复制代码
# 5.7.20 版本之前使用:
SHOW VARIABLES LIKE '%tx_isolation%';

# 5.7.20 版本之后使用(transaction_isolation 替换了 tx_isolation):
SHOW VARIABLES LIKE '%transaction_isolation%';

# 或者
SELECT @@transaction_isolation; # 同时支持全局和会话

3.5 如何设置事务的隔离级别

通过下面的语句修改事务的隔离级别:

  • 方式1:
mysql 复制代码
set [global | session] transaction isolation level 隔离级别;
# 其中,隔离级别格式:
> read uncommitted
> read committed
> repeatable read
> serializable

例如:设置会话中的隔离级别为:读已提交

mysql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
  • 方式2:
mysql 复制代码
set [global | session] transaction_isolation = '隔离级别';
# 其中,隔离级别格式:
> read-uncommitted
> read-committed
> repeatable-read
> serializable

例如:设置会话中的隔离级别为:读已提交

mysql 复制代码
SET SESSION TRANSACTION_ISOLATION = 'read-committed';

关于设置时使用 global 或 session 的影响:

  • 使用 global 关键字(在全局范围影响):
mysql 复制代码
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
#或者
SET GLOBAL TRANSACTION_ISOLATION = 'read-committed';

注意:

  • 当前已经存在的会话无效
  • 只对执行完该语句之后产生的会话起作用
  • 使用 session 关键字(在会话范围影响):
mysql 复制代码
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
#或者
SET SESSION TRANSACTION_ISOLATION = 'read-committed';

注意:

  • 对当前会话的所有后续的事务有效
  • 如果在事务与事务之间执行,则对后续的事务有效
  • 该语句可以在已经开启事务中间执行,但不会影响当前正在执行的事务,只对后续的事务有效

如果在服务器启动时相改变事务的默认隔离级别,可以修改启动参数 transaction_isolation 的值。比如,在启动服务器指定了 transaction_isolation = read-committed。那么事务的默认隔离级别就从原来的 repeatable-read(可重复读) 变成了 read-committed(读已提交)。

小结:

数据库规定了多种事务的隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但相对的并发性能就越差。

3.6 不同隔离级别举例

  • 初始化数据:
mysql 复制代码
# 创建表
create table if not exists xld_transaction_isolation(
id INT PRIMARY KEY auto_increment COMMENT '主键id',
name VARCHAR(15) not NULL COMMENT '名称',
money int DEFAULT 0 COMMENT '金额'
)ENGINE = INNODB DEFAULT charset = utf8;

# 新增数据
INSERT into xld_transaction_isolation (name,money) VALUES('张三',100),('李四',80);

# 查询数据
SELECT * FROM xld_transaction_isolation;
# 初始化表中数据:
id	name monry	
1	张三	100
2	李四	80

1. 演示:脏读 - 读未提交(read-uncommitted):

  1. 事务A 先执行:
mysql 复制代码
# 设置事务隔离级别为:读未提交
SET SESSION transaction_isolation = 'read-uncommitted';

# 查看事务的隔离级别
SELECT @@transaction_isolation;

# 开启事务
BEGIN;

# 修改张三的余额
UPDATE xld_transaction_isolation SET money =  money + 50 WHERE NAME = '张三';

# 查询数据
SELECT * FROM xld_transaction_isolation;
# 查询的结果为:
id	name monry	
1	张三	150
2	李四	80
  1. 事务B 再执行:
mysql 复制代码
# 设置事务隔离级别为:读未提交
SET SESSION transaction_isolation = 'read-uncommitted';

# 查看事务的隔离级别
SELECT @@transaction_isolation;

# 开启事务
BEGIN;

# 查询数据
SELECT * FROM xld_transaction_isolation;
# 查询的结果为:
id	name monry	
1	张三	150
2	李四	80

**此时可以看到在 事务B 中读取了到 事务A 中 修改了但未提交 的数据。这时就出现了 脏读 问题。 **

2. 演示:避免脏读 - (read-committed)

  1. 事务A 先执行:
mysql 复制代码
# 设置事务隔离级别为:读已提交
SET SESSION transaction_isolation = 'read-committed';

# 查看事务的隔离级别
SELECT @@transaction_isolation;

# 开启事务
BEGIN;

# 修改张三的余额
UPDATE xld_transaction_isolation SET money =  money + 50 WHERE NAME = '张三';

# 查询数据
SELECT * FROM xld_transaction_isolation;
# 查询的结果为:
id	name monry	
1	张三	150
2	李四	80
  1. 事务B 再执行:
mysql 复制代码
# 设置事务隔离级别为:读已提交
SET SESSION transaction_isolation = 'read-committed';

# 查看事务的隔离级别
SELECT @@transaction_isolation;

# 开启事务
BEGIN;

# 查询数据
SELECT * FROM xld_transaction_isolation;
# 查询的结果为:
id	name monry	
1	张三	100
2	李四	80

**此时可以看到在 事务B 中并没有读取到 事务A 中 修改了但未提交 的数据。避免了 脏读 问题。 **

  1. 之后 事务A 提交修改的数据:
mysql 复制代码
.......
# 提交
COMMIT;
  1. 随之在 事务B 中再次执行查询:
mysql 复制代码
.......
# 查询数据
SELECT * FROM xld_transaction_isolation;
# 查询的结果为:
id	name monry	
1	张三	150
2	李四	80

此时可以看到 事务B 中读取到了 事务A 中 修改了并提交 的数据。

这时我们可能明显的看到在 事务B 中 ,两次查询的值是不同的,这时就出现了 不可重复读 问题。

3. 演示:避免不可重复读 - (repeatable-read)

  1. 事务A 先执行
mysql 复制代码
# 设置事务的隔离级别为:可重复读
SET SESSION TRANSACTION_ISOLATION = 'repeatable-read';

# 查看事务的隔离级别
SELECT @@transaction_isolation;

#开启事务
BEGIN;

# 查询数据 - 事务B 未提交修改的数据之前
SELECT * from xld_transaction_isolation;
# 查询的结果为:
id	name monry	
1	张三	100
2	李四	80
  1. 事务B 再执行
mysql 复制代码
# 设置事务的隔离级别为:可重复读
SET SESSION TRANSACTION_ISOLATION = 'repeatable-read';

# 查看事务的隔离级别
SELECT @@transaction_isolation;
	
# 开启事务
BEGIN;

# 修改张三余额
UPDATE xld_transaction_isolation SET money = money - 50 where name = '张三';

# 查询数据
SELECT * from xld_transaction_isolation;
# 查询的结果为:
id	name monry	
1	张三	50
2	李四	80
# 提交事务
COMMIT;

# 查询数据
SELECT * from xld_transaction_isolation;
# 查询的结果为:
id	name monry	
1	张三	50
2	李四	80
  1. 之后 事务A 再次查询数据
mysql 复制代码
.......
# 查询数据 - 事务B 提交了修改的数据后
SELECT * from xld_transaction_isolation;
# 查询的结果为:
id	name monry	
1	张三	100
2	李四	80

# 提交事务
COMMIT;

此时可以看到在 事务A 中并没有读取到了 事务B 中 修改了并提交 的数据,避免了 不可重复读 的问题。这时在 事务A 中读取的数据是不受 其他事务 影响的。

4. 演示 - 幻读

mysql 复制代码
# 自己想办法吧!

... 通过锁(独占锁)来解决 幻读 的问题。后续章节会讲到!

4. 事务的常见分类

从事务理论的角度来看,可以把事务分为以下几种类型:

  • 扁平事务
  • 带有保存点的扁平事务
  • 链事务
  • 嵌套事务
  • 分布式事务

下面分别介绍这几种类型:

  • 扁平事务

扁平事务 是事务类型中最简单的一种,也是使用最频繁的事务,在扁平事务中,所有操作都处于同一层次,由 start transaction 或者 begin 来开启,commit 或者 rollback 结束,其间的操作是原子的,要么都执行,要么都回滚。因此,扁平事务是应用程序成为原子操作的基本组成模块。

扁平事务的三种结果:

  1. 事务成功完成。
  2. 应用程序要求停止事务。比如应用程序在捕获到异常时会回滚事务。
  3. 外界因素强制终止事务。比如连接超时或连接断开。
  • 带有保存点的扁平事务

带有保存点的扁平事务 除了支持扁平事务支持的操作外,还允许在事务执行过程中回滚到同一事务中较早的一个状态,这是因为某些事务可能在执行过程中出现了错误并不会导致所有的操作都无效,放弃整个事务不合乎要求,开销太大。

保存点(savepoint)用来通知事务系统应该记住事务当前的状态,以方便发送错误时,事务能回到保存点当时的状态。对于扁平的事务来说,隐式的设置了一个保存点,然而在整个事务中,只有这一个保存点,因此,回滚只能回滚到事务开始的状态。

  • 链事务

链事务 是指一个事务由多个子事务链式组成,它可以被视为保存点模式的一个变种。

带有保存点的扁平事务,当发生系统崩溃时,所有的保存点都将消失,这意味着当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行。

链事务的思想是:在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务,前一个子事务的提交操作和下一个子事务的开始操作合并成一个原子操作,这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行一样。这样,在提交子事务就可以释放不需要的数据对象,而不必等到整个事务完成后才释放。

链事务与带有保存点的扁平事务的不同之处在于:

  • 带有保存点的扁平事务能回滚到任意正确的保存点,而链事务中的回滚仅限于当前事务,即只能恢复到最近的一个保存点。
  • 对于锁的处理,两者也不相同,链事务在执行 commit 后即释放了当前所持有的锁,而带有保存点的扁平事务不影响迄今为止所持有的锁。
  • 嵌套事务

嵌套事务 是一个层次结构框架,由一个顶层事务控制着各个层次的事务,顶层事务之下嵌套的事务称为子事务,其控制着每一个局部的变换,子事务本身也可以是嵌套事务。因此,嵌套事务的层次结构可以看成是一棵树

  • 分布式事务

分布式事务 通常是在一个分布式环境下运行的扁平事务,因此,需要根据数据所在位置访问网络中不同节点的数据库资源。

例如:

一个银行用户从招商银行的账户向工商银行的账户转账 1000 元,这里需要用到分布式事务,因为不能仅调用某一家银行的数据库就完成任何。