MySQL的事务和并发问题浅析

数据库的事务(Transaction)处理技术是很重要的概念,下面结合MySQL讲讲自己对这类概念的理解。

一、事务的基本概念

所谓事务是用户定义的、不可分割的一组操作序列,这些操作只能全做或全都不做,不能存在中间状态。涉及到用户定义,MySQL为我们提供了三种定义事务的语句:

start transaction | begin  # 开始一个新事务

commit   # 提交当前事务,并将修改持久化到数据库

rollback  # 回滚当前事务,取消所有操作

而MySQL初始设置会将autocommit(自动提交)设置为1,表示用户对数据库的每个操作都作为一个事务自动提交,仅当使用 start transaction 或 begin 开始一个新事务后,autocommit才会临时失效,直到出现事务结束语句(commit | rollback)。(在事务未结束之前开启另一个新事务会自动commit)

注意:DDL语句是不能回滚的,包括create、drop、alter等。

二、事务的四个特性

事务具有4个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),简称ACID特性。

原子性:事务作为数据库的逻辑工作单位,是不可分割的,事务内的操作要么全做,要么全不做。

一致性:事务的执行结果必须使数据库从一个一致性状态转换到另一个一致性状态。一致性是和原子性密切相关的特性,一致性状态的转换必然要经过原子性的操作。若数据库在完成事务时突然发生故障而中断,原子性不能保证,那么数据库内的数据就会出错,处于不一致的状态。

隔离性:事务的执行不能互相干扰,即一个事务内的操作和使用的数据对其他并发事务是隔离的,不可见的。事务将数据库由一个一致性状态转换为另一个一致性状态时,不能出现非本事务内的操作或数据,即不能被其他事务干扰。

持久性:一个事务一旦提交,它对数据库的修改应该是永久性的,不会因接下来的操作或故障影响已提交的事务。

事务是恢复和并发控制的基本单位,要保证事务的正常就一定要保证事务的ACID特性。事务的ACID特性被破坏可能有以下的原因:

  1. 多个事务并行执行,不同事务的操作交叉执行。
  2. 由于外部因素数据库异常停止,导致事务在运行过程中被强制中断。

第2个因素会导致事务的原子性遭到破坏,进而数据库处于不一致的状态,避免这类问题是数据库恢复机制的责任,这里暂时不提。第1个因素则会破坏事务的原子性、一致性和隔离性,避免这类问题也就是接下来要说的并发控制的责任。

三、数据库的并发控制

3.1 并发带来的问题

考虑一个超市的两台收银机A、B的活动序列:

  1. A读出当天营业额X(事务A)
  2. B读出当天营业额X(事务B)
  3. A卖出价值为a的商品,修改营业额为X+a,并写回数据库(事务A完成)
  4. B卖出价值为b的商品,修改营业额为X+b,并写回数据库(事务B完成)

不考虑ACID,最终的营业额是X+b,A的修改丢失,导致事务A处于不一致的状态,这种不一致的状态是由并发操作引起的。

并发带来的问题可以分为三种:1. 丢失修改;2.不可重复读;3.读“脏”数据。丢失修改也叫脏写,MySQL的所有隔离级别都不允许脏写。下面看其余两种问题。

3.2 读“脏”数据

也称脏读。在解释脏读前,建议先看一下关于InnoDB的脏页刷新机制:MySQL中InnoDB脏页刷新机制Checkpoint

简单来说,写回磁盘的数据可能是事务提交后的数据,也可能是事务进行中未提交的数据。每次写回磁盘都会建立一个检查点CheckPoint,内存中未写回磁盘的为脏页,事务未提交的数据是脏数据。

下面模拟一下MySQL的脏读。打开两个命令行窗口,并连接到MySQL,运行show processlist发现两个进程分别对应两个窗口的session,我们用这两个窗口模拟并发执行事务的情况。

首先设置窗口的隔离级别为READ UNCOMMITTED(读未提交)

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

  新建一张表my_table,包含两个int类型的列id、cost,id作为主键,插入一条数据,然后按以下顺序执行:

  • A、B两个窗口开始事务(begin)
  • A查询my_table,得到原始数据
  • B修改my_table的数据,不提交
  • A再查询my_table,得到B未提交的数据

MySQL解决这个问题的方法是设置隔离级别为READ COMMITTED(读已提交)。

3.3 不可重复读

不可重复读就是A事务在读取某个数据后,事务B对其进行的修改会让事务A在事务未结束之前读出来,导致了A事务两次读取同一字段时数据不同,数据库状态不一致。

SQL中对数据的修改分为更新、插入和删除,分别对应三种不可重复读的问题,后两种问题也被称为“幻读”。下面我们在MySQL复现这些问题。

3.3.1 第一类不可重复读

首先设置MySQL的隔离级别为READ COMMITTED(读已提交)

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

然后按以下顺序执行:

  • A、B两窗口分别开始事务(begin)
  • A查询my_table,取得原数据
  • B修改这条记录,并commit(commit前A查询出的还是原始数据)
  • 再查询这条记录时,发现B修改后的记录

MySQL解决这个问题的方法是设置隔离级别为REPEATABLE READ(可重复读)。

3.3.2 第二类不可重复读(幻读)

幻读也是一种不可重复读的问题,只是侧重于记录的新增和删除(第一类侧重于修改)。

首先设置MySQL的事务隔离级别为READ COMMITTED(读已提交)

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

 然后按以下顺序执行复现(注意id为自增主键):

  • A、B两窗口分别开始事务(begin)
  • A、B分别查询my_table,得到原始数据
  • A修改表的全部cost为10,同时B也插入一条cost为13的数据。
  • A、B提交后,A再查询,发现还有一条没有修改成功(B新插入的一条)

但是用REPEATABLE READ是不是就完全解决了幻读问题呢?并不是,可重复读级别只能解决一部分幻读问题,还有另一种问题会出现。

设置MySQL的事务隔离级别为REPEATABLE READ(可重复读)

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

按以下顺序复现可重复读的幻读问题:

  • A、B两窗口分别开始事务(begin)
  • A、B分别查询my_table,得到原始数据
  • A使用insert into my_table(cost) values (11)插入一条记录,并自动顺序生成id,且因为读已提交,B不能看到A的插入记录
  • B也使用上述语句插入一条数据,发现id和前面差了2,好像前面已经有了一条数据。
  • 当B想补上之前的id,发现被阻塞,不能插入,好像在修改一条脏数据。

删除记录的复现也类似,表现为A已删除一条记录并提交事务,但是B还是可以查到已删除的记录,而且B不能删掉,像是出现幻觉一样。

(左侧为A事务,右侧为B事务)

这类幻读问题的解决是对查询的数据加锁(select * from my_table where id > 2 for update),这样B可以锁住id大于2的记录的插入,A只有等待B事务提交后才可成功插入记录。

MySQL完全解决幻读问题的方法是设置隔离级别为SERIALIZABLE(串行化)。但是串行化对数据库的并发性能影响较大,一般不采用这个级别的隔离。

四、总结

MySQL提供了4种隔离级别,分别为

  • READ UNCOMMITTED - 读未提交,事务内可读到其他事务未提交的修改
  • READ COMMITTED - 读已提交,事务内可读到其他事务已提交的修改
  • REPEATABLE READ - 可重复读(默认级别),即结果是可以重复读到的,在本事务结束后才能读到其他事务的修改
  • SERIALIZABLE - 串行化,事务不可以交替修改数据
依次解决了脏读、不可重复读和幻读问题,由上到下的并发控制也越来越严格。而且上述四种隔离级别都不支持脏写(写未提交)(假设还有WRITE UNCOMMITTED级别是不支持的)。那么MySQL是如何实现这几种隔离级别的呢?接下来就需要深入研究一下数据库的封锁技术了。

猜你喜欢

转载自www.cnblogs.com/fengg123/p/11030220.html