MySQL 事务的隔离性


# MySQL 事务的隔离性

MySQL 中有三个常见的问题:脏读、不可重复读和幻读。它们都是由于数据库并发访问导致的数据读取问题,都与事务隔离性有关。

# 事务

# 什么是事务

事务是数据库操作的最小工作单元,它是一系列不可再分割的操作集合,这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行。

大白话:事务就是把一组操作打包到一起,执行的时候来保证这一组操作能被正确的执行。

例如网上购物的交易过程:

  • 更新客户所购商品的库存信息
  • 保存客户付款信息
  • 生成订单并且保存到数据库中
  • 更新用户相关信息,例如购物数量等

在正常情况下,这些操作都将顺利执行,最终交易成功,与交易相关的所有数据库信息也成功地更新。但是,如果遇到断电或者是其他意外情况,导致这一系列过程中任何一个环节出了差错,都将导致整个交易过程失败。而一旦交易失败,数据库中所有信息都必须保持交易前的状态不变。否则数据库的信息将会不一致,或者出现更为严重的不可预测的后果。

用伪代码表示事务过程大致是这样的:

try {
    // 设置事务手动提交
    // 默认为自动提交,每当执行一个 update,delete 或 insert 时会自动提交到数据库,无法回滚事务
    connection.setAutoCommit(false);
    
    数据库操作...

    // 提交事务
    connection.commit();
} catch(Exception ex){
    connection.rollback();
} finally {
    connection.setAutoCommit(true);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 事务四大特性

  • 原子性
    事务是一个原子性质的操作单元,事务里面的对数据库的操作要么都执行,要么都不执行(回滚机制)。
  • 一致性
    事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态。
  • 隔离性
    一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
  • 持续性
    也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其执行结果有任何影响。

# 事务的隔离级别

MySQL 提供了四种隔离级别,用来应对多个客户端同时操作数据库的某张表这种情况。根据这种机制,可以让不同的事务在操作数据时,具有隔离性。从而保证数据的一致性。

以下是四种隔离级别:

  • 读未提交(read uncommitted)
    在这种隔离级别下,所有事务能够读取其他事务未提交的数据。读取其他事务未提交的数据,会造成脏读。因此在该种隔离级别下,不能解决脏读、不可重复读和幻读。
  • 读已提交(read committed):Oracle 和 SQLServer 默认的隔离级别 在这种隔离级别下,所有事务只能读取其他事务已经提交的内容。能够彻底解决脏读的现象。但在这种隔离级别下,会出现一个事务的前后多次的查询中却返回了不同内容的数据的现象,也就是出现了不可重复读
  • 可重复读(repeatable read):MySQL 默认的隔离级别
    在这种隔离级别下,所有事务前后多次的读取到的数据内容是不变的。也就是某个事务在执行的过程中,不允许其他事务进行 update 操作,但允许其他事务进行 add 操作,造成某个事务前后多次读取到的数据总量不一致的现象,从而产生幻读
  • 可串行化(serializable)
    在这种隔离级别下,所有的事务顺序执行,所以他们之间不存在冲突,从而能有效地解决脏读、不可重复读和幻读的现象。但是安全和效率不能兼得,这样事务隔离级别,会导致大量的操作超时和锁竞争,从而大大降低数据库的性能,一般不使用这样事务隔离级别。

总结一下如下表所示:(√ 表示解决,× 表示未解决)

  读未提交(read uncommitted)  ×  ×  ×  读已提交(read committed)  √  ×  ×  可重复读(repeatable read)  √  √  ×  可串行化(serializable)  √  √  √  隔离级别  脏读  不可重复读  幻读

# 无隔离性的问题

如果事务没有隔离性,就会引发脏读、不可重复读和幻读的问题。

# 脏读(读取未提交数据)

A 事务读取 B 事务尚未提交的数据,此时如果 B 事务发生错误并执行回滚操作,那么 A 事务读取到的数据就是脏数据。

以转账与取款操作为例:

  时间顺序  转账事务  取款事务  1  开始事务  2  开始事务  3  查询账户余额为 2000 元  4  取款 1000 元,余额被更改为 1000 元  5  查询账户余额为 1000 元(产生脏读)  6  取款操作发生未知错误,事务回滚,余额变更为 2000 元  7  转入 2000 元,余额被更改为 3000 元(脏读的 1000+2000)  8  提交事务  备注  按照正确逻辑,此时账户余额应该为 4000 元

解决脏读的办法:给写操作加锁。

B 事务在修改数据的过程中(提交之前),A 事务尝试读,就会阻塞,一直阻塞到 B 提交数据之后,A 才能读取数据。

但要注意的是:引入写加锁,事务的并发程度就降低了,效率也就低了,隔离性高了。

# 不可重复读(前后多次读取,数据内容不一致)

事务 A 在执行读取操作,由整个事务 A 比较大,前后读取同一条数据需要经历很长的时间。而在事务 A 第一次读取数据,比如此时读取了小明的年龄为 20 岁,事务 B 执行更改操作,将小明的年龄更改为 30 岁,此时事务 A 第二次读取到小明的年龄时,发现其年龄是 30 岁,和之前的数据不一样了,也就是数据不重复了,系统不可以读取到重复的数据,成为不可重复读。

  时间顺序  事务A  事务B  1  开始事务  2  第一次查询,小明的年龄为 20 岁  3  开始事务  4  其他操作  5  更改小明的年龄为 30 岁  6  提交事务  7  第二次查询,小明的年龄为30岁  备注  按照正确逻辑,事务A前后两次读取到的数据应该一致

解决不可重复读的办法:读也加锁。

但要注意的是:引入读加锁,事务的并发程度就更低了,效率也就更低了,隔离性就更高。

# 幻读(前后多次读取,数据总量不一致)

事务 A 在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务 B 执行了新增数据的操作并提交,这个时候事务 A 读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。

  时间顺序  事务A  事务B  1  开始事务  2  第一次查询,数据总量为 100 条  3  开始事务  4  其他操作  5  新增 100 条数据  6  提交事务  7  第二次查询,数据总量为 200 条  备注  按照正确逻辑,事务 A 前后两次读取到的数据总量应该一致

解决幻读的办法:串行化。

此时的并发程度最低,效率也最低,但是数据的可靠性最高。

# 设置事务的隔离级别

首先查看当前的隔离级别:

select @@tx_isolation;
1

然后设置事务的隔离级别:

set session transaction isolation level 事务级别;
1

其中 事务级别 的可选值为:

  • read uncommitted:读未提交,所有事务都可以看到其他未提交事务的执行结果。比较少用,因为性能提升不大,但容易引起脏读。
  • read committed:读已提交,能够彻底解决脏读的现象,但可能会产生不可重复读的现象。
  • repeatable read:可重复读,能够彻底解决脏读和不可重复读的现象,但依然会产生幻读的现象。
  • serializable:串行化,能有效地解决脏读、不可重复读和幻读的现象,但是大大降低了数据库的性能,一般也不常用(安全和效率不能兼得)。

# 总结一下

幻读和不可重复读都是读取了另一条已经提交的事务(脏读不是这样),所不同的是不可重复读查询的是同一个数据项,而幻读针对的是一批数据整体(比如数据的个数)。

  • 不可重复读的重点在修改,在同一事务中,同样的条件,第一次读取的数据和第二次读取的数据不一样(因为中间有其他事务提交了修改)。
  • 幻读的重点在删除和插入,在同一事务中,同样的条件,第一次读取的记录数第二次读取的记录数不一致(因为中间有其他事务提交了插入和删除操作)。

因此解决不可重复读的问题只需锁住满足条件的那一行数据,解决幻读需要锁表。

# 后续

后面可能还要学习一下乐观锁、悲观锁。

(完)