事务
文章主要针对Mysql进行介绍
为什么需要事务?
举个例子:
假如你在街上卖煎饼果子,正好有个用户来买。你熟练的做好一套煎饼果子,加上了肠、辣条、鸡蛋。你心想这一单能挣不少钱。然后你高兴的递给用户,他也很高兴,拿出手机准备扫码付钱。
等用户输入好金额并发送了之后,用户的账户里扣了钱,正准备走了。这时可能扫码支付的软件他服务端出了问题,这笔钱他没有支付成功,但是成功的扣了款。
那怎么办?这岂不是出了大问题了?但现在有了事务就不一样了,数据库中的「事务(Transaction)」就是用来解决这个问题的。下面我们来一起看看如何解决。
事务的特性
事务是由MySQL的引擎来实现的,我们常见的Innodb
引擎就支持事务。
但并不是所有引擎都支持事务,比如Myisam
,Memory
等引擎就不支持事务,因此MySQL中默认的存储引擎就是Innodb
。
要想实现事务,必须支持事务的四大特性 ACID:
- 原子性(Atomictity):一个事务中执行的所有操作,要么全部成功,要么全部失败,不可能存在有中间状态的操作。凡是执行失败的操作都会进行回滚(rollback),也就是在此期间执行的所有操作全部无效。这样就避免了你转账给对方,结果对方没有收到,但是你的钱仍然扣了的情况。
- 一致性(Consistency):启动事务之前和执行事务完成之后,数据满足完整性约束性,数据库状态保持一致。意思是说:你给对方转账100元,对方必须收到100元,不能是50或者150元。要保证执行事务之前和执行事务之后总的数据不会发生变化。
- 隔离性(Isolation):数据库允许多个事务同时对数据进行读写操作。隔离性就是为了防止多个事务并行操作时发生数据错误而导致数据不一致的情况发生。多个事务同时操作时,事务之间都是隔离的,每个事务拥有自己单独的数据空间。
- 持久性(Durability):事务一旦提交造成的影响便是持久性的,不可逆转的。即便系统故障也不会发生变化。
Innodb是如何实现事务的呢?
- 持久性是通过
redo log
(重做日志)来保证的; - 原子性是通过
undo log
(回滚日志) 来保证的; - 隔离性是通过
MVCC
(多版本并发控制) 或锁机制来保证的; - 一致性则是通过持久性+原子性+隔离性来保证;
事务的隔离性
因为数据库是支持多连接并行操作的,因此在并行操作的时候可能会发生以下几个情况:
- 脏读
- 不可重复读
- 幻读
下面来解释一下这三个关键词的意思,并举例说明一下(这里图片都是小林老师的图片,因为我觉得画的非常好)。
脏读
假设有两个数据库连接A、B,他们都开启了事务。
在A对数据库进行了更新操作之后,A还并没有提交,B对该数据进行了读取。此时如果A提交失败,发生回滚的情况下。B无论提交成功与否,都已经拿到了这个不存在的数据。假如B将这个脏数据顺利返回,那就可能会造成严重后果。这就是脏读。下面给出图片示例说明:
注意关键点:
- 同时启动事务。
- 在一方未提交事务之前另一方能够共享数据,也就是说能够互相产生影响。
- 已经查询出的数据有误,全程只读一次。
不可重复读
假如A、B连接都开启了事务。
A 对数据库中一个数据进行了查询操作,这时A未提交。而B对数据进行了更改操作,然后B进行了提交并且成功了。
这时A再次查询数据库中该数据,结果发现和上一个数据不相同。这就是不可重复读的情况。
这里我用一个例子讲一下:
假如你和你朋友出门吃饭,在到了餐厅之后你看了看卡里的余额,发现有1000块钱,觉得够这次吃饭用。
结果在你吃饭的时候,你女朋友把你卡里的钱用了500。
然后等你结账的时候,又看了一下卡里的余额,只剩下500,发现不够了,直接人就傻眼了。
这个故事告诉我们有女朋友就不要和朋友一起吃饭了(doge)。
注意关键点:
- 同时启动事务。
- 在一方未提交事务之前另一方能够共享数据,也就是说能够互相产生影响。
- A在读取数据之后,B对其进行了修改操作,并提交事务成功。
幻读
假如A、B连接都开启了事务。
A对一个数据进行了范围读取,找到了数据并且得到了想要的答案,但需要对每一条进行处理之后才能提交。
这时候B增加了一条数据,刚好包含在A查找数据的范围之内,并且提交了事务。
等A准备提交事务的时候,需要再来检查一下这个数据的答案,发现找到的数据条数多了一个?A揉了揉自己的眼睛,不会是自己出现幻觉了吧!这就是幻读。
注意关键点:
- 同时启动事务。
- 在一方未提交事务之前另一方能够共享数据,也就是说能够互相产生影响。
- A在读取数据之后,B对其进行了增删操作并提交事务成功。
综上所述,有没有发现共同点呢?下面就根据这三个不同的情况,分别采用不同的级别来解决。
事务的隔离级别
由以上结果可以看到,当多个事务并发执行时可能会遇到**「脏读、不可重复读、幻读」**的现象,这些现象会对事务的一致性产生不同程序的影响。
- 脏读:读到其他事务未提交的数据。
- 不可重复读:前后读取的数据不一致。
- 幻读:前后读取的记录数量不一致。
SQL 标准提出了四种隔离级别来规避这些现象,隔离级别越高,性能效率就越低,这四个隔离级别如下:
- **读未提交 read uncommitted:**指一个事务还没提交时,它做的变更就能被其他事务看到;
- **读提交 read committed:**指一个事务提交之后,它做的变更才能被其他事务看到;
- 可重复读 repeatable read:指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,MySQL InnoDB 引擎的默认隔离级别;
- **串行化 serializable :**会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行;
根据不同的隔离级别,解决的问题也不相同:
读已提交:
解决了脏读问题,在没有提交之前不会被其他事务看到该事务,然后对数据造成的影响。全程只读一次,只会读到事务提交之后的一致性的结果。
可重复读:
Mysql的默认隔离级别。解决了脏读和不可重复读的问题。脏读就不用讲了,讲一下为什么能解决不可重复读的问题。
因为可重复读这个级别是在事务开启的时候,单独开了一个数据空间,这个空间的数据在提交之前是不会被其他事务改变的。而不可重复读正是因为在执行事务期间,数据发生了修改,因此解决了这个问题。
串行化:
加锁!,提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。这也验证了级别越高,性能越差的道理,没办法,要获得什么之前必须失去点什么。
所以要想解决不可重复读和脏读,至少要达到可重复读的级别。要想解决幻读、不可重复读和脏读,至少要达到串行化的级别。但是实际上,在Mysql中可重复读级别已经能解决大部分不可重复读的情况。而串行化不建议使用,不支持并行处理事务,性能太低。
举例说明
下面用一张图片来分别讲一下这四个隔离级别:
有两个不同的连接A、B都开启了事务。
A首先对余额进行查询操作,发现有100W。然后B紧接着对该余额进行了修改,变为200W(能这么修改余额真不错)。
这时A再次对余额进行查询,得到了V1。在A第二次查询该余额之后,事务B进行提交并且成功了。
A第三次对余额进行查询并得到了结果V2。A提交事务并且成功了。
A在提交事务之后对余额再次查询,得到结果V3。
接下来我们来分析在不同的隔离级别下,这几种结果究竟是什么?会发生什么变化?
- 在 [读未提交] 级别下,事务与事务之间是可以互相影响的。因此V1将会是B修改之后的结果200W。在B成功提交事务之后,A再次查询得到V2,结果为200W,这就发生了不可重复读。然后A提交事务并且成功,这就发生了脏读。
- 在 [读已提交] 级别下,事务与事务之间通过提交这个操作才能互相影响。因此在B提交之前的所有操作,A是不可得到的,也就不可能发生脏读。但在B提交之后,A是可以得到提交结果的。因此V1的数据为100W,V2的数据为200W,这里就发生了不可重复读。
- 在 [可重复读] 级别下,每个事务都有独立的数据空间,互不影响。因此在A事务提交之前都不会看到B事务操作之后的结果。也就是说V1、V2都是100W,只有V3是200W。避免了脏读和不可重复读。
- 在 [串行化] 级别下,一个事务对数据进行读—写操作会对其加锁处理,直到事务提交。因此在A对该余额进行读取之后便进行了锁操作,B对其进行的修改操作当即不会生效,而是一直在堵塞,等待A事务的提交。等到A事务提交成功之后,B事务才会进行修改操作,然后提交。因此答案应该是不确定的。
那么,Mysql的Innodb引擎事务中中默认的可重复读级别为什么能解决大部分幻读情况呢?继续研究~
默认的可重复读级别为什么能解决大部分幻读情况呢?
首先来看看 MySQL 文档是怎么定义幻读(Phantom Read)的:
The so-called phantom problem occurs within a transaction when the same query produces different sets of rows at different times. For example, if a SELECT is executed twice, but returns a row the second time that was not returned the first time, the row is a “phantom” row.
翻译:当同一个查询在不同的时间产生不同的结果集时,事务中就会出现所谓的幻象问题。例如,如果 SELECT 执行了两次,但第二次返回了第一次没有返回的行,则该行是“幻像”行。
深入理解幻读
在狠狠的查阅了一番资料之后,发现对幻读又有了新的认识:
幻读侧重的是【新插入的行】,也就是行锁。删除和修改都会有锁,但是新增数据没有办法有锁,所以删除不是幻读。
事务A 按照一定条件进行数据读取, 期间事务B 插入了相同搜索条件的新数据,事务A再次按照原先条件进行读取时,发现了事务B 新插入的数据 称为幻读。
如果事务A 按一定条件搜索, 期间事务B 删除了符合条件的某一条数据,导致事务A 再次读取时数据少了一条。这种情况归为 不可重复读。
解决方案:
- 针对快照读(普通的select查询语句),通过MVCC方式解决幻读。因为我们说过了可重复读隔离级别下,相当于开了一个单独的数据空间,事务执行过程中看到的数据和事务刚启动时的数据是一致的。因此即使在事务执行过程中,其他事务增加了一条数据,无论提交与否,这个单独的数据空间也不会收到影响,也就解决了幻读问题。
- 针对当前读(select … for update 等语句),是通过 next-key lock(记录锁+间隙锁)方式解决了幻读,因为当执行 select … for update 语句的时候,会加上 next-key lock,如果有其他事务在 next-key lock 锁范围内插入了一条记录,那么这个插入语句就会被阻塞,无法成功插入,所以就很好了避免幻读问题。
我猜到有些初学的小伙伴到这里可能已经懵逼了,什么是快照读?什么是当前读?什么是MVCC方式?(快点告诉我!!!)
我知道你很急,但你先别急(doge),听我徐徐道来~
快照读
mysql中的快照读是通过MVCC + undo log实现的。不同隔离级别下的快照读也不同!
一言蔽之,就是读取快照的数据,这个快照一般指的是历史快照(用过虚拟机的应该知道什么是快照吧,这里就不解释了)。
快照就是指:你用照相机拍照,每个照片都是一个瞬间的历史,都是已经发生过的,但是你仍然可以从照片中获取到历史数据。
快照读包含的 SQL 语句为简单的 select 语句,就是不包含 ...for update, ...lock in share mode
关键字的。
刚刚提到undo log
,当我们对记录做了变更操作时,就会产生undo
记录。undo
记录中存储的是老版数据,当一个旧的事务需要读取数据时,为了能够读取到老版本的数据,需要顺着undo
列找到满足其可见性的记录,这个找满足可见行的记录依赖。就是说每次都是读取undo log
中的数据。
当前读
Mysql实现当前读是通过共享锁 + 排他锁 + Next-Key Lock实现的。
当前读包含的 SQL 语句如下:
- update , delete , insert
- select…for update
- select…lock in share mode
当前读, 对读取的记录加锁, 阻塞其他事务同时改动相同记录,避免出现安全问题。
那么为什么要对这些当前读的sql语句进行加锁呢?
假设你正在执行update
的sql语句,这时候突然有一个并行操作把这个语句的结果给删除了,那怎么办?因此在进行这些操作的时候就需要进行加锁操作。
MVCC
全称Multi-Version Concurrency Control
,即多版本并发控制,主要是为了提高数据库的并发性能。说的通俗易懂一点就是记录数据的不同版本。
同一行数据平时发生读写请求时,会上锁阻塞住。但mvcc
用更好的方式去处理读—写请求,做到在发生 读—写 请求冲突时不用加锁,从而提升性能。
这个读是指的快照读,而不是当前读,当前读是一种加锁操作,是悲观锁。
悲观锁就是执行一个操作,无论他有没有可能造成并发危险,我都要给他加锁,因为我很悲观,我觉得他一定会有并发危险。
总结
事务是在 MySQL 引擎层实现的,我们常见的 InnoDB 引擎是支持事务的,并且默认的事务级别是可重复读。
事务的四大特性是原子性、一致性、隔离性、持久性,我们这次主要讲的是隔离性,以及详细介绍了幻读。
当多个事务同时并行进行时可能会发生 脏读、不可重复读、幻读的情况出现。而解决这些问题需要用到不同的事务级别。
- 读未提交:什么也没解决
- 读已提交:解决脏读
- 可重复读:解决脏读和不可重复读
- 串行化:全部解决但需要加锁,导致事务不能采用并行处理,降低效率
四个级别由上到下依次提高,性能依次下降。