1688代加工官方网站,领英如何创建公司主页,wordpress深度修改,南通房产网经常提到数据库的事务#xff0c;那你知道数据库还有事务隔离的说法吗#xff0c;事务隔离还有隔离级别#xff0c;那什么是事务隔离#xff0c;隔离级别又是什么呢#xff1f;本文就帮大家梳理一下。 MySQL 事务
本文所说的 MySQL 事务都是指在 InnoDB 引擎下#xff0… 经常提到数据库的事务那你知道数据库还有事务隔离的说法吗事务隔离还有隔离级别那什么是事务隔离隔离级别又是什么呢本文就帮大家梳理一下。 MySQL 事务
本文所说的 MySQL 事务都是指在 InnoDB 引擎下MyISAM 引擎是不支持事务的。
数据库事务指的是一组数据操作事务内的操作要么就是全部成功要么就是全部失败什么都不做其实不是没做是可能做了一部分但是只要有一步失败就要回滚所有操作有点一不做二不休的意思。
假设一个网购付款的操作用户付款后要涉及到订单状态更新、扣库存以及其他一系列动作这就是一个事务如果一切正常那就相安无事一旦中间有某个环节异常那整个事务就要回滚总不能更新了订单状态但是不扣库存吧这问题就大了。
事务具有原子性Atomicity、一致性Consistency、隔离性Isolation、持久性Durability四个特性简称 ACID缺一不可。今天要说的就是隔离性。
概念说明
以下几个概念是事务隔离级别要实际解决的问题所以需要搞清楚都是什么意思。
脏读
脏读指的是读到了其他事务未提交的数据未提交意味着这些数据可能会回滚也就是可能最终不会存到数据库中也就是不存在的数据。读到了并一定最终存在的数据这就是脏读。
可重复读
可重复读指的是在一个事务内最开始读到的数据和事务结束前的任意时刻读到的同一批数据都是一致的。通常针对数据**更新UPDATE**操作。
不可重复读
对比可重复读不可重复读指的是在同一事务内不同的时刻读到的同一批数据可能是不一样的可能会受到其他事务的影响比如其他事务改了这批数据并提交了。通常针对数据**更新UPDATE**操作。
幻读
幻读是针对数据**插入INSERT**操作来说的。假设事务A对某些行的内容作了更改但是还未提交此时事务B插入了与事务A更改前的记录相同的记录行并且在事务A提交之前先提交了而这时在事务A中查询会发现好像刚刚的更改对于某些数据未起作用但其实是事务B刚插入进来的让用户感觉很魔幻感觉出现了幻觉这就叫幻读。
事务隔离级别
SQL 标准定义了四种隔离级别MySQL 全都支持。这四种隔离级别分别是
读未提交READ UNCOMMITTED读提交 READ COMMITTED可重复读 REPEATABLE READ串行化 SERIALIZABLE
从上往下隔离强度逐渐增强性能逐渐变差。采用哪种隔离级别要根据系统需求权衡决定其中可重复读是 MySQL 的默认级别。
事务隔离其实就是为了解决上面提到的脏读、不可重复读、幻读这几个问题下面展示了 4 种隔离级别对这三个问题的解决程度。
隔离级别脏读不可重复读幻读读未提交可能可能可能读提交不可能可能可能可重复读不可能不可能可能串行化不可能不可能不可能
只有串行化的隔离级别解决了全部这 3 个问题其他的 3 个隔离级别都有缺陷。
一探究竟
下面我们来一一分析这 4 种隔离级别到底是怎么个意思。
如何设置隔离级别
我们可以通过以下语句查看当前数据库的隔离级别通过下面语句可以看出我使用的 MySQL 的隔离级别是 REPEATABLE-READ也就是可重复读这也是 MySQL 的默认级别。
# 查看事务隔离级别 5.7.20 之后
show variables like transaction_isolation;
SELECT transaction_isolation# 5.7.20 之后
SELECT tx_isolation
show variables like tx_isolation--------------------------------
| Variable_name | Value |
--------------------------------
| tx_isolation | REPEATABLE-READ |
--------------------------------稍后我们要修改数据库的隔离级别所以先了解一下具体的修改方式。
修改隔离级别的语句是set [作用域] transaction isolation level [事务隔离级别] SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}。
其中作用于可以是 SESSION 或者 GLOBALGLOBAL 是全局的而 SESSION 只针对当前回话窗口。隔离级别是 {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE} 这四种不区分大小写。
比如下面这个语句的意思是设置全局隔离级别为读提交级别。
mysql set global transaction isolation level read committed; MySQL 中执行事务
事务的执行过程如下以 begin 或者 start transaction 开始然后执行一系列操作最后要执行 commit 操作事务才算结束。当然如果进行回滚操作(rollback)事务也会结束。 需要注意的是begin 命令并不代表事务的开始事务开始于 begin 命令之后的第一条语句执行的时候。例如下面示例中select * from xxx 才是事务的开始
begin;
select * from xxx;
commit; -- 或者 rollback;另外通过以下语句可以查询当前有多少事务正在运行。
select * from information_schema.innodb_trx;好了重点来了开始分析这几个隔离级别了。
接下来我会用一张表来做一下验证表结构简单如下
CREATE TABLE user (id int(11) NOT NULL AUTO_INCREMENT,name varchar(30) DEFAULT NULL,age tinyint(4) DEFAULT NULL,PRIMARY KEY (id)
) ENGINEInnoDB AUTO_INCREMENT2 DEFAULT CHARSETutf8初始只有一条记录
mysql SELECT * FROM user;
---------------------------
| id | name | age |
---------------------------
| 1 | 古时的风筝 | 1 |
---------------------------读未提交
MySQL 事务隔离其实是依靠锁来实现的加锁自然会带来性能的损失。而读未提交隔离级别是不加锁的所以它的性能是最好的没有加锁、解锁带来的性能开销。但有利就有弊这基本上就相当于裸奔啊所以它连脏读的问题都没办法解决。
任何事务对数据的修改都会第一时间暴露给其他事务即使事务还没有提交。
下面来做个简单实验验证一下首先设置全局隔离级别为读未提交。
set global transaction isolation level read uncommitted;设置完成后只对之后新起的 session 才起作用对已经启动 session 无效。如果用 shell 客户端那就要重新连接 MySQL如果用 Navicat 那就要创建新的查询窗口。
启动两个事务分别为事务A和事务B在事务A中使用 update 语句修改 age 的值为10初始是1 在执行完 update 语句之后在事务B中查询 user 表会看到 age 的值已经是 10 了这时候事务A还没有提交而此时事务B有可能拿着已经修改过的 age10 去进行其他操作了。在事务B进行操作的过程中很有可能事务A由于某些原因进行了事务回滚操作那其实事务B得到的就是脏数据了拿着脏数据去进行其他的计算那结果肯定也是有问题的。
顺着时间轴往表示两事务中操作的执行顺序重点看图中 age 字段的值。 读未提交其实就是可以读到其他事务未提交的数据但没有办法保证你读到的数据最终一定是提交后的数据如果中间发生回滚那就会出现脏数据问题读未提交没办法解决脏数据问题。更别提可重复读和幻读了想都不要想。
读提交
既然读未提交没办法解决脏数据问题那么就有了读提交。读提交就是一个事务只能读到其他事务已经提交过的数据也就是其他事务调用 commit 命令之后的数据。那脏数据问题迎刃而解了。
读提交事务隔离级别是大多数流行数据库的默认事务隔离界别比如 Oracle但是不是 MySQL 的默认隔离界别。
我们继续来做一下验证首先把事务隔离级别改为读提交级别。
set global transaction isolation level read committed;之后需要重新打开新的 session 窗口也就是新的 shell 窗口才可以。
同样开启事务A和事务B两个事务在事务A中使用 update 语句将 id1 的记录行 age 字段改为 10。此时在事务B中使用 select 语句进行查询我们发现在事务A提交之前事务B中查询到的记录 age 一直是1直到事务A提交此时在事务B中 select 查询发现 age 的值已经是 10 了。
这就出现了一个问题在同一事务中(本例中的事务B)事务的不同时刻同样的查询条件查询出来的记录内容是不一样的事务A的提交影响了事务B的查询结果这就是不可重复读也就是读提交隔离级别。 每个 select 语句都有自己的一份快照而不是一个事务一份所以在不同的时刻查询出来的数据可能是不一致的。
读提交解决了脏读的问题但是无法做到可重复读也没办法解决幻读。
可重复读
可重复是对比不可重复而言的上面说不可重复读是指同一事物不同时刻读到的数据值可能不一致。而可重复读是指事务不会读到其他事务对已有数据的修改及时其他事务已提交也就是说事务开始时读到的已有数据是什么在事务提交前的任意时刻这些数据的值都是一样的。但是对于其他事务新插入的数据是可以读到的这也就引发了幻读问题。
同样的需改全局隔离级别为可重复读级别。
set global transaction isolation level repeatable read;在这个隔离级别下启动两个事务两个事务同时开启。
首先看一下可重复读的效果事务A启动后修改了数据并且在事务B之前提交事务B在事务开始和事务A提交之后两个时间节点都读取的数据相同已经可以看出可重复读的效果。 可重复读做到了这只是针对已有行的更改操作有效但是对于新插入的行记录就没这么幸运了幻读就这么产生了。我们看一下这个过程
事务A开始后执行 update 操作将 age 1 的记录的 name 改为“风筝2号”
事务B开始后在事务执行完 update 后执行 insert 操作插入记录 age 1name 古时的风筝这和事务A修改的那条记录值相同然后提交。
事务B提交后事务A中执行 select查询 age1 的数据这时会发现多了一行并且发现还有一条 name 古时的风筝age 1 的记录这其实就是事务B刚刚插入的这就是幻读。 要说明的是当你在 MySQL 中测试幻读的时候并不会出现上图的结果幻读并没有发生MySQL 的可重复读隔离级别其实解决了幻读问题这会在后面的内容说明
串行化
串行化是4种事务隔离级别中隔离效果最好的解决了脏读、可重复读、幻读的问题但是效果最差它将事务的执行变为顺序执行与其他三个隔离级别相比它就相当于单线程后一个事务的执行必须等待前一个事务结束。
MySQL 中是如何实现事务隔离的
首先说读未提交它是性能最好也可以说它是最野蛮的方式因为它压根儿就不加锁所以根本谈不上什么隔离效果可以理解为没有隔离。
再来说串行化。读的时候加共享锁也就是其他事务可以并发读但是不能写。写的时候加排它锁其他事务不能并发写也不能并发读。
最后说读提交和可重复读。这两种隔离级别是比较复杂的既要允许一定的并发又想要兼顾的解决问题。
实现可重复读
为了解决不可重复读或者为了实现可重复读MySQL 采用了 MVVC (多版本并发控制) 的方式。
我们在数据库表中看到的一行记录可能实际上有多个版本每个版本的记录除了有数据本身外还要有一个表示版本的字段记为 row trx_id而这个字段就是使其产生的事务的 id事务 ID 记为 transaction id它在事务开始的时候向事务系统申请按时间先后顺序递增。 按照上面这张图理解一行记录现在有 3 个版本每一个版本都记录这使其产生的事务 ID比如事务A的transaction id 是100那么版本1的row trx_id 就是 100同理版本2和版本3。
在上面介绍读提交和可重复读的时候都提到了一个词叫做快照学名叫做一致性视图这也是可重复读和不可重复读的关键可重复读是在事务开始的时候生成一个当前事务全局性的快照而读提交则是每次执行语句的时候都重新生成一次快照。
对于一个快照来说它能够读到那些版本数据要遵循以下规则
当前事务内的更新可以读到版本未提交不能读到版本已提交但是却在快照创建后提交的不能读到版本已提交且是在快照创建前提交的可以读到
利用上面的规则再返回去套用到读提交和可重复读的那两张图上就很清晰了。还是要强调两者主要的区别就是在快照的创建上可重复读仅在事务开始是创建一次而读提交每次执行语句的时候都要重新创建一次。
并发写问题
存在这的情况两个事务对同一条数据做修改。最后结果应该是哪个事务的结果呢肯定要是时间靠后的那个对不对。并且更新之前要先读数据这里所说的读和上面说到的读不一样更新之前的读叫做“当前读”总是当前版本的数据也就是多版本中最新一次提交的那版。
假设事务A执行 update 操作 update 的时候要对所修改的行加行锁这个行锁会在提交之后才释放。而在事务A提交之前事务B也想 update 这行数据于是申请行锁但是由于已经被事务A占有事务B是申请不到的此时事务B就会一直处于等待状态直到事务A提交事务B才能继续执行如果事务A的时间太长那么事务B很有可能出现超时异常。如下图所示。 加锁的过程要分有索引和无索引两种情况比如下面这条语句
update user set age11 where id 1id 是这张表的主键是有索引的情况那么 MySQL 直接就在索引数中找到了这行数据然后干净利落的加上行锁就可以了。
而下面这条语句
update user set age11 where age10表中并没有为 age 字段设置索引所以 MySQL 无法直接定位到这行数据。那怎么办呢当然也不是加表锁了。MySQL 会为这张表中所有行加行锁没错是所有行。但是呢在加上行锁后MySQL 会进行一遍过滤发现不满足的行就释放锁最终只留下符合条件的行。虽然最终只为符合条件的行加了锁但是这一锁一释放的过程对性能也是影响极大的。所以如果是大表的话建议合理设计索引如果真的出现这种情况那很难保证并发度。
解决幻读
上面介绍可重复读的时候那张图里标示着出现幻读的地方实际上在 MySQL 中并不会出现MySQL 已经在可重复读隔离级别下解决了幻读的问题。
前面刚说了并发写问题的解决方式就是行锁而解决幻读用的也是锁叫做间隙锁MySQL 把行锁和间隙锁合并在一起解决了并发写和幻读的问题这个锁叫做 Next-Key锁。
假设现在表中有两条记录并且 age 字段已经添加了索引两条记录 age 的值分别为 10 和 30。 此时在数据库中会为索引维护一套B树用来快速定位行记录。B索引树是有序的所以会把这张表的索引分割成几个区间。 如图所示分成了3 个区间(负无穷,10]、(10,30]、(30,正无穷]在这3个区间是可以加间隙锁的。
之后我用下面的两个事务演示一下加锁过程。 在事务A提交之前事务B的插入操作只能等待这就是间隙锁起得作用。当事务A执行update user set name风筝2号’ where age 10; 的时候由于条件 where age 10 数据库不仅在 age 10 的行上添加了行锁而且在这条记录的两边也就是(负无穷,10]、(10,30]这两个区间加了间隙锁从而导致事务B插入操作无法完成只能等待事务A提交。不仅插入 age 10 的记录需要等待事务A提交age10、10age30 的记录页无法完成而大于等于30的记录则不受影响这足以解决幻读问题了。
这是有索引的情况如果 age 不是索引列那么数据库会为整个表加上间隙锁。所以如果是没有索引的话不管 age 是否大于等于30都要等待事务A提交才可以成功插入。
总结
MySQL 的 InnoDB 引擎才支持事务其中可重复读是默认的隔离级别。
读未提交和串行化基本上是不需要考虑的隔离级别前者不加锁限制后者相当于单线程执行效率太差。
读提交解决了脏读问题行锁解决了并发更新的问题。并且 MySQL 在可重复读级别解决了幻读问题是通过行锁和间隙锁的组合 Next-Key 锁实现的。