什么是事务的隔离性?
两个人同时在一个画本上画画,过程中你一笔我一笔,那么最后最后画出来的一定是一个四不像,多个事务同时操作一个数据也会和上面的情况类似,所以为了让不同的事务之间相互不存在干扰,就需要对事务的操作进行隔离,事务的隔离性也就是将操作同一个数据的事务相互分离,让操作之间分开有序的执行
用什么方式实现事务的隔离性
通常数据库里都是采用锁的机制,保证事务之间的隔离性,在一个事务对数据进行修改的时候,首先会对该数据进行加锁,在当前事务没有释放锁之前,后续的事务是无法对该数据再次进行加锁的,所以其它事务只能等待,只有前一个事务释放了锁之后,后面的事务才能进行加锁。通过加锁的方式来保证这种先来后到的顺序,以隔离多个事务对数据的操作,从而实现事务的隔离性。
事务并发问题与事务隔离级别
在事务并发执行的时候,如果不进行事务隔离,那么就会产生脏写、脏读、重复读、幻读的问题。 为了解决这些问题,数据库也针对不同的场景通过加锁的形式进行了不同程度的隔离,下面我们了解下产生这些问题的根源,然后不同的事务隔离级别是通过什么方式解决这些问题的。
1、脏写问题
在事务并发的时候,一个事务可以修改另外一个正在进行中的事务的数据,这可能会导致一个写的事务会覆盖另外一个写的事务数据,这也就是脏写问题。
**案例:**以存取款为例,假设A与B同用一张银行卡,银行卡内余额为1000,如果两个事务可以对同一个数据进行修改时,可能会产下面 事务B 把事务A的数据覆盖了,导致事务A写入的数据丢失了。
如何解决脏写问题
在事务隔离级别READ_UNCOMMITTED 解决了脏写的问题,其原理是在READ_UNCOMMITTED事务隔离级别下,当事务对数据进行修改时,首先会对数据加写锁,加写锁成功后只有等事务提交或者回滚后才会释放,所以已经有一个事务对数据加了写锁,那么其他事务就会因为无法获取对应数据的锁而阻塞,所以在READ_UNCOMMITTED事务隔离级别下,多个事务是无法对同一个数据同时进行修改。
2、脏读问题
在事务并发的时候,一个事务可以读取到另外一个正在进行中的事务数据,这产生了脏读问题。
案例: 以存取款为例,假设A与B同用一张银行卡,银行卡内余额为1000,此时事务B进行取款,事务A进行存款,如果一个事务可以读取到另外一个正在进行中的事务数据,那么可能产生下面问题。
操作:
1、事务B:进行取款1000,此时余额为0,但未提交事务。
2、事务A:此时事务A对银行卡余额进行查询,查询到账户余额为0。
3、事务B:撤销事务,余额恢复为1000.
4、事务A:进行存款500,余额=0+500。
5、事务A:提交事务,最后账户余额为500。
如何解决脏读问题
在事务隔离级别READ_COMMITTED(读提交)解决了脏读的问题,其原理是在READ_COMMITTED的事务隔离级别下,当事务对数据进行修改时,得先要对数据加写锁,当事务读取数据时,首先需要对数据加读锁。 因为写锁与读锁不能共存,所以在修改数据的时候,其它事务会因为无法成功加读锁而阻塞,所以READ_COMMITTED 的事务隔离级别下,一个事务就无法读取另外一个未完成的事务所修改的数据了。
3、不可重复读问题
在事务并发的时候,一个事务里多次对同一个数据进行读取,但是读取到的结果是不一样的,这种问题称为不可重复读问题。
案例: 以取款为例,假设A与B同用一张银行卡,银行卡内余额为1000。
操作: 如下图所示,事务A在同一个事务里两次查询同一条数据,得到的却是不同的结果。
1、事务A:查询银行卡余额为1000.
3、事务B:取款1000。
4、事务B:提交事务(此时余额为0)。
5、事务A:查询余额,余额为0。
如何解决不可重复读
不可重复读产生的核心问题是,在一个事务第1次读取和第2次读取数据的间隔过程中可以被另外一个事务修改,因为在READ_COMMITTED的事务隔离级别下,事务中每次读取数据结束后(事务未结束)就会释放读锁,而一旦读锁释放后另外一个事务就可以加写锁,最终导致事务中多次读取该数据的间隙中可以被其它事务修改。
而REPEATABLE_READ **(可重复读)**的事务隔离级别下,一个事务中的读取操作会对数据加读锁(并且在当前事务结束之前不会释放),此时另外一个事务对该数据修改之前会尝试加写锁(此时不会成功,因为读写锁冲突),所以就避免了一个事务多次读取的数据的间隔可以被另外一个事务修改。
不过实际实现的过程中,数据库解决不可重复读的方式会有所不同,在Mysql innodb引擎中,解决不可重复读的问题并不是通过加锁实现,而是通过MVCC机制实现,使用MVCC后读取数据的时候不会加读锁,而是读取的历史版本数据,在RR事务隔离级别里,MVCC保证了在一个事务里多次读取的数据历史版本是一致的,所以就无法看到最新修改的数据,这样也就保证了一个事务里多次读取到的数据肯定是一致的。 具体MVCC实现原理参考:勤劳的小手:数据库基础(四)Innodb MVCC实现原理
4、幻读问题
在事务并发的时候,一个事务可以往另外一个正在读取的事务查询范围内插入新数据,导致另外一个事务在第二次查询数据里,要比前一次查询的数据要多,同样的SQL后面一次查询凭空多出了数据,像幻觉一样所以称为幻读。
案例: 以打印银行卡账单存款记录为例,假设A与B同用一张银行卡,银行卡内存款记录为两条。
操作: 如下图所示,存款表有2条数据,A对存款表进行读取, 当A第一次读取到存款记录的时候只有2条数据; 但是与此同时B往银行卡进行了一笔存款然后提交了事务,在B提交了存款事务之后,A执行了打印操 作,最后发现打印存款表信息的时候却发现有3条数据,与之前查出的数据不一样,这种平白无故多出来 的数据就好象发生了幻觉一样所以称为幻读;
解决幻读的两种方式
1、设置事务隔离级别为SERIALIZABLE
在SERIALIZABLE事务隔离级别下,所有的事务都串行化执行,一个事务的执行必须等前面的事务结束,这样的话查询的时候就无法有其他事务查询新的数据,所以不会产生幻读问题。
2、加间隙锁
幻读问题的本质在于,没有对查询范围内的所有数据进 (包括不存在的数据)进行加锁,而导致改查询范围内可以被插入新的数据,所以使用间隙锁,对查询的范围进行加锁,此时新插入的数据的事务会因为无法加锁成功而阻塞,所以就避免了幻读。比如表数据如下图, 那么此时如果执行select * from user where id>2 时 ,间隙锁会对id>2的空间加锁,所以此时我们另外一个事务插入ID为3 、4、6、7… 都会因为锁阻塞而无法成功。
总结
为解决不同场景的并发事务问题,事务定义了四种隔离级别,每个隔离级别都针对事务并发问题中的一种或几种进行解决,事务级别越高,解决的并发事务问题也就越多,同时也意味着加的锁就越多,所以性能也会越差。不同的隔离级别下它们加锁的情况如下:
READ_UNCOMMITTED
事务读取:不加锁
事务写入:加写锁
解决问题:脏写
存在问题:脏读,不可重复读、幻读。
READ_COMMITTED
事务读取:加读锁(每次select完成都会释放读锁)
事务写入:加写锁
解决问题:脏写、脏读
存在问题:不可重复读、幻读。
REPEATABLE_READ
事务读取:加读锁(每次select完不会释放锁,而是事务结束后才释放)(如果是Mysql的innodb还会加间隙锁)。
事务写入:加写锁
解决问题:脏写、脏读、不可重复读,幻读(如果是Mysql的innodb则已解决)
存在问题:幻读 (如果是Mysql的innodb则不存在)。
SERIALIZABLE
不管读取还是修改所有的事务串行化执行,一个事务的执行必须等其他事务结束。