事务隔离:为什么你改了我还看不见?

  • 什么是事务?
  • ACID
  • 隔离性与隔离级别
  • 事务隔离的实现
  • 事务的启动方式


主要是为了帮助自己理解。如果同时还能对其他人有所裨益,那就更好不过了。如果有谬误的地方,还请不吝指出。

本文并非对文章的直接复制,并且肯定有理解不到位的情况,如果希望系统地学习,还是要去官网支持原作者。

注意:最好拥有一定的MySQL基础再来看本系列文章,可以去b站搜索动力节点的mysql基础教程,或者翻看我做的走进MySQL系列(笔记做的并不是特别详尽,仅作为参考)

什么是事务?

事务:是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操作集合

在Mysql中是在引擎层实现的。但并非所有引擎都支持事务,比如MyISAM引擎就不支持事务。这也是其被InnoDB取代的重要原因之一。

ACID

提起事务,就会想到ACID(Atomicity,Consistency,Isolation,Durability)

原子性
事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做
一致性
事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。如果系统崩溃导致事务没有提交,之前的修改也不会发生。在提交的时刻发生了状态改变。
隔离性
一个事务所做的修改在最终提交前,对其他事务不可见。
持续性
也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。

一个实现了ACID的数据库,通常需要更强的CPU处理能力、更大的内存和磁盘空间。

隔离性与隔离级别

当数据库上有多个事务同时执行,就有可能出现:
脏读:读到了其它事务未提交的数据
不可重复读:一个事务范围内,多次查询读取的记录不一致
幻读:一个事务范围内,前后读取的记录数量不一致

为了解决这些问题,就有了隔离级别的概念,隔离越强,效率就越低。
读未提交(Read Uncommitted):一个事务还没提交时,变更就被别的事务看到。(即脏读)
读提交(Read Committed):一个事务提交之后,变更才会被其他事务看到。(但没解决不可重复读)
可重复读(Repeatable Read):一个事务执行中看到的数据,总和启动时看到的数据一致。注,还是保证了读提交(但不能解决幻读)
串行化(Serializable):对于同一行记录,写加写锁,读加读锁。出现读写锁冲突时,后访问事务必须等前一个事务执行完成,才能继续执行。

并行性依次降低,安全性依次提高。

实现上,数据库会创建一个视图,访问时以视图的逻辑结果为准。
在RU级别下,直接返回记录的最新值,没有视图概念
在RC级别下,视图在每个SQL语句开始执行时窗口
在RR级别下,视图是在事务启动时创建的,整个事务存在期间都使用这个视图
在S级别下,直接使用加锁来避免并行访问

每一种级别都有其用途。

通过参数transaction-isolation来设置级别

mysql> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name         | Value           |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+

事务隔离的实现

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

假如一个值从1按顺序被改为了2,3,4,则回滚日志里记录类似:

头哥实验初识MySQL第六节视图 mysql实战34讲_回滚

当前值是4,但查询记录时,不同时刻启动的事务会有不同的read-view。同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于read-view A,要得到1,就必须将当前值依次执行所有图中的回滚操作。

系统会判断,如果没有事务需要使用到undo log时,就会被删除。即,系统中没有比这个回滚日志更早的read-view的时候,无数据可以回滚的时候。

为什么尽量不要使用长事务?
长事务意味系统里会存在很老的事务视图,由于事务随时可能访问数据库里的数据,所以事务提交之前,数据库里它可能用到的回滚记录都必须保留,就会导致大量占用存储空间。

在5.5及之前版本,回滚日志和数据字典一起放在ibdata文件里,即使长事务最终提交,回滚段被清理,文件也不会变小。

除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库。

举例(评论区Gavin):
RR级
事务A先被创建,此时视图read view A也创建了,此时查到某记录某字段值为1
事务B接着被创建,read view B开启,更新了那个字段为2,同时创建了一个2->1的回滚日志,事务随后就被commit了
事务C接着被创建,read view C开启,更新了那个字段为3,同时创建了一个3->2的回滚日志,事务随后就被commit了

由于长事务A一直没有被commit,为了保证事务在执行期间数据前后一致(RR级的保证),老的事务视图、回滚日志就必须存在了,所以会占用大量存储空间。
(因为要从当前位置根据回滚日志回滚到与最开始的视图一致)

这也解释了前面的**“没有比这个回滚日志更早的read-view”**的说法,如果回滚日志之前还存在事务的话,这中间的日志必须保留以能够回到那个事务的视图。

事务的启动方式

  1. 显示启动事务语句, begin 或 start transaction 提交语句是commit,回滚是rollback
  2. set autocommit=0,会将线程的自动提交关掉。如果执行一个select,事务就启动了,并且不会自动提交,存在直到主动执行commit或rollback语句,或者断开连接。

注意:

  1. 显示启动后,不管autocommit的值,只有commit以后才会生效。
  2. autocommit为0时,不管有没有显示启动,只有commit才会生效。
  3. 如果autocommit为1,并且没有显示启动,会自动提交,无法调用ROLLBACK。即,每一个操作为一个孤立的事务,每一次都会即时提交或即时回滚。