事务

事务

数据库事务

数据库事务(Database Transaction),是指作为单个逻辑工作单元执行的一系列操作,要么完全执行,要么完全地不执行。

ACID

事务必须具备ACID四个特性

原子性(Atomicity)

原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。

由undog log保证

一致性(Consistency)

数据的一致状态,比如内部数据结构(如 B 树索引或双向链表)必须正确

由其他三个特性共同保证

隔离性(Isolation)

并发执行的事务是隔离的,不会相互影响,保证并发处理数据的正确性

由锁保证

持久性(Durability)

事务结束后的数据固化

由redo log保证

并发事务引起的问题

当多个线程都开启事务操作数据库中数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性。

如果没有隔离,会发生的几种问题

脏读(Dirty Read)

一个事务处理过程里读取了另一个未提交的事务中的数据

不可重复读(Non-Repeatable Read)

对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询的间隔期间,另外一个事务修改并提交了该数据

不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。
在某些情况下,不可重复读并不是问题,比如我们多次查询某个数据当然以最后查询得到的结果为主。但在另一些情况下就有可能发生问题,例如对于同一个数据A和B依次查询就可能不同,A和B就可能打起来了……

总结:

查询检索数据,而同一事务中的后续查询 检索应该是相同数据的情况,但查询返回不同的结果(同时被另一个事务提交更改)。

这种操作违背了 数据库设计的ACID原则。在事务中,数据应该是一致的,具有可预测和稳定的关系。

在不同的隔离级别中,可序列化读可重复读级别防止不可重复读 ,一致性读未提交读级别允许。

幻读

在一个事务中读取到了别的事务插入的数据,导致前后不一致

假设表的id列上有一个索引,child并且您想要读取并锁定表中标识符值大于 100 的所有行,以便稍后更新所选行中的某些列:

SELECT * FROM child WHERE id > 100 FOR UPDATE;

查询从 id大于 100 的第一条记录开始扫描索引。让表包含id值为 90 和 102 的行。如果在扫描范围内的索引记录上设置的锁 不锁定在间隙中进行的插入 (在这种情况下,90 和 102 之间的差距),另一个会话可以将新行插入到表中,其 id值为 101。如果您要 SELECT在同一事务中执行相同的操作,您将看到一个新行,其id值为 101(一个 “幻影”) 在查询返回的结果集中。如果我们将一组行视为一个数据项,那么新的幻像子将违反一个事务应该能够运行的事务的隔离原则,使其读取的数据在事务期间不会发生变化。

总结:同一事务在执行不同查询在不同时间产生不同的行集

隔离级别

隔离性和一致性是需要开发者权衡的问题,在SQL标准中定义了四种数据库的事务隔离级别

  • RAED UNCOMMITED:使用查询语句不会加锁,可能会读到未提交的行(Dirty Read);
  • READ COMMITED:
    写事务会阻止其他读写事务。读事务不会阻止其他任何事务。 (即读事务没有阻塞update,不可重复读)
    存在的问题:不可重复读。即在一次事务之间,进行了两次读取,但是结果不一样,可能第一次id为1的人叫“李三”,第二次读id为1的人就叫了“李四”。因为读取操作不会阻止其他事务。
  • REPEATABLE READ:
    读事务会阻止其他写事务,但是不会阻止其他读事务。 (没有阻塞insert, 幻读)
    存在的问题:幻读。可重复读阻止的写事务包括update和delete(只给存在的表加上了锁),但是不包括insert(新行不存在,所以没有办法加锁),所以一个事务第一次读取可能读取到了10条记录,但是第二次可能读取到11条,这就是幻读。
  • SERIALIZABLE:InnoDB 隐式地将全部的查询语句加上共享锁,解决了幻读的问题;

下面这张图展示了各个隔离层级对几个问题的解决情况。

image-20220826080524706

隔离级别的实现

数据库对于隔离级别的实现就是使用并发控制机制对在同一时间执行的事务进行控制,限制不同的事务对于同一资源的访问和更新,而最重要也最常见的并发控制机制,在这里我们将简单介绍两种种最重要的并发控制器机制的工作原理。

锁是一种最为常见的并发控制机制,在一个事务中,我们并不会将整个数据库都加锁,而是只会锁住那些需要访问的数据项,MySQL和常见数据库中的锁都分为两种,共享锁(Shared)和互斥锁(Exclusive),前者也叫读锁,后者叫写锁。

关于锁,会有专门的文章探讨。

MVCC(多版本并发控制)

这是另一种隔离的解决方案,通过维护多个版本的数据,数据库可以允许事务在数据被其他事务更新时对旧版本的数据进行读取,很多数据库都对这一机制进行了实现。

  • 优点:对读的非阻塞,读不加锁,读写不冲突
  • 缺点:每行记录都需要额外的存储空间

MVCC(Multi-Version Concurrency Control)多版本并发控制,是用来在数据库中控制并发的方法,实现对数据库的并发访问用的。在MySQL中,MVCC只在读取已提交(Read Committed)和可重复读(Repeatable Read)两个事务级别下有效。其是通过Undo日志中的版本链和ReadView一致性视图来实现的。MVCC就是在多个事务同时存在时,SELECT语句找寻到具体是版本链上的哪个版本,然后在找到的版本上返回其中所记录的数据的过程。

首先需要知道的是,在MySQL中,InnoDB为数据库中的每一行添加了三个隐藏字段:DB_TRX_ID(事务版本号)、DB_ROLL_PTR(回滚指针)、DB_ROW_ID(隐藏ID)。:

  • DB_ROW_ID:行ID,MySQL的B+树索引特性要求每个表必须要有一个主键。如果没有设置的话,会自动寻找第一个不包含NULL的唯一索引列作为主键。如果还是找不到,就会-在这个DB_ROW_ID上自动生成一个唯一值,以此来当作主键(该列和MVCC的关系不大);
  • DB_TRX_ID:事务ID,记录的是当前事务在做INSERT或UPDATE语句操作时的事务ID(DELETE语句被当做是UPDATE语句的特殊情况);
  • DB_ROLL_PTR:回滚指针,通过它可以将不同的版本串联起来,形成版本链。相当于链表的next指针。

InnoDB基于事务版本号、回滚指针这两个字段,可以在undo log中形成一个单向链表,最新版本的数据放在链表头部,历史数据通过DB_ROLL_PTR指针进行关联。如下图所示
image-20220826080503176

有了这种结构的数据后,InnoDB可以很方便的管理多个版本的数据,也为MVCC的实现打下来基础。
接下来我们来了解一下MVCC在InnoDB中具体的实现逻辑是怎样的,以及MVCC解决了哪些问题。
首先,InnoDB在事务开启后执行第一个查询时,会创建一个快照(下文称之为ReadView),这个ReadView包含了以下信息:

  • m_ids: 活动事务id列表(活动事务指的是已经开始、尚未提交/回滚的事务)
  • min_trx_id: 最小活动事务id
  • max_trx_id:最大活动事务id
  • creator_trx_id:当前事务id

紧接着InnoDB会通过查询语句定位到最新版本的数据行,并根据以下规则获取到可以访问的数据版本。

  • 如果被访问版本的trx_id,与readview中的creator_trx_id值相同,表明当前事务在访问自己修改过的记录,直接返回该版本的数据;
  • 如果被访问版本的trx_id,小于readview中的min_trx_id值,表明生成该版本的事务在当前事务生成readview前已经提交,直接返回该版本的数据;
  • 如果被访问版本的trx_id,大于或等于readview中的max_trx_id值,表明生成该版本的事务在当前事务生成readview后才开启,此时该版本不可以被当前事务访问,需要通过隐藏的回滚指针从undo log中读取历史版本;
  • 如果被访问版本的trx_id,在readview的min_trx_id和max_trx_id之间,则需要判断trx_id值是否在m_ids列表中?
    • 如果在:说明readview创建时,创建该版本数据的事务还未提交,因此需要通过回滚指针读取历史版本并返回。
    • 如果不在:说明readview创建时,创建该版本数据的事务已经提交,所以直接返回该版本的数据;

可重复读隔离级别下,ReadView只会在第一次查询时创建,同一个事务中后续所有的查询共用一个ReadView,由此便解决了不可重复读的问题。

读已提交隔离级别下,每次查询都会创建一个新的ReadView。新建的ReadView会更新creator_trx_id以外的其余字段,因此不可重复读现象依然存在。但是由于ReadView可以判断出修改此数据的事务是否已经提交,因此可以避免脏读的出现。

其次,从上述MVCC实现逻辑中可以发现,没有任何加锁、获取锁的操作,因此MVCC读操作不会因为等待锁而阻塞(也就是常说的非阻塞读)。

总结
MVCC可以解决脏读、不可重复读,并且实现了非阻塞读的功能。
读已提交隔离级别:每次读操作都会设置和读取自己的新快照(ReadView)。
可重复读隔离级别:同一个事务共用第一次查询时建立的快照(ReadView)。

Mysql中的读操作可以分为两大类:快照读与当前读。

快照读是指通过MVCC实现的非阻塞读,常见的快照读操作如下:

select xxx from xxx
当前读也叫加锁读,每次读取数据都是读取数据的最新版本,并且会对其进行加锁。常见的当前读操作如下

select xxx from xxx lock in share mode (共享锁/读锁)
select xxx from xxx for update (排它锁/写锁)
update 、delete、insert

为什么要区分这两种读操作呢?因为MVCC并不能解决幻读的问题。即使是在可重复读级别,通过当前读依然会出现幻读问题。此问题最终是通过间隙锁来解决的。

posted @ 2022-08-26 08:07  吟风者的旅途  阅读(151)  评论(0)    收藏  举报