文章目录
我们缺乏的不是知识,而是学而不厌的态度
1、数据库事务概述
事务是数据库区别于文件系统的重要特性之一,当我们有了事务就会让数据库始终保持一致性, 同时我们还能通过事务的机制恢复到某个时间点, 这样可以保证已提交到数据库的修改不会因为系统崩溃而丢失 。
1.1、存储引擎支持情况
SHOW ENGINES命令来查看当前 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 | NULL | NULL | NULL |
| 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 |
| ndbinfo | NO | MySQL Cluster system information storage engine | NULL | NULL | NULL |
| BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO |
| ARCHIVE | YES | Archive storage engine | NO | NO | NO |
| ndbcluster | NO | Clustered, fault-tolerant tables | NULL | NULL | NULL |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
11 rows in set (0.00 sec)
能看出在 MySQL 中,只有 lnnoDB 是支持事务的。
1.2、基本概念
事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态。
事务处理的原则: 保证所有事务都作为一个工作单元来执行,即使出现了故障,都不能改变这种执行方式。当在一个事务中执行多个操作时,要么所有的事务都被提交(commit),那么这些修改就永久地保存下来。要么数据库管理系统将放弃所作的所有修改, 整个事务回滚(rollback)到最初状态 。
# 案例: AA 用户给 BB 用户转账 100
update account set money = money - 100 where name = 'AA';
# 服务器宕机
update account set money = money + 100 where name = 'BB';
1.3、事务的ACID特性
1.3.1、原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,要么全部提交,要么全部失败回滚。
即要么转账成功,要么转账失败,是不存在中间的状态。
如果无法保证原子性会怎么样?就会出现数据不一致的情形,A 账户减去100元,而B账户增加100元操作失败,系统将无故丢失100元。
1.3.2、一致性(Consistency)
(国内很多网站上对一致性的阐述有误,具体你可以参考 Wikipedia 对 Consistency 的阐述)
根据定义,一致性是指事务执行前后,数据从一个合法性状态变换到另外一个合法性状态。
这种状态是语义上的而不是语法上的,跟具体的业务有关。
那什么是合法的数据状态呢?满足预定的约束的状态就叫做合法的状态。
通俗一点,这状态是由你自己来定义的(比如满足现实世界中的约束)。
满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的!如果事务中的某个操作失败了,系统就会自动撤销当前正在执行的事务,返回到事务操作之前的状态。
举例1:A 账户有 200 元,转账 300 元出去,此时 A 账户余额为 100 元 你自然就发现了此时数据是不一致的,为什么呢 ? 因为你定义了一个状态,余额这列它 > = 0。
举例2:A 账户 200 元,转账 50 元给 B 账户,A 账户的钱扣了,但是 B 账户因为各种意外,余额并没有增加。你也知道此时数据是不一致的,为什么呢?因为你定义了一个状态,要求 A + B 的总余额必须不变。
举例3:在数据表中我们将姓名字段设置为唯一性约束,这时当事务进行提交或者事务发生回滚的时候,如果数据表中的姓名不唯一,就破坏了事务的一致性要求。
1.3.3、隔离性(Isolation)
事务的隔离性是指一个事务的执行不能被其他事务干扰, 即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事务之间不能互相干扰。
如果无法保证隔离性会怎么样?假设 A 账户有 200 元,B 账户 0 元。
A 账户往 B 账户转账两次,每次金额为 50 元,分别在两个事务中执行。
如果无法保证隔离性,会出现下面清形:
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、事务的状态
我们现在知道事务是一个抽象的概念,它其实对应着一个或多个数据库操作,MySQL根据这些操作所执行的不同阶段把事务大致划分成几个状态
1.4.1、活动的(active)
事务对应的数据库操作正在执行过程中时 , 我们就说该事务处在活动的状态 。
1.4.2、部分提交的(partially committed)
当事务中的最后一个操作执行完成,但由于操作都在内存中执行,所造成的影响并没有刷新到磁盘时,我们就说该事务处在部分提交的状态。
1.4.3、失败的(failed)
当事务处在活动的或者部分提交的状态时,可能遇到了某些错误(数据库自身的错误、操作系统错误或者直接断电等 )而无法继续执行,或者人为的停止当前事务的执行,我们就说该事务处在失败的状态。
1.4.4、中止的(aborted)
如果事务执行了一部分而变为失败的状态,那么就需要把已经修改的事务中的操作还原到事务执行前的状态。
换句话说,就是要撤销失败事务对当前数据库造成的影响。
我们把这个撤销的过程称之为回滚。
当回滚操作执行完毕时,也就是数据库恢复到了执行事务之前的状态,我们就说该事务处在了中止的状态 。
UPDATE accounts SET money = money - 50 WHERE NAME = 'AA';
UPDATE accounts SET money = money + 50 WHERE NAME = 'BB';
1.4.5、提交的(committed)
当一个处在部分提交的状态的事务将修改过的数据都同步到磁盘上之后,我们就可以说该事务处在了提交的状态。
1.4.6、状态转换图
一个基本的状态转换图如下所示 :
图中可见,只有当事务处于提交的或者中止的状态时,一个事务的生命周期才算是结束了。
对于已经提交的事务来说,该事务对数据库所做的修改将永久生效,对于处于中止状态的事务,该事务对数据库所做的所有修改都会被回滚到没执行该事务之前的状态。
2、如何使用事务
使用事务有两种方式,分别为显式事务和隐式事务。
2.1、显示事务
2.1.1、步骤1
START TRANSACTION 或者 BEGIN,作用是显式开启一个事务。
mysql>START TRANSACTION;
# 或者
mysql>BEGIN;
START TRANSACTION语句相较于BEGIN特别之处在于, 后边能跟随几个修饰符:read only / read write / with consistent snapshot
1 READ ONLY
标识当前事务是一个只读事务, 也就是属于该事务的数据库操作只能读取数据,而不能修改数据。
2 补充
只读事务中只是不允许修改那些其它事务也能访问到的表中的数据 ,对于临时表来说( 我们使用CREATE TMEPORARY TABLE 创建的表),由于它们只能在当前会话中可见,所以只读事务其实也是可以对临时表进行增、删、改操作的。
3 READ WRITE
标识当前事务是一个读写事务,也就是属于该事务的数据库操作既可以读取数据,也可以修改数据。
如果没有明确指定修饰符,(默认)是这个。
4 WITH CONSISTENT SNAPSHOT
开启一致性读。
比如:
START TRANSACTION READ ONLY; # 开启一个只读事务
START TRANSACTION READ ONLY, WITH CONSISTENT SNAPSHOT; # 开启只读和一致性读
START TRANSACTION READ WRITE, WITH CONSISTENT SNAPSHOT; # 开启读写事务和一致性读
5 注意
READ ONLY 和 READ WRITE 是用来设置所谓的事务访问模式的,就是以只读还是读写的方式来访问数据库中的数据,一个事务的访问模式不能同时既设置为只读的也设置为读写的,所以不能同时把 READ ONLY 和 READ WRITE 放到 START TRANSACTION 语句后边。
如果我们不显式指定事务的访问模式,那么该事务的访问模式就是读写模式。
2.1.2、步骤2
一系列事务中的操作(主要是 DML, 不含 DDL)。
2.1.3、步骤3
提交事务或中止事务( 即回滚事务 )
# 提交事务。当提交事务后,对数据库的修改是永久性的。
mysql> COMMIT;
# 回滚事务。即撤销正在进行的所有没有提交的修改。
mysql> ROLLBACK;
# 将事务回滚到某个保存点。
mysql> ROLLBACK TO [SAVEPOINT]
其中关于 SAVEPOINT 相关操作有:
# 在事务中创建保存点,方便后续针对保存点进行回滚。一个事务中可以存在多个保存。
SAVEPOINT 保存点名称;
# 删除某个保存点。
RELEASE SAVEPOINT 保存点名称;
2.2、隐式事务
MySQL 中有一个系统变量 autocommit
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set, 1 warning (0.01 sec)
默认情况下,如果我们不显式的使用START TRANSACTION或者BEGIN语句开启一个事务,那么每一条语句都算是一个独立的事务,这种特性称之为事务的自动提交。
也就是说,不以START TRANSACTION或者BEGIN语句显
式的开启一个事务,那么下边这两条语句就相当于放到两个独立的事务中去执行:
UPDATE account SET balance - 10 WHERE id = 1;
UPDATE account SET balance + 10 WHERE id = 2;
当然,如果我们想关闭这种自动提交的功能,可以使用下边两种方法之一:
- 显式的的使用START TRANSACTION或者BEGIN语句开启一个事务。这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能。
- 把系统变量autocomit的值设置为 OFF, 就像这样 :
SET autocommit = OFF;
# 或
SET autocommit;
这样的话,我们写入的多条语句就算是属于同一个事务了,直到我们显式的写出 COMMIT 语句来把这个事务提交掉,或者显式的写出 ROLLBACK 语句来把这个事务回滚掉。
补充 :oracle 默认不自动提交,需要手写 COMMIT 命令,而 MySQL 默认自动提交。
2.3、隐式提交数据的情况
2.3.1、数据定义语言( Data defin ition language 缩写:DDL)
数据库对象,指的就是数据库、表、视图、存储过程等结构.。
当我们使用 CREATE、ALTER、DROP 等语句去修改数据库对象时, 就会隐式的提交前边语句所属于的事务。
即 :
BEGIN;
SELECT ... # 事务中的一条语句
UPDATE ... # 事务中的一条诰句
... # 事务中的其它语句
CREATE TABLE ... # 此语句会隐式的提交前边语句所属于的事务
2.3.2、隐式使用或修改mysql数据库中的表
当我们使用ALTER USER、CREATE USER、DROP USER、GRANT、RENAME USER、REVOKE、 SET PASSWORD等语句时也会隐式的提交前边语句所属于的事务。|
2.3.3、事务控制或关于锁定的语句
1 当我们在一个事务还没提交或者回滚时就又使用START TRANSACTION或者BEGIN语句开启了另一个事务时,会隐式的提交上一个事务。
即 :
BEGIN;
SELECT ... # 事务中的一条语句
UPDATE ... # 事务中的一条句
... # 事务中的其它语句
BEGIN; # 此语句会隐式的提交前边语句所属于的事务
2 当前的autocommit系统变量的值为OFF,我们手动把它调为ON时,也会隐式的提交前边语句所属的事务。
3 使用 LOCK TABLES、UNLOCK TABLES 等关于锁定的语句也会隐式的提交前边语句所属的事务。
2.3.4、加载数据语句
使用LOAD DATA语句来批量往数据库中导入数据时,也会隐式的提交前边语句所属的事务。
2.3.5、关于MySQL复制的一些语句
使用START SLAVE、STOP SLAVE、RESET SLAVE、CHANGE MASTER TO等语句时会隐式的提交前边语句所属的事务。
2.3.6、其它的一些语句
使用ANALYZE TABLE、CACHE INDEX、CHECK TABLE、FLUSH、LOAD INDEX INTO CACHE、OPTIMIZE TABLE、REPAIR TABLE、RESET等语句也会隐式的提交前边语句所属的事务。
2.4、举例1:提交与回滚
我们看下在 MySQL 的默认状态下 , 下面这个事务最后的处理结果是什么。
2.4.1、情况1
CREATE TABLE user(
name varchar(20),
PRIMARY KEY (name)
) ENGINE=InnoDB;
BEGIN; # 开启一个新事物
INSERT INTO user VALUES('张三'); # 此时不会自动提交数据
COMMIT;
SELECT * FROM user;
BEGIN; # 开启一个新事物
INSERT INTO user VALUES('李四'); # 此时不会自动提交事务
INSERT INTO user SELECT('李四'); # 受主键影响,不能添加成功
ROLLBACK;
SELECT * FROM user;
运行结果(1行数据)
mysql> select * from user;
+------+
| name |
+------+
| 张三 |
+------+
1 row in set (0.00 sec)
2.4.2、情况2
TRUNCATE TABLE user; # DDL 操作会自动提交数据,不受autocommit变量的影响。
SELECT * FROM user;
BEGIN; # 开启一个新事物
INSERT INTO user VALUES('张三'); # 此时不会自动提交数据
COMMIT;
SELECT * FROM user;
INSERT INTO user VALUES('李四'); # 默认情况下(即autocommit为true),DML操作也会自动提交数据 。
INSERT INTO user SELECT('李四'); # 受主键影响,不能添加成功
ROLLBACK;
SELECT * FROM user;
运行结果(2行数据)
mysql> select * from user;
+------+
| name |
+------+
| 张三 |
| 李四 |
+------+
2 rows in set (0.01 sec)
2.4.3、情况3
TRUNCATE TABLE user;
SELECT * FROM user;
SELECT @@completion_type;
SET @@completion_type = 1;
BEGIN;
INSERT INTO user VALUES('张三');
COMMIT;
SELECT * FROM user;
INSERT INTO user VALUES('李四');
INSERT INTO user VALUES('李四');
ROLLBACK;
SELECT * FROM user;
运行结果(1行数据)
mysql> select * from user;
+------+
| name |
+------+
| 张三 |
+------+
1 row in set (0.00 sec)
你能看到相同的 SQL 代码,只是在事务开始之前设置了 SET @@completion_type = 1; ,结果就和我们第一次处理的一样, 只有一个"张三"。这是为什么呢?
这里我讲解下 MySQL 中 completion_type 参数的作用,实际上这个参数有 3 种可能:
- completion_type = 0,这是默认情况。当我们执行COMMIT的时候会提交事务,在执行下一个事务时,还需要使用 START TRANSACTION 或者 BEGIN 来开启。
- completion_type = 1,这种情况下,当我们提交事务后,相当于执行了 COMMIT AND CHAIN , 也就是开启一个链式事务,即当我们提交事务之后会开启一个相同隔离级别的事务。
- completion_type = 2,这种情况下COMMIT = COMMIT AND RELEASE ,也就是当我们提交后,会自动与服务器断连接。
2.4.4、总结
当我们设置 completion_type = 1 时,不论是否采用 START TRANSACTION 或者 BEGIN 的方式来开启事务,都需要用COMMIT进行提交,让事务生效,使用 ROLLBACK 对事务进行回滚。
当我们设置 completion_type = 0 时,每条 SQL 语句都会自动进行提交。
不过这时,如果你采用 START TRANSACTION 或者 BEGIN 的方式来显式地开启事务, 那么这个事务只有在
COMMIT 时才会生效,在 ROLLBACK 时才会回滚。
2.5、举例2:测试不支持事务的engine
2.5.1、InnoDB
InnoDB引擎支持事务
CREATE TABLE test1(
i INT PRIMARY KEY
)ENGINE = INNODB;
CREATE TABLE test2(
i INT PRIMARY KEY
)ENGINE = MYISAM;
# 针对innodb表
BEGIN;
INSERT INTO test1 VALUES (1);
ROLLBACK;
SELECT * FROM test1;
执行结果:无数据
mysql> select * from test1;
Empty set (0.00 sec)
2.5.2、MyISAM
MyISAM引擎不支持事务
BEGIN;
INSERT INTO test2 VALUES (1);
ROLLBACK;
SELECT * FROM test2;
查询结果:1条数据
mysql> select * from test2;
+---+
| i |
+---+
| 1 |
+---+
1 row in set (0.00 sec)
2.6、举例3:SAVEPOINT
创建表并添加数据 :
CREATE TABLE account(
id INT PRIMARY KEY AUTO_INCREMENT,
`NAME` VARCHAR(15),
balance DECIMAL(10,2)
);
INSERT INTO account(NAME,balance) VALUES
('张三',1000),
('李四',1000);
mysql> select * from account;
+----+------+---------+
| id | NAME | balance |
+----+------+---------+
| 1 | 张三 | 1000.00 |
| 2 | 李四 | 1000.00 |
+----+------+---------+
2 rows in set (0.00 sec)
用户张三减100 元,在减100元 在语句之后设置一个保存点,张三加1元,回滚到保存点,回滚到最初,查询结果:
BEGIN;
UPDATE account SET balance = balance - 100 WHERE NAME = '张三';
UPDATE account SET balance = balance - 100 WHERE NAME = '张三';
SAVEPOINT s1; # 设置一个保存点
UPDATE account SET balance = balance + 1 WHERE NAME = '张三';
ROLLBACK TO s1; # 回滚到保存点,但事务还未进入终止状态
SELECT * FROM account;
ROLLBACK; # 回滚操作,事务终止状态
SELECT * FROM account;
查询结果,数据未发生改变
mysql> select * from account;
+----+------+---------+
| id | NAME | balance |
+----+------+---------+
| 1 | 张三 | 1000.00 |
| 2 | 李四 | 1000.00 |
+----+------+---------+
2 rows in set (0.00 sec)
3、事务隔离级别
MySQL 是一个客户端/服务器架构的软件,对于同一个服务器来说,可以有若干个客户端与之连接,每个客户端与服务器连接上之后,就可以称为一个会话(Session)。
每个客户端都可以在自己的会话中向服务器发出请求语句,一个请求语句可能是某个事务的一部分,也就是对于服务器来说可能同时处理多个事务。
事务有隔离性的特性,理论上在某个事务对某个数据进行访问时,其它事务应该进行排队,当该事务提交之后 ,其它事务才可以继续访问这个数据。
但是这样对性能影响太大,我们既想保持事务的隔离性,又想让务器在外理访问同一数据的多个事务时性能尽量高些,那就看二者如何权衡取舍了。
3.1、数据准备
我们需要创建一个表:
CREATE TABLE student (
studentno INT,
name VARCHAR(20),
class VARCHAR(20),
PRIMARY KEY (studentno)
) ENGINE = INNODB;
然后向这个表里插入一条数据:
INSERT INTO student VALUES(1,'小谷','1班');
现在表里的数据就是这样的:
mysql> select * from student;
+-----------+------+-------+
| studentno | name | class |
+-----------+------+-------+
| 1 | 小谷 | 1班 |
+-----------+------+-------+
1 row in set (0.00 sec)
3.2、数据并发问题
针对事务的隔离性和并发性,我们怎么做取舍呢?先看一下访问相同数据的事务在不保证串行执行( 也就是执行完一个再执行另一个 )的情况下可能会出现哪些问题:
3.2.1、脏写(Dirty write)
对于两个事务 Session A 、 Session B,如果事务 Session A 修改了另一个未提交事务 Session B 修改过的数据 , 那就意味着发生了脏写 , 示意图如下:
Session A 和 Session B 各开启了一个事务,Session B 中的事务先将 studentno列为 1 的记录的 name 列更新为 ‘李四’ 然后 Session A 中的事务接着又把这条 studentno 列为 1 的记录的 name 列更新为 ’ 张三’ 。
如果之后 Session B 中的事务进行了回滚,那么 Session A 中的更新也将不复存在,这种现象就称之为脏写。
这时 Session A 中的事务就没有效果了,明明把数据更新了,最后也提交事务了,最后看到的数据什么变化也没有。
这里大家对事务的隔离级比较了解的话,会发现默认隔离级别下,上面 Session A 中的更新语句会处于等待状态 , 这里只是跟大家说明一下会出现这样现象。
3.2.2、脏读(Drity Read)
对于两个事务 Session A、Session B,Session A 读取了已经被 Session B 更新 但还没有被提交的字段。
之后若Session B 回滚,Session A 读取的内容就是临时且无效的。
Session A 和 Session B 各开启了一个事务,Session B 中的事务先将 studentno 列为 1 的记录的 name 列更新为 ’ 张三’ 然后 session A 中的事务再去杳询这条 studentno 为 1 的记录,如果读到列 name 的值为 ’ 张三 ’ ,而 session B 中的事务稍后进行了回滚,那么Seession A 中的事务相当于读到了一个不存在的数据,这种现象就称之为 脏读 。
3.2.3、不可重复读(Non-repeatable read)
对于两个事务 Seession A、Seession B,Seession A 读取 了一个字段,然后 Seession B 更新了该字段。
之后 Seession A 再次读取同一个字段,值就不同了。
那就意味着发生了不可重复读。
我们在 Session B 中提交了几个隐式事务(注意是隐式事务,意味着语句结束事务就提交了),这些事务都修改了 studentno 列为 1 的记录的列 name 的值,每次事务提交之后,如果 Session A 中的事务都可以查看到最新的值,这种现象也被称之为不可重复读。
3.2.4、幻读(Phantom Read)
对于两个事务 Session A、Session B,Session A 从一个表中读取了一个字段,然后 Session B 在该表中插入了一些新的行。
之后如果 Session A 再次读取同一个表,就会多出几行。
那就意味着发生了幻读。
Session A 中的事务先根据条件 studentno > 0 这个条件查询表 student,得到了叫 name 列值为 ’ 张三’ 的记录。
之后 Session B 中提交了一个隐式事务,该事务向表 student 中插入了一条新记录。
之后 Session A 中的事务再根据相同的条件 studentno > 0 查询表 student,得到的结果集中包含 Session B 中的事务新插入的那条记录,这种现象也被称之为 幻读。
我们把新插入的那些记录称之为幻影记录。
3.2.5、注意 1
有的同学会有疑问,那如果 Session B 中删除了一些符合 studentno > 0 的记录而不是插入新记录 , 那 Session A 之后再根据studentno > 0 的条件读取的记录变少了,这种现象算不算幻读呢?这种现象不属于幻读,幻读强调的是一个事务按照某个相同条件多次读取记录时,后读取时读到了之前没有读到的记录。
3.2.6、注意 2
那对于先前已经读到的记录,之后又读取不到这种情况,算啥呢?这相当于对每一条记录都发生了不可重复读的现象。
幻读只是重点强调了读取到了之前读取没有获取到的记录。
3.3、SQL中的四种隔离级别
上面介绍了几种并发事务执行过程中可能遇一些问题,这些问题有轻重缓急之分,我们给这些问题按照严重性来排一下序:
脏写 > 脏读 > 不可重复读 > 幻读
我们愿意舍弃一部分隔离性来换取一部分性能在这里就体现在:设立一些隔离级别,隔离级别越低,并发问题发生的就越多。
SQL 标准中设立了 4 个隔离级别 :
3.3.1、Read Uncommitted(读未提交)
在该隔离级别,所有事务都可以看到其它未提交事务的执行结果。
不能免脏读、不可重复读、幻读。
3.3.2、Read Committed(读已提交)
它满足了隔离的简单定义: 一个事务只能看见已经提交事务所做的改变。
这是大多数数据库系统的默认隔离级别( 但不是 MySQL 默认的 )。
可以避免脏读,但不可重复读、幻读问题仍然存在。
3.3.3、repeatable read(可重复读)
事务 A 在读到一条数据之后,此时事务 B 对该数据进行了修改并提交,那么事务 A 再读该数据,读到的还是原来的内容。
可以避免脏读、不可重复读,但幻读问题仍然存在。
这是 MySQL 的默认隔离级别。
3.3.4、serializable(串行化)
确保事务可以从一个表中读取相同的行。
在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。
所有的并发问题都可以避免,但性能十分低下。
能避免脏读、不可重复和幻读。
3.3.5、SQL标准
SQL 标准中规定,针对不同的隔离级别,并发事务可以发生不同严重程度的问题,具体情况如下:
脏写怎么没涉及到?因为脏写这个问题太严重了,不论是哪种隔离级别,都不允许脏写的情况发生。
不同的隔离级别有不同的现象,并有不同的锁和并发机制,隔离级别越高,数据库的并发性能就越差,4 种事务隔离级别与并发性能关系如下 :
3.4、MySQL支持的四种隔离级别
不同的数据库厂商对 SQL 标准中规定的四种隔离级别支持不一样。
比如,Oracle 就只支持 READ COMMITTED ( 默认隔离级别 ) 和 SERIALIZABLE 隔离级别 。
MySQL虽然支持4种隔离级别,但与 SQL 标准中所规定的各级隔离级别允许发生的问题却有些出入,MySQL 在 REPEATABLE READ 隔离级别下,是可以禁止幻读问题的发生的,禁止幻读的原因我们在第 16 章讲解。
MySQL 的默认隔离级别为 REPEATABLE READ,我们可以手动修改一下事务的隔离级别。
# 查看隔离级别,MySQL 5.7.20 的版本之前:
mysql> SHOW VARIABLES LIKE 'tx_isolation';
+----------------+-----------------+
| Variable_name | Value |
+----------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+----------------+-----------------+
1 row in set, 1 warning (0.00 sec)
# MySQL 5.7.20 版本之后,引入transaction_isolation替换tx-isolation
# 查看隔离级别,MySQL 5.7.29 的版本及之后:
mysql> SHOW VARIABLES LIKE 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set, 1 warning (0.00 sec)
# 或者不同 MySQL 版本中都可以使用的:
mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
1 row in set (0.00 sec)
3.5、如何设置事务的隔离级别
3.5.1、通过下面的语句修改事务的隔离级别
SET [GLOBAL|SESSION| TRANSACTION ISOLATION LEVEL 隔离级别;
# 其中,隔离级别格式:
> READ UNCOMMITTED
> READ COMMITTED
> REPEATABLE READ
> SERIALIZABLE
或者:
SET [GLOBAL | SESSION ] TRANSACTION_ISOLATION = '隔离级别'
# 其中,隔离级别格式:
> READ-UNCOMMITTED
> READ-COMMITTED
> REPEATABLE-READ
> SERIALIZABLE
3.5.2、关于设置时使用 GLOBAL 或 SESSION 的影响
使用 GLOBAL 关键字(在全局范围影响):
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
# 或
SET GLOBAL TRANSACTION_ISOLATION = 'SERIALIZABLE';
当前已经存在的会话无效。
只对执行完该语句之后产生的会话起作用。
使用 SESSION 关键字(在会话范围影响)
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
# 或
SET SESSION TRANSACTION_ISOLATION = 'SERIALIZABLE';
对当前会话的所有后续的事务有效。
如果在事务之间执行,则对后续的事务有效。
该语句可以在已经开启的事务中间执行,但不会影响当前正在执行的事务。
3.5.3、注意
如果在服务器启动时想改变事务的默认隔离级别,可以修改启动参数 transaction-isolation 的值。
比如,修改配置文件,在启动服务器时指定了 transaction-isolation=SERIALIZABLE,那么事务的默认隔离级别就从原来的 REPEATABLE-READ变成了 SERIALIZABLE。
3.6、不同隔离级别举例
表结构
CREATE TABLE account(
id INT PRIMARY KEY AUTO_INCREMENT,
`NAME` VARCHAR(15),
balance DECIMAL(10,2)
);
初始化数据
TRUNCATE TABLE account;
INSERT INTO account(id, NAME, balance) VALUES
(1, '张三', 100),
(2, '李四', 0);
3.6.1、脏读
事务1更改隔离级别:读未提交
mysql> set session transaction_isolation = 'read-uncommitted';
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED |
+-------------------------+
1 row in set (0.00 sec)
事务2更改隔离级别:读未提交
mysql> set session transaction_isolation = 'read-uncommitted';
Query OK, 0 rows affected (0.00 sec)
mysql> select @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| READ-UNCOMMITTED |
+-------------------------+
1 row in set (0.00 sec)
事务1开启事务,给张三加100元,但为提交事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set balance = balance + 100 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
事务2查询,可以发现张三添加了100元
mysql> select * from account;
+----+------+---------+
| id | NAME | balance |
+----+------+---------+
| 1 | 张三 | 200.00 |
| 2 | 李四 | 0.00 |
+----+------+---------+
2 rows in set (0.00 sec)
此时事务1回滚事务
mysql> rollback;
Query OK, 0 rows affected (0.01 sec)
事务2查询数据发生变化
mysql> select * from account;
+----+------+---------+
| id | NAME | balance |
+----+------+---------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 0.00 |
+----+------+---------+
2 rows in set (0.00 sec)
3.6.2、不可重复读
恢复表数据
TRUNCATE TABLE account;
INSERT INTO account(id, NAME, balance) VALUES
(1, '张三', 100),
(2, '李四', 0);
设置事务1和事务2的隔离级别:读已提交
mysql> set session transaction_isolation = 'read-committed';
Query OK, 0 rows affected (0.00 sec)
事务2开启事务,查询,事务未结束
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+------+---------+
| id | NAME | balance |
+----+------+---------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 0.00 |
+----+------+---------+
2 rows in set (0.00 sec)
事务1中开启事务,将张三减50,事务未结束
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set balance = balance - 50 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
事务2中查询,没有脏读发生,事务未结束
mysql> select * from account;
+----+------+---------+
| id | NAME | balance |
+----+------+---------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 0.00 |
+----+------+---------+
2 rows in set (0.00 sec)
事务1中提交事务,事务结束,查询写入成功
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from account;
+----+------+---------+
| id | NAME | balance |
+----+------+---------+
| 1 | 张三 | 50.00 |
| 2 | 李四 | 0.00 |
+----+------+---------+
2 rows in set (0.00 sec)
事务2中查询,事务未结束,在当前事务中出现了查询不一致的情况
mysql> select * from account;
+----+------+---------+
| id | NAME | balance |
+----+------+---------+
| 1 | 张三 | 50.00 |
| 2 | 李四 | 0.00 |
+----+------+---------+
2 rows in set (0.00 sec)
事务2结束事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
3.6.3、可重复读
恢复表数据
TRUNCATE TABLE account;
INSERT INTO account(id, NAME, balance) VALUES
(1, '张三', 100),
(2, '李四', 0);
设置隔离级别:可重复读
mysql> set session transaction_isolation = 'repeatable-read';
Query OK, 0 rows affected (0.00 sec)
事务2开启事务,查询,事务未结束
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+------+---------+
| id | NAME | balance |
+----+------+---------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 0.00 |
+----+------+---------+
2 rows in set (0.00 sec)
事务1中开启事务,将张三减50,事务未结束
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set balance = balance - 50 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
事务2中查询,没有脏读发生,事务未结束
mysql> select * from account;
+----+------+---------+
| id | NAME | balance |
+----+------+---------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 0.00 |
+----+------+---------+
2 rows in set (0.00 sec)
事务1中提交事务,事务结束,查询写入成功
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from account;
+----+------+---------+
| id | NAME | balance |
+----+------+---------+
| 1 | 张三 | 50.00 |
| 2 | 李四 | 0.00 |
+----+------+---------+
2 rows in set (0.00 sec)
事务2中查询,事务未结束,在当前事务中查询一致
mysql> select * from account;
+----+------+---------+
| id | NAME | balance |
+----+------+---------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 0.00 |
+----+------+---------+
2 rows in set (0.00 sec)
事务2结束事务,在查询,数据发生改变
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+------+---------+
| id | NAME | balance |
+----+------+---------+
| 1 | 张三 | 50.00 |
| 2 | 李四 | 0.00 |
+----+------+---------+
2 rows in set (0.00 sec)
在事务期间,两次的查询结果都相同,解决了不可重复读的问题。
3.6.4、幻读
恢复表数据
TRUNCATE TABLE account;
INSERT INTO account(id, NAME, balance) VALUES
(1, '张三', 100),
(2, '李四', 0);
设置隔离级别:可重复读
mysql> set session transaction_isolation = 'repeatable-read';
Query OK, 0 rows affected (0.00 sec)
在事务2中开启事务,查看id为3的个数,事务未结束
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select count(*) from account where id = 3;
+----------+
| count(*) |
+----------+
| 0 |
+----------+
1 row in set (0.00 sec)
在事务1中开启事务,插入一条数据,提交事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account(id,name,balance) values(3,'王五',100);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
在事务2中查询事务,发现数据一致,但是无法插入id为3的数据,此时未提交事务
mysql> select count(*) from account where id = 3;
+----------+
| count(*) |
+----------+
| 0 |
+----------+
1 row in set (0.00 sec)
mysql> insert into account(id,name,balance) values(3,'王五',1000);
ERROR 1062 (23000): Duplicate entry '3' for key 'account.PRIMARY'
在MySQL中,无法显示的查询出幻读,但是向数据库添加数据,会包主键唯一约束错误,证明主键重复,数据已经存在。
终止事务2
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)
这里要灵活的理解读取的意思,第一次 select 是读取,第二次的 insert 其实也属于隐式的读取,只不过是在 mysql的机制中读取的,插入数据也是要先读取一下有没有主键冲突才能决定是否执行插入。
幻读,并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。
更为具体一些 : select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。
在 RR 隔离级别下,事务1插入、 事务2的查询 是会正常执行的, 事务2的插入 则会报错主键冲突, 对于事务2 的业务来说是执行失败的 ,这里事务2就是发生了幻读,因为事务2 在 第一次查询 中读取的数据状态并不能支撑后续的业务操作,事务2 : “ 见鬼了 , 我刚才读到的结果应该可以支持我这样操作才对啊,为什么现在不可以”。
事务2 不敢相信的又执行了 查询 ,发现和 第一次查询 读取的结果是一样的(RR 下的 MVCC 机制 。
此时,幻读无疑已经发生,事务2 无论读取多少次,都查不到 id = 3 的记录,但它的确无法插入这条它通过读取来认定不存在的记录(此数据已被事务 1 插入),对于事务2 来说,它幻读了。
其实 RR 也是可以免幻读的,通过对 select 操作手动加行X锁 ( 独占锁 ) (SELECT … FOR UPDATE 这也正是SERIALIZABLE 隔离级别下会隐式为你做的事情 )。
同时,即便当前记录不存在,比如 id = 3 是不存在的,当前事务也会获得一把记录锁(因为 InnoDB的行锁锁定的是索引,故记录实体存在与否没关系,存在就加行X锁,不存在就加间隙锁),其它事务则无去插入此索引的记录,故杜绝了幻读。
在 SERIALIZABLE 隔离级别下 事务2 查询 执行时是会隐式的添加行X锁 / gap(X)锁的,从而 事务1添加 会被阻塞,事务2添加 会正常执行,待事务 2 提交后,事务 1才续执行(主键冲突执行失败),对于事务 2 来说业务是正确的,成功的阻塞扼杀了扰乱业务的事务 1,对于事务 2 来说它前期读取的结果是可以支撑其后续业务的。
所以 MySQL 的幻读并非什么读取两次返回结果集不同,而是事务在适入事先检测不存在的记录时,惊奇的发现这些数据已经存在了,之前的检测读获取到的数据如同鬼影一般。
4、事务的常见分类
从事务理论的角度来看,可以把事务分为以下几种类型:
- 扁平事务( Flat Transactions)
- 带有保存点的扁平事务 (Flat Transactions with Savepoints )
- 链事务(Chained Transactions)
- 嵌套事务 (Nested Transactions)
- 分布式事务(Distributed Transactions)
下面分别介绍这几种类型 :
4.1、扁平事务
扁平事务是事务类型中最简单的一种,但是在实际生产环境中,这可能是使用最频繁的事务,在扁平事务中,所有操作都处于同一层次,其由 BEGIN WORK 开始,由 COMMIT WORK 或 ROLLBACK WORK 结束,其间的操作是原子的,要么都执行,要么都回滚,因此,扁平事务是应用程序成为原子操作的基本组成模块。
扁平事务虽然简单,但是在实际环境中使用最为频繁,也正因为其简单,使用频繁,故每个数据库系统都实现了对扁平事务的支持。
扁平事务的主要限制是不能提交或者回滚事务的某一部分,或分几个步骤提交。
扁平事务一般有三种不同的结果:
1 事务成功完成,在平常应用中约占所有事务的 96 %。
2 应用程序要求停止事务,应用程序在捕获到异常时会回滚事务,约占事务的 3 %。
3 外界因素强制终止事务,如连接超时或连接断开,约占所有事务的 1 % 。
4.2、带有保存点的扁平事务
带有保存点的扁平事务除了支持扁平事务支持的操作外,还允许在事务执行过程中回滚到同一事务中较早的一个状态,这是因为某些事务可能在执行过程中出现的错误并不会导致所有的操作都无效,放弃整个事务不合乎要求 , 开销太大。
保存点 (Savepoint) 用来通知事务系统应该记住事务当前的状态,以便当之后发生错误时,事务能回到保存点当时的状态,对于扁平的事务来说,隐式的设置了一个保存点,然而在整个事务中,只有这一个保存点,因此,回滚只能会滚到事务开始时的状态 。
4.3、链事务
链事务是指一个事务由多个子事务链式组成,它可以被视为保存点模式的一个变种。
带有保存点的扁平事务,当发生系统崩溃时,所有的保存点都将消失,这意味着当进行恢复时,事务需要从开始处重新执行,而不能从最近的一个保存点继续执行。
链事务的思想是:在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务,前一个子事务的提交操作和下一个子事务的开始操作合并成一个原子操作,这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行一样。
这样,在提交子事务时就可以释放不需要的数据对象,而不必等到整个事务完成后才释放。其工作方式如下:
链事务与带有保存点的扁平事务的不同之处体现在:
1 带有保存点的扁平事务能回滚到任意正确的保存点,而链事务中的回滚仅限于当前事务,即只能恢复到最近的一个保存点。
2 对于锁的处理,两者也不相同,链事务在执行 COMMIT 后即释放了当前所持有的锁,而带有保存点的扁平事务不影响迄今为止所持有的锁。
4.4、嵌套事务
嵌套事务是一个层次结构框架,由一个顶层事务 (Top-Level Transaction )控制着各个层次的事务,顶层事务之下嵌套的事务被称为子事务 (Subtransaction) 其控制着每一个局部的变换,子事务本身也可以是嵌套事务。
因此,嵌套事务的层次结构可以看成是一棵树。
4.5、分布式事务
分布式事务通常是在一个分布式环境下运行的扁平事务,因此,需要根据数据所在位置访问网络中不同节点的数据库资源。
例如,一个银行用户从招商银行的账户向工商银行的账户转账 1000 元,这里需要用到分布式事务,因为不能仅调用某一家银行的数据库就完成任务。