MySQL 事务

1.MySQL事务的四大特性

在这里插入图片描述

  • 原子性:事务作为一个整体被执行, 包含在其中的对数据库的操作要么全部被执行,要么都不执行。

  • 一致性:事务开始前和事务结束后,数据库的完整性没有被破坏。即写入的数据必须完全符合所有的预设约束、触发器、级联回滚等。ACID就是说事务能够通过AID来保证这个C的过程,C是目的,AID都是手段。

  • 隔离性:多个事务并发执行时,一个事务的执行不应影响其他事务的执行。

  • 持久性:已被提交的事务对数据库的修改应该永久保存在数据库中。即使系统挂了,数据也不会丢。

2.ACID的实现

2.1 持久性

程序修改数据时,先将数据从磁盘加载到内存,然后修改完再由内存写回磁盘。持久化其实就是将内存里的数据写入磁盘。因此,持久性的关键就在于如何保证数据可以由内存顺利写入磁盘。有以下几个方案:

方案一:

  • 加载数据到内存
  • 修改内存
  • 写回磁盘
  • 提交事务

方案二:

  • 加载数据到内存
  • 修改内存
  • 提交事务
  • 写回磁盘

第一种方案,靠谱是靠谱,但频繁 I/O 性能太低,严重拖累 MySQL 吞吐量。第二种方案虽然性能上来了,但如果在第四步时宕机了,而系统认为事务已提交,这时候就会丢失数据了。那怎么办呢?MySQL 给出的方案是 WAL(Write Ahead Log)机制。翻译过来就是先写日志的意思。这个日志就是 redo log。具体做法是:

  • 加载数据到内存
  • 修改内存
  • 写入 redo log
  • 提交事务
  • 后台写回磁盘

如果第五步时系统宕机,可以通过 redo log 来恢复。

你可能有疑问:写入 redo log 不也有磁盘 I/O 吗?写 redo log 和写表的区别就在于随机写顺序写。MySQL 的表数据是随机存储在磁盘中的,而 redo log 是一块固定大小的连续空间。磁盘顺序写入要比随机写入快几个数量级。因此,这种方案即保证了数据的安全,性能上也能够接受。

2.2 原子性

假如一个事务做了如下操作:

  1. 插入一条数据 insert into user values(‘1’,‘小刘’,‘18’)
  2. 更新一条数据 update user set name = ‘小水’ where id = 2
  3. 删除一条数据 delete from user where id = 3

根据原子性的规定,这三个操作要么都成功,要么都失败。那么问题就来了,如何保证 3 失败的情况下,让 1,2 也回退呢?

答案就是 undo log。

每个事务操作(增删改)都会记录一条与之对应的 undo log:

  • insert 记录插入的主键,回滚则根据该主键删除记录
  • update 记录记录主键和被修改列的当前值,回滚则根据主键和之前的值覆盖
  • delete 为记录添加删除标志,即 MySQL 内部的逻辑删除,回滚根据主键恢复

2.3 隔离性

2.3.1 脏读,不可重复读,幻读
(1)脏读

B事务读取到了A事务尚未提交的数据
在这里插入图片描述
解决:

  • 加锁方式:排它锁
  • MVCC
(2)不可重复读

B事务在A事务提交之前和提交之后读取到的数据内容不一致(AB事务操作同一条数据)
在这里插入图片描述
解决:

  • 加锁方式:共享锁
  • MVCC
(3)幻读

在一个事务中不同时间段查询,记录数不同。与不可重复读的区别是:在幻读中,已经读取的数据不会改变,只是与以前相比,会有更多的数据满足查询条件。
在这里插入图片描述
解决:

  • 加锁方式:针对当前读(select … for update 等语句),通过临键锁(记录锁+间隙锁)
  • MVCC:针对快照读(普通 select 语句),通过 MVCC 方式解决幻读。
2.3.2 事务的隔离级别

数据库事务有四种隔离级别,不同的级别可能会出现各种各样的问题(脏读、幻读、不可重复读)。
在这里插入图片描述

读未提交
  • 事务读不加锁,不阻塞其他事务的读和写
  • 事务写阻塞其他事务写,但不阻塞其他事务读
读已提交(RC)

通过MVCC解决了脏读。每次读取数据前都生成一个ReadView。

可重复读(RR)

MySQL默认的事务隔离级别。
通过MVCC解决了脏读、不可重复读。在第一次读取数据时生成一个ReadView
通过MVCC(快照读时使用) + 间隙锁(当前读时使用) 解决了大部分的幻读问题。

快照读(普通读)

普通的 select 语句。执行方式是生成 readview,直接利用 MVCC 机制来读取,并不会对记录进行加锁。它是基于多版本并发控制即 MVCC机制,既然是多版本,那么快照读读到的数据不一定是当前最新的数据,有可能是之前历史版本的数据。

如下的操作是快照读:不加锁的 select 操作(事务级别不是串行化,串行化的是当前读)

当前读(锁定读)

读取的记录都是数据库中当前的最新版本,会对当前读取的数据进行加锁,防止其他事务修改数据,这种锁是一种悲观锁。当前读的规则,就是要能读到所有已经提交的记录的最新值。如下操作都是当前读:

  • select … lock in share mode 当前读,加读锁 ,也叫共享锁
  • select … for update 当前读,加写锁,又叫排他锁
  • innoDB 里面 update (排他锁)、insert (排他锁)、delete (排他锁),都会自动给涉及的语句添加写锁。
  • 串行化事务的隔离级别

实现方式:next-key(行记录锁+间隙锁)即临键锁,是前开后闭区间。

串行化

解决了脏读 、不可重复读、幻读。
采用读写都加锁的原理。串行化的情况下,对于同一行事务,写会加写锁,读会加读锁。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

2.4 一致性

事务开始前和事务结束后,数据库的完整性没有被破坏。即写入的数据必须完全符合所有的预设约束、触发器、级联回滚等。ACID就是说事务能够通过AID来保证这个C的过程,C是目的,AID都是手段。

3.MVCC原理

MVCC(Multi Version Concurrency Control),中文名是多版本并发控制,简单来说就是通过维护数据历史版本,从而解决并发访问情况下的读一致性问题。关于它的实现,要抓住几个关键点,隐式字段、undo日志、版本链、快照读&当前读、Read View。

版本链

对于InnoDB存储引擎,每一行记录都有两个隐藏列DB_TRX_ID、DB_ROLL_PTR

  • DB_TRX_ID,事务ID,每次修改时,都会把该事务ID复制给DB_TRX_ID;
  • DB_ROLL_PTR,回滚指针,指向回滚段的undo日志。
    在这里插入图片描述
    假如有一张user表,表中只有一行记录,当时插入的事务id为80。此时,该条记录的示例图如下:
    在这里插入图片描述
    接下来有两个DB_TRX_ID分别为100、200的事务对这条记录进行update操作,整个过程如下:
    在这里插入图片描述
    由于每次变动都会先把undo日志记录下来,并用DB_ROLL_PTR指向undo日志地址。因此可以认为,对该条记录的修改日志串联起来就形成了一个版本链,版本链的头节点就是当前记录最新的值。如下:
    在这里插入图片描述

ReadView

对于Read Committed和Repeatable Read隔离级别来说,都需要读取已经提交的事务所修改的记录,也就是说如果版本链中某个版本的修改没有提交,那么该版本的记录时不能被读取的。所以需要确定在Read Committed和Repeatable Read隔离级别下,版本链中哪个版本是能被当前事务读取的。于是就引入了ReadView这个概念来解决这个问题。

Read View就是事务执行快照读时,产生的读视图,相当于某时刻表记录的一个快照,通过这个快照,我们可以获取:
在这里插入图片描述

  • m_ids :表示在生成 ReadView 时当前系统中活跃的读写事务的事务id 列表。
  • min_trx_id :表示在生成 ReadView 时当前系统中活跃的读写事务中最小的 事务id ,也就是 m_ids 中的最小值。
  • max_trx_id :表示生成 ReadView 时系统中应该分配给下一个事务的 id 值。
  • creator_trx_id :表示生成该 ReadView 的事务的 事务id

有了这个 ReadView ,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:

  • 如果被访问版本的 DB_TRX_ID 属性值与 ReadView 中的 creator_trx_id 值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
  • 如果被访问版本的 DB_TRX_ID 属性值小于 ReadView 中的 min_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 前已经提交,所以该版本可以被当前事务访问。
  • 如果被访问版本的 DB_TRX_ID 属性值大于 ReadView 中的 max_trx_id 值,表明生成该版本的事务在当前事务生成 ReadView 后才开启,所以该版本不可以被当前事务访问。
  • 如果被访问版本的 DB_TRX_ID 属性值在 ReadView 的 min_trx_id 和 max_trx_id 之间,那就需要判断一下trx_id 属性值是不是在 m_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。

如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。

在 MySQL 中, READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成ReadView的时机不同。

READ COMMITTED 是每次读取数据前都生成一个ReadView,这样就能保证自己每次都能读到其它事务提交的数据;REPEATABLE READ 是在第一次读取数据时生成一个ReadView,这样就能保证后续读取的结果完全一致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值