事务特性(ACID)

原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)


概念

脏读

读取了其他事务未提交的数据。未提交意味着这些数据有可能回滚,不插入数据库,也就是不存在的数据。读取数据库不存在的数据,就是脏读。

可重复读

在一个事务内,事务开始和事务结束前不同时刻读取的同一批是一致的,通常针对数据更新操作。

不可重复读

在同一事务内读取的同一批数据可能不一致,受其他事务影响,比如其他事务修改了这批数据并且提交了,通常针对更新操作。

幻读

幻读是针对数据插入操作来说的。比如A事务修改了数据但是还未提交,但是B事务插入了A事务修改前相同的数据,且B事务在A事务之前提交了,这样A事务再去读这批数据,感觉没有更新成功,实际上是B事务插入的数据,感觉出现幻觉。


隔离级别

1.读取未提交(READ UNCOMMITTED)

2.读取已提交(READ COMMITTED)

3.可重复读(REPEATABLE READ)

4.可串行化(SERIALIZABLE)

从上往下,隔离强度逐渐增强,性能逐渐变差

对于MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重复读)

而每种隔离级别导致的问题有:



事务隔离级别

脏读

不可重复读

幻读

读取未提交

可能

可能

可能

读取已提交

不可能

可能

可能

可重复读

不可能

不可能

可能

可串行化

不可能

不可能

不可能




设置隔离级别

-- 查看隔离级别  版本5.7.20 之后
show variables like '%iso%'
SELECT @@GLOBAL.tx_isolation;
SELECT @@SESSION.tx_isolation;

-- 设置隔离级别 set [作用域] transaction isolation level [事务隔离级别] 
-- 其中作用域可以是 SESSION 或者 GLOBAL,GLOBAL 是全局的,而 SESSION 只针对当前回话窗口
-- 隔离级别是 READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE
set global transaction isolation level read committed;
-- 设置完成后,只对之后新起的 session 才起作用,对已经启动 session 无效。如果用 shell 客户端那就要重新连接 MySQL,如果用 Navicat 那就要创建新的查询窗口

隔离级别解释

1.读取未提交(READ UNCOMMITTED)

其实就是可以读到其他事务未提交的数据。

启动两个事务,分别为事务A和事务B,在事务A中使用 update 语句,修改 age 的值为10,初始是 1 ,在执行完 update 语句之后,在事务B中查询 user 表,会看到 age 的值已经是 10 了,这时候 事 务A还没有提交,而此时事务B有可能拿着已经修改过的 age=10 去进行其他操作了。在事务B进 行操作的过程中,很有可能事务A由于某些原因,进行了事务回滚操作,那其实事务B得到的就是脏 数据了,拿着脏数据去进行其他的计算,那结果肯定也是有问题的




mysql中的脏读_数据


2.读提交

在事务A中使用 update 语句将 id=1 的记录行 age 字段改为 10。此时,在事务B中使用 select 语句进行查询,我们发现在事务A提交之前,事务B中查询到的记录 age 一直是1,直到事务A提交,此时在事务B中 select 查询,发现 age 的值已经是 10 了。


mysql中的脏读_数据_02


3.可重复读

B事务读取数据,A事务更新了数据也提交了,A事务再去读取数据还是跟之前数据一样。

这只是针对已有行的更改操作有效,但是对于新插入的行记录,就会出现幻读。


mysql中的脏读_mysql_03


可重复读隔离级别出现幻读

A事务更新数据,在更新完数据之后B事务插入一条与A事务修改之前相同的数据,B事务提交之后,事务A查询数据发现多出一行,而且是A事务自己更新之前的数据。


mysql中的脏读_数据库_04


4.串行化

串行化是4种事务隔离级别中隔离效果最好的,但是效果最差,它将事务的执行变为顺序执行,与其他三个隔离级别相比,它就相当于单线程,后一个事务的执行必须等待前一个事务结束

MySQL 中是如何实现事务隔离的

首先说读未提交,它是性能最好,也可以说它是最野蛮的方式,因为它压根儿就不加锁,所以根本谈不上什么隔离效果,可以理解为没有隔离。

再来说串行化。读的时候加共享锁,也就是其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。

最后说读提交和可重复读。这两种隔离级别是比较复杂的,既要允许一定的并发,又想要兼顾的解决问题。

实现可重复读

MySQL 采用了 MVVC (多版本并发控制) 的方式。

可重复读是在事务开始的时候生成一个当前事务全局性的快照,而读提交则是每次执行语句的时候都重新生成一次快照

并发写问题

A执行 update 操作, update 的时候要对所修改的行加行锁,这个行锁会在提交之后才释放。而在事务A提交之前,事务B也想 update 这行数据,于是申请行锁,但是由于已经被事务A占有,事务B是申请不到的,此时,事务B就会一直处于等待状态,直到事务A提交,事务B才能继续执行,如果事务A的时间太长,那么事务B很有可能出现超时异常


mysql中的脏读_mysql_05


加锁的过程要分有索引和无索引两种情况:

  1. 有索引的情况,那么 MySQL 直接就在索引数中找到了这行数据,加上行锁。
  2. 无索引的情况,MySQL 无法直接定位到这行数据,会为这张表中所有行加行锁,MySQL 会进行一遍过滤,发现不满足的行就释放锁,最终只留下符合条件的行。虽然最终只为符合条件的行加了锁,但是这一锁一释放的过程对性能也是影响极大的。所以,如果是大表的话,建议合理设计索引,如果真的出现这种情况,那很难保证并发度。

解决幻读

解决幻读用的也是锁,叫做间隙锁,MySQL 把行锁和间隙锁合并在一起,解决了并发写和幻读的问题,这个锁叫做 Next-Key锁。

假设现在表中有两条记录,并且 age 字段已经添加了索引,两条记录 age 的值分别为 10 和 30。


mysql中的脏读_mysql中的脏读_06


此时,在数据库中会为索引维护一套B+树,用来快速定位行记录。B+索引树是有序的,所以会把这张表的索引分割成几个区间。


mysql中的脏读_mysql中的脏读_07


如图所示,分成了3 个区间,(负无穷,10]、(10,30]、(30,正无穷],在这3个区间是可以加间隙锁的。

之后,我用下面的两个事务演示一下加锁过程。


mysql中的脏读_数据_08


在事务A提交之前,事务B的插入操作只能等待,这就是间隙锁起得作用。当事务A执行update user set name='风筝2号’ where age = 10; 的时候,由于条件 where age = 10 ,数据库不仅在 age =10 的行上添加了行锁,而且在这条记录的两边,也就是(负无穷,10]、(10,30]这两个区间加了间隙锁,从而导致事务B插入操作无法完成,只能等待事务A提交。不仅插入 age = 10 的记录需要等待事务A提交,age<10、10<age<30 的记录页无法完成,而大于等于30的记录则不受影响,这足以解决幻读问题了。

这是有索引的情况,如果 age 不是索引列,那么数据库会为整个表加上间隙锁。所以,如果是没有索引的话,不管 age 是否大于等于30,都要等待事务A提交才可以成功插入。