1、什么是数据库事务

事务是由一个有限的数据库操作序列构成,这些操作要么全部都执行,要么全部都不执行,通俗的说同生共死,是一个不可分割的工作单位。

事务的存在,其实就是为了保证数据库的数据一致性

2、事务的特性

ACID,分别对应原子性,一致性、隔离性、持久性

原子性:事务作为一个整体被执行,包含在其中的数据对数据库的操作,要么全部执行,要么都不会执行

一致性:事务开始之前和事务结束之后,数据不会被破坏,A给B转100元,不管成功与否,A和B的总金额是不变的

隔离性:多个事务并发访问时,事务之间是相互隔离的,一个事务不应该被其他事务所干扰,多个并发事务之间要相互隔离

持久性:事务完成提交后,改事务对象数据库所做的更改,将会永久的保存在数据库之后

3、事务并发带来的问题

事务的并发会引起"脏读","不可重复读","幻读"的并发问题

3.1 脏读

概述:如果一个事务读取到了另外一个未提交事务修改后的数据,则会导致前一个事务查询数据前后不一致的现象,即脏读

示例:A和B俩个事务

  • A的余额是100,事务A正在查询tom账户的余额
  • 事务B先扣减tome的余额,扣了10,但是还没有提交
  • 事务A读取tom账户的余额,发现只有90

这即是脏读,因为事务A读取到了事务B未提交的数据,从而导致前后数据不一致的现象

3.2不可重复读

概述:如果事务A读取到了另外一个提交事务修改后的数据,导致事务A前后查询数据不一致的现象,即不可重复读

示例:

  • 事务A查tom的数据,结果是100
  • 事务B对tom账号余额进行扣减10元,然后提交事务
  • 事务A再次查询tom的账号余额,发现余额变成了90

事务A被事务B干扰到了,在事务A的范围内,俩个相同的查询,读取同一条数据,返回了不同的数据,即不可重复读

3.3 幻读

概述:如果一个事务根据搜索条件查询出一批数据,在该事务未提交前提下,另外一个事务B写入了一些符合条件的数据,这样导致事务A前后查询,发现数据量有所变化,即所谓的幻读

示例:

  • 事务A先查询id大于2的账号记录,得到了2条记录‘
  • 事务B插入一条id=4的数据,并且提交事务
  • 事务A再去查询,却得到了三条数据

事务A查询一个范围内的结果集,另一个并发事务再这个范围内插入新的数据,并提交事务,然后事务A再去查询,发现俩次读取的数据结果集是不一致的

4、事务的隔离级别

为了解决并发事务存在的脏读、幻读、不可重复读等问题,数据库设计了四种隔离级别,分别是读未提交读已提交可重复读串行化

4.1 读未提交

该隔离级别限制俩个数据不能同时修改,但是修改数据的时候,即使事务未提交,都是可以被别的事务读取到,这种隔离级别有脏读,幻读,不可重复读的问题

4.2 读已提交

读已提交隔离级别,当前事务只能读取到其他事务已经提交的数据,这种隔离级别解决了脏读,但是不可重复读幻读的现象解决不了

4.3 可重复读

该隔离级别,限制了读取数据的时候,不可以进行修改,解决了不可重复读的问题,但是读取范围数据的时候,是可以插入数据,所以依旧存在幻读的现象

4.4 串行化

事务的最高隔离级别,所有事务都是进行串行化顺序执行的,可以避免脏读,不可重复读,幻读的并发问题,但是这种隔离级别下,事务执行很耗性能

mysql事务隔离级别是null mysql事务隔离级别实现原理_mysql

 针对这四种事务的隔离级别,数据库是如何实现的

数据库是通过枷锁来实现事务的隔离性,串行化隔离级别就是通过枷锁实现的,但是频繁加锁,导致读取数据的时候,没法进行修改,修改数据的时候没法读取,大大降低了数据的性能

而数据库为了在性能和并发问题俩者之间得到一个均衡,引用了MVCC多版本并发控制,可以让读取数据的同时进行修改数据,修改数据的时候同时能够读取

5、MVCC 多版本并发控制

数据库中同时存在多个版本的数据,并不是整个数据库的多个版本,而是每一条记录都具有多个版本同时存在,在某个事物对其进行操作的时候,需要查看这一条记录的隐藏列事物版本ID,比对事物id并根据事物隔离级别去判断读取哪个版本的数据

数据库的隔离级别读已提交,可重复读都是基于MVCC实现的,相对于加锁的方式,MVCC能够更好的处理读写冲突,有效的提高数据库的并发性能

5.1 事物版本号

对于InnoDB存储引擎,每一行记录都有俩个隐藏列trx_id,roll_pointer,如果表中没有主键和非NULL唯一建的时候,则还会有第三个隐藏的主键列row_id

mysql事务隔离级别是null mysql事务隔离级别实现原理_隔离级别_02

 5.2 undo log

回滚日志,用于记录数据被修改前的信息,在表记录修改之前,会先把数据拷贝到undo log中,如果事务回滚,即可以通过undo log来还原数据

undo log保证事务的原子性和一致性

用于MVCC快照读

5.3 版本链

多个事务并行操作某一行数据,不同事务对该行数据的修改会产生多个版本,然后通过回滚指针(roll_pointer)连成一个链表,这个链表就称为版本链

mysql事务隔离级别是null mysql事务隔离级别实现原理_数据_03

5.4 快照读、当前读

 快照读:读取的是记录数据的可见版本,不加锁,普通的select语句都是快照读

当前读:读取记录数据的最新版本,显示加锁的都是当前读

select * from table where id=2 for update

select * from table where id>2 lock in share node;

5.5 Read View

事务执行SQL语句的时候,会生成读视图,在innodb中,每个sql执行前都会得到一个Read View

它主要是用来做可见性判断的,即判断当前事务可见哪个版本的数据,常见的属性:

m_ids:当前系统中活跃(未提交)的读写事务ID,它数据结构是一个List

min_limit_id:表示在生成Read View时,当前系统中活跃的读写事务中最小的事务id,即m_ids中的最小值

max_limit_id:表示生成Read View ,分配给下一个事务的id值

create_trx_id:创建当前Read View的事务ID

Read View匹配条件的规则:

  1. 如果数据事务ID trx_id<min_limit_id,表明生成该版本的事务再生成read view前,已经提交了事务,所以该版本可以事务可见
  2. 如果trx_id>=max_limit_id 表明生成该版本的事务再生成ReadView之后才生成,所以该版本不可以被当前事务访问
  3. 如果min_limit_id =<trx_id < max_limit_id,分成3种情况

     1)如果m_ids包含trx_id,则代表Read View生成时刻,这个事务还未提交,但是如果数据的trx_ud等于create_trx_id的话,表明数据是自己生成的,因此是可见的

     2)如果m_ids包含trx_id,并且trx_id不等于create_trx_id,则Read view生成时,事务未提交,并且不是自己生产的,所以是不可见的

     3)如果m_ids不包含trx_id,则说明这个事务再Readview生成之前就已经提交了,修改的结果,当前事务是可见的

查询数据的基本流程:

1、获取事务自己的版本号,即事务ID

2、获取Read View

3、查询得到的数据,然后ReadView中的事务版本号进行比较

4、如果不符合可见性规则,就需要undo log中的历史快照

5、返回符合规则的数据

 6、 读已提交(RC)隔离级别

  • 创建user表,插入一条初始化数据

mysql事务隔离级别是null mysql事务隔离级别实现原理_数据_04

  •  隔离级别设置成RC(读已提交),事物A和事物B同时对user表进行查询和修改操作,操作顺序如下

最后查询得到的结果是name=曹操的记录,基于MVCC我们可以很明确的知道工作的原理

1、事物A开启事物,得到一个id:100

2、事物B开启事务,得到事务ID:101

3、事务A生成一个Read View,信息如下

变量


m_ids

100,101

max_limit_id

102

min_limit_id

100

create_trx_id

100

根据读视图的可见性规则

min_limit_id(100)=<trx_id(100)<max_limt_id(102)

create_trx_id = trx_id = 100

由此可得,trx_id=100这条记录,当前事务是可见的,所以查到name的值=孙权

  • 事务B进行修改操作,把name改成曹操,把原数据拷贝到undo log,然后对数据进行修改,标记事务ID和上一个数据版本再undo log的地址
  • 提交事务
  • 事务A进行查询,再生成一个读视图

变量        


m_ids

100

max_limit_id

102

min_limit_id

100

create_trx_id

100

然后再次回到版本立链,从版本链中挑选可见的记录

mysql事务隔离级别是null mysql事务隔离级别实现原理_数据_05

从图中可以知道,最新版本的列name=操作,该版本的值trx_id=101,通过可见性规则

min_limit_id(100)=<trx_id(101)<max_limit_id(102)

但是trx_id=101,不属于m_Ids的集合 ,因此trx_id=101这个记录,对于当前事务是可见的,所以查询到的name值:操作

综上所述,在RC(读已提交)的隔离级别下,同一个事物里,相同的查询读取同一条记录,返回了不同的数据。因此RC隔离级别,存在不可重复读的并发问题

7、可重复读(RR)隔离级别

RR的隔离级别下,将能解决不可重复读的并发问题

mysql事务隔离级别是null mysql事务隔离级别实现原理_mysql_06

在RC隔离级别下,同一个事物里面,每一次查询都会产生一个新的Read View副本,这样就可能造成同一个事务前后读取数据可能不一致的问题

而在RR隔离级别下,一个事务只会获取一次read view视图,从而保证每次查询的数据都是一样的

7.1 实例分析 

事务A再次查询,还是用最开始的Read View副本

mysql事务隔离级别是null mysql事务隔离级别实现原理_数据_07

mysql事务隔离级别是null mysql事务隔离级别实现原理_mysql事务隔离级别是null_08

 从图中最新的版本name的内容是操作,该版本的trx_id:101,进行可见性规则刻制

min_limit_id(100)=<trx_id(101) <max_limit_id(102)

因为m_ids(100,101)包含trx_id(101)

并且creator_trx_id(100) 不等于trx_id(101)

所以trx_id(101)这个记录,对于当前事务是不可见的,这时候根据版本连roll_pointer跳到下一个版本,trx_id=100这个记录,再次校验可见性

min_limit_id(100)=<trx_id(100)<max_limit_id(102)

因为m_ids(100,101)包含trx_id(100)

并且create_trx_id(100)等于trx_id(100)

所以这个记录,对于当前事务是可见的,所以俩次查询的结果是一致的。即在可重复度读的隔离级别下,解决了不可重复读的问题

RR和RC本质的区别是。前者是共用一套Read View副本,后者每次查询的时候都会生成一个新的Read View。从而导致最终的结果不一致