事务篇
事务的隔离级别是怎么实现的
现在SolitudeAlma和我的卡里各有100万,我决定转100万给他,那么最后我的卡里肯定是0元,而SolitudeAlma的卡里是200万元。
转账这一动作在数据库里会涉及到一系列的操作,假设我向你卡里转账的过程是有以下几个步骤组成的:
1. 从数据库读取我的余额
2. 将我的余额减去转账的金额
3. 将修改后的余额更新到数据库里
4. 从数据库读取你的余额
5. 将你的余额加上转账的金额
6. 将修改后的余额更新到数据库里
可以看到其中涉及了两次的数据库操作
假设在执行第三个步骤之后,服务器突然掉点了或者是程序突然出错,就会发生一件蛋疼的事,我的账户扣了100万,但是钱没有到你的账户上,也就是说这100万不见了
要解决这个问题,就要保证转账业务里的所有数据库的操作是不可分割的,要么全部执行,要么全部失败,不允许出现中间状态的数据
数据库中的[事务(transction)]就能达到这样的效果
在转账操作开始之前开启事务,等所有数据库操作执行完之后,才提交事务。对已提交的事务来说,该事务对数据库所做的修改将永久生效,如果中途发生中断或者错误,那么该事务期间对数据库的修改将会被回滚到没执行该事务之前的状态
事务有哪些特性
事务是由MySQL的引擎来实现的,常见的InnoDB引擎是支持事务的
但并不是所有的引擎都支持事务,比如MySQL原生的引擎MyISAM引擎就不支持事务,也正是这样,InnoDB是使用比较多的一个引擎
要实现事务必须遵守4个特性,分别如下:
1. 原子性(Atomicity): 一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,而且事务在执行过程中发生错误,会被回滚到事务开始的状态。就像网购一样,付款成功,商品到手(忽略退款等因素),付款失败,商品还在商家手中,消费者的钱也没花出去
2. 一致性(Constency):是指事务操作前后,数据满足完整性约束,数据库保持一致性状态。比如,用户 A 和用户 B 在银行分别有800元和600元,总共1400元,用户A给用户B转账200元,分为两个步骤,从A的账户扣除200元和对B的账户增加 200元。一致性就是要求上述步骤操作后,最后的结果是用户A还有600元,用户B有800元,总共1400元,而不会出现用户A扣除了200元,但用户B未增加的情况(该情况,用户A和B均为600元,总共1200元)。
3. 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。也就是说,消费者购买商品这个事务,是不影响其他消费者购买的。
4. 持久性(Durability):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
InnoDB引擎通过什么技术来保证事务的四个特性呢?
- 持久性是通过redo log(重做日志)来保证的
- 原子性是通过undo log(回滚日志)来保证的
- 隔离性是通过MVCC(多版本并发控制)或锁机制来保证的
- 一致性是通过持久性+原子性+隔离性来保证的
并行事务会引发什么问题?
MySQL服务端是允许多个客户端连接的,这意味着MySQL会出现同时处理多个事务的情况。
那么在同时处理多个事务的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题。
脏读
如果一个事务「读到」了另一个「未提交事务修改过的数据」,就意味着发生了「脏读」现象。
假设有A和B这两个事务同时在处理,事务A先开始从数据库中读取SolitudeAlma的余额数据,然后再执行更新操作,如果此时事务A还没有提交事务,而此时正好事务B也从数据库中读取SolitudeAlma的余额数据,那么事务B读取到的余额数据是刚才事务A更新后的数据,即使没有提交事务。
因为事务A是还没提交事务的,也就是它随时可能发生回滚操作,如果在上面这种情况事务A发生了回滚,那么事务B刚才得到的数据就是过期的数据,这种现象就被称为脏读。
不可重复读
在一个事务内多次读取同一个数据,如果出现前后两次读到的数据不一样的情况,就意味着发生了「不可重复读」现象。
假设有A和B这两个事务同时在处理,事务A先开始从数据库中读取小林的余额数据,然后继续执行代码逻辑处理,在这过程中如果事务B更新了这条数据,并提交了事务,那么当事务A再次读取该数据时,就会发现前后两次读到的数据是不一致的,这种现象就被称为不可重复读。
幻读
在一个事务内多次查询某个符合查询条件的「记录数量」,如果出现前后两次查询到的记录数量不一样的情况,就意味着发生了「幻读」现象。
假设有A和B这两个事务同时在处理,事务A先开始从数据库查询账户余额大于100万的记录,发现共有5条,然后事务B也按相同的搜索条件也是查询出了5条记录。
接下来,事务A插入了一条余额超过100万的账号,并提交了事务,此时数据库超过100万余额的账号个数就变为6。
然后事务B再次查询账户余额大于100万的记录,此时查询到的记录数量有6条,发现和前一次读到的记录数量不一样了,就感觉发生了幻觉一样,这种现象就被称为幻读。
事务的隔离级别有哪些?
前面提到,当多个事务并发执行时可能会遇到「脏读、不可重复读、幻读」的现象,这些现象会对事务的一致性产生不同程序的影响。
- 脏读:读到其他事务未提交的数据;
- 不可重复读:前后读取的数据不一致;
- 幻读:前后读取的记录数量不一致。
这三个现象的严重性排序如下:
脏读 $ \gt $ 不可重复读 $ \gt $ 幻读
SQL标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低,这四个隔离级别如下:
- $\color{#0000FF}{读未提交}$$\color{#FF00FF}{(read uncommitted)}$,指一个事务还没提交时,它做的变更就能被其他事务看到
- $\color{#0000FF}{读提交}$$\color{#FF00FF}{(read committed)}$,指一个事务提交之后,它做的变更才能被其他事务看到
- $\color{#0000FF}{可重复读}$$\color{#FF00FF}{(repeatable read)}$,指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,InnoDB 引擎的默认隔离级别
- $\color{#0000FF}{串行化}$$\color{#FF00FF}{(serializable )}$,会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行
按隔离水平高低排序如下:
串行化 $\gt$ 可重复读 $\gt$ 读提交 $\gt$ 读未提交
针对不同的隔离级别,并发事务时可能发生的现象也会不同。
- 在「读未提交」隔离级别下,可能发生脏读、不可重复读和幻读现象
- 在「读提交」隔离级别下,可能发生不可重复读和幻读现象,但是不可能发生脏读现象
- 在「可重复读」隔离级别下,可能发生幻读现象,但是不可能脏读和不可重复读现象
- 在「串行化」隔离级别下,脏读、不可重复读和幻读现象都不可能会发生
$\color{#0000FF}{要解决脏读现象,就要升级到「读提交」以上的隔离级别;要解决不可重复读现象,就要升级到「可重复读」的隔离级别}$。
不过,要解决幻读现象不建议将隔离级别升级到「串行化」,因为这样会导致数据库在并发事务时性能很差(加锁)。
$\color{#0000FF}{InnoDB 引擎的默认隔离级别虽然是「可重复读」,但是它通过next-key lock 锁(行锁和间隙锁的组合)来锁住记录之间的“间隙”和记录本身,防止其他事务在这个记录之间插入新的记录,这样就避免了幻读现象。}$
接下来,举个具体的例子来说明这四种隔离级别,有一张账户余额表,里面有一条记录:
然后有两个并发的事务,事务A只负责查询余额,事务B则会将我的余额改成200万,下面是按照时间顺序执行两个事务的行为:
启动事务A | 启动事务B |
---|---|
查询得到余额100万 | |
查询得到余额100万 | |
将余额100万改成200万 | |
查询得到余额V1 | |
提交事务B | |
查询得到余额V2 | |
提交事务A | |
查询得到余额V3 |
在不同隔离级别下,事务A执行过程中查询到的余额可能会不同:
- 在「读未提交」隔离级别下,事务 B 修改余额后,虽然没有提交事务,但是此时的余额已经可以被事务A看见了,于是事务A中余额V1查询的值是200万,余额 V2、V3 自然也是200万了
- 在「读提交」隔离级别下,事务B修改余额后,因为没有提交事务,所以事务A中余额V1的值还是100万,等事务B提交完后,最新的余额数据才能被事务A看见,因此余额V2、V3 都是200万
- 在「可重复读」隔离级别下,事务A只能看见启动事务时的数据,所以余额V1、余额V2的值都是100万,当事务A提交事务后,就能看见最新的余额数据了,所以余额V3的值是200万
- 在「串行化」隔离级别下,事务B在执行将余额100万修改为200万时,由于此前事务A执行了读操作,这样就发生了读写冲突,于是就会被锁住,直到事务A提交后,事务B才可以继续执行,所以从A的角度看,余额V1、V2的值是100万,余额V3的值是200万。
这四种隔离级别具体是如何实现的呢?
- 对于「读未提交」隔离级别的事务来说,因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;
- 对于「串行化」隔离级别的事务来说,通过加读写锁的方式来避免并行访问;
- 对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 ReadView理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。「读提交」隔离级别是在「每个语句执行前」都会重新生成一个 Read View,而「可重复读」隔离级别是「启动事务时」生成一个 ReadView,然后整个事务期间都在用这个ReadView。
注意,执行「开始事务」命令,并不意味着启动了事务。在 MySQL 有两种开启事务的命令,分别是:
- 第一种:begin/start transaction 命令;
- 第二种:start transaction with consistent snapshot 命令;
这两种开启事务的命令,事务的启动时机是不同的:
- 执行了 begin/start transaction 命令后,并不代表事务启动了。只有在执行这个命令后,执行了增删查改操作的 SQL 语句,才是事务真正启动的时机;
- 执行了 start transaction with consistent snapshot 命令,就会马上启动事务。
宝儿姐大几了
大三
收藏一波
hh,基本上是搬运的小林coding,他讲的很不错