数据库事务

数据库事务

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

三种事务

  • 自动提交事务
    • 每—条单独的SQL语句都在其执行完成后进行自动提交事务,即执行SQL语句后就会马上自动隐式执行COMMIT操作。如果出现错误,则进行事务回滚至之前状态。
  • 显式事务
    • 通过指定事务开始语句来显式开启事务来作为开始,并由以提交命令或者回滚命令来提交或者回滚事务作为结束的一段代码就是一个用户定义的显式事务。
    • MYSQLSTART TRANSACTION | BEGIN [WORK]作为开始,以COMMIT [ WORK ] 或者 ROLLBACK [ WORK ] 作为结束。
  • 隐式事务
    • 在隐式事务中,无需使用BEGIN TRANASACTION 来开启事务,每个SQL语句第一次执行就会开启一个事务,直到用COMMIT [TRANSACTION]来提交或者ROLLBACK [TRANSACTION]来回滚结束事务。
    • MYSQL通过关闭自动提交事务,来达到隐式开启事务,显式提交事务的目的。SET session autocommit = 0;

ACID

事务必须具备ACID四个特性

  • 原子性(Atomicity)
    • 原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。
  • 一致性(Consistency)
    • 数据的一致状态,比如内部数据结构(如 B 树索引或双向链表)必须正确
  • 隔离性(Isolation)
    • 并发执行的事务是隔离的,不会相互影响,保证并发处理数据的正确性
  • 持久性(Durability)
    • 事务结束后的数据固化

并发事务引起的问题

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

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

脏读(Dirty Read)

读取未提交数据

A事务读取B事务尚未提交的数据,此时如果B事务发生错误并执行回滚操作,那么A事务读取到的数据就是脏数据。就好像原本的数据比较干净、纯粹,此时由于B事务更改了它,这个数据变得不再纯粹。这个时候A事务立即读取了这个脏数据,但事务B良心发现,又用回滚把数据恢复成原来干净、纯粹的样子,而事务A却什么都不知道,最终结果就是事务A读取了此次的脏数据,称为脏读。

不可重复读(Non-Repeatable Read)

前后多次读取,数据内容不一致(修改)

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

不可重复读和脏读的区别是,脏读是某一事务读取了另一个事务未提交的脏数据,而不可重复读则是读取了前一事务提交的数据。

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

幻读

前后多次读取,数据总量不一致(新增和删除)

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

事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后,这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。

隔离级别

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

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

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

155308444348737

隔离级别的实现

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

读取数据前,对其加锁,阻止其他事务对数据进行修改。

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

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

MVCC(多版本并发控制)

不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。(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指针进行关联。如下图所示
165088291341140

有了这种结构的数据后,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 @   Faetbwac  阅读(60)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示