3事务隔离:为什么你改了我还看不见 - MySQL实战45讲学习笔记

本文解析了数据库事务隔离的概念,包括四种隔离级别:读未提交、读已提交、可重复读和串行化,以及它们如何解决脏读、不可重复读和幻读等问题。详细介绍了MySQL中InnoDB引擎如何实现可重复读隔离级别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

提到事务,大家肯定不陌生,事务相关最经典的一个例子就是银行转账问题:

比如,你的银行卡里有100块,现在你要转账给张三100块,而转账包括这样三个操作:查询余额、做减法、更新余额。

那么,在你转给张三100块时,在做减法之前,你又转给自己另一个账户100块,然后两个转账操作都成功了,我们没有花钱就转给了张三100块。

这就出现了不一致现象,和正常的结果不一致,银行肯定不会那么傻,所以,为了解决那样的问题,我们就要用到事务这个概念。

简单来说,事务就是保证一组数据库操作,要么全部成功,要么全部失败。

在MySQL中,事务支持是在引擎层实现的。

我们现在知道,引擎层支持多个引擎,而MyISAM引擎不支持事务,这也是它被InnoDB取代的原因。

下面的文章,会以InnoDB为例,来剖析事务支持方面的特定实现:

隔离性、隔离级别

提到事务,大家肯定先想到ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性),本章就来讲讲其中的I,隔离性。

当数据库中有多个事务同时执行的时候,可能会出现脏读(读了未提交的数据,这个数据回滚就会出现问题)、不可重复读(事务中的两次读操作之间,数据被其他事务修改了)、幻读(两次读表之间,其他事务增加了表项)这些问题,为了解决这些问题,就有了“隔离级别”的概念。

隔离级别分为不同的等级,不同等级隔离的程度不同,下面的隔离级别依次提高:

  1. 读未提交:事务1可以读取还未提交的事务2变更的数据。这个相当于没有隔离,会造成脏读、不可重复读、幻读。
  2. 读已提交:只有事务2提交了,事务1才可以看到事务2变更的数据,并且读取变更后的数据;在事务2提交之前,只能读取到没有变更的数据。解决了脏读。相当于加了“写锁”。
  3. 可重复读:事务2执行过程中看到的数据,总是和启动时看到的数据时一致的,不会被更改(当然事务2未提交的变更,不会被事务1读取) ,解决了脏读、不可重复读。相当于加了“写锁”和“读锁”。
  4. 串行化,最高的隔离级别,使用“写锁”和“读锁”保证事务逻辑上是串行执行的,互不干扰。解决了脏读、不可重复读、幻读。

(加了写锁:数据不可以被读写;加了读锁:数据不可以被写)

下面来看一个例子,理解这几个隔离级别:

数据库表是这样的

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

img
此图来自于极客时间丁奇老师MySQL实战45讲

来看看不同隔离级别下,V1、V2、V3的值是多少:

  1. 读未提交:V1=2,因为事务A可以看到事务B还未提交的修改;V2=2;V3=2。

  2. 读提交:V1=1,因为事务B还未提交,所以不可以读取它的修改;V2=2;V3=2。

  3. 可重复读:V1=1;V2=1,因为事务A在执行期间,看到的数据前后必须是一致的;V3=2。

  4. 串行化:V1=1;V2=1;V3=2,在事务B执行到“将1修改为2”时,会被锁住,等事务A提交后才会继续执行。

事务隔离的实现

在实现上,数据库里会创建一个视图,访问的时候以视图的逻辑结果为准。

在“可重复读”隔离级别下,这个视图是在事务开始的时候创建的,整个事务期间都会使用这个视图。在“读提交”隔离级别下,视图是在每个sql语句开始执行的时候创建的。

“读未提交”隔离级别直接返回记录上的最新值,没有视图的概念;“串行化”隔离级别直接使用加锁的方式来避免并行访问。

可以看到,在不同隔离级别下,数据库有不同的行为,所以,在进行数据库迁移的时候,要保证数据库的隔离级别一致。

不同隔离级别有不同的应用场景

接下来我们讲一讲“可重复读”隔离级别的应用场景和实现:

应用场景:一个数据校对的案例

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

这时候,使用“可重复读”比较方便。事务启动时的视图可以认为是静态的,不受其他事务的影响。

那么下面来讲一下它的实现

“可重复读”事务隔离的实现

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

假设值1被按顺序改成了2,3,4,在回滚日志里面会有下面的记录:

img此图来自于极客时间丁奇老师MySQL实战45讲

当前值是4,但在查询这条记录时,不同时刻启动的事务会有不同的read-view。在图中,视图A、B、C对应记录的值分别为1,2,4,每个视图其实就对应不同时刻启动的事务。

对于read-view A,它对应的值是1,可当前值是4,如果事务A想得到它对应视图A的值1,我们就必须执行所有的回滚操作得到值1。如果此时,还有另一个事务将值4变为5,这个事务和前面视图A、B、C对应的事务不会冲突。

所以,“可重复读”就是这样实现的,每个事务自己有一个视图,这个视图对应的值是不会改变的,当事务去读值,会回滚到它的视图所对应的值,保证了事务中每次读都和事务开始时的数据是一致的。

一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(MVCC),这个到后面的章节再介绍。

那么,现在有一个问题:回滚日志不能一直存在吧,那什么时候删除呢?

答案:在不需要的时候删除,也就是没有事务再去使用这些回滚日志的时候,系统会去删除。

那什么时候才不需要了呢?

答案:当系统没有比这个回滚日志更早的read-view时。比如:事务A结束了,视图A可以删除了,所以将2改成1这条回滚日志可以删除。(这里如果没有理解不用怕,看看下面就理解了)

为什么尽量不要使用长事务?

在“可重复读”隔离级别的数据库中,每个事务都会有一个视图,如果这个事务是一个长事务的话,那么系统里会存在一个很老的事务视图。因为长事务随时都可能会读取任何数据,所以有必要保存长事务可能用到的所有回滚操作,这就导致占用大量的存储空间。

丁奇老师:在MySQL 5.5及以前的版本,回滚日志是跟数据字典一起放在ibdata文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有20GB,而回滚段有200GB的库。最终只好为了清理回滚段,重建整个库。

除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库,这个会在后面讲锁的时候展开。

事务的启动方式

有些时候并不是我们有意使用长连接,而是我们的一些不好的操作就会导致出现了长连接。

事务启动的两种方式:

  1. 显示启动事务,不管autocommit为0还是1,开启事务使用begin或者start transaction,提交使用commit,回滚使用rollback。(当autocommit=1并且使用了begin启动事务,那么只有使用commit才会提交事务,每条语句不会被看作是一个事务)
  2. 隐示启动事务,set autocommit=0,这个命令会将这个线程的自动提交关闭,当执行到第一条语句的时候自动就开启了事务,只有执行commit或者rollback的时候事务才结束。

有些客户端连接框架在连接成功后会自动执行一条set autocommit=0语句,这就导致接下来的语句都在事务中,如果这个连接是一个长连接,那么就会意外导致了一个长事务。

因此,建议使用set autocommit=1,并通过显示语句的方式启动事务。如果嫌每次都要begin开启事务操作麻烦,那么可以使用commit work and chain,自动启动下一事务,并省去了执行begin语句的开销。

丁奇老师留的思考题:你现在知道了系统里面应该避免长事务,如果你是业务开发负责人同时也是数据库负责人,你会有什么方案来避免出现或者处理这种情况呢?

上期答案:一天一备和一周一备相比,好处就是‘“最长恢复时间”更短,一天一备最坏的情况需要一天的binlog日志,而一周一备最坏需要一周的binlog。但这种好处是用存储空间换来的,所以使用哪个就根据具体需求来了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值