【MySQL笔记】正确的理解MySQL的事务和隔离级别

正确的理解MySQL的事务和隔离级别


如果觉得对你有帮助,能否点个赞或关个注,以示鼓励笔者呢?!博客目录 | 先点这里

!首先声明,MySQL的测试环境是5.7

  • 事务的ACID理论
    • 什么是ACID理论?
    • ACID的四个特性
    • ACID中C与CAP定理中C的区别?
  • MySQL事务的隔离级别
    • 事务操作可能会出现的数据问题
    • 事务操作可能出现的更新丢失问题
    • 事务的隔离级别
    • 标准与实现
    • 小结
  • MySQL SQL命令模拟测试
  • 相关问题
    • InnoDB的RR级别下其实有部分场景还是会出现不可重复读问题的
    • InnoDB的RR级别下是如何避免幻读的?
    • Redis的事务可以实现一致性吗?

事务的ACID理论


什么是ACID理论?

ACID理论是指在数据库管理系统(DBMS)中事务所具有的四个特性:

  • 原子性(Atomicity
  • 一致性(Consistency
  • 隔离性(Isolation,又称独立性)
  • 持久性(Durability

即在数据库系统中,为了保证一系列数据操作的正确性,可能需要事务的支持,而为了做到功能这么强大的事务,那么这个事务功能就需要支持ACID四个特性!!


ACID的四个特性

  • 原子性
    即支持多个操作的原子性,把多个操作放到一个事务中,保证这些操作要么都成功,要么都不成功;MySQL的事务支持操作的回滚,所以支持原子性

  • 一致性
    一致性的确是抽象的概念,也蛮不好理解的,因为这名字总会让我联系到数据一致性的错觉。所以我个人偏向于把它理解成一串对数据进行操作的程序执行下来,不会对数据产生不好的影响,比如凭空产生,或消失; 即在事务执行过程中,无论期间执行了多少个对数据的操作,最终的数据都应该是一个正确的结果,不会凭空的出现一些数据或少了一些数据。比如一个事务中,对某条记录的金额做出改变,原金额是200,先减少200,再增加200,该事务的操作不会影响到该记录的金额正确性,即根据因果关系,总金额应该还是200,不会是其他的数值;当然多事务并发的情况下也是一样要保证事务的一致性,不管事务怎么并发,怎么修改数据,用户A向用户B转了200块钱,不会出现用户A扣了款,而用户B没有收到的情况;又或者说数据库的总金额就是500块,无论你怎么并发转账,最终的总金额也应该是500块钱

  • 隔离性
    隔离性的意思就是多个事务之间互相不干扰,即使是并发事务的情况下,他们只是两个并发执行没有交集,互不影响的东西;当然实现中,也不一定需要这么完整隔离性,即不一定需要这么的互不干扰,有时候还是允许有部分干扰的。所以MySQL可以支持4种事务隔离性

  • 持久性
    持久性就是当某个操作操作完毕了,那么结果就是这样了,并且这个操作会持久化到日志记录中。操作的结果也不会突然地凭空消失或回滚。MySQL的事务就支持这样的持久性,一旦事务被提交,那个这个操作就执行完了,不会莫名其妙的恢复到执行前的结果或数据丢失。同时如果数据库崩溃或暂停服务,当恢复正常的时候,只要操作已经持久化到日志中,也可以根据日志恢复原来的模样


ACID中C与CAP定理中C的区别?

ACID理论和CAP理论都有一个C,也都叫一致性Consistent, 所以很多人都会把这两个概念当做是一个概念,把自己弄的迷迷糊糊,团团转。没错,我说的就是自己,shame…

首先我要声明,这两个C肯定是有区别的:

  • ACID的C指的是事务中的一致性,在一串对数据进行修改的操作中,保证数据的正确性。即数据在事务期间的多个操作中,数据不会凭空的消失或增加,数据的每一个增删改操作都是有因果关系的;比如用户A想用户B转了200块钱,不会出现用户A扣了款,而用户B没有收到的情况
  • CAP的C则指的是分布式环境中,多服务之间的复制是异步,需要一定耗时的,不是即时瞬间完成。所以可能会造成某个节点的数据修改,将修改的数据同步到其他服务需要一定的时间,如果此时有并发请求过来,请求负载均衡到多个节点,可能会出现多个节点获取的数据不一致的问题,因为请求有可能会落在还没完成数据同步的节点上;而C就是为了做到在分布式集群环境读到的数据是一致的;当然这里的C也有分类,如强一致性,弱一致性,最终一致性

所以:
即ACID的C着重强调单数据库事务操作时,要保证数据的完整和正确性;
而CAP理论中的C指的是对一个数据多个备份的读写一致性


MySQL事务的隔离级别


事务操作可能会出现的数据问题

  • 脏读
    事务A修改了数据,但未提交,而事务B查询了事务A修改过却没有提交的数据,这就是脏读,因为事务A可能会回滚
  • 不可重复读
    事务A 查询了金额,是200块钱,未提交 。事务B在事务A查询完之后,修改了金额,变成了300, 在事务A前提交了;如果此时事务A再查询一次数据,就会发现钱跟上一次查询不一致,是300,而不是200。这就是不可重复读。强调事务A对要操作的数据被别人修改了,但在不知请的情况下拿去做之前的用途
  • 幻读
    事务A先修改了某个表的所有纪录的状态字段为已处理,未提交;事务B也在此时新增了一条未处理的记录,并提交了;事务A随后查询记录,却发现有一条记录是未处理的,很是诧异,刚刚不是全部修改为已处理嘛,以为出现了幻觉,这就是幻读

数据库中总共就4个操作: 增,删,改,查
脏读说的是事务知道了自己本不应该知道的东西,强调的动作是查询,我看到了自己不该看的东西 ; 不可重复读强调的是一个人查的时候,其他人却可以增删改, 但我却不知道数据被改了,还拿去做了之前的用途;幻读强调的是我修改了数据,等我要查的时候,却发现有我没有修改的记录,为什么,因为有其他人了一条新的


事务操作可能出现的更新丢失问题

更新丢失问题有两类:

  • 第一类更新丢失
    事务A的事务回滚覆盖了事务B已提交的结果
  • 第二类更新丢失
    事务A的提交覆盖了事务B已提交的结果

说白了,就是两个事务之间的写写冲突,第一类就是一个事务的后回滚覆盖另一个事务的前面的提交;第二类就是一个事务的后提交覆盖另一个事务的前提交;但是我们也不用过于关系更新丢失的问题;
应该经过我自己的测试,MySQL5.7版本下,所有的隔离级别都没有出现更新丢失问题,即MySQL5.7版本下的隔离级别默认都是可以解决更新丢失问题的,所以更新丢失的情况并不好模拟。了解一下就好,我们这里不详细讨论

数据库第一类第二类丢失更新 - @作者: 爱米粒Emily


事务的隔离级别的标准

以下是SQL-92定义的四种事务隔离级别标准:

  • Read Uncommitted 读未提交
    限制最弱的事务级别,忽略其他事务放置的锁,该级别下的事务可以在读取其他事务修改后(插/删/更)但未提交的的数据;说白了这个级别的事务就是个弱鸡,只能保证多个操作的原子性,完整不能解决并发问题;无法解决脏读,不可重复读,幻读

  • Read Committed 读提交
    SQL Server默认级别,指定事务执行期间(未提交)不能读取其他事务还未提交的数据,禁止脏读;但可以读取到其他事务 (插/删/更)操作后并提交了的 数据;从而造成多次查询的数据不一致,即不可重复读,同时也不法避免幻读,因为当前事务执行期间,其他事务可以插入新记录

  • Repeatable Read 可重复读
    MySQL的默认级别,事务执行期间(未提交)不能读取其他事务还未提交的数据,也不能其他事务读取更新操作后并提交的数据,所以可以避免脏读和不可重复读;但是可以读取到其他事务插/删操作并提交的数据,因此因为可以读取到其他事务入并提交的新数据,所以可能会造成幻读

  • Serializable 事务同步,串行
    最严格的事务管理,等于事务同步执行,不允许并发,所以不存在并发问题,所有问题都解决了,但是存在性能问题,执行数据慢

级别隔离强大:读未提交<读已提交<可重复读<串行
Transaction Isolation Levels - 作者:@微软


标准与实现

注意!敲黑板

上面所说的四种事务隔离机制,虽然说是SQL-92标准定义的,但标准毕竟只是标准,不是真正的实现,即各大数据库会对应有这么几个事务隔离级别,但是每一种数据库对这几种隔离级别的实现都不尽相同,甚至可能使用更为复杂的方案来提高数据库的并发性能。

就像MySQL InnoDB引擎的默认事务级别是Repeatable Read,按上面的标准来说,这个级别它会出现上面例子提到的幻读现象,就是事务A先更新了表中所有记录的某个字段状态为已处理,未提交,此时事务B插入了一条新记录 ,提交了。事务A此时重新查询所有记录,却发现有一条记录还是未处理。但事实上,MySQL的RR级别是不会发生这种幻读的。因为当我事务A更新了所有记录的某个字段,此时事务A会获得对这个表的表锁,因为事务A还没有提交,所以事务A获得的锁没有释放,此时事务B在该表插入新记录,会因为无法获得该表的锁,则导致插入操作被阻塞。只有事务A提交了事务后,释放了锁,事务B才能进行接下去的操作

同时我们在RR级别的MySQL去测试就会发现,正在执行的事务既不能读取到其他事务未提交的数据,也读取不到其他事务增/删/改并提交后的数据(不完全,因为MySQL存在并发优化,即存在当前读和快照读两种情况,所以有部分场景还是会出现不可重复读问题),所以我们可以说MySQL的RR级别的隔离是已经实现解决了脏读,不可重复读和幻读的;不过MySQL的Read Committed级别倒是的确没有处理不可重复读和幻读的问题,所以如果想重现这些问题,可以在RC级下测试


小结

标题 脏读 不可重复读 幻读
Read Uncommited × × ×
Read Commited × ×
Repeatable Read ×
Serializable

×即会出现相应的问题,即能解决该类问题。

以上是标准事务隔离的规范处理,但是实际各大数据库是怎么控制自己的事务隔离机制的,就需要我们自己做有针对性的调研了


MySQL SQL命令模拟测试


  • select @@tx_isolation
    查询当前会话的事务隔离级别

  • select @@global.tx_isolation
    查看当前数据库的全局事务隔离级别

  • set session transaction isolation level repeatable read
    设置当前会话隔离级别

  • set global transaction isolation level repeatable read
    查看数据库全局隔离级别

  • set autocommit = 0\ set autocommit = 1
    会话级别,0取消自动提交,则当执行语句commit或rollback时才算结束,1开启自动提交;数据库默认是自动提交

  • show variables like '%autocommit%'
    查看是否开启自动提交

  • begin / start transaction
    开启事务

  • rollback/rollback work
    回滚事务

  • select * from information_schema.INNODB_TRX
    查看当前正在执行的事务


相关问题


InnoDB的RR级别下其实有部分场景还是会出现不可重复读问题的

首先我们都知道标准的SQL定义中,RR级别(可重复读)是能解决不可重复读问题的,即可以避免不可重复读问题;那什么是不可重复读?我们来回忆一下

事务A先查询了金额,是200块钱,未提交 。事务B在事务A查询完之后,修改了金额,变成了300,在事务A提交了;如果此时事务A再查询一次数据,就会发现钱跟上一次查询不一致,这就是不可重复读

但事实上MySQL InnoDB引擎下的RR级别的表现是什么呢?我们可以在MySQL InnoDB的RR级别下用两种场景进行测试当前读快照读

表1:

事务A 事务B
开启事务 开启事务
快照读(无影响)查询金额为500 快照读查询金额为500
更新金额为400
提交事务
select 快照读金额为500
select lock in share mode当前读金额为400

在上表的顺序下,事务B的在事务A提交修改后的快照读是旧版本数据,而当前读是实时新版本实时数据400

表2:

事务A 事务B
开启事务 开启事务
快照读(无影响)查询金额为500
更新金额为400
提交事务
select 快照读金额为400
select lock in share mode当前读金额为400

这两种场景的唯一区别就是表2中事务B在事务A提交事务前执行过一次快照读,如果我们了解快照读的话,我们就会知道同一事务下,后续快照读的结果会受到前面快照读结果的影响

所以我们可以整理出一下观点:

  • 1.RR级别下的当前读,是会出现不可重复读问题的
  • 2.RR级别下,没有提前查询的快照读也是会出现不可重复读问题的(不过这种情况未必属于不可重复读的范畴,因为不可重复读强调的是事务B要提前查询一次,后面再查询一次,发现不一致;而没有提前查询的快照读,只有一次查询,本质上没有前后数据的对比,所以从概念理解上,这种场景不应该划入不可重复读问题中
  • 3.RR级别下,只有提前查询过的快照读才是能避免不可重复读问题的

所以总结:

  • 只有情况1和情况3才符合不可重复读问题定义的验证场景,情况2还无法构成对不可重复读的验证
  • 所以我们可以简单的理解成RR级别下的当前读会出现不可重复读问题,而快照读不会出现不可重复读问题;即MySQL的RR级别解决了快照读(普通select)的不可重复读问题,其他情况属于特殊情况

RR级别以下的RC和RUC级别的本身就会出现不可重复读现象,而S级的串行则连快照读都没有,所以就不再讨论范凑内


InnoDB的RR级别下是如何避免幻读的?

对此我们可以分为表象和内在,表象就是看起来像是什么东西实现的,内在就是实际上是什么实现的,毕竟学习的过程就是透过表象看本质的过程嘛

表象: 快照读(非阻塞读) | 伪MVCC
内在: next-key锁行锁Gap锁

学习之前,我们先来了解一下,什么是MySQL InnoDB下的当前读快照读?

  • 当前读
    像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

  • 快照读
    不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

…未完待续

MySQL 到底是怎么解决幻读的?- @知乎


Redis的事务可以实现一致性吗?

Redis的事务,不支持回滚,所以我自身持有的观点,是认为它不能实现对应MySQL同等意义的ACID,这与《Redis设计与实现》所持有的观点稍有不同

我们可以说Redis可以实现ACID的I,但不能实现ACD,为什么呢?

  • 支持隔离性
    因为Redis的事务可以保证一个客户端一个事务内部的多个操作之间不被其他客户端的操作或同客户端非同事务操作插入,所以他能保证一定程度的事务隔离性;

  • 不支持原子性
    Redis的事务无法回滚,所以无法做到要么都成功,要么都失败

  • 不支持一致性
    因为不支持原子性,所以也无法满足事务的一致性,比如一个场景,用户A要给用户B转200块钱,所以事务中有两个操作,操作A将用户A的余额-200,操作B将用户B的余额+200,但是操作A如果某种原因失败了,因为不支持回滚,所以操作B正确进行而且成功了。那么用户A没有扣钱,而用户B却凭空的多了200块钱

  • 不支完整的持久性
    虽然Redis支持持久化,但因为Redis的持久化是异步的,无论是RDB还是AOF的方式,Redis都无法保证将某个时间点的数据一字不差的完全持久化。如果某个时刻Redis崩溃,总是有丢失一小段时间的数据的可能

当然以上的说法是仅仅是我个人的思考,与一些观点也不一致,希望不要被误导


MySQL系列


  • 【MySQL笔记】正确的理解MySQL的乐观锁与悲观锁,MVCC
  • 【MySQL笔记】正确的理解MySQL的MVCC及实现原理
  • 【MySQL笔记】正确的理解MySQL的事务和隔离级别

参考资料


  • ACID中C与CAP定理中C的区别 - @作者:banq
  • 浅谈事务与一致性问题 - @作者:MarkyLumin
  • Transaction Isolation Levels - 作者:@微软
  • 如果觉得对你有帮助,能否点个赞或关个注,以示鼓励笔者呢?!

你可能感兴趣的:(MySQL)