公司开发个网站多少钱,宁波高端网站开发,专业的建站公司都具备什么条件,福建网站开发速成班事务 前言正式开始事务的四大特性为什么会出现事务事务的版本支持事务提交方式事务常见操作方式启动事务回滚演示提交事务事务的异常autocommit 事务的隔离性隔离级别查看隔离级别修改隔离级别验证四种隔离级别读未提交(read uncommitted) —— 缩写为RU读提交(read committed)… 事务 前言正式开始事务的四大特性为什么会出现事务事务的版本支持事务提交方式事务常见操作方式启动事务回滚演示提交事务事务的异常autocommit 事务的隔离性隔离级别查看隔离级别修改隔离级别验证四种隔离级别读未提交(read uncommitted) —— 缩写为RU读提交(read committed) —— 缩写为RC可重复读(repeatable read) —— 缩写为RR串行化(serializable) 小总结一致性(Consistency) 多版本并发控制(MVCC)数据库并发的场景有三种三个记录隐藏字段undo日志Read View重要字段解释流程演示示例及源码RR 与 RC的本质区别 推荐阅读 前言
显示中编写sql的时候不一定是一条sql就能解决问题有时需要一批sql才有意义。
比如说转账需要对两个账户进行更新用两个update对一个用户账户中加钱再对一个用户账户中扣钱两个update合到一块才能表示转账如果拆开只是在技术层面上上对某一条记录进行更新而对于上层业务逻辑来说单条的sql体现不出来意义。只有两个拼到一块才是上层的转账逻辑。
所以多条DML语句拼到一块就是一个上层的完整逻辑而这多条sql拼到一块的东西就叫做事务。
看待事务一定要站在MySQL的上层去看待sql语句处理一件事分多步也就是多条sql语句这样多条sql拼到一块就是为了解决一个具体的应用场景所有的sql包装到一块就是一个事务。
可能你还不太明白没关系后面用sql进行演示的时候你就懂了。
正式开始
一个很重要的问题MySQL是一个网络服务这在我前面博客也讲过我们用netstat是可以查到本机中的mysql服务器的 上面mysqld的端口默认是3306的不过我前面将我的配置文件中的修改成8080了如果你也想修改配置文件为/etc/my.cnfvim打开之后修改 那么这就意味着mysql客户端一次是可以连接多个的。不光是本地主机中的mysql可以连接多个
远端主机上的mysql客户端也可以进行连接我上面用的是Linux云服务器我也可以用我Windows下的mysql进行连接
本地和远端的都能够看到一个库
那么当多个mysql连接的时候就可能会发生多个客户端并发的对一个表进行读写操作那么此时就可能会出问题类似于多线程的问题比如说 上面就是一个并发执行可能出问题的情况而客户端A和客户端B都是在执行一个事务两个事物之间影响到了对方。
那么如何避免这样的问题呢
买票的过程得是原子的买票互相应该不能影响买完票应该要永久有效买前和买后都要是确定的状态
事务的四大特性
事务就是一组DML语句组成这些语句在逻辑上存在相关性这一组DML语句要么全部成功要么全部失败是一个整体。MySQL提供一种机制保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。
事务就是要做的或所做的事情主要用于处理操作量大复杂度高的数据。假设一种场景你毕业了学校的教务系统后台 MySQL 中不在需要你的数据要删除你的所有信息(一般不会:) ), 那么要删除你的基本信息(姓名电话籍贯等)的同时也删除和你有关的其他信息比如你的各科成绩你在校表现甚至你在论坛发过的文章等。这样就需要多条 MySQL 语句构成那么所有这些操作合起来就构成了一个事务。
正如我们上面所说一个 MySQL 数据库可不止你一个事务在运行同一时刻甚至有大量的请求被包装成事务在向 MySQL 服务器发起事务处理请求。而每条事务至少一条 SQL 最多很多 SQL ,这样如果大家都访问同样的表数据在不加保护的情况就绝对会出现问题。甚至因为事务由多条 SQL 构成那么也会存在执行到一半出错或者不想再执行的情况那么已经执行的怎么办呢 所以一个完整的事务绝对不是简单的 sql 集合还需要满足如下四个属性下面的四个特性mysql已经帮我们实现了我们使用者不必关心其实现细节 原子性一个事务transaction中的所有操作要么全部完成要么全部不完成不会结束在中间某个环节。事务在执行过程中发生错误会被回滚Rollback到事务开始前的状态就像这个事务从来没有执行过一样。中间的过程的sql所造成的影响不能暴露给别的事物如果暴露了原子性就没了。至于回滚是什么等会会有专门的sql来演示先不要急。 一致性在事务开始之前和事务结束以后数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。比如说张三给李四转账50那么张三的账户中就要减50块钱李四的账户中就要加50块钱结果一定是符合预期的。 隔离性数据库允许多个并发事务同时对其数据进行读写和修改的能力隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别包括读未提交 Read uncommitted 、读提交 read committed 、可重复读 repeatable read 和串行化 Serializable 。隔离性后面会重点讲解不懂的同学先不要急。 持久性事务处理结束后对数据的修改就是永久的即便系统故障也不会丢失。持久性就是将内存中的内容刷新到磁盘上。 上面四个属性可以简称为 ACID 。 原子性Atomicity或称不可分割性一致性Consistency隔离性Isolation又称独立性持久性Durability。 实际上mysql并没有对一致性做技术上的实现不过只要保证了原子性、隔离性和持久性就可以在技术上保证数据的一致性。
为什么会出现事务
为了上层在编写代码的时候更舒服。
事务被 MySQL 编写者设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去考虑各种各样的潜在错误和并发问题。
可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?因此事务本质上是为了应用层服务的.而不是伴随着数据库系统天生就有的.
我们在编写上层代码的时候不需要关心原子性、隔离性、一致性等问题只要告诉mysql要干什么把sql语句提交给mysql就行了。mysql会自己将这些sql封装成事务并运行。
事务的版本支持
在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务 MyISAM 不支持。
我们可以用show engines来查看所有的存储引擎
其中transaction(transaction就是事物的意思)字段就是表示是否支持事务的字段可以看到只有InnoDB存储引擎的这个字段才是yes其他的都是no或NULL。
事务提交方式
两种提交方式
手动提交自动提交
我们可以用show variables like autocommit’来查看当前的提交方式 我这里现在是打开的状态。
想要修改提交方式可以用set autocommitxx为0的时候就是关闭为1就是打开。 这个后面会有应用场景目前事务的sql还没有演示等事务演示了之后再说说这个有什么用。
事务常见操作方式
为了便于演示这里将mysql的默认隔离级别设置成读未提交前面讲事务的四大特性中的隔离性的时候提到了这里先不要管这是什么东西后面正式讲隔离性的时候会说。
(可以不看)修改隔离级别 transaction isolation level就是事务隔离级别。
(可以不看)这里是设置全局的隔离级别想要让当前会话的隔离级别也变成读未提交需要重新登录一下mysql还有其他方法 tx为transaction的简写isolation就是隔离。上面的读未提交隔离级别是最低的隔离级别只是为了方便后续观察。
这里为了模拟一下多个mysql客户端并发执行事务的过程登录两个mysql客户端 可以用show processlist可以查看当前有多少人连接这里再开一个客户端 其中ID为22的就是新开的mysql15和16的为刚刚开的两个客户端。其中Time表示某客户端上次命令执行完毕到现在经过了多少秒。
上面两个mysql客户端连接的都是my_test。
左边来创建一张表右边用desc查看
看一下二者的事务提交方式
都是读未提交的。
启动事务 启动事务可以用两种方式。 start transactionbegin 两种方式效果一样都是启动事物。 演示一下
注意从现在开始两边的两个客户端就在同时执行各自的事务了此时就是并发访问。
左边来插入点数据右边查看 右边能够看到。也就是这里读(select)写(insert)能够并发执行。
回滚演示
接着上面的来如果我想要对数据进行回滚那么就要设置保存点就像游戏中的存档一样你觉得你存了档之后玩得不好就可以回档这样就能回到你存档时刻的游戏状态。
事务中设置保存点可以用savepoint 这里设置了s1保存点如果我想要等会回滚就可以直接回滚到现在的状态。
那么我来多插入点数据
直接进行回滚
这里还可以再多搞几个保存点就像游戏中可以有多个存档一样想回档哪个就可以回档哪个比如我这里插入一个数据就保存一个数据
此时我有4个保存点也就是s1 ~ s4下面我来一一进行回滚s4后面再来插入一个
回滚到s4 可以看到田七这条记录没有了。
也可跨保存点进行回滚
不能继续回滚到s4因为回滚到s1的时候还没有s4
如果说你想要放弃本次事务中的所有操作全部进行回滚可以直接用rollback这样就会回滚到最开始启动事务的时候这里再来搞两个保存点
左侧的操作全部回滚
所有的记录就会全部消失。
这就是回滚操作。
提交事务
刚刚手动启动的事务也要手动提交用commit来手动提交 commit操作就是进行持久化的操作会将事务中所有写操作所造成的结果刷新到磁盘当中不过因为左侧的事务回滚了所有的数据都没有保留下来所以也就没有往磁盘中刷新什么东西。
此时右侧的事务还没有提交将右侧事务提交之后再看看 还是什么东西都没有查到。
再演示一下不进行回滚直接commit的操作还是这两个客户端启动事务
这里演示一下左用begin右用start transaction其实都一样。
左边插入右边查看
左边不回滚直接提交此事件就是执行了持久化工作左侧事务的数据就会被刷新到磁盘上 可以看到提交之后右侧的事务中照样能看到刚刚插入的数据。
让右侧的事务也提交
因为已经刷新到磁盘上了提交之后再查也是能查到的。
此时左侧还能进行rollback操作不过刚刚的事务已经commit了提交之后事务就结束了此时再回滚也不会影响到刚刚的数据
这就是事务的启动、回滚和提交操作。
事务的异常 证明未commit客户端崩溃MySQL自动会回滚隔离级别设置为读未提交 还是左侧事务插入数据右侧进行查询
此时我直接关闭掉左侧的会话 赵六这条记录没了。因为左侧事务出现了异常会话直接关闭了就会直接自动进行回滚。所以数据也就查不到了。
再来一个异常信号关闭的先来搞出刚刚的场景
直接对左侧ctrl \
可以看到会自动进行回滚。赵六记录消失。 证明commit了客户端崩溃MySQL数据不会在受影响已经持久化 就搞一个ctrl \的演示。
commit后数据就持久化了持久化后的数据不会因为客户端的崩溃/异常而进行数据回滚。所以就算异常了右侧事务也能查到持久化后的数据。
autocommit
我前面都是设置了autocommit的。但是我手动启动之后客户端异常崩溃并没有自动提交这是为啥
我这里再演示一下关掉autocommit的情况
还是两个会话都关掉
插入演示 这里是没有自动提交的。
刚刚在演示有自动提交ctrl\的时候添加赵六结果照样是没有添加成功。
其实不管是否开启自动提交结果都是不会自动提交的我们手动begin或start启动的事务必须由我们手动来提交。
其实我前面博客中的所有的单sql语句其实就是一个事务比如我现在不用手动begin或start 这里和前面的区别就是没有begin这样的话其实insert语句就是一个完整的事务只不过是一个单sql的事务。
我再来打开autocommit并用单sql插入
这里打开了autocommitinsert是一个单sql的事务执行完毕之后会自动提交ctrl\的前已经自动提交了所以5, 张三的这条记录已经持久化了所以右侧怎么查都能查到。
所以这两次都是单sql的事务没有进行begin和commit但是autocommit打开就会自动提交进行持久化 autocommit没有打开就会直接进行回滚所以前面博客中所讲的单sql语句都是事务只是前面还没讲事务相关的概念我们不知道而已。
再来验证一下关掉autocommit然后将刚刚插入的id为5的记录删除掉。
这里是关掉autocommit的没有commit异常的时候自动回滚了。
我再手动commit
这里能够成功删除所以说没有手动begin后的单个的sql就是一个事务如果有autocommit就会自动提交如果没有autocommit就会回滚。
这里只要记住自动提交是给单sql的事务用的就行我们手动启动的事务必须手动提交。
再来说个比较怪的手动begin后不提交插入数据后再begin启动一个新事务会自动提交这里是打开autocommit了
关闭autocommit照样会自动提交
事务的隔离性
增删查改四个操作其实就是读(查)和写(增删改)一个事务中读写都可以执行。
假如说一个事务要进行查操作另一个事务要进行写(增删改)操作如果是读先执行那么读到的一定是陈旧的数据如果是写先执行那么读到的一定是最新的数据。
但如果一个事务中要进行多次查找其他的事务中进行写那么这个查找的事务在查找的时候是要保证每次查找的结果都一样还是如果有修改了就直接查看到新的数据先不说应该是哪种。
来结合生活说说。 假如说你出生在2008年而且能活到100岁那么你能肉眼看到的事情一定是发生在2008~2108年的。如果在你出生前和入土后的事情还能被你亲眼所见那就有点离谱了。所以说在你出生前的所有事情和你入土后的所有事情应该是和你隔离开的不能直接被你看到也就是说你没有权利去看到你生命周期以外的信息而且每个人所获取到的信息都不一样。
把事务当成一个人来看事务也有生命周期。
一个事务要进行写一个事务要进行读谁先执行完全取决于谁先来不同人之间的生命周期有交叉(在2008 ~ 2108期间不止你一个人存活还可以有其他人而且每个人在不同时刻的年龄都不同)那么不同事务之间的生命周期也是会有交叉。
假如写的事务是在update那么当update没有执行或者是没有完成时此时select就应该查看到update之前的数据只有是update结束后执行select事务才会看到更新后的数据而这两个操作都是在不同事务中的update之后select能直接看到update后的数据这其实就是一种隔离性的体现。不过这里的隔离性比较弱。
一个事务有自己的执行范围且执行时一定要具备原子性前面我所演示的左侧事务进行一部分操作右边事务进行一部分操作其实就是两个事务在并发执行。只有是两个事物在某一时间段同时并发执行的时候才需要谈隔离性在事务运行中为了避免事务出现相互干扰就要有隔离性而干扰的程度不同隔离级别也就不同。类似于你的有些信息可以公开给他人还有的信息是过一段时间后可以公布给他人还有的信息是永远也不回公开的一样。
隔离性要存在而且还要有隔离级别只要一个事务将数据改了另一个事务能够立马看到不分谁先谁后这就是最低的隔离级别。还有其他级别比如果一个事务将数据改了另一个事务查的时候不能直接查到改了之后的事务当改事务提交后查的事务才能看到修改后的数据这里高了一个隔离级别。 所以 数据库中为了保证事务执行过程中尽量不受干扰就有了一个重要特征隔离性 数据库中允许事务受不同程度的干扰就有了一种重要特征隔离级别 隔离级别
mysql中分四种隔离级别。 读未提交【Read Uncommitted】我们上面为了做实验方便用的就是这个隔离性 在演示的时候左侧事务做的任何动作都会影响到右侧事务的读这就是读未提交隔离性没有提交的写所产生的影响都能够读到。这个隔离级别可以理解为多个事务间其实没有任何的隔离性。 读提交【Read Committed】 该隔离级别是大多数数据库的默认的隔离级别不是 MySQL 默认的。它满足了隔离的简单定义:一个事务只能看到其他的已经提交的事务所做的改变。对应到上面的演示就是只有左侧的事务提交了之后右侧才会看到左侧写后所产生的影响这个隔离性后面也会演示这里先不要急。 可重复读【Repeatable Read】 这是 MySQL 默认的隔离级别它确保同一个事务在执行中多次读取操作数据时会看到同样的数据行。对应上面的演示就是左侧不管提交不提交右侧看到的都是最初的数据记录。也是等会演示。 串行化【Serializable】: 所有的事务必须按照到来的先后顺序执行一个事务执行完之后才能执行下一个事务虽然这样做能够保证数据的绝对安全但是效率会非常低而且还可能会出现超时和锁竞争。也是等会演示。 根据前三个隔离级别的名字可以看出好像隔离级别都是和读有关的其实对于增删改这样的写操作必须是串行执行的而查这样的读操作是可以和写并发执行的。所以隔离级别是对于读写两种操作而言的在数据安全的前提下实现读写并发操作对于一个数据库而言是要重点实现的纯写的场景很少大多情况下是读写并发所以读写并发必须要设计好不然这个数据库就没法用。
也就是说读写可并发写写必串行。
所以我后续说的都是一个事务读一个事务写的场景。
演示四个隔离级别之前先说说怎么查看mysql的隔离级别。
查看隔离级别
其实前面都讲过了。
查看当前的隔离级别有三种方式
select global.tx_isolation; #查看全局的隔离级别上面这是查看全局的隔离级别。还有查看当前会话的隔离级别
select session.tx_isolation; #查看当前会话的隔离级别当前会话意思就是当前连接的这个客户端的隔离级别。还有一种查看当前会话隔离级别的方式
select tx_isolation; #查看当前会话的隔离级别就是这上面的三种查看方式你可以理解为第二种和第三种查看方式是一样的而且全局的隔离级别就是当前会话的默认隔离级别登录mysql时的默认隔离级别。
可以想象成有一个全局变量来表示全局的隔离级别当启动一个会话时这个会话中还会有一个局部的变量来表示该会话的隔离级别而且这个局部变量的初始值就是全局变量。
修改隔离级别
set 当前会话(session)或者全局(global) transaction isolation level 四种级别;如果只是设置当前会话的隔离级别就用session如果是设置全局隔离级别就用global。
四种级别就是刚刚介绍的read uncommitted读未提交、read committed读提交、repeatable read可重复读、serializable串行化。
演示一下设置当前会话的隔离级别为读提交
此时设置的是当前会话的隔离级别其他会话的隔离级别和全局的隔离级别是不会受影响的前面设置了全局的隔离级别为读未提交
设置全局的会影响后续连接会话的默认隔离级别但不会影响当前已开启的所有会话的隔离级别这里将全局的隔离级别设置成可重复读的
再来新加一个会话
一般不要随便更改隔离级别如果要设置那就尽量将所有的会话都设置成一致的。
如果想要同时将多个会话的隔离级别都修改成同样的话可以直接先用一个会话将全局的隔离级别设置成你想要的然后将所有会话关掉再重新连接新会话就行。
验证四种隔离级别
下面来验证不同隔离级别的读写并发情况。
还是首先要有两个事物并发的场景这里像前面演示的一样左边一个右边一个
读未提交(read uncommitted) —— 缩写为RU
其实前面讲事务的时候演示的都是读未提交的。
启动事务 此时这两个事务就是并发在跑。
还是用刚刚的account表演示只不过我将其中的数据都删除了插入一条记录
再更新一下
左边还没有commit提交右边就立马能够看到。这就是读未提交。
前面说了事务要有原子性要么事务已经做完了要么是还没有开始做中间做的过程不能让外界看到所以这里读未提交的隔离级别导致了事务原子性的丢失故这个隔离级别是不合理的。
一个事务在执行中读到另一个执行中事务的更新(或其他操作)但是未commit的数据这种现象叫做脏读。
我直接进行回滚 右边查的时候全都没了所以说读未提交这个隔离级别会产生很多问题严重不推荐。
读提交(read committed) —— 缩写为RC
就是读取的时候只有写的事务提交了才会影响到读的事务。
先将两个会话的隔离级别设置为读提交
还是account这张表 可以看到插入后右侧事务立即读取并没有直接生消等左侧事务提交了之后才生效的。
如果我左侧没有提交查的时候看到的是有张三这条记录的我重来一下
可以看到左侧插入之后左侧能看到但是右侧事务是看不到的。
左侧事务提交右侧事务才能看到
当然右侧事务提交之后换成其他事务也是能看到的 因为左侧一提交就已经将数据持久化了持久化后的数据任何事物都可以看到。
执行中的事务看到不到其他执行中的修改后的数据但是只要修改的事务提交了读的事务就可以看到修改后的数据这就是读提交读取提交后的数据。
但是同一个事务内同样的读取在不同的时间段(依旧还在事务操作中)读取到了不同的值这种现象叫做不可重复读(non reapeatable read)
那么不可重复读是不是问题 我来讲个例子。
假如说公司到年底了要按照每个人的月薪发年终奖。 [4000, 8000)的发一个公司定制水杯 [8000, 12000)的发一个阿迪书包 [12000, 20000)的发一个智能手环 20000以上的发一部遥遥领先假如说公司很有钱
此时需要统计出来各个薪资端的人员都有谁而且这个任务落到了小张的头上。
小张一看说简单啊直接启动一个事务用4个select就行
于是就这么干了。
但此时公司一名员工叫张三人家月薪为18000而且人家技术很好但是没有得到重用一年改了不少错但是老板没看到。于是张三就去找老板给老板诉说想要加薪老板一看张三确实薪资和实例不匹配于是就给张三的薪资涨到21000。此时负责涨薪的小王直接去更新薪资了
假如说小王和小强同时起的事务
但是小王更新并提交的时刻正好在挑选[12000, 20000)和20000以上的两条sql之间
那么此时 这两条语句挑选出来的结果中就会有两个张三。
如果说小张直接将结果交给了其领导其领导一看怎么[12000, 20000)范围的和20000以上的有两个张三而且还是同一个张三此时小张就要挨骂喽。
所以说不可重复读是问题。我们要保证同一个事务中的读取结果要是相同的。
可重复读(repeatable read) —— 缩写为RR
这个隔离级别是mysql的默认隔离级别这里来验证一下。
我直接重启服务器
这里重启之后再登录mysql默认的隔离级别为可重复读。
再来继续演示还是两个会话
插入数据演示
可以看到左侧commit了之后右侧照样没有查到张三。
写的事务提交与否都不会影响正在运行的事务的读这就是可重复读。并发运行时能够保证同一事物多次读取的结果都是一致的这样小张就不会挨骂了。
此时左侧已经将数据持久化了右侧commit事务结束后其他事务是可以看到张三的
再来演示一下删除
左侧提交右侧照样能找到张三
右侧提交再查看
这就是可重复读保证同一事务下查看到的数据都是一样的。
再来说说幻读是啥。 一般的数据库在可重复读情况的时候无法屏蔽其他事务insert的数据(为什么因为隔离性实现是对数据加锁完成的而insert待插入的数据因为并不存在那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是可重复读的但是insert的数据在可重复读情况被读取出来导致多次查找时会多查找出来新的记录就如同产生了幻觉。这种现象叫做幻读(phantom read)。
很明显MySQL在RR级别的时候是解决了幻读问题的(解决的方式是用Next-Key锁(GAP行锁)解决的。这块比较难有兴趣同学了解一下)。这里只要知道幻读是insert时会出现的问题就行。
串行化(serializable)
串行化很简单对所有操作全部加锁进行串行化数据上不会出现问题但是效率上会出问题。
只要是并发的事务发生了写操作就会阻塞但是并发的读并不会阻塞
如果一写 右侧就直接阻塞了左侧提交了右侧的才会执行
只有一个事务能顺利执行写操作执行完之后才能让其他事务执行写操作读操作不会受影响。
如果写操作长时间没有成功会出现超时的情况
再来看一个
再来看一下刚刚可重复读两边可以对一张表同时进行插入操作
不过上面只有在写入的数据不冲突时才可以如果冲突了就会有一方卡住没有卡住的一方提交了才能继续执行写入本质上就是没有卡住的一方持有锁卡住的一方没有锁 可以看到update动作经过了9秒多才执行也就是我右边的事务经过了这么多时间才commit的可惜这里没有动图有动图看着更方便。 注意对同一张表中的写操作都是串行的即使是在最低隔离级别RU下 可以看到左侧和右侧对同一条记录进行了修改而左侧阻塞了二者都是在RU隔离级别下的也就是说对于同一条记录的写操作必须是串行的。
小总结 其中隔离级别越严格安全性越高但数据库的并发性能也就越低往往需要在两者之间找一个平衡点。平衡点mysql说了不算实际的应用场景说的才算mysql只是提供多种解决方案根据需求选择合适的方案。 不可重复读的重点是修改和删除同样的条件, 你读取过的数据,再次读取出来发现值不一样了幻读的重点在于新增同样的条件, 第1次和第2次读出来的记录数不一样。 说明 mysql 默认的隔离级别是可重复读,一般情况下不要修改。 上面的例子可以看出事务也有长短事务这样的概念。事务间互相影响指的是事务在并行执行的时候即都没有commit的时候影响会比较大。 一致性(Consistency)
事务执行的结果必须使数据库从一个一致性状态变到另一个一致性状态。当数据库只包含事务成功提交的结果时数据库处于一致性状态。如果系统运行发生中断某个事务尚未完成而被迫中断而改未完成的事务对数据库所做的修改已被写入数据库此时数据库就处于一种不正确不一致的状态。因此一致性是通过原子性来保证的。
其实一致性和用户的业务逻辑强相关一般MySQL提供技术支持但是一致性还是要用户业务逻辑做支撑也就是一致性是由用户决定的。
而技术上通过AID保证C。
多版本并发控制(MVCC)
数据库并发的场景有三种 读-读 不存在任何问题也不需要并发控制。 读-写 有线程安全问题可能会造成事务隔离性问题可能遇到脏读幻读不可重复读。 写-写 有线程安全问题可能会存在更新丢失问题。
多版本并发控制 MVCC 是一种用来解决 读-写 冲突的无锁并发控制。
事务有原子性必须按照一定的先后顺序执行那么如何做到按序执行 为每一个事务分配单项增长的事务IDID越小来的越早通过ID来判断不同事务到来的先后顺序。
比如说两个事务事务A和事务B事务A先begin的话就会给事务A一个ID假如说是1那么事务B后begin获得的事务ID就是2。
每一条SQL语句都是有对应的事务来执行的比如说事务A执行了select、update、select、delete、select这五个sql那么这五个sql会有记录这五个sql的执行事务都是A在记录的时候会保存事务A的事务ID也就是1。
mysqld可能会面临处理多个事务的场景所以需要对多个事务进行管理还是和os一样要先描述再组织所以事务在mysqld中一定是对应的一个或者一套结构体/类对象。所以有事务到来的时候就先new一个事务的对象然后再申请对应的事务ID并将对应的sql语句初始化这个事务对象的等等字段。
理解MVCC要知道三个前提知识
3个记录隐藏字段undo 日志Read View
三个记录隐藏字段
我们创建一张表的时候表中不光会有我们自己设置的列还会有三个列分别是DB_TRX_ID 、DB_ROLL_PTR、DB_ROW_ID 。下面这三个介绍看不懂没关系要结合者等会讲的undo日志来讲才能听懂。
DB_TRX_ID 6 byte最近修改( 修改/插入 )事务ID记录创建这条记录/最后一次修改该记录的事 务ID。
DB_ROLL_PTR : 7 byte回滚指针指向这条记录的上一个版本简单理解成指向历史版本就 行这些数据一般在 undo log 中。
DB_ROW_ID : 6 byte隐含的自增ID隐藏主键如果数据表没有主键 InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引。如果手动指明了主键那么这个隐藏主键就不需要了因为只能有一个主键。
比如我现在创建一个新的student表
里面不仅会有name和age这两列还会有刚刚提到的三列。
其实还有一列未删flag列这列表示数据到底删没删除如果数据删除了就为0没删就为1一个提高效率的东西这里不做讨论。
undo日志
下面我在讲的时候都是按照可重复读的隔离级别来讲的。
当我们对表中数据做修改的时候不会直接对原表中的数据进行操作而是类似于写时拷贝一样先对原表中的数据拷贝一份做记录比如说刚刚的student表插入一条记录
插入的时候insert这单条sql是一个事务会有其对应的事务ID假如说就是10那么记录的时候就是这样
nameageDB_TRX_ID(创建该记录的事务ID)DB_ROW_ID(隐式主键)DB_ROLL_PTR(回滚指针)张三18101null
第一条记录也没有其他版本这里就设置回滚指针为null。
假如我这里对张三这一条记录做了更新
此时不光是张三这条记录被改了在undo log中还会保存原来张三的那条旧记录
同时update这条单sql语句也是一个事务那么就也会有对应的事务ID假如说就是11而且修改后的张三这条记录算是张三的第二条记录那么新记录的前一条记录就是刚刚的旧记录此时回滚指针DB_ROLL_PTR 就能派上用场直接指向前一条记录的地址(假如说是0xffbc327b)隐式的自增主键就还是1那么此时张三这条记录就应该是这样的
nameageDB_TRX_ID(创建该记录的事务ID)DB_ROW_ID(隐式主键)DB_ROLL_PTR(回滚指针)张三281110xffbc327b
对应到刚刚的图中就是这样
如果再次对张三这条记录做修改
此时这条新记录就算是张三这条记录的第三条记录了。同理这个单条的update也是一个事务也有其事务ID假如说就是12第二条记录也有其地址假如说是0xffbc328c第三条记录中的DB_ROLL_PTR列的值就应该是0xffbc328c记录的数据就应该是这样
nameageDB_TRX_ID(创建该记录的事务ID)DB_ROW_ID(隐式主键)DB_ROLL_PTR(回滚指针)张三201210xffbc328c
对应刚刚的那张图
这样我们就有了一个基于链表记录的历史版本链第几条记录就是第几个版本。所谓的回滚无非就是用历史数据覆盖当前数据。
上面的一个一个版本我们可以称之为一个一个的快照专业术语中版本就是快照。
undo log中保存的是历史的数据历史的数据是稳定的不能修改只能修改最新的版本。上面只是演示了update当我们进行insert的时候日志中会记录一个相反的delete当进行delete的时候日志会记录一个相反的insert这样就会形成多版本的数据这些数据完全是由mysql帮我们维护的而这就是MVCC中非常重要的一个机制。
undo log可能会被装满吗 不太可能一个事务中的所有sql写操作对应一套undo log如果当前事务结束对应的undo log中的数据也就被清空了而且一个事务中也不会有太多的sql写操作所以一般不用担心。读操作不会记录到undo log中因为读操作不需要进行回滚没有意义。
那一个事务select读取时是读取最新的版本呢还是读取历史版本 当前读读取最新的记录就是当前读。增删改都叫做当前读select也有可能当前读比如select lock in share mode(共享锁这个后面会有演示)。
快照读读取历史版本(一般而言)就叫做快照读。就是直接进行select*的。(这个我们后面重点讨论)
读写并发时假如写是修改那么如果修改后的数据和读到的数据不一样的话本质原因是二者看到的数据是不同版本的前面的可重复读里面每查的时候看到的结果都是相同的其实就是查的时候一直查的是历史版本而修改的时候只能修改最新版本所以二者不会访问同一个位置就不需要加锁这样就不会出现竞争锁的情况所以并发读写不会出现问题。
这样通过不同的版本就实现了数据层面上的隔离性而这本质上是在版本上做隔离一条记录的版本可能有很多具体看到哪一个版本完全是由隔离级别来决定的。
那如何实现隔离级别呢 通过read view来实现。
Read View
读视图(read view)前面讲事务的时候说了事务就可以看作一个类其实这里你也可以把这个东西想象成一个类里面有很多字段不同的事务通过这里的读视图就可以读到不同版本的记录。事务和读视图的关系就像PCB和进程地址空间的关系一样二者用指针关联起来。 【注】只有在一个已经启动的事务首次进行快照读的时候mysql才会为这个事务创建一个读视图。 读视图中有几个字段很重要我这里挑出来说说下面的这个类不用细看先过一眼
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;// 省略...
};重要字段解释
还记得前面说的每个事务有其ID不这里就要重点说说这个东西。
再强调一遍 只有在一个已经启动的事务首次进行快照读直接进行select不加 lock in share mode的时候mysql才会为这个事务创建一个读视图。 读视图只会在创建的时候初始化后面值不会改变。 先来说说其中的m_ids和m_creator_trx_id字段当一个事务创建了一个读视图对象的时候可能会有其他事务也在执行中此时每个事务都有其事务ID此时m_creator_trx_id就是当前视图对应事务的事务IDm_ids是一个集合其中保存了除当前事务外其他正在执行的事务的事务ID。
所以 m_ids; ⇒ 一张列表用来维护Read View生成时刻系统正活跃的事务ID m_up_limit_id; ⇒ 记录m_ids列表中事务ID最小的ID(没有写错) m_low_limit_id; ⇒ ReadView生成时刻系统尚未分配的下一个事务ID也就是目前已出现过的事务ID的最大值1(也没有写错) m_creator_trx_id; ⇒ 创建该ReadView的事务ID
m_ids中保存的事务ID不一定是连续的事务也是有长事务和短事务的长事务就是sql比较多且执行起来比较慢的短事务比如说一个只有一条sql的事务可能多个事务执行的时刻都很接近但是可能先来的事务最后执行完(长事务)也可能后来的事务先执行完(短事务)所以说多个事务执行的时候中间的事务也有可能先执行完。
所以当一个事务创建读视图的时候可能有的先到来的事务还没有执行完但是后来的事务已经执行完了如图
图中绿色的事务都是到某事务创建视图的时候还没有执行完毕的事务蓝色的是中途就执行完了的事务此时读视图中的m_ids保存的事务ID就会不连续。
前面讲undo log的时候说了每一条保存的历史记录都保存了其被执行的事务的事务ID不同快照(版本)的记录可能事务ID不同因为不同记录可能是由不同的事务执行的比如说更新所有的事务都可以对某一条记录进行修改。可能说有的版本记录中保存的事务ID对应的事务任然还在执行其他的sql此时这些正在执行中的事务对应版本记录就不应该被看到。
那么如何区分这些事务呢
m_ids中保存的是正在执行的事务m_up_limit_id中保存的是m_ids中的最小事务的ID如果一个事务的ID小于m_up_limit_id就可以说这个事务已经执行完毕了。那么当undo log中的不同记录对应事务ID小于这个m_up_limit_id就可以证明这条记录已经是一个历史版本的执行记录了那么这些事务ID的记录可以被看到。
还有一种表示历史版本记录的就是刚刚讲的中间就执行完毕的事务这些事务的事务ID不会被保存在m_ids中但是依旧比m_up_limit_id大那么如果undo log中的历史版本记录中保存的事务ID是这些中间执行完毕的事务的事务ID那么也就表明这些历史版本的记录也是可以被看到的。
还有一个字段m_low_limit_id表示的是还未被分配的事务ID的下一个ID也可以理解为m_ids中的最大事务ID1不过我觉得这种说法不准确个人认为也可能出现最后的事务也执行完毕的情况那么这个最后的事务也不会保存在m_ids中而且事务ID是递增的不会重复所以未被分配的事务ID的下一个ID比较准确一点如果一个事务的ID大于等于m_low_limit_id那么就表明在当前视图创建时事务ID大于m_low_limit_id的事务还没有被执行那么这些事务对应的记录也不应该被看到。
流程演示
假设当前有条记录创建事务先不管给成null
nameageDB_TRX_ID(创建该记录的事务ID)DB_ROW_ID(隐式主键)DB_ROLL_PTR(回滚指针)张三28null1null
事务操作:
事务1 [id1]事务2 [id2]事务3 [id3]事务4 [id4]事务开始事务开始事务开始事务开始………修改且已提交进行中快照读进行中……… 假设事务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的搞两个会话
左边查找并更新右边查找。 两种情况 左边更新前右边直接进行快照读左边提交之后右边再次进行快照读然后再进行当前读。左边提交前右边不读左边提交后右边进行快照读和当前读。 情况一左边更新前右边直接进行快照读左边提交之后右边再次进行快照读然后再进行当前读。
结果如下
可以看到左侧提交前和提交后右侧读快照的查找结果都是相同的原理刚刚也是讲了的左侧事务假如说是事务A和右侧事务假如说是事务B并发执行右侧事务只有在进行读快照的时候才会形成读视图那么B读的时候也就是上图中的第二步A正在执行中此时A的ID就会进入B的读视图中的m_ids中那么后续再进行快照读都会导致读取的结果看不到A所造成的影响就算commit了也没有用。因为B的读视图只有在初始化的时候会赋值后续不会修改B的读视图中的相关字段。
整个流程就是这样的
事务A操作事务A描述事务B描述事务B操作begin开启事务开启事务beginselect * from user快照读(无影响)查询快照读查询select * from userupdate user set age18 where id1;更新age18--commit提交事务--select 快照读 ,没有读到age18select * from userselect lock in share mode当前读 , 读到age18select * from user lock in share mode
情况二左边提交前右边不读左边提交后右边进行快照读和当前读。
此时会话隔离级别还是RR的。
先让左侧提交
右侧再进行快照读
还是以A和B来表示这两个会话。
此时B在进行快照读那么就会形成对应的读视图而此时A事务已经提交了那么B形成的读视图中就不会有A的事务ID也就是说B在后续进行快照读的时候都会认为A这个事务已经结束了那么就会读取到A修改后的结果所以说现在快照读和当前读的结果都是相同的
整个流程就是这样
事务A操作事务A描述事务B描述事务B操作begin开启事务开启事务beginselect * from user快照读查到age18--update user set age28 where id1;更新 age28--commit提交事务--select 快照读 age28select * from userselect lock in share mode当前读 age28select * from user lock in share mode
故read view形成的时机不同会影响事务的可见性看的数据是老的还是新的不重要在RR隔离级别下保证读到的记录是一直的才最重要。
我前面这些演示都是在RR级别下进行的不是所有的隔离级别都是这样。
来说说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才会有不可重复读问题。
推荐阅读
https://blog.csdn.net/SnailMann/article/details/94724197 https://www.cnblogs.com/f-ck-need-u/archive/2018/05/08/9010872.html https://blog.csdn.net/chenghan_yang/article/details/97630626
https://www.jianshu.com/p/398d788e1083 https://tech.meituan.com/2014/08/20/innodb-lock.html https://www.cnblogs.com/aspirant/p/9177978.html 到此结束。。。