MySQL事务

5 阅读9分钟

事务

事务是一组操作的集合,是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交货撤销操作请求,即这些操作要么同时成功,要么同时失败

四大特性(ACID)

A(Atomicity):原子性,事务是原子的,其中的操作要么同时成功,要么同时失败
C(Consistency):一致性,事务完成时,所有的数据保持一致的状态
I(Isolation):隔离性,事务与事务之间是隔离的,互不影响
D(Durability):持久性,事务一旦提交或回滚,数据就会持久化到磁盘中,数据是持久的

并发事务问题

脏读:一个事务读到另一个事务还没有提交的数据
假设A和B两个事务同时进行,事务A先从数据库中读取了用户的余额并进行了更新操作,此时事务A还没有提交,然后事务B也从数据库中读取用户的余额,那么事务B读取到的余额就是事务A更新后的数据,如果事务B读取完数据之后,事务A发生了回滚,事务B得到的数据就是错误的数据
不可重复读:一个事务先后读取同一条数据,但是两次读取到的数据不同
假设A和B两个事务同时进行,事务A先读取了用户余额,如果此时事务B更新了这条数据,并提交了事务。当事务A再次读取这条数据时,前后两次读取的数据不一样
幻读:一个事物按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在
假设A和B两个事务同时进行,事务A先使用条件查询查出了100条数据,然后事务B使用同样的条件查出了100条数据,之后事务A插入了一条满足条件的数据,并提交了事务。之后事务B再次条件查询,就会查出101条数据,与前一次读到的记录数不同

事务隔离级别

读未提交:事务可以读取其他事务未提交的修改
读已提交:只能读取其他事务已提交的修改,解决了脏读问题
可重复读:事务开始时创建快照,整个事务读取同一份快照数据。MySQL数据库默认的隔离级别,解决了不可重复读问题
串行化:所有事务串行执行,解决了所有并发事务问题
MySQL的InnoDB引擎默认使用的事务隔离级别是可重复读,它很大程度上避免了幻读现象,解决方案有以下两种

  1. 针对快照读(普通select语句),是通过MVCC方式解决了幻读,因为可重复读隔离级别下,事务执行过程中看到的数据,跟这个事务启动时看到的数据一直是一致的,即便中途有其他事务插入了一条数据,也无法被当前事务查询到
  2. 针对当前读(select ... for update),是通过next-key lock(记录锁+间隙锁)方式解决了幻读,因为在执行select ... for update语句时,会加上next-key lock,如果有其他事务在next-key lock锁范围内插入了一条数据,那么这个插入语句就会被阻塞,无法成功插入

Read View

Read View是InnoDB引擎在可重复读(RR)和读已提交(RC)隔离级别下,实现MVCC的核心组件,本质上就是一个数据快照,对于RR和RC,其区别就只是生成Read View的时机不同 RC是在启动事务的时候就生成一个Read View,然后整个事务期间都只使用这一个Read View
RR是在每个语句执行前都会重新生成一个Read View

Read View中有四个重要字段

  • m_ids:指的是在创建Read View时,当前数据库中活跃事务的事务id列表(活跃事务指的是启动了但还没有提交的事务)
  • min_trx_id:指的是在创建Read View时,当前数据库中活跃事务中事务id最小的事务,也就是m_ids的最小值
  • max_trx_id:指的是创建Read View时,当前数据库中应该给下一个事务的id值,也就是全局事务中最大的事务id值+1
  • creator_trx_id:指的是创建改Read View的事务的事务id 对于使用InnoDB存储引擎的表,其聚簇索引记录中都包含以下两个隐藏列
  • trx_id:当一个事物对某条聚簇索引记录进行改动时,就会把该事务的事务id记录在trx_id隐藏列里
  • roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到undo日志中,然后这个隐藏列是一个指针,指向每一个旧版本记录

MVCC

当一个事务取访问记录时,有以下几种情况

  1. 如果记录的trx_id小于Read View中的min_trx_id,表示这个版本的记录是在创建Read View前已经提交的事务生成的,所以该版本的记录对当前事务可见
  2. 如果记录的trx_id大于等于Read View中的max_trx_id,表示这个版本的记录是在创建Read View后才启动的事务生成的,这个版本的记录对当前事务不可见
  3. 如果记录的trx_id值在min_trx_id和max_trx_id之间,需要判断trx_id是否在m_ids列表中
    如果trx_id在m_ids列表中,表示生成该版本记录的活跃事务依然活跃着,所以该版本的记录对当前事务不可见
    如果trx_id不在m_ids列表中,表示生成该版本记录的活跃事务已经提交,所以该版本的记录对当前事务可见
    这种通过版本链来控制并发事务访问同一个记录时的行为就叫做MVCC(多版本并发控制)

可重复读

可重复读隔离级别是在启动事务时生成一个Read View,然后整个事务期间都是用这个Read View
假设启动了事务A(trx_id为51)和事务B(trx_id为52)

  • 在事务A的Read View中,trx_id是51,由于启动时只有事务A是活跃的,所以m_ids是51,min_trx_id也是51,max_trx_id是52
  • 在事务B的Read View中,trx_id是52,由于事务B启动时,事务A也是活跃的,所以m_ids是51,52,min_trx_id是51,max_trx_id是53
    然后在可重复读的隔离级别下,事务A和事务B按顺序执行了以下操作
  • 事务B读取了用户的账户余额,读取到的余额是100万
  • 事务A将用户的账户余额修改成200万,没有提交事务
  • 事务B读取到用户的账户余额,还是100万
  • 事务A提交事务
  • 事务B读取到用户的账户余额,依旧是100万 事务B在第一次读取用户的账户余额,在找到记录后,会先看这条记录的trx_id,发现trx_id是50,比事务B的Read View中的min_trx_id还更小,这就意味着这条记录是在创建Read View之前就已经提交过了,所以这个版本的记录对事务B是可见的,因此事务B可以读取到这条记录
    接着事务A通过update语句对这个记录进行了修改,并且没有提交事务,将用户的余额改成200万,并且MySQL会记录相应的undolog,并以链表的形式串联起来,形成版本链
    然后事务B第二次去读取用户余额,发现这条记录的trx_id是51,在事务B的Read View的min_trx_id和max_trx_id之间,且在m_ids里面,这说明事务A修改了数据,但是还没有提交事务,因此事务B并不能读取到当前版本的数据,而是根据undolog链找到旧版本的记录,并读取满足条件的旧记录,也就是trx_id为50的记录
    最后,事务A提交事务后,由于隔离级别是可重复读,所以事务B再次读取记录时,还是根据启动事务创建的Read View来判断当前记录是否可见,所以,即使事务A修改了用户余额并且提交事务,事务B依旧无法读取到修改后的数据

读已提交

这个隔离级别是在每次读取数据后,都会生成一个新的Read View
这就意味着,事务期间多次读取同一条数据,前后两次读取的数据可能会出现不一致,因为同一时间内,其他的事务修改了这条数据,并提交了事务
依旧假设事务A(trx_id为51)和事务B(trx_id为52)

  • 事务B读取数据(创建一个Read View),用户余额为100万
  • 事务A将用户余额修改为200万,并且没有提交事务
  • 事务B读取数据(创建新的Read View),用户余额为100万
  • 事务A提交数据
  • 事务B读取数据(创建新的Read View),用户余额为200万

事务B在第一次和第二次读取数据时,由于此时事务A处于活跃状态,因此Read View中的min_trx_id都是51,事务A修改数据后,记录的trx_id变成51,然后事务B第二次读取记录,记录的trx_id在min_trx_id和max_trx_id之间,且在m_ids中,也就说明此时这条记录对事务B是不可见的,所以要找到旧版本的数据并读取
事务B第三次读取数据时,此时事务A依旧提交,因此Read View中的min_trx_id就变成了52,再次读取记录,记录的trx_id是51,小于事务B Read View中的min_trx_id,就意味着这条记录在创建Read View之前就已经被提交了,也就说明此时这条记录对事务B是可见的