vlambda博客
学习文章列表

mysql事务原理与实践

导读

Mysql事务是后端开发中经常用到的知识点,很多同学都会比较浅的了解事务的特性和基本原理就进行开发,但并没有深入的进行多各种隔离方式的实践,往往会在多线程处理的时候就会掉进坑里。今天我们就一起深入实践,还原每一个场景。

目录

  • 什么是事务?

  • 事务的特性

  • 多个事务并发会出现什么问题?

  • 事务的几种隔离级别

  • 事务隔离级别的实现原理

什么是事务?

事务是数据库一个有限的操作序列,这些操作要么全部执行,要么全部不执行。
举个例子:
银行的用户A账户余额有100块,这时候用户A要转账100块给用户B,银行已经把A的余额扣除了,但突然银行系统出错了,并没有把100块转到用户B的账户上。这种情况这100块就凭空消失了,当然是大家都不能接受的,所以银行就会一个回滚机制,把100块退回到用户A的账户上。这就是事务,A的100块要不成功转账给B,要不就退回给A,并不会凭空消失。

事务的特性

从上面的例子其实就能反应出事务的4个特点:

原子性

原子性指的就是事务中一系列的操作要不全部执行,要不全部不执行。
在刚刚例子中,如果用户A同时转账50给用户B和用户C,要不都转账成功,用户B、C各收到50块。要不银行把2个50块都退回给用户A,用户A账户余额还是100块。

一致性

一致性指的是在事务开始前和事务操作之后数据是不被破坏的。
在刚刚例子中,不管怎么转账,对于用户A、B、C来说他们手上的总金额都是100,不会凭空消失。

隔离性

隔离性指的是多个事务并发访问的时候,他们之间是相互隔离的,一个事务不会被其他事务干扰。
例如用户B在查看余额的时候,看到的是0块。这时候同时用户A给B转账了100块,B看到的依然是0块,要等到B完成查看后A的100块成功转账后再次查看才能看到B账户里面多了100块。(这里是用了串行化的隔离级别,后面会对各种隔离级别进行介绍,不同的隔离级别会出现不同的效果)

持久性

持久性指的是事务完成提交后,所有对数据库的操作都会被永久保存在数据库中。


多个事务并发会出现什么问题?

刚刚不是说事务有隔离性吗?事务之间互不干扰,那为什么多个事务并发的时候还会出现问题呢?
嗯,在一开始学习的时候,我也有这个疑问。其实可以理解为事务最终要实现的是这4大特性,而为了实现这4大特性的我们需要如何设计?我们需要抱着这个思路进行思考。
在单线程中一个事务我们只要考虑原子性和一致性就可以了,要不全部执行,要不都不执行,且数据前后是一致的。
但我们实践中基本都是多线程并发的,那在隔离性的实现上就会出现以下问题:

脏读

例如:

  1. 用户A准备查询自己账户的余额(100块)

  2. 同时用户B给用户A转账50块(未提交)

  3. 最终用户A查询到自己的余额是150块

实际用户A的账户只有100块,用户A事务查询到时候已经被用户B的事务影响到了

不可重复读

例如:

  1. 用户A查询自己账户的余额(100块)

  2. 同时用户B给用户A转账50块(已提交)

  3. 用户A再查询自己账户的时候余额变成了150块

这次实际用户A的余额的确是150块了,但在用户A的事务里面两次的查询金额应该是一致的才对,现在就是被用户B的事务影响到了。

幻读

例如:

  1. 用户A查询自己有多少条收入记录,查询到了有2条

  2. 同时用户B给用户A转账50块(已提交)

  3. 用户A再查询自己有多少条收入记录时,查询到了有3条记录

用户A在事务中第一次查询了一个结果集,但在用户B的事务中插入了记录导致用户A第二次查询的时候结果集不一样了。

有没有发现不可重复读和幻读很像?都是在一个事务内查询了2次,这里很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

事务的几种隔离级别

那上面提到在并发时候有可能出现的脏读、不可重复读、幻读3个问题,InnoDB到底是怎样解决的呢?下面我们一起到mysql设置不同的事务隔离级别进行实践,看看是如何解决的。

下面是事务的4个隔离级别:

- 读未提交(read-uncommitted)

- 读已提交(read-committed)

- 可重复读(repeatable-read)

- 串行化(serializable)


在我们实践之前,先说一下mysql如何设置隔离级别 和 事务提交设置

查询当前隔离级别:select @@tx_isolation;

mysql默认的事务隔离级别是可重复读(repeatable-read)


接下来在说一下如何设置成不同的隔离级别

设置成其他隔离级别:set session transaction isolation level “隔离级别”

例如下图,我们把隔离级别设置成读未提交(read-uncommited)


mysql事务原理与实践

然后是查看一下我们当前的事务提交设置,默认是1,即自动提交事务

select @@autocommit;


mysql事务原理与实践


由于我们的实践是需要我们手动提交的,所以我们先设置取消自动提交事务

set autocommit = 0;

mysql事务原理与实践


好了,我们知道怎样查询和设置后,下面就是我们的实操流程,我们同时打开2个mysql实例开启事务,看看这几种隔离级别到底能不能解决脏读、不可重复读、幻读的问题


读未提交(read-uncommitted)

1、先设置成读未提交级别

set session transaction isolation level read uncommitted;


2、我们先查询看看其中一条纪录,可以看到当前的纪录信息是:小明,22岁

mysql事务原理与实践


3、接下来我们开启事务A,先什么都不做

mysql事务原理与实践


4、然后我们打开另外一个mysql实例,开启事务B,把这条记录的age修改成25,但不提交

mysql事务原理与实践

5、这时候我们回到事务A的实例,查询一下小明的记录,会发现记录的age已经被修改成了25

mysql事务原理与实践

由此看出,上面的例子已经出现了脏读问题,事务B都还没有commit,事务A就已经读到了事务B更新的数据。可以说读未提交级别是事务隔离级别里面最低级的,我觉得根据就没有解决到我们提到的4大问题。

读已提交(read-committed)

1、先设置成读未提交级别

set session transaction isolation level read committed;

2、我们开启事务A,查询以下小明的信息,当前是小明,22岁

mysql事务原理与实践

3、然后我们打开另外一个mysql实例,开启事务B,把这条记录的age修改成25,不提交

mysql事务原理与实践

4、这时候我们回到事务A的实例,查询一下小明的记录,依然还是22岁,证明没有出现脏读的问题了

mysql事务原理与实践

5、然后我们在事务B进行提交

mysql事务原理与实践

再回到事务A查询记录,这时候发现变成25岁了,同一个事务内2次查询的内容不一样了

mysql事务原理与实践

由此看出,事务B的数据需要提交后,事务A查询的记录才会更新,证明读已提交级别已经解决了脏读问题,但还是会出现不可重复读的问题,即同一个事务内,查询同一条件的时候数据内容发生了变化。


可重复读

1、先设置成可重复读级别

set session transaction isolation level repeatable read;

2、我们开启事务A,查询以下小明的信息,当前是小明,22岁

mysql事务原理与实践

3、然后我们打开另外一个mysql实例,开启事务B,把这条记录的age修改成25,不提交

mysql事务原理与实践

4、这时候我们回到事务A的实例,查询一下小明的记录,依然还是22岁,证明没有出现脏读的问题了

mysql事务原理与实践

5、然后我们在事务B进行提交

mysql事务原理与实践

再回到事务A查询记录,依然还是22岁,证明不可重复读问题也没有出现了

mysql事务原理与实践

那我们再看看有没有幻读问题:

6、在事务B插入一条新记录并提交

mysql事务原理与实践

7、回到事务A进行查询结果集,可以看到得到了1条记录,没有出现幻读问题

mysql事务原理与实践

由此看出,可重复读级别能解决的是脏读、不可重复读、幻读的问题。

串行化

从上面的例子大家应该可以猜到,串行化就是最强的隔离级别了。是的,他可以解决脏读、不可重复读、幻读3大问题。下面我们直接实操验证一下是不是能解决幻读问题:

1、先设置成串行化级别

set session transaction isolation level serializable;

2、我们开启事务A,查询以下小明的信息,当前是小明,22岁

3、然后我们打开另外一个mysql实例,开启事务B,插入一条新记录,这时候会发现实例一直在等待

由此看出,当隔离级别是串行化的时候,事务B的写操作在等待事务A的读操作,可以说串行化的隔离级别是最安全的,读写都不允许并发。虽然说是最安全的,但由于阻塞了其他实例执行,性能也会变差。


小结

实践之后我们简单的小结一下这4种事务隔离级别分别能解决什么问题:


事务隔离级别

脏读

不可重复读

幻读

读未提交 不能解决 不能解决 不能解决
读已提交 能解决 不能解决 不能解决
可重复读 能解决 能解决 能解决
串行化 能解决 能解决 能解决


事务隔离级别的实现原理

掌握事务的4大隔离级别之后,当然还要继续深入了解他们分别是怎样实现的?是怎样做到不同事务之间不同程度的互不干扰的。大家应该能猜到,就是锁。除了锁以外,还有另外一种机制:MVCC(多版本并发控制)。

先说使用锁机制的2种隔离级别:

读未提交(read-uncommitted)

采取的是读不加锁原理

  • 事务读不加锁,不阻塞其他事务读和写

  • 事务写加锁(排他锁),阻塞其他事务写,但不阻塞其他事务读

串行化(serializable)

采取的是读加锁、写加锁原理

  • 事务读的时候加锁(共享锁),不阻塞其他事务读,但阻塞其他事务写

  • 事务写的时候加(排他锁),阻塞其他时候的读和写



然后再说一下使用MVCC的另外2种隔离级别,首先介绍一下什么是MVCC:

MVCC(多版本并发控制)

可以将MVCC看成行级别锁的一种妥协,它在许多情况下避免了使用锁,同时可以提供更小的开销。不用加任何锁, 对各个时间点生成一致性数据快照 (Snapshot), MVCC 的实现是通过保存数据在某个时间点的快照来实现的,并用这个快照来提供一定级别事务隔离。同一条记录在系统中可以存在多个版本。

MVCC有2个重要概念,分别是隐藏字段和读视图

  • 在MySQL Innodb中,会默认为我们的表后面添加三个隐藏字段

    • DB_ROW_ID:行ID,MySQL的B+树索引特性要求每个表必须要有一个主键。如果没有设置的话,会自动寻找第一个不包含NULL的唯一索引列作为主键。如果还是找不到,就会在这个DB_ROW_ID上自动生成一个唯一值,以此来当作主键(该列和MVCC的关系不大)

    • DB_TRX_ID:事务ID,记录的是当前事务在做INSERT或UPDATE语句操作时的事务ID(DELETE语句被当做是UPDATE语句的特殊情况)

    • DB_ROLL_PTR:回滚指针,通过它可以将不同的版本串联起来,形成版本链。相当于链表的next指针。

  • Read View(读视图)

    • m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。

    • min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。

    • max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。

    • creator_trx_id:表示生成该ReadView的快照读操作产生的事务id。

    • Read View就是事务进行快照读(select * from)操作的时候生产的读视图(Read View),在该事务执行的快照读的那一刻,会生成事务系统当前的一个快照,记录并维护系统当前活跃事务(未提交事务)的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)。

    •  Read View主要是用来做可见性判断的, 即当我们某个事务执行快照读的时候,对该记录创建一个Read View读视图,把它比作条件用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log里面的某个版本的数据

    • ReadView中主要包含4个比较重要的内容:

好了,理解完MVCC的主要概念后,我们回到隔离级别原理这块,下面我们再看看事务是如何使用MVCC来达到读已提交和可重复读级别的:


读已提交(read-committed)

1、事务B把age修改成25(未提交),事务C把ag修改成28(未提交),这时候的undo log版本链是这样的:

id

name

age

trx_id(隐藏字段)

roll_ptr(隐藏字段)

1 xiaoming 28 10003 指针,指向上一个版本,事务ID为10002
1 xiaoming 25 10002 指针,指向上一个版本,事务ID为10001
1 xiaoming 22 10001 历史已存在的版本

2、这时候事务A进行查询id=1的操作,会先生成一个Read View,记录当前未提交的事务信息

m_ids

min_trx_id

max_trx_id

creator_trx_id

[10002,10003,10004] 10002 10004 10004

然后在undo log版本链找找到最新的记录事务ID10003,但事务10003在Read View里面,证明事务还没提交,不可以展示,那么久依次往上个版本事务10002、10001判断,这时候发现事务10001比min_trx_id还要小,证明10001是已提交的最新版本并展示,这时候展示的数据不会出现脏读问题。

3、当事务C提交后(事务B还没提交),这时候undo log版本链不会改变

4、再一次事务A进行查询,会重新生成Read View,这时候,Read View已经变化了

m_ids

min_trx_id

max_trx_id

creator_trx_id

[10002,10005] 10002 10005 10005

然后在undo log版本链找找到最新的记录事务ID10003,而10003在min_trx_id和max_trx_id之间,这时候还需要判断10003是否在Read View中,发现10003已经不存在Read Viwe中,证明事务已提交了,可以展示,但这个时候age查询到的是28,复现了不可重复读的问题。


可重复读(repeatable-read)

理解好了读已提交的原理的话,可重复读会比较容易理解,我们还是用回刚刚的例子:1、事务B把age修改成25(未提交),事务C把ag修改成28(未提交),这时候的undo log版本链是这样的:

id

name

age

trx_id(隐藏字段)

roll_ptr(隐藏字段)

1 xiaoming 28 10003 指针,指向上一个版本,事务ID为10002
1 xiaoming 25 10002 指针,指向上一个版本,事务ID为10001
1 xiaoming 22 10001 历史已存在的版本

2、这时候事务A进行查询id=1的操作,会先生成一个Read View,记录当前未提交的事务信息

m_ids

min_trx_id

max_trx_id

creator_trx_id

[10002,10003,10004] 10002 10004 10004

跟读已提交一样,这里查询结果是10001,age=22。

3、当事务C提交后(事务B还没提交),这时候undo log版本链不会改变

4、再一次事务A进行查询,当我们的级别设置成可重复读时,不会再重新生成Read View,而是用回我们第一次生成的Read View

m_ids

min_trx_id

max_trx_id

creator_trx_id

[10002,10003,10004] 10002 10004 10004

所以这样查询得到的结果依然是10001,age=22。解决了不可重复读的问题。

总结

以上就是我对Mysql事务隔离级别的理解与分享,大家可以根据以下思路进行重温和深入理解

  • 为什么要用事务?(为了隔离不同数据库实例之间的影响)

  • 有什么隔离级别?(4大隔离级别,解决3大问题)

  • 原理是什么?(锁和MVCC)

希望能带给大家一些启发。