事务与MySQL

66 阅读4分钟

一、背景

还是以经典的银行转账场景作为介绍事务的背景吧。

tips:本文图片取自xiaolincoding.com/mysql/trans…

账户A给账户B转账10000元,对于此业务,具体会有如下操作

image.png

此时,如果在执行完第三步操作,突然系统故障,没有继续执行后续的操作,这就会导致账户A扣款了,账户B却没有增加钱款。所以就需要一种措施,来保证转账时的安全。也就是账户A扣款,账户B打款这两个操作必须都发生,事务就是为了解决这类问题的。

二、事务特性

在MySQL中不同的存储引擎对事务的支持不同,常见的InnoDB是支持事务的。事务必须要遵循四个特性ACID

  • 原子性:顾名思义,事务应该像原子一样,事务中的操作要么都成功,要么都失败,一荣俱荣,一损俱损
  • 隔离性(重点):事务在并发场景下,应该互不影响
  • 持久性:事务完成之后,对数据的修改应该是永久的
  • 一致性(目标):事务操作前后,数据需要满足完整性约束,账户A少了10000元,账户B应该增加10000元

三、并发下的MySQL

MySQL允许多个客户端同时连接,在高并发下,会出现脏读、不可重复读、幻读的问题。

脏读

事务A读到了事务B修改但未提交的数据,就是脏读。通俗一点讲,就是事务B修改了数据,但是没有提交,可能随时发生回滚,事务A读到的数据不一定是事务B操作后的最终数据。

image.png

不可重复读

同一个事务内,多次读取同一个数据,读到的数据不一致,称为不可重复读。

image.png

幻读

在一个事务内,多次查询某个符合条件的记录数量,前后数量不一致,称为幻读。

image.png

四、隔离级别

前面提到在并发场景下,可能会出现脏读、不可重复读、幻读现象,其严重性程度排序如下:

脏读>不可重复读>幻读

SQL标准提供了四种隔离级别来规避这些现象,分别是:

  • 读未提交
  • 读已提交
  • 可重复读(MySQL InnoDB的默认隔离级别)
  • 串行化

需要注意的是,隔离级别越高,性能越低。

不同的隔离级别,在并发场景下会出现不同的现象,一般在实际生产中不会选择串行化。

image.png

五、MVCC

MVCC:多版本并发控制。MySQL通过ReadView实现多版本并发控制,ReadView相当于是数据库某个时刻的快照。

ReadView有四个字段,分别是

  • creator_trx_id:创建该ReadView的事务的事务id
  • m_ids:活跃事务的事务id列表(已创建但未提交的事务)
  • min_trx_id:m_ids中的最小值
  • max_trx_id:m_ids中的最大值+1

MySQL在InnoDB中会为记录自动生成两个隐藏列,用于保存记录的版本信息,分别是:

  • trx_id:与当前记录有关的事务id
  • roll_pointer:指向旧版本记录的指针,旧版本记录会被写入undo日志

多版本并发控制的过程

有了上面的介绍,我们就可以知道MySQL是如何进行多版本并发控制的。其实就是对比记录的trx_id值与m_ids、min_trx_id、max_trx_id值的关系。当一个事务去访问记录的时候会有如下情况

  • trx_id<min_trx_id:表示该版本的记录是在创建ReadView前已经提交得事务,所以该版本的记录对当前事务可见
  • trx_id>man_trx_id:表示该版本的记录是在创建ReadView后启动的事务生成的,所以该版本的记录对当前事务不可见
  • min_trx_id<trx_id<max_trx_id,且trx_id是m_ids列表中的一个值:表示生成该版本记录事务尚未提交,所以该版本的记录对当前事务不可见
  • min_trx_id<trx_id<max_trx_id,且trx_id是m_ids列表中的一个值:同理可见

介绍了MVCC,大家可以思考一下,可重复读和读已提交是怎么实现的。其实很简单就是ReadView的生成时机

  • 可重复读:在事务开始时就创建一个ReadView,整个事务期间都在用
  • 读已提交:每次读取数据都会生成一个ReadView