聊一下Mysql事务的隔离级别

104 阅读6分钟

我们知道mysql是一个支持多引擎的系统,但不是每一个引擎都会支持事务,比如Mysql原生的MyISAM引擎就不支持事务,这也是被InnoDB取代的一个重要的原因之一。

谈到隔离级别,我们都知道隔离级别越高,性能越差,因此我们要在两者之间找到一个平衡点,并且每种隔离级别都有它存在的道理,SQL中的隔离级别有:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )

事务不同隔离级别的区别:

读未提交:一个事务还未提交,它所做的变更就可以被别的事务看到
读提交:一个事务提交之后,它所做的变更才可以被别的事务看到
可重复读:一个事务执行过程中看到的数据是一致的。未提交的更改对其他事务是不可见的
串行化:对应一个记录会加读写锁,出现冲突的时候,后访问的事务必须等前一个事务执行完成才能继续执行

对于某一些隔离级别,数据库会通过创建视图的方式去实现,访问时会通过访问视图获取到逻辑结果,对于“读未提交”,是可以直接获取到数据的,不会进行视图的创建,“读提交”是在每条sql执行时都会进行视图的创建的,“可重复读”当事务开启时会创建一个视图,这个视图将会伴随事务结束,这也是事务执行过程中看到的数据是一致的原因。

可以通过下面命令查看隔离级别:

show variables like 'transaction_isolation';

我们需要根据自己的业务特点,选择合适的隔离级别,对于我们Mysql默认的可重复读的默认级别,有哪些具体的应用场景呢?

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

这时候使用“可重复读”隔离级别就很方便。事务启动时的视图可以认为是静态的,不受其他事务更新的影响。

下面我们展开说明“可重复读”

Mysql中,每一条更新操作都会伴随生成一条回滚记录,更新之后的最新值,都可以通过回滚记录生成前一个状态的数据,如下更新操作num值从9更新到10,再由10更新到11,最终由11更新为12.

截屏2022-10-23 14.41.28.png

对于可重复读的隔离级别,每个事务启动时都会创建一个read-view,数据库中的一条数据可以对应多个read-view版本,这就是数据库中的多版本并发控制(MVCC) ,如果更新的数据想回滚到read-view1,则必须要经过read-view2的回滚视图。我们从这个更新回滚流程图上可以看出,我们尽量不要使用长事务。

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语句的开销。同时带来的好处是从程序开发的角度明确地知道每个语句是否处于事务中。

你可以在information_schema库的innodb_trx这个表中查询长事务,比如下面这个语句,用于查找持续时间超过60s的事务。

select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

长事务是怎么理解的? 可以通过下面这个例子

1 比如,在某个时刻(今天上午9:00)开启了一个事务A(对于可重复读隔离级别,此时一个视图read-view A也创建了),这是一个很长的事务……
2 事务A在今天上午9:20的时候,查询了一个记录R1的一个字段f1的值为1……
3 今天上午9:25的时候,一个事务B(随之而来的read-view B)也被开启了,它更新了R1.f1的值为2(同时也创建了一个由2到1的回滚日志),这是一个短事务,事务随后就被commit了。
4 今天上午9:30的时候,一个事务C(随之而来的read-view C)也被开启了,它更新了R1.f1的值为3(同时也创建了一个由3到2的回滚日志),这是一个短事务,事务随后就被commit了。……
5 到了下午3:00了,长事务A还没有commit,为了保证事务在执行期间看到的数据在前后必须是一致的,那些老的事务视图、回滚日志就必须存在了,这就占用了大量的存储空间。
因此我们需要尽量避免长事务。