vlambda博客
学习文章列表

Mysql事务并发问题与隔离级别深入解析

本文目录

事务基础

事务控制语句

事务并发问题

  • 脏读

  • 不可重复读

  • 幻读

  • 第一类丢失更新(回滚丢失)

  • 第二类丢失更新(逻辑丢失)

用隔离级别来解决并发问题



事务基础


事务四大特性ACID:

  • 原子性

  • 一致性

  • 隔离性

  • 持久性


有些数据库可能不会完全遵循ACID,比如oracle的默认隔离级别是read committed,不满足隔离性,但是能带来性能上的提升。mysql的默认隔离级别是read repeatable,完全满足ACID。
这里留下一个伏笔:一般来说,只有将事务隔离级别设置为串行化才能完全满足ACID,为什么Mysql的可重复读就能完全满足ACID呢?别急,下面会讲到。

事务分类:
  • 扁平事务:最常用的;

  • 带有保存点的扁平事务:可以回滚到某一个保存的,而不是回滚到事务最开始的地方;

  • 链事务;

  • 嵌套事务:Mysql不支持,如果有多个begin语句,则后面的begin会隐式提交;

  • 分布式事务;


Mysql InnoDB引擎对于事务的实现:
  • 隔离性:锁

  • 原子性、持久性:redo log

  • 一致性:undo log


不好的事务习惯
  • 在循环中提交事务

  • 使用自动提交

  • 使用自动回滚


什么是长事务?
  • 执行时间比较长的事务,可以转化为小批量事务来处理。



事务控制语句

开启事务
  • begin

  • start transaction

提交事务
  • commit

回滚事务
  • rollback


什么是隐式提交语句?
  • 比如create table类的ddl语句,这类语句有很多,在次不一一列举。


查看提交次数(不会计算隐式提交)
  • show global status like 'com_commit';


查看自动提交模式是否开启
  • show variables like '%autocommit%';


sql标准定义的四个隔离级别:
  • read uncommitted

  • read committed

  • repeatable read

  • serializable


InnoDB修改事务隔离级别:
  • set global transaction isolation level 隔离级别

  • set session transaction isolation level 隔离级别

查询事务隔离级别:
  • select @@tx_isolation

  • select @@global.tx_isolation


事务并发问题

脏读
一个事务读到了另一个事务中未提交的数据。

不可重复读

一个事务在未提交之前,多次读取同一数据,但读到的结果却是不一样的,称为不可重复读。发生不可重复读的原因是读到了另一个事务已提交的数据。与脏读的区别是,脏读读的是未提交的数据,而不可重复读读的是已提交的数据。

幻读

一个事务在未提交之前,多次读取数据,第一次读到了n条记录,但第二次却读到了>n或<n条数据。幻读其实也是 不可重复读,但幻读侧重的是数据的新增或删除,而不可重复读侧重的是同一行数据的修改。

第一类丢失更新(回滚丢失)

看如下步骤:
  1. 在有一行数据,某个字段值为200

  2. 事务A开启事务

  3. 事务B开始事务

  4. 事务A将200修改为300,并提交

  5. 事务B将200修改为100,但事务B回滚,回滚之后变为200


最终的值应该是300的,但事务B的回滚使得值重新变为200,覆盖了300,这种现象称为第一类更新丢失。
SQL92没有定义这种现象,标准定义的所有隔离级别都不允许第一类丢失更新发生。
InnoDB通过undo日志回滚,比如我执行了一条delete语句,那么在undo中记载的就是对应的insert语句,如果我执行的是一条update语句,那么undo中记载的就是与之相反的update语句。
事务B回滚时,只会执行自己所产生的undo语句,因此不会覆盖事务A的更新。


第二类丢失更新(逻辑丢失)

首先看如下步骤:
  1. 事务A开启事务,并修改了一行数据;

  2. 事务B开启事务,修改了同一行数据;

  3. 事务A提交;

  4. 事务B提交;


由于数据库的update操作会上X锁,并且只有在事务提交和回滚后才会释放锁,因此第二步操作是会阻塞的,就算是最低级别的隔离也会阻塞。因此两次更新操作是顺序进行的,且第二次更新是在第一次更新提交后才进行的(提交才释放锁),因此数据库本身的排它锁机制保证了不会发生更新丢失。

但这里要说的丢失更新,是逻辑情况下的丢失更新,再看如下步骤:
  1. 事务A开启事务,读取了一行数据,假设该行数据有一个age字段,且age=10;

  2. 事务B开启了事务,读取了同一行数据,即age=10。

  3. 事务A进行修改操作,使新age=原age+10,然后提交。

  4. 事务B进行修改操作,使新age=原age+20,注意原age,事务B使用的是自己一开始读取的10,而不是事务A提交后的20,因此修改后的age=30,然后提交;


上述操作会发生丢失更新,因为如果不发生丢失更新则最终的age=40,但最终的age=30,事务B覆盖了事务A的更新。

解决丢失更新问题需要使用序列化读的方式:
  • 将隔离级别设为序列化读;

  • 对读取操作上排它锁,select ... for update;


用隔离级别来解决并发问题

事务并发问题
事务隔离级别
第一类更新丢失


读未提交
脏读


读已提交(MVCC:读最新版本,因此造成了不可重复读)
不可重复读

幻读


可重复读(MVCC:读旧版本;next-key Locking解决幻读)
第二类丢失更新


序列化读

该表能清晰地展示出,相应的隔离级别能解决相应的并发问题。比如读已提交能解决第一类更新丢失和脏读,因为从表格的位置来看它们都在读已提交的上方。

读已提交能解决脏读,而读已提交在MVCC机制中是快照读,会造成不可重复读,可重复度在MVCC机制中是当前读,能解决不可重复读问题。
而第二类丢失更新由于是逻辑丢失,因此只能采用序列化读或对select语句上排它锁才能避免。

可重复读隔离级别利用next-key locking解决幻读请参考我的 Mysql锁原理深入解析 这篇文章!