正确设置数据库的事务访问级别,有助于我们的应用程序达到预期的效果
在mybatis中,提供了事务隔离级别的枚举类:org.apache.ibatis.session.TransactionIsolationLevel.java
来看具体代码:
/**
* @author Clinton Begin
*/
public enum TransactionIsolationLevel {
NONE(Connection.TRANSACTION_NONE),
READ_COMMITTED(Connection.TRANSACTION_READ_COMMITTED),
READ_UNCOMMITTED(Connection.TRANSACTION_READ_UNCOMMITTED),
REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ),
SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE);
private final int level;
private TransactionIsolationLevel(int level) {
this.level = level;
}
public int getLevel() {
return level;
}
}
从代码中,我们看到,mybatis维护了一份Connection连接的事务隔离级别枚举类,作用仅仅是简化变量,方便程序调用.
那么,其中几个事务隔离级别具体代表什么意思呢?
英文名称 | 中文说明 | |
---|---|---|
READ_COMMITTED | 禁止脏读,允许不可重复读和幻读,此级别仅禁止事务读取具有未提交更改的行。 | 2 |
READ_UNCOMMITTED | 允许脏读,不可重复读和幻读,此级别允许在提交该行中的任何更改(“脏读”)之前,由另一个事务读取由一个事务更改的行,如果回滚任何更改,则第二个事务将检索到无效行。 | 1 |
REPEATABLE_READ | 禁止脏读和不可重复读,允许幻读,此级别禁止事务读取具有未提交更改的行,并且还禁止一个事务读取行,第二个事务更改行,第一个事务重新读取行,第二次获取不同值的情况( “不可重复读”)。 | 4 |
SERIALIZABLE | 事务最高隔离级别,禁止脏读、幻读和不可重复读 | 8 |
看了Java中JDK的注释,我们首先需要明白何为脏读、不可重复读及幻读
何为事务
在理解事务隔离级别之前,我们需要知道事务是什么,有什么作用?
当应用程序被许多用户访问获取数据信息时,或者一个用户发出了多次请求时,为使用户获取的数据是完整的是非常重要的事情,而如何保证数据完整性在数据库中称为事务
为确保数据完整性,事务需要遵循四个条件:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),也就是我们通常所说的ACID
可以查阅mariadb的数据库理论文档,充分了解事务
题外话,最近在写博客温习这些知识的期间,有些不明白的还是会在网上查询资料,加深自己的理解,但我发现国内的很多篇幅都介绍的很片面,所以我建议大家都读英文文章,特别是官方文档,就算是一个单词一个单词的啃,对自己理解这个知识点会深刻许多,再结合自己工作中学到的,会事半功倍.
原子性(Atomicity)
我们都知道,原子是原子是元素中的最小单元
那么在事务中,我们把他理解为一个操作要么成功,要么失败,除了这两种,没有其他情况发生.
原子性意味着整个交易必须完成。如果不是这种情况,则中止整个事务。 这可确保数据库永远不会留下部分完成的事务,从而导致数据完整性不佳。
例如,如果您从一个银行帐户中删除资金,但第二个请求失败且系统无法将资金存入另一个银行,则两个请求都必须失败。 这笔钱不能简单地丢失,也不能从一个帐户中取出而不会进入另一个帐户。
一致性(Consistency)
一致性是指满足某些条件时数据所处的状态。
这个我认为需要结合应用程序来说,因为数据库中的数据状态的变更,都是由我们的应用程序来修改的,数据状态从一个状态变为另外一个状态,这其中的过程是不可见的
通过满足我们的业务需求条件,最终将数据的状态设置为我们的认为正确的状态,这就是数据一致性
一致性是目的(我们希望看到的数据状态),而AID是手段
隔离性(Isolation)
隔离意味着在第一个事务完成之前,另一个事务不能使用在处理一个事务期间使用的任何数据。
例如,如果两个人将100美元存入另一个账户,余额为900美元,则第一笔交易必须加100美元至900美元,第二笔交易必须加100美元至1000美元。 如果第二笔交易在第一笔交易完成前读取900美元,那么这两笔交易似乎都会成功,但100美元将会丢失。 第二个事务必须等到它一个人访问数据。
也就是事务之间是相互隔离的
通过上面的例子,我们也有所了解到隔离性也是确保我们的数据状态的一致
持久性(Durability)
持久性是指一旦事务中的数据被提交,即使系统出现故障,其影响也将保持不变。 当交易正在进行时,效果并不持久。 如果数据库崩溃,备份将始终在事务开始之前将其还原到一致状态。 交易没有什么能够改变这个事实。
我所理解的是事务持久性即事务一旦提交,那么所影响的记录行会持久化保存在我们的磁盘上,及时业务系统崩溃,也不会影响我们的数据(如果你说磁盘蹦了那我也只能漏出尴尬而不失礼貌的微笑了)
事务隔离级别
为了得到更详细的说明,我查看了mariadb的官方文档介绍
READ UNCOMMITTED(读取未提交)
SELECT语句以非锁定方式执行,但可能会使用行的早期版本。因此,使用此隔离级别,会导致非一致性.也叫”脏读”,就好像读取到了未提交的行一样.
READ COMMITTED(读取提交)
读取提交内容,关于一致性(非锁定)读取的类似Oracle的隔离级别:即使在同一事务中,每个一致性读取使之读取到的内容都是自己的新快照
对于锁定读取(SELECT FOR FOR UPDATE或LOCK IN SHARE MODE),InnoDB仅锁定索引记录,允许在锁定记录旁边自由插入新记录,对于UPDATE和DELETE语句,锁定取决于语句是使用具有唯一搜索条件的唯一索引(例如WHERE id = 100)还是范围类型搜索条件(例如WHERE id> 100)。对于具有唯一搜索条件的唯一索引,InnoDB仅锁定找到的索引记录,而不是之前的间隙。对于范围类型搜索,InnoDB使用间隙锁或下一键(间隙加索引记录)锁来锁定扫描的索引范围,以阻止其他会话插入范围所涵盖的间隙。这是必要的,因为必须阻止“幻像行”才能使MySQL复制和恢复正常工作
REPEATABLE READ(可重读)
这是InnoDB存储引擎的默认事务隔离级别,关于一致性读取,这和READ COMMITTED
事务隔离级别有很大的不同,同一事务中的所有一致读取读取第一次读取建立的快照。此约定意味着如果在同一事务中发出多个普通(非锁定)SELECT语句,则这些SELECT语句也相互一致
使用锁读取的SELECT语句(FOR UPDATE或LOCK IN SHARE MODE ),UPDATE和DELETE语句,锁定取决于语句是否使用具有唯一搜索条件的唯一索引,或范围类型的搜索条件。对于具有唯一搜索条件的唯一索引,InnoDB仅锁定找到的索引记录,而不是之前的间隙。对于其他搜索条件,InnoDB使用间隙锁或下一键(间隙加索引记录)锁来锁定扫描的索引范围,以阻止其他会话插入范围所覆盖的间隙。
这对于并发操作数据库数据获取的数据一致性是有很大的帮助.
SERIALIZABLE(可串行化)
这个级别就像REPEATABLE READ,但InnoDB隐式地将所有普通SELECT语句转换为SELECT lock …如果禁用自动提交,则锁定共享模式。
如果启用了自动提交,则SELECT是其自己的事务。因此,已知它是只读的,并且如果作为一致(非锁定)读取执行则可以序列化,并且不需要阻止其他事务。(这意味着如果其他事务已修改所选行,则强制普通SELECT阻止,您应禁用自动提交。)
分布式XA事务始终应用是该隔离级别
数据库模拟
脏读
顾名思义,在一个事务中读取到了不该读到的数据,举例来说明:
目前我们有User信息表(id,age,name),假设当前有A、B两个事务对该User表进行操作
我们要模拟脏读的场景,首先就需要先设置我们当前数据库连接的事务隔离级别,设置为允许脏读,通过上面的说明,需要设置为READ_UNCOMMITED
隔离级别
先来查看mariadb中的默认隔离级别,相关的命令可以查阅官方文档:
mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.06 sec)
mysql>
mysql中默认事务隔离借呗为可重复读
先设置为READ_UNCOMMITTED
级别
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Query OK, 0 rows affected (0.04 sec)
mysql> SELECT @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set (0.06 sec)
mysql>
先看A事务开启事务,查询User信息表:
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user;
+----+-----+-------+
| id | age | name |
+----+-----+-------+
| 1 | 33 | 23 |
| 2 | 33 | abc |
| 3 | 12 | ab3ec |
| 4 | 12 | ab3ec |
| 5 | 12 | ab3ec |
+----+-----+-------+
5 rows in set (0.03 sec)
此时,开启B事务,修改User表中id=1的name值,但是并不提交当前事务
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> update user set name='ccccccccccccc' where id=1;
Query OK, 1 row affected (0.19 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from user;
+----+-----+---------------+
| id | age | name |
+----+-----+---------------+
| 1 | 33 | ccccccccccccc |
| 2 | 33 | abc |
| 3 | 12 | ab3ec |
| 4 | 12 | ab3ec |
| 5 | 12 | ab3ec |
+----+-----+---------------+
5 rows in set (0.11 sec)
我们在B事务中修改后,在查询User信息,发现id=1的name值已发生变化,此时我们在回到A事务中查询User信息表
mysql> select * from user;
+----+-----+---------------+
| id | age | name |
+----+-----+---------------+
| 1 | 33 | ccccccccccccc |
| 2 | 33 | abc |
| 3 | 12 | ab3ec |
| 4 | 12 | ab3ec |
| 5 | 12 | ab3ec |
+----+-----+---------------+
5 rows in set (0.11 sec)
A事务已经读取到了B事务对记录行的修改,但是B事务并未提交,这就是所谓的”脏读”了,此时我们回滚B事务
mysql> rollback;
Query OK, 0 rows affected (0.10 sec)
再A事务中再查询User信息记录行:
mysql> select * from user;
+----+-----+-------+
| id | age | name |
+----+-----+-------+
| 1 | 33 | 23 |
| 2 | 33 | abc |
| 3 | 12 | ab3ec |
| 4 | 12 | ab3ec |
| 5 | 12 | ab3ec |
+----+-----+-------+
5 rows in set (0.10 sec)
发现id=1的User表name信息已经跟随B事务一起回滚掉了.
由此我们应该想到,这不是明显不对嘛,我们都知道事务要么成功,要么失败,B事务还未提交的情况下,A事务已经能读取到B事务所做的修改操作,在显示中,加入银行也存在这种操作,那绝对是不允许的,所以在实际生产环境中,READ UNCOMMITTED
这一事务隔离级别需要在特定的场合下使用,一般是不能使用的
不可重复读
在同一个事务之间,两次查询的结果不一致,这有可能是在两次查询之间,另外一个事务对结果记录行做了修改导致的.
同样,我们使用A、B两个事务来进行模拟
首先,将B事务设置为READ COMMITTED隔离级别:
mysql> SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.04 sec)
A、B事务同时开启事务,此时,查询User信息表的数据都是一致的
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select *from user;
+----+-----+------------+
| id | age | name |
+----+-----+------------+
| 1 | 33 | bbbbbbbbbb |
| 2 | 33 | abc |
| 3 | 12 | ab3ec |
| 4 | 12 | ab3ec |
| 5 | 12 | ab3ec |
+----+-----+------------+
5 rows in set (0.05 sec)
此时,A事务修改user表中id=1的name属性值,但并提交
mysql> update user set name='aaaaaaaaaaaaaaaaaa' where id=1;
我们在B事务中进行查询
mysql> select *from user;
+----+-----+------------+
| id | age | name |
+----+-----+------------+
| 1 | 33 | bbbbbbbbbb |
| 2 | 33 | abc |
| 3 | 12 | ab3ec |
| 4 | 12 | ab3ec |
| 5 | 12 | ab3ec |
+----+-----+------------+
5 rows in set (0.05 sec)
发现数据并未产生变化,此时我们提交A事务
mysql> commit;
Query OK, 0 rows affected (0.11 sec)
再在B事务中查询User表信息
mysql> select *from user;
+----+-----+--------------------+
| id | age | name |
+----+-----+--------------------+
| 1 | 33 | aaaaaaaaaaaaaaaaaa |
| 2 | 33 | abc |
| 3 | 12 | ab3ec |
| 4 | 12 | ab3ec |
| 5 | 12 | ab3ec |
+----+-----+--------------------+
5 rows in set (0.11 sec)
此时,B事务已经读取到A事务提交的影响记录,id=1的name值已更改
可重复读
在上面说明中我们知道,同一事务中的所有一致读取读取第一次读取建立的快照
同样是A、B两个事务
首先将B事务设置为REPEATABLE READ
事务隔离级别
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.06 sec)
A\B开启事务,先查询user信息表
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user;
+----+-----+--------------------+
| id | age | name |
+----+-----+--------------------+
| 1 | 33 | aaaaaaaaaaaaaaaaaa |
| 2 | 33 | abc |
| 3 | 12 | ab3ec |
| 4 | 12 | ab3ec |
| 5 | 12 | ab3ec |
+----+-----+--------------------+
5 rows in set (0.09 sec)
在B事务中,修改id=1的name值为123456,并提交B事务
mysql> update user set name='123456' where id=1;
Query OK, 1 row affected (0.05 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from user;
+----+-----+--------+
| id | age | name |
+----+-----+--------+
| 1 | 33 | 123456 |
| 2 | 33 | abc |
| 3 | 12 | ab3ec |
| 4 | 12 | ab3ec |
| 5 | 12 | ab3ec |
+----+-----+--------+
5 rows in set (0.10 sec)
mysql> commit;
Query OK, 0 rows affected (0.16 sec)
B事务未提交,在B事务查询,User信息变更,此时,我们在A事务中查询user表信息
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user;
+----+-----+--------------------+
| id | age | name |
+----+-----+--------------------+
| 1 | 33 | aaaaaaaaaaaaaaaaaa |
| 2 | 33 | abc |
| 3 | 12 | ab3ec |
| 4 | 12 | ab3ec |
| 5 | 12 | ab3ec |
+----+-----+--------------------+
5 rows in set (0.09 sec)
mysql> select * from user;
+----+-----+--------------------+
| id | age | name |
+----+-----+--------------------+
| 1 | 33 | aaaaaaaaaaaaaaaaaa |
| 2 | 33 | abc |
| 3 | 12 | ab3ec |
| 4 | 12 | ab3ec |
| 5 | 12 | ab3ec |
+----+-----+--------------------+
5 rows in set (0.11 sec)
两次读取到的记录是一样的,并未产生任何变化,这也就是和官方说明保持一致,同一事务中的所有一致性查询,都是回去的第一次查询快照.这也就是可重读.
此时,我们在提交A事务,再查询User表,发现记录已变更
mysql> select * from user;
+----+-----+--------+
| id | age | name |
+----+-----+--------+
| 1 | 33 | 123456 |
| 2 | 33 | abc |
| 3 | 12 | ab3ec |
| 4 | 12 | ab3ec |
| 5 | 12 | ab3ec |
+----+-----+--------+
5 rows in set (0.13 sec)
幻读
同一个事务两次查询的数据记录行不一致,导致产生的幻影
同样是基于REPEATABLE READ
的事务隔离级别
开启A\B事务,查询我们的User表信息
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user;
+----+-----+--------+
| id | age | name |
+----+-----+--------+
| 1 | 33 | 123456 |
| 2 | 33 | abc |
| 3 | 12 | ab3ec |
| 4 | 12 | ab3ec |
| 5 | 12 | ab3ec |
+----+-----+--------+
5 rows in set (0.09 sec)
我们在B事务中新增一条数据,并提交
mysql> insert into user(id,age,name) values(6,44,'add');
Query OK, 1 row affected (0.06 sec)
mysql> commit;
Query OK, 0 rows affected (0.10 sec)
此时,A事务中查询User信息表
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user;
+----+-----+--------+
| id | age | name |
+----+-----+--------+
| 1 | 33 | 123456 |
| 2 | 33 | abc |
| 3 | 12 | ab3ec |
| 4 | 12 | ab3ec |
| 5 | 12 | ab3ec |
+----+-----+--------+
5 rows in set (0.09 sec)
我们发现A事务中并未读取到B事务提交的新记录行数,这就是幻读.
提交A事务,在进行读取
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from user;
+----+-----+--------+
| id | age | name |
+----+-----+--------+
| 1 | 33 | 123456 |
| 2 | 33 | abc |
| 3 | 12 | ab3ec |
| 4 | 12 | ab3ec |
| 5 | 12 | ab3ec |
| 6 | 44 | add |
+----+-----+--------+
6 rows in set (0.11 sec)
mysql>
此时已经读取到新的记录行了.
可重复读隔离级别只允许读取已提交记录,而且在一个事务两次读取一个记录期间,其他事务部的更新该记录。但该事务不要求与其他事务可串行化
总结
或许根据事务隔离级别的字面意思,做一个简单的总结
READ UNCOMMITTED:意思是一个事务可以读取另外一个事务(未提交)所做的操作,那么此操作给开发者所造成的影响即有可能是脏读、不可重复读、和幻读
READ COMMITTED:意思是只能读取已提交的内容,这就避免的脏读的出现,但是在一个事务操作期间,另外一个事务对记录行产生了的变化,这就导致了可以不可重复读(A事务两次读取数据不一样)和幻读
REPEATABLE READ:意思是可重复读,此级别是和READ COMMITTED
一致性读取有相似点,却也有不同点,首先是不允许脏读,读取的内容都是已提交的。其二从字面意思来看也能理解,允许重复读,所以它是禁止不可重复读的,当然,幻读允许存在
SERIALIZABLE:可串行化,我是这么理解的,串行化的操作既是一个操作连接着一个操作,就是事情总有先后,所以当一个事务正在操作的时候,其他事务必须等待,等操作的事务操作完成后,其他事务即可以进行操作,我们都知道事务是原子性的,所以该级别的隔离级别不允许脏读、不可重复读、和幻读.
我们在理解了以上的基础概念后,后面再来读mybatis的事务相关代码,会让我们更轻松.
最后
我不能保证我所说的都一定是正确的,但我会确保每一个词,每一个用意都是根据自身的理解结合官方文档所总结的,如果其中任然有纰漏,欢迎同行中的朋友加以指正,我会虚心接受学习.