MySQL事务
MySQL 事务的隔离机制
在MySQL中,事务隔离机制分为了四个级别: - ①Read uncommitted/RU:读未提交 - ②Read committed/RC:读已提交 - ③Repeatable read/RR:可重复读 - ④Serializable:序列化/串行化 上述四个级别,越靠后并发控制度越高,也就是在多线程并发操作的情况下,出现问题的几率越小,但对应的也性能越差,MySQL的事务隔离级别,默认为第三级别:Repeatable read可重复读,但如若想要真正理解这几个隔离级别,得先明白几个因为并发操作造成的问题。 #### 数据库的脏读问题 首先来看看脏读,脏读的意思是指一个事务读到了其他事务还未提交的数据,也就是当前事务读到的数据,由于还未提交,因此有可能会回滚,如下:  比如上图中,DB连接①/事务A正在执行下单业务,目前扣减库存、增加订单两条SQL已经完成了,恰巧此时DB连接②/事务B跑过来读取了一下库存剩余数量,就将事务A已经扣减之后的库存数量读回去了。但好巧不巧,事务A在添加物流信息时,执行异常导致事务A全部回滚,也就是原本扣的库存又会增加回去。 在个案例中,事务A先扣减了库存,然后事务回滚时又加了回去,但连接②已经将扣减后的库存数量读回去操作了,这个过程就被称为数据库脏读问题。这个问题很严重,会导致整个业务系统出现问题,数据最终错乱。 #### 数据库的不可重复读问题 再来看看不可重复读问题,不可重复读问题是指在一个事务中,多次读取同一数据,先后读取到的数据不一致,如下:  你没看错,就是对前面那张图稍微做了一点改造,事务A执行下单业务时,因为添加物流信息的时候出错了,导致整个事务回滚,事务回滚完成后,事务A就结束了。但事务B却并未结束,在事务B中,在事务A执行时读取了一次剩余库存,然后在事务回滚后又读取了一次剩余库存,仔细想想:B事务第一次读到的剩余库存是扣减之后的,第二次读到的剩余库存则是扣减之前的(因为A事务回滚又加回去了)。 在上述这个案例中,同一个事务中读取同一数据,结果却并不一致,也就说明了该数据存在不可重复读问题,这样说似乎有些绕,那再结合可重复读来一起理解: 可重复读的意思是:在同一事务中,不管读取多少次,读到的数据都是相同的。 #### 数据库的幻读问题  做过电商业务的小伙伴都清楚,一般用户购买商品后付的钱会先冻结在平台上,然后由平台在固定的时间内结算用户款,例如七天一结算、半月一结算等方式,在结算业务中通常都会涉及到核销处理,也就是将所有为「已签收状态」的订单改为「已核销状态」。 此时假设连接①/事务A正在执行「半月结算」这个工作,那首先会读取订单表中所有状态为「已签收」的订单,并将其更改为「已核销」状态,然后将用户款打给商家。 但此时恰巧,某个用户的订单正好到了自动确认收货的时间,因此在事务A刚刚改完表中订单的状态时,事务B又向表中插入了一条「已签收状态」的订单并提交了,当事务A完成打款后,再次查询订单表,结果会发现表中还有一条「已签收状态」的订单数据未结算,这就好像产生了幻觉一样,这才是真正的幻读问题。 当然,这样讲似乎还不是那么令人理解,再举个更通俗易懂的栗子,假设此时平台要升级,用户表中的性别字段,原本是以「男、女」的形式保存数据,现在平台升级后要求改为「0、1」代替。 因此事务A开始更改表中所有数据的性别字段,当负责执行事务A的线程正在更改最后一条表数据时,此时事务B来了,正好向用户表中插入了一条「性别=男」的数据并提交了,然后事务A改完原本的最后一条数据后,当再次去查询用户表时,结果会发现表中依旧还存在一条「性别=男」的数据,似乎又跟产生了幻觉一样。 经过上述这两个案例,大家应该能够理解真正的幻读问题,发生幻读问题的原因是在于:另外一个事务在第一个事务要处理的目标数据范围之内新增了数据,然后先于第一个事务提交造成的问题。 #### 数据库脏写问题 其实除开三个读的问题外,还有有一个叫做脏写的问题,也就是多个事务一起操作同一条数据,例如两个事务同时向表中添加一条ID=88的数据,此时就会造成数据覆盖,或者主键冲突的问题,这个问题也被称之为更新丢失问题。 #### 事务的四大隔离级别 在上面连续讲了脏读、不可重复读以及幻读三个问题,那这些问题该怎么解决呢?其实四个事务隔离级别,解决的实际问题就是这三个,因此一起来看看各级别分别解决了什么问题: - ①读未提交:处于该隔离级别的数据库,脏读、不可重复读、幻读问题都有可能发生。 - ②读已提交:处于该隔离级别的数据库,解决了脏读问题,不可重复读、幻读问题依旧存在。 - ③可重复读:处于该隔离级别的数据库,解决了脏读、不可重复读问题,幻读问题依旧存在。 - ④序列化/串行化:处于该隔离级别的数据库,解决了脏读、不可重复读、幻读问题都不存在。 前面提到过,MySQL默认是处于第三级别的,可以通过如下命令查看目前数据库的隔离级别: ```sql -- 查询方式① SELECT @@tx_isolation; -- 查询方式② show variables like '%tx_isolation%'; +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | tx_isolation | REPEATABLE-READ | +---------------+-----------------+ ``` 其实数据库不同的事务隔离级别,是基于不同类型、不同粒度的锁实现的,因此想要真正搞懂隔离机制,还需要弄明白MySQL的锁机制,事务与锁机制二者之间本身就是相辅相成的关系,锁就是为了解决并发事务的一些问题而存在的。 这里先说明一点,事务是基于数据库连接的,数据库连接本身会有一条工作线程来维护,也就是说事务的执行本质上就是工作线程在执行,因此所谓的并发事务也就是指多条线程并发执行。 - 读未提交级别 这种隔离级别是基于「写互斥锁」实现的,当一个事务开始写某一个数据时,另外一个事务也来操作同一个数据,此时为了防止出现问题则需要先获取锁资源,只有获取到锁的事务,才允许对数据进行写操作,同时获取到锁的事务具备排他性/互斥性,也就是其他线程无法再操作这个数据。 但虽然这个级别中,写同一数据时会互斥,但读操作却并不是互斥的,也就是当一个事务在写某个数据时,就算没有提交事务,其他事务来读取该数据时,也可以读到未提交的数据,因此就会导致脏读、不可重复读、幻读一系列问题出现。 但是由于在这个隔离级别中加了「写互斥锁」,因此不会存在多个事务同时操作同一数据的情况,因此这个级别中解决了前面说到的脏写问题。 - 读已提交级别 在这个隔离级别中,对于写操作同样会使用「写互斥锁」,也就是两个事务操作同一事务时,会出现排他性,而对于读操作则使用了一种名为MVCC多版本并发控制的技术处理,也就是有事务中的SQL需要读取当前事务正在操作的数据时,MVCC机制不会让另一个事务读取正在修改的数据,而是读取上一次提交的数据(也就是读原本的老数据)。 也就是在这个隔离级别中,基于同一条数据而言,对于写操作会具备排他性,对于读操作则只能读已提交事务的数据,不会读取正在操作但还未提交的事务数据,为了理解还是简单的说一下其过程,同样有两个事务A、B。 事务A的主要工作是负责更新ID=1的这条数据,事务B中则是读取ID=1的这条数据。 此时当A正在更新数据但还未提交时,事务B开始读取数据,此时MVCC机制则会基于表数据的快照创建一个ReadView,然后读取原本表中上一次提交的老数据。然后等事务A提交之后,事务B再次读取数据,此时MVCC机制又会创建一个新的ReadView,然后读取到最新的已提交的数据,此时事务B中两次读到的数据并不一致,因此出现了不可重复读问题。 - 可重复读级别 在这个隔离级别中,主要就是解决上一个级别中遗留的不可重复读问题,但MySQL依旧是利用MVCC机制来解决这个问题的,只不过在这个级别的MVCC机制会稍微有些不同。在读已提交级别中,一个事务中每次查询数据时,都会创建一个新的ReadView,然后读取最近已提交的事务数据,因此就会造成不可重复读的问题。 而在可重复读级别中,则不会每次查询时都创建新的ReadView,而是在一个事务中,只有第一次执行查询会创建一个ReadView,在这个事务的生命周期内,所有的查询都会从这一个ReadView中读取数据,从而确保了一个事务中多次读取相同数据是一致的,也就是解决了不可重复读问题。 虽然在这个隔离级别中,解决了不可重复读问题,但依旧存在幻读问题,也就是事务A在对表中多行数据进行修改,比如前面的举例,将性别「男、女」改为「0、1」,此时事务B又插入了一条性别为男的数据,当事务A提交后,再次查询表时,会发现表中依旧存在一条性别为男的数据。 - 序列化/串行化级别 这个隔离级别是最高的级别,处于该隔离级别的MySQL绝不会产生任何问题,因为从它的名字上就可以得知:序列化意思是将所有的事务按序排队后串行化处理,也就是操作同一张表的事务只能一个一个执行,事务在执行前需要先获取表级别的锁资源,拿到锁资源的事务才能执行,其余事务则陷入阻塞,等待当前事务释放锁。 但这种隔离级别会导致数据库的性能直线下降,毕竟相当于一张表上只能允许单条线程执行了,虽然安全等级最高,可以解决脏写、脏读、不可重复读、幻读等一系列问题,但也是代价最高的,一般线上很少使用。 这种隔离级别解决问题的思想很简单,之前我们分析过,产生一系列问题的根本原因在于:多事务/多线程并发执行导致的,那在这个隔离级别中,直接将多线程化为了单线程,自然也就从根源上避免了问题产生。 #### 事务隔离机制的命令 ```sql -- 方式①:查询当前数据库的隔离级别 SELECT @@tx_isolation; -- 方式②:查询当前数据库的隔离级别 show variables like '%tx_isolation%'; -- 设置隔离级别为RU级别(当前连接生效) set transaction isolation level read uncommitted; -- 设置隔离级别为RC级别(全局生效) set global transaction isolation level read committed; -- 设置隔离级别为RR级别(当前连接生效) -- 这里和上述的那条命令作用相同,是第二种设置的方式 set tx_isolation = 'repeatable-read'; -- 设置隔离级别为最高的serializable级别(全局生效) set global.tx_isolation = 'serializable'; ```
顶部
收展
底部
[TOC]
目录
MySQL 事务的ACID原则
MySQL 事务的隔离机制
MySQL事务实现原理
MySQL之MVCC机制
相关推荐
MySQL教程
MySQL命令
MySQL索引
MySQL锁机制
MySQL版本特性