目录:
- 前言
-
- CURD不加控制,会有什么问题?
- CURD满足什么属性,能解决上述问题?
- 什么是事务?
- 为什么会出现事务
- 事务的版本支持
- 事务提交方式
- 事务常见操作方式
-
- 正常演示 - 证明事务的开始与回滚
- 非正常演示1 - 证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)
- 非正常演示2 - 证明commit后,客户端崩溃,MySQL数据不会再受影响,已经持久化
- 非正常演示3 - 对比试验:证明begin操作会自动更改提交方式,不会受MySQL是否自动提交影响
- 非正常演示4 - 证明单条 SQL 与事务的关系
- 事务隔离级别
-
- 如何理解隔离性
- 查看与设置隔离性
- 读未提交【Read Uncommitted】
- 读提交【Read Committed】
- 可重复读【Repeatable Read】
- 串行化【serializable】
- 总结
前言
剑指offer:一年又11天 |
---|
CURD不加控制,会有什么问题?
CURD满足什么属性,能解决上述问题?
- 买票的过程要是原子的 -- 要么买了要么没买
- 买票互相不受影响
- 买票的结果要是确定的 -- 买了就有票,没买就没票
- 买票的结果要是持久的 -- 买了票这张票就一直是你的
什么是事务?
事务就是一组DML语句,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,看做一个整体。MySQL提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。
案例:早八起床
现在有一个穿衣服的事务,这个事务有多条穿衣服指令:穿毛衣、穿外套、穿裤子。
开始起床...,穿好了毛衣穿外套,穿好了外套穿... 突然发现裤子找不着了,那么现在该怎么办?
现在有几个选择:1. 找一条新的裤子 2. 不穿裤子直接去上课 3. 脱了衣服继续睡 等等
而在MySQL看来,由于穿衣服这个事务并没有全部执行成功,因此它会自动回到穿衣服之前的状态 - - 也就是把之前穿上的衣服全部脱掉。
所有,一个完整的事务,绝对不是简单的 sql 集合,还需要满足如下四个属性:
- 原子性 :一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被 回滚(Rollback) 到事务开始前的状态,就像这个事务从来没有执行过一样;
- 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作;
- 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交( Readuncommitted )、读提交( read committed )、可重复读( repeatable read )和串行化( Serializable );
- 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
上面四个属性,可以简称为 ACID 。
原子性(A tomicity,或称不可分割性)
一致性(C onsistency)
隔离性(I solation,又称独立性)
持久性(Durability)。
为什么会出现事务
事务被 MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题.可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?因此事务本质上是为了应用层服务的.而不是伴随着数据库系统天生就有的.
(既: 事务并不是MySQL一开始就有的,而是为了方便使用所以后来增加的。)
备注:我们后面把 MySQL 中的一行信息,称为一行记录
事务的版本支持
在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持。
查看数据库引擎
sql
mysql> show engines\G
*************************** 1. row ***************************
Engine: InnoDB -- 引擎名称
Support: DEFAULT -- 默认引擎
Comment: Supports transactions, row-level locking, and foreign keys
Transactions: YES -- 支持事务:是
XA: YES
Savepoints: YES -- 支持事务保存点:是
*************************** 2. row ***************************
Engine: MRG_MYISAM
Support: YES
Comment: Collection of identical MyISAM tables
Transactions: NO
XA: NO
Savepoints: NO
...
...
事务提交方式
- 自动提交
- 手动提交
查看事务提交方式
sql
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
用 SET 来改变 MySQL 的自动提交模式:
sql
mysql> set autocommit = 0; #SET AUTOCOMMIT=0 禁止自动提交
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0.00 sec)
mysql> set autocommit = 1;#SET AUTOCOMMIT=0 开启自动提交
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
事务常见操作方式
sql
-- 为了便于演示,我们将mysql的默认隔离级别设置成读未提交。
-- 具体操作后面会讲,现在以使用为主。
mysql> select @@global.tx_isolation; -- 查看全局隔离等级
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ | -- 可重复读(这是MySQL默认隔离等级)
+-----------------------+
1 row in set, 1 warning (0.00 sec)
mysql> select @@tx_isolation; -- 查看本次会话隔离等级(启动会话时默认与全局一致)
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ | -- 可重复读
+-----------------+
1 row in set, 1 warning (0.00 sec)
mysql> set global transaction isolation level read uncommitted; -- 修改隔离等级为:读未提交
Query OK, 0 rows affected (0.00 sec)
mysql> select @@global.tx_isolation; -- 全局隔离等级
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| READ-UNCOMMITTED | -- 已修改为:读未提交
+-----------------------+
1 row in set, 1 warning (0.00 sec)
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ | -- 仍然是:可重复读
+-----------------+
1 row in set, 1 warning (0.00 sec)
mysql> quit -- 退出
-- 重新连接MySQL
mysql> select @@global.tx_isolation;
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| READ-UNCOMMITTED | -- 已修改为:读未提交
+-----------------------+
1 row in set, 1 warning (0.00 sec)
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED | -- 已修改为:读未提交
+------------------+
1 row in set, 1 warning (0.00 sec)
简单银行用户表
sql
mysql> create table if not exists account(
-> id int primary key,
-> name varchar(50) not null default '',
-> blance decimal(10,2) not null default 0.0
-> )ENGINE=InnoDB DEFAULT CHARSET=UTF8;
Query OK, 0 rows affected (0.03 sec)
正常演示 - 证明事务的开始与回滚
用户A 和 用户B 同时访问数据库,这里是结果图,执行流程看下方代码
sql
-- 用户A
mysql> start transaction;-- 启动一个事务的两种方法之一
Query OK, 0 rows affected (0.00 sec)
mysql> savepoint p1; -- 打一个保存点p1
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values(1, '张三', 4212.1); -- 插入用户张三
Query OK, 1 row affected (0.00 sec)
mysql> savepoint p2; -- 打一个保存点p2
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values(2, '李四', 1242.1); -- 插入用户李四
Query OK, 1 row affected (0.00 sec)
mysql> savepoint p3; -- 打一个保存点p3
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values(3, '王五', 2344.5); -- 插入用户王五
Query OK, 1 row affected (0.00 sec)
-- 回滚到保存点p3 -- p3之后做的事情全部撤销,这里的表现应该是王五的信息没有了
mysql> rollback to p3;
Query OK, 0 rows affected (0.00 sec)
-- 回滚到保存点p1 -- 这里的表现就是张三和李四的数据也都没有了
mysql> rollback to p1;
Query OK, 0 rows affected (0.01 sec)
-- A提交事务(代表事务完成),因为我们全部都回滚了,因此这次事务虽然完成了,但是也相当于什么都没做
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
sql
-- 用户B
-- 启动一个事务的两种方法之二:推荐使用begin,因为简单
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> desc account;
+--------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+---------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | NULL | |
| name | varchar(50) | NO | | | |
| blance | decimal(10,2) | NO | | 0.00 | |
+--------+---------------+------+-----+---------+-------+
3 rows in set (0.00 sec)
-- A插入张三用户后 B进行查看:可以查到A插入的数据(注意,此时A还未提交)
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
+----+--------+---------+
1 row in set (0.00 sec)
-- A插入李四和王五的信息后 B进行查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
| 3 | 王五 | 2344.50 |
+----+--------+---------+
3 rows in set (0.00 sec)
-- A回滚到p3后,B进行查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
+----+--------+---------+
2 rows in set (0.00 sec)
-- A回滚到p1后,B进行查看
mysql> select * from account;
Empty set (0.00 sec)
-- A提交事务后,B进行查看
mysql> select * from account;
Empty set (0.00 sec)
-- B提交事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
补充:不打保存点也可以回滚:rollback;
,直接回滚到事务最开始。
非正常演示1 - 证明未commit,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交)
sql
-- 用户A启动事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values(1, '张三', 4212.1); -- 插入用户张三
Query OK, 1 row affected (0.00 sec)
mysql> insert into account values(2, '李四', 1242.1); -- 插入用户李四
Query OK, 1 row affected (0.00 sec)
-- ctrl + \:终止进程,模拟客户端崩溃
mysql> Aborted
sql
-- 用户B启动事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
-- A插入用户张三 B进行查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
+----+--------+---------+
1 row in set (0.00 sec)
-- A插入用户李四 B进行查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
+----+--------+---------+
2 rows in set (0.00 sec)
-- A客户端崩溃 B进行查看 (account为空,自动回滚到A插入之前的状态)
mysql> select * from account;
Empty set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
非正常演示2 - 证明commit后,客户端崩溃,MySQL数据不会再受影响,已经持久化
sql
-- 用户A启动事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values(1, '张三', 4212.1);
Query OK, 1 row affected (0.00 sec)
mysql> insert into account values(2, '李四', 1242.1);
Query OK, 1 row affected (0.00 sec)
-- 提交事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
-- 客户端崩溃
mysql> Aborted
sql
-- 用户B启动事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
-- A插入用户张三后 B进行查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
+----+--------+---------+
1 row in set (0.00 sec)
-- A插入用户李四后 B进行查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
+----+--------+---------+
2 rows in set (0.00 sec)
-- A提交事务后 B查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
+----+--------+---------+
2 rows in set (0.00 sec)
-- A客户端崩溃 B查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
+----+--------+---------+
2 rows in set (0.00 sec)
-- B提交事务
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
-- B退出客户端
mysql> quit
Bye
-- B重新连接客户端后 查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
+----+--------+---------+
2 rows in set (0.00 sec)
非正常演示3 - 对比试验:证明begin操作会自动更改提交方式,不会受MySQL是否自动提交影响
sql
-- 前面几个测试中,我们的 事务自动提交 一直是开启的,但是为什么如果没手动提交还是会自动回滚
mysql> show variables like "autocommit";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
sql
-- 把 事务自动提交 关闭再测试一下
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like "autocommit";
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0.00 sec)
-- 用户A启动事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values(3, '王五', 5434.5);
Query OK, 1 row affected (0.00 sec)
mysql> insert into account values(4, '赵六', 1231.2);
Query OK, 1 row affected (0.00 sec)
-- 用户A退出mysql
mysql> quit;
Bye
sql
-- 用户B启动事务
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
-- A插入用户王五后 B查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
| 3 | 王五 | 5434.50 |
+----+--------+---------+
3 rows in set (0.00 sec)
-- A插入用户赵六后 B查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
| 3 | 王五 | 5434.50 |
| 4 | 赵六 | 1231.20 |
+----+--------+---------+
4 rows in set (0.00 sec)
-- A退出mysql后 B查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
+----+--------+---------+
2 rows in set (0.00 sec)
我们发现:不管自动提交是否开启,如果事务未提交就退出,事务都会回滚而不是自动提交。
结论?
非正常演示4 - 证明单条 SQL 与事务的关系
实验一:
sql
-- 用户A
-- 自动提交开启
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.00 sec)
mysql> insert into account values(3, '王五', 5434.5);
Query OK, 1 row affected (0.00 sec)
mysql> insert into account values(4, '赵六', 1231.2);
Query OK, 1 row affected (0.01 sec)
mysql> quit
Bye
-- 用户B
-- A插入客户王五后 B查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
| 3 | 王五 | 5434.50 |
+----+--------+---------+
3 rows in set (0.00 sec)
-- A插入客户赵六后 B查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
| 3 | 王五 | 5434.50 |
| 4 | 赵六 | 1231.20 |
+----+--------+---------+
4 rows in set (0.00 sec)
-- A退出后 B查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
| 3 | 王五 | 5434.50 |
| 4 | 赵六 | 1231.20 |
+----+--------+---------+ -- 数据保存了
4 rows in set (0.00 sec)
实验二:
sql
-- 用户A
-- 关闭自动提交
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0.00 sec)
mysql> insert into account values(5, '孙七', 1234.5);
Query OK, 1 row affected (0.00 sec)
mysql> insert into account values(6, '周八', 1234.5);
Query OK, 1 row affected (0.00 sec)
mysql> quit;
Bye
-- 用户B
-- A插入客户孙七后 B查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
| 3 | 王五 | 5434.50 |
| 4 | 赵六 | 1231.20 |
| 5 | 孙七 | 1234.50 |
+----+--------+---------+
5 rows in set (0.00 sec)
-- A插入客户周八后 B查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
| 3 | 王五 | 5434.50 |
| 4 | 赵六 | 1231.20 |
| 5 | 孙七 | 1234.50 |
| 6 | 周八 | 1234.50 |
+----+--------+---------+
6 rows in set (0.00 sec)
-- A退出后 B查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
| 3 | 王五 | 5434.50 |
| 4 | 赵六 | 1231.20 |
+----+--------+---------+ -- 数据未保存
4 rows in set (0.00 sec)
实验三:
sql
-- 用户A
-- 仍然关闭自动提交
mysql> set autocommit=0;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values(5, '孙七', 1234.5);
Query OK, 1 row affected (0.00 sec)
-- A手动提交事务
mysql> commit;
Query OK, 0 rows affected (0.01 sec)
mysql> quit;
Bye
-- 用户B
-- A插入客户孙七后 B查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
| 3 | 王五 | 5434.50 |
| 4 | 赵六 | 1231.20 |
| 5 | 孙七 | 1234.50 |
+----+--------+---------+
5 rows in set (0.00 sec)
-- A提交事务并退出后 B查看
mysql> select * from account;
+----+--------+---------+
| id | name | blance |
+----+--------+---------+
| 1 | 张三 | 4212.10 |
| 2 | 李四 | 1242.10 |
| 3 | 王五 | 5434.50 |
| 4 | 赵六 | 1231.20 |
| 5 | 孙七 | 1234.50 | -- 数据保存成功了
+----+--------+---------+
5 rows in set (0.00 sec)
大家是否理解了单条SQL的意义,以及自动提交的含义?
单条SQL本质上也是一个事务,而自动提交是针对与这样没有在begin内的事务的。
上面我们说事务的提交方式有两种:自动提交和手动提交 当我们使用begin/start
transolation时,提交方式就会变成手动提交,自动提交设置就不起作用了;
当只是单条SQL时,MySQL会将单条SQL封装成一个事务,并且根据自动提交的设置来决定是否提交该事务。
结论:
- 事务在结果上只有执行成功和执行失败两种状态;
- begin和commit之间属于进行中,但是结果只有两种,上面之所以有用户A和用户B两个终端,就是用来模拟两个事务都在进行中时对彼此的影响(多用户并发);
- 事务只有commit之后才能持久化;
- 事务可以手动回滚,如果未commit之前异常退出,mysql也会自动回滚;
- 对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交。(select有特殊情况,因为MySQL 有 MVCC )。
从上面的例子,我们能看到事务本身的原子性(回滚),持久性(commit)
那么隔离性?一致性?
事务操作注意事项:
- 如果没有设置保存点,也可以回滚,只能回滚到事务的开始。 直接使用 rollback(前提是事务还没有提交)
- 如果一个事务被提交了(commit),则不可以回退(rollback)
- 可以选择回退到哪个保存点
- InnoDB 支持事务, MyISAM不支持事务
- 开始事务可以使 start transaction 或者 begin
事务隔离级别
如何理解隔离性
MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行一个事务可能由多条SQL构成,也就意味着,任何一个事务,都有执行前,执行中,执行后的阶段。而所谓的原子性,其实就是让用户层,要么看到执行前,要么看到执行后。执行中出现问题,可以随时回滚。所以单个事务,对用户表现出来的特性,就是原子性。
但,毕竟所有事务都要有个执行过程,那么在多个事务各自执行多个SQL的时候,就还是有可能会出现互相影响的情况。比如:多个事务同时访问同一张表,甚至同一行数据。
就如同你妈妈给你说:你要么别学,要学就学到最好。至于你怎么学,中间有什么困难,你妈妈不关心。那么你的学习,对你妈妈来讲,就是原子的。那么你学习过程中,很容易受别人干扰,此时,就需要将你的学习隔离开,保证你的学习环境是健康的。
数据库中,为了保证事务执行过程中尽量不受干扰 ,就有了一个重要特征:隔离性
数据库中,允许事务受不同程度的干扰 ,就有了一种重要特征:隔离级别
隔离级别
- 读未提交【Read Uncommitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。(实际生产中不可能使用这种隔离级别的),但是相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,我们上面为了做实验方便,用的就是这个隔离性。
- 读提交【Read Committed】 :该隔离级别是大多数数据库的默认的隔离级别(不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次 select, 可能得到不同的结果。
- 可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行。但是会有幻读问题。
- 串行化【Serializable】: 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,。但是可能会导致超时和锁竞争(这种隔离级别太极端,实际生产基本不使用)。
简单理解:
读未提交:其他事务没有提交的执行结果都可以看到;
读提交:可以看到其他事务提交了的执行结果,但是有一个问题:你的这个事务连续读了5次,在你读的期间,其他事务连续提交了好多次,那么你读到的5次结果就不一样,所以会遇到不可重复读的情况;
可重复读:可以看到其他事务提交了的执行结果,而且,在一次事务中,从第一次读开始到这次事务结束,每次读到的数据都是一样的,不受其他事务提交的影响。
串行化:写操作排队,我的事务执行结束之前,不允许其他事务进行写操作。
隔离级别如何实现:隔离,基本都是通过锁实现的,不同的隔离级别,锁的使用是不同的。常见有,表锁,行锁,读锁,写锁,间隙锁(GAP),Next-Key锁(GAP+行锁)等。不过,我们目前现有这个认识就行,先关注上层使用。
查看与设置隔离性
查看:
sql
mysql> select @@global.tx_isolation;-- 查看全局隔离级别
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| READ-UNCOMMITTED |
+-----------------------+
1 row in set, 1 warning (0.00 sec)
mysql> select @@session.tx_isolation;-- 查看当前会话隔离级别
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-UNCOMMITTED |
+------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> select @@tx_isolation;-- 同上
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)
设置:
sql
-- 设置当前会话 or 全局隔离级别语法
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE};
sql
-- 设置全局隔离级别为 可重复读(RR)
mysql> set global transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@global.tx_isolation; -- 查看全局
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ |
+-----------------------+
1 row in set, 1 warning (0.00 sec)
mysql> select @@session.tx_isolation; -- 查看当前会话 ,没有改变
+------------------------+
| @@session.tx_isolation |
+------------------------+
| READ-UNCOMMITTED |
+------------------------+
1 row in set, 1 warning (0.00 sec)
-- 设置会话隔离级别为串行化
mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@session.tx_isolation; -- 查看当前会话
+------------------------+
| @@session.tx_isolation |
+------------------------+
| SERIALIZABLE |
+------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> select @@tx_isolation; -- 查看当前会话
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE |
+----------------+
1 row in set, 1 warning (0.00 sec)
读未提交【Read Uncommitted】
sql
--几乎没有加锁,虽然效率高,但是问题太多,严重不建议采用
--终端A
-- 设置隔离级别为 读未提交
mysql> set global transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
--重启客户端
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set, 1 warning (0.00 sec)
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> begin; --开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> update account set blance=123.0 where id=1; --更新指定行
Query OK, 1 row affected (0.05 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--没有commit哦!!!
--终端B
mysql> begin;
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 123.00 | --读到终端A更新但是未commit的数据[insert,delete同样]
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
--一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读
(dirty read)
读提交【Read Committed】
sql
-- 终端A
mysql> set global transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
--重启客户端
mysql> select * from account; --查看当前数据
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 123.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> begin; --手动开启事务,同步的开始终端B事务
Query OK, 0 rows affected (0.00 sec)
mysql> update account set blance=321.0 where id=1; --更新张三数据
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--切换终端到终端B,查看数据。
mysql> commit; --commit提交!
Query OK, 0 rows affected (0.01 sec)
--切换终端到终端B,再次查看数据。
--终端B
mysql> begin; --手动开启事务,和终端A一前一后
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; --终端A commit之前,查看不到
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 123.00 | --老的值
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
--终端A commit之后,看到了!
--but,此时还在当前事务中,并未commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段
--(依旧还在事务操作中!),读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)!!
--(这个是问题吗??)
mysql> select *from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 | --新的值
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
可重复读【Repeatable Read】
sql
--终端A
mysql> set global transaction isolation level repeatable read; --设置全局隔离级别
RR
Query OK, 0 rows affected (0.01 sec)
--关闭终端重启
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ | --隔离级别RR
+-----------------+
1 row in set, 1 warning (0.00 sec)
mysql> select *from account; --查看当前数据
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> begin; --开启事务,同步的,终端B也开始事务
Query OK, 0 rows affected (0.00 sec)
mysql> update account set blance=4321.0 where id=1; --更新数据
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--切换到终端B,查看另一个事务是否能看到
mysql> commit; --提交事务
--切换终端到终端B,查看数据。
--终端B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; --终端A中事务 commit之前,查看当前表中数据,数据未更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> select * from account; --终端A中事务 commit 之后,查看当前表中数据,数据未更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
--可以看到,在终端B中,事务无论什么时候进行查找,看到的结果都是一致的,这叫做可重复读!
mysql> commit; --结束事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; --再次查看,看到最新的更新数据
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
----------------------------------------------------------------
--如果将上面的终端A中的update操作,改成insert操作,会有什么问题??
--终端A
mysql> select *from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> begin; --开启事务,终端B同步开启
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account (id,name,blance) values(3, '王五', 5432.0);
Query OK, 1 row affected (0.00 sec)
--切换到终端B,查看另一个事务是否能看到
mysql> commit; --提交事务
Query OK, 0 rows affected (0.00 sec)
--切换终端到终端B,查看数据。
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
--终端B
mysql> begin; --开启事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; --终端A commit前 查看
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> select * from account; --终端A commit后 查看
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> select * from account; --多次查看,发现终端A在对应事务中insert的数据,在终端B的事
-- 务周期中,也没有什么影响,也符合可重复的特点。但是,一般的数据库在可重复读情况的时候,无法屏蔽其
-- 他事务insert的数据(为什么?因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存
-- 在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的,但是insert的数据在可重复读
-- 情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读
-- (phantom read)。很明显,MySQL在RR级别的时候,是解决了幻读问题的(解决的方式是用Next-Key锁
-- (GAP+行锁)解决的。这块比较难,有兴趣可以去了解一下)。
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
2 rows in set (0.00 sec)
mysql> commit; --结束事务
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; --看到更新
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
串行化【serializable】
sql
--对所有操作全部加锁,进行串行化,不会有问题,但是只要串行化,效率很低,几乎完全不会被采用
--终端A
mysql> set global transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE |
+----------------+
1 row in set, 1 warning (0.00 sec)
mysql> begin; --开启事务,终端B同步开启
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; --两个读取不会串行化,共享锁
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
mysql> update account set blance=1.00 where id=1; --终端A中有更新或者其他操作,会阻塞。直到终端B事务提交。
Query OK, 1 row affected (18.19 sec)
Rows matched: 1 Changed: 1 Warnings: 0
--终端B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account; --两个读取不会串行化
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
3 rows in set (0.00 sec)
mysql> commit; --提交之后,终端A中的update才会提交。
Query OK, 0 rows affected (0.00 sec)
总结:
- 其中隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
- 不可重复读的重点是修改和删除:同样的条件, 你读取过的数据,再次读取出来发现值不一样了。
- 幻读的重点在于新增:同样的条件, 第1次和第2次读出来的记录数不一样。
- 说明: mysql 默认的隔离级别是可重复读,一般情况下不要修改。
- 上面的例子可以看出,事务也有长短事务这样的概念。事务间互相影响,指的是事务在并行执行的时候,即都没有commit的时候,影响会比较大。
一致性(Consistency)
- 事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。
- 其实一致性和用户的业务逻辑强相关,一般MySQL提供技术支持,但是一致性还是要用户业务逻辑做支撑,也就是,一致性,是由用户决定的。
- 而技术上,通过AID保证C。