一文学会Mysql(三)Mysql事务

Mysql事务

1. ACID

在关系型数据库管理系统中,一个逻辑工作单元要成为事务,必须满足这 4 个特性,即所谓的 ACID: 原子性(Atomicity)、一致性( Consistency)、隔离性( Isolation )和持久性( Durability)。

1.1 原子性

原子性:事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。

修改---》 Buffer Pool修改---》刷盘。可能会有下面两种情况:

  • 事务提交了,如果此时Buffer Pool的脏页没有刷盘,这时候宕机了,如何保证修改的数据生效? Redo

  • 如果事务没提交,但是Buffer Pool的脏页刷盘了,如何保证不该存在的数据撤销? Undo

每一个写事务,都会修改BufferPool ,从而产生相应的Redo/Undo日志,在Buffer Pool 中的页被刷到 磁盘之前,这些日志信息都会先写入到日志文件中,如果 Buffer Pool 中的脏页没有刷成功,此时数据 库挂了,那在数据库再次启动之后,可以通过 Redo 日志将其恢复出来,以保证脏页写的数据不会丢失。如果脏页刷新成功,此时数据库挂了,就需要通过Undo来实现了。

1.2 持久性

持久性:指的是一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,后续的操作或故障不 应该对其有任何影响,不会丢失。

如下图所示,一个“提交”动作触发的操作有: binlog落地、发送binlog、存储引擎提交、 flush_logs, check_point、事务提交标记等。这些都是数据库保证其数据完整性、持久性的手段。

d5880abb40782835f9c72ba5e2451665.jpeg

MySQL的持久性也与WAL技术相关, redo log在系统Crash重启之类的情况时,可以修复数据,从而保 障事务的持久性。通过原子性可以保证逻辑上的持久性,通过存储引擎的数据刷盘可以保证物理上的持 久性。

1.3 隔离性

隔离性:指的是一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对其他的并 发事务是隔离的。

InnoDB 支持的隔离性有 4 种,隔离性从低到高分别为:读未提交、读提交、可重复读、串行化。锁和多版本并发控制( MVCC )技术就是用于保障隔离性的(后面课程详解)。

1.4 一致性

一致性:指的是事务开始之前和事务结束之后,数据库的完整性限制未被破坏。一致性包括两方面的内 容,分别是约束一致性和数据一致性。

约束一致性:创建表结构时所指定的外键、 Check、唯一索引等约束,可惜在 MySQL 中不支持 Check 。

数据一致性:是一个综合性的规定,因为它是由原子性、持久性、隔离性共同保证的结果,而不是 单单依赖于某一种技术。

一致性也可以理解为数据的完整性。数据的完整性是通过原子性、隔离性、持久性来保证的,而这3个 特性又是通过 Redo/Undo 来保证的。逻辑上的一致性,包括唯一索引、外键约束、 check 约束,这属 于业务逻辑范畴。

252041cdffdef88de56c75b240a5ebe7.jpeg

ACID 及它们之间的关系如下图所示, 4个特性中有3个与 WAL 有关系,都需要通过 Redo、 Undo 日志 来保证等。

WAL的全称为Write-Ahead Logging ,先写日志,再写磁盘。

a14ba495ce283359e447b8cbe7d4e7c8.jpeg

2.并发事务控制

2.1 并发事务

事务并发处理可能会带来一些问题,比如:更新丢失、脏读、不可重复读、幻读等。

  • 更新丢失

当两个或多个事务更新同一行记录,会产生更新丢失现象。可以分为回滚覆盖和提交覆盖。

  • 回滚覆盖:一个事务回滚操作,把其他事务已提交的数据给覆盖了。

  • 提交覆盖:一个事务提交操作,把其他事务已提交的数据给覆盖了。

脏读

一个事务读取到了另一个事务修改但未提交的数据。

不可重复读

一个事务中多次读取同一行记录不一致,后面读取的跟前面读取的不一致。

幻读

一个事务中多次按相同条件查询,结果数量不一致。后续查询的结果和面前查询结果不同,多了或少了几行记录。

2.3 排队

最简单的方法,就是完全顺序执行所有事务的数据库操作,不需要加锁,简单的说就是全局排队。序列 化执行所有的事务单元,数据库某个时刻只处理一个事务操作,特点是强一致性,处理性能低。

049551c3902da967bc10dc597a5d52cf.jpeg

2.2 排他锁

引入锁之后就可以支持并发处理事务,如果事务之间涉及到相同的数据项时,会使用排他锁,或叫互斥锁,先进入的事务独占数据项以后,其他事务被阻塞,等待前面的事务释放锁。

ef9b7e74f9c2e0c59f69fc7761cfe47d.jpeg

注意,在整个事务1结束之前,锁是不会被释放的,所以,事务2必须等到事务1结束之后开始。

2.3 读写锁

读和写操作:读读、写写、读写、写读。

读写锁就是进一步细化锁的颗粒度,区分读操作和写操作,让读和读之间不加锁,这样下面的两个事务 就可以同时被执行了。

d7c67155a48f913a3b5dccdd4b3d38fa.jpeg

读写锁,可以让读和读并行,而读和写、写和读、写和写这几种之间还是要加排他锁。

3.隔离级别

3.1 隔离级别类型

前面提到的“更新丢失”、”脏读”、“不可重复读”和“幻读”等并发事务问题,其实都是数据库一致性问题, 为了解决这些问题, MySQL数据库是通过事务隔离级别来解决的,数据库系统提供了以下 4 种事务隔 离级别供用户选择。

9b2c91ffffd5e00418e409c6fa94851d.jpeg

  • 读未提交

Read Uncommitted 读未提交:解决了回滚覆盖类型的更新丢失,但可能发生脏读现象,也就是 可能读取到其他会话中未提交事务修改的数据。

  • 已提交读

Read Committed 读已提交:只能读取到其他会话中已经提交的数据,解决了脏读。但可能发生 不可重复读现象,也就是可能在一个事务中两次查询结果不一致。

  • 可重复度

Repeatable Read 可重复读:解决了不可重复读,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上会出现幻读,简单的说幻读指的的当用户读取某一范围的数 据行时,另一个事务又在该范围插入了新行,当用户在读取该范围的数据时会发现有新的幻影行。

  • 可串行化

Serializable 串行化:所有的增删改查串行执行。它通过强制事务排序,解决相互冲突,从而解决 幻度的问题。这个级别可能导致大量的超时现象的和锁竞争,效率低下。

数据库的事务隔离级别越高,并发问题就越小,但是并发处理能力越差(代价)。读未提交隔离级别最 低,并发问题多,但是并发处理能力好。以后使用时,可以根据系统特点来选择一个合适的隔离级别, 比如对不可重复读和幻读并不敏感,更多关心数据库并发处理能力,此时可以使用Read Commited隔 离级别。

事务隔离级别,针对Innodb引擎,支持事务的功能。像MyISAM引擎没有关系。

事务隔离级别和锁的关系

1 )事务隔离级别是SQL92定制的标准,相当于事务并发控制的整体解决方案,本质上是对锁和MVCC使 用的封装,隐藏了底层细节。

2)锁是数据库实现并发控制的基础,事务隔离性是采用锁来实现,对相应操作加不同的锁,就可以防 止其他事务同时对数据进行读写操作。

3 )对用户来讲,首先选择使用隔离级别,当选用的隔离级别不能解决并发问题或需求时,才有必要在 开发中手动的设置锁。

MySQL默认隔离级别:可重复读

Oracle、SQLServer默认隔离级别:读已提交

一般使用时,建议采用默认隔离级别,然后存在的一些并发问题,可以通过悲观锁、乐观锁等实现处 理。

3.2 MySQL隔离级别控制

MySQL默认的事务隔离级别是Repeatable Read ,查看MySQL当前数据库的事务隔离级别命令如下:

show variables like 'tx_isolation';

select @@tx_isolation;

设置事务隔离级别可以如下命令:

set tx_isolation='READ-UNCOMMITTED';

set tx_isolation='READ-COMMITTED';

set tx_isolation='REPEATABLE-READ';

set tx_isolation='SERIALIZABLE';

4.MVCC(重点)
简介

什么是 MVCC ?

MVCC(Multi-Version Concurrency Control)即多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。MVCC使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能。

MVCCMySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读

什么是当前读和快照读?

在学习 MVCC 多版本并发控制之前,我们必须先了解一下,什么是 MySQL InnoDB 下的当前读和快照读?

  • 当前读 像 select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读。

    为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

  • 快照读 像不加锁的 select 操作就是快照读,即不加锁的非阻塞读;快照读的实现是基于多版本并发控制,而有可能是之前的历史版本。

MVCC 就是为了实现读-写冲突不加锁,而这个读指的就是 快照读 , 而非当前读。

当前读实际上是一种加锁的操作,是悲观锁的实现

当前读,快照读和MVCC的关系

  • MVCC 多版本并发控制是 「维持一个数据的多个版本,使得读写操作没有冲突」 的概念,只是一个抽象概念,并非实现
  • 因为 MVCC 只是一个抽象概念,要实现这么一个概念,MySQL 就需要提供具体的功能去实现它,「快照读就是 MySQL 实现 MVCC 理想模型的其中一个非阻塞读功能」。而相对而言,当前读就是悲观锁的具体功能实现
  • 要说的再细致一些,快照读本身也是一个抽象概念,再深入研究。MVCC 模型在 MySQL 中的具体实现则是由 3 个隐式字段undo 日志Read View 等去完成的,具体可以看下面的 MVCC 实现原理

MVCC 能解决什么问题,好处是?

数据库并发场景有三种,分别为:

  • 读-读:不存在任何问题,也不需要并发控制
  • 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
  • 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

MVCC 带来的好处是? 多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题

  • 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
  • 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题

如何使用MVCC解决并发问题? 有了 MVCC,所以我们可以形成两个组合:

  • MVCC + 悲观锁 MVCC解决读写冲突,悲观锁解决写写冲突
  • MVCC + 乐观锁 MVCC 解决读写冲突,乐观锁解决写写冲突

这种组合的方式就可以最大程度的提高数据库并发性能,并解决读写冲突,和写写冲突导致的问题

MVCC 的实现原理

MVCC 的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段undo日志Read View 来实现的。所以我们先来看看这个三个 point 的概念

隐式字段

每行记录除了我们自定义的字段外,还有数据库隐式定义的 DB_TRX_ID, DB_ROLL_PTR, DB_ROW_ID等字段

  • DB_TRX_ID 6 byte,最近修改(修改/插入)事务 ID:记录创建这条记录/最后一次修改该记录的事务 ID
  • DB_ROLL_PTR 7 byte,回滚指针,指向这条记录的上一个版本的undo log
  • DB_ROW_ID 6 byte,隐含的自增 ID(隐藏主键),如果数据表没有主键,InnoDB 会自动以DB_ROW_ID产生一个聚簇索引

f0efc910330f3d3737dd923173051e9e.jpegundo日志

undo log 主要分为两种:

  • insert undo log 代表事务在 insert 新记录时产生的 undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃
  • update undo log 事务在进行 update 或 delete 时产生的 undo log ; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除

对 MVCC 有帮助的实质是 update undo log ,undo log 实际上就是存在 rollback segment 中旧记录链,

执行流程:

一、 比如一个有个事务插入 persion 表插入了一条新记录

记录如下, name 为 Jerry , age 为 24 岁, 隐式主键 是 1, 事务 ID 回滚指针 ,我们假设为 NULL

362082b83fac7ca4c075c84e08e4c858.jpeg

二、 现在来了一个 事务 1 对该记录的 name 做出了修改,改为 Tom

  • 在事务 1修改该行(记录)数据时,数据库会先对该行加排他锁
  • 然后把该行数据拷贝到 undo log 中,作为旧记录,既在 undo log 中有当前行的拷贝副本
  • 拷贝完毕后,修改该行name为Tom,并且修改隐藏字段的事务 ID 为当前事务 1的 ID, 我们默认从 1 开始,之后递增,回滚指针指向拷贝到 undo log 的副本记录,既表示我的上一个版本就是它
  • 事务提交后,释放锁

a66931ed5fb53f673b093a1513e1c36d.jpeg

三、 又来了个事务 2 修改person 表 的同一个记录,将age 修改为 30 岁

  • 在事务2修改该行数据时,数据库也先为该行加锁
  • 然后把该行数据拷贝到 undo log 中,作为旧记录,发现该行记录已经有 undo log 了,那么最新的旧数据作为链表的表头,插在该行记录的 undo log 最前面
  • 修改该行 age 为 30 岁,并且修改隐藏字段的事务 ID 为当前事务 2的 ID, 那就是 2 ,回滚指针指向刚刚拷贝到 undo log 的副本记录
  • 事务提交,释放锁

6b9b8f830bed468fb578c2e42e000e41.jpeg

不同事务或者相同事务的对同一记录的修改,会导致该记录的undo log成为一条记录版本线链表,undo log 的链首就是最新的旧记录,链尾就是最早的旧记录

Read View 读视图

什么是 Read View?

**Read View 就是事务进行快照读操作的时候生产的读视图 (Read View)**,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID

我们可以把 Read View 简单的理解成有三个全局属性

  • 活跃事务列表

    • 一个数值列表
    • 用于维护 Read View 生成时刻系统 正活跃的事务 ID 列表
  • up_limit_id

    • lower water mark
    • 是 trx_list 列表中事务 ID 最小的 ID
  • low_limit_id

    • hight water mark
    • ReadView 生成时刻系统尚未分配的下一个事务 ID ,也就是 目前已出现过的事务 ID 的最大值 + 1
    • 为什么是 low_limit ? 因为它也是系统此刻可分配的事务 ID 的最小值

1f81ee8bd9968dade965476d1469fa04.jpeg

这两个 ID 其实就可以从当前执行的事务的视角,将所有的事务分为三个部分

  • 小于低水位的部分一定是当前事务开始前就提交了的部分

  • 大于等于高水位的则一定是还未提交的事务,我们一定不可见

  • 处于中间的部分就要分类讨论了:

    • 如果在视图数组中,说明当前事务开始时,这些事务仍在活跃,所以应该是不可见的;
    • 如果不在数组中,说明在仍活跃着的事务范围内,但其中有一些事务虽然不是开始最早的,但是结束的却比活跃数组中的事务早,以至于当前事务开始时,这些事务已经结束,所以就应该是可见的。

可见事务id总结

要么比低水位更早,要么比高水位的 id 小但是不能出现在活跃事物数组中。

整体流程

流程模拟

  • 当事务 2对某行数据执行了快照读,此时还有事务1和事务3在活跃中,事务 4在事务 2快照读前一刻提交更新了。

事务 1 事务 2 事务 3 事务 4 事务开始 事务开始 事务开始 事务开始 … … … 修改且已提交 进行中 快照读 进行中 … … …

  • up_limit_id 就是1,low_limit_id 就是 4 + 1 = 5,活跃事务列表值是 1, 3,Read View 如下图

6fb0b6239a9c32321efc0143226a9b6c.jpeg

  • 我们的例子中,只有事务 4 修改过该行记录,并在事务 2 执行快照读前,就提交了事务。所以当前DB_TRX_ID 为4

bb76566f0869ac99efba217600693078.jpeg

  • 拿该记录 DB_TRX_ID 字段记录的事务 ID 4 去跟 Read View 的 up_limit_id 比较

    • DB_TRX_ID(4)大于 up_limit_id(1),所以不符合可见条件
    • 继续判断 DB_TRX_ID(4) 是否大于等于 low_limit_id(5),不符和不可见条件
    • 最后判断 DB_TRX_ID(4) 是否处于 trx_list 中的活跃事务, 最后发现事务 ID 为 4 的事务不在当前活跃事务列表中, 符合可见性条件
    • 如果上述三点判断后不符合可见性,则去undolog链中找上一条记录的DB_TRX_ID,重复123步查找

    所以事务 4修改后提交的最新结果对事务 2 快照读时是可见的,获取字段的值为'A'

87379fb8387add3d9946bd6461b77c4e.jpeg

MVCC 相关问题

RR 是如何在 RC 级的基础上解决不可重复读的?

Read View 生成时机的不同,从而造成 RC , RR 级别下快照读的结果的不同

当前读和快照读在 RR 级别下的区别:

表1:

事务A 事务B 开启事务 开启事务 快照读(无影响)查询金额为500 快照读查询金额为500 更新金额为400 提交事务 select 快照读金额为500 select lock in share mode当前读金额为400

在上表的顺序下,事务 B 的在事务 A 提交修改后的快照读是旧版本数据,而当前读是实时新数据 400

表2:

事务A 事务B 开启事务 开启事务 快照读(无影响)查询金额为500 更新金额为400 提交事务 select 快照读金额为400 select lock in share mode当前读金额为400

而在表 2这里的顺序中,事务 B 在事务 A 提交后的快照读和当前读都是实时的新数据 400,这是为什么呢?

表2的第一次快照读在事务A更新金额为400而且commit之后发生

RC , RR 级别下的 InnoDB 快照读有什么不同?

正是 Read View 生成时机的不同,从而造成 RC , RR 级别下快照读的结果的不同

在 RC 隔离级别下,是每个快照读都会生成并获取最新的 Read View;

而在 RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View, 之后的快照读获取的都是同一个 Read View。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值