1 介绍
事务就是一个对数据库操作的序列,是一个不可分割的工作单位,要不这个序列里面的操作全部执行,要不全部不执行。
要获得最高的事务隔离性,可以采取序列化/串行的方式,代价是严重影响系统处理事务的吞吐量。
Read Uncommitted,Read Committed,Repeatable Read,Serializable,级别依次递增。
Mysql默认的事务隔离级别是Repeatable Read。
1.1 事务的四大特征(ACID)
特性 | 说明 |
---|---|
Atomicity | 原子性: 事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响 |
Consistency | 一致性: 事务必须始终保证系统数据处于一致的状态,不管在任何给定的时间并发事务有多少 |
Isolation | 隔离性: 当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离 |
Durability | 持久性: 事务完成之后,它对于系统所作的修改应该是持久性的,即使系统出现故障也将一直保持 |
1.2 事务的隔离级别
事务隔离界别 | 丢失更新 | 脏读 | 不可重复读 | 幻读 | 并发模型 | 更新冲突检测 |
---|---|---|---|---|---|---|
Read-Uncommitted(读未提交) | 有 | 有 | 有 | 有 | 悲观 | 无 |
Read-Committed(读已提交) | 有 | 无 | 有 | 有 | 悲观 | 无 |
Repeatable-Read(可重复读) | 无 | 无 | 无 | 有 | 悲观 | 无 |
Serializable(可串行化) | 无 | 无 | 无 | 无 | 悲观 | 无 |
名词解释 | 未提交读 | 在读取数据时不会加任何锁,也不会进行检测,可能会读到没有提交的数据。 | | —- | —- | | 已提交读 | 只读取提交的数据等待其他事物释放排他锁,读数据的共享锁在读操作完成后会立即释放。这个隔离级别是sql server默认的隔离级别。 | | 可重复读 | 像已提交读一样,但共享锁会保持到事物结束才会释放。 | | 可串行读 | 类似于可重复读,但锁不仅会锁定所查询的数据,也会锁定所查询的范围,这样就阻止了新数据插入所查询的范围。 |
可能引发的问题 | 问题 | 现象 | | —- | —- | | 脏读 | 在一个事务处理过程中读取了另一个未提交的事务中的数据 , 导致两次查询结果不一致 | | 不可重复读 | 在一个事务处理过程中读取了另一个事务中修改并已提交的数据, 导致两次查询结果不一致 | | 幻读 | select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入。或不存在执行delete删除,却发现删除成功 |
2 解决方案
2.1 准备数据
//创建数据库
create table account
(
id int primary key not null,
name varchar(50),
money decimal
);
//两个用户注册账号存钱
insert into account(id,name,money) value(1,'刘三',1000);
insert into account(id,name,money) value(2,'刘三的爸爸刘六',1000);
2.2 知识储备
-- 查询数据库隔离级别
SELECT @@TX_ISOLATION;
-- 修改数据库隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串;
-- 修改数据库隔离级别为read uncommitted
SET GLOBAL TRANSACTION ISOLATION LEVEL read uncommitted;
-- 查看隔离级别
SELECT @@TX_ISOLATION; -- 修改后需要断开连接重新开
2.3 Read Committed
2.3.1 解决脏读
2.3.1.1 示例1
- 小明连接数据库去查询自己本学期的成绩,他设置session(当前连接)的事务隔离级别为Read Committed: ```sql xiaoming> set session transaction isolation level read committed; Query OK, 0 rows affected (0.00 sec)
xiaoming> select @@tx_isolation; +————————+ | @@tx_isolation | +————————+ | READ-COMMITTED | +————————+ 1 row in set (0.00 sec)
2. 就在这个时候,小明的班主任王老师也连接了数据库去登记学生本学期的成绩:
```sql
mr.wang> begin;
Query OK, 0 rows affected (0.00 sec)
mr.wang> insert into scores(name,score) values ("xiaoming", 59);
Query OK, 1 row affected (0.00 sec)
- 当王老师还没有提交事务时,小明刚好开始查询自己的成绩,结果他没查到成绩,因为王老师还没提交: ```sql xiaoming> begin; Query OK, 0 rows affected (0.00 sec)
xiaoming> select * from scores where name = ‘xiaoming’;
Empty set (0.00 sec)
4. 小明查成绩之后,王老师发现自己登错了成绩,其实小明考了69分,于是他回滚了当前事务, 并重新录入了小明的正确成绩:
```sql
mr.wang> rollback;
Query OK, 0 rows affected (0.00 sec)
mr.wang> begin;
Query OK, 0 rows affected (0.00 sec)
mr.wang> insert into scores(name,score) values ("xiaoming", 69);
Query OK, 1 row affected (0.00 sec)
mr.wang> commit;
Query OK, 0 rows affected (0.00 sec)
- 接着,小明又查了一次成绩,这次他查到了,他很开心,因为他及格了
xiaoming> select * from scores where name = 'xiaoming';
+----+----------+-------+
| id | name | score |
+----+----------+-------+
| 1 | xiaoming | 69 |
+----+----------+-------+
1 row in set (0.00 sec)
2.3.1.2 示例2
```sql — 设置隔离级别为read committed set global transaction isolation level read committed;
— 开启事务 start transaction;
— 转账 update account set money = money - 500 where id = 1; update account set money = money + 500 where id = 2;
— 窗口2查看转账结果,并没有发生变化(脏读问题被解决了)
— 执行提交事务。 commit;
虽然解决了“脏读”问题,但是Read Committed不能保证在一个事务中每次读都能读到相同的数据,因为在每次读数据之后其他并发事务可能会对刚才读到的数据进行修改。就像上面,小明在一次事务中2次读取成绩返回的结果不一样。这也反映出了Read Committed事务隔离级别存在以下问题:
a. 不可重复读, 也即一个事务范围内两个相同的查询却返回了不同数据
<a name="sf4Ir"></a>
## 2.4 Repeatable Read
<a name="ITdiU"></a>
### 2.4.1 解决不可重复读
<a name="ApZor"></a>
#### 2.4.1.1 示例1
1. 小明很开心自己考了69分,于是他连接到数据库查询自己的成绩来炫耀给小伙伴,由于Repeatable Read是默认的事务隔离级别,因此这次他不需要进行修改:
```sql
xiaoming> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)
xiaoming> begin;
Query OK, 0 rows affected (0.00 sec)
xiaoming> select * from scores where name = 'xiaoming';
+----+----------+-------+
| id | name | score |
+----+----------+-------+
| 1 | xiaoming | 69 |
+----+----------+-------+
1 row in set (0.00 sec)
- 不幸的是,小明的班主任王老师复查试卷后,发现小明的成绩多加了10分,于是他连接到数据库来修改小明的成绩 ```sql mr.wang> begin; Query OK, 0 rows affected (0.00 sec)
mr.wang> update scores set score = 59 where name = ‘xiaoming’; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0
mr.wang> commit; Query OK, 0 rows affected (0.00 sec)
3. 接着小明觉得还不尽兴,于是又查一次,还是69分,可怜的是他不知道自己其实是不及格的:
```sql
mr.wang> begin;
Query OK, 0 rows affected (0.00 sec)
mr.wang> update scores set score = 59 where name = 'xiaoming';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mr.wang> commit;
Query OK, 0 rows affected (0.00 sec)
可见Repeatable Read的确可以解决“不可重复读”的问题,小明在一次事务中2次查询的成绩都是一样的,即使2次查询中王老师修改了成绩。注意我们演示的场景中,王老师是针对一条已有的记录进行了Update, 如果王老师是新增即Insert小明的成绩,那么小明的2次查询的结果还是不一样的,如下所示:
首先小明第一次查询, 没有成绩
xiaoming> select * from scores where name = 'xiaoming';
Empty set (0.00 sec)
然后王老师录入成绩 ```sql mr.wang> begin; Query OK, 0 rows affected (0.00 sec)
mr.wang> insert into scores(name,score) values (“xiaoming”, 59); Query OK, 1 row affected (0.00 sec)
mr.wang> commit; Query OK, 0 rows affected (0.00 sec)
3. 最后小明再次查询成绩,这次有了:
```sql
xiaoming> select * from scores where name = 'xiaoming';
+----+----------+-------+
| id | name | score |
+----+----------+-------+
| 1 | xiaoming | 59 |
+----+----------+-------+
1 row in set (0.00 sec)
通过上述例子,我们可以看出Repeatable Read也是存在以下问题的:
a. 幻读,也即在一次事务范围内多次进行查询,如果其他并发事务中途插入了新的记录,那么之后的查询会读取到这些“幻影”行。
另外,我们也需要注意,不可重复读对应的是修改即Update,幻读对应的是插入即Insert。
2.4.1.2 示例2
-- 设置隔离级别为repeatable read
set global transaction isolation level repeatable read;
-- 开启事务
start transaction;
-- 转账
update account set money = money - 500 where id = 1;
update account set money = money + 500 where id = 2;
-- 窗口2查看转账结果,并没有发生变化
-- 执行提交事务
commit;
-- 这个时候窗口2只要还在上次事务中,看到的结果都是相同的。
-- 只有窗口2结束事务,才能看到变化(不可重复读的问题被解决)
2.5 Serializable
2.5.1 解决幻读
2.3.1.1 示例1
- 小明连接数据库去查询自己本学期的成绩,他设置session(当前连接)的事务隔离级别为Serializable: ```sql xiaoming> set session transaction isolation level serializable; Query OK, 0 rows affected (0.00 sec)
xiaoming> select @@tx_isolation; +————————+ | @@tx_isolation | +————————+ | SERIALIZABLE | +————————+ 1 row in set (0.00 sec)
2. 小明开始查询成绩,由于还没有录入,因此没有成绩:
```sql
xiaoming> begin;
Query OK, 0 rows affected (0.00 sec)
xiaoming select * from scores where name = 'xiaoming';
Empty set (0.00 sec)
- 这时小明的班主任王老师也连接数据库来录入成绩,可是他会卡在插入第一条成绩信息这里, 如下所示,insert语句迟迟不会返回: ```sql mr.wang> begin; Query OK, 0 rows affected (0.00 sec)
mr.wang> insert into scores(name, score) values (‘xiaoming’, 69);
4. 小明结束本次查询:
```sql
xiaoming> commit;
Query OK, 0 rows affected (0.00 sec)
这时王老师插入第一条成绩才完成:
mr.wang> insert into scores(name, score) values ('xiaoming', 69);
Query OK, 1 row affected (3.42 sec)
如果小明久久不结束查询,还会导致王老师录入成绩超时:
xiaoming> insert into scores(name, score) values ('xiaoming', 69);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
从上面的例子我们可以看出,如果一个session设置隔离级别为Serializable时,其执行事务时会阻塞其他并发事务,从上面的错误信息中我们也可以看出应该是通过某种锁来实现的。既然是这样,那么“脏读”、“不可重复读”和“幻读”自然是不可能发生了。
2.3.1.2 示例2
```sql / 窗口1 / — 设置隔离级别为serializable set global transaction isolation level serializable;
— 开启事务 start transaction;
— 添加一条记录 INSERT INTO account VALUES (4,’赵六’,1600);
— 查询账户表,本窗口可以查看到id为4的结果 SELECT * FROM account;
— 提交事务 COMMIT;
<a name="OtYUR"></a>
## 2.6 Read Uncommitted
1. 假设现在有个学生小明连接到数据库去读取自己本学期的成绩,它设置session(当前连接)的事务隔离级别为Read Uncommitted:
```sql
xiaoming> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)
xiaoming> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
xiaoming> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set (0.00 sec)
- 就在这个时候,小明的班主任王老师也连接了数据库去登记学生本学期的成绩: ```sql mr.wang> begin; Query OK, 0 rows affected (0.00 sec)
mr.wang> insert into scores(name,score) values (“xiaoming”, 59); Query OK, 1 row affected (0.00 sec)
3. 当王老师还没有提交事务时,小明刚好开始查询自己的成绩,结果他查到自己考了59分,他伤心的要死:
```sql
xiaoming> begin;
Query OK, 0 rows affected (0.00 sec)
xiaoming> select * from scores where name = 'xiaoming';
+----+----------+-------+
| id | name | score |
+----+----------+-------+
| 1 | xiaoming | 59 |
+----+----------+-------+
1 row in set (0.00 sec)
- 小明查成绩之后,王老师发现自己登错了成绩,其实小明考了69分,于是他回滚了当前事务, 并重新录入了小明的正确成绩: ```sql mr.wang> rollback; Query OK, 0 rows affected (0.00 sec)
mr.wang> begin; Query OK, 0 rows affected (0.00 sec)
mr.wang> insert into scores(name,score) values (“xiaoming”, 69); Query OK, 1 row affected (0.00 sec)
mr.wang> commit; Query OK, 0 rows affected (0.00 sec) ```
- 小明也没有复查成绩,因此整个寒假都过的很不开心,毕竟自己没有”及格”!
通过上述场景,我们发现,Read Uncommitted这个最低的事务隔离级别存在以下这些问题:
a. 允许脏读(dirty reads),就像上面王老师录入的错误成绩(脏数据)被小明读到一样
参考链接:https://blog.csdn.net/oyw5201314ck/category_7518099.html