事务,Oracle,MySQL及Spring事务隔离级别

时间:2023-03-08 19:04:14
事务,Oracle,MySQL及Spring事务隔离级别

一、什么是事务: 
事务逻辑上的一组操作,组成这组操作的各个逻辑单元,要么一起成功,要么一起失败.

二、事务特性(4种): 
原子性 (atomicity):强调事务的不可分割;
一致性 (consistency):事务的执行的前后数据的完整性保持一致;
隔离性 (isolation):一个事务执行的过程中,不应该受到其他事务的干扰 ;
持久性(durability) :事务一旦结束,数据就持久到数据库。

三、如果不考虑隔离性引发安全性问题: 
脏读 :一个事务读到了另一个事务的未提交的数据 
不可重复读 :一个事务读到了另一个事务已经提交的 update 的数据导致多次查询结果不一致. 
虚幻读 :一个事务读到了另一个事务已经提交的 insert 的数据导致多次查询结果不一致.

四、为了处理这些问题,SQL标准定义了以下几种事务隔离级别

READ UNCOMMITTED(读未提交): 不可重复读,脏读及幻读都允许;

READ COMMITTED(不可重复读):允许不可重复读,不允许脏读,允许幻读;

REPEATABLE READ (可重复读):不允许不可重复读和脏读,但允许幻读;

SERIALIZABLE(串行化):不可重复读,脏读和幻读都不允许。

说明:

  1、SQL规范所规定的标准,不同的数据库具体的实现可能会有些差异

  2、mysql中默认事务隔离级别是可重复读时并不会锁住读取到的行

  3、事务隔离级别为读提交时,写数据只会锁住相应的行

  4、事务隔离级别为可重复读时,写数据会锁住整张表

  5、事务隔离级别为串行化时,读写数据都会锁住整张表

   6、隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大,鱼和熊掌不可兼得啊。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、幻读这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。

五、Oracle,MySQL及Spring事务隔离级别

1. Oracle事务隔离级别(2种)

Oracle数据库支持READ COMMITTED 和 SERIALIZABLE这两种事务隔离级别。所以Oracle不支持脏读

SQL标准所定义的默认事务隔离级别是SERIALIZABLE,但是Oracle 默认使用的是READ COMMITTED

设置隔离级别使用 SET TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE]

下面是oracle 设置SERIALIZABLE隔离级别一个示例:

事务,Oracle,MySQL及Spring事务隔离级别

左面是事务T1,右面是事务T2,因为T2级别为SERIALIZABLE,所以即使事务T1在提交了数据之后,事务T2还是看不到T1提交的数据,幻想读和不可重复读都不允许了。

那如何能查看到T1新增的记录呢? 上面T1和T2是并发执行,在T1执行insert的时候事务T2已经开始了,因为T2级别是SERIALIZABLE,所以T2所查询的数据集是T2事务开始前数据库的数据。即事务T1在事务T2开始之后的insert和update操作的影响都不会影响事务T2。现在重新开启一个事务T3 就可以看到T1新增的记录了。

当下列事件发生时,事务就开始了:

1)连接到数据库,并执行第一条DML语句

2)前一个事务结束后,又输入了另一条DML语句

2.MySQL事务隔离级别(4种)

事务,Oracle,MySQL及Spring事务隔离级别

mysql默认的事务隔离级别为repeatable-read

事务,Oracle,MySQL及Spring事务隔离级别

实例说明MySQL各个隔离级别的情况

1)、读未提交:

(1)打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表account的初始值:

事务,Oracle,MySQL及Spring事务隔离级别

(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:

事务,Oracle,MySQL及Spring事务隔离级别

(3)这时,虽然客户端B的事务还没提交,但是客户端A就可以查询到B已经更新的数据:

事务,Oracle,MySQL及Spring事务隔离级别

(4)一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据:

事务,Oracle,MySQL及Spring事务隔离级别

(5)在客户端A执行更新语句update account set balance = balance - 50 where id =1,lilei的balance没有变成350,居然是400,是不是很奇怪,数据的一致性没问啊,如果你这么想就太天真 了,在应用程序中,我们会用400-50=350,并不知道其他会话回滚了,要想解决这个问题可以采用读已提交的隔离级别

事务,Oracle,MySQL及Spring事务隔离级别

2)、读已提交

(1)打开一个客户端A,并设置当前事务模式为read committed(未提交读),查询表account的初始值:

事务,Oracle,MySQL及Spring事务隔离级别

(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:

事务,Oracle,MySQL及Spring事务隔离级别

(3)这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题:

事务,Oracle,MySQL及Spring事务隔离级别

(4)客户端B的事务提交

事务,Oracle,MySQL及Spring事务隔离级别

(5)客户端A执行与上一步相同的查询,结果与上一步不一致,即产生了不可重复读的问题,在应用程序中,假设我们处于客户端A的会话,查询到lilei的balance为450,但是其他事务将lilei的balance值改为400,我们并不知道,如果用450这个值去做其他操作,是有问题的,不过这个概率真的很小哦,要想避免这个问题,可以采用可重复读的隔离级别

事务,Oracle,MySQL及Spring事务隔离级别

3)、可重复读

(1)打开一个客户端A,并设置当前事务模式为repeatable read,查询表account的初始值:

事务,Oracle,MySQL及Spring事务隔离级别

(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account并提交,客户端B的事务居然可以修改客户端A事务查询到的行,也就是mysql的可重复读不会锁住事务查询到的行,这一点出乎我的意料,sql标准中事务隔离级别为可重复读时,读写操作要锁行的,mysql居然没有锁,我了个去。在应用程序中要注意给行加锁,不然你会以步骤(1)中lilei的balance为400作为中间值去做其他操作

事务,Oracle,MySQL及Spring事务隔离级别

(3)在客户端A执行步骤(1)的查询:

事务,Oracle,MySQL及Spring事务隔离级别

(4)执行步骤(1),lilei的balance仍然是400与步骤(1)查询结果一致,没有出现不可重复读的 问题;接着执行update balance = balance - 50 where id = 1,balance没有变成400-50=350,lilei的balance值用的是步骤(2)中的350来算的,所以是300,数据的一致性倒是没有被破坏,这个有点神奇,也许是mysql的特色吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> select * from account;
+------+--------+---------+
| id | name | balance |
+------+--------+---------+
| 1 | lilei |  400 |
| 2 | hanmei | 16000 |
| 3 | lucy | 2400 |
+------+--------+---------+
rows in set (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
mysql> select * from account;
+------+--------+---------+
| id | name | balance |
+------+--------+---------+
| 1 | lilei |  300 |
| 2 | hanmei | 16000 |
| 3 | lucy | 2400 |
+------+--------+---------+
rows in set (0.00 sec)

(5) 在客户端A开启事务,查询表account的初始值

1
2
3
4
5
6
7
8
9
10
11
12
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+------+--------+---------+
| id | name | balance |
+------+--------+---------+
| 1 | lilei | 300 |
| 2 | hanmei | 16000 |
| 3 | lucy | 2400 |
+------+--------+---------+
rows in set (0.00 sec)

(6)在客户端B开启事务,新增一条数据,其中balance字段值为600,并提交

1
2
3
4
5
6
7
8
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values(4,'lily',600);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.01 sec)

(7) 在客户端A计算balance之和,值为300+16000+2400=18700,没有把客户端B的值算进去,客户端A提交后再计算balance之和,居然变成了19300,这是因为把客户端B的600算进去了,站在客户的角度,客户是看不到客户端B的,它会觉得是天下掉馅饼了,多了600块,这就是幻读,站在开发者的角度,数据的 一致性并没有破坏。但是在应用程序中,我们得代码可能会把18700提交给用户了,如果你一定要避免这情况小概率状况的发生,那么就要采取下面要介绍的事务隔离级别“串行化”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> select sum(balance) from account;
+--------------+
| sum(balance) |
+--------------+
| 18700 |
+--------------+
1 row in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select sum(balance) from account;
+--------------+
| sum(balance) |
+--------------+
| 19300 |
+--------------+
1 row in set (0.00 sec)

4).串行化

(1)打开一个客户端A,并设置当前事务模式为serializable,查询表account的初始值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+------+--------+---------+
| id | name | balance |
+------+--------+---------+
| 1 | lilei | 10000 |
| 2 | hanmei | 10000 |
| 3 | lucy | 10000 |
| 4 | lily | 10000 |
+------+--------+---------+
rows in set (0.00 sec)

(2)打开一个客户端B,并设置当前事务模式为serializable,插入一条记录报错,表被锁了插入失败,mysql中事务隔离级别为serializable时会锁表,因此不会出现幻读的情况,这种隔离级别并发性极低,往往一个事务霸占了一张表,其他成千上万个事务只有干瞪眼,得等他用完提交才可以使用,开发中很少会用到。

1
2
3
4
5
6
7
8
mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values(5,'tom',0);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

3.Spring事务隔离级别(5种) 
1)DEFAULT: 这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别. 
2)未提交读(read uncommited) :脏读,不可重复读,虚读都有可能发生 ;
3)已提交读 (read commited):避免脏读。但是不可重复读和虚读有可能发生 ;
4)可重复读 (repeatable read) :避免脏读和不可重复读.但是虚读有可能发生;
5)串行化的 (serializable) :避免以上所有读问题.

Oracle 默认:读已提交
Mysql 默认:可重复读

read uncommited:是最低的事务隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。 
read commited:保证一个事物提交后才能被另外一个事务读取。另外一个事务不能读取该事物未提交的数据。 
repeatable read:这种事务隔离级别可以防止脏读,不可重复读。但是可能会出现幻象读。它除了保证一个事务不能被另外一个事务读取未提交的数据之外还避免了以下情况产生(不可重复读)。 
serializable:这是花费最高代价但最可靠的事务隔离级别。事务被处理为顺序执行。除了防止脏读,不可重复读之外,还避免了幻象读(避免三种)。

事务的传播行为 
PROPAGION_XXX :事务的传播行为 
* 保证同一个事务中 
PROPAGATION_REQUIRED 支持当前事务,如果不存在 就新建一个(默认) 
PROPAGATION_SUPPORTS 支持当前事务,如果不存在,就不使用事务 
PROPAGATION_MANDATORY 支持当前事务,如果不存在,抛出异常 
* 保证没有在同一个事务中 
PROPAGATION_REQUIRES_NEW 如果有事务存在,挂起当前事务,创建一个新的事务 
PROPAGATION_NOT_SUPPORTED 以非事务方式运行,如果有事务存在,挂起当前事务 
PROPAGATION_NEVER 以非事务方式运行,如果有事务存在,抛出异常 
PROPAGATION_NESTED 如果当前事务存在,则嵌套事务执行