【MySQL】事务隔离——为什么你改了,我还看不见

一、前言

       事务就是保证一组数据库操作,要么全部成功,要么全部失败。在MySQL中,事务支持是在引擎层实现的。众所周知,MySQL是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如MySQL原生的MyISAM引擎就不支持事务,这也是MyISAM被InnoDB取代的重要原因之一。

二、隔离性

       提到事务,你肯定会想到ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性),今天我们就来说说其中I,也就是“隔离性”。隔离性研究的是不同事务之间的相互影响。隔离性,是指事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性对应隔离级别中的可串行化(Serializable)。但在实际应用中出于性能方面的考虑,很少会使用可串行化

       为了简单起见,我们仅考虑最简单的读操作和写操作,那么隔离性的探讨,主要可以分为两个方面。

      ①(一个事务)写操作对(另一个事务)写操作的影响锁机制保证隔离性

      ②(一个事务)写操作对(另一个事务)读操作的影响MVCC保证隔离性。 

三、锁机制

       首先来看(一个事务)写操作对(另一个事务)写操作的影响。隔离性要求同一时刻,只能有一个事务对数据进行写操作。InnoDB通过锁机制来保证这一点。

       锁机制的基本原理可以概括为:事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。

       根据粒度,锁可以分为表锁、行锁以及其他位于两者之间的锁(如间隙锁)。表锁在操作数据时会锁定整张表,并发性能较差、行锁只锁定需要操作的数据,并发性能较好。  

四、隔离级别

       介绍完写操作之间的相互影响。接下来讨论写操作对读操作的影响。

       当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantorm read)的问题(现象)。

       ①脏读:当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据)。这种现象是脏读。举例如下(以账户余额表为例)

                      

       ②不可重复读:在事务A中先后两次读取同一数据,两次读取的结果不一样,这种现象称为不可重复读。脏读与不可重复的区别在于:前者读到的是其他事务未提交的数据。后者读到的是其他事务已提交的数据。 

                             

       ③幻读:在事务A中(按某个条件)先后两次读取数据,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以理解为:前者是数据变了,后者是数据的行数变了

                                 

       为了解决上述这些问题,就有了“隔离级别”的概念。

       在谈隔离级别之前,你首先要知道,隔离级别越低,开销越低。可支持的并发越高,但隔离性越差。因此很多时候,我们都要在两者之间寻找一个平衡点。SQL标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable)。并规定了每种隔离级别下,上述问题是否存在。

                                 

      下面我逐一为你解释:

       读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到

       读提交是指,一个事务提交之后,它做的变更才能被其他事务看到

       可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的,当然在可重复读隔离级别下,读未提交对其他事务也是不可见的。

        串行化,顾名思义就是指对于同一行记录,“写”会加“写锁”,读会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

        其中“读提交”和“可重复读”比较难理解,所以我用一个例子说明这几种隔离级别。假设数据表T只有一列,其中一行的值为1,下面是按照时间顺序执行这两个事务的行为。

mysql>create table T(c int) engine=InnoDB;
insert into T(c) values(1);

                                                           

我们来看看在不同的隔离级别下,事务A会有哪些不同的返回结果,也就是图里面V1、V2、V3的返回值分别是什么。

1、若隔离级别是“读未提交”,则V1的值就是2.这时候事务B虽然还没有提交,但是结果已经被A看到了。因此V2,V3也都是2。

2、若隔离级别是“读提交”,则V1是1,V2的值是2。事务B的更新在提交后才能被A看到。所以V3的值也是2.

3、若隔离级别是“可重复读”,则V1、V2是1,V3是2。之所以V2还是1,遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的

4、若隔离级别是“串行化”,则在事务B执行“将1改成2”的时候,会被锁住。直到事务A提交后,事务B才可以继续执行。所以从A的角度看,V1、V2的值是1,V3的值是2.

       在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在“读提交”隔离级别下,这个视图是在每个SQL语句开始执行的时候创建的。这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;而“串行化”隔离级别下直接用加锁的方式来避免并行访问。

       我们可以看到在不同的隔离级别下,数据库行为是有所不同的。总体来说,存在即合理,哪个隔离级别都有它自己的使用场景,你要根据自己的业务情况来定。我想你可能会问那什么时候需要“可重复读”的场景呢?我们来看一个数据校对逻辑的案例。

       假设你再管理一个个人银行账户表,一个表存了每个月月底的余额,一个表存了账单明细。这时候你要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明细一致。你一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果。

       在实际应用中,读未提交在并发时会导致很多问题,因此使用较少可串行化强制事务串行并发效率低因此也使用较少。  因此在大多数数据库系统中,默认的隔离级别是读已提交(Oracle)或可重复读(mysql的InnoDB)。

       mysql的InnoDB默认的隔离级别是可重复读。在SQL标准中,可重复读无法避免幻读问题,但是InnoDB实现的可重复读避免了幻读问题。也就是说mysql的InnoDB存储引擎默认情况下不会发生脏读、不可重复读、幻读的现象。。

       mysql到底是如何解决幻读的

 三、事务隔离的实现

        理解了事务的隔离级别,我们再来看看事务隔离具体是怎么实现的。这里我们说一下“可重复读”。

在MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。

假设一个值被按顺序改成了2、3、4,在回滚日志里面就会有类似下面的记录。

                                        

       当前值是4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的read-view。如图中看到的,在视图A、B、C里面,这一个记录的值分别是1,2,4.同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。可以认为,MVCC是行级锁的一个变种。对于read-viewA,要得到1,就必须将当前值依次执行图中的所有回滚操作得到。

       下面的例子很好地体现了MVCC的特点:在同一时刻不同事务读取到的数据可能是不同的(即多版本)。在T5时刻,事务A和事务C可以读取到不同版本的数据。

                                

       InnoDB实现MVCC,多个版本的数据可以共存,主要依赖数据的隐藏列(也可以称之为标记位)和undo log。其中数据的隐藏列包含了该行数据的版本号删除时间、指向undo log的指针。当读取数据时,MySQL 可以通过隐藏列判断是否需要回滚并找到回滚需要的undo log,从而实现 MVCC;隐藏列的详细格式不再展开。

四、事务的启动方式

       如前面所述,长事务有这些潜在风险,建议你尽量避免。其实很多业务开发同学并不是有意使用长事务,通过是由于误用所致。MySQL的事务启动方式有以下几种:

1、显示启动事务语句,begin或start transaction。配套的提交语句是commit,回滚语句是rollback。

2、set autocommit=0.这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个select语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在知道你主动执行commit或rollback语句,或者断开连接。

      有些客户端连接框架会默认连接成功后先执行一个set autocommit=0的命令。这就导致接下来的查询都在事务中,如果是长连接,就导致了意外的长事务。

       因此建议使用set autocommit=1,通过显式语句的方式来启动事务。

       但是有的开发同学会纠结“多一次交互”的问题,对于一个需要频繁使用事务的业务,第二种方式每个事务在开始时都不需要主动执行一次“begin”,减少了语句的交互次数。如果你也有这个顾虑,我建议你使用commit work and chain语法。

       在autocommit为1的情况下,用begin显式启动的事务,如果执行commit则提交事务。如果执行commit work and chain,则是提交事务并启动下一个事务,这样也省去了再次执行begin语句的开销。同时带来的好处是从程序开发的角度明确地知道每个语句是否处于事务中。

       你可以在infomation_schema库的innodb_trx这个表查询长事务。

五、小结

        mySQL事务隔离级别的现象和实现,根据实现原理分析了长事务存在的风险。以及如何用正确的方法避免长事务。

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值