引言CURD满足什么属性能解决上述问题买票的过程得是原子的吧买票互相应该不能影响吧买完票应该要永久有效吧买前和买后都要是确定的状态吧一.事务定义事务就是一组DML语句组成这些语句在逻辑上存在相关性这一组DML语句要么全部成功要么全部失败是一个整体。MySQL提供一种机制保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。事务就是要做的或所做的事情主要用于处理操作量大复杂度高的数据。假设一种场景你毕业了学校的教务系统后台 MySQL 中不在需要你的数据要删除你的所有信息(一般不会:) ), 那么要删除你的基本信息(姓名电话籍贯等)的同时也删除和你有关的其他信息比如你的各科成绩你在校表现甚至你在论坛发过的文章等。这样就需要多条 MySQL 语句构成那么所有这些操作合起来就构成了一个事务。正如我们上面所说一个 MySQL 数据库可不止你一个事务在运行同一时刻甚至有大量的请求被包装成事务在向 MySQL 服务器发起事务处理请求。而每条事务至少一条 SQL 最多很多 SQL ,这样如果大家都访问同样的表数据在不加保护的情况就绝对会出现问题。甚至因为事务由多条 SQL 构成那么也会存在执行到一半出错或者不想再执行的情况那么已经执行的怎么办呢所有一个完整的事务绝对不是简单的 sql 集合还需要满足如下四个属性事务的属性原子性一个事务transaction中的所有操作要么全部完成要么全部不完成不会结束在中间某个环节。事务在执行过程中发生错误会被回滚Rollback到事务开始前的状态就像这个事务从来没有执行过一样。一致性在事务开始之前和事务结束以后数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。隔离性数据库允许多个并发事务同时对其数据进行读写和修改的能力隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别包括读未提交 Readuncommitted 、读提交 read committed 、可重复读 repeatable read 和串行化 Serializable 持久性事务处理结束后对数据的修改就是永久的即便系统故障也不会丢失。上面四个属性可以简称为 ACID 。原子性Atomicity或称不可分割性一致性Consistency隔离性Isolation又称独立性持久性Durability。DML是Data Manipulation Language数据操纵语言的缩写。DML 包含的操作命令含义示例INSERT插入数据INSERT INTO user VALUES (1, 张三, 20);UPDATE修改数据UPDATE user SET age 21 WHERE id 1;DELETE删除数据DELETE FROM user WHERE id 1;SELECT查询数据SELECT * FROM user;注意有些资料把 SELECT 归为 DQL数据查询语言严格来说 DML 只包括 INSERT、UPDATE、DELETE。事务与 DML 的关系事务 一组 DML 语句INSERT UPDATE DELETE 的组合事务的原因事务被 MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题.可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?因此事务本质上是为了应用层服务的.而不是伴随着数据库系统天生就有的.备注我们后面把 MySQL 中的一行信息称为一行记录二.事务的版本支持在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务 MyISAM 不支持。查看数据库引擎格式化查找序号存储引擎名称核心特点 / 用途1ARCHIVE归档存储引擎仅支持插入和查询不支持事务、XA、保存点适合存储历史数据。2BLACKHOLE写入的数据会被直接丢弃类似/dev/null常用于主从复制的中继节点。3MRG_MYISAM合并引擎用于将多个结构相同的 MyISAM 表合并为一个逻辑表。4FEDERATED联合存储引擎支持访问远程 MySQL 服务器上的表截图中显示为Support: NO表示未启用。5MyISAMMySQL 早期默认引擎不支持事务和行级锁读性能高适合非事务型场景。6PERFORMANCE_SCHEMA性能模式引擎用于收集和监控 MySQL 服务器的性能指标。7InnoDBMySQL 当前默认引擎支持事务、行级锁、外键、XA 和保存点是最通用的事务型引擎。8MEMORY内存引擎数据存储在内存中读写速度极快常用于临时表和缓存。9CSVCSV 引擎数据以 CSV 格式存储在文件中适合直接与外部工具交换数据。三.事务提交方式事务的提交方式常见的有两种自动提交手动提交查看事务提交方式用 SET 来改变 MySQL 的自动提交模式:SET AUTOCOMMIT0 禁止自动提交SET AUTOCOMMIT1 开启自动提交四.事务常见操作方式简单银行用户表提前准备服务器默认开启了 MySQL本地访问、SSH远程登录、DNS 解析三个核心服务。MySQL 只允许本机连接不允许外部访问。正在监听的服务器进程服务端口进程说明MySQL3306mysqld (PID: 3652206)MySQL 默认端口MySQL X Protocol33060mysqld (PID: 3652206)MySQL 新协议端口8.0SSH22sshd (PID: 852)SSH 远程连接服务systemd-resolve53systemd-resolve (PID: 739)DNS 解析服务SSH 子进程6010, 6011sshd: ubuntuSSH 登录会话每个连接一个使用 Win cmd 远程访问 ubuntu云服务器mysqld 服务需要 Win 上也安装了 MySQL这里看到结果即可注意使用本地 MySQL 客户端能看不到链接效果本地可能使用域间套接字查不到链接。在我当前的windows下我没有配置环境变量,所以我采用的是别人配置好的效果这个下面是配置好的过程显示C:\Users\hjqmysql -uroot -p -h42.192.83.143 Enter password: *********** Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 3484 Server version: 5.7.33 MySQL Community Server (GPL) Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type help; or \h for help. Type \c to clear the current input statement.使用netstat查看链接情况可知mysql本质是一个客户端进程[hjqVM-0-3-centos ~]$ sudo netstat -ntp Active Internet connections (w/o servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp6 0 0 172.17.为了便于演示我们将mysql的默认隔离级别设置成读未提交。重新启动终端进行查看创建测试表正常演示证明事务的开始与回滚这是 MySQL 中保存点Savepoint的创建命令。savepoint s1;的含义这条命令创建了一个名为s1的保存点。保存点是什么在一个长事务中你可以设置多个保存点作为回滚的标记位置。如果事务执行到一半出错不需要回滚整个事务只需回滚到某个保存点即可。这是我重新建立的表格数据也有修改非正常演示1.证明未commit客户端崩溃MySQL自动会回滚隔离级别设置为读未提交终端A终端B非正常演示2 . 证明commit了客户端崩溃MySQL数据不会在受影响已经持久化终端A终端B非正常演示3 . 对比试验。证明begin操作会自动更改提交方式不会受MySQL是否自动提交影响终端A终端B非正常演示4 - 证明单条 SQL 与事务的关系实验一终端A终端B实验二终端A终端B结论只要输入begin或者start transaction事务便必须要通过commit提交才会持久化与是否设置set autocommit无关。事务可以手动回滚同时当操作异常MySQL会自动回滚对于 InnoDB 每一条 SQL 语言都默认封装成事务自动提交。select有特殊情况因为MySQL 有 MVCC 从上面的例子我们能看到事务本身的原子性(回滚)持久性(commit)事务操作注意事项如果没有设置保存点也可以回滚只能回滚到事务的开始。直接使用 rollback(前提是事务还没有提交)如果一个事务被提交了commit则不可以回退rollback可以选择回退到哪个保存点InnoDB 支持事务 MyISAM 不支持事务开始事务可以使 start transaction 或者 begin那么隔离性一致性咱们继续讲解五.事务隔离级别如何理解隔离性MySQL服务可能会同时被多个客户端进程(线程)访问访问的方式以事务方式进行一个事务可能由多条SQL构成也就意味着任何一个事务都有执行前、执行中、执行后的阶段。而所谓的原子性其实就是让用户层要么看到执行前要么看到执行后。执行中出现问题可以随时回滚。所以单个事务对用户表现出来的特性就是原子性。但毕竟所有事务都要有个执行过程那么在多个事务各自执行多个SQL的时候就还是有可能会出现互相影响的情况。比如多个事务同时访问同一张表甚至同一行数据。就如同你妈妈给你说你要么别学要学就学到最好。至于你怎么学中间有什么困难你妈妈不关心。那么你的学习对你妈妈来讲就是原子的。那么你学习过程中很容易受到干扰此时就需要将你的学习隔离开保证你的学习环境是健康的。数据库中为了保证事务执行过程中尽量不受干扰就有了一个重要特征隔离性数据库中允许事务受不同程度的干扰就有了一种重要特征隔离级别隔离级别读未提交【Read Uncommitted】在该隔离级别所有的事务都可以看到其他事务没有提交的执行结果。实际生产中不可能使用这种隔离级别的但是相当于没有任何隔离性也会有很多并发问题如脏读、幻读不可重复读等我们上面为了做实验方便用的就是这个隔离性。读提交【Read Committed】该隔离级别是大多数数据库的默认的隔离级别不是MySQL默认的。它满足了隔离的简单定义一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读即一个事务执行时如果多次select可能得到不同的结果。可重复读【Repeatable Read】这是MySQL默认的隔离级别它确保同一个事务在执行中多次读取操作数据时会看到同样的数据行。但是会有幻读问题。串行化【Serializable】这是事务的最高隔离级别它通过强制事务排序使之不可能相互冲突从而解决了幻读的问题。它在每个读的数据行上面加上共享锁但是可能会导致超时和锁竞争这种隔离级别太极端实际生产基本不使用。隔离级别如何实现隔离基本都是通过锁实现的不同的隔离级别锁的使用是不同的。常见有表锁、行锁、读锁、写锁、间隙锁(GAP)、Next-Key锁(GAP行锁)等。不过我们目前现有这个认识就行先关注上层使用。查看与设置隔离性a.查看-- 查看全局隔离级别 SELECT global.transaction_isolation; -- 查看当前会话隔离级别 SELECT session.transaction_isolation; SELECT transaction_isolation; -- 查看所有隔离级别相关变量 SHOW VARIABLES LIKE %isolation%;注意MySQL 版本对照MySQL 版本查询隔离级别的命令5.7 及更早SELECT tx_isolation;8.0 及更高SELECT transaction_isolation;b.设置.-- 设置当前会话 or 全局隔离级别语法 SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}当前会话全局会话注意 1. 改变当前会话隔离级别只对当前连接生效关闭连接后失效重启不受影响 2. 改变全局隔离级别会对之后创建的所有新连接生效重启MySQL后恢复为配置文件中的默认值 3. 已经存在的旧连接不受全局修改的影响仍然保持原来的隔离级别 4. 想要永久修改隔离级别需要在配置文件my.cnf 或 my.ini中设置例如 transaction-isolation READ-COMMITTED 5. 使用 SET GLOBAL 修改后如果不重启全局设置会一直保留即使 MySQL 服务没重启 6. 重启 MySQL 后全局设置会丢失重新读取配置文件中的值 7. 可以通过以下命令验证全局和会话的隔离级别 SELECT global.transaction_isolation, session.transaction_isolation;设置当前会话隔离性另起一个会话看不多只影响当前会话mysql set session transaction isolation level serializable; -- 串行化 Query OK, 0 rows affected (0.00 sec) mysql SELECT global.tx_isolation; --全局隔离性还是RR ----------------------- | global.tx_isolation | ----------------------- | REPEATABLE-READ | ----------------------- 1 row in set, 1 warning (0.00 sec) mysql SELECT session.tx_isolation; --会话隔离性成为串行化 ------------------------ | session.tx_isolation | ------------------------ | SERIALIZABLE | ------------------------ 1 row in set, 1 warning (0.00 sec) mysql SELECT tx_isolation; --同上 ---------------- | tx_isolation | ---------------- | SERIALIZABLE | 读未提交【Read Uncommitted】 ---------------- 1 row in set, 1 warning (0.00 sec)设置全局隔离性另起一个会话会被影响mysql set global transaction isolation level READ UNCOMMITTED; Query OK, 0 rows affected (0.00 sec) mysql SELECT global.tx_isolation; ----------------------- | global.tx_isolation | ----------------------- | READ-UNCOMMITTED | ----------------------- 1 row in set, 1 warning (0.00 sec) mysql SELECT session.tx_isolation; ------------------------ | session.tx_isolation | ------------------------ | READ-UNCOMMITTED | ------------------------ 1 row in set, 1 warning (0.00 sec) mysql SELECT tx_isolation; ------------------ | tx_isolation | ------------------ | READ-UNCOMMITTED | ------------------ 1 row in set, 1 warning (0.00 sec)注意如果没有现象关闭mysql客户端重新连接。读未提交【Read Uncommitted】几乎没有加锁虽然效率高但是问题太多严重不建议采用终端A终端B一个事务在执行中读到另一个执行中事务的更新(或其他操作)但是未commit的数据这种现象叫做脏读(dirty read)读提交【Read Committed】终端A这两步之间终端A要重启哈之后查当前数据手动进行事务开始然后进行更新更新后到commmit提交前切换到终端B终端B手动开启和终端A一前一后然后查看数据commit之前查不到会发现是旧的数据commit之后变成新的数据终端 B 进行第 2 次 select * from account 之前此时还在当前事务中并未commit那么就造成了同一个事务内同样的读取在不同的时间段(依旧还在事务操作中)读取到了不同的值这种现象叫做不可重复读(non reapeatable read)可重复读【Repeatable Read】终端A终端B可以看到在终端B中事务无论什么时候进行查找看到的结果都是一致的这叫做可重复读如果将上面的终端 A 中的 update 操作改成 insert 操作会有什么问题终端A终端B多次查看发现终端A在对应事务中insert的数据在终端B的事务周期中也没有什么影响也符合可重复的特点。但是一般的数据库在可重复读情况时候无法屏蔽其他事务insert的数据(为什么因为隔离性实现是对数据加锁完成的而insert待插入的数据因为并不存在那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的但是insert的数据在可重复读情况被读取出来导致多次查找时会多查找出来新的记录就如同产生了幻觉。这种现象叫做幻读(phantom read)。很明显MySQL在RR级别的时候是解决了幻读问题的(解决的方式是用Next-Key锁(GAP行锁)解决的。串行化【serializable】对所有操作全部加锁进行串行化不会有问题但是只要串行化效率很低几乎完全不会被采用终端A终端B总结其中隔离级别越严格安全性越高但数据库的并发性能也就越低往往需要在两者之间找一个平衡点。不可重复读的重点是修改和删除同样的条件, 你读取过的数据,再次读取出来发现值不一样了幻读的重点在于新增同样的条件, 第1次和第2次读出来的记录数不一样说明 mysql 默认的隔离级别是可重复读,一般情况下不要修改上面的例子可以看出事务也有长短事务这样的概念。事务间互相影响指的是事务在并行执行的时候即都没有commit的时候影响会比较大。六.一致性(Consistency)事务执行的结果必须使数据库从一个一致性状态变到另一个一致性状态。当数据库只包含事务成功提交的结果时数据库处于一致性状态。如果系统运行发生中断某个事务尚未完成而被迫中断而改未完成的事务对数据库所做的修改已被写入数据库此时数据库就处于一种不正确不一致的状态。因此一致性是通过原子性来保证的。其实一致性和用户的业务逻辑强相关一般MySQL提供技术支持但是一致性还是要用户业务逻辑做支撑也就是一致性是由用户决定的。而技术上通过AID保证C七.推荐阅读https://www.jianshu.com/p/398d788e1083https://tech.meituan.com/2014/08/20/innodb-lock.htmlhttps://www.cnblogs.com/aspirant/p/9177978.html八.如何理解隔离性(理论加深数据库并发的场景有三种读-读 不存在任何问题也不需要并发控制读-写 有线程安全问题可能会造成事务隔离性问题可能遇到脏读幻读不可重复读写-写 有线程安全问题可能会存在更新丢失问题比如第一类更新丢失第二类更新丢失(后面补充)1.读-写多版本并发控制 MVCC 是一种用来解决 读-写冲突 的无锁并发控制为事务分配单向增长的事务ID为每个修改保存一个版本版本与事务ID关联读操作只读该事务开始前的数据库的快照。所以 MVCC 可以为数据库解决以下问题在并发读写数据库时可以做到在读操作时不用阻塞写操作写操作也不用阻塞读操作提高了数据库并发读写的性能同时还可以解决脏读幻读不可重复读等事务隔离问题但不能解决更新丢失问题理解 MVCC 需要知道三个前提知识3个记录隐藏字段undo 日志Read View3个记录隐藏列字段DB_TRX_ID 6 byte最近修改( 修改/插入 )事务ID记录创建这条记录/最后一次修改该记录的事务IDDB_ROLL_PTR : 7 byte回滚指针指向这条记录的上一个版本简单理解成指向历史版本就行这些数据一般在 undo log 中DB_ROW_ID : 6 byte隐含的自增ID隐藏主键如果数据表没有主键 InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引补充实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除而是删除flag变了假设测试表结构是上面描述的意思是我们目前并不知道创建该记录的事务ID隐式主键我们就默认设置成null1。第一条记录也没有其他版本我们设置回滚指针为null。undoMySQL 将来是以服务进程的方式在内存中运行。我们之前所讲的所有机制索引事务隔离性日志等都是在内存中完成的即在 MySQL 内部的相关缓冲区中保存相关数据完成各种判断操作。然后在合适的时候将相关数据刷新到磁盘当中的。所以我们这里理解undo log简单理解成就是 MySQL 中的一段内存缓冲区用来保存日志数据的就行。模拟 MVCC现在有一个事务10(仅仅为了好区分)对student表中记录进行修改(update)将name(张三)改成name(李四)。事务10,因为要修改所以要先给该记录加行锁。修改前现将改行记录拷贝到undo log中所以undo log中就有了一行副本数据。(原理就是写时拷贝)所以现在 MySQL 中有两行同样的记录。现在修改原始记录中的name改成 李四。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务10 的ID, 我们默认从 10 开始之后递增。而原始记录的回滚指针 DB_ROLL_PTR 列里面写入undo log中副本数据的地址从而指向副本记录既表示我的上一个版本就是它。事务10提交释放锁。备注此时最新的记录是’李四‘那条记录。现在又有一个事务11对student表中记录进行修改(update)将age(28)改成age(38)。事务11,因为也要修改所以要先给该记录加行锁。该记录是那条修改前现将改行记录拷贝到undo log中所以undo log中就又有了一行副本数据。此时新的副本我们采用头插方式插入undo log。现在修改原始记录中的age改成 38。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务11 的ID。而原始记录的回滚指针 DB_ROLL_PTR 列里面写入undo log中副本数据的地址从而指向副本记录既表示我的上一个版本就是它。事务11提交释放锁。这样我们就有了一个基于链表记录的历史版本链。所谓的回滚无非就是用历史数据覆盖当前数据。上面的一个一个版本我们可以称之为一个一个的快照。上面是以更新update为主讲的如果是 delete 呢一样的别忘了删数据不是清空而是设置 flag 为删除即可也可以形成版本。那 insert 呢因为 insert 是插入也就是之前没有数据那么 insert 也就没有历史版本。但是一般为了回滚操作insert 的数据也是要被放入 undo log 中如果当前事务 commit 了那么这个 undo log 的历史 insert 记录就可以被清空了。总结操作是否形成版本链说明update是修改前后形成版本链delete是标记删除相当于新版本insert否没有旧版本只有 undo log 用于回滚select否只读操作不产生版本select 的特殊性当前读 vs 快照读那么 select 呢首先select不会对数据做任何修改所以为select维护多版本没有意义。此时有个问题select 读取是读取最新的版本呢还是读取历史版本两种读取方式读取方式含义示例是否加锁当前读读取最新的记录SELECT ... LOCK IN SHARE MODESELECT ... FOR UPDATEINSERT、UPDATE、DELETE需要加锁快照读读取历史版本一般而言普通的SELECT不需要加锁为什么需要区分在多个事务同时删改时都是当前读是要加锁的。那同时有 select 过来如果也要读取最新版当前读→ 需要加锁 →串行化如果是快照读读取历史版本 → 不受加锁限制 →可以并行执行换而言之快照读提高了效率这就是 MVCC 的意义所在。隔离级别的作用是什么决定了 select 是当前读还是快照读隔离级别。那为什么要有隔离级别呢事务都是原子的所以无论如何事务总有先后。但是经过上面的操作可以发现事务从begin→CURD→commit是有一个阶段的也就是事务有执行前、执行中、执行后的阶段。不管怎么启动多个事务总是有先有后的。多个事务在执行中的 CURD 操作是会交织在一起的。为了保证事务的有先有后应该让不同的事务看到它该看到的内容。问题重要先来的事务应该看到后来的事务所做的修改吗隔离级别先来事务能否看到后来事务未提交的修改能否看到后来事务已提交的修改读未提交能能读提交不能能但可能不可重复读可重复读不能不能看到的是快照串行化不能且加锁阻塞不能加锁阻塞这就是隔离性与隔离级别要解决的问题。先来的事务应不应该看到后来的事务所做的修改呢?如何保证不同的事务看到不同的内容呢也就是如何如何实现隔离级别请往下看接下来为你揭晓。Read ViewRead View就是事务进行 快照读 操作的时候生产的 读视图 (Read View)在该事务执行的快照读的那一刻会生成数据库系统当前的一个快照记录并维护系统当前活跃事务的ID(当每个事务开启时都会被分配一个ID, 这个ID是递增的所以最新的事务ID值越大)Read View 在 MySQL 源码中,就是一个类本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候对该记录创建一个 Read View 读视图把它比作条件,用来判断当前事务能够看到哪个版本的数据既可能是当前最新的数据也有可能是该行记录的 undo log 里面的某个版本的数据。下面是 ReadView 结构,我们简化一下class ReadView { // 省略... private: /** 高水位大于等于这个ID的事务均不可见*/ trx_id_t m_low_limit_id /** 低水位小于这个ID的事务均可见 */ trx_id_t m_up_limit_id; /** 创建该 Read View 的事务ID*/ trx_id_t m_creator_trx_id; /** 创建视图时的活跃事务id列表*/ ids_t m_ids; /** 配合purge标识该视图不需要小于m_low_limit_no的UNDO LOG * 如果其他视图也不需要则可以删除小于m_low_limit_no的UNDO LOG*/ trx_id_t m_low_limit_no; /** 标记视图是否被关闭*/ bool m_closed; // 省略... };m_ids; //一张列表用来维护Read View生成时刻系统正活跃的事务ID up_limit_id; //记录m_ids列表中事务ID最小的ID(没有写错) low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID也就是目前已出现过的事务ID的 最大值1(也没有写错) creator_trx_id //创建该ReadView的事务ID我们在实际读取数据版本链的时候是能读取到每一个版本对应的事务ID的即当前记录的DB_TRX_ID 。那么我们现在手里面有的东西就有当前快照读的 ReadView 和 版本链中的某一个记录的DB_TRX_ID 。所以现在的问题就是当前快照读应不应该读到当前版本记录。一张图解决所有问题Read View 采用懒加载机制不是事务创建时就有的而是事务首次进行快照读时才生成。RC 级别每次快照读都重新生成RR 级别只生成一次并复用。对应源码策略如果查到不应该看到当前版本接下来就是遍历下一个版本直到符合条件即可以看到。上面的readview 是当你进行select的时候会自动形成。整体流程假设当前有条记录事务操作事务4修改name(张三) 变成name(李四)当 事务2 对某行数据执行了 快照读 数据库为该行数据生成一个 Read View 读视图//事务2的 Read View m_ids; // 1,3 up_limit_id; // 1 low_limit_id; // 4 1 5原因ReadView生成时刻系统尚未分配的下一个事务ID creator_trx_id // 2此时版本链是只有事务4修改过该行记录并在事务2执行快照读前就提交了事务。我们的事务2在快照读该行记录的时候就会拿该行记录的 DB_TRX_ID 去跟up_limit_id,low_limit_id和活跃事务ID列表(trx_list) 进行比较判断当前事务2能看到该记录的版本。//事务2的 Read View m_ids; // 1,3 up_limit_id; // 1 low_limit_id; // 4 1 5原因ReadView生成时刻系统尚未分配的下一个事务ID creator_trx_id // 2 //事务4提交的记录对应的事务ID DB_TRX_ID4 //比较步骤 DB_TRX_ID4 up_limit_id1 ? 不小于下一步 DB_TRX_ID4 low_limit_id(5) ? 不大于下一步 m_ids.contains(DB_TRX_ID) ? 不包含说明事务4不在当前的活跃事务中 //结论 故事务4的更改应该看到。 所以事务2能读到的最新数据记录是事务4所提交的版本而事务4提交的版本也是全局角度上最新的版本RR 与 RC的本质区别当前读和快照读在 RR 级别下的区别select * from user lock in share mode以加共享锁方式进行读取对应的就是当前读。设置RR模式下测试 mysql set global transaction isolation level REPEATABLE READ; Query OK, 0 rows affected (0.00 sec) --重启终端 mysql select transaction_isolation; ----------------- | transaction_isolation | ----------------- | REPEATABLE-READ | ----------------- 1 row in set, 1 warning (0.00 sec) --依旧用之前的表 create table if not exists account( id int primary key, name varchar(50) not null default , blance decimal(10,2) not null default 0.0 )ENGINEInnoDB DEFAULT CHARSETUTF8; --插入一条记录用来测试 mysql insert into user (id, age, name) values (1, 15,黄蓉); Query OK, 1 row affected (0.00 sec)测试用例1-表1测试用例2-表2用例1与用例2唯一区别仅仅是 表1 的事务B在事务A修改age前 快照读 过一次age数据而 表2 的事务B在事务A修改age前没有进行过快照读。结论事务中快照读的结果是非常依赖该事务首次出现快照读的地方即某个事务中首次出现快照读决定该事务后续快照读结果的能力delete同样如此RR 与 RC的本质区别正是Read View生成时机的不同从而造成RC,RR级别下快照读的结果的不同在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来此后在调用快照读的时候还是使用的是同一个Read View所以只要当前事务在其他事务提交更新之前使用过快照读那么之后的快照读使用的都是同一个Read View所以对之后的修改不可见即RR级别下快照读生成Read View时Read View会记录此时所有其他活动事务的快照这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见而在RC级别下的事务中每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因总之在RC隔离级别下是每个快照读都会生成并获取最新的Read View而在RR隔离级别下则同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。正是RC每次快照读都会形成Read View所以RC才会有不可重复读问题。2.读-读不讨论原因多个事务同时执行SELECT快照读彼此之间不会产生任何冲突因为读操作不加锁快照读情况下也不会修改数据。所以不需要讨论。3.写-写现阶段直接理解成都是当前读当前不做深究原因多个事务同时执行INSERT、UPDATE、DELETE都是当前读会涉及加锁、锁等待、死锁等问题。现阶段先理解为“都是当前读”暂时不深入分析锁的细节。4.推荐阅读关于这块有很好的文章推荐大家阅读https://blog.csdn.net/SnailMann/article/details/94724197https://www.cnblogs.com/f-ck-need-u/archive/2018/05/08/9010872.htmlhttps://blog.csdn.net/chenghan_yang/article/details/97630626
【MYSQL】事务管理--详解
发布时间:2026/6/2 2:45:20
引言CURD满足什么属性能解决上述问题买票的过程得是原子的吧买票互相应该不能影响吧买完票应该要永久有效吧买前和买后都要是确定的状态吧一.事务定义事务就是一组DML语句组成这些语句在逻辑上存在相关性这一组DML语句要么全部成功要么全部失败是一个整体。MySQL提供一种机制保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。事务就是要做的或所做的事情主要用于处理操作量大复杂度高的数据。假设一种场景你毕业了学校的教务系统后台 MySQL 中不在需要你的数据要删除你的所有信息(一般不会:) ), 那么要删除你的基本信息(姓名电话籍贯等)的同时也删除和你有关的其他信息比如你的各科成绩你在校表现甚至你在论坛发过的文章等。这样就需要多条 MySQL 语句构成那么所有这些操作合起来就构成了一个事务。正如我们上面所说一个 MySQL 数据库可不止你一个事务在运行同一时刻甚至有大量的请求被包装成事务在向 MySQL 服务器发起事务处理请求。而每条事务至少一条 SQL 最多很多 SQL ,这样如果大家都访问同样的表数据在不加保护的情况就绝对会出现问题。甚至因为事务由多条 SQL 构成那么也会存在执行到一半出错或者不想再执行的情况那么已经执行的怎么办呢所有一个完整的事务绝对不是简单的 sql 集合还需要满足如下四个属性事务的属性原子性一个事务transaction中的所有操作要么全部完成要么全部不完成不会结束在中间某个环节。事务在执行过程中发生错误会被回滚Rollback到事务开始前的状态就像这个事务从来没有执行过一样。一致性在事务开始之前和事务结束以后数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。隔离性数据库允许多个并发事务同时对其数据进行读写和修改的能力隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别包括读未提交 Readuncommitted 、读提交 read committed 、可重复读 repeatable read 和串行化 Serializable 持久性事务处理结束后对数据的修改就是永久的即便系统故障也不会丢失。上面四个属性可以简称为 ACID 。原子性Atomicity或称不可分割性一致性Consistency隔离性Isolation又称独立性持久性Durability。DML是Data Manipulation Language数据操纵语言的缩写。DML 包含的操作命令含义示例INSERT插入数据INSERT INTO user VALUES (1, 张三, 20);UPDATE修改数据UPDATE user SET age 21 WHERE id 1;DELETE删除数据DELETE FROM user WHERE id 1;SELECT查询数据SELECT * FROM user;注意有些资料把 SELECT 归为 DQL数据查询语言严格来说 DML 只包括 INSERT、UPDATE、DELETE。事务与 DML 的关系事务 一组 DML 语句INSERT UPDATE DELETE 的组合事务的原因事务被 MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题.可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?因此事务本质上是为了应用层服务的.而不是伴随着数据库系统天生就有的.备注我们后面把 MySQL 中的一行信息称为一行记录二.事务的版本支持在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务 MyISAM 不支持。查看数据库引擎格式化查找序号存储引擎名称核心特点 / 用途1ARCHIVE归档存储引擎仅支持插入和查询不支持事务、XA、保存点适合存储历史数据。2BLACKHOLE写入的数据会被直接丢弃类似/dev/null常用于主从复制的中继节点。3MRG_MYISAM合并引擎用于将多个结构相同的 MyISAM 表合并为一个逻辑表。4FEDERATED联合存储引擎支持访问远程 MySQL 服务器上的表截图中显示为Support: NO表示未启用。5MyISAMMySQL 早期默认引擎不支持事务和行级锁读性能高适合非事务型场景。6PERFORMANCE_SCHEMA性能模式引擎用于收集和监控 MySQL 服务器的性能指标。7InnoDBMySQL 当前默认引擎支持事务、行级锁、外键、XA 和保存点是最通用的事务型引擎。8MEMORY内存引擎数据存储在内存中读写速度极快常用于临时表和缓存。9CSVCSV 引擎数据以 CSV 格式存储在文件中适合直接与外部工具交换数据。三.事务提交方式事务的提交方式常见的有两种自动提交手动提交查看事务提交方式用 SET 来改变 MySQL 的自动提交模式:SET AUTOCOMMIT0 禁止自动提交SET AUTOCOMMIT1 开启自动提交四.事务常见操作方式简单银行用户表提前准备服务器默认开启了 MySQL本地访问、SSH远程登录、DNS 解析三个核心服务。MySQL 只允许本机连接不允许外部访问。正在监听的服务器进程服务端口进程说明MySQL3306mysqld (PID: 3652206)MySQL 默认端口MySQL X Protocol33060mysqld (PID: 3652206)MySQL 新协议端口8.0SSH22sshd (PID: 852)SSH 远程连接服务systemd-resolve53systemd-resolve (PID: 739)DNS 解析服务SSH 子进程6010, 6011sshd: ubuntuSSH 登录会话每个连接一个使用 Win cmd 远程访问 ubuntu云服务器mysqld 服务需要 Win 上也安装了 MySQL这里看到结果即可注意使用本地 MySQL 客户端能看不到链接效果本地可能使用域间套接字查不到链接。在我当前的windows下我没有配置环境变量,所以我采用的是别人配置好的效果这个下面是配置好的过程显示C:\Users\hjqmysql -uroot -p -h42.192.83.143 Enter password: *********** Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 3484 Server version: 5.7.33 MySQL Community Server (GPL) Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type help; or \h for help. Type \c to clear the current input statement.使用netstat查看链接情况可知mysql本质是一个客户端进程[hjqVM-0-3-centos ~]$ sudo netstat -ntp Active Internet connections (w/o servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp6 0 0 172.17.为了便于演示我们将mysql的默认隔离级别设置成读未提交。重新启动终端进行查看创建测试表正常演示证明事务的开始与回滚这是 MySQL 中保存点Savepoint的创建命令。savepoint s1;的含义这条命令创建了一个名为s1的保存点。保存点是什么在一个长事务中你可以设置多个保存点作为回滚的标记位置。如果事务执行到一半出错不需要回滚整个事务只需回滚到某个保存点即可。这是我重新建立的表格数据也有修改非正常演示1.证明未commit客户端崩溃MySQL自动会回滚隔离级别设置为读未提交终端A终端B非正常演示2 . 证明commit了客户端崩溃MySQL数据不会在受影响已经持久化终端A终端B非正常演示3 . 对比试验。证明begin操作会自动更改提交方式不会受MySQL是否自动提交影响终端A终端B非正常演示4 - 证明单条 SQL 与事务的关系实验一终端A终端B实验二终端A终端B结论只要输入begin或者start transaction事务便必须要通过commit提交才会持久化与是否设置set autocommit无关。事务可以手动回滚同时当操作异常MySQL会自动回滚对于 InnoDB 每一条 SQL 语言都默认封装成事务自动提交。select有特殊情况因为MySQL 有 MVCC 从上面的例子我们能看到事务本身的原子性(回滚)持久性(commit)事务操作注意事项如果没有设置保存点也可以回滚只能回滚到事务的开始。直接使用 rollback(前提是事务还没有提交)如果一个事务被提交了commit则不可以回退rollback可以选择回退到哪个保存点InnoDB 支持事务 MyISAM 不支持事务开始事务可以使 start transaction 或者 begin那么隔离性一致性咱们继续讲解五.事务隔离级别如何理解隔离性MySQL服务可能会同时被多个客户端进程(线程)访问访问的方式以事务方式进行一个事务可能由多条SQL构成也就意味着任何一个事务都有执行前、执行中、执行后的阶段。而所谓的原子性其实就是让用户层要么看到执行前要么看到执行后。执行中出现问题可以随时回滚。所以单个事务对用户表现出来的特性就是原子性。但毕竟所有事务都要有个执行过程那么在多个事务各自执行多个SQL的时候就还是有可能会出现互相影响的情况。比如多个事务同时访问同一张表甚至同一行数据。就如同你妈妈给你说你要么别学要学就学到最好。至于你怎么学中间有什么困难你妈妈不关心。那么你的学习对你妈妈来讲就是原子的。那么你学习过程中很容易受到干扰此时就需要将你的学习隔离开保证你的学习环境是健康的。数据库中为了保证事务执行过程中尽量不受干扰就有了一个重要特征隔离性数据库中允许事务受不同程度的干扰就有了一种重要特征隔离级别隔离级别读未提交【Read Uncommitted】在该隔离级别所有的事务都可以看到其他事务没有提交的执行结果。实际生产中不可能使用这种隔离级别的但是相当于没有任何隔离性也会有很多并发问题如脏读、幻读不可重复读等我们上面为了做实验方便用的就是这个隔离性。读提交【Read Committed】该隔离级别是大多数数据库的默认的隔离级别不是MySQL默认的。它满足了隔离的简单定义一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读即一个事务执行时如果多次select可能得到不同的结果。可重复读【Repeatable Read】这是MySQL默认的隔离级别它确保同一个事务在执行中多次读取操作数据时会看到同样的数据行。但是会有幻读问题。串行化【Serializable】这是事务的最高隔离级别它通过强制事务排序使之不可能相互冲突从而解决了幻读的问题。它在每个读的数据行上面加上共享锁但是可能会导致超时和锁竞争这种隔离级别太极端实际生产基本不使用。隔离级别如何实现隔离基本都是通过锁实现的不同的隔离级别锁的使用是不同的。常见有表锁、行锁、读锁、写锁、间隙锁(GAP)、Next-Key锁(GAP行锁)等。不过我们目前现有这个认识就行先关注上层使用。查看与设置隔离性a.查看-- 查看全局隔离级别 SELECT global.transaction_isolation; -- 查看当前会话隔离级别 SELECT session.transaction_isolation; SELECT transaction_isolation; -- 查看所有隔离级别相关变量 SHOW VARIABLES LIKE %isolation%;注意MySQL 版本对照MySQL 版本查询隔离级别的命令5.7 及更早SELECT tx_isolation;8.0 及更高SELECT transaction_isolation;b.设置.-- 设置当前会话 or 全局隔离级别语法 SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}当前会话全局会话注意 1. 改变当前会话隔离级别只对当前连接生效关闭连接后失效重启不受影响 2. 改变全局隔离级别会对之后创建的所有新连接生效重启MySQL后恢复为配置文件中的默认值 3. 已经存在的旧连接不受全局修改的影响仍然保持原来的隔离级别 4. 想要永久修改隔离级别需要在配置文件my.cnf 或 my.ini中设置例如 transaction-isolation READ-COMMITTED 5. 使用 SET GLOBAL 修改后如果不重启全局设置会一直保留即使 MySQL 服务没重启 6. 重启 MySQL 后全局设置会丢失重新读取配置文件中的值 7. 可以通过以下命令验证全局和会话的隔离级别 SELECT global.transaction_isolation, session.transaction_isolation;设置当前会话隔离性另起一个会话看不多只影响当前会话mysql set session transaction isolation level serializable; -- 串行化 Query OK, 0 rows affected (0.00 sec) mysql SELECT global.tx_isolation; --全局隔离性还是RR ----------------------- | global.tx_isolation | ----------------------- | REPEATABLE-READ | ----------------------- 1 row in set, 1 warning (0.00 sec) mysql SELECT session.tx_isolation; --会话隔离性成为串行化 ------------------------ | session.tx_isolation | ------------------------ | SERIALIZABLE | ------------------------ 1 row in set, 1 warning (0.00 sec) mysql SELECT tx_isolation; --同上 ---------------- | tx_isolation | ---------------- | SERIALIZABLE | 读未提交【Read Uncommitted】 ---------------- 1 row in set, 1 warning (0.00 sec)设置全局隔离性另起一个会话会被影响mysql set global transaction isolation level READ UNCOMMITTED; Query OK, 0 rows affected (0.00 sec) mysql SELECT global.tx_isolation; ----------------------- | global.tx_isolation | ----------------------- | READ-UNCOMMITTED | ----------------------- 1 row in set, 1 warning (0.00 sec) mysql SELECT session.tx_isolation; ------------------------ | session.tx_isolation | ------------------------ | READ-UNCOMMITTED | ------------------------ 1 row in set, 1 warning (0.00 sec) mysql SELECT tx_isolation; ------------------ | tx_isolation | ------------------ | READ-UNCOMMITTED | ------------------ 1 row in set, 1 warning (0.00 sec)注意如果没有现象关闭mysql客户端重新连接。读未提交【Read Uncommitted】几乎没有加锁虽然效率高但是问题太多严重不建议采用终端A终端B一个事务在执行中读到另一个执行中事务的更新(或其他操作)但是未commit的数据这种现象叫做脏读(dirty read)读提交【Read Committed】终端A这两步之间终端A要重启哈之后查当前数据手动进行事务开始然后进行更新更新后到commmit提交前切换到终端B终端B手动开启和终端A一前一后然后查看数据commit之前查不到会发现是旧的数据commit之后变成新的数据终端 B 进行第 2 次 select * from account 之前此时还在当前事务中并未commit那么就造成了同一个事务内同样的读取在不同的时间段(依旧还在事务操作中)读取到了不同的值这种现象叫做不可重复读(non reapeatable read)可重复读【Repeatable Read】终端A终端B可以看到在终端B中事务无论什么时候进行查找看到的结果都是一致的这叫做可重复读如果将上面的终端 A 中的 update 操作改成 insert 操作会有什么问题终端A终端B多次查看发现终端A在对应事务中insert的数据在终端B的事务周期中也没有什么影响也符合可重复的特点。但是一般的数据库在可重复读情况时候无法屏蔽其他事务insert的数据(为什么因为隔离性实现是对数据加锁完成的而insert待插入的数据因为并不存在那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的但是insert的数据在可重复读情况被读取出来导致多次查找时会多查找出来新的记录就如同产生了幻觉。这种现象叫做幻读(phantom read)。很明显MySQL在RR级别的时候是解决了幻读问题的(解决的方式是用Next-Key锁(GAP行锁)解决的。串行化【serializable】对所有操作全部加锁进行串行化不会有问题但是只要串行化效率很低几乎完全不会被采用终端A终端B总结其中隔离级别越严格安全性越高但数据库的并发性能也就越低往往需要在两者之间找一个平衡点。不可重复读的重点是修改和删除同样的条件, 你读取过的数据,再次读取出来发现值不一样了幻读的重点在于新增同样的条件, 第1次和第2次读出来的记录数不一样说明 mysql 默认的隔离级别是可重复读,一般情况下不要修改上面的例子可以看出事务也有长短事务这样的概念。事务间互相影响指的是事务在并行执行的时候即都没有commit的时候影响会比较大。六.一致性(Consistency)事务执行的结果必须使数据库从一个一致性状态变到另一个一致性状态。当数据库只包含事务成功提交的结果时数据库处于一致性状态。如果系统运行发生中断某个事务尚未完成而被迫中断而改未完成的事务对数据库所做的修改已被写入数据库此时数据库就处于一种不正确不一致的状态。因此一致性是通过原子性来保证的。其实一致性和用户的业务逻辑强相关一般MySQL提供技术支持但是一致性还是要用户业务逻辑做支撑也就是一致性是由用户决定的。而技术上通过AID保证C七.推荐阅读https://www.jianshu.com/p/398d788e1083https://tech.meituan.com/2014/08/20/innodb-lock.htmlhttps://www.cnblogs.com/aspirant/p/9177978.html八.如何理解隔离性(理论加深数据库并发的场景有三种读-读 不存在任何问题也不需要并发控制读-写 有线程安全问题可能会造成事务隔离性问题可能遇到脏读幻读不可重复读写-写 有线程安全问题可能会存在更新丢失问题比如第一类更新丢失第二类更新丢失(后面补充)1.读-写多版本并发控制 MVCC 是一种用来解决 读-写冲突 的无锁并发控制为事务分配单向增长的事务ID为每个修改保存一个版本版本与事务ID关联读操作只读该事务开始前的数据库的快照。所以 MVCC 可以为数据库解决以下问题在并发读写数据库时可以做到在读操作时不用阻塞写操作写操作也不用阻塞读操作提高了数据库并发读写的性能同时还可以解决脏读幻读不可重复读等事务隔离问题但不能解决更新丢失问题理解 MVCC 需要知道三个前提知识3个记录隐藏字段undo 日志Read View3个记录隐藏列字段DB_TRX_ID 6 byte最近修改( 修改/插入 )事务ID记录创建这条记录/最后一次修改该记录的事务IDDB_ROLL_PTR : 7 byte回滚指针指向这条记录的上一个版本简单理解成指向历史版本就行这些数据一般在 undo log 中DB_ROW_ID : 6 byte隐含的自增ID隐藏主键如果数据表没有主键 InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引补充实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除而是删除flag变了假设测试表结构是上面描述的意思是我们目前并不知道创建该记录的事务ID隐式主键我们就默认设置成null1。第一条记录也没有其他版本我们设置回滚指针为null。undoMySQL 将来是以服务进程的方式在内存中运行。我们之前所讲的所有机制索引事务隔离性日志等都是在内存中完成的即在 MySQL 内部的相关缓冲区中保存相关数据完成各种判断操作。然后在合适的时候将相关数据刷新到磁盘当中的。所以我们这里理解undo log简单理解成就是 MySQL 中的一段内存缓冲区用来保存日志数据的就行。模拟 MVCC现在有一个事务10(仅仅为了好区分)对student表中记录进行修改(update)将name(张三)改成name(李四)。事务10,因为要修改所以要先给该记录加行锁。修改前现将改行记录拷贝到undo log中所以undo log中就有了一行副本数据。(原理就是写时拷贝)所以现在 MySQL 中有两行同样的记录。现在修改原始记录中的name改成 李四。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务10 的ID, 我们默认从 10 开始之后递增。而原始记录的回滚指针 DB_ROLL_PTR 列里面写入undo log中副本数据的地址从而指向副本记录既表示我的上一个版本就是它。事务10提交释放锁。备注此时最新的记录是’李四‘那条记录。现在又有一个事务11对student表中记录进行修改(update)将age(28)改成age(38)。事务11,因为也要修改所以要先给该记录加行锁。该记录是那条修改前现将改行记录拷贝到undo log中所以undo log中就又有了一行副本数据。此时新的副本我们采用头插方式插入undo log。现在修改原始记录中的age改成 38。并且修改原始记录的隐藏字段 DB_TRX_ID 为当前 事务11 的ID。而原始记录的回滚指针 DB_ROLL_PTR 列里面写入undo log中副本数据的地址从而指向副本记录既表示我的上一个版本就是它。事务11提交释放锁。这样我们就有了一个基于链表记录的历史版本链。所谓的回滚无非就是用历史数据覆盖当前数据。上面的一个一个版本我们可以称之为一个一个的快照。上面是以更新update为主讲的如果是 delete 呢一样的别忘了删数据不是清空而是设置 flag 为删除即可也可以形成版本。那 insert 呢因为 insert 是插入也就是之前没有数据那么 insert 也就没有历史版本。但是一般为了回滚操作insert 的数据也是要被放入 undo log 中如果当前事务 commit 了那么这个 undo log 的历史 insert 记录就可以被清空了。总结操作是否形成版本链说明update是修改前后形成版本链delete是标记删除相当于新版本insert否没有旧版本只有 undo log 用于回滚select否只读操作不产生版本select 的特殊性当前读 vs 快照读那么 select 呢首先select不会对数据做任何修改所以为select维护多版本没有意义。此时有个问题select 读取是读取最新的版本呢还是读取历史版本两种读取方式读取方式含义示例是否加锁当前读读取最新的记录SELECT ... LOCK IN SHARE MODESELECT ... FOR UPDATEINSERT、UPDATE、DELETE需要加锁快照读读取历史版本一般而言普通的SELECT不需要加锁为什么需要区分在多个事务同时删改时都是当前读是要加锁的。那同时有 select 过来如果也要读取最新版当前读→ 需要加锁 →串行化如果是快照读读取历史版本 → 不受加锁限制 →可以并行执行换而言之快照读提高了效率这就是 MVCC 的意义所在。隔离级别的作用是什么决定了 select 是当前读还是快照读隔离级别。那为什么要有隔离级别呢事务都是原子的所以无论如何事务总有先后。但是经过上面的操作可以发现事务从begin→CURD→commit是有一个阶段的也就是事务有执行前、执行中、执行后的阶段。不管怎么启动多个事务总是有先有后的。多个事务在执行中的 CURD 操作是会交织在一起的。为了保证事务的有先有后应该让不同的事务看到它该看到的内容。问题重要先来的事务应该看到后来的事务所做的修改吗隔离级别先来事务能否看到后来事务未提交的修改能否看到后来事务已提交的修改读未提交能能读提交不能能但可能不可重复读可重复读不能不能看到的是快照串行化不能且加锁阻塞不能加锁阻塞这就是隔离性与隔离级别要解决的问题。先来的事务应不应该看到后来的事务所做的修改呢?如何保证不同的事务看到不同的内容呢也就是如何如何实现隔离级别请往下看接下来为你揭晓。Read ViewRead View就是事务进行 快照读 操作的时候生产的 读视图 (Read View)在该事务执行的快照读的那一刻会生成数据库系统当前的一个快照记录并维护系统当前活跃事务的ID(当每个事务开启时都会被分配一个ID, 这个ID是递增的所以最新的事务ID值越大)Read View 在 MySQL 源码中,就是一个类本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候对该记录创建一个 Read View 读视图把它比作条件,用来判断当前事务能够看到哪个版本的数据既可能是当前最新的数据也有可能是该行记录的 undo log 里面的某个版本的数据。下面是 ReadView 结构,我们简化一下class ReadView { // 省略... private: /** 高水位大于等于这个ID的事务均不可见*/ trx_id_t m_low_limit_id /** 低水位小于这个ID的事务均可见 */ trx_id_t m_up_limit_id; /** 创建该 Read View 的事务ID*/ trx_id_t m_creator_trx_id; /** 创建视图时的活跃事务id列表*/ ids_t m_ids; /** 配合purge标识该视图不需要小于m_low_limit_no的UNDO LOG * 如果其他视图也不需要则可以删除小于m_low_limit_no的UNDO LOG*/ trx_id_t m_low_limit_no; /** 标记视图是否被关闭*/ bool m_closed; // 省略... };m_ids; //一张列表用来维护Read View生成时刻系统正活跃的事务ID up_limit_id; //记录m_ids列表中事务ID最小的ID(没有写错) low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID也就是目前已出现过的事务ID的 最大值1(也没有写错) creator_trx_id //创建该ReadView的事务ID我们在实际读取数据版本链的时候是能读取到每一个版本对应的事务ID的即当前记录的DB_TRX_ID 。那么我们现在手里面有的东西就有当前快照读的 ReadView 和 版本链中的某一个记录的DB_TRX_ID 。所以现在的问题就是当前快照读应不应该读到当前版本记录。一张图解决所有问题Read View 采用懒加载机制不是事务创建时就有的而是事务首次进行快照读时才生成。RC 级别每次快照读都重新生成RR 级别只生成一次并复用。对应源码策略如果查到不应该看到当前版本接下来就是遍历下一个版本直到符合条件即可以看到。上面的readview 是当你进行select的时候会自动形成。整体流程假设当前有条记录事务操作事务4修改name(张三) 变成name(李四)当 事务2 对某行数据执行了 快照读 数据库为该行数据生成一个 Read View 读视图//事务2的 Read View m_ids; // 1,3 up_limit_id; // 1 low_limit_id; // 4 1 5原因ReadView生成时刻系统尚未分配的下一个事务ID creator_trx_id // 2此时版本链是只有事务4修改过该行记录并在事务2执行快照读前就提交了事务。我们的事务2在快照读该行记录的时候就会拿该行记录的 DB_TRX_ID 去跟up_limit_id,low_limit_id和活跃事务ID列表(trx_list) 进行比较判断当前事务2能看到该记录的版本。//事务2的 Read View m_ids; // 1,3 up_limit_id; // 1 low_limit_id; // 4 1 5原因ReadView生成时刻系统尚未分配的下一个事务ID creator_trx_id // 2 //事务4提交的记录对应的事务ID DB_TRX_ID4 //比较步骤 DB_TRX_ID4 up_limit_id1 ? 不小于下一步 DB_TRX_ID4 low_limit_id(5) ? 不大于下一步 m_ids.contains(DB_TRX_ID) ? 不包含说明事务4不在当前的活跃事务中 //结论 故事务4的更改应该看到。 所以事务2能读到的最新数据记录是事务4所提交的版本而事务4提交的版本也是全局角度上最新的版本RR 与 RC的本质区别当前读和快照读在 RR 级别下的区别select * from user lock in share mode以加共享锁方式进行读取对应的就是当前读。设置RR模式下测试 mysql set global transaction isolation level REPEATABLE READ; Query OK, 0 rows affected (0.00 sec) --重启终端 mysql select transaction_isolation; ----------------- | transaction_isolation | ----------------- | REPEATABLE-READ | ----------------- 1 row in set, 1 warning (0.00 sec) --依旧用之前的表 create table if not exists account( id int primary key, name varchar(50) not null default , blance decimal(10,2) not null default 0.0 )ENGINEInnoDB DEFAULT CHARSETUTF8; --插入一条记录用来测试 mysql insert into user (id, age, name) values (1, 15,黄蓉); Query OK, 1 row affected (0.00 sec)测试用例1-表1测试用例2-表2用例1与用例2唯一区别仅仅是 表1 的事务B在事务A修改age前 快照读 过一次age数据而 表2 的事务B在事务A修改age前没有进行过快照读。结论事务中快照读的结果是非常依赖该事务首次出现快照读的地方即某个事务中首次出现快照读决定该事务后续快照读结果的能力delete同样如此RR 与 RC的本质区别正是Read View生成时机的不同从而造成RC,RR级别下快照读的结果的不同在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来此后在调用快照读的时候还是使用的是同一个Read View所以只要当前事务在其他事务提交更新之前使用过快照读那么之后的快照读使用的都是同一个Read View所以对之后的修改不可见即RR级别下快照读生成Read View时Read View会记录此时所有其他活动事务的快照这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见而在RC级别下的事务中每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因总之在RC隔离级别下是每个快照读都会生成并获取最新的Read View而在RR隔离级别下则同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。正是RC每次快照读都会形成Read View所以RC才会有不可重复读问题。2.读-读不讨论原因多个事务同时执行SELECT快照读彼此之间不会产生任何冲突因为读操作不加锁快照读情况下也不会修改数据。所以不需要讨论。3.写-写现阶段直接理解成都是当前读当前不做深究原因多个事务同时执行INSERT、UPDATE、DELETE都是当前读会涉及加锁、锁等待、死锁等问题。现阶段先理解为“都是当前读”暂时不深入分析锁的细节。4.推荐阅读关于这块有很好的文章推荐大家阅读https://blog.csdn.net/SnailMann/article/details/94724197https://www.cnblogs.com/f-ck-need-u/archive/2018/05/08/9010872.htmlhttps://blog.csdn.net/chenghan_yang/article/details/97630626